diff --git a/dev/src/reference.ts b/dev/src/reference.ts index 1bd33822d..b5cac033a 100644 --- a/dev/src/reference.ts +++ b/dev/src/reference.ts @@ -1599,26 +1599,24 @@ export class Query implements firestore.Query { } /** - * Returns an `AggregateQuery` that counts the number of documents in the - * result set. + * Returns a query that counts the documents in the result set of this + * query. * - * @return an `AggregateQuery` that counts the number of documents in the - * result set. - */ - count(): AggregateQuery<{count: firestore.AggregateField}> { - return this._aggregate({count: AggregateField.count()}); - } - - /** - * Returns an `AggregateQuery` that performs the given aggregations. + * The returned query, when executed, counts the documents in the result set + * of this query without actually downloading the documents. * - * @param aggregates the aggregations to perform. - * @return an `AggregateQuery` that performs the given aggregations. + * 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 even count the documents if the result set would be + * prohibitively large to download entirely (e.g. 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. */ - private _aggregate( - aggregates: T - ): AggregateQuery { - return new AggregateQuery(this, aggregates); + count(): AggregateQuery<{count: firestore.AggregateField}> { + return new AggregateQuery(this, {count: {}}); } /** @@ -2854,33 +2852,36 @@ export class CollectionReference } } -export class AggregateField implements firestore.AggregateField { - private constructor() {} - - static count(): AggregateField { - return new AggregateField(); - } - - isEqual(other: firestore.AggregateField): boolean { - return this === other || other instanceof AggregateField; - } -} - +/** + * A query that calculates aggregations over an underlying query. + */ export class AggregateQuery implements firestore.AggregateQuery { - private readonly _query: Query; - private readonly _aggregates: T; - - constructor(query: Query, aggregates: T) { - this._query = query; - this._aggregates = aggregates; - } + /** + * @private + * @internal + * + * @param _query The query whose aggregations will be calculated by this + * object. + * @param _aggregates The aggregations that will be performed by this query. + */ + constructor( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private readonly _query: Query, + private readonly _aggregates: T + ) {} + /** The query whose aggregations will be calculated by this object. */ get query(): firestore.Query { return this._query; } + /** + * Executes this query. + * + * @return A promise that will be resolved with the results of the query. + */ get(): Promise> { return this._get(); } @@ -2914,9 +2915,9 @@ export class AggregateQuery /** * Internal streaming method that accepts an optional transaction ID. * - * @param transactionId A transaction ID. * @private * @internal + * @param transactionId A transaction ID. * @returns A stream of document results. */ _stream(transactionId?: Uint8Array): Readable { @@ -2997,13 +2998,12 @@ export class AggregateQuery /** * Internal method to decode values within result. - * - * @param proto * @private */ private decodeResult( proto: api.IAggregationResult ): firestore.AggregateSpecData { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const data: any = {}; const fields = proto.aggregateFields; if (fields) { @@ -3030,7 +3030,8 @@ export class AggregateQuery */ toProto(transactionId?: Uint8Array): api.IRunAggregationQueryRequest { const queryProto = this._query.toProto(); - //TODO(tomandersen) inspect _query to build request - this is just hard coded count right now. + //TODO(tomandersen) inspect _query to build request - this is just hard + // coded count right now. const runQueryRequest: api.IRunAggregationQueryRequest = { parent: queryProto.parent, structuredAggregationQuery: { @@ -3051,76 +3052,104 @@ export class AggregateQuery return runQueryRequest; } + /** + * Compares this object with the given object for equality. + * + * This object is considered "equal" to the other object if and only if + * `other` performs the same aggregations as this `AggregateQuery` and + * the underlying Query of `other` compares equal to that of this object + * using `Query.isEqual()`. + * + * @param other The object to compare to this object for equality. + * @return `true` if this object is "equal" to the given object, as + * defined above, or `false` otherwise. + */ isEqual(other: firestore.AggregateQuery): boolean { if (this === other) { return true; } - if (other instanceof AggregateQuery) { - if (!this._query.isEqual(other._query)) { - return false; - } - - const thisAggregates: [string, AggregateField][] = - Object.entries(this._aggregates); - const otherAggregates = other._aggregates; - - return ( - thisAggregates.length === Object.keys(otherAggregates).length && - thisAggregates.every(([alias, field]) => - field.isEqual(otherAggregates[alias]) - ) - ); + if (!(other instanceof AggregateQuery)) { + return false; } - return false; + if (!this.query.isEqual(other.query)) { + return false; + } + return deepEqual(this._aggregates, other._aggregates); } } +/** + * The results of executing an aggregation query. + */ export class AggregateQuerySnapshot implements firestore.AggregateQuerySnapshot { + /** + * @private + * @internal + * + * @param _query The query that was executed to produce this result. + * @param _readTime The time this snapshot was read. + * @param _data The results of the aggregations performed over the underlying + * query. + */ constructor( private readonly _query: AggregateQuery, private readonly _readTime: Timestamp, private readonly _data: firestore.AggregateSpecData ) {} + /** The query that was executed to produce this result. */ get query(): firestore.AggregateQuery { return this._query; } + /** The time this snapshot was read. */ get readTime(): firestore.Timestamp { return this._readTime; } + /** + * Returns the results of the aggregations performed over the underlying + * query. + * + * The keys of the returned object will be the same as those of the + * `AggregateSpec` object specified to the aggregation method, and the + * values will be the corresponding aggregation result. + * + * @returns The results of the aggregations performed over the underlying + * query. + */ + data(): firestore.AggregateSpecData { + return this._data; + } + + /** + * Compares this object with the given object for equality. + * + * Two `AggregateQuerySnapshot` instances are considered "equal" if they + * have the same data and their underlying queries compare "equal" using + * `AggregateQuery.isEqual()`. + * + * @param other The object to compare to this object for equality. + * @return `true` if this object is "equal" to the given object, as + * defined above, or `false` otherwise. + */ isEqual(other: firestore.AggregateQuerySnapshot): boolean { if (this === other) { return true; } - if (other instanceof AggregateQuerySnapshot) { - if (!this._query.isEqual(other._query)) { - return false; - } - - const thisData = this._data; - const thisDataKeys: string[] = Object.keys(thisData); - - const otherData = other._data; - const otherDataKeys: string[] = Object.keys(otherData); - - return ( - thisDataKeys.length === otherDataKeys.length && - thisDataKeys.every( - alias => - Object.prototype.hasOwnProperty.call(otherData, alias) && - thisData[alias] === otherData[alias] - ) - ); + if (!(other instanceof AggregateQuerySnapshot)) { + return false; + } + // Since the read time is different on every read, we explicitly ignore all + // document metadata in this comparison, just like + // `DocumentSnapshot.isEqual()` does. + if (!this.query.isEqual(other.query)) { + return false; } - return false; - } - data(): firestore.AggregateSpecData { - return this._data; + return deepEqual(this._data, other._data); } } diff --git a/types/firestore.d.ts b/types/firestore.d.ts index d84dcc054..2c5ceaf7c 100644 --- a/types/firestore.d.ts +++ b/types/firestore.d.ts @@ -21,7 +21,7 @@ // Declare a global (ambient) namespace // (used when not using import statement, but just script include). declare namespace FirebaseFirestore { - // Alias for `any` but used where a Firestore field value would be provided. + /** Alias for `any` but used where a Firestore field value would be provided. */ export type DocumentFieldValue = any; /** @@ -1707,9 +1707,21 @@ declare namespace FirebaseFirestore { ): () => void; /** - * Returns count `AggregateQuery` based on this `Query`. + * 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 even count the documents if the result set would be + * prohibitively large to download entirely (e.g. thousands of documents). * - * @return AggregateQuery that contains count aggregate. + * @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(): AggregateQuery<{count: AggregateField}>; @@ -2034,94 +2046,100 @@ declare namespace FirebaseFirestore { } /** - * An `AggregateField`that captures input type T. + * Represents an aggregation that can be performed by Firestore. */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars export class AggregateField { private constructor(); - - /** - * Creates and returns an aggregation field that counts the documents in the result set. - * @returns An `AggregateField` object with number input type. - */ - static count(): AggregateField; - - /** - * Returns true if the aggregate function in this `AggregateField` is equal to the - * provided one. - * - * @param other The `AggregateField` to compare against. - * @return true if this `AggregateField` is equal to the provided one. - */ - isEqual(other: AggregateField): boolean; } /** - * The union of all `AggregateField` types that are returned from the factory - * functions. + * The union of all `AggregateField` types that are supported by Firestore. */ - export type AggregateFieldType = ReturnType; + export type AggregateFieldType = AggregateField; /** - * A type whose values are all `AggregateField` objects. - * This is used as an argument to the "getter" functions, and the snapshot will - * map the same names to the corresponding values. + * A type whose property values are all `AggregateField` objects. */ export interface AggregateSpec { [field: string]: AggregateFieldType; } /** - * A type whose keys are taken from an `AggregateSpec` type, and whose values - * are the result of the aggregation performed by the corresponding + * A type whose keys are taken from an `AggregateSpec`, and whose values are + * the result of the aggregation performed by the corresponding * `AggregateField` from the input `AggregateSpec`. */ export type AggregateSpecData = { [P in keyof T]: T[P] extends AggregateField ? U : never; }; + /** + * A query that calculates aggregations over an underlying query. + */ export class AggregateQuery { private constructor(); + /** The query whose aggregations will be calculated by this object. */ readonly query: Query; + /** + * Executes this query. + * + * @return A promise that will be resolved with the results of the query. + */ get(): Promise>; /** - * Returns true if the query and aggregates in this `AggregateQuery` is equal to the - * provided one. + * Compares this object with the given object for equality. + * + * This object is considered "equal" to the other object if and only if + * `other` performs the same aggregations as this `AggregateQuery` and + * the underlying Query of `other` compares equal to that of this object + * using `Query.isEqual()`. * - * @param other The `AggregateQuery` to compare against. - * @return true if this `AggregateQuery` is equal to the provided one. + * @param other The object to compare to this object for equality. + * @return `true` if this object is "equal" to the given object, as + * defined above, or `false` otherwise. */ isEqual(other: AggregateQuery): boolean; } /** - * An `AggregateQuerySnapshot` contains the results of running an aggregate query. + * The results of executing an aggregation query. */ export class AggregateQuerySnapshot { private constructor(); + /** The query that was executed to produce this result. */ readonly query: AggregateQuery; + /** The time this snapshot was read. */ readonly readTime: Timestamp; /** - * The results of the requested aggregations. The keys of the returned object - * will be the same as those of the `AggregateSpec` object specified to the - * aggregation method, and the values will be the corresponding aggregation - * result. + * Returns the results of the aggregations performed over the underlying + * query. + * + * The keys of the returned object will be the same as those of the + * `AggregateSpec` object specified to the aggregation method, and the + * values will be the corresponding aggregation result. * - * @returns The aggregation statistics result of running a query. + * @returns The results of the aggregations performed over the underlying + * query. */ data(): AggregateSpecData; /** - * Returns true if the query, read time, and data in this `AggregateQuerySnapshot` - * is equal to the provided one. + * Compares this object with the given object for equality. + * + * Two `AggregateQuerySnapshot` instances are considered "equal" if they + * have the same data and their underlying queries compare "equal" using + * `AggregateQuery.isEqual()`. * - * @param other The `AggregateQuerySnapshot` to compare against. - * @return true if this `AggregateQuerySnapshot` is equal to the provided one. + * @param other The object to compare to this object for equality. + * @return `true` if this object is "equal" to the given object, as + * defined above, or `false` otherwise. */ isEqual(other: AggregateQuerySnapshot): boolean; }