Skip to content

Commit

Permalink
feat(firestore): support for aggregate queries including sum() & `a…
Browse files Browse the repository at this point in the history
…verage()`
  • Loading branch information
russellwheatley committed Nov 4, 2024
1 parent 88d3362 commit 47b1416
Show file tree
Hide file tree
Showing 4 changed files with 249 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
import com.facebook.react.bridge.*;
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.firestore.*;

import java.util.ArrayList;

import io.invertase.firebase.common.ReactNativeFirebaseEventEmitter;
import io.invertase.firebase.common.ReactNativeFirebaseModule;

Expand Down Expand Up @@ -193,6 +196,79 @@ public void collectionCount(
});
}

@ReactMethod
public void aggregateQuery(
String appName,
String databaseId,
String path,
String type,
ReadableArray filters,
ReadableArray orders,
ReadableMap options,
ReadableArray aggregateQueries
){
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");
String fieldPath = aggregateQuery.getString("fieldPath");
if (aggregateType && fieldPath) {
switch (aggregateType) {
case "count":
aggregateFields.add(AggregateField.count(fieldPath));
break;
case "sum":
aggregateFields.add(AggregateField.sum(fieldPath));
break;
case "average":
aggregateFields.add(AggregateField.avg(fieldPath));
break;
default:
break;
}
}

AggregateQuery aggregateQuery = firestoreQuery.query.aggregate(aggregateFields);
aggregateQuery
.get(AggregateSource.SERVER)
.addOnCompleteListener(
task -> {
if (task.isSuccessful()) {
WritableMap result = Arguments.createMap();
AggregateQuerySnapshot snapshot = task.getResult();
aggregateFields.forEach(aggregateField -> {
switch (aggregateField.getOperator()) {
case "count":
result.putDouble(aggregateField.getFieldPath(), Long.valueOf(snapshot.getCount()).doubleValue());
break;
case "sum":
result.putDouble(aggregateField.getFieldPath(), snapshot.getSum(aggregateField.getFieldPath()));
break;
case "average":
result.putDouble(aggregateField.getFieldPath(), snapshot.getAverage(aggregateField.getFieldPath()));
break;
default:
break;
}
}
promise.resolve(result);
} else {
rejectPromiseFirestoreException(promise, task.getException());
}
});
}
}

@ReactMethod
public void collectionGet(
String appName,
Expand All @@ -214,6 +290,8 @@ public void collectionGet(
orders,
options);
handleQueryGet(firestoreQuery, getSource(getOptions), promise);


}

