Skip to content

Commit

Permalink
feat(firestore, count): implement AggregateQuery count() on collections
Browse files Browse the repository at this point in the history
  • Loading branch information
Ehesp authored and mikehardy committed Oct 20, 2022
1 parent 13402d5 commit bd52301
Show file tree
Hide file tree
Showing 6 changed files with 295 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,36 @@ public void namedQueryGet(
});
}

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

AggregateQuery aggregateQuery = firestoreQuery.query.count();

aggregateQuery
.get(AggregateSource.SERVER)
.addOnCompleteListener(
task -> {
if (task.isSuccessful()) {
WritableMap result = Arguments.createMap();
result.putDouble("count", Long.valueOf(task.getResult().getCount()).doubleValue());
promise.resolve(result);
} else {
rejectPromiseFirestoreException(promise, task.getException());
}
});
}

@ReactMethod
public void collectionGet(
String appName,
Expand Down
70 changes: 70 additions & 0 deletions packages/firestore/e2e/Aggregate/count.e2e.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright (c) 2022-present Invertase Limited & Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this library except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
const COLLECTION = 'firestore';
const { wipe } = require('../helpers');
describe('firestore().collection().count()', function () {
before(function () {
return wipe();
});

it('throws if no argument provided', function () {
try {
firebase.firestore().collection(COLLECTION).startAt();
return Promise.reject(new Error('Did not throw an Error.'));
} catch (error) {
error.message.should.containEql(
'Expected a DocumentSnapshot or list of field values but got undefined',
);
return Promise.resolve();
}
});

it('gets count of collection reference - unfiltered', async function () {
const colRef = firebase.firestore().collection(`${COLLECTION}/count/collection`);

const doc1 = colRef.doc('doc1');
const doc2 = colRef.doc('doc2');
const doc3 = colRef.doc('doc3');
await Promise.all([
doc1.set({ foo: 1, bar: { value: 1 } }),
doc2.set({ foo: 2, bar: { value: 2 } }),
doc3.set({ foo: 3, bar: { value: 3 } }),
]);

const qs = await colRef.count().get();
qs.data().count.should.eql(3);
});
it('gets correct count of collection reference - where equal', async function () {
const colRef = firebase.firestore().collection(`${COLLECTION}/count/collection`);

const doc1 = colRef.doc('doc1');
const doc2 = colRef.doc('doc2');
const doc3 = colRef.doc('doc3');
await Promise.all([
doc1.set({ foo: 1, bar: { value: 1 } }),
doc2.set({ foo: 2, bar: { value: 2 } }),
doc3.set({ foo: 3, bar: { value: 3 } }),
]);

const qs = await colRef.where('foo', '==', 3).count().get();
qs.data().count.should.eql(1);
});

// TODO
// - test behavior when firestore is offline (network disconnected or actually offline?)
// - test AggregateQuery.query()
});
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,43 @@ - (void)invalidate {
}];
}

RCT_EXPORT_METHOD(collectionCount
: (FIRApp *)firebaseApp
: (NSString *)path
: (NSString *)type
: (NSArray *)filters
: (NSArray *)orders
: (NSDictionary *)options
: (RCTPromiseResolveBlock)resolve
: (RCTPromiseRejectBlock)reject) {
FIRFirestore *firestore = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp];
FIRQuery *query = [RNFBFirestoreCommon getQueryForFirestore:firestore path:path type:type];
RNFBFirestoreQuery *firestoreQuery = [[RNFBFirestoreQuery alloc] initWithModifiers:firestore
query:query
filters:filters
orders:orders
options:options];

// NOTE: There is only "server" as the source at the moment. So this
// is unused for the time being. Using "FIRAggregateSourceServer".
// NSString *source = arguments[@"source"];

FIRAggregateQuery *aggregateQuery = [firestoreQuery.query count];

[aggregateQuery
aggregationWithSource:FIRAggregateSourceServer
completion:^(FIRAggregateQuerySnapshot *_Nullable snapshot,
NSError *_Nullable error) {
if (error) {
[RNFBFirestoreCommon promiseRejectFirestoreException:reject error:error];
} else {
NSMutableDictionary *snapshotMap = [NSMutableDictionary dictionary];
snapshotMap[@"count"] = snapshot.count;
resolve(snapshotMap);
}
}];
}

