Skip to content

Commit 80b9617

Browse files
committed
feat(database): Add unwrapSnapshot option
And add a provider for the default unwrapSnapshot implementation. Closes angular#883 and angular#848.
1 parent 41bcffa commit 80b9617

13 files changed

+233
-34
lines changed

src/database/database.module.ts

+14-5
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,31 @@
1-
import { NgModule } from '@angular/core';
1+
import { NgModule, OpaqueToken } from '@angular/core';
22
import * as firebase from 'firebase/app';
33
import 'firebase/database';
44
import { FirebaseApp } from '../app/index';
55
import { AngularFireModule } from '../angularfire2';
66
import { AngularFireDatabase } from './index';
7+
import { UnwrappedSnapshot, UnwrapSnapshotSignature } from './interfaces';
8+
import { UnwrapSnapshotToken } from './tokens';
9+
import { unwrapSnapshot } from './unwrap_snapshot';
710

8-
export function _getAngularFireDatabase(app: FirebaseApp) {
9-
return new AngularFireDatabase(app);
11+
export function _getAngularFireDatabase(app: FirebaseApp, unwrapSnapshot: UnwrapSnapshotSignature) {
12+
return new AngularFireDatabase(app, unwrapSnapshot);
1013
}
1114

1215
export const AngularFireDatabaseProvider = {
1316
provide: AngularFireDatabase,
1417
useFactory: _getAngularFireDatabase,
15-
deps: [ FirebaseApp ]
18+
deps: [ FirebaseApp, UnwrapSnapshotToken ]
19+
};
20+
21+
export const UnwrapSnapshotProvider = {
22+
provide: UnwrapSnapshotToken,
23+
useValue: unwrapSnapshot
1624
};
1725

1826
export const DATABASE_PROVIDERS = [
19-
AngularFireDatabaseProvider,
27+
UnwrapSnapshotProvider,
28+
AngularFireDatabaseProvider
2029
];
2130

2231
@NgModule({

src/database/database.ts

+9-14
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,22 @@
11
import * as firebase from 'firebase/app';
22
import 'firebase/database';
3-
import { Inject, Injectable } from '@angular/core';
43
import { FirebaseAppConfigToken, FirebaseAppConfig, FirebaseApp } from '../angularfire2';
54
import { FirebaseListFactory } from './index';
6-
import { FirebaseListFactoryOpts, FirebaseObjectFactoryOpts, PathReference } from './interfaces';
7-
import { checkForUrlOrFirebaseRef, getRef } from './utils';
5+
import { FirebaseListFactoryOpts, FirebaseObjectFactoryOpts, PathReference, UnwrapSnapshotSignature } from './interfaces';
6+
import { getRef } from './utils';
87
import { FirebaseListObservable, FirebaseObjectObservable, FirebaseObjectFactory } from './index';
98

10-
@Injectable()
119
export class AngularFireDatabase {
1210

13-
constructor(public app: FirebaseApp) {}
11+
constructor(public app: FirebaseApp, private _unwrapSnapshot: UnwrapSnapshotSignature) {}
1412

15-
list(pathOrRef: PathReference, opts?:FirebaseListFactoryOpts):FirebaseListObservable<any[]> {
16-
const ref = getRef(this.app, pathOrRef);
17-
return FirebaseListFactory(getRef(this.app, ref), opts);
13+
list(pathOrRef: PathReference, opts:FirebaseListFactoryOpts = {}):FirebaseListObservable<any[]> {
14+
const optsWithDefaults = Object.assign({ unwrapSnapshot: this._unwrapSnapshot }, opts);
15+
return FirebaseListFactory(getRef(this.app, pathOrRef), optsWithDefaults);
1816
}
1917

20-
object(pathOrRef: PathReference, opts?:FirebaseObjectFactoryOpts):FirebaseObjectObservable<any> {
21-
return checkForUrlOrFirebaseRef(pathOrRef, {
22-
isUrl: () => FirebaseObjectFactory(this.app.database().ref(<string>pathOrRef), opts),
23-
isRef: () => FirebaseObjectFactory(pathOrRef)
24-
});
18+
object(pathOrRef: PathReference, opts:FirebaseObjectFactoryOpts = {}):FirebaseObjectObservable<any> {
19+
const optsWithDefaults = Object.assign({ unwrapSnapshot: this._unwrapSnapshot }, opts);
20+
return FirebaseObjectFactory(getRef(this.app, pathOrRef), optsWithDefaults);
2521
}
26-
2722
}

src/database/firebase_list_factory.spec.ts

+49
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { FirebaseListFactory, FirebaseListObservable, FirebaseObjectFactory, onC
33
import { FirebaseApp, FirebaseAppConfig, AngularFireModule} from '../angularfire2';
44
import { TestBed, inject } from '@angular/core/testing';
55
import { Query } from './interfaces';
6+
import { unwrapSnapshot } from './unwrap_snapshot';
67
import { Subscription, Observable, Subject } from 'rxjs';
78
import { COMMON_CONFIG } from '../test-config';
89
import { _do } from 'rxjs/operator/do';
@@ -877,6 +878,54 @@ describe('FirebaseListFactory', () => {
877878
});
878879

879880
});
881+
882+
describe('unwrapSnapshot', () => {
883+
884+
let ref: firebase.database.Reference;
885+
let subscription: Subscription;
886+
887+
beforeEach((done: any) => {
888+
889+
ref = firebase.database().ref('test');
890+
ref.remove()
891+
.then(done)
892+
.catch(done.fail);
893+
});
894+
895+
afterEach((done: any) => {
896+
if (subscription && !subscription.closed) {
897+
subscription.unsubscribe();
898+
}
899+
ref.remove()
900+
.then(done)
901+
.catch(done.fail);
902+
});
903+
904+
it('should use the specified unwrapSnapshot implementation', (done: any) => {
905+
906+
ref.set({ 'key1': 'val1' })
907+
.then(() => {
908+
let observable = FirebaseListFactory(ref, {
909+
unwrapSnapshot: (snapshot) => {
910+
const unwrapped = unwrapSnapshot(snapshot);
911+
(unwrapped as any).custom = true;
912+
return unwrapped;
913+
}
914+
});
915+
return toPromise.call(skipAndTake(observable, 1));
916+
})
917+
.then((list: any[]) => {
918+
expect(list.length).toBe(1);
919+
expect(list[0].$key).toBe('key1');
920+
expect(list[0].$value).toBe('val1');
921+
expect(list[0].custom).toBe(true);
922+
})
923+
.then(done)
924+
.catch(done.fail);
925+
});
926+
927+
});
928+
880929
});
881930

882931
function skipAndTake<T>(obs: Observable<T>, takeCount: number = 1, skipCount: number = 0) {

src/database/firebase_list_factory.ts

+12-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as firebase from 'firebase/app';
22
import { hasKey, isNil, isAbsoluteUrl, isEmptyObject, isString, ZoneScheduler } from '../utils';
33
import { checkForUrlOrFirebaseRef, isFirebaseRef } from './utils';
4-
import { unwrapSnapshot } from './unwrap_snapshot';
4+
import { unwrapSnapshot as defaultUnwrapSnapshot } from './unwrap_snapshot';
55
import 'firebase/database';
66
import { FirebaseListObservable } from './firebase_list_observable';
77
import { Observer } from 'rxjs/Observer';
@@ -13,7 +13,14 @@ import { map } from 'rxjs/operator/map';
1313

1414
export function FirebaseListFactory (
1515
pathRef: PathReference,
16-
{ preserveSnapshot, query = {} } :FirebaseListFactoryOpts = {}): FirebaseListObservable<any> {
16+
{ preserveSnapshot, query = {}, unwrapSnapshot } :FirebaseListFactoryOpts = {}): FirebaseListObservable<any> {
17+
18+
if (unwrapSnapshot && preserveSnapshot) {
19+
throw new Error('Cannot use preserveSnapshot with unwrapSnapshot.');
20+
}
21+
if (!unwrapSnapshot) {
22+
unwrapSnapshot = defaultUnwrapSnapshot;
23+
}
1724

1825
let ref: QueryReference;
1926

@@ -34,7 +41,7 @@ export function FirebaseListFactory (
3441
if ((isFirebaseRef(pathRef) ||
3542
isString(pathRef)) &&
3643
isEmptyObject(query)) {
37-
return firebaseListObservable(ref, { preserveSnapshot });
44+
return firebaseListObservable(ref, { preserveSnapshot, unwrapSnapshot });
3845
}
3946

4047
const queryObs = observeQuery(query);
@@ -111,7 +118,7 @@ export function FirebaseListFactory (
111118

112119
return queried;
113120
}), (queryRef: firebase.database.Reference, ix: number) => {
114-
return firebaseListObservable(queryRef, { preserveSnapshot });
121+
return firebaseListObservable(queryRef, { preserveSnapshot, unwrapSnapshot });
115122
})
116123
.subscribe(subscriber);
117124

@@ -126,7 +133,7 @@ export function FirebaseListFactory (
126133
* asynchonous. It creates a initial array from a promise of ref.once('value'), and then starts
127134
* listening to child events. When the initial array is loaded, the observable starts emitting values.
128135
*/
129-
function firebaseListObservable(ref: firebase.database.Reference | DatabaseQuery, {preserveSnapshot}: FirebaseListFactoryOpts = {}): FirebaseListObservable<any> {
136+
function firebaseListObservable(ref: firebase.database.Reference | DatabaseQuery, { preserveSnapshot, unwrapSnapshot }: FirebaseListFactoryOpts = {}): FirebaseListObservable<any> {
130137

131138
const toValue = preserveSnapshot ? (snapshot => snapshot) : unwrapSnapshot;
132139
const toKey = preserveSnapshot ? (value => value.key) : (value => value.$key);

src/database/firebase_list_observable.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import * as firebase from 'firebase/app';
66
import 'firebase/database';
77
import { isString } from '../utils';
88
import { isFirebaseRef, isFirebaseDataSnapshot, isAFUnwrappedSnapshot } from './utils';
9-
import { AFUnwrappedDataSnapshot, FirebaseOperationCases, QueryReference, DatabaseSnapshot, DatabaseReference } from './interfaces';
9+
import { UnwrappedSnapshot, FirebaseOperationCases, QueryReference, DatabaseSnapshot, DatabaseReference } from './interfaces';
1010

11-
export type FirebaseOperation = string | firebase.database.Reference | firebase.database.DataSnapshot | AFUnwrappedDataSnapshot;
11+
export type FirebaseOperation = string | firebase.database.Reference | firebase.database.DataSnapshot | UnwrappedSnapshot;
1212

1313
export class FirebaseListObservable<T> extends Observable<T> {
1414
constructor(public $ref: QueryReference, subscribe?: <R>(subscriber: Subscriber<R>) => Subscription | Function | void) {
@@ -34,7 +34,7 @@ export class FirebaseListObservable<T> extends Observable<T> {
3434
stringCase: () => this.$ref.ref.child(<string>item).update(value),
3535
firebaseCase: () => (<firebase.database.Reference>item).update(value),
3636
snapshotCase: () => (<firebase.database.DataSnapshot>item).ref.update(value),
37-
unwrappedSnapshotCase: () => this.$ref.ref.child((<AFUnwrappedDataSnapshot>item).$key).update(value)
37+
unwrappedSnapshotCase: () => this.$ref.ref.child((<UnwrappedSnapshot>item).$key).update(value)
3838
});
3939
}
4040

@@ -47,7 +47,7 @@ export class FirebaseListObservable<T> extends Observable<T> {
4747
stringCase: () => this.$ref.ref.child(<string>item).remove(),
4848
firebaseCase: () => (<DatabaseReference>item).remove(),
4949
snapshotCase: () => (<DatabaseSnapshot>item).ref.remove(),
50-
unwrappedSnapshotCase: () => this.$ref.ref.child((<AFUnwrappedDataSnapshot>item).$key).remove()
50+
unwrappedSnapshotCase: () => this.$ref.ref.child((<UnwrappedSnapshot>item).$key).remove()
5151
});
5252
}
5353

src/database/firebase_object_factory.spec.ts

+50
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import * as firebase from 'firebase/app';
22
import { Subscription } from 'rxjs';
3+
import { take } from 'rxjs/operator/take';
4+
import { toPromise } from 'rxjs/operator/toPromise';
35
import { FirebaseObjectFactory, FirebaseObjectObservable, AngularFireDatabaseModule, AngularFireDatabase } from './index';
46
import { TestBed, inject } from '@angular/core/testing';
57
import { FirebaseApp, FirebaseAppConfig, AngularFireModule } from '../angularfire2';
68
import { COMMON_CONFIG } from '../test-config';
9+
import { unwrapSnapshot } from './unwrap_snapshot';
710

811
describe('FirebaseObjectFactory', () => {
912
let i = 0;
@@ -155,4 +158,51 @@ describe('FirebaseObjectFactory', () => {
155158
});
156159
});
157160
});
161+
162+
describe('unwrapSnapshot', () => {
163+
164+
let ref: firebase.database.Reference;
165+
let subscription: Subscription;
166+
167+
beforeEach((done: any) => {
168+
169+
ref = firebase.database().ref('test');
170+
ref.remove()
171+
.then(done)
172+
.catch(done.fail);
173+
});
174+
175+
afterEach((done: any) => {
176+
if (subscription && !subscription.closed) {
177+
subscription.unsubscribe();
178+
}
179+
ref.remove()
180+
.then(done)
181+
.catch(done.fail);
182+
});
183+
184+
it('should use the specified unwrapSnapshot implementation', (done: any) => {
185+
186+
ref.set({ 'key1': 'val1' })
187+
.then(() => {
188+
let observable = FirebaseObjectFactory(ref, {
189+
unwrapSnapshot: (snapshot) => {
190+
const unwrapped = unwrapSnapshot(snapshot);
191+
(unwrapped as any).custom = true;
192+
return unwrapped;
193+
}
194+
});
195+
return toPromise.call(take.call(observable, 1));
196+
})
197+
.then((obj: any) => {
198+
expect(obj.$key).toBe('test');
199+
expect(obj.key1).toBe('val1');
200+
expect(obj.custom).toBe(true);
201+
})
202+
.then(done)
203+
.catch(done.fail);
204+
});
205+
206+
});
207+
158208
});

