UNPKG

@firebase/firestore

Version:

The Cloud Firestore component of the Firebase JS SDK.

1,390 lines (1,371 loc) • 1.32 MB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var app = require('@firebase/app'); var component = require('@firebase/component'); var logger = require('@firebase/logger'); var util$1 = require('util'); var util = require('@firebase/util'); var bloomBlob = require('@firebase/webchannel-wrapper/bloom-blob'); var crypto = require('crypto'); var grpc = require('@grpc/grpc-js'); var protoLoader = require('@grpc/proto-loader'); function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n["default"] = e; return Object.freeze(n); } var grpc__namespace = /*#__PURE__*/_interopNamespace(grpc); var protoLoader__namespace = /*#__PURE__*/_interopNamespace(protoLoader); const name$1 = "@firebase/firestore"; const version$1 = "4.9.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. */ /** * Simple wrapper around a nullable UID. Mostly exists to make code more * readable. */ class User { constructor(uid) { this.uid = uid; } isAuthenticated() { return this.uid != null; } /** * Returns a key representing this user, suitable for inclusion in a * dictionary. */ toKey() { if (this.isAuthenticated()) { return 'uid:' + this.uid; } else { return 'anonymous-user'; } } isEqual(otherUser) { return otherUser.uid === this.uid; } } /** A user with a null UID. */ User.UNAUTHENTICATED = new User(null); // TODO(mikelehen): Look into getting a proper uid-equivalent for // non-FirebaseAuth providers. User.GOOGLE_CREDENTIALS = new User('google-credentials-uid'); User.FIRST_PARTY = new User('first-party-uid'); User.MOCK_USER = new User('mock-user'); const version = "12.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. */ let SDK_VERSION = version; function setSDKVersion(version) { SDK_VERSION = version; } /** * @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. */ /** Formats an object as a JSON string, suitable for logging. */ function formatJSON(value) { // util.inspect() results in much more readable output than JSON.stringify() return util$1.inspect(value, { depth: 100 }); } /** * @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 logClient = new logger.Logger('@firebase/firestore'); // Helper methods are needed because variables can't be exported as read/write function getLogLevel() { return logClient.logLevel; } /** * Sets the verbosity of Cloud Firestore logs (debug, error, or silent). * * @param logLevel - The verbosity you set for activity and error logging. Can * be any of the following values: * * <ul> * <li>`debug` for the most verbose logging level, primarily for * debugging.</li> * <li>`error` to log errors only.</li> * <li><code>`silent` to turn off logging.</li> * </ul> */ function setLogLevel(logLevel) { logClient.setLogLevel(logLevel); } function logDebug(msg, ...obj) { if (logClient.logLevel <= logger.LogLevel.DEBUG) { const args = obj.map(argToString); logClient.debug(`Firestore (${SDK_VERSION}): ${msg}`, ...args); } } function logError(msg, ...obj) { if (logClient.logLevel <= logger.LogLevel.ERROR) { const args = obj.map(argToString); logClient.error(`Firestore (${SDK_VERSION}): ${msg}`, ...args); } } /** * @internal */ function logWarn(msg, ...obj) { if (logClient.logLevel <= logger.LogLevel.WARN) { const args = obj.map(argToString); logClient.warn(`Firestore (${SDK_VERSION}): ${msg}`, ...args); } } /** * Converts an additional log parameter to a string representation. */ function argToString(obj) { if (typeof obj === 'string') { return obj; } else { try { return formatJSON(obj); } catch (e) { // Converting to JSON failed, just log the object directly return obj; } } } /** * @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. */ function fail(id, messageOrContext, context) { let message = 'Unexpected state'; if (typeof messageOrContext === 'string') { message = messageOrContext; } else { context = messageOrContext; } _fail(id, message, context); } function _fail(id, failure, context) { // Log the failure in addition to throw an exception, just in case the // exception is swallowed. let message = `FIRESTORE (${SDK_VERSION}) INTERNAL ASSERTION FAILED: ${failure} (ID: ${id.toString(16)})`; if (context !== undefined) { try { const stringContext = JSON.stringify(context); message += ' CONTEXT: ' + stringContext; } catch (e) { message += ' CONTEXT: ' + context; } } logError(message); // NOTE: We don't use FirestoreError here because these are internal failures // that cannot be handled by the user. (Also it would create a circular // dependency between the error and assert modules which doesn't work.) throw new Error(message); } function hardAssert(assertion, id, messageOrContext, context) { let message = 'Unexpected state'; if (typeof messageOrContext === 'string') { message = messageOrContext; } else { context = messageOrContext; } if (!assertion) { _fail(id, message, context); } } /** * Fails if the given assertion condition is false, throwing an Error with the * given message if it did. * * The code of callsites invoking this function are stripped out in production * builds. Any side-effects of code within the debugAssert() invocation will not * happen in this case. * * @internal */ function debugAssert(assertion, message) { if (!assertion) { fail(0xdeb6, message); } } /** * Casts `obj` to `T`. In non-production builds, verifies that `obj` is an * instance of `T` before casting. */ function debugCast(obj, // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor) { return obj; } /** * @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 Code = { // Causes are copied from: // https://github.com/grpc/grpc/blob/bceec94ea4fc5f0085d81235d8e1c06798dc341a/include/grpc%2B%2B/impl/codegen/status_code_enum.h /** Not an error; returned on success. */ OK: 'ok', /** The operation was cancelled (typically by the caller). */ CANCELLED: 'cancelled', /** Unknown error or an error from a different error domain. */ UNKNOWN: 'unknown', /** * Client specified an invalid argument. Note that this differs from * FAILED_PRECONDITION. INVALID_ARGUMENT indicates arguments that are * problematic regardless of the state of the system (e.g., a malformed file * name). */ INVALID_ARGUMENT: 'invalid-argument', /** * Deadline expired before operation could complete. For operations that * change the state of the system, this error may be returned even if the * operation has completed successfully. For example, a successful response * from a server could have been delayed long enough for the deadline to * expire. */ DEADLINE_EXCEEDED: 'deadline-exceeded', /** Some requested entity (e.g., file or directory) was not found. */ NOT_FOUND: 'not-found', /** * Some entity that we attempted to create (e.g., file or directory) already * exists. */ ALREADY_EXISTS: 'already-exists', /** * The caller does not have permission to execute the specified operation. * PERMISSION_DENIED must not be used for rejections caused by exhausting * some resource (use RESOURCE_EXHAUSTED instead for those errors). * PERMISSION_DENIED must not be used if the caller cannot be identified * (use UNAUTHENTICATED instead for those errors). */ PERMISSION_DENIED: 'permission-denied', /** * The request does not have valid authentication credentials for the * operation. */ UNAUTHENTICATED: 'unauthenticated', /** * Some resource has been exhausted, perhaps a per-user quota, or perhaps the * entire file system is out of space. */ RESOURCE_EXHAUSTED: 'resource-exhausted', /** * Operation was rejected because the system is not in a state required for * the operation's execution. For example, directory to be deleted may be * non-empty, an rmdir operation is applied to a non-directory, etc. * * A litmus test that may help a service implementor in deciding * between FAILED_PRECONDITION, ABORTED, and UNAVAILABLE: * (a) Use UNAVAILABLE if the client can retry just the failing call. * (b) Use ABORTED if the client should retry at a higher-level * (e.g., restarting a read-modify-write sequence). * (c) Use FAILED_PRECONDITION if the client should not retry until * the system state has been explicitly fixed. E.g., if an "rmdir" * fails because the directory is non-empty, FAILED_PRECONDITION * should be returned since the client should not retry unless * they have first fixed up the directory by deleting files from it. * (d) Use FAILED_PRECONDITION if the client performs conditional * REST Get/Update/Delete on a resource and the resource on the * server does not match the condition. E.g., conflicting * read-modify-write on the same resource. */ FAILED_PRECONDITION: 'failed-precondition', /** * The operation was aborted, typically due to a concurrency issue like * sequencer check failures, transaction aborts, etc. * * See litmus test above for deciding between FAILED_PRECONDITION, ABORTED, * and UNAVAILABLE. */ ABORTED: 'aborted', /** * Operation was attempted past the valid range. E.g., seeking or reading * past end of file. * * Unlike INVALID_ARGUMENT, this error indicates a problem that may be fixed * if the system state changes. For example, a 32-bit file system will * generate INVALID_ARGUMENT if asked to read at an offset that is not in the * range [0,2^32-1], but it will generate OUT_OF_RANGE if asked to read from * an offset past the current file size. * * There is a fair bit of overlap between FAILED_PRECONDITION and * OUT_OF_RANGE. We recommend using OUT_OF_RANGE (the more specific error) * when it applies so that callers who are iterating through a space can * easily look for an OUT_OF_RANGE error to detect when they are done. */ OUT_OF_RANGE: 'out-of-range', /** Operation is not implemented or not supported/enabled in this service. */ UNIMPLEMENTED: 'unimplemented', /** * Internal errors. Means some invariants expected by underlying System has * been broken. If you see one of these errors, Something is very broken. */ INTERNAL: 'internal', /** * The service is currently unavailable. This is a most likely a transient * condition and may be corrected by retrying with a backoff. * * See litmus test above for deciding between FAILED_PRECONDITION, ABORTED, * and UNAVAILABLE. */ UNAVAILABLE: 'unavailable', /** Unrecoverable data loss or corruption. */ DATA_LOSS: 'data-loss' }; /** An error returned by a Firestore operation. */ class FirestoreError extends util.FirebaseError { /** @hideconstructor */ constructor( /** * The backend error code associated with this error. */ code, /** * A custom error description. */ message) { super(code, message); this.code = code; this.message = message; // HACK: We write a toString property directly because Error is not a real // class and so inheritance does not work correctly. We could alternatively // do the same "back-door inheritance" trick that FirebaseError does. this.toString = () => `${this.name}: [code=${this.code}]: ${this.message}`; } } /** * @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. */ class Deferred { constructor() { this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); } } /** * @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. */ class OAuthToken { constructor(value, user) { this.user = user; this.type = 'OAuth'; this.headers = new Map(); this.headers.set('Authorization', `Bearer ${value}`); } } /** * A CredentialsProvider that always yields an empty token. * @internal */ class EmptyAuthCredentialsProvider { getToken() { return Promise.resolve(null); } invalidateToken() { } start(asyncQueue, changeListener) { // Fire with initial user. asyncQueue.enqueueRetryable(() => changeListener(User.UNAUTHENTICATED)); } shutdown() { } } /** * A CredentialsProvider that always returns a constant token. Used for * emulator token mocking. */ class EmulatorAuthCredentialsProvider { constructor(token) { this.token = token; /** * Stores the listener registered with setChangeListener() * This isn't actually necessary since the UID never changes, but we use this * to verify the listen contract is adhered to in tests. */ this.changeListener = null; } getToken() { return Promise.resolve(this.token); } invalidateToken() { } start(asyncQueue, changeListener) { this.changeListener = changeListener; // Fire with initial user. asyncQueue.enqueueRetryable(() => changeListener(this.token.user)); } shutdown() { this.changeListener = null; } } class FirebaseAuthCredentialsProvider { constructor(authProvider) { this.authProvider = authProvider; /** Tracks the current User. */ this.currentUser = User.UNAUTHENTICATED; /** * Counter used to detect if the token changed while a getToken request was * outstanding. */ this.tokenCounter = 0; this.forceRefresh = false; this.auth = null; } start(asyncQueue, changeListener) { hardAssert(this.tokenListener === undefined, 0xa540); let lastTokenId = this.tokenCounter; // A change listener that prevents double-firing for the same token change. const guardedChangeListener = user => { if (this.tokenCounter !== lastTokenId) { lastTokenId = this.tokenCounter; return changeListener(user); } else { return Promise.resolve(); } }; // A promise that can be waited on to block on the next token change. // This promise is re-created after each change. let nextToken = new Deferred(); this.tokenListener = () => { this.tokenCounter++; this.currentUser = this.getUser(); nextToken.resolve(); nextToken = new Deferred(); asyncQueue.enqueueRetryable(() => guardedChangeListener(this.currentUser)); }; const awaitNextToken = () => { const currentTokenAttempt = nextToken; asyncQueue.enqueueRetryable(async () => { await currentTokenAttempt.promise; await guardedChangeListener(this.currentUser); }); }; const registerAuth = (auth) => { logDebug('FirebaseAuthCredentialsProvider', 'Auth detected'); this.auth = auth; if (this.tokenListener) { this.auth.addAuthTokenListener(this.tokenListener); awaitNextToken(); } }; this.authProvider.onInit(auth => registerAuth(auth)); // Our users can initialize Auth right after Firestore, so we give it // a chance to register itself with the component framework before we // determine whether to start up in unauthenticated mode. setTimeout(() => { if (!this.auth) { const auth = this.authProvider.getImmediate({ optional: true }); if (auth) { registerAuth(auth); } else { // If auth is still not available, proceed with `null` user logDebug('FirebaseAuthCredentialsProvider', 'Auth not yet detected'); nextToken.resolve(); nextToken = new Deferred(); } } }, 0); awaitNextToken(); } getToken() { // Take note of the current value of the tokenCounter so that this method // can fail (with an ABORTED error) if there is a token change while the // request is outstanding. const initialTokenCounter = this.tokenCounter; const forceRefresh = this.forceRefresh; this.forceRefresh = false; if (!this.auth) { return Promise.resolve(null); } return this.auth.getToken(forceRefresh).then(tokenData => { // Cancel the request since the token changed while the request was // outstanding so the response is potentially for a previous user (which // user, we can't be sure). if (this.tokenCounter !== initialTokenCounter) { logDebug('FirebaseAuthCredentialsProvider', 'getToken aborted due to token change.'); return this.getToken(); } else { if (tokenData) { hardAssert(typeof tokenData.accessToken === 'string', 0x7c5d, { tokenData }); return new OAuthToken(tokenData.accessToken, this.currentUser); } else { return null; } } }); } invalidateToken() { this.forceRefresh = true; } shutdown() { if (this.auth && this.tokenListener) { this.auth.removeAuthTokenListener(this.tokenListener); } this.tokenListener = undefined; } // Auth.getUid() can return null even with a user logged in. It is because // getUid() is synchronous, but the auth code populating Uid is asynchronous. // This method should only be called in the AuthTokenListener callback // to guarantee to get the actual user. getUser() { const currentUid = this.auth && this.auth.getUid(); hardAssert(currentUid === null || typeof currentUid === 'string', 0x0807, { currentUid }); return new User(currentUid); } } /* * FirstPartyToken provides a fresh token each time its value * is requested, because if the token is too old, requests will be rejected. * Technically this may no longer be necessary since the SDK should gracefully * recover from unauthenticated errors (see b/33147818 for context), but it's * safer to keep the implementation as-is. */ class FirstPartyToken { constructor(sessionIndex, iamToken, authTokenFactory) { this.sessionIndex = sessionIndex; this.iamToken = iamToken; this.authTokenFactory = authTokenFactory; this.type = 'FirstParty'; this.user = User.FIRST_PARTY; this._headers = new Map(); } /** * Gets an authorization token, using a provided factory function, or return * null. */ getAuthToken() { if (this.authTokenFactory) { return this.authTokenFactory(); } else { return null; } } get headers() { this._headers.set('X-Goog-AuthUser', this.sessionIndex); // Use array notation to prevent minification const authHeaderTokenValue = this.getAuthToken(); if (authHeaderTokenValue) { this._headers.set('Authorization', authHeaderTokenValue); } if (this.iamToken) { this._headers.set('X-Goog-Iam-Authorization-Token', this.iamToken); } return this._headers; } } /* * Provides user credentials required for the Firestore JavaScript SDK * to authenticate the user, using technique that is only available * to applications hosted by Google. */ class FirstPartyAuthCredentialsProvider { constructor(sessionIndex, iamToken, authTokenFactory) { this.sessionIndex = sessionIndex; this.iamToken = iamToken; this.authTokenFactory = authTokenFactory; } getToken() { return Promise.resolve(new FirstPartyToken(this.sessionIndex, this.iamToken, this.authTokenFactory)); } start(asyncQueue, changeListener) { // Fire with initial uid. asyncQueue.enqueueRetryable(() => changeListener(User.FIRST_PARTY)); } shutdown() { } invalidateToken() { } } class AppCheckToken { constructor(value) { this.value = value; this.type = 'AppCheck'; this.headers = new Map(); if (value && value.length > 0) { this.headers.set('x-firebase-appcheck', this.value); } } } class FirebaseAppCheckTokenProvider { constructor(app$1, appCheckProvider) { this.appCheckProvider = appCheckProvider; this.forceRefresh = false; this.appCheck = null; this.latestAppCheckToken = null; this.serverAppAppCheckToken = null; if (app._isFirebaseServerApp(app$1) && app$1.settings.appCheckToken) { this.serverAppAppCheckToken = app$1.settings.appCheckToken; } } start(asyncQueue, changeListener) { hardAssert(this.tokenListener === undefined, 0x0db8); const onTokenChanged = tokenResult => { if (tokenResult.error != null) { logDebug('FirebaseAppCheckTokenProvider', `Error getting App Check token; using placeholder token instead. Error: ${tokenResult.error.message}`); } const tokenUpdated = tokenResult.token !== this.latestAppCheckToken; this.latestAppCheckToken = tokenResult.token; logDebug('FirebaseAppCheckTokenProvider', `Received ${tokenUpdated ? 'new' : 'existing'} token.`); return tokenUpdated ? changeListener(tokenResult.token) : Promise.resolve(); }; this.tokenListener = (tokenResult) => { asyncQueue.enqueueRetryable(() => onTokenChanged(tokenResult)); }; const registerAppCheck = (appCheck) => { logDebug('FirebaseAppCheckTokenProvider', 'AppCheck detected'); this.appCheck = appCheck; if (this.tokenListener) { this.appCheck.addTokenListener(this.tokenListener); } }; this.appCheckProvider.onInit(appCheck => registerAppCheck(appCheck)); // Our users can initialize AppCheck after Firestore, so we give it // a chance to register itself with the component framework. setTimeout(() => { if (!this.appCheck) { const appCheck = this.appCheckProvider.getImmediate({ optional: true }); if (appCheck) { registerAppCheck(appCheck); } else { // If AppCheck is still not available, proceed without it. logDebug('FirebaseAppCheckTokenProvider', 'AppCheck not yet detected'); } } }, 0); } getToken() { if (this.serverAppAppCheckToken) { return Promise.resolve(new AppCheckToken(this.serverAppAppCheckToken)); } const forceRefresh = this.forceRefresh; this.forceRefresh = false; if (!this.appCheck) { return Promise.resolve(null); } return this.appCheck.getToken(forceRefresh).then(tokenResult => { if (tokenResult) { hardAssert(typeof tokenResult.token === 'string', 0xae0e, { tokenResult }); this.latestAppCheckToken = tokenResult.token; return new AppCheckToken(tokenResult.token); } else { return null; } }); } invalidateToken() { this.forceRefresh = true; } shutdown() { if (this.appCheck && this.tokenListener) { this.appCheck.removeTokenListener(this.tokenListener); } this.tokenListener = undefined; } } /** * An AppCheck token provider that always yields an empty token. * @internal */ class EmptyAppCheckTokenProvider { getToken() { return Promise.resolve(new AppCheckToken('')); } invalidateToken() { } start(asyncQueue, changeListener) { } shutdown() { } } /** * Builds a CredentialsProvider depending on the type of * the credentials passed in. */ function makeAuthCredentialsProvider(credentials) { if (!credentials) { return new EmptyAuthCredentialsProvider(); } switch (credentials['type']) { case 'firstParty': return new FirstPartyAuthCredentialsProvider(credentials['sessionIndex'] || '0', credentials['iamToken'] || null, credentials['authTokenFactory'] || null); case 'provider': return credentials['client']; default: throw new FirestoreError(Code.INVALID_ARGUMENT, 'makeAuthCredentialsProvider failed due to invalid credential type'); } } /** * @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. */ /** * Generates `nBytes` of random bytes. * * If `nBytes < 0` , an error will be thrown. */ function randomBytes(nBytes) { return crypto.randomBytes(nBytes); } /** * @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. */ /** * A utility class for generating unique alphanumeric IDs of a specified length. * * @internal * Exported internally for testing purposes. */ class AutoId { static newId() { // Alphanumeric characters const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; // The largest byte value that is a multiple of `char.length`. const maxMultiple = Math.floor(256 / chars.length) * chars.length; let autoId = ''; const targetLength = 20; while (autoId.length < targetLength) { const bytes = randomBytes(40); for (let i = 0; i < bytes.length; ++i) { // Only accept values that are [0, maxMultiple), this ensures they can // be evenly mapped to indices of `chars` via a modulo operation. if (autoId.length < targetLength && bytes[i] < maxMultiple) { autoId += chars.charAt(bytes[i] % chars.length); } } } return autoId; } } function primitiveComparator(left, right) { if (left < right) { return -1; } if (left > right) { return 1; } return 0; } /** Compare strings in UTF-8 encoded byte order */ function compareUtf8Strings(left, right) { // Find the first differing character (a.k.a. "UTF-16 code unit") in the two strings and, // if found, use that character to determine the relative ordering of the two strings as a // whole. Comparing UTF-16 strings in UTF-8 byte order can be done simply and efficiently by // comparing the UTF-16 code units (chars). This serendipitously works because of the way UTF-8 // and UTF-16 happen to represent Unicode code points. // // After finding the first pair of differing characters, there are two cases: // // Case 1: Both characters are non-surrogates (code points less than or equal to 0xFFFF) or // both are surrogates from a surrogate pair (that collectively represent code points greater // than 0xFFFF). In this case their numeric order as UTF-16 code units is the same as the // lexicographical order of their corresponding UTF-8 byte sequences. A direct comparison is // sufficient. // // Case 2: One character is a surrogate and the other is not. In this case the surrogate- // containing string is always ordered after the non-surrogate. This is because surrogates are // used to represent code points greater than 0xFFFF which have 4-byte UTF-8 representations // and are lexicographically greater than the 1, 2, or 3-byte representations of code points // less than or equal to 0xFFFF. // // An example of why Case 2 is required is comparing the following two Unicode code points: // // |-----------------------|------------|---------------------|-----------------| // | Name | Code Point | UTF-8 Encoding | UTF-16 Encoding | // |-----------------------|------------|---------------------|-----------------| // | Replacement Character | U+FFFD | 0xEF 0xBF 0xBD | 0xFFFD | // | Grinning Face | U+1F600 | 0xF0 0x9F 0x98 0x80 | 0xD83D 0xDE00 | // |-----------------------|------------|---------------------|-----------------| // // A lexicographical comparison of the UTF-8 encodings of these code points would order // "Replacement Character" _before_ "Grinning Face" because 0xEF is less than 0xF0. However, a // direct comparison of the UTF-16 code units, as would be done in case 1, would erroneously // produce the _opposite_ ordering, because 0xFFFD is _greater than_ 0xD83D. As it turns out, // this relative ordering holds for all comparisons of UTF-16 code points requiring a surrogate // pair with those that do not. const length = Math.min(left.length, right.length); for (let i = 0; i < length; i++) { const leftChar = left.charAt(i); const rightChar = right.charAt(i); if (leftChar !== rightChar) { return isSurrogate(leftChar) === isSurrogate(rightChar) ? primitiveComparator(leftChar, rightChar) : isSurrogate(leftChar) ? 1 : -1; } } // Use the lengths of the strings to determine the overall comparison result since either the // strings were equal or one is a prefix of the other. return primitiveComparator(left.length, right.length); } const MIN_SURROGATE$1 = 0xd800; const MAX_SURROGATE$1 = 0xdfff; function isSurrogate(s) { const c = s.charCodeAt(0); return c >= MIN_SURROGATE$1 && c <= MAX_SURROGATE$1; } /** Helper to compare arrays using isEqual(). */ function arrayEquals(left, right, comparator) { if (left.length !== right.length) { return false; } return left.every((value, index) => comparator(value, right[index])); } /** * Returns the immediate lexicographically-following string. This is useful to * construct an inclusive range for indexeddb iterators. */ function immediateSuccessor(s) { // Return the input string, with an additional NUL byte appended. return s + '\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. */ const DOCUMENT_KEY_NAME = '__name__'; /** * Path represents an ordered sequence of string segments. */ class BasePath { constructor(segments, offset, length) { if (offset === undefined) { offset = 0; } else if (offset > segments.length) { fail(0x027d, { offset, range: segments.length }); } if (length === undefined) { length = segments.length - offset; } else if (length > segments.length - offset) { fail(0x06d2, { length, range: segments.length - offset }); } this.segments = segments; this.offset = offset; this.len = length; } get length() { return this.len; } isEqual(other) { return BasePath.comparator(this, other) === 0; } child(nameOrPath) { const segments = this.segments.slice(this.offset, this.limit()); if (nameOrPath instanceof BasePath) { nameOrPath.forEach(segment => { segments.push(segment); }); } else { segments.push(nameOrPath); } return this.construct(segments); } /** The index of one past the last segment of the path. */ limit() { return this.offset + this.length; } popFirst(size) { size = size === undefined ? 1 : size; return this.construct(this.segments, this.offset + size, this.length - size); } popLast() { return this.construct(this.segments, this.offset, this.length - 1); } firstSegment() { return this.segments[this.offset]; } lastSegment() { return this.get(this.length - 1); } get(index) { return this.segments[this.offset + index]; } isEmpty() { return this.length === 0; } isPrefixOf(other) { if (other.length < this.length) { return false; } for (let i = 0; i < this.length; i++) { if (this.get(i) !== other.get(i)) { return false; } } return true; } isImmediateParentOf(potentialChild) { if (this.length + 1 !== potentialChild.length) { return false; } for (let i = 0; i < this.length; i++) { if (this.get(i) !== potentialChild.get(i)) { return false; } } return true; } forEach(fn) { for (let i = this.offset, end = this.limit(); i < end; i++) { fn(this.segments[i]); } } toArray() { return this.segments.slice(this.offset, this.limit()); } /** * Compare 2 paths segment by segment, prioritizing numeric IDs * (e.g., "__id123__") in numeric ascending order, followed by string * segments in lexicographical order. */ static comparator(p1, p2) { const len = Math.min(p1.length, p2.length); for (let i = 0; i < len; i++) { const comparison = BasePath.compareSegments(p1.get(i), p2.get(i)); if (comparison !== 0) { return comparison; } } return primitiveComparator(p1.length, p2.length); } static compareSegments(lhs, rhs) { const isLhsNumeric = BasePath.isNumericId(lhs); const isRhsNumeric = BasePath.isNumericId(rhs); if (isLhsNumeric && !isRhsNumeric) { // Only lhs is numeric return -1; } else if (!isLhsNumeric && isRhsNumeric) { // Only rhs is numeric return 1; } else if (isLhsNumeric && isRhsNumeric) { // both numeric return BasePath.extractNumericId(lhs).compare(BasePath.extractNumericId(rhs)); } else { // both non-numeric return compareUtf8Strings(lhs, rhs); } } // Checks if a segment is a numeric ID (starts with "__id" and ends with "__"). static isNumericId(segment) { return segment.startsWith('__id') && segment.endsWith('__'); } static extractNumericId(segment) { return bloomBlob.Integer.fromString(segment.substring(4, segment.length - 2)); } } /** * A slash-separated path for navigating resources (documents and collections) * within Firestore. * * @internal */ class ResourcePath extends BasePath { construct(segments, offset, length) { return new ResourcePath(segments, offset, length); } canonicalString() { // NOTE: The client is ignorant of any path segments containing escape // sequences (e.g. __id123__) and just passes them through raw (they exist // for legacy reasons and should not be used frequently). return this.toArray().join('/'); } toString() { return this.canonicalString(); } /** * Returns a string representation of this path * where each path segment has been encoded with * `encodeURIComponent`. */ toUriEncodedString() { return this.toArray().map(encodeURIComponent).join('/'); } /** * Creates a resource path from the given slash-delimited string. If multiple * arguments are provided, all components are combined. Leading and trailing * slashes from all components are ignored. */ static fromString(...pathComponents) { // NOTE: The client is ignorant of any path segments containing escape // sequences (e.g. __id123__) and just passes them through raw (they exist // for legacy reasons and should not be used frequently). const segments = []; for (const path of pathComponents) { if (path.indexOf('//') >= 0) { throw new FirestoreError(Code.INVALID_ARGUMENT, `Invalid segment (${path}). Paths must not contain // in them.`); } // Strip leading and trailing slashed. segments.push(...path.split('/').filter(segment => segment.length > 0)); } return new ResourcePath(segments); } static emptyPath() { return new ResourcePath([]); } } const identifierRegExp = /^[_a-zA-Z][_a-zA-Z0-9]*$/; /** * A dot-separated path for navigating sub-objects within a document. * @internal */ class FieldPath$1 extends BasePath { construct(segments, offset, length) { return new FieldPath$1(segments, offset, length); } /** * Returns true if the string could be used as a segment in a field path * without escaping. */ static isValidIdentifier(segment) { return identifierRegExp.test(segment); } canonicalString() { return this.toArray() .map(str => { str = str.replace(/\\/g, '\\\\').replace(/`/g, '\\`'); if (!FieldPath$1.isValidIdentifier(str)) { str = '`' + str + '`'; } return str; }) .join('.'); } toString() { return this.canonicalString(); } /** * Returns true if this field references the key of a document. */ isKeyField() { return this.length === 1 && this.get(0) === DOCUMENT_KEY_NAME; } /** * The field designating the key of a document. */ static keyField() { return new FieldPath$1([DOCUMENT_KEY_NAME]); } /** * Parses a field string from the given server-formatted string. * * - Splitting the empty string is not allowed (for now at least). * - Empty segments within the string (e.g. if there are two consecutive * separators) are not allowed. * * TODO(b/37244157): we should make this more strict. Right now, it allows * non-identifier path components, even if they aren't escaped. */ static fromServerFormat(path) { const segments = []; let current = ''; let i = 0; const addCurrentSegment = () => { if (current.length === 0) { throw new FirestoreError(Code.INVALID_ARGUMENT, `Invalid field path (${path}). Paths must not be empty, begin ` + `with '.', end with '.', or contain '..'`); } segments.push(current); current = ''; }; let inBackticks = false; while (i < path.length) { const c = path[i]; if (c === '\\') { if (i + 1 === path.length) { throw new FirestoreError(Code.INVALID_ARGUMENT, 'Path has trailing escape character: ' + path); } const next = path[i + 1]; if (!(next === '\\' || next === '.' || next === '`')) { throw new FirestoreError(Code.INVALID_ARGUMENT, 'Path has invalid escape sequence: ' + path); } current += next; i += 2; } else if (c === '`') { inBackticks = !inBackticks; i++; } else if (c === '.' && !inBackticks) { addCurrentSegment(); i++; } else { current += c; i++; } } addCurrentSegment(); if (inBackticks) { throw new FirestoreError(Code.INVALID_ARGUMENT, 'Unterminated ` in path: ' + path); } return new FieldPath$1(segments); } static emptyPath() { return new FieldPath$1([]); } } /** * @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. */ /** * @internal */ class DocumentKey { constructor(path) { this.path = path; } static fromPath(path) { return new DocumentKey(ResourcePath.fromString(path)); } static fromName(name) { return new DocumentKey(ResourcePath.fromString(name).popFirst(5)); } static empty() { return new DocumentKey(ResourcePath.emptyPath()); } get collectionGroup() { return this.path.popLast().lastSegment(); } /** Returns true if the document is in the specified collectionId. */ hasCollectionId(collectionId) { return (this.path.length >= 2 && this.path.get(this.path.length - 2) === collectionId); } /** Returns the collection group (i.e. the name of the parent collection) for this key. */ getCollectionGroup() { return this.path.get(this.path.length - 2); } /** Returns the fully qualified path to the parent collection. */ getCollectionPath() { return this.path.popLast(); } isEqual(other) { return (other !== null && ResourcePath.comparator(this.path, other.path) === 0); } toString() { return this.path.toString(); } static comparator(k1, k2) { return ResourcePath.comparator(k1.path, k2.path); } static isDocumentKey(path) { return path.length % 2 === 0; } /** * Creates and returns a new document key with the given segments. * * @param segments - The segments of the path to the document * @returns A new instance of DocumentKey */ static fromSegments(segments) { return new DocumentKey(new ResourcePath(segments.slice())); } } /** * @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. */ function validateNonEmptyArgument(functionName, argumentName, argument) { if (!argument) { throw new FirestoreError(Code.INVALID_ARGUMENT, `Function ${functionName}() cannot be called with an empty ${argumentName}.`); } } /** * Validates t