Skip to content

Commit 8542e84

Browse files
committed
Metadata and value
1 parent 03d614a commit 8542e84

File tree

6 files changed

+133
-57
lines changed

6 files changed

+133
-57
lines changed

src/firestore/collection/changes.ts

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { fromCollectionRef } from '../observable/fromRef';
2-
import { Observable } from 'rxjs';
3-
import { map, filter, scan } from 'rxjs/operators';
2+
import { Observable, of } from 'rxjs';
3+
import { map, filter, scan, tap, switchMap } from 'rxjs/operators';
44
import { firestore } from 'firebase/app';
55

6-
import { Query, DocumentChangeType, DocumentChange, DocumentChangeAction, Action } from '../interfaces';
6+
import { Query, DocumentChangeType, DocumentChange, DocumentChangeAction, Action, QuerySnapshot, DocumentChangeOrMetadata } from '../interfaces';
77

88
/**
99
* Return a stream of document changes on a query. These results are not in sort order but in
@@ -22,12 +22,27 @@ export function docChanges<T>(query: Query, options?: firestore.SnapshotListenOp
2222
* Return a stream of document changes on a query. These results are in sort order.
2323
* @param query
2424
*/
25-
export function sortedChanges<T>(query: Query, events: DocumentChangeType[], options?: firestore.SnapshotListenOptions): Observable<DocumentChangeAction<T>[]> {
25+
export function sortedChanges<T>(query: Query, events: DocumentChangeType[]): Observable<DocumentChangeAction<T>[]> {
26+
const options: firestore.SnapshotListenOptions = events.indexOf('metadata') > 0 ? { includeMetadataChanges: true } : {};
27+
let firstEmission = true;
2628
return fromCollectionRef(query, options)
2729
.pipe(
28-
map(changes => changes.payload.docChanges(options)),
29-
scan((current, changes) => combineChanges(current, changes, events), []),
30-
map(changes => changes.map(c => ({ type: c.type, payload: c } as DocumentChangeAction<T>))));
30+
switchMap(changes => {
31+
const docChanges = changes.payload.docChanges(options);
32+
if (firstEmission) {
33+
return of({ type: 'value', payload: combineChanges([], docChanges, events) } as DocumentChangeOrMetadata<T>);
34+
} else if (firstEmission && docChanges.length > 0) {
35+
return of(...docChanges);
36+
} else {
37+
return of({ type: 'metadata', payload: changes.payload.metadata } as DocumentChangeOrMetadata<T>);
38+
}
39+
}),
40+
tap(() => firstEmission = false),
41+
tap(change => console.log("change", change)),
42+
filter(change => events.indexOf(change.type) > -1),
43+
scan((current, changes) => combineChanges(current, changes, events), new Array<DocumentChange<T>>()),
44+
map(changes => changes.map(c => ({ type: c.type, payload: c } as DocumentChangeAction<T>)))
45+
);
3146
}
3247

