diff --git a/.github/workflows/scripts/firestore.rules b/.github/workflows/scripts/firestore.rules index 84924b7e66..d3220037de 100644 --- a/.github/workflows/scripts/firestore.rules +++ b/.github/workflows/scripts/firestore.rules @@ -9,9 +9,13 @@ service cloud.firestore { } match /firestore/{document=**} { allow read, write: if true; - } + } match /{path=**}/collectionGroup/{documentId} { allow read, write: if true; - } + } + match /second-database/{document=**} { + // separate rules are not supported so we need to use the same rules for both databases to prove it is querying different databases + allow read, write: if database == "second-rnfb"; + } } -} \ No newline at end of file +} diff --git a/packages/app/lib/internal/registry/namespace.js b/packages/app/lib/internal/registry/namespace.js index b57ba3496a..d8a41903e6 100644 --- a/packages/app/lib/internal/registry/namespace.js +++ b/packages/app/lib/internal/registry/namespace.js @@ -15,7 +15,7 @@ * */ -import { isString } from '@react-native-firebase/app/lib/common'; +import { isString } from '../../common'; import FirebaseApp from '../../FirebaseApp'; import SDK_VERSION from '../../version'; import { DEFAULT_APP_NAME, KNOWN_NAMESPACES } from '../constants'; @@ -93,19 +93,21 @@ function getOrCreateModuleForApp(app, moduleNamespace) { ); } - // e.g. firebase.storage(customUrlOrRegion) - function firebaseModuleWithArgs(customUrlOrRegion) { - if (customUrlOrRegion !== undefined) { + // e.g. firebase.storage(customUrlOrRegion), firebase.functions(customUrlOrRegion), firebase.firestore(databaseId), firebase.database(url) + function firebaseModuleWithArgs(customUrlOrRegionOrDatabaseId) { + if (customUrlOrRegionOrDatabaseId !== undefined) { if (!hasCustomUrlOrRegionSupport) { // TODO throw Module does not support arguments error } - if (!isString(customUrlOrRegion)) { + if (!isString(customUrlOrRegionOrDatabaseId)) { // TODO throw Module first argument must be a string error } } - const key = customUrlOrRegion ? `${customUrlOrRegion}:${moduleNamespace}` : moduleNamespace; + const key = customUrlOrRegionOrDatabaseId + ? `${customUrlOrRegionOrDatabaseId}:${moduleNamespace}` + : moduleNamespace; if (!APP_MODULE_INSTANCE[app.name]) { APP_MODULE_INSTANCE[app.name] = {}; @@ -115,7 +117,7 @@ function getOrCreateModuleForApp(app, moduleNamespace) { APP_MODULE_INSTANCE[app.name][key] = new ModuleClass( app, NAMESPACE_REGISTRY[moduleNamespace], - customUrlOrRegion, + customUrlOrRegionOrDatabaseId, ); } diff --git a/packages/app/lib/internal/registry/nativeModule.js b/packages/app/lib/internal/registry/nativeModule.js index 5657ef8ce3..2778b2dd75 100644 --- a/packages/app/lib/internal/registry/nativeModule.js +++ b/packages/app/lib/internal/registry/nativeModule.js @@ -153,7 +153,10 @@ function initialiseNativeModule(module) { function subscribeToNativeModuleEvent(eventName) { if (!NATIVE_MODULE_EVENT_SUBSCRIPTIONS[eventName]) { RNFBNativeEventEmitter.addListener(eventName, event => { - if (event.appName) { + if (event.appName && event.databaseId) { + // Firestore requires both appName and databaseId to prefix + SharedEventEmitter.emit(`${event.appName}-${event.databaseId}-${eventName}`, event); + } else if (event.appName) { // native event has an appName property - auto prefix and internally emit SharedEventEmitter.emit(`${event.appName}-${eventName}`, event); } else { diff --git a/packages/firestore/android/src/main/java/io/invertase/firebase/firestore/UniversalFirebaseFirestoreCommon.java b/packages/firestore/android/src/main/java/io/invertase/firebase/firestore/UniversalFirebaseFirestoreCommon.java index d217a42aae..876759eea1 100644 --- a/packages/firestore/android/src/main/java/io/invertase/firebase/firestore/UniversalFirebaseFirestoreCommon.java +++ b/packages/firestore/android/src/main/java/io/invertase/firebase/firestore/UniversalFirebaseFirestoreCommon.java @@ -29,8 +29,13 @@ public class UniversalFirebaseFirestoreCommon { static WeakHashMap> instanceCache = new WeakHashMap<>(); - static FirebaseFirestore getFirestoreForApp(String appName) { - WeakReference cachedInstance = instanceCache.get(appName); + static String createFirestoreKey(String appName, String databaseId) { + return appName + ":" + databaseId; + } + + static FirebaseFirestore getFirestoreForApp(String appName, String databaseId) { + String firestoreKey = createFirestoreKey(appName, databaseId); + WeakReference cachedInstance = instanceCache.get(firestoreKey); if (cachedInstance != null) { return cachedInstance.get(); @@ -38,24 +43,27 @@ static FirebaseFirestore getFirestoreForApp(String appName) { FirebaseApp firebaseApp = FirebaseApp.getInstance(appName); - FirebaseFirestore instance = FirebaseFirestore.getInstance(firebaseApp); + FirebaseFirestore instance = FirebaseFirestore.getInstance(firebaseApp, databaseId); - setFirestoreSettings(instance, appName); + setFirestoreSettings(instance, firestoreKey); instanceCache.put(appName, new WeakReference(instance)); return instance; } - private static void setFirestoreSettings(FirebaseFirestore firebaseFirestore, String appName) { + private static void setFirestoreSettings( + FirebaseFirestore firebaseFirestore, String firestoreKey) { UniversalFirebasePreferences preferences = UniversalFirebasePreferences.getSharedInstance(); FirebaseFirestoreSettings.Builder firestoreSettings = new FirebaseFirestoreSettings.Builder(); - String cacheSizeKey = UniversalFirebaseFirestoreStatics.FIRESTORE_CACHE_SIZE + "_" + appName; - String hostKey = UniversalFirebaseFirestoreStatics.FIRESTORE_HOST + "_" + appName; - String persistenceKey = UniversalFirebaseFirestoreStatics.FIRESTORE_PERSISTENCE + "_" + appName; - String sslKey = UniversalFirebaseFirestoreStatics.FIRESTORE_SSL + "_" + appName; + String cacheSizeKey = + UniversalFirebaseFirestoreStatics.FIRESTORE_CACHE_SIZE + "_" + firestoreKey; + String hostKey = UniversalFirebaseFirestoreStatics.FIRESTORE_HOST + "_" + firestoreKey; + String persistenceKey = + UniversalFirebaseFirestoreStatics.FIRESTORE_PERSISTENCE + "_" + firestoreKey; + String sslKey = UniversalFirebaseFirestoreStatics.FIRESTORE_SSL + "_" + firestoreKey; int cacheSizeBytes = preferences.getIntValue( diff --git a/packages/firestore/android/src/main/java/io/invertase/firebase/firestore/UniversalFirebaseFirestoreModule.java b/packages/firestore/android/src/main/java/io/invertase/firebase/firestore/UniversalFirebaseFirestoreModule.java index 1fcacfaffb..77345d3529 100644 --- a/packages/firestore/android/src/main/java/io/invertase/firebase/firestore/UniversalFirebaseFirestoreModule.java +++ b/packages/firestore/android/src/main/java/io/invertase/firebase/firestore/UniversalFirebaseFirestoreModule.java @@ -17,6 +17,7 @@ * */ +import static io.invertase.firebase.firestore.UniversalFirebaseFirestoreCommon.createFirestoreKey; import static io.invertase.firebase.firestore.UniversalFirebaseFirestoreCommon.getFirestoreForApp; import static io.invertase.firebase.firestore.UniversalFirebaseFirestoreCommon.instanceCache; @@ -40,27 +41,28 @@ public class UniversalFirebaseFirestoreModule extends UniversalFirebaseModule { super(context, serviceName); } - Task disableNetwork(String appName) { - return getFirestoreForApp(appName).disableNetwork(); + Task disableNetwork(String appName, String databaseId) { + return getFirestoreForApp(appName, databaseId).disableNetwork(); } - Task enableNetwork(String appName) { - return getFirestoreForApp(appName).enableNetwork(); + Task enableNetwork(String appName, String databaseId) { + return getFirestoreForApp(appName, databaseId).enableNetwork(); } - Task useEmulator(String appName, String host, int port) { + Task useEmulator(String appName, String databaseId, String host, int port) { return Tasks.call( getExecutor(), () -> { - if (emulatorConfigs.get(appName) == null) { - emulatorConfigs.put(appName, "true"); - getFirestoreForApp(appName).useEmulator(host, port); + String firestoreKey = createFirestoreKey(appName, databaseId); + if (emulatorConfigs.get(firestoreKey) == null) { + emulatorConfigs.put(firestoreKey, "true"); + getFirestoreForApp(appName, databaseId).useEmulator(host, port); } return null; }); } - Task settings(String appName, Map settings) { + Task settings(String firestoreKey, Map settings) { return Tasks.call( getExecutor(), () -> { @@ -70,7 +72,7 @@ Task settings(String appName, Map settings) { UniversalFirebasePreferences.getSharedInstance() .setIntValue( - UniversalFirebaseFirestoreStatics.FIRESTORE_CACHE_SIZE + "_" + appName, + UniversalFirebaseFirestoreStatics.FIRESTORE_CACHE_SIZE + "_" + firestoreKey, Objects.requireNonNull(cacheSizeBytesDouble).intValue()); } @@ -78,7 +80,7 @@ Task settings(String appName, Map settings) { if (settings.containsKey("host")) { UniversalFirebasePreferences.getSharedInstance() .setStringValue( - UniversalFirebaseFirestoreStatics.FIRESTORE_HOST + "_" + appName, + UniversalFirebaseFirestoreStatics.FIRESTORE_HOST + "_" + firestoreKey, (String) settings.get("host")); } @@ -86,7 +88,7 @@ Task settings(String appName, Map settings) { if (settings.containsKey("persistence")) { UniversalFirebasePreferences.getSharedInstance() .setBooleanValue( - UniversalFirebaseFirestoreStatics.FIRESTORE_PERSISTENCE + "_" + appName, + UniversalFirebaseFirestoreStatics.FIRESTORE_PERSISTENCE + "_" + firestoreKey, (boolean) settings.get("persistence")); } @@ -94,7 +96,7 @@ Task settings(String appName, Map settings) { if (settings.containsKey("ssl")) { UniversalFirebasePreferences.getSharedInstance() .setBooleanValue( - UniversalFirebaseFirestoreStatics.FIRESTORE_SSL + "_" + appName, + UniversalFirebaseFirestoreStatics.FIRESTORE_SSL + "_" + firestoreKey, (boolean) settings.get("ssl")); } @@ -104,7 +106,7 @@ Task settings(String appName, Map settings) { .setStringValue( UniversalFirebaseFirestoreStatics.FIRESTORE_SERVER_TIMESTAMP_BEHAVIOR + "_" - + appName, + + firestoreKey, (String) settings.get("serverTimestampBehavior")); } @@ -112,25 +114,25 @@ Task settings(String appName, Map settings) { }); } - LoadBundleTask loadBundle(String appName, String bundle) { + LoadBundleTask loadBundle(String appName, String databaseId, String bundle) { byte[] bundleData = bundle.getBytes(StandardCharsets.UTF_8); - return getFirestoreForApp(appName).loadBundle(bundleData); + return getFirestoreForApp(appName, databaseId).loadBundle(bundleData); } - Task clearPersistence(String appName) { - return getFirestoreForApp(appName).clearPersistence(); + Task clearPersistence(String appName, String databaseId) { + return getFirestoreForApp(appName, databaseId).clearPersistence(); } - Task waitForPendingWrites(String appName) { - return getFirestoreForApp(appName).waitForPendingWrites(); + Task waitForPendingWrites(String appName, String databaseId) { + return getFirestoreForApp(appName, databaseId).waitForPendingWrites(); } - Task terminate(String appName) { - FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName); - - if (instanceCache.get(appName) != null) { - instanceCache.get(appName).clear(); - instanceCache.remove(appName); + Task terminate(String appName, String databaseId) { + FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName, databaseId); + String firestoreKey = createFirestoreKey(appName, databaseId); + if (instanceCache.get(firestoreKey) != null) { + instanceCache.get(firestoreKey).clear(); + instanceCache.remove(firestoreKey); } return firebaseFirestore.terminate(); diff --git a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreCollectionModule.java b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreCollectionModule.java index 84f15c5906..982e38680c 100644 --- a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreCollectionModule.java +++ b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreCollectionModule.java @@ -53,6 +53,7 @@ public void onCatalystInstanceDestroy() { @ReactMethod public void namedQueryOnSnapshot( String appName, + String databaseId, String queryName, String type, ReadableArray filters, @@ -64,7 +65,7 @@ public void namedQueryOnSnapshot( return; } - FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName); + FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName, databaseId); firebaseFirestore .getNamedQuery(queryName) .addOnCompleteListener( @@ -72,15 +73,16 @@ public void namedQueryOnSnapshot( if (task.isSuccessful()) { Query query = task.getResult(); if (query == null) { - sendOnSnapshotError(appName, listenerId, new NullPointerException()); + sendOnSnapshotError(appName, databaseId, listenerId, new NullPointerException()); } else { ReactNativeFirebaseFirestoreQuery firestoreQuery = new ReactNativeFirebaseFirestoreQuery( - appName, query, filters, orders, options); - handleQueryOnSnapshot(firestoreQuery, appName, listenerId, listenerOptions); + appName, databaseId, query, filters, orders, options); + handleQueryOnSnapshot( + firestoreQuery, appName, databaseId, listenerId, listenerOptions); } } else { - sendOnSnapshotError(appName, listenerId, task.getException()); + sendOnSnapshotError(appName, databaseId, listenerId, task.getException()); } }); } @@ -88,6 +90,7 @@ public void namedQueryOnSnapshot( @ReactMethod public void collectionOnSnapshot( String appName, + String databaseId, String path, String type, ReadableArray filters, @@ -99,16 +102,21 @@ public void collectionOnSnapshot( return; } - FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName); + FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName, databaseId); ReactNativeFirebaseFirestoreQuery firestoreQuery = new ReactNativeFirebaseFirestoreQuery( - appName, getQueryForFirestore(firebaseFirestore, path, type), filters, orders, options); - - handleQueryOnSnapshot(firestoreQuery, appName, listenerId, listenerOptions); + appName, + databaseId, + getQueryForFirestore(firebaseFirestore, path, type), + filters, + orders, + options); + + handleQueryOnSnapshot(firestoreQuery, appName, databaseId, listenerId, listenerOptions); } @ReactMethod - public void collectionOffSnapshot(String appName, int listenerId) { + public void collectionOffSnapshot(String appName, String databaseId, int listenerId) { ListenerRegistration listenerRegistration = collectionSnapshotListeners.get(listenerId); if (listenerRegistration != null) { listenerRegistration.remove(); @@ -120,6 +128,7 @@ public void collectionOffSnapshot(String appName, int listenerId) { @ReactMethod public void namedQueryGet( String appName, + String databaseId, String queryName, String type, ReadableArray filters, @@ -127,7 +136,7 @@ public void namedQueryGet( ReadableMap options, ReadableMap getOptions, Promise promise) { - FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName); + FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName, databaseId); firebaseFirestore .getNamedQuery(queryName) .addOnCompleteListener( @@ -139,7 +148,7 @@ public void namedQueryGet( } else { ReactNativeFirebaseFirestoreQuery firestoreQuery = new ReactNativeFirebaseFirestoreQuery( - appName, query, filters, orders, options); + appName, databaseId, query, filters, orders, options); handleQueryGet(firestoreQuery, getSource(getOptions), promise); } } else { @@ -151,16 +160,22 @@ public void namedQueryGet( @ReactMethod public void collectionCount( String appName, + String databaseId, String path, String type, ReadableArray filters, ReadableArray orders, ReadableMap options, Promise promise) { - FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName); + FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName, databaseId); ReactNativeFirebaseFirestoreQuery firestoreQuery = new ReactNativeFirebaseFirestoreQuery( - appName, getQueryForFirestore(firebaseFirestore, path, type), filters, orders, options); + appName, + databaseId, + getQueryForFirestore(firebaseFirestore, path, type), + filters, + orders, + options); AggregateQuery aggregateQuery = firestoreQuery.query.count(); @@ -181,6 +196,7 @@ public void collectionCount( @ReactMethod public void collectionGet( String appName, + String databaseId, String path, String type, ReadableArray filters, @@ -188,16 +204,22 @@ public void collectionGet( ReadableMap options, ReadableMap getOptions, Promise promise) { - FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName); + FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName, databaseId); ReactNativeFirebaseFirestoreQuery firestoreQuery = new ReactNativeFirebaseFirestoreQuery( - appName, getQueryForFirestore(firebaseFirestore, path, type), filters, orders, options); + appName, + databaseId, + getQueryForFirestore(firebaseFirestore, path, type), + filters, + orders, + options); handleQueryGet(firestoreQuery, getSource(getOptions), promise); } private void handleQueryOnSnapshot( ReactNativeFirebaseFirestoreQuery firestoreQuery, String appName, + String databaseId, int listenerId, ReadableMap listenerOptions) { MetadataChanges metadataChanges; @@ -218,9 +240,9 @@ private void handleQueryOnSnapshot( listenerRegistration.remove(); collectionSnapshotListeners.remove(listenerId); } - sendOnSnapshotError(appName, listenerId, exception); + sendOnSnapshotError(appName, databaseId, listenerId, exception); } else { - sendOnSnapshotEvent(appName, listenerId, querySnapshot, metadataChanges); + sendOnSnapshotEvent(appName, databaseId, listenerId, querySnapshot, metadataChanges); } }; @@ -246,12 +268,15 @@ private void handleQueryGet( private void sendOnSnapshotEvent( String appName, + String databaseId, int listenerId, QuerySnapshot querySnapshot, MetadataChanges metadataChanges) { Tasks.call( getTransactionalExecutor(Integer.toString(listenerId)), - () -> snapshotToWritableMap(appName, "onSnapshot", querySnapshot, metadataChanges)) + () -> + snapshotToWritableMap( + appName, databaseId, "onSnapshot", querySnapshot, metadataChanges)) .addOnCompleteListener( task -> { if (task.isSuccessful()) { @@ -266,14 +291,16 @@ private void sendOnSnapshotEvent( ReactNativeFirebaseFirestoreEvent.COLLECTION_EVENT_SYNC, body, appName, + databaseId, listenerId)); } else { - sendOnSnapshotError(appName, listenerId, task.getException()); + sendOnSnapshotError(appName, databaseId, listenerId, task.getException()); } }); } - private void sendOnSnapshotError(String appName, int listenerId, Exception exception) { + private void sendOnSnapshotError( + String appName, String databaseId, int listenerId, Exception exception) { WritableMap body = Arguments.createMap(); WritableMap error = Arguments.createMap(); @@ -293,7 +320,11 @@ private void sendOnSnapshotError(String appName, int listenerId, Exception excep emitter.sendEvent( new ReactNativeFirebaseFirestoreEvent( - ReactNativeFirebaseFirestoreEvent.COLLECTION_EVENT_SYNC, body, appName, listenerId)); + ReactNativeFirebaseFirestoreEvent.COLLECTION_EVENT_SYNC, + body, + appName, + databaseId, + listenerId)); } private Source getSource(ReadableMap getOptions) { diff --git a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreCommon.java b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreCommon.java index 021400d088..00b714a893 100644 --- a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreCommon.java +++ b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreCommon.java @@ -19,6 +19,7 @@ import static io.invertase.firebase.common.ReactNativeFirebaseModule.rejectPromiseWithCodeAndMessage; import static io.invertase.firebase.common.ReactNativeFirebaseModule.rejectPromiseWithExceptionMap; +import static io.invertase.firebase.firestore.UniversalFirebaseFirestoreCommon.createFirestoreKey; import com.facebook.react.bridge.Promise; import com.google.firebase.firestore.DocumentSnapshot; @@ -48,10 +49,12 @@ static void rejectPromiseFirestoreException(Promise promise, Exception exception } } - static DocumentSnapshot.ServerTimestampBehavior getServerTimestampBehavior(String appName) { + static DocumentSnapshot.ServerTimestampBehavior getServerTimestampBehavior( + String appName, String databaseId) { + String firestoreKey = createFirestoreKey(appName, databaseId); UniversalFirebasePreferences preferences = UniversalFirebasePreferences.getSharedInstance(); String key = - UniversalFirebaseFirestoreStatics.FIRESTORE_SERVER_TIMESTAMP_BEHAVIOR + "_" + appName; + UniversalFirebaseFirestoreStatics.FIRESTORE_SERVER_TIMESTAMP_BEHAVIOR + "_" + firestoreKey; String behavior = preferences.getStringValue(key, "none"); if ("estimate".equals(behavior)) { diff --git a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreDocumentModule.java b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreDocumentModule.java index 07fca6fc49..e99531b3f9 100644 --- a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreDocumentModule.java +++ b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreDocumentModule.java @@ -56,12 +56,12 @@ public void onCatalystInstanceDestroy() { @ReactMethod public void documentOnSnapshot( - String appName, String path, int listenerId, ReadableMap listenerOptions) { + String appName, String databaseId, String path, int listenerId, ReadableMap listenerOptions) { if (documentSnapshotListeners.get(listenerId) != null) { return; } - FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName); + FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName, databaseId); DocumentReference documentReference = getDocumentForFirestore(firebaseFirestore, path); final EventListener listener = @@ -72,9 +72,9 @@ public void documentOnSnapshot( listenerRegistration.remove(); documentSnapshotListeners.remove(listenerId); } - sendOnSnapshotError(appName, listenerId, exception); + sendOnSnapshotError(appName, databaseId, listenerId, exception); } else { - sendOnSnapshotEvent(appName, listenerId, documentSnapshot); + sendOnSnapshotEvent(appName, databaseId, listenerId, documentSnapshot); } }; @@ -95,7 +95,7 @@ public void documentOnSnapshot( } @ReactMethod - public void documentOffSnapshot(String appName, int listenerId) { + public void documentOffSnapshot(String appName, String databaseId, int listenerId) { ListenerRegistration listenerRegistration = documentSnapshotListeners.get(listenerId); if (listenerRegistration != null) { listenerRegistration.remove(); @@ -104,8 +104,9 @@ public void documentOffSnapshot(String appName, int listenerId) { } @ReactMethod - public void documentGet(String appName, String path, ReadableMap getOptions, Promise promise) { - FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName); + public void documentGet( + String appName, String databaseId, String path, ReadableMap getOptions, Promise promise) { + FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName, databaseId); DocumentReference documentReference = getDocumentForFirestore(firebaseFirestore, path); Source source; @@ -127,7 +128,7 @@ public void documentGet(String appName, String path, ReadableMap getOptions, Pro getExecutor(), () -> { DocumentSnapshot documentSnapshot = Tasks.await(documentReference.get(source)); - return snapshotToWritableMap(appName, documentSnapshot); + return snapshotToWritableMap(appName, databaseId, documentSnapshot); }) .addOnCompleteListener( task -> { @@ -140,8 +141,8 @@ public void documentGet(String appName, String path, ReadableMap getOptions, Pro } @ReactMethod - public void documentDelete(String appName, String path, Promise promise) { - FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName); + public void documentDelete(String appName, String databaseId, String path, Promise promise) { + FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName, databaseId); DocumentReference documentReference = getDocumentForFirestore(firebaseFirestore, path); Tasks.call(getTransactionalExecutor(), documentReference::delete) .addOnCompleteListener( @@ -156,8 +157,13 @@ public void documentDelete(String appName, String path, Promise promise) { @ReactMethod public void documentSet( - String appName, String path, ReadableMap data, ReadableMap options, Promise promise) { - FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName); + String appName, + String databaseId, + String path, + ReadableMap data, + ReadableMap options, + Promise promise) { + FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName, databaseId); DocumentReference documentReference = getDocumentForFirestore(firebaseFirestore, path); Tasks.call(getTransactionalExecutor(), () -> parseReadableMap(firebaseFirestore, data)) @@ -195,8 +201,9 @@ public void documentSet( } @ReactMethod - public void documentUpdate(String appName, String path, ReadableMap data, Promise promise) { - FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName); + public void documentUpdate( + String appName, String databaseId, String path, ReadableMap data, Promise promise) { + FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName, databaseId); DocumentReference documentReference = getDocumentForFirestore(firebaseFirestore, path); Tasks.call(getTransactionalExecutor(), () -> parseReadableMap(firebaseFirestore, data)) @@ -214,8 +221,9 @@ public void documentUpdate(String appName, String path, ReadableMap data, Promis } @ReactMethod - public void documentBatch(String appName, ReadableArray writes, Promise promise) { - FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName); + public void documentBatch( + String appName, String databaseId, ReadableArray writes, Promise promise) { + FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName, databaseId); Tasks.call(getTransactionalExecutor(), () -> parseDocumentBatches(firebaseFirestore, writes)) .continueWithTask( @@ -282,8 +290,8 @@ public void documentBatch(String appName, ReadableArray writes, Promise promise) } private void sendOnSnapshotEvent( - String appName, int listenerId, DocumentSnapshot documentSnapshot) { - Tasks.call(getExecutor(), () -> snapshotToWritableMap(appName, documentSnapshot)) + String appName, String databaseId, int listenerId, DocumentSnapshot documentSnapshot) { + Tasks.call(getExecutor(), () -> snapshotToWritableMap(appName, databaseId, documentSnapshot)) .addOnCompleteListener( task -> { if (task.isSuccessful()) { @@ -298,14 +306,16 @@ private void sendOnSnapshotEvent( ReactNativeFirebaseFirestoreEvent.DOCUMENT_EVENT_SYNC, body, appName, + databaseId, listenerId)); } else { - sendOnSnapshotError(appName, listenerId, task.getException()); + sendOnSnapshotError(appName, databaseId, listenerId, task.getException()); } }); } - private void sendOnSnapshotError(String appName, int listenerId, Exception exception) { + private void sendOnSnapshotError( + String appName, String databaseId, int listenerId, Exception exception) { WritableMap body = Arguments.createMap(); WritableMap error = Arguments.createMap(); @@ -325,6 +335,10 @@ private void sendOnSnapshotError(String appName, int listenerId, Exception excep emitter.sendEvent( new ReactNativeFirebaseFirestoreEvent( - ReactNativeFirebaseFirestoreEvent.DOCUMENT_EVENT_SYNC, body, appName, listenerId)); + ReactNativeFirebaseFirestoreEvent.DOCUMENT_EVENT_SYNC, + body, + appName, + databaseId, + listenerId)); } } diff --git a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreEvent.java b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreEvent.java index 614172423e..535c1e0998 100644 --- a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreEvent.java +++ b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreEvent.java @@ -30,16 +30,19 @@ public class ReactNativeFirebaseFirestoreEvent implements NativeEvent { private static final String KEY_BODY = "body"; private static final String KEY_APP_NAME = "appName"; private static final String KEY_EVENT_NAME = "eventName"; + private static final String DATABASE_ID = "databaseId"; private String eventName; private WritableMap eventBody; private String appName; + private String databaseId; private int listenerId; ReactNativeFirebaseFirestoreEvent( - String eventName, WritableMap eventBody, String appName, int listenerId) { + String eventName, WritableMap eventBody, String appName, String databaseId, int listenerId) { this.eventName = eventName; this.eventBody = eventBody; this.appName = appName; + this.databaseId = databaseId; this.listenerId = listenerId; } @@ -54,6 +57,7 @@ public WritableMap getEventBody() { event.putInt(KEY_ID, listenerId); event.putMap(KEY_BODY, eventBody); event.putString(KEY_APP_NAME, appName); + event.putString(DATABASE_ID, databaseId); event.putString(KEY_EVENT_NAME, eventName); return event; } diff --git a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreModule.java b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreModule.java index 323188ee47..d3405533ef 100644 --- a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreModule.java +++ b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreModule.java @@ -19,6 +19,7 @@ import static io.invertase.firebase.common.RCTConvertFirebase.toHashMap; import static io.invertase.firebase.firestore.ReactNativeFirebaseFirestoreCommon.rejectPromiseFirestoreException; +import static io.invertase.firebase.firestore.UniversalFirebaseFirestoreCommon.createFirestoreKey; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; @@ -49,9 +50,9 @@ public void setLogLevel(String logLevel) { } @ReactMethod - public void loadBundle(String appName, String bundle, Promise promise) { + public void loadBundle(String appName, String databaseId, String bundle, Promise promise) { module - .loadBundle(appName, bundle) + .loadBundle(appName, databaseId, bundle) .addOnCompleteListener( task -> { if (task.isSuccessful()) { @@ -64,9 +65,9 @@ public void loadBundle(String appName, String bundle, Promise promise) { } @ReactMethod - public void clearPersistence(String appName, Promise promise) { + public void clearPersistence(String appName, String databaseId, Promise promise) { module - .clearPersistence(appName) + .clearPersistence(appName, databaseId) .addOnCompleteListener( task -> { if (task.isSuccessful()) { @@ -78,9 +79,9 @@ public void clearPersistence(String appName, Promise promise) { } @ReactMethod - public void waitForPendingWrites(String appName, Promise promise) { + public void waitForPendingWrites(String appName, String databaseId, Promise promise) { module - .waitForPendingWrites(appName) + .waitForPendingWrites(appName, databaseId) .addOnCompleteListener( task -> { if (task.isSuccessful()) { @@ -92,9 +93,9 @@ public void waitForPendingWrites(String appName, Promise promise) { } @ReactMethod - public void disableNetwork(String appName, Promise promise) { + public void disableNetwork(String appName, String databaseId, Promise promise) { module - .disableNetwork(appName) + .disableNetwork(appName, databaseId) .addOnCompleteListener( task -> { if (task.isSuccessful()) { @@ -106,9 +107,9 @@ public void disableNetwork(String appName, Promise promise) { } @ReactMethod - public void enableNetwork(String appName, Promise promise) { + public void enableNetwork(String appName, String databaseId, Promise promise) { module - .enableNetwork(appName) + .enableNetwork(appName, databaseId) .addOnCompleteListener( task -> { if (task.isSuccessful()) { @@ -120,9 +121,10 @@ public void enableNetwork(String appName, Promise promise) { } @ReactMethod - public void useEmulator(String appName, String host, int port, Promise promise) { + public void useEmulator( + String appName, String databaseId, String host, int port, Promise promise) { module - .useEmulator(appName, host, port) + .useEmulator(appName, databaseId, host, port) .addOnCompleteListener( task -> { if (task.isSuccessful()) { @@ -134,9 +136,10 @@ public void useEmulator(String appName, String host, int port, Promise promise) } @ReactMethod - public void settings(String appName, ReadableMap settings, Promise promise) { + public void settings(String appName, String databaseId, ReadableMap settings, Promise promise) { + String firestoreKey = createFirestoreKey(appName, databaseId); module - .settings(appName, toHashMap(settings)) + .settings(firestoreKey, toHashMap(settings)) .addOnCompleteListener( task -> { if (task.isSuccessful()) { @@ -148,9 +151,9 @@ public void settings(String appName, ReadableMap settings, Promise promise) { } @ReactMethod - public void terminate(String appName, Promise promise) { + public void terminate(String appName, String databaseId, Promise promise) { module - .terminate(appName) + .terminate(appName, databaseId) .addOnCompleteListener( task -> { if (task.isSuccessful()) { diff --git a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreQuery.java b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreQuery.java index 18eaef2e2b..990bc8415a 100644 --- a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreQuery.java +++ b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreQuery.java @@ -38,10 +38,12 @@ public class ReactNativeFirebaseFirestoreQuery { String appName; + String databaseId; Query query; ReactNativeFirebaseFirestoreQuery( String appName, + String databaseId, Query query, ReadableArray filters, ReadableArray orders, @@ -58,7 +60,7 @@ public Task get(Executor executor, Source source) { executor, () -> { QuerySnapshot querySnapshot = Tasks.await(query.get(source)); - return snapshotToWritableMap(this.appName, "get", querySnapshot, null); + return snapshotToWritableMap(this.appName, this.databaseId, "get", querySnapshot, null); }); } diff --git a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreSerialize.java b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreSerialize.java index 58c86233f4..35a9f0727f 100644 --- a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreSerialize.java +++ b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreSerialize.java @@ -98,7 +98,8 @@ public class ReactNativeFirebaseFirestoreSerialize { * @param documentSnapshot DocumentSnapshot * @return WritableMap */ - static WritableMap snapshotToWritableMap(String appName, DocumentSnapshot documentSnapshot) { + static WritableMap snapshotToWritableMap( + String appName, String databaseId, DocumentSnapshot documentSnapshot) { WritableArray metadata = Arguments.createArray(); WritableMap documentMap = Arguments.createMap(); SnapshotMetadata snapshotMetadata = documentSnapshot.getMetadata(); @@ -112,7 +113,7 @@ static WritableMap snapshotToWritableMap(String appName, DocumentSnapshot docume documentMap.putBoolean(KEY_EXISTS, documentSnapshot.exists()); DocumentSnapshot.ServerTimestampBehavior timestampBehavior = - getServerTimestampBehavior(appName); + getServerTimestampBehavior(appName, databaseId); if (documentSnapshot.exists()) { if (documentSnapshot.getData(timestampBehavior) != null) { @@ -132,6 +133,7 @@ static WritableMap snapshotToWritableMap(String appName, DocumentSnapshot docume */ static WritableMap snapshotToWritableMap( String appName, + String databaseId, String source, QuerySnapshot querySnapshot, @Nullable MetadataChanges metadataChanges) { @@ -148,7 +150,8 @@ static WritableMap snapshotToWritableMap( // indicating the data does not include these changes writableMap.putBoolean("excludesMetadataChanges", true); writableMap.putArray( - KEY_CHANGES, documentChangesToWritableArray(appName, documentChangesList, null)); + KEY_CHANGES, + documentChangesToWritableArray(appName, databaseId, 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 @@ -159,7 +162,7 @@ static WritableMap snapshotToWritableMap( writableMap.putArray( KEY_CHANGES, documentChangesToWritableArray( - appName, documentMetadataChangesList, documentChangesList)); + appName, databaseId, documentMetadataChangesList, documentChangesList)); } SnapshotMetadata snapshotMetadata = querySnapshot.getMetadata(); @@ -167,7 +170,7 @@ static WritableMap snapshotToWritableMap( // set documents for (DocumentSnapshot documentSnapshot : documentSnapshots) { - documents.pushMap(snapshotToWritableMap(appName, documentSnapshot)); + documents.pushMap(snapshotToWritableMap(appName, databaseId, documentSnapshot)); } writableMap.putArray(KEY_DOCUMENTS, documents); @@ -188,6 +191,7 @@ static WritableMap snapshotToWritableMap( */ private static WritableArray documentChangesToWritableArray( String appName, + String databaseId, List documentChanges, @Nullable List comparableDocumentChanges) { WritableArray documentChangesWritable = Arguments.createArray(); @@ -212,7 +216,7 @@ private static WritableArray documentChangesToWritableArray( } documentChangesWritable.pushMap( - documentChangeToWritableMap(appName, documentChange, isMetadataChange)); + documentChangeToWritableMap(appName, databaseId, documentChange, isMetadataChange)); } return documentChangesWritable; @@ -225,7 +229,7 @@ private static WritableArray documentChangesToWritableArray( * @return WritableMap */ private static WritableMap documentChangeToWritableMap( - String appName, DocumentChange documentChange, boolean isMetadataChange) { + String appName, String databaseId, DocumentChange documentChange, boolean isMetadataChange) { WritableMap documentChangeMap = Arguments.createMap(); documentChangeMap.putBoolean("isMetadataChange", isMetadataChange); @@ -242,7 +246,8 @@ private static WritableMap documentChangeToWritableMap( } documentChangeMap.putMap( - KEY_DOC_CHANGE_DOCUMENT, snapshotToWritableMap(appName, documentChange.getDocument())); + KEY_DOC_CHANGE_DOCUMENT, + snapshotToWritableMap(appName, databaseId, documentChange.getDocument())); documentChangeMap.putInt(KEY_DOC_CHANGE_NEW_INDEX, documentChange.getNewIndex()); documentChangeMap.putInt(KEY_DOC_CHANGE_OLD_INDEX, documentChange.getOldIndex()); diff --git a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreTransactionModule.java b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreTransactionModule.java index d22efc6f33..fd92061a09 100644 --- a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreTransactionModule.java +++ b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreTransactionModule.java @@ -62,7 +62,7 @@ public void onCatalystInstanceDestroy() { @ReactMethod public void transactionGetDocument( - String appName, int transactionId, String path, Promise promise) { + String appName, String databaseId, int transactionId, String path, Promise promise) { ReactNativeFirebaseFirestoreTransactionHandler transactionHandler = transactionHandlers.get(transactionId); @@ -74,12 +74,14 @@ public void transactionGetDocument( return; } - FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName); + FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName, databaseId); DocumentReference documentReference = getDocumentForFirestore(firebaseFirestore, path); Tasks.call( getTransactionalExecutor(), - () -> snapshotToWritableMap(appName, transactionHandler.getDocument(documentReference))) + () -> + snapshotToWritableMap( + appName, databaseId, transactionHandler.getDocument(documentReference))) .addOnCompleteListener( task -> { if (task.isSuccessful()) { @@ -91,7 +93,7 @@ public void transactionGetDocument( } @ReactMethod - public void transactionDispose(String appName, int transactionId) { + public void transactionDispose(String appName, String databaseId, int transactionId) { ReactNativeFirebaseFirestoreTransactionHandler transactionHandler = transactionHandlers.get(transactionId); @@ -103,7 +105,7 @@ public void transactionDispose(String appName, int transactionId) { @ReactMethod public void transactionApplyBuffer( - String appName, int transactionId, ReadableArray commandBuffer) { + String appName, String databaseId, int transactionId, ReadableArray commandBuffer) { ReactNativeFirebaseFirestoreTransactionHandler handler = transactionHandlers.get(transactionId); if (handler != null) { @@ -112,12 +114,12 @@ public void transactionApplyBuffer( } @ReactMethod - public void transactionBegin(String appName, int transactionId) { + public void transactionBegin(String appName, String databaseId, int transactionId) { ReactNativeFirebaseFirestoreTransactionHandler transactionHandler = new ReactNativeFirebaseFirestoreTransactionHandler(appName, transactionId); transactionHandlers.put(transactionId, transactionHandler); - FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName); + FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName, databaseId); ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance(); // Provides its own executor @@ -138,6 +140,7 @@ public void transactionBegin(String appName, int transactionId) { ReactNativeFirebaseFirestoreEvent.TRANSACTION_EVENT_SYNC, eventMap, transactionHandler.getAppName(), + databaseId, transactionHandler.getTransactionId())); }); @@ -227,6 +230,7 @@ public void transactionBegin(String appName, int transactionId) { ReactNativeFirebaseFirestoreEvent.TRANSACTION_EVENT_SYNC, eventMap, transactionHandler.getAppName(), + databaseId, transactionHandler.getTransactionId())); } else { eventMap.putString("type", "error"); @@ -247,6 +251,7 @@ public void transactionBegin(String appName, int transactionId) { ReactNativeFirebaseFirestoreEvent.TRANSACTION_EVENT_SYNC, eventMap, transactionHandler.getAppName(), + databaseId, transactionHandler.getTransactionId())); } }); diff --git a/packages/firestore/e2e/SecondDatabase/second.Transation.e2e.js b/packages/firestore/e2e/SecondDatabase/second.Transation.e2e.js new file mode 100644 index 0000000000..35a18b7998 --- /dev/null +++ b/packages/firestore/e2e/SecondDatabase/second.Transation.e2e.js @@ -0,0 +1,774 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +const NO_RULE_COLLECTION = 'no_rules'; + +// This collection is only allowed on the second database +const COLLECTION = 'second-database'; +const SECOND_DATABASE_ID = 'second-rnfb'; + +describe('Second Database', function () { + describe('firestore.Transaction', function () { + describe('v8 compatibility', function () { + let firestore; + + before(function () { + firestore = firebase.app().firestore(SECOND_DATABASE_ID); + }); + + it('should throw if updateFunction is not a Promise', async function () { + try { + await firestore.runTransaction(() => 123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'updateFunction' must return a Promise"); + return Promise.resolve(); + } + }); + + it('should return an instance of FirestoreTransaction', async function () { + await firestore.runTransaction(async transaction => { + transaction.constructor.name.should.eql('FirestoreTransaction'); + return null; + }); + }); + + it('should resolve with user value', async function () { + const expected = Date.now(); + + const value = await firestore.runTransaction(async () => { + return expected; + }); + + value.should.eql(expected); + }); + + it('should reject with user Error', async function () { + const message = `Error: ${Date.now()}`; + + try { + await firestore.runTransaction(async () => { + throw new Error(message); + }); + return Promise.reject(new Error('Did not throw Error.')); + } catch (error) { + error.message.should.eql(message); + return Promise.resolve(); + } + }); + + it('should reject a native error', async function () { + const docRef = firestore.doc(`${NO_RULE_COLLECTION}/foo`); + + try { + await firestore.runTransaction(async t => { + t.set(docRef, { + foo: 'bar', + }); + }); + return Promise.reject(new Error('Did not throw Error.')); + } catch (error) { + error.code.should.eql('firestore/permission-denied'); + return Promise.resolve(); + } + }); + + describe('transaction.get()', function () { + it('should throw if not providing a document reference', async function () { + try { + await firestore.runTransaction(t => { + return t.get(123); + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'documentRef' expected a DocumentReference"); + return Promise.resolve(); + } + }); + + it('should get a document and return a DocumentSnapshot', async function () { + const docRef = firestore.doc(`${COLLECTION}/transactions/transaction/get-delete`); + await docRef.set({}); + + await firestore.runTransaction(async t => { + const docSnapshot = await t.get(docRef); + docSnapshot.constructor.name.should.eql('FirestoreDocumentSnapshot'); + docSnapshot.exists.should.eql(true); + docSnapshot.id.should.eql('get-delete'); + + t.delete(docRef); + }); + }); + }); + + describe('transaction.delete()', function () { + it('should throw if not providing a document reference', async function () { + try { + await firestore.runTransaction(async t => { + t.delete(123); + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'documentRef' expected a DocumentReference"); + return Promise.resolve(); + } + }); + + it('should delete documents', async function () { + const docRef1 = firestore.doc(`${COLLECTION}/transactions/transaction/delete-delete1`); + await docRef1.set({}); + + const docRef2 = firestore.doc(`${COLLECTION}/transactions/transaction/delete-delete2`); + await docRef2.set({}); + + await firestore.runTransaction(async t => { + t.delete(docRef1); + t.delete(docRef2); + }); + + const snapshot1 = await docRef1.get(); + snapshot1.exists.should.eql(false); + + const snapshot2 = await docRef2.get(); + snapshot2.exists.should.eql(false); + }); + }); + + describe('transaction.update()', function () { + it('should throw if not providing a document reference', async function () { + try { + await firestore.runTransaction(async t => { + t.update(123); + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'documentRef' expected a DocumentReference"); + return Promise.resolve(); + } + }); + + it('should throw if update args are invalid', async function () { + const docRef = firestore.doc(`${COLLECTION}/foo`); + + try { + await firestore.runTransaction(async t => { + t.update(docRef, 123); + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('it must be an object'); + return Promise.resolve(); + } + }); + + it('should update documents', async function () { + const value = Date.now(); + + const docRef1 = firestore.doc(`${COLLECTION}/transactions/transaction/delete-delete1`); + await docRef1.set({ + foo: 'bar', + bar: 'baz', + }); + + const docRef2 = firestore.doc(`${COLLECTION}/transactions/transaction/delete-delete2`); + await docRef2.set({ + foo: 'bar', + bar: 'baz', + }); + + await firestore.runTransaction(async t => { + t.update(docRef1, { + bar: value, + }); + t.update(docRef2, 'bar', value); + }); + + const expected = { + foo: 'bar', + bar: value, + }; + + const snapshot1 = await docRef1.get(); + snapshot1.exists.should.eql(true); + snapshot1.data().should.eql(jet.contextify(expected)); + + const snapshot2 = await docRef2.get(); + snapshot2.exists.should.eql(true); + snapshot2.data().should.eql(jet.contextify(expected)); + }); + }); + + describe('transaction.set()', function () { + it('should throw if not providing a document reference', async function () { + try { + await firestore.runTransaction(async t => { + t.set(123); + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'documentRef' expected a DocumentReference"); + return Promise.resolve(); + } + }); + + it('should throw if set data is invalid', async function () { + const docRef = firestore.doc(`${COLLECTION}/foo`); + + try { + await firestore.runTransaction(async t => { + t.set(docRef, 123); + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'data' must be an object."); + return Promise.resolve(); + } + }); + + it('should throw if set options are invalid', async function () { + const docRef = firestore.doc(`${COLLECTION}/foo`); + + try { + await firestore.runTransaction(async t => { + t.set( + docRef, + {}, + { + merge: true, + mergeFields: [], + }, + ); + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "'options' must not contain both 'merge' & 'mergeFields'", + ); + return Promise.resolve(); + } + }); + + it('should set data', async function () { + const docRef = firestore.doc(`${COLLECTION}/transactions/transaction/set`); + await docRef.set({ + foo: 'bar', + }); + const expected = { + foo: 'baz', + }; + + await firestore.runTransaction(async t => { + t.set(docRef, expected); + }); + + const snapshot = await docRef.get(); + snapshot.data().should.eql(jet.contextify(expected)); + }); + + it('should set data with merge', async function () { + const docRef = firestore.doc(`${COLLECTION}/transactions/transaction/set-merge`); + await docRef.set({ + foo: 'bar', + bar: 'baz', + }); + const expected = { + foo: 'bar', + bar: 'foo', + }; + + await firestore.runTransaction(async t => { + t.set( + docRef, + { + bar: 'foo', + }, + { + merge: true, + }, + ); + }); + + const snapshot = await docRef.get(); + snapshot.data().should.eql(jet.contextify(expected)); + }); + + it('should set data with merge fields', async function () { + const docRef = firestore.doc(`${COLLECTION}/transactions/transaction/set-mergefields`); + await docRef.set({ + foo: 'bar', + bar: 'baz', + baz: 'ben', + }); + const expected = { + foo: 'bar', + bar: 'foo', + baz: 'foo', + }; + + await firestore.runTransaction(async t => { + t.set( + docRef, + { + bar: 'foo', + baz: 'foo', + }, + { + mergeFields: ['bar', new firebase.firestore.FieldPath('baz')], + }, + ); + }); + + const snapshot = await docRef.get(); + snapshot.data().should.eql(jet.contextify(expected)); + }); + + it('should roll back any updates that failed', async function () { + const docRef = firestore.doc(`${COLLECTION}/transactions/transaction/rollback`); + + await docRef.set({ + turn: 0, + }); + + const prop1 = 'prop1'; + const prop2 = 'prop2'; + const turn = 0; + const errorMessage = 'turn cannot exceed 1'; + + const createTransaction = prop => { + return firestore.runTransaction(async transaction => { + const doc = await transaction.get(docRef); + const data = doc.data(); + + if (data.turn !== turn) { + throw new Error(errorMessage); + } + + const update = { + turn: turn + 1, + [prop]: 1, + }; + + transaction.update(docRef, update); + }); + }; + + const promises = [createTransaction(prop1), createTransaction(prop2)]; + + try { + await Promise.all(promises); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql(errorMessage); + } + const result = await docRef.get(); + should(result.data()).not.have.properties([prop1, prop2]); + }); + }); + }); + + describe('modular', function () { + let firestore; + + before(function () { + const { getFirestore } = firestoreModular; + firestore = getFirestore(null, SECOND_DATABASE_ID); + }); + + it('should throw if updateFunction is not a Promise', async function () { + const { runTransaction } = firestoreModular; + + try { + await runTransaction(firestore, () => 123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'updateFunction' must return a Promise"); + return Promise.resolve(); + } + }); + + it('should return an instance of FirestoreTransaction', async function () { + const { runTransaction } = firestoreModular; + await runTransaction(firestore, async transaction => { + transaction.constructor.name.should.eql('FirestoreTransaction'); + return null; + }); + }); + + it('should resolve with user value', async function () { + const { runTransaction } = firestoreModular; + const expected = Date.now(); + + const value = await runTransaction(firestore, async () => { + return expected; + }); + + value.should.eql(expected); + }); + + it('should reject with user Error', async function () { + const { runTransaction } = firestoreModular; + const message = `Error: ${Date.now()}`; + + try { + await runTransaction(firestore, async () => { + throw new Error(message); + }); + return Promise.reject(new Error('Did not throw Error.')); + } catch (error) { + error.message.should.eql(message); + return Promise.resolve(); + } + }); + + it('should reject a native error', async function () { + const { runTransaction, doc } = firestoreModular; + const db = firestore; + const docRef = doc(db, `${NO_RULE_COLLECTION}/foo`); + + try { + await runTransaction(db, async t => { + t.set(docRef, { + foo: 'bar', + }); + }); + return Promise.reject(new Error('Did not throw Error.')); + } catch (error) { + error.code.should.eql('firestore/permission-denied'); + return Promise.resolve(); + } + }); + + describe('transaction.get()', function () { + it('should throw if not providing a document reference', async function () { + const { runTransaction } = firestoreModular; + try { + await runTransaction(firestore, t => { + return t.get(123); + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'documentRef' expected a DocumentReference"); + return Promise.resolve(); + } + }); + + it('should get a document and return a DocumentSnapshot', async function () { + const { runTransaction, doc, setDoc } = firestoreModular; + const db = firestore; + const docRef = doc(db, `${COLLECTION}/transactions/transaction/get-delete`); + await setDoc(docRef, {}); + + await runTransaction(db, async t => { + const docSnapshot = await t.get(docRef); + docSnapshot.constructor.name.should.eql('FirestoreDocumentSnapshot'); + docSnapshot.exists.should.eql(true); + docSnapshot.id.should.eql('get-delete'); + + t.delete(docRef); + }); + }); + }); + + describe('transaction.delete()', function () { + it('should throw if not providing a document reference', async function () { + const { runTransaction } = firestoreModular; + try { + await runTransaction(firestore, async t => { + t.delete(123); + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'documentRef' expected a DocumentReference"); + return Promise.resolve(); + } + }); + + it('should delete documents', async function () { + const { runTransaction, doc, setDoc, getDoc } = firestoreModular; + const db = firestore; + const docRef1 = doc(db, `${COLLECTION}/transactions/transaction/delete-delete1`); + await setDoc(docRef1, {}); + + const docRef2 = doc(db, `${COLLECTION}/transactions/transaction/delete-delete2`); + await setDoc(docRef2, {}); + + await runTransaction(db, async t => { + t.delete(docRef1); + t.delete(docRef2); + }); + + const snapshot1 = await getDoc(docRef1); + snapshot1.exists.should.eql(false); + + const snapshot2 = await getDoc(docRef2); + snapshot2.exists.should.eql(false); + }); + }); + + describe('transaction.update()', function () { + it('should throw if not providing a document reference', async function () { + const { runTransaction } = firestoreModular; + try { + await runTransaction(firestore, async t => { + t.update(123); + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'documentRef' expected a DocumentReference"); + return Promise.resolve(); + } + }); + + it('should throw if update args are invalid', async function () { + const { runTransaction, doc } = firestoreModular; + const db = firestore; + const docRef = doc(db, `${COLLECTION}/foo`); + + try { + await runTransaction(db, async t => { + t.update(docRef, 123); + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('it must be an object'); + return Promise.resolve(); + } + }); + + it('should update documents', async function () { + const { runTransaction, doc, setDoc, getDoc } = firestoreModular; + const db = firestore; + const value = Date.now(); + + const docRef1 = doc(db, `${COLLECTION}/transactions/transaction/delete-delete1`); + await setDoc(docRef1, { + foo: 'bar', + bar: 'baz', + }); + + const docRef2 = doc(db, `${COLLECTION}/transactions/transaction/delete-delete2`); + await setDoc(docRef2, { + foo: 'bar', + bar: 'baz', + }); + + await runTransaction(db, async t => { + t.update(docRef1, { + bar: value, + }); + t.update(docRef2, 'bar', value); + }); + + const expected = { + foo: 'bar', + bar: value, + }; + + const snapshot1 = await getDoc(docRef1); + snapshot1.exists.should.eql(true); + snapshot1.data().should.eql(jet.contextify(expected)); + + const snapshot2 = await getDoc(docRef2); + snapshot2.exists.should.eql(true); + snapshot2.data().should.eql(jet.contextify(expected)); + }); + }); + + describe('transaction.set()', function () { + it('should throw if not providing a document reference', async function () { + const { runTransaction } = firestoreModular; + try { + await runTransaction(firestore, async t => { + t.set(123); + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'documentRef' expected a DocumentReference"); + return Promise.resolve(); + } + }); + + it('should throw if set data is invalid', async function () { + const { runTransaction, doc } = firestoreModular; + const db = firestore; + const docRef = doc(db, `${COLLECTION}/foo`); + + try { + await runTransaction(db, async t => { + t.set(docRef, 123); + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'data' must be an object."); + return Promise.resolve(); + } + }); + + it('should throw if set options are invalid', async function () { + const { runTransaction, doc } = firestoreModular; + const db = firestore; + const docRef = doc(db, `${COLLECTION}/foo`); + + try { + await runTransaction(db, async t => { + t.set( + docRef, + {}, + { + merge: true, + mergeFields: [], + }, + ); + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "'options' must not contain both 'merge' & 'mergeFields'", + ); + return Promise.resolve(); + } + }); + + it('should set data', async function () { + const { runTransaction, doc, getDoc, setDoc } = firestoreModular; + const db = firestore; + const docRef = doc(db, `${COLLECTION}/transactions/transaction/set`); + await setDoc(docRef, { + foo: 'bar', + }); + const expected = { + foo: 'baz', + }; + + await runTransaction(db, async t => { + t.set(docRef, expected); + }); + + const snapshot = await getDoc(docRef); + snapshot.data().should.eql(jet.contextify(expected)); + }); + + it('should set data with merge', async function () { + const { runTransaction, doc, getDoc, setDoc } = firestoreModular; + const db = firestore; + const docRef = doc(db, `${COLLECTION}/transactions/transaction/set-merge`); + await setDoc(docRef, { + foo: 'bar', + bar: 'baz', + }); + const expected = { + foo: 'bar', + bar: 'foo', + }; + + await runTransaction(db, async t => { + t.set( + docRef, + { + bar: 'foo', + }, + { + merge: true, + }, + ); + }); + + const snapshot = await getDoc(docRef); + snapshot.data().should.eql(jet.contextify(expected)); + }); + + it('should set data with merge fields', async function () { + const { runTransaction, doc, getDoc, setDoc } = firestoreModular; + const db = firestore; + + const docRef = doc(db, `${COLLECTION}/transactions/transaction/set-mergefields`); + await setDoc(docRef, { + foo: 'bar', + bar: 'baz', + baz: 'ben', + }); + const expected = { + foo: 'bar', + bar: 'foo', + baz: 'foo', + }; + + await runTransaction(db, async t => { + t.set( + docRef, + { + bar: 'foo', + baz: 'foo', + }, + { + mergeFields: ['bar', new firebase.firestore.FieldPath('baz')], + }, + ); + }); + + const snapshot = await getDoc(docRef); + snapshot.data().should.eql(jet.contextify(expected)); + }); + + it('should roll back any updates that failed', async function () { + const { runTransaction, doc, getDoc, setDoc } = firestoreModular; + const db = firestore; + + const docRef = doc(db, `${COLLECTION}/transactions/transaction/rollback`); + + await setDoc(docRef, { + turn: 0, + }); + + const prop1 = 'prop1'; + const prop2 = 'prop2'; + const turn = 0; + const errorMessage = 'turn cannot exceed 1'; + + const createTransaction = prop => { + return runTransaction(db, async transaction => { + const doc = await transaction.get(docRef); + const data = doc.data(); + + if (data.turn !== turn) { + throw new Error(errorMessage); + } + + const update = { + turn: turn + 1, + [prop]: 1, + }; + + transaction.update(docRef, update); + }); + }; + + const promises = [createTransaction(prop1), createTransaction(prop2)]; + + try { + await Promise.all(promises); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql(errorMessage); + } + const result = await getDoc(docRef); + should(result.data()).not.have.properties([prop1, prop2]); + }); + }); + }); + }); +}); diff --git a/packages/firestore/e2e/SecondDatabase/second.onSnapshot.e2e.js b/packages/firestore/e2e/SecondDatabase/second.onSnapshot.e2e.js new file mode 100644 index 0000000000..2fa8e9fb3c --- /dev/null +++ b/packages/firestore/e2e/SecondDatabase/second.onSnapshot.e2e.js @@ -0,0 +1,696 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +const { wipe } = require('../helpers'); + +const NO_RULE_COLLECTION = 'no_rules'; + +// This collection is only allowed on the second database +const COLLECTION = 'second-database'; +const SECOND_DATABASE_ID = 'second-rnfb'; + +describe('Second Database', function () { + describe('firestore().collection().onSnapshot()', function () { + describe('v8 compatibility', function () { + let firestore; + + before(function () { + firestore = firebase.app().firestore(SECOND_DATABASE_ID); + }); + + beforeEach(async function () { + return await wipe(false, SECOND_DATABASE_ID); + }); + + it('throws if no arguments are provided', function () { + try { + firestore.collection(COLLECTION).onSnapshot(); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('expected at least one argument'); + return Promise.resolve(); + } + }); + + it('returns an unsubscribe function', function () { + const unsub = firestore.collection(`${COLLECTION}/foo/bar1`).onSnapshot(() => {}); + + unsub.should.be.a.Function(); + unsub(); + }); + + it('accepts a single callback function with snapshot', async function () { + if (Platform.other) { + return; + } + const callback = sinon.spy(); + const unsub = firestore.collection(`${COLLECTION}/foo/bar2`).onSnapshot(callback); + + await Utils.spyToBeCalledOnceAsync(callback); + + callback.should.be.calledOnce(); + callback.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); + should.equal(callback.args[0][1], null); + unsub(); + }); + + it('accepts a single callback function with Error', async function () { + if (Platform.other) { + return; + } + const callback = sinon.spy(); + const unsub = firestore.collection(NO_RULE_COLLECTION).onSnapshot(callback); + + await Utils.spyToBeCalledOnceAsync(callback); + + callback.should.be.calledOnce(); + + callback.args[0][1].code.should.containEql('firestore/permission-denied'); + should.equal(callback.args[0][0], null); + unsub(); + }); + + describe('multiple callbacks', function () { + if (Platform.other) { + return; + } + + it('calls onNext when successful', async function () { + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = firestore.collection(`${COLLECTION}/foo/bar3`).onSnapshot(onNext, onError); + + await Utils.spyToBeCalledOnceAsync(onNext); + + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + onNext.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); + should.equal(onNext.args[0][1], undefined); + unsub(); + }); + + it('calls onError with Error', async function () { + if (Platform.other) { + return; + } + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = firestore.collection(NO_RULE_COLLECTION).onSnapshot(onNext, onError); + + await Utils.spyToBeCalledOnceAsync(onError); + + onError.should.be.calledOnce(); + onNext.should.be.callCount(0); + onError.args[0][0].code.should.containEql('firestore/permission-denied'); + should.equal(onError.args[0][1], undefined); + unsub(); + }); + }); + + describe('objects of callbacks', function () { + if (Platform.other) { + return; + } + + it('calls next when successful', async function () { + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = firestore.collection(`${COLLECTION}/foo/bar4`).onSnapshot({ + next: onNext, + error: onError, + }); + + await Utils.spyToBeCalledOnceAsync(onNext); + + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + onNext.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); + should.equal(onNext.args[0][1], undefined); + unsub(); + }); + + it('calls error with Error', async function () { + if (Platform.other) { + return; + } + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = firestore.collection(NO_RULE_COLLECTION).onSnapshot({ + next: onNext, + error: onError, + }); + + await Utils.spyToBeCalledOnceAsync(onError); + + onError.should.be.calledOnce(); + onNext.should.be.callCount(0); + onError.args[0][0].code.should.containEql('firestore/permission-denied'); + should.equal(onError.args[0][1], undefined); + unsub(); + }); + }); + + describe('SnapshotListenerOptions + callbacks', function () { + if (Platform.other) { + return; + } + + it('calls callback with snapshot when successful', async function () { + const callback = sinon.spy(); + const unsub = firestore.collection(`${COLLECTION}/foo/bar5`).onSnapshot( + { + includeMetadataChanges: false, + }, + callback, + ); + + await Utils.spyToBeCalledOnceAsync(callback); + + callback.should.be.calledOnce(); + callback.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); + should.equal(callback.args[0][1], null); + unsub(); + }); + + it('calls callback with Error', async function () { + const callback = sinon.spy(); + const unsub = firestore.collection(NO_RULE_COLLECTION).onSnapshot( + { + includeMetadataChanges: false, + }, + callback, + ); + + await Utils.spyToBeCalledOnceAsync(callback); + + callback.should.be.calledOnce(); + callback.args[0][1].code.should.containEql('firestore/permission-denied'); + should.equal(callback.args[0][0], null); + unsub(); + }); + + it('calls next with snapshot when successful', async function () { + const onNext = sinon.spy(); + const onError = sinon.spy(); + const colRef = firestore + // Firestore caches aggressively, even if you wipe the emulator, local documents are cached + // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating + .collection(`${COLLECTION}/${Utils.randString(12, '#aA')}/next-with-snapshot`); + const unsub = colRef.onSnapshot( + { + includeMetadataChanges: false, + }, + onNext, + onError, + ); + + await Utils.spyToBeCalledOnceAsync(onNext); + + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + onNext.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); + should.equal(onNext.args[0][1], undefined); + unsub(); + }); + + it('calls error with Error', async function () { + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = firestore.collection(NO_RULE_COLLECTION).onSnapshot( + { + includeMetadataChanges: false, + }, + onNext, + onError, + ); + + await Utils.spyToBeCalledOnceAsync(onError); + + onError.should.be.calledOnce(); + onNext.should.be.callCount(0); + onError.args[0][0].code.should.containEql('firestore/permission-denied'); + should.equal(onError.args[0][1], undefined); + unsub(); + }); + }); + + describe('SnapshotListenerOptions + object of callbacks', function () { + if (Platform.other) { + return; + } + + it('calls next with snapshot when successful', async function () { + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = firestore.collection(`${COLLECTION}/foo/bar7`).onSnapshot( + { + includeMetadataChanges: false, + }, + { + next: onNext, + error: onError, + }, + ); + + await Utils.spyToBeCalledOnceAsync(onNext); + + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + onNext.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); + should.equal(onNext.args[0][1], undefined); + unsub(); + }); + + it('calls error with Error', async function () { + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = firestore.collection(NO_RULE_COLLECTION).onSnapshot( + { + includeMetadataChanges: false, + }, + { + next: onNext, + error: onError, + }, + ); + + await Utils.spyToBeCalledOnceAsync(onError); + + onError.should.be.calledOnce(); + onNext.should.be.callCount(0); + onError.args[0][0].code.should.containEql('firestore/permission-denied'); + should.equal(onError.args[0][1], undefined); + unsub(); + }); + }); + + it('throws if SnapshotListenerOptions is invalid', function () { + try { + firestore.collection(NO_RULE_COLLECTION).onSnapshot({ + includeMetadataChanges: 123, + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "'options' SnapshotOptions.includeMetadataChanges must be a boolean value", + ); + return Promise.resolve(); + } + }); + + it('throws if next callback is invalid', function () { + try { + firestore.collection(NO_RULE_COLLECTION).onSnapshot({ + next: 'foo', + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'observer.next' or 'onNext' expected a function"); + return Promise.resolve(); + } + }); + + it('throws if error callback is invalid', function () { + try { + firestore.collection(NO_RULE_COLLECTION).onSnapshot({ + error: 'foo', + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'observer.error' or 'onError' expected a function"); + return Promise.resolve(); + } + }); + + // FIXME test disabled due to flakiness in CI E2E tests. + // Registered 4 of 3 expected calls once (!?), 3 of 2 expected calls once. + it('unsubscribes from further updates', async function () { + if (Platform.other) { + return; + } + const callback = sinon.spy(); + + const collection = firestore + // Firestore caches aggressively, even if you wipe the emulator, local documents are cached + // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating + .collection(`${COLLECTION}/${Utils.randString(12, '#aA')}/unsubscribe-updates`); + + const unsub = collection.onSnapshot(callback); + await Utils.sleep(2000); + await collection.add({}); + await collection.add({}); + unsub(); + await Utils.sleep(2000); + await collection.add({}); + await Utils.sleep(2000); + callback.should.be.callCount(3); + }); + }); + + describe('modular', function () { + let firestore; + + before(function () { + const { getFirestore } = firestoreModular; + firestore = getFirestore(null, SECOND_DATABASE_ID); + }); + + beforeEach(async function () { + return await wipe(false, SECOND_DATABASE_ID); + }); + + it('throws if no arguments are provided', function () { + const { collection, onSnapshot } = firestoreModular; + try { + onSnapshot(collection(firestore, COLLECTION)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('expected at least one argument'); + return Promise.resolve(); + } + }); + + it('returns an unsubscribe function', function () { + const { collection, onSnapshot } = firestoreModular; + const unsub = onSnapshot(collection(firestore, `${COLLECTION}/foo/bar1`), () => {}); + + unsub.should.be.a.Function(); + unsub(); + }); + + it('accepts a single callback function with snapshot', async function () { + if (Platform.other) { + return; + } + const { collection, onSnapshot } = firestoreModular; + const callback = sinon.spy(); + const unsub = onSnapshot(collection(firestore, `${COLLECTION}/foo/bar2`), callback); + + await Utils.spyToBeCalledOnceAsync(callback); + + callback.should.be.calledOnce(); + callback.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); + should.equal(callback.args[0][1], null); + unsub(); + }); + + describe('multiple callbacks', function () { + if (Platform.other) { + return; + } + + it('calls onNext when successful', async function () { + const { collection, onSnapshot } = firestoreModular; + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot( + collection(firestore, `${COLLECTION}/foo/bar3`), + onNext, + onError, + ); + + await Utils.spyToBeCalledOnceAsync(onNext); + + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + onNext.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); + should.equal(onNext.args[0][1], undefined); + unsub(); + }); + + it('calls onError with Error', async function () { + const { collection, onSnapshot } = firestoreModular; + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot(collection(firestore, NO_RULE_COLLECTION), onNext, onError); + + await Utils.spyToBeCalledOnceAsync(onError); + + onError.should.be.calledOnce(); + onNext.should.be.callCount(0); + onError.args[0][0].code.should.containEql('firestore/permission-denied'); + should.equal(onError.args[0][1], undefined); + unsub(); + }); + }); + + describe('objects of callbacks', function () { + if (Platform.other) { + return; + } + + it('calls next when successful', async function () { + const { collection, onSnapshot } = firestoreModular; + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot(collection(firestore, `${COLLECTION}/foo/bar4`), { + next: onNext, + error: onError, + }); + + await Utils.spyToBeCalledOnceAsync(onNext); + + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + onNext.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); + should.equal(onNext.args[0][1], undefined); + unsub(); + }); + + it('calls error with Error', async function () { + const { collection, onSnapshot } = firestoreModular; + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot(collection(firestore, NO_RULE_COLLECTION), { + next: onNext, + error: onError, + }); + + await Utils.spyToBeCalledOnceAsync(onError); + + onError.should.be.calledOnce(); + onNext.should.be.callCount(0); + onError.args[0][0].code.should.containEql('firestore/permission-denied'); + should.equal(onError.args[0][1], undefined); + unsub(); + }); + }); + + describe('SnapshotListenerOptions + callbacks', function () { + if (Platform.other) { + return; + } + + it('calls callback with snapshot when successful', async function () { + const { collection, onSnapshot } = firestoreModular; + const callback = sinon.spy(); + const unsub = onSnapshot( + collection(firestore, `${COLLECTION}/foo/bar5`), + { + includeMetadataChanges: false, + }, + callback, + ); + + await Utils.spyToBeCalledOnceAsync(callback); + + callback.should.be.calledOnce(); + callback.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); + should.equal(callback.args[0][1], null); + unsub(); + }); + + it('calls next with snapshot when successful', async function () { + if (Platform.other) { + return; + } + const { collection, onSnapshot } = firestoreModular; + const onNext = sinon.spy(); + const onError = sinon.spy(); + const colRef = collection( + firestore, + // Firestore caches aggressively, even if you wipe the emulator, local documents are cached + // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating + `${COLLECTION}/${Utils.randString(12, '#aA')}/next-with-snapshot`, + ); + const unsub = onSnapshot( + colRef, + { + includeMetadataChanges: false, + }, + onNext, + onError, + ); + + await Utils.spyToBeCalledOnceAsync(onNext); + + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + onNext.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); + should.equal(onNext.args[0][1], undefined); + unsub(); + }); + + it('calls error with Error', async function () { + if (Platform.other) { + return; + } + const { collection, onSnapshot } = firestoreModular; + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot( + collection(firestore, NO_RULE_COLLECTION), + { + includeMetadataChanges: false, + }, + onNext, + onError, + ); + + await Utils.spyToBeCalledOnceAsync(onError); + + onError.should.be.calledOnce(); + onNext.should.be.callCount(0); + onError.args[0][0].code.should.containEql('firestore/permission-denied'); + should.equal(onError.args[0][1], undefined); + unsub(); + }); + }); + + describe('SnapshotListenerOptions + object of callbacks', function () { + if (Platform.other) { + return; + } + + it('calls next with snapshot when successful', async function () { + const { collection, onSnapshot } = firestoreModular; + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot( + collection(firestore, `${COLLECTION}/foo/bar7`), + { + includeMetadataChanges: false, + }, + { + next: onNext, + error: onError, + }, + ); + + await Utils.spyToBeCalledOnceAsync(onNext); + + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + onNext.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); + should.equal(onNext.args[0][1], undefined); + unsub(); + }); + + it('calls error with Error', async function () { + const { collection, onSnapshot } = firestoreModular; + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot( + collection(firestore, NO_RULE_COLLECTION), + { + includeMetadataChanges: false, + }, + { + next: onNext, + error: onError, + }, + ); + + await Utils.spyToBeCalledOnceAsync(onError); + + onError.should.be.calledOnce(); + onNext.should.be.callCount(0); + onError.args[0][0].code.should.containEql('firestore/permission-denied'); + should.equal(onError.args[0][1], undefined); + unsub(); + }); + }); + + it('throws if SnapshotListenerOptions is invalid', function () { + const { collection, onSnapshot } = firestoreModular; + try { + onSnapshot(collection(firestore, NO_RULE_COLLECTION), { + includeMetadataChanges: 123, + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "'options' SnapshotOptions.includeMetadataChanges must be a boolean value", + ); + return Promise.resolve(); + } + }); + + it('throws if next callback is invalid', function () { + const { collection, onSnapshot } = firestoreModular; + try { + onSnapshot(collection(firestore, NO_RULE_COLLECTION), { + next: 'foo', + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'observer.next' or 'onNext' expected a function"); + return Promise.resolve(); + } + }); + + it('throws if error callback is invalid', function () { + const { collection, onSnapshot } = firestoreModular; + try { + onSnapshot(collection(firestore, NO_RULE_COLLECTION), { + error: 'foo', + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'observer.error' or 'onError' expected a function"); + return Promise.resolve(); + } + }); + + // FIXME test disabled due to flakiness in CI E2E tests. + // Registered 4 of 3 expected calls once (!?), 3 of 2 expected calls once. + it('unsubscribes from further updates', async function () { + if (Platform.other) { + return; + } + const { collection, onSnapshot, addDoc } = firestoreModular; + const callback = sinon.spy(); + + const collectionRef = collection( + firestore, + // Firestore caches aggressively, even if you wipe the emulator, local documents are cached + // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating + `${COLLECTION}/${Utils.randString(12, '#aA')}/unsubscribe-updates`, + ); + + const unsub = onSnapshot(collectionRef, callback); + await Utils.sleep(2000); + await addDoc(collectionRef, {}); + await addDoc(collectionRef, {}); + unsub(); + await Utils.sleep(2000); + await addDoc(collectionRef, {}); + await Utils.sleep(2000); + callback.should.be.callCount(3); + }); + }); + }); +}); diff --git a/packages/firestore/e2e/SecondDatabase/second.where.e2e.js b/packages/firestore/e2e/SecondDatabase/second.where.e2e.js new file mode 100644 index 0000000000..0000371d42 --- /dev/null +++ b/packages/firestore/e2e/SecondDatabase/second.where.e2e.js @@ -0,0 +1,1095 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +const { wipe } = require('../helpers'); + +// This collection is only allowed on the second database +const COLLECTION = 'second-database'; +const SECOND_DATABASE_ID = 'second-rnfb'; + +describe('Second Database', function () { + describe('firestore().collection().where()', function () { + describe('v8 compatibility', function () { + let firestore; + + before(function () { + firestore = firebase.app().firestore(SECOND_DATABASE_ID); + }); + + beforeEach(async function () { + return await wipe(false, SECOND_DATABASE_ID); + }); + + it('throws if fieldPath is invalid', function () { + try { + firestore.collection(COLLECTION).where(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'must be a string, instance of FieldPath or instance of Filter', + ); + return Promise.resolve(); + } + }); + + it('throws if fieldPath string is invalid', function () { + try { + firestore.collection(COLLECTION).where('.foo.bar'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'fieldPath' Invalid field path"); + return Promise.resolve(); + } + }); + + it('throws if operator string is invalid', function () { + try { + firestore.collection(COLLECTION).where('foo.bar', '!'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'opStr' is invalid"); + return Promise.resolve(); + } + }); + + it('throws if query contains multiple array-contains', function () { + try { + firestore + .collection(COLLECTION) + .where('foo.bar', 'array-contains', 123) + .where('foo.bar', 'array-contains', 123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Queries only support a single array-contains filter'); + return Promise.resolve(); + } + }); + + it('throws if value is not defined', function () { + try { + firestore.collection(COLLECTION).where('foo.bar', 'array-contains'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'value' argument expected"); + return Promise.resolve(); + } + }); + + it('throws if null value and no equal operator', function () { + try { + firestore.collection(COLLECTION).where('foo.bar', 'array-contains', null); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('You can only perform equals comparisons on null'); + return Promise.resolve(); + } + }); + + it('allows null to be used with equal operator', function () { + firestore.collection(COLLECTION).where('foo.bar', '==', null); + }); + + it('allows null to be used with not equal operator', function () { + firestore.collection(COLLECTION).where('foo.bar', '!=', null); + }); + + it('allows inequality on the same path', function () { + firestore + .collection(COLLECTION) + .where('foo.bar', '>', 123) + .where(new firebase.firestore.FieldPath('foo', 'bar'), '>', 1234); + }); + + it('throws if in query with no array value', function () { + try { + firestore.collection(COLLECTION).where('foo.bar', 'in', '123'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('A non-empty array is required'); + return Promise.resolve(); + } + }); + + it('throws if array-contains-any query with no array value', function () { + try { + firestore.collection(COLLECTION).where('foo.bar', 'array-contains-any', '123'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('A non-empty array is required'); + return Promise.resolve(); + } + }); + + it('throws if in query array length is greater than 30', function () { + try { + const queryArray = Array.from({ length: 31 }, (_, i) => i + 1); + + firestore.collection(COLLECTION).where('foo.bar', 'in', queryArray); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('maximum of 30 elements in the value'); + return Promise.resolve(); + } + }); + + it('throws if query has multiple array-contains-any filter', function () { + try { + firestore + .collection(COLLECTION) + .where('foo.bar', 'array-contains-any', [1]) + .where('foo.bar', 'array-contains-any', [2]); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "You cannot use more than one 'array-contains-any' filter", + ); + return Promise.resolve(); + } + }); + + /* Queries */ + + it('returns with where equal filter', async function () { + const colRef = firestore.collection(`${COLLECTION}/filter/equal`); + + const search = Date.now(); + await Promise.all([ + colRef.add({ foo: search }), + colRef.add({ foo: search }), + colRef.add({ foo: search + 1234 }), + ]); + + const snapshot = await colRef.where('foo', '==', search).get(); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(search); + }); + }); + + it('returns with where greater than filter', async function () { + const colRef = firestore.collection(`${COLLECTION}/filter/greater`); + + const search = Date.now(); + await Promise.all([ + colRef.add({ foo: search - 1234 }), + colRef.add({ foo: search }), + colRef.add({ foo: search + 1234 }), + colRef.add({ foo: search + 1234 }), + ]); + + const snapshot = await colRef.where('foo', '>', search).get(); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(search + 1234); + }); + }); + + it('returns with where greater than or equal filter', async function () { + const colRef = firestore.collection(`${COLLECTION}/filter/greaterequal`); + + const search = Date.now(); + await Promise.all([ + colRef.add({ foo: search - 1234 }), + colRef.add({ foo: search }), + colRef.add({ foo: search + 1234 }), + colRef.add({ foo: search + 1234 }), + ]); + + const snapshot = await colRef.where('foo', '>=', search).get(); + + snapshot.size.should.eql(3); + snapshot.forEach(s => { + s.data().foo.should.be.aboveOrEqual(search); + }); + }); + + it('returns with where less than filter', async function () { + const colRef = firestore.collection(`${COLLECTION}/filter/less`); + + const search = -Date.now(); + await Promise.all([ + colRef.add({ foo: search + -1234 }), + colRef.add({ foo: search + -1234 }), + colRef.add({ foo: search }), + ]); + + const snapshot = await colRef.where('foo', '<', search).get(); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.be.below(search); + }); + }); + + it('returns with where less than or equal filter', async function () { + const colRef = firestore.collection(`${COLLECTION}/filter/lessequal`); + + const search = -Date.now(); + await Promise.all([ + colRef.add({ foo: search + -1234 }), + colRef.add({ foo: search + -1234 }), + colRef.add({ foo: search }), + colRef.add({ foo: search + 1234 }), + ]); + + const snapshot = await colRef.where('foo', '<=', search).get(); + + snapshot.size.should.eql(3); + snapshot.forEach(s => { + s.data().foo.should.be.belowOrEqual(search); + }); + }); + + it('returns when combining greater than and lesser than on the same nested field', async function () { + const colRef = firestore.collection(`${COLLECTION}/filter/greaterandless`); + + await Promise.all([ + colRef.doc('doc1').set({ foo: { bar: 1 } }), + colRef.doc('doc2').set({ foo: { bar: 2 } }), + colRef.doc('doc3').set({ foo: { bar: 3 } }), + ]); + + const snapshot = await colRef + .where('foo.bar', '>', 1) + .where('foo.bar', '<', 3) + .orderBy('foo.bar') + .get(); + + snapshot.size.should.eql(1); + }); + + it('returns when combining greater than and lesser than on the same nested field using FieldPath', async function () { + const colRef = firestore.collection(`${COLLECTION}/filter/greaterandless`); + + await Promise.all([ + colRef.doc('doc1').set({ foo: { bar: 1 } }), + colRef.doc('doc2').set({ foo: { bar: 2 } }), + colRef.doc('doc3').set({ foo: { bar: 3 } }), + ]); + + const snapshot = await colRef + .where(new firebase.firestore.FieldPath('foo', 'bar'), '>', 1) + .where(new firebase.firestore.FieldPath('foo', 'bar'), '<', 3) + .orderBy(new firebase.firestore.FieldPath('foo', 'bar')) + .get(); + + snapshot.size.should.eql(1); + }); + + it('returns with where array-contains filter', async function () { + const colRef = firestore.collection(`${COLLECTION}/filter/array-contains`); + + const match = Date.now(); + await Promise.all([ + colRef.add({ foo: [1, '2', match] }), + colRef.add({ foo: [1, '2', match.toString()] }), + colRef.add({ foo: [1, '2', match.toString()] }), + ]); + + const snapshot = await colRef.where('foo', 'array-contains', match.toString()).get(); + const expected = [1, '2', match.toString()]; + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(jet.contextify(expected)); + }); + }); + + it('returns with in filter', async function () { + const colRef = firestore.collection(`${COLLECTION}/filter/in${Date.now() + ''}`); + + await Promise.all([ + colRef.add({ status: 'Ordered' }), + colRef.add({ status: 'Ready to Ship' }), + colRef.add({ status: 'Ready to Ship' }), + colRef.add({ status: 'Incomplete' }), + ]); + + const expect = ['Ready to Ship', 'Ordered']; + const snapshot = await colRef.where('status', 'in', expect).get(); + snapshot.size.should.eql(3); + + snapshot.forEach(s => { + s.data().status.should.equalOneOf(...expect); + }); + }); + + it('returns with array-contains-any filter', async function () { + const colRef = firestore.collection( + `${COLLECTION}/filter/array-contains-any${Date.now() + ''}`, + ); + + await Promise.all([ + colRef.add({ category: ['Appliances', 'Housewares', 'Cooking'] }), + colRef.add({ category: ['Appliances', 'Electronics', 'Nursery'] }), + colRef.add({ category: ['Audio/Video', 'Electronics'] }), + colRef.add({ category: ['Beauty'] }), + ]); + + const expect = ['Appliances', 'Electronics']; + const snapshot = await colRef.where('category', 'array-contains-any', expect).get(); + snapshot.size.should.eql(3); // 2nd record should only be returned once + }); + + it('returns with a FieldPath', async function () { + const colRef = firestore.collection( + `${COLLECTION}/filter/where-fieldpath${Date.now() + ''}`, + ); + const fieldPath = new firebase.firestore.FieldPath('map', 'foo.bar@gmail.com'); + + await colRef.add({ + map: { + 'foo.bar@gmail.com': true, + }, + }); + await colRef.add({ + map: { + 'bar.foo@gmail.com': true, + }, + }); + + const snapshot = await colRef.where(fieldPath, '==', true).get(); + snapshot.size.should.eql(1); // 2nd record should only be returned once + const data = snapshot.docs[0].data(); + should.equal(data.map['foo.bar@gmail.com'], true); + }); + + it('should throw an error if you use a FieldPath on a filter in conjunction with an orderBy() parameter that is not FieldPath', async function () { + try { + firestore + .collection(COLLECTION) + .where(firebase.firestore.FieldPath.documentId(), 'in', ['document-id']) + .orderBy('differentOrderBy', 'desc'); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'FirestoreFieldPath' cannot be used in conjunction"); + return Promise.resolve(); + } + }); + + it('should correctly query integer values with in operator', async function () { + const ref = firestore.collection(`${COLLECTION}/filter/int-in${Date.now() + ''}`); + + await ref.add({ status: 1 }); + + const items = []; + await ref + .where('status', 'in', [1, 2]) + .get() + .then($ => $.forEach(doc => items.push(doc.data()))); + + items.length.should.equal(1); + }); + + it('should correctly query integer values with array-contains operator', async function () { + const ref = firestore.collection( + `${COLLECTION}/filter/int-array-contains${Date.now() + ''}`, + ); + + await ref.add({ status: [1, 2, 3] }); + + const items = []; + await ref + .where('status', 'array-contains', 2) + .get() + .then($ => $.forEach(doc => items.push(doc.data()))); + + items.length.should.equal(1); + }); + + it("should correctly retrieve data when using 'not-in' operator", async function () { + const ref = firestore.collection(`${COLLECTION}/filter/not-in${Date.now() + ''}`); + + await Promise.all([ref.add({ notIn: 'here' }), ref.add({ notIn: 'now' })]); + + const result = await ref.where('notIn', 'not-in', ['here', 'there', 'everywhere']).get(); + should(result.docs.length).equal(1); + should(result.docs[0].data().notIn).equal('now'); + }); + + it("should throw error when using 'not-in' operator twice", async function () { + const ref = firestore.collection(COLLECTION); + + try { + ref.where('test', 'not-in', [1]).where('test', 'not-in', [2]); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one 'not-in' filter."); + return Promise.resolve(); + } + }); + + it("should throw error when combining 'not-in' operator with '!=' operator", async function () { + const ref = firestore.collection(COLLECTION); + + try { + ref.where('test', '!=', 1).where('test', 'not-in', [1]); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "You cannot use 'not-in' filters with '!=' inequality filters", + ); + return Promise.resolve(); + } + }); + + it("should throw error when combining 'not-in' operator with 'in' operator", async function () { + const ref = firestore.collection(COLLECTION); + + try { + ref.where('test', 'in', [2]).where('test', 'not-in', [1]); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use 'not-in' filters with 'in' filters."); + return Promise.resolve(); + } + }); + + it("should throw error when combining 'not-in' operator with 'array-contains-any' operator", async function () { + const ref = firestore.collection(COLLECTION); + + try { + ref.where('test', 'array-contains-any', [2]).where('test', 'not-in', [1]); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "You cannot use 'not-in' filters with 'array-contains-any' filters.", + ); + return Promise.resolve(); + } + }); + + it("should throw error when 'not-in' filter has a list of more than 10 items", async function () { + const ref = firestore.collection(COLLECTION); + const queryArray = Array.from({ length: 31 }, (_, i) => i + 1); + + try { + ref.where('test', 'not-in', queryArray); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'filters support a maximum of 30 elements in the value array.', + ); + return Promise.resolve(); + } + }); + + it("should correctly retrieve data when using '!=' operator", async function () { + const ref = firestore.collection(`${COLLECTION}/filter/bang-equals${Date.now() + ''}`); + + await Promise.all([ref.add({ notEqual: 'here' }), ref.add({ notEqual: 'now' })]); + + const result = await ref.where('notEqual', '!=', 'here').get(); + + should(result.docs.length).equal(1); + should(result.docs[0].data().notEqual).equal('now'); + }); + + it("should throw error when using '!=' operator twice ", async function () { + const ref = firestore.collection(COLLECTION); + + try { + ref.where('test', '!=', 1).where('test', '!=', 2); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one '!=' inequality filter."); + return Promise.resolve(); + } + }); + + it('should handle where clause after sort by', async function () { + const ref = firestore.collection(`${COLLECTION}/filter/sort-by-where${Date.now() + ''}`); + + await ref.add({ status: 1 }); + await ref.add({ status: 2 }); + await ref.add({ status: 3 }); + + const items = []; + await ref + .orderBy('status', 'desc') + .where('status', '<=', 2) + .get() + .then($ => $.forEach(doc => items.push(doc.data()))); + + items.length.should.equal(2); + items[0].status.should.equal(2); + items[1].status.should.equal(1); + }); + }); + + describe('modular', function () { + let firestore; + + before(function () { + const { getFirestore } = firestoreModular; + firestore = getFirestore(null, SECOND_DATABASE_ID); + }); + + beforeEach(async function () { + return await wipe(false, SECOND_DATABASE_ID); + }); + + it('throws if fieldPath is invalid', function () { + const { collection, query, where } = firestoreModular; + try { + query(collection(firestore, COLLECTION), where(123)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'must be a string, instance of FieldPath or instance of Filter', + ); + return Promise.resolve(); + } + }); + + it('throws if fieldPath string is invalid', function () { + const { collection, query, where } = firestoreModular; + try { + query(collection(firestore, COLLECTION), where('.foo.bar')); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'fieldPath' Invalid field path"); + return Promise.resolve(); + } + }); + + it('throws if operator string is invalid', function () { + const { collection, query, where } = firestoreModular; + try { + query(collection(firestore, COLLECTION), where('foo.bar', '!')); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'opStr' is invalid"); + return Promise.resolve(); + } + }); + + it('throws if query contains multiple array-contains', function () { + const { collection, query, where } = firestoreModular; + try { + query( + collection(firestore, COLLECTION), + where('foo.bar', 'array-contains', 123), + where('foo.bar', 'array-contains', 123), + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Queries only support a single array-contains filter'); + return Promise.resolve(); + } + }); + + it('throws if value is not defined', function () { + const { collection, query, where } = firestoreModular; + try { + query(collection(firestore, COLLECTION), where('foo.bar', 'array-contains')); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'value' argument expected"); + return Promise.resolve(); + } + }); + + it('throws if null value and no equal operator', function () { + const { collection, query, where } = firestoreModular; + try { + query(collection(firestore, COLLECTION), where('foo.bar', 'array-contains', null)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('You can only perform equals comparisons on null'); + return Promise.resolve(); + } + }); + + it('allows null to be used with equal operator', function () { + const { collection, query, where } = firestoreModular; + query(collection(firestore, COLLECTION), where('foo.bar', '==', null)); + }); + + it('allows null to be used with not equal operator', function () { + const { collection, query, where } = firestoreModular; + query(collection(firestore, COLLECTION), where('foo.bar', '!=', null)); + }); + + it('allows inequality on the same path', function () { + const { collection, query, where, FieldPath } = firestoreModular; + query( + collection(firestore, COLLECTION), + where('foo.bar', '>', 123), + where(new FieldPath('foo', 'bar'), '>', 1234), + ); + }); + + it('throws if in query with no array value', function () { + const { collection, query, where } = firestoreModular; + try { + query(collection(firestore, COLLECTION), where('foo.bar', 'in', '123')); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('A non-empty array is required'); + return Promise.resolve(); + } + }); + + it('throws if array-contains-any query with no array value', function () { + const { collection, query, where } = firestoreModular; + try { + query(collection(firestore, COLLECTION), where('foo.bar', 'array-contains-any', '123')); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('A non-empty array is required'); + return Promise.resolve(); + } + }); + + it('throws if in query array length is greater than 30', function () { + const { collection, query, where } = firestoreModular; + const queryArray = Array.from({ length: 31 }, (_, i) => i + 1); + + try { + query(collection(firestore, COLLECTION), where('foo.bar', 'in', queryArray)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('maximum of 30 elements in the value'); + return Promise.resolve(); + } + }); + + it('throws if query has multiple array-contains-any filter', function () { + const { collection, query, where } = firestoreModular; + try { + query( + collection(firestore, COLLECTION), + where('foo.bar', 'array-contains-any', [1]), + where('foo.bar', 'array-contains-any', [2]), + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "You cannot use more than one 'array-contains-any' filter", + ); + return Promise.resolve(); + } + }); + + /* Queries */ + + it('returns with where equal filter', async function () { + const { collection, addDoc, query, where, getDocs } = firestoreModular; + const colRef = collection(firestore, `${COLLECTION}/filter/equal`); + + const search = Date.now(); + await Promise.all([ + addDoc(colRef, { foo: search }), + addDoc(colRef, { foo: search }), + addDoc(colRef, { foo: search + 1234 }), + ]); + + const snapshot = await getDocs(query(colRef, where('foo', '==', search))); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(search); + }); + }); + + it('returns with where greater than filter', async function () { + const { collection, addDoc, query, where, getDocs } = firestoreModular; + const colRef = collection(firestore, `${COLLECTION}/filter/greater`); + + const search = Date.now(); + await Promise.all([ + addDoc(colRef, { foo: search - 1234 }), + addDoc(colRef, { foo: search }), + addDoc(colRef, { foo: search + 1234 }), + addDoc(colRef, { foo: search + 1234 }), + ]); + + const snapshot = await getDocs(query(colRef, where('foo', '>', search))); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(search + 1234); + }); + }); + + it('returns with where greater than or equal filter', async function () { + const { collection, addDoc, query, where, getDocs } = firestoreModular; + const colRef = collection(firestore, `${COLLECTION}/filter/greaterequal`); + + const search = Date.now(); + await Promise.all([ + addDoc(colRef, { foo: search - 1234 }), + addDoc(colRef, { foo: search }), + addDoc(colRef, { foo: search + 1234 }), + addDoc(colRef, { foo: search + 1234 }), + ]); + + const snapshot = await getDocs(query(colRef, where('foo', '>=', search))); + + snapshot.size.should.eql(3); + snapshot.forEach(s => { + s.data().foo.should.be.aboveOrEqual(search); + }); + }); + + it('returns with where less than filter', async function () { + const { collection, addDoc, query, where, getDocs } = firestoreModular; + const colRef = collection(firestore, `${COLLECTION}/filter/less`); + + const search = -Date.now(); + await Promise.all([ + addDoc(colRef, { foo: search + -1234 }), + addDoc(colRef, { foo: search + -1234 }), + addDoc(colRef, { foo: search }), + ]); + + const snapshot = await getDocs(query(colRef, where('foo', '<', search))); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.be.below(search); + }); + }); + + it('returns with where less than or equal filter', async function () { + const { collection, addDoc, query, where, getDocs } = firestoreModular; + const colRef = collection(firestore, `${COLLECTION}/filter/lessequal`); + + const search = -Date.now(); + await Promise.all([ + addDoc(colRef, { foo: search + -1234 }), + addDoc(colRef, { foo: search + -1234 }), + addDoc(colRef, { foo: search }), + addDoc(colRef, { foo: search + 1234 }), + ]); + + const snapshot = await getDocs(query(colRef, where('foo', '<=', search))); + + snapshot.size.should.eql(3); + snapshot.forEach(s => { + s.data().foo.should.be.belowOrEqual(search); + }); + }); + + it('returns when combining greater than and lesser than on the same nested field', async function () { + const { collection, doc, setDoc, query, where, orderBy, getDocs } = firestoreModular; + const colRef = collection(firestore, `${COLLECTION}/filter/greaterandless`); + + await Promise.all([ + setDoc(doc(colRef, 'doc1'), { foo: { bar: 1 } }), + setDoc(doc(colRef, 'doc2'), { foo: { bar: 2 } }), + setDoc(doc(colRef, 'doc3'), { foo: { bar: 3 } }), + ]); + + const snapshot = await getDocs( + query(colRef, where('foo.bar', '>', 1), where('foo.bar', '<', 3), orderBy('foo.bar')), + ); + + snapshot.size.should.eql(1); + }); + + it('returns when combining greater than and lesser than on the same nested field using FieldPath', async function () { + const { collection, doc, setDoc, query, where, getDocs, orderBy, FieldPath } = + firestoreModular; + const colRef = collection(firestore, `${COLLECTION}/filter/greaterandless`); + + await Promise.all([ + setDoc(doc(colRef, 'doc1'), { foo: { bar: 1 } }), + setDoc(doc(colRef, 'doc2'), { foo: { bar: 2 } }), + setDoc(doc(colRef, 'doc3'), { foo: { bar: 3 } }), + ]); + + const snapshot = await getDocs( + query( + colRef, + where(new FieldPath('foo', 'bar'), '>', 1), + where(new FieldPath('foo', 'bar'), '<', 3), + orderBy(new FieldPath('foo', 'bar')), + ), + ); + + snapshot.size.should.eql(1); + }); + + it('returns with where array-contains filter', async function () { + const { collection, addDoc, query, where, getDocs } = firestoreModular; + const colRef = collection(firestore, `${COLLECTION}/filter/array-contains`); + + const match = Date.now(); + await Promise.all([ + addDoc(colRef, { foo: [1, '2', match] }), + addDoc(colRef, { foo: [1, '2', match.toString()] }), + addDoc(colRef, { foo: [1, '2', match.toString()] }), + ]); + + const snapshot = await getDocs( + query(colRef, where('foo', 'array-contains', match.toString())), + ); + const expected = [1, '2', match.toString()]; + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(jet.contextify(expected)); + }); + }); + + it('returns with in filter', async function () { + const { collection, addDoc, query, where, getDocs } = firestoreModular; + const colRef = collection(firestore, `${COLLECTION}/filter/in${Date.now() + ''}`); + + await Promise.all([ + addDoc(colRef, { status: 'Ordered' }), + addDoc(colRef, { status: 'Ready to Ship' }), + addDoc(colRef, { status: 'Ready to Ship' }), + addDoc(colRef, { status: 'Incomplete' }), + ]); + + const expect = ['Ready to Ship', 'Ordered']; + const snapshot = await getDocs(query(colRef, where('status', 'in', expect))); + snapshot.size.should.eql(3); + + snapshot.forEach(s => { + s.data().status.should.equalOneOf(...expect); + }); + }); + + it('returns with array-contains-any filter', async function () { + const { collection, addDoc, query, where, getDocs } = firestoreModular; + const colRef = collection( + firestore, + `${COLLECTION}/filter/array-contains-any${Date.now() + ''}`, + ); + + await Promise.all([ + addDoc(colRef, { category: ['Appliances', 'Housewares', 'Cooking'] }), + addDoc(colRef, { category: ['Appliances', 'Electronics', 'Nursery'] }), + addDoc(colRef, { category: ['Audio/Video', 'Electronics'] }), + addDoc(colRef, { category: ['Beauty'] }), + ]); + + const expect = ['Appliances', 'Electronics']; + const snapshot = await getDocs( + query(colRef, where('category', 'array-contains-any', expect)), + ); + snapshot.size.should.eql(3); // 2nd record should only be returned once + }); + + it('returns with a FieldPath', async function () { + const { collection, addDoc, query, where, getDocs, FieldPath } = firestoreModular; + const colRef = collection( + firestore, + `${COLLECTION}/filter/where-fieldpath${Date.now() + ''}`, + ); + const fieldPath = new FieldPath('map', 'foo.bar@gmail.com'); + + await addDoc(colRef, { + map: { + 'foo.bar@gmail.com': true, + }, + }); + await addDoc(colRef, { + map: { + 'bar.foo@gmail.com': true, + }, + }); + + const snapshot = await getDocs(query(colRef, where(fieldPath, '==', true))); + snapshot.size.should.eql(1); // 2nd record should only be returned once + const data = snapshot.docs[0].data(); + should.equal(data.map['foo.bar@gmail.com'], true); + }); + + it('should throw an error if you use a FieldPath on a filter in conjunction with an orderBy() parameter that is not FieldPath', async function () { + const { collection, query, where, orderBy, FieldPath } = firestoreModular; + try { + query( + collection(firestore, COLLECTION), + where(FieldPath.documentId(), 'in', ['document-id']), + orderBy('differentOrderBy', 'desc'), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'FirestoreFieldPath' cannot be used in conjunction"); + return Promise.resolve(); + } + }); + + it('should correctly query integer values with in operator', async function () { + const { collection, addDoc, query, where, getDocs } = firestoreModular; + const ref = collection(firestore, `${COLLECTION}/filter/int-in${Date.now() + ''}`); + + await addDoc(ref, { status: 1 }); + + const items = []; + await getDocs(query(ref, where('status', 'in', [1, 2]))).then($ => + $.forEach(doc => items.push(doc.data())), + ); + + items.length.should.equal(1); + }); + + it('should correctly query integer values with array-contains operator', async function () { + const { collection, addDoc, query, where, getDocs } = firestoreModular; + const ref = collection( + firestore, + `${COLLECTION}/filter/int-array-contains${Date.now() + ''}`, + ); + + await addDoc(ref, { status: [1, 2, 3] }); + + const items = []; + await getDocs(query(ref, where('status', 'array-contains', 2))).then($ => + $.forEach(doc => items.push(doc.data())), + ); + + items.length.should.equal(1); + }); + + it("should correctly retrieve data when using 'not-in' operator", async function () { + const { collection, addDoc, query, where, getDocs } = firestoreModular; + const ref = collection(firestore, `${COLLECTION}/filter/not-in${Date.now() + ''}`); + + await Promise.all([addDoc(ref, { notIn: 'here' }), addDoc(ref, { notIn: 'now' })]); + + const result = await getDocs( + query(ref, where('notIn', 'not-in', ['here', 'there', 'everywhere'])), + ); + should(result.docs.length).equal(1); + should(result.docs[0].data().notIn).equal('now'); + }); + + it("should throw error when using 'not-in' operator twice", async function () { + const { collection, query, where } = firestoreModular; + const ref = collection(firestore, COLLECTION); + + try { + query(ref, where('test', 'not-in', [1]), where('test', 'not-in', [2])); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one 'not-in' filter."); + return Promise.resolve(); + } + }); + + it("should throw error when combining 'not-in' operator with '!=' operator", async function () { + const { collection, query, where } = firestoreModular; + const ref = collection(firestore, COLLECTION); + + try { + query(ref, where('test', 'not-in', [1]), where('test', '!=', 1)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "You cannot use 'not-in' filters with '!=' inequality filters", + ); + return Promise.resolve(); + } + }); + + it("should throw error when combining 'not-in' operator with 'in' operator", async function () { + const { collection, query, where } = firestoreModular; + const ref = collection(firestore, COLLECTION); + + try { + query(ref, where('test', 'in', [2]), where('test', 'not-in', [1])); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use 'not-in' filters with 'in' filters."); + return Promise.resolve(); + } + }); + + it("should throw error when combining 'not-in' operator with 'array-contains-any' operator", async function () { + const { collection, query, where } = firestoreModular; + const ref = collection(firestore, COLLECTION); + + try { + query(ref, where('test', 'array-contains-any', [2]), where('test', 'not-in', [1])); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "You cannot use 'not-in' filters with 'array-contains-any' filters.", + ); + return Promise.resolve(); + } + }); + + it("should throw error when 'not-in' filter has a list of more than 10 items", async function () { + const { collection, query, where } = firestoreModular; + const ref = collection(firestore, COLLECTION); + const queryArray = Array.from({ length: 31 }, (_, i) => i + 1); + + try { + query(ref, where('test', 'not-in', queryArray)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'filters support a maximum of 30 elements in the value array.', + ); + return Promise.resolve(); + } + }); + + it("should correctly retrieve data when using '!=' operator", async function () { + const { collection, addDoc, query, where, getDocs } = firestoreModular; + const ref = collection(firestore, `${COLLECTION}/filter/bang-equals${Date.now() + ''}`); + + await Promise.all([addDoc(ref, { notEqual: 'here' }), addDoc(ref, { notEqual: 'now' })]); + + const result = await getDocs(query(ref, where('notEqual', '!=', 'here'))); + + should(result.docs.length).equal(1); + should(result.docs[0].data().notEqual).equal('now'); + }); + + it("should throw error when using '!=' operator twice ", async function () { + const { collection, query, where } = firestoreModular; + const ref = collection(firestore, COLLECTION); + + try { + query(ref, where('test', '!=', 1), where('test', '!=', 2)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one '!=' inequality filter."); + return Promise.resolve(); + } + }); + + it('should handle where clause after sort by', async function () { + const { collection, addDoc, query, where, orderBy, getDocs } = firestoreModular; + const ref = collection(firestore, `${COLLECTION}/filter/sort-by-where${Date.now() + ''}`); + + await addDoc(ref, { status: 1 }); + await addDoc(ref, { status: 2 }); + await addDoc(ref, { status: 3 }); + + const items = []; + await getDocs(query(ref, orderBy('status', 'desc'), where('status', '<=', 2))).then($ => + $.forEach(doc => items.push(doc.data())), + ); + + items.length.should.equal(2); + items[0].status.should.equal(2); + items[1].status.should.equal(1); + }); + }); + }); +}); diff --git a/packages/firestore/e2e/helpers.js b/packages/firestore/e2e/helpers.js index 3291e6dc63..99cab707d3 100644 --- a/packages/firestore/e2e/helpers.js +++ b/packages/firestore/e2e/helpers.js @@ -18,7 +18,7 @@ const { getE2eTestProject, getE2eEmulatorHost } = require('../../app/e2e/helpers * */ -exports.wipe = async function wipe(debug = false) { +exports.wipe = async function wipe(debug = false, databaseId = '(default)') { const deleteOptions = { method: 'DELETE', headers: { @@ -27,7 +27,7 @@ exports.wipe = async function wipe(debug = false) { }, port: 8080, host: getE2eEmulatorHost(), - path: '/emulator/v1/projects/' + getE2eTestProject() + '/databases/(default)/documents', + path: '/emulator/v1/projects/' + getE2eTestProject() + `/databases/${databaseId}/documents`, }; try { diff --git a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCollectionModule.m b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCollectionModule.m index 9700238c1e..963f6fec11 100644 --- a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCollectionModule.m +++ b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCollectionModule.m @@ -63,6 +63,7 @@ - (void)invalidate { RCT_EXPORT_METHOD(namedQueryOnSnapshot : (FIRApp *)firebaseApp + : (NSString *)databaseId : (NSString *)name : (NSString *)type : (NSArray *)filters @@ -74,31 +75,35 @@ - (void)invalidate { return; } - FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp]; - [[FIRFirestore firestore] getQueryNamed:name - completion:^(FIRQuery *query) { - if (query == nil) { - [self sendSnapshotError:firebaseApp - listenerId:listenerId - error:nil]; - return; - } - - RNFBFirestoreQuery *firestoreQuery = - [[RNFBFirestoreQuery alloc] initWithModifiers:firestore - query:query - filters:filters - orders:orders - options:options]; - [self handleQueryOnSnapshot:firebaseApp - firestoreQuery:firestoreQuery - listenerId:listenerId - listenerOptions:listenerOptions]; - }]; + FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp + databaseId:databaseId]; + [firestore getQueryNamed:name + completion:^(FIRQuery *query) { + if (query == nil) { + [self sendSnapshotError:firebaseApp + databaseId:databaseId + listenerId:listenerId + error:nil]; + return; + } + + RNFBFirestoreQuery *firestoreQuery = + [[RNFBFirestoreQuery alloc] initWithModifiers:firestore + query:query + filters:filters + orders:orders + options:options]; + [self handleQueryOnSnapshot:firebaseApp + databaseId:databaseId + firestoreQuery:firestoreQuery + listenerId:listenerId + listenerOptions:listenerOptions]; + }]; } RCT_EXPORT_METHOD(collectionOnSnapshot : (FIRApp *)firebaseApp + : (NSString *)databaseId : (NSString *)path : (NSString *)type : (NSArray *)filters @@ -110,7 +115,8 @@ - (void)invalidate { return; } - FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp]; + FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp + databaseId:databaseId]; FIRQuery *query = [RNFBFirestoreCommon getQueryForFirestore:firestore path:path type:type]; RNFBFirestoreQuery *firestoreQuery = [[RNFBFirestoreQuery alloc] initWithModifiers:firestore @@ -119,12 +125,16 @@ - (void)invalidate { orders:orders options:options]; [self handleQueryOnSnapshot:firebaseApp + databaseId:databaseId firestoreQuery:firestoreQuery listenerId:listenerId listenerOptions:listenerOptions]; } -RCT_EXPORT_METHOD(collectionOffSnapshot : (FIRApp *)firebaseApp : (nonnull NSNumber *)listenerId) { +RCT_EXPORT_METHOD(collectionOffSnapshot + : (FIRApp *)firebaseApp + : (NSString *)databaseId + : (nonnull NSNumber *)listenerId) { id listener = collectionSnapshotListeners[listenerId]; if (listener) { [listener remove]; @@ -134,6 +144,7 @@ - (void)invalidate { RCT_EXPORT_METHOD(namedQueryGet : (FIRApp *)firebaseApp + : (NSString *)databaseId : (NSString *)name : (NSString *)type : (NSArray *)filters @@ -142,31 +153,33 @@ - (void)invalidate { : (NSDictionary *)getOptions : (RCTPromiseResolveBlock)resolve : (RCTPromiseRejectBlock)reject) { - FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp]; - [[FIRFirestore firestore] - getQueryNamed:name - completion:^(FIRQuery *query) { - if (query == nil) { - return [RNFBFirestoreCommon promiseRejectFirestoreException:reject error:nil]; - } - - RNFBFirestoreQuery *firestoreQuery = - [[RNFBFirestoreQuery alloc] initWithModifiers:firestore - query:query - filters:filters - orders:orders - options:options]; - FIRFirestoreSource source = [self getSource:getOptions]; - [self handleQueryGet:firebaseApp - firestoreQuery:firestoreQuery - source:source - resolve:resolve - reject:reject]; - }]; + FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp + databaseId:databaseId]; + [firestore getQueryNamed:name + completion:^(FIRQuery *query) { + if (query == nil) { + return [RNFBFirestoreCommon promiseRejectFirestoreException:reject error:nil]; + } + + RNFBFirestoreQuery *firestoreQuery = + [[RNFBFirestoreQuery alloc] initWithModifiers:firestore + query:query + filters:filters + orders:orders + options:options]; + FIRFirestoreSource source = [self getSource:getOptions]; + [self handleQueryGet:firebaseApp + databaseId:databaseId + firestoreQuery:firestoreQuery + source:source + resolve:resolve + reject:reject]; + }]; } RCT_EXPORT_METHOD(collectionCount : (FIRApp *)firebaseApp + : (NSString *)databaseId : (NSString *)path : (NSString *)type : (NSArray *)filters @@ -174,7 +187,8 @@ - (void)invalidate { : (NSDictionary *)options : (RCTPromiseResolveBlock)resolve : (RCTPromiseRejectBlock)reject) { - FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp]; + FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp + databaseId:databaseId]; FIRQuery *query = [RNFBFirestoreCommon getQueryForFirestore:firestore path:path type:type]; RNFBFirestoreQuery *firestoreQuery = [[RNFBFirestoreQuery alloc] initWithModifiers:firestore query:query @@ -204,6 +218,7 @@ - (void)invalidate { RCT_EXPORT_METHOD(collectionGet : (FIRApp *)firebaseApp + : (NSString *)databaseId : (NSString *)path : (NSString *)type : (NSArray *)filters @@ -212,7 +227,8 @@ - (void)invalidate { : (NSDictionary *)getOptions : (RCTPromiseResolveBlock)resolve : (RCTPromiseRejectBlock)reject) { - FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp]; + FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp + databaseId:databaseId]; FIRQuery *query = [RNFBFirestoreCommon getQueryForFirestore:firestore path:path type:type]; RNFBFirestoreQuery *firestoreQuery = [[RNFBFirestoreQuery alloc] initWithModifiers:firestore @@ -222,6 +238,7 @@ - (void)invalidate { options:options]; FIRFirestoreSource source = [self getSource:getOptions]; [self handleQueryGet:firebaseApp + databaseId:databaseId firestoreQuery:firestoreQuery source:source resolve:resolve @@ -229,6 +246,7 @@ - (void)invalidate { } - (void)handleQueryOnSnapshot:(FIRApp *)firebaseApp + databaseId:(NSString *)databaseId firestoreQuery:(RNFBFirestoreQuery *)firestoreQuery listenerId:(nonnull NSNumber *)listenerId listenerOptions:(NSDictionary *)listenerOptions { @@ -245,9 +263,13 @@ - (void)handleQueryOnSnapshot:(FIRApp *)firebaseApp [listener remove]; [collectionSnapshotListeners removeObjectForKey:listenerId]; } - [weakSelf sendSnapshotError:firebaseApp listenerId:listenerId error:error]; + [weakSelf sendSnapshotError:firebaseApp + databaseId:databaseId + listenerId:listenerId + error:error]; } else { [weakSelf sendSnapshotEvent:firebaseApp + databaseId:databaseId listenerId:listenerId snapshot:snapshot includeMetadataChanges:includeMetadataChanges]; @@ -261,6 +283,7 @@ - (void)handleQueryOnSnapshot:(FIRApp *)firebaseApp } - (void)handleQueryGet:(FIRApp *)firebaseApp + databaseId:(NSString *)databaseId firestoreQuery:(RNFBFirestoreQuery *)firestoreQuery source:(FIRFirestoreSource)source resolve:(RCTPromiseResolveBlock)resolve @@ -277,13 +300,15 @@ - (void)handleQueryGet:(FIRApp *)firebaseApp [RNFBFirestoreSerialize querySnapshotToDictionary:@"get" snapshot:snapshot includeMetadataChanges:false - appName:appName]; + appName:appName + databaseId:databaseId]; resolve(serialized); } }]; } - (void)sendSnapshotEvent:(FIRApp *)firApp + databaseId:(NSString *)databaseId listenerId:(nonnull NSNumber *)listenerId snapshot:(FIRQuerySnapshot *)snapshot includeMetadataChanges:(BOOL)includeMetadataChanges { @@ -292,11 +317,13 @@ - (void)sendSnapshotEvent:(FIRApp *)firApp [RNFBFirestoreSerialize querySnapshotToDictionary:@"onSnapshot" snapshot:snapshot includeMetadataChanges:includeMetadataChanges - appName:appName]; + appName:appName + databaseId:databaseId]; [[RNFBRCTEventEmitter shared] sendEventWithName:RNFB_FIRESTORE_COLLECTION_SYNC body:@{ @"appName" : [RNFBSharedUtils getAppJavaScriptName:firApp.name], + @"databaseId" : databaseId, @"listenerId" : listenerId, @"body" : @{ @"snapshot" : serialized, @@ -305,6 +332,7 @@ - (void)sendSnapshotEvent:(FIRApp *)firApp } - (void)sendSnapshotError:(FIRApp *)firApp + databaseId:(NSString *)databaseId listenerId:(nonnull NSNumber *)listenerId error:(NSError *)error { NSArray *codeAndMessage = [RNFBFirestoreCommon getCodeAndMessage:error]; @@ -312,6 +340,7 @@ - (void)sendSnapshotError:(FIRApp *)firApp sendEventWithName:RNFB_FIRESTORE_COLLECTION_SYNC body:@{ @"appName" : [RNFBSharedUtils getAppJavaScriptName:firApp.name], + @"databaseId" : databaseId, @"listenerId" : listenerId, @"body" : @{ @"error" : @{ diff --git a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCommon.h b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCommon.h index a97090773a..b2cd6d2fac 100644 --- a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCommon.h +++ b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCommon.h @@ -23,9 +23,13 @@ + (dispatch_queue_t)getFirestoreQueue; -+ (FIRFirestore *)getFirestoreForApp:(FIRApp *)firebaseApp; ++ (FIRFirestore *)getFirestoreForApp:(FIRApp *)firebaseApp databaseId:(NSString *)databaseId; -+ (void)setFirestoreSettings:(FIRFirestore *)firestore appName:(NSString *)appName; ++ (NSString *)createFirestoreKeyWithAppName:(NSString *)appName databaseId:(NSString *)databaseId; + ++ (void)setFirestoreSettings:(FIRFirestore *)firestore + appName:(NSString *)appName + databaseId:(NSString *)databaseId; + (FIRDocumentReference *)getDocumentForFirestore:(FIRFirestore *)firestore path:(NSString *)path; diff --git a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCommon.m b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCommon.m index 9a5aa19edf..36237670d0 100644 --- a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCommon.m +++ b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCommon.m @@ -29,26 +29,33 @@ NSMutableDictionary *instanceCache; @implementation RNFBFirestoreCommon -+ (FIRFirestore *)getFirestoreForApp:(FIRApp *)app { ++ (FIRFirestore *)getFirestoreForApp:(FIRApp *)app databaseId:(NSString *)databaseId { if (instanceCache == nil) { instanceCache = [[NSMutableDictionary alloc] init]; } - - FIRFirestore *cachedInstance = instanceCache[[app name]]; + NSString *firestoreKey = [RNFBFirestoreCommon createFirestoreKeyWithAppName:[app name] + databaseId:databaseId]; + FIRFirestore *cachedInstance = instanceCache[firestoreKey]; if (cachedInstance) { return cachedInstance; } - FIRFirestore *instance = [FIRFirestore firestoreForApp:app]; + FIRFirestore *instance = [FIRFirestore firestoreForApp:app database:databaseId]; - [self setFirestoreSettings:instance appName:[RNFBSharedUtils getAppJavaScriptName:app.name]]; + [self setFirestoreSettings:instance + appName:[RNFBSharedUtils getAppJavaScriptName:app.name] + databaseId:databaseId]; instanceCache[[app name]] = instance; return instance; } ++ (NSString *)createFirestoreKeyWithAppName:(NSString *)appName databaseId:(NSString *)databaseId { + return [NSString stringWithFormat:@"%@:%@", appName, databaseId]; +} + + (dispatch_queue_t)getFirestoreQueue { static dispatch_queue_t firestoreQueue; static dispatch_once_t once; @@ -59,13 +66,18 @@ + (dispatch_queue_t)getFirestoreQueue { return firestoreQueue; } -+ (void)setFirestoreSettings:(FIRFirestore *)firestore appName:(NSString *)appName { ++ (void)setFirestoreSettings:(FIRFirestore *)firestore + appName:(NSString *)appName + databaseId:(NSString *)databaseId { FIRFirestoreSettings *firestoreSettings = [[FIRFirestoreSettings alloc] init]; RNFBPreferences *preferences = [RNFBPreferences shared]; firestoreSettings.dispatchQueue = [self getFirestoreQueue]; - NSString *cacheKey = [NSString stringWithFormat:@"%@_%@", FIRESTORE_CACHE_SIZE, appName]; + NSString *firestoreKey = [RNFBFirestoreCommon createFirestoreKeyWithAppName:appName + databaseId:databaseId]; + + NSString *cacheKey = [NSString stringWithFormat:@"%@_%@", FIRESTORE_CACHE_SIZE, firestoreKey]; NSInteger size = [preferences getIntegerValue:cacheKey defaultValue:0]; if (size == -1) { @@ -76,16 +88,17 @@ + (void)setFirestoreSettings:(FIRFirestore *)firestore appName:(NSString *)appNa firestoreSettings.cacheSizeBytes = size; } - NSString *hostKey = [NSString stringWithFormat:@"%@_%@", FIRESTORE_HOST, appName]; + NSString *hostKey = [NSString stringWithFormat:@"%@_%@", FIRESTORE_HOST, firestoreKey]; firestoreSettings.host = [preferences getStringValue:hostKey defaultValue:firestore.settings.host]; - NSString *persistenceKey = [NSString stringWithFormat:@"%@_%@", FIRESTORE_PERSISTENCE, appName]; + NSString *persistenceKey = + [NSString stringWithFormat:@"%@_%@", FIRESTORE_PERSISTENCE, firestoreKey]; firestoreSettings.persistenceEnabled = (BOOL)[preferences getBooleanValue:persistenceKey defaultValue:firestore.settings.persistenceEnabled]; - NSString *sslKey = [NSString stringWithFormat:@"%@_%@", FIRESTORE_SSL, appName]; + NSString *sslKey = [NSString stringWithFormat:@"%@_%@", FIRESTORE_SSL, firestoreKey]; firestoreSettings.sslEnabled = (BOOL)[preferences getBooleanValue:sslKey defaultValue:firestore.settings.sslEnabled]; diff --git a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreDocumentModule.m b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreDocumentModule.m index 3770f7d426..2805b6d34e 100644 --- a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreDocumentModule.m +++ b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreDocumentModule.m @@ -63,6 +63,7 @@ - (void)invalidate { RCT_EXPORT_METHOD(documentOnSnapshot : (FIRApp *)firebaseApp + : (NSString *)databaseId : (NSString *)path : (nonnull NSNumber *)listenerId : (NSDictionary *)listenerOptions) { @@ -70,7 +71,8 @@ - (void)invalidate { return; } - FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp]; + FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp + databaseId:databaseId]; FIRDocumentReference *documentReference = [RNFBFirestoreCommon getDocumentForFirestore:firestore path:path]; @@ -82,9 +84,15 @@ - (void)invalidate { [listener remove]; [documentSnapshotListeners removeObjectForKey:listenerId]; } - [weakSelf sendSnapshotError:firebaseApp listenerId:listenerId error:error]; + [weakSelf sendSnapshotError:firebaseApp + databaseId:databaseId + listenerId:listenerId + error:error]; } else { - [weakSelf sendSnapshotEvent:firebaseApp listenerId:listenerId snapshot:snapshot]; + [weakSelf sendSnapshotEvent:firebaseApp + databaseId:databaseId + listenerId:listenerId + snapshot:snapshot]; } }; @@ -99,7 +107,10 @@ - (void)invalidate { documentSnapshotListeners[listenerId] = listener; } -RCT_EXPORT_METHOD(documentOffSnapshot : (FIRApp *)firebaseApp : (nonnull NSNumber *)listenerId) { +RCT_EXPORT_METHOD(documentOffSnapshot + : (FIRApp *)firebaseApp + : (NSString *)databaseId + : (nonnull NSNumber *)listenerId) { id listener = documentSnapshotListeners[listenerId]; if (listener) { [listener remove]; @@ -109,11 +120,13 @@ - (void)invalidate { RCT_EXPORT_METHOD(documentGet : (FIRApp *)firebaseApp + : (NSString *)databaseId : (NSString *)path : (NSDictionary *)getOptions : (RCTPromiseResolveBlock)resolve : (RCTPromiseRejectBlock)reject) { - FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp]; + FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp + databaseId:databaseId]; FIRDocumentReference *documentReference = [RNFBFirestoreCommon getDocumentForFirestore:firestore path:path]; @@ -139,9 +152,12 @@ - (void)invalidate { error:error]; } else { NSString *appName = [RNFBSharedUtils getAppJavaScriptName:firebaseApp.name]; + NSString *firestoreKey = + [RNFBFirestoreCommon createFirestoreKeyWithAppName:appName + databaseId:databaseId]; NSDictionary *serialized = [RNFBFirestoreSerialize documentSnapshotToDictionary:snapshot - appName:appName]; + firestoreKey:firestoreKey]; resolve(serialized); } }]; @@ -149,10 +165,12 @@ - (void)invalidate { RCT_EXPORT_METHOD(documentDelete : (FIRApp *)firebaseApp + : (NSString *)databaseId : (NSString *)path : (RCTPromiseResolveBlock)resolve : (RCTPromiseRejectBlock)reject) { - FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp]; + FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp + databaseId:databaseId]; FIRDocumentReference *documentReference = [RNFBFirestoreCommon getDocumentForFirestore:firestore path:path]; @@ -167,12 +185,14 @@ - (void)invalidate { RCT_EXPORT_METHOD(documentSet : (FIRApp *)firebaseApp + : (NSString *)databaseId : (NSString *)path : (NSDictionary *)data : (NSDictionary *)options : (RCTPromiseResolveBlock)resolve : (RCTPromiseRejectBlock)reject) { - FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp]; + FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp + databaseId:databaseId]; FIRDocumentReference *documentReference = [RNFBFirestoreCommon getDocumentForFirestore:firestore path:path]; @@ -199,11 +219,13 @@ - (void)invalidate { RCT_EXPORT_METHOD(documentUpdate : (FIRApp *)firebaseApp + : (NSString *)databaseId : (NSString *)path : (NSDictionary *)data : (RCTPromiseResolveBlock)resolve : (RCTPromiseRejectBlock)reject) { - FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp]; + FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp + databaseId:databaseId]; FIRDocumentReference *documentReference = [RNFBFirestoreCommon getDocumentForFirestore:firestore path:path]; @@ -222,10 +244,12 @@ - (void)invalidate { RCT_EXPORT_METHOD(documentBatch : (FIRApp *)firebaseApp + : (NSString *)databaseId : (NSArray *)writes : (RCTPromiseResolveBlock)resolve : (RCTPromiseRejectBlock)reject) { - FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp]; + FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp + databaseId:databaseId]; FIRWriteBatch *batch = [firestore batch]; for (NSDictionary *write in writes) { @@ -263,15 +287,19 @@ - (void)invalidate { } - (void)sendSnapshotEvent:(FIRApp *)firApp + databaseId:(NSString *)databaseId listenerId:(nonnull NSNumber *)listenerId snapshot:(FIRDocumentSnapshot *)snapshot { NSString *appName = [RNFBSharedUtils getAppJavaScriptName:firApp.name]; + NSString *firestoreKey = [RNFBFirestoreCommon createFirestoreKeyWithAppName:appName + databaseId:databaseId]; NSDictionary *serialized = [RNFBFirestoreSerialize documentSnapshotToDictionary:snapshot - appName:appName]; + firestoreKey:firestoreKey]; [[RNFBRCTEventEmitter shared] sendEventWithName:RNFB_FIRESTORE_DOCUMENT_SYNC body:@{ @"appName" : [RNFBSharedUtils getAppJavaScriptName:firApp.name], + @"databaseId" : databaseId, @"listenerId" : listenerId, @"body" : @{ @"snapshot" : serialized, @@ -280,6 +308,7 @@ - (void)sendSnapshotEvent:(FIRApp *)firApp } - (void)sendSnapshotError:(FIRApp *)firApp + databaseId:(NSString *)databaseId listenerId:(nonnull NSNumber *)listenerId error:(NSError *)error { NSArray *codeAndMessage = [RNFBFirestoreCommon getCodeAndMessage:error]; @@ -287,6 +316,7 @@ - (void)sendSnapshotError:(FIRApp *)firApp sendEventWithName:RNFB_FIRESTORE_DOCUMENT_SYNC body:@{ @"appName" : [RNFBSharedUtils getAppJavaScriptName:firApp.name], + @"databaseId" : databaseId, @"listenerId" : listenerId, @"body" : @{ @"error" : @{ diff --git a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreModule.m b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreModule.m index 38e5b312e2..2563487aeb 100644 --- a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreModule.m +++ b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreModule.m @@ -45,9 +45,10 @@ + (BOOL)requiresMainQueueSetup { RCT_EXPORT_METHOD(disableNetwork : (FIRApp *)firebaseApp + : (NSString *)databaseId : (RCTPromiseResolveBlock)resolve : (RCTPromiseRejectBlock)reject) { - [[RNFBFirestoreCommon getFirestoreForApp:firebaseApp] + [[RNFBFirestoreCommon getFirestoreForApp:firebaseApp databaseId:databaseId] disableNetworkWithCompletion:^(NSError *error) { if (error) { [RNFBFirestoreCommon promiseRejectFirestoreException:reject error:error]; @@ -59,9 +60,10 @@ + (BOOL)requiresMainQueueSetup { RCT_EXPORT_METHOD(enableNetwork : (FIRApp *)firebaseApp + : (NSString *)databaseId : (RCTPromiseResolveBlock)resolve : (RCTPromiseRejectBlock)reject) { - [[RNFBFirestoreCommon getFirestoreForApp:firebaseApp] + [[RNFBFirestoreCommon getFirestoreForApp:firebaseApp databaseId:databaseId] enableNetworkWithCompletion:^(NSError *error) { if (error) { [RNFBFirestoreCommon promiseRejectFirestoreException:reject error:error]; @@ -73,36 +75,40 @@ + (BOOL)requiresMainQueueSetup { RCT_EXPORT_METHOD(settings : (FIRApp *)firebaseApp + : (NSString *)databaseId : (NSDictionary *)settings : (RCTPromiseResolveBlock)resolve : (RCTPromiseRejectBlock)reject) { NSString *appName = [RNFBSharedUtils getAppJavaScriptName:firebaseApp.name]; + NSString *firestoreKey = [RNFBFirestoreCommon createFirestoreKeyWithAppName:appName + databaseId:databaseId]; if (settings[@"cacheSizeBytes"]) { - NSString *cacheKey = [NSString stringWithFormat:@"%@_%@", FIRESTORE_CACHE_SIZE, appName]; + NSString *cacheKey = [NSString stringWithFormat:@"%@_%@", FIRESTORE_CACHE_SIZE, firestoreKey]; [[RNFBPreferences shared] setIntegerValue:cacheKey integerValue:[settings[@"cacheSizeBytes"] integerValue]]; } if (settings[@"host"]) { - NSString *hostKey = [NSString stringWithFormat:@"%@_%@", FIRESTORE_HOST, appName]; + NSString *hostKey = [NSString stringWithFormat:@"%@_%@", FIRESTORE_HOST, firestoreKey]; [[RNFBPreferences shared] setStringValue:hostKey stringValue:settings[@"host"]]; } if (settings[@"persistence"]) { - NSString *persistenceKey = [NSString stringWithFormat:@"%@_%@", FIRESTORE_PERSISTENCE, appName]; + NSString *persistenceKey = + [NSString stringWithFormat:@"%@_%@", FIRESTORE_PERSISTENCE, firestoreKey]; [[RNFBPreferences shared] setBooleanValue:persistenceKey boolValue:[settings[@"persistence"] boolValue]]; } if (settings[@"ssl"]) { - NSString *sslKey = [NSString stringWithFormat:@"%@_%@", FIRESTORE_SSL, appName]; + NSString *sslKey = [NSString stringWithFormat:@"%@_%@", FIRESTORE_SSL, firestoreKey]; [[RNFBPreferences shared] setBooleanValue:sslKey boolValue:[settings[@"ssl"] boolValue]]; } if (settings[@"serverTimestampBehavior"]) { NSString *key = - [NSString stringWithFormat:@"%@_%@", FIRESTORE_SERVER_TIMESTAMP_BEHAVIOR, appName]; + [NSString stringWithFormat:@"%@_%@", FIRESTORE_SERVER_TIMESTAMP_BEHAVIOR, firestoreKey]; [[RNFBPreferences shared] setStringValue:key stringValue:settings[@"serverTimestampBehavior"]]; } @@ -111,11 +117,12 @@ + (BOOL)requiresMainQueueSetup { RCT_EXPORT_METHOD(loadBundle : (FIRApp *)firebaseApp + : (NSString *)databaseId : (nonnull NSString *)bundle : (RCTPromiseResolveBlock)resolve : (RCTPromiseRejectBlock)reject) { NSData *bundleData = [bundle dataUsingEncoding:NSUTF8StringEncoding]; - [[RNFBFirestoreCommon getFirestoreForApp:firebaseApp] + [[RNFBFirestoreCommon getFirestoreForApp:firebaseApp databaseId:databaseId] loadBundle:bundleData completion:^(FIRLoadBundleTaskProgress *progress, NSError *error) { if (error) { @@ -128,9 +135,10 @@ + (BOOL)requiresMainQueueSetup { RCT_EXPORT_METHOD(clearPersistence : (FIRApp *)firebaseApp + : (NSString *)databaseId : (RCTPromiseResolveBlock)resolve : (RCTPromiseRejectBlock)reject) { - [[RNFBFirestoreCommon getFirestoreForApp:firebaseApp] + [[RNFBFirestoreCommon getFirestoreForApp:firebaseApp databaseId:databaseId] clearPersistenceWithCompletion:^(NSError *error) { if (error) { [RNFBFirestoreCommon promiseRejectFirestoreException:reject error:error]; @@ -142,15 +150,20 @@ + (BOOL)requiresMainQueueSetup { RCT_EXPORT_METHOD(useEmulator : (FIRApp *)firebaseApp + : (NSString *)databaseId : (nonnull NSString *)host : (NSInteger)port) { if (emulatorConfigs == nil) { emulatorConfigs = [[NSMutableDictionary alloc] init]; } - if (!emulatorConfigs[firebaseApp.name]) { - FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp]; + + NSString *firestoreKey = [RNFBFirestoreCommon createFirestoreKeyWithAppName:firebaseApp.name + databaseId:databaseId]; + if (!emulatorConfigs[firestoreKey]) { + FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp + databaseId:databaseId]; [firestore useEmulatorWithHost:host port:port]; - emulatorConfigs[firebaseApp.name] = @YES; + emulatorConfigs[firestoreKey] = @YES; // It is not sufficient to just use emulator. You have toggle SSL off too. FIRFirestoreSettings *settings = firestore.settings; @@ -161,9 +174,10 @@ + (BOOL)requiresMainQueueSetup { RCT_EXPORT_METHOD(waitForPendingWrites : (FIRApp *)firebaseApp + : (NSString *)databaseId : (RCTPromiseResolveBlock)resolve : (RCTPromiseRejectBlock)reject) { - [[RNFBFirestoreCommon getFirestoreForApp:firebaseApp] + [[RNFBFirestoreCommon getFirestoreForApp:firebaseApp databaseId:databaseId] waitForPendingWritesWithCompletion:^(NSError *error) { if (error) { [RNFBFirestoreCommon promiseRejectFirestoreException:reject error:error]; @@ -175,15 +189,19 @@ + (BOOL)requiresMainQueueSetup { RCT_EXPORT_METHOD(terminate : (FIRApp *)firebaseApp + : (NSString *)databaseId : (RCTPromiseResolveBlock)resolve : (RCTPromiseRejectBlock)reject) { - FIRFirestore *instance = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp]; + FIRFirestore *instance = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp + databaseId:databaseId]; [instance terminateWithCompletion:^(NSError *error) { if (error) { [RNFBFirestoreCommon promiseRejectFirestoreException:reject error:error]; } else { - [instanceCache removeObjectForKey:[firebaseApp name]]; + NSString *firestoreKey = [RNFBFirestoreCommon createFirestoreKeyWithAppName:firebaseApp.name + databaseId:databaseId]; + [instanceCache removeObjectForKey:firestoreKey]; resolve(nil); } }]; diff --git a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.h b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.h index 180c37224a..6c601af7af 100644 --- a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.h +++ b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.h @@ -24,14 +24,16 @@ + (NSDictionary *)querySnapshotToDictionary:(NSString *)source snapshot:(FIRQuerySnapshot *)snapshot includeMetadataChanges:(BOOL)includeMetadataChanges - appName:(NSString *)appName; + appName:(NSString *)appName + databaseId:(NSString *)databaseId; + (NSDictionary *)documentChangeToDictionary:(FIRDocumentChange *)documentChange isMetadataChange:(BOOL)isMetadataChange - appName:(NSString *)appName; + appName:(NSString *)appName + databaseId:(NSString *)databaseId; + (NSDictionary *)documentSnapshotToDictionary:(FIRDocumentSnapshot *)snapshot - appName:(NSString *)appName; + firestoreKey:(NSString *)firestoreKey; + (NSDictionary *)serializeDictionary:(NSDictionary *)dictionary; diff --git a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.m b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.m index 09bbf943ee..33f20e0a57 100644 --- a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.m +++ b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.m @@ -65,7 +65,8 @@ @implementation RNFBFirestoreSerialize + (NSDictionary *)querySnapshotToDictionary:(NSString *)source snapshot:(FIRQuerySnapshot *)snapshot includeMetadataChanges:(BOOL)includeMetadataChanges - appName:(NSString *)appName { + appName:(NSString *)appName + databaseId:(NSString *)databaseId { NSMutableArray *metadata = [[NSMutableArray alloc] init]; NSMutableDictionary *snapshotMap = [[NSMutableDictionary alloc] init]; @@ -84,7 +85,8 @@ + (NSDictionary *)querySnapshotToDictionary:(NSString *)source for (FIRDocumentChange *documentChange in documentChangesList) { [changes addObject:[self documentChangeToDictionary:documentChange isMetadataChange:false - appName:appName]]; + appName:appName + databaseId:databaseId]]; } } else { // If listening to metadata changes, get the changes list with document changes array. @@ -119,16 +121,19 @@ + (NSDictionary *)querySnapshotToDictionary:(NSString *)source [changes addObject:[self documentChangeToDictionary:documentMetadataChange isMetadataChange:isMetadataChange - appName:appName]]; + appName:appName + databaseId:databaseId]]; } } snapshotMap[KEY_CHANGES] = changes; - + NSString *firestoreKey = [RNFBFirestoreCommon createFirestoreKeyWithAppName:appName + databaseId:databaseId]; // set documents NSMutableArray *documents = [[NSMutableArray alloc] init]; for (FIRDocumentSnapshot *documentSnapshot in documentSnapshots) { - [documents addObject:[self documentSnapshotToDictionary:documentSnapshot appName:appName]]; + [documents addObject:[self documentSnapshotToDictionary:documentSnapshot + firestoreKey:firestoreKey]]; } snapshotMap[KEY_DOCUMENTS] = documents; @@ -143,7 +148,8 @@ + (NSDictionary *)querySnapshotToDictionary:(NSString *)source + (NSDictionary *)documentChangeToDictionary:(FIRDocumentChange *)documentChange isMetadataChange:(BOOL)isMetadataChange - appName:(NSString *)appName { + appName:(NSString *)appName + databaseId:(NSString *)databaseId { NSMutableDictionary *changeMap = [[NSMutableDictionary alloc] init]; changeMap[@"isMetadataChange"] = @(isMetadataChange); @@ -154,9 +160,10 @@ + (NSDictionary *)documentChangeToDictionary:(FIRDocumentChange *)documentChange } else { changeMap[KEY_DOC_CHANGE_TYPE] = CHANGE_REMOVED; } - + NSString *firestoreKey = [RNFBFirestoreCommon createFirestoreKeyWithAppName:appName + databaseId:databaseId]; changeMap[KEY_DOC_CHANGE_DOCUMENT] = [self documentSnapshotToDictionary:documentChange.document - appName:appName]; + firestoreKey:firestoreKey]; // Note the Firestore C++ SDK here returns a maxed UInt that is != NSUIntegerMax, so we make one // ourselves so we can convert to -1 for JS land @@ -180,7 +187,7 @@ + (NSDictionary *)documentChangeToDictionary:(FIRDocumentChange *)documentChange // Native DocumentSnapshot -> NSDictionary (for JS) + (NSDictionary *)documentSnapshotToDictionary:(FIRDocumentSnapshot *)snapshot - appName:(NSString *)appName { + firestoreKey:(NSString *)firestoreKey { NSMutableArray *metadata = [[NSMutableArray alloc] init]; NSMutableDictionary *documentMap = [[NSMutableDictionary alloc] init]; @@ -194,7 +201,7 @@ + (NSDictionary *)documentSnapshotToDictionary:(FIRDocumentSnapshot *)snapshot if (snapshot.exists) { NSString *key = - [NSString stringWithFormat:@"%@_%@", FIRESTORE_SERVER_TIMESTAMP_BEHAVIOR, appName]; + [NSString stringWithFormat:@"%@_%@", FIRESTORE_SERVER_TIMESTAMP_BEHAVIOR, firestoreKey]; NSString *behavior = [[RNFBPreferences shared] getStringValue:key defaultValue:@"none"]; FIRServerTimestampBehavior serverTimestampBehavior; diff --git a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreTransactionModule.m b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreTransactionModule.m index 53088c235f..bf3dad6dda 100644 --- a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreTransactionModule.m +++ b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreTransactionModule.m @@ -61,6 +61,7 @@ - (void)invalidate { RCT_EXPORT_METHOD(transactionGetDocument : (FIRApp *)firebaseApp + : (NSString *)databaseId : (nonnull NSNumber *)transactionId : (NSString *)path : (RCTPromiseResolveBlock)resolve @@ -75,7 +76,8 @@ - (void)invalidate { NSError *error = nil; FIRTransaction *transaction = [transactionState valueForKey:@"transaction"]; - FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp]; + FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp + databaseId:databaseId]; FIRDocumentReference *ref = [RNFBFirestoreCommon getDocumentForFirestore:firestore path:path]; FIRDocumentSnapshot *snapshot = [transaction getDocument:ref error:&error]; @@ -83,8 +85,10 @@ - (void)invalidate { [RNFBFirestoreCommon promiseRejectFirestoreException:reject error:error]; } else { NSString *appName = [RNFBSharedUtils getAppJavaScriptName:firebaseApp.name]; - NSDictionary *snapshotDict = [RNFBFirestoreSerialize documentSnapshotToDictionary:snapshot - appName:appName]; + NSString *firestoreKey = [RNFBFirestoreCommon createFirestoreKeyWithAppName:appName + databaseId:databaseId]; + NSDictionary *snapshotDict = + [RNFBFirestoreSerialize documentSnapshotToDictionary:snapshot firestoreKey:firestoreKey]; NSString *snapshotPath = snapshotDict[@"path"]; if (snapshotPath == nil) { @@ -96,7 +100,10 @@ - (void)invalidate { } } -RCT_EXPORT_METHOD(transactionDispose : (FIRApp *)firebaseApp : (nonnull NSNumber *)transactionId) { +RCT_EXPORT_METHOD(transactionDispose + : (FIRApp *)firebaseApp + : (NSString *)databaseId + : (nonnull NSNumber *)transactionId) { @synchronized(transactions[[transactionId stringValue]]) { NSMutableDictionary *transactionState = transactions[[transactionId stringValue]]; @@ -112,6 +119,7 @@ - (void)invalidate { RCT_EXPORT_METHOD(transactionApplyBuffer : (FIRApp *)firebaseApp + : (NSString *)databaseId : (nonnull NSNumber *)transactionId : (NSArray *)commandBuffer) { @synchronized(transactions[[transactionId stringValue]]) { @@ -128,8 +136,12 @@ - (void)invalidate { } } -RCT_EXPORT_METHOD(transactionBegin : (FIRApp *)firebaseApp : (nonnull NSNumber *)transactionId) { - FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp]; +RCT_EXPORT_METHOD(transactionBegin + : (FIRApp *)firebaseApp + : (NSString *)databaseId + : (nonnull NSNumber *)transactionId) { + FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp + databaseId:databaseId]; __block BOOL aborted = false; __block NSMutableDictionary *transactionState = [NSMutableDictionary new]; @@ -153,6 +165,7 @@ - (void)invalidate { body:@{ @"listenerId" : transactionId, @"appName" : [RNFBSharedUtils getAppJavaScriptName:firebaseApp.name], + @"databaseId" : databaseId, @"body" : eventMap, }]; }); @@ -241,6 +254,7 @@ - (void)invalidate { body:@{ @"listenerId" : transactionId, @"appName" : [RNFBSharedUtils getAppJavaScriptName:firebaseApp.name], + @"databaseId" : databaseId, @"body" : eventMap, }]; } @@ -252,4 +266,4 @@ - (void)invalidate { [firestore runTransactionWithBlock:transactionBlock completion:completionBlock]; } -@end \ No newline at end of file +@end diff --git a/packages/firestore/lib/index.d.ts b/packages/firestore/lib/index.d.ts index 6747e705ec..ed816e2cfe 100644 --- a/packages/firestore/lib/index.d.ts +++ b/packages/firestore/lib/index.d.ts @@ -2367,7 +2367,7 @@ declare module '@react-native-firebase/app' { >; } interface FirebaseApp { - firestore(): FirebaseFirestoreTypes.Module; + firestore(databaseId?: string): FirebaseFirestoreTypes.Module; } } } diff --git a/packages/firestore/lib/index.js b/packages/firestore/lib/index.js index f6e7d6c992..16cd0d2ad9 100644 --- a/packages/firestore/lib/index.js +++ b/packages/firestore/lib/index.js @@ -57,8 +57,13 @@ const nativeEvents = [ ]; class FirebaseFirestoreModule extends FirebaseModule { - constructor(app, config) { + constructor(app, config, databaseId) { super(app, config); + if (isString(databaseId) || databaseId === undefined) { + this._customUrlOrRegion = databaseId || '(default)'; + } else if (!isString(databaseId)) { + throw new Error('firebase.app().firestore(*) database ID must be a string'); + } this._referencePath = new FirestorePath(); this._transactionHandler = new FirestoreTransactionHandler(this); @@ -81,6 +86,10 @@ class FirebaseFirestoreModule extends FirebaseModule { ignoreUndefinedProperties: false, }; } + // We override the FirebaseModule's `eventNameForApp()` method to include the customUrlOrRegion + eventNameForApp(...args) { + return `${this.app.name}-${this._customUrlOrRegion}-${args.join('-')}`; + } batch() { return new FirestoreWriteBatch(this); @@ -372,7 +381,7 @@ export default createModuleNamespace({ nativeModuleName, nativeEvents, hasMultiAppSupport: true, - hasCustomUrlOrRegionSupport: false, + hasCustomUrlOrRegionSupport: true, ModuleClass: FirebaseFirestoreModule, }); diff --git a/packages/firestore/lib/modular/index.d.ts b/packages/firestore/lib/modular/index.d.ts index c161da7ae0..c1f8ef8007 100644 --- a/packages/firestore/lib/modular/index.d.ts +++ b/packages/firestore/lib/modular/index.d.ts @@ -117,6 +117,18 @@ export declare function getFirestore(app: FirebaseApp): Firestore; export function getFirestore(app?: FirebaseApp): Firestore; +/** + * Returns the existing default {@link Firestore} instance that is associated with the + * provided {@link @firebase/app#FirebaseApp} and database ID. If no instance exists, initializes a new + * instance with default settings. + * + * @param app - The {@link @firebase/app#FirebaseApp} instance that the returned {@link Firestore} + * instance is associated with. + * @param databaseId - The ID of the Firestore database to use. If not provided, the default database is used. + * @returns The {@link Firestore} + */ +export declare function getFirestore(app?: FirebaseApp, databaseId?: string): Firestore; + /** * Gets a `DocumentReference` instance that refers to the document at the * specified absolute path. diff --git a/packages/firestore/lib/modular/index.js b/packages/firestore/lib/modular/index.js index d6e8d52921..ee6552fc5c 100644 --- a/packages/firestore/lib/modular/index.js +++ b/packages/firestore/lib/modular/index.js @@ -15,14 +15,22 @@ import { firebase } from '../index'; /** * @param {FirebaseApp?} app + * @param {String?} databaseId * @returns {Firestore} */ -export function getFirestore(app) { +export function getFirestore(app, databaseId) { if (app) { - return firebase.firestore(app); + if (databaseId) { + return firebase.app(app.name).firestore(databaseId); + } else { + return firebase.app(app.name).firestore(); + } + } + if (databaseId) { + return firebase.app().firestore(databaseId); } - return firebase.firestore(); + return firebase.app().firestore(); } /** diff --git a/packages/firestore/lib/web/RNFBFirestoreModule.js b/packages/firestore/lib/web/RNFBFirestoreModule.js index 3ffd5ce550..e8ffe63f94 100644 --- a/packages/firestore/lib/web/RNFBFirestoreModule.js +++ b/packages/firestore/lib/web/RNFBFirestoreModule.js @@ -68,19 +68,24 @@ function getCachedAppInstance(appName) { return (appInstances[appName] ??= getApp(appName)); } +function createFirestoreKey(appName, databaseId) { + return `${appName}:${databaseId}`; +} + // Returns a cached Firestore instance. -function getCachedFirestoreInstance(appName) { - let instance = firestoreInstances[appName]; +function getCachedFirestoreInstance(appName, databaseId) { + const firestoreKey = createFirestoreKey(appName, databaseId); + let instance = firestoreInstances[firestoreKey]; if (!instance) { - instance = getFirestore(getCachedAppInstance(appName)); - if (emulatorForApp[appName]) { + instance = getFirestore(getCachedAppInstance(appName), databaseId); + if (emulatorForApp[firestoreKey]) { connectFirestoreEmulator( instance, - emulatorForApp[appName].host, - emulatorForApp[appName].port, + emulatorForApp[firestoreKey].host, + emulatorForApp[firestoreKey].port, ); } - firestoreInstances[appName] = instance; + firestoreInstances[firestoreKey] = instance; } return instance; } @@ -126,27 +131,30 @@ export default { /** * Use the Firestore emulator. * @param {string} appName - The app name. + * @param {string} databaseId - The database ID. * @param {string} host - The emulator host. * @param {number} port - The emulator port. * @returns {Promise} An empty promise. */ - useEmulator(appName, host, port) { + useEmulator(appName, databaseId, host, port) { return guard(async () => { - const firestore = getCachedFirestoreInstance(appName); + const firestore = getCachedFirestoreInstance(appName, databaseId); connectFirestoreEmulator(firestore, host, port); - emulatorForApp[appName] = { host, port }; + const firestoreKey = createFirestoreKey(appName, databaseId); + emulatorForApp[firestoreKey] = { host, port }; }); }, /** * Initializes a Firestore instance with settings. * @param {string} appName - The app name. + * @param {string} databaseId - The database ID. * @param {object} settings - The Firestore settings. * @returns {Promise} An empty promise. */ - settings(appName, settings) { + settings(appName, databaseId, settings) { return guard(() => { - const instance = initializeFirestore(getCachedAppInstance(appName), settings); + const instance = initializeFirestore(getCachedAppInstance(appName), settings, databaseId); firestoreInstances[appName] = instance; }); }, @@ -154,11 +162,12 @@ export default { /** * Terminates a Firestore instance. * @param {string} appName - The app name. + * @param {string} databaseId - The database ID. * @returns {Promise} An empty promise. */ - terminate(appName) { + terminate(appName, databaseId) { return guard(async () => { - const firestore = getCachedFirestoreInstance(appName); + const firestore = getCachedFirestoreInstance(appName, databaseId); await terminate(firestore); return null; }); @@ -184,6 +193,7 @@ export default { /** * Get a collection count from Firestore. * @param {string} appName - The app name. + * @param {string} databaseId - The database ID. * @param {string} path - The collection path. * @param {string} type - The collection type (e.g. collectionGroup). * @param {object[]} filters - The collection filters. @@ -191,9 +201,9 @@ export default { * @param {object} options - The collection options. * @returns {Promise} The collection count object. */ - collectionCount(appName, path, type, filters, orders, options) { + collectionCount(appName, databaseId, path, type, filters, orders, options) { return guard(async () => { - const firestore = getCachedFirestoreInstance(appName); + const firestore = getCachedFirestoreInstance(appName, databaseId); const queryRef = type === 'collectionGroup' ? collectionGroup(firestore, path) : collection(firestore, path); const query = buildQuery(queryRef, filters, orders, options); @@ -208,6 +218,7 @@ export default { /** * Get a collection from Firestore. * @param {string} appName - The app name. + * @param {string} databaseId - The database ID. * @param {string} path - The collection path. * @param {string} type - The collection type (e.g. collectionGroup). * @param {object[]} filters - The collection filters. @@ -216,7 +227,7 @@ export default { * @param {object} getOptions - The get options. * @returns {Promise} The collection object. */ - collectionGet(appName, path, type, filters, orders, options, getOptions) { + collectionGet(appName, databaseId, path, type, filters, orders, options, getOptions) { if (getOptions && getOptions.source === 'cache') { return rejectWithCodeAndMessage( 'unsupported', @@ -225,7 +236,7 @@ export default { } return guard(async () => { - const firestore = getCachedFirestoreInstance(appName); + const firestore = getCachedFirestoreInstance(appName, databaseId); const queryRef = type === 'collectionGroup' ? collectionGroup(firestore, path) : collection(firestore, path); const query = buildQuery(queryRef, filters, orders, options); @@ -246,11 +257,12 @@ export default { /** * Get a document from Firestore. * @param {string} appName - The app name. + * @param {string} databaseId - The database ID. * @param {string} path - The document path. * @param {object} getOptions - The get options. * @returns {Promise} The document object. */ - documentGet(appName, path, getOptions) { + documentGet(appName, databaseId, path, getOptions) { return guard(async () => { if (getOptions && getOptions.source === 'cache') { return rejectWithCodeAndMessage( @@ -259,7 +271,7 @@ export default { ); } - const firestore = getCachedFirestoreInstance(appName); + const firestore = getCachedFirestoreInstance(appName, databaseId); const ref = doc(firestore, path); const snapshot = await getDoc(ref); return documentSnapshotToObject(snapshot); @@ -269,12 +281,13 @@ export default { /** * Delete a document from Firestore. * @param {string} appName - The app name. + * @param {string} databaseId - The database ID. * @param {string} path - The document path. * @returns {Promise} An empty promise. */ - documentDelete(appName, path) { + documentDelete(appName, databaseId, path) { return guard(async () => { - const firestore = getCachedFirestoreInstance(appName); + const firestore = getCachedFirestoreInstance(appName, databaseId); const ref = doc(firestore, path); await deleteDoc(ref); return null; @@ -284,14 +297,15 @@ export default { /** * Set a document in Firestore. * @param {string} appName - The app name. + * @param {string} databaseId - The database ID. * @param {string} path - The document path. * @param {object} data - The document data. * @param {object} options - The set options. * @returns {Promise} An empty promise. */ - documentSet(appName, path, data, options) { + documentSet(appName, databaseId, path, data, options) { return guard(async () => { - const firestore = getCachedFirestoreInstance(appName); + const firestore = getCachedFirestoreInstance(appName, databaseId); const ref = doc(firestore, path); const setOptions = {}; if ('merge' in options) { @@ -306,13 +320,14 @@ export default { /** * Update a document in Firestore. * @param {string} appName - The app name. + * @param {string} databaseId - The database ID. * @param {string} path - The document path. * @param {object} data - The document data. * @returns {Promise} An empty promise. */ - documentUpdate(appName, path, data) { + documentUpdate(appName, databaseId, path, data) { return guard(async () => { - const firestore = getCachedFirestoreInstance(appName); + const firestore = getCachedFirestoreInstance(appName, databaseId); const ref = doc(firestore, path); await updateDoc(ref, readableToObject(firestore, data)); }); @@ -321,11 +336,12 @@ export default { /** * Batch write documents in Firestore. * @param {string} appName - The app name. + * @param {string} databaseId - The database ID. * @param {object[]} writes - The document writes in write batches format. */ - documentBatch(appName, writes) { + documentBatch(appName, databaseId, writes) { return guard(async () => { - const firestore = getCachedFirestoreInstance(appName); + const firestore = getCachedFirestoreInstance(appName, databaseId); const batch = writeBatch(firestore); const writesArray = parseDocumentBatches(firestore, writes); @@ -360,11 +376,12 @@ export default { /** * Get a document from a Firestore transaction. * @param {string} appName - The app name. + * @param {string} databaseId - The database ID. * @param {string} transactionId - The transaction id. * @param {string} path - The document path. * @returns {Promise} The document object. */ - transactionGetDocument(appName, transactionId, path) { + transactionGetDocument(appName, databaseId, transactionId, path) { if (!transactionHandler[transactionId]) { return rejectWithCodeAndMessage( 'internal-error', @@ -373,7 +390,7 @@ export default { } return guard(async () => { - const firestore = getCachedFirestoreInstance(appName); + const firestore = getCachedFirestoreInstance(appName, databaseId); const docRef = doc(firestore, path); const tsx = transactionHandler[transactionId]; const snapshot = await tsx.get(docRef); @@ -384,9 +401,10 @@ export default { /** * Dispose a transaction instance. * @param {string} appName - The app name. + * @param {string} databaseId - The database ID. * @param {string} transactionId - The transaction id. */ - transactionDispose(appName, transactionId) { + transactionDispose(appName, databaseId, transactionId) { // There's no abort method in the JS SDK, so we just remove the transaction handler. delete transactionHandler[transactionId]; }, @@ -394,10 +412,11 @@ export default { /** * Applies a buffer of commands to a Firestore transaction. * @param {string} appName - The app name. + * @param {string} databaseId - The database ID. * @param {string} transactionId - The transaction id. * @param {object[]} commandBuffer - The readable array of buffer commands. */ - transactionApplyBuffer(appName, transactionId, commandBuffer) { + transactionApplyBuffer(appName, databaseId, transactionId, commandBuffer) { if (transactionHandler[transactionId]) { transactionBuffer[transactionId] = commandBuffer; } @@ -406,12 +425,13 @@ export default { /** * Begins a Firestore transaction. * @param {string} appName - The app name. + * @param {string} databaseId - The database ID. * @param {string} transactionId - The transaction id. * @returns {Promise} An empty promise. */ - transactionBegin(appName, transactionId) { + transactionBegin(appName, databaseId, transactionId) { return guard(async () => { - const firestore = getCachedFirestoreInstance(appName); + const firestore = getCachedFirestoreInstance(appName, databaseId); try { await runTransaction(firestore, async tsx => { @@ -421,6 +441,7 @@ export default { eventName: 'firestore_transaction_event', body: { type: 'update' }, appName, + databaseId, listenerId: transactionId, }); @@ -468,6 +489,7 @@ export default { eventName: 'firestore_transaction_event', body: { type: 'complete' }, appName, + databaseId, listenerId: transactionId, }); } catch (e) { @@ -475,6 +497,7 @@ export default { eventName: 'firestore_transaction_event', body: { type: 'error', error: getWebError(e) }, appName, + databaseId, listenerId: transactionId, }); } diff --git a/packages/storage/android/src/main/java/io/invertase/firebase/storage/ReactNativeFirebaseStorageModule.java b/packages/storage/android/src/main/java/io/invertase/firebase/storage/ReactNativeFirebaseStorageModule.java index 02d60cf06a..27f4cddaec 100644 --- a/packages/storage/android/src/main/java/io/invertase/firebase/storage/ReactNativeFirebaseStorageModule.java +++ b/packages/storage/android/src/main/java/io/invertase/firebase/storage/ReactNativeFirebaseStorageModule.java @@ -281,7 +281,8 @@ public void setMaxUploadRetryTime(String appName, double milliseconds, Promise p * @link https://firebase.google.com/docs/reference/js/firebase.storage.Storage#useEmulator */ @ReactMethod - public void useEmulator(String appName, String host, int port, String bucketUrl, Promise promise) { + public void useEmulator( + String appName, String host, int port, String bucketUrl, Promise promise) { FirebaseApp firebaseApp = FirebaseApp.getInstance(appName); FirebaseStorage firebaseStorage = FirebaseStorage.getInstance(firebaseApp, bucketUrl); diff --git a/tests/app.js b/tests/app.js index 0240e33038..ebb4018f35 100644 --- a/tests/app.js +++ b/tests/app.js @@ -81,6 +81,7 @@ function loadTests(_) { firebase.auth().useEmulator('http://localhost:9099'); if (platformSupportedModules.includes('firestore')) { firebase.firestore().useEmulator('localhost', 8080); + firebase.app().firestore('second-rnfb').useEmulator('localhost', 8080); // Firestore caches documents locally (a great feature!) and that confounds tests // as data from previous runs pollutes following runs until re-install the app. Clear it. if (!Platform.other) { diff --git a/tests/ios/Podfile.lock b/tests/ios/Podfile.lock index 6b9e7fa7de..c1aa026f45 100644 --- a/tests/ios/Podfile.lock +++ b/tests/ios/Podfile.lock @@ -1326,76 +1326,78 @@ PODS: - React-logger (= 0.73.4) - React-perflogger (= 0.73.4) - RecaptchaInterop (100.0.0) + - RNCAsyncStorage (1.24.0): + - React-Core - RNDeviceInfo (11.1.0): - React-Core - - RNFBAnalytics (20.1.0): + - RNFBAnalytics (20.3.0): - Firebase/Analytics (= 10.29.0) - GoogleAppMeasurementOnDeviceConversion (= 10.29.0) - React-Core - RNFBApp - - RNFBApp (20.1.0): + - RNFBApp (20.3.0): - Firebase/CoreOnly (= 10.29.0) - React-Core - - RNFBAppCheck (20.1.0): + - RNFBAppCheck (20.3.0): - Firebase/AppCheck (= 10.29.0) - React-Core - RNFBApp - - RNFBAppDistribution (20.1.0): + - RNFBAppDistribution (20.3.0): - Firebase/AppDistribution (= 10.29.0) - React-Core - RNFBApp - - RNFBAuth (20.1.0): + - RNFBAuth (20.3.0): - Firebase/Auth (= 10.29.0) - React-Core - RNFBApp - - RNFBCrashlytics (20.1.0): + - RNFBCrashlytics (20.3.0): - Firebase/Crashlytics (= 10.29.0) - FirebaseCoreExtension - React-Core - RNFBApp - - RNFBDatabase (20.1.0): + - RNFBDatabase (20.3.0): - Firebase/Database (= 10.29.0) - React-Core - RNFBApp - - RNFBDynamicLinks (20.1.0): + - RNFBDynamicLinks (20.3.0): - Firebase/DynamicLinks (= 10.29.0) - GoogleUtilities/AppDelegateSwizzler - React-Core - RNFBApp - - RNFBFirestore (20.1.0): + - RNFBFirestore (20.3.0): - Firebase/Firestore (= 10.29.0) - nanopb (< 2.30910.0, >= 2.30908.0) - React-Core - RNFBApp - - RNFBFunctions (20.1.0): + - RNFBFunctions (20.3.0): - Firebase/Functions (= 10.29.0) - React-Core - RNFBApp - - RNFBInAppMessaging (20.1.0): + - RNFBInAppMessaging (20.3.0): - Firebase/InAppMessaging (= 10.29.0) - React-Core - RNFBApp - - RNFBInstallations (20.1.0): + - RNFBInstallations (20.3.0): - Firebase/Installations (= 10.29.0) - React-Core - RNFBApp - - RNFBMessaging (20.1.0): + - RNFBMessaging (20.3.0): - Firebase/Messaging (= 10.29.0) - FirebaseCoreExtension - React-Core - RNFBApp - - RNFBML (20.1.0): + - RNFBML (20.3.0): - React-Core - RNFBApp - - RNFBPerf (20.1.0): + - RNFBPerf (20.3.0): - Firebase/Performance (= 10.29.0) - React-Core - RNFBApp - - RNFBRemoteConfig (20.1.0): + - RNFBRemoteConfig (20.3.0): - Firebase/RemoteConfig (= 10.29.0) - React-Core - RNFBApp - - RNFBStorage (20.1.0): + - RNFBStorage (20.3.0): - Firebase/Storage (= 10.29.0) - React-Core - RNFBApp @@ -1454,6 +1456,7 @@ DEPENDENCIES: - React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`) - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - RNDeviceInfo (from `../node_modules/react-native-device-info`) - "RNFBAnalytics (from `../node_modules/@react-native-firebase/analytics`)" - "RNFBApp (from `../node_modules/@react-native-firebase/app`)" @@ -1621,6 +1624,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/react/utils" ReactCommon: :path: "../node_modules/react-native/ReactCommon" + RNCAsyncStorage: + :path: "../node_modules/@react-native-async-storage/async-storage" RNDeviceInfo: :path: "../node_modules/react-native-device-info" RNFBAnalytics: @@ -1758,26 +1763,27 @@ SPEC CHECKSUMS: React-utils: 21a798438d45e70ed9c2e2fe0894ee32ba7b7c5b ReactCommon: dcc65c813041388dead6c8b477444757425ce961 RecaptchaInterop: 7d1a4a01a6b2cb1610a47ef3f85f0c411434cb21 + RNCAsyncStorage: ec53e44dc3e75b44aa2a9f37618a49c3bc080a7a RNDeviceInfo: b899ce37a403a4dea52b7cb85e16e49c04a5b88e - RNFBAnalytics: 8aa2c79f8ced7036e14907e6ae86f9f55bd1cf49 - RNFBApp: 94776f5e68f403793b86b91dc6a22ac6f03cf51f - RNFBAppCheck: 920940691bdd352b023c091a07d867d2ef968abe - RNFBAppDistribution: 50c6178315a8ac419cdbf09e2b3657c80ad9b80d - RNFBAuth: 76bde6ea67e2d6a1c250867f35bcd7c21e9a914c - RNFBCrashlytics: a63b5d5d9e02589fe8a3a59c4805bde4b9261972 - RNFBDatabase: 6448909a9f07d2b2f6c39f6765fd0d125ce378e9 - RNFBDynamicLinks: 4e181c96ad07a4d310aedcd37d3030aad9859c23 - RNFBFirestore: 54a9a3dacaa311bc1016d8c22a2acdc0009260bc - RNFBFunctions: 8392e6225e2cccf0022cea05cdf3122787de86b8 - RNFBInAppMessaging: 4ea54b07b8aa30c252508f3683ca2dee36c0e74b - RNFBInstallations: eb59cb11bdc16c13feddfd9ad0c8c02a652287cc - RNFBMessaging: ee80027da5c5eba48109c5066dc568fb98f9c27d - RNFBML: 617f875def61029d0c4632af07bde4be4c76bff0 - RNFBPerf: 1853af83b06f70099c30baeae84de22d1fce55c2 - RNFBRemoteConfig: 68b8f0a65dcb7c06af92a2b4c7cc674703019ccf - RNFBStorage: a569fc000b2ff3befeb2aa7d2c6455ca0c21da7c + RNFBAnalytics: 921cce283e56e0775b10708bd1856eb5d44011ef + RNFBApp: a7aff07a7f212149539fce51a1326c6d208b03ad + RNFBAppCheck: 2463063b94cf3178e0398175636bcc3766f4f98e + RNFBAppDistribution: 35f4080726886e005c9af0097ebcc624bfca8aad + RNFBAuth: 3ed676e60d0ca3b2c71023240658e1f02bc752d7 + RNFBCrashlytics: 0194114803bf2984cf241fd79198a14c575b317e + RNFBDatabase: d46a1d1cbcf3769179deaed11e74753999e6f3a1 + RNFBDynamicLinks: 5c83930d0ba2478501e1e330322f1ef57059720d + RNFBFirestore: 2c8d40c5a28007d6a3d183e21141e2fedf7f6d41 + RNFBFunctions: bb2c9cf33f64efddf8852d1077e599436f99fa9c + RNFBInAppMessaging: 8f2cb9ebfbd83c1923e44822ddd585aebd20c71d + RNFBInstallations: e4ba9770162024c6471d3ce3c9cfe5690207e041 + RNFBMessaging: f1fdc7ac62df96e3c8362d516caaf84fab69085d + RNFBML: e4045427926875c7a469332f1e75fd96999d8202 + RNFBPerf: 818dc56dee8203dbdf5ab2bef2675a62c0df2978 + RNFBRemoteConfig: 3d9c04beffe742f407d89d2b295a02420df4d2e8 + RNFBStorage: 5c67f0b3d55ef904b87ad7826ce0d588c2e028fb SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 - Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312 + Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70 PODFILE CHECKSUM: 95dd7da90f1ff7fdc1017f409737e31d23b1c46a