File: /var/www/api.javaapp.co.uk/node_modules/@google-cloud/firestore/build/src/reference/query.js
"use strict";
/**
* Copyright 2024 Google LLC. 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 });
exports.Query = void 0;
const stream_1 = require("stream");
const query_util_1 = require("./query-util");
const index_1 = require("../index");
const field_order_1 = require("./field-order");
const field_filter_internal_1 = require("./field-filter-internal");
const composite_filter_internal_1 = require("./composite-filter-internal");
const constants_1 = require("./constants");
const vector_query_options_1 = require("./vector-query-options");
const document_reference_1 = require("./document-reference");
const query_snapshot_1 = require("./query-snapshot");
const serializer_1 = require("../serializer");
const query_profile_1 = require("../query-profile");
const filter_1 = require("../filter");
const path_1 = require("../path");
const helpers_1 = require("./helpers");
const validate_1 = require("../validate");
const types_1 = require("./types");
const aggregate_query_1 = require("./aggregate-query");
const vector_query_1 = require("./vector-query");
const order_1 = require("../order");
const types_2 = require("../types");
/**
* 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 {
/**
* @internal
* @private
*
* @param _firestore The Firestore Database client.
* @param _queryOptions Options that define the query.
*/
constructor(
/**
* @internal
* @private
**/
_firestore,
/**
* @internal
* @private
**/
_queryOptions) {
this._firestore = _firestore;
this._queryOptions = _queryOptions;
this._serializer = new serializer_1.Serializer(_firestore);
this._allowUndefined =
!!this._firestore._settings.ignoreUndefinedProperties;
this._queryUtil = new query_util_1.QueryUtil(_firestore, _queryOptions, this._serializer);
}
/**
* Extracts field values from the DocumentSnapshot based on the provided
* field order.
*
* @private
* @internal
* @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.
*/
static _extractFieldValues(documentSnapshot, fieldOrders) {
const fieldValues = [];
for (const fieldOrder of fieldOrders) {
if (index_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;
}
where(fieldPathOrFilter, opStr, value) {
let filter;
if (fieldPathOrFilter instanceof index_1.Filter) {
filter = fieldPathOrFilter;
}
else {
filter = index_1.Filter.where(fieldPathOrFilter, opStr, value);
}
if (this._queryOptions.startAt || this._queryOptions.endAt) {
throw new Error('Cannot specify a where() filter after calling startAt(), ' +
'startAfter(), endBefore() or endAt().');
}
const parsedFilter = this._parseFilter(filter);
if (parsedFilter.getFilters().length === 0) {
// Return the existing query if not adding any more filters (e.g. an empty composite filter).
return this;
}
const options = this._queryOptions.with({
filters: this._queryOptions.filters.concat(parsedFilter),
});
return new Query(this._firestore, options);
}
/**
* @internal
* @private
*/
_parseFilter(filter) {
if (filter instanceof filter_1.UnaryFilter) {
return this._parseFieldFilter(filter);
}
return this._parseCompositeFilter(filter);
}
/**
* @internal
* @private
*/
_parseFieldFilter(fieldFilterData) {
let value = fieldFilterData._getValue();
let operator = fieldFilterData._getOperator();
const fieldPath = fieldFilterData._getField();
(0, path_1.validateFieldPath)('fieldPath', fieldPath);
operator = (0, helpers_1.validateQueryOperator)('opStr', operator, value);
(0, helpers_1.validateQueryValue)('value', value, this._allowUndefined);
const path = index_1.FieldPath.fromArgument(fieldPath);
if (index_1.FieldPath.documentId().isEqual(path)) {
if (operator === 'array-contains' || operator === 'array-contains-any') {
throw new Error(`Invalid Query. You can't perform '${operator}' ` +
'queries on FieldPath.documentId().');
}
else if (operator === 'in' || operator === 'not-in') {
if (!Array.isArray(value) || value.length === 0) {
throw new Error(`Invalid Query. A non-empty array is required for '${operator}' filters.`);
}
value = value.map(el => this.validateReference(el));
}
else {
value = this.validateReference(value);
}
}
return new field_filter_internal_1.FieldFilterInternal(this._serializer, path, constants_1.comparisonOperators[operator], value);
}
/**
* @internal
* @private
*/
_parseCompositeFilter(compositeFilterData) {
const parsedFilters = compositeFilterData
._getFilters()
.map(filter => this._parseFilter(filter))
.filter(parsedFilter => parsedFilter.getFilters().length > 0);
// For composite filters containing 1 filter, return the only filter.
// For example: AND(FieldFilter1) == FieldFilter1
if (parsedFilters.length === 1) {
return parsedFilters[0];
}
return new composite_filter_internal_1.CompositeFilterInternal(parsedFilters, compositeFilterData._getOperator() === 'AND' ? 'AND' : 'OR');
}
/**
* 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.
*
* Queries that contain field masks cannot be listened to via `onSnapshot()`
* listeners.
*
* 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: index_1.FieldPath.documentId().formattedName });
}
else {
for (let i = 0; i < fieldPaths.length; ++i) {
(0, path_1.validateFieldPath)(i, fieldPaths[i]);
fields.push({
fieldPath: index_1.FieldPath.fromArgument(fieldPaths[i]).formattedName,
});
}
}
// By specifying a field mask, the query result no longer conforms to type
// `T`. We there return `Query<DocumentData>`;
const options = this._queryOptions.with({
projection: { fields },
});
return new Query(this._firestore, 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) {
(0, path_1.validateFieldPath)('fieldPath', fieldPath);
directionStr = (0, helpers_1.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 field_order_1.FieldOrder(index_1.FieldPath.fromArgument(fieldPath), constants_1.directionOperators[directionStr || 'asc']);
const options = this._queryOptions.with({
fieldOrders: this._queryOptions.fieldOrders.concat(newOrder),
});
return new Query(this._firestore, options);
}
/**
* Creates and returns a new [Query]{@link Query} that only returns the
* first matching 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) {
(0, validate_1.validateInteger)('limit', limit);
const options = this._queryOptions.with({
limit,
limitType: types_1.LimitType.First,
});
return new Query(this._firestore, options);
}
/**
* Creates and returns a new [Query]{@link Query} that only returns the
* last matching documents.
*
* You must specify at least one orderBy clause for limitToLast queries,
* otherwise an exception will be thrown during execution.
*
* Results for limitToLast queries cannot be streamed via the `stream()` API.
*
* @param limit The maximum number of items to return.
* @return The created Query.
*
* @example
* ```
* let query = firestore.collection('col').where('foo', '>', 42);
*
* query.limitToLast(1).get().then(querySnapshot => {
* querySnapshot.forEach(documentSnapshot => {
* console.log(`Last matching document is ${documentSnapshot.ref.path}`);
* });
* });
* ```
*/
limitToLast(limit) {
(0, validate_1.validateInteger)('limitToLast', limit);
const options = this._queryOptions.with({ limit, limitType: types_1.LimitType.Last });
return new Query(this._firestore, 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) {
(0, validate_1.validateInteger)('offset', offset);
const options = this._queryOptions.with({ offset });
return new Query(this._firestore, options);
}
/**
* Returns a query that counts the documents in the result set of this
* query.
*
* The returned query, when executed, counts the documents in the result set
* of this query without actually downloading the documents.
*
* Using the returned query to count the documents is efficient because only
* the final count, not the documents' data, is downloaded. The returned
* query can count the documents in cases where the result set is
* prohibitively large to download entirely (thousands of documents).
*
* @return a query that counts the documents in the result set of this
* query. The count can be retrieved from `snapshot.data().count`, where
* `snapshot` is the `AggregateQuerySnapshot` resulting from running the
* returned query.
*/
count() {
return this.aggregate({
count: index_1.AggregateField.count(),
});
}
/**
* Returns a query that can perform the given aggregations.
*
* The returned query, when executed, calculates the specified aggregations
* over the documents in the result set of this query without actually
* downloading the documents.
*
* Using the returned query to perform aggregations is efficient because only
* the final aggregation values, not the documents' data, is downloaded. The
* returned query can perform aggregations of the documents count the
* documents in cases where the result set is prohibitively large to download
* entirely (thousands of documents).
*
* @param aggregateSpec An `AggregateSpec` object that specifies the aggregates
* to perform over the result set. The AggregateSpec specifies aliases for each
* aggregate, which can be used to retrieve the aggregate result.
* @example
* ```typescript
* const aggregateQuery = col.aggregate(query, {
* countOfDocs: count(),
* totalHours: sum('hours'),
* averageScore: average('score')
* });
*
* const aggregateSnapshot = await aggregateQuery.get();
* const countOfDocs: number = aggregateSnapshot.data().countOfDocs;
* const totalHours: number = aggregateSnapshot.data().totalHours;
* const averageScore: number | null = aggregateSnapshot.data().averageScore;
* ```
*/
aggregate(aggregateSpec) {
return new aggregate_query_1.AggregateQuery(this, aggregateSpec);
}
/**
* Returns a query that can perform vector distance (similarity) search with given parameters.
*
* The returned query, when executed, performs a distance (similarity) search on the specified
* `vectorField` against the given `queryVector` and returns the top documents that are closest
* to the `queryVector`.
*
* Only documents whose `vectorField` field is a {@link VectorValue} of the same dimension as `queryVector`
* participate in the query, all other documents are ignored.
*
* @example
* ```
* // Returns the closest 10 documents whose Euclidean distance from their 'embedding' fields are closed to [41, 42].
* const vectorQuery = col.findNearest('embedding', [41, 42], {limit: 10, distanceMeasure: 'EUCLIDEAN'});
*
* const querySnapshot = await aggregateQuery.get();
* querySnapshot.forEach(...);
* ```
*
* @param vectorField - A string or {@link FieldPath} specifying the vector field to search on.
* @param queryVector - The {@link VectorValue} used to measure the distance from `vectorField` values in the documents.
* @param options - Options control the vector query. `limit` specifies the upper bound of documents to return, must
* be a positive integer with a maximum value of 1000. `distanceMeasure` specifies what type of distance is calculated
* when performing the query.
*/
findNearest(vectorField, queryVector, options) {
(0, path_1.validateFieldPath)('vectorField', vectorField);
if (options.limit <= 0) {
throw (0, validate_1.invalidArgumentMessage)('options.limit', 'positive limit number');
}
if ((Array.isArray(queryVector)
? queryVector.length
: queryVector.toArray().length) === 0) {
throw (0, validate_1.invalidArgumentMessage)('queryVector', 'vector size must be larger than 0');
}
return new vector_query_1.VectorQuery(this, vectorField, queryVector, new vector_query_options_1.VectorQueryOptions(options.limit, options.distanceMeasure));
}
/**
* 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._queryOptions.isEqual(other._queryOptions));
}
/**
* Returns the sorted array of inequality filter fields used in this query.
*
* @return An array of inequality filter fields sorted lexicographically by FieldPath.
*/
getInequalityFilterFields() {
const inequalityFields = [];
for (const filter of this._queryOptions.filters) {
for (const subFilter of filter.getFlattenedFilters()) {
if (subFilter.isInequalityFilter()) {
inequalityFields.push(subFilter.field);
}
}
}
return inequalityFields.sort((a, b) => a.compareTo(b));
}
/**
* Computes the backend ordering semantics for DocumentSnapshot cursors.
*
* @private
* @internal
* @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) {
// Add an implicit orderBy if the only cursor value is a DocumentSnapshot.
if (cursorValuesOrDocumentSnapshot.length !== 1 ||
!(cursorValuesOrDocumentSnapshot[0] instanceof index_1.DocumentSnapshot)) {
return this._queryOptions.fieldOrders;
}
const fieldOrders = this._queryOptions.fieldOrders.slice();
const fieldsNormalized = new Set([
...fieldOrders.map(item => item.field.toString()),
]);
/** The order of the implicit ordering always matches the last explicit order by. */
const lastDirection = fieldOrders.length === 0
? constants_1.directionOperators.ASC
: fieldOrders[fieldOrders.length - 1].direction;
/**
* Any inequality fields not explicitly ordered should be implicitly ordered in a
* lexicographical order. When there are multiple inequality filters on the same field, the
* field should be added only once.
* Note: getInequalityFilterFields function sorts the key field before
* other fields. However, we want the key field to be sorted last.
*/
const inequalityFields = this.getInequalityFilterFields();
for (const field of inequalityFields) {
if (!fieldsNormalized.has(field.toString()) &&
!field.isEqual(index_1.FieldPath.documentId())) {
fieldOrders.push(new field_order_1.FieldOrder(field, lastDirection));
fieldsNormalized.add(field.toString());
}
}
// Add the document key field to the last if it is not explicitly ordered.
if (!fieldsNormalized.has(index_1.FieldPath.documentId().toString())) {
fieldOrders.push(new field_order_1.FieldOrder(index_1.FieldPath.documentId(), lastDirection));
}
return fieldOrders;
}
/**
* Builds a Firestore 'Position' proto message.
*
* @private
* @internal
* @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 (cursorValuesOrDocumentSnapshot.length === 1 &&
cursorValuesOrDocumentSnapshot[0] instanceof index_1.DocumentSnapshot) {
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: [], before };
for (let i = 0; i < fieldValues.length; ++i) {
let fieldValue = fieldValues[i];
if (index_1.FieldPath.documentId().isEqual(fieldOrders[i].field)) {
fieldValue = this.validateReference(fieldValue);
}
(0, helpers_1.validateQueryValue)(i, fieldValue, this._allowUndefined);
options.values.push(this._serializer.encodeValue(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
* @internal
*/
validateReference(val) {
const basePath = this._queryOptions.allDescendants
? this._queryOptions.parentPath
: this._queryOptions.parentPath.append(this._queryOptions.collectionId);
let reference;
if (typeof val === 'string') {
const path = basePath.append(val);
if (this._queryOptions.allDescendants) {
if (!path.isDocument) {
throw new Error('When querying a collection group and ordering by ' +
'FieldPath.documentId(), the corresponding value must result in ' +
`a valid document path, but '${val}' is not because it ` +
'contains an odd number of segments.');
}
}
else if (val.indexOf('/') !== -1) {
throw new Error('When querying a collection and ordering by FieldPath.documentId(), ' +
`the corresponding value must be a plain document ID, but '${val}' ` +
'contains a slash.');
}
reference = new document_reference_1.DocumentReference(this._firestore, basePath.append(val), this._queryOptions.converter);
}
else if (val instanceof document_reference_1.DocumentReference) {
reference = val;
if (!basePath.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, but was "${val}".`);
}
if (!this._queryOptions.allDescendants &&
reference._path.parent().compareTo(basePath) !== 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) {
(0, validate_1.validateMinNumberOfArguments)('Query.startAt', fieldValuesOrDocumentSnapshot, 1);
const fieldOrders = this.createImplicitOrderBy(fieldValuesOrDocumentSnapshot);
const startAt = this.createCursor(fieldOrders, fieldValuesOrDocumentSnapshot, true);
const options = this._queryOptions.with({ fieldOrders, startAt });
return new Query(this._firestore, 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) {
(0, validate_1.validateMinNumberOfArguments)('Query.startAfter', fieldValuesOrDocumentSnapshot, 1);
const fieldOrders = this.createImplicitOrderBy(fieldValuesOrDocumentSnapshot);
const startAt = this.createCursor(fieldOrders, fieldValuesOrDocumentSnapshot, false);
const options = this._queryOptions.with({ fieldOrders, startAt });
return new Query(this._firestore, 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) {
(0, validate_1.validateMinNumberOfArguments)('Query.endBefore', fieldValuesOrDocumentSnapshot, 1);
const fieldOrders = this.createImplicitOrderBy(fieldValuesOrDocumentSnapshot);
const endAt = this.createCursor(fieldOrders, fieldValuesOrDocumentSnapshot, true);
const options = this._queryOptions.with({ fieldOrders, endAt });
return new Query(this._firestore, 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) {
(0, validate_1.validateMinNumberOfArguments)('Query.endAt', fieldValuesOrDocumentSnapshot, 1);
const fieldOrders = this.createImplicitOrderBy(fieldValuesOrDocumentSnapshot);
const endAt = this.createCursor(fieldOrders, fieldValuesOrDocumentSnapshot, false);
const options = this._queryOptions.with({ fieldOrders, endAt });
return new Query(this._firestore, 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}`);
* });
* });
* ```
*/
async get() {
const { result } = await this._get();
return result;
}
/**
* Plans and optionally executes this query. Returns a Promise that will be
* resolved with the planner information, statistics from the query execution (if any),
* and the query results (if any).
*
* @return A Promise that will be resolved with the planner information, statistics
* from the query execution (if any), and the query results (if any).
*/
async explain(options) {
if (options === undefined) {
options = {};
}
const { result, explainMetrics } = await this._getResponse(undefined, options);
if (!explainMetrics) {
throw new Error('No explain results');
}
return new query_profile_1.ExplainResults(explainMetrics, result || null);
}
/**
* Internal get() method that accepts an optional transaction options, and
* returns a query snapshot with transaction and explain metadata.
*
* @private
* @internal
* @param transactionOrReadTime A transaction ID, options to start a new
* transaction, or timestamp to use as read time.
*/
async _get(transactionOrReadTime) {
const result = await this._getResponse(transactionOrReadTime);
if (!result.result) {
throw new Error('No QuerySnapshot result');
}
return result;
}
_getResponse(transactionOrReadTime, explainOptions) {
return this._queryUtil._getResponse(this, transactionOrReadTime, true, explainOptions);
}
/**
* 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() {
return this._queryUtil.stream(this);
}
/**
* Executes the query and streams the results as the following object:
* {document?: DocumentSnapshot, metrics?: ExplainMetrics}
*
* The stream surfaces documents one at a time as they are received from the
* server, and at the end, it will surface the metrics associated with
* executing the query.
*
* @example
* ```
* let query = firestore.collection('col').where('foo', '==', 'bar');
*
* let count = 0;
*
* query.explainStream({analyze: true}).on('data', (data) => {
* if (data.document) {
* // Use data.document which is a DocumentSnapshot instance.
* console.log(`Found document with name '${data.document.id}'`);
* ++count;
* }
* if (data.metrics) {
* // Use data.metrics which is an ExplainMetrics instance.
* }
* }).on('end', () => {
* console.log(`Received ${count} documents.`);
* });
* ```
*/
explainStream(explainOptions) {
if (explainOptions === undefined) {
explainOptions = {};
}
if (this._queryOptions.limitType === types_1.LimitType.Last) {
throw new Error('Query results for queries that include limitToLast() ' +
'constraints cannot be streamed. Use Query.explain() instead.');
}
const responseStream = this._stream(undefined, explainOptions);
const transform = new stream_1.Transform({
objectMode: true,
transform(chunk, encoding, callback) {
if (chunk.document || chunk.explainMetrics) {
callback(undefined, {
document: chunk.document,
metrics: chunk.explainMetrics,
});
}
},
});
responseStream.pipe(transform);
responseStream.on('error', e => transform.destroy(e));
return transform;
}
/**
* Converts a QueryCursor to its proto representation.
*
* @param cursor The original cursor value
* @private
* @internal
*/
toCursor(cursor) {
if (cursor) {
return cursor.before
? { before: true, values: cursor.values }
: { values: cursor.values };
}
return undefined;
}
/**
* Internal method for serializing a query to its RunQuery proto
* representation with an optional transaction id or read time.
*
* @param transactionOrReadTime A transaction ID, options to start a new
* transaction, or timestamp to use as read time.
* @param explainOptions Options to use for explaining the query (if any).
* @private
* @internal
* @returns Serialized JSON for the query.
*/
toProto(transactionOrReadTime, explainOptions) {
const projectId = this.firestore.projectId;
const databaseId = this.firestore.databaseId;
const parentPath = this._queryOptions.parentPath.toQualifiedResourcePath(projectId, databaseId);
const structuredQuery = this.toStructuredQuery();
// For limitToLast queries, the structured query has to be translated to a version with
// reversed ordered, and flipped startAt/endAt to work properly.
if (this._queryOptions.limitType === types_1.LimitType.Last) {
if (!this._queryOptions.hasFieldOrders()) {
throw new Error('limitToLast() queries require specifying at least one orderBy() clause.');
}
structuredQuery.orderBy = this._queryOptions.fieldOrders.map(order => {
// Flip the orderBy directions since we want the last results
const dir = order.direction === 'DESCENDING' ? 'ASCENDING' : 'DESCENDING';
return new field_order_1.FieldOrder(order.field, dir).toProto();
});
// Swap the cursors to match the now-flipped query ordering.
structuredQuery.startAt = this._queryOptions.endAt
? this.toCursor({
values: this._queryOptions.endAt.values,
before: !this._queryOptions.endAt.before,
})
: undefined;
structuredQuery.endAt = this._queryOptions.startAt
? this.toCursor({
values: this._queryOptions.startAt.values,
before: !this._queryOptions.startAt.before,
})
: undefined;
}
const runQueryRequest = {
parent: parentPath.formattedName,
structuredQuery,
};
if (transactionOrReadTime instanceof Uint8Array) {
runQueryRequest.transaction = transactionOrReadTime;
}
else if (transactionOrReadTime instanceof index_1.Timestamp) {
runQueryRequest.readTime = transactionOrReadTime.toProto().timestampValue;
}
else if (transactionOrReadTime) {
runQueryRequest.newTransaction = transactionOrReadTime;
}
if (explainOptions) {
runQueryRequest.explainOptions = explainOptions;
}
return runQueryRequest;
}
/**
* Converts current Query to an IBundledQuery.
*
* @private
* @internal
*/
_toBundledQuery() {
const projectId = this.firestore.projectId;
const databaseId = this.firestore.databaseId;
const parentPath = this._queryOptions.parentPath.toQualifiedResourcePath(projectId, databaseId);
const structuredQuery = this.toStructuredQuery();
const bundledQuery = {
parent: parentPath.formattedName,
structuredQuery,
};
if (this._queryOptions.limitType === types_1.LimitType.First) {
bundledQuery.limitType = 'FIRST';
}
else if (this._queryOptions.limitType === types_1.LimitType.Last) {
bundledQuery.limitType = 'LAST';
}
return bundledQuery;
}
toStructuredQuery() {
const structuredQuery = {
from: [{}],
};
if (this._queryOptions.allDescendants) {
structuredQuery.from[0].allDescendants = true;
}
// Kindless queries select all descendant documents, so we remove the
// collectionId field.
if (!this._queryOptions.kindless) {
structuredQuery.from[0].collectionId = this._queryOptions.collectionId;
}
if (this._queryOptions.filters.length >= 1) {
structuredQuery.where = new composite_filter_internal_1.CompositeFilterInternal(this._queryOptions.filters, 'AND').toProto();
}
if (this._queryOptions.hasFieldOrders()) {
structuredQuery.orderBy = this._queryOptions.fieldOrders.map(o => o.toProto());
}
structuredQuery.startAt = this.toCursor(this._queryOptions.startAt);
structuredQuery.endAt = this.toCursor(this._queryOptions.endAt);
if (this._queryOptions.limit) {
structuredQuery.limit = { value: this._queryOptions.limit };
}
structuredQuery.offset = this._queryOptions.offset;
structuredQuery.select = this._queryOptions.projection;
return structuredQuery;
}
/**
* @internal
* @private
* This method exists solely to maintain backward compatability.
*/
_isPermanentRpcError(err, methodName) {
return this._queryUtil._isPermanentRpcError(err, methodName);
}
/**
* @internal
* @private
* This method exists solely to maintain backward compatability.
*/
_hasRetryTimedOut(methodName, startTime) {
return this._queryUtil._hasRetryTimedOut(methodName, startTime);
}
/**
* Internal streaming method that accepts an optional transaction ID.
*
* BEWARE: If `transactionOrReadTime` is `ITransactionOptions`, then the first
* response in the stream will be a transaction response.
*
* @param transactionOrReadTime A transaction ID, options to start a new
* transaction, or timestamp to use as read time.
* @param explainOptions Options to use for explaining the query (if any).
* @private
* @internal
* @returns A stream of document results, optionally preceded by a transaction response.
*/
_stream(transactionOrReadTime, explainOptions) {
return this._queryUtil._stream(this, transactionOrReadTime, true, explainOptions);
}
/**
* Attaches a listener for QuerySnapshot events.
*
* @param {querySnapshotCallback} onNext A callback to be called every time
* a new [QuerySnapshot]{@link QuerySnapshot} is available.
* @param {errorCallback=} onError A callback to be called if the listen
* fails or is cancelled. No further callbacks will occur.
*
* @returns {function()} An unsubscribe function that can be called to cancel
* the snapshot listener.
*
* @example
* ```
* let query = firestore.collection('col').where('foo', '==', 'bar');
*
* let unsubscribe = query.onSnapshot(querySnapshot => {
* console.log(`Received query snapshot of size ${querySnapshot.size}`);
* }, err => {
* console.log(`Encountered error: ${err}`);
* });
*
* // Remove this listener.
* unsubscribe();
* ```
*/
onSnapshot(onNext, onError) {
(0, validate_1.validateFunction)('onNext', onNext);
(0, validate_1.validateFunction)('onError', onError, { optional: true });
const watch = new (require('../watch').QueryWatch)(this.firestore, this, this._queryOptions.converter);
return watch.onSnapshot((readTime, size, docs, changes) => {
onNext(new query_snapshot_1.QuerySnapshot(this, readTime, size, docs, changes));
}, onError || console.error);
}
/**
* Returns a function that can be used to sort QueryDocumentSnapshots
* according to the sort criteria of this query.
*
* @private
* @internal
*/
comparator() {
return (doc1, doc2) => {
// Add implicit sorting by name, using the last specified direction.
const lastDirection = this._queryOptions.hasFieldOrders()
? this._queryOptions.fieldOrders[this._queryOptions.fieldOrders.length - 1].direction
: 'ASCENDING';
const orderBys = this._queryOptions.fieldOrders.concat(new field_order_1.FieldOrder(index_1.FieldPath.documentId(), lastDirection));
for (const orderBy of orderBys) {
let comp;
if (index_1.FieldPath.documentId().isEqual(orderBy.field)) {
comp = doc1.ref._path.compareTo(doc2.ref._path);
}
else {
const v1 = doc1.protoField(orderBy.field);
const v2 = doc2.protoField(orderBy.field);
if (v1 === undefined || v2 === undefined) {
throw new Error('Trying to compare documents on fields that ' +
"don't exist. Please include the fields you are ordering on " +
'in your select() call.');
}
comp = (0, order_1.compare)(v1, v2);
}
if (comp !== 0) {
const direction = orderBy.direction === 'ASCENDING' ? 1 : -1;
return direction * comp;
}
}
return 0;
};
}
/**
* Applies a custom data converter to this Query, allowing you to use your
* own custom model objects with Firestore. When you call get() on the
* returned Query, the provided converter will convert between Firestore
* data of type `NewDbModelType` and your custom type `NewAppModelType`.
*
* Using the converter allows you to specify generic type arguments when
* storing and retrieving objects from Firestore.
*
* Passing in `null` as the converter parameter removes the current
* converter.
*
* @example
* ```
* class Post {
* constructor(readonly title: string, readonly author: string) {}
*
* toString(): string {
* return this.title + ', by ' + this.author;
* }
* }
*
* const postConverter = {
* toFirestore(post: Post): FirebaseFirestore.DocumentData {
* return {title: post.title, author: post.author};
* },
* fromFirestore(
* snapshot: FirebaseFirestore.QueryDocumentSnapshot
* ): Post {
* const data = snapshot.data();
* return new Post(data.title, data.author);
* }
* };
*
* const postSnap = await Firestore()
* .collection('posts')
* .withConverter(postConverter)
* .doc().get();
* const post = postSnap.data();
* if (post !== undefined) {
* post.title; // string
* post.toString(); // Should be defined
* post.someNonExistentProperty; // TS error
* }
*
* ```
* @param {FirestoreDataConverter | null} converter Converts objects to and
* from Firestore. Passing in `null` removes the current converter.
* @return A Query that uses the provided converter.
*/
withConverter(converter) {
return new Query(this.firestore, this._queryOptions.withConverter(converter !== null && converter !== void 0 ? converter : (0, types_2.defaultConverter)()));
}
/**
* Construct the resulting snapshot for this query with given documents.
*
* @private
* @internal
*/
_createSnapshot(readTime, size, docs, changes) {
return new query_snapshot_1.QuerySnapshot(this, readTime, size, docs, changes);
}
}
exports.Query = Query;
//# sourceMappingURL=query.js.map