From be1d732664c538632f657e7dcab3f7ad4e786839 Mon Sep 17 00:00:00 2001 From: Feiyang Date: Thu, 19 Aug 2021 17:36:12 -0700 Subject: [PATCH] Split database and database-compat (#5276) * compile database * pass database tests * compile and test database-compat * pass all tests * prettier * cleanup * fix lint * address comments * what is going on with ci * use correct case in import path * uppercase * rename * fix component name --- packages-exp/app-exp/src/constants.ts | 2 +- packages/database-compat/.eslintrc.js | 64 ++ packages/database-compat/README.md | 5 + packages/database-compat/karma.conf.js | 34 + packages/database-compat/package.json | 37 + .../rollup.config.js} | 34 +- packages/database-compat/src/api/Database.ts | 123 +++ packages/database-compat/src/api/Reference.ts | 790 +++++++++++++++++ .../src/api/TransactionResult.ts | 0 .../src/api/internal.ts | 65 +- .../src/api/onDisconnect.ts | 19 +- .../src}/index.node.ts | 24 +- .../compat => database-compat/src}/index.ts | 25 +- packages/database-compat/src/util/util.ts | 8 + .../database-compat/src/util/validation.ts | 42 + .../test/browser/crawler_support.test.ts | 2 +- .../test/database.test.ts | 4 +- .../test/datasnapshot.test.ts | 7 +- .../test/helpers/events.ts | 2 +- packages/database-compat/test/helpers/util.ts | 177 ++++ .../test/info.test.ts | 2 +- .../test/order.test.ts | 2 +- .../test/order_by.test.ts | 2 +- .../test/promise.test.ts | 0 .../test/query.test.ts | 16 +- .../test/servervalues.test.ts | 0 .../test/transaction.test.ts | 13 +- packages/database-compat/tsconfig.json | 11 + packages/database-types/index.d.ts | 2 +- packages/database/.npmignore | 10 - packages/database/api-extractor.json | 8 +- packages/database/compat/package.json | 13 - packages/database/exp/package.json | 9 - packages/database/index.node.ts | 158 ---- packages/database/index.ts | 102 --- packages/database/karma.conf.js | 2 - packages/database/package.json | 24 +- packages/database/rollup.config.compat.js | 144 --- packages/database/rollup.config.js | 53 +- packages/database/{exp => src}/api.ts | 40 +- packages/database/src/api/Database.ts | 507 ++++++++--- .../database/src/{exp => api}/OnDisconnect.ts | 0 packages/database/src/api/Reference.ts | 829 ++---------------- .../src/{exp => api}/Reference_impl.ts | 0 .../database/src/{exp => api}/ServerValue.ts | 0 .../database/src/{exp => api}/Transaction.ts | 0 packages/database/src/api/test_access.ts | 11 +- packages/database/src/core/SyncPoint.ts | 2 +- packages/database/src/core/SyncTree.ts | 9 +- .../database/src/core/snap/ChildrenNode.ts | 2 +- packages/database/src/core/snap/childSet.ts | 12 +- .../database/src/core/util/ImmutableTree.ts | 9 +- packages/database/src/core/util/SortedMap.ts | 4 +- .../database/src/core/util/libs/parser.ts | 4 +- packages/database/src/core/util/validation.ts | 47 +- packages/database/src/core/version.ts | 5 +- packages/database/src/core/view/Event.ts | 2 +- .../src/core/view/EventRegistration.ts | 4 +- .../database/src/core/view/QueryParams.ts | 2 + .../database/src/core/view/ViewProcessor.ts | 13 +- packages/database/src/exp/Database.ts | 408 --------- packages/database/src/exp/Reference.ts | 135 --- packages/database/{exp => src}/index.node.ts | 0 packages/database/{exp => src}/index.ts | 7 + .../src/realtime/BrowserPollConnection.ts | 31 +- packages/database/src/realtime/Constants.ts | 3 +- packages/database/{exp => src}/register.ts | 9 +- .../database/test/exp/integration.test.ts | 4 +- packages/database/test/helpers/util.ts | 176 +--- 69 files changed, 2001 insertions(+), 2309 deletions(-) create mode 100644 packages/database-compat/.eslintrc.js create mode 100644 packages/database-compat/README.md create mode 100644 packages/database-compat/karma.conf.js create mode 100644 packages/database-compat/package.json rename packages/{database/rollup.config.exp.js => database-compat/rollup.config.js} (75%) create mode 100644 packages/database-compat/src/api/Database.ts create mode 100644 packages/database-compat/src/api/Reference.ts rename packages/{database => database-compat}/src/api/TransactionResult.ts (100%) rename packages/{database => database-compat}/src/api/internal.ts (55%) rename packages/{database => database-compat}/src/api/onDisconnect.ts (85%) rename packages/{database/compat => database-compat/src}/index.node.ts (88%) rename packages/{database/compat => database-compat/src}/index.ts (81%) create mode 100644 packages/database-compat/src/util/util.ts create mode 100644 packages/database-compat/src/util/validation.ts rename packages/{database => database-compat}/test/browser/crawler_support.test.ts (98%) rename packages/{database => database-compat}/test/database.test.ts (99%) rename packages/{database => database-compat}/test/datasnapshot.test.ts (97%) rename packages/{database => database-compat}/test/helpers/events.ts (99%) create mode 100644 packages/database-compat/test/helpers/util.ts rename packages/{database => database-compat}/test/info.test.ts (98%) rename packages/{database => database-compat}/test/order.test.ts (99%) rename packages/{database => database-compat}/test/order_by.test.ts (99%) rename packages/{database => database-compat}/test/promise.test.ts (100%) rename packages/{database => database-compat}/test/query.test.ts (99%) rename packages/{database => database-compat}/test/servervalues.test.ts (100%) rename packages/{database => database-compat}/test/transaction.test.ts (99%) create mode 100644 packages/database-compat/tsconfig.json delete mode 100644 packages/database/.npmignore delete mode 100644 packages/database/compat/package.json delete mode 100644 packages/database/exp/package.json delete mode 100644 packages/database/index.node.ts delete mode 100644 packages/database/index.ts delete mode 100644 packages/database/rollup.config.compat.js rename packages/database/{exp => src}/api.ts (58%) rename packages/database/src/{exp => api}/OnDisconnect.ts (100%) rename packages/database/src/{exp => api}/Reference_impl.ts (100%) rename packages/database/src/{exp => api}/ServerValue.ts (100%) rename packages/database/src/{exp => api}/Transaction.ts (100%) delete mode 100644 packages/database/src/exp/Database.ts delete mode 100644 packages/database/src/exp/Reference.ts rename packages/database/{exp => src}/index.node.ts (100%) rename packages/database/{exp => src}/index.ts (83%) rename packages/database/{exp => src}/register.ts (88%) diff --git a/packages-exp/app-exp/src/constants.ts b/packages-exp/app-exp/src/constants.ts index 34feb88f791..75ab259944b 100644 --- a/packages-exp/app-exp/src/constants.ts +++ b/packages-exp/app-exp/src/constants.ts @@ -24,7 +24,7 @@ import { name as appCheckName } from '../../../packages-exp/app-check-exp/packag import { name as authName } from '../../../packages-exp/auth-exp/package.json'; import { name as authCompatName } from '../../../packages-exp/auth-compat-exp/package.json'; import { name as databaseName } from '../../../packages/database/package.json'; -import { name as databaseCompatName } from '../../../packages/database/compat/package.json'; +import { name as databaseCompatName } from '../../../packages/database-compat/package.json'; import { name as functionsName } from '../../../packages-exp/functions-exp/package.json'; import { name as functionsCompatName } from '../../../packages-exp/functions-compat/package.json'; import { name as installationsName } from '../../../packages-exp/installations-exp/package.json'; diff --git a/packages/database-compat/.eslintrc.js b/packages/database-compat/.eslintrc.js new file mode 100644 index 00000000000..3a04a986c41 --- /dev/null +++ b/packages/database-compat/.eslintrc.js @@ -0,0 +1,64 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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. + */ + +module.exports = { + extends: '../../config/.eslintrc.js', + parserOptions: { + project: 'tsconfig.json', + // to make vscode-eslint work with monorepo + // https://github.com/typescript-eslint/typescript-eslint/issues/251#issuecomment-463943250 + tsconfigRootDir: __dirname + }, + rules: { + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/no-floating-promises': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + 'no-restricted-properties': 'off', + 'no-restricted-globals': 'off', + 'no-throw-literal': 'off', + 'id-blacklist': 'off', + 'import/order': [ + 'error', + { + 'groups': [ + 'builtin', + 'external', + 'internal', + 'parent', + 'sibling', + 'index' + ], + 'newlines-between': 'always', + 'alphabetize': { 'order': 'asc', 'caseInsensitive': true } + } + ] + }, + overrides: [ + { + files: ['**/*.d.ts'], + rules: { + '@typescript-eslint/no-explicit-any': 'off' + } + }, + { + files: ['scripts/*.ts'], + rules: { + 'import/no-extraneous-dependencies': 'off' + } + } + ] +}; diff --git a/packages/database-compat/README.md b/packages/database-compat/README.md new file mode 100644 index 00000000000..656ab8397b9 --- /dev/null +++ b/packages/database-compat/README.md @@ -0,0 +1,5 @@ +# @firebase/database-compat + +This is the compatibility layer for the Firebase Realtime Database component of the Firebase JS SDK. + +**This package is not intended for direct usage, and should only be used via the officially supported [firebase](https://www.npmjs.com/package/firebase) package.** diff --git a/packages/database-compat/karma.conf.js b/packages/database-compat/karma.conf.js new file mode 100644 index 00000000000..d51e08d046e --- /dev/null +++ b/packages/database-compat/karma.conf.js @@ -0,0 +1,34 @@ +/** + * @license + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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 karmaBase = require('../../config/karma.base'); + +const files = [`test/**/*.test.ts`]; + +module.exports = function (config) { + const karmaConfig = Object.assign({}, karmaBase, { + // files to load into karma + files: files, + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['mocha'] + }); + + config.set(karmaConfig); +}; + +module.exports.files = files; diff --git a/packages/database-compat/package.json b/packages/database-compat/package.json new file mode 100644 index 00000000000..fc7c394e81e --- /dev/null +++ b/packages/database-compat/package.json @@ -0,0 +1,37 @@ +{ + "name": "@firebase/database-compat", + "version": "0.0.900", + "description": "The Realtime Database component of the Firebase JS SDK.", + "author": "Firebase (https://firebase.google.com/)", + "main": "dist/index.js", + "browser": "dist/index.esm2017.js", + "module": "dist/index.esm2017.js", + "esm5": "dist/index.esm5.js", + "license": "Apache-2.0", + "typings": "dist/database-compat/src/index.d.ts", + "scripts": { + "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", + "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", + "prettier": "prettier --write '*.js' '*.ts' '@(src|test)/**/*.ts'", + "build": "rollup -c rollup.config.js", + "build:release": "yarn build && yarn add-compat-overloads", + "build:deps": "lerna run --scope @firebase/database-compat --include-dependencies build", + "dev": "rollup -c -w", + "test": "run-p lint test:browser test:node", + "test:ci": "node ../../scripts/run_tests_in_ci.js -s test", + "test:browser": "karma start --single-run", + "test:node": "TS_NODE_FILES=true TS_NODE_CACHE=NO TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --reporter lcovonly -- mocha 'test/{,!(browser)/**/}*.test.ts' --file src/index.node.ts --config ../../config/mocharc.node.js", + "add-compat-overloads": "ts-node-script ../../scripts/exp/create-overloads.ts -i ../database/dist/public.d.ts -o dist/database-compat/src/index.d.ts -a -r FirebaseDatabase:types.FirebaseDatabase -r Query:types.Query -r Reference:types.Reference -r FirebaseApp:FirebaseAppCompat --moduleToEnhance @firebase/database" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + }, + "dependencies": { + "@firebase/database": "0.11.0", + "@firebase/database-types": "0.8.0", + "@firebase/logger": "0.2.6", + "@firebase/util": "1.3.0", + "@firebase/component": "0.5.6", + "tslib": "^2.1.0" + } +} \ No newline at end of file diff --git a/packages/database/rollup.config.exp.js b/packages/database-compat/rollup.config.js similarity index 75% rename from packages/database/rollup.config.exp.js rename to packages/database-compat/rollup.config.js index bc913a6c987..14afce274e0 100644 --- a/packages/database/rollup.config.exp.js +++ b/packages/database-compat/rollup.config.js @@ -1,6 +1,6 @@ /** * @license - * Copyright 2018 Google LLC + * Copyright 2021 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,16 +18,12 @@ import json from '@rollup/plugin-json'; import typescriptPlugin from 'rollup-plugin-typescript2'; import typescript from 'typescript'; -import path from 'path'; -import { importPathTransformer } from '../../scripts/exp/ts-transform-import-path'; -import expPkg from './exp/package.json'; import pkg from './package.json'; -const deps = [ - ...Object.keys(Object.assign({}, pkg.peerDependencies, pkg.dependencies)), - '@firebase/app' -]; +const deps = Object.keys( + Object.assign({}, pkg.peerDependencies, pkg.dependencies) +); function onWarn(warning, defaultWarn) { if (warning.code === 'CIRCULAR_DEPENDENCY') { @@ -42,8 +38,7 @@ function onWarn(warning, defaultWarn) { const es5BuildPlugins = [ typescriptPlugin({ typescript, - abortOnError: false, - transformers: [importPathTransformer] + abortOnError: false }), json() ]; @@ -53,9 +48,13 @@ const es5Builds = [ * Node.js Build */ { - input: 'exp/index.node.ts', + input: 'src/index.node.ts', output: [ - { file: path.resolve('exp', expPkg.main), format: 'cjs', sourcemap: true } + { + file: pkg.main, + format: 'cjs', + sourcemap: true + } ], plugins: es5BuildPlugins, treeshake: { @@ -68,10 +67,10 @@ const es5Builds = [ * Browser Builds */ { - input: 'exp/index.ts', + input: 'src/index.ts', output: [ { - file: path.resolve('exp', expPkg.esm5), + file: pkg.esm5, format: 'es', sourcemap: true } @@ -96,8 +95,7 @@ const es2017BuildPlugins = [ target: 'es2017' } }, - abortOnError: false, - transformers: [importPathTransformer] + abortOnError: false }), json({ preferConst: true }) ]; @@ -107,10 +105,10 @@ const es2017Builds = [ * Browser Build */ { - input: 'exp/index.ts', + input: 'src/index.ts', output: [ { - file: path.resolve('exp', expPkg.browser), + file: pkg.browser, format: 'es', sourcemap: true } diff --git a/packages/database-compat/src/api/Database.ts b/packages/database-compat/src/api/Database.ts new file mode 100644 index 00000000000..a00e985219e --- /dev/null +++ b/packages/database-compat/src/api/Database.ts @@ -0,0 +1,123 @@ +/** + * @license + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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. + */ +// eslint-disable-next-line import/no-extraneous-dependencies + +import { FirebaseApp } from '@firebase/app-types'; +import { FirebaseService } from '@firebase/app-types/private'; +import { + goOnline, + connectDatabaseEmulator, + goOffline, + ref, + refFromURL, + increment, + serverTimestamp, + Database as ModularDatabase +} from '@firebase/database'; +import { + validateArgCount, + Compat, + EmulatorMockTokenOptions +} from '@firebase/util'; + + +import { Reference } from './Reference'; + +/** + * Class representing a firebase database. + */ +export class Database implements FirebaseService, Compat { + static readonly ServerValue = { + TIMESTAMP: serverTimestamp(), + increment: (delta: number) => increment(delta) + }; + + /** + * The constructor should not be called by users of our public API. + */ + constructor(readonly _delegate: ModularDatabase, readonly app: FirebaseApp) {} + + INTERNAL = { + delete: () => this._delegate._delete() + }; + + /** + * Modify this instance to communicate with the Realtime Database emulator. + * + *

Note: This method must be called before performing any other operation. + * + * @param host - the emulator host (ex: localhost) + * @param port - the emulator port (ex: 8080) + * @param options.mockUserToken - the mock auth token to use for unit testing Security Rules + */ + useEmulator( + host: string, + port: number, + options: { + mockUserToken?: EmulatorMockTokenOptions; + } = {} + ): void { + connectDatabaseEmulator(this._delegate, host, port, options); + } + + /** + * Returns a reference to the root or to the path specified in the provided + * argument. + * + * @param path - The relative string path or an existing Reference to a database + * location. + * @throws If a Reference is provided, throws if it does not belong to the + * same project. + * @returns Firebase reference. + */ + ref(path?: string): Reference; + ref(path?: Reference): Reference; + ref(path?: string | Reference): Reference { + validateArgCount('database.ref', 0, 1, arguments.length); + if (path instanceof Reference) { + const childRef = refFromURL(this._delegate, path.toString()); + return new Reference(this, childRef); + } else { + const childRef = ref(this._delegate, path); + return new Reference(this, childRef); + } + } + + /** + * Returns a reference to the root or the path specified in url. + * We throw a exception if the url is not in the same domain as the + * current repo. + * @returns Firebase reference. + */ + refFromURL(url: string): Reference { + const apiName = 'database.refFromURL'; + validateArgCount(apiName, 1, 1, arguments.length); + const childRef = refFromURL(this._delegate, url); + return new Reference(this, childRef); + } + + // Make individual repo go offline. + goOffline(): void { + validateArgCount('database.goOffline', 0, 0, arguments.length); + return goOffline(this._delegate); + } + + goOnline(): void { + validateArgCount('database.goOnline', 0, 0, arguments.length); + return goOnline(this._delegate); + } +} diff --git a/packages/database-compat/src/api/Reference.ts b/packages/database-compat/src/api/Reference.ts new file mode 100644 index 00000000000..e0ecfaee049 --- /dev/null +++ b/packages/database-compat/src/api/Reference.ts @@ -0,0 +1,790 @@ +/** + * @license + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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. + */ + +import { + OnDisconnect as ModularOnDisconnect, + off, + onChildAdded, + onChildChanged, + onChildMoved, + onChildRemoved, + onValue, + EventType, + limitToFirst, + query, + limitToLast, + orderByChild, + orderByKey, + orderByValue, + orderByPriority, + startAt, + startAfter, + endAt, + endBefore, + equalTo, + get, + set, + update, + setWithPriority, + remove, + setPriority, + push, + runTransaction, + child, + DataSnapshot as ModularDataSnapshot, + Query as ExpQuery, + DatabaseReference as ModularReference, + _QueryImpl, + _ReferenceImpl, + _validatePathString, + _validateWritablePath, + _UserCallback, + _QueryParams +} from '@firebase/database'; +import { + Compat, + Deferred, + errorPrefix, + validateArgCount, + validateCallback, + validateContextObject +} from '@firebase/util'; + +import { warn } from '../util/util'; +import { validateBoolean, validateEventType } from '../util/validation'; + +import { Database } from './Database'; +import { OnDisconnect } from './onDisconnect'; +import { TransactionResult } from './TransactionResult'; + +/** + * Class representing a firebase data snapshot. It wraps a SnapshotNode and + * surfaces the public methods (val, forEach, etc.) we want to expose. + */ +export class DataSnapshot implements Compat { + constructor( + readonly _database: Database, + readonly _delegate: ModularDataSnapshot + ) {} + + /** + * Retrieves the snapshot contents as JSON. Returns null if the snapshot is + * empty. + * + * @returns JSON representation of the DataSnapshot contents, or null if empty. + */ + val(): unknown { + validateArgCount('DataSnapshot.val', 0, 0, arguments.length); + return this._delegate.val(); + } + + /** + * Returns the snapshot contents as JSON, including priorities of node. Suitable for exporting + * the entire node contents. + * @returns JSON representation of the DataSnapshot contents, or null if empty. + */ + exportVal(): unknown { + validateArgCount('DataSnapshot.exportVal', 0, 0, arguments.length); + return this._delegate.exportVal(); + } + + // Do not create public documentation. This is intended to make JSON serialization work but is otherwise unnecessary + // for end-users + toJSON(): unknown { + // Optional spacer argument is unnecessary because we're depending on recursion rather than stringifying the content + validateArgCount('DataSnapshot.toJSON', 0, 1, arguments.length); + return this._delegate.toJSON(); + } + + /** + * Returns whether the snapshot contains a non-null value. + * + * @returns Whether the snapshot contains a non-null value, or is empty. + */ + exists(): boolean { + validateArgCount('DataSnapshot.exists', 0, 0, arguments.length); + return this._delegate.exists(); + } + + /** + * Returns a DataSnapshot of the specified child node's contents. + * + * @param path - Path to a child. + * @returns DataSnapshot for child node. + */ + child(path: string): DataSnapshot { + validateArgCount('DataSnapshot.child', 0, 1, arguments.length); + // Ensure the childPath is a string (can be a number) + path = String(path); + _validatePathString('DataSnapshot.child', 'path', path, false); + return new DataSnapshot(this._database, this._delegate.child(path)); + } + + /** + * Returns whether the snapshot contains a child at the specified path. + * + * @param path - Path to a child. + * @returns Whether the child exists. + */ + hasChild(path: string): boolean { + validateArgCount('DataSnapshot.hasChild', 1, 1, arguments.length); + _validatePathString('DataSnapshot.hasChild', 'path', path, false); + return this._delegate.hasChild(path); + } + + /** + * Returns the priority of the object, or null if no priority was set. + * + * @returns The priority. + */ + getPriority(): string | number | null { + validateArgCount('DataSnapshot.getPriority', 0, 0, arguments.length); + return this._delegate.priority; + } + + /** + * Iterates through child nodes and calls the specified action for each one. + * + * @param action - Callback function to be called + * for each child. + * @returns True if forEach was canceled by action returning true for + * one of the child nodes. + */ + forEach(action: (snapshot: DataSnapshot) => boolean | void): boolean { + validateArgCount('DataSnapshot.forEach', 1, 1, arguments.length); + validateCallback('DataSnapshot.forEach', 'action', action, false); + return this._delegate.forEach(expDataSnapshot => + action(new DataSnapshot(this._database, expDataSnapshot)) + ); + } + + /** + * Returns whether this DataSnapshot has children. + * @returns True if the DataSnapshot contains 1 or more child nodes. + */ + hasChildren(): boolean { + validateArgCount('DataSnapshot.hasChildren', 0, 0, arguments.length); + return this._delegate.hasChildren(); + } + + get key() { + return this._delegate.key; + } + + /** + * Returns the number of children for this DataSnapshot. + * @returns The number of children that this DataSnapshot contains. + */ + numChildren(): number { + validateArgCount('DataSnapshot.numChildren', 0, 0, arguments.length); + return this._delegate.size; + } + + /** + * @returns The Firebase reference for the location this snapshot's data came + * from. + */ + getRef(): Reference { + validateArgCount('DataSnapshot.ref', 0, 0, arguments.length); + return new Reference(this._database, this._delegate.ref); + } + + get ref(): Reference { + return this.getRef(); + } +} + +export interface SnapshotCallback { + (dataSnapshot: DataSnapshot, previousChildName?: string | null): unknown; +} + +/** + * A Query represents a filter to be applied to a firebase location. This object purely represents the + * query expression (and exposes our public API to build the query). The actual query logic is in ViewBase.js. + * + * Since every Firebase reference is a query, Firebase inherits from this object. + */ +export class Query implements Compat { + constructor(readonly database: Database, readonly _delegate: ExpQuery) {} + + on( + eventType: string, + callback: SnapshotCallback, + cancelCallbackOrContext?: ((a: Error) => unknown) | object | null, + context?: object | null + ): SnapshotCallback { + validateArgCount('Query.on', 2, 4, arguments.length); + validateCallback('Query.on', 'callback', callback, false); + + const ret = Query.getCancelAndContextArgs_( + 'Query.on', + cancelCallbackOrContext, + context + ); + const valueCallback = (expSnapshot, previousChildName?) => { + callback.call( + ret.context, + new DataSnapshot(this.database, expSnapshot), + previousChildName + ); + }; + valueCallback.userCallback = callback; + valueCallback.context = ret.context; + const cancelCallback = ret.cancel?.bind(ret.context); + + switch (eventType) { + case 'value': + onValue(this._delegate, valueCallback, cancelCallback); + return callback; + case 'child_added': + onChildAdded(this._delegate, valueCallback, cancelCallback); + return callback; + case 'child_removed': + onChildRemoved(this._delegate, valueCallback, cancelCallback); + return callback; + case 'child_changed': + onChildChanged(this._delegate, valueCallback, cancelCallback); + return callback; + case 'child_moved': + onChildMoved(this._delegate, valueCallback, cancelCallback); + return callback; + default: + throw new Error( + errorPrefix('Query.on', 'eventType') + + 'must be a valid event type = "value", "child_added", "child_removed", ' + + '"child_changed", or "child_moved".' + ); + } + } + + off( + eventType?: string, + callback?: SnapshotCallback, + context?: object | null + ): void { + validateArgCount('Query.off', 0, 3, arguments.length); + validateEventType('Query.off', eventType, true); + validateCallback('Query.off', 'callback', callback, true); + validateContextObject('Query.off', 'context', context, true); + if (callback) { + const valueCallback: _UserCallback = () => {}; + valueCallback.userCallback = callback; + valueCallback.context = context; + off(this._delegate, eventType as EventType, valueCallback); + } else { + off(this._delegate, eventType as EventType | undefined); + } + } + + /** + * Get the server-value for this query, or return a cached value if not connected. + */ + get(): Promise { + return get(this._delegate).then(expSnapshot => { + return new DataSnapshot(this.database, expSnapshot); + }); + } + + /** + * Attaches a listener, waits for the first event, and then removes the listener + */ + once( + eventType: string, + callback?: SnapshotCallback, + failureCallbackOrContext?: ((a: Error) => void) | object | null, + context?: object | null + ): Promise { + validateArgCount('Query.once', 1, 4, arguments.length); + validateCallback('Query.once', 'callback', callback, true); + + const ret = Query.getCancelAndContextArgs_( + 'Query.once', + failureCallbackOrContext, + context + ); + const deferred = new Deferred(); + const valueCallback: _UserCallback = (expSnapshot, previousChildName?) => { + const result = new DataSnapshot(this.database, expSnapshot); + if (callback) { + callback.call(ret.context, result, previousChildName); + } + deferred.resolve(result); + }; + valueCallback.userCallback = callback; + valueCallback.context = ret.context; + const cancelCallback = (error: Error) => { + if (ret.cancel) { + ret.cancel.call(ret.context, error); + } + deferred.reject(error); + }; + + switch (eventType) { + case 'value': + onValue(this._delegate, valueCallback, cancelCallback, { + onlyOnce: true + }); + break; + case 'child_added': + onChildAdded(this._delegate, valueCallback, cancelCallback, { + onlyOnce: true + }); + break; + case 'child_removed': + onChildRemoved(this._delegate, valueCallback, cancelCallback, { + onlyOnce: true + }); + break; + case 'child_changed': + onChildChanged(this._delegate, valueCallback, cancelCallback, { + onlyOnce: true + }); + break; + case 'child_moved': + onChildMoved(this._delegate, valueCallback, cancelCallback, { + onlyOnce: true + }); + break; + default: + throw new Error( + errorPrefix('Query.once', 'eventType') + + 'must be a valid event type = "value", "child_added", "child_removed", ' + + '"child_changed", or "child_moved".' + ); + } + + return deferred.promise; + } + + /** + * Set a limit and anchor it to the start of the window. + */ + limitToFirst(limit: number): Query { + validateArgCount('Query.limitToFirst', 1, 1, arguments.length); + return new Query(this.database, query(this._delegate, limitToFirst(limit))); + } + + /** + * Set a limit and anchor it to the end of the window. + */ + limitToLast(limit: number): Query { + validateArgCount('Query.limitToLast', 1, 1, arguments.length); + return new Query(this.database, query(this._delegate, limitToLast(limit))); + } + + /** + * Given a child path, return a new query ordered by the specified grandchild path. + */ + orderByChild(path: string): Query { + validateArgCount('Query.orderByChild', 1, 1, arguments.length); + return new Query(this.database, query(this._delegate, orderByChild(path))); + } + + /** + * Return a new query ordered by the KeyIndex + */ + orderByKey(): Query { + validateArgCount('Query.orderByKey', 0, 0, arguments.length); + return new Query(this.database, query(this._delegate, orderByKey())); + } + + /** + * Return a new query ordered by the PriorityIndex + */ + orderByPriority(): Query { + validateArgCount('Query.orderByPriority', 0, 0, arguments.length); + return new Query(this.database, query(this._delegate, orderByPriority())); + } + + /** + * Return a new query ordered by the ValueIndex + */ + orderByValue(): Query { + validateArgCount('Query.orderByValue', 0, 0, arguments.length); + return new Query(this.database, query(this._delegate, orderByValue())); + } + + startAt( + value: number | string | boolean | null = null, + name?: string | null + ): Query { + validateArgCount('Query.startAt', 0, 2, arguments.length); + return new Query( + this.database, + query(this._delegate, startAt(value, name)) + ); + } + + startAfter( + value: number | string | boolean | null = null, + name?: string | null + ): Query { + validateArgCount('Query.startAfter', 0, 2, arguments.length); + return new Query( + this.database, + query(this._delegate, startAfter(value, name)) + ); + } + + endAt( + value: number | string | boolean | null = null, + name?: string | null + ): Query { + validateArgCount('Query.endAt', 0, 2, arguments.length); + return new Query(this.database, query(this._delegate, endAt(value, name))); + } + + endBefore( + value: number | string | boolean | null = null, + name?: string | null + ): Query { + validateArgCount('Query.endBefore', 0, 2, arguments.length); + return new Query( + this.database, + query(this._delegate, endBefore(value, name)) + ); + } + + /** + * Load the selection of children with exactly the specified value, and, optionally, + * the specified name. + */ + equalTo(value: number | string | boolean | null, name?: string) { + validateArgCount('Query.equalTo', 1, 2, arguments.length); + return new Query( + this.database, + query(this._delegate, equalTo(value, name)) + ); + } + + /** + * @returns URL for this location. + */ + toString(): string { + validateArgCount('Query.toString', 0, 0, arguments.length); + return this._delegate.toString(); + } + + // Do not create public documentation. This is intended to make JSON serialization work but is otherwise unnecessary + // for end-users. + toJSON() { + // An optional spacer argument is unnecessary for a string. + validateArgCount('Query.toJSON', 0, 1, arguments.length); + return this._delegate.toJSON(); + } + + /** + * Return true if this query and the provided query are equivalent; otherwise, return false. + */ + isEqual(other: Query): boolean { + validateArgCount('Query.isEqual', 1, 1, arguments.length); + if (!(other instanceof Query)) { + const error = + 'Query.isEqual failed: First argument must be an instance of firebase.database.Query.'; + throw new Error(error); + } + return this._delegate.isEqual(other._delegate); + } + + /** + * Helper used by .on and .once to extract the context and or cancel arguments. + * @param fnName - The function name (on or once) + * + */ + private static getCancelAndContextArgs_( + fnName: string, + cancelOrContext?: ((a: Error) => void) | object | null, + context?: object | null + ): { cancel: ((a: Error) => void) | undefined; context: object | undefined } { + const ret: { + cancel: ((a: Error) => void) | null; + context: object | null; + } = { cancel: undefined, context: undefined }; + if (cancelOrContext && context) { + ret.cancel = cancelOrContext as (a: Error) => void; + validateCallback(fnName, 'cancel', ret.cancel, true); + + ret.context = context; + validateContextObject(fnName, 'context', ret.context, true); + } else if (cancelOrContext) { + // we have either a cancel callback or a context. + if (typeof cancelOrContext === 'object' && cancelOrContext !== null) { + // it's a context! + ret.context = cancelOrContext; + } else if (typeof cancelOrContext === 'function') { + ret.cancel = cancelOrContext as (a: Error) => void; + } else { + throw new Error( + errorPrefix(fnName, 'cancelOrContext') + + ' must either be a cancel callback or a context object.' + ); + } + } + return ret; + } + + get ref(): Reference { + return new Reference( + this.database, + new _ReferenceImpl(this._delegate._repo, this._delegate._path) + ); + } +} + +export class Reference extends Query implements Compat { + then: Promise['then']; + catch: Promise['catch']; + + /** + * Call options: + * new Reference(Repo, Path) or + * new Reference(url: string, string|RepoManager) + * + * Externally - this is the firebase.database.Reference type. + */ + constructor( + readonly database: Database, + readonly _delegate: ModularReference + ) { + super( + database, + new _QueryImpl( + _delegate._repo, + _delegate._path, + new _QueryParams(), + false + ) + ); + } + + /** @returns {?string} */ + getKey(): string | null { + validateArgCount('Reference.key', 0, 0, arguments.length); + return this._delegate.key; + } + + child(pathString: string): Reference { + validateArgCount('Reference.child', 1, 1, arguments.length); + if (typeof pathString === 'number') { + pathString = String(pathString); + } + return new Reference(this.database, child(this._delegate, pathString)); + } + + /** @returns {?Reference} */ + getParent(): Reference | null { + validateArgCount('Reference.parent', 0, 0, arguments.length); + const parent = this._delegate.parent; + return parent ? new Reference(this.database, parent) : null; + } + + /** @returns {!Reference} */ + getRoot(): Reference { + validateArgCount('Reference.root', 0, 0, arguments.length); + return new Reference(this.database, this._delegate.root); + } + + set( + newVal: unknown, + onComplete?: (error: Error | null) => void + ): Promise { + validateArgCount('Reference.set', 1, 2, arguments.length); + validateCallback('Reference.set', 'onComplete', onComplete, true); + const result = set(this._delegate, newVal); + if (onComplete) { + result.then( + () => onComplete(null), + error => onComplete(error) + ); + } + return result; + } + + update( + values: object, + onComplete?: (a: Error | null) => void + ): Promise { + validateArgCount('Reference.update', 1, 2, arguments.length); + + if (Array.isArray(values)) { + const newObjectToMerge: { [k: string]: unknown } = {}; + for (let i = 0; i < values.length; ++i) { + newObjectToMerge['' + i] = values[i]; + } + values = newObjectToMerge; + warn( + 'Passing an Array to Firebase.update() is deprecated. ' + + 'Use set() if you want to overwrite the existing data, or ' + + 'an Object with integer keys if you really do want to ' + + 'only update some of the children.' + ); + } + _validateWritablePath('Reference.update', this._delegate._path); + validateCallback('Reference.update', 'onComplete', onComplete, true); + + const result = update(this._delegate, values); + if (onComplete) { + result.then( + () => onComplete(null), + error => onComplete(error) + ); + } + return result; + } + + setWithPriority( + newVal: unknown, + newPriority: string | number | null, + onComplete?: (a: Error | null) => void + ): Promise { + validateArgCount('Reference.setWithPriority', 2, 3, arguments.length); + validateCallback( + 'Reference.setWithPriority', + 'onComplete', + onComplete, + true + ); + + const result = setWithPriority(this._delegate, newVal, newPriority); + if (onComplete) { + result.then( + () => onComplete(null), + error => onComplete(error) + ); + } + return result; + } + + remove(onComplete?: (a: Error | null) => void): Promise { + validateArgCount('Reference.remove', 0, 1, arguments.length); + validateCallback('Reference.remove', 'onComplete', onComplete, true); + + const result = remove(this._delegate); + if (onComplete) { + result.then( + () => onComplete(null), + error => onComplete(error) + ); + } + return result; + } + + transaction( + transactionUpdate: (currentData: unknown) => unknown, + onComplete?: ( + error: Error | null, + committed: boolean, + dataSnapshot: DataSnapshot | null + ) => void, + applyLocally?: boolean + ): Promise { + validateArgCount('Reference.transaction', 1, 3, arguments.length); + validateCallback( + 'Reference.transaction', + 'transactionUpdate', + transactionUpdate, + false + ); + validateCallback('Reference.transaction', 'onComplete', onComplete, true); + validateBoolean( + 'Reference.transaction', + 'applyLocally', + applyLocally, + true + ); + + const result = runTransaction(this._delegate, transactionUpdate, { + applyLocally + }).then( + transactionResult => + new TransactionResult( + transactionResult.committed, + new DataSnapshot(this.database, transactionResult.snapshot) + ) + ); + if (onComplete) { + result.then( + transactionResult => + onComplete( + null, + transactionResult.committed, + transactionResult.snapshot + ), + error => onComplete(error, false, null) + ); + } + return result; + } + + setPriority( + priority: string | number | null, + onComplete?: (a: Error | null) => void + ): Promise { + validateArgCount('Reference.setPriority', 1, 2, arguments.length); + validateCallback('Reference.setPriority', 'onComplete', onComplete, true); + + const result = setPriority(this._delegate, priority); + if (onComplete) { + result.then( + () => onComplete(null), + error => onComplete(error) + ); + } + return result; + } + + push(value?: unknown, onComplete?: (a: Error | null) => void): Reference { + validateArgCount('Reference.push', 0, 2, arguments.length); + validateCallback('Reference.push', 'onComplete', onComplete, true); + + const expPromise = push(this._delegate, value); + const promise = expPromise.then( + expRef => new Reference(this.database, expRef) + ); + + if (onComplete) { + promise.then( + () => onComplete(null), + error => onComplete(error) + ); + } + + const result = new Reference(this.database, expPromise); + result.then = promise.then.bind(promise); + result.catch = promise.catch.bind(promise, undefined); + return result; + } + + onDisconnect(): OnDisconnect { + _validateWritablePath('Reference.onDisconnect', this._delegate._path); + return new OnDisconnect( + new ModularOnDisconnect(this._delegate._repo, this._delegate._path) + ); + } + + get key(): string | null { + return this.getKey(); + } + + get parent(): Reference | null { + return this.getParent(); + } + + get root(): Reference { + return this.getRoot(); + } +} diff --git a/packages/database/src/api/TransactionResult.ts b/packages/database-compat/src/api/TransactionResult.ts similarity index 100% rename from packages/database/src/api/TransactionResult.ts rename to packages/database-compat/src/api/TransactionResult.ts diff --git a/packages/database/src/api/internal.ts b/packages/database-compat/src/api/internal.ts similarity index 55% rename from packages/database/src/api/internal.ts rename to packages/database-compat/src/api/internal.ts index ca3344d2440..6a8defcef7e 100644 --- a/packages/database/src/api/internal.ts +++ b/packages/database-compat/src/api/internal.ts @@ -26,68 +26,13 @@ import { ComponentType, Provider } from '@firebase/component'; -import * as types from '@firebase/database-types'; - -import { _repoManagerDatabaseFromApp } from '../../exp/index'; import { - repoInterceptServerData, - repoStats, - repoStatsIncrementCounter -} from '../core/Repo'; -import { setSDKVersion } from '../core/version'; -import { BrowserPollConnection } from '../realtime/BrowserPollConnection'; -import { WebSocketConnection } from '../realtime/WebSocketConnection'; + _repoManagerDatabaseFromApp, + _setSDKVersion +} from '@firebase/database'; +import * as types from '@firebase/database-types'; import { Database } from './Database'; -import { Reference } from './Reference'; - -/** - * INTERNAL methods for internal-use only (tests, etc.). - * - * Customers shouldn't use these or else should be aware that they could break at any time. - */ - -export const forceLongPolling = function () { - WebSocketConnection.forceDisallow(); - BrowserPollConnection.forceAllow(); -}; - -export const forceWebSockets = function () { - BrowserPollConnection.forceDisallow(); -}; - -/* Used by App Manager */ -export const isWebSocketsAvailable = function (): boolean { - return WebSocketConnection['isAvailable'](); -}; - -export const setSecurityDebugCallback = function ( - ref: Reference, - callback: (a: object) => void -) { - const connection = ref._delegate._repo.persistentConnection_; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (connection as any).securityDebugCallback_ = callback; -}; - -export const stats = function (ref: Reference, showDelta?: boolean) { - repoStats(ref._delegate._repo, showDelta); -}; - -export const statsIncrementCounter = function (ref: Reference, metric: string) { - repoStatsIncrementCounter(ref._delegate._repo, metric); -}; - -export const dataUpdateCount = function (ref: Reference): number { - return ref._delegate._repo.dataUpdateCount; -}; - -export const interceptServerData = function ( - ref: Reference, - callback: ((a: string, b: unknown) => void) | null -) { - return repoInterceptServerData(ref._delegate._repo, callback); -}; /** * Used by console to create a database based on the app, @@ -116,7 +61,7 @@ export function initStandalone({ instance: types.Database; namespace: T; } { - setSDKVersion(version); + _setSDKVersion(version); /** * ComponentContainer('database-standalone') is just a placeholder that doesn't perform diff --git a/packages/database/src/api/onDisconnect.ts b/packages/database-compat/src/api/onDisconnect.ts similarity index 85% rename from packages/database/src/api/onDisconnect.ts rename to packages/database-compat/src/api/onDisconnect.ts index 3a655f08337..577a8491029 100644 --- a/packages/database/src/api/onDisconnect.ts +++ b/packages/database-compat/src/api/onDisconnect.ts @@ -15,21 +15,12 @@ * limitations under the License. */ +import { OnDisconnect as ModularOnDisconnect } from '@firebase/database'; import { validateArgCount, validateCallback, Compat } from '@firebase/util'; -import { Indexable } from '../core/util/misc'; -import { warn } from '../core/util/util'; - -// TODO: revert to import { OnDisconnect as ExpOnDisconnect } from '../../exp/index'; once the modular SDK goes GA -/** - * This is a workaround for an issue in the no-modular '@firebase/database' where its typings - * reference types from `@firebase/app-exp`. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type ExpOnDisconnect = any; - -export class OnDisconnect implements Compat { - constructor(readonly _delegate: ExpOnDisconnect) {} +import { warn } from '../util/util'; +export class OnDisconnect implements Compat { + constructor(readonly _delegate: ModularOnDisconnect) {} cancel(onComplete?: (a: Error | null) => void): Promise { validateArgCount('OnDisconnect.cancel', 0, 1, arguments.length); @@ -93,7 +84,7 @@ export class OnDisconnect implements Compat { } update( - objectToMerge: Indexable, + objectToMerge: Record, onComplete?: (a: Error | null) => void ): Promise { validateArgCount('OnDisconnect.update', 1, 2, arguments.length); diff --git a/packages/database/compat/index.node.ts b/packages/database-compat/src/index.node.ts similarity index 88% rename from packages/database/compat/index.node.ts rename to packages/database-compat/src/index.node.ts index fcb89e54f8f..de26b9c43a7 100644 --- a/packages/database/compat/index.node.ts +++ b/packages/database-compat/src/index.node.ts @@ -19,21 +19,14 @@ import { FirebaseApp, FirebaseNamespace } from '@firebase/app-types'; import { _FirebaseNamespace } from '@firebase/app-types/private'; import { FirebaseAuthInternal } from '@firebase/auth-interop-types'; import { Component, ComponentType } from '@firebase/component'; +import { enableLogging } from '@firebase/database'; import * as types from '@firebase/database-types'; import { CONSTANTS, isNodeSdk } from '@firebase/util'; -import { Client } from 'faye-websocket'; -import { enableLogging } from '../exp/index'; +import { name, version } from '../package.json'; import { Database } from '../src/api/Database'; import * as INTERNAL from '../src/api/internal'; import { DataSnapshot, Query, Reference } from '../src/api/Reference'; -import * as TEST_ACCESS from '../src/api/test_access'; -import { setSDKVersion } from '../src/core/version'; -import { setWebSocketImpl } from '../src/realtime/WebSocketConnection'; - -import { name, version } from './package.json'; - -setWebSocketImpl(Client); const ServerValue = Database.ServerValue; @@ -67,17 +60,13 @@ export function initStandalone( DataSnapshot, enableLogging, INTERNAL, - ServerValue, - TEST_ACCESS + ServerValue }, nodeAdmin }); } export function registerDatabase(instance: FirebaseNamespace) { - // set SDK_VERSION - setSDKVersion(instance.SDK_VERSION); - // Register the Database Service with the 'firebase' namespace. const namespace = (instance as _FirebaseNamespace).INTERNAL.registerComponent( new Component( @@ -87,7 +76,7 @@ export function registerDatabase(instance: FirebaseNamespace) { // getImmediate for FirebaseApp will always succeed const app = container.getProvider('app-compat').getImmediate(); const databaseExp = container - .getProvider('database-exp') + .getProvider('database') .getImmediate({ identifier: url }); return new Database(databaseExp, app); }, @@ -102,8 +91,7 @@ export function registerDatabase(instance: FirebaseNamespace) { DataSnapshot, enableLogging, INTERNAL, - ServerValue, - TEST_ACCESS + ServerValue } ) .setMultipleInstances(true) @@ -136,7 +124,7 @@ try { // Types to export for the admin SDK export { Database, Query, Reference, enableLogging, ServerValue }; -export { OnDisconnect } from '../src/api/onDisconnect'; +export { OnDisconnect } from '@firebase/database/src/api/OnDisconnect'; declare module '@firebase/app-compat' { interface FirebaseNamespace { diff --git a/packages/database/compat/index.ts b/packages/database-compat/src/index.ts similarity index 81% rename from packages/database/compat/index.ts rename to packages/database-compat/src/index.ts index a254c07864f..5ea3d61c083 100644 --- a/packages/database/compat/index.ts +++ b/packages/database-compat/src/index.ts @@ -19,31 +19,21 @@ import firebase, { FirebaseNamespace } from '@firebase/app-compat'; import { _FirebaseNamespace } from '@firebase/app-types/private'; import { Component, ComponentType } from '@firebase/component'; +import { enableLogging } from '@firebase/database'; import * as types from '@firebase/database-types'; -import { enableLogging } from '../exp/index'; +import { name, version } from '../package.json'; import { Database } from '../src/api/Database'; import * as INTERNAL from '../src/api/internal'; import { DataSnapshot, Query, Reference } from '../src/api/Reference'; -import * as TEST_ACCESS from '../src/api/test_access'; -import { setSDKVersion } from '../src/core/version'; - -import { name, version } from './package.json'; - -declare module '@firebase/component' { - interface NameServiceMapping { - 'database-compat': Database; - } -} const ServerValue = Database.ServerValue; export function registerDatabase(instance: FirebaseNamespace) { - // set SDK_VERSION - setSDKVersion(instance.SDK_VERSION); - // Register the Database Service with the 'firebase' namespace. - const namespace = ((instance as unknown) as _FirebaseNamespace).INTERNAL.registerComponent( + const namespace = ( + instance as unknown as _FirebaseNamespace + ).INTERNAL.registerComponent( new Component( 'database-compat', (container, { instanceIdentifier: url }) => { @@ -51,7 +41,7 @@ export function registerDatabase(instance: FirebaseNamespace) { // getImmediate for FirebaseApp will always succeed const app = container.getProvider('app-compat').getImmediate(); const databaseExp = container - .getProvider('database-exp') + .getProvider('database') .getImmediate({ identifier: url }); return new Database(databaseExp, app); }, @@ -66,8 +56,7 @@ export function registerDatabase(instance: FirebaseNamespace) { DataSnapshot, enableLogging, INTERNAL, - ServerValue, - TEST_ACCESS + ServerValue } ) .setMultipleInstances(true) diff --git a/packages/database-compat/src/util/util.ts b/packages/database-compat/src/util/util.ts new file mode 100644 index 00000000000..34e691f8a43 --- /dev/null +++ b/packages/database-compat/src/util/util.ts @@ -0,0 +1,8 @@ +import { Logger } from '@firebase/logger'; + +const logClient = new Logger('@firebase/database-compat'); + +export const warn = function (msg: string) { + const message = 'FIREBASE WARNING: ' + msg; + logClient.warn(message); +}; diff --git a/packages/database-compat/src/util/validation.ts b/packages/database-compat/src/util/validation.ts new file mode 100644 index 00000000000..56eeaeabd04 --- /dev/null +++ b/packages/database-compat/src/util/validation.ts @@ -0,0 +1,42 @@ +import { errorPrefix as errorPrefixFxn } from '@firebase/util'; + +export const validateBoolean = function ( + fnName: string, + argumentName: string, + bool: unknown, + optional: boolean +) { + if (optional && bool === undefined) { + return; + } + if (typeof bool !== 'boolean') { + throw new Error( + errorPrefixFxn(fnName, argumentName) + 'must be a boolean.' + ); + } +}; + +export const validateEventType = function ( + fnName: string, + eventType: string, + optional: boolean +) { + if (optional && eventType === undefined) { + return; + } + + switch (eventType) { + case 'value': + case 'child_added': + case 'child_removed': + case 'child_changed': + case 'child_moved': + break; + default: + throw new Error( + errorPrefixFxn(fnName, 'eventType') + + 'must be a valid event type = "value", "child_added", "child_removed", ' + + '"child_changed", or "child_moved".' + ); + } +}; diff --git a/packages/database/test/browser/crawler_support.test.ts b/packages/database-compat/test/browser/crawler_support.test.ts similarity index 98% rename from packages/database/test/browser/crawler_support.test.ts rename to packages/database-compat/test/browser/crawler_support.test.ts index 8b20caaebff..417845253fa 100644 --- a/packages/database/test/browser/crawler_support.test.ts +++ b/packages/database-compat/test/browser/crawler_support.test.ts @@ -15,9 +15,9 @@ * limitations under the License. */ +import { _TEST_ACCESS_forceRestClient as forceRestClient } from '@firebase/database'; import { expect } from 'chai'; -import { forceRestClient } from '../../src/api/test_access'; import { getRandomNode, getFreshRepoFromReference } from '../helpers/util'; // Some sanity checks for the ReadonlyRestClient crawler support. diff --git a/packages/database/test/database.test.ts b/packages/database-compat/test/database.test.ts similarity index 99% rename from packages/database/test/database.test.ts rename to packages/database-compat/test/database.test.ts index e89d98e373f..e72d7e53c34 100644 --- a/packages/database/test/database.test.ts +++ b/packages/database-compat/test/database.test.ts @@ -15,11 +15,11 @@ * limitations under the License. */ -import firebase from '@firebase/app'; +import firebase from '@firebase/app-compat'; import { expect } from 'chai'; import { DATABASE_ADDRESS, createTestApp } from './helpers/util'; -import '../index'; +import '../src/index'; describe('Database Tests', () => { let defaultApp; diff --git a/packages/database/test/datasnapshot.test.ts b/packages/database-compat/test/datasnapshot.test.ts similarity index 97% rename from packages/database/test/datasnapshot.test.ts rename to packages/database-compat/test/datasnapshot.test.ts index d6114b39ff1..50ee7f79f1f 100644 --- a/packages/database/test/datasnapshot.test.ts +++ b/packages/database-compat/test/datasnapshot.test.ts @@ -15,12 +15,13 @@ * limitations under the License. */ + +import { DataSnapshot as ExpDataSnapshot } from '@firebase/database'; import { expect } from 'chai'; +import { PRIORITY_INDEX } from '../../database/src/core/snap/indexes/PriorityIndex'; +import { nodeFromJSON } from '../../database/src/core/snap/nodeFromJSON'; import { DataSnapshot, Reference } from '../src/api/Reference'; -import { PRIORITY_INDEX } from '../src/core/snap/indexes/PriorityIndex'; -import { nodeFromJSON } from '../src/core/snap/nodeFromJSON'; -import { DataSnapshot as ExpDataSnapshot } from '../src/exp/Reference_impl'; import { getRandomNode } from './helpers/util'; diff --git a/packages/database/test/helpers/events.ts b/packages/database-compat/test/helpers/events.ts similarity index 99% rename from packages/database/test/helpers/events.ts rename to packages/database-compat/test/helpers/events.ts index 144ea2b10d5..d50ab5b7b7c 100644 --- a/packages/database/test/helpers/events.ts +++ b/packages/database-compat/test/helpers/events.ts @@ -15,8 +15,8 @@ * limitations under the License. */ +import { pathParent } from '../../../database/src/core/util/Path'; import { Reference } from '../../src/api/Reference'; -import { pathParent } from '../../src/core/util/Path'; import { TEST_PROJECT } from './util'; diff --git a/packages/database-compat/test/helpers/util.ts b/packages/database-compat/test/helpers/util.ts new file mode 100644 index 00000000000..a932c929843 --- /dev/null +++ b/packages/database-compat/test/helpers/util.ts @@ -0,0 +1,177 @@ +/** + * @license + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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. + */ + +declare let MozWebSocket: WebSocket; + +import '../../src/index'; + +import firebase from '@firebase/app-compat'; +import { _FirebaseNamespace } from '@firebase/app-types/private'; +import { Component, ComponentType } from '@firebase/component'; + +import { Path } from '../../../database/src/core/util/Path'; +import { Query, Reference } from '../../src/api/Reference'; + +// eslint-disable-next-line @typescript-eslint/no-require-imports +export const TEST_PROJECT = require('../../../../config/project.json'); + +const EMULATOR_PORT = process.env.RTDB_EMULATOR_PORT; +const EMULATOR_NAMESPACE = process.env.RTDB_EMULATOR_NAMESPACE; + +const USE_EMULATOR = !!EMULATOR_PORT; + +/* + * When running against the emulator, the hostname will be "localhost" rather + * than ".firebaseio.com", and so we need to append the namespace + * as a query param. + * + * Some tests look for hostname only while others need full url (with the + * namespace provided as a query param), hence below declarations. + */ +export const DATABASE_ADDRESS = USE_EMULATOR + ? `http://localhost:${EMULATOR_PORT}` + : TEST_PROJECT.databaseURL; + +export const DATABASE_URL = USE_EMULATOR + ? `${DATABASE_ADDRESS}?ns=${EMULATOR_NAMESPACE}` + : TEST_PROJECT.databaseURL; + +console.log(`USE_EMULATOR: ${USE_EMULATOR}. DATABASE_URL: ${DATABASE_URL}.`); + +let numDatabases = 0; + +// mock authentication functions for testing +(firebase as unknown as _FirebaseNamespace).INTERNAL.registerComponent( + new Component( + 'auth-internal', + () => ({ + getToken: async () => null, + addAuthTokenListener: () => {}, + removeAuthTokenListener: () => {}, + getUid: () => null + }), + ComponentType.PRIVATE + ) +); + +export function createTestApp() { + const app = firebase.initializeApp({ databaseURL: DATABASE_URL }); + return app; +} + +/** + * Gets or creates a root node to the test namespace. All calls sharing the + * value of opt_i will share an app context. + */ +export function getRootNode(i = 0, ref?: string) { + if (i + 1 > numDatabases) { + numDatabases = i + 1; + } + let app; + try { + app = firebase.app('TEST-' + i); + } catch (e) { + app = firebase.initializeApp({ databaseURL: DATABASE_URL }, 'TEST-' + i); + } + const db = app.database(); + return db.ref(ref); +} + +/** + * Create multiple refs to the same top level + * push key - each on it's own Firebase.Context. + */ +export function getRandomNode(numNodes?): Reference | Reference[] { + if (numNodes === undefined) { + return getRandomNode(1)[0] as Reference; + } + + let child; + const nodeList = []; + for (let i = 0; i < numNodes; i++) { + const ref = getRootNode(i); + if (child === undefined) { + child = ref.push().key; + } + + nodeList[i] = ref.child(child); + } + + return nodeList as Reference[]; +} + +export function getQueryValue(query: Query) { + return query.once('value').then(snap => snap.val()); +} + +export function pause(milliseconds: number) { + return new Promise(resolve => { + setTimeout(() => resolve(), milliseconds); + }); +} + +export function getPath(query: Query) { + return query.toString().replace(DATABASE_ADDRESS, ''); +} + +export function shuffle(arr, randFn = Math.random) { + for (let i = arr.length - 1; i > 0; i--) { + const j = Math.floor(randFn() * (i + 1)); + const tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; + } +} + +let freshRepoId = 1; +const activeFreshApps = []; + +export function getFreshRepo(path: Path) { + const app = firebase.initializeApp( + { databaseURL: DATABASE_URL }, + 'ISOLATED_REPO_' + freshRepoId++ + ); + activeFreshApps.push(app); + return (app as any).database().ref(path.toString()); +} + +export function getFreshRepoFromReference(ref) { + const host = ref.root.toString(); + const path = ref.toString().replace(host, ''); + return getFreshRepo(path); +} + +// Little helpers to get the currently cached snapshot / value. +export function getSnap(path) { + let snap; + const callback = function (snapshot) { + snap = snapshot; + }; + path.once('value', callback); + return snap; +} + +export function getVal(path) { + const snap = getSnap(path); + return snap ? snap.val() : undefined; +} + +export function canCreateExtraConnections() { + return ( + typeof MozWebSocket !== 'undefined' || typeof WebSocket !== 'undefined' + ); +} diff --git a/packages/database/test/info.test.ts b/packages/database-compat/test/info.test.ts similarity index 98% rename from packages/database/test/info.test.ts rename to packages/database-compat/test/info.test.ts index 0bea5859b37..45afd66f90e 100644 --- a/packages/database/test/info.test.ts +++ b/packages/database-compat/test/info.test.ts @@ -17,9 +17,9 @@ import { expect } from 'chai'; +import { EventAccumulator } from '../../database/test/helpers/EventAccumulator'; import { Reference } from '../src/api/Reference'; -import { EventAccumulator } from './helpers/EventAccumulator'; import { getFreshRepo, getRootNode, diff --git a/packages/database/test/order.test.ts b/packages/database-compat/test/order.test.ts similarity index 99% rename from packages/database/test/order.test.ts rename to packages/database-compat/test/order.test.ts index 6d0957271e3..913f61c6a1c 100644 --- a/packages/database/test/order.test.ts +++ b/packages/database-compat/test/order.test.ts @@ -17,9 +17,9 @@ import { expect } from 'chai'; +import { EventAccumulator } from '../../database/test/helpers/EventAccumulator'; import { Reference } from '../src/api/Reference'; -import { EventAccumulator } from './helpers/EventAccumulator'; import { eventTestHelper } from './helpers/events'; import { getRandomNode } from './helpers/util'; diff --git a/packages/database/test/order_by.test.ts b/packages/database-compat/test/order_by.test.ts similarity index 99% rename from packages/database/test/order_by.test.ts rename to packages/database-compat/test/order_by.test.ts index b9f676e8bbe..75cb40ff21f 100644 --- a/packages/database/test/order_by.test.ts +++ b/packages/database-compat/test/order_by.test.ts @@ -17,9 +17,9 @@ import { expect } from 'chai'; +import { EventAccumulatorFactory } from '../../database/test/helpers/EventAccumulator'; import { Reference } from '../src/api/Reference'; -import { EventAccumulatorFactory } from './helpers/EventAccumulator'; import { getRandomNode } from './helpers/util'; describe('.orderBy tests', () => { diff --git a/packages/database/test/promise.test.ts b/packages/database-compat/test/promise.test.ts similarity index 100% rename from packages/database/test/promise.test.ts rename to packages/database-compat/test/promise.test.ts diff --git a/packages/database/test/query.test.ts b/packages/database-compat/test/query.test.ts similarity index 99% rename from packages/database/test/query.test.ts rename to packages/database-compat/test/query.test.ts index 87f97a65e41..4a82c295b56 100644 --- a/packages/database/test/query.test.ts +++ b/packages/database-compat/test/query.test.ts @@ -19,13 +19,16 @@ import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as _ from 'lodash'; -import { DataSnapshot, Query, Reference } from '../src/api/Reference'; -import { INTEGER_32_MAX, INTEGER_32_MIN } from '../src/core/util/util'; - +import { + INTEGER_32_MAX, + INTEGER_32_MIN +} from '../../database/src/core/util/util'; import { EventAccumulator, EventAccumulatorFactory -} from './helpers/EventAccumulator'; +} from '../../database/test/helpers/EventAccumulator'; +import { DataSnapshot, Query, Reference } from '../src/api/Reference'; + import { getFreshRepo, getPath, getRandomNode, pause } from './helpers/util'; use(chaiAsPromised); @@ -2511,8 +2514,9 @@ describe('Query Tests', () => { }); function dumpListens(node: Query) { - const listens: Map> = (node._delegate._repo - .persistentConnection_ as any).listens; + const listens: Map> = ( + node._delegate._repo.persistentConnection_ as any + ).listens; const nodePath = getPath(node); const listenPaths = []; for (const path of listens.keys()) { diff --git a/packages/database/test/servervalues.test.ts b/packages/database-compat/test/servervalues.test.ts similarity index 100% rename from packages/database/test/servervalues.test.ts rename to packages/database-compat/test/servervalues.test.ts diff --git a/packages/database/test/transaction.test.ts b/packages/database-compat/test/transaction.test.ts similarity index 99% rename from packages/database/test/transaction.test.ts rename to packages/database-compat/test/transaction.test.ts index 5bf5caabd2b..5ede5ff22f0 100644 --- a/packages/database/test/transaction.test.ts +++ b/packages/database-compat/test/transaction.test.ts @@ -15,17 +15,18 @@ * limitations under the License. */ -import firebase from '@firebase/app'; +import firebase from '@firebase/app-compat'; +import { _TEST_ACCESS_hijackHash as hijackHash } from '@firebase/database'; import { Deferred } from '@firebase/util'; import { expect } from 'chai'; -import { Reference } from '../src/api/Reference'; -import { hijackHash } from '../src/api/test_access'; - import { EventAccumulator, EventAccumulatorFactory -} from './helpers/EventAccumulator'; +} from '../../database/test/helpers/EventAccumulator'; +import { Reference } from '../src/api/Reference'; + + import { eventTestHelper } from './helpers/events'; import { canCreateExtraConnections, @@ -34,7 +35,7 @@ import { getVal } from './helpers/util'; -import '../index'; +import '../src/index'; describe('Transaction Tests', () => { // Tests that use hijackHash() should set restoreHash to the restore function diff --git a/packages/database-compat/tsconfig.json b/packages/database-compat/tsconfig.json new file mode 100644 index 00000000000..ce12ac3c5dc --- /dev/null +++ b/packages/database-compat/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../config/tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "strict": false, + "downlevelIteration": true + }, + "exclude": [ + "dist/**/*" + ] +} \ No newline at end of file diff --git a/packages/database-types/index.d.ts b/packages/database-types/index.d.ts index 00b0154a4bf..9d4cce3742d 100644 --- a/packages/database-types/index.d.ts +++ b/packages/database-types/index.d.ts @@ -161,6 +161,6 @@ export function enableLogging( declare module '@firebase/component' { interface NameServiceMapping { - 'database': FirebaseDatabase; + 'database-compat': FirebaseDatabase; } } diff --git a/packages/database/.npmignore b/packages/database/.npmignore deleted file mode 100644 index c0e147cc9fc..00000000000 --- a/packages/database/.npmignore +++ /dev/null @@ -1,10 +0,0 @@ -# Directories not needed by end users -/src -test - -# Files not needed by end users -gulpfile.js -index.ts -index.node.ts -karma.conf.js -tsconfig.json \ No newline at end of file diff --git a/packages/database/api-extractor.json b/packages/database/api-extractor.json index af5554eb1e4..ff85fe7d1c5 100644 --- a/packages/database/api-extractor.json +++ b/packages/database/api-extractor.json @@ -1,5 +1,11 @@ { "extends": "../../config/api-extractor.json", // Point it to your entry point d.ts file. - "mainEntryPointFilePath": "/dist/exp/index.d.ts" + "mainEntryPointFilePath": "/dist/src/index.d.ts", + "apiReport": { + /** + * apiReport is handled by repo-scripts/prune-dts/extract-public-api.ts + */ + "enabled": false + } } \ No newline at end of file diff --git a/packages/database/compat/package.json b/packages/database/compat/package.json deleted file mode 100644 index 7a6f2c755be..00000000000 --- a/packages/database/compat/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "@firebase/database-compat", - "version": "0.0.900", - "description": "The Realtime Database component of the Firebase JS SDK.", - "author": "Firebase (https://firebase.google.com/)", - "main": "../dist/compat/cjs/index.js", - "browser": "../dist/compat/esm2017/index.js", - "module": "../dist/compat/esm2017/index.js", - "esm5": "../dist/compat/esm5/index.js", - "license": "Apache-2.0", - "typings": "../dist/compat/esm2017/compat/index.d.ts" - } - \ No newline at end of file diff --git a/packages/database/exp/package.json b/packages/database/exp/package.json deleted file mode 100644 index e11ab55d34d..00000000000 --- a/packages/database/exp/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "@firebase/database-exp", - "description": "A version of the Realtime Database SDK that is compatible with the tree-shakeable Firebase SDK", - "main": "../dist/exp/index.node.cjs.js", - "browser": "../dist/exp/index.esm2017.js", - "module": "../dist/exp/index.esm2017.js", - "esm5": "../dist/exp/index.esm5.js", - "typings": "../dist/exp/index.d.ts" -} diff --git a/packages/database/index.node.ts b/packages/database/index.node.ts deleted file mode 100644 index fd0fd6b1358..00000000000 --- a/packages/database/index.node.ts +++ /dev/null @@ -1,158 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file 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. - */ - -import { FirebaseApp, FirebaseNamespace } from '@firebase/app-types'; -import { _FirebaseNamespace } from '@firebase/app-types/private'; -import { FirebaseAuthInternal } from '@firebase/auth-interop-types'; -import { Component, ComponentType } from '@firebase/component'; -import * as types from '@firebase/database-types'; -import { CONSTANTS, isNodeSdk } from '@firebase/util'; -import { Client } from 'faye-websocket'; - -import { name, version } from './package.json'; -import { Database } from './src/api/Database'; -import * as INTERNAL from './src/api/internal'; -import { DataSnapshot, Query, Reference } from './src/api/Reference'; -import * as TEST_ACCESS from './src/api/test_access'; -import { setSDKVersion } from './src/core/version'; -import { enableLogging, repoManagerDatabaseFromApp } from './src/exp/Database'; -import { setWebSocketImpl } from './src/realtime/WebSocketConnection'; - -setWebSocketImpl(Client); - -const ServerValue = Database.ServerValue; - -/** - * A one off register function which returns a database based on the app and - * passed database URL. (Used by the Admin SDK) - * - * @param app - A valid FirebaseApp-like object - * @param url - A valid Firebase databaseURL - * @param version - custom version e.g. firebase-admin version - * @param nodeAdmin - true if the SDK is being initialized from Firebase Admin. - */ -export function initStandalone( - app: FirebaseApp, - url: string, - version: string, - nodeAdmin = true -) { - CONSTANTS.NODE_ADMIN = nodeAdmin; - return INTERNAL.initStandalone({ - app, - url, - version, - // firebase-admin-node's app.INTERNAL implements FirebaseAuthInternal interface - // eslint-disable-next-line @typescript-eslint/no-explicit-any - customAuthImpl: (app as any).INTERNAL as FirebaseAuthInternal, - namespace: { - Reference, - Query, - Database, - DataSnapshot, - enableLogging, - INTERNAL, - ServerValue, - TEST_ACCESS - }, - nodeAdmin - }); -} - -export function registerDatabase(instance: FirebaseNamespace) { - // set SDK_VERSION - setSDKVersion(instance.SDK_VERSION); - - // Register the Database Service with the 'firebase' namespace. - const namespace = (instance as _FirebaseNamespace).INTERNAL.registerComponent( - new Component( - 'database', - (container, { instanceIdentifier: url }) => { - /* Dependencies */ - // getImmediate for FirebaseApp will always succeed - const app = container.getProvider('app').getImmediate(); - const authProvider = container.getProvider('auth-internal'); - const appCheckProvider = container.getProvider('app-check-internal'); - - return new Database( - repoManagerDatabaseFromApp(app, authProvider, appCheckProvider, url), - app - ); - }, - ComponentType.PUBLIC - ) - .setServiceProps( - // firebase.database namespace properties - { - Reference, - Query, - Database, - DataSnapshot, - enableLogging, - INTERNAL, - ServerValue, - TEST_ACCESS - } - ) - .setMultipleInstances(true) - ); - - instance.registerVersion(name, version, 'node'); - - if (isNodeSdk()) { - module.exports = Object.assign({}, namespace, { initStandalone }); - } -} - -try { - // If @firebase/app is not present, skip registering database. - // It could happen when this package is used in firebase-admin which doesn't depend on @firebase/app. - // Previously firebase-admin depends on @firebase/app, which causes version conflict on - // @firebase/app when used together with the js sdk. More detail: - // https://github.com/firebase/firebase-js-sdk/issues/1696#issuecomment-501546596 - // eslint-disable-next-line import/no-extraneous-dependencies, @typescript-eslint/no-require-imports - const firebase = require('@firebase/app').default; // Only present for v8, undefined for v9 (should skip). - if (firebase) { - registerDatabase(firebase); - } -} catch (err) { - // catch and ignore 'MODULE_NOT_FOUND' error in firebase-admin context - // we can safely ignore this error because RTDB in firebase-admin works without @firebase/app - if (err.code !== 'MODULE_NOT_FOUND') { - throw err; - } -} - -// Types to export for the admin SDK -export { Database, Query, Reference, enableLogging, ServerValue }; - -export { OnDisconnect } from './src/api/onDisconnect'; - -declare module '@firebase/app-types' { - interface FirebaseNamespace { - database?: { - (app?: FirebaseApp): types.FirebaseDatabase; - enableLogging: typeof types.enableLogging; - ServerValue: types.ServerValue; - Database: typeof types.FirebaseDatabase; - }; - } - interface FirebaseApp { - database?(): types.FirebaseDatabase; - } -} -export { DataSnapshot } from './src/api/Reference'; diff --git a/packages/database/index.ts b/packages/database/index.ts deleted file mode 100644 index 23e2bf6ded8..00000000000 --- a/packages/database/index.ts +++ /dev/null @@ -1,102 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file 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. - */ - -// eslint-disable-next-line import/no-extraneous-dependencies -import firebase from '@firebase/app'; -import { FirebaseNamespace } from '@firebase/app-types'; -import { _FirebaseNamespace } from '@firebase/app-types/private'; -import { Component, ComponentType } from '@firebase/component'; -import * as types from '@firebase/database-types'; -import { isNodeSdk } from '@firebase/util'; - -import { name, version } from './package.json'; -import { Database } from './src/api/Database'; -import * as INTERNAL from './src/api/internal'; -import { DataSnapshot, Query, Reference } from './src/api/Reference'; -import * as TEST_ACCESS from './src/api/test_access'; -import { enableLogging } from './src/core/util/util'; -import { setSDKVersion } from './src/core/version'; -import { repoManagerDatabaseFromApp } from './src/exp/Database'; - -const ServerValue = Database.ServerValue; - -export function registerDatabase(instance: FirebaseNamespace) { - // set SDK_VERSION - setSDKVersion(instance.SDK_VERSION); - - // Register the Database Service with the 'firebase' namespace. - const namespace = (instance as _FirebaseNamespace).INTERNAL.registerComponent( - new Component( - 'database', - (container, { instanceIdentifier: url }) => { - /* Dependencies */ - // getImmediate for FirebaseApp will always succeed - const app = container.getProvider('app').getImmediate(); - const authProvider = container.getProvider('auth-internal'); - const appCheckProvider = container.getProvider('app-check-internal'); - - return new Database( - repoManagerDatabaseFromApp(app, authProvider, appCheckProvider, url), - app - ); - }, - ComponentType.PUBLIC - ) - .setServiceProps( - // firebase.database namespace properties - { - Reference, - Query, - Database, - DataSnapshot, - enableLogging, - INTERNAL, - ServerValue, - TEST_ACCESS - } - ) - .setMultipleInstances(true) - ); - - instance.registerVersion(name, version); - - if (isNodeSdk()) { - module.exports = namespace; - } -} - -registerDatabase(firebase); - -// Types to export for the admin SDK -export { Database, Query, Reference, enableLogging, ServerValue }; - -export { DataSnapshot } from './src/api/Reference'; -export { OnDisconnect } from './src/api/onDisconnect'; - -declare module '@firebase/app-types' { - interface FirebaseNamespace { - database?: { - (app?: FirebaseApp): types.FirebaseDatabase; - enableLogging: typeof types.enableLogging; - ServerValue: types.ServerValue; - Database: typeof types.FirebaseDatabase; - }; - } - interface FirebaseApp { - database?(databaseURL?: string): types.FirebaseDatabase; - } -} diff --git a/packages/database/karma.conf.js b/packages/database/karma.conf.js index 566897507b1..d51e08d046e 100644 --- a/packages/database/karma.conf.js +++ b/packages/database/karma.conf.js @@ -15,8 +15,6 @@ * limitations under the License. */ -const karma = require('karma'); -const path = require('path'); const karmaBase = require('../../config/karma.base'); const files = [`test/**/*.test.ts`]; diff --git a/packages/database/package.json b/packages/database/package.json index 341ea36c98f..2e5b231489c 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -4,9 +4,9 @@ "description": "", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.node.cjs.js", - "browser": "dist/index.esm.js", - "module": "dist/index.esm.js", - "esm2017": "dist/index.esm2017.js", + "browser": "dist/index.esm2017.js", + "module": "dist/index.esm2017.js", + "esm5": "dist/index.esm5.js", "files": [ "dist" ], @@ -14,29 +14,22 @@ "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", "prettier": "prettier --write '*.js' '*.ts' '@(exp|src|test)/**/*.ts'", - "build": "run-p build:classic build:exp && yarn build:compat", - "build:classic": "rollup -c rollup.config.js", - "build:exp": "rollup -c rollup.config.exp.js && yarn api-report", - "build:compat": "rollup -c rollup.config.compat.js && yarn add-compat-overloads", - "build:exp:release": "yarn build:exp && yarn build:compat", + "build": "rollup -c rollup.config.js && yarn api-report", "build:deps": "lerna run --scope @firebase/'{app,database}' --include-dependencies build", "dev": "rollup -c -w", "test": "run-p lint test:emulator", "test:ci": "node ../../scripts/run_tests_in_ci.js -s test:emulator", "test:all": "run-p lint test:browser test:node", "test:browser": "karma start --single-run", - "test:node": "TS_NODE_FILES=true TS_NODE_CACHE=NO TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --reporter lcovonly -- mocha 'test/{,!(browser)/**/}*.test.ts' --file index.node.ts --config ../../config/mocharc.node.js", + "test:node": "TS_NODE_FILES=true TS_NODE_CACHE=NO TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --reporter lcovonly -- mocha 'test/{,!(browser)/**/}*.test.ts' --file src/index.node.ts --config ../../config/mocharc.node.js", "test:emulator": "ts-node --compiler-options='{\"module\":\"commonjs\"}' ../../scripts/emulator-testing/database-test-runner.ts", - "api-report": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' ts-node ../../repo-scripts/prune-dts/extract-public-api.ts --package database --packageRoot . --typescriptDts ./dist/exp/exp/index.d.ts --rollupDts ./dist/exp/private.d.ts --untrimmedRollupDts ./dist/exp/internal.d.ts --publicDts ./dist/exp/index.d.ts && yarn api-report:api-json", + "api-report": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' ts-node ../../repo-scripts/prune-dts/extract-public-api.ts --package database --packageRoot . --typescriptDts ./dist/src/index.d.ts --rollupDts ./dist/private.d.ts --untrimmedRollupDts ./dist/internal.d.ts --publicDts ./dist/public.d.ts && yarn api-report:api-json", "api-report:api-json": "rm -rf temp && api-extractor run --local --verbose", - "predoc": "node ../../scripts/exp/remove-exp.js temp", - "doc": "api-documenter markdown --input temp --output docs", - "add-compat-overloads": "ts-node-script ../../scripts/exp/create-overloads.ts -i dist/exp/index.d.ts -o dist/compat/esm2017/compat/index.d.ts -a -r FirebaseDatabase:types.FirebaseDatabase -r Query:types.Query -r Reference:types.Reference -r FirebaseApp:FirebaseAppCompat --moduleToEnhance @firebase/database" + "doc": "api-documenter markdown --input temp --output docs" }, "license": "Apache-2.0", "peerDependencies": {}, "dependencies": { - "@firebase/database-types": "0.8.0", "@firebase/logger": "0.2.6", "@firebase/util": "1.3.0", "@firebase/component": "0.5.6", @@ -46,7 +39,6 @@ }, "devDependencies": { "@firebase/app": "0.6.30", - "@firebase/app-types": "0.6.3", "rollup": "2.52.2", "rollup-plugin-typescript2": "0.30.0", "typescript": "4.2.2" @@ -59,7 +51,7 @@ "bugs": { "url": "https://github.com/firebase/firebase-js-sdk/issues" }, - "typings": "dist/index.d.ts", + "typings": "dist/src/index.d.ts", "nyc": { "extension": [ ".ts" diff --git a/packages/database/rollup.config.compat.js b/packages/database/rollup.config.compat.js deleted file mode 100644 index 4892413e42d..00000000000 --- a/packages/database/rollup.config.compat.js +++ /dev/null @@ -1,144 +0,0 @@ -/** - * @license - * Copyright 2021 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file 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. - */ - -import json from '@rollup/plugin-json'; -import typescriptPlugin from 'rollup-plugin-typescript2'; -import typescript from 'typescript'; -import path from 'path'; -import { getImportPathTransformer } from '../../scripts/exp/ts-transform-import-path'; - -import compatPkg from './compat/package.json'; -import pkg from './package.json'; - -const deps = [ - ...Object.keys(Object.assign({}, pkg.peerDependencies, pkg.dependencies)), - '@firebase/app', - '@firebase/database' -]; - -function onWarn(warning, defaultWarn) { - if (warning.code === 'CIRCULAR_DEPENDENCY') { - throw new Error(warning); - } - defaultWarn(warning); -} - -/** - * ES5 Builds - */ -const es5BuildPlugins = [ - typescriptPlugin({ - typescript, - abortOnError: false, - transformers: [ - getImportPathTransformer({ - // ../../exp/index - pattern: /^.*exp\/index$/, - template: ['@firebase/database'] - }) - ] - }), - json() -]; - -const es5Builds = [ - /** - * Node.js Build - */ - { - input: 'compat/index.node.ts', - output: [ - { - file: path.resolve('compat', compatPkg.main), - format: 'cjs', - sourcemap: true - } - ], - plugins: es5BuildPlugins, - treeshake: { - moduleSideEffects: false - }, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), - onwarn: onWarn - }, - /** - * Browser Builds - */ - { - input: 'compat/index.ts', - output: [ - { - file: path.resolve('compat', compatPkg.esm5), - format: 'es', - sourcemap: true - } - ], - plugins: es5BuildPlugins, - treeshake: { - moduleSideEffects: false - }, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), - onwarn: onWarn - } -]; - -/** - * ES2017 Builds - */ -const es2017BuildPlugins = [ - typescriptPlugin({ - typescript, - tsconfigOverride: { - compilerOptions: { - target: 'es2017' - } - }, - abortOnError: false, - transformers: [ - getImportPathTransformer({ - // ../../exp/index - pattern: /^.*exp\/index$/, - template: ['@firebase/database'] - }) - ] - }), - json({ preferConst: true }) -]; - -const es2017Builds = [ - /** - * Browser Build - */ - { - input: 'compat/index.ts', - output: [ - { - file: path.resolve('compat', compatPkg.browser), - format: 'es', - sourcemap: true - } - ], - plugins: es2017BuildPlugins, - treeshake: { - moduleSideEffects: false - }, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), - onwarn: onWarn - } -]; - -export default [...es5Builds, ...es2017Builds]; diff --git a/packages/database/rollup.config.js b/packages/database/rollup.config.js index f44d9c59251..d334f2a0486 100644 --- a/packages/database/rollup.config.js +++ b/packages/database/rollup.config.js @@ -20,9 +20,10 @@ import typescriptPlugin from 'rollup-plugin-typescript2'; import typescript from 'typescript'; import pkg from './package.json'; -const deps = Object.keys( - Object.assign({}, pkg.peerDependencies, pkg.dependencies) -); +const deps = [ + ...Object.keys({ ...pkg.peerDependencies, ...pkg.dependencies }), + '@firebase/app' +]; function onWarn(warning, defaultWarn) { if (warning.code === 'CIRCULAR_DEPENDENCY') { @@ -36,7 +37,8 @@ function onWarn(warning, defaultWarn) { */ const es5BuildPlugins = [ typescriptPlugin({ - typescript + typescript, + abortOnError: false }), json() ]; @@ -46,27 +48,33 @@ const es5Builds = [ * Node.js Build */ { - input: 'index.node.ts', + input: 'src/index.node.ts', output: [{ file: pkg.main, format: 'cjs', sourcemap: true }], plugins: es5BuildPlugins, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), - onwarn: onWarn, treeshake: { moduleSideEffects: false - } + }, + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), + onwarn: onWarn }, /** * Browser Builds */ { - input: 'index.ts', - output: [{ file: pkg.module, format: 'es', sourcemap: true }], + input: 'src/index.ts', + output: [ + { + file: pkg.esm5, + format: 'es', + sourcemap: true + } + ], plugins: es5BuildPlugins, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), - onwarn: onWarn, treeshake: { moduleSideEffects: false - } + }, + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), + onwarn: onWarn } ]; @@ -80,7 +88,8 @@ const es2017BuildPlugins = [ compilerOptions: { target: 'es2017' } - } + }, + abortOnError: false }), json({ preferConst: true }) ]; @@ -90,14 +99,20 @@ const es2017Builds = [ * Browser Build */ { - input: 'index.ts', - output: [{ file: pkg.esm2017, format: 'es', sourcemap: true }], + input: 'src/index.ts', + output: [ + { + file: pkg.browser, + format: 'es', + sourcemap: true + } + ], plugins: es2017BuildPlugins, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), - onwarn: onWarn, treeshake: { moduleSideEffects: false - } + }, + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), + onwarn: onWarn } ]; diff --git a/packages/database/exp/api.ts b/packages/database/src/api.ts similarity index 58% rename from packages/database/exp/api.ts rename to packages/database/src/api.ts index 974e6976f51..9228a2be63e 100644 --- a/packages/database/exp/api.ts +++ b/packages/database/src/api.ts @@ -21,17 +21,16 @@ export { getDatabase, goOffline, goOnline, - connectDatabaseEmulator, - repoManagerDatabaseFromApp as _repoManagerDatabaseFromApp -} from '../src/exp/Database'; + connectDatabaseEmulator +} from './api/Database'; export { Query, DatabaseReference, ListenOptions, Unsubscribe, ThenableReference -} from '../src/exp/Reference'; -export { OnDisconnect } from '../src/exp/OnDisconnect'; +} from './api/Reference'; +export { OnDisconnect } from './api/OnDisconnect'; export { DataSnapshot, EventType, @@ -65,13 +64,32 @@ export { startAfter, startAt, update, - child, - ReferenceImpl as _ReferenceImpl, - QueryImpl as _QueryImpl -} from '../src/exp/Reference_impl'; -export { increment, serverTimestamp } from '../src/exp/ServerValue'; + child +} from './api/Reference_impl'; +export { increment, serverTimestamp } from './api/ServerValue'; export { runTransaction, TransactionOptions, TransactionResult -} from '../src/exp/Transaction'; +} from './api/Transaction'; + +// internal exports +export { setSDKVersion as _setSDKVersion } from './core/version'; +export { + ReferenceImpl as _ReferenceImpl, + QueryImpl as _QueryImpl +} from './api/Reference_impl'; +export { repoManagerDatabaseFromApp as _repoManagerDatabaseFromApp } from './api/Database'; +export { + validatePathString as _validatePathString, + validateWritablePath as _validateWritablePath +} from './core/util/validation'; +export { UserCallback as _UserCallback } from './core/view/EventRegistration'; +export { QueryParams as _QueryParams } from './core/view/QueryParams'; + +/* eslint-disable camelcase */ +export { + hijackHash as _TEST_ACCESS_hijackHash, + forceRestClient as _TEST_ACCESS_forceRestClient +} from './api/test_access'; +/* eslint-enable camelcase */ diff --git a/packages/database/src/api/Database.ts b/packages/database/src/api/Database.ts index 671104f848e..3a3ac3db723 100644 --- a/packages/database/src/api/Database.ts +++ b/packages/database/src/api/Database.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,117 +14,396 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -// eslint-disable-next-line import/no-extraneous-dependencies -import { FirebaseApp } from '@firebase/app-types'; -import { FirebaseService } from '@firebase/app-types/private'; -import { - validateArgCount, - Compat, - EmulatorMockTokenOptions -} from '@firebase/util'; - -import { - goOnline, - connectDatabaseEmulator, - goOffline, - ref, - refFromURL, - increment, - serverTimestamp -} from '../../exp/index'; // import from the exp public API - -import { Reference } from './Reference'; - -// TODO: revert to import {FirebaseDatabase as ExpDatabase} from '@firebase/database' once modular SDK goes GA -/** - * This is a workaround for an issue in the no-modular '@firebase/database' where its typings - * reference types from `@firebase/app-exp`. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type ExpDatabase = any; - -/** - * Class representing a firebase database. - */ -export class Database implements FirebaseService, Compat { - static readonly ServerValue = { - TIMESTAMP: serverTimestamp(), - increment: (delta: number) => increment(delta) - }; - - /** - * The constructor should not be called by users of our public API. - */ - constructor(readonly _delegate: ExpDatabase, readonly app: FirebaseApp) {} - - INTERNAL = { - delete: () => this._delegate._delete() - }; - - /** - * Modify this instance to communicate with the Realtime Database emulator. - * - *

Note: This method must be called before performing any other operation. - * - * @param host - the emulator host (ex: localhost) - * @param port - the emulator port (ex: 8080) - * @param options.mockUserToken - the mock auth token to use for unit testing Security Rules - */ - useEmulator( - host: string, - port: number, - options: { - mockUserToken?: EmulatorMockTokenOptions; - } = {} - ): void { - connectDatabaseEmulator(this._delegate, host, port, options); - } - - /** - * Returns a reference to the root or to the path specified in the provided - * argument. - * - * @param path - The relative string path or an existing Reference to a database - * location. - * @throws If a Reference is provided, throws if it does not belong to the - * same project. - * @returns Firebase reference. - */ - ref(path?: string): Reference; - ref(path?: Reference): Reference; - ref(path?: string | Reference): Reference { - validateArgCount('database.ref', 0, 1, arguments.length); - if (path instanceof Reference) { - const childRef = refFromURL(this._delegate, path.toString()); - return new Reference(this, childRef); - } else { - const childRef = ref(this._delegate, path); - return new Reference(this, childRef); - } - } - - /** - * Returns a reference to the root or the path specified in url. - * We throw a exception if the url is not in the same domain as the - * current repo. - * @returns Firebase reference. - */ - refFromURL(url: string): Reference { - const apiName = 'database.refFromURL'; - validateArgCount(apiName, 1, 1, arguments.length); - const childRef = refFromURL(this._delegate, url); - return new Reference(this, childRef); - } - - // Make individual repo go offline. - goOffline(): void { - validateArgCount('database.goOffline', 0, 0, arguments.length); - return goOffline(this._delegate); - } - - goOnline(): void { - validateArgCount('database.goOnline', 0, 0, arguments.length); - return goOnline(this._delegate); - } -} + import { AppCheckInternalComponentName } from '@firebase/app-check-interop-types'; + // eslint-disable-next-line import/no-extraneous-dependencies + import { + _FirebaseService, + _getProvider, + FirebaseApp, + getApp + } from '@firebase/app-exp'; + import { FirebaseAuthInternalName } from '@firebase/auth-interop-types'; + import { Provider } from '@firebase/component'; + import { + getModularInstance, + createMockUserToken, + EmulatorMockTokenOptions + } from '@firebase/util'; + + import { AppCheckTokenProvider } from '../core/AppCheckTokenProvider'; + import { + AuthTokenProvider, + EmulatorTokenProvider, + FirebaseAuthTokenProvider + } from '../core/AuthTokenProvider'; + import { Repo, repoInterrupt, repoResume, repoStart } from '../core/Repo'; + import { RepoInfo } from '../core/RepoInfo'; + import { parseRepoInfo } from '../core/util/libs/parser'; + import { newEmptyPath, pathIsEmpty } from '../core/util/Path'; + import { + fatal, + log, + enableLogging as enableLoggingImpl + } from '../core/util/util'; + import { validateUrl } from '../core/util/validation'; + + import { ReferenceImpl } from './Reference_impl'; + + /** + * This variable is also defined in the firebase Node.js Admin SDK. Before + * modifying this definition, consult the definition in: + * + * https://github.com/firebase/firebase-admin-node + * + * and make sure the two are consistent. + */ + const FIREBASE_DATABASE_EMULATOR_HOST_VAR = 'FIREBASE_DATABASE_EMULATOR_HOST'; + + /** + * Creates and caches `Repo` instances. + */ + const repos: { + [appName: string]: { + [dbUrl: string]: Repo; + }; + } = {}; + + /** + * If true, any new `Repo` will be created to use `ReadonlyRestClient` (for testing purposes). + */ + let useRestClient = false; + + /** + * Update an existing `Repo` in place to point to a new host/port. + */ + function repoManagerApplyEmulatorSettings( + repo: Repo, + host: string, + port: number, + tokenProvider?: AuthTokenProvider + ): void { + repo.repoInfo_ = new RepoInfo( + `${host}:${port}`, + /* secure= */ false, + repo.repoInfo_.namespace, + repo.repoInfo_.webSocketOnly, + repo.repoInfo_.nodeAdmin, + repo.repoInfo_.persistenceKey, + repo.repoInfo_.includeNamespaceInQueryParams + ); + + if (tokenProvider) { + repo.authTokenProvider_ = tokenProvider; + } + } + + /** + * This function should only ever be called to CREATE a new database instance. + * @internal + */ + export function repoManagerDatabaseFromApp( + app: FirebaseApp, + authProvider: Provider, + appCheckProvider?: Provider, + url?: string, + nodeAdmin?: boolean + ): Database { + let dbUrl: string | undefined = url || app.options.databaseURL; + if (dbUrl === undefined) { + if (!app.options.projectId) { + fatal( + "Can't determine Firebase Database URL. Be sure to include " + + ' a Project ID when calling firebase.initializeApp().' + ); + } + + log('Using default host for project ', app.options.projectId); + dbUrl = `${app.options.projectId}-default-rtdb.firebaseio.com`; + } + + let parsedUrl = parseRepoInfo(dbUrl, nodeAdmin); + let repoInfo = parsedUrl.repoInfo; + + let isEmulator: boolean; + + let dbEmulatorHost: string | undefined = undefined; + if (typeof process !== 'undefined') { + dbEmulatorHost = process.env[FIREBASE_DATABASE_EMULATOR_HOST_VAR]; + } + + if (dbEmulatorHost) { + isEmulator = true; + dbUrl = `http://${dbEmulatorHost}?ns=${repoInfo.namespace}`; + parsedUrl = parseRepoInfo(dbUrl, nodeAdmin); + repoInfo = parsedUrl.repoInfo; + } else { + isEmulator = !parsedUrl.repoInfo.secure; + } + + const authTokenProvider = + nodeAdmin && isEmulator + ? new EmulatorTokenProvider(EmulatorTokenProvider.OWNER) + : new FirebaseAuthTokenProvider(app.name, app.options, authProvider); + + validateUrl('Invalid Firebase Database URL', parsedUrl); + if (!pathIsEmpty(parsedUrl.path)) { + fatal( + 'Database URL must point to the root of a Firebase Database ' + + '(not including a child path).' + ); + } + + const repo = repoManagerCreateRepo( + repoInfo, + app, + authTokenProvider, + new AppCheckTokenProvider(app.name, appCheckProvider) + ); + return new Database(repo, app); + } + + /** + * Remove the repo and make sure it is disconnected. + * + */ + function repoManagerDeleteRepo(repo: Repo, appName: string): void { + const appRepos = repos[appName]; + // This should never happen... + if (!appRepos || appRepos[repo.key] !== repo) { + fatal(`Database ${appName}(${repo.repoInfo_}) has already been deleted.`); + } + repoInterrupt(repo); + delete appRepos[repo.key]; + } + + /** + * Ensures a repo doesn't already exist and then creates one using the + * provided app. + * + * @param repoInfo - The metadata about the Repo + * @returns The Repo object for the specified server / repoName. + */ + function repoManagerCreateRepo( + repoInfo: RepoInfo, + app: FirebaseApp, + authTokenProvider: AuthTokenProvider, + appCheckProvider: AppCheckTokenProvider + ): Repo { + let appRepos = repos[app.name]; + + if (!appRepos) { + appRepos = {}; + repos[app.name] = appRepos; + } + + let repo = appRepos[repoInfo.toURLString()]; + if (repo) { + fatal( + 'Database initialized multiple times. Please make sure the format of the database URL matches with each database() call.' + ); + } + repo = new Repo(repoInfo, useRestClient, authTokenProvider, appCheckProvider); + appRepos[repoInfo.toURLString()] = repo; + + return repo; + } + + /** + * Forces us to use ReadonlyRestClient instead of PersistentConnection for new Repos. + */ + export function repoManagerForceRestClient(forceRestClient: boolean): void { + useRestClient = forceRestClient; + } + + /** + * Class representing a Firebase Realtime Database. + */ + export class Database implements _FirebaseService { + /** Represents a `Database` instance. */ + readonly 'type' = 'database'; + + /** Track if the instance has been used (root or repo accessed) */ + _instanceStarted: boolean = false; + + /** Backing state for root_ */ + private _rootInternal?: ReferenceImpl; + + /** @hideconstructor */ + constructor( + public _repoInternal: Repo, + /** The {@link @firebase/app#FirebaseApp} associated with this Realtime Database instance. */ + readonly app: FirebaseApp + ) {} + + get _repo(): Repo { + if (!this._instanceStarted) { + repoStart( + this._repoInternal, + this.app.options.appId, + this.app.options['databaseAuthVariableOverride'] + ); + this._instanceStarted = true; + } + return this._repoInternal; + } + + get _root(): ReferenceImpl { + if (!this._rootInternal) { + this._rootInternal = new ReferenceImpl(this._repo, newEmptyPath()); + } + return this._rootInternal; + } + + _delete(): Promise { + if (this._rootInternal !== null) { + repoManagerDeleteRepo(this._repo, this.app.name); + this._repoInternal = null; + this._rootInternal = null; + } + return Promise.resolve(); + } + + _checkNotDeleted(apiName: string) { + if (this._rootInternal === null) { + fatal('Cannot call ' + apiName + ' on a deleted database.'); + } + } + } + + /** + * Returns the instance of the Realtime Database SDK that is associated + * with the provided {@link @firebase/app#FirebaseApp}. Initializes a new instance with + * with default settings if no instance exists or if the existing instance uses + * a custom database URL. + * + * @param app - The {@link @firebase/app#FirebaseApp} instance that the returned Realtime + * Database instance is associated with. + * @param url - The URL of the Realtime Database instance to connect to. If not + * provided, the SDK connects to the default instance of the Firebase App. + * @returns The `Database` instance of the provided app. + */ + export function getDatabase( + app: FirebaseApp = getApp(), + url?: string + ): Database { + return _getProvider(app, 'database').getImmediate({ + identifier: url + }) as Database; + } + + /** + * Modify the provided instance to communicate with the Realtime Database + * emulator. + * + *

Note: This method must be called before performing any other operation. + * + * @param db - The instance to modify. + * @param host - The emulator host (ex: localhost) + * @param port - The emulator port (ex: 8080) + * @param options.mockUserToken - the mock auth token to use for unit testing Security Rules + */ + export function connectDatabaseEmulator( + db: Database, + host: string, + port: number, + options: { + mockUserToken?: EmulatorMockTokenOptions | string; + } = {} + ): void { + db = getModularInstance(db); + db._checkNotDeleted('useEmulator'); + if (db._instanceStarted) { + fatal( + 'Cannot call useEmulator() after instance has already been initialized.' + ); + } + + const repo = db._repoInternal; + let tokenProvider: EmulatorTokenProvider | undefined = undefined; + if (repo.repoInfo_.nodeAdmin) { + if (options.mockUserToken) { + fatal( + 'mockUserToken is not supported by the Admin SDK. For client access with mock users, please use the "firebase" package instead of "firebase-admin".' + ); + } + tokenProvider = new EmulatorTokenProvider(EmulatorTokenProvider.OWNER); + } else if (options.mockUserToken) { + const token = + typeof options.mockUserToken === 'string' + ? options.mockUserToken + : createMockUserToken(options.mockUserToken, db.app.options.projectId); + tokenProvider = new EmulatorTokenProvider(token); + } + + // Modify the repo to apply emulator settings + repoManagerApplyEmulatorSettings(repo, host, port, tokenProvider); + } + + /** + * Disconnects from the server (all Database operations will be completed + * offline). + * + * The client automatically maintains a persistent connection to the Database + * server, which will remain active indefinitely and reconnect when + * disconnected. However, the `goOffline()` and `goOnline()` methods may be used + * to control the client connection in cases where a persistent connection is + * undesirable. + * + * While offline, the client will no longer receive data updates from the + * Database. However, all Database operations performed locally will continue to + * immediately fire events, allowing your application to continue behaving + * normally. Additionally, each operation performed locally will automatically + * be queued and retried upon reconnection to the Database server. + * + * To reconnect to the Database and begin receiving remote events, see + * `goOnline()`. + * + * @param db - The instance to disconnect. + */ + export function goOffline(db: Database): void { + db = getModularInstance(db); + db._checkNotDeleted('goOffline'); + repoInterrupt(db._repo); + } + + /** + * Reconnects to the server and synchronizes the offline Database state + * with the server state. + * + * This method should be used after disabling the active connection with + * `goOffline()`. Once reconnected, the client will transmit the proper data + * and fire the appropriate events so that your client "catches up" + * automatically. + * + * @param db - The instance to reconnect. + */ + export function goOnline(db: Database): void { + db = getModularInstance(db); + db._checkNotDeleted('goOnline'); + repoResume(db._repo); + } + + /** + * Logs debugging information to the console. + * + * @param enabled - Enables logging if `true`, disables logging if `false`. + * @param persistent - Remembers the logging state between page refreshes if + * `true`. + */ + export function enableLogging(enabled: boolean, persistent?: boolean); + + /** + * Logs debugging information to the console. + * + * @param logger - A custom logger function to control how things get logged. + */ + export function enableLogging(logger: (message: string) => unknown); + + export function enableLogging( + logger: boolean | ((message: string) => unknown), + persistent?: boolean + ): void { + enableLoggingImpl(logger, persistent); + } + \ No newline at end of file diff --git a/packages/database/src/exp/OnDisconnect.ts b/packages/database/src/api/OnDisconnect.ts similarity index 100% rename from packages/database/src/exp/OnDisconnect.ts rename to packages/database/src/api/OnDisconnect.ts diff --git a/packages/database/src/api/Reference.ts b/packages/database/src/api/Reference.ts index 4bc9b63365b..c2e97aa229e 100644 --- a/packages/database/src/api/Reference.ts +++ b/packages/database/src/api/Reference.ts @@ -1,6 +1,10 @@ +import { Repo } from '../core/Repo'; +import { Path } from '../core/util/Path'; +import { QueryContext } from '../core/view/EventRegistration'; + /** * @license - * Copyright 2017 Google LLC + * Copyright 2021 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,782 +19,117 @@ * limitations under the License. */ -import { - Compat, - Deferred, - errorPrefix, - validateArgCount, - validateCallback, - validateContextObject -} from '@firebase/util'; - -import { - OnDisconnect as ExpOnDisconnect, - off, - onChildAdded, - onChildChanged, - onChildMoved, - onChildRemoved, - onValue, - EventType, - limitToFirst, - query, - limitToLast, - orderByChild, - orderByKey, - orderByValue, - orderByPriority, - startAt, - startAfter, - endAt, - endBefore, - equalTo, - get, - set, - update, - setWithPriority, - remove, - setPriority, - push, - runTransaction, - _QueryImpl, - _ReferenceImpl, - child -} from '../../exp/index'; // import from the exp public API -import { warn } from '../core/util/util'; -import { - validateBoolean, - validateEventType, - validatePathString, - validateWritablePath -} from '../core/util/validation'; -import { UserCallback } from '../core/view/EventRegistration'; -import { QueryParams } from '../core/view/QueryParams'; -import { ThenableReferenceImpl } from '../exp/Reference_impl'; - -import { Database } from './Database'; -import { OnDisconnect } from './onDisconnect'; -import { TransactionResult } from './TransactionResult'; - -// TODO: revert to import { DataSnapshot as ExpDataSnapshot, Query as ExpQuery, -// Reference as ExpReference,} from '../../exp/index'; once the modular SDK goes GA -/** - * This is part of a workaround for an issue in the no-modular '@firebase/database' where its typings - * reference types from `@firebase/app-exp`. - */ - -/* eslint-disable @typescript-eslint/no-explicit-any */ -type ExpDataSnapshot = any; -type ExpQuery = any; -type ExpReference = any; -/* eslint-enable @typescript-eslint/no-explicit-any */ - /** - * Class representing a firebase data snapshot. It wraps a SnapshotNode and - * surfaces the public methods (val, forEach, etc.) we want to expose. + * A `Query` sorts and filters the data at a Database location so only a subset + * of the child data is included. This can be used to order a collection of + * data by some attribute (for example, height of dinosaurs) as well as to + * restrict a large list of items (for example, chat messages) down to a number + * suitable for synchronizing to the client. Queries are created by chaining + * together one or more of the filter methods defined here. + * + * Just as with a `DatabaseReference`, you can receive data from a `Query` by using the + * `on*()` methods. You will only receive events and `DataSnapshot`s for the + * subset of the data that matches your query. + * + * See {@link https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data} + * for more information. */ -export class DataSnapshot implements Compat { - constructor( - readonly _database: Database, - readonly _delegate: ExpDataSnapshot - ) {} +export interface Query extends QueryContext { + /** The `DatabaseReference` for the `Query`'s location. */ + readonly ref: DatabaseReference; /** - * Retrieves the snapshot contents as JSON. Returns null if the snapshot is - * empty. + * Returns whether or not the current and provided queries represent the same + * location, have the same query parameters, and are from the same instance of + * `FirebaseApp`. * - * @returns JSON representation of the DataSnapshot contents, or null if empty. - */ - val(): unknown { - validateArgCount('DataSnapshot.val', 0, 0, arguments.length); - return this._delegate.val(); - } - - /** - * Returns the snapshot contents as JSON, including priorities of node. Suitable for exporting - * the entire node contents. - * @returns JSON representation of the DataSnapshot contents, or null if empty. - */ - exportVal(): unknown { - validateArgCount('DataSnapshot.exportVal', 0, 0, arguments.length); - return this._delegate.exportVal(); - } - - // Do not create public documentation. This is intended to make JSON serialization work but is otherwise unnecessary - // for end-users - toJSON(): unknown { - // Optional spacer argument is unnecessary because we're depending on recursion rather than stringifying the content - validateArgCount('DataSnapshot.toJSON', 0, 1, arguments.length); - return this._delegate.toJSON(); - } - - /** - * Returns whether the snapshot contains a non-null value. + * Two `DatabaseReference` objects are equivalent if they represent the same location + * and are from the same instance of `FirebaseApp`. * - * @returns Whether the snapshot contains a non-null value, or is empty. - */ - exists(): boolean { - validateArgCount('DataSnapshot.exists', 0, 0, arguments.length); - return this._delegate.exists(); - } - - /** - * Returns a DataSnapshot of the specified child node's contents. + * Two `Query` objects are equivalent if they represent the same location, + * have the same query parameters, and are from the same instance of + * `FirebaseApp`. Equivalent queries share the same sort order, limits, and + * starting and ending points. * - * @param path - Path to a child. - * @returns DataSnapshot for child node. + * @param other - The query to compare against. + * @returns Whether or not the current and provided queries are equivalent. */ - child(path: string): DataSnapshot { - validateArgCount('DataSnapshot.child', 0, 1, arguments.length); - // Ensure the childPath is a string (can be a number) - path = String(path); - validatePathString('DataSnapshot.child', 'path', path, false); - return new DataSnapshot(this._database, this._delegate.child(path)); - } + isEqual(other: Query | null): boolean; /** - * Returns whether the snapshot contains a child at the specified path. + * Returns a JSON-serializable representation of this object. * - * @param path - Path to a child. - * @returns Whether the child exists. + * @returns A JSON-serializable representation of this object. */ - hasChild(path: string): boolean { - validateArgCount('DataSnapshot.hasChild', 1, 1, arguments.length); - validatePathString('DataSnapshot.hasChild', 'path', path, false); - return this._delegate.hasChild(path); - } + toJSON(): string; /** - * Returns the priority of the object, or null if no priority was set. + * Gets the absolute URL for this location. * - * @returns The priority. - */ - getPriority(): string | number | null { - validateArgCount('DataSnapshot.getPriority', 0, 0, arguments.length); - return this._delegate.priority; - } - - /** - * Iterates through child nodes and calls the specified action for each one. + * The `toString()` method returns a URL that is ready to be put into a + * browser, curl command, or a `refFromURL()` call. Since all of those expect + * the URL to be url-encoded, `toString()` returns an encoded URL. * - * @param action - Callback function to be called - * for each child. - * @returns True if forEach was canceled by action returning true for - * one of the child nodes. - */ - forEach(action: (snapshot: DataSnapshot) => boolean | void): boolean { - validateArgCount('DataSnapshot.forEach', 1, 1, arguments.length); - validateCallback('DataSnapshot.forEach', 'action', action, false); - return this._delegate.forEach(expDataSnapshot => - action(new DataSnapshot(this._database, expDataSnapshot)) - ); - } - - /** - * Returns whether this DataSnapshot has children. - * @returns True if the DataSnapshot contains 1 or more child nodes. - */ - hasChildren(): boolean { - validateArgCount('DataSnapshot.hasChildren', 0, 0, arguments.length); - return this._delegate.hasChildren(); - } - - get key() { - return this._delegate.key; - } - - /** - * Returns the number of children for this DataSnapshot. - * @returns The number of children that this DataSnapshot contains. - */ - numChildren(): number { - validateArgCount('DataSnapshot.numChildren', 0, 0, arguments.length); - return this._delegate.size; - } - - /** - * @returns The Firebase reference for the location this snapshot's data came - * from. + * Append '.json' to the returned URL when typed into a browser to download + * JSON-formatted data. If the location is secured (that is, not publicly + * readable), you will get a permission-denied error. + * + * @returns The absolute URL for this location. */ - getRef(): Reference { - validateArgCount('DataSnapshot.ref', 0, 0, arguments.length); - return new Reference(this._database, this._delegate.ref); - } - - get ref(): Reference { - return this.getRef(); - } -} - -export interface SnapshotCallback { - (dataSnapshot: DataSnapshot, previousChildName?: string | null): unknown; + toString(): string; } /** - * A Query represents a filter to be applied to a firebase location. This object purely represents the - * query expression (and exposes our public API to build the query). The actual query logic is in ViewBase.js. + * A `DatabaseReference` represents a specific location in your Database and can be used + * for reading or writing data to that Database location. * - * Since every Firebase reference is a query, Firebase inherits from this object. + * You can reference the root or child location in your Database by calling + * `ref()` or `ref("child/path")`. + * + * Writing is done with the `set()` method and reading can be done with the + * `on*()` method. See {@link + * https://firebase.google.com/docs/database/web/read-and-write} */ -export class Query implements Compat { - constructor(readonly database: Database, readonly _delegate: ExpQuery) {} - - on( - eventType: string, - callback: SnapshotCallback, - cancelCallbackOrContext?: ((a: Error) => unknown) | object | null, - context?: object | null - ): SnapshotCallback { - validateArgCount('Query.on', 2, 4, arguments.length); - validateCallback('Query.on', 'callback', callback, false); - - const ret = Query.getCancelAndContextArgs_( - 'Query.on', - cancelCallbackOrContext, - context - ); - const valueCallback: UserCallback = (expSnapshot, previousChildName?) => { - callback.call( - ret.context, - new DataSnapshot(this.database, expSnapshot), - previousChildName - ); - }; - valueCallback.userCallback = callback; - valueCallback.context = ret.context; - const cancelCallback = ret.cancel?.bind(ret.context); - - switch (eventType) { - case 'value': - onValue(this._delegate, valueCallback, cancelCallback); - return callback; - case 'child_added': - onChildAdded(this._delegate, valueCallback, cancelCallback); - return callback; - case 'child_removed': - onChildRemoved(this._delegate, valueCallback, cancelCallback); - return callback; - case 'child_changed': - onChildChanged(this._delegate, valueCallback, cancelCallback); - return callback; - case 'child_moved': - onChildMoved(this._delegate, valueCallback, cancelCallback); - return callback; - default: - throw new Error( - errorPrefix('Query.on', 'eventType') + - 'must be a valid event type = "value", "child_added", "child_removed", ' + - '"child_changed", or "child_moved".' - ); - } - } - - off( - eventType?: string, - callback?: SnapshotCallback, - context?: object | null - ): void { - validateArgCount('Query.off', 0, 3, arguments.length); - validateEventType('Query.off', eventType, true); - validateCallback('Query.off', 'callback', callback, true); - validateContextObject('Query.off', 'context', context, true); - if (callback) { - const valueCallback: UserCallback = () => {}; - valueCallback.userCallback = callback; - valueCallback.context = context; - off(this._delegate, eventType as EventType, valueCallback); - } else { - off(this._delegate, eventType as EventType | undefined); - } - } - - /** - * Get the server-value for this query, or return a cached value if not connected. - */ - get(): Promise { - return get(this._delegate).then(expSnapshot => { - return new DataSnapshot(this.database, expSnapshot); - }); - } - - /** - * Attaches a listener, waits for the first event, and then removes the listener - */ - once( - eventType: string, - callback?: SnapshotCallback, - failureCallbackOrContext?: ((a: Error) => void) | object | null, - context?: object | null - ): Promise { - validateArgCount('Query.once', 1, 4, arguments.length); - validateCallback('Query.once', 'callback', callback, true); - - const ret = Query.getCancelAndContextArgs_( - 'Query.once', - failureCallbackOrContext, - context - ); - const deferred = new Deferred(); - const valueCallback: UserCallback = (expSnapshot, previousChildName?) => { - const result = new DataSnapshot(this.database, expSnapshot); - if (callback) { - callback.call(ret.context, result, previousChildName); - } - deferred.resolve(result); - }; - valueCallback.userCallback = callback; - valueCallback.context = ret.context; - const cancelCallback = (error: Error) => { - if (ret.cancel) { - ret.cancel.call(ret.context, error); - } - deferred.reject(error); - }; - - switch (eventType) { - case 'value': - onValue(this._delegate, valueCallback, cancelCallback, { - onlyOnce: true - }); - break; - case 'child_added': - onChildAdded(this._delegate, valueCallback, cancelCallback, { - onlyOnce: true - }); - break; - case 'child_removed': - onChildRemoved(this._delegate, valueCallback, cancelCallback, { - onlyOnce: true - }); - break; - case 'child_changed': - onChildChanged(this._delegate, valueCallback, cancelCallback, { - onlyOnce: true - }); - break; - case 'child_moved': - onChildMoved(this._delegate, valueCallback, cancelCallback, { - onlyOnce: true - }); - break; - default: - throw new Error( - errorPrefix('Query.once', 'eventType') + - 'must be a valid event type = "value", "child_added", "child_removed", ' + - '"child_changed", or "child_moved".' - ); - } - - return deferred.promise; - } - - /** - * Set a limit and anchor it to the start of the window. - */ - limitToFirst(limit: number): Query { - validateArgCount('Query.limitToFirst', 1, 1, arguments.length); - return new Query(this.database, query(this._delegate, limitToFirst(limit))); - } - - /** - * Set a limit and anchor it to the end of the window. - */ - limitToLast(limit: number): Query { - validateArgCount('Query.limitToLast', 1, 1, arguments.length); - return new Query(this.database, query(this._delegate, limitToLast(limit))); - } - - /** - * Given a child path, return a new query ordered by the specified grandchild path. - */ - orderByChild(path: string): Query { - validateArgCount('Query.orderByChild', 1, 1, arguments.length); - return new Query(this.database, query(this._delegate, orderByChild(path))); - } - - /** - * Return a new query ordered by the KeyIndex - */ - orderByKey(): Query { - validateArgCount('Query.orderByKey', 0, 0, arguments.length); - return new Query(this.database, query(this._delegate, orderByKey())); - } - - /** - * Return a new query ordered by the PriorityIndex - */ - orderByPriority(): Query { - validateArgCount('Query.orderByPriority', 0, 0, arguments.length); - return new Query(this.database, query(this._delegate, orderByPriority())); - } - - /** - * Return a new query ordered by the ValueIndex - */ - orderByValue(): Query { - validateArgCount('Query.orderByValue', 0, 0, arguments.length); - return new Query(this.database, query(this._delegate, orderByValue())); - } - - startAt( - value: number | string | boolean | null = null, - name?: string | null - ): Query { - validateArgCount('Query.startAt', 0, 2, arguments.length); - return new Query( - this.database, - query(this._delegate, startAt(value, name)) - ); - } - - startAfter( - value: number | string | boolean | null = null, - name?: string | null - ): Query { - validateArgCount('Query.startAfter', 0, 2, arguments.length); - return new Query( - this.database, - query(this._delegate, startAfter(value, name)) - ); - } - - endAt( - value: number | string | boolean | null = null, - name?: string | null - ): Query { - validateArgCount('Query.endAt', 0, 2, arguments.length); - return new Query(this.database, query(this._delegate, endAt(value, name))); - } - - endBefore( - value: number | string | boolean | null = null, - name?: string | null - ): Query { - validateArgCount('Query.endBefore', 0, 2, arguments.length); - return new Query( - this.database, - query(this._delegate, endBefore(value, name)) - ); - } - - /** - * Load the selection of children with exactly the specified value, and, optionally, - * the specified name. - */ - equalTo(value: number | string | boolean | null, name?: string) { - validateArgCount('Query.equalTo', 1, 2, arguments.length); - return new Query( - this.database, - query(this._delegate, equalTo(value, name)) - ); - } - - /** - * @returns URL for this location. - */ - toString(): string { - validateArgCount('Query.toString', 0, 0, arguments.length); - return this._delegate.toString(); - } - - // Do not create public documentation. This is intended to make JSON serialization work but is otherwise unnecessary - // for end-users. - toJSON() { - // An optional spacer argument is unnecessary for a string. - validateArgCount('Query.toJSON', 0, 1, arguments.length); - return this._delegate.toJSON(); - } - - /** - * Return true if this query and the provided query are equivalent; otherwise, return false. - */ - isEqual(other: Query): boolean { - validateArgCount('Query.isEqual', 1, 1, arguments.length); - if (!(other instanceof Query)) { - const error = - 'Query.isEqual failed: First argument must be an instance of firebase.database.Query.'; - throw new Error(error); - } - return this._delegate.isEqual(other._delegate); - } - +export interface DatabaseReference extends Query { /** - * Helper used by .on and .once to extract the context and or cancel arguments. - * @param fnName - The function name (on or once) + * The last part of the `DatabaseReference`'s path. * + * For example, `"ada"` is the key for + * `https://.firebaseio.com/users/ada`. + * + * The key of a root `DatabaseReference` is `null`. */ - private static getCancelAndContextArgs_( - fnName: string, - cancelOrContext?: ((a: Error) => void) | object | null, - context?: object | null - ): { cancel: ((a: Error) => void) | undefined; context: object | undefined } { - const ret: { - cancel: ((a: Error) => void) | null; - context: object | null; - } = { cancel: undefined, context: undefined }; - if (cancelOrContext && context) { - ret.cancel = cancelOrContext as (a: Error) => void; - validateCallback(fnName, 'cancel', ret.cancel, true); - - ret.context = context; - validateContextObject(fnName, 'context', ret.context, true); - } else if (cancelOrContext) { - // we have either a cancel callback or a context. - if (typeof cancelOrContext === 'object' && cancelOrContext !== null) { - // it's a context! - ret.context = cancelOrContext; - } else if (typeof cancelOrContext === 'function') { - ret.cancel = cancelOrContext as (a: Error) => void; - } else { - throw new Error( - errorPrefix(fnName, 'cancelOrContext') + - ' must either be a cancel callback or a context object.' - ); - } - } - return ret; - } - - get ref(): Reference { - return new Reference( - this.database, - new _ReferenceImpl(this._delegate._repo, this._delegate._path) - ); - } -} - -export class Reference extends Query implements Compat { - then: Promise['then']; - catch: Promise['catch']; + readonly key: string | null; /** - * Call options: - * new Reference(Repo, Path) or - * new Reference(url: string, string|RepoManager) + * The parent location of a `DatabaseReference`. * - * Externally - this is the firebase.database.Reference type. + * The parent of a root `DatabaseReference` is `null`. */ - constructor(readonly database: Database, readonly _delegate: ExpReference) { - super( - database, - new _QueryImpl(_delegate._repo, _delegate._path, new QueryParams(), false) - ); - } - - /** @returns {?string} */ - getKey(): string | null { - validateArgCount('Reference.key', 0, 0, arguments.length); - return this._delegate.key; - } - - child(pathString: string): Reference { - validateArgCount('Reference.child', 1, 1, arguments.length); - if (typeof pathString === 'number') { - pathString = String(pathString); - } - return new Reference(this.database, child(this._delegate, pathString)); - } - - /** @returns {?Reference} */ - getParent(): Reference | null { - validateArgCount('Reference.parent', 0, 0, arguments.length); - const parent = this._delegate.parent; - return parent ? new Reference(this.database, parent) : null; - } - - /** @returns {!Reference} */ - getRoot(): Reference { - validateArgCount('Reference.root', 0, 0, arguments.length); - return new Reference(this.database, this._delegate.root); - } - - set( - newVal: unknown, - onComplete?: (error: Error | null) => void - ): Promise { - validateArgCount('Reference.set', 1, 2, arguments.length); - validateCallback('Reference.set', 'onComplete', onComplete, true); - const result = set(this._delegate, newVal); - if (onComplete) { - result.then( - () => onComplete(null), - error => onComplete(error) - ); - } - return result; - } - - update( - values: object, - onComplete?: (a: Error | null) => void - ): Promise { - validateArgCount('Reference.update', 1, 2, arguments.length); - - if (Array.isArray(values)) { - const newObjectToMerge: { [k: string]: unknown } = {}; - for (let i = 0; i < values.length; ++i) { - newObjectToMerge['' + i] = values[i]; - } - values = newObjectToMerge; - warn( - 'Passing an Array to Firebase.update() is deprecated. ' + - 'Use set() if you want to overwrite the existing data, or ' + - 'an Object with integer keys if you really do want to ' + - 'only update some of the children.' - ); - } - validateWritablePath('Reference.update', this._delegate._path); - validateCallback('Reference.update', 'onComplete', onComplete, true); - - const result = update(this._delegate, values); - if (onComplete) { - result.then( - () => onComplete(null), - error => onComplete(error) - ); - } - return result; - } - - setWithPriority( - newVal: unknown, - newPriority: string | number | null, - onComplete?: (a: Error | null) => void - ): Promise { - validateArgCount('Reference.setWithPriority', 2, 3, arguments.length); - validateCallback( - 'Reference.setWithPriority', - 'onComplete', - onComplete, - true - ); + readonly parent: DatabaseReference | null; - const result = setWithPriority(this._delegate, newVal, newPriority); - if (onComplete) { - result.then( - () => onComplete(null), - error => onComplete(error) - ); - } - return result; - } - - remove(onComplete?: (a: Error | null) => void): Promise { - validateArgCount('Reference.remove', 0, 1, arguments.length); - validateCallback('Reference.remove', 'onComplete', onComplete, true); - - const result = remove(this._delegate); - if (onComplete) { - result.then( - () => onComplete(null), - error => onComplete(error) - ); - } - return result; - } - - transaction( - transactionUpdate: (currentData: unknown) => unknown, - onComplete?: ( - error: Error | null, - committed: boolean, - dataSnapshot: DataSnapshot | null - ) => void, - applyLocally?: boolean - ): Promise { - validateArgCount('Reference.transaction', 1, 3, arguments.length); - validateCallback( - 'Reference.transaction', - 'transactionUpdate', - transactionUpdate, - false - ); - validateCallback('Reference.transaction', 'onComplete', onComplete, true); - validateBoolean( - 'Reference.transaction', - 'applyLocally', - applyLocally, - true - ); - - const result = runTransaction(this._delegate, transactionUpdate, { - applyLocally - }).then( - transactionResult => - new TransactionResult( - transactionResult.committed, - new DataSnapshot(this.database, transactionResult.snapshot) - ) - ); - if (onComplete) { - result.then( - transactionResult => - onComplete( - null, - transactionResult.committed, - transactionResult.snapshot - ), - error => onComplete(error, false, null) - ); - } - return result; - } - - setPriority( - priority: string | number | null, - onComplete?: (a: Error | null) => void - ): Promise { - validateArgCount('Reference.setPriority', 1, 2, arguments.length); - validateCallback('Reference.setPriority', 'onComplete', onComplete, true); - - const result = setPriority(this._delegate, priority); - if (onComplete) { - result.then( - () => onComplete(null), - error => onComplete(error) - ); - } - return result; - } - - push(value?: unknown, onComplete?: (a: Error | null) => void): Reference { - validateArgCount('Reference.push', 0, 2, arguments.length); - validateCallback('Reference.push', 'onComplete', onComplete, true); - - const expPromise = push(this._delegate, value) as ThenableReferenceImpl; - const promise = expPromise.then( - expRef => new Reference(this.database, expRef) - ); - - if (onComplete) { - promise.then( - () => onComplete(null), - error => onComplete(error) - ); - } - - const result = new Reference(this.database, expPromise); - result.then = promise.then.bind(promise); - result.catch = promise.catch.bind(promise, undefined); - return result; - } + /** The root `DatabaseReference` of the Database. */ + readonly root: DatabaseReference; +} - onDisconnect(): OnDisconnect { - validateWritablePath('Reference.onDisconnect', this._delegate._path); - return new OnDisconnect( - new ExpOnDisconnect(this._delegate._repo, this._delegate._path) - ); - } +/** + * A `Promise` that can also act as a `DatabaseReference` when returned by + * {@link push}. The reference is available immediately and the `Promise` resolves + * as the write to the backend completes. + */ +export interface ThenableReference + extends DatabaseReference, + Pick, 'then' | 'catch'> {} - get key(): string | null { - return this.getKey(); - } +/** A callback that can invoked to remove a listener. */ +export type Unsubscribe = () => void; - get parent(): Reference | null { - return this.getParent(); - } +/** An options objects that can be used to customize a listener. */ +export interface ListenOptions { + /** Whether to remove the listener after its first invocation. */ + readonly onlyOnce?: boolean; +} - get root(): Reference { - return this.getRoot(); - } +export interface ReferenceConstructor { + new (repo: Repo, path: Path): DatabaseReference; } diff --git a/packages/database/src/exp/Reference_impl.ts b/packages/database/src/api/Reference_impl.ts similarity index 100% rename from packages/database/src/exp/Reference_impl.ts rename to packages/database/src/api/Reference_impl.ts diff --git a/packages/database/src/exp/ServerValue.ts b/packages/database/src/api/ServerValue.ts similarity index 100% rename from packages/database/src/exp/ServerValue.ts rename to packages/database/src/api/ServerValue.ts diff --git a/packages/database/src/exp/Transaction.ts b/packages/database/src/api/Transaction.ts similarity index 100% rename from packages/database/src/exp/Transaction.ts rename to packages/database/src/api/Transaction.ts diff --git a/packages/database/src/api/test_access.ts b/packages/database/src/api/test_access.ts index 9e523158e10..2a4872eb3fc 100644 --- a/packages/database/src/api/test_access.ts +++ b/packages/database/src/api/test_access.ts @@ -17,10 +17,9 @@ import { PersistentConnection } from '../core/PersistentConnection'; import { RepoInfo } from '../core/RepoInfo'; -import { repoManagerForceRestClient } from '../exp/Database'; import { Connection } from '../realtime/Connection'; -import { Query } from './Reference'; +import { repoManagerForceRestClient } from './Database'; export const DataConnection = PersistentConnection; @@ -43,6 +42,9 @@ export const DataConnection = PersistentConnection; // RealTimeConnection properties that we use in tests. export const RealTimeConnection = Connection; +/** + * @internal + */ export const hijackHash = function (newHash: () => string) { const oldPut = PersistentConnection.prototype.put; PersistentConnection.prototype.put = function ( @@ -63,12 +65,9 @@ export const hijackHash = function (newHash: () => string) { export const ConnectionTarget = RepoInfo; -export const queryIdentifier = function (query: Query) { - return query._delegate._queryIdentifier; -}; - /** * Forces the RepoManager to create Repos that use ReadonlyRestClient instead of PersistentConnection. + * @internal */ export const forceRestClient = function (forceRestClient: boolean) { repoManagerForceRestClient(forceRestClient); diff --git a/packages/database/src/core/SyncPoint.ts b/packages/database/src/core/SyncPoint.ts index 6751b2242a8..39e65770142 100644 --- a/packages/database/src/core/SyncPoint.ts +++ b/packages/database/src/core/SyncPoint.ts @@ -17,7 +17,7 @@ import { assert } from '@firebase/util'; -import { ReferenceConstructor } from '../exp/Reference'; +import { ReferenceConstructor } from '../api/Reference'; import { Operation } from './operation/Operation'; import { ChildrenNode } from './snap/ChildrenNode'; diff --git a/packages/database/src/core/SyncTree.ts b/packages/database/src/core/SyncTree.ts index 0fad43f5976..a2d0888f03c 100644 --- a/packages/database/src/core/SyncTree.ts +++ b/packages/database/src/core/SyncTree.ts @@ -17,7 +17,7 @@ import { assert } from '@firebase/util'; -import { ReferenceConstructor } from '../exp/Reference'; +import { ReferenceConstructor } from '../api/Reference'; import { AckUserWrite } from './operation/AckUserWrite'; import { ListenComplete } from './operation/ListenComplete'; @@ -831,9 +831,10 @@ function syncTreeQueryKeyForTag_( /** * Given a queryKey (created by makeQueryKey), parse it back into a path and queryId. */ -function syncTreeParseQueryKey_( - queryKey: string -): { queryId: string; path: Path } { +function syncTreeParseQueryKey_(queryKey: string): { + queryId: string; + path: Path; +} { const splitIndex = queryKey.indexOf('$'); assert( splitIndex !== -1 && splitIndex < queryKey.length - 1, diff --git a/packages/database/src/core/snap/ChildrenNode.ts b/packages/database/src/core/snap/ChildrenNode.ts index fce34e484c3..f3ec7f3b109 100644 --- a/packages/database/src/core/snap/ChildrenNode.ts +++ b/packages/database/src/core/snap/ChildrenNode.ts @@ -219,7 +219,7 @@ export class ChildrenNode implements Node { const array: unknown[] = []; // eslint-disable-next-line guard-for-in for (const key in obj) { - array[(key as unknown) as number] = obj[key]; + array[key as unknown as number] = obj[key]; } return array; diff --git a/packages/database/src/core/snap/childSet.ts b/packages/database/src/core/snap/childSet.ts index bef2b193f1f..7fd5d0c36ce 100644 --- a/packages/database/src/core/snap/childSet.ts +++ b/packages/database/src/core/snap/childSet.ts @@ -77,10 +77,10 @@ export const buildChildSet = function ( return null; } else if (length === 1) { namedNode = childList[low]; - key = keyFn ? keyFn(namedNode) : ((namedNode as unknown) as K); + key = keyFn ? keyFn(namedNode) : (namedNode as unknown as K); return new LLRBNode( key, - (namedNode.node as unknown) as V, + namedNode.node as unknown as V, LLRBNode.BLACK, null, null @@ -91,10 +91,10 @@ export const buildChildSet = function ( const left = buildBalancedTree(low, middle); const right = buildBalancedTree(middle + 1, high); namedNode = childList[middle]; - key = keyFn ? keyFn(namedNode) : ((namedNode as unknown) as K); + key = keyFn ? keyFn(namedNode) : (namedNode as unknown as K); return new LLRBNode( key, - (namedNode.node as unknown) as V, + namedNode.node as unknown as V, LLRBNode.BLACK, left, right @@ -113,11 +113,11 @@ export const buildChildSet = function ( index -= chunkSize; const childTree = buildBalancedTree(low + 1, high); const namedNode = childList[low]; - const key: K = keyFn ? keyFn(namedNode) : ((namedNode as unknown) as K); + const key: K = keyFn ? keyFn(namedNode) : (namedNode as unknown as K); attachPennant( new LLRBNode( key, - (namedNode.node as unknown) as V, + namedNode.node as unknown as V, color, null, childTree diff --git a/packages/database/src/core/util/ImmutableTree.ts b/packages/database/src/core/util/ImmutableTree.ts index a6449878dad..cac9d0d4c9c 100644 --- a/packages/database/src/core/util/ImmutableTree.ts +++ b/packages/database/src/core/util/ImmutableTree.ts @@ -91,10 +91,11 @@ export class ImmutableTree { const front = pathGetFront(relativePath); const child = this.children.get(front); if (child !== null) { - const childExistingPathAndValue = child.findRootMostMatchingPathAndValue( - pathPopFront(relativePath), - predicate - ); + const childExistingPathAndValue = + child.findRootMostMatchingPathAndValue( + pathPopFront(relativePath), + predicate + ); if (childExistingPathAndValue != null) { const fullPath = pathChild( new Path(front), diff --git a/packages/database/src/core/util/SortedMap.ts b/packages/database/src/core/util/SortedMap.ts index c27cd7d5226..fa5c6bb65d2 100644 --- a/packages/database/src/core/util/SortedMap.ts +++ b/packages/database/src/core/util/SortedMap.ts @@ -96,7 +96,7 @@ export class SortedMapIterator { if (this.resultGenerator_) { result = this.resultGenerator_(node.key, node.value); } else { - result = ({ key: node.key, value: node.value } as unknown) as T; + result = { key: node.key, value: node.value } as unknown as T; } if (this.isReverse_) { @@ -129,7 +129,7 @@ export class SortedMapIterator { if (this.resultGenerator_) { return this.resultGenerator_(node.key, node.value); } else { - return ({ key: node.key, value: node.value } as unknown) as T; + return { key: node.key, value: node.value } as unknown as T; } } } diff --git a/packages/database/src/core/util/libs/parser.ts b/packages/database/src/core/util/libs/parser.ts index e4f006a635a..92be50daa2f 100644 --- a/packages/database/src/core/util/libs/parser.ts +++ b/packages/database/src/core/util/libs/parser.ts @@ -101,9 +101,7 @@ export const parseRepoInfo = function ( }; }; -export const parseDatabaseURL = function ( - dataURL: string -): { +export const parseDatabaseURL = function (dataURL: string): { host: string; port: number; domain: string; diff --git a/packages/database/src/core/util/validation.ts b/packages/database/src/core/util/validation.ts index 356f03f3ac2..073b99fe844 100644 --- a/packages/database/src/core/util/validation.ts +++ b/packages/database/src/core/util/validation.ts @@ -315,31 +315,6 @@ export const validatePriority = function ( } }; -export const validateEventType = function ( - fnName: string, - eventType: string, - optional: boolean -) { - if (optional && eventType === undefined) { - return; - } - - switch (eventType) { - case 'value': - case 'child_added': - case 'child_removed': - case 'child_changed': - case 'child_moved': - break; - default: - throw new Error( - errorPrefixFxn(fnName, 'eventType') + - 'must be a valid event type = "value", "child_added", "child_removed", ' + - '"child_changed", or "child_moved".' - ); - } -}; - export const validateKey = function ( fnName: string, argumentName: string, @@ -360,6 +335,9 @@ export const validateKey = function ( } }; +/** + * @internal + */ export const validatePathString = function ( fnName: string, argumentName: string, @@ -395,6 +373,9 @@ export const validateRootPathString = function ( validatePathString(fnName, argumentName, pathString, optional); }; +/** + * @internal + */ export const validateWritablePath = function (fnName: string, path: Path) { if (pathGetFront(path) === '.info') { throw new Error(fnName + " failed = Can't modify data under /.info/"); @@ -422,22 +403,6 @@ export const validateUrl = function ( } }; -export const validateBoolean = function ( - fnName: string, - argumentName: string, - bool: unknown, - optional: boolean -) { - if (optional && bool === undefined) { - return; - } - if (typeof bool !== 'boolean') { - throw new Error( - errorPrefixFxn(fnName, argumentName) + 'must be a boolean.' - ); - } -}; - export const validateString = function ( fnName: string, argumentName: string, diff --git a/packages/database/src/core/version.ts b/packages/database/src/core/version.ts index d09ef1c244b..7c18e8c2949 100644 --- a/packages/database/src/core/version.ts +++ b/packages/database/src/core/version.ts @@ -18,7 +18,10 @@ /** The semver (www.semver.org) version of the SDK. */ export let SDK_VERSION = ''; -// SDK_VERSION should be set before any database instance is created +/** + * SDK_VERSION should be set before any database instance is created + * @internal + */ export function setSDKVersion(version: string): void { SDK_VERSION = version; } diff --git a/packages/database/src/core/view/Event.ts b/packages/database/src/core/view/Event.ts index 81d5d3b5d2e..26795a71124 100644 --- a/packages/database/src/core/view/Event.ts +++ b/packages/database/src/core/view/Event.ts @@ -17,7 +17,7 @@ import { stringify } from '@firebase/util'; -import { DataSnapshot as ExpDataSnapshot } from '../../exp/Reference_impl'; +import { DataSnapshot as ExpDataSnapshot } from '../../api/Reference_impl'; import { Path } from '../util/Path'; import { EventRegistration } from './EventRegistration'; diff --git a/packages/database/src/core/view/EventRegistration.ts b/packages/database/src/core/view/EventRegistration.ts index 1a814163e24..bbe18e49d1c 100644 --- a/packages/database/src/core/view/EventRegistration.ts +++ b/packages/database/src/core/view/EventRegistration.ts @@ -17,7 +17,7 @@ import { assert } from '@firebase/util'; -import { DataSnapshot } from '../../exp/Reference_impl'; +import { DataSnapshot } from '../../api/Reference_impl'; import { Repo } from '../Repo'; import { Path } from '../util/Path'; @@ -30,6 +30,8 @@ import { QueryParams } from './QueryParams'; * to the original user-issued callbacks, which allows equality * comparison by reference even though this callbacks are wrapped before * they can be passed to the firebase@exp SDK. + * + * @internal */ export interface UserCallback { (dataSnapshot: DataSnapshot, previousChildName?: string | null): unknown; diff --git a/packages/database/src/core/view/QueryParams.ts b/packages/database/src/core/view/QueryParams.ts index 577970fb255..4deebec0531 100644 --- a/packages/database/src/core/view/QueryParams.ts +++ b/packages/database/src/core/view/QueryParams.ts @@ -63,6 +63,8 @@ const enum REST_QUERY_CONSTANTS { * This class is an immutable-from-the-public-api struct containing a set of query parameters defining a * range to be returned for a particular location. It is assumed that validation of parameters is done at the * user-facing API level, so it is not done here. + * + * @internal */ export class QueryParams { limitSet_ = false; diff --git a/packages/database/src/core/view/ViewProcessor.ts b/packages/database/src/core/view/ViewProcessor.ts index 2739815fcbd..5011d192b7a 100644 --- a/packages/database/src/core/view/ViewProcessor.ts +++ b/packages/database/src/core/view/ViewProcessor.ts @@ -297,12 +297,13 @@ function viewProcessorGenerateEventCacheAfterServerEvent( let newEventChild; if (oldEventSnap.isCompleteForChild(childKey)) { serverNode = viewCache.serverCache.getNode(); - const eventChildUpdate = writeTreeRefCalcEventCacheAfterServerOverwrite( - writesCache, - changePath, - oldEventSnap.getNode(), - serverNode - ); + const eventChildUpdate = + writeTreeRefCalcEventCacheAfterServerOverwrite( + writesCache, + changePath, + oldEventSnap.getNode(), + serverNode + ); if (eventChildUpdate != null) { newEventChild = oldEventSnap .getNode() diff --git a/packages/database/src/exp/Database.ts b/packages/database/src/exp/Database.ts deleted file mode 100644 index d59ca53e191..00000000000 --- a/packages/database/src/exp/Database.ts +++ /dev/null @@ -1,408 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file 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. - */ - -import { AppCheckInternalComponentName } from '@firebase/app-check-interop-types'; -// eslint-disable-next-line import/no-extraneous-dependencies -import { - _FirebaseService, - _getProvider, - FirebaseApp, - getApp -} from '@firebase/app-exp'; -import { FirebaseAuthInternalName } from '@firebase/auth-interop-types'; -import { Provider } from '@firebase/component'; -import { - getModularInstance, - createMockUserToken, - EmulatorMockTokenOptions -} from '@firebase/util'; - -import { AppCheckTokenProvider } from '../core/AppCheckTokenProvider'; -import { - AuthTokenProvider, - EmulatorTokenProvider, - FirebaseAuthTokenProvider -} from '../core/AuthTokenProvider'; -import { Repo, repoInterrupt, repoResume, repoStart } from '../core/Repo'; -import { RepoInfo } from '../core/RepoInfo'; -import { parseRepoInfo } from '../core/util/libs/parser'; -import { newEmptyPath, pathIsEmpty } from '../core/util/Path'; -import { - fatal, - log, - enableLogging as enableLoggingImpl -} from '../core/util/util'; -import { validateUrl } from '../core/util/validation'; - -import { ReferenceImpl } from './Reference_impl'; - -/** - * This variable is also defined in the firebase Node.js Admin SDK. Before - * modifying this definition, consult the definition in: - * - * https://github.com/firebase/firebase-admin-node - * - * and make sure the two are consistent. - */ -const FIREBASE_DATABASE_EMULATOR_HOST_VAR = 'FIREBASE_DATABASE_EMULATOR_HOST'; - -/** - * Creates and caches `Repo` instances. - */ -const repos: { - [appName: string]: { - [dbUrl: string]: Repo; - }; -} = {}; - -/** - * If true, any new `Repo` will be created to use `ReadonlyRestClient` (for testing purposes). - */ -let useRestClient = false; - -/** - * Update an existing `Repo` in place to point to a new host/port. - */ -function repoManagerApplyEmulatorSettings( - repo: Repo, - host: string, - port: number, - tokenProvider?: AuthTokenProvider -): void { - repo.repoInfo_ = new RepoInfo( - `${host}:${port}`, - /* secure= */ false, - repo.repoInfo_.namespace, - repo.repoInfo_.webSocketOnly, - repo.repoInfo_.nodeAdmin, - repo.repoInfo_.persistenceKey, - repo.repoInfo_.includeNamespaceInQueryParams - ); - - if (tokenProvider) { - repo.authTokenProvider_ = tokenProvider; - } -} - -/** - * This function should only ever be called to CREATE a new database instance. - * @internal - */ -export function repoManagerDatabaseFromApp( - app: FirebaseApp, - authProvider: Provider, - appCheckProvider?: Provider, - url?: string, - nodeAdmin?: boolean -): Database { - let dbUrl: string | undefined = url || app.options.databaseURL; - if (dbUrl === undefined) { - if (!app.options.projectId) { - fatal( - "Can't determine Firebase Database URL. Be sure to include " + - ' a Project ID when calling firebase.initializeApp().' - ); - } - - log('Using default host for project ', app.options.projectId); - dbUrl = `${app.options.projectId}-default-rtdb.firebaseio.com`; - } - - let parsedUrl = parseRepoInfo(dbUrl, nodeAdmin); - let repoInfo = parsedUrl.repoInfo; - - let isEmulator: boolean; - - let dbEmulatorHost: string | undefined = undefined; - if (typeof process !== 'undefined') { - dbEmulatorHost = process.env[FIREBASE_DATABASE_EMULATOR_HOST_VAR]; - } - - if (dbEmulatorHost) { - isEmulator = true; - dbUrl = `http://${dbEmulatorHost}?ns=${repoInfo.namespace}`; - parsedUrl = parseRepoInfo(dbUrl, nodeAdmin); - repoInfo = parsedUrl.repoInfo; - } else { - isEmulator = !parsedUrl.repoInfo.secure; - } - - const authTokenProvider = - nodeAdmin && isEmulator - ? new EmulatorTokenProvider(EmulatorTokenProvider.OWNER) - : new FirebaseAuthTokenProvider(app.name, app.options, authProvider); - - validateUrl('Invalid Firebase Database URL', parsedUrl); - if (!pathIsEmpty(parsedUrl.path)) { - fatal( - 'Database URL must point to the root of a Firebase Database ' + - '(not including a child path).' - ); - } - - const repo = repoManagerCreateRepo( - repoInfo, - app, - authTokenProvider, - new AppCheckTokenProvider(app.name, appCheckProvider) - ); - return new Database(repo, app); -} - -/** - * Remove the repo and make sure it is disconnected. - * - */ -function repoManagerDeleteRepo(repo: Repo, appName: string): void { - const appRepos = repos[appName]; - // This should never happen... - if (!appRepos || appRepos[repo.key] !== repo) { - fatal(`Database ${appName}(${repo.repoInfo_}) has already been deleted.`); - } - repoInterrupt(repo); - delete appRepos[repo.key]; -} - -/** - * Ensures a repo doesn't already exist and then creates one using the - * provided app. - * - * @param repoInfo - The metadata about the Repo - * @returns The Repo object for the specified server / repoName. - */ -function repoManagerCreateRepo( - repoInfo: RepoInfo, - app: FirebaseApp, - authTokenProvider: AuthTokenProvider, - appCheckProvider: AppCheckTokenProvider -): Repo { - let appRepos = repos[app.name]; - - if (!appRepos) { - appRepos = {}; - repos[app.name] = appRepos; - } - - let repo = appRepos[repoInfo.toURLString()]; - if (repo) { - fatal( - 'Database initialized multiple times. Please make sure the format of the database URL matches with each database() call.' - ); - } - repo = new Repo(repoInfo, useRestClient, authTokenProvider, appCheckProvider); - appRepos[repoInfo.toURLString()] = repo; - - return repo; -} - -/** - * Forces us to use ReadonlyRestClient instead of PersistentConnection for new Repos. - */ -export function repoManagerForceRestClient(forceRestClient: boolean): void { - useRestClient = forceRestClient; -} - -/** - * Class representing a Firebase Realtime Database. - */ -export class Database implements _FirebaseService { - /** Represents a `Database` instance. */ - readonly 'type' = 'database'; - - /** Track if the instance has been used (root or repo accessed) */ - _instanceStarted: boolean = false; - - /** Backing state for root_ */ - private _rootInternal?: ReferenceImpl; - - /** @hideconstructor */ - constructor( - public _repoInternal: Repo, - /** The {@link @firebase/app#FirebaseApp} associated with this Realtime Database instance. */ - readonly app: FirebaseApp - ) {} - - get _repo(): Repo { - if (!this._instanceStarted) { - repoStart( - this._repoInternal, - this.app.options.appId, - this.app.options['databaseAuthVariableOverride'] - ); - this._instanceStarted = true; - } - return this._repoInternal; - } - - get _root(): ReferenceImpl { - if (!this._rootInternal) { - this._rootInternal = new ReferenceImpl(this._repo, newEmptyPath()); - } - return this._rootInternal; - } - - _delete(): Promise { - if (this._rootInternal !== null) { - repoManagerDeleteRepo(this._repo, this.app.name); - this._repoInternal = null; - this._rootInternal = null; - } - return Promise.resolve(); - } - - _checkNotDeleted(apiName: string) { - if (this._rootInternal === null) { - fatal('Cannot call ' + apiName + ' on a deleted database.'); - } - } -} - -/** - * Returns the instance of the Realtime Database SDK that is associated - * with the provided {@link @firebase/app#FirebaseApp}. Initializes a new instance with - * with default settings if no instance exists or if the existing instance uses - * a custom database URL. - * - * @param app - The {@link @firebase/app#FirebaseApp} instance that the returned Realtime - * Database instance is associated with. - * @param url - The URL of the Realtime Database instance to connect to. If not - * provided, the SDK connects to the default instance of the Firebase App. - * @returns The `Database` instance of the provided app. - */ -export function getDatabase( - app: FirebaseApp = getApp(), - url?: string -): Database { - return _getProvider(app, 'database-exp').getImmediate({ - identifier: url - }) as Database; -} - -/** - * Modify the provided instance to communicate with the Realtime Database - * emulator. - * - *

Note: This method must be called before performing any other operation. - * - * @param db - The instance to modify. - * @param host - The emulator host (ex: localhost) - * @param port - The emulator port (ex: 8080) - * @param options.mockUserToken - the mock auth token to use for unit testing Security Rules - */ -export function connectDatabaseEmulator( - db: Database, - host: string, - port: number, - options: { - mockUserToken?: EmulatorMockTokenOptions | string; - } = {} -): void { - db = getModularInstance(db); - db._checkNotDeleted('useEmulator'); - if (db._instanceStarted) { - fatal( - 'Cannot call useEmulator() after instance has already been initialized.' - ); - } - - const repo = db._repoInternal; - let tokenProvider: EmulatorTokenProvider | undefined = undefined; - if (repo.repoInfo_.nodeAdmin) { - if (options.mockUserToken) { - fatal( - 'mockUserToken is not supported by the Admin SDK. For client access with mock users, please use the "firebase" package instead of "firebase-admin".' - ); - } - tokenProvider = new EmulatorTokenProvider(EmulatorTokenProvider.OWNER); - } else if (options.mockUserToken) { - const token = - typeof options.mockUserToken === 'string' - ? options.mockUserToken - : createMockUserToken(options.mockUserToken, db.app.options.projectId); - tokenProvider = new EmulatorTokenProvider(token); - } - - // Modify the repo to apply emulator settings - repoManagerApplyEmulatorSettings(repo, host, port, tokenProvider); -} - -/** - * Disconnects from the server (all Database operations will be completed - * offline). - * - * The client automatically maintains a persistent connection to the Database - * server, which will remain active indefinitely and reconnect when - * disconnected. However, the `goOffline()` and `goOnline()` methods may be used - * to control the client connection in cases where a persistent connection is - * undesirable. - * - * While offline, the client will no longer receive data updates from the - * Database. However, all Database operations performed locally will continue to - * immediately fire events, allowing your application to continue behaving - * normally. Additionally, each operation performed locally will automatically - * be queued and retried upon reconnection to the Database server. - * - * To reconnect to the Database and begin receiving remote events, see - * `goOnline()`. - * - * @param db - The instance to disconnect. - */ -export function goOffline(db: Database): void { - db = getModularInstance(db); - db._checkNotDeleted('goOffline'); - repoInterrupt(db._repo); -} - -/** - * Reconnects to the server and synchronizes the offline Database state - * with the server state. - * - * This method should be used after disabling the active connection with - * `goOffline()`. Once reconnected, the client will transmit the proper data - * and fire the appropriate events so that your client "catches up" - * automatically. - * - * @param db - The instance to reconnect. - */ -export function goOnline(db: Database): void { - db = getModularInstance(db); - db._checkNotDeleted('goOnline'); - repoResume(db._repo); -} - -/** - * Logs debugging information to the console. - * - * @param enabled - Enables logging if `true`, disables logging if `false`. - * @param persistent - Remembers the logging state between page refreshes if - * `true`. - */ -export function enableLogging(enabled: boolean, persistent?: boolean); - -/** - * Logs debugging information to the console. - * - * @param logger - A custom logger function to control how things get logged. - */ -export function enableLogging(logger: (message: string) => unknown); - -export function enableLogging( - logger: boolean | ((message: string) => unknown), - persistent?: boolean -): void { - enableLoggingImpl(logger, persistent); -} diff --git a/packages/database/src/exp/Reference.ts b/packages/database/src/exp/Reference.ts deleted file mode 100644 index c2e97aa229e..00000000000 --- a/packages/database/src/exp/Reference.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { Repo } from '../core/Repo'; -import { Path } from '../core/util/Path'; -import { QueryContext } from '../core/view/EventRegistration'; - -/** - * @license - * Copyright 2021 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file 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. - */ - -/** - * A `Query` sorts and filters the data at a Database location so only a subset - * of the child data is included. This can be used to order a collection of - * data by some attribute (for example, height of dinosaurs) as well as to - * restrict a large list of items (for example, chat messages) down to a number - * suitable for synchronizing to the client. Queries are created by chaining - * together one or more of the filter methods defined here. - * - * Just as with a `DatabaseReference`, you can receive data from a `Query` by using the - * `on*()` methods. You will only receive events and `DataSnapshot`s for the - * subset of the data that matches your query. - * - * See {@link https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data} - * for more information. - */ -export interface Query extends QueryContext { - /** The `DatabaseReference` for the `Query`'s location. */ - readonly ref: DatabaseReference; - - /** - * Returns whether or not the current and provided queries represent the same - * location, have the same query parameters, and are from the same instance of - * `FirebaseApp`. - * - * Two `DatabaseReference` objects are equivalent if they represent the same location - * and are from the same instance of `FirebaseApp`. - * - * Two `Query` objects are equivalent if they represent the same location, - * have the same query parameters, and are from the same instance of - * `FirebaseApp`. Equivalent queries share the same sort order, limits, and - * starting and ending points. - * - * @param other - The query to compare against. - * @returns Whether or not the current and provided queries are equivalent. - */ - isEqual(other: Query | null): boolean; - - /** - * Returns a JSON-serializable representation of this object. - * - * @returns A JSON-serializable representation of this object. - */ - toJSON(): string; - - /** - * Gets the absolute URL for this location. - * - * The `toString()` method returns a URL that is ready to be put into a - * browser, curl command, or a `refFromURL()` call. Since all of those expect - * the URL to be url-encoded, `toString()` returns an encoded URL. - * - * Append '.json' to the returned URL when typed into a browser to download - * JSON-formatted data. If the location is secured (that is, not publicly - * readable), you will get a permission-denied error. - * - * @returns The absolute URL for this location. - */ - toString(): string; -} - -/** - * A `DatabaseReference` represents a specific location in your Database and can be used - * for reading or writing data to that Database location. - * - * You can reference the root or child location in your Database by calling - * `ref()` or `ref("child/path")`. - * - * Writing is done with the `set()` method and reading can be done with the - * `on*()` method. See {@link - * https://firebase.google.com/docs/database/web/read-and-write} - */ -export interface DatabaseReference extends Query { - /** - * The last part of the `DatabaseReference`'s path. - * - * For example, `"ada"` is the key for - * `https://.firebaseio.com/users/ada`. - * - * The key of a root `DatabaseReference` is `null`. - */ - readonly key: string | null; - - /** - * The parent location of a `DatabaseReference`. - * - * The parent of a root `DatabaseReference` is `null`. - */ - readonly parent: DatabaseReference | null; - - /** The root `DatabaseReference` of the Database. */ - readonly root: DatabaseReference; -} - -/** - * A `Promise` that can also act as a `DatabaseReference` when returned by - * {@link push}. The reference is available immediately and the `Promise` resolves - * as the write to the backend completes. - */ -export interface ThenableReference - extends DatabaseReference, - Pick, 'then' | 'catch'> {} - -/** A callback that can invoked to remove a listener. */ -export type Unsubscribe = () => void; - -/** An options objects that can be used to customize a listener. */ -export interface ListenOptions { - /** Whether to remove the listener after its first invocation. */ - readonly onlyOnce?: boolean; -} - -export interface ReferenceConstructor { - new (repo: Repo, path: Path): DatabaseReference; -} diff --git a/packages/database/exp/index.node.ts b/packages/database/src/index.node.ts similarity index 100% rename from packages/database/exp/index.node.ts rename to packages/database/src/index.node.ts diff --git a/packages/database/exp/index.ts b/packages/database/src/index.ts similarity index 83% rename from packages/database/exp/index.ts rename to packages/database/src/index.ts index 30c71177e81..2e763d6e36e 100644 --- a/packages/database/exp/index.ts +++ b/packages/database/src/index.ts @@ -21,8 +21,15 @@ * limitations under the License. */ +import { Database } from './api/Database'; import { registerDatabase } from './register'; export * from './api'; registerDatabase(); + +declare module '@firebase/component' { + interface NameServiceMapping { + 'database': Database; + } +} diff --git a/packages/database/src/realtime/BrowserPollConnection.ts b/packages/database/src/realtime/BrowserPollConnection.ts index c41a9fb97a1..9f2340417b5 100644 --- a/packages/database/src/realtime/BrowserPollConnection.ts +++ b/packages/database/src/realtime/BrowserPollConnection.ts @@ -209,9 +209,8 @@ export class BrowserPollConnection implements Transport { Math.random() * 100000000 ); if (this.scriptTagHolder.uniqueCallbackIdentifier) { - urlParams[ - FIREBASE_LONGPOLL_CALLBACK_ID_PARAM - ] = this.scriptTagHolder.uniqueCallbackIdentifier; + urlParams[FIREBASE_LONGPOLL_CALLBACK_ID_PARAM] = + this.scriptTagHolder.uniqueCallbackIdentifier; } urlParams[VERSION_PARAM] = PROTOCOL_VERSION; if (this.transportSessionId) { @@ -456,9 +455,8 @@ export class FirebaseIFrameScriptHolder { window[ FIREBASE_LONGPOLL_COMMAND_CB_NAME + this.uniqueCallbackIdentifier ] = commandCB; - window[ - FIREBASE_LONGPOLL_DATA_CB_NAME + this.uniqueCallbackIdentifier - ] = onMessageCB; + window[FIREBASE_LONGPOLL_DATA_CB_NAME + this.uniqueCallbackIdentifier] = + onMessageCB; //Create an iframe for us to add script tags to. this.myIFrame = FirebaseIFrameScriptHolder.createIFrame_(); @@ -721,18 +719,19 @@ export class FirebaseIFrameScriptHolder { newScript.async = true; newScript.src = url; // eslint-disable-next-line @typescript-eslint/no-explicit-any - newScript.onload = (newScript as any).onreadystatechange = function () { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const rstate = (newScript as any).readyState; - if (!rstate || rstate === 'loaded' || rstate === 'complete') { + newScript.onload = (newScript as any).onreadystatechange = + function () { // eslint-disable-next-line @typescript-eslint/no-explicit-any - newScript.onload = (newScript as any).onreadystatechange = null; - if (newScript.parentNode) { - newScript.parentNode.removeChild(newScript); + const rstate = (newScript as any).readyState; + if (!rstate || rstate === 'loaded' || rstate === 'complete') { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + newScript.onload = (newScript as any).onreadystatechange = null; + if (newScript.parentNode) { + newScript.parentNode.removeChild(newScript); + } + loadCB(); } - loadCB(); - } - }; + }; newScript.onerror = () => { log('Long-poll script failed to load: ' + url); this.sendNewPolls = false; diff --git a/packages/database/src/realtime/Constants.ts b/packages/database/src/realtime/Constants.ts index 4dba53f75dc..f7bca227db3 100644 --- a/packages/database/src/realtime/Constants.ts +++ b/packages/database/src/realtime/Constants.ts @@ -27,7 +27,8 @@ export const FORGE_REF = 'f'; // Matches console.firebase.google.com, firebase-console-*.corp.google.com and // firebase.corp.google.com -export const FORGE_DOMAIN_RE = /(console\.firebase|firebase-console-\w+\.corp|firebase\.corp)\.google\.com/; +export const FORGE_DOMAIN_RE = + /(console\.firebase|firebase-console-\w+\.corp|firebase\.corp)\.google\.com/; export const LAST_SESSION_PARAM = 'ls'; diff --git a/packages/database/exp/register.ts b/packages/database/src/register.ts similarity index 88% rename from packages/database/exp/register.ts rename to packages/database/src/register.ts index 4f06e49727f..bd3f9f8456c 100644 --- a/packages/database/exp/register.ts +++ b/packages/database/src/register.ts @@ -24,19 +24,14 @@ import { Component, ComponentType } from '@firebase/component'; import { name, version } from '../package.json'; import { setSDKVersion } from '../src/core/version'; -import { Database, repoManagerDatabaseFromApp } from '../src/exp/Database'; -declare module '@firebase/component' { - interface NameServiceMapping { - 'database-exp': Database; - } -} +import { repoManagerDatabaseFromApp } from './api/Database'; export function registerDatabase(variant?: string): void { setSDKVersion(SDK_VERSION); _registerComponent( new Component( - 'database-exp', + 'database', (container, { instanceIdentifier: url }) => { const app = container.getProvider('app-exp').getImmediate()!; const authProvider = container.getProvider('auth-internal'); diff --git a/packages/database/test/exp/integration.test.ts b/packages/database/test/exp/integration.test.ts index c2e276d8794..f524ce9f546 100644 --- a/packages/database/test/exp/integration.test.ts +++ b/packages/database/test/exp/integration.test.ts @@ -20,6 +20,7 @@ import { initializeApp, deleteApp } from '@firebase/app-exp'; import { Deferred } from '@firebase/util'; import { expect } from 'chai'; +import { onValue, set } from '../../src/api/Reference_impl'; import { get, getDatabase, @@ -29,8 +30,7 @@ import { ref, refFromURL, runTransaction -} from '../../exp/index'; -import { onValue, set } from '../../src/exp/Reference_impl'; +} from '../../src/index'; import { EventAccumulatorFactory } from '../helpers/EventAccumulator'; import { DATABASE_ADDRESS, DATABASE_URL } from '../helpers/util'; diff --git a/packages/database/test/helpers/util.ts b/packages/database/test/helpers/util.ts index ec2a0a8f733..fc27e9afcd3 100644 --- a/packages/database/test/helpers/util.ts +++ b/packages/database/test/helpers/util.ts @@ -1,38 +1,9 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file 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. - */ - -declare let MozWebSocket: WebSocket; - -import '../../index'; - -import firebase from '@firebase/app'; -import { _FirebaseNamespace } from '@firebase/app-types/private'; -import { Component, ComponentType } from '@firebase/component'; - -import { Query, Reference } from '../../src/api/Reference'; import { ConnectionTarget } from '../../src/api/test_access'; -import { Path } from '../../src/core/util/Path'; // eslint-disable-next-line @typescript-eslint/no-require-imports export const TEST_PROJECT = require('../../../../config/project.json'); - const EMULATOR_PORT = process.env.RTDB_EMULATOR_PORT; const EMULATOR_NAMESPACE = process.env.RTDB_EMULATOR_NAMESPACE; - const USE_EMULATOR = !!EMULATOR_PORT; /* @@ -51,144 +22,6 @@ export const DATABASE_URL = USE_EMULATOR ? `${DATABASE_ADDRESS}?ns=${EMULATOR_NAMESPACE}` : TEST_PROJECT.databaseURL; -console.log(`USE_EMULATOR: ${USE_EMULATOR}. DATABASE_URL: ${DATABASE_URL}.`); - -let numDatabases = 0; - -// mock authentication functions for testing -(firebase as _FirebaseNamespace).INTERNAL.registerComponent( - new Component( - 'auth-internal', - () => ({ - getToken: async () => null, - addAuthTokenListener: () => {}, - removeAuthTokenListener: () => {}, - getUid: () => null - }), - ComponentType.PRIVATE - ) -); - -export function createTestApp() { - const app = firebase.initializeApp({ databaseURL: DATABASE_URL }); - return app; -} - -/** - * Gets or creates a root node to the test namespace. All calls sharing the - * value of opt_i will share an app context. - */ -export function getRootNode(i = 0, ref?: string) { - if (i + 1 > numDatabases) { - numDatabases = i + 1; - } - let app; - try { - app = firebase.app('TEST-' + i); - } catch (e) { - app = firebase.initializeApp({ databaseURL: DATABASE_URL }, 'TEST-' + i); - } - const db = app.database(); - return db.ref(ref); -} - -/** - * Create multiple refs to the same top level - * push key - each on it's own Firebase.Context. - */ -export function getRandomNode(numNodes?): Reference | Reference[] { - if (numNodes === undefined) { - return getRandomNode(1)[0] as Reference; - } - - let child; - const nodeList = []; - for (let i = 0; i < numNodes; i++) { - const ref = getRootNode(i); - if (child === undefined) { - child = ref.push().key; - } - - nodeList[i] = ref.child(child); - } - - return nodeList as Reference[]; -} - -export function getQueryValue(query: Query) { - return query.once('value').then(snap => snap.val()); -} - -export function pause(milliseconds: number) { - return new Promise(resolve => { - setTimeout(() => resolve(), milliseconds); - }); -} - -export function getPath(query: Query) { - return query.toString().replace(DATABASE_ADDRESS, ''); -} - -export function shuffle(arr, randFn = Math.random) { - for (let i = arr.length - 1; i > 0; i--) { - const j = Math.floor(randFn() * (i + 1)); - const tmp = arr[i]; - arr[i] = arr[j]; - arr[j] = tmp; - } -} - -let freshRepoId = 1; -const activeFreshApps = []; - -export function getFreshRepo(path: Path) { - const app = firebase.initializeApp( - { databaseURL: DATABASE_URL }, - 'ISOLATED_REPO_' + freshRepoId++ - ); - activeFreshApps.push(app); - return (app as any).database().ref(path.toString()); -} - -export function getFreshRepoFromReference(ref) { - const host = ref.root.toString(); - const path = ref.toString().replace(host, ''); - return getFreshRepo(path); -} - -// Little helpers to get the currently cached snapshot / value. -export function getSnap(path) { - let snap; - const callback = function (snapshot) { - snap = snapshot; - }; - path.once('value', callback); - return snap; -} - -export function getVal(path) { - const snap = getSnap(path); - return snap ? snap.val() : undefined; -} - -export function canCreateExtraConnections() { - return ( - typeof MozWebSocket !== 'undefined' || typeof WebSocket !== 'undefined' - ); -} - -export function buildObjFromKey(key) { - const keys = key.split('.'); - const obj = {}; - let parent = obj; - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - parent[key] = i < keys.length - 1 ? {} : 'test_value'; - parent = parent[key]; - } - return obj; -} - export function testRepoInfo(url) { const regex = /https?:\/\/(.*).firebaseio.com/; const match = url.match(regex); @@ -211,3 +44,12 @@ export function repoInfoForConnectionTest() { return testRepoInfo(TEST_PROJECT.databaseURL); } } + +export function shuffle(arr, randFn = Math.random) { + for (let i = arr.length - 1; i > 0; i--) { + const j = Math.floor(randFn() * (i + 1)); + const tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; + } +}