Skip to content

Commit dff8ddf

Browse files
jamesdanielsdavideast
authored andcommitted
feat(firestore): types for collection, audit trail, state, and snapshot changes (#1644)
* feat(fs): types for collection, audit log, state, and snapshot changes * Add exists vs. not interfaces for type guards * Clean up the types some more
1 parent 2c2fe02 commit dff8ddf

File tree

7 files changed

+60
-56
lines changed

7 files changed

+60
-56
lines changed

src/firestore/collection/changes.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,24 @@ import { DocumentChangeAction, Action } from '../interfaces';
1010
* order of occurence.
1111
* @param query
1212
*/
13-
export function docChanges(query: Query): Observable<DocumentChangeAction[]> {
13+
export function docChanges<T>(query: Query): Observable<DocumentChangeAction<T>[]> {
1414
return fromCollectionRef(query)
1515
.pipe(
1616
map(action =>
1717
action.payload.docChanges()
18-
.map(change => ({ type: change.type, payload: change }))));
18+
.map(change => ({ type: change.type, payload: change } as DocumentChangeAction<T>))));
1919
}
2020

2121
/**
2222
* Return a stream of document changes on a query. These results are in sort order.
2323
* @param query
2424
*/
25-
export function sortedChanges(query: Query, events: DocumentChangeType[]): Observable<DocumentChangeAction[]> {
25+
export function sortedChanges<T>(query: Query, events: DocumentChangeType[]): Observable<DocumentChangeAction<T>[]> {
2626
return fromCollectionRef(query)
2727
.pipe(
2828
map(changes => changes.payload.docChanges()),
2929
scan((current, changes) => combineChanges(current, changes, events), []),
30-
map(changes => changes.map(c => ({ type: c.type, payload: c }))));
30+
map(changes => changes.map(c => ({ type: c.type, payload: c } as DocumentChangeAction<T>))));
3131
}
3232