private void handleQueryOnSnapshot(
Expand Down
35 changes: 35 additions & 0 deletions packages/firestore/lib/FirestoreAggregate.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*
*/

import FirestoreFieldPath, { fromDotSeparatedString } from './FirestoreFieldPath';

export class FirestoreAggregateQuery {
constructor(firestore, query, collectionPath, modifiers) {
this._firestore = firestore;
Expand Down Expand Up @@ -50,3 +52,36 @@ export class FirestoreAggregateQuerySnapshot {
return { count: this._data.count };
}
}

export const AggregateType = {
SUM: 'sum',
AVG: 'average',
COUNT: 'count',
};

export class AggregateField {
/** Indicates the aggregation operation of this AggregateField. */
aggregateType;
fieldPath;

/**
* Create a new AggregateField<T>
* @param aggregateType Specifies the type of aggregation operation to perform.
* @param _internalFieldPath Optionally specifies the field that is aggregated.
* @internal
*/
constructor(aggregateType, fieldPath) {
this.aggregateType = aggregateType;
this.fieldPath = fieldPath;
}
}

export function fieldPathFromArgument(path) {
if (path instanceof FirestoreFieldPath) {
return path;
} else if (typeof path === 'string') {
return fromDotSeparatedString(methodName, path);
} else {
throw new Error('Field path arguments must be of type `string` or `FieldPath`');
}
}
71 changes: 71 additions & 0 deletions packages/firestore/lib/modular/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,77 @@ export function getCountFromServer<AppModelType, DbModelType extends DocumentDat
>
>;

/**
* Specifies a set of aggregations and their aliases.
*/
interface AggregateSpec {
[field: string]: AggregateFieldType;
}

/**
* The union of all `AggregateField` types that are supported by Firestore.
*/
export type AggregateFieldType =
| ReturnType<typeof sum>
| ReturnType<typeof average>
| ReturnType<typeof count>;

export function getAggregateFromServer<
AggregateSpecType extends AggregateSpec,
AppModelType,
DbModelType extends FirebaseFirestoreTypes.DocumentData,
>(
query: Query<AppModelType, DbModelType>,
aggregateSpec: AggregateSpecType,
): Promise<
FirebaseFirestoreTypes.AggregateQuerySnapshot<AggregateSpecType, AppModelType, DbModelType>
>;

/**
* Create an AggregateField object that can be used to compute the sum of
* a specified field over a range of documents in the result set of a query.
* @param field Specifies the field to sum across the result set.
*/
export function sum(field: string | FieldPath): AggregateField<number>;

/**
* Create an AggregateField object that can be used to compute the average of
* a specified field over a range of documents in the result set of a query.
* @param field Specifies the field to average across the result set.
*/
export function average(field: string | FieldPath): AggregateField<number | null>;

/**
* Create an AggregateField object that can be used to compute the count of
* documents in the result set of a query.
*/
export function count(): AggregateField<number>;

/**
* Represents an aggregation that can be performed by Firestore.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export class AggregateField<T> {
/** A type string to uniquely identify instances of this class. */
readonly type = 'AggregateField';

/** Indicates the aggregation operation of this AggregateField. */
readonly aggregateType: AggregateType;

/**
* Create a new AggregateField<T>
* @param aggregateType Specifies the type of aggregation operation to perform.
* @param _internalFieldPath Optionally specifies the field that is aggregated.
* @internal
*/
constructor(
aggregateType: AggregateType = 'count',
readonly _internalFieldPath?: InternalFieldPath,
) {
this.aggregateType = aggregateType;
}
}

/**
* Represents the task of loading a Firestore bundle.
* It provides progress of bundle loading, as well as task completion and error events.
Expand Down
65 changes: 65 additions & 0 deletions packages/firestore/lib/modular/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
*/

import { firebase } from '../index';
import { AggregateField, AggregateType } from '../FirestoreAggregate';
import FirestorePath from '../FirestorePath';

/**
* @param {FirebaseApp?} app
Expand Down Expand Up @@ -192,6 +194,69 @@ export function getCountFromServer(query) {
return query.count().get();
}

export async function getAggregateFromServer(query, aggregateSpec) {
const aggregateQueries = [];
for (const key in aggregateSpec) {
if (aggregateSpec.hasOwnProperty(key)) {
const value = aggregateSpec[key];

if (value instanceof AggregateField) {
switch (value.aggregateType) {
case AggregateType.AVG:
case AggregateType.SUM:
case AggregateType.COUNT:
const aggregateQuery = {
aggregateType: value.aggregateType,
// TODO - how is this sent over the wire? Think it is serialized automatically
field: value.fieldPath,
};
aggregateQueries.push(aggregateQuery);
break;
default:
throw new Error(
`"AggregateField" has an an unknown "AggregateType" : ${value.aggregateType}`,
);
}
}
}
}

return query._firestore.native.aggregateQuery(
query._collectionPath.relativeName,
query._modifiers.type,
query._modifiers.filters,
query._modifiers.orders,
query._modifiers.options,
aggregateQueries,
);
}

/**
* Create an AggregateField object that can be used to compute the sum of
* a specified field over a range of documents in the result set of a query.
* @param field Specifies the field to sum across the result set.
*/
export function sum(field) {
return new AggregateField(AggregateType.SUM, FirestorePath.fromName(field));
}

/**
* Create an AggregateField object that can be used to compute the average of
* a specified field over a range of documents in the result set of a query.
* @param field Specifies the field to average across the result set.
*/
export function average(field) {
return new AggregateField(AggregateType.AVG, FirestorePath.fromName(field));
}

/**
* Create an AggregateField object that can be used to compute the count of
* documents in the result set of a query.
*/
export function count() {
return new AggregateField(AggregateType.COUNT);
}

/**
* @param {Firestore} firestore
* @param {ReadableStream<Uint8Array> | ArrayBuffer | string} bundleData
Expand Down

0 comments on commit 47b1416

Please sign in to comment.