Skip to content

Commit

Permalink
Merge pull request FirebaseExtended#73 from FirebaseExtended/de-count
Browse files Browse the repository at this point in the history
feat(firestore): add count observables
  • Loading branch information
davideast authored Jul 13, 2023
2 parents 1ec633c + 280c6b6 commit aadbc39
Show file tree
Hide file tree
Showing 12 changed files with 1,825 additions and 1,825 deletions.
76 changes: 76 additions & 0 deletions docs/firestore.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,82 @@ deleteDoc(davidDocRef);
*/
```

### `collectionCount()`

Create an observable that emits the server-calculated number of documents in a collection or query. [Learn more about count queries in the Firebase docs](https://firebase.google.com/docs/firestore/query-data/aggregation-queries).

| | |
|-----------------|------------------------------------------|
| **function** | `collectionCount()` |
| **params** | query: `import('firebase/firestore').CollectionReference \| import('firebase/firestore').Query`|
| **import path** | `rxfire/firestore` or `rxfire/firestore/lite` |
| **return** | `Observable<number>` |

#### TypeScript Example
```ts
import { collectionCount } from 'rxfire/firestore';
// Also available in firestore/lite
import { collectionCount } from 'rxfire/firestore/lite';

import { getFirestore, collection } from 'firebase/firestore';

const db = getFirestore();
const likesCol = collection(db, 'posts/post_id_123/likes');

collectionCount(likesCol).subscribe(count => {
console.log(count);
});
```

Note that the observable will complete after the first fetch. This is not a long-lived subscription. To update the count value over time, use the `repeat` operator:

```ts
import { repeat } from 'rxjs';

import { collectionCount} from 'rxfire/firestore';
import { getFirestore, collection } from 'firebase/firestore';

const db = getFirestore();
const likesCol = collection(db, 'posts/post_id_123/likes');

collectionCount(likesCol)
.pipe(
// re-fetch every 30 seconds.
// Stop fetching after 100 re-fetches so we don't do too many reads
repeat({ count: 100, delay: 30 * 1000 }),
)
.subscribe((count) => {
console.log(count);
});
```

### `collectionCountSnap()`

Create an observable that emits the server-calculated number of documents in a collection or query. [Learn more about count queries in the Firebase docs](https://firebase.google.com/docs/firestore/query-data/aggregation-queries).

| | |
|-----------------|------------------------------------------|
| **function** | `collectionCountSnap()` |
| **params** | query: `import('firebase/firestore').CollectionReference \| import('firebase/firestore').Query`|
| **import path** | `rxfire/firestore` or `firebase/firestore/lite` |
| **return** | `Observable<CountSnapshot>` |

#### TypeScript Example
```ts
import { collectionCountSnap } from 'rxfire/firestore';
// Also available in firestore/lite
import { collectionCountSnap } from 'rxfire/firestore/lite';
import { getFirestore, collection } from 'firebase/firestore';

const db = getFirestore();
const likesCol = collection(db, 'posts/post_id_123/likes');

