diff --git a/common/api-review/storage.api.md b/common/api-review/storage.api.md index 92906ced407..3908f2db229 100644 --- a/common/api-review/storage.api.md +++ b/common/api-review/storage.api.md @@ -10,7 +10,7 @@ import { EmulatorMockTokenOptions } from '@firebase/util'; import { FirebaseApp } from '@firebase/app'; import { FirebaseAuthInternalName } from '@firebase/auth-interop-types'; import { FirebaseError } from '@firebase/util'; -import { _FirebaseService } from '@firebase/app'; +import { _FirebaseService } from '@firebase/app-exp'; import { NextFn } from '@firebase/util'; import { Provider } from '@firebase/component'; import { Subscribe } from '@firebase/util'; @@ -21,6 +21,11 @@ export function connectStorageEmulator(storage: FirebaseStorage, host: string, p mockUserToken?: EmulatorMockTokenOptions | string; }): void; +// Warning: (ae-forgotten-export) The symbol "StringData" needs to be exported by the entry point index.d.ts +// +// @internal (undocumented) +export function _dataFromString(format: StringFormat, stringData: string): StringData; + // @public export function deleteObject(ref: StorageReference): Promise; @@ -51,6 +56,55 @@ export interface FirebaseStorageError extends FirebaseError { serverResponse: string | null; } +// @public +export class _FirebaseStorageImpl implements FirebaseStorage { + constructor( + app: FirebaseApp, _authProvider: Provider, + _appCheckProvider: Provider, + _pool: ConnectionPool, _url?: string | undefined, _firebaseVersion?: string | undefined); + readonly app: FirebaseApp; + // @internal (undocumented) + readonly _appCheckProvider: Provider; + // (undocumented) + protected readonly _appId: string | null; + // (undocumented) + readonly _authProvider: Provider; + // Warning: (ae-incompatible-release-tags) The symbol "_bucket" is marked as @public, but its signature references "Location" which is marked as @internal + // + // (undocumented) + _bucket: _Location | null; + _delete(): Promise; + // (undocumented) + readonly _firebaseVersion?: string | undefined; + // (undocumented) + _getAppCheckToken(): Promise; + // (undocumented) + _getAuthToken(): Promise; + // (undocumented) + get host(): string; + set host(host: string); + // Warning: (ae-forgotten-export) The symbol "RequestInfo" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "Request" needs to be exported by the entry point index.d.ts + // + // (undocumented) + _makeRequest(requestInfo: RequestInfo_2, authToken: string | null, appCheckToken: string | null): Request_2; + // (undocumented) + makeRequestWithTokens(requestInfo: RequestInfo_2): Promise>; + // Warning: (ae-incompatible-release-tags) The symbol "_makeStorageReference" is marked as @public, but its signature references "Location" which is marked as @internal + // Warning: (ae-incompatible-release-tags) The symbol "_makeStorageReference" is marked as @public, but its signature references "Reference" which is marked as @internal + _makeStorageReference(loc: _Location): _Reference; + get maxOperationRetryTime(): number; + set maxOperationRetryTime(time: number); + get maxUploadRetryTime(): number; + set maxUploadRetryTime(time: number); + // Warning: (ae-forgotten-export) The symbol "ConnectionPool" needs to be exported by the entry point index.d.ts + // + // @internal (undocumented) + readonly _pool: ConnectionPool; + // (undocumented) + readonly _url?: string | undefined; +} + // @public export interface FullMetadata extends UploadMetadata { bucket: string; @@ -77,6 +131,14 @@ export function getMetadata(ref: StorageReference): Promise; // @public export function getStorage(app?: FirebaseApp, bucketUrl?: string): FirebaseStorage; +// Warning: (ae-forgotten-export) The symbol "FirebaseStorageError" needs to be exported by the entry point index.d.ts +// +// @internal (undocumented) +export function _invalidArgument(message: string): FirebaseStorageError_2; + +// @internal (undocumented) +export function _invalidRootOperation(name: string): FirebaseStorageError_2; + // @public export function list(ref: StorageReference, options?: ListOptions): Promise; @@ -123,18 +185,17 @@ export function ref(storageOrRef: FirebaseStorage | StorageReference, path?: str // @internal export class _Reference { - // Warning: (ae-forgotten-export) The symbol "FirebaseStorageImpl" needs to be exported by the entry point index.d.ts - constructor(_service: FirebaseStorageImpl, location: string | _Location); + constructor(_service: _FirebaseStorageImpl, location: string | _Location); get bucket(): string; get fullPath(): string; // (undocumented) _location: _Location; get name(): string; // (undocumented) - protected _newRef(service: FirebaseStorageImpl, location: _Location): _Reference; + protected _newRef(service: _FirebaseStorageImpl, location: _Location): _Reference; get parent(): _Reference | null; get root(): _Reference; - get storage(): FirebaseStorageImpl; + get storage(): _FirebaseStorageImpl; _throwIfRoot(name: string): void; // @override toString(): string; @@ -187,9 +248,29 @@ export const StringFormat: { // @public export type TaskEvent = 'state_changed'; +// @public +export type _TaskEvent = string; + +// @public +export const _TaskEvent: { + STATE_CHANGED: string; +}; + // @public export type TaskState = 'running' | 'paused' | 'success' | 'canceled' | 'error'; +// @public +export type _TaskState = typeof _TaskState[keyof typeof _TaskState]; + +// @public +export const _TaskState: { + readonly RUNNING: "running"; + readonly PAUSED: "paused"; + readonly SUCCESS: "success"; + readonly CANCELED: "canceled"; + readonly ERROR: "error"; +}; + // @public export function updateMetadata(ref: StorageReference, metadata: SettableMetadata): Promise; @@ -232,21 +313,15 @@ export class _UploadTask { catch(onRejected: (p1: FirebaseStorageError_2) => T | Promise): Promise; // Warning: (ae-forgotten-export) The symbol "Metadata" needs to be exported by the entry point index.d.ts _metadata: Metadata | null; - // Warning: (ae-forgotten-export) The symbol "TaskEvent" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "StorageObserver" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "ErrorFn" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "CompleteFn" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "Unsubscribe" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "Subscribe" needs to be exported by the entry point index.d.ts - on(type: TaskEvent_2, nextOrObserver?: StorageObserver_2 | ((a: UploadTaskSnapshot_2) => unknown), error?: ErrorFn, completed?: CompleteFn_2): Unsubscribe_2 | Subscribe_2; + on(type: _TaskEvent, nextOrObserver?: StorageObserver | null | ((snapshot: UploadTaskSnapshot) => unknown), error?: ((a: FirebaseStorageError_2) => unknown) | null, completed?: Unsubscribe_2 | null): Unsubscribe_2 | Subscribe_2; pause(): boolean; resume(): boolean; - // Warning: (ae-forgotten-export) The symbol "UploadTaskSnapshot" needs to be exported by the entry point index.d.ts - get snapshot(): UploadTaskSnapshot_2; + get snapshot(): UploadTaskSnapshot; // Warning: (ae-forgotten-export) The symbol "InternalTaskState" needs to be exported by the entry point index.d.ts _state: InternalTaskState; - // Warning: (ae-forgotten-export) The symbol "FirebaseStorageError" needs to be exported by the entry point index.d.ts - then(onFulfilled?: ((value: UploadTaskSnapshot_2) => U | Promise) | null, onRejected?: ((error: FirebaseStorageError_2) => U | Promise) | null): Promise; + then(onFulfilled?: ((value: UploadTaskSnapshot) => U | Promise) | null, onRejected?: ((error: FirebaseStorageError_2) => U | Promise) | null): Promise; _transferred: number; } diff --git a/packages-exp/app-exp/src/constants.ts b/packages-exp/app-exp/src/constants.ts index f00a93f0da9..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'; @@ -36,7 +36,7 @@ import { name as performanceCompatName } from '../../../packages-exp/performance import { name as remoteConfigName } from '../../../packages-exp/remote-config-exp/package.json'; import { name as remoteConfigCompatName } from '../../../packages-exp/remote-config-compat/package.json'; import { name as storageName } from '../../../packages/storage/package.json'; -import { name as storageCompatName } from '../../../packages/storage/compat/package.json'; +import { name as storageCompatName } from '../../../packages/storage-compat/package.json'; import { name as firestoreName } from '../../../packages/firestore/package.json'; import { name as firestoreCompatName } from '../../../packages/firestore/compat/package.json'; import { name as packageName } from '../../../packages-exp/firebase-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; + } +} diff --git a/packages/rules-unit-testing/src/api/index.ts b/packages/rules-unit-testing/src/api/index.ts index 51d16209187..9cf12e24af9 100644 --- a/packages/rules-unit-testing/src/api/index.ts +++ b/packages/rules-unit-testing/src/api/index.ts @@ -160,7 +160,7 @@ export type FirebaseEmulatorOptions = { function trimmedBase64Encode(val: string): string { // Use base64url encoding and remove padding in the end (dot characters). - return base64Encode(val).replace(/\./g, ""); + return base64Encode(val).replace(/\./g, ''); } function createUnsecuredJwt(token: TokenOptions, projectId?: string): string { @@ -498,7 +498,7 @@ function initializeApp( ComponentType.PRIVATE ); - ((app as unknown) as _FirebaseApp)._addOrOverwriteComponent( + (app as unknown as _FirebaseApp)._addOrOverwriteComponent( mockAuthComponent ); } @@ -703,7 +703,7 @@ export function assertFails(pr: Promise): any { errCode === 'permission-denied' || errCode === 'permission_denied' || errMessage.indexOf('permission_denied') >= 0 || - errMessage.indexOf('permission denied') >= 0 || + errMessage.indexOf('permission denied') >= 0 || // Storage permission errors contain message: (storage/unauthorized) errMessage.indexOf('unauthorized') >= 0; diff --git a/packages/storage/rollup.shared.js b/packages/storage-compat/.eslintrc.js similarity index 55% rename from packages/storage/rollup.shared.js rename to packages/storage-compat/.eslintrc.js index 59562480d49..ffe0e481071 100644 --- a/packages/storage/rollup.shared.js +++ b/packages/storage-compat/.eslintrc.js @@ -1,6 +1,6 @@ /** * @license - * Copyright 2021 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. @@ -15,20 +15,23 @@ * limitations under the License. */ -/** - * Returns an replacement configuration for `@rollup/plugin-alias` that replaces - * references to platform-specific files with implementations for the provided - * target platform. - */ -function generateAliasConfig(platform) { - return { - entries: [ +const path = require('path'); + +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': [ + 'error', { - find: /^(.*)\/platform\/([^.\/]*)(\.ts)?$/, - replacement: `$1\/platform/${platform}/$2.ts` + varsIgnorePattern: '^_', + args: 'none' } ] - }; -} - -exports.generateAliasConfig = generateAliasConfig; + } +}; diff --git a/packages/storage-compat/README.md b/packages/storage-compat/README.md new file mode 100644 index 00000000000..7c1c82bd424 --- /dev/null +++ b/packages/storage-compat/README.md @@ -0,0 +1,5 @@ +# @firebase/storage + +This is the Cloud Storage 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/storage-compat/karma.conf.js b/packages/storage-compat/karma.conf.js new file mode 100644 index 00000000000..3b5d7e3f39b --- /dev/null +++ b/packages/storage-compat/karma.conf.js @@ -0,0 +1,46 @@ +/** + * @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 { argv } = require('yargs'); + +module.exports = function (config) { + const karmaConfig = Object.assign({}, karmaBase, { + // files to load into karma + files: getTestFiles(argv), + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['mocha'] + }); + + config.set(karmaConfig); +}; + +function getTestFiles(argv) { + let unitTestFiles = ['test/unit/*']; + let integrationTestFiles = ['test/integration/*']; + + if (argv.unit) { + return unitTestFiles; + } else if (argv.integration) { + return integrationTestFiles; + } else { + return [...unitTestFiles, ...integrationTestFiles]; + } +} + +module.exports.files = getTestFiles(argv); diff --git a/packages/storage-compat/package.json b/packages/storage-compat/package.json new file mode 100644 index 00000000000..b629bf8298a --- /dev/null +++ b/packages/storage-compat/package.json @@ -0,0 +1,49 @@ +{ + "name": "@firebase/storage-compat", + "version": "0.0.900", + "description": "The Firebase Firestore compatibility package", + "author": "Firebase (https://firebase.google.com/)", + "main": "./dist/index.cjs.js", + "browser": "./dist/index.esm2017.js", + "module": "./dist/index.esm2017.js", + "esm5": "./dist/index.esm5.js", + "license": "Apache-2.0", + "typings": "./dist/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'", + "build": "rollup -c rollup.config.js && yarn add-compat-overloads", + "build:deps": "lerna run --scope @firebase/storage-compat --include-dependencies build", + "dev": "rollup -c -w", + "test": "run-p test:browser test:node lint", + "test:ci": "node ../../scripts/run_tests_in_ci.js -s test:browser test:node", + "test:browser:unit": "karma start --single-run --unit", + "test:browser:integration": "karma start --single-run --integration", + "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.ts --config ../../config/mocharc.node.js", + "test:debug": "karma start --browser=Chrome", + "prettier": "prettier --write 'src/**/*.ts' 'test/**/*.ts'", + "add-compat-overloads": "ts-node-script ../../scripts/exp/create-overloads.ts -i ../storage/dist/storage-public.d.ts -o dist/src/index.d.ts -a -r FirebaseStorage:types.FirebaseStorage -r StorageReference:types.Reference -r FirebaseApp:FirebaseAppCompat --moduleToEnhance @firebase/storage" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + }, + "dependencies": { + "@firebase/storage": "0.7.0", + "@firebase/storage-types": "0.5.0", + "@firebase/util": "1.3.0", + "@firebase/component": "0.5.6", + "tslib": "^2.1.0" + }, + "devDependencies": { + "@firebase/app-compat": "0.x", + "@firebase/auth-compat": "0.x", + "rollup": "2.52.2", + "@rollup/plugin-json": "4.1.0", + "rollup-plugin-typescript2": "0.30.0", + "typescript": "4.2.2" + }, + "files": [ + "dist" + ] +} \ No newline at end of file diff --git a/packages/storage/rollup.config.compat.js b/packages/storage-compat/rollup.config.js similarity index 59% rename from packages/storage/rollup.config.compat.js rename to packages/storage-compat/rollup.config.js index a1b6321157d..c220eb88893 100644 --- a/packages/storage/rollup.config.compat.js +++ b/packages/storage-compat/rollup.config.js @@ -18,55 +18,39 @@ import json from '@rollup/plugin-json'; import typescriptPlugin from 'rollup-plugin-typescript2'; import typescript from 'typescript'; -import alias from '@rollup/plugin-alias'; import pkg from './package.json'; -import { getImportPathTransformer } from '../../scripts/exp/ts-transform-import-path'; - -const { generateAliasConfig } = require('./rollup.shared'); - -const deps = [ - ...Object.keys(Object.assign({}, pkg.peerDependencies, pkg.dependencies)), - '@firebase/storage' -]; +const deps = Object.keys( + Object.assign({}, pkg.peerDependencies, pkg.dependencies) +); /** * ES5 Builds */ const es5BuildPlugins = [ typescriptPlugin({ typescript, - abortOnError: false, - transformers: [ - getImportPathTransformer({ - // ../exp/index - pattern: /^.*exp\/api$/, - template: ['@firebase/storage'] - }) - ] + abortOnError: false }), json() ]; const es5Builds = [ { - input: './compat/index.ts', + input: './src/index.ts', output: [ { - dir: 'dist/compat/cjs', + file: pkg.main, format: 'cjs', sourcemap: true }, { - dir: 'dist/compat/esm5', + file: pkg.esm5, format: 'es', sourcemap: true } ], - plugins: [alias(generateAliasConfig('browser')), ...es5BuildPlugins], - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), - treeshake: { - moduleSideEffects: false - } + plugins: [...es5BuildPlugins], + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) } ]; @@ -77,13 +61,6 @@ const es2017BuildPlugins = [ typescriptPlugin({ typescript, abortOnError: false, - transformers: [ - getImportPathTransformer({ - // ../exp/index - pattern: /^.*exp\/api$/, - template: ['@firebase/storage'] - }) - ], tsconfigOverride: { compilerOptions: { target: 'es2017' @@ -95,17 +72,14 @@ const es2017BuildPlugins = [ const es2017Builds = [ { - input: './compat/index.ts', + input: './src/index.ts', output: { - dir: 'dist/compat/esm2017', + file: pkg.browser, format: 'es', sourcemap: true }, - plugins: [alias(generateAliasConfig('browser')), ...es2017BuildPlugins], - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), - treeshake: { - moduleSideEffects: false - } + plugins: [...es2017BuildPlugins], + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) } ]; diff --git a/packages/storage/compat/index.ts b/packages/storage-compat/src/index.ts similarity index 90% rename from packages/storage/compat/index.ts rename to packages/storage-compat/src/index.ts index 88ac166e1f9..a4d7e913f3d 100644 --- a/packages/storage/compat/index.ts +++ b/packages/storage-compat/src/index.ts @@ -18,8 +18,11 @@ // eslint-disable-next-line import/no-extraneous-dependencies import firebase from '@firebase/app-compat'; import { _FirebaseNamespace } from '@firebase/app-types/private'; -import { StringFormat } from '../src/implementation/string'; -import { TaskEvent, TaskState } from '../src/implementation/taskenums'; +import { + StringFormat, + _TaskEvent as TaskEvent, + _TaskState as TaskState +} from '@firebase/storage'; import { ReferenceCompat } from './reference'; import { StorageServiceCompat } from './service'; @@ -31,12 +34,12 @@ import { InstanceFactoryOptions } from '@firebase/component'; -import { name, version } from './package.json'; +import { name, version } from '../package.json'; /** * Type constant for Firebase Storage. */ -const STORAGE_TYPE = 'storage'; +const STORAGE_TYPE = 'storage-compat'; function factory( container: ComponentContainer, @@ -45,7 +48,7 @@ function factory( // Dependencies const app = container.getProvider('app-compat').getImmediate(); const storageExp = container - .getProvider('storage-exp') + .getProvider('storage') .getImmediate({ identifier: url }); const storageServiceCompat: StorageServiceCompat = new StorageServiceCompat( @@ -73,7 +76,7 @@ export function registerStorage(instance: _FirebaseNamespace): void { instance.registerVersion(name, version); } -registerStorage((firebase as unknown) as _FirebaseNamespace); +registerStorage(firebase as unknown as _FirebaseNamespace); /** * Define extension behavior for `registerStorage` diff --git a/packages/storage/compat/list.ts b/packages/storage-compat/src/list.ts similarity index 96% rename from packages/storage/compat/list.ts rename to packages/storage-compat/src/list.ts index da394d20849..60c6a3b34e1 100644 --- a/packages/storage/compat/list.ts +++ b/packages/storage-compat/src/list.ts @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ListResult } from '../exp/api'; +import { ListResult } from '@firebase/storage'; import * as types from '@firebase/storage-types'; import { ReferenceCompat } from './reference'; import { StorageServiceCompat } from './service'; diff --git a/packages/storage/compat/reference.ts b/packages/storage-compat/src/reference.ts similarity index 92% rename from packages/storage/compat/reference.ts rename to packages/storage-compat/src/reference.ts index d882642d6bf..ea2af1004fd 100644 --- a/packages/storage/compat/reference.ts +++ b/packages/storage-compat/src/reference.ts @@ -26,20 +26,22 @@ import { deleteObject, UploadTask, StringFormat, + UploadMetadata, + FullMetadata, + SettableMetadata, _UploadTask, _getChild, _Reference, - _FbsBlob -} from '../exp/api'; // import from the exp public API + _FbsBlob, + _dataFromString, + _invalidRootOperation +} from '@firebase/storage'; import { UploadTaskCompat } from './task'; import { ListResultCompat } from './list'; import { StorageServiceCompat } from './service'; import * as types from '@firebase/storage-types'; -import { Metadata } from '../src/metadata'; -import { dataFromString } from '../src/implementation/string'; -import { invalidRootOperation } from '../src/implementation/error'; import { Compat } from '@firebase/util'; export class ReferenceCompat @@ -104,7 +106,7 @@ export class ReferenceCompat ): types.UploadTask { this._throwIfRoot('put'); return new UploadTaskCompat( - uploadBytesResumable(this._delegate, data, metadata as Metadata), + uploadBytesResumable(this._delegate, data, metadata as UploadMetadata), this ); } @@ -119,11 +121,11 @@ export class ReferenceCompat putString( value: string, format: StringFormat = StringFormat.RAW, - metadata?: Metadata + metadata?: types.UploadMetadata ): types.UploadTask { this._throwIfRoot('putString'); - const data = dataFromString(format, value); - const metadataClone = { ...metadata } as Metadata; + const data = _dataFromString(format, value); + const metadataClone = { ...metadata }; if (metadataClone['contentType'] == null && data.contentType != null) { metadataClone['contentType'] = data.contentType; } @@ -131,7 +133,7 @@ export class ReferenceCompat new _UploadTask( this._delegate as _Reference, new _FbsBlob(data.data, true), - metadataClone + metadataClone as FullMetadata & { [k: string]: string } ) as UploadTask, this ); @@ -208,7 +210,7 @@ export class ReferenceCompat ): Promise { return updateMetadata( this._delegate, - metadata as Metadata + metadata as SettableMetadata ) as Promise; } @@ -231,7 +233,7 @@ export class ReferenceCompat private _throwIfRoot(name: string): void { if ((this._delegate as _Reference)._location.path === '') { - throw invalidRootOperation(name); + throw _invalidRootOperation(name); } } } diff --git a/packages/storage/compat/service.ts b/packages/storage-compat/src/service.ts similarity index 86% rename from packages/storage/compat/service.ts rename to packages/storage-compat/src/service.ts index 17a4cb952d4..a3e122efb5d 100644 --- a/packages/storage/compat/service.ts +++ b/packages/storage-compat/src/service.ts @@ -20,13 +20,13 @@ import { FirebaseApp } from '@firebase/app-types'; import { ref, - _Location, connectStorageEmulator, - FirebaseStorage -} from '../exp/api'; // import from the exp public API + FirebaseStorage, + _Location, + _invalidArgument, + _FirebaseStorageImpl +} from '@firebase/storage'; // import from the exp public API import { ReferenceCompat } from './reference'; -import { isUrl, FirebaseStorageImpl } from '../src/service'; -import { invalidArgument } from '../src/implementation/error'; import { Compat, EmulatorMockTokenOptions } from '@firebase/util'; /** @@ -52,7 +52,7 @@ export class StorageServiceCompat */ ref(path?: string): types.Reference { if (isUrl(path)) { - throw invalidArgument( + throw _invalidArgument( 'ref() expected a child path but got a URL, use refFromURL instead.' ); } @@ -65,14 +65,14 @@ export class StorageServiceCompat */ refFromURL(url: string): types.Reference { if (!isUrl(url)) { - throw invalidArgument( + throw _invalidArgument( 'refFromURL() expected a full URL but got a child path, use ref() instead.' ); } try { - _Location.makeFromUrl(url, (this._delegate as FirebaseStorageImpl).host); + _Location.makeFromUrl(url, (this._delegate as _FirebaseStorageImpl).host); } catch (e) { - throw invalidArgument( + throw _invalidArgument( 'refFromUrl() expected a valid full URL but got an invalid one.' ); } @@ -97,3 +97,7 @@ export class StorageServiceCompat connectStorageEmulator(this._delegate, host, port, options); } } + +function isUrl(path?: string): boolean { + return /^[A-Za-z]+:\/\//.test(path as string); +} diff --git a/packages/storage/compat/task.ts b/packages/storage-compat/src/task.ts similarity index 99% rename from packages/storage/compat/task.ts rename to packages/storage-compat/src/task.ts index a358c47c40a..13fcc6e8d03 100644 --- a/packages/storage/compat/task.ts +++ b/packages/storage-compat/src/task.ts @@ -21,7 +21,7 @@ import { UploadTaskSnapshot, TaskEvent, StorageObserver -} from '../exp/api'; +} from '@firebase/storage'; import { UploadTaskSnapshotCompat } from './tasksnapshot'; import { ReferenceCompat } from './reference'; import * as types from '@firebase/storage-types'; diff --git a/packages/storage/compat/tasksnapshot.ts b/packages/storage-compat/src/tasksnapshot.ts similarity index 95% rename from packages/storage/compat/tasksnapshot.ts rename to packages/storage-compat/src/tasksnapshot.ts index 4d092994199..9faaa3485e9 100644 --- a/packages/storage/compat/tasksnapshot.ts +++ b/packages/storage-compat/src/tasksnapshot.ts @@ -15,14 +15,15 @@ * limitations under the License. */ -import { UploadTaskSnapshot } from '../exp/api'; +import { UploadTaskSnapshot } from '@firebase/storage'; import { ReferenceCompat } from './reference'; import { UploadTaskCompat } from './task'; import * as types from '@firebase/storage-types'; import { Compat } from '@firebase/util'; export class UploadTaskSnapshotCompat - implements types.UploadTaskSnapshot, Compat { + implements types.UploadTaskSnapshot, Compat +{ constructor( readonly _delegate: UploadTaskSnapshot, readonly task: UploadTaskCompat, diff --git a/packages/storage/test/integration/integration.compat.test.ts b/packages/storage-compat/test/integration/integration.test.ts similarity index 96% rename from packages/storage/test/integration/integration.compat.test.ts rename to packages/storage-compat/test/integration/integration.test.ts index e2b5c6f8508..047410c65d0 100644 --- a/packages/storage/test/integration/integration.compat.test.ts +++ b/packages/storage-compat/test/integration/integration.test.ts @@ -15,15 +15,16 @@ * limitations under the License. */ -import firebase from '@firebase/app'; -import '@firebase/auth'; +import firebase from '@firebase/app-compat'; +// eslint-disable-next-line import/no-extraneous-dependencies +import '@firebase/auth-compat'; // See https://github.com/typescript-eslint/typescript-eslint/issues/363 // eslint-disable-next-line @typescript-eslint/no-unused-vars import * as storage from '@firebase/storage-types'; import { expect } from 'chai'; -import '../../index'; +import '../../src/index'; // eslint-disable-next-line @typescript-eslint/no-require-imports const PROJECT_CONFIG = require('../../../../config/project.json'); diff --git a/packages/storage-compat/test/setup.ts b/packages/storage-compat/test/setup.ts new file mode 100644 index 00000000000..22ed9b0d112 --- /dev/null +++ b/packages/storage-compat/test/setup.ts @@ -0,0 +1,28 @@ +/** + * @license + * Copyright 2019 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 { use } from 'chai'; +import { restore } from 'sinon'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +use(chaiAsPromised); +use(sinonChai); + +afterEach(async () => { + restore(); +}); diff --git a/packages/storage/test/unit/index.compat.test.ts b/packages/storage-compat/test/unit/index.test.ts similarity index 84% rename from packages/storage/test/unit/index.compat.test.ts rename to packages/storage-compat/test/unit/index.test.ts index 451821ab2b5..d048407a0b8 100644 --- a/packages/storage/test/unit/index.compat.test.ts +++ b/packages/storage-compat/test/unit/index.test.ts @@ -14,12 +14,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import '../setup'; import { expect } from 'chai'; -import '../../index'; -import firebase from '@firebase/app'; +import '../../src/index'; +import firebase from '@firebase/app-compat'; // eslint-disable-next-line import/no-extraneous-dependencies -import { StorageServiceCompat } from '../../compat/service'; -import { FirebaseStorageImpl } from '../../src/service'; +import { StorageServiceCompat } from '../../src/service'; +import { _FirebaseStorageImpl } from '@firebase/storage'; // eslint-disable-next-line @typescript-eslint/no-require-imports const PROJECT_CONFIG = require('../../../../config/project.json'); @@ -39,7 +40,7 @@ describe('Firebase Storage > API', () => { }); const storage = firebase.storage!(); expect( - ((storage as StorageServiceCompat)._delegate as FirebaseStorageImpl) + ((storage as StorageServiceCompat)._delegate as _FirebaseStorageImpl) ._bucket?.bucket ).to.equal(STORAGE_BUCKET); await app.delete(); @@ -53,7 +54,7 @@ describe('Firebase Storage > API', () => { }); const storage = firebase.storage!(app, 'gs://foo-bar.appspot.com'); expect( - ((storage as StorageServiceCompat)._delegate as FirebaseStorageImpl) + ((storage as StorageServiceCompat)._delegate as _FirebaseStorageImpl) ._bucket?.bucket ).to.equal(STORAGE_BUCKET); await app.delete(); diff --git a/packages/storage-compat/test/unit/reference.test.ts b/packages/storage-compat/test/unit/reference.test.ts new file mode 100644 index 00000000000..0d809a3c823 --- /dev/null +++ b/packages/storage-compat/test/unit/reference.test.ts @@ -0,0 +1,214 @@ +/** + * @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 '../setup'; +import { expect } from 'chai'; +import { ReferenceCompat } from '../../src/reference'; +import { StorageServiceCompat } from '../../src/service'; +import { makeTestCompatStorage, fakeApp, fakeStorage } from '../utils'; +import firebase from '@firebase/app-compat'; +import { + StorageReference, + getStorage, + FirebaseStorage +} from '@firebase/storage'; +import { fake } from 'sinon'; +import { FirebaseApp } from '@firebase/app-types'; +import { Reference } from '@firebase/storage-types'; + +describe('Firebase Storage > Reference', () => { + let testCompatApp: FirebaseApp; + let testModularStorage: FirebaseStorage; + let service: StorageServiceCompat; + before(() => { + testCompatApp = firebase.initializeApp({}); + testModularStorage = getStorage(testCompatApp); + service = makeTestCompatStorage(testCompatApp, testModularStorage); + }); + + after(() => { + return testCompatApp.delete(); + }); + describe('toString', () => { + it('delegates to the modular Reference.toString()', () => { + const fakeToString = fake.returns('test123'); + const ref = new ReferenceCompat( + { + toString: fakeToString + } as unknown as StorageReference, + makeTestCompatStorage(fakeApp, fakeStorage) + ); + + expect(ref.toString()).to.equal('test123'); + expect(fakeToString).to.have.been.calledOnceWithExactly(); + }); + }); + + describe('parent', () => { + it('Returns null at root', () => { + const root = service.refFromURL('gs://test-bucket'); + expect(root.parent).to.be.null; + }); + it('Returns root one level down', () => { + const child = service.refFromURL('gs://test-bucket/hello'); + expect(child.parent!.toString()).to.equal('gs://test-bucket/'); + }); + it('Works correctly with empty levels', () => { + const s = service.refFromURL('gs://test-bucket/a///'); + expect(s.parent!.toString()).to.equal('gs://test-bucket/a/'); + }); + }); + + describe('root', () => { + it('Returns self at root', () => { + const root = service.refFromURL('gs://test-bucket'); + expect(root.root.toString()).to.equal('gs://test-bucket/'); + }); + + it('Returns root multiple levels down', () => { + const s = service.refFromURL('gs://test-bucket/a/b/c/d'); + expect(s.root.toString()).to.equal('gs://test-bucket/'); + }); + }); + + describe('bucket', () => { + it('Returns bucket name', () => { + const root = service.refFromURL('gs://test-bucket'); + expect(root.bucket).to.equal('test-bucket'); + }); + }); + + describe('fullPath', () => { + it('Returns full path without leading slash', () => { + const s = service.refFromURL('gs://test-bucket/full/path'); + expect(s.fullPath).to.equal('full/path'); + }); + }); + + describe('name', () => { + it('Works at top level', () => { + const s = service.refFromURL('gs://test-bucket/toplevel.txt'); + expect(s.name).to.equal('toplevel.txt'); + }); + + it('Works at not the top level', () => { + const s = service.refFromURL('gs://test-bucket/not/toplevel.txt'); + expect('toplevel.txt').to.equal(s.name); + }); + }); + + describe('child', () => { + let root: Reference; + before(() => { + root = service.refFromURL('gs://test-bucket'); + }); + it('works with a simple string', () => { + expect(root.child('a').toString()).to.equal('gs://test-bucket/a'); + }); + it('drops a trailing slash', () => { + expect(root.child('ab/').toString()).to.equal('gs://test-bucket/ab'); + }); + it('compresses repeated slashes', () => { + expect(root.child('//a///b/////').toString()).to.equal( + 'gs://test-bucket/a/b' + ); + }); + it('works chained multiple times with leading slashes', () => { + expect( + root.child('a').child('/b').child('c').child('d/e').toString() + ).to.equal('gs://test-bucket/a/b/c/d/e'); + }); + }); + + describe('putString', () => { + let child: Reference; + before(() => { + child = service.refFromURL('gs://test-bucket/hello'); + }); + it('Uses metadata.contentType for RAW format', () => { + // Regression test for b/30989476 + const task = child.putString('hello', 'raw', { + contentType: 'lol/wut' + }); + expect(task.snapshot.metadata!.contentType).to.equal('lol/wut'); + task.cancel(); + }); + it('Uses embedded content type in DATA_URL format', () => { + const task = child.putString('data:lol/wat;base64,aaaa', 'data_url'); + expect(task.snapshot.metadata!.contentType).to.equal('lol/wat'); + task.cancel(); + }); + it('Lets metadata.contentType override embedded content type in DATA_URL format', () => { + const task = child.putString('data:ignore/me;base64,aaaa', 'data_url', { + contentType: 'tomato/soup' + }); + expect(task.snapshot.metadata!.contentType).to.equal('tomato/soup'); + task.cancel(); + }); + }); + + describe('Argument verification', () => { + describe('list', () => { + it('throws on invalid maxResults', async () => { + const child = service.refFromURL('gs://test-bucket/hello'); + await expect(child.list({ maxResults: 0 })).to.be.rejectedWith( + 'storage/invalid-argument' + ); + await expect(child.list({ maxResults: -4 })).to.be.rejectedWith( + 'storage/invalid-argument' + ); + await expect(child.list({ maxResults: 1001 })).to.be.rejectedWith( + 'storage/invalid-argument' + ); + }); + }); + }); + + describe('root operations', () => { + let root: Reference; + before(() => { + root = service.refFromURL('gs://test-bucket'); + }); + it('put throws', () => { + expect(() => root.put(new Uint8Array())).to.throw( + 'storage/invalid-root-operation' + ); + }); + it('putString throws', () => { + expect(() => root.putString('raw', 'raw')).to.throw( + 'storage/invalid-root-operation' + ); + }); + it('delete throws', () => { + expect(() => root.delete()).to.throw('storage/invalid-root-operation'); + }); + it('getMetadata throws', async () => { + await expect(root.getMetadata()).to.be.rejectedWith( + 'storage/invalid-root-operation' + ); + }); + it('updateMetadata throws', async () => { + await expect(root.updateMetadata({})).to.be.rejectedWith( + 'storage/invalid-root-operation' + ); + }); + it('getDownloadURL throws', async () => { + await expect(root.getDownloadURL()).to.be.rejectedWith( + 'storage/invalid-root-operation' + ); + }); + }); +}); diff --git a/packages/storage-compat/test/unit/service.test.ts b/packages/storage-compat/test/unit/service.test.ts new file mode 100644 index 00000000000..5059052218b --- /dev/null +++ b/packages/storage-compat/test/unit/service.test.ts @@ -0,0 +1,234 @@ +/** + * @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 '../setup'; +import { expect } from 'chai'; +import { stub } from 'sinon'; +import * as modularStorage from '@firebase/storage'; +import { makeTestCompatStorage, fakeApp, fakeStorage } from '../utils'; +import { + FirebaseStorage, + getStorage, + FirebaseStorageError +} from '@firebase/storage'; +import firebase from '@firebase/app-compat'; +import { StorageServiceCompat } from '../../src/service'; +import { FirebaseApp } from '@firebase/app-types'; + +const DEFAULT_HOST = 'firebasestorage.googleapis.com'; + +describe('Firebase Storage > Service', () => { + describe('useEmulator(host, port)', () => { + it('calls connectStorageEmulator() correctly', () => { + const connectStorageEmulatorStub = stub( + modularStorage, + 'connectStorageEmulator' + ).callsFake(() => {}); + const service = makeTestCompatStorage(fakeApp, fakeStorage); + service.useEmulator('test.host.org', 1234); + + expect(connectStorageEmulatorStub).to.have.been.calledWithExactly( + fakeStorage, + 'test.host.org', + 1234, + {} + ); + }); + }); + + describe('refFromURL', () => { + let service: StorageServiceCompat; + let testCompatApp: FirebaseApp; + let testModularStorage: FirebaseStorage; + before(() => { + testCompatApp = firebase.initializeApp({}); + testModularStorage = getStorage(testCompatApp); + service = makeTestCompatStorage(testCompatApp, testModularStorage); + }); + + after(() => { + return testCompatApp.delete(); + }); + + it('Works with gs:// URLs', () => { + const ref = service.refFromURL('gs://mybucket/child/path/image.png'); + expect(ref.toString()).to.equal('gs://mybucket/child/path/image.png'); + }); + it('Works with http:// URLs', () => { + const ref = service.refFromURL( + `http://${DEFAULT_HOST}/v0/b/` + + 'mybucket/o/child%2Fpath%2Fimage.png?downloadToken=hello' + ); + expect(ref.toString()).to.equal('gs://mybucket/child/path/image.png'); + }); + it('Works with https:// URLs', () => { + const ref = service.refFromURL( + `https://${DEFAULT_HOST}/v0/b/` + + 'mybucket/o/child%2Fpath%2Fimage.png?downloadToken=hello' + ); + expect(ref.toString()).to.equal('gs://mybucket/child/path/image.png'); + }); + it('Works with storage.googleapis.com URLs', () => { + const ref = service.refFromURL( + `https://storage.googleapis.com/mybucket/path%20with%20space/image.png` + ); + expect(ref.toString()).to.equal( + 'gs://mybucket/path with space/image.png' + ); + }); + it('Works with storage.googleapis.com URLs with query params', () => { + const ref = service.refFromURL( + `https://storage.googleapis.com/mybucket/path%20with%20space/image.png?X-Goog-Algorithm= +GOOG4-RSA-SHA256` + ); + expect(ref.toString()).to.equal( + 'gs://mybucket/path with space/image.png' + ); + }); + it('Works with storage.cloud.google.com URLs', () => { + const ref = service.refFromURL( + `https://storage.cloud.google.com/mybucket/path%20with%20space/image.png` + ); + expect(ref.toString()).to.equal( + 'gs://mybucket/path with space/image.png' + ); + }); + it('Works with storage.cloud.google.com URLs and escaped slash', () => { + const ref = service.refFromURL( + `https://storage.cloud.google.com/mybucket/path%20with%20space%2Fimage.png` + ); + expect(ref.toString()).to.equal( + 'gs://mybucket/path with space/image.png' + ); + }); + }); + + describe('Argument verification', () => { + let service: StorageServiceCompat; + let testCompatApp: FirebaseApp; + let testModularStorage: FirebaseStorage; + before(() => { + testCompatApp = firebase.initializeApp({}); + testModularStorage = getStorage(testCompatApp); + service = makeTestCompatStorage(testCompatApp, testModularStorage); + }); + + after(() => { + return testCompatApp.delete(); + }); + + describe('ref', () => { + it('Throws on gs:// argument', () => { + expect(() => service.ref('gs://yo')).to.throw( + 'storage/invalid-argument' + ); + }); + }); + + describe('refFromURL', () => { + it('Throws with a non-URL string arg', () => { + expect(() => { + service.refFromURL('child'); + }).to.throw( + /expected a full URL but got a child path.*storage\/invalid-argument/i + ); + }); + it('Throws with an invalid URL arg', () => { + expect(() => { + service.refFromURL('notlegit://url'); + }).to.throw('storage/invalid-argument'); + }); + }); + + describe('MaxUploadRetryTime', () => { + const modularStorage = {} as FirebaseStorage; + const service = makeTestCompatStorage(fakeApp, modularStorage); + it('reads from the modular instance', () => { + modularStorage.maxUploadRetryTime = 999; + expect(service.maxUploadRetryTime).to.equal(999); + }); + + it('sets value on the modular instance', () => { + service.setMaxUploadRetryTime(888); + expect(modularStorage.maxUploadRetryTime).to.equal(888); + }); + }); + describe('MaxOperationRetryTime', () => { + const modularStorage = {} as FirebaseStorage; + const service = makeTestCompatStorage(fakeApp, modularStorage); + it('reads from the modular instance', () => { + modularStorage.maxOperationRetryTime = 999; + expect(service.maxOperationRetryTime).to.equal(999); + }); + + it('sets value on the modular instance', () => { + service.setMaxOperationRetryTime(888); + expect(modularStorage.maxOperationRetryTime).to.equal(888); + }); + }); + }); + + describe('Deletion', () => { + let service: StorageServiceCompat; + let testCompatApp: FirebaseApp; + let testModularStorage: FirebaseStorage; + before(() => { + testCompatApp = firebase.initializeApp({}); + testModularStorage = getStorage(testCompatApp); + service = makeTestCompatStorage(testCompatApp, testModularStorage); + }); + + after(() => { + return testCompatApp.delete(); + }); + + it('In-flight requests are canceled when the service is deleted', async () => { + const ref = service.refFromURL('gs://mybucket/image.jpg'); + const metadataPromise = ref.getMetadata(); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + service._delegate._delete(); + await expect(metadataPromise).to.be.rejectedWith('storage/app-deleted'); + }); + it('Requests fail when started after the service is deleted', async () => { + const ref = service.refFromURL('gs://mybucket/image.jpg'); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + service._delegate._delete(); + + await expect(ref.getMetadata()).to.be.rejectedWith('storage/app-deleted'); + }); + it('Running uploads fail when the service is deleted', () => { + const ref = service.refFromURL('gs://mybucket/image.jpg'); + const toReturn = new Promise((resolve, reject) => { + ref.put(new Uint8Array([97])).on( + 'state_changed', + null, + (err: FirebaseStorageError | Error) => { + expect((err as FirebaseStorageError).code).to.equal( + 'storage/app-deleted' + ); + resolve(); + }, + () => { + reject('Upload completed, should have been canceled'); + } + ); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + service._delegate._delete(); + }); + return toReturn; + }); + }); +}); diff --git a/packages/storage-compat/test/utils.ts b/packages/storage-compat/test/utils.ts new file mode 100644 index 00000000000..18da96ea996 --- /dev/null +++ b/packages/storage-compat/test/utils.ts @@ -0,0 +1,34 @@ +/** + * @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 { FirebaseApp } from '@firebase/app-types'; +import { FirebaseStorage } from '@firebase/storage'; +import { StorageServiceCompat } from '../src/service'; + +export function makeTestCompatStorage( + app: FirebaseApp, + storage: FirebaseStorage +): StorageServiceCompat { + const storageServiceCompat: StorageServiceCompat = new StorageServiceCompat( + app, + storage + ); + return storageServiceCompat; +} + +export const fakeApp = {} as FirebaseApp; +export const fakeStorage = {} as FirebaseStorage; diff --git a/packages/storage-compat/tsconfig.json b/packages/storage-compat/tsconfig.json new file mode 100644 index 00000000000..a06ed9a374c --- /dev/null +++ b/packages/storage-compat/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../config/tsconfig.base.json", + "compilerOptions": { + "outDir": "dist" + }, + "exclude": [ + "dist/**/*" + ] +} \ No newline at end of file diff --git a/packages/storage-types/index.d.ts b/packages/storage-types/index.d.ts index 7707043777b..c6a8fc7178d 100644 --- a/packages/storage-types/index.d.ts +++ b/packages/storage-types/index.d.ts @@ -153,6 +153,6 @@ export class FirebaseStorage { declare module '@firebase/component' { interface NameServiceMapping { - 'storage': FirebaseStorage; + 'storage-compat': FirebaseStorage; } } diff --git a/packages/storage/.eslintrc.js b/packages/storage/.eslintrc.js index 00bf88df31c..ffe0e481071 100644 --- a/packages/storage/.eslintrc.js +++ b/packages/storage/.eslintrc.js @@ -26,23 +26,12 @@ module.exports = { tsconfigRootDir: __dirname }, rules: { - 'no-throw-literal': 'off', '@typescript-eslint/no-unused-vars': [ 'error', { varsIgnorePattern: '^_', args: 'none' } - ], - 'import/no-extraneous-dependencies': [ - 'error', - { - 'packageDir': [ - path.resolve(__dirname, '../../'), - __dirname, - path.resolve(__dirname, 'exp') - ] - } ] } }; diff --git a/packages/storage/.npmignore b/packages/storage/.npmignore deleted file mode 100644 index 682c8f74a52..00000000000 --- a/packages/storage/.npmignore +++ /dev/null @@ -1,9 +0,0 @@ -# Directories not needed by end users -/src -test - -# Files not needed by end users -gulpfile.js -index.ts -karma.conf.js -tsconfig.json \ No newline at end of file diff --git a/packages/storage/api-extractor.json b/packages/storage/api-extractor.json index c85607441f0..8a3c6cb251e 100644 --- a/packages/storage/api-extractor.json +++ b/packages/storage/api-extractor.json @@ -1,10 +1,10 @@ { "extends": "../../config/api-extractor.json", // Point it to your entry point d.ts file. - "mainEntryPointFilePath": "/exp/dist/exp/index.d.ts", + "mainEntryPointFilePath": "/dist/src/index.d.ts", "dtsRollup": { "enabled": true, - "untrimmedFilePath": "/exp/dist/.d.ts", - "publicTrimmedFilePath": "/exp/dist/-public.d.ts" + "untrimmedFilePath": "/dist/.d.ts", + "publicTrimmedFilePath": "/dist/-public.d.ts" } } \ No newline at end of file diff --git a/packages/storage/compat/package.json b/packages/storage/compat/package.json deleted file mode 100644 index 2c149a59ccb..00000000000 --- a/packages/storage/compat/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "@firebase/storage-compat", - "version": "0.0.900", - "description": "The Cloud Storage 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/storage/exp/package.json b/packages/storage/exp/package.json deleted file mode 100644 index 896db7ac0c1..00000000000 --- a/packages/storage/exp/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "@firebase/storage-exp", - "description": "A tree-shakeable version of the Storage SDK", - "main": "./dist/index.node.cjs.js", - "module": "./dist/index.browser.esm2017.js", - "browser": "./dist/index.browser.esm2017.js", - "esm5": "./dist/index.browser.esm5.js", - "typings": "./dist/storage-public.d.ts", - "private": true -} diff --git a/packages/storage/index.ts b/packages/storage/index.ts deleted file mode 100644 index 8128907d79b..00000000000 --- a/packages/storage/index.ts +++ /dev/null @@ -1,88 +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 firebase from '@firebase/app'; -import { _FirebaseNamespace } from '@firebase/app-types/private'; -import { StringFormat } from './src/implementation/string'; -import { TaskEvent, TaskState } from './src/implementation/taskenums'; - -import { ConnectionPool } from './src/implementation/connectionPool'; -import { ReferenceCompat } from './compat/reference'; -import { StorageServiceCompat } from './compat/service'; -import { FirebaseStorageImpl } from './src/service'; -import * as types from '@firebase/storage-types'; -import { - Component, - ComponentType, - ComponentContainer, - InstanceFactoryOptions -} from '@firebase/component'; - -import { name, version } from './package.json'; - -import './register-module'; - -/** - * Type constant for Firebase Storage. - */ -const STORAGE_TYPE = 'storage'; - -function factory( - container: ComponentContainer, - { instanceIdentifier: url }: InstanceFactoryOptions -): types.FirebaseStorage { - // Dependencies - // TODO: This should eventually be 'app-compat' - const app = container.getProvider('app').getImmediate(); - const authProvider = container.getProvider('auth-internal'); - const appCheckProvider = container.getProvider('app-check-internal'); - - // TODO: get StorageService instance from component framework instead - // of creating a new one. - const storageServiceCompat: StorageServiceCompat = new StorageServiceCompat( - app, - new FirebaseStorageImpl( - app, - authProvider, - appCheckProvider, - new ConnectionPool(), - url, - firebase.SDK_VERSION - ) - ); - return storageServiceCompat; -} - -export function registerStorage(instance: _FirebaseNamespace): void { - const namespaceExports = { - // no-inline - TaskState, - TaskEvent, - StringFormat, - Storage: FirebaseStorageImpl, - Reference: ReferenceCompat - }; - instance.INTERNAL.registerComponent( - new Component(STORAGE_TYPE, factory, ComponentType.PUBLIC) - .setServiceProps(namespaceExports) - .setMultipleInstances(true) - ); - - instance.registerVersion(name, version); -} - -registerStorage(firebase as _FirebaseNamespace); diff --git a/packages/storage/karma.conf.js b/packages/storage/karma.conf.js index ab37877fc20..3b5d7e3f39b 100644 --- a/packages/storage/karma.conf.js +++ b/packages/storage/karma.conf.js @@ -32,20 +32,8 @@ module.exports = function (config) { function getTestFiles(argv) { let unitTestFiles = ['test/unit/*']; - let integrationTestFiles = []; - if (argv.exp) { - unitTestFiles = unitTestFiles.filter( - filename => !filename.includes('.compat.') - ); - integrationTestFiles = ['test/integration/*exp*']; - } else if (argv.compat) { - unitTestFiles = unitTestFiles.filter( - filename => !filename.includes('.exp.') - ); - integrationTestFiles = ['test/integration/*compat*']; - } else { - integrationTestFiles = ['test/integration/*']; - } + let integrationTestFiles = ['test/integration/*']; + if (argv.unit) { return unitTestFiles; } else if (argv.integration) { diff --git a/packages/storage/package.json b/packages/storage/package.json index 1a58efaba03..6b64a3c3eb5 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -3,50 +3,39 @@ "version": "0.7.0", "description": "", "author": "Firebase (https://firebase.google.com/)", - "main": "dist/index.node.cjs.js", - "module": "dist/index.browser.esm.js", - "browser": "dist/index.browser.esm.js", - "esm2017": "dist/index.browser.esm2017.js", + "main": "dist/index.cjs.js", + "module": "dist/index.esm2017.js", + "browser": "dist/index.esm2017.js", + "esm5": "dist/index.esm5.js", "files": [ - "dist", - "exp/dist" + "dist" ], "scripts": { "bundle": "rollup -c", "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "build": "run-p 'bundle rollup.config.js' build:exp && yarn build:compat", - "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/storage --include-dependencies build", "dev": "rollup -c -w", "test": "run-p test:browser test:node lint", "test:ci": "node ../../scripts/run_tests_in_ci.js -s test:browser test:node", - "test:browser:compat:unit": "karma start --single-run --compat --unit", - "test:browser:exp:unit": "karma start --single-run --exp --unit", - "test:browser:compat:integration": "karma start --single-run --compat --integration", - "test:browser:exp:integration": "karma start --single-run --exp --integration", - "test:browser:compat": "karma start --single-run --compat", - "test:browser:exp": "karma start --single-run --exp", + "test:browser:unit": "karma start --single-run --unit", + "test:browser:integration": "karma start --single-run --integration", "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.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.ts --config ../../config/mocharc.node.js", "test:debug": "karma start --browser=Chrome", "prettier": "prettier --write 'src/**/*.ts' 'test/**/*.ts'", - "api-report": "api-extractor run --local --verbose && ts-node-script ../../repo-scripts/prune-dts/prune-dts.ts --input exp/dist/storage-public.d.ts --output exp/dist/storage-public.d.ts", - "add-compat-overloads": "ts-node-script ../../scripts/exp/create-overloads.ts -i exp/dist/storage-public.d.ts -o dist/compat/esm2017/compat/index.d.ts -a -r StorageService:types.FirebaseStorage -r StorageReference:types.Reference -r FirebaseApp:FirebaseAppCompat --moduleToEnhance @firebase/storage" + "api-report": "api-extractor run --local --verbose && ts-node-script ../../repo-scripts/prune-dts/prune-dts.ts --input dist/storage-public.d.ts --output dist/storage-public.d.ts" }, "license": "Apache-2.0", "dependencies": { - "@firebase/storage-types": "0.5.0", "@firebase/util": "1.3.0", "@firebase/component": "0.5.6", "node-fetch": "2.6.1", "tslib": "^2.1.0" }, "peerDependencies": { - "@firebase/app": "0.x", - "@firebase/app-types": "0.x" + "@firebase/app": "0.x" }, "devDependencies": { "@firebase/app": "0.6.30", @@ -65,5 +54,5 @@ "bugs": { "url": "https://github.com/firebase/firebase-js-sdk/issues" }, - "typings": "dist/index.d.ts" + "typings": "dist/src/index.d.ts" } diff --git a/packages/storage/register-module.ts b/packages/storage/register-module.ts deleted file mode 100644 index 23d09759347..00000000000 --- a/packages/storage/register-module.ts +++ /dev/null @@ -1,50 +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 * as types from '@firebase/storage-types'; - -/** - * Define extension behavior for `registerStorage` - */ -declare module '@firebase/app-types' { - interface FirebaseNamespace { - storage?: { - (app?: FirebaseApp, url?: string): types.FirebaseStorage; - Storage: typeof types.FirebaseStorage; - - StringFormat: { - BASE64: types.StringFormat; - BASE64URL: types.StringFormat; - DATA_URL: types.StringFormat; - RAW: types.StringFormat; - }; - TaskEvent: { - STATE_CHANGED: types.TaskEvent; - }; - TaskState: { - CANCELED: types.TaskState; - ERROR: types.TaskState; - PAUSED: types.TaskState; - RUNNING: types.TaskState; - SUCCESS: types.TaskState; - }; - }; - } - interface FirebaseApp { - storage?(storageBucket?: string): types.FirebaseStorage; - } -} diff --git a/packages/storage/rollup.config.exp.js b/packages/storage/rollup.config.exp.js deleted file mode 100644 index 0a6c310656a..00000000000 --- a/packages/storage/rollup.config.exp.js +++ /dev/null @@ -1,110 +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 json from '@rollup/plugin-json'; -import typescriptPlugin from 'rollup-plugin-typescript2'; -import typescript from 'typescript'; -import pkgExp from './exp/package.json'; -import alias from '@rollup/plugin-alias'; -import pkg from './package.json'; -import path from 'path'; -import { importPathTransformer } from '../../scripts/exp/ts-transform-import-path'; - -const { generateAliasConfig } = require('./rollup.shared'); - -const deps = [ - ...Object.keys(Object.assign({}, pkg.peerDependencies, pkg.dependencies)), - '@firebase/app' -]; - -const nodeDeps = [...deps, 'util']; - -const es5Plugins = [ - typescriptPlugin({ - typescript, - abortOnError: false, - transformers: [importPathTransformer] - }), - json() -]; - -const es5Builds = [ - // Browser - { - input: './exp/index.ts', - output: { - file: path.resolve('./exp', pkgExp.esm5), - format: 'es', - sourcemap: true - }, - plugins: [alias(generateAliasConfig('browser')), ...es5Plugins], - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), - treeshake: { - moduleSideEffects: false - } - } -]; - -const es2017Plugins = [ - typescriptPlugin({ - typescript, - tsconfigOverride: { - compilerOptions: { - target: 'es2017' - } - }, - abortOnError: false, - transformers: [importPathTransformer] - }), - json({ preferConst: true }) -]; - -const es2017Builds = [ - // Node - { - input: './exp/index.ts', - output: { - file: path.resolve('./exp', pkgExp.main), - format: 'cjs', - sourcemap: true - }, - plugins: [alias(generateAliasConfig('node')), ...es2017Plugins], - external: id => - nodeDeps.some(dep => id === dep || id.startsWith(`${dep}/`)), - treeshake: { - moduleSideEffects: false - } - }, - - // Browser - { - input: './exp/index.ts', - output: { - file: path.resolve('./exp', pkgExp.browser), - format: 'es', - sourcemap: true - }, - plugins: [alias(generateAliasConfig('browser')), ...es2017Plugins], - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), - treeshake: { - moduleSideEffects: false - } - } -]; - -// eslint-disable-next-line import/no-default-export -export default [...es5Builds, ...es2017Builds]; diff --git a/packages/storage/rollup.config.js b/packages/storage/rollup.config.js index fcb28940b0c..f9c66fc7432 100644 --- a/packages/storage/rollup.config.js +++ b/packages/storage/rollup.config.js @@ -21,7 +21,16 @@ import typescript from 'typescript'; import alias from '@rollup/plugin-alias'; import pkg from './package.json'; -const { generateAliasConfig } = require('./rollup.shared'); +function generateAliasConfig(platform) { + return { + entries: [ + { + find: /^(.*)\/platform\/([^.\/]*)(\.ts)?$/, + replacement: `$1\/platform/${platform}/$2.ts` + } + ] + }; +} const deps = Object.keys( Object.assign({}, pkg.peerDependencies, pkg.dependencies) @@ -29,24 +38,23 @@ const deps = Object.keys( const nodeDeps = [...deps, 'util']; -/** - * ES5 Builds - */ -const es5BuildPlugins = [ +const es5Plugins = [ typescriptPlugin({ - typescript + typescript, + abortOnError: false }), json() ]; const es5Builds = [ + // Browser { - input: './index.ts', + input: './src/index.ts', output: [ { file: 'dist/index.browser.cjs.js', format: 'cjs', sourcemap: true }, - { file: pkg.module, format: 'es', sourcemap: true } + { file: pkg.esm5, format: 'es', sourcemap: true } ], - plugins: [alias(generateAliasConfig('browser')), ...es5BuildPlugins], + plugins: [alias(generateAliasConfig('browser')), ...es5Plugins], external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), treeshake: { moduleSideEffects: false @@ -54,17 +62,15 @@ const es5Builds = [ } ]; -/** - * ES2017 Builds - */ -const es2017BuildPlugins = [ +const es2017Plugins = [ typescriptPlugin({ typescript, tsconfigOverride: { compilerOptions: { target: 'es2017' } - } + }, + abortOnError: false }), json({ preferConst: true }) ]; @@ -72,28 +78,29 @@ const es2017BuildPlugins = [ const es2017Builds = [ // Node { - input: './index.ts', + input: './src/index.ts', output: { file: pkg.main, format: 'cjs', sourcemap: true }, - plugins: [alias(generateAliasConfig('node')), ...es2017BuildPlugins], + plugins: [alias(generateAliasConfig('node')), ...es2017Plugins], external: id => nodeDeps.some(dep => id === dep || id.startsWith(`${dep}/`)), treeshake: { moduleSideEffects: false } }, + // Browser { - input: './index.ts', + input: './src/index.ts', output: { - file: pkg.esm2017, + file: pkg.browser, format: 'es', sourcemap: true }, - plugins: [alias(generateAliasConfig('browser')), ...es2017BuildPlugins], + plugins: [alias(generateAliasConfig('browser')), ...es2017Plugins], external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), treeshake: { moduleSideEffects: false diff --git a/packages/storage/exp/api.ts b/packages/storage/src/api.ts similarity index 91% rename from packages/storage/exp/api.ts rename to packages/storage/src/api.ts index 171d7641e19..b729946c7a3 100644 --- a/packages/storage/exp/api.ts +++ b/packages/storage/src/api.ts @@ -21,7 +21,7 @@ import { ref as refInternal, FirebaseStorageImpl, connectStorageEmulator as connectEmulatorInternal -} from '../src/service'; +} from './service'; import { Provider } from '@firebase/component'; import { @@ -35,7 +35,7 @@ import { UploadMetadata, FullMetadata } from './public-types'; -import { Metadata as MetadataInternal } from '../src/metadata'; +import { Metadata as MetadataInternal } from './metadata'; import { uploadBytes as uploadBytesInternal, uploadBytesResumable as uploadBytesResumableInternal, @@ -48,7 +48,7 @@ import { deleteObject as deleteObjectInternal, Reference, _getChild as _getChildInternal -} from '../src/reference'; +} from './reference'; import { STORAGE_TYPE } from './constants'; import { EmulatorMockTokenOptions, getModularInstance } from '@firebase/util'; @@ -57,10 +57,20 @@ import { EmulatorMockTokenOptions, getModularInstance } from '@firebase/util'; */ export * from './public-types'; -export { Location as _Location } from '../src/implementation/location'; -export { UploadTask as _UploadTask } from '../src/task'; -export type { Reference as _Reference } from '../src/reference'; -export { FbsBlob as _FbsBlob } from '../src/implementation/blob'; +export { Location as _Location } from './implementation/location'; +export { UploadTask as _UploadTask } from './task'; +export type { Reference as _Reference } from './reference'; +export type { FirebaseStorageImpl as _FirebaseStorageImpl } from './service'; +export { FbsBlob as _FbsBlob } from './implementation/blob'; +export { dataFromString as _dataFromString } from './implementation/string'; +export { + invalidRootOperation as _invalidRootOperation, + invalidArgument as _invalidArgument +} from './implementation/error'; +export { + TaskEvent as _TaskEvent, + TaskState as _TaskState +} from './implementation/taskenums'; /** * Uploads data to this object's location. @@ -277,7 +287,7 @@ export function _getChild(ref: StorageReference, childPath: string): Reference { return _getChildInternal(ref as Reference, childPath); } -export { StringFormat } from '../src/implementation/string'; +export { StringFormat } from './implementation/string'; /** * Gets a {@link FirebaseStorage} instance for the given Firebase app. @@ -292,10 +302,7 @@ export function getStorage( bucketUrl?: string ): FirebaseStorage { app = getModularInstance(app); - const storageProvider: Provider<'storage-exp'> = _getProvider( - app, - STORAGE_TYPE - ); + const storageProvider: Provider<'storage'> = _getProvider(app, STORAGE_TYPE); const storageInstance = storageProvider.getImmediate({ identifier: bucketUrl }); diff --git a/packages/storage/exp/constants.ts b/packages/storage/src/constants.ts similarity index 93% rename from packages/storage/exp/constants.ts rename to packages/storage/src/constants.ts index a14f489490e..21ea91291b3 100644 --- a/packages/storage/exp/constants.ts +++ b/packages/storage/src/constants.ts @@ -18,4 +18,4 @@ /** * Type constant for Firebase Storage. */ -export const STORAGE_TYPE = 'storage-exp'; +export const STORAGE_TYPE = 'storage'; diff --git a/packages/storage/src/implementation/error.ts b/packages/storage/src/implementation/error.ts index 41a29fd5dff..a603237b455 100644 --- a/packages/storage/src/implementation/error.ts +++ b/packages/storage/src/implementation/error.ts @@ -250,6 +250,9 @@ export function noDownloadURL(): FirebaseStorageError { ); } +/** + * @internal + */ export function invalidArgument(message: string): FirebaseStorageError { return new FirebaseStorageError(StorageErrorCode.INVALID_ARGUMENT, message); } @@ -292,6 +295,8 @@ export function appDeleted(): FirebaseStorageError { /** * @param name - The name of the operation that was invalid. + * + * @internal */ export function invalidRootOperation(name: string): FirebaseStorageError { return new FirebaseStorageError( diff --git a/packages/storage/src/implementation/observer.ts b/packages/storage/src/implementation/observer.ts index 5c480758ca3..d1ab432610d 100644 --- a/packages/storage/src/implementation/observer.ts +++ b/packages/storage/src/implementation/observer.ts @@ -81,8 +81,8 @@ export class Observer implements StorageObserver { isFunction(nextOrObserver) || error != null || complete != null; if (asFunctions) { this.next = nextOrObserver as NextFn; - this.error = error; - this.complete = complete; + this.error = error ?? undefined; + this.complete = complete ?? undefined; } else { const observer = nextOrObserver as { next?: NextFn; diff --git a/packages/storage/src/implementation/string.ts b/packages/storage/src/implementation/string.ts index ea9ba2dc275..8497190389f 100644 --- a/packages/storage/src/implementation/string.ts +++ b/packages/storage/src/implementation/string.ts @@ -70,6 +70,9 @@ export class StringData { } } +/** + * @internal + */ export function dataFromString( format: StringFormat, stringData: string diff --git a/packages/storage/src/implementation/taskenums.ts b/packages/storage/src/implementation/taskenums.ts index 735474ff974..c57c58a9f45 100644 --- a/packages/storage/src/implementation/taskenums.ts +++ b/packages/storage/src/implementation/taskenums.ts @@ -59,8 +59,9 @@ export const enum InternalTaskState { /** * Represents the current state of a running upload. */ -export type TaskState = string; +export type TaskState = typeof TaskState[keyof typeof TaskState]; +// type keys = keyof TaskState /** * Represents the current state of a running upload. */ @@ -79,7 +80,7 @@ export const TaskState = { /** The task failed with an error. */ ERROR: 'error' -}; +} as const; export function taskStateFromInternalTaskState( state: InternalTaskState diff --git a/packages/storage/exp/index.ts b/packages/storage/src/index.ts similarity index 100% rename from packages/storage/exp/index.ts rename to packages/storage/src/index.ts diff --git a/packages/storage/src/metadata.ts b/packages/storage/src/metadata.ts index 61af339068f..f91fd73a519 100644 --- a/packages/storage/src/metadata.ts +++ b/packages/storage/src/metadata.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { FullMetadata } from '../exp/public-types'; +import { FullMetadata } from './public-types'; /** * @fileoverview Documentation for the metadata format. diff --git a/packages/storage/exp/public-types.ts b/packages/storage/src/public-types.ts similarity index 99% rename from packages/storage/exp/public-types.ts rename to packages/storage/src/public-types.ts index 210e096d23e..39b6276ccda 100644 --- a/packages/storage/exp/public-types.ts +++ b/packages/storage/src/public-types.ts @@ -487,6 +487,6 @@ export interface UploadResult { declare module '@firebase/component' { interface NameServiceMapping { - 'storage-exp': FirebaseStorage; + 'storage': FirebaseStorage; } } diff --git a/packages/storage/src/reference.ts b/packages/storage/src/reference.ts index cd237acd80c..d925ba2091a 100644 --- a/packages/storage/src/reference.ts +++ b/packages/storage/src/reference.ts @@ -31,7 +31,7 @@ import { deleteObject as requestsDeleteObject, multipartUpload } from './implementation/requests'; -import { ListOptions } from '../exp/public-types'; +import { ListOptions, UploadResult } from './public-types'; import { StringFormat, dataFromString } from './implementation/string'; import { Metadata } from './metadata'; import { FirebaseStorageImpl } from './service'; @@ -39,7 +39,6 @@ import { ListResult } from './list'; import { UploadTask } from './task'; import { invalidRootOperation, noDownloadURL } from './implementation/error'; import { validateNumber } from './implementation/type'; -import { UploadResult } from './tasksnapshot'; /** * Provides methods to interact with a bucket in the Firebase Storage service. diff --git a/packages/storage/src/service.ts b/packages/storage/src/service.ts index af3e3df2f73..0848a2262b3 100644 --- a/packages/storage/src/service.ts +++ b/packages/storage/src/service.ts @@ -38,7 +38,7 @@ import { noDefaultBucket } from './implementation/error'; import { validateNumber } from './implementation/type'; -import { FirebaseStorage } from '../exp/public-types'; +import { FirebaseStorage } from './public-types'; import { createMockUserToken, EmulatorMockTokenOptions } from '@firebase/util'; export function isUrl(path?: string): boolean { @@ -148,7 +148,6 @@ export function connectStorageEmulator( /** * A service that provides Firebase Storage Reference instances. - * @public * @param opt_url - gs:// url to a custom Storage Bucket */ export class FirebaseStorageImpl implements FirebaseStorage { diff --git a/packages/storage/src/task.ts b/packages/storage/src/task.ts index e7edbc4e076..04c58d4ca1f 100644 --- a/packages/storage/src/task.ts +++ b/packages/storage/src/task.ts @@ -32,15 +32,14 @@ import { } from './implementation/taskenums'; import { Metadata } from './metadata'; import { - CompleteFn, - ErrorFn, Observer, - StorageObserver, Subscribe, - Unsubscribe + Unsubscribe, + StorageObserver as StorageObserverInternal, + NextFn } from './implementation/observer'; import { Request } from './implementation/request'; -import { UploadTaskSnapshot } from './tasksnapshot'; +import { UploadTaskSnapshot, StorageObserver } from './public-types'; import { async as fbsAsync } from './implementation/async'; import { Mappings, getMappings } from './implementation/metadata'; import { @@ -76,7 +75,7 @@ export class UploadTask { _transferred: number = 0; private _needToFetchStatus: boolean = false; private _needToFetchMetadata: boolean = false; - private _observers: Array> = []; + private _observers: Array> = []; private _resumable: boolean; /** * Upload state. @@ -483,11 +482,18 @@ export class UploadTask { type: TaskEvent, nextOrObserver?: | StorageObserver - | ((a: UploadTaskSnapshot) => unknown), - error?: ErrorFn, - completed?: CompleteFn + | null + | ((snapshot: UploadTaskSnapshot) => unknown), + error?: ((a: FirebaseStorageError) => unknown) | null, + completed?: Unsubscribe | null ): Unsubscribe | Subscribe { - const observer = new Observer(nextOrObserver, error, completed); + const observer = new Observer( + (nextOrObserver as + | StorageObserverInternal + | NextFn) || undefined, + error || undefined, + completed || undefined + ); this._addObserver(observer); return () => { this._removeObserver(observer); diff --git a/packages/storage/src/tasksnapshot.ts b/packages/storage/src/tasksnapshot.ts deleted file mode 100644 index 01341bf5df6..00000000000 --- a/packages/storage/src/tasksnapshot.ts +++ /dev/null @@ -1,73 +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 { TaskState } from './implementation/taskenums'; -import { Metadata } from './metadata'; -import { Reference } from './reference'; -import { UploadTask } from './task'; - -/** - * Result returned from a non-resumable upload. - * @public - */ -export interface UploadResult { - /** - * Contains the metadata sent back from the server. - */ - readonly metadata: Metadata; - - /** - * The reference that spawned this upload. - */ - readonly ref: Reference; -} - -/** - * Holds data about the current state of the upload task. - * @public - */ -export interface UploadTaskSnapshot { - /** - * The number of bytes that have been successfully uploaded so far. - */ - readonly bytesTransferred: number; - - /** - * The total number of bytes to be uploaded. - */ - readonly totalBytes: number; - - /** - * The current state of the task. - */ - readonly state: TaskState; - - /** - * Before the upload completes, contains the metadata sent to the server. - * After the upload completes, contains the metadata sent back from the server. - */ - readonly metadata: Metadata; - - /** - * The task of which this is a snapshot. - */ - readonly task: UploadTask; - - /** - * The reference that spawned this snapshot's upload task. - */ - readonly ref: Reference; -} diff --git a/packages/storage/test/integration/integration.exp.test.ts b/packages/storage/test/integration/integration.test.ts similarity index 97% rename from packages/storage/test/integration/integration.exp.test.ts rename to packages/storage/test/integration/integration.test.ts index 282ee9d6860..63b1c2c3cc9 100644 --- a/packages/storage/test/integration/integration.exp.test.ts +++ b/packages/storage/test/integration/integration.test.ts @@ -30,11 +30,11 @@ import { getMetadata, updateMetadata, listAll -} from '../../exp/index'; +} from '../../src/index'; import { use, expect } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; -import * as types from '../../exp/public-types'; +import * as types from '../../src/public-types'; use(chaiAsPromised); @@ -46,7 +46,7 @@ export const STORAGE_BUCKET = PROJECT_CONFIG.storageBucket; export const API_KEY = PROJECT_CONFIG.apiKey; export const AUTH_DOMAIN = PROJECT_CONFIG.authDomain; -describe('FirebaseStorage Exp', () => { +describe('FirebaseStorage Integration tests', () => { let app: FirebaseApp; let storage: types.FirebaseStorage; diff --git a/packages/storage/test/unit/index.exp.test.ts b/packages/storage/test/unit/index.test.ts similarity index 97% rename from packages/storage/test/unit/index.exp.test.ts rename to packages/storage/test/unit/index.test.ts index 9c57c2583eb..8f2a6b37785 100644 --- a/packages/storage/test/unit/index.exp.test.ts +++ b/packages/storage/test/unit/index.test.ts @@ -15,7 +15,7 @@ * limitations under the License. */ import { expect } from 'chai'; -import { getStorage } from '../../exp/index'; +import { getStorage } from '../../src/index'; import { FirebaseStorageImpl } from '../../src/service'; // eslint-disable-next-line import/no-extraneous-dependencies import { initializeApp, deleteApp } from '@firebase/app-exp'; diff --git a/packages/storage/test/unit/reference.compat.test.ts b/packages/storage/test/unit/reference.compat.test.ts deleted file mode 100644 index bc725558d08..00000000000 --- a/packages/storage/test/unit/reference.compat.test.ts +++ /dev/null @@ -1,315 +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 { expect } from 'chai'; -import { FirebaseApp } from '@firebase/app-types'; -import { StringFormat } from '../../src/implementation/string'; -import { Headers } from '../../src/implementation/connection'; -import { Metadata } from '../../src/metadata'; -import { ReferenceCompat } from '../../compat/reference'; -import { StorageServiceCompat } from '../../compat/service'; -import * as testShared from './testshared'; -import { SendHook, TestingConnection } from './connection'; -import { DEFAULT_HOST } from '../../src/implementation/constants'; -import { FirebaseAuthInternalName } from '@firebase/auth-interop-types'; -import { Provider } from '@firebase/component'; -import { FirebaseStorageImpl } from '../../src/service'; -import { Reference } from '../../src/reference'; -import { AppCheckInternalComponentName } from '@firebase/app-check-interop-types'; - -/* eslint-disable @typescript-eslint/no-floating-promises */ -function makeFakeService( - app: FirebaseApp, - authProvider: Provider, - appCheckProvider: Provider, - sendHook: SendHook -): StorageServiceCompat { - const storageServiceCompat: StorageServiceCompat = new StorageServiceCompat( - app, - new FirebaseStorageImpl( - app, - authProvider, - appCheckProvider, - testShared.makePool(sendHook) - ) - ); - return storageServiceCompat; -} - -function makeStorage(url: string): ReferenceCompat { - const service = new FirebaseStorageImpl( - {} as FirebaseApp, - testShared.emptyAuthProvider, - testShared.fakeAppCheckTokenProvider, - testShared.makePool(null) - ); - const storageServiceCompat: StorageServiceCompat = new StorageServiceCompat( - {} as FirebaseApp, - service - ); - return new ReferenceCompat(new Reference(service, url), storageServiceCompat); -} - -describe('Firebase Storage > Reference', () => { - const root = makeStorage('gs://test-bucket/'); - const child = makeStorage('gs://test-bucket/hello'); - describe('Path constructor', () => { - it('root', () => { - expect(root.toString()).to.equal('gs://test-bucket/'); - }); - it('keeps characters after ? on a gs:// string', () => { - const s = makeStorage('gs://test-bucket/this/ismyobject?hello'); - expect(s.toString()).to.equal('gs://test-bucket/this/ismyobject?hello'); - }); - it("doesn't URL-decode on a gs:// string", () => { - const s = makeStorage('gs://test-bucket/%3F'); - expect(s.toString()).to.equal('gs://test-bucket/%3F'); - }); - it('ignores URL params and fragments on an http URL', () => { - const s = makeStorage( - `http://${DEFAULT_HOST}/v0/b/test-bucket/o/my/object.txt` + - '?ignoreme#please' - ); - expect(s.toString()).to.equal('gs://test-bucket/my/object.txt'); - }); - it('URL-decodes and ignores fragment on an http URL', () => { - const s = makeStorage( - `http://${DEFAULT_HOST}/v0/b/test-bucket/o/%3F?ignore` - ); - expect(s.toString()).to.equal('gs://test-bucket/?'); - }); - - it('ignores URL params and fragments on an https URL', () => { - const s = makeStorage( - `https://${DEFAULT_HOST}/v0/b/test-bucket/o/my/object.txt` + - '?ignoreme#please' - ); - expect(s.toString()).to.equal('gs://test-bucket/my/object.txt'); - }); - - it('URL-decodes and ignores fragment on an https URL', () => { - const s = makeStorage( - `https://${DEFAULT_HOST}/v0/b/test-bucket/o/%3F?ignore` - ); - expect(s.toString()).to.equal('gs://test-bucket/?'); - }); - }); - - describe('toString', () => { - it("Doesn't add trailing slash", () => { - const s = makeStorage('gs://test-bucket/foo'); - expect(s.toString()).to.equal('gs://test-bucket/foo'); - }); - it('Strips trailing slash', () => { - const s = makeStorage('gs://test-bucket/foo/'); - expect(s.toString()).to.equal('gs://test-bucket/foo'); - }); - }); - - describe('parent', () => { - it('Returns null at root', () => { - expect(root.parent).to.be.null; - }); - it('Returns root one level down', () => { - expect(child.parent!.toString()).to.equal('gs://test-bucket/'); - }); - it('Works correctly with empty levels', () => { - const s = makeStorage('gs://test-bucket/a///'); - expect(s.parent!.toString()).to.equal('gs://test-bucket/a/'); - }); - }); - - describe('root', () => { - it('Returns self at root', () => { - expect(root.root.toString()).to.equal('gs://test-bucket/'); - }); - - it('Returns root multiple levels down', () => { - const s = makeStorage('gs://test-bucket/a/b/c/d'); - expect(s.root.toString()).to.equal('gs://test-bucket/'); - }); - }); - - describe('bucket', () => { - it('Returns bucket name', () => { - expect(root.bucket).to.equal('test-bucket'); - }); - }); - - describe('fullPath', () => { - it('Returns full path without leading slash', () => { - const s = makeStorage('gs://test-bucket/full/path'); - expect(s.fullPath).to.equal('full/path'); - }); - }); - - describe('name', () => { - it('Works at top level', () => { - const s = makeStorage('gs://test-bucket/toplevel.txt'); - expect(s.name).to.equal('toplevel.txt'); - }); - - it('Works at not the top level', () => { - const s = makeStorage('gs://test-bucket/not/toplevel.txt'); - expect('toplevel.txt').to.equal(s.name); - }); - }); - - describe('child', () => { - it('works with a simple string', () => { - expect(root.child('a').toString()).to.equal('gs://test-bucket/a'); - }); - it('drops a trailing slash', () => { - expect(root.child('ab/').toString()).to.equal('gs://test-bucket/ab'); - }); - it('compresses repeated slashes', () => { - expect(root.child('//a///b/////').toString()).to.equal( - 'gs://test-bucket/a/b' - ); - }); - it('works chained multiple times with leading slashes', () => { - expect( - root.child('a').child('/b').child('c').child('d/e').toString() - ).to.equal('gs://test-bucket/a/b/c/d/e'); - }); - }); - - it("Doesn't send Authorization on null auth token", done => { - function newSend( - connection: TestingConnection, - url: string, - method: string, - body?: ArrayBufferView | Blob | string | null, - headers?: Headers - ): void { - expect(headers).to.not.be.undefined; - expect(headers!['Authorization']).to.be.undefined; - done(); - } - - const service = makeFakeService( - testShared.fakeApp, - testShared.emptyAuthProvider, - testShared.fakeAppCheckTokenProvider, - newSend - ); - const ref = service.refFromURL('gs://test-bucket'); - ref.child('foo').getMetadata(); - }); - - it('Works if the user logs in before creating the storage reference', done => { - // Regression test for b/27227221 - function newSend( - connection: TestingConnection, - url: string, - method: string, - body?: ArrayBufferView | Blob | string | null, - headers?: Headers - ): void { - expect(headers).to.not.be.undefined; - expect(headers!['Authorization']).to.equal( - 'Firebase ' + testShared.authToken - ); - done(); - } - - const service = makeFakeService( - testShared.fakeApp, - testShared.fakeAuthProvider, - testShared.fakeAppCheckTokenProvider, - newSend - ); - const ref = service.refFromURL('gs://test-bucket'); - ref.child('foo').getMetadata(); - }); - - describe('putString', () => { - it('Uses metadata.contentType for RAW format', () => { - // Regression test for b/30989476 - const task = child.putString('hello', StringFormat.RAW, { - contentType: 'lol/wut' - } as Metadata); - expect(task.snapshot.metadata!.contentType).to.equal('lol/wut'); - task.cancel(); - }); - it('Uses embedded content type in DATA_URL format', () => { - const task = child.putString( - 'data:lol/wat;base64,aaaa', - StringFormat.DATA_URL - ); - expect(task.snapshot.metadata!.contentType).to.equal('lol/wat'); - task.cancel(); - }); - it('Lets metadata.contentType override embedded content type in DATA_URL format', () => { - const task = child.putString( - 'data:ignore/me;base64,aaaa', - StringFormat.DATA_URL, - { contentType: 'tomato/soup' } as Metadata - ); - expect(task.snapshot.metadata!.contentType).to.equal('tomato/soup'); - task.cancel(); - }); - }); - - describe('Argument verification', () => { - describe('list', () => { - it('throws on invalid maxResults', () => { - it('throws on invalid maxResults', async () => { - await expect(child.list({ maxResults: 0 })).to.be.rejectedWith( - 'storage/invalid-argument' - ); - await expect(child.list({ maxResults: -4 })).to.be.rejectedWith( - 'storage/invalid-argument' - ); - await expect(child.list({ maxResults: 1001 })).to.be.rejectedWith( - 'storage/invalid-argument' - ); - }); - }); - }); - }); - - describe('root operations', () => { - it('put throws', () => { - expect(() => root.put(new Uint8Array())).to.throw( - 'storage/invalid-root-operation' - ); - }); - it('putString throws', () => { - expect(() => root.putString('raw', StringFormat.RAW)).to.throw( - 'storage/invalid-root-operation' - ); - }); - it('delete throws', () => { - expect(() => root.delete()).to.throw('storage/invalid-root-operation'); - }); - it('getMetadata throws', async () => { - await expect(root.getMetadata()).to.be.rejectedWith( - 'storage/invalid-root-operation' - ); - }); - it('updateMetadata throws', async () => { - await expect(root.updateMetadata({} as Metadata)).to.be.rejectedWith( - 'storage/invalid-root-operation' - ); - }); - it('getDownloadURL throws', async () => { - await expect(root.getDownloadURL()).to.be.rejectedWith( - 'storage/invalid-root-operation' - ); - }); - }); -}); diff --git a/packages/storage/test/unit/reference.exp.test.ts b/packages/storage/test/unit/reference.test.ts similarity index 100% rename from packages/storage/test/unit/reference.exp.test.ts rename to packages/storage/test/unit/reference.test.ts diff --git a/packages/storage/test/unit/service.compat.test.ts b/packages/storage/test/unit/service.compat.test.ts deleted file mode 100644 index f6796e3de05..00000000000 --- a/packages/storage/test/unit/service.compat.test.ts +++ /dev/null @@ -1,370 +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 { expect } from 'chai'; -import { TaskEvent } from '../../src/implementation/taskenums'; -import { Headers } from '../../src/implementation/connection'; -import { ConnectionPool } from '../../src/implementation/connectionPool'; -import { StorageServiceCompat } from '../../compat/service'; -import * as testShared from './testshared'; -import { DEFAULT_HOST } from '../../src/implementation/constants'; -import { FirebaseStorageError } from '../../src/implementation/error'; -import { FirebaseStorageImpl } from '../../src/service'; -import { FirebaseApp } from '@firebase/app-types'; -import { Provider } from '@firebase/component'; -import { FirebaseAuthInternalName } from '@firebase/auth-interop-types'; -import { AppCheckInternalComponentName } from '@firebase/app-check-interop-types'; -import { TestingConnection } from './connection'; - -const fakeAppGs = testShared.makeFakeApp('gs://mybucket'); -const fakeAppGsEndingSlash = testShared.makeFakeApp('gs://mybucket/'); -const fakeAppInvalidGs = testShared.makeFakeApp('gs://mybucket/hello'); -const connectionPool = new ConnectionPool(); - -function makeGsUrl(child: string = ''): string { - return 'gs://' + testShared.bucket + '/' + child; -} - -function makeService( - app: FirebaseApp, - authProvider: Provider, - appCheckProvider: Provider, - pool: ConnectionPool, - url?: string -): StorageServiceCompat { - const storageServiceCompat: StorageServiceCompat = new StorageServiceCompat( - app, - new FirebaseStorageImpl(app, authProvider, appCheckProvider, pool, url) - ); - return storageServiceCompat; -} - -describe('Firebase Storage > Service', () => { - describe('simple constructor', () => { - const service = makeService( - testShared.fakeApp, - testShared.fakeAuthProvider, - testShared.fakeAppCheckTokenProvider, - connectionPool - ); - it('Root refs point to the right place', () => { - const ref = service.ref(); - expect(ref.toString()).to.equal(makeGsUrl()); - }); - it('Child refs point to the right place', () => { - const ref = service.ref('path/to/child'); - expect(ref.toString()).to.equal(makeGsUrl('path/to/child')); - }); - it('Throws calling ref with a gs:// URL', () => { - const error = testShared.assertThrows(() => { - service.ref('gs://bucket/object'); - }, 'storage/invalid-argument'); - expect(error.message).to.match(/refFromURL/); - }); - it('Throws calling ref with an http:// URL', () => { - const error = testShared.assertThrows(() => { - service.ref(`http://${DEFAULT_HOST}/etc`); - }, 'storage/invalid-argument'); - expect(error.message).to.match(/refFromURL/); - }); - it('Throws calling ref with an https:// URL', () => { - const error = testShared.assertThrows(() => { - service.ref(`https://${DEFAULT_HOST}/etc`); - }, 'storage/invalid-argument'); - expect(error.message).to.match(/refFromURL/); - }); - }); - describe('custom bucket constructor', () => { - it('gs:// custom bucket constructor refs point to the right place', () => { - const service = makeService( - testShared.fakeApp, - testShared.fakeAuthProvider, - testShared.fakeAppCheckTokenProvider, - connectionPool, - 'gs://foo-bar.appspot.com' - ); - const ref = service.ref(); - expect(ref.toString()).to.equal('gs://foo-bar.appspot.com/'); - }); - it('http:// custom bucket constructor refs point to the right place', () => { - const service = makeService( - testShared.fakeApp, - testShared.fakeAuthProvider, - testShared.fakeAppCheckTokenProvider, - connectionPool, - `http://${DEFAULT_HOST}/v1/b/foo-bar.appspot.com/o` - ); - const ref = service.ref(); - expect(ref.toString()).to.equal('gs://foo-bar.appspot.com/'); - }); - it('https:// custom bucket constructor refs point to the right place', () => { - const service = makeService( - testShared.fakeApp, - testShared.fakeAuthProvider, - testShared.fakeAppCheckTokenProvider, - connectionPool, - `https://${DEFAULT_HOST}/v1/b/foo-bar.appspot.com/o` - ); - const ref = service.ref(); - expect(ref.toString()).to.equal('gs://foo-bar.appspot.com/'); - }); - - it('Bare bucket name constructor refs point to the right place', () => { - const service = makeService( - testShared.fakeApp, - testShared.fakeAuthProvider, - testShared.fakeAppCheckTokenProvider, - connectionPool, - 'foo-bar.appspot.com' - ); - const ref = service.ref(); - expect(ref.toString()).to.equal('gs://foo-bar.appspot.com/'); - }); - it('Child refs point to the right place', () => { - const service = makeService( - testShared.fakeApp, - testShared.fakeAuthProvider, - testShared.fakeAppCheckTokenProvider, - connectionPool, - 'foo-bar.appspot.com' - ); - const ref = service.ref('path/to/child'); - expect(ref.toString()).to.equal('gs://foo-bar.appspot.com/path/to/child'); - }); - it('Throws trying to construct with a gs:// URL containing an object path', () => { - const error = testShared.assertThrows(() => { - makeService( - testShared.fakeApp, - testShared.fakeAuthProvider, - testShared.fakeAppCheckTokenProvider, - connectionPool, - 'gs://bucket/object/' - ); - }, 'storage/invalid-default-bucket'); - expect(error.message).to.match(/Invalid default bucket/); - }); - }); - describe('default bucket config', () => { - it('gs:// works without ending slash', () => { - const service = makeService( - fakeAppGs, - testShared.fakeAuthProvider, - testShared.fakeAppCheckTokenProvider, - connectionPool - ); - expect(service.ref().toString()).to.equal('gs://mybucket/'); - }); - it('gs:// works with ending slash', () => { - const service = makeService( - fakeAppGsEndingSlash, - testShared.fakeAuthProvider, - testShared.fakeAppCheckTokenProvider, - connectionPool - ); - expect(service.ref().toString()).to.equal('gs://mybucket/'); - }); - it('Throws when config bucket is gs:// with an object path', () => { - testShared.assertThrows(() => { - makeService( - fakeAppInvalidGs, - testShared.fakeAuthProvider, - testShared.fakeAppCheckTokenProvider, - connectionPool - ); - }, 'storage/invalid-default-bucket'); - }); - }); - describe('connectStorageEmulator(service, host, port)', () => { - it('sets emulator host correctly', done => { - function newSend( - connection: TestingConnection, - url: string, - method: string, - body?: ArrayBufferView | Blob | string | null, - headers?: Headers - ): void { - // Expect emulator host to be in url of storage operations requests, - // in this case getDownloadURL. - expect(url).to.match(/^http:\/\/test\.host\.org:1234.+/); - connection.abort(); - done(); - } - const service = makeService( - testShared.fakeApp, - testShared.fakeAuthProvider, - testShared.fakeAppCheckTokenProvider, - testShared.makePool(newSend) - ); - service.useEmulator('test.host.org', 1234); - expect((service._delegate as FirebaseStorageImpl).host).to.equal( - 'http://test.host.org:1234' - ); - void service.ref('test.png').getDownloadURL(); - }); - }); - describe('refFromURL', () => { - const service = makeService( - testShared.fakeApp, - testShared.fakeAuthProvider, - testShared.fakeAppCheckTokenProvider, - connectionPool - ); - it('Works with gs:// URLs', () => { - const ref = service.refFromURL('gs://mybucket/child/path/image.png'); - expect(ref.toString()).to.equal('gs://mybucket/child/path/image.png'); - }); - it('Works with http:// URLs', () => { - const ref = service.refFromURL( - `http://${DEFAULT_HOST}/v0/b/` + - 'mybucket/o/child%2Fpath%2Fimage.png?downloadToken=hello' - ); - expect(ref.toString()).to.equal('gs://mybucket/child/path/image.png'); - }); - it('Works with https:// URLs', () => { - const ref = service.refFromURL( - `https://${DEFAULT_HOST}/v0/b/` + - 'mybucket/o/child%2Fpath%2Fimage.png?downloadToken=hello' - ); - expect(ref.toString()).to.equal('gs://mybucket/child/path/image.png'); - }); - it('Works with storage.googleapis.com URLs', () => { - const ref = service.refFromURL( - `https://storage.googleapis.com/mybucket/path%20with%20space/image.png` - ); - expect(ref.toString()).to.equal( - 'gs://mybucket/path with space/image.png' - ); - }); - it('Works with storage.googleapis.com URLs with query params', () => { - const ref = service.refFromURL( - `https://storage.googleapis.com/mybucket/path%20with%20space/image.png?X-Goog-Algorithm= -GOOG4-RSA-SHA256` - ); - expect(ref.toString()).to.equal( - 'gs://mybucket/path with space/image.png' - ); - }); - it('Works with storage.cloud.google.com URLs', () => { - const ref = service.refFromURL( - `https://storage.cloud.google.com/mybucket/path%20with%20space/image.png` - ); - expect(ref.toString()).to.equal( - 'gs://mybucket/path with space/image.png' - ); - }); - it('Works with storage.cloud.google.com URLs and escaped slash', () => { - const ref = service.refFromURL( - `https://storage.cloud.google.com/mybucket/path%20with%20space%2Fimage.png` - ); - expect(ref.toString()).to.equal( - 'gs://mybucket/path with space/image.png' - ); - }); - }); - describe('Argument verification', () => { - const service = makeService( - testShared.fakeApp, - testShared.fakeAuthProvider, - testShared.fakeAppCheckTokenProvider, - connectionPool - ); - describe('ref', () => { - it('Throws on gs:// argument', () => { - testShared.assertThrows( - testShared.bind(service.ref, service, 'gs://yo'), - 'storage/invalid-argument' - ); - }); - }); - describe('refFromURL', () => { - it('Throws with a non-URL string arg', () => { - const error = testShared.assertThrows( - testShared.bind(service.refFromURL, service, 'child'), - 'storage/invalid-argument' - ); - expect(error.message).to.match( - /expected a full URL but got a child path/i - ); - }); - it('Throws with an invalid URL arg', () => { - testShared.assertThrows( - testShared.bind(service.refFromURL, service, 'notlegit://url'), - 'storage/invalid-argument' - ); - }); - }); - describe('setMaxUploadRetryTime', () => { - it('Throws on negative arg', () => { - testShared.assertThrows( - testShared.bind(service.setMaxUploadRetryTime, service, -10), - 'storage/invalid-argument' - ); - }); - }); - describe('setMaxOperationRetryTime', () => { - it('Throws on negative arg', () => { - testShared.assertThrows( - testShared.bind(service.setMaxOperationRetryTime, service, -10), - 'storage/invalid-argument' - ); - }); - }); - }); - - describe('Deletion', () => { - const service = makeService( - testShared.fakeApp, - testShared.fakeAuthProvider, - testShared.fakeAppCheckTokenProvider, - connectionPool - ); - it('In-flight requests are canceled when the service is deleted', async () => { - const ref = service.refFromURL('gs://mybucket/image.jpg'); - const metadataPromise = ref.getMetadata(); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - service._delegate._delete(); - await expect(metadataPromise).to.be.rejectedWith('storage/app-deleted'); - }); - it('Requests fail when started after the service is deleted', async () => { - const ref = service.refFromURL('gs://mybucket/image.jpg'); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - service._delegate._delete(); - - await expect(ref.getMetadata()).to.be.rejectedWith('storage/app-deleted'); - }); - it('Running uploads fail when the service is deleted', () => { - const ref = service.refFromURL('gs://mybucket/image.jpg'); - const toReturn = new Promise((resolve, reject) => { - ref.put(new Uint8Array([97])).on( - TaskEvent.STATE_CHANGED, - null, - (err: FirebaseStorageError | Error) => { - expect((err as FirebaseStorageError).code).to.equal( - 'storage/app-deleted' - ); - resolve(); - }, - () => { - reject('Upload completed, should have been canceled'); - } - ); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - service._delegate._delete(); - }); - return toReturn; - }); - }); -}); diff --git a/packages/storage/test/unit/service.exp.test.ts b/packages/storage/test/unit/service.test.ts similarity index 95% rename from packages/storage/test/unit/service.exp.test.ts rename to packages/storage/test/unit/service.test.ts index a68ea1bea85..423eb732f8a 100644 --- a/packages/storage/test/unit/service.exp.test.ts +++ b/packages/storage/test/unit/service.test.ts @@ -418,4 +418,29 @@ GOOG4-RSA-SHA256` return toReturn; }); }); + + describe('Argument verification', () => { + const service = new FirebaseStorageImpl( + testShared.fakeApp, + testShared.fakeAuthProvider, + testShared.fakeAppCheckTokenProvider, + connectionPool + ); + describe('setMaxUploadRetryTime', () => { + it('Throws on negative arg', () => { + testShared.assertThrows( + () => (service.maxOperationRetryTime = -10), + 'storage/invalid-argument' + ); + }); + }); + describe('setMaxOperationRetryTime', () => { + it('Throws on negative arg', () => { + testShared.assertThrows( + () => (service.maxOperationRetryTime = -10), + 'storage/invalid-argument' + ); + }); + }); + }); });