Skip to content

Commit

Permalink
feat(firestore): implement sum() & average() aggregate queries (#…
Browse files Browse the repository at this point in the history
…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]>
  • Loading branch information
russellwheatley and mikehardy authored Nov 14, 2024
1 parent 1c4301c commit e4c57fa
Show file tree
Hide file tree
Showing 11 changed files with 1,031 additions and 9 deletions.
20 changes: 20 additions & 0 deletions packages/firestore/__tests__/firestore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import firestore, {
firebase,
Filter,
getFirestore,
getAggregateFromServer,
count,
average,
sum,
addDoc,
doc,
collection,
Expand Down Expand Up @@ -651,6 +655,22 @@ describe('Firestore', function () {
it('`enablePersistentCacheIndexAutoCreation` is properly exposed to end user', function () {
expect(enablePersistentCacheIndexAutoCreation).toBeDefined();
});

it('`getAggregateFromServer` is properly exposed to end user', function () {
expect(getAggregateFromServer).toBeDefined();
});

it('`count` is properly exposed to end user', function () {
expect(count).toBeDefined();
});

it('`average` is properly exposed to end user', function () {
expect(average).toBeDefined();
});

it('`sum` is properly exposed to end user', function () {
expect(sum).toBeDefined();
});
});

describe('FirestorePersistentCacheIndexManager', function () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
*
*/

import static com.google.firebase.firestore.AggregateField.average;
import static com.google.firebase.firestore.AggregateField.sum;
import static io.invertase.firebase.firestore.ReactNativeFirebaseFirestoreCommon.rejectPromiseFirestoreException;
import static io.invertase.firebase.firestore.ReactNativeFirebaseFirestoreSerialize.snapshotToWritableMap;
import static io.invertase.firebase.firestore.UniversalFirebaseFirestoreCommon.getFirestoreForApp;
Expand All @@ -28,6 +30,7 @@
import com.google.firebase.firestore.*;
import io.invertase.firebase.common.ReactNativeFirebaseEventEmitter;
import io.invertase.firebase.common.ReactNativeFirebaseModule;
import java.util.ArrayList;

public class ReactNativeFirebaseFirestoreCollectionModule extends ReactNativeFirebaseModule {
private static final String SERVICE_NAME = "FirestoreCollection";
Expand Down Expand Up @@ -193,6 +196,114 @@ public void collectionCount(
});
}

@ReactMethod
public void aggregateQuery(
String appName,
String databaseId,
String path,
String type,
ReadableArray filters,
ReadableArray orders,
ReadableMap options,
ReadableArray aggregateQueries,
Promise promise) {
FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName, databaseId);
ReactNativeFirebaseFirestoreQuery firestoreQuery =
new ReactNativeFirebaseFirestoreQuery(
appName,
databaseId,
getQueryForFirestore(firebaseFirestore, path, type),
filters,
orders,
options);

ArrayList<AggregateField> aggregateFields = new ArrayList<>();

for (int i = 0; i < aggregateQueries.size(); i++) {
ReadableMap aggregateQuery = aggregateQueries.getMap(i);
String aggregateType = aggregateQuery.getString("aggregateType");
if (aggregateType == null) aggregateType = "";
String fieldPath = aggregateQuery.getString("field");

switch (aggregateType) {
case "count":
aggregateFields.add(AggregateField.count());
break;
case "sum":
aggregateFields.add(AggregateField.sum(fieldPath));
break;
case "average":
aggregateFields.add(AggregateField.average(fieldPath));
break;
default:
rejectPromiseWithCodeAndMessage(
promise, "firestore/invalid-argument", "Invalid AggregateType: " + aggregateType);
return;
}
}
AggregateQuery firestoreAggregateQuery =
firestoreQuery.query.aggregate(
aggregateFields.get(0),
aggregateFields.subList(1, aggregateFields.size()).toArray(new AggregateField[0]));

firestoreAggregateQuery
.get(AggregateSource.SERVER)
.addOnCompleteListener(
task -> {
if (task.isSuccessful()) {
WritableMap result = Arguments.createMap();
AggregateQuerySnapshot snapshot = task.getResult();

for (int k = 0; k < aggregateQueries.size(); k++) {
ReadableMap aggQuery = aggregateQueries.getMap(k);
String aggType = aggQuery.getString("aggregateType");
if (aggType == null) aggType = "";
String field = aggQuery.getString("field");
String key = aggQuery.getString("key");

if (key == null) {
rejectPromiseWithCodeAndMessage(
promise, "firestore/invalid-argument", "key may not be null");
return;
}

switch (aggType) {
case "count":
result.putDouble(key, Long.valueOf(snapshot.getCount()).doubleValue());
break;
case "sum":
Number sum = (Number) snapshot.get(sum(field));
if (sum == null) {
rejectPromiseWithCodeAndMessage(
promise, "firestore/unknown", "sum unexpectedly null");
return;
}
result.putDouble(key, sum.doubleValue());
break;
case "average":
Number average = snapshot.get(average(field));
if (average == null) {
result.putNull(key);
} else {
result.putDouble(key, average.doubleValue());
}
break;
default:
rejectPromiseWithCodeAndMessage(
promise,
"firestore/invalid-argument",
"Invalid AggregateType: " + aggType);
return;
}
}

promise.resolve(result);
} else {
rejectPromiseFirestoreException(promise, task.getException());
}
});
}

@ReactMethod
public void collectionGet(
String appName,
Expand Down
Loading

0 comments on commit e4c57fa

Please sign in to comment.