// One version with the snapshot
countSnap$(likesCol).subscribe(snapshot => {
console.log(snapshot.data().count);
});
```

## Event Observables

### `fromDocRef()`
Expand Down
14 changes: 12 additions & 2 deletions firestore/collection/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2018 Google LLC
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -22,6 +22,7 @@ import {
OperatorFunction,
pipe,
UnaryFunction,
from,
} from 'rxjs';
import {
map,
Expand All @@ -33,7 +34,8 @@ import {
} from 'rxjs/operators';
import {snapToData} from '../document';
import {DocumentChangeType, DocumentChange, Query, QueryDocumentSnapshot, QuerySnapshot, DocumentData} from '../interfaces';
import {refEqual} from 'firebase/firestore';
import {getCountFromServer, refEqual} from 'firebase/firestore';
import {CountSnapshot} from '../lite/interfaces';
const ALL_EVENTS: DocumentChangeType[] = ['added', 'modified', 'removed'];

/**
Expand Down Expand Up @@ -295,3 +297,11 @@ export function collectionData<T=DocumentData, U extends string=never>(
}),
);
}

export function collectionCountSnap(query: Query<unknown>): Observable<CountSnapshot> {
return from(getCountFromServer(query));
}

export function collectionCount(query: Query<unknown>): Observable<number> {
return collectionCountSnap(query).pipe(map((snap) => snap.data().count));
}
16 changes: 12 additions & 4 deletions firestore/lite/collection/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2018 Google LLC
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -18,15 +18,15 @@
import {Observable, from} from 'rxjs';
import {map} from 'rxjs/operators';
import {snapToData} from '../document';
import {Query, QueryDocumentSnapshot, DocumentData} from '../interfaces';
import {getDocs} from 'firebase/firestore/lite';
import {Query, QueryDocumentSnapshot, DocumentData, CountSnapshot} from '../interfaces';
import {getDocs, getCount} from 'firebase/firestore/lite';

/**
* Return a stream of document snapshots on a query. These results are in sort order.
* @param query
*/
export function collection<T=DocumentData>(query: Query<T>): Observable<QueryDocumentSnapshot<T>[]> {
return from(getDocs<T>(query)).pipe(
return from(getDocs<T, DocumentData>(query)).pipe(
map((changes) => changes.docs),
);
}
Expand All @@ -47,3 +47,11 @@ export function collectionData<T=DocumentData>(
}),
);
}

export function collectionCountSnap(query: Query<unknown>): Observable<CountSnapshot> {
return from(getCount(query));
}

export function collectionCount(query: Query<unknown>): Observable<number> {
return collectionCountSnap(query).pipe(map((snap) => snap.data().count));
}
2 changes: 1 addition & 1 deletion firestore/lite/document/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {from, Observable} from 'rxjs';
import {getDoc} from 'firebase/firestore/lite';

export function doc<T=DocumentData>(ref: DocumentReference<T>): Observable<DocumentSnapshot<T>> {
return from(getDoc<T>(ref));
return from(getDoc<T, DocumentData>(ref));
}

/**
Expand Down
4 changes: 2 additions & 2 deletions firestore/lite/fromRef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ export function fromRef<T=DocumentData>(ref: DocumentReference<T>): Observable<D
export function fromRef<T=DocumentData>(ref: Query<T>): Observable<QuerySnapshot<T>>;
export function fromRef<T=DocumentData>(ref: DocumentReference<T>|Query<T>): Observable<DocumentSnapshot<T> | QuerySnapshot<T>> {
if (ref.type === 'document') {
return from(getDoc<T>(ref));
return from(getDoc<T, DocumentData>(ref));
} else {
return from(getDocs<T>(ref));
return from(getDocs<T, DocumentData>(ref));
}
}
17 changes: 11 additions & 6 deletions firestore/lite/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
export type DocumentReference<T> = import('firebase/firestore/lite').DocumentReference<T>;
export type DocumentData = import('firebase/firestore/lite').DocumentData;
export type Query<T> = import('firebase/firestore/lite').Query<T>;
export type DocumentSnapshot<T> = import('firebase/firestore/lite').DocumentSnapshot<T>;
export type QuerySnapshot<T> = import('firebase/firestore/lite').QuerySnapshot<T>;
export type QueryDocumentSnapshot<T> = import('firebase/firestore/lite').QueryDocumentSnapshot<T>;
import type * as lite from 'firebase/firestore/lite';

export type DocumentReference<T> = lite.DocumentReference<T>;
export type DocumentData = lite.DocumentData;
export type Query<T> = lite.Query<T>;
export type DocumentSnapshot<T> = lite.DocumentSnapshot<T>;
export type QuerySnapshot<T> = lite.QuerySnapshot<T>;
export type QueryDocumentSnapshot<T> = lite.QueryDocumentSnapshot<T>;
export type CountSnapshot = lite.AggregateQuerySnapshot<{
count: lite.AggregateField<number>;
}, any, DocumentData>;
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@
"build:docs": "cp README.md ./dist/ && cp -r ./docs ./dist/",
"dev": "rollup -c -w",
"echo:chrome": "echo 'Open Chrome DevTools: \nchrome://inspect/#devices'",
"test": "FIREBASE_CLI_PREVIEWS=storageemulator firebase emulators:exec \"jest --detectOpenHandles\" --project=rxfire-test-c497c ",
"test:debug": "yarn echo:chrome && FIREBASE_CLI_PREVIEWS=storageemulator firebase emulators:exec ./test-debug.sh --project=rxfire-test-c497c"
"test": "firebase emulators:exec \"jest --detectOpenHandles\" --project=rxfire-test-c497c ",
"test:debug": "yarn echo:chrome && firebase emulators:exec ./test-debug.sh --project=rxfire-test-c497c"
},
"dependencies": {},
"peerDependencies": {
Expand All @@ -95,8 +95,8 @@
"cross-fetch": "^3.1.4",
"eslint": "^7.32.0",
"eslint-config-google": "^0.14.0",
"firebase": "^9.0.0",
"firebase-tools": "^9.10.2",
"firebase": "^10.0.0",
"firebase-tools": "^12.4.3",
"glob": "^7.1.6",
"jest": "^26.6.3",
"md5": "^2.3.0",
Expand Down
46 changes: 41 additions & 5 deletions test/firestore-lite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

/**
* @license
* Copyright 2021 Google LLC
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -27,10 +27,12 @@ import {
collection,
docData,
collectionData,
collectionCountSnap,
collectionCount,
} from '../dist/firestore/lite';
import {map} from 'rxjs/operators';
import {default as TEST_PROJECT, firestoreEmulatorPort} from './config';
import {doc as firestoreDoc, getDocs, collection as firestoreCollection, getDoc, Firestore as FirebaseFirestore, CollectionReference, getFirestore, DocumentReference, connectFirestoreEmulator, doc, setDoc, collection as baseCollection, QueryDocumentSnapshot} from 'firebase/firestore/lite';
import {doc as firestoreDoc, getDocs, collection as firestoreCollection, getDoc, Firestore as FirebaseFirestore, CollectionReference, getFirestore, DocumentReference, connectFirestoreEmulator, doc, setDoc, collection as baseCollection, QueryDocumentSnapshot, addDoc} from 'firebase/firestore/lite';
import {initializeApp, deleteApp, FirebaseApp} from 'firebase/app';

const createId = (): string => Math.random().toString(36).substring(5);
Expand Down Expand Up @@ -80,8 +82,10 @@ describe('RxFire firestore/lite', () => {
connectFirestoreEmulator(firestore, 'localhost', firestoreEmulatorPort);
});

afterEach(() => {
deleteApp(app).catch(() => undefined);
afterEach((done) => {
deleteApp(app)
.then(() => done())
.catch(() => undefined);
});

describe('collection', () => {
Expand All @@ -105,7 +109,6 @@ describe('RxFire firestore/lite', () => {
});
});


describe('collection w/converter', () => {
/**
* This is a simple test to see if the collection() method
Expand Down Expand Up @@ -222,4 +225,37 @@ describe('RxFire firestore/lite', () => {
});
});
});

describe('Aggregations', () => {
it('should provide an observable with a count aggregate', async (done) => {
const colRef = createRandomCol(firestore);
const entries = [
addDoc(colRef, {id: createId()}),
addDoc(colRef, {id: createId()}),
];
await Promise.all(entries);

collectionCountSnap(colRef).subscribe((snap) => {
expect(snap.data().count).toEqual(entries.length);
done();
});
});

it('should provide an observable with a count aggregate number', async (done) => {
const colRef = createRandomCol(firestore);
const entries = [
addDoc(colRef, {id: createId()}),
addDoc(colRef, {id: createId()}),
addDoc(colRef, {id: createId()}),
addDoc(colRef, {id: createId()}),
addDoc(colRef, {id: createId()}),
];
await Promise.all(entries);

collectionCount(colRef).subscribe((count) => {
expect(count).toEqual(entries.length);
done();
});
});
});
});
39 changes: 37 additions & 2 deletions test/firestore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

/**
* @license
* Copyright 2021 Google LLC
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -30,10 +30,12 @@ import {
auditTrail,
docData,
collectionData,
collectionCountSnap,
collectionCount,
} from '../dist/firestore';
import {map, take, skip} from 'rxjs/operators';
import {default as TEST_PROJECT, firestoreEmulatorPort} from './config';
import {getDocs, collection as firestoreCollection, getDoc, DocumentReference, doc as firestoreDoc, Firestore as FirebaseFirestore, CollectionReference, getFirestore, updateDoc, connectFirestoreEmulator, doc, setDoc, DocumentChange, collection as baseCollection, QueryDocumentSnapshot} from 'firebase/firestore';
import {getDocs, collection as firestoreCollection, getDoc, DocumentReference, doc as firestoreDoc, Firestore as FirebaseFirestore, CollectionReference, getFirestore, updateDoc, connectFirestoreEmulator, doc, setDoc, DocumentChange, collection as baseCollection, QueryDocumentSnapshot, addDoc} from 'firebase/firestore';
import {initializeApp, deleteApp, FirebaseApp} from 'firebase/app';

const createId = (): string => Math.random().toString(36).substring(5);
Expand Down Expand Up @@ -422,4 +424,37 @@ describe('RxFire Firestore', () => {
});
});
});

describe('Aggregations', () => {
it('should provide an observable with a count aggregate snapshot', async (done) => {
const colRef = createRandomCol(firestore);
const entries = [
addDoc(colRef, {id: createId()}),
addDoc(colRef, {id: createId()}),
];
await Promise.all(entries);

collectionCountSnap(colRef).subscribe((snap) => {
expect(snap.data().count).toEqual(entries.length);
done();
});
});

it('should provide an observable with a count aggregate number', async (done) => {
const colRef = createRandomCol(firestore);
const entries = [
addDoc(colRef, {id: createId()}),
addDoc(colRef, {id: createId()}),
addDoc(colRef, {id: createId()}),
addDoc(colRef, {id: createId()}),
addDoc(colRef, {id: createId()}),
];
await Promise.all(entries);

collectionCount(colRef).subscribe((count) => {
expect(count).toEqual(entries.length);
done();
});
});
});
});
9 changes: 6 additions & 3 deletions test/functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@
},
"main": "index.js",
"dependencies": {
"firebase-admin": "^9.2.0",
"firebase-functions": "^3.11.0"
"firebase-admin": "^11.9.0",
"firebase-functions": "^4.4.1"
},
"devDependencies": {
"firebase-functions-test": "^0.2.0"
"firebase-functions-test": "^3.1.0"
},
"engines": {
"node": "20"
},
"private": true
}
Loading

0 comments on commit aadbc39

Please sign in to comment.