Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(firestore): serverTimestampBehavior #5556

Merged
merged 3 commits into from
Jul 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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