Skip to content

Commit

Permalink
feat(firestore): serverTimestampBehavior (#5556)
Browse files Browse the repository at this point in the history
* feat: serverTimestampBehavior
* Add test for firestore().settings({ serverTimestampBehavior })
* Add e2e tests

Co-authored-by: Jan Erik Foss <solafosstek@gmail.com>
  • Loading branch information
mikehardy and JanErikFoss authored Jul 30, 2021
1 parent dcc8911 commit 60fe72e
Show file tree
Hide file tree
Showing 20 changed files with 213 additions and 34 deletions.
12 changes: 12 additions & 0 deletions packages/firestore/__tests__/firestore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,18 @@ describe('Storage', function () {
return expect(e.message).toContain("ignoreUndefinedProperties' must be a boolean value.");
}
});

it("throws if serverTimestampBehavior is not one of 'estimate', 'previous', 'none'", async function () {
try {
// @ts-ignore the type is incorrect *on purpose* to test type checking in javascript
await firestore().settings({ serverTimestampBehavior: 'bogus' });
return Promise.reject(new Error('Should throw'));
} catch (e) {
return expect(e.message).toContain(
"serverTimestampBehavior' must be one of 'estimate', 'previous', 'none'",
);
}
});
});

describe('runTransaction()', function () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ Task<Void> settings(String appName, Map<String, Object> settings) {
UniversalFirebaseFirestoreStatics.FIRESTORE_SSL + "_" + appName, (boolean) settings.get("ssl"));
}

// settings.serverTimestampBehavior
if (settings.containsKey("serverTimestampBehavior")) {
UniversalFirebasePreferences.getSharedInstance().setStringValue(
UniversalFirebaseFirestoreStatics.FIRESTORE_SERVER_TIMESTAMP_BEHAVIOR + "_" + appName, (String) settings.get("serverTimestampBehavior"));
}

