Skip to content

Commit bd52301

Browse files
Ehespmikehardy
authored andcommitted
feat(firestore, count): implement AggregateQuery count() on collections
1 parent 13402d5 commit bd52301

File tree

6 files changed

+295
-0
lines changed

6 files changed

+295
-0
lines changed

packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreCollectionModule.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,36 @@ public void namedQueryGet(
148148
});
149149
}
150150

151+
@ReactMethod
152+
public void collectionCount(
153+
String appName,
154+
String path,
155+
String type,
156+
ReadableArray filters,
157+
ReadableArray orders,
158+
ReadableMap options,
159+
Promise promise) {
160+
FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName);
161+
ReactNativeFirebaseFirestoreQuery firestoreQuery =
162+
new ReactNativeFirebaseFirestoreQuery(
163+
appName, getQueryForFirestore(firebaseFirestore, path, type), filters, orders, options);
164+
165+
AggregateQuery aggregateQuery = firestoreQuery.query.count();
166+
167+
aggregateQuery
168+
.get(AggregateSource.SERVER)
169+
.addOnCompleteListener(
170+
task -> {
171+
if (task.isSuccessful()) {
172+
WritableMap result = Arguments.createMap();
173+
result.putDouble("count", Long.valueOf(task.getResult().getCount()).doubleValue());
174+
promise.resolve(result);
175+
} else {
176+
rejectPromiseFirestoreException(promise, task.getException());
177+
}
178+
});
179+
}
180+
151181
@ReactMethod
152182
public void collectionGet(
153183
String appName,
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright (c) 2022-present Invertase Limited & Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this library except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
const COLLECTION = 'firestore';
18+
const { wipe } = require('../helpers');
19+
describe('firestore().collection().count()', function () {
20+
before(function () {
21+
return wipe();
22+
});
23+
24+
it('throws if no argument provided', function () {
25+
try {
26+
firebase.firestore().collection(COLLECTION).startAt();
27+
return Promise.reject(new Error('Did not throw an Error.'));
28+
} catch (error) {
29+
error.message.should.containEql(
30+
'Expected a DocumentSnapshot or list of field values but got undefined',
31+
);
32+
return Promise.resolve();
33+
}
34+
});
35+
36+
it('gets count of collection reference - unfiltered', async function () {
37+
const colRef = firebase.firestore().collection(`${COLLECTION}/count/collection`);
38+
39+
const doc1 = colRef.doc('doc1');
40+
const doc2 = colRef.doc('doc2');
41+
const doc3 = colRef.doc('doc3');
42+
await Promise.all([
43+
doc1.set({ foo: 1, bar: { value: 1 } }),
44+
doc2.set({ foo: 2, bar: { value: 2 } }),
45+
doc3.set({ foo: 3, bar: { value: 3 } }),
46+
]);
47+
48+
const qs = await colRef.count().get();
49+
qs.data().count.should.eql(3);
50+
});
51+
it('gets correct count of collection reference - where equal', async function () {
52+
const colRef = firebase.firestore().collection(`${COLLECTION}/count/collection`);
53+
54+
const doc1 = colRef.doc('doc1');
55+
const doc2 = colRef.doc('doc2');
56+
const doc3 = colRef.doc('doc3');
57+
await Promise.all([
58+
doc1.set({ foo: 1, bar: { value: 1 } }),
59+
doc2.set({ foo: 2, bar: { value: 2 } }),
60+
doc3.set({ foo: 3, bar: { value: 3 } }),
61+
]);
62+
63+
const qs = await colRef.where('foo', '==', 3).count().get();
64+
qs.data().count.should.eql(1);
65+
});
66+
67+
// TODO
68+
// - test behavior when firestore is offline (network disconnected or actually offline?)
69+
// - test AggregateQuery.query()
70+
});

packages/firestore/ios/RNFBFirestore/RNFBFirestoreCollectionModule.m

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,43 @@ - (void)invalidate {
165165
}];
166166
}
167167

