Skip to content

Commit e4c57fa

Browse files
feat(firestore): implement sum() & average() aggregate queries (#8115)
* feat(firestore): support for aggregate queries including `sum()` & `average()` * feat(firestore, android): working version of aggregate query * feat: iOS implementation of aggregate queries * test: getAggregateFromServer() * test: update e2e tests * chore: improve typing * chore: format * chore: rm assertions * chore: format * feat: 'other' platform support * tes: fix test scopes * fix: firestore lite has different name for API * test: ensure exposed to end user * test: fix broken tests * fix(android): allow null value for average * chore: fix typo * fix(firestore, android): send null errors through promise reject path having native module exceptions vs promise rejects requires JS level code to handle multiple types of error vs being able to use one style * test: update aggregate query to see what happens with float handling * fix: update exception handling iOS * chore: AggregateQuerySnapshot type update * fix: return after promise rejection * fix: android, fieldPath can be null for count. fix promise.reject * chore: remove tag * test: edge cases for aggregate queries * chore: remove only() for test * test: update what test produces * test: correct return type expected * test: ensure aggregate fields are exposed to end user --------- Co-authored-by: Mike Hardy <[email protected]>
1 parent 1c4301c commit e4c57fa

File tree

11 files changed

+1031
-9
lines changed

11 files changed

+1031
-9
lines changed

packages/firestore/__tests__/firestore.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import firestore, {
44
firebase,
55
Filter,
66
getFirestore,
7+
getAggregateFromServer,
8+
count,
9+
average,
10+
sum,
711
addDoc,
812
doc,
913
collection,
@@ -651,6 +655,22 @@ describe('Firestore', function () {
651655
it('`enablePersistentCacheIndexAutoCreation` is properly exposed to end user', function () {
652656
expect(enablePersistentCacheIndexAutoCreation).toBeDefined();
653657
});
658+
659+
it('`getAggregateFromServer` is properly exposed to end user', function () {
660+
expect(getAggregateFromServer).toBeDefined();
661+
});
662+
663+
it('`count` is properly exposed to end user', function () {
664+
expect(count).toBeDefined();
665+
});
666+
667+
it('`average` is properly exposed to end user', function () {
668+
expect(average).toBeDefined();
669+
});
670+
671+
it('`sum` is properly exposed to end user', function () {
672+
expect(sum).toBeDefined();
673+
});
654674
});
655675

656676
describe('FirestorePersistentCacheIndexManager', function () {

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

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
*
1818
*/
1919

20+
import static com.google.firebase.firestore.AggregateField.average;
21+
import static com.google.firebase.firestore.AggregateField.sum;
2022
import static io.invertase.firebase.firestore.ReactNativeFirebaseFirestoreCommon.rejectPromiseFirestoreException;
2123
import static io.invertase.firebase.firestore.ReactNativeFirebaseFirestoreSerialize.snapshotToWritableMap;
2224
import static io.invertase.firebase.firestore.UniversalFirebaseFirestoreCommon.getFirestoreForApp;
@@ -28,6 +30,7 @@
2830
import com.google.firebase.firestore.*;
2931
import io.invertase.firebase.common.ReactNativeFirebaseEventEmitter;
3032
import io.invertase.firebase.common.ReactNativeFirebaseModule;
33+
import java.util.ArrayList;
3134

3235
public class ReactNativeFirebaseFirestoreCollectionModule extends ReactNativeFirebaseModule {
3336
private static final String SERVICE_NAME = "FirestoreCollection";
@@ -193,6 +196,114 @@ public void collectionCount(
193196
});
194197
}
195198

199+
@ReactMethod
200+
public void aggregateQuery(
201+
String appName,
202+
String databaseId,
203+
String path,
204+
String type,
205+
ReadableArray filters,
206+
ReadableArray orders,
207+
ReadableMap options,
208+
ReadableArray aggregateQueries,
209+
Promise promise) {
210+
FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName, databaseId);
211+
ReactNativeFirebaseFirestoreQuery firestoreQuery =
212+
new ReactNativeFirebaseFirestoreQuery(
213+
appName,
214+
databaseId,
215+
getQueryForFirestore(firebaseFirestore, path, type),
216+
filters,
217+
orders,
218+
options);
219+
220+
ArrayList<AggregateField> aggregateFields = new ArrayList<>();
221+
222+
for (int i = 0; i < aggregateQueries.size(); i++) {
223+
ReadableMap aggregateQuery = aggregateQueries.getMap(i);
224+
String aggregateType = aggregateQuery.getString("aggregateType");
225+
if (aggregateType == null) aggregateType = "";
226+
String fieldPath = aggregateQuery.getString("field");
227+
228+
switch (aggregateType) {
229+
case "count":
230+
aggregateFields.add(AggregateField.count());
231+
break;
232+
case "sum":
233+
aggregateFields.add(AggregateField.sum(fieldPath));
234+
break;
235+
case "average":
236+
aggregateFields.add(AggregateField.average(fieldPath));
237+
break;
238+
default:
239+
rejectPromiseWithCodeAndMessage(
240+
promise, "firestore/invalid-argument", "Invalid AggregateType: " + aggregateType);
241+
return;
242+
}
243+
}
244+
AggregateQuery firestoreAggregateQuery =
245+
firestoreQuery.query.aggregate(
246+
aggregateFields.get(0),
247+
aggregateFields.subList(1, aggregateFields.size()).toArray(new AggregateField[0]));
248+
249+
firestoreAggregateQuery
250+
.get(AggregateSource.SERVER)
251+
.addOnCompleteListener(
252+
task -> {
253+
if (task.isSuccessful()) {
254+
WritableMap result = Arguments.createMap();
255+
AggregateQuerySnapshot snapshot = task.getResult();
256+
257+
for (int k = 0; k < aggregateQueries.size(); k++) {
258+
ReadableMap aggQuery = aggregateQueries.getMap(k);
259+
String aggType = aggQuery.getString("aggregateType");
260+
if (aggType == null) aggType = "";
261+
String field = aggQuery.getString("field");
262+
String key = aggQuery.getString("key");
263+
264+
if (key == null) {
265+
rejectPromiseWithCodeAndMessage(
266+
promise, "firestore/invalid-argument", "key may not be null");
267+
return;
268+
}
269+
270+
switch (aggType) {
271+
case "count":
272+
result.putDouble(key, Long.valueOf(snapshot.getCount()).doubleValue());
273+
break;
274+
case "sum":
275+
Number sum = (Number) snapshot.get(sum(field));
276+
if (sum == null) {
277+
rejectPromiseWithCodeAndMessage(
278+
promise, "firestore/unknown", "sum unexpectedly null");
279+
return;
280+
}
281+
result.putDouble(key, sum.doubleValue());
282+
break;
283+
case "average":
284+
Number average = snapshot.get(average(field));
285+
if (average == null) {
286+
result.putNull(key);
287+
} else {
288+
result.putDouble(key, average.doubleValue());
289+
}
290+
break;
291+
default:
292+
rejectPromiseWithCodeAndMessage(
293+
promise,
294+
"firestore/invalid-argument",
295+
"Invalid AggregateType: " + aggType);
296+
return;
297+
}
298+
}
299+
300+
promise.resolve(result);
301+
} else {
302+
rejectPromiseFirestoreException(promise, task.getException());
303+
}
304+
});
305+
}
306+
196307
@ReactMethod
197308
public void collectionGet(
198309
String appName,

0 commit comments

Comments
 (0)