From e65281923154cb02e942a487eb0786d6ada27ff4 Mon Sep 17 00:00:00 2001 From: David East Date: Mon, 13 Aug 2018 14:57:13 -0600 Subject: [PATCH 1/3] feat(firestore): Add support for SnapshotListenOptions --- src/firestore/collection/changes.ts | 11 ++++++----- src/firestore/collection/collection.ts | 19 ++++++++++--------- src/firestore/interfaces.ts | 4 +--- src/firestore/observable/fromRef.ts | 17 +++++++++-------- 4 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/firestore/collection/changes.ts b/src/firestore/collection/changes.ts index dfa415bd0..50fe743a9 100644 --- a/src/firestore/collection/changes.ts +++ b/src/firestore/collection/changes.ts @@ -1,6 +1,7 @@ import { fromCollectionRef } from '../observable/fromRef'; import { Observable } from 'rxjs'; -import { map, filter, scan } from 'rxjs/operators'; +import { map, scan } from 'rxjs/operators'; +import { firestore } from 'firebase'; import { Query, DocumentChangeType, DocumentChange, DocumentChangeAction, Action } from '../interfaces'; @@ -9,8 +10,8 @@ import { Query, DocumentChangeType, DocumentChange, DocumentChangeAction, Action * order of occurence. * @param query */ -export function docChanges(query: Query): Observable[]> { - return fromCollectionRef(query) +export function docChanges(query: Query, options?: firestore.SnapshotListenOptions): Observable[]> { + return fromCollectionRef(query, options) .pipe( map(action => action.payload.docChanges() @@ -21,8 +22,8 @@ export function docChanges(query: Query): Observable[ * Return a stream of document changes on a query. These results are in sort order. * @param query */ -export function sortedChanges(query: Query, events: DocumentChangeType[]): Observable[]> { - return fromCollectionRef(query) +export function sortedChanges(query: Query, events: DocumentChangeType[], options?: firestore.SnapshotListenOptions): Observable[]> { + return fromCollectionRef(query, options) .pipe( map(changes => changes.payload.docChanges()), scan((current, changes) => combineChanges(current, changes, events), []), diff --git a/src/firestore/collection/collection.ts b/src/firestore/collection/collection.ts index 6513c12cd..33a1bc212 100644 --- a/src/firestore/collection/collection.ts +++ b/src/firestore/collection/collection.ts @@ -1,6 +1,7 @@ import { Observable, Subscriber } from 'rxjs'; import { fromCollectionRef } from '../observable/fromRef'; import { map, filter, scan } from 'rxjs/operators'; +import { firestore } from 'firebase'; import { Injectable } from '@angular/core'; @@ -61,17 +62,17 @@ export class AngularFirestoreCollection { * your own data structure. * @param events */ - stateChanges(events?: DocumentChangeType[]): Observable[]> { + stateChanges(events?: DocumentChangeType[], options?: firestore.SnapshotListenOptions): Observable[]> { if(!events || events.length === 0) { return this.afs.scheduler.keepUnstableUntilFirst( this.afs.scheduler.runOutsideAngular( - docChanges(this.query) + docChanges(this.query, options) ) ); } return this.afs.scheduler.keepUnstableUntilFirst( this.afs.scheduler.runOutsideAngular( - docChanges(this.query) + docChanges(this.query, options) ) ) .pipe( @@ -85,8 +86,8 @@ export class AngularFirestoreCollection { * but it collects each event in an array over time. * @param events */ - auditTrail(events?: DocumentChangeType[]): Observable[]> { - return this.stateChanges(events).pipe(scan((current, action) => [...current, ...action], [])); + auditTrail(events?: DocumentChangeType[], options?: firestore.SnapshotListenOptions): Observable[]> { + return this.stateChanges(events, options).pipe(scan((current, action) => [...current, ...action], [])); } /** @@ -94,9 +95,9 @@ export class AngularFirestoreCollection { * query order. * @param events */ - snapshotChanges(events?: DocumentChangeType[]): Observable[]> { + snapshotChanges(events?: DocumentChangeType[], options?: firestore.SnapshotListenOptions): Observable[]> { const validatedEvents = validateEventsArray(events); - const sortedChanges$ = sortedChanges(this.query, validatedEvents); + const sortedChanges$ = sortedChanges(this.query, validatedEvents, options); const scheduledSortedChanges$ = this.afs.scheduler.runOutsideAngular(sortedChanges$); return this.afs.scheduler.keepUnstableUntilFirst(scheduledSortedChanges$); } @@ -104,8 +105,8 @@ export class AngularFirestoreCollection { /** * Listen to all documents in the collection and its possible query as an Observable. */ - valueChanges(): Observable { - const fromCollectionRef$ = fromCollectionRef(this.query); + valueChanges(options?: firestore.SnapshotListenOptions): Observable { + const fromCollectionRef$ = fromCollectionRef(this.query, options); const scheduled$ = this.afs.scheduler.runOutsideAngular(fromCollectionRef$); return this.afs.scheduler.keepUnstableUntilFirst(scheduled$) .pipe( diff --git a/src/firestore/interfaces.ts b/src/firestore/interfaces.ts index 55985d5d9..3e4b2258f 100644 --- a/src/firestore/interfaces.ts +++ b/src/firestore/interfaces.ts @@ -48,9 +48,7 @@ export interface Action { payload: T; }; -export interface Reference { - onSnapshot: (sub: Subscriber) => any; -} +export interface Reference extends Query { } // A convience type for making a query. // Example: const query = (ref) => ref.where('name', == 'david'); diff --git a/src/firestore/observable/fromRef.ts b/src/firestore/observable/fromRef.ts index 4c3f0aed7..46cc54b42 100644 --- a/src/firestore/observable/fromRef.ts +++ b/src/firestore/observable/fromRef.ts @@ -1,25 +1,26 @@ import { Observable, Subscriber } from 'rxjs'; import { DocumentReference, Query, Action, Reference, DocumentSnapshot, QuerySnapshot } from '../interfaces'; import { map, share } from 'rxjs/operators'; +import { firestore } from 'firebase'; -function _fromRef(ref: Reference): Observable { +function _fromRef(ref: any, options?: firestore.SnapshotListenOptions): Observable { return new Observable(subscriber => { - const unsubscribe = ref.onSnapshot(subscriber); + const unsubscribe = ref.onSnapshot(options || {}, ref as any) return { unsubscribe }; }); } -export function fromRef(ref: DocumentReference | Query) { - return _fromRef(ref).pipe(share()); +export function fromRef(ref: any, options?: firestore.SnapshotListenOptions) { + return _fromRef(ref, options).pipe(share()); } -export function fromDocRef(ref: DocumentReference): Observable>>{ - return fromRef>(ref) +export function fromDocRef(ref: any, options?: firestore.SnapshotListenOptions): Observable>>{ + return fromRef>(options, ref) .pipe( map(payload => ({ payload, type: 'value' })) ); } -export function fromCollectionRef(ref: Query): Observable>> { - return fromRef>(ref).pipe(map(payload => ({ payload, type: 'query' }))); +export function fromCollectionRef(ref: Query, options?: firestore.SnapshotListenOptions,): Observable>> { + return fromRef>(ref, options).pipe(map(payload => ({ payload, type: 'query' }))); } From 0e3d1a7819cc48890522f69fd60f5ca48fbf0db8 Mon Sep 17 00:00:00 2001 From: David East Date: Mon, 13 Aug 2018 16:04:53 -0600 Subject: [PATCH 2/3] feat(firestore): overload options --- src/firestore/collection/collection.spec.ts | 12 +++++++ src/firestore/collection/collection.ts | 37 ++++++++++++++++----- src/firestore/document/document.spec.ts | 8 ++--- src/firestore/document/document.ts | 8 ++--- src/firestore/observable/fromRef.ts | 14 ++++---- 5 files changed, 53 insertions(+), 26 deletions(-) diff --git a/src/firestore/collection/collection.spec.ts b/src/firestore/collection/collection.spec.ts index 4e4605402..ea447a839 100644 --- a/src/firestore/collection/collection.spec.ts +++ b/src/firestore/collection/collection.spec.ts @@ -203,6 +203,7 @@ describe('AngularFirestoreCollection', () => { deleteThemAll(names, ref).then(done).catch(done.fail); } }); + }); it('should be able to filter snapshotChanges() types - modified', async (done) => { @@ -286,6 +287,17 @@ describe('AngularFirestoreCollection', () => { delayDelete(stocks, names[0], 400); }); + it('should work with snapshot listener options', async (done: any) => { + const ITEMS = 4; + const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS); + debugger; + const sub = stocks.snapshotChanges({ includeMetadataChanges: true }).subscribe(actions => { + sub.unsubscribe(); + debugger; + done(); + }); + }); + }); describe('stateChanges()', () => { diff --git a/src/firestore/collection/collection.ts b/src/firestore/collection/collection.ts index 33a1bc212..bade567f4 100644 --- a/src/firestore/collection/collection.ts +++ b/src/firestore/collection/collection.ts @@ -3,8 +3,6 @@ import { fromCollectionRef } from '../observable/fromRef'; import { map, filter, scan } from 'rxjs/operators'; import { firestore } from 'firebase'; -import { Injectable } from '@angular/core'; - import { DocumentChangeType, CollectionReference, Query, DocumentReference, DocumentData, QueryFn, AssociatedReference, DocumentChangeAction, DocumentChange } from '../interfaces'; import { docChanges, sortedChanges } from './changes'; import { AngularFirestoreDocument } from '../document/document'; @@ -17,6 +15,17 @@ export function validateEventsArray(events?: DocumentChangeType[]) { return events; } +function validateEventsOrOptions(eventsOrOptions, options) { + let events: DocumentChangeType[] = []; + let listenerOptions: firestore.SnapshotListenOptions | undefined = options; + if(Array.isArray(eventsOrOptions)) { + events = eventsOrOptions; + } else { + listenerOptions = eventsOrOptions; + } + return { events, listenerOptions }; +} + /** * AngularFirestoreCollection service * @@ -62,17 +71,18 @@ export class AngularFirestoreCollection { * your own data structure. * @param events */ - stateChanges(events?: DocumentChangeType[], options?: firestore.SnapshotListenOptions): Observable[]> { + stateChanges(eventsOrOptions?: DocumentChangeType[] | firestore.SnapshotListenOptions, options?: firestore.SnapshotListenOptions): Observable[]> { + const { events, listenerOptions } = validateEventsOrOptions(eventsOrOptions, options); if(!events || events.length === 0) { return this.afs.scheduler.keepUnstableUntilFirst( this.afs.scheduler.runOutsideAngular( - docChanges(this.query, options) + docChanges(this.query, listenerOptions) ) ); } return this.afs.scheduler.keepUnstableUntilFirst( this.afs.scheduler.runOutsideAngular( - docChanges(this.query, options) + docChanges(this.query, listenerOptions) ) ) .pipe( @@ -86,8 +96,9 @@ export class AngularFirestoreCollection { * but it collects each event in an array over time. * @param events */ - auditTrail(events?: DocumentChangeType[], options?: firestore.SnapshotListenOptions): Observable[]> { - return this.stateChanges(events, options).pipe(scan((current, action) => [...current, ...action], [])); + auditTrail(eventsOrOptions?: DocumentChangeType[] | firestore.SnapshotListenOptions, options?: firestore.SnapshotListenOptions): Observable[]> { + const { events, listenerOptions } = validateEventsOrOptions(eventsOrOptions, options); + return this.stateChanges(events, listenerOptions).pipe(scan((current, action) => [...current, ...action], [])); } /** @@ -95,9 +106,17 @@ export class AngularFirestoreCollection { * query order. * @param events */ - snapshotChanges(events?: DocumentChangeType[], options?: firestore.SnapshotListenOptions): Observable[]> { + snapshotChanges(eventsOrOptions?: DocumentChangeType[] | firestore.SnapshotListenOptions, options?: firestore.SnapshotListenOptions): Observable[]> { + let events: DocumentChangeType[] = []; + let listenerOptions: firestore.SnapshotListenOptions | undefined = options; + if(Array.isArray(eventsOrOptions)) { + events = eventsOrOptions; + } else { + listenerOptions = eventsOrOptions; + } + const validatedEvents = validateEventsArray(events); - const sortedChanges$ = sortedChanges(this.query, validatedEvents, options); + const sortedChanges$ = sortedChanges(this.query, validatedEvents, listenerOptions); const scheduledSortedChanges$ = this.afs.scheduler.runOutsideAngular(sortedChanges$); return this.afs.scheduler.keepUnstableUntilFirst(scheduledSortedChanges$); } diff --git a/src/firestore/document/document.spec.ts b/src/firestore/document/document.spec.ts index 276f4c645..7f080e3b4 100644 --- a/src/firestore/document/document.spec.ts +++ b/src/firestore/document/document.spec.ts @@ -2,7 +2,7 @@ import { FirebaseApp, AngularFireModule } from 'angularfire2'; import { AngularFirestore } from '../firestore'; import { AngularFirestoreModule } from '../firestore.module'; import { AngularFirestoreDocument } from '../document/document'; -import { Observable, Subscription } from 'rxjs'; +import { Subscription } from 'rxjs'; import { take } from 'rxjs/operators'; import { TestBed, inject } from '@angular/core/testing'; @@ -40,10 +40,10 @@ describe('AngularFirestoreDocument', () => { await stock.set(FAKE_STOCK_DATA); const sub = stock .snapshotChanges() - .subscribe(async a => { + .subscribe(a => { sub.unsubscribe(); - if (a.payload.exists) { - expect(a.payload.data()).toEqual(FAKE_STOCK_DATA); + if (a.exists) { + expect(a.data()).toEqual(FAKE_STOCK_DATA); stock.delete().then(done).catch(done.fail); } }); diff --git a/src/firestore/document/document.ts b/src/firestore/document/document.ts index 39d554b05..08f24d70c 100644 --- a/src/firestore/document/document.ts +++ b/src/firestore/document/document.ts @@ -3,8 +3,6 @@ import { DocumentReference, SetOptions, DocumentData, QueryFn, AssociatedReferen import { fromDocRef } from '../observable/fromRef'; import { map } from 'rxjs/operators'; -import { Injectable } from '@angular/core'; - import { AngularFirestore, associateQuery } from '../firestore'; import { AngularFirestoreCollection } from '../collection/collection'; @@ -78,7 +76,7 @@ export class AngularFirestoreDocument { /** * Listen to snapshot updates from the document. */ - snapshotChanges(): Observable>> { + snapshotChanges(): Observable> { const fromDocRef$ = fromDocRef(this.ref); const scheduledFromDocRef$ = this.afs.scheduler.runOutsideAngular(fromDocRef$); return this.afs.scheduler.keepUnstableUntilFirst(scheduledFromDocRef$); @@ -89,9 +87,7 @@ export class AngularFirestoreDocument { */ valueChanges(): Observable { return this.snapshotChanges().pipe( - map(action => { - return action.payload.data(); - }) + map(snap => snap.data()) ); } } diff --git a/src/firestore/observable/fromRef.ts b/src/firestore/observable/fromRef.ts index 46cc54b42..2ede6ac85 100644 --- a/src/firestore/observable/fromRef.ts +++ b/src/firestore/observable/fromRef.ts @@ -3,9 +3,9 @@ import { DocumentReference, Query, Action, Reference, DocumentSnapshot, QuerySna import { map, share } from 'rxjs/operators'; import { firestore } from 'firebase'; -function _fromRef(ref: any, options?: firestore.SnapshotListenOptions): Observable { +function _fromRef(ref: Query, options?: firestore.SnapshotListenOptions): Observable { return new Observable(subscriber => { - const unsubscribe = ref.onSnapshot(options || {}, ref as any) + const unsubscribe = ref.onSnapshot(options || {}, subscriber as any) return { unsubscribe }; }); } @@ -14,11 +14,11 @@ export function fromRef(ref: any, options?: firestore.SnapshotListenOptions) return _fromRef(ref, options).pipe(share()); } -export function fromDocRef(ref: any, options?: firestore.SnapshotListenOptions): Observable>>{ - return fromRef>(options, ref) - .pipe( - map(payload => ({ payload, type: 'value' })) - ); +export function fromDocRef(ref: DocumentReference): Observable>{ + return new Observable(subscriber => { + const unsubscribe = ref.onSnapshot(subscriber as any) + return { unsubscribe }; + }); } export function fromCollectionRef(ref: Query, options?: firestore.SnapshotListenOptions,): Observable>> { From 30dd2a92fb82845ac9e1140a6936d105f3f8326a Mon Sep 17 00:00:00 2001 From: David East Date: Mon, 13 Aug 2018 16:22:14 -0600 Subject: [PATCH 3/3] tests for options --- src/firestore/collection/collection.spec.ts | 35 ++++++++++++++------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/firestore/collection/collection.spec.ts b/src/firestore/collection/collection.spec.ts index ea447a839..1327067f3 100644 --- a/src/firestore/collection/collection.spec.ts +++ b/src/firestore/collection/collection.spec.ts @@ -275,7 +275,7 @@ describe('AngularFirestoreCollection', () => { const ITEMS = 10; const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS); - const sub = stocks.snapshotChanges(['added', 'removed']).pipe(skip(1)).subscribe(data => { + const sub = stocks.snapshotChanges(['added', 'removed'], { includeMetadataChanges: true }).pipe(skip(1)).subscribe(data => { sub.unsubscribe(); const change = data.filter(x => x.payload.doc.id === names[0]); expect(data.length).toEqual(ITEMS - 1); @@ -287,14 +287,27 @@ describe('AngularFirestoreCollection', () => { delayDelete(stocks, names[0], 400); }); - it('should work with snapshot listener options', async (done: any) => { - const ITEMS = 4; + it('should listen to all snapshotChanges() by default with listener options', async (done) => { + const ITEMS = 10; + let count = 0; const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS); - debugger; - const sub = stocks.snapshotChanges({ includeMetadataChanges: true }).subscribe(actions => { - sub.unsubscribe(); - debugger; - done(); + const sub = stocks.snapshotChanges({ includeMetadataChanges: true }).subscribe(data => { + const ids = data.map(d => d.payload.doc.id); + count = count + 1; + // the first time should all be 'added' + if(count === 1) { + // make an update + stocks.doc(names[0]).update({ price: 2}); + } + // on the second round, make sure the array is still the same + // length but the updated item is now modified + if(count === 2) { + expect(data.length).toEqual(ITEMS); + const change = data.filter(x => x.payload.doc.id === names[0])[0]; + expect(change.type).toEqual('modified'); + sub.unsubscribe(); + deleteThemAll(names, ref).then(done).catch(done.fail); + } }); }); @@ -358,7 +371,7 @@ describe('AngularFirestoreCollection', () => { it('should handle multiple subscriptions (warm)', async (done: any) => { const ITEMS = 4; const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS); - const changes = stocks.stateChanges(); + const changes = stocks.stateChanges({ includeMetadataChanges: true }); changes.pipe(take(1)).subscribe(() => {}).add(() => { const sub = changes.pipe(take(1)).subscribe(data => { expect(data.length).toEqual(ITEMS); @@ -373,7 +386,7 @@ describe('AngularFirestoreCollection', () => { let count = 0; const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS); - const sub = stocks.stateChanges(['modified']).subscribe(data => { + const sub = stocks.stateChanges(['modified'], { includeMetadataChanges: true }).subscribe(data => { sub.unsubscribe(); expect(data.length).toEqual(1); expect(data[0].payload.doc.data().price).toEqual(2); @@ -443,7 +456,7 @@ describe('AngularFirestoreCollection', () => { const ITEMS = 10; const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS); - const sub = stocks.auditTrail(['removed']).subscribe(data => { + const sub = stocks.auditTrail(['removed'], { includeMetadataChanges: true }).subscribe(data => { sub.unsubscribe(); expect(data.length).toEqual(1); expect(data[0].type).toEqual('removed');