3333
/**

src/firestore/collection/collection.ts

+11-11
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { DocumentChangeType, CollectionReference, Query, DocumentReference } from '@firebase/firestore-types';
1+
import { DocumentChangeType, CollectionReference, Query, DocumentReference, DocumentData } from '@firebase/firestore-types';
22
import { Observable, Subscriber } from 'rxjs';
33
import { fromCollectionRef } from '../observable/fromRef';
44
import { map, filter, scan } from 'rxjs/operators';
55

66
import { Injectable } from '@angular/core';
77

8-
import { QueryFn, AssociatedReference, DocumentChangeAction } from '../interfaces';
8+
import { QueryFn, AssociatedReference, DocumentChangeAction, DocumentChange } from '../interfaces';
99
import { docChanges, sortedChanges } from './changes';
1010
import { AngularFirestoreDocument } from '../document/document';
1111
import { AngularFirestore } from '../firestore';
@@ -40,7 +40,7 @@ export function validateEventsArray(events?: DocumentChangeType[]) {
4040
* // Subscribe to changes as snapshots. This provides you data updates as well as delta updates.
4141
* fakeStock.valueChanges().subscribe(value => console.log(value));
4242
*/
43-
export class AngularFirestoreCollection<T> {
43+
export class AngularFirestoreCollection<T=DocumentData> {
4444
/**
4545
* The constructor takes in a CollectionReference and Query to provide wrapper methods
4646
* for data operations and data streaming.
@@ -62,17 +62,17 @@ export class AngularFirestoreCollection<T> {
6262
* your own data structure.
6363
* @param events
6464
*/
65-
stateChanges(events?: DocumentChangeType[]): Observable<DocumentChangeAction[]> {
65+
stateChanges(events?: DocumentChangeType[]): Observable<DocumentChangeAction<T>[]> {
6666
if(!events || events.length === 0) {
6767
return this.afs.scheduler.keepUnstableUntilFirst(
6868
this.afs.scheduler.runOutsideAngular(
69-
docChanges(this.query)
69+
docChanges<T>(this.query)
7070
)
7171
);
7272
}
7373
return this.afs.scheduler.keepUnstableUntilFirst(
7474
this.afs.scheduler.runOutsideAngular(
75-
docChanges(this.query)
75+
docChanges<T>(this.query)
7676
)
7777
)
7878
.pipe(
@@ -86,7 +86,7 @@ export class AngularFirestoreCollection<T> {
8686
* but it collects each event in an array over time.
8787
* @param events
8888
*/
89-
auditTrail(events?: DocumentChangeType[]): Observable<DocumentChangeAction[]> {
89+
auditTrail(events?: DocumentChangeType[]): Observable<DocumentChangeAction<T>[]> {
9090
return this.stateChanges(events).pipe(scan((current, action) => [...current, ...action], []));
9191
}
9292

@@ -95,9 +95,9 @@ export class AngularFirestoreCollection<T> {
9595
* query order.
9696
* @param events
9797
*/
98-
snapshotChanges(events?: DocumentChangeType[]): Observable<DocumentChangeAction[]> {
98+
snapshotChanges(events?: DocumentChangeType[]): Observable<DocumentChangeAction<T>[]> {
9999
const validatedEvents = validateEventsArray(events);
100-
const sortedChanges$ = sortedChanges(this.query, validatedEvents);
100+
const sortedChanges$ = sortedChanges<T>(this.query, validatedEvents);
101101
const scheduledSortedChanges$ = this.afs.scheduler.runOutsideAngular(sortedChanges$);
102102
return this.afs.scheduler.keepUnstableUntilFirst(scheduledSortedChanges$);
103103
}
@@ -106,11 +106,11 @@ export class AngularFirestoreCollection<T> {
106106
* Listen to all documents in the collection and its possible query as an Observable.
107107
*/
108108
valueChanges(): Observable<T[]> {
109-
const fromCollectionRef$ = fromCollectionRef(this.query);
109+
const fromCollectionRef$ = fromCollectionRef<T>(this.query);
110110
const scheduled$ = this.afs.scheduler.runOutsideAngular(fromCollectionRef$);
111111
return this.afs.scheduler.keepUnstableUntilFirst(scheduled$)
112112
.pipe(
113-
map(actions => actions.payload.docs.map(a => a.data()) as T[])
113+
map(actions => actions.payload.docs.map(a => a.data()))
114114
);
115115
}
116116

src/firestore/document/document.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ describe('AngularFirestoreDocument', () => {
5757
const stock = new AngularFirestoreDocument<Stock>(ref, afs);
5858
await stock.set(FAKE_STOCK_DATA);
5959
const obs$ = stock.valueChanges();
60-
obs$.pipe(take(1)).subscribe(async (data: Stock) => {
60+
obs$.pipe(take(1)).subscribe(async data => {
6161
expect(JSON.stringify(data)).toBe(JSON.stringify(FAKE_STOCK_DATA));
6262
stock.delete().then(done).catch(done.fail);
6363
});

src/firestore/document/document.ts

+9-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { DocumentReference, SetOptions, DocumentSnapshot } from '@firebase/firestore-types';
1+
import { DocumentReference, SetOptions, DocumentData } from '@firebase/firestore-types';
22
import { Observable, Subscriber } from 'rxjs';
3-
import { QueryFn, AssociatedReference, Action } from '../interfaces';
3+
import { QueryFn, AssociatedReference, Action, DocumentSnapshot } from '../interfaces';
44
import { fromDocRef } from '../observable/fromRef';
55
import { map } from 'rxjs/operators';
66

@@ -31,7 +31,7 @@ import { AngularFirestoreCollection } from '../collection/collection';
3131
* // OR! Transform using Observable.from() and the data is unwrapped for you
3232
* Observable.from(fakeStock).subscribe(value => console.log(value));
3333
*/
34-
export class AngularFirestoreDocument<T> {
34+
export class AngularFirestoreDocument<T=DocumentData> {
3535

3636
/**
3737
* The contstuctor takes in a DocumentReference to provide wrapper methods
@@ -70,28 +70,28 @@ export class AngularFirestoreDocument<T> {
7070
* @param path
7171
* @param queryFn
7272
*/
73-
collection<T>(path: string, queryFn?: QueryFn): AngularFirestoreCollection<T> {
73+
collection<R>(path: string, queryFn?: QueryFn): AngularFirestoreCollection<R> {
7474
const collectionRef = this.ref.collection(path);
7575
const { ref, query } = associateQuery(collectionRef, queryFn);
76-
return new AngularFirestoreCollection<T>(ref, query, this.afs);
76+
return new AngularFirestoreCollection<R>(ref, query, this.afs);
7777
}
7878

7979
/**
8080
* Listen to snapshot updates from the document.
8181
*/
82-
snapshotChanges(): Observable<Action<DocumentSnapshot>> {
83-
const fromDocRef$ = fromDocRef(this.ref);
82+
snapshotChanges(): Observable<Action<DocumentSnapshot<T>>> {
83+
const fromDocRef$ = fromDocRef<T>(this.ref);
8484
const scheduledFromDocRef$ = this.afs.scheduler.runOutsideAngular(fromDocRef$);
8585
return this.afs.scheduler.keepUnstableUntilFirst(scheduledFromDocRef$);
8686
}
8787

8888
/**
8989
* Listen to unwrapped snapshot updates from the document.
9090
*/
91-
valueChanges(): Observable<T|null> {
91+
valueChanges(): Observable<T|undefined> {
9292
return this.snapshotChanges().pipe(
9393
map(action => {
94-
return action.payload.exists ? action.payload.data() as T : null;
94+
return action.payload.data();
9595
})
9696
);
9797
}

src/firestore/interfaces.ts

+28-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,34 @@
11
import { Subscriber } from 'rxjs';
2-
import { DocumentChangeType, DocumentChange, CollectionReference, Query } from '@firebase/firestore-types';
2+
import { DocumentChangeType, QuerySnapshot as _QuerySnapshot, FieldPath, DocumentSnapshot as _DocumentSnapshot, SnapshotOptions, QueryDocumentSnapshot as _QueryDocumentSnapshot, DocumentChange as _DocumentChange, CollectionReference, Query } from '@firebase/firestore-types';
33

4-
export interface DocumentChangeAction {
4+
export interface DocumentSnapshotExists<T> extends _DocumentSnapshot {
5+
readonly exists: true;
6+
data(options?: SnapshotOptions): T;
7+
}
8+
9+
export interface DocumentSnapshotDoesNotExist extends _DocumentSnapshot {
10+
readonly exists: false;
11+
data(options?: SnapshotOptions): undefined;
12+
get(fieldPath: string | FieldPath, options?: SnapshotOptions): undefined;
13+
}
14+
15+
export type DocumentSnapshot<T> = DocumentSnapshotExists<T> | DocumentSnapshotDoesNotExist;
16+
17+
export interface QueryDocumentSnapshot<T> extends _QueryDocumentSnapshot {
18+
data(options?: SnapshotOptions): T;
19+
}
20+
21+
export interface QuerySnapshot<T> extends _QuerySnapshot {
22+
readonly docs: QueryDocumentSnapshot<T>[];
23+
}
24+
25+
export interface DocumentChange<T> extends _DocumentChange {
26+
readonly doc: QueryDocumentSnapshot<T>;
27+
}
28+
29+
export interface DocumentChangeAction<T> {
530
type: DocumentChangeType;
6-
payload: DocumentChange;
31+
payload: DocumentChange<T>;
732
}
833

934
export interface Action<T> {

src/firestore/observable/fromRef.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { DocumentReference, Query, QuerySnapshot, DocumentSnapshot } from '@firebase/firestore-types';
1+
import { DocumentReference, Query } from '@firebase/firestore-types';
22
import { Observable, Subscriber } from 'rxjs';
3-
import { Action, Reference } from '../interfaces';
3+
import { Action, Reference, DocumentSnapshot, QuerySnapshot } from '../interfaces';
44
import { map, share } from 'rxjs/operators';
55

66
function _fromRef<T, R>(ref: Reference<T>): Observable<R> {
@@ -14,13 +14,13 @@ export function fromRef<R>(ref: DocumentReference | Query) {
1414
return _fromRef<typeof ref, R>(ref).pipe(share());
1515
}
1616

17-
export function fromDocRef(ref: DocumentReference): Observable<Action<DocumentSnapshot>>{
18-
return fromRef<DocumentSnapshot>(ref)
17+
export function fromDocRef<T>(ref: DocumentReference): Observable<Action<DocumentSnapshot<T>>>{
18+
return fromRef<DocumentSnapshot<T>>(ref)
1919
.pipe(
2020
map(payload => ({ payload, type: 'value' }))
2121
);
2222
}
2323

24-
export function fromCollectionRef(ref: Query): Observable<Action<QuerySnapshot>> {
25-
return fromRef<QuerySnapshot>(ref).pipe(map(payload => ({ payload, type: 'query' })));
24+
export function fromCollectionRef<T>(ref: Query): Observable<Action<QuerySnapshot<T>>> {
25+
return fromRef<QuerySnapshot<T>>(ref).pipe(map(payload => ({ payload, type: 'query' })));
2626
}

yarn.lock

+1-22
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
dependencies:
7474
"@firebase/auth-types" "0.3.2"
7575

76-
"@firebase/database-types@0.3.1", "@firebase/database-types@^0.3.1":
76+
"@firebase/database-types@^0.3.1":
7777
version "0.3.1"
7878
resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-0.3.1.tgz#4a15423f3b2cb3bed111f5a353c5c1bb2e2787ba"
7979

@@ -1825,12 +1825,6 @@ fast-levenshtein@~2.0.4:
18251825
version "2.0.6"
18261826
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
18271827

1828-
1829-
version "0.11.1"
1830-
resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.1.tgz#f0efe18c4f56e4f40afc7e06c719fd5ee6188f38"
1831-
dependencies:
1832-
websocket-driver ">=0.5.1"
1833-
18341828
file-uri-to-path@1:
18351829
version "1.0.0"
18361830
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
@@ -2594,10 +2588,6 @@ http-errors@~1.6.2:
25942588
setprototypeof "1.1.0"
25952589
statuses ">= 1.4.0 < 2"
25962590

2597-
http-parser-js@>=0.4.0:
2598-
version "0.4.12"
2599-
resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.12.tgz#b9cfbf4a2cf26f0fc34b10ca1489a27771e3474f"
2600-
26012591
http-proxy-agent@1:
26022592
version "1.0.0"
26032593
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-1.0.0.tgz#cc1ce38e453bf984a0f7702d2dd59c73d081284a"
@@ -5780,17 +5770,6 @@ void-elements@^2.0.0:
57805770
version "2.0.1"
57815771
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
57825772

5783-
websocket-driver@>=0.5.1:
5784-
version "0.7.0"
5785-
resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.0.tgz#0caf9d2d755d93aee049d4bdd0d3fe2cca2a24eb"
5786-
dependencies:
5787-
http-parser-js ">=0.4.0"
5788-
websocket-extensions ">=0.1.1"
5789-
5790-
websocket-extensions@>=0.1.1:
5791-
version "0.1.3"
5792-
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29"
5793-
57945773
[email protected], whatwg-fetch@>=0.10.0:
57955774
version "2.0.4"
57965775
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f"

0 commit comments

Comments
 (0)