Skip to content

Commit 90c8ede

Browse files
committed
feat(afs): auditTrail() and tests
1 parent 86e89a3 commit 90c8ede

File tree

3 files changed

+215
-79
lines changed

3 files changed

+215
-79
lines changed

src/firestore/collection/changes.ts

+17-9
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { fromCollectionRef } from '../observable/fromRef';
22
import { Query, DocumentChangeType, DocumentChange, DocumentSnapshot, QuerySnapshot } from 'firestore';
33
import { Observable } from 'rxjs/Observable';
44
import 'rxjs/add/observable/map';
5+
import 'rxjs/add/observable/filter';
56
import 'rxjs/add/operator/scan';
67

78
import { DocumentChangeAction, Action } from '../interfaces';
@@ -26,7 +27,8 @@ export function sortedChanges(query: Query, events: DocumentChangeType[]): Obser
2627
return fromCollectionRef(query)
2728
.map(changes => changes.payload.docChanges)
2829
.scan((current, changes) => combineChanges(current, changes, events), [])
29-
.map(changes => changes.map(c => ({ type: c.type, payload: c })));
30+
.map(changes => changes.map(c => ({ type: c.type, payload: c })))
31+
.filter(changes => changes.length > 0);
3032
}
3133

3234
/**
@@ -37,14 +39,13 @@ export function sortedChanges(query: Query, events: DocumentChangeType[]): Obser
3739
* @param events
3840
*/
3941
export function combineChanges(current: DocumentChange[], changes: DocumentChange[], events: DocumentChangeType[]) {
40-
let combined: DocumentChange[] = [];
4142
changes.forEach(change => {
4243
// skip unwanted change types
4344
if(events.indexOf(change.type) > -1) {
44-
combined = combineChange(combined, change);
45+
current = combineChange(current, change);
4546
}
4647
});
47-
return combined;
48+
return current;
4849
}
4950

