Skip to content

Commit 60fe72e

Browse files
feat(firestore): serverTimestampBehavior (#5556)
* feat: serverTimestampBehavior * Add test for firestore().settings({ serverTimestampBehavior }) * Add e2e tests Co-authored-by: Jan Erik Foss <[email protected]>
1 parent dcc8911 commit 60fe72e

20 files changed

+213
-34
lines changed

packages/firestore/__tests__/firestore.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,18 @@ describe('Storage', function () {
112112
return expect(e.message).toContain("ignoreUndefinedProperties' must be a boolean value.");
113113
}
114114
});
115+
116+
it("throws if serverTimestampBehavior is not one of 'estimate', 'previous', 'none'", async function () {
117+
try {
118+
// @ts-ignore the type is incorrect *on purpose* to test type checking in javascript
119+
await firestore().settings({ serverTimestampBehavior: 'bogus' });
120+
return Promise.reject(new Error('Should throw'));
121+
} catch (e) {
122+
return expect(e.message).toContain(
123+
"serverTimestampBehavior' must be one of 'estimate', 'previous', 'none'",
124+
);
125+
}
126+
});
115127
});
116128

117129
describe('runTransaction()', function () {

packages/firestore/android/src/main/java/io/invertase/firebase/firestore/UniversalFirebaseFirestoreModule.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ Task<Void> settings(String appName, Map<String, Object> settings) {
7575
UniversalFirebaseFirestoreStatics.FIRESTORE_SSL + "_" + appName, (boolean) settings.get("ssl"));
7676
}
7777

78+
// settings.serverTimestampBehavior
79+
if (settings.containsKey("serverTimestampBehavior")) {
80+
UniversalFirebasePreferences.getSharedInstance().setStringValue(
81+
UniversalFirebaseFirestoreStatics.FIRESTORE_SERVER_TIMESTAMP_BEHAVIOR + "_" + appName, (String) settings.get("serverTimestampBehavior"));
82+
}
83+
7884
return null;
7985
});
8086
}

packages/firestore/android/src/main/java/io/invertase/firebase/firestore/UniversalFirebaseFirestoreStatics.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ public class UniversalFirebaseFirestoreStatics {
2222
public static String FIRESTORE_HOST = "firebase_firestore_host";
2323
public static String FIRESTORE_PERSISTENCE = "firebase_firestore_persistence";
2424
public static String FIRESTORE_SSL = "firebase_firestore_ssl";
25+
public static String FIRESTORE_SERVER_TIMESTAMP_BEHAVIOR = "firebase_firestore_server_timestamp_behavior";
2526
}

packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreCollectionModule.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ public void collectionOnSnapshot(
6666

6767
FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName);
6868
ReactNativeFirebaseFirestoreQuery firestoreQuery = new ReactNativeFirebaseFirestoreQuery(
69+
appName,
6970
getQueryForFirestore(firebaseFirestore, path, type),
7071
filters,
7172
orders,
@@ -128,6 +129,7 @@ public void collectionGet(
128129
) {
129130
FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName);
130131
ReactNativeFirebaseFirestoreQuery query = new ReactNativeFirebaseFirestoreQuery(
132+
appName,
131133
getQueryForFirestore(firebaseFirestore, path, type),
132134
filters,
133135
orders,
@@ -160,7 +162,7 @@ public void collectionGet(
160162
}
161163

162164
private void sendOnSnapshotEvent(String appName, int listenerId, QuerySnapshot querySnapshot, MetadataChanges metadataChanges) {
163-
Tasks.call(getTransactionalExecutor(Integer.toString(listenerId)), () -> snapshotToWritableMap("onSnapshot", querySnapshot, metadataChanges)).addOnCompleteListener(task -> {
165+
Tasks.call(getTransactionalExecutor(Integer.toString(listenerId)), () -> snapshotToWritableMap(appName, "onSnapshot", querySnapshot, metadataChanges)).addOnCompleteListener(task -> {
164166
if (task.isSuccessful()) {
165167
WritableMap body = Arguments.createMap();
166168
body.putMap("snapshot", task.getResult());

packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreCommon.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@
1919

2020

2121
import com.facebook.react.bridge.Promise;
22+
import com.google.firebase.firestore.DocumentSnapshot;
2223
import com.google.firebase.firestore.FirebaseFirestoreException;
2324

25+
import io.invertase.firebase.common.UniversalFirebasePreferences;
26+
2427
import static io.invertase.firebase.common.ReactNativeFirebaseModule.rejectPromiseWithCodeAndMessage;
2528
import static io.invertase.firebase.common.ReactNativeFirebaseModule.rejectPromiseWithExceptionMap;
2629

@@ -39,4 +42,20 @@ static void rejectPromiseFirestoreException(Promise promise, Exception exception
3942
rejectPromiseWithExceptionMap(promise, exception);
4043
}
4144
}
45+
46+
static DocumentSnapshot.ServerTimestampBehavior getServerTimestampBehavior(String appName) {
47+
UniversalFirebasePreferences preferences = UniversalFirebasePreferences.getSharedInstance();
48+
String key = UniversalFirebaseFirestoreStatics.FIRESTORE_SERVER_TIMESTAMP_BEHAVIOR + "_" + appName;
49+
String behavior = preferences.getStringValue(key, "none");
50+
51+
if ("estimate".equals(behavior)) {
52+
return DocumentSnapshot.ServerTimestampBehavior.ESTIMATE;
53+
}
54+
55+
if ("previous".equals(behavior)) {
56+
return DocumentSnapshot.ServerTimestampBehavior.PREVIOUS;
57+
}
58+
59+
return DocumentSnapshot.ServerTimestampBehavior.NONE;
60+
}
4261
}

packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreDocumentModule.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ public void documentGet(String appName, String path, ReadableMap getOptions, Pro
131131

132132
Tasks.call(getExecutor(), () -> {
133133
DocumentSnapshot documentSnapshot = Tasks.await(documentReference.get(source));
134-
return snapshotToWritableMap(documentSnapshot);
134+
return snapshotToWritableMap(appName, documentSnapshot);
135135
}).addOnCompleteListener(task -> {
136136
if (task.isSuccessful()) {
137137
promise.resolve(task.getResult());
@@ -261,7 +261,7 @@ public void documentBatch(String appName, ReadableArray writes, Promise promise)
261261
}
262262

263263
private void sendOnSnapshotEvent(String appName, int listenerId, DocumentSnapshot documentSnapshot) {
264-
Tasks.call(getExecutor(), () -> snapshotToWritableMap(documentSnapshot)).addOnCompleteListener(task -> {
264+
Tasks.call(getExecutor(), () -> snapshotToWritableMap(appName, documentSnapshot)).addOnCompleteListener(task -> {
265265
if (task.isSuccessful()) {
266266
WritableMap body = Arguments.createMap();
267267
body.putMap("snapshot", task.getResult());

packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreQuery.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,17 @@
3737
import static io.invertase.firebase.firestore.ReactNativeFirebaseFirestoreSerialize.*;
3838

3939
public class ReactNativeFirebaseFirestoreQuery {
40+
String appName;
4041
Query query;
4142

4243
ReactNativeFirebaseFirestoreQuery(
44+
String appName,
4345
Query query,
4446
ReadableArray filters,
4547
ReadableArray orders,
4648
ReadableMap options
4749
) {
50+
this.appName = appName;
4851
this.query = query;
4952
applyFilters(filters);
5053
applyOrders(orders);
@@ -54,7 +57,7 @@ public class ReactNativeFirebaseFirestoreQuery {
5457
public Task<WritableMap> get(Executor executor, Source source) {
5558
return Tasks.call(executor, () -> {
5659
QuerySnapshot querySnapshot = Tasks.await(query.get(source));
57-
return snapshotToWritableMap("get", querySnapshot, null);
60+
return snapshotToWritableMap(this.appName, "get", querySnapshot, null);
5861
});
5962
}
6063

packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreSerialize.java

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848

4949
import javax.annotation.Nullable;
5050

51+
import static io.invertase.firebase.firestore.ReactNativeFirebaseFirestoreCommon.getServerTimestampBehavior;
5152
import static io.invertase.firebase.common.RCTConvertFirebase.toHashMap;
5253

5354
// public access for native re-use in brownfield apps
@@ -99,7 +100,7 @@ public class ReactNativeFirebaseFirestoreSerialize {
99100
* @param documentSnapshot DocumentSnapshot
100101
* @return WritableMap
101102
*/
102-
static WritableMap snapshotToWritableMap(DocumentSnapshot documentSnapshot) {
103+
static WritableMap snapshotToWritableMap(String appName, DocumentSnapshot documentSnapshot) {
103104
WritableArray metadata = Arguments.createArray();
104105
WritableMap documentMap = Arguments.createMap();
105106
SnapshotMetadata snapshotMetadata = documentSnapshot.getMetadata();
@@ -112,9 +113,11 @@ static WritableMap snapshotToWritableMap(DocumentSnapshot documentSnapshot) {
112113
documentMap.putString(KEY_PATH, documentSnapshot.getReference().getPath());
113114
documentMap.putBoolean(KEY_EXISTS, documentSnapshot.exists());
114115

116+
DocumentSnapshot.ServerTimestampBehavior timestampBehavior = getServerTimestampBehavior(appName);
117+
115118
if (documentSnapshot.exists()) {
116-
if (documentSnapshot.getData() != null) {
117-
documentMap.putMap(KEY_DATA, objectMapToWritable(documentSnapshot.getData()));
119+
if (documentSnapshot.getData(timestampBehavior) != null) {
120+
documentMap.putMap(KEY_DATA, objectMapToWritable(documentSnapshot.getData(timestampBehavior)));
118121
}
119122
}
120123

@@ -127,7 +130,7 @@ static WritableMap snapshotToWritableMap(DocumentSnapshot documentSnapshot) {
127130
* @param querySnapshot QuerySnapshot
128131
* @return WritableMap
129132
*/
130-
static WritableMap snapshotToWritableMap(String source, QuerySnapshot querySnapshot, @Nullable MetadataChanges metadataChanges) {
133+
static WritableMap snapshotToWritableMap(String appName, String source, QuerySnapshot querySnapshot, @Nullable MetadataChanges metadataChanges) {
131134
WritableMap writableMap = Arguments.createMap();
132135
writableMap.putString("source", source);
133136

@@ -140,22 +143,22 @@ static WritableMap snapshotToWritableMap(String source, QuerySnapshot querySnaps
140143
// If not listening to metadata changes, send the data back to JS land with a flag
141144
// indicating the data does not include these changes
142145
writableMap.putBoolean("excludesMetadataChanges", true);
143-
writableMap.putArray(KEY_CHANGES, documentChangesToWritableArray(documentChangesList, null));
146+
writableMap.putArray(KEY_CHANGES, documentChangesToWritableArray(appName, documentChangesList, null));
144147
} else {
145148
// If listening to metadata changes, get the changes list with document changes array.
146149
// To indicate whether a document change was because of metadata change, we check whether
147150
// its in the raw list by document key.
148151
writableMap.putBoolean("excludesMetadataChanges", false);
149152
List<DocumentChange> documentMetadataChangesList = querySnapshot.getDocumentChanges(MetadataChanges.INCLUDE);
150-
writableMap.putArray(KEY_CHANGES, documentChangesToWritableArray(documentMetadataChangesList, documentChangesList));
153+
writableMap.putArray(KEY_CHANGES, documentChangesToWritableArray(appName, documentMetadataChangesList, documentChangesList));
151154
}
152155

153156
SnapshotMetadata snapshotMetadata = querySnapshot.getMetadata();
154157
List<DocumentSnapshot> documentSnapshots = querySnapshot.getDocuments();
155158

156159
// set documents
157160
for (DocumentSnapshot documentSnapshot : documentSnapshots) {
158-
documents.pushMap(snapshotToWritableMap(documentSnapshot));
161+
documents.pushMap(snapshotToWritableMap(appName, documentSnapshot));
159162
}
160163
writableMap.putArray(KEY_DOCUMENTS, documents);
161164

@@ -174,7 +177,7 @@ static WritableMap snapshotToWritableMap(String source, QuerySnapshot querySnaps
174177
* @param documentChanges List<DocumentChange>
175178
* @return WritableArray
176179
*/
177-
private static WritableArray documentChangesToWritableArray(List<DocumentChange> documentChanges, @Nullable List<DocumentChange> comparableDocumentChanges) {
180+
private static WritableArray documentChangesToWritableArray(String appName, List<DocumentChange> documentChanges, @Nullable List<DocumentChange> comparableDocumentChanges) {
178181
WritableArray documentChangesWritable = Arguments.createArray();
179182

180183
boolean checkIfMetadataChange = comparableDocumentChanges != null;
@@ -191,7 +194,7 @@ private static WritableArray documentChangesToWritableArray(List<DocumentChange>
191194
}
192195
}
193196

194-
documentChangesWritable.pushMap(documentChangeToWritableMap(documentChange, isMetadataChange));
197+
documentChangesWritable.pushMap(documentChangeToWritableMap(appName, documentChange, isMetadataChange));
195198
}
196199

197200
return documentChangesWritable;
@@ -203,7 +206,7 @@ private static WritableArray documentChangesToWritableArray(List<DocumentChange>
203206
* @param documentChange DocumentChange
204207
* @return WritableMap
205208
*/
206-
private static WritableMap documentChangeToWritableMap(DocumentChange documentChange, boolean isMetadataChange) {
209+
private static WritableMap documentChangeToWritableMap(String appName, DocumentChange documentChange, boolean isMetadataChange) {
207210
WritableMap documentChangeMap = Arguments.createMap();
208211
documentChangeMap.putBoolean("isMetadataChange", isMetadataChange);
209212

@@ -221,7 +224,7 @@ private static WritableMap documentChangeToWritableMap(DocumentChange documentCh
221224

222225
documentChangeMap.putMap(
223226
KEY_DOC_CHANGE_DOCUMENT,
224-
snapshotToWritableMap(documentChange.getDocument())
227+
snapshotToWritableMap(appName, documentChange.getDocument())
225228
);
226229

227230
documentChangeMap.putInt(KEY_DOC_CHANGE_NEW_INDEX, documentChange.getNewIndex());

packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreTransactionModule.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public void transactionGetDocument(String appName, int transactionId, String pat
7272
DocumentReference documentReference = getDocumentForFirestore(firebaseFirestore, path);
7373

7474
Tasks
75-
.call(getTransactionalExecutor(), () -> snapshotToWritableMap(transactionHandler.getDocument(documentReference)))
75+
.call(getTransactionalExecutor(), () -> snapshotToWritableMap(appName, transactionHandler.getDocument(documentReference)))
7676
.addOnCompleteListener(task -> {
7777
if (task.isSuccessful()) {
7878
promise.resolve(task.getResult());

packages/firestore/e2e/firestore.e2e.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,4 +197,76 @@ describe('firestore()', function () {
197197
should(timedOutWithNetworkEnabled).equal(false);
198198
});
199199
});
200+
201+
describe('settings', function () {
202+
describe('serverTimestampBehavior', function () {
203+
it("handles 'estimate'", async function () {
204+
firebase.firestore().settings({ serverTimestampBehavior: 'estimate' });
205+
const ref = firebase.firestore().doc(`${COLLECTION}/getData`);
206+
207+
const promise = new Promise((resolve, reject) => {
208+
const subscription = ref.onSnapshot(snapshot => {
209+
should(snapshot.get('timestamp')).be.an.instanceOf(firebase.firestore.Timestamp);
210+
subscription();
211+
resolve();
212+
}, reject);
213+
});
214+
215+
await ref.set({ timestamp: firebase.firestore.FieldValue.serverTimestamp() });
216+
await promise;
217+
await ref.delete();
218+
});
219+
it("handles 'previous'", async function () {
220+
firebase.firestore().settings({ serverTimestampBehavior: 'previous' });
221+
const ref = firebase.firestore().doc(`${COLLECTION}/getData`);
222+
223+
const promise = new Promise((resolve, reject) => {
224+
let counter = 0;
225+
let previous = null;
226+
const subscription = ref.onSnapshot(snapshot => {
227+
switch (counter++) {
228+
case 0:
229+
break;
230+
case 1:
231+
should(snapshot.get('timestamp')).be.an.instanceOf(firebase.firestore.Timestamp);
232+
break;
233+
case 2:
234+
should(snapshot.get('timestamp')).be.an.instanceOf(firebase.firestore.Timestamp);
235+
should(snapshot.get('timestamp').isEqual(previous.get('timestamp'))).equal(true);
236+
break;
237+
case 3:
238+
should(snapshot.get('timestamp')).be.an.instanceOf(firebase.firestore.Timestamp);
239+
should(snapshot.get('timestamp').isEqual(previous.get('timestamp'))).equal(false);
240+
subscription();
241+
resolve();
242+
break;
243+
}
244+
previous = snapshot;
245+
}, reject);
246+
});
247+
248+
await ref.set({ timestamp: firebase.firestore.FieldValue.serverTimestamp() });
249+
await new Promise(resolve => setTimeout(resolve, 1));
250+
await ref.set({ timestamp: firebase.firestore.FieldValue.serverTimestamp() });
251+
await promise;
252+
await ref.delete();
253+
});
254+
it("handles 'none'", async function () {
255+
firebase.firestore().settings({ serverTimestampBehavior: 'none' });
256+
const ref = firebase.firestore().doc(`${COLLECTION}/getData`);
257+
258+
const promise = new Promise((resolve, reject) => {
259+
const subscription = ref.onSnapshot(snapshot => {
260+
should(snapshot.get('timestamp')).equal(null);
261+
subscription();
262+
resolve();
263+
}, reject);
264+
});
265+
266+
await ref.set({ timestamp: firebase.firestore.FieldValue.serverTimestamp() });
267+
await promise;
268+
await ref.delete();
269+
});
270+
});
271+
});
200272
});

0 commit comments

Comments
 (0)