/*! * Copyright 2017 Google Inc. All Rights Reserved. * * 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. */ 'use strict'; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; result["default"] = mod; return result; }; Object.defineProperty(exports, "__esModule", { value: true }); const assert_1 = __importDefault(require("assert")); const bun_1 = __importDefault(require("bun")); const extend_1 = __importDefault(require("extend")); const is_1 = __importDefault(require("is")); const through2_1 = __importDefault(require("through2")); const projectify_1 = require("@google-cloud/projectify"); const reference_1 = require("./reference"); const document_1 = require("./document"); const field_value_1 = require("./field-value"); const validate_1 = require("./validate"); const write_batch_1 = require("./write-batch"); const transaction_1 = require("./transaction"); const timestamp_1 = require("./timestamp"); const path_1 = require("./path"); const pool_1 = require("./pool"); const document_change_1 = require("./document-change"); const serializer_1 = require("./serializer"); const geo_point_1 = require("./geo-point"); const logger_1 = require("./logger"); const util_1 = require("./util"); const convert = __importStar(require("./convert")); const libVersion = require('../../package.json').version; logger_1.setLibVersion(libVersion); /*! * DO NOT REMOVE THE FOLLOWING NAMESPACE DEFINITIONS */ /** * @namespace google.protobuf */ /** * @namespace google.rpc */ /** * @namespace google.firestore.v1beta1 */ /*! * @see v1beta1 */ let v1beta1; // Lazy-loaded in `_runRequest()` /*! * HTTP header for the resource prefix to improve routing and project isolation * by the backend. * @type {string} */ const CLOUD_RESOURCE_HEADER = 'google-cloud-resource-prefix'; /*! * The maximum number of times to retry idempotent requests. * @type {number} */ const MAX_REQUEST_RETRIES = 5; /*! * The maximum number of concurrent requests supported by a single GRPC channel, * as enforced by Google's Frontend. If the SDK issues more than 100 concurrent * operations, we need to use more than one GAPIC client since these clients * multiplex all requests over a single channel. * * @type {number} */ const MAX_CONCURRENT_REQUESTS_PER_CLIENT = 100; /*! * GRPC Error code for 'UNAVAILABLE'. * @type {number} */ const GRPC_UNAVAILABLE = 14; /*! * The maximum depth of a Firestore object. * * @type {number} */ const MAX_DEPTH = 20; /** * Document data (e.g. for use with * [set()]{@link DocumentReference#set}) consisting of fields mapped * to values. * * @typedef {Object.} DocumentData */ /** * Update data (for use with [update]{@link DocumentReference#update}) * that contains paths (e.g. 'foo' or 'foo.baz') mapped to values. Fields that * contain dots reference nested fields within the document. * * @typedef {Object.} UpdateData */ /** * An options object that configures conditional behavior of * [update()]{@link DocumentReference#update} and * [delete()]{@link DocumentReference#delete} calls in * [DocumentReference]{@link DocumentReference}, * [WriteBatch]{@link WriteBatch}, and * [Transaction]{@link Transaction}. Using Preconditions, these calls * can be restricted to only apply to documents that match the specified * conditions. * * @property {string} lastUpdateTime - The update time to enforce (specified as * an ISO 8601 string). * @typedef {Object} Precondition */ /** * An options object that configures the behavior of * [set()]{@link DocumentReference#set} calls in * [DocumentReference]{@link DocumentReference}, * [WriteBatch]{@link WriteBatch}, and * [Transaction]{@link Transaction}. These calls can be * configured to perform granular merges instead of overwriting the target * documents in their entirety by providing a SetOptions object with * { merge : true }. * * @property {boolean} merge - Changes the behavior of a set() call to only * replace the values specified in its data argument. Fields omitted from the * set() call remain untouched. * @typedef {Object} SetOptions */ /** * The Firestore client represents a Firestore Database and is the entry point * for all Firestore operations. * * @see [Firestore Documentation]{@link https://firebase.google.com/docs/firestore/} * * @class * * @example Install the client library with npm: npm install --save * @google-cloud/firestore * * @example Import the client library * var Firestore = require('@google-cloud/firestore'); * * @example Create a client that uses Application * Default Credentials (ADC): var firestore = new Firestore(); * * @example Create a client with explicit * credentials: var firestore = new Firestore({ projectId: * 'your-project-id', keyFilename: '/path/to/keyfile.json' * }); * * @example include:samples/quickstart.js * region_tag:firestore_quickstart * Full quickstart example: */ class Firestore { /** * @param {Object=} settings - [Configuration object](#/docs). * @param {string=} settings.projectId The Firestore Project ID. Can be * omitted in environments that support `Application Default Credentials` * {@see https://cloud.google.com/docs/authentication} * @param {string=} settings.keyFilename Local file containing the Service * Account credentials. Can be omitted in environments that support * `Application Default Credentials` * {@see https://cloud.google.com/docs/authentication} * @param {boolean=} settings.timestampsInSnapshots Enables the use of * `Timestamp`s for timestamp fields in `DocumentSnapshots`.
* Currently, Firestore returns timestamp fields as `Date` but `Date` only * supports millisecond precision, which leads to truncation and causes * unexpected behavior when using a timestamp from a snapshot as a part * of a subsequent query. *
Setting `timestampsInSnapshots` to true will cause Firestore to return * `Timestamp` values instead of `Date` avoiding this kind of problem. To * make this work you must also change any code that uses `Date` to use * `Timestamp` instead. *
NOTE: in the future `timestampsInSnapshots: true` will become the * default and this option will be removed so you should change your code to * use `Timestamp` now and opt-in to this new behavior as soon as you can. */ constructor(settings) { settings = extend_1.default({}, settings, { libName: 'gccl', libVersion: libVersion, }); this._validator = new validate_1.Validator({ ArrayElement: (name, value) => validateFieldValue(name, value, /* depth */ 0, /*inArray=*/ true), DeletePrecondition: precondition => document_1.validatePrecondition(precondition, /* allowExists= */ true), Document: validateDocumentData, DocumentReference: reference_1.validateDocumentReference, FieldPath: path_1.FieldPath.validateFieldPath, FieldValue: validateFieldValue, FieldOrder: reference_1.validateFieldOrder, QueryComparison: reference_1.validateComparisonOperator, QueryValue: validateFieldValue, ResourcePath: path_1.ResourcePath.validateResourcePath, SetOptions: document_1.validateSetOptions, UpdateMap: write_batch_1.validateUpdateMap, UpdatePrecondition: precondition => document_1.validatePrecondition(precondition, /* allowExists= */ false), }); /** * A client pool to distribute requests over multiple GAPIC clients in order * to work around a connection limit of 100 concurrent requests per client. * @private * @type {ClientPool|null} */ this._clientPool = null; /** * Whether the initialization settings can still be changed by invoking * `settings()`. * @private * @type {boolean} */ this._settingsFrozen = false; /** * A Promise that resolves when client initialization completes. Can be * 'null' if initialization hasn't started yet. * @private * @type {Promise|null} */ this._clientInitialized = null; /** * The configuration options for the GAPIC client. * @private * @type {Object} */ this._initalizationSettings = null; /** * The serializer to use for the Protobuf transformation. * @private * @type {Serializer} */ this._serializer = null; this.validateAndApplySettings(settings); // GCF currently tears down idle connections after two minutes. Requests // that are issued after this period may fail. On GCF, we therefore issue // these requests as part of a transaction so that we can safely retry until // the network link is reestablished. // // The environment variable FUNCTION_TRIGGER_TYPE is used to detect the GCF // environment. this._preferTransactions = is_1.default.defined(process.env.FUNCTION_TRIGGER_TYPE); this._lastSuccessfulRequest = null; if (this._preferTransactions) { logger_1.logger('Firestore', null, 'Detected GCF environment'); } logger_1.logger('Firestore', null, 'Initialized Firestore'); } /** * Specifies custom settings to be used to configure the `Firestore` * instance. Can only be invoked once and before any other Firestore method. * * If settings are provided via both `settings()` and the `Firestore` * constructor, both settings objects are merged and any settings provided via * `settings()` take precedence. * * @param {object} settings The settings to use for all Firestore operations. */ settings(settings) { this._validator.isObject('settings', settings); this._validator.isOptionalString('settings.projectId', settings.projectId); this._validator.isOptionalBoolean('settings.timestampsInSnapshots', settings.timestampsInSnapshots); if (this._clientInitialized) { throw new Error('Firestore has already been started and its settings can no longer ' + 'be changed. You can only call settings() before calling any other ' + 'methods on a Firestore object.'); } if (this._settingsFrozen) { throw new Error('Firestore.settings() has already be called. You can only call ' + 'settings() once, and only before calling any other methods on a ' + 'Firestore object.'); } const mergedSettings = extend_1.default({}, this._initalizationSettings, settings); this.validateAndApplySettings(mergedSettings); this._settingsFrozen = true; } validateAndApplySettings(settings) { this._validator.isOptionalBoolean('settings.timestampsInSnapshots', settings.timestampsInSnapshots); this._timestampsInSnapshotsEnabled = !!settings.timestampsInSnapshots; if (settings && settings.projectId) { this._validator.isString('settings.projectId', settings.projectId); this._referencePath = new path_1.ResourcePath(settings.projectId, '(default)'); } else { // Initialize a temporary reference path that will be overwritten during // project ID detection. this._referencePath = new path_1.ResourcePath('{{projectId}}', '(default)'); } this._initalizationSettings = settings; this._serializer = new serializer_1.Serializer(this, this._timestampsInSnapshotsEnabled); } /** * The root path to the database. * * @private * @type {string} */ get formattedName() { return this._referencePath.formattedName; } /** * Gets a [DocumentReference]{@link DocumentReference} instance that * refers to the document at the specified path. * * @param {string} documentPath - A slash-separated path to a document. * @returns {DocumentReference} The * [DocumentReference]{@link DocumentReference} instance. * * @example * let documentRef = firestore.doc('collection/document'); * console.log(`Path of document is ${documentRef.path}`); */ doc(documentPath) { this._validator.isResourcePath('documentPath', documentPath); let path = this._referencePath.append(documentPath); if (!path.isDocument) { throw new Error(`Argument "documentPath" must point to a document, but was "${documentPath}". Your path does not contain an even number of components.`); } return new reference_1.DocumentReference(this, path); } /** * Gets a [CollectionReference]{@link CollectionReference} instance * that refers to the collection at the specified path. * * @param {string} collectionPath - A slash-separated path to a collection. * @returns {CollectionReference} The * [CollectionReference]{@link CollectionReference} instance. * * @example * let collectionRef = firestore.collection('collection'); * * // Add a document with an auto-generated ID. * collectionRef.add({foo: 'bar'}).then((documentRef) => { * console.log(`Added document at ${documentRef.path})`); * }); */ collection(collectionPath) { this._validator.isResourcePath('collectionPath', collectionPath); let path = this._referencePath.append(collectionPath); if (!path.isCollection) { throw new Error(`Argument "collectionPath" must point to a collection, but was "${collectionPath}". Your path does not contain an odd number of components.`); } return new reference_1.CollectionReference(this, path); } /** * Creates a [WriteBatch]{@link WriteBatch}, used for performing * multiple writes as a single atomic operation. * * @returns {WriteBatch} A WriteBatch that operates on this Firestore * client. * * @example * let writeBatch = firestore.batch(); * * // Add two documents in an atomic batch. * let data = { foo: 'bar' }; * writeBatch.set(firestore.doc('col/doc1'), data); * writeBatch.set(firestore.doc('col/doc2'), data); * * writeBatch.commit().then(res => { * console.log(`Added document at ${res.writeResults[0].updateTime}`); * }); */ batch() { return new write_batch_1.WriteBatch(this); } /** * Creates a [DocumentSnapshot]{@link DocumentSnapshot} or a * [QueryDocumentSnapshot]{@link QueryDocumentSnapshot} from a * `firestore.v1beta1.Document` proto (or from a resource name for missing * documents). * * This API is used by Google Cloud Functions and can be called with both * 'Proto3 JSON' and 'Protobuf JS' encoded data. * * @private * @param {object|string} documentOrName - The Firestore 'Document' proto or * the resource name of a missing document. * @param {object=} readTime - A 'Timestamp' proto indicating the time this * document was read. * @param {string=} encoding - One of 'json' or 'protobufJS'. Applies to both * the 'document' Proto and 'readTime'. Defaults to 'protobufJS'. * @returns {DocumentSnapshot|QueryDocumentSnapshot} - A QueryDocumentSnapshot * for existing documents, otherwise a DocumentSnapshot. */ snapshot_(documentOrName, readTime, encoding) { // TODO: Assert that Firestore Project ID is valid. let convertTimestamp; let convertDocument; if (!is_1.default.defined(encoding) || encoding === 'protobufJS') { convertTimestamp = data => data; convertDocument = data => data; } else if (encoding === 'json') { // Google Cloud Functions calls us with Proto3 JSON format data, which we // must convert to Protobuf JS. convertTimestamp = convert.timestampFromJson; convertDocument = convert.documentFromJson; } else { throw new Error(`Unsupported encoding format. Expected 'json' or 'protobufJS', ` + `but was '${encoding}'.`); } const document = new document_1.DocumentSnapshot.Builder(); if (is_1.default.string(documentOrName)) { document.ref = new reference_1.DocumentReference(this, path_1.ResourcePath.fromSlashSeparatedString(documentOrName)); } else { document.ref = new reference_1.DocumentReference(this, path_1.ResourcePath.fromSlashSeparatedString(documentOrName.name)); document.fieldsProto = documentOrName.fields ? convertDocument(documentOrName.fields) : {}; document.createTime = timestamp_1.Timestamp.fromProto(convertTimestamp(documentOrName.createTime, 'documentOrName.createTime')); document.updateTime = timestamp_1.Timestamp.fromProto(convertTimestamp(documentOrName.updateTime, 'documentOrName.updateTime')); } if (readTime) { document.readTime = timestamp_1.Timestamp.fromProto(convertTimestamp(readTime, 'readTime')); } return document.build(); } /** * Executes the given updateFunction and commits the changes applied within * the transaction. * * You can use the transaction object passed to 'updateFunction' to read and * modify Firestore documents under lock. Transactions are committed once * 'updateFunction' resolves and attempted up to five times on failure. * * @param {function(Transaction)} updateFunction - The * function to execute within the transaction * context. * @param {object=} transactionOptions - Transaction options. * @param {number=} transactionOptions.maxAttempts - The maximum number of * attempts for this transaction. * @returns {Promise} If the transaction completed successfully or was * explicitly aborted (by the updateFunction returning a failed Promise), the * Promise returned by the updateFunction will be returned here. Else if the * transaction failed, a rejected Promise with the corresponding failure * error will be returned. * * @example * let counterTransaction = firestore.runTransaction(transaction => { * let documentRef = firestore.doc('col/doc'); * return transaction.get(documentRef).then(doc => { * if (doc.exists) { * let count = doc.get('count') || 0; * if (count > 10) { * return Promise.reject('Reached maximum count'); * } * transaction.update(documentRef, { count: ++count }); * return Promise.resolve(count); * } * * transaction.create(documentRef, { count: 1 }); * return Promise.resolve(1); * }); * }); * * counterTransaction.then(res => { * console.log(`Count updated to ${res}`); * }); */ runTransaction(updateFunction, transactionOptions) { this._validator.isFunction('updateFunction', updateFunction); const defaultAttempts = 5; let attemptsRemaining = defaultAttempts; let previousTransaction; if (is_1.default.defined(transactionOptions)) { this._validator.isObject('transactionOptions', transactionOptions); this._validator.isOptionalInteger('transactionOptions.maxAttempts', transactionOptions.maxAttempts, 1); attemptsRemaining = transactionOptions.maxAttempts || attemptsRemaining; previousTransaction = transactionOptions.previousTransaction; } const transaction = new transaction_1.Transaction(this, previousTransaction); const requestTag = transaction.requestTag; let result; --attemptsRemaining; return transaction.begin() .then(() => { let promise = updateFunction(transaction); result = is_1.default.instanceof(promise, Promise) ? promise : Promise.reject(new Error('You must return a Promise in your transaction()-callback.')); return result.catch(err => { logger_1.logger('Firestore.runTransaction', requestTag, 'Rolling back transaction after callback error:', err); // Rollback the transaction and return the failed result. return transaction.rollback().then(() => { return result; }); }); }) .then(() => { return transaction.commit().then(() => result).catch(err => { if (attemptsRemaining > 0) { logger_1.logger('Firestore.runTransaction', requestTag, `Retrying transaction after error: ${JSON.stringify(err)}.`); return this.runTransaction(updateFunction, { previousTransaction: transaction, maxAttempts: attemptsRemaining, }); } logger_1.logger('Firestore.runTransaction', requestTag, 'Exhausted transaction retries, returning error: %s', err); return Promise.reject(err); }); }); } /** * Fetches the root collections that are associated with this Firestore * database. * * @returns {Promise.>} A Promise that resolves * with an array of CollectionReferences. * * @example * firestore.getCollections().then(collections => { * for (let collection of collections) { * console.log(`Found collection with id: ${collection.id}`); * } * }); */ getCollections() { let rootDocument = new reference_1.DocumentReference(this, this._referencePath); return rootDocument.getCollections(); } /** * Retrieves multiple documents from Firestore. * * @param {...DocumentReference} documents - The document references * to receive. * @returns {Promise>} A Promise that * contains an array with the resulting document snapshots. * * @example * let documentRef1 = firestore.doc('col/doc1'); * let documentRef2 = firestore.doc('col/doc2'); * * firestore.getAll(documentRef1, documentRef2).then(docs => { * console.log(`First document: ${JSON.stringify(docs[0])}`); * console.log(`Second document: ${JSON.stringify(docs[1])}`); * }); */ getAll(documents) { documents = is_1.default.array(arguments[0]) ? arguments[0].slice() : Array.prototype.slice.call(arguments); for (let i = 0; i < documents.length; ++i) { this._validator.isDocumentReference(i, documents[i]); } return this.getAll_(documents, util_1.requestTag()); } /** * Internal method to retrieve multiple documents from Firestore, optionally * as part of a transaction. * * @private * @param {Array.} docRefs - The documents * to receive. * @param {string} requestTag A unique client-assigned identifier for this * request. * @param {bytes=} transactionId - transactionId - The transaction ID to use * for this read. * @returns {Promise>} A Promise that contains an array with * the resulting documents. */ getAll_(docRefs, requestTag, transactionId) { const requestedDocuments = new Set(); const retrievedDocuments = new Map(); let request = { database: this.formattedName, transaction: transactionId, }; for (let docRef of docRefs) { requestedDocuments.add(docRef.formattedName); } request.documents = Array.from(requestedDocuments); const self = this; return self.readStream('batchGetDocuments', request, requestTag, true) .then(stream => { return new Promise((resolve, reject) => { stream .on('error', err => { logger_1.logger('Firestore.getAll_', requestTag, 'GetAll failed with error:', err); reject(err); }) .on('data', response => { try { let document; if (response.found) { logger_1.logger('Firestore.getAll_', requestTag, 'Received document: %s', response.found.name); document = self.snapshot_(response.found, response.readTime); } else { logger_1.logger('Firestore.getAll_', requestTag, 'Document missing: %s', response.missing); document = self.snapshot_(response.missing, response.readTime); } let path = document.ref.path; retrievedDocuments.set(path, document); } catch (err) { logger_1.logger('Firestore.getAll_', requestTag, 'GetAll failed with exception:', err); reject(err); } }) .on('end', () => { logger_1.logger('Firestore.getAll_', requestTag, 'Received %d results', retrievedDocuments.size); // BatchGetDocuments doesn't preserve document order. We use // the request order to sort the resulting documents. const orderedDocuments = []; for (let docRef of docRefs) { let document = retrievedDocuments.get(docRef.path); if (!is_1.default.defined(document)) { reject(new Error(`Did not receive document for "${docRef.path}".`)); } orderedDocuments.push(document); } resolve(orderedDocuments); }); stream.resume(); }); }); } /** * Executes a new request using the first available GAPIC client. * * @private */ _runRequest(op) { // Initialize the client pool if this is the first request. if (!this._clientInitialized) { if (!this._timestampsInSnapshotsEnabled) { console.error(` The behavior for Date objects stored in Firestore is going to change AND YOUR APP MAY BREAK. To hide this warning and ensure your app does not break, you need to add the following code to your app before calling any other Cloud Firestore methods: const firestore = new Firestore(); const settings = {/* your settings... */ timestampsInSnapshots: true}; firestore.settings(settings); With this change, timestamps stored in Cloud Firestore will be read back as Firebase Timestamp objects instead of as system Date objects. So you will also need to update code expecting a Date to instead expect a Timestamp. For example: // Old: const date = snapshot.get('created_at'); // New: const timestamp = snapshot.get('created_at'); const date = timestamp.toDate(); Please audit all existing usages of Date when you enable the new behavior. In a future release, the behavior will change to the new behavior, so if you do not follow these steps, YOUR APP MAY BREAK.`); } this._clientInitialized = this._initClientPool().then(clientPool => { this._clientPool = clientPool; }); } return this._clientInitialized.then(() => this._clientPool.run(op)); } /** * Initializes the client pool and invokes Project ID detection. Returns a * Promise on completion. * * @private * @return {Promise} */ _initClientPool() { assert_1.default(!this._clientInitialized, 'Client pool already initialized'); const clientPool = new pool_1.ClientPool(MAX_CONCURRENT_REQUESTS_PER_CLIENT, () => { const gapicClient = module.exports.v1beta1(this._initalizationSettings); logger_1.logger('Firestore', null, 'Initialized Firestore GAPIC Client'); return gapicClient; }); const projectIdProvided = this._referencePath.projectId !== '{{projectId}}'; if (projectIdProvided) { return Promise.resolve(clientPool); } else { return clientPool.run(client => this._detectProjectId(client)) .then(projectId => { this._referencePath = new path_1.ResourcePath(projectId, this._referencePath.databaseId); return clientPool; }); } } /** * Auto-detects the Firestore Project ID. * * @private * @param {object} gapicClient - The Firestore GAPIC client. * @return {Promise} A Promise that resolves with the Project ID. */ _detectProjectId(gapicClient) { return new Promise((resolve, reject) => { gapicClient.getProjectId((err, projectId) => { if (err) { logger_1.logger('Firestore._detectProjectId', null, 'Failed to detect project ID: %s', err); reject(err); } else { logger_1.logger('Firestore._detectProjectId', null, 'Detected project ID: %s', projectId); resolve(projectId); } }); }); } /** * Decorate the request options of an API request. This is used to replace * any `{{projectId}}` placeholders with the value detected from the user's * environment, if one wasn't provided manually. * * @private */ _decorateRequest(request) { let decoratedRequest = extend_1.default(true, {}, request); decoratedRequest = projectify_1.replaceProjectIdToken(decoratedRequest, this._referencePath.projectId); let decoratedGax = { otherArgs: { headers: {} } }; decoratedGax.otherArgs.headers[CLOUD_RESOURCE_HEADER] = this.formattedName; return { request: decoratedRequest, gax: decoratedGax }; } /** * A function returning a Promise that can be retried. * * @private * @callback retryFunction * @returns {Promise} A Promise indicating the function's success. */ /** * Helper method that retries failed Promises. * * If 'delayMs' is specified, waits 'delayMs' between invocations. Otherwise, * schedules the first attempt immediately, and then waits 100 milliseconds * for further attempts. * * @private * @param {number} attemptsRemaining - The number of available attempts. * @param {string} requestTag - A unique client-assigned identifier for this * request. * @param {retryFunction} func - Method returning a Promise than can be * retried. * @param {number=} delayMs - How long to wait before issuing a this retry. * Defaults to zero. * @returns {Promise} - A Promise with the function's result if successful * within `attemptsRemaining`. Otherwise, returns the last rejected Promise. */ _retry(attemptsRemaining, requestTag, func, delayMs) { let self = this; let currentDelay = delayMs || 0; let nextDelay = delayMs || 100; --attemptsRemaining; return new Promise(resolve => { setTimeout(resolve, currentDelay); }) .then(func) .then(result => { self._lastSuccessfulRequest = new Date().getTime(); return result; }) .catch(err => { if (is_1.default.defined(err.code) && err.code !== GRPC_UNAVAILABLE) { logger_1.logger('Firestore._retry', requestTag, 'Request failed with unrecoverable error:', err); return Promise.reject(err); } if (attemptsRemaining === 0) { logger_1.logger('Firestore._retry', requestTag, 'Request failed with error:', err); return Promise.reject(err); } logger_1.logger('Firestore._retry', requestTag, 'Retrying request that failed with error:', err); return self._retry(attemptsRemaining, requestTag, func, nextDelay); }); } /** * Opens the provided stream and waits for it to become healthy. If an error * occurs before the first byte is read, the method rejects the returned * Promise. * * @private * @param {Stream} resultStream - The Node stream to monitor. * @param {string} requestTag A unique client-assigned identifier for this * request. * @param {Object=} request - If specified, the request that should be written * to the stream after it opened. * @returns {Promise.} The given Stream once it is considered healthy. */ _initializeStream(resultStream, requestTag, request) { /** The last error we received and have not forwarded yet. */ let errorReceived = null; /** * Whether we have resolved the Promise and returned the stream to the * caller. */ let streamReleased = false; /** * Whether the stream end has been reached. This has to be forwarded to the * caller.. */ let endCalled = false; return new Promise((resolve, reject) => { const releaseStream = () => { if (errorReceived) { logger_1.logger('Firestore._initializeStream', requestTag, 'Emit error:', errorReceived); resultStream.emit('error', errorReceived); errorReceived = null; } else if (!streamReleased) { logger_1.logger('Firestore._initializeStream', requestTag, 'Releasing stream'); streamReleased = true; resultStream.pause(); // Calling 'stream.pause()' only holds up 'data' events and not the // 'end' event we intend to forward here. We therefore need to wait // until the API consumer registers their listeners (in the .then() // call) before emitting any further events. resolve(resultStream); // We execute the forwarding of the 'end' event via setTimeout() as // V8 guarantees that the above the Promise chain is resolved before // any calls invoked via setTimeout(). setTimeout(() => { if (endCalled) { logger_1.logger('Firestore._initializeStream', requestTag, 'Forwarding stream close'); resultStream.emit('end'); } }, 0); } }; // We capture any errors received and buffer them until the caller has // registered a listener. We register our event handler as early as // possible to avoid the default stream behavior (which is just to log and // continue). resultStream.on('readable', () => { releaseStream(); }); resultStream.on('end', () => { logger_1.logger('Firestore._initializeStream', requestTag, 'Received stream end'); endCalled = true; releaseStream(); }); resultStream.on('error', err => { logger_1.logger('Firestore._initializeStream', requestTag, 'Received stream error:', err); // If we receive an error before we were able to receive any data, // reject this stream. if (!streamReleased) { logger_1.logger('Firestore._initializeStream', requestTag, 'Received initial error:', err); streamReleased = true; reject(err); } else { errorReceived = err; } }); if (is_1.default.defined(request)) { logger_1.logger('Firestore._initializeStream', requestTag, 'Sending request: %j', request); resultStream.write(request, 'utf-8', () => { logger_1.logger('Firestore._initializeStream', requestTag, 'Marking stream as healthy'); releaseStream(); }); } }); } /** * A funnel for all non-streaming API requests, assigning a project ID where * necessary within the request options. * * @private * @param {function} methodName - Name of the veneer API endpoint that takes a * request and GAX options. * @param {Object} request - The Protobuf request to send. * @param {string} requestTag A unique client-assigned identifier for this * request. * @param {boolean} allowRetries - Whether this is an idempotent request that * can be retried. * @returns {Promise.} A Promise with the request result. */ request(methodName, request, requestTag, allowRetries) { let attempts = allowRetries ? MAX_REQUEST_RETRIES : 1; return this._runRequest(gapicClient => { const decorated = this._decorateRequest(request); return this._retry(attempts, requestTag, () => { return new Promise((resolve, reject) => { logger_1.logger('Firestore.request', requestTag, 'Sending request: %j', decorated.request); gapicClient[methodName](decorated.request, decorated.gax, (err, result) => { if (err) { logger_1.logger('Firestore.request', requestTag, 'Received error:', err); reject(err); } else { logger_1.logger('Firestore.request', requestTag, 'Received response: %j', result); resolve(result); } }); }); }); }); } /** * A funnel for read-only streaming API requests, assigning a project ID where * necessary within the request options. * * The stream is returned in paused state and needs to be resumed once all * listeners are attached. * * @private * @param {string} methodName - Name of the streaming Veneer API endpoint that * takes a request and GAX options. * @param {Object} request - The Protobuf request to send. * @param {string} requestTag A unique client-assigned identifier for this * request. * @param {boolean} allowRetries - Whether this is an idempotent request that * can be retried. * @returns {Promise.} A Promise with the resulting read-only stream. */ readStream(methodName, request, requestTag, allowRetries) { let attempts = allowRetries ? MAX_REQUEST_RETRIES : 1; return this._runRequest(gapicClient => { const decorated = this._decorateRequest(request); return this._retry(attempts, requestTag, () => { return new Promise((resolve, reject) => { try { logger_1.logger('Firestore.readStream', requestTag, 'Sending request: %j', decorated.request); let stream = gapicClient[methodName](decorated.request, decorated.gax); let logStream = through2_1.default.obj(function (chunk, enc, callback) { logger_1.logger('Firestore.readStream', requestTag, 'Received response: %j', chunk); this.push(chunk); callback(); }); resolve(bun_1.default([stream, logStream])); } catch (err) { logger_1.logger('Firestore.readStream', requestTag, 'Received error:', err); reject(err); } }) .then(stream => this._initializeStream(stream, requestTag)); }); }); } /** * A funnel for read-write streaming API requests, assigning a project ID * where necessary for all writes. * * The stream is returned in paused state and needs to be resumed once all * listeners are attached. * * @private * @param {string} methodName - Name of the streaming Veneer API endpoint that * takes GAX options. * @param {Object} request - The Protobuf request to send as the first stream * message. * @param {string} requestTag A unique client-assigned identifier for this * request. * @param {boolean} allowRetries - Whether this is an idempotent request that * can be retried. * @returns {Promise.} A Promise with the resulting read/write stream. */ readWriteStream(methodName, request, requestTag, allowRetries) { let self = this; let attempts = allowRetries ? MAX_REQUEST_RETRIES : 1; return this._runRequest(gapicClient => { const decorated = this._decorateRequest(request); return this._retry(attempts, requestTag, () => { return Promise.resolve().then(() => { logger_1.logger('Firestore.readWriteStream', requestTag, 'Opening stream'); // The generated bi-directional streaming API takes the list of GAX // headers as its second argument. let requestStream = gapicClient[methodName]({}, decorated.gax); // The transform stream to assign the project ID. let transform = through2_1.default.obj(function (chunk, encoding, callback) { let decoratedChunk = extend_1.default(true, {}, chunk); projectify_1.replaceProjectIdToken(decoratedChunk, self._referencePath.projectId); logger_1.logger('Firestore.readWriteStream', requestTag, 'Streaming request: %j', decoratedChunk); requestStream.write(decoratedChunk, encoding, callback); }); let logStream = through2_1.default.obj(function (chunk, enc, callback) { logger_1.logger('Firestore.readWriteStream', requestTag, 'Received response: %j', chunk); this.push(chunk); callback(); }); let resultStream = bun_1.default([transform, requestStream, logStream]); return this._initializeStream(resultStream, requestTag, request); }); }); }); } } /** * Validates a JavaScript value for usage as a Firestore value. * * @private * @param {Object} val JavaScript value to validate. * @param {string} options.allowDeletes At what level field deletes are * supported (acceptable values are 'none', 'root' or 'all'). * @param {boolean} options.allowServerTimestamps Whether server timestamps * are supported. * @param {number=} depth The current depth of the traversal. * @param {number=} inArray Whether we are inside an array. * @returns {boolean} 'true' when the object is valid. * @throws {Error} when the object is invalid. */ function validateFieldValue(val, options, depth, inArray) { assert_1.default(['none', 'root', 'all'].indexOf(options.allowDeletes) !== -1, 'Expected \'none\', \'root\', or \'all\' for \'options.allowDeletes\''); assert_1.default(typeof options.allowTransforms === 'boolean', 'Expected boolean for \'options.allowTransforms\''); if (!depth) { depth = 1; } else if (depth > MAX_DEPTH) { throw new Error(`Input object is deeper than ${MAX_DEPTH} levels or contains a cycle.`); } inArray = inArray || false; if (is_1.default.array(val)) { for (let prop of val) { validateFieldValue(val[prop], options, depth + 1, /* inArray= */ true); } } else if (serializer_1.isPlainObject(val)) { for (let prop in val) { if (val.hasOwnProperty(prop)) { validateFieldValue(val[prop], options, depth + 1, inArray); } } } else if (val instanceof field_value_1.DeleteTransform) { if (inArray) { throw new Error(`${val.methodName}() cannot be used inside of an array.`); } else if ((options.allowDeletes === 'root' && depth > 1) || options.allowDeletes === 'none') { throw new Error(`${val.methodName}() must appear at the top-level and can only be used in update() or set() with {merge:true}.`); } } else if (val instanceof field_value_1.FieldTransform) { if (inArray) { throw new Error(`${val.methodName}() cannot be used inside of an array.`); } else if (!options.allowTransforms) { throw new Error(`${val.methodName}() can only be used in set(), create() or update().`); } } else if (is_1.default.instanceof(val, reference_1.DocumentReference)) { return true; } else if (is_1.default.instanceof(val, geo_point_1.GeoPoint)) { return true; } else if (is_1.default.instanceof(val, timestamp_1.Timestamp)) { return true; } else if (is_1.default.instanceof(val, path_1.FieldPath)) { throw new Error('Cannot use object of type "FieldPath" as a Firestore value.'); } else if (is_1.default.object(val)) { throw validate_1.customObjectError(val); } return true; } /** * Validates a JavaScript object for usage as a Firestore document. * * @private * @param {Object} obj JavaScript object to validate. * @param {string} options.allowDeletes At what level field deletes are * supported (acceptable values are 'none', 'root' or 'all'). * @param {boolean} options.allowServerTimestamps Whether server timestamps * are supported. * @param {boolean} options.allowEmpty Whether empty documents are supported. * @returns {boolean} 'true' when the object is valid. * @throws {Error} when the object is invalid. */ function validateDocumentData(obj, options) { assert_1.default(typeof options.allowEmpty === 'boolean', 'Expected boolean for \'options.allowEmpty\''); if (!serializer_1.isPlainObject(obj)) { throw new Error('Input is not a plain JavaScript object.'); } options = options || {}; let isEmpty = true; for (let prop in obj) { if (obj.hasOwnProperty(prop)) { isEmpty = false; validateFieldValue(obj[prop], options, /* depth= */ 1); } } if (options.allowEmpty === false && isEmpty) { throw new Error('At least one field must be updated.'); } return true; } /** * A logging function that takes a single string. * * @callback Firestore~logFunction * @param {string} Log message */ /** * Sets the log function for all active Firestore instances. * * @method Firestore.setLogFunction * @param {Firestore~logFunction} logger - A log function that takes a single * string. */ Firestore.setLogFunction = logger_1.setLogFunction; /** * The default export of the `@google-cloud/firestore` package is the * {@link Firestore} class. * * See {@link Firestore} and {@link ClientConfig} for client methods and * configuration options. * * @module {Firestore} @google-cloud/firestore * @alias nodejs-firestore * * @example Install the client library with npm: npm install --save * @google-cloud/firestore * * @example Import the client library * var Firestore = require('@google-cloud/firestore'); * * @example Create a client that uses Application * Default Credentials (ADC): var firestore = new Firestore(); * * @example Create a client with explicit * credentials: var firestore = new Firestore({ projectId: * 'your-project-id', keyFilename: '/path/to/keyfile.json' * }); * * @example include:samples/quickstart.js * region_tag:firestore_quickstart * Full quickstart example: */ module.exports = Firestore; module.exports.default = Firestore; module.exports.Firestore = Firestore; /** * {@link v1beta1} factory function. * * @name Firestore.v1beta1 * @see v1beta1 * @type {function} */ Object.defineProperty(module.exports, 'v1beta1', { // The v1beta1 module is very large. To avoid pulling it in from static // scope, we lazy-load and cache the module. get: () => { if (!v1beta1) { v1beta1 = require('./v1beta1'); } return v1beta1; }, }); /** * {@link GeoPoint} class. * * @name Firestore.GeoPoint * @see GeoPoint * @type {Constructor} */ module.exports.GeoPoint = geo_point_1.GeoPoint; /** * {@link Transaction} class. * * @name Firestore.Transaction * @see Transaction * @type Transaction */ module.exports.Transaction = transaction_1.Transaction; /** * {@link WriteBatch} class. * * @name Firestore.WriteBatch * @see WriteBatch * @type WriteBatch */ module.exports.WriteBatch = write_batch_1.WriteBatch; /** * {@link DocumentReference} class. * * @name Firestore.DocumentReference * @see DocumentReference * @type DocumentReference */ module.exports.DocumentReference = reference_1.DocumentReference; /** * {@link WriteResult} class. * * @name Firestore.WriteResult * @see WriteResult * @type WriteResult */ module.exports.WriteResult = write_batch_1.WriteResult; /** * {@link DocumentSnapshot} DocumentSnapshot. * * @name Firestore.DocumentSnapshot * @see DocumentSnapshot * @type DocumentSnapshot */ module.exports.DocumentSnapshot = document_1.DocumentSnapshot; /** * {@link QueryDocumentSnapshot} class. * * @name Firestore.QueryDocumentSnapshot * @see QueryDocumentSnapshot * @type QueryDocumentSnapshot */ module.exports.QueryDocumentSnapshot = document_1.QueryDocumentSnapshot; /** * {@link CollectionReference} class. * * @name Firestore.CollectionReference * @see CollectionReference * @type CollectionReference */ module.exports.CollectionReference = reference_1.CollectionReference; /** * {@link QuerySnapshot} class. * * @name Firestore.QuerySnapshot * @see QuerySnapshot * @type QuerySnapshot */ module.exports.QuerySnapshot = reference_1.QuerySnapshot; /** * {@link DocumentChange} class. * * @name Firestore.DocumentChange * @see DocumentChange * @type DocumentChange */ module.exports.DocumentChange = document_change_1.DocumentChange; /** * {@link Query} class. * * @name Firestore.Query * @see Query * @type Query */ module.exports.Query = reference_1.Query; /** * {@link FieldValue} class. * * @name Firestore.FieldValue * @see FieldValue */ module.exports.FieldValue = field_value_1.FieldValue; /** * {@link FieldPath} class. * * @name Firestore.FieldPath * @see FieldPath * @type {Constructor} */ module.exports.FieldPath = path_1.FieldPath; /** * {@link Timestamp} class. * * @name Firestore.Timestamp * @see Timestamp * @type Timestamp */ module.exports.Timestamp = timestamp_1.Timestamp;