5051
/**
@@ -55,12 +56,19 @@ export function combineChanges(current: DocumentChange[], changes: DocumentChang
5556
export function combineChange(combined: DocumentChange[], change: DocumentChange): DocumentChange[] {
5657
switch(change.type) {
5758
case 'added':
58-
return [...combined, change];
59-
case 'modified':
60-
return combined.map(x => x.doc.id === change.doc.id ? change : x);
59+
combined.splice(change.newIndex, 0, change);
60+
break;
61+
case 'modified':
62+
// When an item changes position we first remove it
63+
// and then add it's new position
64+
if(change.oldIndex !== change.newIndex) {
65+
combined.splice(change.oldIndex, 1);
66+
}
67+
combined.splice(change.newIndex, 0, change);
68+
break;
6169
case 'removed':
62-
return combined.filter(x => x.doc.id !== change.doc.id);
70+
combined.splice(change.oldIndex, 1);
71+
break;
6372
}
6473
return combined;
6574
}
66-

src/firestore/collection/collection.spec.ts

+193-65
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { AngularFirestore } from '../firestore';
33
import { AngularFirestoreModule } from '../firestore.module';
44
import { AngularFirestoreDocument } from '../document/document';
55
import { AngularFirestoreCollection } from './collection';
6+
import { QueryFn } from '../interfaces';
67

78
import * as firebase from 'firebase/app';
89
import * as firestore from 'firestore';
@@ -16,6 +17,15 @@ import { COMMON_CONFIG } from '../test-config';
1617

1718
import { Stock, randomName, FAKE_STOCK_DATA, createRandomStocks, delayAdd, delayDelete, delayUpdate, deleteThemAll } from '../utils.spec';
1819

20+
async function collectionHarness(afs: AngularFirestore, items: number, queryFn?: QueryFn) {
21+
const randomCollectionName = randomName(afs.firestore);
22+
const ref = afs.firestore.collection(`${randomCollectionName}`);
23+
if(!queryFn) { queryFn = (ref) => ref; }
24+
const stocks = new AngularFirestoreCollection<Stock>(ref, queryFn(ref));
25+
let names = await createRandomStocks(afs.firestore, ref, items);
26+
return { randomCollectionName, ref, stocks, names };
27+
}
28+
1929
describe('AngularFirestoreCollection', () => {
2030
let app: firebase.app.App;
2131
let afs: AngularFirestore;
@@ -39,76 +49,168 @@ describe('AngularFirestoreCollection', () => {
3949
done();
4050
});
4151

42-
it('should get unwrapped snapshot', async (done: any) => {
43-
const randomCollectionName = randomName(afs.firestore);
44-
const ref = afs.firestore.collection(`${randomCollectionName}`);
45-
const stocks = new AngularFirestoreCollection<Stock>(ref, ref);
46-
const ITEMS = 4;
47-
48-
const names = await createRandomStocks(afs.firestore, ref, ITEMS)
49-
50-
const sub = stocks.valueChanges().subscribe(data => {
51-
// unsub immediately as we will be deleting data at the bottom
52-
// and that will trigger another subscribe callback and fail
53-
// the test
54-
sub.unsubscribe();
55-
// We added four things. This should be four.
56-
// This could not be four if the batch failed or
57-
// if the collection state is altered during a test run
58-
expect(data.length).toEqual(ITEMS);
59-
data.forEach(stock => {
60-
// We used the same piece of data so they should all equal
61-
expect(stock).toEqual(FAKE_STOCK_DATA);
52+
describe('valueChanges()', () => {
53+
54+
it('should get unwrapped snapshot', async (done: any) => {
55+
const ITEMS = 4;
56+
const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS);
57+
58+
const sub = stocks.valueChanges().subscribe(data => {
59+
// unsub immediately as we will be deleting data at the bottom
60+
// and that will trigger another subscribe callback and fail
61+
// the test
62+
sub.unsubscribe();
63+
// We added four things. This should be four.
64+
// This could not be four if the batch failed or
65+
// if the collection state is altered during a test run
66+
expect(data.length).toEqual(ITEMS);
67+
data.forEach(stock => {
68+
// We used the same piece of data so they should all equal
69+
expect(stock).toEqual(FAKE_STOCK_DATA);
70+
});
71+
// Delete them all
72+
const promises = names.map(name => ref.doc(name).delete());
73+
Promise.all(promises).then(done).catch(fail);
6274
});
63-
// Delete them all
64-
const promises = names.map(name => ref.doc(name).delete());
65-
Promise.all(promises).then(done).catch(fail);
75+
6676
});
67-
6877
});
6978

70-
it('should get stateChanges() updates', async (done: any) => {
71-
const randomCollectionName = randomName(afs.firestore);
72-
const ref = afs.firestore.collection(`${randomCollectionName}`);
73-
const stocks = new AngularFirestoreCollection<Stock>(ref, ref);
74-
const ITEMS = 10;
75-
76-
const names = await createRandomStocks(afs.firestore, ref, ITEMS);
77-
78-
const sub = stocks.stateChanges().subscribe(data => {
79-
// unsub immediately as we will be deleting data at the bottom
80-
// and that will trigger another subscribe callback and fail
81-
// the test
82-
sub.unsubscribe();
83-
// We added ten things. This should be ten.
84-
// This could not be ten if the batch failed or
85-
// if the collection state is altered during a test run
86-
expect(data.length).toEqual(ITEMS);
87-
data.forEach(action => {
88-
// We used the same piece of data so they should all equal
89-
expect(action.payload.doc.data()).toEqual(FAKE_STOCK_DATA);
79+
describe('snapshotChanges()', () => {
80+
81+
it('should listen to all snapshotChanges() by default', async (done) => {
82+
const ITEMS = 10;
83+
let count = 0;
84+
const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS);
85+
const sub = stocks.snapshotChanges().subscribe(data => {
86+
count = count + 1;
87+
// the first time should all be 'added'
88+
if(count === 1) {
89+
// make an update
90+
stocks.doc(names[0]).update({ price: 2});
91+
}
92+
// on the second round, make sure the array is still the same
93+
// length but the updated item is now modified
94+
if(count === 2) {
95+
expect(data.length).toEqual(ITEMS);
96+
const change = data.filter(x => x.payload.doc.id === names[0])[0];
97+
expect(change.type).toEqual('modified');
98+
sub.unsubscribe();
99+
deleteThemAll(names, ref).then(done).catch(done.fail);
100+
}
90101
});
91-
deleteThemAll(names, ref).then(done).catch(done.fail);
92102
});
93-
94-
});
95103

96-
fdescribe('snapshotChanges()', () => {
104+
it('should update order on queries', async (done) => {
105+
const ITEMS = 10;
106+
let count = 0;
107+
let firstIndex = 0;
108+
const { randomCollectionName, ref, stocks, names } =
109+
await collectionHarness(afs, ITEMS, ref => ref.orderBy('price', 'desc'));
110+
const sub = stocks.snapshotChanges().subscribe(data => {
111+
debugger;
112+
count = count + 1;
113+
// the first time should all be 'added'
114+
if(count === 1) {
115+
// make an update
116+
firstIndex = data.filter(d => d.payload.doc.id === names[0])[0].payload.newIndex;
117+
stocks.doc(names[0]).update({ price: 2 });
118+
}
119+
// on the second round, make sure the array is still the same
120+
// length but the updated item is now modified
121+
if(count === 2) {
122+
expect(data.length).toEqual(ITEMS);
123+
const change = data.filter(x => x.payload.doc.id === names[0])[0];
124+
expect(change.type).toEqual('modified');
125+
expect(change.payload.oldIndex).toEqual(firstIndex);
126+
sub.unsubscribe();
127+
deleteThemAll(names, ref).then(done).catch(done.fail);
128+
}
129+
});
130+
});
131+
132+
it('should be able to filter snapshotChanges() types - modified', async (done) => {
133+
const ITEMS = 10;
134+
const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS);
135+
136+
const sub = stocks.snapshotChanges(['modified']).subscribe(data => {
137+
sub.unsubscribe();
138+
const change = data.filter(x => x.payload.doc.id === names[0])[0];
139+
expect(data.length).toEqual(1);
140+
expect(change.payload.doc.data().price).toEqual(2);
141+
expect(change.type).toEqual('modified');
142+
deleteThemAll(names, ref).then(done).catch(done.fail);
143+
});
144+
145+
delayUpdate(stocks, names[0], { price: 2 });
146+
});
97147

98-
it('should listen to all snapshotChanges() by default', async (done) => {
148+
it('should be able to filter snapshotChanges() types - added', async (done) => {
149+
const ITEMS = 10;
150+
let { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS);
151+
const nextId = ref.doc('a').id;
99152

153+
const sub = stocks.snapshotChanges(['added']).skip(1).subscribe(data => {
154+
sub.unsubscribe();
155+
const change = data.filter(x => x.payload.doc.id === nextId)[0];
156+
expect(data.length).toEqual(ITEMS + 1);
157+
expect(change.payload.doc.data().price).toEqual(2);
158+
expect(change.type).toEqual('added');
159+
deleteThemAll(names, ref).then(done).catch(done.fail);
160+
done();
161+
});
162+
163+
164+
names = names.concat([nextId]);
165+
delayAdd(stocks, nextId, { price: 2 });
100166
});
101167

168+
it('should be able to filter snapshotChanges() types - removed', async (done) => {
169+
const ITEMS = 10;
170+
const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS);
171+
172+
const sub = stocks.snapshotChanges(['added', 'removed']).skip(1).subscribe(data => {
173+
sub.unsubscribe();
174+
const change = data.filter(x => x.payload.doc.id === names[0]);
175+
expect(data.length).toEqual(ITEMS - 1);
176+
expect(change.length).toEqual(0);
177+
deleteThemAll(names, ref).then(done).catch(done.fail);
178+
done();
179+
});
180+
181+
delayDelete(stocks, names[0], 400);
182+
});
183+
102184
});
103185

104186
describe('stateChanges()', () => {
187+
188+
it('should get stateChanges() updates', async (done: any) => {
189+
const ITEMS = 10;
190+
const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS);
191+
192+
const sub = stocks.stateChanges().subscribe(data => {
193+
// unsub immediately as we will be deleting data at the bottom
194+
// and that will trigger another subscribe callback and fail
195+
// the test
196+
sub.unsubscribe();
197+
// We added ten things. This should be ten.
198+
// This could not be ten if the batch failed or
199+
// if the collection state is altered during a test run
200+
expect(data.length).toEqual(ITEMS);
201+
data.forEach(action => {
202+
// We used the same piece of data so they should all equal
203+
expect(action.payload.doc.data()).toEqual(FAKE_STOCK_DATA);
204+
});
205+
deleteThemAll(names, ref).then(done).catch(done.fail);
206+
});
207+
208+
});
209+
105210
it('should listen to all stateChanges() by default', async (done) => {
106-
const randomCollectionName = randomName(afs.firestore);
107-
const ref = afs.firestore.collection(`${randomCollectionName}`);
108-
const stocks = new AngularFirestoreCollection<Stock>(ref, ref);
109211
const ITEMS = 10;
110212
let count = 0;
111-
const names = await createRandomStocks(afs.firestore, ref, ITEMS);
213+
const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS);
112214
const sub = stocks.stateChanges().subscribe(data => {
113215
count = count + 1;
114216
if(count === 1) {
@@ -123,12 +225,9 @@ describe('AngularFirestoreCollection', () => {
123225
});
124226

125227
it('should be able to filter stateChanges() types - modified', async (done) => {
126-
const randomCollectionName = randomName(afs.firestore);
127-
const ref = afs.firestore.collection(`${randomCollectionName}`);
128-
const stocks = new AngularFirestoreCollection<Stock>(ref, ref);
129228
const ITEMS = 10;
130229
let count = 0;
131-
const names = await createRandomStocks(afs.firestore, ref, ITEMS);
230+
const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS);
132231

133232
const sub = stocks.stateChanges(['modified']).subscribe(data => {
134233
sub.unsubscribe();
@@ -143,12 +242,9 @@ describe('AngularFirestoreCollection', () => {
143242
});
144243

145244
it('should be able to filter stateChanges() types - added', async (done) => {
146-
const randomCollectionName = randomName(afs.firestore);
147-
const ref = afs.firestore.collection(`${randomCollectionName}`);
148-
const stocks = new AngularFirestoreCollection<Stock>(ref, ref);
149245
const ITEMS = 10;
150246
let count = 0;
151-
let names = await createRandomStocks(afs.firestore, ref, ITEMS);
247+
let { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS);
152248

153249
const sub = stocks.stateChanges(['added']).skip(1).subscribe(data => {
154250
sub.unsubscribe();
@@ -165,11 +261,8 @@ describe('AngularFirestoreCollection', () => {
165261
});
166262

167263
it('should be able to filter stateChanges() types - removed', async (done) => {
168-
const randomCollectionName = randomName(afs.firestore);
169-
const ref = afs.firestore.collection(`${randomCollectionName}`);
170-
const stocks = new AngularFirestoreCollection<Stock>(ref, ref);
171264
const ITEMS = 10;
172-
let names = await createRandomStocks(afs.firestore, ref, ITEMS);
265+
const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS);
173266

174267
const sub = stocks.stateChanges(['removed']).subscribe(data => {
175268
sub.unsubscribe();
@@ -183,4 +276,39 @@ describe('AngularFirestoreCollection', () => {
183276
});
184277
});
185278

279+
describe('auditTrail()', () => {
280+
it('should listen to all events for auditTrail() by default', async (done) => {
281+
const ITEMS = 10;
282+
let count = 0;
283+
const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS);
284+
const sub = stocks.auditTrail().subscribe(data => {
285+
count = count + 1;
286+
if(count === 1) {
287+
stocks.doc(names[0]).update({ price: 2});
288+
}
289+
if(count === 2) {
290+
sub.unsubscribe();
291+
expect(data.length).toEqual(ITEMS + 1);
292+
expect(data[data.length - 1].type).toEqual('modified');
293+
deleteThemAll(names, ref).then(done).catch(done.fail);
294+
}
295+
});
296+
});
297+
298+
it('should be able to filter auditTrail() types - removed', async (done) => {
299+
const ITEMS = 10;
300+
const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS);
301+
302+
const sub = stocks.auditTrail(['removed']).subscribe(data => {
303+
sub.unsubscribe();
304+
expect(data.length).toEqual(1);
305+
expect(data[0].type).toEqual('removed');
306+
deleteThemAll(names, ref).then(done).catch(done.fail);
307+
done();
308+
});
309+
310+
delayDelete(stocks, names[0], 400);
311+
});
312+
});
313+
186314
});

0 commit comments

Comments
 (0)