Skip to content

Commit 0ebab69

Browse files
authored
Mila/count update api surface (#6589)
1 parent 98189d5 commit 0ebab69

File tree

15 files changed

+251
-247
lines changed

15 files changed

+251
-247
lines changed

packages/firebase/compat/index.d.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8227,11 +8227,15 @@ declare namespace firebase.storage {
82278227
}
82288228

82298229
declare namespace firebase.firestore {
8230+
8231+
/** Alias dynamic document field value types to any */
8232+
export type DocumentFieldValue = any;
8233+
82308234
/**
82318235
* Document data (for use with `DocumentReference.set()`) consists of fields
82328236
* mapped to values.
82338237
*/
8234-
export type DocumentData = { [field: string]: any };
8238+
export type DocumentData = { [field: string]: DocumentFieldValue };
82358239

82368240
/**
82378241
* Update data (for use with `DocumentReference.update()`) consists of field

packages/firestore-types/index.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717

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

20-
export type DocumentData = { [field: string]: any };
20+
export type DocumentFieldValue = any;
21+
22+
export type DocumentData = { [field: string]: DocumentFieldValue };
2123

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

packages/firestore/lite/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ registerFirestore();
2929

3030
export { FirestoreSettings as Settings } from '../src/lite-api/settings';
3131

32+
33+
export {
34+
AggregateField,
35+
AggregateSpec,
36+
AggregateSpecData,
37+
AggregateQuerySnapshot,
38+
getCount,
39+
aggregateQuerySnapshotEqual
40+
} from '../src/lite-api/aggregate';
41+
3242
export {
3343
Firestore as Firestore,
3444
EmulatorMockTokenOptions,

packages/firestore/src/api.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@
1616
*/
1717

1818
export {
19-
AggregateQuery,
19+
AggregateField,
20+
AggregateSpec,
21+
AggregateSpecData,
2022
AggregateQuerySnapshot,
21-
aggregateQueryEqual,
22-
aggregateQuerySnapshotEqual,
23-
countQuery,
24-
getAggregateFromServerDirect
23+
getCountFromServer,
24+
aggregateQuerySnapshotEqual
2525
} from './api/aggregate';
2626

2727
export { FieldPath, documentId } from './api/field_path';

packages/firestore/src/api/aggregate.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,33 @@
1414
* See the License for the specific language governing permissions and
1515
* limitations under the License.
1616
*/
17-
18-
import { firestoreClientRunAggregationQuery } from '../core/firestore_client';
19-
import { AggregateQuery, AggregateQuerySnapshot } from '../lite-api/aggregate';
17+
import { Query } from '../api';
18+
import { firestoreClientRunCountQuery } from '../core/firestore_client';
19+
import { AggregateField, AggregateQuerySnapshot } from '../lite-api/aggregate';
2020
import { cast } from '../util/input_validation';
2121

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

2424
export {
25-
AggregateQuery,
25+
AggregateField,
26+
AggregateSpec,
27+
AggregateSpecData,
2628
AggregateQuerySnapshot,
27-
aggregateQueryEqual,
28-
aggregateQuerySnapshotEqual,
29-
countQuery
29+
aggregateQuerySnapshotEqual
3030
} from '../lite-api/aggregate';
3131

32-
export function getAggregateFromServerDirect(
33-
query: AggregateQuery
34-
): Promise<AggregateQuerySnapshot> {
35-
const firestore = cast(query.query.firestore, Firestore);
32+
/**
33+
* Executes the query and returns the results as a `AggregateQuerySnapshot` from the
34+
* server. Returns an error if the network is not available.
35+
*
36+
* @param query - The `Query` to execute.
37+
*
38+
* @returns A `Promise` that will be resolved with the results of the query.
39+
*/
40+
export function getCountFromServer(
41+
query: Query<unknown>
42+
): Promise<AggregateQuerySnapshot<{ count: AggregateField<number> }>> {
43+
const firestore = cast(query.firestore, Firestore);
3644
const client = ensureFirestoreConfigured(firestore);
37-
return firestoreClientRunAggregationQuery(client, query);
45+
return firestoreClientRunCountQuery(client, query);
3846
}

packages/firestore/src/core/firestore_client.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@
1717

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

20-
import { AggregateQuery, AggregateQuerySnapshot } from '../api';
20+
import { AggregateField, AggregateQuerySnapshot } from '../api/aggregate';
2121
import { LoadBundleTask } from '../api/bundle';
2222
import {
2323
CredentialChangeListener,
2424
CredentialsProvider
2525
} from '../api/credentials';
2626
import { User } from '../auth/user';
27-
import { getAggregate } from '../lite-api/aggregate';
27+
import { getCount } from '../lite-api/aggregate';
28+
import { Query as LiteQuery } from '../lite-api/reference';
2829
import { LocalStore } from '../local/local_store';
2930
import {
3031
localStoreExecuteQuery,
@@ -504,23 +505,25 @@ export function firestoreClientTransaction<T>(
504505
return deferred.promise;
505506
}
506507

507-
export function firestoreClientRunAggregationQuery(
508+
export function firestoreClientRunCountQuery(
508509
client: FirestoreClient,
509-
query: AggregateQuery
510-
): Promise<AggregateQuerySnapshot> {
511-
const deferred = new Deferred<AggregateQuerySnapshot>();
510+
query: LiteQuery<unknown>
511+
): Promise<AggregateQuerySnapshot<{ count: AggregateField<number> }>> {
512+
const deferred = new Deferred<
513+
AggregateQuerySnapshot<{ count: AggregateField<number> }>
514+
>();
512515
client.asyncQueue.enqueueAndForget(async () => {
513516
const remoteStore = await getRemoteStore(client);
514517
if (!canUseNetwork(remoteStore)) {
515518
deferred.reject(
516519
new FirestoreError(
517520
Code.UNAVAILABLE,
518-
'Failed to get aggregate result because the client is offline.'
521+
'Failed to get count result because the client is offline.'
519522
)
520523
);
521524
} else {
522525
try {
523-
const result = await getAggregate(query);
526+
const result = await getCount(query);
524527
deferred.resolve(result);
525528
} catch (e) {
526529
deferred.reject(e as Error);

packages/firestore/src/lite-api/aggregate.ts

Lines changed: 86 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
* limitations under the License.
1616
*/
1717

18+
import { deepEqual } from '@firebase/util';
19+
1820
import { Value } from '../protos/firestore_proto_api';
1921
import { invokeRunAggregationQueryRpc } from '../remote/datastore';
2022
import { hardAssert } from '../util/assert';
@@ -26,61 +28,87 @@ import { Query, queryEqual } from './reference';
2628
import { LiteUserDataWriter } from './reference_impl';
2729

2830
/**
29-
* An `AggregateQuery` computes some aggregation statistics from the result set of
30-
* a base `Query`.
31+
* An `AggregateField`that captures input type T.
3132
*/
32-
export class AggregateQuery {
33-
readonly type = 'AggregateQuery';
34-
/**
35-
* The query on which you called `countQuery` in order to get this `AggregateQuery`.
36-
*/
37-
readonly query: Query<unknown>;
33+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
34+
export class AggregateField<T> {
35+
type = 'AggregateField';
36+
}
3837

39-
/** @hideconstructor */
40-
constructor(query: Query<unknown>) {
41-
this.query = query;
42-
}
38+
/**
39+
* Creates and returns an aggregation field that counts the documents in the result set.
40+
* @returns An `AggregateField` object with number input type.
41+
*/
42+
export function count(): AggregateField<number> {
43+
return new AggregateField<number>();
4344
}
4445

4546
/**
46-
* An `AggregateQuerySnapshot` contains results of a `AggregateQuery`.
47+
* The union of all `AggregateField` types that are returned from the factory
48+
* functions.
49+
*/
50+
type AggregateFieldType = ReturnType<typeof count>;
51+
52+
/**
53+
* A type whose values are all `AggregateField` objects.
54+
* This is used as an argument to the "getter" functions, and the snapshot will
55+
* map the same names to the corresponding values.
56+
*/
57+
export interface AggregateSpec {
58+
[field: string]: AggregateFieldType;
59+
}
60+
61+
/**
62+
* A type whose keys are taken from an `AggregateSpec` type, and whose values
63+
* are the result of the aggregation performed by the corresponding
64+
* `AggregateField` from the input `AggregateSpec`.
65+
*/
66+
export type AggregateSpecData<T extends AggregateSpec> = {
67+
[P in keyof T]: T[P] extends AggregateField<infer U> ? U : never;
68+
};
69+
70+
/**
71+
* An `AggregateQuerySnapshot` contains the results of running an aggregate query.
4772
*/
48-
export class AggregateQuerySnapshot {
73+
export class AggregateQuerySnapshot<T extends AggregateSpec> {
4974
readonly type = 'AggregateQuerySnapshot';
50-
readonly query: AggregateQuery;
5175

5276
/** @hideconstructor */
53-
constructor(query: AggregateQuery, private readonly _count: number) {
54-
this.query = query;
55-
}
77+
constructor(
78+
readonly query: Query<unknown>,
79+
private readonly _data: AggregateSpecData<T>
80+
) {}
5681

5782
/**
58-
* @returns The result of a document count aggregation. Returns null if no count aggregation is
59-
* available in the result.
83+
* The results of the requested aggregations. The keys of the returned object
84+
* will be the same as those of the `AggregateSpec` object specified to the
85+
* aggregation method, and the values will be the corresponding aggregation
86+
* result.
87+
*
88+
* @returns The aggregation statistics result of running a query.
6089
*/
61-
getCount(): number | null {
62-
return this._count;
90+
data(): AggregateSpecData<T> {
91+
return this._data;
6392
}
6493
}
6594

6695
/**
67-
* Creates an `AggregateQuery` counting the number of documents matching this query.
96+
* Counts the number of documents in the result set of the given query, ignoring
97+
* any locally-cached data and any locally-pending writes and simply surfacing
98+
* whatever the server returns. If the server cannot be reached then the
99+
* returned promise will be rejected.
100+
*
101+
* @param query - The `Query` to execute.
68102
*
69-
* @returns An `AggregateQuery` object that can be used to count the number of documents in
70-
* the result set of this query.
103+
* @returns An `AggregateQuerySnapshot` that contains the number of documents.
71104
*/
72-
export function countQuery(query: Query<unknown>): AggregateQuery {
73-
return new AggregateQuery(query);
74-
}
75-
76-
export function getAggregate(
77-
query: AggregateQuery
78-
): Promise<AggregateQuerySnapshot> {
79-
const firestore = cast(query.query.firestore, Firestore);
105+
export function getCount(
106+
query: Query<unknown>
107+
): Promise<AggregateQuerySnapshot<{ count: AggregateField<number> }>> {
108+
const firestore = cast(query.firestore, Firestore);
80109
const datastore = getDatastore(firestore);
81110
const userDataWriter = new LiteUserDataWriter(firestore);
82-
83-
return invokeRunAggregationQueryRpc(datastore, query).then(result => {
111+
return invokeRunAggregationQueryRpc(datastore, query._query).then(result => {
84112
hardAssert(
85113
result[0] !== undefined,
86114
'Aggregation fields are missing from result.'
@@ -90,29 +118,36 @@ export function getAggregate(
90118
.filter(([key, value]) => key === 'count_alias')
91119
.map(([key, value]) => userDataWriter.convertValue(value as Value));
92120

93-
const count = counts[0];
121+
const countValue = counts[0];
122+
94123
hardAssert(
95-
typeof count === 'number',
96-
'Count aggeragte field value is not a number: ' + count
124+
typeof countValue === 'number',
125+
'Count aggregate field value is not a number: ' + countValue
97126
);
98127

99-
return Promise.resolve(new AggregateQuerySnapshot(query, count));
128+
return Promise.resolve(
129+
new AggregateQuerySnapshot<{ count: AggregateField<number> }>(query, {
130+
count: countValue
131+
})
132+
);
100133
});
101134
}
102135

103-
export function aggregateQueryEqual(
104-
left: AggregateQuery,
105-
right: AggregateQuery
106-
): boolean {
107-
return queryEqual(left.query, right.query);
108-
}
109-
110-
export function aggregateQuerySnapshotEqual(
111-
left: AggregateQuerySnapshot,
112-
right: AggregateQuerySnapshot
136+
/**
137+
* Compares two `AggregateQuerySnapshot` instances for equality.
138+
* Two `AggregateQuerySnapshot` instances are considered "equal" if they have
139+
* the same underlying query, the same metadata, and the same data.
140+
*
141+
* @param left - The `AggregateQuerySnapshot` to compare.
142+
* @param right - The `AggregateQuerySnapshot` to compare.
143+
*
144+
* @returns true if the AggregateQuerySnapshos are equal.
145+
*/
146+
export function aggregateQuerySnapshotEqual<T extends AggregateSpec>(
147+
left: AggregateQuerySnapshot<T>,
148+
right: AggregateQuerySnapshot<T>
113149
): boolean {
114150
return (
115-
aggregateQueryEqual(left.query, right.query) &&
116-
left.getCount() === right.getCount()
151+
queryEqual(left.query, right.query) && deepEqual(left.data(), right.data())
117152
);
118153
}

packages/firestore/src/lite-api/reference.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,17 @@ import { FieldValue } from './field_value';
4040
import { FirestoreDataConverter } from './snapshot';
4141
import { NestedUpdateFields, Primitive } from './types';
4242

43+
/** Alias dynamic document field value types to any */
44+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
45+
export type DocumentFieldValue = any;
46+
4347
/**
4448
* Document data (for use with {@link @firebase/firestore/lite#(setDoc:1)}) consists of fields mapped to
4549
* values.
4650
*/
4751
export interface DocumentData {
4852
/** A mapping between a field and its value. */
49-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
50-
[field: string]: any;
53+
[field: string]: DocumentFieldValue;
5154
}
5255

5356
/**

packages/firestore/src/remote/connection.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export interface Connection {
9090
* request message must include the path. If false, then the request message must NOT
9191
* include the path.
9292
*/
93-
get shouldResourcePathBeIncludedInRequest(): boolean;
93+
readonly shouldResourcePathBeIncludedInRequest: boolean;
9494

9595
// TODO(mcg): subscribe to connection state changes.
9696
}

0 commit comments

Comments
 (0)