Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mila/count update api surface #6589

Merged
merged 13 commits into from
Sep 13, 2022
3 changes: 2 additions & 1 deletion packages/firebase/compat/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8227,11 +8227,12 @@ declare namespace firebase.storage {
}

declare namespace firebase.firestore {
export type DocumentFieldValue = any;
dconeybe marked this conversation as resolved.
Show resolved Hide resolved
/**
* Document data (for use with `DocumentReference.set()`) consists of fields
* mapped to values.
*/
export type DocumentData = { [field: string]: any };
export type DocumentData = { [field: string]: DocumentFieldValue };

/**
* Update data (for use with `DocumentReference.update()`) consists of field
Expand Down
4 changes: 3 additions & 1 deletion packages/firestore-types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@

import { EmulatorMockTokenOptions } from '@firebase/util';

export type DocumentData = { [field: string]: any };
export type DocumentFieldValue = any;

export type DocumentData = { [field: string]: DocumentFieldValue };

export type UpdateData = { [fieldPath: string]: any };

Expand Down
9 changes: 4 additions & 5 deletions packages/firestore/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,11 @@
*/

export {
AggregateQuery,
AggregateField,
AggregateSpec,
AggregateSpecData,
AggregateQuerySnapshot,
aggregateQueryEqual,
aggregateQuerySnapshotEqual,
countQuery,
getAggregateFromServerDirect
getCountFromServer
} from './api/aggregate';

export { FieldPath, documentId } from './api/field_path';
Expand Down
51 changes: 39 additions & 12 deletions packages/firestore/src/api/aggregate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,52 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { firestoreClientRunAggregationQuery } from '../core/firestore_client';
import { AggregateQuery, AggregateQuerySnapshot } from '../lite-api/aggregate';
import { Query, SnapshotOptions } from '../api';
import { firestoreClientRunCountQuery } from '../core/firestore_client';
import {
AggregateField,
AggregateQuerySnapshot as AggregateQuerySnapshotLite,
dconeybe marked this conversation as resolved.
Show resolved Hide resolved
AggregateSpec,
AggregateSpecData
} from '../lite-api/aggregate';
import { cast } from '../util/input_validation';

import { ensureFirestoreConfigured, Firestore } from './database';

export {
AggregateQuery,
AggregateField,
AggregateSpec,
AggregateSpecData,
AggregateQuerySnapshot,
aggregateQueryEqual,
aggregateQuerySnapshotEqual,
countQuery
aggregateSnapshotEqual,
aggregateFieldEqual
} from '../lite-api/aggregate';

export function getAggregateFromServerDirect(
query: AggregateQuery
): Promise<AggregateQuerySnapshot> {
const firestore = cast(query.query.firestore, Firestore);
/**
* A `AggregateQuerySnapshot` contains the result of running an aggregation query.
* The result data can be extracted with `.data()`.
*/
class AggregateQuerySnapshot<
dconeybe marked this conversation as resolved.
Show resolved Hide resolved
T extends AggregateSpec
> extends AggregateQuerySnapshotLite<T> {
constructor(query: Query<unknown>, _data: AggregateSpecData<T>) {
dconeybe marked this conversation as resolved.
Show resolved Hide resolved
dconeybe marked this conversation as resolved.
Show resolved Hide resolved
super(query, _data);
}
data(options: SnapshotOptions = {}): AggregateSpecData<T> {
dconeybe marked this conversation as resolved.
Show resolved Hide resolved
return super.data();
}
}

/**
* Executes the query and returns the results as a `QuerySnapshot` from the
dconeybe marked this conversation as resolved.
Show resolved Hide resolved
* server. Returns an error if the network is not available.
*
* @returns A `Promise` that will be resolved with the results of the query.
*/
export function getCountFromServer(
query: Query<unknown>
): Promise<AggregateQuerySnapshot<{ count: AggregateField<number> }>> {
const firestore = cast(query.firestore, Firestore);
const client = ensureFirestoreConfigured(firestore);
return firestoreClientRunAggregationQuery(client, query);
return firestoreClientRunCountQuery(client, query);
dconeybe marked this conversation as resolved.
Show resolved Hide resolved
}
23 changes: 15 additions & 8 deletions packages/firestore/src/core/firestore_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,19 @@

import { GetOptions } from '@firebase/firestore-types';

import { AggregateQuery, AggregateQuerySnapshot } from '../api';
import {
AggregateField,
AggregateSpec,
dconeybe marked this conversation as resolved.
Show resolved Hide resolved
AggregateQuerySnapshot
} from '../api/aggregate';
import { LoadBundleTask } from '../api/bundle';
import {
CredentialChangeListener,
CredentialsProvider
} from '../api/credentials';
import { User } from '../auth/user';
import { getAggregate } from '../lite-api/aggregate';
import { getCountFromServer } from '../lite-api/aggregate';
import { Query as LiteQuery } from '../lite-api/reference';
import { LocalStore } from '../local/local_store';
import {
localStoreExecuteQuery,
Expand Down Expand Up @@ -504,23 +509,25 @@ export function firestoreClientTransaction<T>(
return deferred.promise;
}

export function firestoreClientRunAggregationQuery(
export function firestoreClientRunCountQuery(
client: FirestoreClient,
query: AggregateQuery
): Promise<AggregateQuerySnapshot> {
const deferred = new Deferred<AggregateQuerySnapshot>();
query: LiteQuery<unknown>
): Promise<AggregateQuerySnapshot<{ count: AggregateField<number> }>> {
const deferred = new Deferred<
AggregateQuerySnapshot<{ count: AggregateField<number> }>
>();
client.asyncQueue.enqueueAndForget(async () => {
const remoteStore = await getRemoteStore(client);
if (!canUseNetwork(remoteStore)) {
deferred.reject(
new FirestoreError(
Code.UNAVAILABLE,
'Failed to get aggregate result because the client is offline.'
'Failed to get count result because the client is offline.'
)
);
} else {
try {
const result = await getAggregate(query);
const result = await getCountFromServer(query);
deferred.resolve(result);
} catch (e) {
deferred.reject(e as Error);
Expand Down
149 changes: 96 additions & 53 deletions packages/firestore/src/lite-api/aggregate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,72 +15,113 @@
* limitations under the License.
*/

import { deepEqual } from '@firebase/util';

import { Value } from '../protos/firestore_proto_api';
import { invokeRunAggregationQueryRpc } from '../remote/datastore';
import { hardAssert } from '../util/assert';
import { cast } from '../util/input_validation';

import { getDatastore } from './components';
import { Firestore } from './database';
import { Query, queryEqual } from './reference';
import { DocumentFieldValue, Query, queryEqual } from './reference';
import { LiteUserDataWriter } from './reference_impl';

/**
* An `AggregateQuery` computes some aggregation statistics from the result set of
* a base `Query`.
* An `AggregateField` computes some aggregation statistics from the result set of
* an aggregation query.
dconeybe marked this conversation as resolved.
Show resolved Hide resolved
*/
export class AggregateQuery {
readonly type = 'AggregateQuery';
/**
* The query on which you called `countQuery` in order to get this `AggregateQuery`.
*/
readonly query: Query<unknown>;
export class AggregateField<T> {
type = 'AggregateField';
_datum?: T;
dconeybe marked this conversation as resolved.
Show resolved Hide resolved

/** @hideconstructor */
constructor(query: Query<unknown>) {
this.query = query;
}
constructor(readonly subType: string) {}
dconeybe marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* The union of all `AggregateField` types that are returned from the factory
* functions.
*/
export type AggregateFieldType =
dconeybe marked this conversation as resolved.
Show resolved Hide resolved
| AggregateField<number>
| AggregateField<DocumentFieldValue | undefined>
| AggregateField<number | undefined>;

/**
* 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.
*/
export type AggregateSpec = { [field: string]: AggregateFieldType };
dconeybe marked this conversation as resolved.
Show resolved Hide resolved

/**
* A type whose keys are taken from an `AggregateSpec` type, and whose values
* are the result of the aggregation performed by the corresponding
* `AggregateField` from the input `AggregateSpec`.
*/
export type AggregateSpecData<T extends AggregateSpec> = {
[Property in keyof T]-?: T[Property]['_datum'];
};

/**
* Creates and returns an aggregation field that counts the documents in the result set.
* @returns An `AggregateField` object that includes number of documents.
*/
export function count(): AggregateField<number> {
return new AggregateField<number>('count');
}

/**
* An `AggregateQuerySnapshot` contains results of a `AggregateQuery`.
* Compares two `AggregateField` instances for equality.
* The two `AggregateField` instances are considered "equal" if and only if
* they were created by the same factory function (e.g. `count()`, `min()`, and
* `sum()`) with "equal" arguments.
*/
export class AggregateQuerySnapshot {
export function aggregateFieldEqual(
dconeybe marked this conversation as resolved.
Show resolved Hide resolved
left: AggregateField<unknown>,
right: AggregateField<unknown>
): boolean {
return typeof left === typeof right && left.subType === right.subType;
}

/**
* An `AggregateQuerySnapshot` contains the results of running an aggregate query.
*/
export class AggregateQuerySnapshot<T extends AggregateSpec> {
readonly type = 'AggregateQuerySnapshot';
readonly query: AggregateQuery;

/** @hideconstructor */
constructor(query: AggregateQuery, private readonly _count: number) {
this.query = query;
}
constructor(
readonly query: Query<unknown>,
protected readonly _data: AggregateSpecData<T>
dconeybe marked this conversation as resolved.
Show resolved Hide resolved
) {}

/**
* @returns The result of a document count aggregation. Returns null if no count aggregation is
* available in the result.
* 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.
*/
getCount(): number | null {
return this._count;
data(): AggregateSpecData<T> {
return this._data;
}
}

/**
* Creates an `AggregateQuery` counting the number of documents matching this query.
* Counts the number of documents in the result set of the given query, ignoring
* any locally-cached data and any locally-pending writes and simply surfacing
* whatever the server returns. If the server cannot be reached then the
* returned promise will be rejected.
*
* @returns An `AggregateQuery` object that can be used to count the number of documents in
* the result set of this query.
* This is a convenience shorthand for:
dconeybe marked this conversation as resolved.
Show resolved Hide resolved
* getAggregateFromServer(query, { count: count() }).
*/
export function countQuery(query: Query<unknown>): AggregateQuery {
return new AggregateQuery(query);
}

export function getAggregate(
query: AggregateQuery
): Promise<AggregateQuerySnapshot> {
const firestore = cast(query.query.firestore, Firestore);
export function getCountFromServer(
query: Query<unknown>
): Promise<AggregateQuerySnapshot<{ count: AggregateField<number> }>> {
const firestore = cast(query.firestore, Firestore);
const datastore = getDatastore(firestore);
const userDataWriter = new LiteUserDataWriter(firestore);

return invokeRunAggregationQueryRpc(datastore, query).then(result => {
return invokeRunAggregationQueryRpc(datastore, query._query).then(result => {
hardAssert(
result[0] !== undefined,
'Aggregation fields are missing from result.'
Expand All @@ -90,29 +131,31 @@ export function getAggregate(
.filter(([key, value]) => key === 'count_alias')
.map(([key, value]) => userDataWriter.convertValue(value as Value));

const count = counts[0];
const countValue = counts[0];

hardAssert(
typeof count === 'number',
'Count aggeragte field value is not a number: ' + count
typeof countValue === 'number',
'Count aggregate field value is not a number: ' + countValue
);

return Promise.resolve(new AggregateQuerySnapshot(query, count));
return Promise.resolve(
new AggregateQuerySnapshot<{ count: AggregateField<number> }>(query, {
count: countValue
})
);
});
}

export function aggregateQueryEqual(
left: AggregateQuery,
right: AggregateQuery
): boolean {
return queryEqual(left.query, right.query);
}

export function aggregateQuerySnapshotEqual(
left: AggregateQuerySnapshot,
right: AggregateQuerySnapshot
/**
* Compares two `AggregateQuerySnapshot` instances for equality.
* Two `AggregateQuerySnapshot` instances are considered "equal" if they have
* the same underlying query, the same metadata, and the same data.
*/
export function aggregateSnapshotEqual<T extends AggregateSpec>(
left: AggregateQuerySnapshot<T>,
right: AggregateQuerySnapshot<T>
): boolean {
return (
aggregateQueryEqual(left.query, right.query) &&
left.getCount() === right.getCount()
queryEqual(left.query, right.query) && deepEqual(left.data(), right.data())
);
}
6 changes: 4 additions & 2 deletions packages/firestore/src/lite-api/reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,16 @@ import { FieldValue } from './field_value';
import { FirestoreDataConverter } from './snapshot';
import { NestedUpdateFields, Primitive } from './types';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type DocumentFieldValue = any;

/**
* Document data (for use with {@link @firebase/firestore/lite#(setDoc:1)}) consists of fields mapped to
* values.
*/
export interface DocumentData {
/** A mapping between a field and its value. */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[field: string]: any;
[field: string]: DocumentFieldValue;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/firestore/src/remote/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export interface Connection {
* request message must include the path. If false, then the request message must NOT
* include the path.
*/
get shouldResourcePathBeIncludedInRequest(): boolean;
readonly shouldResourcePathBeIncludedInRequest: boolean;
dconeybe marked this conversation as resolved.
Show resolved Hide resolved

// TODO(mcg): subscribe to connection state changes.
}
Expand Down
Loading