From bbc5559486083a0a86342d494512cd63bfe76437 Mon Sep 17 00:00:00 2001 From: David East Date: Mon, 2 Jul 2018 10:58:46 -0600 Subject: [PATCH 01/14] initial database code --- packages/rxfire/database/fromRef.ts | 36 ++++++++++ packages/rxfire/database/index.ts | 4 ++ packages/rxfire/database/interfaces.ts | 11 ++++ packages/rxfire/database/list.ts | 91 ++++++++++++++++++++++++++ packages/rxfire/database/object.ts | 12 ++++ packages/rxfire/database/package.json | 5 ++ packages/rxfire/database/utils.ts | 11 ++++ packages/rxfire/rollup.config.js | 6 +- packages/rxfire/test/database.test.ts | 64 ++++++++++++++++++ 9 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 packages/rxfire/database/fromRef.ts create mode 100644 packages/rxfire/database/index.ts create mode 100644 packages/rxfire/database/interfaces.ts create mode 100644 packages/rxfire/database/list.ts create mode 100644 packages/rxfire/database/object.ts create mode 100644 packages/rxfire/database/package.json create mode 100644 packages/rxfire/database/utils.ts create mode 100644 packages/rxfire/test/database.test.ts diff --git a/packages/rxfire/database/fromRef.ts b/packages/rxfire/database/fromRef.ts new file mode 100644 index 00000000000..ca19ea62ef3 --- /dev/null +++ b/packages/rxfire/database/fromRef.ts @@ -0,0 +1,36 @@ +import { database } from 'firebase'; +import { Observable } from 'rxjs'; +import { map, delay, share } from 'rxjs/operators'; +import { ListenEvent, SnapshotPrevKey } from './interfaces'; + +/** + * Create an observable from a Database Reference or Database Query. + * @param ref Database Reference + * @param event Listen event type ('value', 'added', 'changed', 'removed', 'moved') + */ +export function fromRef(ref: database.Query, event: ListenEvent, listenType = 'on'): Observable { + return new Observable(subscriber => { + const fn = ref[listenType](event, (snapshot, prevKey) => { + subscriber.next({ snapshot, prevKey, event }); + if (listenType == 'once') { subscriber.complete(); } + }, subscriber.error.bind(subscriber)); + if (listenType == 'on') { + return { unsubscribe() { ref.off(event, fn)} }; + } else { + return { unsubscribe() { } }; + } + }).pipe( + // Ensures subscribe on observable is async. This handles + // a quirk in the SDK where on/once callbacks can happen + // synchronously. + delay(0), + share() + ); +} + +export const unwrap = () => map((payload: SnapshotPrevKey) => { + const { snapshot, prevKey } = payload; + let key: string | null = null; + if (snapshot.exists()) { key = snapshot.key; } + return { type: event, payload: snapshot, prevKey, key }; +}); diff --git a/packages/rxfire/database/index.ts b/packages/rxfire/database/index.ts new file mode 100644 index 00000000000..e12a9ba9d01 --- /dev/null +++ b/packages/rxfire/database/index.ts @@ -0,0 +1,4 @@ +export * from './fromRef'; +export * from './interfaces'; +export * from './list'; +export * from './object'; diff --git a/packages/rxfire/database/interfaces.ts b/packages/rxfire/database/interfaces.ts new file mode 100644 index 00000000000..6496c0af7b4 --- /dev/null +++ b/packages/rxfire/database/interfaces.ts @@ -0,0 +1,11 @@ +import { database } from 'firebase'; + +export type QueryFn = (ref: database.Reference) => database.Query; +export type ChildEvent = 'child_added' | 'child_removed' | 'child_changed' | 'child_moved'; +export type ListenEvent = 'value' | ChildEvent; + +export interface SnapshotPrevKey { + snapshot: database.DataSnapshot; + prevKey: string | null | undefined; + event: ListenEvent; +} diff --git a/packages/rxfire/database/list.ts b/packages/rxfire/database/list.ts new file mode 100644 index 00000000000..2b16ea4b31d --- /dev/null +++ b/packages/rxfire/database/list.ts @@ -0,0 +1,91 @@ +import { database } from 'firebase'; +import { ChildEvent, SnapshotPrevKey } from './interfaces'; +import { Observable, of, merge } from 'rxjs'; +import { validateEventsArray, isNil } from './utils'; +import { fromRef } from './fromRef'; +import { switchMap, scan, distinctUntilChanged } from 'rxjs/operators'; + +export function stateChanges(query: database.Query, events?: ChildEvent[]) { + events = validateEventsArray(events); + const childEvent$ = events.map(event => fromRef(query, event)); + return merge(...childEvent$); +} + +export function list(query: database.Query, events?: ChildEvent[]): Observable { + events = validateEventsArray(events); + return fromRef(query, 'value', 'once').pipe( + switchMap(snapshotAction => { + const childEvent$ = [of(snapshotAction)]; + events.forEach(event => childEvent$.push(fromRef(query, event))); + return merge(...childEvent$).pipe(scan(buildView, [])) + }), + distinctUntilChanged() + ); +} + +function positionFor(changes: SnapshotPrevKey[], key) { + const len = changes.length; + for(let i=0; i(changes: SnapshotPrevKey[], prevKey?: string) { + if(isNil(prevKey)) { + return 0; + } else { + const i = positionFor(changes, prevKey); + if( i === -1) { + return changes.length; + } else { + return i + 1; + } + } +} + +function buildView(current: SnapshotPrevKey[], action: SnapshotPrevKey) { + const { snapshot, prevKey, event } = action; + const { key } = snapshot; + const currentKeyPosition = positionFor(current, key); + const afterPreviousKeyPosition = positionAfter(current, prevKey); + switch (event) { + case 'value': + if (action.snapshot && action.snapshot.exists()) { + current = [...current, action]; + } + return current; + case 'child_added': + if (currentKeyPosition > -1) { + // check that the previouskey is what we expect, else reorder + const previous = current[currentKeyPosition - 1]; + if ((previous && previous.snapshot.key || null) != prevKey) { + current = current.filter(x => x.snapshot.key !== snapshot.key); + current.splice(afterPreviousKeyPosition, 0, action); + } + } else if (prevKey == null) { + return [action, ...current]; + } else { + current = current.slice() + current.splice(afterPreviousKeyPosition, 0, action); + } + return current; + case 'child_removed': + return current.filter(x => x.snapshot.key !== snapshot.key); + case 'child_changed': + return current.map(x => x.snapshot.key === key ? action : x); + case 'child_moved': + if(currentKeyPosition > -1) { + const data = current.splice(currentKeyPosition, 1)[0]; + current = current.slice() + current.splice(afterPreviousKeyPosition, 0, data); + return current; + } + return current; + // default will also remove null results + default: + return current; + } +} diff --git a/packages/rxfire/database/object.ts b/packages/rxfire/database/object.ts new file mode 100644 index 00000000000..3eaaa7dc454 --- /dev/null +++ b/packages/rxfire/database/object.ts @@ -0,0 +1,12 @@ +import { database } from 'firebase'; +import { SnapshotPrevKey } from "./interfaces"; +import { fromRef } from './fromRef'; +import { Observable } from 'rxjs'; + +/** + * Get the snapshot changes of an object + * @param query + */ +export function object(query: database.Query): Observable { + return fromRef(query, 'value'); +} diff --git a/packages/rxfire/database/package.json b/packages/rxfire/database/package.json new file mode 100644 index 00000000000..aac222092e9 --- /dev/null +++ b/packages/rxfire/database/package.json @@ -0,0 +1,5 @@ +{ + "name": "rxfire/database", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js" +} diff --git a/packages/rxfire/database/utils.ts b/packages/rxfire/database/utils.ts new file mode 100644 index 00000000000..58c02e5a4cc --- /dev/null +++ b/packages/rxfire/database/utils.ts @@ -0,0 +1,11 @@ + +export function isNil(obj: any): boolean { + return obj === undefined || obj === null; +} + +export function validateEventsArray(events?: any[]) { + if(isNil(events) || events!.length === 0) { + events = ['child_added', 'child_removed', 'child_changed', 'child_moved']; + } + return events; +} diff --git a/packages/rxfire/rollup.config.js b/packages/rxfire/rollup.config.js index d5dfa677161..1e5dc49f9c8 100644 --- a/packages/rxfire/rollup.config.js +++ b/packages/rxfire/rollup.config.js @@ -25,12 +25,14 @@ import authPkg from './auth/package.json'; import storagePkg from './storage/package.json'; import functionsPkg from './functions/package.json'; import firestorePkg from './firestore/package.json'; +import databasePkg from './database/package.json'; const pkgsByName = { auth: authPkg, storage: storagePkg, functions: functionsPkg, - firestore: firestorePkg + firestore: firestorePkg, + database: databasePkg }; const plugins = [ @@ -48,7 +50,7 @@ const external = [...Object.keys(pkg.dependencies || {}), 'rxjs/operators']; */ const GLOBAL_NAME = 'rxfire'; -const components = ['auth', 'storage', 'functions', 'firestore']; +const components = ['auth', 'storage', 'functions', 'firestore', 'database']; const componentBuilds = components .map(component => { const pkg = pkgsByName[component]; diff --git a/packages/rxfire/test/database.test.ts b/packages/rxfire/test/database.test.ts new file mode 100644 index 00000000000..e5e06e82554 --- /dev/null +++ b/packages/rxfire/test/database.test.ts @@ -0,0 +1,64 @@ +import { expect } from 'chai'; +import { initializeApp, database, app } from 'firebase'; +import { fromRef } from '../database/fromRef'; + +let ref: (path: string) => database.Reference; +let batch = () => { + let batch = {}; + const items = [{ name: 'one' }, { name: 'two' }, { name: 'three' }] + .map(item => ( { key: createId(), ...item } )); + Object.keys(items).forEach(function (key) { + const itemValue = items[key]; + batch[itemValue.key] = itemValue; + }); + // make batch immutable to preserve integrity + return Object.freeze(batch); +} + +const createId = () => + Math.random() + .toString(36) + .substring(5); + +describe('RxFire Database', () => { + let app: app.App = null; + let database: database.Database = null; + + /** + * Each test runs inside it's own app instance and the app + * is deleted after the test runs. + * + * Database tests run "offline" to reduce "flakeyness". + * + * Each test is responsible for seeding and removing data. Helper + * functions are useful if the process becomes brittle or tedious. + * Note that removing is less necessary since the tests are run + * offline. + * + * Note: Database tests do not run exactly the same offline as + * they do online. Querying can act differently, tests must + * account for this. + */ + beforeEach(() => { + app = initializeApp({ projectId: 'rxfire-test-db' }); + database = app.database(); + database.goOffline(); + }); + + afterEach((done: MochaDone) => { + app.delete().then(() => done()); + }); + + describe('fromRef', () => { + + it('once should complete', (done) => { + const itemRef = ref(createId()); + itemRef.set(batch); + const obs = fromRef(itemRef, 'value', 'once'); + obs.subscribe(_ => {}, () => {}, done); + }); + + }); + + +}); From 9e319f62727e305e9e84bd4249d6784b51460b1b Mon Sep 17 00:00:00 2001 From: David East Date: Mon, 2 Jul 2018 11:21:45 -0600 Subject: [PATCH 02/14] test setup --- packages/rxfire/test/database.test.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/rxfire/test/database.test.ts b/packages/rxfire/test/database.test.ts index e5e06e82554..732402a0f36 100644 --- a/packages/rxfire/test/database.test.ts +++ b/packages/rxfire/test/database.test.ts @@ -23,6 +23,10 @@ const createId = () => describe('RxFire Database', () => { let app: app.App = null; let database: database.Database = null; + let ref = (path: string) => { + app.database().goOffline(); + return app.database().ref(path); + }; /** * Each test runs inside it's own app instance and the app @@ -40,7 +44,10 @@ describe('RxFire Database', () => { * account for this. */ beforeEach(() => { - app = initializeApp({ projectId: 'rxfire-test-db' }); + app = initializeApp({ + projectId: 'rxfire-test-db', + databaseURL: "https://rxfire-test.firebaseio.com", + }); database = app.database(); database.goOffline(); }); @@ -51,9 +58,9 @@ describe('RxFire Database', () => { describe('fromRef', () => { - it('once should complete', (done) => { + it('should complete using a once', (done) => { const itemRef = ref(createId()); - itemRef.set(batch); + itemRef.set(batch()); const obs = fromRef(itemRef, 'value', 'once'); obs.subscribe(_ => {}, () => {}, done); }); From 3136d171ca55c055c4c4c979130f0fe5c7ea3178 Mon Sep 17 00:00:00 2001 From: David East Date: Tue, 3 Jul 2018 11:01:49 -0600 Subject: [PATCH 03/14] database tests --- packages/rxfire/database/fromRef.ts | 4 +- packages/rxfire/database/list.ts | 26 +- packages/rxfire/test/database.test.ts | 373 ++++++++++++++++++++++++- packages/rxfire/test/firestore.test.ts | 2 +- 4 files changed, 380 insertions(+), 25 deletions(-) diff --git a/packages/rxfire/database/fromRef.ts b/packages/rxfire/database/fromRef.ts index ca19ea62ef3..cbf064e87d9 100644 --- a/packages/rxfire/database/fromRef.ts +++ b/packages/rxfire/database/fromRef.ts @@ -8,14 +8,14 @@ import { ListenEvent, SnapshotPrevKey } from './interfaces'; * @param ref Database Reference * @param event Listen event type ('value', 'added', 'changed', 'removed', 'moved') */ -export function fromRef(ref: database.Query, event: ListenEvent, listenType = 'on'): Observable { +export function fromRef(ref: database.Query, event: ListenEvent, listenType = 'on'): Observable { return new Observable(subscriber => { const fn = ref[listenType](event, (snapshot, prevKey) => { subscriber.next({ snapshot, prevKey, event }); if (listenType == 'once') { subscriber.complete(); } }, subscriber.error.bind(subscriber)); if (listenType == 'on') { - return { unsubscribe() { ref.off(event, fn)} }; + return { unsubscribe() { ref.off(event, fn) } }; } else { return { unsubscribe() { } }; } diff --git a/packages/rxfire/database/list.ts b/packages/rxfire/database/list.ts index 2b16ea4b31d..5aad7855ae8 100644 --- a/packages/rxfire/database/list.ts +++ b/packages/rxfire/database/list.ts @@ -14,8 +14,8 @@ export function stateChanges(query: database.Query, events?: ChildEvent[]) { export function list(query: database.Query, events?: ChildEvent[]): Observable { events = validateEventsArray(events); return fromRef(query, 'value', 'once').pipe( - switchMap(snapshotAction => { - const childEvent$ = [of(snapshotAction)]; + switchMap(change => { + const childEvent$ = [of(change)]; events.forEach(event => childEvent$.push(fromRef(query, event))); return merge(...childEvent$).pipe(scan(buildView, [])) }), @@ -46,15 +46,21 @@ function positionAfter(changes: SnapshotPrevKey[], prevKey?: string) { } } -function buildView(current: SnapshotPrevKey[], action: SnapshotPrevKey) { - const { snapshot, prevKey, event } = action; +function buildView(current: SnapshotPrevKey[], change: SnapshotPrevKey) { + const { snapshot, prevKey, event } = change; const { key } = snapshot; const currentKeyPosition = positionFor(current, key); const afterPreviousKeyPosition = positionAfter(current, prevKey); switch (event) { case 'value': - if (action.snapshot && action.snapshot.exists()) { - current = [...current, action]; + if (change.snapshot && change.snapshot.exists()) { + let prevKey = null; + change.snapshot.forEach(snapshot => { + const action: SnapshotPrevKey = { snapshot, event: 'value', prevKey }; + prevKey = snapshot.key; + current = [...current, action]; + return false; + }); } return current; case 'child_added': @@ -63,19 +69,19 @@ function buildView(current: SnapshotPrevKey[], action: SnapshotPrevKey) { const previous = current[currentKeyPosition - 1]; if ((previous && previous.snapshot.key || null) != prevKey) { current = current.filter(x => x.snapshot.key !== snapshot.key); - current.splice(afterPreviousKeyPosition, 0, action); + current.splice(afterPreviousKeyPosition, 0, change); } } else if (prevKey == null) { - return [action, ...current]; + return [change, ...current]; } else { current = current.slice() - current.splice(afterPreviousKeyPosition, 0, action); + current.splice(afterPreviousKeyPosition, 0, change); } return current; case 'child_removed': return current.filter(x => x.snapshot.key !== snapshot.key); case 'child_changed': - return current.map(x => x.snapshot.key === key ? action : x); + return current.map(x => x.snapshot.key === key ? change : x); case 'child_moved': if(currentKeyPosition > -1) { const data = current.splice(currentKeyPosition, 1)[0]; diff --git a/packages/rxfire/test/database.test.ts b/packages/rxfire/test/database.test.ts index 732402a0f36..48d1d227386 100644 --- a/packages/rxfire/test/database.test.ts +++ b/packages/rxfire/test/database.test.ts @@ -1,12 +1,18 @@ import { expect } from 'chai'; import { initializeApp, database, app } from 'firebase'; import { fromRef } from '../database/fromRef'; +import { list, ChildEvent } from '../database'; +import { take, skip, switchMap } from 'rxjs/operators'; +import { BehaviorSubject } from 'rxjs'; -let ref: (path: string) => database.Reference; -let batch = () => { +const rando = () => + Math.random() + .toString(36) + .substring(5); + + +let batch = (items: any[]) => { let batch = {}; - const items = [{ name: 'one' }, { name: 'two' }, { name: 'three' }] - .map(item => ( { key: createId(), ...item } )); Object.keys(items).forEach(function (key) { const itemValue = items[key]; batch[itemValue.key] = itemValue; @@ -15,11 +21,6 @@ let batch = () => { return Object.freeze(batch); } -const createId = () => - Math.random() - .toString(36) - .substring(5); - describe('RxFire Database', () => { let app: app.App = null; let database: database.Database = null; @@ -28,6 +29,16 @@ describe('RxFire Database', () => { return app.database().ref(path); }; + function prepareList(opts: { events?: ChildEvent[], skipnumber: number } = { skipnumber: 0 }) { + const { events, skipnumber } = opts; + const aref = ref(rando()); + const snapChanges = list(aref, events); + return { + snapChanges: snapChanges.pipe(skip(skipnumber)), + ref: aref + }; + } + /** * Each test runs inside it's own app instance and the app * is deleted after the test runs. @@ -56,16 +67,354 @@ describe('RxFire Database', () => { app.delete().then(() => done()); }); - describe('fromRef', () => { + xdescribe('fromRef', () => { + const items = [{ name: 'one' }, { name: 'two' }, { name: 'three' }] + .map(item => ( { key: rando(), ...item } )); + const itemsObj = batch(items); it('should complete using a once', (done) => { - const itemRef = ref(createId()); - itemRef.set(batch()); + const itemRef = ref(rando()); + itemRef.set(itemsObj); const obs = fromRef(itemRef, 'value', 'once'); obs.subscribe(_ => {}, () => {}, done); }); + it('it should should handle non-existence', (done) => { + const itemRef = ref(rando()); + itemRef.set({}); + const obs = fromRef(itemRef, 'value'); + obs.pipe(take(1)).subscribe(change => { + expect(change.snapshot.exists()).to.equal(false); + expect(change.snapshot.val()).to.equal(null); + }).add(done); + }); + + it('it should listen and then unsubscribe', (done) => { + const itemRef = ref(rando()); + itemRef.set(itemsObj); + const obs = fromRef(itemRef, 'value'); + let count = 0; + const sub = obs.subscribe(_ => { + count = count + 1; + // hard coding count to one will fail if the unsub + // doesn't actually unsub + expect(count).to.equal(1); + done(); + sub.unsubscribe(); + itemRef.push({ name: 'anotha one' }); + }); + }); + + describe('events', () => { + it('should stream back a child_added event', (done: any) => { + const itemRef = ref(rando()); + const data = itemsObj; + itemRef.set(data); + const obs = fromRef(itemRef, 'child_added'); + let count = 0; + const sub = obs.subscribe(change => { + count = count + 1; + const { event, snapshot } = change; + expect(event).to.equal('child_added'); + expect(snapshot.val()).to.eql(data[snapshot.key]); + if (count === items.length) { + done(); + sub.unsubscribe(); + expect(sub.closed).to.equal(true); + } + }); + }); + + it('should stream back a child_changed event', (done: any) => { + const itemRef = ref(rando()); + itemRef.set(itemsObj); + const obs = fromRef(itemRef, 'child_changed'); + const name = 'look at what you made me do'; + const key = items[0].key; + const sub = obs.subscribe(change => { + const { event, snapshot } = change; + expect(event).to.equal('child_changed'); + expect(snapshot.key).to.equal(key); + expect(snapshot.val()).to.eql({ key, name }); + sub.unsubscribe(); + done(); + }); + itemRef.child(key).update({ name }); + }); + + it('should stream back a child_removed event', (done: any) => { + const itemRef = ref(rando()); + itemRef.set(itemsObj); + const obs = fromRef(itemRef, 'child_removed'); + const key = items[0].key; + const name = items[0].name; + const sub = obs.subscribe(change => { + const { event, snapshot } = change; + expect(event).to.equal('child_removed'); + expect(snapshot.key).to.equal(key); + expect(snapshot.val()).to.eql({ key, name }); + sub.unsubscribe(); + done(); + }); + itemRef.child(key).remove(); + }); + + it('should stream back a child_moved event', (done: any) => { + const itemRef = ref(rando()); + itemRef.set(itemsObj); + const obs = fromRef(itemRef, 'child_moved'); + const key = items[2].key; + const name = items[2].name; + const sub = obs.subscribe(change => { + const { event, snapshot } = change; + expect(event).to.equal('child_moved'); + expect(snapshot.key).to.equal(key); + expect(snapshot.val()).to.eql({ key, name }); + sub.unsubscribe(); + done(); + }); + itemRef.child(key).setPriority(-100, () => {}); + }); + + it('should stream back a value event', (done: any) => { + const itemRef = ref(rando()); + const data = itemsObj; + itemRef.set(data); + const obs = fromRef(itemRef, 'value'); + const sub = obs.subscribe(change => { + const { event, snapshot } = change; + expect(event).to.equal('value'); + expect(snapshot.val()).to.eql(data); + done(); + sub.unsubscribe(); + expect(sub.closed).to.equal(true); + }); + }); + + it('should stream back query results', (done: any) => { + const itemRef = ref(rando()); + itemRef.set(itemsObj); + const query = itemRef.orderByChild('name').equalTo(items[0].name); + const obs = fromRef(query, 'value'); + obs.subscribe(change => { + let child; + change.snapshot.forEach(snap => { child = snap.val(); return true; }); + expect(child).to.eql(items[0]); + done(); + }); + }); + + }); + }); + + describe('list', () => { + + const items = [{ name: 'zero' }, { name: 'one' }, { name: 'two' }] + .map((item, i) => ( { key: `${i}`, ...item } )); + + const itemsObj = batch(items); + + describe('events', () => { + + it('should stream value at first', (done) => { + const someRef = ref(rando()); + const obs = list(someRef, ['child_added']); + obs + .pipe(take(1)) + .subscribe(changes => { + const data = changes.map(change => change.snapshot.val()); + expect(data).to.eql(items); + }) + .add(done); + + someRef.set(itemsObj); + }); + + it('should process a new child_added event', done => { + const aref = ref(rando()); + const obs = list(aref, ['child_added']); + obs + .pipe(skip(1),take(1)) + .subscribe(changes => { + const data = changes.map(change => change.snapshot.val()); + expect(data[3]).to.eql({ name: 'anotha one' }); + }) + .add(done); + aref.set(itemsObj); + aref.push({ name: 'anotha one' }); + }); + it('should stream in order events', (done) => { + const aref = ref(rando()); + const obs = list(aref.orderByChild('name'), ['child_added']); + const sub = obs.pipe(take(1)).subscribe(changes => { + const names = changes.map(change => change.snapshot.val().name); + expect(names[0]).to.eql('one'); + expect(names[1]).to.eql('two'); + expect(names[2]).to.eql('zero'); + }).add(done); + aref.set(itemsObj); + }); + it('should stream in order events w/child_added', (done) => { + const aref = ref(rando()); + const obs = list(aref.orderByChild('name'), ['child_added']); + const sub = obs.pipe(skip(1),take(1)).subscribe(changes => { + const names = changes.map(change => change.snapshot.val().name); + expect(names[0]).to.eql('anotha one'); + expect(names[1]).to.eql('one'); + expect(names[2]).to.eql('two'); + expect(names[3]).to.eql('zero'); + }).add(done); + aref.set(itemsObj); + aref.push({ name: 'anotha one' }); + }); + + it('should stream events filtering', (done) => { + const aref = ref(rando()); + const obs = list(aref.orderByChild('name').equalTo('zero'), ['child_added']); + obs.pipe(skip(1),take(1)).subscribe(changes => { + const names = changes.map(change => change.snapshot.val().name); + expect(names[0]).to.eql('zero'); + expect(names[1]).to.eql('zero'); + }).add(done); + aref.set(itemsObj); + aref.push({ name: 'zero' }); + }); + + it('should process a new child_removed event', done => { + const aref = ref(rando()); + const obs = list(aref, ['child_added','child_removed']); + const sub = obs.pipe(skip(1),take(1)).subscribe(changes => { + const data = changes.map(change => change.snapshot.val()); + expect(data.length).to.eql(items.length - 1); + }).add(done); + app.database().goOnline(); + aref.set(itemsObj).then(() => { + aref.child(items[0].key).remove(); + }); + }); + + it('should process a new child_changed event', (done) => { + const aref = ref(rando()); + const obs = list(aref, ['child_added','child_changed']) + const sub = obs.pipe(skip(1),take(1)).subscribe(changes => { + const data = changes.map(change => change.snapshot.val()); + expect(data[1].name).to.eql('lol'); + }).add(done); + app.database().goOnline(); + aref.set(itemsObj).then(() => { + aref.child(items[1].key).update({ name: 'lol'}); + }); + }); + + it('should process a new child_moved event', (done) => { + const aref = ref(rando()); + const obs = list(aref, ['child_added','child_moved']) + const sub = obs.pipe(skip(1),take(1)).subscribe(changes => { + const data = changes.map(change => change.snapshot.val()); + // We moved the first item to the last item, so we check that + // the new result is now the last result + expect(data[data.length - 1]).to.eql(items[0]); + }).add(done); + app.database().goOnline(); + aref.set(itemsObj).then(() => { + aref.child(items[0].key).setPriority('a', () => {}); + }); + }); + + it('should listen to all events by default', (done) => { + const { snapChanges, ref } = prepareList(); + snapChanges.pipe(take(1)).subscribe(actions => { + const data = actions.map(a => a.snapshot.val()); + expect(data).to.eql(items); + }).add(done); + ref.set(itemsObj); + }); + + it('should handle multiple subscriptions (hot)', (done) => { + const { snapChanges, ref } = prepareList(); + const sub = snapChanges.subscribe(() => {}).add(done); + snapChanges.pipe(take(1)).subscribe(actions => { + const data = actions.map(a => a.snapshot.val()); + expect(data).to.eql(items); + }).add(sub); + ref.set(itemsObj); + }); + + it('should handle multiple subscriptions (warm)', done => { + const { snapChanges, ref } = prepareList(); + snapChanges.pipe(take(1)).subscribe(() => {}).add(() => { + snapChanges.pipe(take(1)).subscribe(actions => { + const data = actions.map(a => a.snapshot.val()); + expect(data).to.eql(items); + }).add(done); + }); + ref.set(itemsObj); + }); + + it('should listen to only child_added events', (done) => { + const { snapChanges, ref } = prepareList({ events: ['child_added'], skipnumber: 0 }); + snapChanges.pipe(take(1)).subscribe(actions => { + const data = actions.map(a => a.snapshot.val()); + expect(data).to.eql(items); + }).add(done); + ref.set(itemsObj); + }); + + it('should listen to only child_added, child_changed events', (done) => { + const { snapChanges, ref } = prepareList({ + events: ['child_added', 'child_changed'], + skipnumber: 1 + }); + const name = 'ligatures'; + snapChanges.pipe(take(1)).subscribe(actions => { + const data = actions.map(a => a.snapshot.val());; + const copy = [...items]; + copy[0].name = name; + expect(data).to.eql(copy); + }).add(done); + app.database().goOnline(); + ref.set(itemsObj).then(() => { + ref.child(items[0].key).update({ name }) + }); + }); + + it('should handle empty sets', done => { + const aref = ref(rando()); + aref.set({}); + list(aref).pipe(take(1)).subscribe(data => { + expect(data.length).to.eql(0); + }).add(done); + }); + + it('should handle dynamic queries that return empty sets', done => { + const ITEMS = 10; + let count = 0; + let firstIndex = 0; + let namefilter$ = new BehaviorSubject(null); + const aref = ref(rando()); + aref.set(itemsObj); + namefilter$.pipe(switchMap(name => { + const filteredRef = name ? aref.child('name').equalTo(name) : aref + return list(filteredRef); + }),take(2)).subscribe(data => { + count = count + 1; + // the first time should all be 'added' + if(count === 1) { + expect(Object.keys(data).length).to.eql(3); + namefilter$.next(-1); + } + // on the second round, we should have filtered out everything + if(count === 2) { + expect(Object.keys(data).length).to.eql(0); + } + }).add(done); + }); + + }); + + }); + }); diff --git a/packages/rxfire/test/firestore.test.ts b/packages/rxfire/test/firestore.test.ts index e61b3ce0e24..8dbe0974031 100644 --- a/packages/rxfire/test/firestore.test.ts +++ b/packages/rxfire/test/firestore.test.ts @@ -62,7 +62,7 @@ const seedTest = firestore => { return { colRef, davidDoc, shannonDoc, expectedNames, expectedEvents }; }; -describe('RxFire Firestore', () => { +xdescribe('RxFire Firestore', () => { let app: app.App = null; let firestore: firestore.Firestore = null; From 56972fc6b938644907c4891ac8f425e7fd2f0018 Mon Sep 17 00:00:00 2001 From: David East Date: Wed, 4 Jul 2018 06:46:57 -0600 Subject: [PATCH 04/14] auditTrail and database tests --- packages/rxfire/database/list/audit-trail.ts | 62 +++++ .../database/{list.ts => list/index.ts} | 6 +- .../database/{object.ts => object/index.ts} | 4 +- packages/rxfire/database/utils.ts | 5 + packages/rxfire/test/database.test.ts | 243 ++++++++++++++---- packages/rxfire/test/firestore.test.ts | 2 +- 6 files changed, 266 insertions(+), 56 deletions(-) create mode 100644 packages/rxfire/database/list/audit-trail.ts rename packages/rxfire/database/{list.ts => list/index.ts} (95%) rename packages/rxfire/database/{object.ts => object/index.ts} (74%) diff --git a/packages/rxfire/database/list/audit-trail.ts b/packages/rxfire/database/list/audit-trail.ts new file mode 100644 index 00000000000..7079edfd7d8 --- /dev/null +++ b/packages/rxfire/database/list/audit-trail.ts @@ -0,0 +1,62 @@ +import { database } from "firebase"; +import { Observable } from 'rxjs'; +import { SnapshotPrevKey, ChildEvent } from '../interfaces'; +import { fromRef } from '../fromRef'; +import { map, withLatestFrom, scan, skipWhile } from 'rxjs/operators'; +import { stateChanges } from './'; + +interface LoadedMetadata { + data: SnapshotPrevKey; + lastKeyToLoad: any; +} + +export function auditTrail(query: database.Query, events?: ChildEvent[]): Observable { + const auditTrail$ = stateChanges(query, events) + .pipe( + scan((current, changes) => [...current, changes], []) + ); + return waitForLoaded(query, auditTrail$); +} + +function loadedData(query: database.Query): Observable { + // Create an observable of loaded values to retrieve the + // known dataset. This will allow us to know what key to + // emit the "whole" array at when listening for child events. + return fromRef(query, 'value') + .pipe( + map(data => { + // Store the last key in the data set + let lastKeyToLoad; + // Loop through loaded dataset to find the last key + data.snapshot.forEach(child => { + lastKeyToLoad = child.key; return false; + }); + // return data set and the current last key loaded + return { data, lastKeyToLoad }; + }) + ); +} + +function waitForLoaded(query: database.Query, snap$: Observable) { + const loaded$ = loadedData(query); + return loaded$ + .pipe( + withLatestFrom(snap$), + // Get the latest values from the "loaded" and "child" datasets + // We can use both datasets to form an array of the latest values. + map(([loaded, changes]) => { + // Store the last key in the data set + let lastKeyToLoad = loaded.lastKeyToLoad; + // Store all child keys loaded at this point + const loadedKeys = changes.map(change => change.snapshot.key); + return { changes, lastKeyToLoad, loadedKeys } + }), + // This is the magical part, only emit when the last load key + // in the dataset has been loaded by a child event. At this point + // we can assume the dataset is "whole". + skipWhile(meta => meta.loadedKeys.indexOf(meta.lastKeyToLoad) === -1), + // Pluck off the meta data because the user only cares + // to iterate through the snapshots + map(meta => meta.changes) + ); +} diff --git a/packages/rxfire/database/list.ts b/packages/rxfire/database/list/index.ts similarity index 95% rename from packages/rxfire/database/list.ts rename to packages/rxfire/database/list/index.ts index 5aad7855ae8..ef8d8ac16ae 100644 --- a/packages/rxfire/database/list.ts +++ b/packages/rxfire/database/list/index.ts @@ -1,8 +1,8 @@ import { database } from 'firebase'; -import { ChildEvent, SnapshotPrevKey } from './interfaces'; +import { ChildEvent, SnapshotPrevKey } from '../interfaces'; import { Observable, of, merge } from 'rxjs'; -import { validateEventsArray, isNil } from './utils'; -import { fromRef } from './fromRef'; +import { validateEventsArray, isNil } from '../utils'; +import { fromRef } from '../fromRef'; import { switchMap, scan, distinctUntilChanged } from 'rxjs/operators'; export function stateChanges(query: database.Query, events?: ChildEvent[]) { diff --git a/packages/rxfire/database/object.ts b/packages/rxfire/database/object/index.ts similarity index 74% rename from packages/rxfire/database/object.ts rename to packages/rxfire/database/object/index.ts index 3eaaa7dc454..5eb67c2ce44 100644 --- a/packages/rxfire/database/object.ts +++ b/packages/rxfire/database/object/index.ts @@ -1,6 +1,6 @@ import { database } from 'firebase'; -import { SnapshotPrevKey } from "./interfaces"; -import { fromRef } from './fromRef'; +import { SnapshotPrevKey } from "../interfaces"; +import { fromRef } from '../fromRef'; import { Observable } from 'rxjs'; /** diff --git a/packages/rxfire/database/utils.ts b/packages/rxfire/database/utils.ts index 58c02e5a4cc..ee9866227ae 100644 --- a/packages/rxfire/database/utils.ts +++ b/packages/rxfire/database/utils.ts @@ -3,6 +3,11 @@ export function isNil(obj: any): boolean { return obj === undefined || obj === null; } +/** + * Check the length of the provided array. If it is empty return an array + * that is populated with all the Realtime Database child events. + * @param events + */ export function validateEventsArray(events?: any[]) { if(isNil(events) || events!.length === 0) { events = ['child_added', 'child_removed', 'child_changed', 'child_moved']; diff --git a/packages/rxfire/test/database.test.ts b/packages/rxfire/test/database.test.ts index 48d1d227386..61b980da0c7 100644 --- a/packages/rxfire/test/database.test.ts +++ b/packages/rxfire/test/database.test.ts @@ -4,12 +4,12 @@ import { fromRef } from '../database/fromRef'; import { list, ChildEvent } from '../database'; import { take, skip, switchMap } from 'rxjs/operators'; import { BehaviorSubject } from 'rxjs'; +import { auditTrail } from '../database/list/audit-trail'; const rando = () => Math.random() .toString(36) .substring(5); - let batch = (items: any[]) => { let batch = {}; @@ -24,9 +24,9 @@ let batch = (items: any[]) => { describe('RxFire Database', () => { let app: app.App = null; let database: database.Database = null; - let ref = (path: string) => { - app.database().goOffline(); - return app.database().ref(path); + let ref = (path: string) => { + app.database().goOffline(); + return app.database().ref(path); }; function prepareList(opts: { events?: ChildEvent[], skipnumber: number } = { skipnumber: 0 }) { @@ -55,7 +55,7 @@ describe('RxFire Database', () => { * account for this. */ beforeEach(() => { - app = initializeApp({ + app = initializeApp({ projectId: 'rxfire-test-db', databaseURL: "https://rxfire-test.firebaseio.com", }); @@ -67,18 +67,36 @@ describe('RxFire Database', () => { app.delete().then(() => done()); }); - xdescribe('fromRef', () => { + describe('fromRef', () => { const items = [{ name: 'one' }, { name: 'two' }, { name: 'three' }] - .map(item => ( { key: rando(), ...item } )); + .map(item => ({ key: rando(), ...item })); const itemsObj = batch(items); + /** + * fromRef() takes in a reference, an event, an optionally a listenType + * parameter. This listenType determines if the listen will be a realtime + * `on` or a one-time `once`. + * + * This test checks that providing the `once` listenType will result in a + * one-time read. This is determined by setting a value at a reference, + * subscribing to the observable and calling the MochaDone callback when + * the observable completes. + * + * The fact that the observable completes without any added operators + * indicates it was a one-time read. Realtime reads do not complete unless + * accompanied by an operator that forces a complete. + */ it('should complete using a once', (done) => { const itemRef = ref(rando()); itemRef.set(itemsObj); const obs = fromRef(itemRef, 'value', 'once'); - obs.subscribe(_ => {}, () => {}, done); + obs.subscribe(_ => { }, () => { }, done); }); + /** + * This test checks that "non-existent" or null value references are + * handled. + */ it('it should should handle non-existence', (done) => { const itemRef = ref(rando()); itemRef.set({}); @@ -89,6 +107,12 @@ describe('RxFire Database', () => { }).add(done); }); + /** + * This test checks that the Observable unsubscribe mechanism works. + * + * Calling unsubscribe should trigger the ref.off() method when using a + * `on` listenType. + */ it('it should listen and then unsubscribe', (done) => { const itemRef = ref(rando()); itemRef.set(itemsObj); @@ -106,6 +130,11 @@ describe('RxFire Database', () => { }); describe('events', () => { + + /** + * This test provides the `child_added` event and tests that only + * `child_added` events are received. + */ it('should stream back a child_added event', (done: any) => { const itemRef = ref(rando()); const data = itemsObj; @@ -124,7 +153,11 @@ describe('RxFire Database', () => { } }); }); - + + /** + * This test provides the `child_changed` event and tests that only + * `child_changed` events are received. + */ it('should stream back a child_changed event', (done: any) => { const itemRef = ref(rando()); itemRef.set(itemsObj); @@ -141,7 +174,11 @@ describe('RxFire Database', () => { }); itemRef.child(key).update({ name }); }); - + + /** + * This test provides the `child_removed` event and tests that only + * `child_removed` events are received. + */ it('should stream back a child_removed event', (done: any) => { const itemRef = ref(rando()); itemRef.set(itemsObj); @@ -158,7 +195,11 @@ describe('RxFire Database', () => { }); itemRef.child(key).remove(); }); - + + /** + * This test provides the `child_moved` event and tests that only + * `child_moved` events are received. + */ it('should stream back a child_moved event', (done: any) => { const itemRef = ref(rando()); itemRef.set(itemsObj); @@ -173,9 +214,13 @@ describe('RxFire Database', () => { sub.unsubscribe(); done(); }); - itemRef.child(key).setPriority(-100, () => {}); + itemRef.child(key).setPriority(-100, () => { }); }); - + + /** + * This test provides the `value` event and tests that only + * `value` events are received. + */ it('should stream back a value event', (done: any) => { const itemRef = ref(rando()); const data = itemsObj; @@ -190,7 +235,11 @@ describe('RxFire Database', () => { expect(sub.closed).to.equal(true); }); }); - + + /** + * This test provides queries a reference and checks that the queried + * values are streamed back. + */ it('should stream back query results', (done: any) => { const itemRef = ref(rando()); itemRef.set(itemsObj); @@ -203,20 +252,24 @@ describe('RxFire Database', () => { done(); }); }); - + }); }); - + describe('list', () => { const items = [{ name: 'zero' }, { name: 'one' }, { name: 'two' }] - .map((item, i) => ( { key: `${i}`, ...item } )); + .map((item, i) => ({ key: `${i}`, ...item })); const itemsObj = batch(items); describe('events', () => { + /** + * `value` events are provided first when subscribing to a list. We need + * to know what the "intial" data list is, so a value event is used. + */ it('should stream value at first', (done) => { const someRef = ref(rando()); const obs = list(someRef, ['child_added']); @@ -231,11 +284,18 @@ describe('RxFire Database', () => { someRef.set(itemsObj); }); + /** + * This test checks that `child_added` events are only triggered when + * specified in the events array. + * + * The first result is skipped because it is always `value`. A `take(1)` + * is used to close the stream after the `child_added` event occurs. + */ it('should process a new child_added event', done => { const aref = ref(rando()); const obs = list(aref, ['child_added']); obs - .pipe(skip(1),take(1)) + .pipe(skip(1), take(1)) .subscribe(changes => { const data = changes.map(change => change.snapshot.val()); expect(data[3]).to.eql({ name: 'anotha one' }); @@ -245,10 +305,14 @@ describe('RxFire Database', () => { aref.push({ name: 'anotha one' }); }); + /** + * This test checks that events are emitted in proper order. The reference + * is queried and the test ensures that the array is in proper order. + */ it('should stream in order events', (done) => { const aref = ref(rando()); const obs = list(aref.orderByChild('name'), ['child_added']); - const sub = obs.pipe(take(1)).subscribe(changes => { + obs.pipe(take(1)).subscribe(changes => { const names = changes.map(change => change.snapshot.val().name); expect(names[0]).to.eql('one'); expect(names[1]).to.eql('two'); @@ -256,11 +320,17 @@ describe('RxFire Database', () => { }).add(done); aref.set(itemsObj); }); - + + /** + * This test checks that the array is in order with child_added specified. + * A new record is added that appears on top of the query and the test + * skips the first value event and checks that the newly added item is + * on top. + */ it('should stream in order events w/child_added', (done) => { const aref = ref(rando()); const obs = list(aref.orderByChild('name'), ['child_added']); - const sub = obs.pipe(skip(1),take(1)).subscribe(changes => { + obs.pipe(skip(1), take(1)).subscribe(changes => { const names = changes.map(change => change.snapshot.val().name); expect(names[0]).to.eql('anotha one'); expect(names[1]).to.eql('one'); @@ -270,11 +340,14 @@ describe('RxFire Database', () => { aref.set(itemsObj); aref.push({ name: 'anotha one' }); }); - + + /** + * This test checks that a filtered reference still emits the proper events. + */ it('should stream events filtering', (done) => { const aref = ref(rando()); const obs = list(aref.orderByChild('name').equalTo('zero'), ['child_added']); - obs.pipe(skip(1),take(1)).subscribe(changes => { + obs.pipe(skip(1), take(1)).subscribe(changes => { const names = changes.map(change => change.snapshot.val().name); expect(names[0]).to.eql('zero'); expect(names[1]).to.eql('zero'); @@ -282,11 +355,16 @@ describe('RxFire Database', () => { aref.set(itemsObj); aref.push({ name: 'zero' }); }); - + + /** + * This test checks that the a `child_removed` event is processed in the + * array by testing that the new length is shorter than the original + * length. + */ it('should process a new child_removed event', done => { const aref = ref(rando()); - const obs = list(aref, ['child_added','child_removed']); - const sub = obs.pipe(skip(1),take(1)).subscribe(changes => { + const obs = list(aref, ['child_added', 'child_removed']); + const sub = obs.pipe(skip(1), take(1)).subscribe(changes => { const data = changes.map(change => change.snapshot.val()); expect(data.length).to.eql(items.length - 1); }).add(done); @@ -295,24 +373,32 @@ describe('RxFire Database', () => { aref.child(items[0].key).remove(); }); }); - + + /** + * This test checks that the `child_changed` event is processed by + * checking the new value of the object in the array. + */ it('should process a new child_changed event', (done) => { const aref = ref(rando()); - const obs = list(aref, ['child_added','child_changed']) - const sub = obs.pipe(skip(1),take(1)).subscribe(changes => { + const obs = list(aref, ['child_added', 'child_changed']) + const sub = obs.pipe(skip(1), take(1)).subscribe(changes => { const data = changes.map(change => change.snapshot.val()); expect(data[1].name).to.eql('lol'); }).add(done); app.database().goOnline(); aref.set(itemsObj).then(() => { - aref.child(items[1].key).update({ name: 'lol'}); + aref.child(items[1].key).update({ name: 'lol' }); }); }); - + + /** + * This test checks the `child_moved` event is processed by checking that + * the new position is properly updated. + */ it('should process a new child_moved event', (done) => { const aref = ref(rando()); - const obs = list(aref, ['child_added','child_moved']) - const sub = obs.pipe(skip(1),take(1)).subscribe(changes => { + const obs = list(aref, ['child_added', 'child_moved']) + const sub = obs.pipe(skip(1), take(1)).subscribe(changes => { const data = changes.map(change => change.snapshot.val()); // We moved the first item to the last item, so we check that // the new result is now the last result @@ -320,10 +406,16 @@ describe('RxFire Database', () => { }).add(done); app.database().goOnline(); aref.set(itemsObj).then(() => { - aref.child(items[0].key).setPriority('a', () => {}); + aref.child(items[0].key).setPriority('a', () => { }); }); }); + /** + * If no events array is provided in `list()` all events are listened to. + * + * This test checks that all events are processed without providing the + * array. + */ it('should listen to all events by default', (done) => { const { snapChanges, ref } = prepareList(); snapChanges.pipe(take(1)).subscribe(actions => { @@ -332,20 +424,26 @@ describe('RxFire Database', () => { }).add(done); ref.set(itemsObj); }); - + + /** + * This test checks that multiple subscriptions work properly. + */ it('should handle multiple subscriptions (hot)', (done) => { const { snapChanges, ref } = prepareList(); - const sub = snapChanges.subscribe(() => {}).add(done); + const sub = snapChanges.subscribe(() => { }).add(done); snapChanges.pipe(take(1)).subscribe(actions => { const data = actions.map(a => a.snapshot.val()); expect(data).to.eql(items); }).add(sub); ref.set(itemsObj); }); - + + /** + * This test checks that multiple subscriptions work properly. + */ it('should handle multiple subscriptions (warm)', done => { const { snapChanges, ref } = prepareList(); - snapChanges.pipe(take(1)).subscribe(() => {}).add(() => { + snapChanges.pipe(take(1)).subscribe(() => { }).add(() => { snapChanges.pipe(take(1)).subscribe(actions => { const data = actions.map(a => a.snapshot.val()); expect(data).to.eql(items); @@ -353,8 +451,11 @@ describe('RxFire Database', () => { }); ref.set(itemsObj); }); - - it('should listen to only child_added events', (done) => { + + /** + * This test checks that only `child_added` events are processed. + */ + it('should listen to only child_added events', (done) => { const { snapChanges, ref } = prepareList({ events: ['child_added'], skipnumber: 0 }); snapChanges.pipe(take(1)).subscribe(actions => { const data = actions.map(a => a.snapshot.val()); @@ -362,7 +463,11 @@ describe('RxFire Database', () => { }).add(done); ref.set(itemsObj); }); - + + /** + * This test checks that only `child_added` and `child_changed` events are + * processed. + */ it('should listen to only child_added, child_changed events', (done) => { const { snapChanges, ref } = prepareList({ events: ['child_added', 'child_changed'], @@ -380,7 +485,10 @@ describe('RxFire Database', () => { ref.child(items[0].key).update({ name }) }); }); - + + /** + * This test checks that empty sets are processed. + */ it('should handle empty sets', done => { const aref = ref(rando()); aref.set({}); @@ -388,33 +496,68 @@ describe('RxFire Database', () => { expect(data.length).to.eql(0); }).add(done); }); - + + /** + * This test checks that dynamic querying works even with results that + * are empty. + */ it('should handle dynamic queries that return empty sets', done => { - const ITEMS = 10; let count = 0; - let firstIndex = 0; - let namefilter$ = new BehaviorSubject(null); + let namefilter$ = new BehaviorSubject(null); const aref = ref(rando()); aref.set(itemsObj); namefilter$.pipe(switchMap(name => { const filteredRef = name ? aref.child('name').equalTo(name) : aref return list(filteredRef); - }),take(2)).subscribe(data => { + }), take(2)).subscribe(data => { count = count + 1; // the first time should all be 'added' - if(count === 1) { + if (count === 1) { expect(Object.keys(data).length).to.eql(3); namefilter$.next(-1); } // on the second round, we should have filtered out everything - if(count === 2) { + if (count === 2) { expect(Object.keys(data).length).to.eql(0); } }).add(done); }); }); - + + }); + + describe('auditTrail', () => { + const items = [{ name: 'zero' }, { name: 'one' }, { name: 'two' }] + .map((item, i) => ({ key: `${i}`, ...item })); + + const itemsObj = batch(items); + + function prepareAuditTrail(opts: { events?: ChildEvent[], skipnumber: number } = { skipnumber: 0 }) { + const { events, skipnumber } = opts; + const aref = ref(rando()); + aref.set(itemsObj); + const changes = auditTrail(aref, events); + return { + changes: changes.pipe(skip(skipnumber)), + ref: aref + }; + } + + /** + * This test checks that auditTrail retuns all events by default. + */ + it('should listen to all events by default', (done) => { + + const { changes } = prepareAuditTrail(); + changes.subscribe(actions => { + const data = actions.map(a => a.snapshot.val()); + expect(data).to.eql(items); + done(); + }); + + }); + }); }); diff --git a/packages/rxfire/test/firestore.test.ts b/packages/rxfire/test/firestore.test.ts index 8dbe0974031..e61b3ce0e24 100644 --- a/packages/rxfire/test/firestore.test.ts +++ b/packages/rxfire/test/firestore.test.ts @@ -62,7 +62,7 @@ const seedTest = firestore => { return { colRef, davidDoc, shannonDoc, expectedNames, expectedEvents }; }; -xdescribe('RxFire Firestore', () => { +describe('RxFire Firestore', () => { let app: app.App = null; let firestore: firestore.Firestore = null; From a940247d79791ae8f8fe143ce1a7eb145df26d2b Mon Sep 17 00:00:00 2001 From: David East Date: Wed, 4 Jul 2018 06:53:36 -0600 Subject: [PATCH 05/14] [AUTOMATED]: Prettier Code Styling --- packages/rxfire/database/fromRef.ts | 43 ++- packages/rxfire/database/interfaces.ts | 6 +- packages/rxfire/database/list/audit-trail.ts | 66 ++-- packages/rxfire/database/list/index.ts | 29 +- packages/rxfire/database/object/index.ts | 4 +- packages/rxfire/database/utils.ts | 5 +- packages/rxfire/test/database.test.ts | 345 +++++++++++-------- 7 files changed, 290 insertions(+), 208 deletions(-) diff --git a/packages/rxfire/database/fromRef.ts b/packages/rxfire/database/fromRef.ts index cbf064e87d9..79cf1d8378a 100644 --- a/packages/rxfire/database/fromRef.ts +++ b/packages/rxfire/database/fromRef.ts @@ -8,16 +8,30 @@ import { ListenEvent, SnapshotPrevKey } from './interfaces'; * @param ref Database Reference * @param event Listen event type ('value', 'added', 'changed', 'removed', 'moved') */ -export function fromRef(ref: database.Query, event: ListenEvent, listenType = 'on'): Observable { +export function fromRef( + ref: database.Query, + event: ListenEvent, + listenType = 'on' +): Observable { return new Observable(subscriber => { - const fn = ref[listenType](event, (snapshot, prevKey) => { - subscriber.next({ snapshot, prevKey, event }); - if (listenType == 'once') { subscriber.complete(); } - }, subscriber.error.bind(subscriber)); + const fn = ref[listenType]( + event, + (snapshot, prevKey) => { + subscriber.next({ snapshot, prevKey, event }); + if (listenType == 'once') { + subscriber.complete(); + } + }, + subscriber.error.bind(subscriber) + ); if (listenType == 'on') { - return { unsubscribe() { ref.off(event, fn) } }; + return { + unsubscribe() { + ref.off(event, fn); + } + }; } else { - return { unsubscribe() { } }; + return { unsubscribe() {} }; } }).pipe( // Ensures subscribe on observable is async. This handles @@ -28,9 +42,12 @@ export function fromRef(ref: database.Query, event: ListenEvent, listenType = 'o ); } -export const unwrap = () => map((payload: SnapshotPrevKey) => { - const { snapshot, prevKey } = payload; - let key: string | null = null; - if (snapshot.exists()) { key = snapshot.key; } - return { type: event, payload: snapshot, prevKey, key }; -}); +export const unwrap = () => + map((payload: SnapshotPrevKey) => { + const { snapshot, prevKey } = payload; + let key: string | null = null; + if (snapshot.exists()) { + key = snapshot.key; + } + return { type: event, payload: snapshot, prevKey, key }; + }); diff --git a/packages/rxfire/database/interfaces.ts b/packages/rxfire/database/interfaces.ts index 6496c0af7b4..b6d30c972f8 100644 --- a/packages/rxfire/database/interfaces.ts +++ b/packages/rxfire/database/interfaces.ts @@ -1,7 +1,11 @@ import { database } from 'firebase'; export type QueryFn = (ref: database.Reference) => database.Query; -export type ChildEvent = 'child_added' | 'child_removed' | 'child_changed' | 'child_moved'; +export type ChildEvent = + | 'child_added' + | 'child_removed' + | 'child_changed' + | 'child_moved'; export type ListenEvent = 'value' | ChildEvent; export interface SnapshotPrevKey { diff --git a/packages/rxfire/database/list/audit-trail.ts b/packages/rxfire/database/list/audit-trail.ts index 7079edfd7d8..29d9a864af7 100644 --- a/packages/rxfire/database/list/audit-trail.ts +++ b/packages/rxfire/database/list/audit-trail.ts @@ -1,20 +1,22 @@ -import { database } from "firebase"; +import { database } from 'firebase'; import { Observable } from 'rxjs'; import { SnapshotPrevKey, ChildEvent } from '../interfaces'; import { fromRef } from '../fromRef'; import { map, withLatestFrom, scan, skipWhile } from 'rxjs/operators'; import { stateChanges } from './'; - + interface LoadedMetadata { data: SnapshotPrevKey; lastKeyToLoad: any; } -export function auditTrail(query: database.Query, events?: ChildEvent[]): Observable { - const auditTrail$ = stateChanges(query, events) - .pipe( - scan((current, changes) => [...current, changes], []) - ); +export function auditTrail( + query: database.Query, + events?: ChildEvent[] +): Observable { + const auditTrail$ = stateChanges(query, events).pipe( + scan((current, changes) => [...current, changes], []) + ); return waitForLoaded(query, auditTrail$); } @@ -22,14 +24,14 @@ function loadedData(query: database.Query): Observable { // Create an observable of loaded values to retrieve the // known dataset. This will allow us to know what key to // emit the "whole" array at when listening for child events. - return fromRef(query, 'value') - .pipe( + return fromRef(query, 'value').pipe( map(data => { // Store the last key in the data set let lastKeyToLoad; // Loop through loaded dataset to find the last key data.snapshot.forEach(child => { - lastKeyToLoad = child.key; return false; + lastKeyToLoad = child.key; + return false; }); // return data set and the current last key loaded return { data, lastKeyToLoad }; @@ -37,26 +39,28 @@ function loadedData(query: database.Query): Observable { ); } -function waitForLoaded(query: database.Query, snap$: Observable) { +function waitForLoaded( + query: database.Query, + snap$: Observable +) { const loaded$ = loadedData(query); - return loaded$ - .pipe( - withLatestFrom(snap$), - // Get the latest values from the "loaded" and "child" datasets - // We can use both datasets to form an array of the latest values. - map(([loaded, changes]) => { - // Store the last key in the data set - let lastKeyToLoad = loaded.lastKeyToLoad; - // Store all child keys loaded at this point - const loadedKeys = changes.map(change => change.snapshot.key); - return { changes, lastKeyToLoad, loadedKeys } - }), - // This is the magical part, only emit when the last load key - // in the dataset has been loaded by a child event. At this point - // we can assume the dataset is "whole". - skipWhile(meta => meta.loadedKeys.indexOf(meta.lastKeyToLoad) === -1), - // Pluck off the meta data because the user only cares - // to iterate through the snapshots - map(meta => meta.changes) - ); + return loaded$.pipe( + withLatestFrom(snap$), + // Get the latest values from the "loaded" and "child" datasets + // We can use both datasets to form an array of the latest values. + map(([loaded, changes]) => { + // Store the last key in the data set + let lastKeyToLoad = loaded.lastKeyToLoad; + // Store all child keys loaded at this point + const loadedKeys = changes.map(change => change.snapshot.key); + return { changes, lastKeyToLoad, loadedKeys }; + }), + // This is the magical part, only emit when the last load key + // in the dataset has been loaded by a child event. At this point + // we can assume the dataset is "whole". + skipWhile(meta => meta.loadedKeys.indexOf(meta.lastKeyToLoad) === -1), + // Pluck off the meta data because the user only cares + // to iterate through the snapshots + map(meta => meta.changes) + ); } diff --git a/packages/rxfire/database/list/index.ts b/packages/rxfire/database/list/index.ts index ef8d8ac16ae..0929699dcf5 100644 --- a/packages/rxfire/database/list/index.ts +++ b/packages/rxfire/database/list/index.ts @@ -11,13 +11,16 @@ export function stateChanges(query: database.Query, events?: ChildEvent[]) { return merge(...childEvent$); } -export function list(query: database.Query, events?: ChildEvent[]): Observable { +export function list( + query: database.Query, + events?: ChildEvent[] +): Observable { events = validateEventsArray(events); return fromRef(query, 'value', 'once').pipe( switchMap(change => { const childEvent$ = [of(change)]; events.forEach(event => childEvent$.push(fromRef(query, event))); - return merge(...childEvent$).pipe(scan(buildView, [])) + return merge(...childEvent$).pipe(scan(buildView, [])); }), distinctUntilChanged() ); @@ -25,8 +28,8 @@ export function list(query: database.Query, events?: ChildEvent[]): Observable(changes: SnapshotPrevKey[], key) { const len = changes.length; - for(let i=0; i(changes: SnapshotPrevKey[], key) { } function positionAfter(changes: SnapshotPrevKey[], prevKey?: string) { - if(isNil(prevKey)) { - return 0; + if (isNil(prevKey)) { + return 0; } else { const i = positionFor(changes, prevKey); - if( i === -1) { + if (i === -1) { return changes.length; } else { return i + 1; @@ -47,7 +50,7 @@ function positionAfter(changes: SnapshotPrevKey[], prevKey?: string) { } function buildView(current: SnapshotPrevKey[], change: SnapshotPrevKey) { - const { snapshot, prevKey, event } = change; + const { snapshot, prevKey, event } = change; const { key } = snapshot; const currentKeyPosition = positionFor(current, key); const afterPreviousKeyPosition = positionAfter(current, prevKey); @@ -67,25 +70,25 @@ function buildView(current: SnapshotPrevKey[], change: SnapshotPrevKey) { if (currentKeyPosition > -1) { // check that the previouskey is what we expect, else reorder const previous = current[currentKeyPosition - 1]; - if ((previous && previous.snapshot.key || null) != prevKey) { + if (((previous && previous.snapshot.key) || null) != prevKey) { current = current.filter(x => x.snapshot.key !== snapshot.key); current.splice(afterPreviousKeyPosition, 0, change); } } else if (prevKey == null) { return [change, ...current]; } else { - current = current.slice() + current = current.slice(); current.splice(afterPreviousKeyPosition, 0, change); } return current; case 'child_removed': return current.filter(x => x.snapshot.key !== snapshot.key); case 'child_changed': - return current.map(x => x.snapshot.key === key ? change : x); + return current.map(x => (x.snapshot.key === key ? change : x)); case 'child_moved': - if(currentKeyPosition > -1) { + if (currentKeyPosition > -1) { const data = current.splice(currentKeyPosition, 1)[0]; - current = current.slice() + current = current.slice(); current.splice(afterPreviousKeyPosition, 0, data); return current; } diff --git a/packages/rxfire/database/object/index.ts b/packages/rxfire/database/object/index.ts index 5eb67c2ce44..865eebc49b0 100644 --- a/packages/rxfire/database/object/index.ts +++ b/packages/rxfire/database/object/index.ts @@ -1,11 +1,11 @@ import { database } from 'firebase'; -import { SnapshotPrevKey } from "../interfaces"; +import { SnapshotPrevKey } from '../interfaces'; import { fromRef } from '../fromRef'; import { Observable } from 'rxjs'; /** * Get the snapshot changes of an object - * @param query + * @param query */ export function object(query: database.Query): Observable { return fromRef(query, 'value'); diff --git a/packages/rxfire/database/utils.ts b/packages/rxfire/database/utils.ts index ee9866227ae..2ebfdbdb244 100644 --- a/packages/rxfire/database/utils.ts +++ b/packages/rxfire/database/utils.ts @@ -1,4 +1,3 @@ - export function isNil(obj: any): boolean { return obj === undefined || obj === null; } @@ -6,10 +5,10 @@ export function isNil(obj: any): boolean { /** * Check the length of the provided array. If it is empty return an array * that is populated with all the Realtime Database child events. - * @param events + * @param events */ export function validateEventsArray(events?: any[]) { - if(isNil(events) || events!.length === 0) { + if (isNil(events) || events!.length === 0) { events = ['child_added', 'child_removed', 'child_changed', 'child_moved']; } return events; diff --git a/packages/rxfire/test/database.test.ts b/packages/rxfire/test/database.test.ts index 61b980da0c7..efee6ef901f 100644 --- a/packages/rxfire/test/database.test.ts +++ b/packages/rxfire/test/database.test.ts @@ -13,13 +13,13 @@ const rando = () => let batch = (items: any[]) => { let batch = {}; - Object.keys(items).forEach(function (key) { + Object.keys(items).forEach(function(key) { const itemValue = items[key]; batch[itemValue.key] = itemValue; }); // make batch immutable to preserve integrity return Object.freeze(batch); -} +}; describe('RxFire Database', () => { let app: app.App = null; @@ -29,7 +29,9 @@ describe('RxFire Database', () => { return app.database().ref(path); }; - function prepareList(opts: { events?: ChildEvent[], skipnumber: number } = { skipnumber: 0 }) { + function prepareList( + opts: { events?: ChildEvent[]; skipnumber: number } = { skipnumber: 0 } + ) { const { events, skipnumber } = opts; const aref = ref(rando()); const snapChanges = list(aref, events); @@ -49,7 +51,7 @@ describe('RxFire Database', () => { * functions are useful if the process becomes brittle or tedious. * Note that removing is less necessary since the tests are run * offline. - * + * * Note: Database tests do not run exactly the same offline as * they do online. Querying can act differently, tests must * account for this. @@ -57,7 +59,7 @@ describe('RxFire Database', () => { beforeEach(() => { app = initializeApp({ projectId: 'rxfire-test-db', - databaseURL: "https://rxfire-test.firebaseio.com", + databaseURL: 'https://rxfire-test.firebaseio.com' }); database = app.database(); database.goOffline(); @@ -68,52 +70,56 @@ describe('RxFire Database', () => { }); describe('fromRef', () => { - const items = [{ name: 'one' }, { name: 'two' }, { name: 'three' }] - .map(item => ({ key: rando(), ...item })); + const items = [{ name: 'one' }, { name: 'two' }, { name: 'three' }].map( + item => ({ key: rando(), ...item }) + ); const itemsObj = batch(items); /** * fromRef() takes in a reference, an event, an optionally a listenType * parameter. This listenType determines if the listen will be a realtime * `on` or a one-time `once`. - * - * This test checks that providing the `once` listenType will result in a + * + * This test checks that providing the `once` listenType will result in a * one-time read. This is determined by setting a value at a reference, * subscribing to the observable and calling the MochaDone callback when - * the observable completes. - * - * The fact that the observable completes without any added operators - * indicates it was a one-time read. Realtime reads do not complete unless + * the observable completes. + * + * The fact that the observable completes without any added operators + * indicates it was a one-time read. Realtime reads do not complete unless * accompanied by an operator that forces a complete. */ - it('should complete using a once', (done) => { + it('should complete using a once', done => { const itemRef = ref(rando()); itemRef.set(itemsObj); const obs = fromRef(itemRef, 'value', 'once'); - obs.subscribe(_ => { }, () => { }, done); + obs.subscribe(_ => {}, () => {}, done); }); /** * This test checks that "non-existent" or null value references are * handled. */ - it('it should should handle non-existence', (done) => { + it('it should should handle non-existence', done => { const itemRef = ref(rando()); itemRef.set({}); const obs = fromRef(itemRef, 'value'); - obs.pipe(take(1)).subscribe(change => { - expect(change.snapshot.exists()).to.equal(false); - expect(change.snapshot.val()).to.equal(null); - }).add(done); + obs + .pipe(take(1)) + .subscribe(change => { + expect(change.snapshot.exists()).to.equal(false); + expect(change.snapshot.val()).to.equal(null); + }) + .add(done); }); /** * This test checks that the Observable unsubscribe mechanism works. - * + * * Calling unsubscribe should trigger the ref.off() method when using a * `on` listenType. */ - it('it should listen and then unsubscribe', (done) => { + it('it should listen and then unsubscribe', done => { const itemRef = ref(rando()); itemRef.set(itemsObj); const obs = fromRef(itemRef, 'value'); @@ -130,10 +136,9 @@ describe('RxFire Database', () => { }); describe('events', () => { - /** * This test provides the `child_added` event and tests that only - * `child_added` events are received. + * `child_added` events are received. */ it('should stream back a child_added event', (done: any) => { const itemRef = ref(rando()); @@ -156,7 +161,7 @@ describe('RxFire Database', () => { /** * This test provides the `child_changed` event and tests that only - * `child_changed` events are received. + * `child_changed` events are received. */ it('should stream back a child_changed event', (done: any) => { const itemRef = ref(rando()); @@ -177,7 +182,7 @@ describe('RxFire Database', () => { /** * This test provides the `child_removed` event and tests that only - * `child_removed` events are received. + * `child_removed` events are received. */ it('should stream back a child_removed event', (done: any) => { const itemRef = ref(rando()); @@ -198,7 +203,7 @@ describe('RxFire Database', () => { /** * This test provides the `child_moved` event and tests that only - * `child_moved` events are received. + * `child_moved` events are received. */ it('should stream back a child_moved event', (done: any) => { const itemRef = ref(rando()); @@ -214,12 +219,12 @@ describe('RxFire Database', () => { sub.unsubscribe(); done(); }); - itemRef.child(key).setPriority(-100, () => { }); + itemRef.child(key).setPriority(-100, () => {}); }); /** * This test provides the `value` event and tests that only - * `value` events are received. + * `value` events are received. */ it('should stream back a value event', (done: any) => { const itemRef = ref(rando()); @@ -237,7 +242,7 @@ describe('RxFire Database', () => { }); /** - * This test provides queries a reference and checks that the queried + * This test provides queries a reference and checks that the queried * values are streamed back. */ it('should stream back query results', (done: any) => { @@ -247,30 +252,30 @@ describe('RxFire Database', () => { const obs = fromRef(query, 'value'); obs.subscribe(change => { let child; - change.snapshot.forEach(snap => { child = snap.val(); return true; }); + change.snapshot.forEach(snap => { + child = snap.val(); + return true; + }); expect(child).to.eql(items[0]); done(); }); }); - }); - }); describe('list', () => { - - const items = [{ name: 'zero' }, { name: 'one' }, { name: 'two' }] - .map((item, i) => ({ key: `${i}`, ...item })); + const items = [{ name: 'zero' }, { name: 'one' }, { name: 'two' }].map( + (item, i) => ({ key: `${i}`, ...item }) + ); const itemsObj = batch(items); describe('events', () => { - /** * `value` events are provided first when subscribing to a list. We need * to know what the "intial" data list is, so a value event is used. */ - it('should stream value at first', (done) => { + it('should stream value at first', done => { const someRef = ref(rando()); const obs = list(someRef, ['child_added']); obs @@ -285,9 +290,9 @@ describe('RxFire Database', () => { }); /** - * This test checks that `child_added` events are only triggered when - * specified in the events array. - * + * This test checks that `child_added` events are only triggered when + * specified in the events array. + * * The first result is skipped because it is always `value`. A `take(1)` * is used to close the stream after the `child_added` event occurs. */ @@ -309,15 +314,18 @@ describe('RxFire Database', () => { * This test checks that events are emitted in proper order. The reference * is queried and the test ensures that the array is in proper order. */ - it('should stream in order events', (done) => { + it('should stream in order events', done => { const aref = ref(rando()); const obs = list(aref.orderByChild('name'), ['child_added']); - obs.pipe(take(1)).subscribe(changes => { - const names = changes.map(change => change.snapshot.val().name); - expect(names[0]).to.eql('one'); - expect(names[1]).to.eql('two'); - expect(names[2]).to.eql('zero'); - }).add(done); + obs + .pipe(take(1)) + .subscribe(changes => { + const names = changes.map(change => change.snapshot.val().name); + expect(names[0]).to.eql('one'); + expect(names[1]).to.eql('two'); + expect(names[2]).to.eql('zero'); + }) + .add(done); aref.set(itemsObj); }); @@ -327,16 +335,19 @@ describe('RxFire Database', () => { * skips the first value event and checks that the newly added item is * on top. */ - it('should stream in order events w/child_added', (done) => { + it('should stream in order events w/child_added', done => { const aref = ref(rando()); const obs = list(aref.orderByChild('name'), ['child_added']); - obs.pipe(skip(1), take(1)).subscribe(changes => { - const names = changes.map(change => change.snapshot.val().name); - expect(names[0]).to.eql('anotha one'); - expect(names[1]).to.eql('one'); - expect(names[2]).to.eql('two'); - expect(names[3]).to.eql('zero'); - }).add(done); + obs + .pipe(skip(1), take(1)) + .subscribe(changes => { + const names = changes.map(change => change.snapshot.val().name); + expect(names[0]).to.eql('anotha one'); + expect(names[1]).to.eql('one'); + expect(names[2]).to.eql('two'); + expect(names[3]).to.eql('zero'); + }) + .add(done); aref.set(itemsObj); aref.push({ name: 'anotha one' }); }); @@ -344,30 +355,38 @@ describe('RxFire Database', () => { /** * This test checks that a filtered reference still emits the proper events. */ - it('should stream events filtering', (done) => { + it('should stream events filtering', done => { const aref = ref(rando()); - const obs = list(aref.orderByChild('name').equalTo('zero'), ['child_added']); - obs.pipe(skip(1), take(1)).subscribe(changes => { - const names = changes.map(change => change.snapshot.val().name); - expect(names[0]).to.eql('zero'); - expect(names[1]).to.eql('zero'); - }).add(done); + const obs = list(aref.orderByChild('name').equalTo('zero'), [ + 'child_added' + ]); + obs + .pipe(skip(1), take(1)) + .subscribe(changes => { + const names = changes.map(change => change.snapshot.val().name); + expect(names[0]).to.eql('zero'); + expect(names[1]).to.eql('zero'); + }) + .add(done); aref.set(itemsObj); aref.push({ name: 'zero' }); }); /** * This test checks that the a `child_removed` event is processed in the - * array by testing that the new length is shorter than the original + * array by testing that the new length is shorter than the original * length. */ it('should process a new child_removed event', done => { const aref = ref(rando()); const obs = list(aref, ['child_added', 'child_removed']); - const sub = obs.pipe(skip(1), take(1)).subscribe(changes => { - const data = changes.map(change => change.snapshot.val()); - expect(data.length).to.eql(items.length - 1); - }).add(done); + const sub = obs + .pipe(skip(1), take(1)) + .subscribe(changes => { + const data = changes.map(change => change.snapshot.val()); + expect(data.length).to.eql(items.length - 1); + }) + .add(done); app.database().goOnline(); aref.set(itemsObj).then(() => { aref.child(items[0].key).remove(); @@ -375,16 +394,19 @@ describe('RxFire Database', () => { }); /** - * This test checks that the `child_changed` event is processed by + * This test checks that the `child_changed` event is processed by * checking the new value of the object in the array. */ - it('should process a new child_changed event', (done) => { + it('should process a new child_changed event', done => { const aref = ref(rando()); - const obs = list(aref, ['child_added', 'child_changed']) - const sub = obs.pipe(skip(1), take(1)).subscribe(changes => { - const data = changes.map(change => change.snapshot.val()); - expect(data[1].name).to.eql('lol'); - }).add(done); + const obs = list(aref, ['child_added', 'child_changed']); + const sub = obs + .pipe(skip(1), take(1)) + .subscribe(changes => { + const data = changes.map(change => change.snapshot.val()); + expect(data[1].name).to.eql('lol'); + }) + .add(done); app.database().goOnline(); aref.set(itemsObj).then(() => { aref.child(items[1].key).update({ name: 'lol' }); @@ -395,46 +417,55 @@ describe('RxFire Database', () => { * This test checks the `child_moved` event is processed by checking that * the new position is properly updated. */ - it('should process a new child_moved event', (done) => { + it('should process a new child_moved event', done => { const aref = ref(rando()); - const obs = list(aref, ['child_added', 'child_moved']) - const sub = obs.pipe(skip(1), take(1)).subscribe(changes => { - const data = changes.map(change => change.snapshot.val()); - // We moved the first item to the last item, so we check that - // the new result is now the last result - expect(data[data.length - 1]).to.eql(items[0]); - }).add(done); + const obs = list(aref, ['child_added', 'child_moved']); + const sub = obs + .pipe(skip(1), take(1)) + .subscribe(changes => { + const data = changes.map(change => change.snapshot.val()); + // We moved the first item to the last item, so we check that + // the new result is now the last result + expect(data[data.length - 1]).to.eql(items[0]); + }) + .add(done); app.database().goOnline(); aref.set(itemsObj).then(() => { - aref.child(items[0].key).setPriority('a', () => { }); + aref.child(items[0].key).setPriority('a', () => {}); }); }); /** * If no events array is provided in `list()` all events are listened to. - * + * * This test checks that all events are processed without providing the * array. */ - it('should listen to all events by default', (done) => { + it('should listen to all events by default', done => { const { snapChanges, ref } = prepareList(); - snapChanges.pipe(take(1)).subscribe(actions => { - const data = actions.map(a => a.snapshot.val()); - expect(data).to.eql(items); - }).add(done); + snapChanges + .pipe(take(1)) + .subscribe(actions => { + const data = actions.map(a => a.snapshot.val()); + expect(data).to.eql(items); + }) + .add(done); ref.set(itemsObj); }); /** * This test checks that multiple subscriptions work properly. */ - it('should handle multiple subscriptions (hot)', (done) => { + it('should handle multiple subscriptions (hot)', done => { const { snapChanges, ref } = prepareList(); - const sub = snapChanges.subscribe(() => { }).add(done); - snapChanges.pipe(take(1)).subscribe(actions => { - const data = actions.map(a => a.snapshot.val()); - expect(data).to.eql(items); - }).add(sub); + const sub = snapChanges.subscribe(() => {}).add(done); + snapChanges + .pipe(take(1)) + .subscribe(actions => { + const data = actions.map(a => a.snapshot.val()); + expect(data).to.eql(items); + }) + .add(sub); ref.set(itemsObj); }); @@ -443,46 +474,62 @@ describe('RxFire Database', () => { */ it('should handle multiple subscriptions (warm)', done => { const { snapChanges, ref } = prepareList(); - snapChanges.pipe(take(1)).subscribe(() => { }).add(() => { - snapChanges.pipe(take(1)).subscribe(actions => { - const data = actions.map(a => a.snapshot.val()); - expect(data).to.eql(items); - }).add(done); - }); + snapChanges + .pipe(take(1)) + .subscribe(() => {}) + .add(() => { + snapChanges + .pipe(take(1)) + .subscribe(actions => { + const data = actions.map(a => a.snapshot.val()); + expect(data).to.eql(items); + }) + .add(done); + }); ref.set(itemsObj); }); /** * This test checks that only `child_added` events are processed. */ - it('should listen to only child_added events', (done) => { - const { snapChanges, ref } = prepareList({ events: ['child_added'], skipnumber: 0 }); - snapChanges.pipe(take(1)).subscribe(actions => { - const data = actions.map(a => a.snapshot.val()); - expect(data).to.eql(items); - }).add(done); + it('should listen to only child_added events', done => { + const { snapChanges, ref } = prepareList({ + events: ['child_added'], + skipnumber: 0 + }); + snapChanges + .pipe(take(1)) + .subscribe(actions => { + const data = actions.map(a => a.snapshot.val()); + expect(data).to.eql(items); + }) + .add(done); ref.set(itemsObj); }); /** * This test checks that only `child_added` and `child_changed` events are * processed. - */ - it('should listen to only child_added, child_changed events', (done) => { + */ + + it('should listen to only child_added, child_changed events', done => { const { snapChanges, ref } = prepareList({ events: ['child_added', 'child_changed'], skipnumber: 1 }); const name = 'ligatures'; - snapChanges.pipe(take(1)).subscribe(actions => { - const data = actions.map(a => a.snapshot.val());; - const copy = [...items]; - copy[0].name = name; - expect(data).to.eql(copy); - }).add(done); + snapChanges + .pipe(take(1)) + .subscribe(actions => { + const data = actions.map(a => a.snapshot.val()); + const copy = [...items]; + copy[0].name = name; + expect(data).to.eql(copy); + }) + .add(done); app.database().goOnline(); ref.set(itemsObj).then(() => { - ref.child(items[0].key).update({ name }) + ref.child(items[0].key).update({ name }); }); }); @@ -492,9 +539,12 @@ describe('RxFire Database', () => { it('should handle empty sets', done => { const aref = ref(rando()); aref.set({}); - list(aref).pipe(take(1)).subscribe(data => { - expect(data.length).to.eql(0); - }).add(done); + list(aref) + .pipe(take(1)) + .subscribe(data => { + expect(data.length).to.eql(0); + }) + .add(done); }); /** @@ -506,34 +556,43 @@ describe('RxFire Database', () => { let namefilter$ = new BehaviorSubject(null); const aref = ref(rando()); aref.set(itemsObj); - namefilter$.pipe(switchMap(name => { - const filteredRef = name ? aref.child('name').equalTo(name) : aref - return list(filteredRef); - }), take(2)).subscribe(data => { - count = count + 1; - // the first time should all be 'added' - if (count === 1) { - expect(Object.keys(data).length).to.eql(3); - namefilter$.next(-1); - } - // on the second round, we should have filtered out everything - if (count === 2) { - expect(Object.keys(data).length).to.eql(0); - } - }).add(done); + namefilter$ + .pipe( + switchMap(name => { + const filteredRef = name + ? aref.child('name').equalTo(name) + : aref; + return list(filteredRef); + }), + take(2) + ) + .subscribe(data => { + count = count + 1; + // the first time should all be 'added' + if (count === 1) { + expect(Object.keys(data).length).to.eql(3); + namefilter$.next(-1); + } + // on the second round, we should have filtered out everything + if (count === 2) { + expect(Object.keys(data).length).to.eql(0); + } + }) + .add(done); }); - }); - }); describe('auditTrail', () => { - const items = [{ name: 'zero' }, { name: 'one' }, { name: 'two' }] - .map((item, i) => ({ key: `${i}`, ...item })); + const items = [{ name: 'zero' }, { name: 'one' }, { name: 'two' }].map( + (item, i) => ({ key: `${i}`, ...item }) + ); const itemsObj = batch(items); - function prepareAuditTrail(opts: { events?: ChildEvent[], skipnumber: number } = { skipnumber: 0 }) { + function prepareAuditTrail( + opts: { events?: ChildEvent[]; skipnumber: number } = { skipnumber: 0 } + ) { const { events, skipnumber } = opts; const aref = ref(rando()); aref.set(itemsObj); @@ -547,17 +606,13 @@ describe('RxFire Database', () => { /** * This test checks that auditTrail retuns all events by default. */ - it('should listen to all events by default', (done) => { - + it('should listen to all events by default', done => { const { changes } = prepareAuditTrail(); changes.subscribe(actions => { const data = actions.map(a => a.snapshot.val()); expect(data).to.eql(items); done(); }); - }); - }); - }); From c9d7d2d0c29334755a520f74ce2e3592ed88be6a Mon Sep 17 00:00:00 2001 From: David East Date: Wed, 4 Jul 2018 06:53:37 -0600 Subject: [PATCH 06/14] [AUTOMATED]: License Headers --- packages/rxfire/database/fromRef.ts | 16 ++++++++++++++++ packages/rxfire/database/index.ts | 16 ++++++++++++++++ packages/rxfire/database/interfaces.ts | 16 ++++++++++++++++ packages/rxfire/database/list/audit-trail.ts | 16 ++++++++++++++++ packages/rxfire/database/list/index.ts | 16 ++++++++++++++++ packages/rxfire/database/object/index.ts | 16 ++++++++++++++++ packages/rxfire/database/utils.ts | 16 ++++++++++++++++ packages/rxfire/test/database.test.ts | 16 ++++++++++++++++ 8 files changed, 128 insertions(+) diff --git a/packages/rxfire/database/fromRef.ts b/packages/rxfire/database/fromRef.ts index 79cf1d8378a..9e1c2cd8cf4 100644 --- a/packages/rxfire/database/fromRef.ts +++ b/packages/rxfire/database/fromRef.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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. + */ + import { database } from 'firebase'; import { Observable } from 'rxjs'; import { map, delay, share } from 'rxjs/operators'; diff --git a/packages/rxfire/database/index.ts b/packages/rxfire/database/index.ts index e12a9ba9d01..cdafece2c8d 100644 --- a/packages/rxfire/database/index.ts +++ b/packages/rxfire/database/index.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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 * from './fromRef'; export * from './interfaces'; export * from './list'; diff --git a/packages/rxfire/database/interfaces.ts b/packages/rxfire/database/interfaces.ts index b6d30c972f8..11263b7cd29 100644 --- a/packages/rxfire/database/interfaces.ts +++ b/packages/rxfire/database/interfaces.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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. + */ + import { database } from 'firebase'; export type QueryFn = (ref: database.Reference) => database.Query; diff --git a/packages/rxfire/database/list/audit-trail.ts b/packages/rxfire/database/list/audit-trail.ts index 29d9a864af7..bdfffeae752 100644 --- a/packages/rxfire/database/list/audit-trail.ts +++ b/packages/rxfire/database/list/audit-trail.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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. + */ + import { database } from 'firebase'; import { Observable } from 'rxjs'; import { SnapshotPrevKey, ChildEvent } from '../interfaces'; diff --git a/packages/rxfire/database/list/index.ts b/packages/rxfire/database/list/index.ts index 0929699dcf5..dcaba35d07a 100644 --- a/packages/rxfire/database/list/index.ts +++ b/packages/rxfire/database/list/index.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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. + */ + import { database } from 'firebase'; import { ChildEvent, SnapshotPrevKey } from '../interfaces'; import { Observable, of, merge } from 'rxjs'; diff --git a/packages/rxfire/database/object/index.ts b/packages/rxfire/database/object/index.ts index 865eebc49b0..db92e548c61 100644 --- a/packages/rxfire/database/object/index.ts +++ b/packages/rxfire/database/object/index.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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. + */ + import { database } from 'firebase'; import { SnapshotPrevKey } from '../interfaces'; import { fromRef } from '../fromRef'; diff --git a/packages/rxfire/database/utils.ts b/packages/rxfire/database/utils.ts index 2ebfdbdb244..8f7171dd5d2 100644 --- a/packages/rxfire/database/utils.ts +++ b/packages/rxfire/database/utils.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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 function isNil(obj: any): boolean { return obj === undefined || obj === null; } diff --git a/packages/rxfire/test/database.test.ts b/packages/rxfire/test/database.test.ts index efee6ef901f..27f178c7942 100644 --- a/packages/rxfire/test/database.test.ts +++ b/packages/rxfire/test/database.test.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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. + */ + import { expect } from 'chai'; import { initializeApp, database, app } from 'firebase'; import { fromRef } from '../database/fromRef'; From f88d8bafa0a03ce3727a4c8b22419ecaae108537 Mon Sep 17 00:00:00 2001 From: David East Date: Tue, 10 Jul 2018 10:28:02 -0600 Subject: [PATCH 07/14] Josh's comments. Database docs --- packages/rxfire/database/docs/database.md | 248 +++++++++++++++++++ packages/rxfire/database/fromRef.ts | 33 +-- packages/rxfire/database/interfaces.ts | 16 +- packages/rxfire/database/list/audit-trail.ts | 16 +- packages/rxfire/database/list/index.ts | 45 ++-- packages/rxfire/database/object/index.ts | 6 +- packages/rxfire/database/utils.ts | 6 +- packages/rxfire/package.json | 6 +- packages/rxfire/test/database.test.ts | 76 ++---- 9 files changed, 344 insertions(+), 108 deletions(-) create mode 100644 packages/rxfire/database/docs/database.md diff --git a/packages/rxfire/database/docs/database.md b/packages/rxfire/database/docs/database.md new file mode 100644 index 00000000000..d0b5f69d352 --- /dev/null +++ b/packages/rxfire/database/docs/database.md @@ -0,0 +1,248 @@ +# RxFire Database + +## Object Observables + +### `object()` +The `object()` function creates an observable that emits object changes. + +| | | +|-----------------|------------------------------------------| +| **function** | `object()` | +| **params** | `database.Reference` or `database.Query` | +| **import path** | `rxfire/database` | +| **return** | `Observable` | + +#### TypeScript Example +```ts +import { object } from 'rxfire/database'; +import { database, initializeApp } from 'firebase'; +import 'firebase/database'; +import { map } from 'rxjs/operators'; + +// Set up Firebase +const app = initializeApp({ /* config */ }); +const db = app.database(); +const ref = db.ref('users/david'); + +// Seed the database +ref.set({ name: 'David' }); + +object(ref).subscribe(change => { + const { event, snapshot, prevKey } = change; + console.log(event, ' will always be value'); + console.log(prevKey, ' the previous key'); + console.log(snapshot.val(), ' this is the data'); +}); + +// Retrieve the data and key +object(ref) + .pipe(map(change => ({ _key: change.snapshot.key, ...change.snapshot.val() }))) + .subscribe(data => { console.log(data); }); +``` + +## List Observables + +### `list()` +The `list()` function creates an observable that emits a sorted array for each child event change. The optional `events` parameter will filter which child events populate the array. + +| | | +|-----------------|-------------------------------------------------------| +| **function** | `list()` | +| **params** | ref: `database.Reference` or `database.Query`, events?: `ListenEvent[]` | +| **import path** | `rxfire/database` | +| **return** | `Observable` | + +#### TypeScript Example +```ts +import { list, ListenEvent } from 'rxfire/database'; +import { database } from 'firebase'; +import 'firebase/database'; +import { map } from 'rxjs/operators'; + +// Set up Firebase +const app = initializeApp({ /* config */ }); +const db = app.database(); +const ref = db.ref('users'); + +// Seed the database +ref.push({ name: 'David' }); + +list(ref).subscribe(changes => { + changes.forEach(change => { + const { snapshot, event, prevKey } = change; + console.log(event, ' the event that populated the array'); + console.log(prevKey, ' the previous key'); + console.log(snapshot.val(), ' this is the data of the single change'); + }); +}); + +// Retrieve the data, key, and event +list(ref) + .pipe( + map(changes => changes.map(c => { + return { _key: c.snapshot.key, event: c.event, ...c.snapshot.val(); }; + )) + ) + .subscribe(users => { console.log(users); }) + +// Listen only to 'child_added' events +list(ref, [ListenEvent.added] /* 'child_added' for js */) + .subscribe(addedChanges => { console.log(addedChanges); }); + +// Listen only to 'child_added' and 'child_removed' events +list(ref, [ListenEvent.added, ListenEvent.removed] /* 'child_added', 'child_removed' for js */) + .subscribe(addedChanges => { console.log(addedChanges); }); +``` + +### `stateChanges()` +The `stateChanges()` function creates an observable that emits each time a change occurs at the reference or query passed. This is useful for tracking the changes in your list. The optional `events` parameter will filter which child events populate the array. + +| | | +|-----------------|------------------------------------------------------| +| **function** | `stateChanges()` | +| **params** | ref: `database.Reference` or `database.Query`, events?: `ListenEvent[]` | +| **import path** | `rxfire/database` | +| **return** | `Observable` | + +#### Example +```ts +import { stateChanges, ListenEvent } from 'rxfire/database'; +import { database } from 'firebase'; +import 'firebase/database'; +import { map } from 'rxjs/operators'; + +// Set up Firebase +const app = initializeApp({ /* config */ }); +const db = app.database(); +const ref = db.ref('users'); + +// Seed the database +ref.push({ name: 'David' }); + +stateChanges(ref).subscribe(change => { + const { event, snapshot, prevKey } = change; + console.log(event, ' the event type that just occured'); + console.log(snapshot.val(), ' the value of the change'); +}); + +// Retrieve the data, event, and key +stateChanges(ref).pipe( + map(change => { + return { + _key: change.snapshot.key, + event: change.event, + ...change.snapshot.val(); + }; + }) +).subscribe(data => { console.log(data); }); + +// Listen only to 'child_added' events +stateChanges(ref, [ListenEvent.added] /* 'child_added' for js */) + .subscribe(addedChanges => { console.log(addedChanges); }); + +// Listen only to 'child_added' and 'child_removed' events +stateChanges(ref, [ListenEvent.added, ListenEvent.removed] /* 'child_added', 'child_removed' for js */) + .subscribe(addedChanges => { console.log(addedChanges); }); + +``` + +### `auditTrail()` +The `auditTrail()` function creates an observable that emits the entire state trail. This is useful for debugging or replaying the state of a list in your app. The optional `events` parameter will filter which child events populate the array. + +| | | +|-----------------|------------------------------------------------------| +| **function** | `auditTrail()` | +| **params** | ref: `database.Reference` or `database.Query`, events?: `ListenEvent[]` | +| **import path** | `rxfire/database` | +| **return** | `Observable` | + +#### TypeScript Example +```ts +import { auditTrail, ListenEvent } from 'rxfire/database'; +import { database } from 'firebase'; +import 'firebase/database'; +import { map } from 'rxjs/operators'; + +// Set up Firebase +const app = initializeApp({ /* config */ }); +const db = app.database(); +const ref = db.ref('users'); + +// Seed the database +const davidRef = ref.push(); +davidRef.set({ name: 'David' }); + +auditTrail(ref).pipe( + map(change => { + return { + _key: change.snapshot.key, + event: change.event, + ...change.snapshot.val(); + }; + }) +).subscribe(stateTrail => { + console.log(stateTrail); + /** + first emission: + [{ _key: '3qtWqaKga8jA; name: 'David', event: 'child_added' }] + + second emission: + [ + { _key: '3qtWqaKga8jA; name: 'David', event: 'child_added' }, + { _key: '3qtWqaKga8jA; name: 'David', event: 'child_removed' } + ] + */ +}); + +// When more events occur the trail still contains the previous events +// In this case we'll remove the only item +davidRef.remove(); + +// Now this will trigger the subscribe function above +``` + +## Event Observables + +The `fromRef()` function creates an observable that emits reference changes. + +| | | +|-----------------|------------------------------------------| +| **function** | `fromRef()` | +| **params** | ref: `database.Reference` or `database.Query`, event: `ListenEvent` | +| **import path** | `rxfire/database` | +| **return** | `Observable` | + +#### TypeScript Example +```ts +import { fromRef, ListenEvent } from 'rxfire/database'; +import { database, initializeApp } from 'firebase'; +import 'firebase/database'; +import { merge } from 'rxjs'; +import { map } from 'rxjs/operators'; + +// Set up Firebase +const app = initializeApp({ /* config */ }); +const db = app.database(); +const ref = db.ref('users'); + +// Seed the database +ref.child('david').set({ name: 'David' }); + +// Subscribe to events +fromRef(ref, ListenEvent.value /* 'value' for js users */) + .subscribe(change => { + // Get value changes, this is basically what `object()` does + }); + +// Merge multiple events (however this is really what `stateChanges()` does) +const addedChanges = fromRef(ref, ListenEvent.added); +const removedChanges = fromRef(ref, ListenEvent.removed); +merge(addedChanges, removedChanges) + .subscribe(change => { + const { event, snapshot, prevKey } = change; + console.log(event); // This will be 'child_added' or 'child_removed' + // Note: Don't write this yourself. Use `stateChanges()` for this type of + // functionality. This is just an example of using fromRef for custom + // behavior. + }); +``` diff --git a/packages/rxfire/database/fromRef.ts b/packages/rxfire/database/fromRef.ts index 9e1c2cd8cf4..77963b02c8a 100644 --- a/packages/rxfire/database/fromRef.ts +++ b/packages/rxfire/database/fromRef.ts @@ -17,7 +17,7 @@ import { database } from 'firebase'; import { Observable } from 'rxjs'; import { map, delay, share } from 'rxjs/operators'; -import { ListenEvent, SnapshotPrevKey } from './interfaces'; +import { ListenEvent, QueryChange } from './interfaces'; /** * Create an observable from a Database Reference or Database Query. @@ -26,40 +26,31 @@ import { ListenEvent, SnapshotPrevKey } from './interfaces'; */ export function fromRef( ref: database.Query, - event: ListenEvent, - listenType = 'on' -): Observable { - return new Observable(subscriber => { - const fn = ref[listenType]( + event: ListenEvent +): Observable { + return new Observable(subscriber => { + const fn = ref.on( event, (snapshot, prevKey) => { subscriber.next({ snapshot, prevKey, event }); - if (listenType == 'once') { - subscriber.complete(); - } }, subscriber.error.bind(subscriber) ); - if (listenType == 'on') { - return { - unsubscribe() { - ref.off(event, fn); - } - }; - } else { - return { unsubscribe() {} }; - } + return { + unsubscribe() { + ref.off(event, fn); + } + }; }).pipe( // Ensures subscribe on observable is async. This handles // a quirk in the SDK where on/once callbacks can happen // synchronously. - delay(0), - share() + delay(0) ); } export const unwrap = () => - map((payload: SnapshotPrevKey) => { + map((payload: QueryChange) => { const { snapshot, prevKey } = payload; let key: string | null = null; if (snapshot.exists()) { diff --git a/packages/rxfire/database/interfaces.ts b/packages/rxfire/database/interfaces.ts index 11263b7cd29..54d0b7d48ff 100644 --- a/packages/rxfire/database/interfaces.ts +++ b/packages/rxfire/database/interfaces.ts @@ -16,15 +16,15 @@ import { database } from 'firebase'; -export type QueryFn = (ref: database.Reference) => database.Query; -export type ChildEvent = - | 'child_added' - | 'child_removed' - | 'child_changed' - | 'child_moved'; -export type ListenEvent = 'value' | ChildEvent; +export enum ListenEvent { + added = 'child_added', + removed = 'child_removed', + changed = 'child_changed', + moved = 'child_moved', + value = 'value' +}; -export interface SnapshotPrevKey { +export interface QueryChange { snapshot: database.DataSnapshot; prevKey: string | null | undefined; event: ListenEvent; diff --git a/packages/rxfire/database/list/audit-trail.ts b/packages/rxfire/database/list/audit-trail.ts index bdfffeae752..80765b60100 100644 --- a/packages/rxfire/database/list/audit-trail.ts +++ b/packages/rxfire/database/list/audit-trail.ts @@ -16,22 +16,22 @@ import { database } from 'firebase'; import { Observable } from 'rxjs'; -import { SnapshotPrevKey, ChildEvent } from '../interfaces'; +import { QueryChange, ListenEvent } from '../interfaces'; import { fromRef } from '../fromRef'; import { map, withLatestFrom, scan, skipWhile } from 'rxjs/operators'; -import { stateChanges } from './'; +import { stateChanges } from './index'; interface LoadedMetadata { - data: SnapshotPrevKey; + data: QueryChange; lastKeyToLoad: any; } export function auditTrail( query: database.Query, - events?: ChildEvent[] -): Observable { + events?: ListenEvent[] +): Observable { const auditTrail$ = stateChanges(query, events).pipe( - scan((current, changes) => [...current, changes], []) + scan((current, changes) => [...current, changes], []) ); return waitForLoaded(query, auditTrail$); } @@ -40,7 +40,7 @@ function loadedData(query: database.Query): Observable { // Create an observable of loaded values to retrieve the // known dataset. This will allow us to know what key to // emit the "whole" array at when listening for child events. - return fromRef(query, 'value').pipe( + return fromRef(query, ListenEvent.value).pipe( map(data => { // Store the last key in the data set let lastKeyToLoad; @@ -57,7 +57,7 @@ function loadedData(query: database.Query): Observable { function waitForLoaded( query: database.Query, - snap$: Observable + snap$: Observable ) { const loaded$ = loadedData(query); return loaded$.pipe( diff --git a/packages/rxfire/database/list/index.ts b/packages/rxfire/database/list/index.ts index dcaba35d07a..0cdc38f46b5 100644 --- a/packages/rxfire/database/list/index.ts +++ b/packages/rxfire/database/list/index.ts @@ -15,24 +15,33 @@ */ import { database } from 'firebase'; -import { ChildEvent, SnapshotPrevKey } from '../interfaces'; -import { Observable, of, merge } from 'rxjs'; +import { QueryChange, ListenEvent } from '../interfaces'; +import { Observable, of, merge, from } from 'rxjs'; import { validateEventsArray, isNil } from '../utils'; import { fromRef } from '../fromRef'; -import { switchMap, scan, distinctUntilChanged } from 'rxjs/operators'; +import { switchMap, scan, distinctUntilChanged, map } from 'rxjs/operators'; -export function stateChanges(query: database.Query, events?: ChildEvent[]) { +export function stateChanges(query: database.Query, events?: ListenEvent[]) { events = validateEventsArray(events); const childEvent$ = events.map(event => fromRef(query, event)); return merge(...childEvent$); } +function fromOnce(query: database.Query): Observable { + return from(query.once(ListenEvent.value)).pipe( + map((snapshot) => { + const event = ListenEvent.value; + return { snapshot, prevKey: null, event }; + }) + ); +} + export function list( query: database.Query, - events?: ChildEvent[] -): Observable { + events?: ListenEvent[] +): Observable { events = validateEventsArray(events); - return fromRef(query, 'value', 'once').pipe( + return fromOnce(query).pipe( switchMap(change => { const childEvent$ = [of(change)]; events.forEach(event => childEvent$.push(fromRef(query, event))); @@ -42,7 +51,7 @@ export function list( ); } -function positionFor(changes: SnapshotPrevKey[], key) { +function positionFor(changes: QueryChange[], key) { const len = changes.length; for (let i = 0; i < len; i++) { if (changes[i].snapshot.key === key) { @@ -52,7 +61,7 @@ function positionFor(changes: SnapshotPrevKey[], key) { return -1; } -function positionAfter(changes: SnapshotPrevKey[], prevKey?: string) { +function positionAfter(changes: QueryChange[], prevKey?: string) { if (isNil(prevKey)) { return 0; } else { @@ -65,24 +74,28 @@ function positionAfter(changes: SnapshotPrevKey[], prevKey?: string) { } } -function buildView(current: SnapshotPrevKey[], change: SnapshotPrevKey) { +function buildView(current: QueryChange[], change: QueryChange) { const { snapshot, prevKey, event } = change; const { key } = snapshot; const currentKeyPosition = positionFor(current, key); const afterPreviousKeyPosition = positionAfter(current, prevKey); switch (event) { - case 'value': + case ListenEvent.value: if (change.snapshot && change.snapshot.exists()) { let prevKey = null; change.snapshot.forEach(snapshot => { - const action: SnapshotPrevKey = { snapshot, event: 'value', prevKey }; + const action: QueryChange = { + snapshot, + event: ListenEvent.value, + prevKey + }; prevKey = snapshot.key; current = [...current, action]; return false; }); } return current; - case 'child_added': + case ListenEvent.added: if (currentKeyPosition > -1) { // check that the previouskey is what we expect, else reorder const previous = current[currentKeyPosition - 1]; @@ -97,11 +110,11 @@ function buildView(current: SnapshotPrevKey[], change: SnapshotPrevKey) { current.splice(afterPreviousKeyPosition, 0, change); } return current; - case 'child_removed': + case ListenEvent.removed: return current.filter(x => x.snapshot.key !== snapshot.key); - case 'child_changed': + case ListenEvent.changed: return current.map(x => (x.snapshot.key === key ? change : x)); - case 'child_moved': + case ListenEvent.moved: if (currentKeyPosition > -1) { const data = current.splice(currentKeyPosition, 1)[0]; current = current.slice(); diff --git a/packages/rxfire/database/object/index.ts b/packages/rxfire/database/object/index.ts index db92e548c61..240b61aac95 100644 --- a/packages/rxfire/database/object/index.ts +++ b/packages/rxfire/database/object/index.ts @@ -15,7 +15,7 @@ */ import { database } from 'firebase'; -import { SnapshotPrevKey } from '../interfaces'; +import { QueryChange, ListenEvent } from '../interfaces'; import { fromRef } from '../fromRef'; import { Observable } from 'rxjs'; @@ -23,6 +23,6 @@ import { Observable } from 'rxjs'; * Get the snapshot changes of an object * @param query */ -export function object(query: database.Query): Observable { - return fromRef(query, 'value'); +export function object(query: database.Query): Observable { + return fromRef(query, ListenEvent.value); } diff --git a/packages/rxfire/database/utils.ts b/packages/rxfire/database/utils.ts index 8f7171dd5d2..276fed1da72 100644 --- a/packages/rxfire/database/utils.ts +++ b/packages/rxfire/database/utils.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { ListenEvent } from './interfaces'; + export function isNil(obj: any): boolean { return obj === undefined || obj === null; } @@ -23,9 +25,9 @@ export function isNil(obj: any): boolean { * that is populated with all the Realtime Database child events. * @param events */ -export function validateEventsArray(events?: any[]) { +export function validateEventsArray(events?: ListenEvent[]) { if (isNil(events) || events!.length === 0) { - events = ['child_added', 'child_removed', 'child_changed', 'child_moved']; + events = [ListenEvent.added, ListenEvent.removed, ListenEvent.changed, ListenEvent.moved]; } return events; } diff --git a/packages/rxfire/package.json b/packages/rxfire/package.json index dedeff2eb43..e499d31ffb4 100644 --- a/packages/rxfire/package.json +++ b/packages/rxfire/package.json @@ -76,6 +76,8 @@ "/storage/dist", "/functions/package.json", "/functions/dist", + "/database/dist", + "/database/package.json", "/rxfire-auth.js", "/rxfire-auth.js.map", "/rxfire-firestore.js", @@ -83,6 +85,8 @@ "/rxfire-functions.js", "/rxfire-functions.js.map", "/rxfire-storage.js", - "/rxfire-storage.js.map" + "/rxfire-storage.js.map", + "/rxfire-database.js", + "/rxfire-database.js.map" ] } diff --git a/packages/rxfire/test/database.test.ts b/packages/rxfire/test/database.test.ts index 27f178c7942..e06a7f668e6 100644 --- a/packages/rxfire/test/database.test.ts +++ b/packages/rxfire/test/database.test.ts @@ -17,7 +17,7 @@ import { expect } from 'chai'; import { initializeApp, database, app } from 'firebase'; import { fromRef } from '../database/fromRef'; -import { list, ChildEvent } from '../database'; +import { list, ListenEvent } from '../database'; import { take, skip, switchMap } from 'rxjs/operators'; import { BehaviorSubject } from 'rxjs'; import { auditTrail } from '../database/list/audit-trail'; @@ -46,7 +46,7 @@ describe('RxFire Database', () => { }; function prepareList( - opts: { events?: ChildEvent[]; skipnumber: number } = { skipnumber: 0 } + opts: { events?: ListenEvent[]; skipnumber: number } = { skipnumber: 0 } ) { const { events, skipnumber } = opts; const aref = ref(rando()); @@ -91,27 +91,6 @@ describe('RxFire Database', () => { ); const itemsObj = batch(items); - /** - * fromRef() takes in a reference, an event, an optionally a listenType - * parameter. This listenType determines if the listen will be a realtime - * `on` or a one-time `once`. - * - * This test checks that providing the `once` listenType will result in a - * one-time read. This is determined by setting a value at a reference, - * subscribing to the observable and calling the MochaDone callback when - * the observable completes. - * - * The fact that the observable completes without any added operators - * indicates it was a one-time read. Realtime reads do not complete unless - * accompanied by an operator that forces a complete. - */ - it('should complete using a once', done => { - const itemRef = ref(rando()); - itemRef.set(itemsObj); - const obs = fromRef(itemRef, 'value', 'once'); - obs.subscribe(_ => {}, () => {}, done); - }); - /** * This test checks that "non-existent" or null value references are * handled. @@ -119,7 +98,7 @@ describe('RxFire Database', () => { it('it should should handle non-existence', done => { const itemRef = ref(rando()); itemRef.set({}); - const obs = fromRef(itemRef, 'value'); + const obs = fromRef(itemRef, ListenEvent.value); obs .pipe(take(1)) .subscribe(change => { @@ -132,13 +111,12 @@ describe('RxFire Database', () => { /** * This test checks that the Observable unsubscribe mechanism works. * - * Calling unsubscribe should trigger the ref.off() method when using a - * `on` listenType. + * Calling unsubscribe should trigger the ref.off() method. */ it('it should listen and then unsubscribe', done => { const itemRef = ref(rando()); itemRef.set(itemsObj); - const obs = fromRef(itemRef, 'value'); + const obs = fromRef(itemRef, ListenEvent.value); let count = 0; const sub = obs.subscribe(_ => { count = count + 1; @@ -160,12 +138,12 @@ describe('RxFire Database', () => { const itemRef = ref(rando()); const data = itemsObj; itemRef.set(data); - const obs = fromRef(itemRef, 'child_added'); + const obs = fromRef(itemRef, ListenEvent.added); let count = 0; const sub = obs.subscribe(change => { count = count + 1; const { event, snapshot } = change; - expect(event).to.equal('child_added'); + expect(event).to.equal(ListenEvent.added); expect(snapshot.val()).to.eql(data[snapshot.key]); if (count === items.length) { done(); @@ -182,12 +160,12 @@ describe('RxFire Database', () => { it('should stream back a child_changed event', (done: any) => { const itemRef = ref(rando()); itemRef.set(itemsObj); - const obs = fromRef(itemRef, 'child_changed'); + const obs = fromRef(itemRef, ListenEvent.changed); const name = 'look at what you made me do'; const key = items[0].key; const sub = obs.subscribe(change => { const { event, snapshot } = change; - expect(event).to.equal('child_changed'); + expect(event).to.equal(ListenEvent.changed); expect(snapshot.key).to.equal(key); expect(snapshot.val()).to.eql({ key, name }); sub.unsubscribe(); @@ -203,12 +181,12 @@ describe('RxFire Database', () => { it('should stream back a child_removed event', (done: any) => { const itemRef = ref(rando()); itemRef.set(itemsObj); - const obs = fromRef(itemRef, 'child_removed'); + const obs = fromRef(itemRef, ListenEvent.removed); const key = items[0].key; const name = items[0].name; const sub = obs.subscribe(change => { const { event, snapshot } = change; - expect(event).to.equal('child_removed'); + expect(event).to.equal(ListenEvent.removed); expect(snapshot.key).to.equal(key); expect(snapshot.val()).to.eql({ key, name }); sub.unsubscribe(); @@ -224,12 +202,12 @@ describe('RxFire Database', () => { it('should stream back a child_moved event', (done: any) => { const itemRef = ref(rando()); itemRef.set(itemsObj); - const obs = fromRef(itemRef, 'child_moved'); + const obs = fromRef(itemRef, ListenEvent.moved); const key = items[2].key; const name = items[2].name; const sub = obs.subscribe(change => { const { event, snapshot } = change; - expect(event).to.equal('child_moved'); + expect(event).to.equal(ListenEvent.moved); expect(snapshot.key).to.equal(key); expect(snapshot.val()).to.eql({ key, name }); sub.unsubscribe(); @@ -246,10 +224,10 @@ describe('RxFire Database', () => { const itemRef = ref(rando()); const data = itemsObj; itemRef.set(data); - const obs = fromRef(itemRef, 'value'); + const obs = fromRef(itemRef, ListenEvent.value); const sub = obs.subscribe(change => { const { event, snapshot } = change; - expect(event).to.equal('value'); + expect(event).to.equal(ListenEvent.value); expect(snapshot.val()).to.eql(data); done(); sub.unsubscribe(); @@ -265,7 +243,7 @@ describe('RxFire Database', () => { const itemRef = ref(rando()); itemRef.set(itemsObj); const query = itemRef.orderByChild('name').equalTo(items[0].name); - const obs = fromRef(query, 'value'); + const obs = fromRef(query, ListenEvent.value); obs.subscribe(change => { let child; change.snapshot.forEach(snap => { @@ -293,7 +271,7 @@ describe('RxFire Database', () => { */ it('should stream value at first', done => { const someRef = ref(rando()); - const obs = list(someRef, ['child_added']); + const obs = list(someRef, [ListenEvent.added]); obs .pipe(take(1)) .subscribe(changes => { @@ -314,7 +292,7 @@ describe('RxFire Database', () => { */ it('should process a new child_added event', done => { const aref = ref(rando()); - const obs = list(aref, ['child_added']); + const obs = list(aref, [ListenEvent.added]); obs .pipe(skip(1), take(1)) .subscribe(changes => { @@ -332,7 +310,7 @@ describe('RxFire Database', () => { */ it('should stream in order events', done => { const aref = ref(rando()); - const obs = list(aref.orderByChild('name'), ['child_added']); + const obs = list(aref.orderByChild('name'), [ListenEvent.added]); obs .pipe(take(1)) .subscribe(changes => { @@ -353,7 +331,7 @@ describe('RxFire Database', () => { */ it('should stream in order events w/child_added', done => { const aref = ref(rando()); - const obs = list(aref.orderByChild('name'), ['child_added']); + const obs = list(aref.orderByChild('name'), [ListenEvent.added]); obs .pipe(skip(1), take(1)) .subscribe(changes => { @@ -374,7 +352,7 @@ describe('RxFire Database', () => { it('should stream events filtering', done => { const aref = ref(rando()); const obs = list(aref.orderByChild('name').equalTo('zero'), [ - 'child_added' + ListenEvent.added ]); obs .pipe(skip(1), take(1)) @@ -395,7 +373,7 @@ describe('RxFire Database', () => { */ it('should process a new child_removed event', done => { const aref = ref(rando()); - const obs = list(aref, ['child_added', 'child_removed']); + const obs = list(aref, [ListenEvent.added, ListenEvent.removed]); const sub = obs .pipe(skip(1), take(1)) .subscribe(changes => { @@ -415,7 +393,7 @@ describe('RxFire Database', () => { */ it('should process a new child_changed event', done => { const aref = ref(rando()); - const obs = list(aref, ['child_added', 'child_changed']); + const obs = list(aref, [ListenEvent.added, ListenEvent.changed]); const sub = obs .pipe(skip(1), take(1)) .subscribe(changes => { @@ -435,7 +413,7 @@ describe('RxFire Database', () => { */ it('should process a new child_moved event', done => { const aref = ref(rando()); - const obs = list(aref, ['child_added', 'child_moved']); + const obs = list(aref, [ListenEvent.added, ListenEvent.moved]); const sub = obs .pipe(skip(1), take(1)) .subscribe(changes => { @@ -510,7 +488,7 @@ describe('RxFire Database', () => { */ it('should listen to only child_added events', done => { const { snapChanges, ref } = prepareList({ - events: ['child_added'], + events: [ListenEvent.added], skipnumber: 0 }); snapChanges @@ -530,7 +508,7 @@ describe('RxFire Database', () => { it('should listen to only child_added, child_changed events', done => { const { snapChanges, ref } = prepareList({ - events: ['child_added', 'child_changed'], + events: [ListenEvent.added, ListenEvent.changed], skipnumber: 1 }); const name = 'ligatures'; @@ -607,7 +585,7 @@ describe('RxFire Database', () => { const itemsObj = batch(items); function prepareAuditTrail( - opts: { events?: ChildEvent[]; skipnumber: number } = { skipnumber: 0 } + opts: { events?: ListenEvent[]; skipnumber: number } = { skipnumber: 0 } ) { const { events, skipnumber } = opts; const aref = ref(rando()); From 9425cc4bec9877bfe574619b6da516ad114151b9 Mon Sep 17 00:00:00 2001 From: David East Date: Tue, 10 Jul 2018 10:31:54 -0600 Subject: [PATCH 08/14] [AUTOMATED]: Prettier Code Styling --- packages/rxfire/database/interfaces.ts | 2 +- packages/rxfire/database/list/index.ts | 10 +++++----- packages/rxfire/database/utils.ts | 7 ++++++- packages/rxfire/{database => }/docs/database.md | 0 4 files changed, 12 insertions(+), 7 deletions(-) rename packages/rxfire/{database => }/docs/database.md (100%) diff --git a/packages/rxfire/database/interfaces.ts b/packages/rxfire/database/interfaces.ts index 54d0b7d48ff..88f72ca1fcd 100644 --- a/packages/rxfire/database/interfaces.ts +++ b/packages/rxfire/database/interfaces.ts @@ -22,7 +22,7 @@ export enum ListenEvent { changed = 'child_changed', moved = 'child_moved', value = 'value' -}; +} export interface QueryChange { snapshot: database.DataSnapshot; diff --git a/packages/rxfire/database/list/index.ts b/packages/rxfire/database/list/index.ts index 0cdc38f46b5..9edcb0d9050 100644 --- a/packages/rxfire/database/list/index.ts +++ b/packages/rxfire/database/list/index.ts @@ -29,7 +29,7 @@ export function stateChanges(query: database.Query, events?: ListenEvent[]) { function fromOnce(query: database.Query): Observable { return from(query.once(ListenEvent.value)).pipe( - map((snapshot) => { + map(snapshot => { const event = ListenEvent.value; return { snapshot, prevKey: null, event }; }) @@ -84,10 +84,10 @@ function buildView(current: QueryChange[], change: QueryChange) { if (change.snapshot && change.snapshot.exists()) { let prevKey = null; change.snapshot.forEach(snapshot => { - const action: QueryChange = { - snapshot, - event: ListenEvent.value, - prevKey + const action: QueryChange = { + snapshot, + event: ListenEvent.value, + prevKey }; prevKey = snapshot.key; current = [...current, action]; diff --git a/packages/rxfire/database/utils.ts b/packages/rxfire/database/utils.ts index 276fed1da72..e34b33b6964 100644 --- a/packages/rxfire/database/utils.ts +++ b/packages/rxfire/database/utils.ts @@ -27,7 +27,12 @@ export function isNil(obj: any): boolean { */ export function validateEventsArray(events?: ListenEvent[]) { if (isNil(events) || events!.length === 0) { - events = [ListenEvent.added, ListenEvent.removed, ListenEvent.changed, ListenEvent.moved]; + events = [ + ListenEvent.added, + ListenEvent.removed, + ListenEvent.changed, + ListenEvent.moved + ]; } return events; } diff --git a/packages/rxfire/database/docs/database.md b/packages/rxfire/docs/database.md similarity index 100% rename from packages/rxfire/database/docs/database.md rename to packages/rxfire/docs/database.md From e0fb675f07f5da5bc722fe0152ed2aa0f8a1e214 Mon Sep 17 00:00:00 2001 From: David East Date: Tue, 10 Jul 2018 11:17:53 -0600 Subject: [PATCH 09/14] Firestore docs --- packages/rxfire/docs/database.md | 4 +- packages/rxfire/docs/firestore.md | 246 ++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 packages/rxfire/docs/firestore.md diff --git a/packages/rxfire/docs/database.md b/packages/rxfire/docs/database.md index d0b5f69d352..bca8be1c7d2 100644 --- a/packages/rxfire/docs/database.md +++ b/packages/rxfire/docs/database.md @@ -8,7 +8,7 @@ The `object()` function creates an observable that emits object changes. | | | |-----------------|------------------------------------------| | **function** | `object()` | -| **params** | `database.Reference` or `database.Query` | +| **params** | `database.Reference` | | **import path** | `rxfire/database` | | **return** | `Observable` | @@ -104,7 +104,7 @@ The `stateChanges()` function creates an observable that emits each time a chang | **import path** | `rxfire/database` | | **return** | `Observable` | -#### Example +#### TypeScript Example ```ts import { stateChanges, ListenEvent } from 'rxfire/database'; import { database } from 'firebase'; diff --git a/packages/rxfire/docs/firestore.md b/packages/rxfire/docs/firestore.md new file mode 100644 index 00000000000..e2fe9375dec --- /dev/null +++ b/packages/rxfire/docs/firestore.md @@ -0,0 +1,246 @@ +# RxFire Firestore + +## Document Observables + +### `doc()` +The `doc()` function creates an observable that emits document changes. + +| | | +|-----------------|------------------------------------------| +| **function** | `doc()` | +| **params** | `firestore.DocumentReference` | +| **import path** | `rxfire/firestore` | +| **return** | `Observable` | + +#### TypeScript Example +```ts +import { doc } from 'rxfire/firestore'; +import { firestore, initializeApp } from 'firebase'; +import 'firebase/firestore'; +import { map } from 'rxjs/operators'; + +// Set up Firebase +const app = initializeApp({ /* config */ }); +const db = app.firestore(); +const davidDoc = db.doc('users/david'); + +// Seed the firestore +davidDoc.set({ name: 'David' }); + +doc(davidDoc).subscribe(snapshot => { + console.log(snapshot.id); + console.log(snapshot.data()); +}); +``` + +## Collection Observables + +### `collection()` +The `collection()` function creates an observable that emits collection changes. + +| | | +|-----------------|------------------------------------------| +| **function** | `collection()` | +| **params** | `firestore.CollectionReference` | `firestore.Query` | +| **import path** | `rxfire/firestore` | +| **return** | `Observable` | + +#### TypeScript Example +```ts +import { collection } from 'rxfire/firestore'; +import { firestore, initializeApp } from 'firebase'; +import 'firebase/firestore'; +import { map } from 'rxjs/operators'; + +// Set up Firebase +const app = initializeApp({ /* config */ }); +const db = app.firestore(); +const davidDoc = db.doc('users/david'); + +// Seed the firestore +davidDoc.set({ name: 'David' }); + +collection(db.collection('users')) + .pipe(map(docs => docs.map(d => d.data()))) + .subscribe(users => { console.log(users) }); +``` + +### `docChanges()` +The `docChanges()` function creates an observable that emits the event changes on a collection. This is different than the collection function in that it does not contain the state of your application but only the individual changes. The optional `events` parameter will filter which child events populate the array. + +| | | +|-----------------|------------------------------------------| +| **function** | `docChanges()` | +| **params** | query: `firestore.CollectionReference` | `firestore.Query`, events?: `firestore.DocumentChangeType[]` | +| **import path** | `rxfire/firestore` | +| **return** | `Observable` | + +#### TypeScript Example +```ts +import { docChanges } from 'rxfire/firestore'; +import { firestore, initializeApp } from 'firebase'; +import 'firebase/firestore'; +import { map } from 'rxjs/operators'; + +// Set up Firebase +const app = initializeApp({ /* config */ }); +const db = app.firestore(); +const davidDoc = db.doc('users/david'); + +// Seed the firestore +davidDoc.set({ name: 'David' }); + +docChanges(db.collection('users')) + .subscribe(changes => { console.log(users) }); + +// Listen to only 'added' events +docChanges(db.collection('users'), ['added']) + .subscribe(addedEvents => { console.log(addedEvents) }); +``` + +### `sortedChanges()` +The `sortedChanges()` function creates an observable that emits the reduced state of individual changes. This is different than the collection function in that it creates an array out of every individual change to occur. It also contains the `type` property to indicate what kind of change occured. The optional `events` parameter will filter which child events populate the array. + +| | | +|-----------------|------------------------------------------| +| **function** | `sortedChanges()` | +| **params** | query: `firestore.CollectionReference` | `firestore.Query`, events?: `firestore.DocumentChangeType[]` | +| **import path** | `rxfire/firestore` | +| **return** | `Observable` | + +#### TypeScript Example +```ts +import { sortedChanges } from 'rxfire/firestore'; +import { firestore, initializeApp } from 'firebase'; +import 'firebase/firestore'; +import { map } from 'rxjs/operators'; + +// Set up Firebase +const app = initializeApp({ /* config */ }); +const db = app.firestore(); +const davidDoc = db.doc('users/david'); + +// Seed the firestore +davidDoc.set({ name: 'David' }); + +sortedChanges(db.collection('users')) + .subscribe(changes => { console.log(users) }); + +// Listen to only 'added' events +docChanges(db.collection('users'), ['added']) + .subscribe(addedEvents => { console.log(addedEvents) }); +``` + +### `auditTrail()` +The `auditTrail()` function creates an observable that emits the entire state trail. This is useful for debugging or replaying the state of a list in your app. The optional `events` parameter will filter which child events populate the array. + +| | | +|-----------------|------------------------------------------------------| +| **function** | `auditTrail()` | +| **params** | ref: `firestore.Reference` or `firestore.Query`, events?: `firestore.DocumentChangeType[]` | +| **import path** | `rxfire/firestore` | +| **return** | `Observable` | + +#### TypeScript Example +```ts +import { auditTrail } from 'rxfire/firestore'; +import { firestore } from 'firebase'; +import 'firebase/firestore'; +import { map } from 'rxjs/operators'; + +// Set up Firebase +const app = initializeApp({ /* config */ }); +const db = app.firestore(); +const collection = db.collection('users'); + +// Seed Firestore +const davidDoc = collection.doc('users/david'); +davidDoc.set({ name: 'David' }); + +auditTrail(collection).pipe( + map(change => { + return { + _key: change.snapshot.key, + event: change.event, + ...change.snapshot.val(); + }; + }) +).subscribe(stateTrail => { + console.log(stateTrail); + /** + first emission: + [{ _key: '3qtWqaKga8jA; name: 'David', event: 'added' }] + + second emission: + [ + { _key: '3qtWqaKga8jA; name: 'David', event: 'added' }, + { _key: '3qtWqaKga8jA; name: 'David', event: 'removed' } + ] + */ +}); + +// When more events occur the trail still contains the previous events +// In this case we'll remove the only item +davidDoc.delete(); + +// Now this will trigger the subscribe function above +``` + +## Event Observables + +### `fromDocRef()` +The `fromDocRef()` function creates an observable that emits document changes. This is an alias to the `doc()` function. + +| | | +|-----------------|------------------------------------------| +| **function** | `fromDocRef()` | +| **params** | ref: `firestore.Reference` | +| **import path** | `rxfire/firestore` | +| **return** | `Observable` | + +#### TypeScript Example +```ts +import { fromDocRef } from 'rxfire/firestore'; +import { firestore, initializeApp } from 'firebase'; +import 'firebase/firestore'; +import { map } from 'rxjs/operators'; + +// Set up Firebase +const app = initializeApp({ /* config */ }); +const db = app.firestore(); +const davidDoc = db.doc('users/david'); + +// Seed Firestore +davidDoc.set({ name: 'David' }); + +fromDocRef(davidDoc).subscribe(snap => { console.log(snap); }) +``` + +### `fromCollectionRef()` +The `fromCollectionRef()` function creates an observable that emits document changes. This is different than the `collection()` function in that it returns the full `QuerySnapshot` instead of plucking off the `QueryDocumentSnapshot[]` array. + +| | | +|-----------------|------------------------------------------| +| **function** | `fromCollectionRef()` | +| **params** | ref: `firestore.CollectionReference` or `firestore.Query` | +| **import path** | `rxfire/firestore` | +| **return** | `Observable` | + +#### TypeScript Example +```ts +import { fromCollectionRef } from 'rxfire/firestore'; +import { firestore, initializeApp } from 'firebase'; +import 'firebase/firestore'; +import { map } from 'rxjs/operators'; + +// Set up Firebase +const app = initializeApp({ /* config */ }); +const db = app.firestore(); +const collection = db.collection('users'); +const davidDoc = collection.doc('david'); + +// Seed Firestore +davidDoc.set({ name: 'David' }); + +fromCollectionRef(collection).subscribe(snap => { console.log(snap.docs); }) +``` From 438d0cdf18496a7742aceaaa694155eb9216f784 Mon Sep 17 00:00:00 2001 From: David East Date: Tue, 10 Jul 2018 11:27:05 -0600 Subject: [PATCH 10/14] auth docs --- packages/rxfire/auth/index.ts | 4 +- packages/rxfire/docs/auth.md | 78 +++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 packages/rxfire/docs/auth.md diff --git a/packages/rxfire/auth/index.ts b/packages/rxfire/auth/index.ts index 14ce26ce63a..d8e0ba8f00f 100644 --- a/packages/rxfire/auth/index.ts +++ b/packages/rxfire/auth/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { auth, User } from 'firebase/app'; +import { auth, User } from 'firebase'; import { Observable, from, of } from 'rxjs'; import { switchMap } from 'rxjs/operators'; @@ -47,7 +47,7 @@ export function user(auth: auth.Auth): Observable { * sign-out, and token refresh events * @param auth firebase.auth.Auth */ -export function idToken(auth: auth.Auth) { +export function idToken(auth: auth.Auth): Observable { return user(auth).pipe( switchMap(user => (user ? from(user.getIdToken()) : of(null))) ); diff --git a/packages/rxfire/docs/auth.md b/packages/rxfire/docs/auth.md new file mode 100644 index 00000000000..c5f7d0da150 --- /dev/null +++ b/packages/rxfire/docs/auth.md @@ -0,0 +1,78 @@ +# RxFire Auth + +## Auth State Observables + +### `authState()` +The `authState()` function creates an observable that emits authentication changes such as a logged out or logged in state. + +| | | +|-----------------|------------------------------------------| +| **function** | `authState()` | +| **params** | `auth.Auth` | +| **import path** | `rxfire/auth` | +| **return** | `Observable` | + +#### TypeScript Example +```ts +import { authState } from 'rxfire/firestore'; +import { auth, initializeApp } from 'firebase'; +import 'firebase/auth'; +import { filter } from 'rxjs/operators'; + +// Set up Firebase +const app = initializeApp({ /* config */ }); +const auth = app.auth(); +authState(auth).subscribe(user => { + console.log(user, ' will be null if logged out'); +}); + +// Listen only for logged in state +const loggedIn$ = authState(auth).pipe(filter(user => !!user)); +loggedIn$.subscribe(user => { console.log(user); }); +``` + +### `user()` +The `user()` function creates an observable that emits authentication changes such as a logged out, logged in, and token refresh state. The token refresh emissions is what makes `user()` different from `authState()`. + +| | | +|-----------------|------------------------------------------| +| **function** | `user()` | +| **params** | `auth.Auth` | +| **import path** | `rxfire/auth` | +| **return** | `Observable` | + +#### TypeScript Example +```ts +import { user } from 'rxfire/firestore'; +import { auth, initializeApp } from 'firebase'; +import 'firebase/auth'; +import { filter } from 'rxjs/operators'; + +// Set up Firebase +const app = initializeApp({ /* config */ }); +const auth = app.auth(); +user(auth).subscribe(u => { console.log(u); ); +``` + +### `idToken()` +The `idToken()` function creates an observable that emits the `idToken` refreshes. This is useful for keeping third party authentication in sync with Firebase Auth refreshes. + +| | | +|-----------------|------------------------------------------| +| **function** | `idToken()` | +| **params** | `auth.Auth` | +| **import path** | `rxfire/auth` | +| **return** | `Observable` | + +#### TypeScript Example +```ts +import { idToken } from 'rxfire/firestore'; +import { auth, initializeApp } from 'firebase'; +import 'firebase/auth'; +import { filter } from 'rxjs/operators'; + +// Set up Firebase +const app = initializeApp({ /* config */ }); +const auth = app.auth(); +idToken(auth).subscribe(token => { console.log(token); ); +``` From cc7caf02b77bb65dda09b387c578cce811d6cddc Mon Sep 17 00:00:00 2001 From: David East Date: Tue, 10 Jul 2018 14:27:23 -0600 Subject: [PATCH 11/14] declaration fixes --- packages/rxfire/auth/package.json | 3 +- packages/rxfire/database/fromRef.d.ts | 24 ++++++++++++++++ packages/rxfire/database/fromRef.ts | 10 ------- packages/rxfire/database/interfaces.d.ts | 28 +++++++++++++++++++ .../rxfire/database/list/audit-trail.d.ts | 19 +++++++++++++ packages/rxfire/database/utils.d.ts | 23 +++++++++++++++ packages/rxfire/firestore/document/index.ts | 1 + packages/rxfire/firestore/package.json | 3 +- packages/rxfire/functions/package.json | 3 +- packages/rxfire/storage/package.json | 3 +- packages/rxfire/tsconfig.json | 5 ++-- 11 files changed, 106 insertions(+), 16 deletions(-) create mode 100644 packages/rxfire/database/fromRef.d.ts create mode 100644 packages/rxfire/database/interfaces.d.ts create mode 100644 packages/rxfire/database/list/audit-trail.d.ts create mode 100644 packages/rxfire/database/utils.d.ts diff --git a/packages/rxfire/auth/package.json b/packages/rxfire/auth/package.json index c891b72ad2e..d8e19b41d10 100644 --- a/packages/rxfire/auth/package.json +++ b/packages/rxfire/auth/package.json @@ -1,5 +1,6 @@ { "name": "rxfire/auth", "main": "dist/index.cjs.js", - "module": "dist/index.esm.js" + "module": "dist/index.esm.js", + "typings": "dist/auth/index.d.ts" } diff --git a/packages/rxfire/database/fromRef.d.ts b/packages/rxfire/database/fromRef.d.ts new file mode 100644 index 00000000000..c8edd9a143f --- /dev/null +++ b/packages/rxfire/database/fromRef.d.ts @@ -0,0 +1,24 @@ +/** + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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. + */ +import { database } from 'firebase'; +import { Observable } from 'rxjs'; +import { ListenEvent, QueryChange } from './interfaces'; +/** + * Create an observable from a Database Reference or Database Query. + * @param ref Database Reference + * @param event Listen event type ('value', 'added', 'changed', 'removed', 'moved') + */ +export declare function fromRef(ref: database.Query, event: ListenEvent): Observable; diff --git a/packages/rxfire/database/fromRef.ts b/packages/rxfire/database/fromRef.ts index 77963b02c8a..9cd8b6a5c23 100644 --- a/packages/rxfire/database/fromRef.ts +++ b/packages/rxfire/database/fromRef.ts @@ -48,13 +48,3 @@ export function fromRef( delay(0) ); } - -export const unwrap = () => - map((payload: QueryChange) => { - const { snapshot, prevKey } = payload; - let key: string | null = null; - if (snapshot.exists()) { - key = snapshot.key; - } - return { type: event, payload: snapshot, prevKey, key }; - }); diff --git a/packages/rxfire/database/interfaces.d.ts b/packages/rxfire/database/interfaces.d.ts new file mode 100644 index 00000000000..47cd45e1492 --- /dev/null +++ b/packages/rxfire/database/interfaces.d.ts @@ -0,0 +1,28 @@ +/** + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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. + */ +import { database } from 'firebase'; +export declare enum ListenEvent { + added = "child_added", + removed = "child_removed", + changed = "child_changed", + moved = "child_moved", + value = "value", +} +export interface QueryChange { + snapshot: database.DataSnapshot; + prevKey: string | null | undefined; + event: ListenEvent; +} diff --git a/packages/rxfire/database/list/audit-trail.d.ts b/packages/rxfire/database/list/audit-trail.d.ts new file mode 100644 index 00000000000..9856f862441 --- /dev/null +++ b/packages/rxfire/database/list/audit-trail.d.ts @@ -0,0 +1,19 @@ +/** + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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. + */ +import { database } from 'firebase'; +import { Observable } from 'rxjs'; +import { QueryChange, ListenEvent } from '../interfaces'; +export declare function auditTrail(query: database.Query, events?: ListenEvent[]): Observable; diff --git a/packages/rxfire/database/utils.d.ts b/packages/rxfire/database/utils.d.ts new file mode 100644 index 00000000000..c418d5e4cfc --- /dev/null +++ b/packages/rxfire/database/utils.d.ts @@ -0,0 +1,23 @@ +/** + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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. + */ +import { ListenEvent } from './interfaces'; +export declare function isNil(obj: any): boolean; +/** + * Check the length of the provided array. If it is empty return an array + * that is populated with all the Realtime Database child events. + * @param events + */ +export declare function validateEventsArray(events?: ListenEvent[]): ListenEvent[]; diff --git a/packages/rxfire/firestore/document/index.ts b/packages/rxfire/firestore/document/index.ts index c808a8a5a3e..323acf3bb75 100644 --- a/packages/rxfire/firestore/document/index.ts +++ b/packages/rxfire/firestore/document/index.ts @@ -16,6 +16,7 @@ import { firestore } from 'firebase/app'; import { fromDocRef } from '../fromRef'; +import { Observable } from 'rxjs'; export function doc(ref: firestore.DocumentReference) { return fromDocRef(ref); diff --git a/packages/rxfire/firestore/package.json b/packages/rxfire/firestore/package.json index 63c78472064..4de130539a4 100644 --- a/packages/rxfire/firestore/package.json +++ b/packages/rxfire/firestore/package.json @@ -1,5 +1,6 @@ { "name": "rxfire/firestore", "main": "dist/index.cjs.js", - "module": "dist/index.esm.js" + "module": "dist/index.esm.js", + "typings": "dist/firestore/index.d.ts" } diff --git a/packages/rxfire/functions/package.json b/packages/rxfire/functions/package.json index 95a9b36735f..5c07e0bf8bc 100644 --- a/packages/rxfire/functions/package.json +++ b/packages/rxfire/functions/package.json @@ -1,5 +1,6 @@ { "name": "rxfire/functions", "main": "dist/index.cjs.js", - "module": "dist/index.esm.js" + "module": "dist/index.esm.js", + "typings": "dist/functions/index.d.ts" } diff --git a/packages/rxfire/storage/package.json b/packages/rxfire/storage/package.json index ff40a8e3dc9..96ee8641809 100644 --- a/packages/rxfire/storage/package.json +++ b/packages/rxfire/storage/package.json @@ -1,5 +1,6 @@ { "name": "rxfire/storage", "main": "dist/index.cjs.js", - "module": "dist/index.esm.js" + "module": "dist/index.esm.js", + "typings": "dist/storage/index.d.ts" } diff --git a/packages/rxfire/tsconfig.json b/packages/rxfire/tsconfig.json index e557fdf8668..64082d8c046 100644 --- a/packages/rxfire/tsconfig.json +++ b/packages/rxfire/tsconfig.json @@ -2,9 +2,10 @@ "extends": "../../config/tsconfig.base.json", "compilerOptions": { "outDir": "dist", - "declaration": false + "declaration": true }, "exclude": [ - "dist/**/*" + "dist/**/*", + "test/**/*" ] } From 40ad987a9616d9401970f95495b6407f9e237048 Mon Sep 17 00:00:00 2001 From: David East Date: Thu, 12 Jul 2018 11:05:50 -0600 Subject: [PATCH 12/14] switch to peerDeps --- packages/rxfire/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/rxfire/package.json b/packages/rxfire/package.json index e499d31ffb4..dc084573c55 100644 --- a/packages/rxfire/package.json +++ b/packages/rxfire/package.json @@ -13,7 +13,8 @@ "firebase", "realtime", "storage", - "rxjs" + "rxjs", + "notifications" ], "repository": { "type": "git", @@ -29,8 +30,7 @@ "main": "dist/index.node.cjs.js", "browser": "dist/index.cjs.js", "module": "dist/index.esm.js", - "react-native": "dist/index.rn.cjs.js", - "dependencies": { + "peerDependencies": { "firebase": "5.2.0", "rxjs": "6.2.0" }, From fa0fd53dd10d3a67ce9ec30d1e532655f78f4699 Mon Sep 17 00:00:00 2001 From: David East Date: Thu, 12 Jul 2018 11:06:26 -0600 Subject: [PATCH 13/14] [AUTOMATED]: Prettier Code Styling --- packages/rxfire/database/fromRef.d.ts | 5 ++++- packages/rxfire/database/interfaces.d.ts | 16 ++++++++-------- packages/rxfire/database/list/audit-trail.d.ts | 5 ++++- packages/rxfire/database/utils.d.ts | 4 +++- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/rxfire/database/fromRef.d.ts b/packages/rxfire/database/fromRef.d.ts index c8edd9a143f..c0a08b78c5c 100644 --- a/packages/rxfire/database/fromRef.d.ts +++ b/packages/rxfire/database/fromRef.d.ts @@ -21,4 +21,7 @@ import { ListenEvent, QueryChange } from './interfaces'; * @param ref Database Reference * @param event Listen event type ('value', 'added', 'changed', 'removed', 'moved') */ -export declare function fromRef(ref: database.Query, event: ListenEvent): Observable; +export declare function fromRef( + ref: database.Query, + event: ListenEvent +): Observable; diff --git a/packages/rxfire/database/interfaces.d.ts b/packages/rxfire/database/interfaces.d.ts index 47cd45e1492..a67024536e0 100644 --- a/packages/rxfire/database/interfaces.d.ts +++ b/packages/rxfire/database/interfaces.d.ts @@ -15,14 +15,14 @@ */ import { database } from 'firebase'; export declare enum ListenEvent { - added = "child_added", - removed = "child_removed", - changed = "child_changed", - moved = "child_moved", - value = "value", + added = 'child_added', + removed = 'child_removed', + changed = 'child_changed', + moved = 'child_moved', + value = 'value' } export interface QueryChange { - snapshot: database.DataSnapshot; - prevKey: string | null | undefined; - event: ListenEvent; + snapshot: database.DataSnapshot; + prevKey: string | null | undefined; + event: ListenEvent; } diff --git a/packages/rxfire/database/list/audit-trail.d.ts b/packages/rxfire/database/list/audit-trail.d.ts index 9856f862441..8f452f33022 100644 --- a/packages/rxfire/database/list/audit-trail.d.ts +++ b/packages/rxfire/database/list/audit-trail.d.ts @@ -16,4 +16,7 @@ import { database } from 'firebase'; import { Observable } from 'rxjs'; import { QueryChange, ListenEvent } from '../interfaces'; -export declare function auditTrail(query: database.Query, events?: ListenEvent[]): Observable; +export declare function auditTrail( + query: database.Query, + events?: ListenEvent[] +): Observable; diff --git a/packages/rxfire/database/utils.d.ts b/packages/rxfire/database/utils.d.ts index c418d5e4cfc..ac2cdeec7ab 100644 --- a/packages/rxfire/database/utils.d.ts +++ b/packages/rxfire/database/utils.d.ts @@ -20,4 +20,6 @@ export declare function isNil(obj: any): boolean; * that is populated with all the Realtime Database child events. * @param events */ -export declare function validateEventsArray(events?: ListenEvent[]): ListenEvent[]; +export declare function validateEventsArray( + events?: ListenEvent[] +): ListenEvent[]; From 9982b4a3f4e045cc95dcb99bd2ffde8cd7a99872 Mon Sep 17 00:00:00 2001 From: David East Date: Thu, 12 Jul 2018 15:48:29 -0600 Subject: [PATCH 14/14] test config --- packages/rxfire/test/database.test.ts | 6 ++++-- packages/rxfire/test/firestore.test.ts | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/rxfire/test/database.test.ts b/packages/rxfire/test/database.test.ts index e06a7f668e6..fd06f28f1b6 100644 --- a/packages/rxfire/test/database.test.ts +++ b/packages/rxfire/test/database.test.ts @@ -22,6 +22,8 @@ import { take, skip, switchMap } from 'rxjs/operators'; import { BehaviorSubject } from 'rxjs'; import { auditTrail } from '../database/list/audit-trail'; +export const TEST_PROJECT = require('../../../config/project.json'); + const rando = () => Math.random() .toString(36) @@ -74,8 +76,8 @@ describe('RxFire Database', () => { */ beforeEach(() => { app = initializeApp({ - projectId: 'rxfire-test-db', - databaseURL: 'https://rxfire-test.firebaseio.com' + projectId: TEST_PROJECT.projectId, + databaseURL: TEST_PROJECT.databaseURL }); database = app.database(); database.goOffline(); diff --git a/packages/rxfire/test/firestore.test.ts b/packages/rxfire/test/firestore.test.ts index e61b3ce0e24..ee9c217a992 100644 --- a/packages/rxfire/test/firestore.test.ts +++ b/packages/rxfire/test/firestore.test.ts @@ -25,6 +25,8 @@ import { } from '../firestore'; import { map, take, skip } from 'rxjs/operators'; +export const TEST_PROJECT = require('../../../config/project.json'); + const createId = () => Math.random() .toString(36) @@ -78,7 +80,7 @@ describe('RxFire Firestore', () => { * offline. */ beforeEach(() => { - app = initializeApp({ projectId: 'rxfire-test' }); + app = initializeApp({ projectId: TEST_PROJECT.projectId }); firestore = app.firestore(); firestore.settings({ timestampsInSnapshots: true }); firestore.disableNetwork();