diff --git a/packages/app/lib/internal/web/firebaseDatabase.js b/packages/app/lib/internal/web/firebaseDatabase.js new file mode 100644 index 0000000000..bf5ae640b4 --- /dev/null +++ b/packages/app/lib/internal/web/firebaseDatabase.js @@ -0,0 +1,4 @@ +// We need to share firebase imports between modules, otherwise +// apps and instances of the firebase modules are not shared. +export * from 'firebase/app'; +export * from 'firebase/database'; diff --git a/packages/app/lib/internal/web/utils.js b/packages/app/lib/internal/web/utils.js index 7b013891df..95686ec43a 100644 --- a/packages/app/lib/internal/web/utils.js +++ b/packages/app/lib/internal/web/utils.js @@ -12,10 +12,18 @@ export function getWebError(error) { code: error.code || 'unknown', message: error.message, }; + + // Some modules send codes as PERMISSION_DENIED, which is not + // the same as the Firebase error code format. + obj.code = obj.code.toLowerCase(); + // Replace _ with - in code + obj.code = obj.code.replace(/_/g, '-'); + // Split out prefix, since we internally prefix all error codes already. if (obj.code.includes('/')) { obj.code = obj.code.split('/')[1]; } + return { ...obj, userInfo: obj, diff --git a/packages/database/e2e/helpers.js b/packages/database/e2e/helpers.js index 009c9986e2..7aee880498 100644 --- a/packages/database/e2e/helpers.js +++ b/packages/database/e2e/helpers.js @@ -6,7 +6,23 @@ const ID = Date.now(); const PATH = `tests/${ID}`; const DB_NAME = getE2eTestProject(); -const DB_RULES = `{ "rules": {".read": false, ".write": false, "tests": {".read": true, ".write": true } } }`; +const DB_RULES = { + rules: { + '.read': false, + '.write': false, + tests: { + '.read': true, + '.write': true, + $dynamic: { + once: { + childMoved: { + '.indexOn': ['nuggets'], + }, + }, + }, + }, + }, +}; const CONTENT = { TYPES: { @@ -40,9 +56,15 @@ exports.seed = function seed(path) { firebase.database().ref(`${path}/types`).set(CONTENT.TYPES), firebase.database().ref(`${path}/query`).set(CONTENT.QUERY), // The database emulator does not load rules correctly. We force them pre-test. + // TODO(ehesp): This is current erroring - however without it, we can't test rules. testingUtils.initializeTestEnvironment({ projectId: getE2eTestProject(), - database: { databaseName: DB_NAME, rules: DB_RULES, host: getE2eEmulatorHost(), port: 9000 }, + database: { + databaseName: DB_NAME, + rules: JSON.stringify(DB_RULES), + host: getE2eEmulatorHost(), + port: 9000, + }, }), ]); }; diff --git a/packages/database/e2e/query/keepSynced.e2e.js b/packages/database/e2e/query/keepSynced.e2e.js index ce7e5ba68e..7873021a7f 100644 --- a/packages/database/e2e/query/keepSynced.e2e.js +++ b/packages/database/e2e/query/keepSynced.e2e.js @@ -28,6 +28,7 @@ describe('database().ref().keepSynced()', function () { }); it('toggles keepSynced on and off without throwing', async function () { + if (Platform.other) return; const ref = firebase.database().ref('noop').orderByValue(); await ref.keepSynced(true); await ref.keepSynced(false); @@ -48,6 +49,7 @@ describe('database().ref().keepSynced()', function () { }); it('toggles keepSynced on and off without throwing', async function () { + if (Platform.other) return; const { getDatabase, ref, orderByValue, query, keepSynced } = databaseModular; const dbRef = query(ref(getDatabase(), 'noop'), orderByValue()); diff --git a/packages/database/e2e/query/on.e2e.js b/packages/database/e2e/query/on.e2e.js index 844f4e6a84..890c36a5b7 100644 --- a/packages/database/e2e/query/on.e2e.js +++ b/packages/database/e2e/query/on.e2e.js @@ -94,19 +94,20 @@ describe('database().ref().on()', function () { ref.off('value'); }); - xit('should callback multiple times when the value changes', async function () { + it('should callback multiple times when the value changes', async function () { const callback = sinon.spy(); - const ref = firebase.database().ref(`${TEST_PATH}/changes`); + const date = Date.now(); + const ref = firebase.database().ref(`${TEST_PATH}/multi-change/${date}`); ref.on('value', $ => { - // console.error('callback with ' + $.val()); callback($.val()); }); await ref.set('foo'); + await Utils.sleep(100); await ref.set('bar'); await Utils.spyToBeCalledTimesAsync(callback, 2); ref.off('value'); - callback.getCall(0).args[0].should.equal('foo'); // FIXME these simply do *not* come back - callback.getCall(1).args[0].should.equal('bar'); // in the right order every time. ?? + callback.getCall(0).args[0].should.equal('foo'); + callback.getCall(1).args[0].should.equal('bar'); }); // the cancelCallback is never called for ref.on but ref.once works? @@ -134,7 +135,7 @@ describe('database().ref().on()', function () { // FIXME super flaky on android emulator it('subscribe to child added events', async function () { - if (Platform.ios) { + if (Platform.ios || Platform.other) { const successCallback = sinon.spy(); const cancelCallback = sinon.spy(); const ref = firebase.database().ref(`${TEST_PATH}/childAdded`); @@ -161,9 +162,15 @@ describe('database().ref().on()', function () { } }); - // FIXME super flaky on Jet - xit('subscribe to child changed events', async function () { - if (Platform.ios) { + // FIXME super flaky on Jet for ios/android + it('subscribe to child changed events', async function () { + if (Platform.other) { + this.skip('Errors on JS SDK about a missing index.'); + return; + } + this.skip('Flakey'); + return; + if (Platform.other) { const successCallback = sinon.spy(); const cancelCallback = sinon.spy(); const ref = firebase.database().ref(`${TEST_PATH}/childChanged`); @@ -195,14 +202,13 @@ describe('database().ref().on()', function () { } }); - // FIXME super flaky on jet - xit('subscribe to child removed events', async function () { + it('subscribe to child removed events', async function () { const successCallback = sinon.spy(); const cancelCallback = sinon.spy(); const ref = firebase.database().ref(`${TEST_PATH}/childRemoved`); const child = ref.child('removeme'); await child.set('foo'); - + await Utils.sleep(250); ref.on( 'child_removed', $ => { @@ -212,7 +218,7 @@ describe('database().ref().on()', function () { cancelCallback(); }, ); - + await Utils.sleep(250); await child.remove(); await Utils.spyToBeCalledOnceAsync(successCallback, 5000); ref.off('child_removed'); @@ -221,6 +227,10 @@ describe('database().ref().on()', function () { }); it('subscribe to child moved events', async function () { + if (Platform.other) { + this.skip('Errors on JS SDK about a missing index.'); + return; + } const callback = sinon.spy(); const ref = firebase.database().ref(`${TEST_PATH}/childMoved`); const orderedRef = ref.orderByChild('nuggets'); diff --git a/packages/database/e2e/query/onChildMoved.e2e.js b/packages/database/e2e/query/onChildMoved.e2e.js index 3f481138a2..60ea239295 100644 --- a/packages/database/e2e/query/onChildMoved.e2e.js +++ b/packages/database/e2e/query/onChildMoved.e2e.js @@ -29,8 +29,9 @@ describe('onChildMoved', function () { }); // FIXME super flaky on ios simulator + // FIXME errors on 'Other' platforms with a missing index on 'nuggets' it('should stop listening if ListeningOptions.onlyOnce is true', async function () { - if (Platform.ios) { + if (Platform.ios || Platform.other) { this.skip(); } @@ -63,7 +64,11 @@ describe('onChildMoved', function () { callback.should.be.calledWith({ nuggets: 57 }); }); + // FIXME errors on 'Other' platforms with a missing index on 'nuggets' it('subscribe to child moved events', async function () { + if (Platform.other) { + this.skip(); + } const { getDatabase, ref, query, orderByChild, onChildMoved, set, child } = databaseModular; const callback = sinon.spy(); diff --git a/packages/database/e2e/query/onValue.e2e.js b/packages/database/e2e/query/onValue.e2e.js index 8702b4e202..5070bcd695 100644 --- a/packages/database/e2e/query/onValue.e2e.js +++ b/packages/database/e2e/query/onValue.e2e.js @@ -57,9 +57,15 @@ describe('onValue()', function () { const dbRef = ref(getDatabase(), `${TEST_PATH}/init`); const callback = sinon.spy(); - const unsubscribe = onValue(dbRef, $ => { - callback($.val()); - }); + const unsubscribe = onValue( + dbRef, + $ => { + callback($.val()); + }, + error => { + callback(error); + }, + ); const value = Date.now(); await set(dbRef, value); diff --git a/packages/database/e2e/query/once.e2e.js b/packages/database/e2e/query/once.e2e.js index 96f516d71c..d6280c355c 100644 --- a/packages/database/e2e/query/once.e2e.js +++ b/packages/database/e2e/query/once.e2e.js @@ -120,8 +120,10 @@ describe('database().ref().once()', function () { const value = Date.now(); const callback = sinon.spy(); const ref = firebase.database().ref(`${TEST_PATH}/childAdded`); - - ref.once('child_added').then($ => callback($.val())); + ref + .once('child_added') + .then($ => callback($.val())) + .catch(e => callback(e)); await ref.child('foo').set(value); await Utils.spyToBeCalledOnceAsync(callback, 5000); callback.should.be.calledWith(value); @@ -144,8 +146,8 @@ describe('database().ref().once()', function () { }); // FIXME too flaky against android in CI - it('resolves when a child is removed', async function () { - if (Platform.ios) { + xit('resolves when a child is removed', async function () { + if (Platform.ios || Platform.other) { const callbackAdd = sinon.spy(); const callbackRemove = sinon.spy(); const ref = firebase.database().ref(`${TEST_PATH}/childRemoved`); @@ -153,8 +155,10 @@ describe('database().ref().once()', function () { const child = ref.child('removeme'); await child.set('foo'); await Utils.spyToBeCalledOnceAsync(callbackAdd, 10000); - - ref.once('child_removed').then($ => callbackRemove($.val())); + ref + .once('child_removed') + .then($ => callbackRemove($.val())) + .catch(e => callback(e)); await child.remove(); await Utils.spyToBeCalledOnceAsync(callbackRemove, 10000); callbackRemove.should.be.calledWith('foo'); @@ -165,6 +169,10 @@ describe('database().ref().once()', function () { // https://github.com/firebase/firebase-js-sdk/blob/6b53e0058483c9002d2fe56119f86fc9fb96b56c/packages/database/test/order_by.test.ts#L104 it('resolves when a child is moved', async function () { + if (Platform.other) { + this.skip('Errors on JS SDK about a missing index.'); + return; + } const callback = sinon.spy(); const ref = firebase.database().ref(`${TEST_PATH}/childMoved`); const orderedRef = ref.orderByChild('nuggets'); @@ -176,8 +184,10 @@ describe('database().ref().once()', function () { tony: { nuggets: 52 }, greg: { nuggets: 52 }, }; - - orderedRef.once('child_moved').then($ => callback($.val())); + orderedRef + .once('child_moved') + .then($ => callback($.val())) + .catch(e => callback(e)); await ref.set(initial); await ref.child('greg/nuggets').set(57); await Utils.spyToBeCalledOnceAsync(callback, 5000); diff --git a/packages/database/e2e/reference/push.e2e.js b/packages/database/e2e/reference/push.e2e.js index f57bff6b5d..5dc0d73d68 100644 --- a/packages/database/e2e/reference/push.e2e.js +++ b/packages/database/e2e/reference/push.e2e.js @@ -73,17 +73,22 @@ describe('database().ref().push()', function () { it('throws if push errors', async function () { const ref = firebase.database().ref('nope'); - return ref.push('foo').catch(error => { - error.message.should.containEql("doesn't have permission to access"); - return Promise.resolve(); - }); + return ref + .push('foo') + .then(() => { + throw new Error('Did not error'); + }) + .catch(error => { + error.code.should.equal('database/permission-denied'); + return Promise.resolve(); + }); }); it('returns an error to the callback', async function () { const callback = sinon.spy(); const ref = firebase.database().ref('nope'); ref.push('foo', error => { - error.message.should.containEql("doesn't have permission to access"); + error.code.should.equal('database/permission-denied'); callback(); }); await Utils.spyToBeCalledOnceAsync(callback); @@ -134,7 +139,7 @@ describe('database().ref().push()', function () { await push(dbRef, 'foo'); return Promise.reject(new Error('Did not throw Error')); } catch (error) { - error.message.should.containEql("doesn't have permission to access"); + error.code.should.equal('database/permission-denied'); return Promise.resolve(); } }); diff --git a/packages/database/e2e/reference/transaction.e2e.js b/packages/database/e2e/reference/transaction.e2e.js index b3186e293c..3c38e4d6a2 100644 --- a/packages/database/e2e/reference/transaction.e2e.js +++ b/packages/database/e2e/reference/transaction.e2e.js @@ -60,17 +60,18 @@ describe('database().ref().transaction()', function () { } }); - // FIXME this test works in isolation but not running in the suite? - xit('updates the value via a transaction', async function () { + it('updates the value via a transaction', async function () { const ref = firebase.database().ref(`${TEST_PATH}/transactionUpdate`); - await ref.set(1); - await Utils.sleep(2000); + const beforeValue = (await ref.once('value')).val() || 0; const { committed, snapshot } = await ref.transaction(value => { + if (!value) { + return 1; + } return value + 1; }); should.equal(committed, true, 'Transaction did not commit.'); - snapshot.val().should.equal(2); + snapshot.val().should.equal(beforeValue + 1); }); it('aborts transaction if undefined returned', async function () { @@ -99,7 +100,7 @@ describe('database().ref().transaction()', function () { // FIXME flaky on android local against emulator? it('passes valid data through the callback', async function () { - if (Platform.ios) { + if (Platform.ios || Platform.other) { const ref = firebase.database().ref(`${TEST_PATH}/transactionCallback`); await ref.set(1); @@ -129,7 +130,7 @@ describe('database().ref().transaction()', function () { // FIXME flaky on android local against emulator? it('throws when an error occurs', async function () { - if (Platform.ios) { + if (Platform.ios || Platform.other) { const ref = firebase.database().ref('nope'); try { @@ -138,9 +139,7 @@ describe('database().ref().transaction()', function () { }); return Promise.reject(new Error('Did not throw error.')); } catch (error) { - error.message.should.containEql( - "Client doesn't have permission to access the desired data", - ); + error.message.should.containEql('permission'); return Promise.resolve(); } } else { @@ -150,7 +149,7 @@ describe('database().ref().transaction()', function () { // FIXME flaky on android in CI? works most of the time... it('passes error back to the callback', async function () { - if (Platform.ios || !global.isCI) { + if (Platform.ios || !global.isCI || Platform.other) { const ref = firebase.database().ref('nope'); return new Promise((resolve, reject) => { @@ -168,9 +167,7 @@ describe('database().ref().transaction()', function () { return reject(new Error('Transaction should not have committed')); } - error.message.should.containEql( - "Client doesn't have permission to access the desired data", - ); + error.message.should.containEql('permission'); return resolve(); }, ) @@ -185,6 +182,7 @@ describe('database().ref().transaction()', function () { it('sets a value if one does not exist', async function () { const ref = firebase.database().ref(`${TEST_PATH}/transactionCreate`); + await ref.remove(); const value = Date.now(); return new Promise((resolve, reject) => { @@ -242,19 +240,19 @@ describe('database().ref().transaction()', function () { } }); - // FIXME this test works in isolation but not running in the suite? - xit('updates the value via a transaction', async function () { - const { getDatabase, set, ref, runTransaction } = databaseModular; - + it('updates the value via a transaction', async function () { + const { getDatabase, get, ref, runTransaction } = databaseModular; const dbRef = ref(getDatabase(), `${TEST_PATH}/transactionUpdate`); - await set(dbRef, 1); - await Utils.sleep(2000); + const beforeValue = (await get(dbRef)).val() || 0; const { committed, snapshot } = await runTransaction(dbRef, value => { + if (!value) { + return 1; + } return value + 1; }); should.equal(committed, true, 'Transaction did not commit.'); - snapshot.val().should.equal(2); + snapshot.val().should.equal(beforeValue + 1); }); it('aborts transaction if undefined returned', async function () { @@ -276,7 +274,7 @@ describe('database().ref().transaction()', function () { // FIXME flaky on android local against emulator? it('throws when an error occurs', async function () { - if (Platform.ios) { + if (Platform.ios || Platform.other) { const { getDatabase, ref, runTransaction } = databaseModular; const dbRef = ref(getDatabase(), 'nope'); @@ -287,9 +285,7 @@ describe('database().ref().transaction()', function () { }); return Promise.reject(new Error('Did not throw error.')); } catch (error) { - error.message.should.containEql( - "Client doesn't have permission to access the desired data", - ); + error.message.should.containEql('permission'); return Promise.resolve(); } } else { @@ -297,11 +293,12 @@ describe('database().ref().transaction()', function () { } }); - // FIXME runs in isolation but not in suite. Crashes on iOS, and gets stuck on Android. - xit('sets a value if one does not exist', async function () { + it('sets a value if one does not exist', async function () { const { getDatabase, ref, runTransaction } = databaseModular; const dbRef = ref(getDatabase(), `${TEST_PATH}/transactionCreate`); + await dbRef.remove(); + const value = Date.now(); const { committed, snapshot } = await runTransaction(dbRef, $ => { diff --git a/packages/database/lib/DatabaseSyncTree.js b/packages/database/lib/DatabaseSyncTree.js index f53065e6f1..a0947c2fc8 100644 --- a/packages/database/lib/DatabaseSyncTree.js +++ b/packages/database/lib/DatabaseSyncTree.js @@ -16,9 +16,9 @@ */ import { isString } from '@react-native-firebase/app/lib/common'; +import { getReactNativeModule } from '@react-native-firebase/app/lib/internal/nativeModule'; import NativeError from '@react-native-firebase/app/lib/internal/NativeFirebaseError'; import SharedEventEmitter from '@react-native-firebase/app/lib/internal/SharedEventEmitter'; -import { NativeModules } from 'react-native'; import DatabaseDataSnapshot from './DatabaseDataSnapshot'; class DatabaseSyncTree { @@ -39,7 +39,7 @@ class DatabaseSyncTree { } get native() { - return NativeModules.RNFBDatabaseQueryModule; + return getReactNativeModule('RNFBDatabaseQueryModule'); } // from upstream EventEmitter: initialize registrations for an emitter key diff --git a/packages/database/lib/DatabaseTransaction.js b/packages/database/lib/DatabaseTransaction.js index d85a0104ff..03b0b652fd 100644 --- a/packages/database/lib/DatabaseTransaction.js +++ b/packages/database/lib/DatabaseTransaction.js @@ -16,6 +16,7 @@ */ import NativeError from '@react-native-firebase/app/lib/internal/NativeFirebaseError'; +import { isOther } from '@react-native-firebase/app/lib/common'; let transactionId = 0; @@ -59,7 +60,11 @@ export default class DatabaseTransaction { started: true, }; - this._database.native.transactionStart(reference.path, id, applyLocally); + if (isOther) { + this._database.native.transactionStart(reference.path, id, applyLocally, transactionUpdater); + } else { + this._database.native.transactionStart(reference.path, id, applyLocally); + } } /** diff --git a/packages/database/lib/index.js b/packages/database/lib/index.js index fd21e16f55..1cb869d02d 100644 --- a/packages/database/lib/index.js +++ b/packages/database/lib/index.js @@ -21,10 +21,12 @@ import { FirebaseModule, getFirebaseRoot, } from '@react-native-firebase/app/lib/internal'; +import { setReactNativeModule } from '@react-native-firebase/app/lib/internal/nativeModule'; import DatabaseReference from './DatabaseReference'; import DatabaseStatics from './DatabaseStatics'; import DatabaseTransaction from './DatabaseTransaction'; import version from './version'; +import fallBackModule from './web/RNFBDatabaseModule'; const namespace = 'database'; @@ -222,3 +224,7 @@ export * from './modular'; // database().X(...); // firebase.database().X(...); export const firebase = getFirebaseRoot(); + +for (let i = 0; i < nativeModuleName.length; i++) { + setReactNativeModule(nativeModuleName[i], fallBackModule); +} diff --git a/packages/database/lib/web/RNFBDatabaseModule.android.js b/packages/database/lib/web/RNFBDatabaseModule.android.js new file mode 100644 index 0000000000..af77c859b1 --- /dev/null +++ b/packages/database/lib/web/RNFBDatabaseModule.android.js @@ -0,0 +1,2 @@ +// No-op for android. +export default {}; diff --git a/packages/database/lib/web/RNFBDatabaseModule.ios.js b/packages/database/lib/web/RNFBDatabaseModule.ios.js new file mode 100644 index 0000000000..a3429ada0e --- /dev/null +++ b/packages/database/lib/web/RNFBDatabaseModule.ios.js @@ -0,0 +1,2 @@ +// No-op for ios. +export default {}; diff --git a/packages/database/lib/web/RNFBDatabaseModule.js b/packages/database/lib/web/RNFBDatabaseModule.js new file mode 100644 index 0000000000..ded1a284b9 --- /dev/null +++ b/packages/database/lib/web/RNFBDatabaseModule.js @@ -0,0 +1,545 @@ +import { + getApp, + getDatabase, + connectDatabaseEmulator, + enableLogging, + goOnline, + goOffline, + ref, + set, + update, + setWithPriority, + remove, + setPriority, + onDisconnect, + onValue, + onChildAdded, + onChildChanged, + onChildMoved, + onChildRemoved, + runTransaction, +} from '@react-native-firebase/app/lib/internal/web/firebaseDatabase'; +import { guard, getWebError, emitEvent } from '@react-native-firebase/app/lib/internal/web/utils'; +import { getQueryInstance } from './query'; + +// Converts a DataSnapshot to an object. +function snapshotToObject(snapshot) { + const childKeys = []; + + if (snapshot.hasChildren()) { + snapshot.forEach(childSnapshot => { + childKeys.push(childSnapshot.key); + }); + } + + return { + key: snapshot.key, + exists: snapshot.exists(), + hasChildren: snapshot.hasChildren(), + childrenCount: snapshot.size, + childKeys, + priority: snapshot.priority, + value: snapshot.val(), + }; +} + +function getDatabaseWebError(error) { + // Possible to override messages/codes here if necessary. + return getWebError(error); +} + +// Converts a DataSnapshot and previous child name to an object. +function snapshotWithPreviousChildToObject(snapshot, previousChildName) { + return { + snapshot: snapshotToObject(snapshot), + previousChildName, + }; +} + +const appInstances = {}; +const databaseInstances = {}; +const onDisconnectRef = {}; +const listeners = {}; +const emulatorForApp = {}; + +function getCachedAppInstance(appName) { + return (appInstances[appName] ??= getApp(appName)); +} + +// Returns a cached Database instance. +function getCachedDatabaseInstance(appName, dbURL) { + let instance = databaseInstances[`${appName}|${dbURL}`]; + if (!instance) { + instance = getDatabase(getCachedAppInstance(appName), dbURL); + // Relying on internals here so need to be careful between SDK versions. + if (emulatorForApp[appName] && !instance._instanceStarted) { + const { host, port } = emulatorForApp[appName]; + connectDatabaseEmulator(instance, host, port); + emulatorForApp[appName].connected = true; + } + } + return instance; +} + +// Returns a cached onDisconnect instance. +function getCachedOnDisconnectInstance(ref) { + return (onDisconnectRef[ref.key] ??= onDisconnect(ref)); +} + +export default { + /** + * Reconnects to the server. + * @param {string} appName - The app name. + * @param {string} dbURL - The database URL. + * @returns {Promise} + */ + goOnline(appName, dbURL) { + return guard(async () => { + const db = getCachedDatabaseInstance(appName, dbURL); + goOnline(db); + }); + }, + + /** + * Disconnects from the server. + * @param {string} appName - The app name. + * @param {string} dbURL - The database URL. + * @returns {Promise} + */ + goOffline(appName, dbURL) { + return guard(async () => { + const db = getCachedDatabaseInstance(appName, dbURL); + goOffline(db); + }); + }, + + setPersistenceEnabled() { + if (__DEV__) { + // eslint-disable-next-line no-console + console.warn( + 'The Firebase Database `setPersistenceEnabled` method is not available in the this environment.', + ); + } + return Promise.resolve(); + }, + + /** + * Sets the logging enabled state. + * @param {string} appName - The app name, not used. + * @param {string} dbURL - The database URL, not used. + * @param {boolean} enabled - The logging enabled state. + */ + setLoggingEnabled(app, dbURL, enabled) { + return guard(async () => { + enableLogging(enabled); + }); + }, + + setPersistenceCacheSizeBytes() { + // no-op on other platforms + return Promise.resolve(); + }, + + /** + * Connects to the Firebase database emulator. + * @param {string} appName - The app name. + * @param {string} dbURL - The database URL. + * @param {string} host - The emulator host. + * @param {number} port - The emulator + * @returns {Promise} + */ + useEmulator(appName, dbURL, host, port) { + return guard(async () => { + const db = getCachedDatabaseInstance(appName, dbURL); + connectDatabaseEmulator(db, host, port); + emulatorForApp[appName] = { host, port }; + }); + }, + + /** + * Reference + */ + + /** + * Sets a value at the specified path. + * @param {string} appName - The app name. + * @param {string} dbURL - The database URL. + * @param {string} path - The path. + * @param {object} props - The properties + * @returns {Promise} + */ + set(appName, dbURL, path, props) { + return guard(async () => { + const db = getCachedDatabaseInstance(appName, dbURL); + const dbRef = ref(db, path); + const value = props.value; + await set(dbRef, value); + }); + }, + + /** + * Updates the specified path with the provided values. + * @param {string} appName - The app name. + * @param {string} dbURL - The database URL. + * @param {string} path - The path. + * @param {object} props - The properties + * @returns {Promise} + */ + update(appName, dbURL, path, props) { + return guard(async () => { + const db = getCachedDatabaseInstance(appName, dbURL); + const dbRef = ref(db, path); + const values = props.values; + await update(dbRef, values); + }); + }, + + /** + * Sets a value at the specified path with a priority. + * @param {string} appName - The app name. + * @param {string} dbURL - The database URL. + * @param {string} path - The path. + * @param {object} props - The properties, including value and priority. + * @returns {Promise} + */ + setWithPriority(appName, dbURL, path, props) { + return guard(async () => { + const db = getCachedDatabaseInstance(appName, dbURL); + const dbRef = ref(db, path); + const value = props.value; + const priority = props.priority; + await setWithPriority(dbRef, value, priority); + }); + }, + + /** + * Removes the nodd at the specified path. + * @param {string} appName - The app name. + * @param {string} dbURL - The database URL. + * @param {string} path - The path. + * @returns {Promise} + */ + remove(appName, dbURL, path) { + return guard(async () => { + const db = getCachedDatabaseInstance(appName, dbURL); + const dbRef = ref(db, path); + await remove(dbRef); + }); + }, + + /** + * Sets the priority of the node at the specified path. + * @param {string} appName - The app name. + * @param {string} dbURL - The database URL. + * @param {string} path - The path. + * @param {object} props - The properties, including priority. + * @returns {Promise} + */ + setPriority(appName, dbURL, path, props) { + return guard(async () => { + const db = getCachedDatabaseInstance(appName, dbURL); + const dbRef = ref(db, path); + const priority = props.priority; + await setPriority(dbRef, priority); + }); + }, + + /** + * Query + */ + + /** + * Listens for data changes at the specified path once. + * @param {string} appName - The app name. + * @param {string} dbURL - The database URL. + * @param {string} path - The path. + * @param {object} modifiers - The modifiers. + * @param {string} eventType - The event type. + * @returns {Promise} + */ + once(appName, dbURL, path, modifiers, eventType) { + return guard(async () => { + const db = getCachedDatabaseInstance(appName, dbURL); + const dbRef = ref(db, path); + const queryRef = getQueryInstance(dbRef, modifiers); + + if (eventType === 'value') { + const snapshot = await new Promise((resolve, reject) => { + onValue(queryRef, resolve, reject, { onlyOnce: true }); + }); + + return snapshotToObject(snapshot); + } else { + let fn = null; + + if (eventType === 'child_added') { + fn = onChildAdded; + } else if (eventType === 'child_changed') { + fn = onChildChanged; + } else if (eventType === 'child_removed') { + fn = onChildRemoved; + } else if (eventType === 'child_moved') { + fn = onChildMoved; + } + + if (fn) { + const { snapshot, previousChildName } = await new Promise((resolve, reject) => { + fn( + queryRef, + (snapshot, previousChildName) => { + resolve({ snapshot, previousChildName }); + }, + reject, + { onlyOnce: true }, + ); + }); + + return snapshotWithPreviousChildToObject(snapshot, previousChildName); + } + } + + const snapshot = await get(dbRef, modifiers); + return snapshot; + }); + }, + + on(appName, dbURL, props) { + return guard(async () => { + const db = getCachedDatabaseInstance(appName, dbURL); + const { key, modifiers, path, eventType, registration } = props; + const { eventRegistrationKey } = registration; + const dbRef = ref(db, path); + + const queryRef = getQueryInstance(dbRef, modifiers); + + function sendEvent(data) { + const event = { + eventName: 'database_sync_event', + body: { + data, + key, + registration, + eventType, + }, + }; + + emitEvent('database_sync_event', event); + } + + function sendError(error) { + const event = { + eventName: 'database_sync_event', + body: { + key, + registration, + error: getDatabaseWebError(error), + }, + }; + + emitEvent('database_sync_event', event); + } + + let listener = null; + + // Ignore if the listener already exists. + if (listeners[eventRegistrationKey]) { + return; + } + + if (eventType === 'value') { + listener = onValue(queryRef, snapshot => sendEvent(snapshotToObject(snapshot)), sendError); + } else { + let fn = null; + + if (eventType === 'child_added') { + fn = onChildAdded; + } else if (eventType === 'child_changed') { + fn = onChildChanged; + } else if (eventType === 'child_removed') { + fn = onChildRemoved; + } else if (eventType === 'child_moved') { + fn = onChildMoved; + } + + if (fn) { + listener = fn( + queryRef, + (snapshot, previousChildName) => { + sendEvent(snapshotWithPreviousChildToObject(snapshot, previousChildName)); + }, + sendError, + ); + } + } + + listeners[eventRegistrationKey] = listener; + }); + }, + + off(queryKey, eventRegistrationKey) { + const listener = listeners[eventRegistrationKey]; + if (listener) { + listener(); + delete listeners[eventRegistrationKey]; + } + }, + + keepSynced() { + return rejectPromiseWithCodeAndMessage( + 'unsupported', + 'This operation is not supported on this environment.', + ); + }, + + /** + * OnDisconnect + */ + + /** + * Cancels the onDisconnect instance at the specified path. + * @param {string} appName - The app name. + * @param {string} dbURL - The database URL. + * @param {string} path - The path. + * @returns {Promise} + */ + onDisconnectCancel(appName, dbURL, path) { + return guard(async () => { + const db = getCachedDatabaseInstance(appName, dbURL); + const dbRef = ref(db, path); + const instance = getCachedOnDisconnectInstance(dbRef); + await instance.cancel(); + + // Delete the onDisconnect instance from the cache. + delete onDisconnectRef[dbRef.key]; + }); + }, + + /** + * Sets a value to be written to the database on disconnect. + * @param {string} appName - The app name. + * @param {string} dbURL - The database URL. + * @param {string} path - The path. + * @returns {Promise} + */ + onDisconnectRemove(appName, dbURL, path) { + return guard(async () => { + const db = getCachedDatabaseInstance(appName, dbURL); + const dbRef = ref(db, path); + const instance = getCachedOnDisconnectInstance(dbRef); + await instance.remove(); + }); + }, + + /** + * Sets a value to be written to the database on disconnect. + * @param {string} appName - The app name. + * @param {string} dbURL - The database URL. + * @param {string} path - The path. + * @param {object} props - The properties, including value. + * @returns {Promise} + */ + onDisconnectSet(appName, dbURL, path, props) { + return guard(async () => { + const db = getCachedDatabaseInstance(appName, dbURL); + const dbRef = ref(db, path); + const instance = getCachedOnDisconnectInstance(dbRef); + const value = props.value; + await instance.set(value); + }); + }, + + /** + * Sets a value to be written to the database on disconnect with a priority. + * @param {string} appName - The app name. + * @param {string} dbURL - The database URL. + * @param {string} path - The path. + * @param {object} props - The properties, including value and priority. + * @returns {Promise} + */ + onDisconnectSetWithPriority(appName, dbURL, path, props) { + return guard(async () => { + const db = getCachedDatabaseInstance(appName, dbURL); + const dbRef = ref(db, path); + const instance = getCachedOnDisconnectInstance(dbRef); + const value = props.value; + const priority = props.priority; + await instance.setWithPriority(value, priority); + }); + }, + + /** + * Updates the specified path with the provided values on disconnect. + * @param {string} appName - The app name. + * @param {string} dbURL - The database URL. + * @param {string} path - The path. + * @param {object} props - The properties, including values. + * @returns {Promise} + */ + onDisconnectUpdate(appName, dbURL, path, props) { + return guard(async () => { + const db = getCachedDatabaseInstance(appName, dbURL); + const dbRef = ref(db, path); + const instance = getCachedOnDisconnectInstance(dbRef); + const values = props.values; + await instance.update(values); + }); + }, + + /** + * Transaction + */ + + transactionStart(appName, dbURL, path, transactionId, applyLocally, userExecutor) { + return guard(async () => { + const db = getCachedDatabaseInstance(appName, dbURL); + const dbRef = ref(db, path); + + try { + const { committed, snapshot } = await runTransaction(dbRef, userExecutor, { + applyLocally, + }); + + const event = { + body: { + committed, + type: 'complete', + snapshot: snapshotToObject(snapshot), + }, + appName, + id: transactionId, + eventName: 'database_transaction_event', + }; + + emitEvent('database_transaction_event', event); + } catch (e) { + const event = { + body: { + committed: false, + type: 'error', + error: getDatabaseWebError(e), + }, + appName, + id: transactionId, + eventName: 'database_transaction_event', + }; + + emitEvent('database_transaction_event', event); + } + }); + }, + + /** + * Commits the transaction with the specified updates. + * @param {string} appName - The app name. + * @param {string} dbURL - The database URL. + * @param {string} transactionId - The transaction ID. + * @param {object} updates - The updates. + * @returns {Promise} + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async transactionTryCommit(appName, dbURL, transactionId, updates) { + // We don't need to implement this as for 'Other' platforms + // we pass the users transaction function to the Firebase JS SDK directly. + throw new Error('Not implemented'); + }, +}; diff --git a/packages/database/lib/web/query.js b/packages/database/lib/web/query.js new file mode 100644 index 0000000000..b1c70d43fb --- /dev/null +++ b/packages/database/lib/web/query.js @@ -0,0 +1,71 @@ +import { + query, + orderByKey, + orderByPriority, + orderByValue, + orderByChild, + limitToLast, + limitToFirst, + endAt, + endBefore, + startAt, + startAfter, +} from '@react-native-firebase/app/lib/internal/web/firebaseDatabase'; + +export function getQueryInstance(dbRef, modifiers) { + const constraints = []; + + for (const modifier of modifiers) { + const { type, name } = modifier; + + if (type === 'orderBy') { + switch (name) { + case 'orderByKey': + constraints.push(orderByKey()); + break; + case 'orderByPriority': + constraints.push(orderByPriority()); + break; + case 'orderByValue': + constraints.push(orderByValue()); + break; + case 'orderByChild': + constraints.push(orderByChild(modifier.key)); + break; + } + } + + if (type === 'limit') { + const { value } = modifier; + + switch (name) { + case 'limitToLast': + constraints.push(limitToLast(value)); + break; + case 'limitToFirst': + constraints.push(limitToFirst(value)); + break; + } + } + + if (type === 'filter') { + const { key, value } = modifier; + + switch (name) { + case 'endAt': + constraints.push(endAt(value, key)); + break; + case 'endBefore': + constraints.push(endBefore(value, key)); + break; + case 'startAt': + constraints.push(startAt(value, key)); + break; + case 'startAfter': + constraints.push(startAfter(value, key)); + break; + } + } + } + return query(dbRef, ...constraints); +} diff --git a/tests/.jetrc.js b/tests/.jetrc.js index 7b1a350e6a..945fb9f6bb 100644 --- a/tests/.jetrc.js +++ b/tests/.jetrc.js @@ -55,6 +55,9 @@ module.exports = { process.exit(1); } }); + macApp.on('spawn', () => { + console.log('[💻] macOS app started'); + }); return config; }, async after(config) { diff --git a/tests/app.js b/tests/app.js index 5cfbe4ebfc..8dec12b842 100644 --- a/tests/app.js +++ b/tests/app.js @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ /* * Copyright (c) 2016-present Invertase Limited & Contributors * @@ -25,6 +26,7 @@ const platformSupportedModules = []; if (Platform.other) { platformSupportedModules.push('app'); platformSupportedModules.push('functions'); + platformSupportedModules.push('database'); platformSupportedModules.push('auth'); platformSupportedModules.push('storage'); // TODO add more modules here once they are supported. @@ -57,6 +59,7 @@ ErrorUtils.setGlobalHandler((err, isFatal) => { throw err; }); +// eslint-disable-next-line @typescript-eslint/no-unused-vars function loadTests(_) { describe('React Native Firebase', function () { if (!global.RNFBDebug) {