ran-boilerplate
Version:
React . Apollo (GraphQL) . Next.js Toolkit
1,795 lines (1,653 loc) • 59.6 kB
JavaScript
/*!
* 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';
const bun = require('bun');
const extend = require('extend');
const is = require('is');
const order = require('./order');
const through = require('through2');
/*!
* Injected.
*
* @see Firestore
*/
let Firestore;
/*!
* Injected.
*
* @see DocumentSnapshot
*/
let DocumentSnapshot;
/*!
* Injected.
*
* @see DocumentTransform
*/
let DocumentTransform;
/*!
* Injected.
*/
let validate;
/*!
* Injected.
*
* @see Watch
*/
let Watch;
/*!
* Injected.
*
* @see WriteBatch
*/
let WriteBatch;
const path = require('./path');
/*!
* @private
* @see ResourcePath
*/
const ResourcePath = path.ResourcePath;
/*!
* @private
* @see FieldPath
*/
const FieldPath = path.FieldPath;
/*!
* The direction of a `Query.orderBy()` clause is specified as 'desc' or 'asc'
* (descending or ascending).
*
* @private
*/
const directionOperators = {
asc: 'ASCENDING',
ASC: 'ASCENDING',
desc: 'DESCENDING',
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',
'==': 'EQUAL',
'>': 'GREATER_THAN',
'>=': 'GREATER_THAN_OR_EQUAL',
};
/**
* 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} firestore - The Firestore Database client.
* @param {ResourcePath} path - The Path of this reference.
*/
constructor(firestore, path) {
this._firestore = firestore;
this._referencePath = path;
}
/**
* The string representation of the DocumentReference's location.
* @private
* @type {string}
* @name DocumentReference#formattedName
*/
get formattedName() {
return this._referencePath.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._referencePath.relativeName;
}
/**
* The last path document 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._referencePath.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 createCollectionReference(
this._firestore,
this._referencePath.parent()
);
}
/**
* Returns the [ResourcePath]{@link ResourcePath} for this
* DocumentReference.
*
* @private
* @type {ResourcePath}
* @readonly
*/
get ref() {
return this._referencePath;
}
/**
* Retrieve a document from the database. Fails the Promise if the document is
* not found.
*
* @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 => {
return result[0];
});
}
/**
* 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) {
validate.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 createCollectionReference(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.getCollections().then(collections => {
* for (let collection of collections) {
* console.log(`Found subcollection with id: ${collection.id}`);
* }
* });
*/
getCollections() {
let request = {
parent: this._referencePath.formattedName,
};
let api = this._firestore.api.Firestore;
return this._firestore
.request(api.listCollectionIds.bind(api), request)
.then(collectionIds => {
let collections = [];
// We can just sort this list using the default comparator since it will
// only contain collection ids.
collectionIds.sort();
for (let collectionId of collectionIds) {
collections.push(this.collection(collectionId));
}
return collections;
});
}
/**
* 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) {
let writeBatch = new WriteBatch(this._firestore);
return writeBatch
.create(this, data)
.commit()
.then(writeResults => {
return Promise.resolve(writeResults[0]);
});
}
/**
* 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 {string=} precondition.lastUpdateTime If set, enforces that the
* document was last updated at lastUpdateTime (as ISO 8601 string). 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) {
let writeBatch = new WriteBatch(this._firestore);
return writeBatch
.delete(this, precondition)
.commit()
.then(writeResults => {
return Promise.resolve(writeResults[0]);
});
}
/**
* 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() only replaces the
* values specified in its data argument. Fields omitted from this set() call
* remain 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) {
let writeBatch = new WriteBatch(this._firestore);
return writeBatch
.set(this, data, options)
.commit()
.then(writeResults => {
return Promise.resolve(writeResults[0]);
});
}
/**
* 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.minNumberOfArguments('update', arguments, 1);
let writeBatch = new WriteBatch(this._firestore);
preconditionOrValues = Array.prototype.slice.call(arguments, 1);
return writeBatch.update
.apply(writeBatch, [this, dataOrField].concat(preconditionOrValues))
.commit()
.then(writeResults => {
return Promise.resolve(writeResults[0]);
});
}
/**
* 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.isFunction('onNext', onNext);
validate.isOptionalFunction('onError', onError);
if (!is.defined(onError)) {
onError = console.error; // eslint-disable-line no-console
}
let watch = Watch.forDocument(this);
return watch.onSnapshot((readTime, docs) => {
for (let document of docs()) {
if (document.ref.path === this.path) {
onNext(document);
return;
}
}
// The document is missing.
let document = new DocumentSnapshot.Builder();
document.ref = this._referencePath;
document.readTime = readTime;
onNext(document.build());
}, onError);
}
}
/**
* A DocumentChange represents a change to the documents matching a query.
* It contains the document affected and the type of change that occurred.
*
* @class
*/
class DocumentChange {
/**
* @private
* @hideconstructor
*
* @param {string} type - 'added' | 'removed' | 'modified'.
* @param {QueryDocumentSnapshot} document - The document.
* @param {number} oldIndex - The index in the documents array prior to this
* change.
* @param {number} newIndex - The index in the documents array after this
* change.
*/
constructor(type, document, oldIndex, newIndex) {
this._type = type;
this._document = document;
this._oldIndex = oldIndex;
this._newIndex = newIndex;
}
/**
* The type of change ('added', 'modified', or 'removed').
*
* @type {string}
* @name DocumentChange#type
* @readonly
*
* @example
* let query = firestore.collection('col').where('foo', '==', 'bar');
* let docsArray = [];
*
* let unsubscribe = query.onSnapshot(querySnapshot => {
* for (let change of querySnapshot.docChanges) {
* console.log(`Type of change is ${change.type}`);
* }
* });
*
* // Remove this listener.
* unsubscribe();
*/
get type() {
return this._type;
}
/**
* The document affected by this change.
*
* @type {QueryDocumentSnapshot}
* @name DocumentChange#doc
* @readonly
*
* @example
* let query = firestore.collection('col').where('foo', '==', 'bar');
*
* let unsubscribe = query.onSnapshot(querySnapshot => {
* for (let change of querySnapshot.docChanges) {
* console.log(change.doc.data());
* }
* });
*
* // Remove this listener.
* unsubscribe();
*/
get doc() {
return this._document;
}
/**
* The index of the changed document in the result set immediately prior to
* this DocumentChange (i.e. supposing that all prior DocumentChange objects
* have been applied). Is -1 for 'added' events.
*
* @type {number}
* @name DocumentChange#oldIndex
* @readonly
*
* @example
* let query = firestore.collection('col').where('foo', '==', 'bar');
* let docsArray = [];
*
* let unsubscribe = query.onSnapshot(querySnapshot => {
* for (let change of querySnapshot.docChanges) {
* if (change.oldIndex !== -1) {
* docsArray.splice(change.oldIndex, 1);
* }
* if (change.newIndex !== -1) {
* docsArray.splice(change.newIndex, 0, change.doc);
* }
* }
* });
*
* // Remove this listener.
* unsubscribe();
*/
get oldIndex() {
return this._oldIndex;
}
/**
* The index of the changed document in the result set immediately after
* this DocumentChange (i.e. supposing that all prior DocumentChange
* objects and the current DocumentChange object have been applied).
* Is -1 for 'removed' events.
*
* @type {number}
* @name DocumentChange#newIndex
* @readonly
*
* @example
* let query = firestore.collection('col').where('foo', '==', 'bar');
* let docsArray = [];
*
* let unsubscribe = query.onSnapshot(querySnapshot => {
* for (let change of querySnapshot.docChanges) {
* if (change.oldIndex !== -1) {
* docsArray.splice(change.oldIndex, 1);
* }
* if (change.newIndex !== -1) {
* docsArray.splice(change.newIndex, 0, change.doc);
* }
* }
* });
*
* // Remove this listener.
* unsubscribe();
*/
get newIndex() {
return this._newIndex;
}
}
/**
* A Query order-by field.
*
* @private
* @class
*/
class FieldOrder {
/**
* @private
* @hideconstructor
*
* @param {FieldPath} field - The name of a document field (member)
* on which to order query results.
* @param {string=} direction One of 'ASCENDING' (default) or 'DESCENDING' to
* set the ordering direction to ascending or descending, respectively.
*/
constructor(field, direction) {
this._field = field;
this._direction = direction || directionOperators.ASC;
}
/**
* The path of the field on which to order query results.
*
* @private
* @type {FieldPath}
*/
get field() {
return this._field;
}
/**
* One of 'ASCENDING' (default) or 'DESCENDING'.
*
* @private
* @type {string}
*/
get direction() {
return this._direction;
}
/**
* Generates the proto representation for this field order.
*
* @private
* @returns {Object}
*/
toProto() {
return {
field: {
fieldPath: this._field.formattedName,
},
direction: this._direction,
};
}
}
/*!
* A field constraint for a Query where clause.
*
* @private
* @class
*/
class FieldFilter {
/**
* @private
* @hideconstructor
*
* @param {FieldPath} field - The path of the property value to
* compare.
* @param {string} opString - A comparison operation.
* @param {*} value The value to which to compare the
* field for inclusion in a query.
*/
constructor(field, opString, value) {
this._field = field;
this._opString = opString;
this._value = value;
}
/**
* Returns the field path of this filter.
*
* @private
* @return {FieldPath}
*/
get field() {
return this._field;
}
/**
* Returns whether this FieldFilter uses an equals comparison.
*
* @private
* @return {boolean}
*/
isEqualsFilter() {
return this._opString === 'EQUAL';
}
/**
* Generates the proto representation for this field filter.
*
* @private
* @returns {Object}
*/
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._opString,
value: DocumentSnapshot.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} query - The originating query.
* @param {string} readTime - The ISO 8601 time when this query snapshot was
* current.
*
* @param {function} docs - A callback returning a sorted array of documents
* matching this query
* @param {function} changes - A callback returning a sorted array of
* document change events for this snapshot.
*/
constructor(query, readTime, docs, changes) {
this._query = query;
this._comparator = query.comparator();
this._readTime = readTime;
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();
return this._materializedDocs;
}
/**
* An array of all changes in this QuerySnapshot.
*
* @type {Array.<DocumentChange>}
* @name QuerySnapshot#docChanges
* @readonly
*/
get docChanges() {
if (this._materializedChanges) {
return this._materializedChanges;
}
this._materializedChanges = this._changes();
return this._materializedChanges;
}
/**
* 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.docs.length === 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.docs.length;
}
/**
* The time this query snapshot was obtained.
*
* @type {string}
* @name QuerySnapshot#readTime
*
* @example
* let query = firestore.collection('col').where('foo', '==', 'bar');
*
* query.get().then((querySnapshot) => {
* console.log(`Query results returned at '${querySnapshot.readTime}'`);
* });
*/
get readTime() {
return this._readTime;
}
/**
* Enumerates all of the documents in the QuerySnapshot.
*
* @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.isFunction('callback', callback);
for (let doc of this.docs) {
callback.call(thisArg, doc);
}
}
}
/**
* 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} firestore - The Firestore Database client.
* @param {ResourcePath} path Path of the collection to be queried.
* @param {Array.<FieldOrder>=} fieldOrders - Sequence of fields to
* control the order of results.
* @param {Array.<FieldFilter>=} fieldFilters - Sequence of fields
* constraining the results of the query.
* @param {object=} queryOptions Additional query options.
*/
constructor(firestore, path, fieldFilters, fieldOrders, queryOptions) {
this._firestore = firestore;
this._api = firestore.api;
this._referencePath = path;
this._fieldFilters = fieldFilters || [];
this._fieldOrders = fieldOrders || [];
this._queryOptions = queryOptions || {};
}
/**
* Detects the argument type for Firestore cursors.
*
* @private
* @param {Array.<DocumentSnapshot|*>} fieldValuesOrDocumentSnapshot - A
* snapshot of the document or a set of field values.
* @returns {boolean} 'true' if the input is a single DocumentSnapshot..
*/
static _isDocumentSnapshot(fieldValuesOrDocumentSnapshot) {
return (
fieldValuesOrDocumentSnapshot.length === 1 &&
is.instance(fieldValuesOrDocumentSnapshot[0], DocumentSnapshot)
);
}
/**
* Extracts field values from the DocumentSnapshot based on the provided
* field order.
*
* @private
* @param {DocumentSnapshot} documentSnapshot - The document to extract the
* fields from.
* @param {Array.<FieldOrder>} fieldOrders - The field order that defines what
* fields we should extract.
* @return {Array.<*>} The field values to use.
* @private
*/
static _extractFieldValues(documentSnapshot, fieldOrders) {
let fieldValues = [];
for (let fieldOrder of fieldOrders) {
if (fieldOrder.field === FieldPath._DOCUMENT_ID) {
fieldValues.push(documentSnapshot.ref);
} else {
let fieldValue = documentSnapshot.get(fieldOrder.field);
if (is.undefined(fieldValue)) {
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 string representation of the Query's location.
* @private
* @type {string}
* @name Query#formattedName
*/
get formattedName() {
return this._referencePath.formattedName;
}
/**
* 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) {
validate.isFieldPath('fieldPath', fieldPath);
validate.isFieldComparison('opStr', opStr, value);
if (this._queryOptions.startAt || this._queryOptions.endAt) {
throw new Error(
'Cannot specify a where() filter after calling startAt(), ' +
'startAfter(), endBefore() or endAt().'
);
}
let newFilter = new FieldFilter(
FieldPath.fromArgument(fieldPath),
comparisonOperators[opStr],
value
);
let combinedFilters = this._fieldFilters.concat(newFilter);
return new Query(
this._firestore,
this._referencePath,
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) {
fieldPaths = [].slice.call(arguments);
let result = [];
if (fieldPaths.length === 0) {
result.push({fieldPath: FieldPath._DOCUMENT_ID.formattedName});
} else {
for (let i = 0; i < fieldPaths.length; ++i) {
validate.isFieldPath(i, fieldPaths[i]);
result.push({
fieldPath: FieldPath.fromArgument(fieldPaths[i]).formattedName,
});
}
}
let options = extend(true, {}, this._queryOptions);
options.selectFields = {fields: result};
return new Query(
this._firestore,
this._referencePath,
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) {
validate.isFieldPath('fieldPath', fieldPath);
validate.isOptionalFieldOrder('directionStr', directionStr);
if (this._queryOptions.startAt || this._queryOptions.endAt) {
throw new Error(
'Cannot specify an orderBy() constraint after calling ' +
'startAt(), startAfter(), endBefore() or endAt().'
);
}
let newOrder = new FieldOrder(
FieldPath.fromArgument(fieldPath),
directionOperators[directionStr]
);
let combinedOrders = this._fieldOrders.concat(newOrder);
return new Query(
this._firestore,
this._referencePath,
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.isInteger('limit', limit);
let options = extend(true, {}, this._queryOptions);
options.limit = limit;
return new Query(
this._firestore,
this._referencePath,
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.isInteger('offset', offset);
let options = extend(true, {}, this._queryOptions);
options.offset = offset;
return new Query(
this._firestore,
this._referencePath,
this._fieldFilters,
this._fieldOrders,
options
);
}
/**
* Computes the backend ordering semantics for DocumentSnapshot cursors.
*
* @private
* @param {Array.<DocumentSnapshot|*>} cursorValuesOrDocumentSnapshot - The
* snapshot of the document or the set of field values to use as the boundary.
* @returns {Array.<FieldOrder>} The implicit ordering semantics.
*/
_createImplicitOrderBy(cursorValuesOrDocumentSnapshot) {
if (!Query._isDocumentSnapshot(cursorValuesOrDocumentSnapshot)) {
return this._fieldOrders;
}
let 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 (let fieldFilter of this._fieldFilters) {
if (!fieldFilter.isEqualsFilter()) {
fieldOrders.push(new FieldOrder(fieldFilter.field, 'ASCENDING'));
break;
}
}
} else {
for (let fieldOrder of fieldOrders) {
if (fieldOrder.field === FieldPath._DOCUMENT_ID) {
hasDocumentId = true;
}
}
}
if (!hasDocumentId) {
// Add implicit sorting by name, using the last specified direction.
let lastDirection =
fieldOrders.length === 0
? directionOperators.ASC
: fieldOrders[fieldOrders.length - 1].direction;
fieldOrders.push(new FieldOrder(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.'
);
}
let options = {
before: before,
values: [],
};
for (let i = 0; i < fieldValues.length; ++i) {
let fieldValue = fieldValues[i];
if (fieldOrders[i].field === FieldPath._DOCUMENT_ID) {
if (is.string(fieldValue)) {
fieldValue = new DocumentReference(
this._firestore,
this._referencePath.append(fieldValue)
);
} else if (is.instance(fieldValue, DocumentReference)) {
if (!this._referencePath.isPrefixOf(fieldValue.ref)) {
throw new Error(
`'${fieldValue.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 (fieldValue.ref.parent().compareTo(this._referencePath) !== 0) {
throw new Error(
'Only a direct child can be used as a query boundary. ' +
`Found: '${fieldValue.path}'.`
);
}
}
if (DocumentTransform.isTransformSentinel(fieldValue)) {
throw new Error(
`Cannot use FieldValue.delete() or FieldValue.serverTimestamp() in ` +
`a query boundary. Found at index ${i}.`
);
}
options.values.push(DocumentSnapshot.encodeValue(fieldValue));
}
return options;
}
/**
* 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) {
let options = extend(true, {}, this._queryOptions);
fieldValuesOrDocumentSnapshot = [].slice.call(arguments);
let fieldOrders = this._createImplicitOrderBy(
fieldValuesOrDocumentSnapshot
);
options.startAt = this._createCursor(
fieldOrders,
fieldValuesOrDocumentSnapshot,
true
);
return new Query(
this._firestore,
this._referencePath,
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) {
let options = extend(true, {}, this._queryOptions);
fieldValuesOrDocumentSnapshot = [].slice.call(arguments);
let fieldOrders = this._createImplicitOrderBy(
fieldValuesOrDocumentSnapshot
);
options.startAt = this._createCursor(
fieldOrders,
fieldValuesOrDocumentSnapshot,
false
);
return new Query(
this._firestore,
this._referencePath,
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) {
let options = extend(true, {}, this._queryOptions);
fieldValuesOrDocumentSnapshot = [].slice.call(arguments);
let fieldOrders = this._createImplicitOrderBy(
fieldValuesOrDocumentSnapshot
);
options.endAt = this._createCursor(
fieldOrders,
fieldValuesOrDocumentSnapshot,
true
);
return new Query(
this._firestore,
this._referencePath,
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) {
let options = extend(true, {}, this._queryOptions);
fieldValuesOrDocumentSnapshot = [].slice.call(arguments);
let fieldOrders = this._createImplicitOrderBy(
fieldValuesOrDocumentSnapshot
);
options.endAt = this._createCursor(
fieldOrders,
fieldValuesOrDocumentSnapshot,
false
);
return new Query(
this._firestore,
this._referencePath,
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=} queryOptions.transactionId - A transaction ID.
*/
_get(queryOptions) {
let self = this;
let docs = [];
let changes = [];
return new Promise((resolve, reject) => {
let readTime;
self
._stream(queryOptions)
.on('error', err => {
reject(err);
})
.on('data', result => {
readTime = result.readTime;
if (result.document) {
let document = result.document;
changes.push(
new DocumentChange(
DocumentChange.ADDED,
document,
-1,
docs.length
)
);
docs.push(document);
}
})
.on('end', () => {
resolve(new QuerySnapshot(this, readTime, () => docs, () => 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 ${count}`);
* });
*/
stream() {
let responseStream = this._stream();
let transform = through.obj(function(chunk, encoding, callback) {
// Only send chunks with documents.
if (chunk.document) {
this.push(chunk.document);
}
callback();
});
return bun([responseStream, transform]);
}
/**
* Internal method for serializing a query to its RunQuery proto
* representation with an optional transaction id.
*
* @param {bytes=} queryOptions.transactionId - A transaction ID.
* @private
* @returns Serialized JSON for the query.
*/
toProto(queryOptions) {
let reqOpts = {
parent: this._referencePath.parent().formattedName,
structuredQuery: {
from: [
{
collectionId: this._referencePath.id,
},
],
},
};
let structuredQuery = reqOpts.structuredQuery;
if (this._fieldFilters.length) {
let filters = [];
for (let fieldFilter of this._fieldFilters) {
filters.push(fieldFilter.toProto());
}
structuredQuery.where = {
compositeFilter: {
op: 'AND',
filters: filters,
},
};
}
if (this._fieldOrders.length) {
let orderBy = [];
for (let fieldOrder of this._fieldOrders) {
orderBy.push(fieldOrder.toProto());
}
structuredQuery.orderBy = orderBy;
}
if (this._queryOptions.limit) {
structuredQuery.limit = {value: this._queryOptions.limit};
}
if (this._queryOptions.offset) {
structuredQuery.offset = this._queryOptions.offset;
}
if (this._queryOptions.startAt) {
structuredQuery.startAt = this._queryOptions.startAt;
}
if (this._queryOptions.endAt) {
structuredQuery.endAt = this._queryOptions.endAt;
}
if (this._queryOptions.selectFields) {
structuredQuery.select = this._queryOptions.selectFields;
}
if (queryOptions && queryOptions.transactionId) {
reqOpts.transaction = queryOptions.transactionId;
}
return reqOpts;
}
/**
* Internal streaming method that accepts an optional transaction id.
*
* @param {bytes=} queryOptions.transactionId - A transaction ID.
* @private
* @returns {stream} A stream of document results.
*/
_stream(queryOptions) {
let request = this.toProto(queryOptions);
let self = this;
let stream = through.obj(function(proto, enc, callback) {
let readTime = DocumentSnapshot.toISOTime(proto.readTime);
if (proto.document) {
let document = self.firestore.snapshot_(proto.document, proto.readTime);
this.push({document, readTime});
} else {
this.push({readTime});
}
callback();
});
this._firestore
.readStream(
this._api.Firestore.runQuery.bind(this._api.Firestore),
request,
/* allowRetries= */ true
)