Skip to content

Commit 4476c1f

Browse files
committed
refactor(db): stateChanges, auditTrail, action contract
1 parent 1fd4779 commit 4476c1f

16 files changed

+133
-71
lines changed

src/database/interfaces.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ export type FirebaseOperation = string | firebase.database.Reference | firebase.
66
export interface ListReference<T> {
77
query: DatabaseQuery;
88
valueChanges<T>(events?: ChildEvent[]): Observable<T[]>;
9-
snapshotChanges<T>(events?: ChildEvent[]): Observable<DatabaseSnapshot[]>;
9+
snapshotChanges(events?: ChildEvent[]): Observable<SnapshotAction[]>;
10+
stateChanges(events?: ChildEvent[]): Observable<SnapshotAction>;
11+
auditTrail(events?: ChildEvent[]): Observable<SnapshotAction[]>;
1012
update(item: FirebaseOperation, data: T): Promise<void>;
1113
set(item: FirebaseOperation, data: T): Promise<void>;
1214
push(data: T): firebase.database.ThenableReference;
@@ -16,7 +18,7 @@ export interface ListReference<T> {
1618
export interface ObjectReference<T> {
1719
query: DatabaseQuery;
1820
valueChanges<T>(): Observable<T | null>;
19-
snapshotChanges<T>(): Observable<DatabaseSnapshot | null>;
21+
snapshotChanges<T>(): Observable<SnapshotAction>;
2022
update(data: T): Promise<any>;
2123
set(data: T): Promise<void>;
2224
remove(): Promise<any>;
@@ -39,17 +41,22 @@ export type SnapshotChange = {
3941
prevKey: string | undefined;
4042
}
4143

42-
export type Action<T> = {
44+
export interface Action<T> {
4345
type: string;
4446
payload: T;
4547
};
4648

49+
export interface AngularFireAction<T> extends Action<T> {
50+
prevKey: string | undefined;
51+
key: string | null;
52+
}
53+
4754
export interface SnapshotPrevKey {
4855
snapshot: DatabaseSnapshot | null;
4956
prevKey: string | undefined;
5057
}
5158

52-
export type SnapshotAction = Action<SnapshotPrevKey>;
59+
export type SnapshotAction = AngularFireAction<DatabaseSnapshot | null>;
5360

5461
export type Primitive = number | string | boolean;
5562

src/database/list/audit-trail.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { DatabaseQuery, ChildEvent, AngularFireAction, SnapshotAction } from '../interfaces';
2+
import { stateChanges } from './state-changes';
3+
import { waitForLoaded } from './loaded';
4+
import { Observable } from 'rxjs/Observable';
5+
import { database } from 'firebase/app';
6+
import 'rxjs/add/operator/skipWhile';
7+
import 'rxjs/add/operator/withLatestFrom';
8+
import 'rxjs/add/operator/map';
9+
10+
export function createAuditTrail(query: DatabaseQuery) {
11+
return (events?: ChildEvent[]) => auditTrail(query, events);
12+
}
13+
14+
export function auditTrail(query: DatabaseQuery, events?: ChildEvent[]): Observable<SnapshotAction[]> {
15+
const auditTrail$ = stateChanges(query, events)
16+
.scan((current, action) => [...current, action], []);
17+
return waitForLoaded(query, auditTrail$);
18+
}

src/database/list/changes.spec.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ describe('listChanges', () => {
4848
someRef.set(batch);
4949
const obs = listChanges(someRef, ['child_added']);
5050
const sub = obs.skip(2).subscribe(changes => {
51-
const data = changes.map(change => change.payload.snapshot!.val());
51+
const data = changes.map(change => change.payload!.val());
5252
expect(data).toEqual(items);
5353
done();
5454
});
@@ -59,7 +59,7 @@ describe('listChanges', () => {
5959
aref.set(batch);
6060
const obs = listChanges(aref, ['child_added']);
6161
const sub = obs.skip(3).subscribe(changes => {
62-
const data = changes.map(change => change.payload.snapshot!.val());
62+
const data = changes.map(change => change.payload!.val());
6363
expect(data[3]).toEqual({ name: 'anotha one' });
6464
done();
6565
});
@@ -72,7 +72,7 @@ describe('listChanges', () => {
7272
const obs = listChanges(aref, ['child_added','child_removed'])
7373

7474
const sub = obs.skip(3).subscribe(changes => {
75-
const data = changes.map(change => change.payload.snapshot!.val());
75+
const data = changes.map(change => change.payload!.val());
7676
expect(data.length).toEqual(items.length - 1);
7777
done();
7878
});
@@ -84,9 +84,8 @@ describe('listChanges', () => {
8484
const aref = ref(rando());
8585
aref.set(batch);
8686
const obs = listChanges(aref, ['child_added','child_changed'])
87-
debugger;
8887
const sub = obs.skip(3).subscribe(changes => {
89-
const data = changes.map(change => change.payload.snapshot!.val());
88+
const data = changes.map(change => change.payload!.val());
9089
expect(data[0].name).toEqual('lol');
9190
done();
9291
});
@@ -99,7 +98,7 @@ describe('listChanges', () => {
9998
aref.set(batch);
10099
const obs = listChanges(aref, ['child_added','child_moved'])
101100
const sub = obs.skip(3).subscribe(changes => {
102-
const data = changes.map(change => change.payload.snapshot!.val());
101+
const data = changes.map(change => change.payload!.val());
103102
// We moved the first item to the last item, so we check that
104103
// the new result is now the last result
105104
expect(data[data.length - 1]).toEqual(items[0]);

src/database/list/changes.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,30 @@
11
import { fromRef } from '../observable/fromRef';
22
import { Observable } from 'rxjs/Observable';
3-
import { DatabaseQuery, ChildEvent, SnapshotChange, Action} from '../interfaces';
3+
import { DatabaseQuery, ChildEvent, SnapshotChange, AngularFireAction} from '../interfaces';
44
import { positionFor, positionAfter } from './utils';
55
import 'rxjs/add/operator/scan';
66
import 'rxjs/add/observable/merge';
77

88
// TODO(davideast): check safety of ! operator in scan
9-
export function listChanges<T>(ref: DatabaseQuery, events: ChildEvent[]): Observable<Action<any>[]> {
9+
export function listChanges<T>(ref: DatabaseQuery, events: ChildEvent[]): Observable<AngularFireAction<any>[]> {
1010
const childEvent$ = events.map(event => fromRef(ref, event));
1111
return Observable.merge(...childEvent$)
1212
.scan((current, action) => {
13-
const { payload, type } = action;
13+
const { payload, type, prevKey, key } = action;
1414
switch (action.type) {
1515
case 'child_added':
1616
return [...current, action];
1717
case 'child_removed':
1818
// ! is okay here because only value events produce null results
19-
return current.filter(x => x.payload.snapshot!.key !== payload.snapshot!.key);
19+
return current.filter(x => x.payload!.key !== payload!.key);
2020
case 'child_changed':
21-
return current.map(x => x.payload.snapshot!.key === payload.snapshot!.key ? action : x);
21+
return current.map(x => x.payload!.key === key ? action : x);
2222
// default will also remove null results
2323
case 'child_moved':
24-
const curPos = positionFor(current, payload.snapshot!.key)
24+
const curPos = positionFor(current, payload!.key)
2525
if(curPos > -1) {
2626
const data = current.splice(curPos, 1)[0];
27-
const newPost = positionAfter(current, payload.prevKey!);
27+
const newPost = positionAfter(current, prevKey);
2828
current.splice(newPost, 0, data);
2929
return current;
3030
}

src/database/list/create-reference.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { DatabaseQuery, ListReference, ChildEvent } from '../interfaces';
22
import { createLoadedChanges, loadedSnapshotChanges } from './loaded';
3+
import { createStateChanges } from './state-changes';
4+
import { createAuditTrail } from './audit-trail';
35
import { createDataOperationMethod } from './data-operation';
46
import { createRemoveMethod } from './remove';
57

@@ -11,9 +13,11 @@ export function createListReference<T>(query: DatabaseQuery): ListReference<T> {
1113
push: (data: T) => query.ref.push(data),
1214
remove: createRemoveMethod(query.ref),
1315
snapshotChanges: createLoadedChanges(query),
16+
stateChanges: createStateChanges(query),
17+
auditTrail: createAuditTrail(query),
1418
valueChanges<T>(events?: ChildEvent[]) {
1519
return loadedSnapshotChanges(query, events)
16-
.map(snaps => snaps.map(snap => snap!.val()));
20+
.map(actions => actions.map(a => a.payload!.val()));
1721
}
1822
}
1923
}

src/database/list/loaded.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ describe('createLoadedChanges', () => {
4444
it('should not emit until the array is whole', (done) => {
4545
const ref = createRef(rando());
4646
ref.set(batch);
47-
createLoadedChanges(ref)().subscribe(snaps => {
48-
const data = snaps.map(s => s.val());
47+
createLoadedChanges(ref)().subscribe(actions => {
48+
const data = actions.map(a => a.payload!.val());
4949
expect(data).toEqual(items);
5050
done();
5151
});

src/database/list/loaded.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { DatabaseQuery, ChildEvent, DatabaseSnapshot, SnapshotPrevKey } from '../interfaces';
1+
import { DatabaseQuery, ChildEvent, DatabaseSnapshot, AngularFireAction, SnapshotAction } from '../interfaces';
22
import { fromRef } from '../observable/fromRef';
33
import { snapshotChanges } from './snapshot-changes';
4+
import { database } from 'firebase/app';
45
import { Observable } from 'rxjs/Observable';
56
import 'rxjs/add/operator/skipWhile';
67
import 'rxjs/add/operator/withLatestFrom';
7-
import { database } from 'firebase/app';
8+
import 'rxjs/add/operator/map';
89

910
/**
1011
* Creates a function that creates a "loaded observable".
@@ -15,11 +16,16 @@ import { database } from 'firebase/app';
1516
* set is "whole" so the user is inundated with child_added updates.
1617
* @param query
1718
*/
18-
export function createLoadedChanges(query: DatabaseQuery) {
19+
export function createLoadedChanges(query: DatabaseQuery): (events?: ChildEvent[]) => Observable<SnapshotAction[]> {
1920
return (events?: ChildEvent[]) => loadedSnapshotChanges(query, events);
2021
}
2122

22-
export function createLoadedList(query: DatabaseQuery) {
23+
export interface LoadedMetadata {
24+
data: AngularFireAction<database.DataSnapshot | null>;
25+
lastKeyToLoad: any;
26+
}
27+
28+
export function loadedData(query: DatabaseQuery): Observable<LoadedMetadata> {
2329
// Create an observable of loaded values to retrieve the
2430
// known dataset. This will allow us to know what key to
2531
// emit the "whole" array at when listening for child events.
@@ -28,33 +34,37 @@ export function createLoadedList(query: DatabaseQuery) {
2834
// Store the last key in the data set
2935
let lastKeyToLoad;
3036
// Loop through loaded dataset to find the last key
31-
data.payload.snapshot!.forEach(child => {
37+
data.payload!.forEach(child => {
3238
lastKeyToLoad = child.key; return false;
3339
});
3440
// return data set and the current last key loaded
3541
return { data, lastKeyToLoad };
3642
});
3743
}
3844

39-
export function loadedSnapshotChanges(query: DatabaseQuery, events?: ChildEvent[]) {
40-
const snapChanges$ = snapshotChanges(query, events);
41-
const loaded$ = createLoadedList(query);
45+
export function waitForLoaded(query: DatabaseQuery, action$: Observable<SnapshotAction[]>) {
46+
const loaded$ = loadedData(query);
4247
return loaded$
43-
.withLatestFrom(snapChanges$)
48+
.withLatestFrom(action$)
4449
// Get the latest values from the "loaded" and "child" datasets
4550
// We can use both datasets to form an array of the latest values.
46-
.map(([loaded, snaps]) => {
51+
.map(([loaded, actions]) => {
4752
// Store the last key in the data set
4853
let lastKeyToLoad = loaded.lastKeyToLoad;
4954
// Store all child keys loaded at this point
50-
const loadedKeys = snaps.map(snap => snap.key);
51-
return { snaps, lastKeyToLoad, loadedKeys }
55+
const loadedKeys = actions.map(snap => snap.key);
56+
return { actions, lastKeyToLoad, loadedKeys }
5257
})
5358
// This is the magical part, only emit when the last load key
5459
// in the dataset has been loaded by a child event. At this point
5560
// we can assume the dataset is "whole".
5661
.skipWhile(meta => meta.loadedKeys.indexOf(meta.lastKeyToLoad) === -1)
5762
// Pluck off the meta data because the user only cares
5863
// to iterate through the snapshots
59-
.map(meta => meta.snaps);
64+
.map(meta => meta.actions);
65+
}
66+
67+
export function loadedSnapshotChanges(query: DatabaseQuery, events?: ChildEvent[]): Observable<SnapshotAction[]> {
68+
const snapChanges$ = snapshotChanges(query, events);
69+
return waitForLoaded(query, snapChanges$);
6070
}

src/database/list/snapshot-changes.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ describe('snapshotChanges', () => {
5454

5555
it('should listen to all events by default', (done) => {
5656
const { snapChanges } = prepareSnapshotChanges({ skip: 2 });
57-
const sub = snapChanges.subscribe(snaps => {
58-
const data = snaps.map(snap => snap.val())
57+
const sub = snapChanges.subscribe(actions => {
58+
const data = actions.map(a => a.payload!.val());
5959
expect(data).toEqual(items);
6060
done();
6161
sub.unsubscribe();
@@ -64,8 +64,8 @@ describe('snapshotChanges', () => {
6464

6565
it('should listen to only child_added events', (done) => {
6666
const { snapChanges } = prepareSnapshotChanges({ events: ['child_added'], skip: 2 });
67-
const sub = snapChanges.subscribe(snaps => {
68-
const data = snaps.map(snap => snap.val())
67+
const sub = snapChanges.subscribe(actions => {
68+
const data = actions.map(a => a.payload!.val());
6969
expect(data).toEqual(items);
7070
done();
7171
sub.unsubscribe();
@@ -78,8 +78,8 @@ describe('snapshotChanges', () => {
7878
skip: 3
7979
});
8080
const name = 'ligatures';
81-
const sub = snapChanges.subscribe(snaps => {
82-
const data = snaps.map(snap => snap.val());
81+
const sub = snapChanges.subscribe(actions => {
82+
const data = actions.map(a => a.payload!.val());;
8383
const copy = [...items];
8484
copy[0].name = name;
8585
expect(data).toEqual(copy);

src/database/list/snapshot-changes.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { Observable } from 'rxjs/Observable';
22
import { listChanges } from './changes';
3-
import { DatabaseQuery, ChildEvent, DatabaseSnapshot } from '../interfaces';
3+
import { DatabaseQuery, ChildEvent, SnapshotAction } from '../interfaces';
44
import { database } from 'firebase/app';
55
import { validateEventsArray } from './utils';
66
import 'rxjs/add/operator/map';
77

88
// TODO(davideast): Test safety of ! unwrap
9-
export function snapshotChanges(query: DatabaseQuery, events?: ChildEvent[]): Observable<DatabaseSnapshot[]> {
9+
export function snapshotChanges(query: DatabaseQuery, events?: ChildEvent[]): Observable<SnapshotAction[]> {
1010
events = validateEventsArray(events);
11-
return listChanges(query, events!)
12-
.map(changes => changes.map(change => change.payload.snapshot!));
11+
return listChanges(query, events!);
1312
}

src/database/list/state-changes.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,34 @@
1-
import { DatabaseQuery, ChildEvent, SnapshotChange, SnapshotPrevKey } from '../interfaces';
1+
import { DatabaseQuery, ChildEvent, AngularFireAction, SnapshotAction } from '../interfaces';
22
import { fromRef } from '../observable/fromRef';
33
import { validateEventsArray } from './utils';
44
import { Observable } from 'rxjs/Observable';
5+
import { database } from 'firebase/app';
56
import 'rxjs/add/observable/merge';
67
import 'rxjs/add/operator/scan';
78

9+
/**
10+
* NOTES FOR LATER:
11+
* Consider have snapshotChanges() return a SnapshotAction because it
12+
* retains the event information.
13+
*
14+
* Consider having valueChanges(), snapshotChanges(), and stateChanges()
15+
* return an Action. For snapshotChanges it would be an
16+
* Action<SnapshotPrevKey> and for valueChanges it would be an Action<T>.
17+
*
18+
* Consider providing the delta changes for both valueChanges() and
19+
* snapshotChanges().
20+
*
21+
* Consider providing an auditTrail() method that scans over stateChanges()
22+
* to provide each action as an array at that occurred at that location. This
23+
* would be a loaded dataset.
24+
*/
25+
826
export function createStateChanges(query: DatabaseQuery) {
927
return (events?: ChildEvent[]) => stateChanges(query, events);
1028
}
1129

1230
export function stateChanges(query: DatabaseQuery, events?: ChildEvent[]) {
1331
events = validateEventsArray(events)!;
1432
const childEvent$ = events.map(event => fromRef(query, event));
15-
return Observable
16-
.merge(...childEvent$)
17-
.scan((current, change) => [...current, change], []);
33+
return Observable.merge(...childEvent$);
1834
}

0 commit comments

Comments
 (0)