168+
RCT_EXPORT_METHOD(collectionCount
169+
: (FIRApp *)firebaseApp
170+
: (NSString *)path
171+
: (NSString *)type
172+
: (NSArray *)filters
173+
: (NSArray *)orders
174+
: (NSDictionary *)options
175+
: (RCTPromiseResolveBlock)resolve
176+
: (RCTPromiseRejectBlock)reject) {
177+
FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp];
178+
FIRQuery *query = [RNFBFirestoreCommon getQueryForFirestore:firestore path:path type:type];
179+
RNFBFirestoreQuery *firestoreQuery = [[RNFBFirestoreQuery alloc] initWithModifiers:firestore
180+
query:query
181+
filters:filters
182+
orders:orders
183+
options:options];
184+
185+
// NOTE: There is only "server" as the source at the moment. So this
186+
// is unused for the time being. Using "FIRAggregateSourceServer".
187+
// NSString *source = arguments[@"source"];
188+
189+
FIRAggregateQuery *aggregateQuery = [firestoreQuery.query count];
190+
191+
[aggregateQuery
192+
aggregationWithSource:FIRAggregateSourceServer
193+
completion:^(FIRAggregateQuerySnapshot *_Nullable snapshot,
194+
NSError *_Nullable error) {
195+
if (error) {
196+
[RNFBFirestoreCommon promiseRejectFirestoreException:reject error:error];
197+
} else {
198+
NSMutableDictionary *snapshotMap = [NSMutableDictionary dictionary];
199+
snapshotMap[@"count"] = snapshot.count;
200+
resolve(snapshotMap);
201+
}
202+
}];
203+
}
204+
168205
RCT_EXPORT_METHOD(collectionGet
169206
: (FIRApp *)firebaseApp
170207
: (NSString *)path
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright (c) 2022-present Invertase Limited & Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this library except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
export class FirestoreAggregateQuery {
19+
constructor(firestore, query, collectionPath, modifiers) {
20+
this._firestore = firestore;
21+
this._query = query;
22+
this._collectionPath = collectionPath;
23+
this._modifiers = modifiers;
24+
}
25+
26+
get query() {
27+
return this._query;
28+
}
29+
30+
get() {
31+
return this._firestore.native
32+
.collectionCount(
33+
this._collectionPath.relativeName,
34+
this._modifiers.type,
35+
this._modifiers.filters,
36+
this._modifiers.orders,
37+
this._modifiers.options,
38+
)
39+
.then(data => new FirestoreAggregateQuerySnapshot(this._query, data));
40+
}
41+
}
42+
43+
export class FirestoreAggregateQuerySnapshot {
44+
constructor(query, data) {
45+
this._query = query;
46+
this._data = data;
47+
}
48+
49+
data() {
50+
return { count: this._data.count };
51+
}
52+
}

packages/firestore/lib/FirestoreQuery.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import NativeError from '@react-native-firebase/app/lib/internal/NativeFirebaseE
2626
import FirestoreDocumentSnapshot from './FirestoreDocumentSnapshot';
2727
import FirestoreFieldPath, { fromDotSeparatedString } from './FirestoreFieldPath';
2828
import FirestoreQuerySnapshot from './FirestoreQuerySnapshot';
29+
import { FirestoreAggregateQuery } from './FirestoreAggregate';
2930
import { parseSnapshotArgs } from './utils';
3031

3132
let _id = 0;
@@ -130,6 +131,15 @@ export default class FirestoreQuery {
130131
return modifiers.setFieldsCursor(cursor, allFields);
131132
}
132133

134+
count() {
135+
return new FirestoreAggregateQuery(
136+
this._firestore,
137+
this,
138+
this._collectionPath,
139+
this._modifiers,
140+
);
141+
}
142+
133143
endAt(docOrField, ...fields) {
134144
return new FirestoreQuery(
135145
this._firestore,

packages/firestore/lib/index.d.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -840,11 +840,107 @@ export namespace FirebaseFirestoreTypes {
840840
source: 'default' | 'server' | 'cache';
841841
}
842842

843+
/**
844+
* Represents an aggregation that can be performed by Firestore.
845+
*/
846+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
847+
export class AggregateField<T> {
848+
/** A type string to uniquely identify instances of this class. */
849+
type = 'AggregateField';
850+
}
851+
852+
/**
853+
* The union of all `AggregateField` types that are supported by Firestore.
854+
*/
855+
export type AggregateFieldType = AggregateField<number>;
856+
857+
/**
858+
* A type whose property values are all `AggregateField` objects.
859+
*/
860+
export interface AggregateSpec {
861+
[field: string]: AggregateFieldType;
862+
}
863+
864+
/**
865+
* A type whose keys are taken from an `AggregateSpec`, and whose values are the
866+
* result of the aggregation performed by the corresponding `AggregateField`
867+
* from the input `AggregateSpec`.
868+
*/
869+
export type AggregateSpecData<T extends AggregateSpec> = {
870+
[P in keyof T]: T[P] extends AggregateField<infer U> ? U : never;
871+
};
872+
873+
/**
874+
* The results of executing an aggregation query.
875+
*/
876+
export interface AggregateQuerySnapshot<T extends AggregateSpec> {
877+
/**
878+
* The underlying query over which the aggregations recorded in this
879+
* `AggregateQuerySnapshot` were performed.
880+
*/
881+
get query(): Query<unknown>;
882+
883+
/**
884+
* Returns the results of the aggregations performed over the underlying
885+
* query.
886+
*
887+
* The keys of the returned object will be the same as those of the
888+
* `AggregateSpec` object specified to the aggregation method, and the values
889+
* will be the corresponding aggregation result.
890+
*
891+
* @returns The results of the aggregations performed over the underlying
892+
* query.
893+
*/
894+
data(): AggregateSpecData<T>;
895+
}
896+
897+
/**
898+
* The results of requesting an aggregated query.
899+
*/
900+
export interface AggregateQuery<T extends AggregateSpec> {
901+
/**
902+
* The underlying query for this instance.
903+
*/
904+
get query(): Query<unknown>;
905+
906+
/**
907+
* Executes the query and returns the results as a AggregateQuerySnapshot.
908+
*
909+
*
910+
* #### Example
911+
*
912+
* ```js
913+
* const querySnapshot = await firebase.firestore()
914+
* .collection('users')
915+
* .count()
916+
* .get();
917+
* ```
918+
*
919+
* @param options An object to configure the get behavior.
920+
*/
921+
get(): Promise<AggregateQuerySnapshot<T>>;
922+
}
923+
843924
/**
844925
* A Query refers to a `Query` which you can read or listen to. You can also construct refined `Query` objects by
845926
* adding filters and ordering.
846927
*/
847928
export interface Query<T extends DocumentData = DocumentData> {
929+
/**
930+
* Calculates the number of documents in the result set of the given query, without actually downloading
931+
* the documents.
932+
*
933+
* Using this function to count the documents is efficient because only the final count, not the
934+
* documents' data, is downloaded. This function can even count the documents if the result set
935+
* would be prohibitively large to download entirely (e.g. thousands of documents).
936+
*
937+
* The result received from the server is presented, unaltered, without considering any local state.
938+
* That is, documents in the local cache are not taken into consideration, neither are local
939+
* modifications not yet synchronized with the server. Previously-downloaded results, if any,
940+
* are not used: every request using this source necessarily involves a round trip to the server.
941+
*/
942+
count(): AggregateQuery<{ count: AggregateField<number> }>;
943+
848944
/**
849945
* Creates and returns a new Query that ends at the provided document (inclusive). The end
850946
* position is relative to the order of the query. The document must contain all of the

0 commit comments

Comments
 (0)