UNPKG

owltech

Version:
1,338 lines 66.6 kB
"use strict"; /*! * 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. */ Object.defineProperty(exports, "__esModule", { value: true }); const deepEqual = require('deep-equal'); const bun = require("bun"); const through2 = require("through2"); const document_1 = require("./document"); const document_change_1 = require("./document-change"); const logger_1 = require("./logger"); const order_1 = require("./order"); const path_1 = require("./path"); const serializer_1 = require("./serializer"); const timestamp_1 = require("./timestamp"); const util_1 = require("./util"); const validate_1 = require("./validate"); const watch_1 = require("./watch"); const write_batch_1 = require("./write-batch"); /** * The direction of a `Query.orderBy()` clause is specified as 'desc' or 'asc' * (descending or ascending). * * @private */ const directionOperators = { asc: 'ASCENDING', desc: 'DESCENDING', }; /** * Filter conditions in a `Query.where()` clause are specified using the * strings '<', '<=', '==', '>=', and '>'. * * @private */ const comparisonOperators = { '<': 'LESS_THAN', '<=': 'LESS_THAN_OR_EQUAL', '==': 'EQUAL', '>': 'GREATER_THAN', '>=': 'GREATER_THAN_OR_EQUAL', 'array-contains': 'ARRAY_CONTAINS' }; /** * onSnapshot() callback that receives a QuerySnapshot. * * @callback querySnapshotCallback * @param {QuerySnapshot} snapshot A query snapshot. */ /** * onSnapshot() callback that receives a DocumentSnapshot. * * @callback documentSnapshotCallback * @param {DocumentSnapshot} snapshot A document snapshot. */ /** * onSnapshot() callback that receives an error. * * @callback errorCallback * @param {Error} err An error from a listen. */ /** * A DocumentReference refers to a document location in a Firestore database * and can be used to write, read, or listen to the location. The document at * the referenced location may or may not exist. A DocumentReference can * also be used to create a * [CollectionReference]{@link CollectionReference} to a * subcollection. * * @class */ class DocumentReference { /** * @private * @hideconstructor * * @param _firestore The Firestore Database client. * @param _path The Path of this reference. */ constructor(_firestore, _path) { this._firestore = _firestore; this._path = _path; } /** * The string representation of the DocumentReference's location. * @private * @type {string} * @name DocumentReference#formattedName */ get formattedName() { const projectId = this.firestore.projectId; return this._path.toQualifiedResourcePath(projectId).formattedName; } /** * The [Firestore]{@link Firestore} instance for the Firestore * database (useful for performing transactions, etc.). * * @type {Firestore} * @name DocumentReference#firestore * @readonly * * @example * let collectionRef = firestore.collection('col'); * * collectionRef.add({foo: 'bar'}).then(documentReference => { * let firestore = documentReference.firestore; * console.log(`Root location for document is ${firestore.formattedName}`); * }); */ get firestore() { return this._firestore; } /** * A string representing the path of the referenced document (relative * to the root of the database). * * @type {string} * @name DocumentReference#path * @readonly * * @example * let collectionRef = firestore.collection('col'); * * collectionRef.add({foo: 'bar'}).then(documentReference => { * console.log(`Added document at '${documentReference.path}'`); * }); */ get path() { return this._path.relativeName; } /** * The last path element of the referenced document. * * @type {string} * @name DocumentReference#id * @readonly * * @example * let collectionRef = firestore.collection('col'); * * collectionRef.add({foo: 'bar'}).then(documentReference => { * console.log(`Added document with name '${documentReference.id}'`); * }); */ get id() { return this._path.id; } /** * A reference to the collection to which this DocumentReference belongs. * * @name DocumentReference#parent * @type {CollectionReference} * @readonly * * @example * let documentRef = firestore.doc('col/doc'); * let collectionRef = documentRef.parent; * * collectionRef.where('foo', '==', 'bar').get().then(results => { * console.log(`Found ${results.size} matches in parent collection`); * }): */ get parent() { return new CollectionReference(this._firestore, this._path.parent()); } /** * Reads the document referred to by this DocumentReference. * * @returns {Promise.<DocumentSnapshot>} A Promise resolved with a * DocumentSnapshot for the retrieved document on success. For missing * documents, DocumentSnapshot.exists will be false. If the get() fails for * other reasons, the Promise will be rejected. * * @example * let documentRef = firestore.doc('col/doc'); * * documentRef.get().then(documentSnapshot => { * if (documentSnapshot.exists) { * console.log('Document retrieved successfully.'); * } * }); */ get() { return this._firestore.getAll(this).then(([result]) => result); } /** * 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} A reference to the new * subcollection. * * @example * let documentRef = firestore.doc('col/doc'); * let subcollection = documentRef.collection('subcollection'); * console.log(`Path to subcollection: ${subcollection.path}`); */ collection(collectionPath) { path_1.validateResourcePath('collectionPath', collectionPath); const path = this._path.append(collectionPath); if (!path.isCollection) { throw new Error(`Value for argument "collectionPath" must point to a collection, but was "${collectionPath}". Your path does not contain an odd number of components.`); } return new CollectionReference(this._firestore, path); } /** * Fetches the subcollections that are direct children of this document. * * @returns {Promise.<Array.<CollectionReference>>} A Promise that resolves * with an array of CollectionReferences. * * @example * let documentRef = firestore.doc('col/doc'); * * documentRef.listCollections().then(collections => { * for (let collection of collections) { * console.log(`Found subcollection with id: ${collection.id}`); * } * }); */ listCollections() { return this.firestore.initializeIfNeeded().then(() => { const request = { parent: this.formattedName }; return this._firestore .request('listCollectionIds', request, util_1.requestTag(), /* allowRetries= */ true) .then(collectionIds => { const collections = []; // We can just sort this list using the default comparator since it // will only contain collection ids. collectionIds.sort(); for (const collectionId of collectionIds) { collections.push(this.collection(collectionId)); } return collections; }); }); } /** * Fetches the subcollections that are direct children of this document. * * @deprecated Use `.listCollections()`. * * @returns {Promise.<Array.<CollectionReference>>} A Promise that resolves * with an array of CollectionReferences. */ getCollections() { return this.listCollections(); } /** * Create a document with the provided object values. This will fail the write * if a document exists at its location. * * @param {DocumentData} data An object that contains the fields and data to * serialize as the document. * @returns {Promise.<WriteResult>} A Promise that resolves with the * write time of this create. * * @example * let documentRef = firestore.collection('col').doc(); * * documentRef.create({foo: 'bar'}).then((res) => { * console.log(`Document created at ${res.updateTime}`); * }).catch((err) => { * console.log(`Failed to create document: ${err}`); * }); */ create(data) { const writeBatch = new write_batch_1.WriteBatch(this._firestore); return writeBatch.create(this, data).commit().then(([writeResult]) => writeResult); } /** * Deletes the document referred to by this `DocumentReference`. * * A delete for a non-existing document is treated as a success (unless * lastUptimeTime is provided). * * @param {Precondition=} precondition A precondition to enforce for this * delete. * @param {Timestamp=} precondition.lastUpdateTime If set, enforces that the * document was last updated at lastUpdateTime. Fails the delete if the * document was last updated at a different time. * @returns {Promise.<WriteResult>} A Promise that resolves with the * delete time. * * @example * let documentRef = firestore.doc('col/doc'); * * documentRef.delete().then(() => { * console.log('Document successfully deleted.'); * }); */ delete(precondition) { const writeBatch = new write_batch_1.WriteBatch(this._firestore); return writeBatch.delete(this, precondition) .commit() .then(([writeResult]) => writeResult); } /** * Writes to the document referred to by this DocumentReference. If the * document does not yet exist, it will be created. If you pass * [SetOptions]{@link SetOptions}, the provided data can be merged into an * existing document. * * @param {DocumentData} data A map of the fields and values for the document. * @param {SetOptions=} options An object to configure the set behavior. * @param {boolean=} options.merge If true, set() merges the values specified * in its data argument. Fields omitted from this set() call remain untouched. * @param {Array.<string|FieldPath>=} options.mergeFields If provided, * set() only replaces the specified field paths. Any field path that is not * specified is ignored and remains untouched. * @returns {Promise.<WriteResult>} A Promise that resolves with the * write time of this set. * * @example * let documentRef = firestore.doc('col/doc'); * * documentRef.set({foo: 'bar'}).then(res => { * console.log(`Document written at ${res.updateTime}`); * }); */ set(data, options) { const writeBatch = new write_batch_1.WriteBatch(this._firestore); return writeBatch.set(this, data, options).commit().then(([writeResult]) => writeResult); } /** * Updates fields in the document referred to by this DocumentReference. * If the document doesn't yet exist, the update fails and the returned * Promise will be rejected. * * The update() method accepts either an object with field paths encoded as * keys and field values encoded as values, or a variable number of arguments * that alternate between field paths and field values. * * A Precondition restricting this update can be specified as the last * argument. * * @param {UpdateData|string|FieldPath} dataOrField An object containing the * fields and values with which to update the document or the path of the * first field to update. * @param { * ...(*|string|FieldPath|Precondition)} preconditionOrValues An alternating * list of field paths and values to update or a Precondition to restrict * this update. * @returns Promise.<WriteResult> A Promise that resolves once the * data has been successfully written to the backend. * * @example * let documentRef = firestore.doc('col/doc'); * * documentRef.update({foo: 'bar'}).then(res => { * console.log(`Document updated at ${res.updateTime}`); * }); */ update(dataOrField, ...preconditionOrValues) { validate_1.validateMinNumberOfArguments('DocumentReference.update', arguments, 1); const writeBatch = new write_batch_1.WriteBatch(this._firestore); return writeBatch.update .apply(writeBatch, [this, dataOrField].concat(preconditionOrValues)) .commit() .then(([writeResult]) => writeResult); } /** * Attaches a listener for DocumentSnapshot events. * * @param {documentSnapshotCallback} onNext A callback to be called every * time a new `DocumentSnapshot` is available. * @param {errorCallback=} onError A callback to be called if the listen fails * or is cancelled. No further callbacks will occur. If unset, errors will be * logged to the console. * * @returns {function()} An unsubscribe function that can be called to cancel * the snapshot listener. * * @example * let documentRef = firestore.doc('col/doc'); * * let unsubscribe = documentRef.onSnapshot(documentSnapshot => { * if (documentSnapshot.exists) { * console.log(documentSnapshot.data()); * } * }, err => { * console.log(`Encountered error: ${err}`); * }); * * // Remove this listener. * unsubscribe(); */ onSnapshot(onNext, onError) { validate_1.validateFunction('onNext', onNext); validate_1.validateFunction('onError', onError, { optional: true }); const watch = new watch_1.DocumentWatch(this.firestore, this); return watch.onSnapshot((readTime, size, docs) => { for (const document of docs()) { if (document.ref.path === this.path) { onNext(document); return; } } // The document is missing. const document = new document_1.DocumentSnapshotBuilder(); document.ref = new DocumentReference(this._firestore, this._path); document.readTime = readTime; onNext(document.build()); }, onError || console.error); } /** * Returns true if this `DocumentReference` is equal to the provided value. * * @param {*} other The value to compare against. * @return {boolean} true if this `DocumentReference` is equal to the provided * value. */ isEqual(other) { return (this === other || (other instanceof DocumentReference && this._firestore === other._firestore && this._path.isEqual(other._path))); } /** * Converts this DocumentReference to the Firestore Proto representation. * * @private */ toProto() { return { referenceValue: this.formattedName }; } } exports.DocumentReference = DocumentReference; /** * A Query order-by field. * * @private * @class */ class FieldOrder { /** * @param field The name of a document field (member) on which to order query * results. * @param direction One of 'ASCENDING' (default) or 'DESCENDING' to * set the ordering direction to ascending or descending, respectively. */ constructor(field, direction = 'ASCENDING') { this.field = field; this.direction = direction; } /** * Generates the proto representation for this field order. * @private */ toProto() { return { field: { fieldPath: this.field.formattedName, }, direction: this.direction, }; } } /** * A field constraint for a Query where clause. * * @private * @class */ class FieldFilter { /** * @param serializer The Firestore serializer * @param field The path of the property value to compare. * @param op A comparison operation. * @param value The value to which to compare the field for inclusion in a * query. */ constructor(serializer, field, op, value) { this.serializer = serializer; this.field = field; this.op = op; this.value = value; } /** * Returns whether this FieldFilter uses an equals comparison. * * @private */ isInequalityFilter() { switch (this.op) { case 'GREATER_THAN': case 'GREATER_THAN_OR_EQUAL': case 'LESS_THAN': case 'LESS_THAN_OR_EQUAL': return true; default: return false; } } /** * Generates the proto representation for this field filter. * * @private */ toProto() { if (typeof this.value === 'number' && isNaN(this.value)) { return { unaryFilter: { field: { fieldPath: this.field.formattedName, }, op: 'IS_NAN' }, }; } if (this.value === null) { return { unaryFilter: { field: { fieldPath: this.field.formattedName, }, op: 'IS_NULL', }, }; } return { fieldFilter: { field: { fieldPath: this.field.formattedName, }, op: this.op, value: this.serializer.encodeValue(this.value), }, }; } } /** * A QuerySnapshot contains zero or more * [QueryDocumentSnapshot]{@link QueryDocumentSnapshot} objects * representing the results of a query. The documents can be accessed as an * array via the [documents]{@link QuerySnapshot#documents} property * or enumerated using the [forEach]{@link QuerySnapshot#forEach} * method. The number of documents can be determined via the * [empty]{@link QuerySnapshot#empty} and * [size]{@link QuerySnapshot#size} properties. * * @class QuerySnapshot */ class QuerySnapshot { /** * @private * @hideconstructor * * @param _query The originating query. * @param _readTime The time when this query snapshot was obtained. * @param _size The number of documents in the result set. * @param docs A callback returning a sorted array of documents matching * this query * @param changes A callback returning a sorted array of document change * events for this snapshot. */ constructor(_query, _readTime, _size, docs, changes) { this._query = _query; this._readTime = _readTime; this._size = _size; this._materializedDocs = null; this._materializedChanges = null; this._docs = null; this._changes = null; this._docs = docs; this._changes = changes; } /** * The query on which you called get() or onSnapshot() in order to get this * QuerySnapshot. * * @type {Query} * @name QuerySnapshot#query * @readonly * * @example * let query = firestore.collection('col').where('foo', '==', 'bar'); * * query.limit(10).get().then(querySnapshot => { * console.log(`Returned first batch of results`); * let query = querySnapshot.query; * return query.offset(10).get(); * }).then(() => { * console.log(`Returned second batch of results`); * }); */ get query() { return this._query; } /** * An array of all the documents in this QuerySnapshot. * * @type {Array.<QueryDocumentSnapshot>} * @name QuerySnapshot#docs * @readonly * * @example * let query = firestore.collection('col').where('foo', '==', 'bar'); * * query.get().then(querySnapshot => { * let docs = querySnapshot.docs; * for (let doc of docs) { * console.log(`Document found at path: ${doc.ref.path}`); * } * }); */ get docs() { if (this._materializedDocs) { return this._materializedDocs; } this._materializedDocs = this._docs(); this._docs = null; return this._materializedDocs; } /** * True if there are no documents in the QuerySnapshot. * * @type {boolean} * @name QuerySnapshot#empty * @readonly * * @example * let query = firestore.collection('col').where('foo', '==', 'bar'); * * query.get().then(querySnapshot => { * if (querySnapshot.empty) { * console.log('No documents found.'); * } * }); */ get empty() { return this._size === 0; } /** * The number of documents in the QuerySnapshot. * * @type {number} * @name QuerySnapshot#size * @readonly * * @example * let query = firestore.collection('col').where('foo', '==', 'bar'); * * query.get().then(querySnapshot => { * console.log(`Found ${querySnapshot.size} documents.`); * }); */ get size() { return this._size; } /** * The time this query snapshot was obtained. * * @type {Timestamp} * @name QuerySnapshot#readTime * * @example * let query = firestore.collection('col').where('foo', '==', 'bar'); * * query.get().then((querySnapshot) => { * let readTime = querySnapshot.readTime; * console.log(`Query results returned at '${readTime.toDate()}'`); * }); */ get readTime() { return this._readTime; } /** * Returns an array of the documents changes since the last snapshot. If * this is the first snapshot, all documents will be in the list as added * changes. * * @return {Array.<DocumentChange>} * * @example * let query = firestore.collection('col').where('foo', '==', 'bar'); * * query.onSnapshot(querySnapshot => { * let changes = querySnapshot.docChanges(); * for (let change of changes) { * console.log(`A document was ${change.type}.`); * } * }); */ docChanges() { if (this._materializedChanges) { return this._materializedChanges; } this._materializedChanges = this._changes(); this._changes = null; return this._materializedChanges; } /** * Enumerates all of the documents in the QuerySnapshot. This is a convenience * method for running the same callback on each {@link QueryDocumentSnapshot} * that is returned. * * @param {function} callback A callback to be called with a * [QueryDocumentSnapshot]{@link QueryDocumentSnapshot} for each document in * the snapshot. * @param {*=} thisArg The `this` binding for the callback.. * * @example * let query = firestore.collection('col').where('foo', '==', 'bar'); * * query.get().then(querySnapshot => { * querySnapshot.forEach(documentSnapshot => { * console.log(`Document found at path: ${documentSnapshot.ref.path}`); * }); * }); */ forEach(callback, thisArg) { validate_1.validateFunction('callback', callback); for (const doc of this.docs) { callback.call(thisArg, doc); } } /** * Returns true if the document data in this `QuerySnapshot` is equal to the * provided value. * * @param {*} other The value to compare against. * @return {boolean} true if this `QuerySnapshot` is equal to the provided * value. */ isEqual(other) { // Since the read time is different on every query read, we explicitly // ignore all metadata in this comparison. if (this === other) { return true; } if (!(other instanceof QuerySnapshot)) { return false; } if (this._size !== other._size) { return false; } if (!this._query.isEqual(other._query)) { return false; } if (this._materializedDocs && !this._materializedChanges) { // If we have only materialized the documents, we compare them first. return (isArrayEqual(this.docs, other.docs) && isArrayEqual(this.docChanges(), other.docChanges())); } // Otherwise, we compare the changes first as we expect there to be fewer. return (isArrayEqual(this.docChanges(), other.docChanges()) && isArrayEqual(this.docs, other.docs)); } } exports.QuerySnapshot = QuerySnapshot; // TODO: As of v0.17.0, we're changing docChanges from an array into a method. // Because this is a runtime breaking change and somewhat subtle (both Array and // Function have a .length, etc.), we'll replace commonly-used properties // (including Symbol.iterator) to throw a custom error message. By our v1.0 // release, we should remove this code. function throwDocChangesMethodError() { throw new Error('QuerySnapshot.docChanges has been changed from a property into a ' + 'method, so usages like "querySnapshot.docChanges" should become ' + '"querySnapshot.docChanges()"'); } const docChangesPropertiesToOverride = [ 'length', 'forEach', 'map', ...(typeof Symbol !== 'undefined' ? [Symbol.iterator] : []) ]; docChangesPropertiesToOverride.forEach(property => { Object.defineProperty(QuerySnapshot.prototype.docChanges, property, { get: () => throwDocChangesMethodError() }); }); /** * A Query refers to a query which you can read or stream from. You can also * construct refined Query objects by adding filters and ordering. * * @class Query */ class Query { /** * @private * @hideconstructor * * @param _firestore The Firestore Database client. * @param _path Path of the collection to be queried. * @param _fieldFilters Sequence of fields constraining the results of the * query. * @param _fieldOrders Sequence of fields to control the order of results. * @param _queryOptions Additional query options. */ constructor(_firestore, _path, _fieldFilters = [], _fieldOrders = [], _queryOptions = {}) { this._firestore = _firestore; this._path = _path; this._fieldFilters = _fieldFilters; this._fieldOrders = _fieldOrders; this._queryOptions = _queryOptions; this._serializer = new serializer_1.Serializer(_firestore); } /** * Detects the argument type for Firestore cursors. * * @private * @param fieldValuesOrDocumentSnapshot A snapshot of the document or a set * of field values. * @returns 'true' if the input is a single DocumentSnapshot.. */ static _isDocumentSnapshot(fieldValuesOrDocumentSnapshot) { return (fieldValuesOrDocumentSnapshot.length === 1 && (fieldValuesOrDocumentSnapshot[0] instanceof document_1.DocumentSnapshot)); } /** * Extracts field values from the DocumentSnapshot based on the provided * field order. * * @private * @param documentSnapshot The document to extract the fields from. * @param fieldOrders The field order that defines what fields we should * extract. * @return {Array.<*>} The field values to use. * @private */ static _extractFieldValues(documentSnapshot, fieldOrders) { const fieldValues = []; for (const fieldOrder of fieldOrders) { if (path_1.FieldPath.documentId().isEqual(fieldOrder.field)) { fieldValues.push(documentSnapshot.ref); } else { const fieldValue = documentSnapshot.get(fieldOrder.field); if (fieldValue === undefined) { throw new Error(`Field "${fieldOrder .field}" is missing in the provided DocumentSnapshot. ` + 'Please provide a document that contains values for all specified ' + 'orderBy() and where() constraints.'); } else { fieldValues.push(fieldValue); } } } return fieldValues; } /** * The [Firestore]{@link Firestore} instance for the Firestore * database (useful for performing transactions, etc.). * * @type {Firestore} * @name Query#firestore * @readonly * * @example * let collectionRef = firestore.collection('col'); * * collectionRef.add({foo: 'bar'}).then(documentReference => { * let firestore = documentReference.firestore; * console.log(`Root location for document is ${firestore.formattedName}`); * }); */ get firestore() { return this._firestore; } /** * Creates and returns a new [Query]{@link Query} with the additional filter * that documents must contain the specified field and that its value should * satisfy the relation constraint provided. * * Returns a new Query that constrains the value of a Document property. * * This function returns a new (immutable) instance of the Query (rather than * modify the existing instance) to impose the filter. * * @param {string|FieldPath} fieldPath The name of a property value to compare. * @param {string} opStr A comparison operation in the form of a string * (e.g., "<"). * @param {*} value The value to which to compare the field for inclusion in * a query. * @returns {Query} The created Query. * * @example * let collectionRef = firestore.collection('col'); * * collectionRef.where('foo', '==', 'bar').get().then(querySnapshot => { * querySnapshot.forEach(documentSnapshot => { * console.log(`Found document at ${documentSnapshot.ref.path}`); * }); * }); */ where(fieldPath, opStr, value) { path_1.validateFieldPath('fieldPath', fieldPath); opStr = validateQueryOperator('opStr', opStr, value); validateQueryValue('value', value); if (this._queryOptions.startAt || this._queryOptions.endAt) { throw new Error('Cannot specify a where() filter after calling startAt(), ' + 'startAfter(), endBefore() or endAt().'); } fieldPath = path_1.FieldPath.fromArgument(fieldPath); if (path_1.FieldPath.documentId().isEqual(fieldPath)) { value = this.validateReference(value); } const combinedFilters = this._fieldFilters.concat(new FieldFilter(this._serializer, fieldPath, comparisonOperators[opStr], value)); return new Query(this._firestore, this._path, combinedFilters, this._fieldOrders, this._queryOptions); } /** * Creates and returns a new [Query]{@link Query} instance that applies a * field mask to the result and returns only the specified subset of fields. * You can specify a list of field paths to return, or use an empty list to * only return the references of matching documents. * * This function returns a new (immutable) instance of the Query (rather than * modify the existing instance) to impose the field mask. * * @param {...(string|FieldPath)} fieldPaths The field paths to return. * @returns {Query} The created Query. * * @example * let collectionRef = firestore.collection('col'); * let documentRef = collectionRef.doc('doc'); * * return documentRef.set({x:10, y:5}).then(() => { * return collectionRef.where('x', '>', 5).select('y').get(); * }).then((res) => { * console.log(`y is ${res.docs[0].get('y')}.`); * }); */ select(...fieldPaths) { const fields = []; if (fieldPaths.length === 0) { fields.push({ fieldPath: path_1.FieldPath.documentId().formattedName }); } else { for (let i = 0; i < fieldPaths.length; ++i) { path_1.validateFieldPath(i, fieldPaths[i]); fields.push({ fieldPath: path_1.FieldPath.fromArgument(fieldPaths[i]).formattedName }); } } const options = Object.assign({}, this._queryOptions); options.projection = { fields }; return new Query(this._firestore, this._path, this._fieldFilters, this._fieldOrders, options); } /** * Creates and returns a new [Query]{@link Query} that's additionally sorted * by the specified field, optionally in descending order instead of * ascending. * * This function returns a new (immutable) instance of the Query (rather than * modify the existing instance) to impose the field mask. * * @param {string|FieldPath} fieldPath The field to sort by. * @param {string=} directionStr Optional direction to sort by ('asc' or * 'desc'). If not specified, order will be ascending. * @returns {Query} The created Query. * * @example * let query = firestore.collection('col').where('foo', '>', 42); * * query.orderBy('foo', 'desc').get().then(querySnapshot => { * querySnapshot.forEach(documentSnapshot => { * console.log(`Found document at ${documentSnapshot.ref.path}`); * }); * }); */ orderBy(fieldPath, directionStr) { path_1.validateFieldPath('fieldPath', fieldPath); directionStr = validateQueryOrder('directionStr', directionStr); if (this._queryOptions.startAt || this._queryOptions.endAt) { throw new Error('Cannot specify an orderBy() constraint after calling ' + 'startAt(), startAfter(), endBefore() or endAt().'); } const newOrder = new FieldOrder(path_1.FieldPath.fromArgument(fieldPath), directionOperators[directionStr || 'asc']); const combinedOrders = this._fieldOrders.concat(newOrder); return new Query(this._firestore, this._path, this._fieldFilters, combinedOrders, this._queryOptions); } /** * Creates and returns a new [Query]{@link Query} that's additionally limited * to only return up to the specified number of documents. * * This function returns a new (immutable) instance of the Query (rather than * modify the existing instance) to impose the limit. * * @param {number} limit The maximum number of items to return. * @returns {Query} The created Query. * * @example * let query = firestore.collection('col').where('foo', '>', 42); * * query.limit(1).get().then(querySnapshot => { * querySnapshot.forEach(documentSnapshot => { * console.log(`Found document at ${documentSnapshot.ref.path}`); * }); * }); */ limit(limit) { validate_1.validateInteger('limit', limit); const options = Object.assign({}, this._queryOptions); options.limit = limit; return new Query(this._firestore, this._path, this._fieldFilters, this._fieldOrders, options); } /** * Specifies the offset of the returned results. * * This function returns a new (immutable) instance of the * [Query]{@link Query} (rather than modify the existing instance) * to impose the offset. * * @param {number} offset The offset to apply to the Query results * @returns {Query} The created Query. * * @example * let query = firestore.collection('col').where('foo', '>', 42); * * query.limit(10).offset(20).get().then(querySnapshot => { * querySnapshot.forEach(documentSnapshot => { * console.log(`Found document at ${documentSnapshot.ref.path}`); * }); * }); */ offset(offset) { validate_1.validateInteger('offset', offset); const options = Object.assign({}, this._queryOptions); options.offset = offset; return new Query(this._firestore, this._path, this._fieldFilters, this._fieldOrders, options); } /** * Returns true if this `Query` is equal to the provided value. * * @param {*} other The value to compare against. * @return {boolean} true if this `Query` is equal to the provided value. */ isEqual(other) { if (this === other) { return true; } return (other instanceof Query && this._path.isEqual(other._path) && deepEqual(this._fieldFilters, other._fieldFilters, { strict: true }) && deepEqual(this._fieldOrders, other._fieldOrders, { strict: true }) && deepEqual(this._queryOptions, other._queryOptions, { strict: true })); } /** * Computes the backend ordering semantics for DocumentSnapshot cursors. * * @private * @param cursorValuesOrDocumentSnapshot The snapshot of the document or the * set of field values to use as the boundary. * @returns The implicit ordering semantics. */ createImplicitOrderBy(cursorValuesOrDocumentSnapshot) { if (!Query._isDocumentSnapshot(cursorValuesOrDocumentSnapshot)) { return this._fieldOrders; } const fieldOrders = this._fieldOrders.slice(); let hasDocumentId = false; if (fieldOrders.length === 0) { // If no explicit ordering is specified, use the first inequality to // define an implicit order. for (const fieldFilter of this._fieldFilters) { if (fieldFilter.isInequalityFilter()) { fieldOrders.push(new FieldOrder(fieldFilter.field)); break; } } } else { for (const fieldOrder of fieldOrders) { if (path_1.FieldPath.documentId().isEqual(fieldOrder.field)) { hasDocumentId = true; } } } if (!hasDocumentId) { // Add implicit sorting by name, using the last specified direction. const lastDirection = fieldOrders.length === 0 ? directionOperators.ASC : fieldOrders[fieldOrders.length - 1].direction; fieldOrders.push(new FieldOrder(path_1.FieldPath.documentId(), lastDirection)); } return fieldOrders; } /** * Builds a Firestore 'Position' proto message. * * @private * @param {Array.<FieldOrder>} fieldOrders The field orders to use for this * cursor. * @param {Array.<DocumentSnapshot|*>} cursorValuesOrDocumentSnapshot The * snapshot of the document or the set of field values to use as the boundary. * @param before Whether the query boundary lies just before or after the * provided data. * @returns {Object} The proto message. */ createCursor(fieldOrders, cursorValuesOrDocumentSnapshot, before) { let fieldValues; if (Query._isDocumentSnapshot(cursorValuesOrDocumentSnapshot)) { fieldValues = Query._extractFieldValues(cursorValuesOrDocumentSnapshot[0], fieldOrders); } else { fieldValues = cursorValuesOrDocumentSnapshot; } if (fieldValues.length > fieldOrders.length) { throw new Error('Too many cursor values specified. The specified ' + 'values must match the orderBy() constraints of the query.'); } const options = { values: [] }; if (before) { options.before = true; } for (let i = 0; i < fieldValues.length; ++i) { let fieldValue = fieldValues[i]; if (path_1.FieldPath.documentId().isEqual(fieldOrders[i].field)) { fieldValue = this.validateReference(fieldValue); } validateQueryValue(i, fieldValue); options.values.push(fieldValue); } return options; } /** * Validates that a value used with FieldValue.documentId() is either a * string or a DocumentReference that is part of the query`s result set. * Throws a validation error or returns a DocumentReference that can * directly be used in the Query. * * @param val The value to validate. * @throws If the value cannot be used for this query. * @return If valid, returns a DocumentReference that can be used with the * query. * @private */ validateReference(val) { let reference; if (typeof val === 'string') { reference = new DocumentReference(this._firestore, this._path.append(val)); } else if (val instanceof DocumentReference) { reference = val; if (!this._path.isPrefixOf(reference._path)) { throw new Error(`"${reference.path}" is not part of the query result set and ` + 'cannot be used as a query boundary.'); } } else { throw new Error('The corresponding value for FieldPath.documentId() must be a ' + 'string or a DocumentReference.'); } if (reference._path.parent().compareTo(this._path) !== 0) { throw new Error('Only a direct child can be used as a query boundary. ' + `Found: "${reference.path}".`); } return reference; } /** * Creates and returns a new [Query]{@link Query} that starts at the provided * set of field values relative to the order of the query. The order of the * provided values must match the order of the order by clauses of the query. * * @param {...*|DocumentSnapshot} fieldValuesOrDocumentSnapshot The snapshot * of the document the query results should start at or the field values to * start this query at, in order of the query's order by. * @returns {Query} A query with the new starting point. * * @example * let query = firestore.collection('col'); * * query.orderBy('foo').startAt(42).get().then(querySnapshot => { * querySnapshot.forEach(documentSnapshot => { * console.log(`Found document at ${documentSnapshot.ref.path}`); * }); * }); */ startAt(...fieldValuesOrDocumentSnapshot) { validate_1.validateMinNumberOfArguments('Query.startAt', arguments, 1); const options = Object.assign({}, this._queryOptions); const fieldOrders = this.createImplicitOrderBy(fieldValuesOrDocumentSnapshot); options.startAt = this.createCursor(fieldOrders, fieldValuesOrDocumentSnapshot, true); return new Query(this._firestore, this._path, this._fieldFilters, fieldOrders, options); } /** * Creates and returns a new [Query]{@link Query} that starts after the * provided set of field values relative to the order of the query. The order * of the provided values must match the order of the order by clauses of the * query. * * @param {...*|DocumentSnapshot} fieldValuesOrDocumentSnapshot The snapshot * of the document the query results should start after or the field values to * start this query after, in order of the query's order by. * @returns {Query} A query with the new starting point. * * @example * let query = firestore.collection('col'); * * query.orderBy('foo').startAfter(42).get().then(querySnapshot => { * querySnapshot.forEach(documentSnapshot => { * console.log(`Found document at ${documentSnapshot.ref.path}`); * }); * }); */ startAfter(...fieldValuesOrDocumentSnapshot) { validate_1.validateMinNumberOfArguments('Query.startAfter', arguments, 1); const options = Object.assign({}, this._queryOptions); const fieldOrders = this.createImplicitOrderBy(fieldValuesOrDocumentSnapshot); options.startAt = this.createCursor(fieldOrders, fieldValuesOrDocumentSnapshot, false); return new Query(this._firestore, this._path, this._fieldFilters, fieldOrders, options); } /** * Creates and returns a new [Query]{@link Query} that ends before the set of * field values relative to the order of the query. The order of the provided * values must match the order of the order by clauses of the query. * * @param {...*|DocumentSnapshot} fieldValuesOrDocumentSnapshot The snapshot * of the document the query results should end before or the field values to * end this query before, in order of the query's order by. * @returns {Query} A query with the new ending point. * * @example * let query = firestore.collection('col'); * * query.orderBy('foo').endBefore(42).get().then(querySnapshot => { * querySnapshot.forEach(documentSnapshot => { * console.log(`Found document at ${documentSnapshot.ref.path}`); * }); * }); */ endBefore(...fieldValuesOrDocumentSnapshot) { validate_1.validateMinNumberOfArguments('Query.endBefore', arguments, 1); const options = Object.assign({}, this._queryOptions); const fieldOrders = this.createImplicitOrderBy(fieldValuesOrDocumentSnapshot); options.endAt = this.createCursor(fieldOrders, fieldValuesOrDocumentSnapshot, true); return new Query(this._firestore, this._path, this._fieldFilters, fieldOrders, options); } /** * Creates and returns a new [Query]{@link Query} that ends at the provided * set of field values relative to the order of the query. The order of the * provided values must match the order of the order by clauses of the query. * * @param {...*|DocumentSnapshot} fieldValuesOrDocumentSnapshot The snapshot * of the document the query results should end at or the field values to end * this query at, in order of the query's order by. * @returns {Query} A query with the new ending point. * * @example * let query = firestore.collection('col'); * * query.orderBy('foo').endAt(42).get().then(querySnapshot => { * querySnapshot.forEach(documentSnapshot => { * console.log(`Found document at ${documentSnapshot.ref.path}`); * }); * }); */ endAt(...fieldValuesOrDocumentSnapshot) { validate_1.validateMinNumberOfArguments('Query.endAt', arguments, 1); const options = Object.assign({}, this._queryOptions); const fieldOrders = this.createImplicitOrderBy(fieldValuesOrDocumentSnapshot); options.endAt = this.createCursor(fieldOrders, fieldValuesOrDocumentSnapshot, false); return new Query(this._firestore, this._path, this._fieldFilters, fieldOrders, options); } /** * Executes the query and returns the results as a * [QuerySnapshot]{@link QuerySnapshot}. * * @returns {Promise.<QuerySnapshot>} A Promise that resolves with the results * of the Query. * * @example * let query = firestore.collection('col').where('foo', '==', 'bar'); * * query.get().then(querySnapshot => { * querySnapshot.forEach(documentSnapshot => { * console.log(`Found document at ${documentSnapshot.ref.path}`); * }); * }); */ get() { return this._get(); } /** * Internal get() method that accepts an optional transaction id. * * @private * @param {bytes=} transactionId A transaction ID. */ _get(transactionId) { const self = this; const docs = []; return new Promise((resolve, reject) => { let readTime; self._stream(transactionId) .on('error', err => { reject(err); }) .on('data', result => { readTime = result.readTime; if (result.document) { const document = result.document; docs.push(document); } }) .on('end', () => { resolve(new QuerySnapshot(this, readTime, docs.length, () => docs, () => { const changes = []; for (let i = 0; i < docs.length; ++i) { changes.push(new document_change_1.DocumentChange('added', docs[i], -1, i)); } return changes; })); }); }); } /** * Executes the query and streams the results as * [QueryDocumentSnapshots]{@link QueryDocumentSnapshot}. * * @returns {Stream.<QueryDocumentSnapshot>} A stream of * QueryDocumentSnapshots. * * @example * let query = firestore.collection('col').where('foo', '==', 'bar'); * * let count = 0; * * query.stream().on('data', (documentSnapshot) => { * console.log(`Found document with name '${documentSnapshot.id}'`); * ++count; * }).on('end', () => { * console.log(`Total count is ${coun