src/database/firebase_object_factory.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,19 @@ import * as firebase from 'firebase/app';
55
import 'firebase/database';
66
import { isAbsoluteUrl, ZoneScheduler } from '../utils';
77
import { checkForUrlOrFirebaseRef } from './utils';
8-
import { unwrapSnapshot } from './unwrap_snapshot';
8+
import { unwrapSnapshot as defaultUnwrapSnapshot } from './unwrap_snapshot';
99
import { FirebaseObjectFactoryOpts, PathReference, DatabaseReference } from './interfaces';
1010

1111
export function FirebaseObjectFactory (
1212
pathRef: PathReference,
13-
{ preserveSnapshot }: FirebaseObjectFactoryOpts = {}): FirebaseObjectObservable<any> {
13+
{ preserveSnapshot, unwrapSnapshot }: FirebaseObjectFactoryOpts = {}): FirebaseObjectObservable<any> {
14+
15+
if (unwrapSnapshot && preserveSnapshot) {
16+
throw new Error('Cannot use preserveSnapshot with unwrapSnapshot.');
17+
}
18+
if (!unwrapSnapshot) {
19+
unwrapSnapshot = defaultUnwrapSnapshot;
20+
}
1421

1522
let ref: DatabaseReference;
1623

src/database/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ export * from './firebase_object_observable';
66
export * from './query_observable';
77
export * from './database.module';
88
export * from './interfaces';
9+
export * from './tokens';
910
export * from './utils';

src/database/interfaces.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@ export interface FirebaseOperationCases {
88
unwrappedSnapshotCase?: () => firebase.Promise<void>;
99
}
1010

11-
export interface AFUnwrappedDataSnapshot {
11+
export type UnwrapSnapshotSignature = (snapshot: firebase.database.DataSnapshot) => UnwrappedSnapshot;
12+
13+
export interface UnwrappedSnapshot {
1214
$key: string;
1315
$value?: string | number | boolean;
1416
$exists: () => boolean;
17+
[key: string]: any;
1518
}
1619

1720
export interface Query {
@@ -53,10 +56,12 @@ export interface LimitToSelection {
5356
export interface FirebaseListFactoryOpts {
5457
preserveSnapshot?: boolean;
5558
query?: Query;
59+
unwrapSnapshot?: UnwrapSnapshotSignature;
5660
}
5761

5862
export interface FirebaseObjectFactoryOpts {
5963
preserveSnapshot?: boolean;
64+
unwrapSnapshot?: UnwrapSnapshotSignature;
6065
}
6166

6267
export enum OrderByOptions {

src/database/tokens.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import { OpaqueToken } from '@angular/core';
2+
export const UnwrapSnapshotToken = new OpaqueToken('UnwrapSnapshot');

0 commit comments

Comments
 (0)