3348
/**
@@ -37,7 +52,7 @@ export function sortedChanges<T>(query: Query, events: DocumentChangeType[], opt
3752
* @param changes
3853
* @param events
3954
*/
40-
export function combineChanges<T>(current: DocumentChange<T>[], changes: DocumentChange<T>[], events: DocumentChangeType[]) {
55+
export function combineChanges<T>(current: DocumentChange<T>[], changes: DocumentChangeOrMetadata<T>[], events: DocumentChangeType[]) {
4156
changes.forEach(change => {
4257
// skip unwanted change types
4358
if(events.indexOf(change.type) > -1) {
@@ -52,32 +67,39 @@ export function combineChanges<T>(current: DocumentChange<T>[], changes: Documen
5267
* @param combined
5368
* @param change
5469
*/
55-
export function combineChange<T>(combined: DocumentChange<T>[], change: DocumentChange<T>): DocumentChange<T>[] {
70+
export function combineChange<T>(combined: DocumentChange<T>[], change: DocumentChangeOrMetadata<T>): DocumentChange<T>[] {
71+
if (change.type == 'value') { return change.payload }
72+
const next = [...combined];
5673
switch(change.type) {
5774
case 'added':
58-
if (combined[change.newIndex] && combined[change.newIndex].doc.id == change.doc.id) {
75+
if (next[change.newIndex] && next[change.newIndex].doc.id == change.doc.id) {
5976
// Not sure why the duplicates are getting fired
6077
} else {
61-
combined.splice(change.newIndex, 0, change);
78+
next.splice(change.newIndex, 0, change);
6279
}
6380
break;
6481
case 'modified':
65-
if (combined[change.oldIndex] == null || combined[change.oldIndex].doc.id == change.doc.id) {
82+
if (next[change.oldIndex] == null || next[change.oldIndex].doc.id == change.doc.id) {
6683
// When an item changes position we first remove it
6784
// and then add it's new position
6885
if(change.oldIndex !== change.newIndex) {
69-
combined.splice(change.oldIndex, 1);
70-
combined.splice(change.newIndex, 0, change);
86+
next.splice(change.oldIndex, 1);
87+
next.splice(change.newIndex, 0, change);
7188
} else {
72-
combined.splice(change.newIndex, 1, change);
89+
next.splice(change.newIndex, 1, change);
7390
}
7491
}
7592
break;
7693
case 'removed':
77-
if (combined[change.oldIndex] && combined[change.oldIndex].doc.id == change.doc.id) {
78-
combined.splice(change.oldIndex, 1);
94+
if (next[change.oldIndex] && next[change.oldIndex].doc.id == change.doc.id) {
95+
next.splice(change.oldIndex, 1);
7996
}
8097
break;
98+
case 'metadata':
99+
next.forEach(c => {
100+
(<any>c.doc.metadata) = change.payload;
101+
});
102+
break;
81103
}
82-
return combined;
104+
return next;
83105
}

src/firestore/collection/collection.spec.ts

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ describe('AngularFirestoreCollection', () => {
4949
const ITEMS = 4;
5050
const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS);
5151

52-
const sub = stocks.valueChanges({idField: 'id', metadataField: 'meta'}).subscribe(data => {
52+
const sub = stocks.valueChanges().subscribe(data => {
5353
// unsub immediately as we will be deleting data at the bottom
5454
// and that will trigger another subscribe callback and fail
5555
// the test
@@ -59,8 +59,6 @@ describe('AngularFirestoreCollection', () => {
5959
// if the collection state is altered during a test run
6060
expect(data.length).toEqual(ITEMS);
6161
data.forEach(stock => {
62-
console.log(stock.id);
63-
console.log(stock.meta);
6462
// We used the same piece of data so they should all equal
6563
expect(stock).toEqual(FAKE_STOCK_DATA);
6664
});
@@ -75,13 +73,41 @@ describe('AngularFirestoreCollection', () => {
7573
const ITEMS = 1;
7674
const { ref, stocks, names } = await collectionHarness(afs, ITEMS);
7775
const idField = 'myCustomID';
78-
const sub = stocks.valueChanges({idField}).subscribe(data => {
79-
sub.unsubscribe();
76+
stocks.valueChanges({idField}).pipe(take(1)).subscribe(data => {
8077
const stock = data[0];
8178
expect(stock[idField]).toBeDefined();
8279
expect(stock).toEqual(jasmine.objectContaining(FAKE_STOCK_DATA));
83-
deleteThemAll(names, ref).then(done).catch(fail);
84-
})
80+
}).add(() =>
81+
deleteThemAll(names, ref).then(done).catch(fail)
82+
);
83+
});
84+
85+
it('should optionally map the metadata to the emitted data object', async (done: any) => {
86+
const ITEMS = 1;
87+
const { ref, stocks, names } = await collectionHarness(afs, ITEMS);
88+
const metadataField = 'myCustomMeta';
89+
stocks.valueChanges({metadataField}).pipe(take(1)).subscribe(data => {
90+
const stock = data[0];
91+
expect(stock[metadataField]).toBeDefined();
92+
expect(stock).toEqual(jasmine.objectContaining(FAKE_STOCK_DATA));
93+
}).add(() =>
94+
deleteThemAll(names, ref).then(done).catch(fail)
95+
);
96+
});
97+
98+
it('should optionally map the doc ID and metadata to the emitted data object', async (done: any) => {
99+
const ITEMS = 1;
100+
const { ref, stocks, names } = await collectionHarness(afs, ITEMS);
101+
const idField = 'myCustomID';
102+
const metadataField = 'myCustomMeta';
103+
stocks.valueChanges({idField, metadataField}).pipe(take(1)).subscribe(data => {
104+
const stock = data[0];
105+
expect(stock[idField]).toBeDefined();
106+
expect(stock[metadataField]).toBeDefined();
107+
expect(stock).toEqual(jasmine.objectContaining(FAKE_STOCK_DATA));
108+
}).add(() => {
109+
deleteThemAll(names, ref).then(done).catch(fail)
110+
});
85111
});
86112

87113
it('should handle multiple subscriptions (hot)', async (done: any) => {
@@ -235,6 +261,28 @@ describe('AngularFirestoreCollection', () => {
235261
delayUpdate(stocks, names[0], { price: 2 });
236262
});
237263

264+
it('should be able to includeMetadataChanges', async (done) => {
265+
const ITEMS = 10;
266+
const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS);
267+
var count = 0;
268+
269+
stocks.snapshotChanges(['modified', 'metadata']).pipe(take(3)).subscribe(data => {
270+
console.log("data", pending, data);
271+
if (count == 0) {
272+
delayUpdate(stocks, names[0], { price: 4 });
273+
} else {
274+
const change = data.filter(x => x.payload.doc.id === names[0])[0];
275+
console.log(pending, change.payload.doc.metadata);
276+
expect(data.length).toEqual(1);
277+
expect(change.payload.doc.data().price).toEqual(4);
278+
expect(change.type).toEqual('modified');
279+
expect(change.payload.doc.metadata.hasPendingWrites).toEqual(count == 1);
280+
expect(change.payload.doc.metadata.fromCache).toEqual(count == 1);
281+
}
282+
});
283+
284+
});
285+
238286
it('should be able to filter snapshotChanges() types - added', async (done) => {
239287
const ITEMS = 10;
240288
let { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS);

src/firestore/collection/collection.ts

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,13 @@ import { runInZone } from '@angular/fire';
1111

1212
export function validateEventsArray(events?: DocumentChangeType[]) {
1313
if(!events || events!.length === 0) {
14-
events = ['added', 'removed', 'modified'];
14+
events = ['value', 'added', 'removed', 'modified']; // SEMVER @ v6 add 'metadata'
1515
}
1616
return events;
1717
}
1818

1919
export type ChangeOptions = firestore.SnapshotListenOptions & { events?: DocumentChangeType[] };
2020

21-
// SEMVER @ v6 only allow options
2221
/**
2322
* AngularFirestoreCollection service
2423
*
@@ -64,16 +63,14 @@ export class AngularFirestoreCollection<T=DocumentData> {
6463
* your own data structure.
6564
* @param events
6665
*/
67-
stateChanges(eventsOrOptions?: DocumentChangeType[]|ChangeOptions): Observable<DocumentChangeAction<T>[]> {
68-
const events = eventsOrOptions && (Array.isArray(eventsOrOptions) ? eventsOrOptions : eventsOrOptions.events) || [];
69-
const options = eventsOrOptions && !Array.isArray(eventsOrOptions) ? eventsOrOptions : {};
70-
const ret = this.afs.scheduler.keepUnstableUntilFirst(
66+
stateChanges(events?: DocumentChangeType[]): Observable<DocumentChangeAction<T>[]> {
67+
const validatedEvents = validateEventsArray(events);
68+
return this.afs.scheduler.keepUnstableUntilFirst(
7169
this.afs.scheduler.runOutsideAngular(
72-
docChanges<T>(this.query, options)
70+
docChanges<T>(this.query)
7371
)
74-
);
75-
return events.length === 0 ? ret : ret.pipe(
76-
map(actions => actions.filter(change => events.indexOf(change.type) > -1)),
72+
).pipe(
73+
map(actions => actions.filter(change => validatedEvents.indexOf(change.type) > -1)),
7774
filter(changes => changes.length > 0)
7875
);
7976
}
@@ -83,20 +80,18 @@ export class AngularFirestoreCollection<T=DocumentData> {
8380
* but it collects each event in an array over time.
8481
* @param events
8582
*/
86-
auditTrail(eventsOrOptions?: DocumentChangeType[]|ChangeOptions): Observable<DocumentChangeAction<T>[]> {
87-
return this.stateChanges(eventsOrOptions).pipe(scan((current, action) => [...current, ...action], []));
83+
auditTrail(events?: DocumentChangeType[]): Observable<DocumentChangeAction<T>[]> {
84+
return this.stateChanges(events).pipe(scan((current, action) => [...current, ...action], []));
8885
}
8986

9087
/**
9188
* Create a stream of synchronized changes. This method keeps the local array in sorted
9289
* query order.
9390
* @param events
9491
*/
95-
snapshotChanges(eventsOrOptions?: DocumentChangeType[]|ChangeOptions): Observable<DocumentChangeAction<T>[]> {
96-
const events = eventsOrOptions && (Array.isArray(eventsOrOptions) ? eventsOrOptions : eventsOrOptions.events) || [];
97-
const options = eventsOrOptions && !Array.isArray(eventsOrOptions) ? eventsOrOptions : {};
92+
snapshotChanges(events?: DocumentChangeType[]): Observable<DocumentChangeAction<T>[]> {
9893
const validatedEvents = validateEventsArray(events);
99-
const sortedChanges$ = sortedChanges<T>(this.query, validatedEvents, options);
94+
const sortedChanges$ = sortedChanges<T>(this.query, validatedEvents);
10095
const scheduledSortedChanges$ = this.afs.scheduler.runOutsideAngular(sortedChanges$);
10196
return this.afs.scheduler.keepUnstableUntilFirst(scheduledSortedChanges$);
10297
}
@@ -118,6 +113,7 @@ export class AngularFirestoreCollection<T=DocumentData> {
118113
const scheduled$ = this.afs.scheduler.runOutsideAngular(fromCollectionRef$);
119114
return this.afs.scheduler.keepUnstableUntilFirst(scheduled$)
120115
.pipe(
116+
// TODO do we need to handle no doc changes, like snapshot?
121117
map(actions => actions.payload.docs.map(a => ({
122118
...(a.data() as Object),
123119
...(options.metadataField ? { [options.metadataField]: a.metadata } : {}),

src/firestore/interfaces.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export type Settings = firestore.Settings;
55
export type CollectionReference = firestore.CollectionReference;
66
export type DocumentReference = firestore.DocumentReference;
77
export type PersistenceSettings = firestore.PersistenceSettings;
8-
export type DocumentChangeType = firestore.DocumentChangeType;
8+
export type DocumentChangeType = firestore.DocumentChangeType | "metadata" | "value";
99
export type SnapshotOptions = firestore.SnapshotOptions;
1010
export type FieldPath = firestore.FieldPath;
1111
export type Query = firestore.Query;
@@ -38,6 +38,14 @@ export interface DocumentChange<T> extends firestore.DocumentChange {
3838
readonly doc: QueryDocumentSnapshot<T>;
3939
}
4040

41+
export type DocumentChangeOrMetadata<T> = DocumentChange<T> | {
42+
type: 'metadata';
43+
payload: firestore.SnapshotMetadata;
44+
} | {
45+
type: 'value',
46+
payload: DocumentChange<T>[]
47+
}
48+
4149
export interface DocumentChangeAction<T> {
4250
type: DocumentChangeType;
4351
payload: DocumentChange<T>;

src/firestore/observable/fromRef.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,7 @@ export function fromDocRef<T>(ref: DocumentReference, options?: firestore.Snapsh
2222
}
2323

2424
export function fromCollectionRef<T>(ref: Query, options?: firestore.SnapshotListenOptions): Observable<Action<QuerySnapshot<T>>> {
25-
return fromRef<QuerySnapshot<T>>(ref, options).pipe(map(payload => ({ payload, type: 'query' })));
25+
return fromRef<QuerySnapshot<T>>(ref, options).pipe(
26+
map(payload => ({ payload, type: 'query' }))
27+
);
2628
}

src/root.spec.js

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
// These paths are written to use the dist build
2-
export * from './packages-dist/angularfire2.spec';
3-
export * from './packages-dist/auth/auth.spec';
4-
export * from './packages-dist/firestore/firestore.spec';
5-
export * from './packages-dist/firestore/document/document.spec';
2+
//export * from './packages-dist/angularfire2.spec';
3+
//export * from './packages-dist/auth/auth.spec';
4+
//export * from './packages-dist/firestore/firestore.spec';
5+
//export * from './packages-dist/firestore/document/document.spec';
66
export * from './packages-dist/firestore/collection/collection.spec';
7-
export * from './packages-dist/functions/functions.spec';
8-
export * from './packages-dist/database/database.spec';
9-
export * from './packages-dist/database/utils.spec';
10-
export * from './packages-dist/database/observable/fromRef.spec';
11-
export * from './packages-dist/database/list/changes.spec';
12-
export * from './packages-dist/database/list/snapshot-changes.spec';
13-
export * from './packages-dist/database/list/state-changes.spec';
14-
export * from './packages-dist/database/list/audit-trail.spec';
15-
export * from './packages-dist/storage/storage.spec';
16-
export * from './packages-dist/performance/performance.spec';
7+
//export * from './packages-dist/functions/functions.spec';
8+
//export * from './packages-dist/database/database.spec';
9+
//export * from './packages-dist/database/utils.spec';
10+
//export * from './packages-dist/database/observable/fromRef.spec';
11+
//export * from './packages-dist/database/list/changes.spec';
12+
//export * from './packages-dist/database/list/snapshot-changes.spec';
13+
//export * from './packages-dist/database/list/state-changes.spec';
14+
//export * from './packages-dist/database/list/audit-trail.spec';
15+
//export * from './packages-dist/storage/storage.spec';
16+
//export * from './packages-dist/performance/performance.spec';
1717
//export * from './packages-dist/messaging/messaging.spec';
1818

1919
// // Since this a deprecated API, we run on it on manual tests only

0 commit comments

Comments
 (0)