RCT_EXPORT_METHOD(collectionGet
: (FIRApp *)firebaseApp
: (NSString *)path
Expand Down
52 changes: 52 additions & 0 deletions packages/firestore/lib/FirestoreAggregate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright (c) 2022-present Invertase Limited & Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this library except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

export class FirestoreAggregateQuery {
constructor(firestore, query, collectionPath, modifiers) {
this._firestore = firestore;
this._query = query;
this._collectionPath = collectionPath;
this._modifiers = modifiers;
}

get query() {
return this._query;
}

get() {
return this._firestore.native
.collectionCount(
this._collectionPath.relativeName,
this._modifiers.type,
this._modifiers.filters,
this._modifiers.orders,
this._modifiers.options,
)
.then(data => new FirestoreAggregateQuerySnapshot(this._query, data));
}
}

export class FirestoreAggregateQuerySnapshot {
constructor(query, data) {
this._query = query;
this._data = data;
}

data() {
return { count: this._data.count };
}
}
10 changes: 10 additions & 0 deletions packages/firestore/lib/FirestoreQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import NativeError from '@react-native-firebase/app/lib/internal/NativeFirebaseE
import FirestoreDocumentSnapshot from './FirestoreDocumentSnapshot';
import FirestoreFieldPath, { fromDotSeparatedString } from './FirestoreFieldPath';
import FirestoreQuerySnapshot from './FirestoreQuerySnapshot';
import { FirestoreAggregateQuery } from './FirestoreAggregate';
import { parseSnapshotArgs } from './utils';

let _id = 0;
Expand Down Expand Up @@ -130,6 +131,15 @@ export default class FirestoreQuery {
return modifiers.setFieldsCursor(cursor, allFields);
}

count() {
return new FirestoreAggregateQuery(
this._firestore,
this,
this._collectionPath,
this._modifiers,
);
}

endAt(docOrField, ...fields) {
return new FirestoreQuery(
this._firestore,
Expand Down
96 changes: 96 additions & 0 deletions packages/firestore/lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -840,11 +840,107 @@ export namespace FirebaseFirestoreTypes {
source: 'default' | 'server' | 'cache';
}

/**
* 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. */
type = 'AggregateField';
}

/**
* The union of all `AggregateField` types that are supported by Firestore.
*/
export type AggregateFieldType = AggregateField<number>;

/**
* A type whose property values are all `AggregateField` objects.
*/
export interface AggregateSpec {
[field: string]: AggregateFieldType;
}

/**
* A type whose keys are taken from an `AggregateSpec`, and whose values are the
* result of the aggregation performed by the corresponding `AggregateField`
* from the input `AggregateSpec`.
*/
export type AggregateSpecData<T extends AggregateSpec> = {
[P in keyof T]: T[P] extends AggregateField<infer U> ? U : never;
};

/**
* The results of executing an aggregation query.
*/
export interface AggregateQuerySnapshot<T extends AggregateSpec> {
/**
* The underlying query over which the aggregations recorded in this
* `AggregateQuerySnapshot` were performed.
*/
get query(): Query<unknown>;

/**
* Returns the results of the aggregations performed over the underlying
* query.
*
* The keys of the returned object will be the same as those of the
* `AggregateSpec` object specified to the aggregation method, and the values
* will be the corresponding aggregation result.
*
* @returns The results of the aggregations performed over the underlying
* query.
*/
data(): AggregateSpecData<T>;
}

/**
* The results of requesting an aggregated query.
*/
export interface AggregateQuery<T extends AggregateSpec> {
/**
* The underlying query for this instance.
*/
get query(): Query<unknown>;

/**
* Executes the query and returns the results as a AggregateQuerySnapshot.
*
*
* #### Example
*
* ```js
* const querySnapshot = await firebase.firestore()
* .collection('users')
* .count()
* .get();
* ```
*
* @param options An object to configure the get behavior.
*/
get(): Promise<AggregateQuerySnapshot<T>>;
}

/**
* A Query refers to a `Query` which you can read or listen to. You can also construct refined `Query` objects by
* adding filters and ordering.
*/
export interface Query<T extends DocumentData = DocumentData> {
/**
* Calculates the number of documents in the result set of the given query, without actually downloading
* the documents.
*
* Using this function to count the documents is efficient because only the final count, not the
* documents' data, is downloaded. This function can even count the documents if the result set
* would be prohibitively large to download entirely (e.g. thousands of documents).
*
* The result received from the server is presented, unaltered, without considering any local state.
* That is, documents in the local cache are not taken into consideration, neither are local
* modifications not yet synchronized with the server. Previously-downloaded results, if any,
* are not used: every request using this source necessarily involves a round trip to the server.
*/
count(): AggregateQuery<{ count: AggregateField<number> }>;

/**
* Creates and returns a new Query that ends at the provided document (inclusive). The end
* position is relative to the order of the query. The document must contain all of the
Expand Down

0 comments on commit bd52301

Please sign in to comment.