return null;
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ public class UniversalFirebaseFirestoreStatics {
public static String FIRESTORE_HOST = "firebase_firestore_host";
public static String FIRESTORE_PERSISTENCE = "firebase_firestore_persistence";
public static String FIRESTORE_SSL = "firebase_firestore_ssl";
public static String FIRESTORE_SERVER_TIMESTAMP_BEHAVIOR = "firebase_firestore_server_timestamp_behavior";
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public void collectionOnSnapshot(

FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName);
ReactNativeFirebaseFirestoreQuery firestoreQuery = new ReactNativeFirebaseFirestoreQuery(
appName,
getQueryForFirestore(firebaseFirestore, path, type),
filters,
orders,
Expand Down Expand Up @@ -128,6 +129,7 @@ public void collectionGet(
) {
FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName);
ReactNativeFirebaseFirestoreQuery query = new ReactNativeFirebaseFirestoreQuery(
appName,
getQueryForFirestore(firebaseFirestore, path, type),
filters,
orders,
Expand Down Expand Up @@ -160,7 +162,7 @@ public void collectionGet(
}

private void sendOnSnapshotEvent(String appName, int listenerId, QuerySnapshot querySnapshot, MetadataChanges metadataChanges) {
Tasks.call(getTransactionalExecutor(Integer.toString(listenerId)), () -> snapshotToWritableMap("onSnapshot", querySnapshot, metadataChanges)).addOnCompleteListener(task -> {
Tasks.call(getTransactionalExecutor(Integer.toString(listenerId)), () -> snapshotToWritableMap(appName, "onSnapshot", querySnapshot, metadataChanges)).addOnCompleteListener(task -> {
if (task.isSuccessful()) {
WritableMap body = Arguments.createMap();
body.putMap("snapshot", task.getResult());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@


import com.facebook.react.bridge.Promise;
import com.google.firebase.firestore.DocumentSnapshot;
import com.google.firebase.firestore.FirebaseFirestoreException;

import io.invertase.firebase.common.UniversalFirebasePreferences;

import static io.invertase.firebase.common.ReactNativeFirebaseModule.rejectPromiseWithCodeAndMessage;
import static io.invertase.firebase.common.ReactNativeFirebaseModule.rejectPromiseWithExceptionMap;

Expand All @@ -39,4 +42,20 @@ static void rejectPromiseFirestoreException(Promise promise, Exception exception
rejectPromiseWithExceptionMap(promise, exception);
}
}

static DocumentSnapshot.ServerTimestampBehavior getServerTimestampBehavior(String appName) {
UniversalFirebasePreferences preferences = UniversalFirebasePreferences.getSharedInstance();
String key = UniversalFirebaseFirestoreStatics.FIRESTORE_SERVER_TIMESTAMP_BEHAVIOR + "_" + appName;
String behavior = preferences.getStringValue(key, "none");

if ("estimate".equals(behavior)) {
return DocumentSnapshot.ServerTimestampBehavior.ESTIMATE;
}

if ("previous".equals(behavior)) {
return DocumentSnapshot.ServerTimestampBehavior.PREVIOUS;
}

return DocumentSnapshot.ServerTimestampBehavior.NONE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public void documentGet(String appName, String path, ReadableMap getOptions, Pro

Tasks.call(getExecutor(), () -> {
DocumentSnapshot documentSnapshot = Tasks.await(documentReference.get(source));
return snapshotToWritableMap(documentSnapshot);
return snapshotToWritableMap(appName, documentSnapshot);
}).addOnCompleteListener(task -> {
if (task.isSuccessful()) {
promise.resolve(task.getResult());
Expand Down Expand Up @@ -261,7 +261,7 @@ public void documentBatch(String appName, ReadableArray writes, Promise promise)
}

private void sendOnSnapshotEvent(String appName, int listenerId, DocumentSnapshot documentSnapshot) {
Tasks.call(getExecutor(), () -> snapshotToWritableMap(documentSnapshot)).addOnCompleteListener(task -> {
Tasks.call(getExecutor(), () -> snapshotToWritableMap(appName, documentSnapshot)).addOnCompleteListener(task -> {
if (task.isSuccessful()) {
WritableMap body = Arguments.createMap();
body.putMap("snapshot", task.getResult());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,17 @@
import static io.invertase.firebase.firestore.ReactNativeFirebaseFirestoreSerialize.*;

public class ReactNativeFirebaseFirestoreQuery {
String appName;
Query query;

ReactNativeFirebaseFirestoreQuery(
String appName,
Query query,
ReadableArray filters,
ReadableArray orders,
ReadableMap options
) {
this.appName = appName;
this.query = query;
applyFilters(filters);
applyOrders(orders);
Expand All @@ -54,7 +57,7 @@ public class ReactNativeFirebaseFirestoreQuery {
public Task<WritableMap> get(Executor executor, Source source) {
return Tasks.call(executor, () -> {
QuerySnapshot querySnapshot = Tasks.await(query.get(source));
return snapshotToWritableMap("get", querySnapshot, null);
return snapshotToWritableMap(this.appName, "get", querySnapshot, null);
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@

import javax.annotation.Nullable;

import static io.invertase.firebase.firestore.ReactNativeFirebaseFirestoreCommon.getServerTimestampBehavior;
import static io.invertase.firebase.common.RCTConvertFirebase.toHashMap;

// public access for native re-use in brownfield apps
Expand Down Expand Up @@ -99,7 +100,7 @@ public class ReactNativeFirebaseFirestoreSerialize {
* @param documentSnapshot DocumentSnapshot
* @return WritableMap
*/
static WritableMap snapshotToWritableMap(DocumentSnapshot documentSnapshot) {
static WritableMap snapshotToWritableMap(String appName, DocumentSnapshot documentSnapshot) {
WritableArray metadata = Arguments.createArray();
WritableMap documentMap = Arguments.createMap();
SnapshotMetadata snapshotMetadata = documentSnapshot.getMetadata();
Expand All @@ -112,9 +113,11 @@ static WritableMap snapshotToWritableMap(DocumentSnapshot documentSnapshot) {
documentMap.putString(KEY_PATH, documentSnapshot.getReference().getPath());
documentMap.putBoolean(KEY_EXISTS, documentSnapshot.exists());

DocumentSnapshot.ServerTimestampBehavior timestampBehavior = getServerTimestampBehavior(appName);

if (documentSnapshot.exists()) {
if (documentSnapshot.getData() != null) {
documentMap.putMap(KEY_DATA, objectMapToWritable(documentSnapshot.getData()));
if (documentSnapshot.getData(timestampBehavior) != null) {
documentMap.putMap(KEY_DATA, objectMapToWritable(documentSnapshot.getData(timestampBehavior)));
}
}

Expand All @@ -127,7 +130,7 @@ static WritableMap snapshotToWritableMap(DocumentSnapshot documentSnapshot) {
* @param querySnapshot QuerySnapshot
* @return WritableMap
*/
static WritableMap snapshotToWritableMap(String source, QuerySnapshot querySnapshot, @Nullable MetadataChanges metadataChanges) {
static WritableMap snapshotToWritableMap(String appName, String source, QuerySnapshot querySnapshot, @Nullable MetadataChanges metadataChanges) {
WritableMap writableMap = Arguments.createMap();
writableMap.putString("source", source);

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

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

// set documents
for (DocumentSnapshot documentSnapshot : documentSnapshots) {
documents.pushMap(snapshotToWritableMap(documentSnapshot));
documents.pushMap(snapshotToWritableMap(appName, documentSnapshot));
}
writableMap.putArray(KEY_DOCUMENTS, documents);

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

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

documentChangesWritable.pushMap(documentChangeToWritableMap(documentChange, isMetadataChange));
documentChangesWritable.pushMap(documentChangeToWritableMap(appName, documentChange, isMetadataChange));
}

return documentChangesWritable;
Expand All @@ -203,7 +206,7 @@ private static WritableArray documentChangesToWritableArray(List<DocumentChange>
* @param documentChange DocumentChange
* @return WritableMap
*/
private static WritableMap documentChangeToWritableMap(DocumentChange documentChange, boolean isMetadataChange) {
private static WritableMap documentChangeToWritableMap(String appName, DocumentChange documentChange, boolean isMetadataChange) {
WritableMap documentChangeMap = Arguments.createMap();
documentChangeMap.putBoolean("isMetadataChange", isMetadataChange);

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

documentChangeMap.putMap(
KEY_DOC_CHANGE_DOCUMENT,
snapshotToWritableMap(documentChange.getDocument())
snapshotToWritableMap(appName, documentChange.getDocument())
);

documentChangeMap.putInt(KEY_DOC_CHANGE_NEW_INDEX, documentChange.getNewIndex());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public void transactionGetDocument(String appName, int transactionId, String pat
DocumentReference documentReference = getDocumentForFirestore(firebaseFirestore, path);

Tasks
.call(getTransactionalExecutor(), () -> snapshotToWritableMap(transactionHandler.getDocument(documentReference)))
.call(getTransactionalExecutor(), () -> snapshotToWritableMap(appName, transactionHandler.getDocument(documentReference)))
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
promise.resolve(task.getResult());
Expand Down
72 changes: 72 additions & 0 deletions packages/firestore/e2e/firestore.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,4 +197,76 @@ describe('firestore()', function () {
should(timedOutWithNetworkEnabled).equal(false);
});
});

describe('settings', function () {
describe('serverTimestampBehavior', function () {
it("handles 'estimate'", async function () {
firebase.firestore().settings({ serverTimestampBehavior: 'estimate' });
const ref = firebase.firestore().doc(`${COLLECTION}/getData`);

const promise = new Promise((resolve, reject) => {
const subscription = ref.onSnapshot(snapshot => {
should(snapshot.get('timestamp')).be.an.instanceOf(firebase.firestore.Timestamp);
subscription();
resolve();
}, reject);
});

await ref.set({ timestamp: firebase.firestore.FieldValue.serverTimestamp() });
await promise;
await ref.delete();
});
it("handles 'previous'", async function () {
firebase.firestore().settings({ serverTimestampBehavior: 'previous' });
const ref = firebase.firestore().doc(`${COLLECTION}/getData`);

const promise = new Promise((resolve, reject) => {
let counter = 0;
let previous = null;
const subscription = ref.onSnapshot(snapshot => {
switch (counter++) {
case 0:
break;
case 1:
should(snapshot.get('timestamp')).be.an.instanceOf(firebase.firestore.Timestamp);
break;
case 2:
should(snapshot.get('timestamp')).be.an.instanceOf(firebase.firestore.Timestamp);
should(snapshot.get('timestamp').isEqual(previous.get('timestamp'))).equal(true);
break;
case 3:
should(snapshot.get('timestamp')).be.an.instanceOf(firebase.firestore.Timestamp);
should(snapshot.get('timestamp').isEqual(previous.get('timestamp'))).equal(false);
subscription();
resolve();
break;
}
previous = snapshot;
}, reject);
});

await ref.set({ timestamp: firebase.firestore.FieldValue.serverTimestamp() });
await new Promise(resolve => setTimeout(resolve, 1));
await ref.set({ timestamp: firebase.firestore.FieldValue.serverTimestamp() });
await promise;
await ref.delete();
});
it("handles 'none'", async function () {
firebase.firestore().settings({ serverTimestampBehavior: 'none' });
const ref = firebase.firestore().doc(`${COLLECTION}/getData`);

const promise = new Promise((resolve, reject) => {
const subscription = ref.onSnapshot(snapshot => {
should(snapshot.get('timestamp')).equal(null);
subscription();
resolve();
}, reject);
});

await ref.set({ timestamp: firebase.firestore.FieldValue.serverTimestamp() });
await promise;
await ref.delete();
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ - (void)invalidate {
if (error) {
return [RNFBFirestoreCommon promiseRejectFirestoreException:reject error:error];
} else {
NSDictionary *serialized = [RNFBFirestoreSerialize querySnapshotToDictionary:@"get" snapshot:snapshot includeMetadataChanges:false];
NSString *appName = [RNFBSharedUtils getAppJavaScriptName:firebaseApp.name];
NSDictionary *serialized = [RNFBFirestoreSerialize querySnapshotToDictionary:@"get" snapshot:snapshot includeMetadataChanges:false appName:appName];
resolve(serialized);
}
}];
Expand All @@ -158,7 +159,8 @@ - (void)sendSnapshotEvent:(FIRApp *)firApp
listenerId:(nonnull NSNumber *)listenerId
snapshot:(FIRQuerySnapshot *)snapshot
includeMetadataChanges:(BOOL)includeMetadataChanges {
NSDictionary *serialized = [RNFBFirestoreSerialize querySnapshotToDictionary:@"onSnapshot" snapshot:snapshot includeMetadataChanges:includeMetadataChanges];
NSString *appName = [RNFBSharedUtils getAppJavaScriptName:firApp.name];
NSDictionary *serialized = [RNFBFirestoreSerialize querySnapshotToDictionary:@"onSnapshot" snapshot:snapshot includeMetadataChanges:includeMetadataChanges appName:appName];
[[RNFBRCTEventEmitter shared] sendEventWithName:RNFB_FIRESTORE_COLLECTION_SYNC body:@{
@"appName": [RNFBSharedUtils getAppJavaScriptName:firApp.name],
@"listenerId": listenerId,
Expand Down
3 changes: 2 additions & 1 deletion packages/firestore/ios/RNFBFirestore/RNFBFirestoreCommon.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,5 @@ extern NSString *const FIRESTORE_CACHE_SIZE;
extern NSString *const FIRESTORE_HOST;
extern NSString *const FIRESTORE_PERSISTENCE;
extern NSString *const FIRESTORE_SSL;
extern NSMutableDictionary * instanceCache;
extern NSString *const FIRESTORE_SERVER_TIMESTAMP_BEHAVIOR;
extern NSMutableDictionary *instanceCache;
1 change: 1 addition & 0 deletions packages/firestore/ios/RNFBFirestore/RNFBFirestoreCommon.m
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
NSString *const FIRESTORE_HOST = @"firebase_firestore_host";
NSString *const FIRESTORE_PERSISTENCE = @"firebase_firestore_persistence";
NSString *const FIRESTORE_SSL = @"firebase_firestore_ssl";
NSString *const FIRESTORE_SERVER_TIMESTAMP_BEHAVIOR = @"firebase_firestore_server_timestamp_behavior";

NSMutableDictionary * instanceCache;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ - (void)invalidate {
if (error) {
return [RNFBFirestoreCommon promiseRejectFirestoreException:reject error:error];
} else {
NSDictionary *serialized = [RNFBFirestoreSerialize documentSnapshotToDictionary:snapshot];
NSString *appName = [RNFBSharedUtils getAppJavaScriptName:firebaseApp.name];
NSDictionary *serialized = [RNFBFirestoreSerialize documentSnapshotToDictionary:snapshot appName:appName];
resolve(serialized);
}
}];
Expand Down Expand Up @@ -259,7 +260,8 @@ - (void)invalidate {
- (void)sendSnapshotEvent:(FIRApp *)firApp
listenerId:(nonnull NSNumber *)listenerId
snapshot:(FIRDocumentSnapshot *)snapshot {
NSDictionary *serialized = [RNFBFirestoreSerialize documentSnapshotToDictionary:snapshot];
NSString *appName = [RNFBSharedUtils getAppJavaScriptName:firApp.name];
NSDictionary *serialized = [RNFBFirestoreSerialize documentSnapshotToDictionary:snapshot appName:appName];
[[RNFBRCTEventEmitter shared] sendEventWithName:RNFB_FIRESTORE_DOCUMENT_SYNC body:@{
@"appName": [RNFBSharedUtils getAppJavaScriptName:firApp.name],
@"listenerId": listenerId,
Expand Down
5 changes: 5 additions & 0 deletions packages/firestore/ios/RNFBFirestore/RNFBFirestoreModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ + (BOOL)requiresMainQueueSetup {
[[RNFBPreferences shared] setBooleanValue:sslKey boolValue:[settings[@"ssl"] boolValue]];
}

if (settings[@"serverTimestampBehavior"]) {
NSString *key = [NSString stringWithFormat:@"%@_%@", FIRESTORE_SERVER_TIMESTAMP_BEHAVIOR, appName];
[[RNFBPreferences shared] setStringValue:key stringValue:settings[@"serverTimestampBehavior"]];
}

resolve([NSNull null]);
}

Expand Down
Loading

1 comment on commit 60fe72e

@vercel
Copy link

@vercel vercel bot commented on 60fe72e Jul 30, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.