@firebase/firestore
Version:
The Cloud Firestore component of the Firebase JS SDK.
1,341 lines (1,325 loc) • 299 kB
JavaScript
import { _getProvider, getApp, _removeServiceInstance, _registerComponent, registerVersion, SDK_VERSION as SDK_VERSION$1 } from '@firebase/app';
import { Component } from '@firebase/component';
import { createMockUserToken, getModularInstance } from '@firebase/util';
import { Logger, LogLevel } from '@firebase/logger';
import { inspect } from 'util';
import * as nodeFetch from 'node-fetch';
import { randomBytes as randomBytes$1 } from 'crypto';
const version = "2.3.6";
const version$1 = "8.6.7";
/**
* @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$1;
function setSDKVersion(version) {
SDK_VERSION = version;
}
/**
* @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');
/**
* @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 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('@firebase/firestore');
/**
* 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 <= LogLevel.DEBUG) {
const args = obj.map(argToString);
logClient.debug(`Firestore (${SDK_VERSION}): ${msg}`, ...args);
}
}
function logError(msg, ...obj) {
if (logClient.logLevel <= LogLevel.ERROR) {
const args = obj.map(argToString);
logClient.error(`Firestore (${SDK_VERSION}): ${msg}`, ...args);
}
}
function logWarn(msg, ...obj) {
if (logClient.logLevel <= 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.
*/
/**
* Unconditionally fails, throwing an Error with the given message.
* Messages are stripped in production builds.
*
* Returns `never` and can be used in expressions:
* @example
* let futureVar = fail('not implemented yet');
*/
function fail(failure = 'Unexpected state') {
// Log the failure in addition to throw an exception, just in case the
// exception is swallowed.
const message = `FIRESTORE (${SDK_VERSION}) INTERNAL ASSERTION FAILED: ` + failure;
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);
}
/**
* Fails if the given assertion condition is false, throwing an Error with the
* given message if it did.
*
* Messages are stripped in production builds.
*/
function hardAssert(assertion, message) {
if (!assertion) {
fail();
}
}
/**
* 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 can not 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 Error {
/** @hideconstructor */
constructor(
/**
* The backend error code associated with this error.
*/
code,
/**
* A custom error description.
*/
message) {
super(message);
this.code = code;
this.message = message;
/** The custom name for all FirestoreErrors. */
this.name = 'FirebaseError';
// 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.authHeaders = {};
// Set the headers using Object Literal notation to avoid minification
this.authHeaders['Authorization'] = `Bearer ${value}`;
}
}
/** A CredentialsProvider that always yields an empty token. */
class EmptyCredentialsProvider {
constructor() {
/**
* 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(null);
}
invalidateToken() { }
setChangeListener(asyncQueue, changeListener) {
this.changeListener = changeListener;
// Fire with initial user.
asyncQueue.enqueueRetryable(() => changeListener(User.UNAUTHENTICATED));
}
removeChangeListener() {
this.changeListener = null;
}
}
/**
* A CredentialsProvider that always returns a constant token. Used for
* emulator token mocking.
*/
class EmulatorCredentialsProvider {
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() { }
setChangeListener(asyncQueue, changeListener) {
this.changeListener = changeListener;
// Fire with initial user.
asyncQueue.enqueueRetryable(() => changeListener(this.token.user));
}
removeChangeListener() {
this.changeListener = null;
}
}
class FirebaseCredentialsProvider {
constructor(authProvider) {
/** Tracks the current User. */
this.currentUser = User.UNAUTHENTICATED;
/** Promise that allows blocking on the initialization of Firebase Auth. */
this.authDeferred = new Deferred();
/**
* Counter used to detect if the token changed while a getToken request was
* outstanding.
*/
this.tokenCounter = 0;
this.forceRefresh = false;
this.auth = null;
this.asyncQueue = null;
this.tokenListener = () => {
this.tokenCounter++;
this.currentUser = this.getUser();
this.authDeferred.resolve();
if (this.changeListener) {
this.asyncQueue.enqueueRetryable(() => this.changeListener(this.currentUser));
}
};
const registerAuth = (auth) => {
logDebug('FirebaseCredentialsProvider', 'Auth detected');
this.auth = auth;
this.auth.addAuthTokenListener(this.tokenListener);
};
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 = authProvider.getImmediate({ optional: true });
if (auth) {
registerAuth(auth);
}
else {
// If auth is still not available, proceed with `null` user
logDebug('FirebaseCredentialsProvider', 'Auth not yet detected');
this.authDeferred.resolve();
}
}
}, 0);
}
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('FirebaseCredentialsProvider', 'getToken aborted due to token change.');
return this.getToken();
}
else {
if (tokenData) {
hardAssert(typeof tokenData.accessToken === 'string');
return new OAuthToken(tokenData.accessToken, this.currentUser);
}
else {
return null;
}
}
});
}
invalidateToken() {
this.forceRefresh = true;
}
setChangeListener(asyncQueue, changeListener) {
this.asyncQueue = asyncQueue;
// Blocks the AsyncQueue until the next user is available.
this.asyncQueue.enqueueRetryable(async () => {
await this.authDeferred.promise;
await changeListener(this.currentUser);
this.changeListener = changeListener;
});
}
removeChangeListener() {
if (this.auth) {
this.auth.removeAuthTokenListener(this.tokenListener);
}
this.changeListener = () => Promise.resolve();
}
// 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');
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(gapi, sessionIndex, iamToken) {
this.gapi = gapi;
this.sessionIndex = sessionIndex;
this.iamToken = iamToken;
this.type = 'FirstParty';
this.user = User.FIRST_PARTY;
}
get authHeaders() {
const headers = {
'X-Goog-AuthUser': this.sessionIndex
};
// Use array notation to prevent minification
const authHeader = this.gapi['auth']['getAuthHeaderValueForFirstParty']([]);
if (authHeader) {
headers['Authorization'] = authHeader;
}
if (this.iamToken) {
headers['X-Goog-Iam-Authorization-Token'] = this.iamToken;
}
return 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 FirstPartyCredentialsProvider {
constructor(gapi, sessionIndex, iamToken) {
this.gapi = gapi;
this.sessionIndex = sessionIndex;
this.iamToken = iamToken;
}
getToken() {
return Promise.resolve(new FirstPartyToken(this.gapi, this.sessionIndex, this.iamToken));
}
setChangeListener(asyncQueue, changeListener) {
// Fire with initial uid.
asyncQueue.enqueueRetryable(() => changeListener(User.FIRST_PARTY));
}
removeChangeListener() { }
invalidateToken() { }
}
/**
* Builds a CredentialsProvider depending on the type of
* the credentials passed in.
*/
function makeCredentialsProvider(credentials) {
if (!credentials) {
return new EmptyCredentialsProvider();
}
switch (credentials['type']) {
case 'gapi':
const client = credentials['client'];
// Make sure this really is a Gapi client.
hardAssert(!!(typeof client === 'object' &&
client !== null &&
client['auth'] &&
client['auth']['getAuthHeaderValueForFirstParty']));
return new FirstPartyCredentialsProvider(client, credentials['sessionIndex'] || '0', credentials['iamToken'] || null);
case 'provider':
return credentials['client'];
default:
throw new FirestoreError(Code.INVALID_ARGUMENT, 'makeCredentialsProvider failed due to invalid credential type');
}
}
/**
* @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 DatabaseInfo {
/**
* Constructs a DatabaseInfo using the provided host, databaseId and
* persistenceKey.
*
* @param databaseId - The database to use.
* @param appId - The Firebase App Id.
* @param persistenceKey - A unique identifier for this Firestore's local
* storage (used in conjunction with the databaseId).
* @param host - The Firestore backend host to connect to.
* @param ssl - Whether to use SSL when connecting.
* @param forceLongPolling - Whether to use the forceLongPolling option
* when using WebChannel as the network transport.
* @param autoDetectLongPolling - Whether to use the detectBufferingProxy
* option when using WebChannel as the network transport.
* @param useFetchStreams Whether to use the Fetch API instead of
* XMLHTTPRequest
*/
constructor(databaseId, appId, persistenceKey, host, ssl, forceLongPolling, autoDetectLongPolling, useFetchStreams) {
this.databaseId = databaseId;
this.appId = appId;
this.persistenceKey = persistenceKey;
this.host = host;
this.ssl = ssl;
this.forceLongPolling = forceLongPolling;
this.autoDetectLongPolling = autoDetectLongPolling;
this.useFetchStreams = useFetchStreams;
}
}
/** The default database name for a project. */
const DEFAULT_DATABASE_NAME = '(default)';
/** Represents the database ID a Firestore client is associated with. */
class DatabaseId {
constructor(projectId, database) {
this.projectId = projectId;
this.database = database ? database : DEFAULT_DATABASE_NAME;
}
get isDefaultDatabase() {
return this.database === DEFAULT_DATABASE_NAME;
}
isEqual(other) {
return (other instanceof DatabaseId &&
other.projectId === this.projectId &&
other.database === this.database);
}
}
/**
* @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();
}
if (length === undefined) {
length = segments.length - offset;
}
else if (length > segments.length - offset) {
fail();
}
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());
}
static comparator(p1, p2) {
const len = Math.min(p1.length, p2.length);
for (let i = 0; i < len; i++) {
const left = p1.get(i);
const right = p2.get(i);
if (left < right) {
return -1;
}
if (left > right) {
return 1;
}
}
if (p1.length < p2.length) {
return -1;
}
if (p1.length > p2.length) {
return 1;
}
return 0;
}
}
/**
* A slash-separated path for navigating resources (documents and collections)
* within Firestore.
*/
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();
}
/**
* 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 traling 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. */
class FieldPath extends BasePath {
construct(segments, offset, length) {
return new FieldPath(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.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([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(segments);
}
static emptyPath() {
return new FieldPath([]);
}
}
/**
* @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 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));
}
/** 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);
}
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 that two boolean options are not set at the same time.
*/
function validateIsNotUsedTogether(optionName1, argument1, optionName2, argument2) {
if (argument1 === true && argument2 === true) {
throw new FirestoreError(Code.INVALID_ARGUMENT, `${optionName1} and ${optionName2} cannot be used together.`);
}
}
/**
* Validates that `path` refers to a document (indicated by the fact it contains
* an even numbers of segments).
*/
function validateDocumentPath(path) {
if (!DocumentKey.isDocumentKey(path)) {
throw new FirestoreError(Code.INVALID_ARGUMENT, `Invalid document reference. Document references must have an even number of segments, but ${path} has ${path.length}.`);
}
}
/**
* Validates that `path` refers to a collection (indicated by the fact it
* contains an odd numbers of segments).
*/
function validateCollectionPath(path) {
if (DocumentKey.isDocumentKey(path)) {
throw new FirestoreError(Code.INVALID_ARGUMENT, `Invalid collection reference. Collection references must have an odd number of segments, but ${path} has ${path.length}.`);
}
}
/**
* Returns true if it's a non-null object without a custom prototype
* (i.e. excludes Array, Date, etc.).
*/
function isPlainObject(input) {
return (typeof input === 'object' &&
input !== null &&
(Object.getPrototypeOf(input) === Object.prototype ||
Object.getPrototypeOf(input) === null));
}
/** Returns a string describing the type / value of the provided input. */
function valueDescription(input) {
if (input === undefined) {
return 'undefined';
}
else if (input === null) {
return 'null';
}
else if (typeof input === 'string') {
if (input.length > 20) {
input = `${input.substring(0, 20)}...`;
}
return JSON.stringify(input);
}
else if (typeof input === 'number' || typeof input === 'boolean') {
return '' + input;
}
else if (typeof input === 'object') {
if (input instanceof Array) {
return 'an array';
}
else {
const customObjectName = tryGetCustomObjectType(input);
if (customObjectName) {
return `a custom ${customObjectName} object`;
}
else {
return 'an object';
}
}
}
else if (typeof input === 'function') {
return 'a function';
}
else {
return fail();
}
}
/** Hacky method to try to get the constructor name for an object. */
function tryGetCustomObjectType(input) {
if (input.constructor) {
const funcNameRegex = /function\s+([^\s(]+)\s*\(/;
const results = funcNameRegex.exec(input.constructor.toString());
if (results && results.length > 1) {
return results[1];
}
}
return null;
}
/**
* Casts `obj` to `T`, optionally unwrapping Compat types to expose the
* underlying instance. Throws if `obj` is not an instance of `T`.
*
* This cast is used in the Lite and Full SDK to verify instance types for
* arguments passed to the public API.
*/
function cast(obj,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor) {
if ('_delegate' in obj) {
// Unwrap Compat types
// eslint-disable-next-line @typescript-eslint/no-explicit-any
obj = obj._delegate;
}
if (!(obj instanceof constructor)) {
if (constructor.name === obj.constructor.name) {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Type does not match the expected instance. Did you pass a ' +
`reference from a different Firestore SDK?`);
}
else {
const description = valueDescription(obj);
throw new FirestoreError(Code.INVALID_ARGUMENT, `Expected type '${constructor.name}', but it was: ${description}`);
}
}
return obj;
}
function validatePositiveNumber(functionName, n) {
if (n <= 0) {
throw new FirestoreError(Code.INVALID_ARGUMENT, `Function ${functionName}() requires a positive number, but it was: ${n}.`);
}
}
/**
* @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.
*/
const LOG_TAG = 'RestConnection';
/**
* Maps RPC names to the corresponding REST endpoint name.
*
* We use array notation to avoid mangling.
*/
const RPC_NAME_URL_MAPPING = {};
RPC_NAME_URL_MAPPING['BatchGetDocuments'] = 'batchGet';
RPC_NAME_URL_MAPPING['Commit'] = 'commit';
RPC_NAME_URL_MAPPING['RunQuery'] = 'runQuery';
const RPC_URL_VERSION = 'v1';
// SDK_VERSION is updated to different value at runtime depending on the entry point,
// so we need to get its value when we need it in a function.
function getGoogApiClientValue() {
return 'gl-js/ fire/' + SDK_VERSION;
}
/**
* Base class for all Rest-based connections to the backend (WebChannel and
* HTTP).
*/
class RestConnection {
constructor(databaseInfo) {
this.databaseInfo = databaseInfo;
this.databaseId = databaseInfo.databaseId;
const proto = databaseInfo.ssl ? 'https' : 'http';
this.baseUrl = proto + '://' + databaseInfo.host;
this.databaseRoot =
'projects/' +
this.databaseId.projectId +
'/databases/' +
this.databaseId.database +
'/documents';
}
invokeRPC(rpcName, path, req, token) {
const url = this.makeUrl(rpcName, path);
logDebug(LOG_TAG, 'Sending: ', url, req);
const headers = {};
this.modifyHeadersForRequest(headers, token);
return this.performRPCRequest(rpcName, url, headers, req).then(response => {
logDebug(LOG_TAG, 'Received: ', response);
return response;
}, (err) => {
logWarn(LOG_TAG, `${rpcName} failed with error: `, err, 'url: ', url, 'request:', req);
throw err;
});
}
invokeStreamingRPC(rpcName, path, request, token) {
// The REST API automatically aggregates all of the streamed results, so we
// can just use the normal invoke() method.
return this.invokeRPC(rpcName, path, request, token);
}
/**
* Modifies the headers for a request, adding any authorization token if
* present and any additional headers for the request.
*/
modifyHeadersForRequest(headers, token) {
headers['X-Goog-Api-Client'] = getGoogApiClientValue();
// Content-Type: text/plain will avoid preflight requests which might
// mess with CORS and redirects by proxies. If we add custom headers
// we will need to change this code to potentially use the $httpOverwrite
// parameter supported by ESF to avoid triggering preflight requests.
headers['Content-Type'] = 'text/plain';
if (this.databaseInfo.appId) {
headers['X-Firebase-GMPID'] = this.databaseInfo.appId;
}
if (token) {
for (const header in token.authHeaders) {
if (token.authHeaders.hasOwnProperty(header)) {
headers[header] = token.authHeaders[header];
}
}
}
}
makeUrl(rpcName, path) {
const urlRpcName = RPC_NAME_URL_MAPPING[rpcName];
return `${this.baseUrl}/${RPC_URL_VERSION}/${path}:${urlRpcName}`;
}
}
/**
* @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.
*/
/**
* Error Codes describing the different ways GRPC can fail. These are copied
* directly from GRPC's sources here:
*
* https://github.com/grpc/grpc/blob/bceec94ea4fc5f0085d81235d8e1c06798dc341a/include/grpc%2B%2B/impl/codegen/status_code_enum.h
*
* Important! The names of these identifiers matter because the string forms
* are used for reverse lookups from the webchannel stream. Do NOT change the
* names of these identifiers or change this into a const enum.
*/
var RpcCode;
(function (RpcCode) {
RpcCode[RpcCode["OK"] = 0] = "OK";
RpcCode[RpcCode["CANCELLED"] = 1] = "CANCELLED";
RpcCode[RpcCode["UNKNOWN"] = 2] = "UNKNOWN";
RpcCode[RpcCode["INVALID_ARGUMENT"] = 3] = "INVALID_ARGUMENT";
RpcCode[RpcCode["DEADLINE_EXCEEDED"] = 4] = "DEADLINE_EXCEEDED";
RpcCode[RpcCode["NOT_FOUND"] = 5] = "NOT_FOUND";
RpcCode[RpcCode["ALREADY_EXISTS"] = 6] = "ALREADY_EXISTS";
RpcCode[RpcCode["PERMISSION_DENIED"] = 7] = "PERMISSION_DENIED";
RpcCode[RpcCode["UNAUTHENTICATED"] = 16] = "UNAUTHENTICATED";
RpcCode[RpcCode["RESOURCE_EXHAUSTED"] = 8] = "RESOURCE_EXHAUSTED";
RpcCode[RpcCode["FAILED_PRECONDITION"] = 9] = "FAILED_PRECONDITION";
RpcCode[RpcCode["ABORTED"] = 10] = "ABORTED";
RpcCode[RpcCode["OUT_OF_RANGE"] = 11] = "OUT_OF_RANGE";
RpcCode[RpcCode["UNIMPLEMENTED"] = 12] = "UNIMPLEMENTED";
RpcCode[RpcCode["INTERNAL"] = 13] = "INTERNAL";
RpcCode[RpcCode["UNAVAILABLE"] = 14] = "UNAVAILABLE";
RpcCode[RpcCode["DATA_LOSS"] = 15] = "DATA_LOSS";
})(RpcCode || (RpcCode = {}));
/**
* Determines whether an error code represents a permanent error when received
* in response to a non-write operation.
*
* See isPermanentWriteError for classifying write errors.
*/
function isPermanentError(code) {
switch (code) {
case Code.OK:
return fail();
case Code.CANCELLED:
case Code.UNKNOWN:
case Code.DEADLINE_EXCEEDED:
case Code.RESOURCE_EXHAUSTED:
case Code.INTERNAL:
case Code.UNAVAILABLE:
// Unauthenticated means something went wrong