@dereekb/firebase 12.6.21 → 13.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/LICENSE +1 -1
  2. package/index.cjs.js +53 -2239
  3. package/index.esm.js +34 -2216
  4. package/package.json +21 -15
  5. package/src/lib/client/storage/driver.accessor.d.ts +1 -1
  6. package/src/lib/common/firestore/query/iterator.d.ts +0 -4
  7. package/src/lib/common/firestore/snapshot/snapshot.field.d.ts +0 -6
  8. package/src/lib/common/model/model.service.d.ts +1 -1
  9. package/src/lib/common/storage/driver/accessor.d.ts +1 -1
  10. package/src/lib/common/storage/types.d.ts +3 -3
  11. package/src/lib/model/notification/notification.api.d.ts +1 -1
  12. package/src/lib/model/notification/notification.config.d.ts +5 -5
  13. package/src/lib/model/notification/notification.d.ts +15 -15
  14. package/src/lib/model/notification/notification.details.d.ts +0 -4
  15. package/src/lib/model/notification/notification.item.d.ts +1 -1
  16. package/src/lib/model/storagefile/storagefile.api.d.ts +4 -4
  17. package/src/lib/model/storagefile/storagefile.d.ts +7 -7
  18. package/src/lib/model/storagefile/storagefile.task.d.ts +1 -13
  19. package/src/lib/model/system/system.d.ts +2 -2
  20. package/test/index.cjs.js +4043 -0
  21. package/test/index.esm.js +3957 -0
  22. package/test/package.json +27 -9
  23. package/test/src/lib/client/firebase.authorized.d.ts +2 -2
  24. package/test/src/lib/client/firebase.d.ts +4 -3
  25. package/test/src/lib/client/firestore.mock.item.fixture.authorized.d.ts +2 -2
  26. package/test/src/lib/common/firebase.instance.d.ts +7 -3
  27. package/test/src/lib/common/firestore/firestore.instance.d.ts +7 -3
  28. package/test/src/lib/common/mock/mock.item.collection.fixture.d.ts +5 -2
  29. package/test/src/lib/common/mock/mock.item.storage.fixture.d.ts +4 -4
  30. package/test/src/lib/common/storage/storage.instance.d.ts +7 -3
  31. package/test/CHANGELOG.md +0 -2114
  32. package/test/README.md +0 -11
  33. package/test/src/index.js +0 -5
  34. package/test/src/index.js.map +0 -1
  35. package/test/src/lib/client/firebase.authorized.js +0 -35
  36. package/test/src/lib/client/firebase.authorized.js.map +0 -1
  37. package/test/src/lib/client/firebase.js +0 -125
  38. package/test/src/lib/client/firebase.js.map +0 -1
  39. package/test/src/lib/client/firestore.mock.item.fixture.authorized.js +0 -19
  40. package/test/src/lib/client/firestore.mock.item.fixture.authorized.js.map +0 -1
  41. package/test/src/lib/client/index.js +0 -7
  42. package/test/src/lib/client/index.js.map +0 -1
  43. package/test/src/lib/common/firebase.instance.js +0 -35
  44. package/test/src/lib/common/firebase.instance.js.map +0 -1
  45. package/test/src/lib/common/firestore/firestore.instance.js +0 -24
  46. package/test/src/lib/common/firestore/firestore.instance.js.map +0 -1
  47. package/test/src/lib/common/firestore/firestore.js +0 -67
  48. package/test/src/lib/common/firestore/firestore.js.map +0 -1
  49. package/test/src/lib/common/firestore/index.js +0 -9
  50. package/test/src/lib/common/firestore/index.js.map +0 -1
  51. package/test/src/lib/common/firestore/test.driver.accessor.js +0 -767
  52. package/test/src/lib/common/firestore/test.driver.accessor.js.map +0 -1
  53. package/test/src/lib/common/firestore/test.driver.query.js +0 -1361
  54. package/test/src/lib/common/firestore/test.driver.query.js.map +0 -1
  55. package/test/src/lib/common/firestore/test.iterator.js +0 -221
  56. package/test/src/lib/common/firestore/test.iterator.js.map +0 -1
  57. package/test/src/lib/common/index.js +0 -8
  58. package/test/src/lib/common/index.js.map +0 -1
  59. package/test/src/lib/common/mock/index.js +0 -10
  60. package/test/src/lib/common/mock/index.js.map +0 -1
  61. package/test/src/lib/common/mock/mock.item.collection.fixture.js +0 -64
  62. package/test/src/lib/common/mock/mock.item.collection.fixture.js.map +0 -1
  63. package/test/src/lib/common/mock/mock.item.id.js +0 -3
  64. package/test/src/lib/common/mock/mock.item.id.js.map +0 -1
  65. package/test/src/lib/common/mock/mock.item.js +0 -339
  66. package/test/src/lib/common/mock/mock.item.js.map +0 -1
  67. package/test/src/lib/common/mock/mock.item.query.js +0 -33
  68. package/test/src/lib/common/mock/mock.item.query.js.map +0 -1
  69. package/test/src/lib/common/mock/mock.item.service.js +0 -77
  70. package/test/src/lib/common/mock/mock.item.service.js.map +0 -1
  71. package/test/src/lib/common/mock/mock.item.storage.fixture.js +0 -40
  72. package/test/src/lib/common/mock/mock.item.storage.fixture.js.map +0 -1
  73. package/test/src/lib/common/storage/index.js +0 -7
  74. package/test/src/lib/common/storage/index.js.map +0 -1
  75. package/test/src/lib/common/storage/storage.instance.js +0 -24
  76. package/test/src/lib/common/storage/storage.instance.js.map +0 -1
  77. package/test/src/lib/common/storage/storage.js +0 -37
  78. package/test/src/lib/common/storage/storage.js.map +0 -1
  79. package/test/src/lib/common/storage/test.driver.accessor.js +0 -669
  80. package/test/src/lib/common/storage/test.driver.accessor.js.map +0 -1
  81. package/test/src/lib/index.js +0 -6
  82. package/test/src/lib/index.js.map +0 -1
  83. /package/{index.cjs.d.ts → index.d.ts} +0 -0
  84. /package/{index.esm.d.ts → test/index.d.ts} +0 -0
@@ -0,0 +1,3957 @@
1
+ import 'core-js/modules/es.iterator.constructor.js';
2
+ import 'core-js/modules/es.iterator.for-each.js';
3
+ import 'core-js/modules/es.iterator.map.js';
4
+ import 'core-js/modules/es.map.get-or-insert.js';
5
+ import 'core-js/modules/es.map.get-or-insert-computed.js';
6
+ import { performAsyncTasks, cachedGetter, bitwiseObjectDencoder, modelFieldConversions, isEvenNumber, arrayFactory, mapGetter, randomNumberFactory, randomFromArrayFactory, idBatchFactory, unique, waitForMs, arrayContainsDuplicateValue, useCallback, readableStreamToBuffer, SLASH_PATH_SEPARATOR } from '@dereekb/util';
7
+ import { AbstractTestContextFixture, testContextBuilder, instanceWrapTestContextFactory, AbstractWrappedFixtureWithInstance, itShouldFail, expectFail, callbackTest } from '@dereekb/util/test';
8
+ import { initializeTestEnvironment } from '@firebase/rules-unit-testing';
9
+ import { firebaseStorageClientDrivers, firebaseFirestoreClientDrivers, firestoreContextFactory, firebaseStorageContextFactory, firestoreModelIdentity, snapshotConverterFunctions, firestoreBoolean, optionalFirestoreNumber, optionalFirestoreDate, optionalFirestoreArray, optionalFirestoreString, firestoreDate, firestoreBitwiseObjectMap, firestoreUniqueStringArray, firestoreNumber, firestoreString, firestoreUID, copyUserRelatedDataAccessorFactoryFunction, firestoreSubObject, AbstractFirestoreDocumentWithParent, AbstractFirestoreDocument, firebaseModelServiceFactory, grantFullAccessIfAdmin, firebaseModelsService, systemStateFirestoreCollection, allChildDocumentsUnderParent, where, makeDocuments, getDocumentSnapshotPairs, useDocumentSnapshot, useDocumentSnapshotData, firestoreIdBatchVerifierFactory, whereDocumentId, loadAllFirestoreDocumentSnapshotPairs, loadAllFirestoreDocumentSnapshot, iterateFirestoreDocumentSnapshotPairs, iterateFirestoreDocumentSnapshots, iterateFirestoreDocumentSnapshotPairBatches, iterateFirestoreDocumentSnapshotBatches, limit, orderBy, limitToLast, whereStringHasRootIdentityModelKey, whereStringValueHasPrefix, whereDateIsAfterWithSort, whereDateIsBeforeWithSort, whereDateIsOnOrAfterWithSort, whereDateIsOnOrBeforeWithSort, whereDateIsInRange, whereDateIsBetween, startAt, orderByDocumentId, startAtValue, startAfter, endAt, endAtValue, endBefore, firebaseQuerySnapshotAccumulator, firebaseQueryItemAccumulator, uploadFileWithStream, iterateStorageListFilesByEachFile } from '@dereekb/firebase';
10
+ import { setLogLevel } from 'firebase/firestore';
11
+ import 'core-js/modules/es.set.difference.v2.js';
12
+ import 'core-js/modules/es.set.symmetric-difference.v2.js';
13
+ import 'core-js/modules/es.set.union.v2.js';
14
+ import { firstValueFrom, filter, skip, from, first, switchMap } from 'rxjs';
15
+ import { SubscriptionObject, iteratorNextPageUntilPage, flattenAccumulatorResultItemArray, accumulatorCurrentPageListLoadingState, isLoadingStateFinishedLoading, accumulatorFlattenPageListLoadingState } from '@dereekb/rxjs';
16
+ import 'core-js/modules/es.iterator.filter.js';
17
+ import { addDays, startOfDay, addHours } from 'date-fns';
18
+ import { DateRangeType } from '@dereekb/date';
19
+ import 'core-js/modules/es.iterator.find.js';
20
+ import 'core-js/modules/es.typed-array.with.js';
21
+ import 'core-js/modules/es.uint8-array.set-from-base64.js';
22
+ import 'core-js/modules/es.uint8-array.set-from-hex.js';
23
+ import 'core-js/modules/es.uint8-array.to-base64.js';
24
+ import 'core-js/modules/es.uint8-array.to-hex.js';
25
+ import { Readable } from 'stream';
26
+ import { createReadStream } from 'fs';
27
+
28
+ function makeTestingFirestoreAccesorDriver(driver) {
29
+ let fuzzerKey = 0;
30
+ const time = new Date().getTime();
31
+ const fuzzedMap = new Map();
32
+ const {
33
+ collection,
34
+ subcollection,
35
+ collectionGroup
36
+ } = driver;
37
+ const fuzzedPathForPath = path => {
38
+ let fuzzedPath = fuzzedMap.get(path);
39
+ if (!fuzzedPath) {
40
+ const random = Math.ceil(Math.random() * 9999) % 9999;
41
+ fuzzedPath = `${time}_${random}_${path}_${fuzzerKey += 1}`;
42
+ fuzzedMap.set(path, fuzzedPath);
43
+ }
44
+ return fuzzedPath;
45
+ };
46
+ const fuzzedCollection = (f, path) => {
47
+ const fuzzedPath = fuzzedPathForPath(path);
48
+ return collection(f, fuzzedPath);
49
+ };
50
+ const fuzzedSubcollection = (document, path, ...pathSegments) => {
51
+ const fuzzedPath = fuzzedPathForPath(path);
52
+ const fuzzedPathSegments = pathSegments.map(x => fuzzedPathForPath(x));
53
+ return subcollection(document, fuzzedPath, ...fuzzedPathSegments);
54
+ };
55
+ const fuzzedCollectionGroup = (f, collectionId) => {
56
+ const fuzzedPath = fuzzedPathForPath(collectionId);
57
+ return collectionGroup(f, fuzzedPath);
58
+ };
59
+ const initWithCollectionNames = collectionNames => {
60
+ collectionNames.forEach(x => fuzzedPathForPath(x));
61
+ return fuzzedMap;
62
+ };
63
+ const injectedDriver = {
64
+ ...driver,
65
+ collection: fuzzedCollection,
66
+ collectionGroup: fuzzedCollectionGroup,
67
+ subcollection: fuzzedSubcollection,
68
+ getFuzzedCollectionsNameMap: () => fuzzedMap,
69
+ initWithCollectionNames,
70
+ fuzzedPathForPath
71
+ };
72
+ return injectedDriver;
73
+ }
74
+ /**
75
+ * Extends the input drivers to generate new drivers for a testing environment.
76
+ *
77
+ * @param drivers
78
+ * @returns
79
+ */
80
+ function makeTestingFirestoreDrivers(drivers) {
81
+ return {
82
+ ...drivers,
83
+ firestoreDriverType: 'testing',
84
+ firestoreAccessorDriver: makeTestingFirestoreAccesorDriver(drivers.firestoreAccessorDriver)
85
+ };
86
+ }
87
+ async function clearTestFirestoreContextCollections(context, clearCollection) {
88
+ const names = context.drivers.firestoreAccessorDriver.getFuzzedCollectionsNameMap();
89
+ const tuples = Array.from(names.entries());
90
+ await performAsyncTasks(tuples, ([name, fuzzyPath]) => clearCollection(name, fuzzyPath));
91
+ }
92
+
93
+ let bucketTestNameKey = 0;
94
+ function makeTestingFirebaseStorageAccesorDriver(driver, config) {
95
+ const {
96
+ useTestDefaultBucket
97
+ } = config ?? {};
98
+ // The default bucket is only used if another bucket is not input.
99
+ const defaultBucket = !driver.getDefaultBucket && useTestDefaultBucket !== false || useTestDefaultBucket === true ? cachedGetter(() => {
100
+ const time = new Date().getTime();
101
+ const random = Math.ceil(Math.random() * 999999) % 999999;
102
+ const testBucketName = `test-bucket-${time}-${random}-${bucketTestNameKey += 1}`;
103
+ return testBucketName;
104
+ }) : driver.getDefaultBucket;
105
+ const injectedDriver = {
106
+ ...driver,
107
+ getDefaultBucket: defaultBucket
108
+ };
109
+ return injectedDriver;
110
+ }
111
+ /**
112
+ * Extends the input drivers to generate new drivers for a testing environment.
113
+ *
114
+ * @param drivers
115
+ * @returns
116
+ */
117
+ function makeTestingFirebaseStorageDrivers(drivers, config) {
118
+ return {
119
+ ...drivers,
120
+ storageDriverType: 'testing',
121
+ storageAccessorDriver: makeTestingFirebaseStorageAccesorDriver(drivers.storageAccessorDriver, config)
122
+ };
123
+ }
124
+
125
+ class TestFirebaseInstance {
126
+ constructor(firestoreContext, storageContext) {
127
+ this.firestoreContext = void 0;
128
+ this.storageContext = void 0;
129
+ this.firestoreContext = firestoreContext;
130
+ this.storageContext = storageContext;
131
+ }
132
+ get firestore() {
133
+ return this.firestoreContext.firestore;
134
+ }
135
+ get storage() {
136
+ return this.storageContext.storage;
137
+ }
138
+ }
139
+ class TestFirebaseContextFixture extends AbstractTestContextFixture {
140
+ get firestore() {
141
+ return this.instance.firestore;
142
+ }
143
+ get firestoreContext() {
144
+ return this.instance.firestoreContext;
145
+ }
146
+ get storage() {
147
+ return this.instance.storage;
148
+ }
149
+ get storageContext() {
150
+ return this.instance.storageContext;
151
+ }
152
+ }
153
+
154
+ function makeRulesTestFirestoreContext(drivers, rulesTestEnvironment, rulesTestContext) {
155
+ const context = {
156
+ ...firestoreContextFactory(drivers)(rulesTestContext.firestore()),
157
+ drivers,
158
+ rulesTestContext,
159
+ rulesTestEnvironment
160
+ };
161
+ return context;
162
+ }
163
+ function makeRulesTestFirebaseStorageContext(drivers, rulesTestEnvironment, rulesTestContext) {
164
+ const context = {
165
+ ...firebaseStorageContextFactory(drivers)(rulesTestContext.storage()),
166
+ drivers,
167
+ rulesTestContext,
168
+ rulesTestEnvironment
169
+ };
170
+ return context;
171
+ }
172
+ class RulesUnitTestTestFirebaseInstance {
173
+ constructor(drivers, rulesTestEnvironment, rulesTestContext) {
174
+ this.drivers = void 0;
175
+ this.rulesTestEnvironment = void 0;
176
+ this.rulesTestContext = void 0;
177
+ this._firestoreContext = cachedGetter(() => makeRulesTestFirestoreContext(this.drivers, this.rulesTestEnvironment, this.rulesTestContext));
178
+ this._storageContext = cachedGetter(() => makeRulesTestFirebaseStorageContext(this.drivers, this.rulesTestEnvironment, this.rulesTestContext));
179
+ this.drivers = drivers;
180
+ this.rulesTestEnvironment = rulesTestEnvironment;
181
+ this.rulesTestContext = rulesTestContext;
182
+ }
183
+ get firestoreContext() {
184
+ return this._firestoreContext();
185
+ }
186
+ get storageContext() {
187
+ return this._storageContext();
188
+ }
189
+ get firestore() {
190
+ return this.firestoreContext.firestore;
191
+ }
192
+ get storage() {
193
+ return this.storageContext.storage;
194
+ }
195
+ }
196
+ class RulesUnitTestFirebaseTestingContextFixture extends TestFirebaseContextFixture {}
197
+ /**
198
+ * A TestContextBuilderFunction for building firebase test context factories using @firebase/firebase and @firebase/rules-unit-testing. This means CLIENT TESTING ONLY. For server testing, look at @dereekb/firestore-server.
199
+ *
200
+ * This can be used to easily build a testing context that sets up RulesTestEnvironment for tests that sets itself up and tears itself down.
201
+ */
202
+ const firebaseRulesUnitTestBuilder = testContextBuilder({
203
+ buildConfig: input => {
204
+ const config = {
205
+ testEnvironment: input?.testEnvironment ?? {},
206
+ rulesContext: input?.rulesContext
207
+ };
208
+ return config;
209
+ },
210
+ buildFixture: () => new RulesUnitTestFirebaseTestingContextFixture(),
211
+ setupInstance: async config => {
212
+ const drivers = {
213
+ ...makeTestingFirestoreDrivers(firebaseFirestoreClientDrivers()),
214
+ ...makeTestingFirebaseStorageDrivers(firebaseStorageClientDrivers(), {
215
+ useTestDefaultBucket: true
216
+ })
217
+ };
218
+ let testEnvironment = config.testEnvironment;
219
+ if (config.testEnvironment.collectionNames) {
220
+ drivers.firestoreAccessorDriver.initWithCollectionNames(config.testEnvironment.collectionNames);
221
+ testEnvironment = {
222
+ ...testEnvironment,
223
+ firestore: rewriteEmulatorConfigRulesForFuzzedCollectionNames(testEnvironment.firestore)
224
+ };
225
+ }
226
+ const rulesTestEnv = await initializeTestEnvironment(config.testEnvironment);
227
+ const rulesTestContext = rulesTestContextForConfig(rulesTestEnv, config.rulesContext);
228
+ return new RulesUnitTestTestFirebaseInstance(drivers, rulesTestEnv, rulesTestContext);
229
+ },
230
+ teardownInstance: async (instance, config) => {
231
+ await instance.rulesTestEnvironment.cleanup().catch(e => {
232
+ console.warn('firebaseRulesUnitTestBuilder(): Failed to cleanup rules test environment', e);
233
+ throw e;
234
+ });
235
+ }
236
+ });
237
+ // MARK: Internal
238
+ function rulesTestContextForConfig(rulesTestEnv, testingRulesConfig) {
239
+ let rulesTestContext;
240
+ if (testingRulesConfig != null) {
241
+ rulesTestContext = rulesTestEnv.authenticatedContext(testingRulesConfig.userId, testingRulesConfig.tokenOptions ?? undefined);
242
+ } else {
243
+ rulesTestContext = rulesTestEnv.unauthenticatedContext();
244
+ }
245
+ return rulesTestContext;
246
+ }
247
+ function rewriteEmulatorConfigRulesForFuzzedCollectionNames(config, fuzzedCollectionNamesMap) {
248
+ if (config && config.rules) {
249
+ config = {
250
+ ...config,
251
+ rules: rewriteRulesForFuzzedCollectionNames(config.rules)
252
+ };
253
+ }
254
+ return config;
255
+ }
256
+ function rewriteRulesForFuzzedCollectionNames(rules, fuzzedCollectionNamesMap) {
257
+ // TODO: rewrite the rules using regex matching/replacement.
258
+ return rules;
259
+ }
260
+ // MARK: Utility
261
+ function changeFirestoreLogLevelBeforeAndAfterTests() {
262
+ beforeAll(() => setLogLevel('error'));
263
+ afterAll(() => setLogLevel('warn'));
264
+ }
265
+
266
+ const TESTING_AUTHORIZED_FIREBASE_USER_ID = '0';
267
+ const authorizedFirebaseFactory = firebaseRulesUnitTestBuilder({
268
+ testEnvironment: {
269
+ firestore: {
270
+ rules: `
271
+ rules_version = '2';
272
+ service cloud.firestore {
273
+ match /databases/{database}/documents {
274
+ match /{document=**} {
275
+ allow read, write: if true;
276
+ }
277
+ }
278
+ }
279
+ `
280
+ },
281
+ storage: {
282
+ rules: `
283
+ rules_version = '2';
284
+ service firebase.storage {
285
+ match /b/{bucket}/o {
286
+ match /{allPaths=**} {
287
+ allow read, write: if true;
288
+ }
289
+ }
290
+ }
291
+ `
292
+ }
293
+ },
294
+ rulesContext: {
295
+ userId: TESTING_AUTHORIZED_FIREBASE_USER_ID
296
+ }
297
+ });
298
+
299
+ // MARK: Mock Item
300
+ const mockItemIdentity = firestoreModelIdentity('mockItem', 'mi');
301
+ class MockItemDocument extends AbstractFirestoreDocument {
302
+ get modelIdentity() {
303
+ return mockItemIdentity;
304
+ }
305
+ }
306
+ /**
307
+ * Used to build a FirestoreDataConverter. Fields are configured via configuration. See the SnapshotConverterFunctions for more info.
308
+ */
309
+ const mockItemConverter = snapshotConverterFunctions({
310
+ fields: {
311
+ value: optionalFirestoreString(),
312
+ tags: optionalFirestoreArray(),
313
+ date: optionalFirestoreDate(),
314
+ number: optionalFirestoreNumber(),
315
+ test: firestoreBoolean({
316
+ default: true
317
+ })
318
+ }
319
+ });
320
+ /**
321
+ * Used to build a mockItemCollection from a firestore instance with a converter setup.
322
+ *
323
+ * @param firestore
324
+ * @returns
325
+ */
326
+ function mockItemCollectionReference(context) {
327
+ return context.collection(mockItemIdentity.collectionName);
328
+ }
329
+ function mockItemFirestoreCollection(firestoreContext) {
330
+ return firestoreContext.firestoreCollection({
331
+ converter: mockItemConverter,
332
+ modelIdentity: mockItemIdentity,
333
+ collection: mockItemCollectionReference(firestoreContext),
334
+ makeDocument: (a, d) => new MockItemDocument(a, d),
335
+ firestoreContext
336
+ });
337
+ }
338
+ // MARK: MockItemPrivate
339
+ const mockItemPrivateIdentity = firestoreModelIdentity(mockItemIdentity, 'mockItemPrivate', 'mip');
340
+ var MockItemSettingsItemEnum;
341
+ (function (MockItemSettingsItemEnum) {
342
+ MockItemSettingsItemEnum[MockItemSettingsItemEnum["NORTH"] = 0] = "NORTH";
343
+ MockItemSettingsItemEnum[MockItemSettingsItemEnum["SOUTH"] = 1] = "SOUTH";
344
+ MockItemSettingsItemEnum[MockItemSettingsItemEnum["EAST"] = 2] = "EAST";
345
+ MockItemSettingsItemEnum[MockItemSettingsItemEnum["WEST"] = 3] = "WEST";
346
+ })(MockItemSettingsItemEnum || (MockItemSettingsItemEnum = {}));
347
+ const mockItemSettingsItemDencoder = bitwiseObjectDencoder({
348
+ maxIndex: 4,
349
+ toSetFunction: x => {
350
+ const set = new Set();
351
+ if (x.north) {
352
+ set.add(MockItemSettingsItemEnum.NORTH);
353
+ }
354
+ if (x.south) {
355
+ set.add(MockItemSettingsItemEnum.SOUTH);
356
+ }
357
+ if (x.east) {
358
+ set.add(MockItemSettingsItemEnum.EAST);
359
+ }
360
+ if (x.west) {
361
+ set.add(MockItemSettingsItemEnum.WEST);
362
+ }
363
+ return set;
364
+ },
365
+ fromSetFunction: x => {
366
+ const object = {};
367
+ if (x.has(MockItemSettingsItemEnum.NORTH)) {
368
+ object.north = true;
369
+ }
370
+ if (x.has(MockItemSettingsItemEnum.SOUTH)) {
371
+ object.south = true;
372
+ }
373
+ if (x.has(MockItemSettingsItemEnum.EAST)) {
374
+ object.east = true;
375
+ }
376
+ if (x.has(MockItemSettingsItemEnum.WEST)) {
377
+ object.west = true;
378
+ }
379
+ return object;
380
+ }
381
+ });
382
+ /**
383
+ * FirestoreDocument for MockItem
384
+ */
385
+ class MockItemPrivateDocument extends AbstractFirestoreDocument {
386
+ get modelIdentity() {
387
+ return mockItemPrivateIdentity;
388
+ }
389
+ }
390
+ /**
391
+ * Used to build a FirestoreDataConverter. Fields are configured via configuration. See the SnapshotConverterFunctions for more info.
392
+ */
393
+ const mockItemPrivateConverter = snapshotConverterFunctions({
394
+ fieldConversions: modelFieldConversions({
395
+ num: firestoreNumber({
396
+ default: 0,
397
+ defaultBeforeSave: 0
398
+ }),
399
+ comments: optionalFirestoreString(),
400
+ values: firestoreUniqueStringArray(),
401
+ settings: firestoreBitwiseObjectMap({
402
+ dencoder: mockItemSettingsItemDencoder
403
+ }),
404
+ createdAt: firestoreDate({
405
+ saveDefaultAsNow: true
406
+ })
407
+ })
408
+ });
409
+ /**
410
+ * Used to build a mockItemCollection from a firestore instance with a converter setup.
411
+ *
412
+ * @param firestore
413
+ * @returns
414
+ */
415
+ function mockItemPrivateCollectionReferenceFactory(context) {
416
+ return parent => {
417
+ return context.subcollection(parent.documentRef, mockItemPrivateIdentity.collectionName);
418
+ };
419
+ }
420
+ function mockItemPrivateFirestoreCollection(firestoreContext) {
421
+ const factory = mockItemPrivateCollectionReferenceFactory(firestoreContext);
422
+ return parent => {
423
+ return firestoreContext.singleItemFirestoreCollection({
424
+ modelIdentity: mockItemPrivateIdentity,
425
+ converter: mockItemPrivateConverter,
426
+ collection: factory(parent),
427
+ makeDocument: (a, d) => new MockItemPrivateDocument(a, d),
428
+ firestoreContext,
429
+ parent
430
+ });
431
+ };
432
+ }
433
+ function mockItemPrivateCollectionReference(context) {
434
+ return context.collectionGroup(mockItemPrivateIdentity.collectionName);
435
+ }
436
+ function mockItemPrivateFirestoreCollectionGroup(firestoreContext) {
437
+ return firestoreContext.firestoreCollectionGroup({
438
+ modelIdentity: mockItemPrivateIdentity,
439
+ converter: mockItemPrivateConverter,
440
+ queryLike: mockItemPrivateCollectionReference(firestoreContext),
441
+ makeDocument: (accessor, documentAccessor) => new MockItemPrivateDocument(accessor, documentAccessor),
442
+ firestoreContext
443
+ });
444
+ }
445
+ // MARK: MockItemUser
446
+ const mockItemUserIdentity = firestoreModelIdentity(mockItemIdentity, 'mockItemUser', 'miu');
447
+ /**
448
+ * FirestoreDocument for MockItem
449
+ */
450
+ class MockItemUserDocument extends AbstractFirestoreDocument {
451
+ get modelIdentity() {
452
+ return mockItemUserIdentity;
453
+ }
454
+ }
455
+ /**
456
+ * Firestore collection path name.
457
+ */
458
+ const mockItemUserCollectionName = 'mockItemUser';
459
+ const mockItemUserIdentifier = '0';
460
+ /**
461
+ * Used to build a FirestoreDataConverter. Fields are configured via configuration. See the SnapshotConverterFunctions for more info.
462
+ */
463
+ const mockItemUserConverter = snapshotConverterFunctions({
464
+ fieldConversions: modelFieldConversions({
465
+ uid: firestoreUID(),
466
+ name: firestoreString()
467
+ })
468
+ });
469
+ /**
470
+ * Used to build a mockItemCollection from a firestore instance with a converter setup.
471
+ *
472
+ * @param firestore
473
+ * @returns
474
+ */
475
+ function mockItemUserCollectionReferenceFactory(context) {
476
+ return parent => {
477
+ return context.subcollection(parent.documentRef, mockItemUserCollectionName);
478
+ };
479
+ }
480
+ const mockItemUserAccessorFactory = copyUserRelatedDataAccessorFactoryFunction();
481
+ function mockItemUserFirestoreCollection(firestoreContext) {
482
+ const factory = mockItemUserCollectionReferenceFactory(firestoreContext);
483
+ return parent => {
484
+ return firestoreContext.firestoreCollectionWithParent({
485
+ modelIdentity: mockItemUserIdentity,
486
+ converter: mockItemUserConverter,
487
+ collection: factory(parent),
488
+ accessorFactory: mockItemUserAccessorFactory,
489
+ makeDocument: (a, d) => new MockItemUserDocument(a, d),
490
+ firestoreContext,
491
+ parent
492
+ });
493
+ };
494
+ }
495
+ function mockItemUserCollectionReference(context) {
496
+ return context.collectionGroup(mockItemUserCollectionName);
497
+ }
498
+ function mockItemUserFirestoreCollectionGroup(firestoreContext) {
499
+ return firestoreContext.firestoreCollectionGroup({
500
+ modelIdentity: mockItemUserIdentity,
501
+ converter: mockItemUserConverter,
502
+ queryLike: mockItemUserCollectionReference(firestoreContext),
503
+ accessorFactory: mockItemUserAccessorFactory,
504
+ makeDocument: (accessor, documentAccessor) => new MockItemUserDocument(accessor, documentAccessor),
505
+ firestoreContext
506
+ });
507
+ }
508
+ // MARK: MockItemSubItem
509
+ const mockItemSubItemIdentity = firestoreModelIdentity(mockItemIdentity, 'mockItemSub', 'misi');
510
+ /**
511
+ * FirestoreDocument for MockItem
512
+ */
513
+ class MockItemSubItemDocument extends AbstractFirestoreDocumentWithParent {
514
+ get modelIdentity() {
515
+ return mockItemSubItemIdentity;
516
+ }
517
+ }
518
+ /**
519
+ * Used to build a FirestoreDataConverter. Fields are configured via configuration. See the SnapshotConverterFunctions for more info.
520
+ */
521
+ const mockItemSubItemConverter = snapshotConverterFunctions({
522
+ fields: {
523
+ value: optionalFirestoreNumber()
524
+ }
525
+ });
526
+ function mockItemSubItemCollectionReferenceFactory(context) {
527
+ return parent => {
528
+ return context.subcollection(parent.documentRef, mockItemSubItemIdentity.collectionName);
529
+ };
530
+ }
531
+ function mockItemSubItemFirestoreCollection(firestoreContext) {
532
+ const factory = mockItemSubItemCollectionReferenceFactory(firestoreContext);
533
+ return parent => {
534
+ return firestoreContext.firestoreCollectionWithParent({
535
+ modelIdentity: mockItemSubItemIdentity,
536
+ converter: mockItemSubItemConverter,
537
+ collection: factory(parent),
538
+ makeDocument: (a, d) => new MockItemSubItemDocument(a, d),
539
+ firestoreContext,
540
+ parent
541
+ });
542
+ };
543
+ }
544
+ function mockItemSubItemCollectionReference(context) {
545
+ return context.collectionGroup(mockItemSubItemIdentity.collectionName);
546
+ }
547
+ function mockItemSubItemFirestoreCollectionGroup(firestoreContext) {
548
+ return firestoreContext.firestoreCollectionGroup({
549
+ modelIdentity: mockItemSubItemIdentity,
550
+ converter: mockItemSubItemConverter,
551
+ queryLike: mockItemSubItemCollectionReference(firestoreContext),
552
+ makeDocument: (accessor, documentAccessor) => new MockItemSubItemDocument(accessor, documentAccessor),
553
+ firestoreContext
554
+ });
555
+ }
556
+ // MARK: Sub-Sub Item
557
+ const mockItemSubItemDeepIdentity = firestoreModelIdentity(mockItemSubItemIdentity, 'mockItemSubItemDeep', 'misid');
558
+ /**
559
+ * FirestoreDocument for MockSubItem
560
+ */
561
+ class MockItemSubItemDeepDocument extends AbstractFirestoreDocumentWithParent {
562
+ get modelIdentity() {
563
+ return mockItemSubItemDeepIdentity;
564
+ }
565
+ }
566
+ /**
567
+ * Used to build a FirestoreDataConverter. Fields are configured via configuration. See the SnapshotConverterFunctions for more info.
568
+ */
569
+ const mockItemSubItemDeepConverter = snapshotConverterFunctions({
570
+ fields: {
571
+ value: optionalFirestoreNumber()
572
+ }
573
+ });
574
+ function mockItemSubItemDeepCollectionReferenceFactory(context) {
575
+ return parent => {
576
+ return context.subcollection(parent.documentRef, mockItemSubItemDeepIdentity.collectionName);
577
+ };
578
+ }
579
+ function mockItemSubItemDeepFirestoreCollection(firestoreContext) {
580
+ const factory = mockItemSubItemDeepCollectionReferenceFactory(firestoreContext);
581
+ return parent => {
582
+ return firestoreContext.firestoreCollectionWithParent({
583
+ modelIdentity: mockItemSubItemDeepIdentity,
584
+ converter: mockItemSubItemDeepConverter,
585
+ collection: factory(parent),
586
+ makeDocument: (a, d) => new MockItemSubItemDeepDocument(a, d),
587
+ firestoreContext,
588
+ parent
589
+ });
590
+ };
591
+ }
592
+ function mockItemSubItemDeepCollectionReference(context) {
593
+ return context.collectionGroup(mockItemSubItemDeepIdentity.collectionName);
594
+ }
595
+ function mockItemSubItemDeepFirestoreCollectionGroup(firestoreContext) {
596
+ return firestoreContext.firestoreCollectionGroup({
597
+ modelIdentity: mockItemSubItemDeepIdentity,
598
+ converter: mockItemSubItemDeepConverter,
599
+ queryLike: mockItemSubItemDeepCollectionReference(firestoreContext),
600
+ makeDocument: (accessor, documentAccessor) => new MockItemSubItemDeepDocument(accessor, documentAccessor),
601
+ firestoreContext
602
+ });
603
+ }
604
+ // MARK: Mock System Item
605
+ const MOCK_SYSTEM_STATE_TYPE = 'mockitemsystemstate';
606
+ const mockItemSystemDataConverter = firestoreSubObject({
607
+ objectField: {
608
+ fields: {
609
+ lat: firestoreDate({
610
+ saveDefaultAsNow: true
611
+ })
612
+ }
613
+ }
614
+ });
615
+ const mockItemSystemStateStoredDataConverterMap = {
616
+ [MOCK_SYSTEM_STATE_TYPE]: mockItemSystemDataConverter
617
+ };
618
+
619
+ // MARK: Collections
620
+ class MockItemCollections {}
621
+ function makeMockItemCollections(firestoreContext) {
622
+ return {
623
+ mockItemCollection: mockItemFirestoreCollection(firestoreContext),
624
+ mockItemPrivateCollectionFactory: mockItemPrivateFirestoreCollection(firestoreContext),
625
+ mockItemPrivateCollectionGroup: mockItemPrivateFirestoreCollectionGroup(firestoreContext),
626
+ mockItemUserCollectionFactory: mockItemUserFirestoreCollection(firestoreContext),
627
+ mockItemUserCollectionGroup: mockItemUserFirestoreCollectionGroup(firestoreContext),
628
+ mockItemSubItemCollectionFactory: mockItemSubItemFirestoreCollection(firestoreContext),
629
+ mockItemSubItemCollectionGroup: mockItemSubItemFirestoreCollectionGroup(firestoreContext),
630
+ mockItemSubItemDeepCollectionFactory: mockItemSubItemDeepFirestoreCollection(firestoreContext),
631
+ mockItemSubItemDeepCollectionGroup: mockItemSubItemDeepFirestoreCollectionGroup(firestoreContext),
632
+ mockItemSystemStateCollection: systemStateFirestoreCollection(firestoreContext, mockItemSystemStateStoredDataConverterMap)
633
+ };
634
+ }
635
+ // MARK: Models
636
+ const mockItemFirebaseModelServiceFactory = firebaseModelServiceFactory({
637
+ roleMapForModel: function (output, context, model) {
638
+ const roles = context.rolesToReturn ?? {
639
+ read: true
640
+ };
641
+ return roles;
642
+ },
643
+ getFirestoreCollection: c => c.app.mockItemCollection
644
+ });
645
+ const mockItemPrivateFirebaseModelServiceFactory = firebaseModelServiceFactory({
646
+ roleMapForModel: function (output, context, model) {
647
+ const roles = context.rolesToReturn ?? {
648
+ read: true
649
+ };
650
+ return roles;
651
+ },
652
+ getFirestoreCollection: c => c.app.mockItemPrivateCollectionGroup
653
+ });
654
+ const mockItemUserFirebaseModelServiceFactory = firebaseModelServiceFactory({
655
+ roleMapForModel: function (output, context, model) {
656
+ const isOwnerUser = context.auth?.uid === model.documentRef.id;
657
+ const roles = context.rolesToReturn ?? {
658
+ read: isOwnerUser
659
+ };
660
+ return roles;
661
+ },
662
+ getFirestoreCollection: c => c.app.mockItemUserCollectionGroup
663
+ });
664
+ const mockItemSubItemFirebaseModelServiceFactory = firebaseModelServiceFactory({
665
+ roleMapForModel: function (output, context, model) {
666
+ const roles = context.rolesToReturn ?? {
667
+ read: true
668
+ };
669
+ return roles;
670
+ },
671
+ getFirestoreCollection: c => c.app.mockItemSubItemCollectionGroup
672
+ });
673
+ const mockItemSubItemDeepFirebaseModelServiceFactory = firebaseModelServiceFactory({
674
+ roleMapForModel: function (output, context, model) {
675
+ const roles = context.rolesToReturn ?? {
676
+ read: true
677
+ };
678
+ return roles;
679
+ },
680
+ getFirestoreCollection: c => c.app.mockItemSubItemDeepCollectionGroup
681
+ });
682
+ const mockItemSystemStateFirebaseModelServiceFactory = firebaseModelServiceFactory({
683
+ roleMapForModel: function (output, context, model) {
684
+ return grantFullAccessIfAdmin(context); // only sys-admin allowed
685
+ },
686
+ getFirestoreCollection: c => c.app.mockItemSystemStateCollection
687
+ });
688
+ const MOCK_FIREBASE_MODEL_SERVICE_FACTORIES = {
689
+ systemState: mockItemSystemStateFirebaseModelServiceFactory,
690
+ mockItem: mockItemFirebaseModelServiceFactory,
691
+ mockItemPrivate: mockItemPrivateFirebaseModelServiceFactory,
692
+ mockItemUser: mockItemUserFirebaseModelServiceFactory,
693
+ mockItemSub: mockItemSubItemFirebaseModelServiceFactory,
694
+ mockItemSubItemDeep: mockItemSubItemDeepFirebaseModelServiceFactory
695
+ };
696
+ const mockFirebaseModelServices = firebaseModelsService(MOCK_FIREBASE_MODEL_SERVICE_FACTORIES);
697
+
698
+ // MARK: Test Item Testing Fixture
699
+ class MockItemCollectionFixtureInstance {
700
+ get collection() {
701
+ return this.mockItemCollection.collection;
702
+ }
703
+ /**
704
+ * @deprecated Use mockItemCollection instead.
705
+ */
706
+ get firestoreCollection() {
707
+ return this.collections.mockItemCollection;
708
+ }
709
+ get mockItemCollection() {
710
+ return this.collections.mockItemCollection;
711
+ }
712
+ get mockItemPrivateCollection() {
713
+ return this.collections.mockItemPrivateCollectionFactory;
714
+ }
715
+ get mockItemSubItemCollection() {
716
+ return this.collections.mockItemSubItemCollectionFactory;
717
+ }
718
+ get mockItemSubItemCollectionGroup() {
719
+ return this.collections.mockItemSubItemCollectionGroup;
720
+ }
721
+ get mockItemUserCollection() {
722
+ return this.collections.mockItemUserCollectionFactory;
723
+ }
724
+ get mockItemUserCollectionGroup() {
725
+ return this.collections.mockItemUserCollectionGroup;
726
+ }
727
+ get mockItemSubItemDeepCollection() {
728
+ return this.collections.mockItemSubItemDeepCollectionFactory;
729
+ }
730
+ get mockItemSubItemDeepCollectionGroup() {
731
+ return this.collections.mockItemSubItemDeepCollectionGroup;
732
+ }
733
+ get mockItemSystemState() {
734
+ return this.collections.mockItemSystemStateCollection;
735
+ }
736
+ constructor(fixture) {
737
+ this.fixture = void 0;
738
+ this.collections = void 0;
739
+ this.fixture = fixture;
740
+ this.collections = makeMockItemCollections(fixture.parent.firestoreContext);
741
+ }
742
+ }
743
+ /**
744
+ * Used to expose a CollectionReference to MockItem for simple tests.
745
+ */
746
+ class MockItemCollectionFixture extends AbstractWrappedFixtureWithInstance {}
747
+ function testWithMockItemCollectionFixture(config) {
748
+ return instanceWrapTestContextFactory({
749
+ wrapFixture: fixture => new MockItemCollectionFixture(fixture),
750
+ makeInstance: wrap => new MockItemCollectionFixtureInstance(wrap),
751
+ teardownInstance: instance => {}
752
+ // TODO(FUTURE): Utilize config here using the setup/teardown later if needed.
753
+ });
754
+ }
755
+
756
+ // MARK: Test Item Testing Fixture
757
+ class MockItemStorageFixtureInstance {
758
+ constructor(fixture) {
759
+ this.fixture = void 0;
760
+ this.fixture = fixture;
761
+ }
762
+ get storage() {
763
+ return this.fixture.parent.storage;
764
+ }
765
+ get storageContext() {
766
+ return this.fixture.parent.storageContext;
767
+ }
768
+ }
769
+ /**
770
+ * Used to expose a CollectionReference to MockItem for simple tests.
771
+ */
772
+ class MockItemStorageFixture extends AbstractWrappedFixtureWithInstance {
773
+ get storage() {
774
+ return this.instance.storage;
775
+ }
776
+ get storageContext() {
777
+ return this.instance.storageContext;
778
+ }
779
+ }
780
+ function testWithMockItemStorageFixture(config) {
781
+ return instanceWrapTestContextFactory({
782
+ wrapFixture: fixture => new MockItemStorageFixture(fixture),
783
+ makeInstance: wrap => new MockItemStorageFixtureInstance(wrap),
784
+ teardownInstance: instance => {}
785
+ // TODO(FUTURE): Utilize config here using the setup/teardown later if needed.
786
+ });
787
+ }
788
+
789
+ /**
790
+ * Convenience mock instance for collection tests within an authorized firebase context.
791
+ *
792
+ * Uses @firebase/firestore. This is ONLY for the client.
793
+ */
794
+ const authorizedTestWithMockItemCollection = testWithMockItemCollectionFixture()(authorizedFirebaseFactory);
795
+ /**
796
+ * Convenience mock instance for storage tests within an authorized firebase context.
797
+ *
798
+ * Uses @firebase/storage. This is ONLY for the client.
799
+ */
800
+ const authorizedTestWithMockItemStorage = testWithMockItemStorageFixture()(authorizedFirebaseFactory);
801
+
802
+ class TestFirestoreInstance {
803
+ constructor(firestoreContext) {
804
+ this.firestoreContext = void 0;
805
+ this.firestoreContext = firestoreContext;
806
+ }
807
+ get firestore() {
808
+ return this.firestoreContext.firestore;
809
+ }
810
+ }
811
+ class TestFirestoreContextFixture extends AbstractTestContextFixture {
812
+ get firestore() {
813
+ return this.instance.firestore;
814
+ }
815
+ get firestoreContext() {
816
+ return this.instance.firestoreContext;
817
+ }
818
+ }
819
+
820
+ function mockItemWithValue(value) {
821
+ return where('value', '==', value);
822
+ }
823
+ function mockItemWithTestValue(test) {
824
+ return where('test', '==', test);
825
+ }
826
+ /**
827
+ * This sorts all fields by their document ID, then filters in between two specific document id paths in order to only return values between a specific path.
828
+ *
829
+ * Visual Example:
830
+ *
831
+ * /a/b/c/c/a
832
+ * /a/b/c/d/A
833
+ * /a/b/c/d/B
834
+ * /a/b/c/d/C
835
+ * /a/b/c/e/a
836
+ *
837
+ * From:
838
+ * https://medium.com/firebase-developers/how-to-query-collections-in-firestore-under-a-certain-path-6a0d686cebd2
839
+ *
840
+ * @param parent
841
+ * @returns
842
+ */
843
+ function allChildMockItemSubItemDeepsWithinMockItem(mockItem) {
844
+ return allChildDocumentsUnderParent(mockItem);
845
+ }
846
+
847
+ /**
848
+ * Describes accessor driver tests, using a MockItemCollectionFixture.
849
+ *
850
+ * @param f
851
+ */
852
+ function describeFirestoreAccessorDriverTests(f) {
853
+ describe('FirestoreAccessorDriver', () => {
854
+ const testDocumentCount = 5;
855
+ let mockItemFirestoreDocumentAccessor;
856
+ let items;
857
+ beforeEach(async () => {
858
+ mockItemFirestoreDocumentAccessor = f.instance.firestoreCollection.documentAccessor();
859
+ items = await makeDocuments(f.instance.firestoreCollection.documentAccessor(), {
860
+ count: testDocumentCount,
861
+ init: i => {
862
+ return {
863
+ value: `${i}`,
864
+ test: true,
865
+ string: ''
866
+ };
867
+ }
868
+ });
869
+ });
870
+ describe('MockItem', () => {
871
+ let itemDocument;
872
+ beforeEach(() => {
873
+ itemDocument = items[0];
874
+ itemDocument.accessor;
875
+ });
876
+ describe('accessors', () => {
877
+ describeFirestoreDocumentAccessorTests(() => ({
878
+ context: f.parent.firestoreContext,
879
+ firestoreDocument: () => itemDocument,
880
+ dataForFirstOfTwoUpdates: () => ({
881
+ test: true,
882
+ tags: ['a']
883
+ }),
884
+ dataForUpdate: () => ({
885
+ test: false
886
+ }),
887
+ hasRemainingDataFromFirstOfTwoUpdate: data => (data.tags?.length || 0) > 0 && data.tags?.[0] === 'a',
888
+ hasDataFromUpdate: data => data.test === false,
889
+ loadDocumentForTransaction: (transaction, ref) => f.instance.firestoreCollection.documentAccessorForTransaction(transaction).loadDocument(ref),
890
+ loadDocumentForWriteBatch: (writeBatch, ref) => f.instance.firestoreCollection.documentAccessorForWriteBatch(writeBatch).loadDocument(ref)
891
+ }));
892
+ describe('increment()', () => {
893
+ it(`should increase the item's value`, async () => {
894
+ let data = await itemDocument.snapshotData();
895
+ expect(data?.number).toBe(undefined);
896
+ const update = {
897
+ number: 3
898
+ };
899
+ await itemDocument.increment(update);
900
+ data = await itemDocument.snapshotData();
901
+ expect(data?.number).toBe(update.number);
902
+ // increment again
903
+ await itemDocument.increment(update);
904
+ data = await itemDocument.snapshotData();
905
+ expect(data?.number).toBe(update.number * 2);
906
+ });
907
+ it(`should decrease the item's value`, async () => {
908
+ let data = await itemDocument.snapshotData();
909
+ expect(data?.number).toBe(undefined);
910
+ const update = {
911
+ number: -3
912
+ };
913
+ await itemDocument.increment(update);
914
+ data = await itemDocument.snapshotData();
915
+ expect(data?.number).toBe(update.number);
916
+ // increment again
917
+ await itemDocument.increment(update);
918
+ data = await itemDocument.snapshotData();
919
+ expect(data?.number).toBe(update.number * 2);
920
+ });
921
+ it(`should increase and decrease the item's value`, async () => {
922
+ let data = await itemDocument.snapshotData();
923
+ expect(data?.number).toBe(undefined);
924
+ const update = {
925
+ number: 3
926
+ };
927
+ await itemDocument.increment(update);
928
+ const update2 = {
929
+ number: -6
930
+ };
931
+ await itemDocument.increment(update2);
932
+ data = await itemDocument.snapshotData();
933
+ expect(data?.number).toBe(update.number + update2.number);
934
+ });
935
+ describe('in transaction', () => {
936
+ it(`should increase the item's value`, async () => {
937
+ const update = {
938
+ number: 3
939
+ };
940
+ await f.parent.firestoreContext.runTransaction(async transaction => {
941
+ const itemDocumentInTransaction = await f.instance.firestoreCollection.documentAccessorForTransaction(transaction).loadDocumentForId(itemDocument.id);
942
+ const data = await itemDocumentInTransaction.snapshotData();
943
+ expect(data?.number).toBe(undefined);
944
+ await itemDocumentInTransaction.increment(update);
945
+ });
946
+ const result = await itemDocument.snapshotData();
947
+ expect(result?.number).toBe(update.number);
948
+ });
949
+ });
950
+ describe('in write batch', () => {
951
+ it(`should increase the item's value`, async () => {
952
+ const update = {
953
+ number: 3
954
+ };
955
+ const writeBatch = f.parent.firestoreContext.batch();
956
+ const itemDocumentForWriteBatch = await f.instance.firestoreCollection.documentAccessorForWriteBatch(writeBatch).loadDocumentForId(itemDocument.id);
957
+ await itemDocumentForWriteBatch.increment(update);
958
+ await writeBatch.commit();
959
+ const result = await itemDocument.snapshotData();
960
+ expect(result?.number).toBe(update.number);
961
+ });
962
+ });
963
+ });
964
+ describe('arrayUpdate()', () => {
965
+ describe('union', () => {
966
+ it('should add to the array', async () => {
967
+ await itemDocument.accessor.update({
968
+ tags: ['a']
969
+ });
970
+ await itemDocument.arrayUpdate({
971
+ union: {
972
+ tags: ['b', 'c']
973
+ }
974
+ });
975
+ const result = await itemDocument.snapshotData();
976
+ expect(result?.tags).toEqual(['a', 'b', 'c']);
977
+ });
978
+ });
979
+ describe('remove', () => {
980
+ it('should remove from the array', async () => {
981
+ await itemDocument.accessor.update({
982
+ tags: ['a', 'b', 'c']
983
+ });
984
+ await itemDocument.arrayUpdate({
985
+ remove: {
986
+ tags: ['a', 'b']
987
+ }
988
+ });
989
+ const result = await itemDocument.snapshotData();
990
+ expect(result?.tags).toEqual(['c']);
991
+ });
992
+ });
993
+ });
994
+ });
995
+ describe('Subcollections', () => {
996
+ describe('singleItemFirestoreCollection (MockItemUser)', () => {
997
+ let testUserId;
998
+ let mockItemUserFirestoreCollection;
999
+ let itemUserDataDocument;
1000
+ beforeEach(() => {
1001
+ testUserId = 'userid' + Math.ceil(Math.random() * 100000);
1002
+ mockItemUserFirestoreCollection = f.instance.collections.mockItemUserCollectionFactory(itemDocument);
1003
+ itemUserDataDocument = mockItemUserFirestoreCollection.documentAccessor().loadDocumentForId(testUserId);
1004
+ itemUserDataDocument.accessor;
1005
+ });
1006
+ describe('create()', () => {
1007
+ describe('mockItemUserAccessorFactory usage', () => {
1008
+ it('should copy the documents identifier to the uid field on create.', async () => {
1009
+ await itemUserDataDocument.accessor.create({
1010
+ uid: '',
1011
+ // the mockItemUserAccessorFactory silently enforces the uid to be the same as the document.
1012
+ name: 'hello'
1013
+ });
1014
+ const snapshot = await itemUserDataDocument.accessor.get();
1015
+ expect(snapshot.data()?.uid).toBe(testUserId);
1016
+ });
1017
+ });
1018
+ });
1019
+ describe('set()', () => {
1020
+ describe('mockItemUserAccessorFactory usage', () => {
1021
+ it('should copy the documents identifier to the uid field on set.', async () => {
1022
+ await itemUserDataDocument.accessor.set({
1023
+ uid: '',
1024
+ // the mockItemUserAccessorFactory silently enforces the uid to be the same as the document.
1025
+ name: 'hello'
1026
+ });
1027
+ const snapshot = await itemUserDataDocument.accessor.get();
1028
+ expect(snapshot.data()?.uid).toBe(testUserId);
1029
+ });
1030
+ });
1031
+ });
1032
+ });
1033
+ describe('singleItemFirestoreCollection (MockItemPrivate)', () => {
1034
+ let mockItemPrivateFirestoreCollection;
1035
+ let itemPrivateDataDocument;
1036
+ let privateDataAccessor;
1037
+ let privateSub;
1038
+ beforeEach(() => {
1039
+ mockItemPrivateFirestoreCollection = f.instance.collections.mockItemPrivateCollectionFactory(itemDocument);
1040
+ itemPrivateDataDocument = mockItemPrivateFirestoreCollection.loadDocument();
1041
+ privateDataAccessor = itemPrivateDataDocument.accessor;
1042
+ privateSub = new SubscriptionObject();
1043
+ });
1044
+ afterEach(() => {
1045
+ privateSub.destroy();
1046
+ });
1047
+ describe('singleItemFirestoreCollection accessor', () => {
1048
+ it('should implement FirestoreSingleDocumentAccessor', () => {
1049
+ expect(mockItemPrivateFirestoreCollection.singleItemIdentifier).toBeDefined();
1050
+ expect(mockItemPrivateFirestoreCollection.documentRef).toBeDefined();
1051
+ expect(mockItemPrivateFirestoreCollection.loadDocument).toBeDefined();
1052
+ expect(mockItemPrivateFirestoreCollection.loadDocumentForTransaction).toBeDefined();
1053
+ expect(mockItemPrivateFirestoreCollection.loadDocumentForWriteBatch).toBeDefined();
1054
+ });
1055
+ });
1056
+ describe('get()', () => {
1057
+ it('should read that data using the configured converter', async () => {
1058
+ await itemPrivateDataDocument.accessor.set({
1059
+ values: null
1060
+ });
1061
+ const dataWithoutConverter = (await itemPrivateDataDocument.accessor.getWithConverter(null)).data();
1062
+ expect(dataWithoutConverter).toBeDefined();
1063
+ expect(dataWithoutConverter.values).toBeNull();
1064
+ // converter on client, _converter on server
1065
+ expect(itemPrivateDataDocument.documentRef.converter ?? itemPrivateDataDocument.documentRef._converter).toBeDefined();
1066
+ const data = await itemPrivateDataDocument.snapshotData();
1067
+ expect(data?.values).toBeDefined();
1068
+ expect(data?.values).not.toBeNull(); // should not be null due to the snapshot converter config
1069
+ });
1070
+ });
1071
+ describe('getWithConverter()', () => {
1072
+ it('should get the results with the input converter', async () => {
1073
+ await itemPrivateDataDocument.accessor.set({
1074
+ values: null
1075
+ });
1076
+ const data = await itemPrivateDataDocument.snapshotData();
1077
+ expect(data?.values).toBeDefined();
1078
+ const dataWithoutConverter = (await itemPrivateDataDocument.accessor.getWithConverter(null)).data();
1079
+ expect(dataWithoutConverter).toBeDefined();
1080
+ expect(dataWithoutConverter.values).toBeNull();
1081
+ });
1082
+ it('should get the results with the input converter with a type', async () => {
1083
+ await itemPrivateDataDocument.accessor.set({
1084
+ values: null
1085
+ });
1086
+ const data = await itemPrivateDataDocument.snapshotData();
1087
+ expect(data?.values).toBeDefined();
1088
+ const converter = mockItemConverter;
1089
+ const dataWithoutConverter = await itemPrivateDataDocument.accessor.getWithConverter(converter);
1090
+ expect(dataWithoutConverter).toBeDefined();
1091
+ });
1092
+ });
1093
+ describe('update()', () => {
1094
+ itShouldFail('if the item does not exist', async () => {
1095
+ const exists = await itemPrivateDataDocument.accessor.exists();
1096
+ expect(exists).toBe(false);
1097
+ await expectFail(() => itemPrivateDataDocument.update({
1098
+ createdAt: new Date()
1099
+ }));
1100
+ });
1101
+ it('should update the item if it exist', async () => {
1102
+ await itemPrivateDataDocument.create({
1103
+ createdAt: new Date(),
1104
+ num: 0,
1105
+ values: [],
1106
+ settings: {
1107
+ test: {
1108
+ north: true,
1109
+ south: true
1110
+ }
1111
+ }
1112
+ });
1113
+ const newDate = new Date(0);
1114
+ const exists = await itemPrivateDataDocument.accessor.exists();
1115
+ expect(exists).toBe(true);
1116
+ await itemPrivateDataDocument.update({
1117
+ createdAt: newDate
1118
+ });
1119
+ const data = await itemPrivateDataDocument.snapshotData();
1120
+ expect(data?.createdAt.getTime()).toBe(newDate.getTime());
1121
+ // check was not modified
1122
+ expect(data?.settings['test'].north).toBe(true);
1123
+ expect(data?.settings['test'].south).toBe(true);
1124
+ expect(data?.settings['test'].east).toBeUndefined();
1125
+ expect(data?.settings['test'].west).toBeUndefined();
1126
+ });
1127
+ });
1128
+ describe('set()', () => {
1129
+ it('should create the item', async () => {
1130
+ let exists = await privateDataAccessor.exists();
1131
+ expect(exists).toBe(false);
1132
+ const createdAt = new Date();
1133
+ const settings = {
1134
+ test: {
1135
+ north: true
1136
+ }
1137
+ };
1138
+ await privateDataAccessor.set({
1139
+ values: [],
1140
+ num: 0,
1141
+ createdAt,
1142
+ settings
1143
+ });
1144
+ exists = await privateDataAccessor.exists();
1145
+ expect(exists).toBe(true);
1146
+ const getResult = await privateDataAccessor.get();
1147
+ const data = getResult.data();
1148
+ expect(data).toBeDefined();
1149
+ expect(data?.num).toBe(0);
1150
+ expect(data?.values).toEqual([]);
1151
+ expect(data?.createdAt).toBeInstanceOf(Date);
1152
+ expect(data?.createdAt.toISOString()).toBe(createdAt.toISOString());
1153
+ expect(data?.settings).toEqual(settings);
1154
+ });
1155
+ });
1156
+ describe('with item', () => {
1157
+ beforeEach(async () => {
1158
+ await privateDataAccessor.set({
1159
+ num: 0,
1160
+ values: [],
1161
+ createdAt: new Date(),
1162
+ settings: {}
1163
+ });
1164
+ });
1165
+ describe('increment()', () => {
1166
+ it(`should increase the item's value`, async () => {
1167
+ let data = await itemPrivateDataDocument.snapshotData();
1168
+ expect(data?.num).toBe(0);
1169
+ const update = {
1170
+ num: 3
1171
+ };
1172
+ await itemPrivateDataDocument.increment(update);
1173
+ data = await itemPrivateDataDocument.snapshotData();
1174
+ expect(data?.num).toBe(update.num);
1175
+ });
1176
+ });
1177
+ describe('accessors', () => {
1178
+ const TEST_COMMENTS = 'test';
1179
+ describeFirestoreDocumentAccessorTests(() => ({
1180
+ context: f.parent.firestoreContext,
1181
+ firestoreDocument: () => itemPrivateDataDocument,
1182
+ dataForFirstOfTwoUpdates: () => ({
1183
+ comments: 'not_test_comments',
1184
+ values: ['a']
1185
+ }),
1186
+ hasRemainingDataFromFirstOfTwoUpdate: data => data.values.length > 0 && data.values[0] === 'a',
1187
+ dataForUpdate: () => ({
1188
+ comments: TEST_COMMENTS
1189
+ }),
1190
+ hasDataFromUpdate: data => data.comments === TEST_COMMENTS,
1191
+ loadDocumentForTransaction: (transaction, ref) => mockItemPrivateFirestoreCollection.loadDocumentForTransaction(transaction),
1192
+ loadDocumentForWriteBatch: (writeBatch, ref) => mockItemPrivateFirestoreCollection.loadDocumentForWriteBatch(writeBatch)
1193
+ }));
1194
+ });
1195
+ });
1196
+ });
1197
+ describe('MockItemSubItem', () => {
1198
+ let subItemDocument;
1199
+ beforeEach(async () => {
1200
+ subItemDocument = f.instance.collections.mockItemSubItemCollectionFactory(itemDocument).documentAccessor().newDocument();
1201
+ await subItemDocument.accessor.set({
1202
+ value: 0
1203
+ });
1204
+ });
1205
+ describe('firestoreCollectionWithParent (MockItemSubItem)', () => {
1206
+ let mockItemSubItemFirestoreCollection;
1207
+ beforeEach(() => {
1208
+ mockItemSubItemFirestoreCollection = f.instance.collections.mockItemSubItemCollectionFactory(itemDocument);
1209
+ });
1210
+ describe('with item', () => {
1211
+ describe('accessors', () => {
1212
+ const TEST_VALUE = 1234;
1213
+ describeFirestoreDocumentAccessorTests(() => ({
1214
+ context: f.parent.firestoreContext,
1215
+ firestoreDocument: () => subItemDocument,
1216
+ dataForFirstOfTwoUpdates: () => ({
1217
+ value: TEST_VALUE - 10
1218
+ }),
1219
+ dataForUpdate: () => ({
1220
+ value: TEST_VALUE
1221
+ }),
1222
+ hasDataFromUpdate: data => data.value === TEST_VALUE,
1223
+ loadDocumentForTransaction: (transaction, ref) => mockItemSubItemFirestoreCollection.documentAccessorForTransaction(transaction).loadDocument(ref),
1224
+ loadDocumentForWriteBatch: (writeBatch, ref) => mockItemSubItemFirestoreCollection.documentAccessorForWriteBatch(writeBatch).loadDocument(ref)
1225
+ }));
1226
+ });
1227
+ });
1228
+ });
1229
+ describe('firestoreCollectionGroup (MockItemSubItem)', () => {
1230
+ let mockItemSubItemFirestoreCollectionGroup;
1231
+ beforeEach(() => {
1232
+ mockItemSubItemFirestoreCollectionGroup = f.instance.collections.mockItemSubItemCollectionGroup;
1233
+ });
1234
+ describe('with item', () => {
1235
+ describe('accessors', () => {
1236
+ const TEST_VALUE = 1234;
1237
+ describeFirestoreDocumentAccessorTests(() => ({
1238
+ context: f.parent.firestoreContext,
1239
+ firestoreDocument: () => subItemDocument,
1240
+ dataForFirstOfTwoUpdates: () => ({
1241
+ value: TEST_VALUE - 10
1242
+ }),
1243
+ dataForUpdate: () => ({
1244
+ value: TEST_VALUE
1245
+ }),
1246
+ hasDataFromUpdate: data => data.value === TEST_VALUE,
1247
+ loadDocumentForTransaction: (transaction, ref) => mockItemSubItemFirestoreCollectionGroup.documentAccessorForTransaction(transaction).loadDocument(ref),
1248
+ loadDocumentForWriteBatch: (writeBatch, ref) => mockItemSubItemFirestoreCollectionGroup.documentAccessorForWriteBatch(writeBatch).loadDocument(ref)
1249
+ }));
1250
+ });
1251
+ });
1252
+ });
1253
+ });
1254
+ });
1255
+ });
1256
+ describe('documentAccessor()', () => {
1257
+ describe('loadDocumentForKey()', () => {
1258
+ it('should load an existing document from the path.', async () => {
1259
+ const document = mockItemFirestoreDocumentAccessor.loadDocumentForKey(items[0].key);
1260
+ const exists = await document.accessor.exists();
1261
+ expect(exists).toBe(true);
1262
+ });
1263
+ itShouldFail('if the path is invalid (points to collection)', () => {
1264
+ expectFail(() => {
1265
+ mockItemFirestoreDocumentAccessor.loadDocumentForKey('path');
1266
+ });
1267
+ });
1268
+ itShouldFail('if the path points to a different type/collection', () => {
1269
+ expectFail(() => {
1270
+ mockItemFirestoreDocumentAccessor.loadDocumentForKey('path/id');
1271
+ });
1272
+ });
1273
+ itShouldFail('if the path is empty.', () => {
1274
+ expectFail(() => {
1275
+ mockItemFirestoreDocumentAccessor.loadDocumentForKey('');
1276
+ });
1277
+ });
1278
+ itShouldFail('if the path is undefined.', () => {
1279
+ expectFail(() => {
1280
+ mockItemFirestoreDocumentAccessor.loadDocumentForKey(undefined);
1281
+ });
1282
+ });
1283
+ itShouldFail('if the path is null.', () => {
1284
+ expectFail(() => {
1285
+ mockItemFirestoreDocumentAccessor.loadDocumentForKey(null);
1286
+ });
1287
+ });
1288
+ });
1289
+ describe('loadDocumentForId()', () => {
1290
+ it('should return a document with the given id.', () => {
1291
+ const document = mockItemFirestoreDocumentAccessor.loadDocumentForId('id');
1292
+ expect(document).toBeDefined();
1293
+ });
1294
+ itShouldFail('if the id is empty.', () => {
1295
+ expectFail(() => {
1296
+ mockItemFirestoreDocumentAccessor.loadDocumentForId('');
1297
+ });
1298
+ });
1299
+ itShouldFail('if the id is undefined.', () => {
1300
+ expectFail(() => {
1301
+ mockItemFirestoreDocumentAccessor.loadDocumentForId(undefined);
1302
+ });
1303
+ });
1304
+ });
1305
+ });
1306
+ });
1307
+ }
1308
+ function describeFirestoreDocumentAccessorTests(init) {
1309
+ let c;
1310
+ let sub;
1311
+ let firestoreDocument;
1312
+ let accessor;
1313
+ beforeEach(() => {
1314
+ sub = new SubscriptionObject();
1315
+ c = init();
1316
+ firestoreDocument = c.firestoreDocument();
1317
+ accessor = firestoreDocument.accessor;
1318
+ });
1319
+ afterEach(() => {
1320
+ sub.destroy();
1321
+ });
1322
+ describe('utilities', () => {
1323
+ describe('getDocumentSnapshotPairs()', () => {
1324
+ it('should return the document and snapshot pairs for the input.', async () => {
1325
+ const pairs = await getDocumentSnapshotPairs([firestoreDocument]);
1326
+ expect(pairs.length).toBe(1);
1327
+ expect(pairs[0]).toBeDefined();
1328
+ expect(pairs[0].document).toBe(firestoreDocument);
1329
+ expect(pairs[0].snapshot).toBeDefined();
1330
+ expect(pairs[0].snapshot.data()).toBeDefined();
1331
+ });
1332
+ });
1333
+ describe('useDocumentSnapshot()', () => {
1334
+ it(`should use the input document value if it exists`, async () => {
1335
+ const exists = await firestoreDocument.exists();
1336
+ expect(exists).toBe(true);
1337
+ let snapshotUsed = false;
1338
+ await useDocumentSnapshot(firestoreDocument, snapshot => {
1339
+ expect(snapshot).toBeDefined();
1340
+ snapshotUsed = true;
1341
+ });
1342
+ expect(snapshotUsed).toBe(true);
1343
+ });
1344
+ it(`should not use the input undefined value`, async () => {
1345
+ let snapshotUsed = false;
1346
+ await useDocumentSnapshot(undefined, snapshot => {
1347
+ expect(snapshot).toBeDefined();
1348
+ snapshotUsed = true;
1349
+ });
1350
+ expect(snapshotUsed).toBe(false);
1351
+ });
1352
+ });
1353
+ describe('useDocumentSnapshotData()', () => {
1354
+ it(`should use the input document's snapshot data if it exists`, async () => {
1355
+ const exists = await firestoreDocument.exists();
1356
+ expect(exists).toBe(true);
1357
+ let snapshotUsed = false;
1358
+ await useDocumentSnapshotData(firestoreDocument, data => {
1359
+ expect(data).toBeDefined();
1360
+ snapshotUsed = true;
1361
+ });
1362
+ expect(snapshotUsed).toBe(true);
1363
+ });
1364
+ });
1365
+ });
1366
+ describe('AbstractFirestoreDocument', () => {
1367
+ describe('snapshot()', () => {
1368
+ it('should return the snapshot.', async () => {
1369
+ const snapshot = await firestoreDocument.snapshot();
1370
+ expect(snapshot).toBeDefined();
1371
+ });
1372
+ });
1373
+ describe('snapshotData()', () => {
1374
+ it('should return the snapshot data if the model exists.', async () => {
1375
+ const exists = await firestoreDocument.exists();
1376
+ expect(exists).toBe(true);
1377
+ const data = await firestoreDocument.snapshotData();
1378
+ expect(data).toBeDefined();
1379
+ });
1380
+ it('should return the undefined if the model does not exist.', async () => {
1381
+ await accessor.delete();
1382
+ const exists = await firestoreDocument.exists();
1383
+ expect(exists).toBe(false);
1384
+ const data = await firestoreDocument.snapshotData();
1385
+ expect(data).toBeUndefined();
1386
+ });
1387
+ });
1388
+ describe('create()', () => {
1389
+ it('should create the document if it does not exist.', async () => {
1390
+ const snapshot = await firestoreDocument.snapshot();
1391
+ await accessor.delete();
1392
+ let exists = await firestoreDocument.exists();
1393
+ expect(exists).toBe(false);
1394
+ await firestoreDocument.create(snapshot.data());
1395
+ exists = await firestoreDocument.exists();
1396
+ expect(exists).toBe(true);
1397
+ });
1398
+ itShouldFail('if the document exists.', async () => {
1399
+ const snapshot = await firestoreDocument.snapshot();
1400
+ const exists = await firestoreDocument.exists();
1401
+ expect(exists).toBe(true);
1402
+ await expectFail(() => firestoreDocument.create(snapshot.data()));
1403
+ });
1404
+ });
1405
+ describe('update()', () => {
1406
+ it('should update the data if the document exists.', async () => {
1407
+ const data = c.dataForUpdate();
1408
+ await firestoreDocument.update(data);
1409
+ const snapshot = await firestoreDocument.snapshot();
1410
+ expect(c.hasDataFromUpdate(snapshot.data())).toBe(true);
1411
+ });
1412
+ itShouldFail('if the document does not exist.', async () => {
1413
+ await accessor.delete();
1414
+ const snapshot = await firestoreDocument.snapshot();
1415
+ expect(snapshot.data()).toBe(undefined);
1416
+ const exists = await firestoreDocument.exists();
1417
+ expect(exists).toBe(false);
1418
+ await expectFail(() => firestoreDocument.update(c.dataForUpdate()));
1419
+ });
1420
+ it('should not throw an error if the input update data is empty.', async () => {
1421
+ await firestoreDocument.update({});
1422
+ });
1423
+ });
1424
+ describe('transaction', () => {
1425
+ describe('stream$', () => {
1426
+ it('should not cause the transaction to fail if the document is loaded after changes have begun.', async () => {
1427
+ await c.context.runTransaction(async transaction => {
1428
+ const transactionDocument = await c.loadDocumentForTransaction(transaction, firestoreDocument.documentRef);
1429
+ const currentData = await transactionDocument.snapshotData();
1430
+ expect(currentData).toBeDefined();
1431
+ const data = c.dataForUpdate();
1432
+ await transactionDocument.update(data);
1433
+ // stream$ and data$ do not call stream() until called directly.
1434
+ const secondLoading = await c.loadDocumentForTransaction(transaction, firestoreDocument.documentRef);
1435
+ expect(secondLoading).toBeDefined();
1436
+ });
1437
+ });
1438
+ itShouldFail('if stream$ is called after an update has occured in the transaction', async () => {
1439
+ await expectFail(() => c.context.runTransaction(async transaction => {
1440
+ const transactionDocument = await c.loadDocumentForTransaction(transaction, firestoreDocument.documentRef);
1441
+ const currentData = await transactionDocument.snapshotData();
1442
+ expect(currentData).toBeDefined();
1443
+ const data = c.dataForUpdate();
1444
+ await transactionDocument.update(data);
1445
+ // read the stream using a promise so the error is captured
1446
+ await firstValueFrom(c.loadDocumentForTransaction(transaction, firestoreDocument.documentRef).stream$);
1447
+ }));
1448
+ });
1449
+ });
1450
+ describe('update()', () => {
1451
+ it('should update the data if the document exists.', async () => {
1452
+ await c.context.runTransaction(async transaction => {
1453
+ const transactionDocument = await c.loadDocumentForTransaction(transaction, firestoreDocument.documentRef);
1454
+ const currentData = await transactionDocument.snapshotData();
1455
+ expect(currentData).toBeDefined();
1456
+ const data = c.dataForUpdate();
1457
+ await transactionDocument.update(data);
1458
+ });
1459
+ const snapshot = await firestoreDocument.snapshot();
1460
+ expect(c.hasDataFromUpdate(snapshot.data())).toBe(true);
1461
+ });
1462
+ describe('multiple updates', () => {
1463
+ it('should merge the updates together and override the values from the first update that are defined in the second update', async () => {
1464
+ await c.context.runTransaction(async transaction => {
1465
+ const transactionDocument = await c.loadDocumentForTransaction(transaction, firestoreDocument.documentRef);
1466
+ const currentData = await transactionDocument.snapshotData();
1467
+ expect(currentData).toBeDefined();
1468
+ const firstData = c.dataForFirstOfTwoUpdates();
1469
+ await transactionDocument.update(firstData);
1470
+ const data = c.dataForUpdate();
1471
+ await transactionDocument.update(data);
1472
+ });
1473
+ const snapshot = await firestoreDocument.snapshot();
1474
+ expect(c.hasDataFromUpdate(snapshot.data())).toBe(true);
1475
+ if (c.hasRemainingDataFromFirstOfTwoUpdate != null) {
1476
+ expect(c.hasRemainingDataFromFirstOfTwoUpdate(snapshot.data())).toBe(true);
1477
+ }
1478
+ });
1479
+ });
1480
+ });
1481
+ });
1482
+ describe('write batch', () => {
1483
+ describe('update()', () => {
1484
+ it('should update the data if the document exists.', async () => {
1485
+ const batch = c.context.batch();
1486
+ const batchDocument = await c.loadDocumentForWriteBatch(batch, firestoreDocument.documentRef);
1487
+ const data = c.dataForUpdate();
1488
+ await batchDocument.update(data);
1489
+ await batch.commit();
1490
+ const snapshot = await firestoreDocument.snapshot();
1491
+ expect(c.hasDataFromUpdate(snapshot.data())).toBe(true);
1492
+ });
1493
+ });
1494
+ });
1495
+ });
1496
+ describe('accessor', () => {
1497
+ describe('stream()', () => {
1498
+ it('should return a snapshot stream', async () => {
1499
+ const result = await accessor.stream();
1500
+ expect(result).toBeDefined();
1501
+ });
1502
+ it('should emit values on updates from the observable.', callbackTest(done => {
1503
+ let count = 0;
1504
+ sub.subscription = accessor.stream().subscribe(item => {
1505
+ count += 1;
1506
+ if (count === 1) {
1507
+ expect(c.hasDataFromUpdate(item.data())).toBe(false);
1508
+ } else if (count === 2) {
1509
+ expect(c.hasDataFromUpdate(item.data())).toBe(true);
1510
+ done();
1511
+ }
1512
+ });
1513
+ setTimeout(() => {
1514
+ accessor.update(c.dataForUpdate());
1515
+ }, 100);
1516
+ }));
1517
+ describe('in transition context', () => {
1518
+ let runTransaction;
1519
+ beforeEach(() => {
1520
+ runTransaction = c.context.runTransaction;
1521
+ });
1522
+ it('should return the first emitted value (observable completes immediately)', async () => {
1523
+ await runTransaction(async transaction => {
1524
+ const transactionItemDocument = c.loadDocumentForTransaction(transaction, accessor.documentRef);
1525
+ // load the value
1526
+ const value = await firstValueFrom(transactionItemDocument.accessor.stream());
1527
+ expect(value).toBeDefined();
1528
+ // set to make the transaction valid
1529
+ await transactionItemDocument.accessor.set({
1530
+ value: 0
1531
+ }, {
1532
+ merge: true
1533
+ });
1534
+ return value;
1535
+ });
1536
+ });
1537
+ });
1538
+ describe('in batch context', () => {
1539
+ it('should return the first emitted value (observable completes immediately)', async () => {
1540
+ const writeBatch = c.context.batch();
1541
+ const batchItemDocument = c.loadDocumentForWriteBatch(writeBatch, accessor.documentRef);
1542
+ // load the value
1543
+ const value = await firstValueFrom(batchItemDocument.accessor.stream());
1544
+ expect(value).toBeDefined();
1545
+ // set to make the batch changes valid
1546
+ await batchItemDocument.accessor.set({
1547
+ value: 0
1548
+ }, {
1549
+ merge: true
1550
+ });
1551
+ // commit the changes
1552
+ await writeBatch.commit();
1553
+ });
1554
+ });
1555
+ });
1556
+ describe('create()', () => {
1557
+ it('should create the document if it does not exist.', async () => {
1558
+ const snapshot = await accessor.get();
1559
+ await accessor.delete();
1560
+ let exists = await accessor.exists();
1561
+ expect(exists).toBe(false);
1562
+ await accessor.create(snapshot.data());
1563
+ exists = await accessor.exists();
1564
+ expect(exists).toBe(true);
1565
+ });
1566
+ itShouldFail('if the document exists.', async () => {
1567
+ const snapshot = await accessor.get();
1568
+ const exists = await accessor.exists();
1569
+ expect(exists).toBe(true);
1570
+ await expectFail(() => accessor.create(snapshot.data()));
1571
+ });
1572
+ });
1573
+ describe('get()', () => {
1574
+ it('should return a snapshot', async () => {
1575
+ const result = await accessor.get();
1576
+ expect(result).toBeDefined();
1577
+ expect(result.id).toBeDefined();
1578
+ });
1579
+ });
1580
+ describe('exists()', () => {
1581
+ it('should return true if the document exists', async () => {
1582
+ const exists = await accessor.exists();
1583
+ expect(exists).toBe(true);
1584
+ });
1585
+ it('should return false if the document does not exist', async () => {
1586
+ await accessor.delete();
1587
+ const exists = await accessor.exists();
1588
+ expect(exists).toBe(false);
1589
+ });
1590
+ });
1591
+ describe('update()', () => {
1592
+ it('should update the data if the document exists.', async () => {
1593
+ const data = c.dataForUpdate();
1594
+ await accessor.update(data);
1595
+ const snapshot = await accessor.get();
1596
+ expect(c.hasDataFromUpdate(snapshot.data())).toBe(true);
1597
+ });
1598
+ itShouldFail('if the document does not exist.', async () => {
1599
+ await accessor.delete();
1600
+ const snapshot = await accessor.get();
1601
+ expect(snapshot.data()).toBe(undefined);
1602
+ const exists = await accessor.exists();
1603
+ expect(exists).toBe(false);
1604
+ await expectFail(() => accessor.update(c.dataForUpdate()));
1605
+ });
1606
+ itShouldFail('if the input is an empty object.', async () => {
1607
+ await expectFail(() => accessor.update({}));
1608
+ });
1609
+ // TODO(TEST): test that update does not call the converter when setting values.
1610
+ });
1611
+ describe('set()', () => {
1612
+ it('should create the object if it does not exist.', async () => {
1613
+ await accessor.delete();
1614
+ let exists = await accessor.exists();
1615
+ expect(exists).toBe(false);
1616
+ const data = c.dataForUpdate();
1617
+ await accessor.set(data);
1618
+ exists = await accessor.exists();
1619
+ expect(exists).toBe(true);
1620
+ const snapshot = await accessor.get();
1621
+ expect(c.hasDataFromUpdate(snapshot.data())).toBe(true);
1622
+ });
1623
+ it('should update the data on the document for fields that are not undefined.', async () => {
1624
+ const data = c.dataForUpdate();
1625
+ await accessor.set(data);
1626
+ const snapshot = await accessor.get();
1627
+ expect(c.hasDataFromUpdate(snapshot.data())).toBe(true);
1628
+ });
1629
+ describe('merge=true', () => {
1630
+ it('should update the data if the document exists.', async () => {
1631
+ const data = c.dataForUpdate();
1632
+ await accessor.set(data, {
1633
+ merge: true
1634
+ });
1635
+ const snapshot = await accessor.get();
1636
+ expect(c.hasDataFromUpdate(snapshot.data())).toBe(true);
1637
+ });
1638
+ it('should succeed if the document does not exist.', async () => {
1639
+ await accessor.delete();
1640
+ let snapshot = await accessor.get();
1641
+ expect(snapshot.data()).toBe(undefined);
1642
+ const exists = await accessor.exists();
1643
+ expect(exists).toBe(false);
1644
+ await accessor.set(c.dataForUpdate(), {
1645
+ merge: true
1646
+ });
1647
+ snapshot = await accessor.get();
1648
+ expect(c.hasDataFromUpdate(snapshot.data())).toBe(true);
1649
+ });
1650
+ });
1651
+ // TODO(TEST): test that set calls the converter when setting values.
1652
+ });
1653
+ describe('delete()', () => {
1654
+ it('should delete the document.', async () => {
1655
+ await accessor.delete();
1656
+ const snapshot = await accessor.get();
1657
+ expect(snapshot.data()).toBe(undefined);
1658
+ const exists = await accessor.exists();
1659
+ expect(exists).toBe(false);
1660
+ });
1661
+ });
1662
+ });
1663
+ }
1664
+
1665
+ /**
1666
+ * Describes query driver tests, using a MockItemCollectionFixture.
1667
+ *
1668
+ * @param f
1669
+ */
1670
+ function describeFirestoreQueryDriverTests(f) {
1671
+ describe('FirestoreQueryDriver', () => {
1672
+ const testDocumentCount = 5;
1673
+ let items;
1674
+ const startDate = addDays(startOfDay(new Date()), 1);
1675
+ const EVEN_TAG = 'even';
1676
+ const ODD_TAG = 'odd';
1677
+ beforeEach(async () => {
1678
+ items = await makeDocuments(f.instance.firestoreCollection.documentAccessor(), {
1679
+ count: testDocumentCount,
1680
+ init: i => {
1681
+ return {
1682
+ value: `${i}`,
1683
+ number: i,
1684
+ date: addHours(startDate, i),
1685
+ tags: [`${i}`, `${isEvenNumber(i) ? EVEN_TAG : ODD_TAG}`],
1686
+ test: true
1687
+ };
1688
+ }
1689
+ });
1690
+ });
1691
+ describe('firestoreIdBatchVerifierFactory()', () => {
1692
+ const mockItemIdBatchVerifier = firestoreIdBatchVerifierFactory({
1693
+ readKeys: x => [x.id],
1694
+ fieldToQuery: '_id'
1695
+ });
1696
+ it('should query on the id field.', async () => {
1697
+ const takenIds = items.map(x => x.id);
1698
+ const result = await f.instance.mockItemCollection.queryDocument(whereDocumentId('in', takenIds)).getDocs();
1699
+ expect(result).toBeDefined();
1700
+ expect(result.length).toBe(takenIds.length);
1701
+ expect(result.map(x => x.id)).toContain(takenIds[0]);
1702
+ });
1703
+ it('should return ids that are not taken.', async () => {
1704
+ const takenIds = items.map(x => x.id);
1705
+ const idFactory = arrayFactory(mapGetter(randomNumberFactory(10000000), x => `test-id-${x}`));
1706
+ const random = randomFromArrayFactory(takenIds);
1707
+ const factory = idBatchFactory({
1708
+ verifier: mockItemIdBatchVerifier(f.instance.mockItemCollection),
1709
+ factory: count => {
1710
+ const ids = [random(), ...idFactory(count)];
1711
+ return ids;
1712
+ }
1713
+ });
1714
+ const idsToMake = 30;
1715
+ const result = await factory(idsToMake);
1716
+ expect(result).toBeDefined();
1717
+ expect(unique(result).length).toBe(idsToMake);
1718
+ expect(unique(result, takenIds).length).toBe(idsToMake);
1719
+ });
1720
+ });
1721
+ describe('mockItemUser', () => {
1722
+ let testUserId;
1723
+ let allMockUserItems;
1724
+ beforeEach(async () => {
1725
+ testUserId = 'userid' + Math.ceil(Math.random() * 100000);
1726
+ const results = await Promise.all(items.map(parent => makeDocuments(f.instance.mockItemUserCollection(parent).documentAccessor(), {
1727
+ count: 1,
1728
+ newDocument: x => x.loadDocumentForId(testUserId),
1729
+ init: i => {
1730
+ return {
1731
+ uid: '',
1732
+ name: `name ${i}`
1733
+ };
1734
+ }
1735
+ })));
1736
+ allMockUserItems = results.flat();
1737
+ });
1738
+ describe('utils', () => {
1739
+ describe('iterate load firestore utilities', () => {
1740
+ describe('loadAllFirestoreDocumentSnapshotPairs()', () => {
1741
+ it('should iterate batches of snapshot pairs.', async () => {
1742
+ const documentAccessor = f.instance.mockItemUserCollectionGroup.documentAccessor();
1743
+ const mockUserItemsVisited = new Set();
1744
+ const result = await loadAllFirestoreDocumentSnapshotPairs({
1745
+ documentAccessor,
1746
+ iterateSnapshotPairsBatch: async x => {
1747
+ x.forEach(y => mockUserItemsVisited.add(y.document.key));
1748
+ const pair = x[0];
1749
+ expect(pair.data).toBeDefined();
1750
+ expect(pair.snapshot).toBeDefined();
1751
+ expect(pair.document).toBeDefined();
1752
+ },
1753
+ queryFactory: f.instance.mockItemUserCollectionGroup,
1754
+ constraintsFactory: [] // no constraints
1755
+ });
1756
+ expect(result.totalSnapshotsVisited).toBe(allMockUserItems.length);
1757
+ expect(mockUserItemsVisited.size).toBe(allMockUserItems.length);
1758
+ expect(result.snapshotPairs.length).toBe(allMockUserItems.length);
1759
+ expect(result.snapshotPairs[0].data).toBeDefined();
1760
+ expect(result.snapshotPairs[0].document).toBeDefined();
1761
+ expect(result.snapshotPairs[0].snapshot).toBeDefined();
1762
+ });
1763
+ });
1764
+ describe('loadAllFirestoreDocumentSnapshot()', () => {
1765
+ it('should iterate batches of snapshot pairs.', async () => {
1766
+ const mockUserItemsVisited = new Set();
1767
+ const result = await loadAllFirestoreDocumentSnapshot({
1768
+ iterateSnapshotsForCheckpoint: async x => {
1769
+ x.forEach(y => mockUserItemsVisited.add(y.ref.path));
1770
+ const snapshot = x[0];
1771
+ expect(snapshot.ref).toBeDefined();
1772
+ expect(snapshot.data()).toBeDefined();
1773
+ },
1774
+ queryFactory: f.instance.mockItemUserCollectionGroup,
1775
+ constraintsFactory: [] // no constraints
1776
+ });
1777
+ expect(result.totalSnapshotsVisited).toBe(allMockUserItems.length);
1778
+ expect(mockUserItemsVisited.size).toBe(allMockUserItems.length);
1779
+ expect(result.snapshots.length).toBe(allMockUserItems.length);
1780
+ expect(result.snapshots[0].ref).toBeDefined();
1781
+ expect(result.snapshots[0].data()).toBeDefined();
1782
+ });
1783
+ });
1784
+ });
1785
+ describe('iterate firestore utilities', () => {
1786
+ describe('iterateFirestoreDocumentSnapshotPairs()', () => {
1787
+ it('should iterate across all mock users by each snapshot pair.', async () => {
1788
+ const documentAccessor = f.instance.mockItemUserCollectionGroup.documentAccessor();
1789
+ const mockUserItemsVisited = new Set();
1790
+ const batchSize = 2;
1791
+ const result = await iterateFirestoreDocumentSnapshotPairs({
1792
+ batchSize,
1793
+ handleRepeatCursor: false,
1794
+ // exit immediately if the cursor is visited again
1795
+ filterCheckpointSnapshots: async x => {
1796
+ return x;
1797
+ },
1798
+ iterateSnapshotPair: async x => {
1799
+ expect(x.data).toBeDefined();
1800
+ expect(x.snapshot).toBeDefined();
1801
+ expect(x.document).toBeDefined();
1802
+ const key = x.document.key;
1803
+ if (mockUserItemsVisited.has(key)) {
1804
+ throw new Error('encountered repeat key');
1805
+ } else {
1806
+ mockUserItemsVisited.add(key);
1807
+ }
1808
+ },
1809
+ useCheckpointResult: async x => {
1810
+ if (x.docSnapshots.length > 0) {
1811
+ expect(x.results[0].snapshots.length).toBeLessThanOrEqual(batchSize);
1812
+ }
1813
+ },
1814
+ documentAccessor,
1815
+ queryFactory: f.instance.mockItemUserCollectionGroup,
1816
+ constraintsFactory: [] // no constraints
1817
+ });
1818
+ expect(result.totalSnapshotsVisited).toBe(allMockUserItems.length);
1819
+ expect(mockUserItemsVisited.size).toBe(allMockUserItems.length);
1820
+ });
1821
+ describe('1 item exists', () => {
1822
+ let onlyItem;
1823
+ beforeEach(async () => {
1824
+ onlyItem = allMockUserItems.pop();
1825
+ await Promise.all(allMockUserItems.map(x => x.accessor.delete()));
1826
+ allMockUserItems = [onlyItem];
1827
+ });
1828
+ it('should iterate the single item', async () => {
1829
+ const documentAccessor = f.instance.mockItemUserCollectionGroup.documentAccessor();
1830
+ const mockUserItemsVisited = new Set();
1831
+ expect(allMockUserItems).toHaveLength(1);
1832
+ const result = await iterateFirestoreDocumentSnapshotPairs({
1833
+ iterateSnapshotPair: async x => {
1834
+ expect(x.data).toBeDefined();
1835
+ expect(x.snapshot).toBeDefined();
1836
+ expect(x.document).toBeDefined();
1837
+ const key = x.document.key;
1838
+ if (mockUserItemsVisited.has(key)) {
1839
+ throw new Error('encountered repeat key');
1840
+ } else {
1841
+ mockUserItemsVisited.add(key);
1842
+ }
1843
+ },
1844
+ documentAccessor,
1845
+ queryFactory: f.instance.mockItemUserCollectionGroup,
1846
+ batchSize: null,
1847
+ limitPerCheckpoint: 200,
1848
+ totalSnapshotsLimit: 100,
1849
+ performTasksConfig: {
1850
+ maxParallelTasks: 20
1851
+ },
1852
+ constraintsFactory: [] // no constraints
1853
+ });
1854
+ expect(mockUserItemsVisited.size).toBe(allMockUserItems.length);
1855
+ expect(result.totalSnapshotsVisited).toBe(allMockUserItems.length);
1856
+ });
1857
+ // TODO(TEST): Case where a document was visited twice via iteration after it was updated. Assumed
1858
+ // to occur when the updated item matches an "or" case or other value when using "in". Cannot
1859
+ // reproduce at the moment.
1860
+ /*
1861
+ describe('scenario', () => {
1862
+ it('should visit the item twice if it is updated and matches a different filter', async () => {
1863
+ const onlyItemValue = await onlyItem.snapshotData() as MockItemUser;
1864
+ const nameToChangeTo = `${onlyItemValue.name}-changed`;
1865
+ const namesToFilter = [onlyItemValue.name, nameToChangeTo];
1866
+ const documentAccessor = f.instance.mockItemUserCollectionGroup.documentAccessor();
1867
+ const mockUserItemsVisited = new Set<MockItemUserKey>();
1868
+ let updates = 0;
1869
+ expect(allMockUserItems).toHaveLength(1);
1870
+ const result = await iterateFirestoreDocumentSnapshotPairs({
1871
+ iterateSnapshotPair: async (x) => {
1872
+ expect(x.data).toBeDefined();
1873
+ expect(x.snapshot).toBeDefined();
1874
+ expect(x.document).toBeDefined();
1875
+ await x.document.update({ name: nameToChangeTo });
1876
+ updates += 1;
1877
+ const key = x.document.key;
1878
+ mockUserItemsVisited.add(key);
1879
+ },
1880
+ documentAccessor,
1881
+ queryFactory: f.instance.mockItemUserCollectionGroup,
1882
+ batchSize: null,
1883
+ limitPerCheckpoint: 200,
1884
+ totalSnapshotsLimit: 100,
1885
+ performTasksConfig: {
1886
+ maxParallelTasks: 20
1887
+ },
1888
+ constraintsFactory: () => [where<MockItemUser>('name', 'in', namesToFilter)],
1889
+ });
1890
+ expect(updates).toBe(2);
1891
+ expect(result.totalSnapshotsVisited).toBe(2);
1892
+ expect(mockUserItemsVisited.size).toBe(1);
1893
+ });
1894
+ });
1895
+ */
1896
+ });
1897
+ describe('0 items exists', () => {
1898
+ beforeEach(async () => {
1899
+ await Promise.all(allMockUserItems.map(x => x.accessor.delete()));
1900
+ });
1901
+ it('should iterate no items', async () => {
1902
+ const documentAccessor = f.instance.mockItemUserCollectionGroup.documentAccessor();
1903
+ const mockUserItemsVisited = new Set();
1904
+ const result = await iterateFirestoreDocumentSnapshotPairs({
1905
+ iterateSnapshotPair: async x => {
1906
+ expect(x.data).toBeDefined();
1907
+ expect(x.snapshot).toBeDefined();
1908
+ expect(x.document).toBeDefined();
1909
+ const key = x.document.key;
1910
+ if (mockUserItemsVisited.has(key)) {
1911
+ throw new Error('encountered repeat key');
1912
+ } else {
1913
+ mockUserItemsVisited.add(key);
1914
+ }
1915
+ },
1916
+ documentAccessor,
1917
+ queryFactory: f.instance.mockItemUserCollectionGroup,
1918
+ constraintsFactory: [] // no constraints
1919
+ });
1920
+ expect(result.totalSnapshotsVisited).toBe(0);
1921
+ expect(mockUserItemsVisited.size).toBe(0);
1922
+ });
1923
+ });
1924
+ });
1925
+ describe('iterateFirestoreDocumentSnapshots()', () => {
1926
+ it('should iterate across all mock users by each snapshot.', async () => {
1927
+ f.instance.mockItemUserCollectionGroup.documentAccessor();
1928
+ const mockUserItemsVisited = new Set();
1929
+ const batchSize = 2;
1930
+ const result = await iterateFirestoreDocumentSnapshots({
1931
+ batchSize,
1932
+ iterateSnapshot: async x => {
1933
+ const key = x.ref.path;
1934
+ if (mockUserItemsVisited.has(key)) {
1935
+ throw new Error('encountered repeat key');
1936
+ } else {
1937
+ mockUserItemsVisited.add(key);
1938
+ }
1939
+ },
1940
+ useCheckpointResult: async x => {
1941
+ if (x.docSnapshots.length > 0) {
1942
+ expect(x.results[0].snapshots.length).toBeLessThanOrEqual(batchSize);
1943
+ }
1944
+ },
1945
+ queryFactory: f.instance.mockItemUserCollectionGroup,
1946
+ constraintsFactory: [] // no constraints
1947
+ });
1948
+ expect(result.totalSnapshotsVisited).toBe(allMockUserItems.length);
1949
+ expect(mockUserItemsVisited.size).toBe(allMockUserItems.length);
1950
+ });
1951
+ });
1952
+ describe('iterateFirestoreDocumentSnapshotPairBatches()', () => {
1953
+ it('should iterate batches of snapshot pairs.', async () => {
1954
+ const documentAccessor = f.instance.mockItemUserCollectionGroup.documentAccessor();
1955
+ const mockUserItemsVisited = new Set();
1956
+ const batchSize = 2;
1957
+ const result = await iterateFirestoreDocumentSnapshotPairBatches({
1958
+ documentAccessor,
1959
+ batchSize,
1960
+ // use specific batch size
1961
+ iterateSnapshotPairsBatch: async x => {
1962
+ expect(x.length).toBeLessThanOrEqual(batchSize);
1963
+ const pair = x[0];
1964
+ expect(pair.data).toBeDefined();
1965
+ expect(pair.snapshot).toBeDefined();
1966
+ expect(pair.document).toBeDefined();
1967
+ },
1968
+ useCheckpointResult: async x => {
1969
+ x.docSnapshots.forEach(y => mockUserItemsVisited.add(y.ref.path));
1970
+ },
1971
+ queryFactory: f.instance.mockItemUserCollectionGroup,
1972
+ constraintsFactory: [] // no constraints
1973
+ });
1974
+ expect(result.totalSnapshotsVisited).toBe(allMockUserItems.length);
1975
+ expect(mockUserItemsVisited.size).toBe(allMockUserItems.length);
1976
+ });
1977
+ });
1978
+ describe('iterateFirestoreDocumentSnapshotBatches()', () => {
1979
+ it('should iterate batches of snapshots.', async () => {
1980
+ const mockUserItemsVisited = new Set();
1981
+ const batchSize = 2;
1982
+ const result = await iterateFirestoreDocumentSnapshotBatches({
1983
+ batchSize,
1984
+ // use specific batch size
1985
+ iterateSnapshotBatch: async x => {
1986
+ expect(x.length).toBeLessThanOrEqual(batchSize);
1987
+ },
1988
+ useCheckpointResult: async x => {
1989
+ x.docSnapshots.forEach(y => mockUserItemsVisited.add(y.ref.path));
1990
+ },
1991
+ queryFactory: f.instance.mockItemUserCollectionGroup,
1992
+ constraintsFactory: [] // no constraints
1993
+ });
1994
+ expect(result.totalSnapshotsVisited).toBe(allMockUserItems.length);
1995
+ expect(mockUserItemsVisited.size).toBe(allMockUserItems.length);
1996
+ });
1997
+ describe('limitPerCheckpoint', () => {
1998
+ describe('limitPerCheckpoint = 0', () => {
1999
+ it('should not iterate any batches', async () => {
2000
+ const result = await iterateFirestoreDocumentSnapshotBatches({
2001
+ limitPerCheckpoint: 0,
2002
+ iterateSnapshotBatch: async x => {
2003
+ expect(x.length).toBe(0);
2004
+ },
2005
+ useCheckpointResult: async x => {
2006
+ expect(x.docSnapshots.length).toBe(0);
2007
+ },
2008
+ queryFactory: f.instance.mockItemUserCollectionGroup,
2009
+ constraintsFactory: [] // no constraints
2010
+ });
2011
+ expect(result.totalSnapshotsVisited).toBe(0);
2012
+ expect(result.totalSnapshotsLimitReached).toBe(true);
2013
+ });
2014
+ });
2015
+ });
2016
+ describe('maxParallelCheckpoints>1', () => {
2017
+ it('should process the checkpoints in parallel.', async () => {
2018
+ const mockUserItemsVisited = new Set();
2019
+ const batchSize = 1;
2020
+ const maxParallelCheckpoints = 4;
2021
+ let currentRunningTasks = 0;
2022
+ let maxRunningTasks = 0;
2023
+ const result = await iterateFirestoreDocumentSnapshotBatches({
2024
+ batchSize,
2025
+ // use specific batch size
2026
+ limitPerCheckpoint: 1,
2027
+ maxParallelCheckpoints,
2028
+ // do four checkpoints in parallel
2029
+ iterateSnapshotBatch: async (x, batchIndex) => {
2030
+ currentRunningTasks += 1;
2031
+ await waitForMs(1000);
2032
+ maxRunningTasks = Math.max(maxRunningTasks, currentRunningTasks);
2033
+ currentRunningTasks -= 1;
2034
+ },
2035
+ useCheckpointResult: async x => {
2036
+ x.docSnapshots.forEach(y => mockUserItemsVisited.add(y.ref.path));
2037
+ },
2038
+ queryFactory: f.instance.mockItemUserCollectionGroup,
2039
+ constraintsFactory: [] // no constraints
2040
+ });
2041
+ expect(maxRunningTasks).toBe(maxParallelCheckpoints);
2042
+ expect(result.totalSnapshotsVisited).toBe(allMockUserItems.length);
2043
+ expect(mockUserItemsVisited.size).toBe(allMockUserItems.length);
2044
+ });
2045
+ });
2046
+ describe('batchSize=null', () => {
2047
+ it('should iterate with a single batch', async () => {
2048
+ const mockUserItemsVisited = new Set();
2049
+ const batchSize = null;
2050
+ const result = await iterateFirestoreDocumentSnapshotBatches({
2051
+ batchSize,
2052
+ // use specific batch size
2053
+ iterateSnapshotBatch: async x => {
2054
+ expect(x.length).toBe(allMockUserItems.length);
2055
+ },
2056
+ useCheckpointResult: async x => {
2057
+ x.docSnapshots.forEach(y => mockUserItemsVisited.add(y.ref.path));
2058
+ },
2059
+ queryFactory: f.instance.mockItemUserCollectionGroup,
2060
+ constraintsFactory: [] // no constraints
2061
+ });
2062
+ expect(result.totalSnapshotsVisited).toBe(allMockUserItems.length);
2063
+ expect(mockUserItemsVisited.size).toBe(allMockUserItems.length);
2064
+ });
2065
+ });
2066
+ describe('batchSizeForSnapshots: () => null', () => {
2067
+ it('should iterate with a single batch', async () => {
2068
+ const mockUserItemsVisited = new Set();
2069
+ const result = await iterateFirestoreDocumentSnapshotBatches({
2070
+ batchSizeForSnapshots: () => null,
2071
+ iterateSnapshotBatch: async x => {
2072
+ expect(x.length).toBe(allMockUserItems.length);
2073
+ },
2074
+ useCheckpointResult: async x => {
2075
+ x.docSnapshots.forEach(y => mockUserItemsVisited.add(y.ref.path));
2076
+ },
2077
+ queryFactory: f.instance.mockItemUserCollectionGroup,
2078
+ constraintsFactory: [] // no constraints
2079
+ });
2080
+ expect(result.totalSnapshotsVisited).toBe(allMockUserItems.length);
2081
+ expect(mockUserItemsVisited.size).toBe(allMockUserItems.length);
2082
+ });
2083
+ });
2084
+ });
2085
+ });
2086
+ });
2087
+ describe('collection group', () => {
2088
+ describe('query', () => {
2089
+ describe('constraints', () => {
2090
+ describe('where', () => {
2091
+ it('should return the documents matching the input uid', async () => {
2092
+ const result = await f.instance.mockItemUserCollectionGroup.query(where('uid', '==', testUserId)).getDocs();
2093
+ expect(result.docs.length).toBe(testDocumentCount);
2094
+ result.docs.forEach(x => {
2095
+ expect(x.data().uid).toBe(testUserId);
2096
+ });
2097
+ });
2098
+ });
2099
+ });
2100
+ });
2101
+ });
2102
+ });
2103
+ describe('nested items', () => {
2104
+ const subItemCountPerItem = 2;
2105
+ const totalSubItemsCount = subItemCountPerItem * testDocumentCount;
2106
+ let parentA;
2107
+ let querySubItems;
2108
+ let allSubItems;
2109
+ beforeEach(async () => {
2110
+ querySubItems = f.instance.mockItemSubItemCollectionGroup.query;
2111
+ parentA = items[0];
2112
+ const results = await Promise.all(items.map(parent => makeDocuments(f.instance.mockItemSubItemCollection(parent).documentAccessor(), {
2113
+ count: subItemCountPerItem,
2114
+ init: i => {
2115
+ return {
2116
+ value: i
2117
+ };
2118
+ }
2119
+ })));
2120
+ allSubItems = results.flat();
2121
+ });
2122
+ describe('sub sub item', () => {
2123
+ const deepSubItemCountPerItem = 1;
2124
+ const totalDeepSubItemsPerMockItem = subItemCountPerItem * deepSubItemCountPerItem;
2125
+ let queryDeepSubItems;
2126
+ beforeEach(async () => {
2127
+ queryDeepSubItems = f.instance.mockItemSubItemDeepCollectionGroup.query;
2128
+ allSubItems[0];
2129
+ const results = await Promise.all(allSubItems.map(parent => makeDocuments(f.instance.mockItemSubItemDeepCollection(parent).documentAccessor(), {
2130
+ count: deepSubItemCountPerItem,
2131
+ init: i => {
2132
+ return {
2133
+ value: i
2134
+ };
2135
+ }
2136
+ })));
2137
+ results.flat();
2138
+ });
2139
+ // tests querying for all nested items under a parent
2140
+ it('querying for only items belonging to mock item parentA', async () => {
2141
+ const result = await queryDeepSubItems(allChildMockItemSubItemDeepsWithinMockItem(parentA.documentRef)).getDocs();
2142
+ expect(result.docs.length).toBe(totalDeepSubItemsPerMockItem);
2143
+ result.docs.forEach(x => expect(x.ref.parent?.parent?.parent?.parent?.path).toBe(parentA.documentRef.path));
2144
+ });
2145
+ // TODO(TEST): Add tests for allChildDocumentsUnderRelativePath
2146
+ });
2147
+ describe('sub item', () => {
2148
+ describe('collection group', () => {
2149
+ describe('query', () => {
2150
+ it('should return sub items', async () => {
2151
+ const result = await querySubItems().getDocs();
2152
+ expect(result.docs.length).toBe(totalSubItemsCount);
2153
+ });
2154
+ describe('constraints', () => {
2155
+ describe('where', () => {
2156
+ it('should return the documents matching the query.', async () => {
2157
+ const value = 0;
2158
+ const result = await querySubItems(where('value', '==', value)).getDocs();
2159
+ expect(result.docs.length).toBe(testDocumentCount);
2160
+ expect(result.docs[0].data().value).toBe(value);
2161
+ const ref = result.docs[0].ref;
2162
+ expect(ref).toBeDefined();
2163
+ expect(ref.parent).toBeDefined();
2164
+ });
2165
+ });
2166
+ describe('whereDocumentId', () => {
2167
+ itShouldFail('to query on collection groups.', async () => {
2168
+ // https://stackoverflow.com/questions/56149601/firestore-collection-group-query-on-documentid
2169
+ const targetId = 'targetid';
2170
+ /*
2171
+ const results = await Promise.all(
2172
+ allSubItems.map((parent: MockItemSubItemDocument) =>
2173
+ makeDocuments(f.instance.mockItemSubItemDeepCollection(parent).documentAccessor(), {
2174
+ count: 1,
2175
+ newDocument: (x) => x.loadDocumentForId(targetId),
2176
+ init: (i) => {
2177
+ return {
2178
+ value: i
2179
+ };
2180
+ }
2181
+ })
2182
+ )
2183
+ );
2184
+ */
2185
+ await expectFail(() => querySubItems(whereDocumentId('==', targetId)).getDocs());
2186
+ });
2187
+ });
2188
+ });
2189
+ describe('streamDocs()', () => {
2190
+ let sub;
2191
+ beforeEach(() => {
2192
+ sub = new SubscriptionObject();
2193
+ });
2194
+ afterEach(() => {
2195
+ sub.destroy();
2196
+ });
2197
+ it('should emit when the query results update (an item is added).', callbackTest(done => {
2198
+ const itemsToAdd = 1;
2199
+ let addCompleted = false;
2200
+ let addSeen = false;
2201
+ function tryComplete() {
2202
+ if (addSeen && addCompleted) {
2203
+ done();
2204
+ }
2205
+ }
2206
+ sub.subscription = querySubItems().streamDocs().pipe(filter(x => x.docs.length > allSubItems.length)).subscribe(results => {
2207
+ addSeen = true;
2208
+ expect(results.docs.length).toBe(allSubItems.length + itemsToAdd);
2209
+ tryComplete();
2210
+ });
2211
+ // add one item
2212
+ makeDocuments(f.instance.mockItemSubItemCollection(parentA).documentAccessor(), {
2213
+ count: itemsToAdd,
2214
+ init: i => {
2215
+ return {
2216
+ value: i
2217
+ };
2218
+ }
2219
+ }).then(() => {
2220
+ addCompleted = true;
2221
+ tryComplete();
2222
+ });
2223
+ }));
2224
+ it('should emit when the query results update (an item is removed).', callbackTest(done => {
2225
+ const itemsToRemove = 1;
2226
+ let deleteCompleted = false;
2227
+ let deleteSeen = false;
2228
+ function tryComplete() {
2229
+ if (deleteSeen && deleteCompleted) {
2230
+ done();
2231
+ }
2232
+ }
2233
+ sub.subscription = querySubItems().streamDocs().pipe(filter(x => x.docs.length < allSubItems.length)).subscribe(results => {
2234
+ deleteSeen = true;
2235
+ expect(results.docs.length).toBe(allSubItems.length - itemsToRemove);
2236
+ tryComplete();
2237
+ });
2238
+ allSubItems[0].accessor.exists().then(exists => {
2239
+ expect(exists).toBe(true);
2240
+ // remove one item
2241
+ return allSubItems[0].accessor.delete().then(() => {
2242
+ deleteCompleted = true;
2243
+ tryComplete();
2244
+ });
2245
+ });
2246
+ }));
2247
+ });
2248
+ });
2249
+ });
2250
+ });
2251
+ });
2252
+ describe('queryDocument', () => {
2253
+ let queryDocument;
2254
+ beforeEach(async () => {
2255
+ queryDocument = f.instance.firestoreCollection.queryDocument;
2256
+ });
2257
+ describe('filter()', () => {
2258
+ it('should apply the filter to the query', async () => {
2259
+ const results = await queryDocument().filter(where('tags', 'array-contains', EVEN_TAG)).getDocSnapshotDataPairs();
2260
+ expect(results).toBeDefined();
2261
+ results.forEach(result => {
2262
+ expect(result.data).toBeDefined();
2263
+ expect(result.data?.tags).toContain(EVEN_TAG);
2264
+ expect(result.document).toBeDefined();
2265
+ expect(result.document instanceof MockItemDocument).toBe(true);
2266
+ expect(result.snapshot).toBeDefined();
2267
+ expect(result.snapshot.data()).toBeDefined();
2268
+ expect(result.snapshot.ref).toBeDefined();
2269
+ expect(result.snapshot.id).toBe(result.document.id);
2270
+ });
2271
+ });
2272
+ it('should add more filters to the existing query', async () => {
2273
+ const results = await queryDocument().filter(where('tags', 'array-contains', EVEN_TAG)).filter(where('number', '>=', 4)).getDocSnapshotDataPairs();
2274
+ expect(results).toBeDefined();
2275
+ expect(results.length).toBe(1);
2276
+ results.forEach(result => {
2277
+ expect(result.data).toBeDefined();
2278
+ expect(result.data?.tags).toContain(EVEN_TAG);
2279
+ expect(result.data?.number).toBeGreaterThanOrEqual(4);
2280
+ expect(result.document).toBeDefined();
2281
+ expect(result.document instanceof MockItemDocument).toBe(true);
2282
+ expect(result.snapshot).toBeDefined();
2283
+ expect(result.snapshot.data()).toBeDefined();
2284
+ expect(result.snapshot.ref).toBeDefined();
2285
+ expect(result.snapshot.id).toBe(result.document.id);
2286
+ });
2287
+ });
2288
+ });
2289
+ describe('getFirstDocSnapshotDataPair()', () => {
2290
+ it('should return undefined if the query contains nothing', async () => {
2291
+ const result = await queryDocument(where('value', '==', '_DOES_NOT_EXIST_')).getFirstDocSnapshotDataPair();
2292
+ expect(result).not.toBeDefined();
2293
+ });
2294
+ it('should return the first doc that matches if it exists', async () => {
2295
+ const result = await queryDocument().getFirstDocSnapshotDataPair();
2296
+ expect(result).toBeDefined();
2297
+ expect(result.data).toBeDefined();
2298
+ expect(result.document).toBeDefined();
2299
+ expect(result.document instanceof MockItemDocument).toBe(true);
2300
+ expect(result.snapshot).toBeDefined();
2301
+ expect(result.snapshot.data()).toBeDefined();
2302
+ expect(result.snapshot.ref).toBeDefined();
2303
+ expect(result.snapshot.id).toBe(result.document.id);
2304
+ });
2305
+ });
2306
+ describe('getDocSnapshotDataPairs()', () => {
2307
+ it('should return an empty array if the query returns nothing', async () => {
2308
+ const result = await queryDocument(where('value', '==', '_DOES_NOT_EXIST_')).getDocSnapshotDataPairs();
2309
+ expect(result).toBeDefined();
2310
+ expect(result.length).toBe(0);
2311
+ });
2312
+ it('should return the matching results', async () => {
2313
+ const results = await queryDocument().getDocSnapshotDataPairs();
2314
+ expect(results).toBeDefined();
2315
+ expect(results.length).toBeGreaterThan(0);
2316
+ results.forEach(result => {
2317
+ expect(result).toBeDefined();
2318
+ expect(result.data).toBeDefined();
2319
+ expect(result.document).toBeDefined();
2320
+ expect(result.document instanceof MockItemDocument).toBe(true);
2321
+ expect(result.snapshot).toBeDefined();
2322
+ expect(result.snapshot.data()).toBeDefined();
2323
+ expect(result.snapshot.ref).toBeDefined();
2324
+ expect(result.snapshot.id).toBe(result.document.id);
2325
+ });
2326
+ });
2327
+ });
2328
+ describe('streamDocs()', () => {
2329
+ let sub;
2330
+ beforeEach(() => {
2331
+ sub = new SubscriptionObject();
2332
+ });
2333
+ afterEach(() => {
2334
+ sub.destroy();
2335
+ });
2336
+ it('should emit when the query results update (an item is added).', callbackTest(done => {
2337
+ const itemsToAdd = 1;
2338
+ let addCompleted = false;
2339
+ let addSeen = false;
2340
+ function tryComplete() {
2341
+ if (addSeen && addCompleted) {
2342
+ done();
2343
+ }
2344
+ }
2345
+ sub.subscription = queryDocument().streamDocs().pipe(filter(documents => documents.length > items.length)).subscribe(documents => {
2346
+ addSeen = true;
2347
+ expect(documents.length).toBe(items.length + itemsToAdd);
2348
+ tryComplete();
2349
+ });
2350
+ // add one item
2351
+ waitForMs(10).then(() => makeDocuments(f.instance.firestoreCollection.documentAccessor(), {
2352
+ count: itemsToAdd,
2353
+ init: i => {
2354
+ return {
2355
+ value: `${i + items.length}`,
2356
+ test: true
2357
+ };
2358
+ }
2359
+ }).then(() => {
2360
+ addCompleted = true;
2361
+ tryComplete();
2362
+ }));
2363
+ }));
2364
+ it('should emit when the query results update (an item is removed).', callbackTest(done => {
2365
+ const itemsToRemove = 1;
2366
+ let deleteCompleted = false;
2367
+ let deleteSeen = false;
2368
+ function tryComplete() {
2369
+ if (deleteSeen && deleteCompleted) {
2370
+ done();
2371
+ }
2372
+ }
2373
+ sub.subscription = queryDocument().streamDocs().pipe(skip(1)).subscribe(documents => {
2374
+ deleteSeen = true;
2375
+ expect(documents.length).toBe(items.length - itemsToRemove);
2376
+ tryComplete();
2377
+ });
2378
+ waitForMs(10).then(() => items[0].exists().then(exists => {
2379
+ expect(exists).toBe(true);
2380
+ // remove one item
2381
+ return items[0].accessor.delete().then(() => {
2382
+ deleteCompleted = true;
2383
+ tryComplete();
2384
+ });
2385
+ }));
2386
+ }));
2387
+ });
2388
+ describe('streamDocSnapshotDataPairs()', () => {
2389
+ let sub;
2390
+ beforeEach(() => {
2391
+ sub = new SubscriptionObject();
2392
+ });
2393
+ afterEach(() => {
2394
+ sub.destroy();
2395
+ });
2396
+ it('should emit when the query results update (an item is added).', callbackTest(done => {
2397
+ const itemsToAdd = 1;
2398
+ let addCompleted = false;
2399
+ let addSeen = false;
2400
+ function tryComplete() {
2401
+ if (addSeen && addCompleted) {
2402
+ done();
2403
+ }
2404
+ }
2405
+ sub.subscription = queryDocument().streamDocSnapshotDataPairs().pipe(filter(documents => documents.length > items.length)).subscribe(documents => {
2406
+ addSeen = true;
2407
+ expect(documents.length).toBe(items.length + itemsToAdd);
2408
+ documents.forEach(x => {
2409
+ // validate each document returned
2410
+ expect(x.data).toBeDefined();
2411
+ expect(x.document).toBeDefined();
2412
+ expect(x.document instanceof MockItemDocument).toBe(true);
2413
+ expect(x.snapshot).toBeDefined();
2414
+ expect(x.snapshot.data()).toBeDefined();
2415
+ expect(x.snapshot.ref).toBeDefined();
2416
+ expect(x.snapshot.id).toBe(x.document.id);
2417
+ });
2418
+ tryComplete();
2419
+ });
2420
+ // add one item
2421
+ waitForMs(10).then(() => makeDocuments(f.instance.firestoreCollection.documentAccessor(), {
2422
+ count: itemsToAdd,
2423
+ init: i => {
2424
+ return {
2425
+ value: `${i + items.length}`,
2426
+ test: true
2427
+ };
2428
+ }
2429
+ }).then(() => {
2430
+ addCompleted = true;
2431
+ tryComplete();
2432
+ }));
2433
+ }));
2434
+ it('should emit when the query results update (an item is removed).', callbackTest(done => {
2435
+ const itemsToRemove = 1;
2436
+ let deleteCompleted = false;
2437
+ let deleteSeen = false;
2438
+ function tryComplete() {
2439
+ if (deleteSeen && deleteCompleted) {
2440
+ done();
2441
+ }
2442
+ }
2443
+ sub.subscription = queryDocument().streamDocs().pipe(skip(1)).subscribe(documents => {
2444
+ deleteSeen = true;
2445
+ expect(documents.length).toBe(items.length - itemsToRemove);
2446
+ tryComplete();
2447
+ });
2448
+ waitForMs(10).then(() => items[0].exists().then(exists => {
2449
+ expect(exists).toBe(true);
2450
+ // remove one item
2451
+ return items[0].accessor.delete().then(() => {
2452
+ deleteCompleted = true;
2453
+ tryComplete();
2454
+ });
2455
+ }));
2456
+ }));
2457
+ });
2458
+ });
2459
+ describe('query', () => {
2460
+ let query;
2461
+ beforeEach(async () => {
2462
+ query = f.instance.firestoreCollection.query;
2463
+ });
2464
+ describe('streamDocs()', () => {
2465
+ let sub;
2466
+ beforeEach(() => {
2467
+ sub = new SubscriptionObject();
2468
+ });
2469
+ afterEach(() => {
2470
+ sub.destroy();
2471
+ });
2472
+ it('should emit when the query results update (an item is added).', callbackTest(done => {
2473
+ const itemsToAdd = 1;
2474
+ let addCompleted = false;
2475
+ let addSeen = false;
2476
+ function tryComplete() {
2477
+ if (addSeen && addCompleted) {
2478
+ done();
2479
+ }
2480
+ }
2481
+ sub.subscription = query().streamDocs().pipe(filter(x => x.docs.length > items.length)).subscribe(results => {
2482
+ addSeen = true;
2483
+ expect(results.docs.length).toBe(items.length + itemsToAdd);
2484
+ tryComplete();
2485
+ });
2486
+ // add one item
2487
+ waitForMs(10).then(() => makeDocuments(f.instance.firestoreCollection.documentAccessor(), {
2488
+ count: itemsToAdd,
2489
+ init: i => {
2490
+ return {
2491
+ value: `${i + items.length}`,
2492
+ test: true
2493
+ };
2494
+ }
2495
+ }).then(() => {
2496
+ addCompleted = true;
2497
+ tryComplete();
2498
+ }));
2499
+ }));
2500
+ it('should emit when the query results update (an item is removed).', callbackTest(done => {
2501
+ const itemsToRemove = 1;
2502
+ let deleteCompleted = false;
2503
+ let deleteSeen = false;
2504
+ function tryComplete() {
2505
+ if (deleteSeen && deleteCompleted) {
2506
+ done();
2507
+ }
2508
+ }
2509
+ sub.subscription = query().streamDocs().pipe(skip(1)).subscribe(results => {
2510
+ deleteSeen = true;
2511
+ expect(results.docs.length).toBe(items.length - itemsToRemove);
2512
+ tryComplete();
2513
+ });
2514
+ waitForMs(10).then(() => items[0].accessor.exists().then(exists => {
2515
+ expect(exists).toBe(true);
2516
+ // remove one item
2517
+ return items[0].accessor.delete().then(() => {
2518
+ deleteCompleted = true;
2519
+ tryComplete();
2520
+ });
2521
+ }));
2522
+ }));
2523
+ });
2524
+ describe('constraint', () => {
2525
+ describe('limit', () => {
2526
+ it('should limit the number of items returned.', async () => {
2527
+ const limitCount = 2;
2528
+ const unlimited = await query().getDocs();
2529
+ expect(unlimited.docs.length).toBe(testDocumentCount);
2530
+ const result = await query(limit(limitCount)).getDocs();
2531
+ expect(result.docs.length).toBe(limitCount);
2532
+ });
2533
+ it('should limit the streamed results.', callbackTest(done => {
2534
+ const limitCount = 2;
2535
+ const resultObs = query(limit(limitCount)).streamDocs();
2536
+ from(resultObs).pipe(first()).subscribe(results => {
2537
+ expect(results.docs.length).toBe(limitCount);
2538
+ done();
2539
+ });
2540
+ }));
2541
+ it('should limit the number of items counted.', async () => {
2542
+ const limitCount = 2;
2543
+ const unlimited = await query().countDocs();
2544
+ expect(unlimited).toBe(testDocumentCount);
2545
+ const result = await query(limit(limitCount)).countDocs();
2546
+ expect(result).toBe(limitCount);
2547
+ });
2548
+ });
2549
+ describe('limitToLast', () => {
2550
+ it('should limit the number of items returned.', async () => {
2551
+ const limitCount = 2;
2552
+ const unlimited = await query().getDocs();
2553
+ expect(unlimited.docs.length).toBe(testDocumentCount);
2554
+ const result = await query(orderBy('value'), limitToLast(limitCount)).getDocs();
2555
+ expect(result.docs.length).toBe(limitCount);
2556
+ });
2557
+ it('the results should be returned from the end of the list. The results are still in the same order as requested.', async () => {
2558
+ const limitCount = 2;
2559
+ const result = await query(orderBy('value', 'asc'), limitToLast(limitCount)).getDocs();
2560
+ expect(result.docs.length).toBe(limitCount);
2561
+ expect(result.docs[0].data().value).toBe('3');
2562
+ expect(result.docs[1].data().value).toBe('4');
2563
+ });
2564
+ itShouldFail('if orderby is not provided.', async () => {
2565
+ const limitCount = 2;
2566
+ const unlimited = await query().getDocs();
2567
+ expect(unlimited.docs.length).toBe(testDocumentCount);
2568
+ await expectFail(() => query(limitToLast(limitCount)).getDocs());
2569
+ });
2570
+ it('should stream results.', callbackTest(done => {
2571
+ const limitCount = 2;
2572
+ const resultObs = query(orderBy('value'), limitToLast(limitCount)).streamDocs();
2573
+ from(resultObs).pipe(first()).subscribe(results => {
2574
+ expect(results.docs.length).toBe(limitCount);
2575
+ done();
2576
+ });
2577
+ }));
2578
+ it('should limit the number of items counted.', async () => {
2579
+ const limitCount = 2;
2580
+ const unlimited = await query().countDocs();
2581
+ expect(unlimited).toBe(testDocumentCount);
2582
+ const result = await query(orderBy('value'), limitToLast(limitCount)).countDocs();
2583
+ expect(result).toBe(limitCount);
2584
+ });
2585
+ });
2586
+ describe('orderBy', () => {
2587
+ it('should return values sorted in ascending order.', async () => {
2588
+ const results = await query(orderBy('value', 'asc')).getDocs();
2589
+ expect(results.docs[0].data().value).toBe('0');
2590
+ });
2591
+ it('should return values sorted in descending order.', async () => {
2592
+ const results = await query(orderBy('value', 'desc')).getDocs();
2593
+ expect(results.docs[0].data().value).toBe(`${items.length - 1}`);
2594
+ });
2595
+ });
2596
+ describe('where', () => {
2597
+ describe('==', () => {
2598
+ it('should return the documents matching the query.', async () => {
2599
+ const value = '0';
2600
+ const result = await query(where('value', '==', value)).getDocs();
2601
+ expect(result.docs.length).toBe(1);
2602
+ expect(result.docs[0].data().value).toBe(value);
2603
+ });
2604
+ it('should return the count of the documents matching the query.', async () => {
2605
+ const value = '0';
2606
+ const result = await query(where('value', '==', value)).countDocs();
2607
+ expect(result).toBe(1);
2608
+ });
2609
+ });
2610
+ describe('in', () => {
2611
+ it('should return the documents with any of the input values.', async () => {
2612
+ const targetValue = ['0', '1', '2'];
2613
+ const result = await query(where('value', 'in', targetValue)).getDocs();
2614
+ expect(result.docs.length).toBe(3);
2615
+ const values = result.docs.map(x => x.data().value);
2616
+ expect(values).toContain('0');
2617
+ expect(values).toContain('1');
2618
+ expect(values).toContain('2');
2619
+ });
2620
+ it('should return the count of documents with any of the input values.', async () => {
2621
+ const targetValue = ['0', '1', '2'];
2622
+ const result = await query(where('value', 'in', targetValue)).countDocs();
2623
+ expect(result).toBe(3);
2624
+ });
2625
+ });
2626
+ describe('not-in', () => {
2627
+ it('should return the documents that do not contain any of the input values.', async () => {
2628
+ const targetValue = ['0', '1', '2'];
2629
+ const result = await query(where('value', 'not-in', targetValue)).getDocs();
2630
+ expect(result.docs.length).toBe(2);
2631
+ const values = result.docs.map(x => x.data().value);
2632
+ expect(values).not.toContain('0');
2633
+ expect(values).not.toContain('1');
2634
+ expect(values).not.toContain('2');
2635
+ expect(values).toContain('3');
2636
+ expect(values).toContain('4');
2637
+ });
2638
+ it('should return the count of documents that do not contain any of the input values.', async () => {
2639
+ const targetValue = ['0', '1', '2'];
2640
+ const result = await query(where('value', 'not-in', targetValue)).countDocs();
2641
+ expect(result).toBe(2);
2642
+ });
2643
+ });
2644
+ describe('searching array values', () => {
2645
+ describe('in', () => {
2646
+ it('should return the documents with arrays that only have the given values.', async () => {
2647
+ // NOTE: we pass an array to match exactly
2648
+ const targetValue = [['0', 'even']];
2649
+ const result = await query(where('tags', 'in', targetValue)).getDocs();
2650
+ expect(result.docs.length).toBe(1);
2651
+ expect(result.docs[0].data().value).toBe('0');
2652
+ });
2653
+ it('should not return the document with arrays that have more than the requested values.', async () => {
2654
+ const targetValue = [['0']];
2655
+ const result = await query(where('tags', 'in', targetValue)).getDocs();
2656
+ expect(result.docs.length).toBe(0);
2657
+ });
2658
+ it('should return the count of documents with arrays that only have the given values.', async () => {
2659
+ // NOTE: we pass an array to match exactly
2660
+ const targetValue = [['0', 'even']];
2661
+ const result = await query(where('tags', 'in', targetValue)).countDocs();
2662
+ expect(result).toBe(1);
2663
+ });
2664
+ });
2665
+ describe('array-contains', () => {
2666
+ it('should return the documents that contain the given value.', async () => {
2667
+ const targetValue = '0';
2668
+ const result = await query(where('tags', 'array-contains', targetValue)).getDocs();
2669
+ expect(result.docs.length).toBe(1);
2670
+ expect(result.docs[0].data().value).toBe('0');
2671
+ });
2672
+ itShouldFail('if an array is passed to where with array-contains', async () => {
2673
+ const targetValues = ['0', 'even'];
2674
+ await expectFail(() => query(where('tags', 'array-contains', targetValues)).getDocs());
2675
+ });
2676
+ });
2677
+ describe('array-contains-any', () => {
2678
+ it('should return the documents that contain the given value, even if it is not passed as an array.', async () => {
2679
+ const targetValues = 'even';
2680
+ const result = await query(where('tags', 'array-contains-any', targetValues)).getDocs();
2681
+ expect(result.docs.length).toBe(Math.floor(testDocumentCount / 2) + 1);
2682
+ result.docs.forEach(x => {
2683
+ expect(isEvenNumber(Number(x.data().value)));
2684
+ });
2685
+ });
2686
+ it('should return the documents that contain any of the given values.', async () => {
2687
+ const targetValues = ['0', 'even'];
2688
+ const result = await query(where('tags', 'array-contains-any', targetValues)).getDocs();
2689
+ expect(result.docs.length).toBe(Math.floor(testDocumentCount / 2) + 1);
2690
+ result.docs.forEach(x => {
2691
+ expect(isEvenNumber(Number(x.data().value)));
2692
+ });
2693
+ });
2694
+ });
2695
+ });
2696
+ describe('Compound Queries', () => {
2697
+ describe('Searching Strings', () => {
2698
+ /*
2699
+ Create models that have model key like string values for prefix searching.
2700
+ */
2701
+ const evenPrefix = mockItemIdentity.collectionType + '/';
2702
+ const oddPrefix = mockItemIdentity.collectionType + 'd' + '/'; // similar, but not quite the same
2703
+ const expectedNumberOfEvenValues = Math.ceil(testDocumentCount / 2);
2704
+ beforeEach(async () => {
2705
+ items = await makeDocuments(f.instance.firestoreCollection.documentAccessor(), {
2706
+ count: testDocumentCount,
2707
+ init: i => {
2708
+ const isEven = isEvenNumber(i);
2709
+ const prefix = isEven ? evenPrefix : oddPrefix;
2710
+ return {
2711
+ value: `${prefix}${i}`,
2712
+ date: new Date(),
2713
+ tags: [],
2714
+ test: true
2715
+ };
2716
+ }
2717
+ });
2718
+ });
2719
+ describe('whereStringHasRootIdentityModelKey()', () => {
2720
+ it('should return only models with searched prefix', async () => {
2721
+ const result = await query(whereStringHasRootIdentityModelKey('value', mockItemIdentity)).getDocs();
2722
+ const values = result.docs.map(x => x.data().value);
2723
+ values.forEach(x => {
2724
+ expect(x.startsWith(evenPrefix));
2725
+ });
2726
+ expect(result.docs.length).toBe(expectedNumberOfEvenValues);
2727
+ });
2728
+ it('should return the count of only models with searched prefix', async () => {
2729
+ const result = await query(whereStringHasRootIdentityModelKey('value', mockItemIdentity)).countDocs();
2730
+ expect(result).toBe(expectedNumberOfEvenValues);
2731
+ });
2732
+ });
2733
+ describe('whereStringValueHasPrefix()', () => {
2734
+ it('should return only models with searched prefix', async () => {
2735
+ const result = await query(whereStringValueHasPrefix('value', evenPrefix)).getDocs();
2736
+ const values = result.docs.map(x => x.data().value);
2737
+ values.forEach(x => {
2738
+ expect(x.startsWith(evenPrefix));
2739
+ });
2740
+ expect(result.docs.length).toBe(expectedNumberOfEvenValues);
2741
+ });
2742
+ });
2743
+ });
2744
+ /**
2745
+ * Since we choose to store dates as strings, we can compare ranges of dates.
2746
+ */
2747
+ describe('Searching Date Strings', () => {
2748
+ describe('whereDateIsAfterWithSort()', () => {
2749
+ it('should return models with dates after the input.', async () => {
2750
+ const startHoursLater = 2;
2751
+ const start = addHours(startDate, startHoursLater);
2752
+ const result = await query(whereDateIsAfterWithSort('date', start)).getDocs();
2753
+ expect(result.docs.length).toBe(startHoursLater);
2754
+ // ascending order by default
2755
+ expect(result.docs[0].data().date?.toISOString()).toBe(addHours(start, 1).toISOString());
2756
+ expect(result.docs[1].data().date?.toISOString()).toBe(addHours(start, 2).toISOString());
2757
+ });
2758
+ it('should return models with dates after the input in descending order.', async () => {
2759
+ const startHoursLater = 2;
2760
+ const start = addHours(startDate, startHoursLater);
2761
+ const result = await query(whereDateIsAfterWithSort('date', start, 'desc')).getDocs();
2762
+ expect(result.docs.length).toBe(startHoursLater);
2763
+ // check descending order
2764
+ expect(result.docs[0].data().date?.toISOString()).toBe(addHours(start, 2).toISOString());
2765
+ expect(result.docs[1].data().date?.toISOString()).toBe(addHours(start, 1).toISOString());
2766
+ });
2767
+ });
2768
+ describe('whereDateIsBeforeWithSort()', () => {
2769
+ it('should return models with dates before the input.', async () => {
2770
+ const startHoursLater = 2;
2771
+ const endDate = addHours(startDate, startHoursLater);
2772
+ const result = await query(whereDateIsBeforeWithSort('date', endDate)).getDocs();
2773
+ expect(result.docs.length).toBe(startHoursLater);
2774
+ // descending order by default
2775
+ expect(result.docs[0].data().date?.toISOString()).toBe(addHours(endDate, -1).toISOString());
2776
+ expect(result.docs[1].data().date?.toISOString()).toBe(addHours(endDate, -2).toISOString());
2777
+ });
2778
+ it('should return models with dates before the input in ascending order.', async () => {
2779
+ const startHoursLater = 2;
2780
+ const endDate = addHours(startDate, startHoursLater);
2781
+ const result = await query(whereDateIsBeforeWithSort('date', endDate, 'asc')).getDocs();
2782
+ expect(result.docs.length).toBe(startHoursLater);
2783
+ // check ascending order
2784
+ expect(result.docs[0].data().date?.toISOString()).toBe(addHours(endDate, -2).toISOString());
2785
+ expect(result.docs[1].data().date?.toISOString()).toBe(addHours(endDate, -1).toISOString());
2786
+ });
2787
+ });
2788
+ describe('whereDateIsOnOrAfterWithSort()', () => {
2789
+ it('should return models with dates after the input.', async () => {
2790
+ const startHoursLater = 2;
2791
+ const start = addHours(startDate, startHoursLater);
2792
+ const result = await query(whereDateIsOnOrAfterWithSort('date', start)).getDocs();
2793
+ expect(result.docs.length).toBe(3);
2794
+ // ascending order by default
2795
+ expect(result.docs[0].data().date?.toISOString()).toBe(addHours(start, 0).toISOString());
2796
+ expect(result.docs[1].data().date?.toISOString()).toBe(addHours(start, 1).toISOString());
2797
+ expect(result.docs[2].data().date?.toISOString()).toBe(addHours(start, 2).toISOString());
2798
+ });
2799
+ it('should return models with dates after the input in descending order.', async () => {
2800
+ const startHoursLater = 2;
2801
+ const start = addHours(startDate, startHoursLater);
2802
+ const result = await query(whereDateIsOnOrAfterWithSort('date', start, 'desc')).getDocs();
2803
+ expect(result.docs.length).toBe(3);
2804
+ // check descending order
2805
+ expect(result.docs[0].data().date?.toISOString()).toBe(addHours(start, 2).toISOString());
2806
+ expect(result.docs[1].data().date?.toISOString()).toBe(addHours(start, 1).toISOString());
2807
+ expect(result.docs[2].data().date?.toISOString()).toBe(addHours(start, 0).toISOString());
2808
+ });
2809
+ });
2810
+ describe('whereDateIsOnOrBeforeWithSort()', () => {
2811
+ it('should return models with dates before the input.', async () => {
2812
+ const startHoursLater = 2;
2813
+ const endDate = addHours(startDate, startHoursLater);
2814
+ const result = await query(whereDateIsOnOrBeforeWithSort('date', endDate)).getDocs();
2815
+ expect(result.docs.length).toBe(3);
2816
+ // descending order by default
2817
+ expect(result.docs[0].data().date?.toISOString()).toBe(addHours(endDate, 0).toISOString());
2818
+ expect(result.docs[1].data().date?.toISOString()).toBe(addHours(endDate, -1).toISOString());
2819
+ expect(result.docs[2].data().date?.toISOString()).toBe(addHours(endDate, -2).toISOString());
2820
+ });
2821
+ it('should return models with dates before the input in ascending order.', async () => {
2822
+ const startHoursLater = 2;
2823
+ const endDate = addHours(startDate, startHoursLater);
2824
+ const result = await query(whereDateIsOnOrBeforeWithSort('date', endDate, 'asc')).getDocs();
2825
+ expect(result.docs.length).toBe(3);
2826
+ // check ascending order
2827
+ expect(result.docs[0].data().date?.toISOString()).toBe(addHours(endDate, -2).toISOString());
2828
+ expect(result.docs[1].data().date?.toISOString()).toBe(addHours(endDate, -1).toISOString());
2829
+ expect(result.docs[2].data().date?.toISOString()).toBe(addHours(endDate, 0).toISOString());
2830
+ });
2831
+ });
2832
+ describe('whereDateIsInRange()', () => {
2833
+ it('should return the date values within the given range.', async () => {
2834
+ const startHoursLater = 1;
2835
+ const totalHoursInRange = 2;
2836
+ const start = addHours(startDate, startHoursLater);
2837
+ const result = await query(whereDateIsInRange('date', {
2838
+ date: start,
2839
+ distance: totalHoursInRange - 1,
2840
+ type: DateRangeType.HOURS_RANGE
2841
+ })).getDocs();
2842
+ expect(result.docs.length).toBe(totalHoursInRange);
2843
+ expect(result.docs[0].data().date?.toISOString()).toBe(start.toISOString());
2844
+ expect(result.docs[1].data().date?.toISOString()).toBe(addHours(start, 1).toISOString());
2845
+ });
2846
+ });
2847
+ describe('whereDateIsBetween()', () => {
2848
+ it('should return the date values within the given range.', async () => {
2849
+ const startHoursLater = 1;
2850
+ const hoursRange = 2;
2851
+ const start = addHours(startDate, startHoursLater);
2852
+ const end = addHours(start, hoursRange);
2853
+ const result = await query(whereDateIsBetween('date', {
2854
+ start,
2855
+ end
2856
+ })).getDocs();
2857
+ expect(result.docs.length).toBe(hoursRange);
2858
+ expect(result.docs[0].data().date?.toISOString()).toBe(start.toISOString());
2859
+ expect(result.docs[1].data().date?.toISOString()).toBe(addHours(start, 1).toISOString());
2860
+ });
2861
+ describe('with searching array value', () => {
2862
+ it('should search the date range and values that are tagged even.', async () => {
2863
+ const targetTag = 'even';
2864
+ const startHoursLater = 1;
2865
+ const hoursRange = 2;
2866
+ const start = addHours(startDate, startHoursLater);
2867
+ const end = addHours(start, hoursRange);
2868
+ const result = await query([
2869
+ // filter by dates first
2870
+ ...whereDateIsBetween('date', {
2871
+ start,
2872
+ end
2873
+ }),
2874
+ // only allow even items
2875
+ where('tags', 'array-contains-any', targetTag)]).getDocs();
2876
+ expect(result.docs.length).toBe(1);
2877
+ const onlyResultData = result.docs[0].data();
2878
+ expect(onlyResultData.date?.toISOString()).toBe(addHours(start, 1).toISOString());
2879
+ expect(onlyResultData.tags).toContain(targetTag);
2880
+ });
2881
+ });
2882
+ });
2883
+ });
2884
+ });
2885
+ });
2886
+ describe('whereDocumentId', () => {
2887
+ it('should return the documents matching the query.', async () => {
2888
+ const targetId = items[0].id;
2889
+ const result = await query(whereDocumentId('==', targetId)).getDocs();
2890
+ expect(result.docs.length).toBe(1);
2891
+ expect(result.docs[0].id).toBe(targetId);
2892
+ });
2893
+ });
2894
+ describe('startAt', () => {
2895
+ it('should return values starting from the specified startAt document.', async () => {
2896
+ const limitCount = 2;
2897
+ const firstQuery = query(limit(limitCount));
2898
+ const first = await firstQuery.getDocs();
2899
+ expect(first.docs.length).toBe(limitCount);
2900
+ const second = await firstQuery.filter(startAt(first.docs[1])).getDocs();
2901
+ expect(second.docs.length).toBe(limitCount);
2902
+ expect(second.docs[0].id).toBe(first.docs[1].id);
2903
+ });
2904
+ it('should return the count of values starting from the specified startAt document.', async () => {
2905
+ const limitCount = 2;
2906
+ const firstQuery = query(limit(limitCount));
2907
+ const first = await firstQuery.getDocs();
2908
+ expect(first.docs.length).toBe(limitCount);
2909
+ // NOTE: startAt with count requires an orderBy to be set.
2910
+ const secondCount = await firstQuery.filter(orderByDocumentId(), startAt(first.docs[1])).countDocs();
2911
+ expect(secondCount).toBe(limitCount);
2912
+ });
2913
+ });
2914
+ describe('startAtValue', () => {
2915
+ it('should return values starting from the specified startAt path.', async () => {
2916
+ const limitCount = testDocumentCount;
2917
+ const firstQuery = query(orderBy('value'), limit(limitCount));
2918
+ const first = await firstQuery.getDocs();
2919
+ expect(first.docs.length).toBe(limitCount);
2920
+ const indexToStartAt = 3;
2921
+ const docToStartAt = first.docs[indexToStartAt];
2922
+ const docToStartAtValue = docToStartAt.data().value;
2923
+ const second = await firstQuery.filter(startAtValue(docToStartAtValue)).getDocs();
2924
+ expect(second.docs.length).toBe(limitCount - indexToStartAt);
2925
+ expect(second.docs[0].id).toBe(docToStartAt.id);
2926
+ });
2927
+ });
2928
+ describe('startAfter', () => {
2929
+ it('should return values starting after the specified startAt point.', async () => {
2930
+ const limitCount = 3;
2931
+ const firstQuery = query(limit(limitCount));
2932
+ const first = await firstQuery.getDocs();
2933
+ expect(first.docs.length).toBe(limitCount);
2934
+ const startAfterDoc = first.docs[1];
2935
+ const expectedFirstDoc = first.docs[2];
2936
+ const second = await firstQuery.filter(startAfter(startAfterDoc)).getDocs();
2937
+ expect(second.docs.length).toBe(limitCount);
2938
+ expect(second.docs[0].id).toBe(expectedFirstDoc.id);
2939
+ });
2940
+ });
2941
+ describe('endAt', () => {
2942
+ it('should return values ending with the specified endAt point (inclusive).', async () => {
2943
+ const limitCount = 2;
2944
+ const firstQuery = query(limit(limitCount));
2945
+ const first = await firstQuery.getDocs();
2946
+ expect(first.docs.length).toBe(limitCount);
2947
+ const second = await firstQuery.filter(endAt(first.docs[0])).getDocs();
2948
+ expect(second.docs.length).toBe(limitCount - 1);
2949
+ expect(second.docs[0].id).toBe(first.docs[0].id);
2950
+ });
2951
+ });
2952
+ describe('endAtValue', () => {
2953
+ it('should return values starting from the specified startAt path.', async () => {
2954
+ const limitCount = testDocumentCount;
2955
+ const firstQuery = query(orderBy('value'), limit(limitCount));
2956
+ const first = await firstQuery.getDocs();
2957
+ expect(first.docs.length).toBe(limitCount);
2958
+ const indexToEndAt = 2;
2959
+ const docToEndAt = first.docs[indexToEndAt];
2960
+ const docToEndAtValue = docToEndAt.data().value;
2961
+ const second = await firstQuery.filter(endAtValue(docToEndAtValue)).getDocs();
2962
+ expect(second.docs.length).toBe(indexToEndAt + 1);
2963
+ expect(second.docs[second.docs.length - 1].id).toBe(docToEndAt.id);
2964
+ });
2965
+ });
2966
+ describe('endBefore', () => {
2967
+ it('should return values ending with the specified endBefore point (exclusive).', async () => {
2968
+ const limitCount = 2;
2969
+ const firstQuery = query(limit(limitCount));
2970
+ const first = await firstQuery.getDocs();
2971
+ expect(first.docs.length).toBe(limitCount);
2972
+ const second = await firstQuery.filter(endBefore(first.docs[1])).getDocs();
2973
+ expect(second.docs.length).toBe(limitCount - 1);
2974
+ expect(second.docs[0].id).toBe(first.docs[0].id);
2975
+ });
2976
+ });
2977
+ });
2978
+ });
2979
+ });
2980
+ }
2981
+
2982
+ /**
2983
+ * Describes accessor driver tests, using a MockItemCollectionFixture.
2984
+ *
2985
+ * @param f
2986
+ */
2987
+ function describeFirestoreIterationTests(f) {
2988
+ describe('firestoreItemPageIteration', () => {
2989
+ const testDocumentCount = 10;
2990
+ let firestoreIteration;
2991
+ let items;
2992
+ let sub;
2993
+ beforeEach(async () => {
2994
+ firestoreIteration = f.instance.firestoreCollection.firestoreIteration;
2995
+ items = await makeDocuments(f.instance.firestoreCollection.documentAccessor(), {
2996
+ count: testDocumentCount,
2997
+ init: i => {
2998
+ return {
2999
+ value: `${i}`,
3000
+ test: true
3001
+ };
3002
+ }
3003
+ });
3004
+ sub = new SubscriptionObject();
3005
+ });
3006
+ afterEach(() => {
3007
+ sub.destroy();
3008
+ });
3009
+ describe('filter', () => {
3010
+ describe('limit', () => {
3011
+ it('should use the input limit for page size.', callbackTest(done => {
3012
+ const limit = 4;
3013
+ const iteration = firestoreIteration({
3014
+ limit
3015
+ });
3016
+ sub.subscription = iteration.latestState$.subscribe(x => {
3017
+ const results = x.value;
3018
+ expect(results.length).toBe(limit);
3019
+ done();
3020
+ });
3021
+ }));
3022
+ });
3023
+ describe('constraint', () => {
3024
+ it('should use the constraints', callbackTest(done => {
3025
+ const iteration = firestoreIteration({
3026
+ constraints: mockItemWithValue('0')
3027
+ });
3028
+ sub.subscription = iteration.latestState$.subscribe(x => {
3029
+ const results = x.value;
3030
+ expect(results.length).toBe(1);
3031
+ expect(results[0].id).toBe(items[0].documentRef.id);
3032
+ done();
3033
+ });
3034
+ }));
3035
+ });
3036
+ });
3037
+ describe('pagination', () => {
3038
+ const limit = 4;
3039
+ let iteration;
3040
+ beforeEach(() => {
3041
+ iteration = firestoreIteration({
3042
+ limit
3043
+ });
3044
+ });
3045
+ afterEach(() => {
3046
+ iteration.destroy();
3047
+ });
3048
+ describe('latestState$', () => {
3049
+ it('should load the first state when subscribed to for the first time.', callbackTest(done => {
3050
+ sub.subscription = iteration.latestState$.subscribe(latestState => {
3051
+ const page = latestState.page;
3052
+ expect(page).toBe(0);
3053
+ const values = latestState.value;
3054
+ expect(values.length).toBe(limit);
3055
+ done();
3056
+ });
3057
+ }));
3058
+ });
3059
+ describe('currentState$', () => {
3060
+ it('should load the first items when subscribed to for the first time.', callbackTest(done => {
3061
+ sub.subscription = iteration.currentState$.pipe(filter(x => Boolean(x.value))).subscribe(currentState => {
3062
+ const page = currentState.page;
3063
+ expect(page).toBe(0);
3064
+ const values = currentState.value;
3065
+ expect(values.length).toBe(limit);
3066
+ done();
3067
+ });
3068
+ }));
3069
+ });
3070
+ describe('nextPage()', () => {
3071
+ it('should load the next page and return when the page has finished loading.', callbackTest(done => {
3072
+ iteration.nextPage().then(() => {
3073
+ const nextPageResult = from(iteration.nextPage());
3074
+ sub.subscription = nextPageResult.pipe(switchMap(x => iteration.currentState$)).subscribe(latestState => {
3075
+ const page = latestState.page;
3076
+ expect(page).toBe(1);
3077
+ const values = latestState.value;
3078
+ expect(values.length).toBe(limit);
3079
+ done();
3080
+ });
3081
+ });
3082
+ }));
3083
+ });
3084
+ describe('with accumulator', () => {
3085
+ let accumulatorSub;
3086
+ beforeEach(() => {
3087
+ accumulatorSub = new SubscriptionObject();
3088
+ });
3089
+ afterEach(() => {
3090
+ accumulatorSub.destroy();
3091
+ });
3092
+ describe('firebaseQuerySnapshotAccumulator()', () => {
3093
+ let accumulator;
3094
+ beforeEach(() => {
3095
+ accumulator = firebaseQuerySnapshotAccumulator(iteration);
3096
+ });
3097
+ it('should accumulate values from the query.', () => {
3098
+ // todo
3099
+ });
3100
+ describe('flattenAccumulatorResultItemArray()', () => {
3101
+ it(`should aggregate the array of results into a single array.`, callbackTest(done => {
3102
+ const pagesToLoad = 2;
3103
+ // load up to page 2
3104
+ iteratorNextPageUntilPage(iteration, pagesToLoad).then(page => {
3105
+ expect(page).toBe(pagesToLoad - 1);
3106
+ const obs = flattenAccumulatorResultItemArray(accumulator);
3107
+ accumulatorSub.subscription = obs.pipe(first()).subscribe(values => {
3108
+ expect(values.length).toBe(pagesToLoad * limit);
3109
+ expect(arrayContainsDuplicateValue(values.map(x => x.id))).toBe(false);
3110
+ // should not be a query snapshot
3111
+ expect(values[0].ref).toBeDefined();
3112
+ done();
3113
+ });
3114
+ });
3115
+ }));
3116
+ });
3117
+ describe('accumulatorCurrentPageListLoadingState()', () => {
3118
+ it('should return a loading state for the current page.', callbackTest(done => {
3119
+ const obs = accumulatorCurrentPageListLoadingState(accumulator);
3120
+ accumulatorSub.subscription = obs.pipe(filter(x => !x.loading)).subscribe(state => {
3121
+ const value = state.value;
3122
+ expect(isLoadingStateFinishedLoading(state)).toBe(true);
3123
+ expect(value).toBeDefined();
3124
+ expect(Array.isArray(value)).toBe(true);
3125
+ expect(Array.isArray(value[0])).toBe(true);
3126
+ done();
3127
+ });
3128
+ }));
3129
+ });
3130
+ });
3131
+ describe('firebaseQueryItemAccumulator()', () => {
3132
+ let itemAccumulator;
3133
+ beforeEach(() => {
3134
+ itemAccumulator = firebaseQueryItemAccumulator(iteration);
3135
+ });
3136
+ describe('flattenAccumulatorResultItemArray()', () => {
3137
+ it(`should aggregate the array of results into a single array.`, callbackTest(done => {
3138
+ const pagesToLoad = 2;
3139
+ // load up to page 2
3140
+ iteratorNextPageUntilPage(iteration, pagesToLoad).then(page => {
3141
+ expect(page).toBe(pagesToLoad - 1);
3142
+ const obs = flattenAccumulatorResultItemArray(itemAccumulator);
3143
+ accumulatorSub.subscription = obs.pipe(first()).subscribe(values => {
3144
+ expect(values.length).toBe(pagesToLoad * limit);
3145
+ expect(arrayContainsDuplicateValue(values.map(x => x.id))).toBe(false);
3146
+ done();
3147
+ });
3148
+ });
3149
+ }));
3150
+ });
3151
+ describe('flattenAccumulatorResultItemArray()', () => {
3152
+ it(`should aggregate the array of results into a single array of the items.`, callbackTest(done => {
3153
+ const pagesToLoad = 2;
3154
+ // load up to page 2
3155
+ iteratorNextPageUntilPage(iteration, pagesToLoad).then(page => {
3156
+ expect(page).toBe(pagesToLoad - 1);
3157
+ const obs = flattenAccumulatorResultItemArray(itemAccumulator);
3158
+ accumulatorSub.subscription = obs.pipe(first()).subscribe(values => {
3159
+ expect(values.length).toBe(pagesToLoad * limit);
3160
+ expect(arrayContainsDuplicateValue(values.map(x => x.id))).toBe(false);
3161
+ // should not be a query snapshot
3162
+ expect(values[0].ref).not.toBeDefined();
3163
+ done();
3164
+ });
3165
+ });
3166
+ }));
3167
+ });
3168
+ describe('accumulatorFlattenPageListLoadingState()', () => {
3169
+ it('should return a loading state for the current page with all items in a single array.', callbackTest(done => {
3170
+ const obs = accumulatorFlattenPageListLoadingState(itemAccumulator);
3171
+ accumulatorSub.subscription = obs.pipe(filter(x => !x.loading)).subscribe(state => {
3172
+ const value = state.value;
3173
+ expect(isLoadingStateFinishedLoading(state)).toBe(true);
3174
+ expect(value).toBeDefined();
3175
+ expect(Array.isArray(value)).toBe(true);
3176
+ expect(Array.isArray(value[0])).toBe(false);
3177
+ done();
3178
+ });
3179
+ }));
3180
+ });
3181
+ describe('accumulatorCurrentPageListLoadingState()', () => {
3182
+ it('should return a loading state for the current page.', callbackTest(done => {
3183
+ const obs = accumulatorCurrentPageListLoadingState(itemAccumulator);
3184
+ accumulatorSub.subscription = obs.pipe(filter(x => !x.loading)).subscribe(state => {
3185
+ const value = state.value;
3186
+ expect(isLoadingStateFinishedLoading(state)).toBe(true);
3187
+ expect(value).toBeDefined();
3188
+ expect(Array.isArray(value)).toBe(true);
3189
+ expect(Array.isArray(value[0])).toBe(true);
3190
+ done();
3191
+ });
3192
+ }));
3193
+ });
3194
+ });
3195
+ });
3196
+ });
3197
+ });
3198
+ }
3199
+
3200
+ class TestFirebaseStorageInstance {
3201
+ constructor(storageContext) {
3202
+ this.storageContext = void 0;
3203
+ this.storageContext = storageContext;
3204
+ }
3205
+ get storage() {
3206
+ return this.storageContext.storage;
3207
+ }
3208
+ }
3209
+ class TestFirebaseStorageContextFixture extends AbstractTestContextFixture {
3210
+ get storage() {
3211
+ return this.instance.storage;
3212
+ }
3213
+ get storageContext() {
3214
+ return this.instance.storageContext;
3215
+ }
3216
+ }
3217
+
3218
+ /**
3219
+ * Describes accessor driver tests, using a MockItemCollectionFixture.
3220
+ *
3221
+ * @param f
3222
+ */
3223
+ function describeFirebaseStorageAccessorDriverTests(f) {
3224
+ describe('FirebaseStorageAccessor', () => {
3225
+ describe('file()', () => {
3226
+ const secondBucket = 'second-bucket';
3227
+ const doesNotExistFilePath = 'test.png';
3228
+ let doesNotExistFile;
3229
+ const existsFilePath = 'test/exists.txt';
3230
+ const existsFileContent = 'Hello! \ud83d\ude0a';
3231
+ const existsFileContentType = 'text/plain';
3232
+ let existsFile;
3233
+ let secondBucketTarget;
3234
+ beforeEach(async () => {
3235
+ doesNotExistFile = f.storageContext.file(doesNotExistFilePath);
3236
+ existsFile = f.storageContext.file(existsFilePath);
3237
+ await existsFile.upload(existsFileContent, {
3238
+ stringFormat: 'raw',
3239
+ contentType: existsFileContentType
3240
+ }); // re-upload for each test
3241
+ // delete the does not exist file and second bucket target if it exists
3242
+ await doesNotExistFile.delete().catch(() => null);
3243
+ secondBucketTarget = f.storageContext.file({
3244
+ bucketId: secondBucket,
3245
+ pathString: doesNotExistFilePath
3246
+ });
3247
+ await secondBucketTarget.delete().catch(() => null);
3248
+ });
3249
+ describe('uploading', () => {
3250
+ let uploadFile;
3251
+ beforeEach(() => {
3252
+ uploadFile = f.storageContext.file('upload.txt');
3253
+ });
3254
+ describe('upload()', () => {
3255
+ describe('string types', () => {
3256
+ itShouldFail('if stringFormat is not defined in the options', async () => {
3257
+ const contentType = 'text/plain';
3258
+ const data = existsFileContent;
3259
+ await expectFail(() => uploadFile.upload(data, {
3260
+ contentType
3261
+ }));
3262
+ });
3263
+ it('should upload a raw UTF-16 string.', async () => {
3264
+ const contentType = 'text/plain';
3265
+ const data = existsFileContent;
3266
+ await uploadFile.upload(data, {
3267
+ stringFormat: 'raw',
3268
+ contentType
3269
+ });
3270
+ const metadata = await uploadFile.getMetadata();
3271
+ expect(metadata.contentType).toBe(contentType);
3272
+ const result = await uploadFile.getBytes();
3273
+ expect(result).toBeDefined();
3274
+ const decoded = Buffer.from(result).toString('utf-8');
3275
+ expect(decoded).toBe(data);
3276
+ });
3277
+ it('should upload a base64 string.', async () => {
3278
+ const bytes = await existsFile.getBytes();
3279
+ const data = Buffer.from(bytes).toString('base64');
3280
+ const contentType = 'text/plain';
3281
+ await uploadFile.upload(data, {
3282
+ stringFormat: 'base64',
3283
+ contentType
3284
+ });
3285
+ const metadata = await uploadFile.getMetadata();
3286
+ expect(metadata.contentType).toBe(contentType);
3287
+ const result = await uploadFile.getBytes();
3288
+ expect(result).toBeDefined();
3289
+ const decoded = Buffer.from(result).toString('utf-8');
3290
+ expect(decoded).toBe(existsFileContent);
3291
+ });
3292
+ it('should upload a base64url string.', async () => {
3293
+ const bytes = await existsFile.getBytes();
3294
+ const data = Buffer.from(bytes).toString('base64url');
3295
+ const contentType = 'text/plain';
3296
+ await uploadFile.upload(data, {
3297
+ stringFormat: 'base64url',
3298
+ contentType
3299
+ });
3300
+ const metadata = await uploadFile.getMetadata();
3301
+ expect(metadata.contentType).toBe(contentType);
3302
+ const result = await uploadFile.getBytes();
3303
+ expect(result).toBeDefined();
3304
+ const decoded = Buffer.from(result).toString('utf-8');
3305
+ expect(decoded).toBe(existsFileContent);
3306
+ });
3307
+ });
3308
+ describe('data types', () => {
3309
+ // NOTE: We can really only test how a NodeJS environment will behave here.
3310
+ it('should upload a Uint8Array', async () => {
3311
+ const dataBuffer = Buffer.from(existsFileContent, 'utf-8');
3312
+ const data = new Uint8Array(dataBuffer);
3313
+ const contentType = 'text/plain';
3314
+ await uploadFile.upload(data, {
3315
+ contentType
3316
+ });
3317
+ const metadata = await uploadFile.getMetadata();
3318
+ expect(metadata.contentType).toBe(contentType);
3319
+ });
3320
+ it('should upload a Buffer', async () => {
3321
+ const buffer = Buffer.from(existsFileContent, 'utf-8');
3322
+ const contentType = 'text/plain';
3323
+ await uploadFile.upload(buffer, {
3324
+ contentType
3325
+ });
3326
+ const metadata = await uploadFile.getMetadata();
3327
+ expect(metadata.contentType).toBe(contentType);
3328
+ });
3329
+ it('should upload a Blob', async () => {
3330
+ const buffer = Buffer.from(existsFileContent, 'utf-8');
3331
+ const data = new Uint8Array(buffer);
3332
+ const blob = data.buffer; // blob-like
3333
+ const contentType = 'text/plain';
3334
+ await uploadFile.upload(blob, {
3335
+ contentType
3336
+ });
3337
+ const metadata = await uploadFile.getMetadata();
3338
+ expect(metadata.contentType).toBe(contentType);
3339
+ });
3340
+ // NOTE: File extends Blob, so above test should cover it ok.
3341
+ });
3342
+ // TODO(TEST): Test uploading other types.
3343
+ describe('custom metadata', () => {
3344
+ it('should upload custom metadata via customMetadata', async () => {
3345
+ const customMetadataKey = 'x-amz-meta-custom-key';
3346
+ const customMetadataValue = 'custom-value';
3347
+ const customMetadata = {
3348
+ [customMetadataKey]: customMetadataValue
3349
+ };
3350
+ const contentType = 'text/plain';
3351
+ const data = existsFileContent;
3352
+ await uploadFile.upload(data, {
3353
+ stringFormat: 'raw',
3354
+ contentType,
3355
+ customMetadata
3356
+ });
3357
+ const metadata = await uploadFile.getMetadata();
3358
+ expect(metadata.customMetadata).toEqual(customMetadata);
3359
+ });
3360
+ it('should upload custom metadata via metadata', async () => {
3361
+ const customMetadataKey = 'x-amz-meta-custom-key';
3362
+ const customMetadataValue = 'custom-value';
3363
+ const customMetadata = {
3364
+ [customMetadataKey]: customMetadataValue
3365
+ };
3366
+ const contentType = 'text/plain';
3367
+ const data = existsFileContent;
3368
+ await uploadFile.upload(data, {
3369
+ stringFormat: 'raw',
3370
+ contentType,
3371
+ metadata: {
3372
+ customMetadata
3373
+ }
3374
+ });
3375
+ const metadata = await uploadFile.getMetadata();
3376
+ expect(metadata.customMetadata).toEqual(customMetadata);
3377
+ });
3378
+ it('should upload the merged custom metadatas', async () => {
3379
+ const customMetadataAKey = 'x-amz-meta-custom-key';
3380
+ const customMetadataAValue = '1';
3381
+ const customMetadataBKey = 'x-axx-meta-custom-key';
3382
+ const customMetadataBValue = 'true';
3383
+ const customMetadataA = {
3384
+ [customMetadataAKey]: customMetadataAValue
3385
+ };
3386
+ const customMetadataB = {
3387
+ [customMetadataBKey]: customMetadataBValue
3388
+ };
3389
+ const contentType = 'text/plain';
3390
+ const data = existsFileContent;
3391
+ await uploadFile.upload(data, {
3392
+ stringFormat: 'raw',
3393
+ contentType,
3394
+ customMetadata: customMetadataA,
3395
+ metadata: {
3396
+ customMetadata: customMetadataB
3397
+ }
3398
+ });
3399
+ const metadata = await uploadFile.getMetadata();
3400
+ expect(metadata.customMetadata).toEqual({
3401
+ ...customMetadataA,
3402
+ ...customMetadataB
3403
+ });
3404
+ });
3405
+ });
3406
+ });
3407
+ describe('uploadStream()', () => {
3408
+ it('should upload a string using a WritableStream', async () => {
3409
+ if (uploadFile.uploadStream != null) {
3410
+ const contentType = 'text/plain';
3411
+ const data = existsFileContent;
3412
+ const stream = uploadFile.uploadStream();
3413
+ await useCallback(cb => stream.write(data, 'utf-8', cb));
3414
+ await useCallback(cb => stream.end(cb));
3415
+ const exists = await uploadFile.exists();
3416
+ expect(exists).toBe(true);
3417
+ const metadata = await uploadFile.getMetadata();
3418
+ expect(metadata.contentType).toBe(contentType);
3419
+ const result = await uploadFile.getBytes();
3420
+ expect(result).toBeDefined();
3421
+ const decoded = Buffer.from(result).toString('utf-8');
3422
+ expect(decoded).toBe(data);
3423
+ }
3424
+ });
3425
+ it('should upload a string using a stream using a WritableStream', async () => {
3426
+ if (uploadFile.uploadStream != null) {
3427
+ const myText = 'This is a test string.';
3428
+ // Create a readable stream from the string
3429
+ const readableStream = Readable.from(myText, {
3430
+ encoding: 'utf-8'
3431
+ });
3432
+ await uploadFileWithStream(uploadFile, readableStream);
3433
+ const exists = await uploadFile.exists();
3434
+ expect(exists).toBe(true);
3435
+ const metadata = await uploadFile.getMetadata();
3436
+ expect(metadata.contentType).toBe('text/plain');
3437
+ const result = await uploadFile.getBytes();
3438
+ expect(result).toBeDefined();
3439
+ const decoded = Buffer.from(result).toString('utf-8');
3440
+ expect(decoded).toBe(myText);
3441
+ }
3442
+ });
3443
+ it('should upload a png using a stream using a WritableStream', async () => {
3444
+ if (uploadFile.uploadStream != null) {
3445
+ const testFilePath = `${__dirname}/assets/testpng.png`;
3446
+ const contentType = 'image/png';
3447
+ const testFileStream = createReadStream(testFilePath, {});
3448
+ await uploadFileWithStream(uploadFile, testFileStream, {
3449
+ contentType
3450
+ });
3451
+ const exists = await uploadFile.exists();
3452
+ expect(exists).toBe(true);
3453
+ const metadata = await uploadFile.getMetadata();
3454
+ expect(metadata.contentType).toBe(contentType);
3455
+ const result = await uploadFile.getBytes();
3456
+ expect(result).toBeDefined();
3457
+ }
3458
+ });
3459
+ });
3460
+ });
3461
+ describe('copy()', () => {
3462
+ it('should copy the file to a new location in the same bucket.', async () => {
3463
+ if (existsFile.copy != null) {
3464
+ const exists = await doesNotExistFile.exists();
3465
+ expect(exists).toBe(false);
3466
+ const targetPath = doesNotExistFile.storagePath;
3467
+ const result = await existsFile.copy(targetPath);
3468
+ expect(result.storagePath.pathString).toBe(targetPath.pathString);
3469
+ const doesNotExistFileExists = await doesNotExistFile.exists();
3470
+ expect(doesNotExistFileExists).toBe(true);
3471
+ const existsStillExists = await existsFile.exists();
3472
+ expect(existsStillExists).toBe(true); // original still exists
3473
+ }
3474
+ });
3475
+ it('should copy the file to a new location to a different bucket.', async () => {
3476
+ if (existsFile.copy != null) {
3477
+ const secondBucket = {
3478
+ bucketId: 'second-bucket',
3479
+ pathString: secondBucketTarget.storagePath.pathString
3480
+ };
3481
+ const targetFile = f.storageContext.file(secondBucket);
3482
+ const exists = await targetFile.exists();
3483
+ expect(exists).toBe(false);
3484
+ const targetPath = targetFile.storagePath;
3485
+ const result = await existsFile.copy(targetPath);
3486
+ expect(result.storagePath.pathString).toBe(targetPath.pathString);
3487
+ const targetFileExists = await targetFile.exists();
3488
+ expect(targetFileExists).toBe(true);
3489
+ const doesNotExistExists = await doesNotExistFile.exists();
3490
+ expect(doesNotExistExists).toBe(false); // on a different bucket
3491
+ const existsStillExists = await existsFile.exists();
3492
+ expect(existsStillExists).toBe(true); // original still exists
3493
+ }
3494
+ });
3495
+ });
3496
+ describe('move()', () => {
3497
+ it('should move the file to a new location in the same bucket.', async () => {
3498
+ if (existsFile.move != null) {
3499
+ const exists = await doesNotExistFile.exists();
3500
+ expect(exists).toBe(false);
3501
+ const targetPath = doesNotExistFile.storagePath;
3502
+ const result = await existsFile.move(targetPath);
3503
+ expect(result.storagePath.pathString).toBe(targetPath.pathString);
3504
+ const doesNotExistExists = await doesNotExistFile.exists();
3505
+ expect(doesNotExistExists).toBe(true);
3506
+ const existsStillExists = await existsFile.exists();
3507
+ expect(existsStillExists).toBe(false); // check was moved
3508
+ }
3509
+ });
3510
+ it('should move the file to a new location to a different bucket.', async () => {
3511
+ if (existsFile.move != null) {
3512
+ const secondBucket = {
3513
+ bucketId: 'second-bucket',
3514
+ pathString: doesNotExistFile.storagePath.pathString
3515
+ };
3516
+ const targetFile = f.storageContext.file(secondBucket);
3517
+ const exists = await targetFile.exists();
3518
+ expect(exists).toBe(false);
3519
+ const targetPath = targetFile.storagePath;
3520
+ const result = await existsFile.move(targetPath);
3521
+ expect(result.storagePath.pathString).toBe(targetPath.pathString);
3522
+ const targetFileExists = await targetFile.exists();
3523
+ expect(targetFileExists).toBe(true);
3524
+ const doesNotExistStillDoesNotExists = await doesNotExistFile.exists();
3525
+ expect(doesNotExistStillDoesNotExists).toBe(false);
3526
+ const existsStillExists = await existsFile.exists();
3527
+ expect(existsStillExists).toBe(false); // check was moved
3528
+ }
3529
+ });
3530
+ });
3531
+ describe('exists()', () => {
3532
+ it('should return true if the file exists.', async () => {
3533
+ const result = await existsFile.exists();
3534
+ expect(result).toBe(true);
3535
+ });
3536
+ it('should return false if the file exists.', async () => {
3537
+ const result = await doesNotExistFile.exists();
3538
+ expect(result).toBe(false);
3539
+ });
3540
+ });
3541
+ describe('getMetadata()', () => {
3542
+ itShouldFail('if the file does not exist.', async () => {
3543
+ await expectFail(() => doesNotExistFile.getMetadata());
3544
+ });
3545
+ it('should return the metadata.', async () => {
3546
+ const result = await existsFile.getMetadata();
3547
+ expect(result.bucket).toBe(existsFile.storagePath.bucketId);
3548
+ expect(result.fullPath).toBe(existsFilePath);
3549
+ expect(typeof result.size).toBe('number');
3550
+ expect(result.size).toBeGreaterThan(0);
3551
+ expect(result.contentType).toBe(existsFileContentType);
3552
+ expect(result).toBeDefined();
3553
+ });
3554
+ });
3555
+ describe('setMetadata()', () => {
3556
+ itShouldFail('if the file does not exist.', async () => {
3557
+ await expectFail(() => doesNotExistFile.setMetadata({}));
3558
+ });
3559
+ it('should replace the content type field.', async () => {
3560
+ const currentMetadata = await existsFile.getMetadata();
3561
+ expect(currentMetadata.contentType).toBe(existsFileContentType);
3562
+ const nextContentType = 'application/json';
3563
+ const result = await existsFile.setMetadata({
3564
+ contentType: nextContentType
3565
+ });
3566
+ expect(result.contentType).toBe(nextContentType);
3567
+ const updatedMetadata = await existsFile.getMetadata();
3568
+ expect(updatedMetadata.contentType).toBe(nextContentType);
3569
+ });
3570
+ it('should replace the metadata for only the provided fields.', async () => {
3571
+ const currentMetadata = await existsFile.getMetadata();
3572
+ expect(currentMetadata.contentType).toBe(existsFileContentType);
3573
+ const customMetadataA = {
3574
+ foo: 'bar'
3575
+ };
3576
+ const result = await existsFile.setMetadata({
3577
+ contentType: undefined,
3578
+ // should not change
3579
+ customMetadata: customMetadataA
3580
+ });
3581
+ expect(result.contentType).toBe(existsFileContentType);
3582
+ expect(result.customMetadata).toEqual(customMetadataA);
3583
+ const updatedMetadata = await existsFile.getMetadata();
3584
+ expect(updatedMetadata.contentType).toBe(existsFileContentType);
3585
+ expect(updatedMetadata.customMetadata).toEqual(customMetadataA);
3586
+ // update again. All custom metadata is replaced
3587
+ const customMetadataB = {
3588
+ foo: 'baz'
3589
+ };
3590
+ const result2 = await existsFile.setMetadata({
3591
+ customMetadata: customMetadataB
3592
+ });
3593
+ expect(result2.contentType).toBe(existsFileContentType);
3594
+ expect(result2.customMetadata).toEqual(customMetadataB);
3595
+ const updatedMetadata2 = await existsFile.getMetadata();
3596
+ expect(updatedMetadata2.contentType).toBe(existsFileContentType);
3597
+ expect(updatedMetadata2.customMetadata).toEqual(customMetadataB);
3598
+ });
3599
+ });
3600
+ describe('getBytes()', () => {
3601
+ itShouldFail('if the file does not exist.', async () => {
3602
+ await expectFail(() => doesNotExistFile.getBytes());
3603
+ });
3604
+ it('should download the file.', async () => {
3605
+ const result = await existsFile.getBytes();
3606
+ expect(result).toBeDefined();
3607
+ const decoded = Buffer.from(result).toString('utf-8');
3608
+ expect(decoded).toBe(existsFileContent);
3609
+ });
3610
+ describe('with maxDownloadSizeBytes configuration', () => {
3611
+ it('should download up to the maxDownloadSizeBytes number of bytes', async () => {
3612
+ const charactersToTake = 5;
3613
+ const result = await existsFile.getBytes(charactersToTake); // each normal utf-8 character is 1 byte
3614
+ expect(result).toBeDefined();
3615
+ const decoded = Buffer.from(result).toString('utf-8');
3616
+ expect(decoded).toBe(existsFileContent.substring(0, charactersToTake));
3617
+ });
3618
+ });
3619
+ });
3620
+ describe('getStream()', () => {
3621
+ it('should download the file.', async () => {
3622
+ if (existsFile.getStream != null) {
3623
+ // only test if the driver/file has getStream available
3624
+ const stream = existsFile.getStream();
3625
+ expect(stream).toBeDefined();
3626
+ const buffer = await readableStreamToBuffer(stream);
3627
+ const decoded = buffer.toString('utf-8');
3628
+ expect(decoded).toBe(existsFileContent);
3629
+ }
3630
+ });
3631
+ });
3632
+ describe('getDownloadUrl()', () => {
3633
+ itShouldFail('if the file does not exist.', async () => {
3634
+ const doesNotExistFileExists = await doesNotExistFile.exists();
3635
+ expect(doesNotExistFileExists).toBe(false);
3636
+ await expectFail(() => doesNotExistFile.getDownloadUrl());
3637
+ });
3638
+ it('should return the download url.', async () => {
3639
+ const result = await existsFile.getDownloadUrl();
3640
+ expect(result).toBeDefined();
3641
+ expect(typeof result).toBe('string');
3642
+ });
3643
+ });
3644
+ // Cannot be tested, will throw "Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information."
3645
+ describe('getSignedUrl()', () => {
3646
+ it('should return the signed read url.', async () => {
3647
+ if (existsFile.getSignedUrl) {
3648
+ const result = await existsFile.getSignedUrl({});
3649
+ expect(result).toBeDefined();
3650
+ expect(typeof result).toBe('string');
3651
+ }
3652
+ });
3653
+ });
3654
+ describe('makePublic()', () => {
3655
+ beforeEach(async () => {
3656
+ await existsFile.delete();
3657
+ await existsFile.upload(existsFileContent, {
3658
+ stringFormat: 'raw',
3659
+ contentType: existsFileContentType
3660
+ }); // re-upload for each test
3661
+ });
3662
+ it('should make the file public.', async () => {
3663
+ if (existsFile.makePublic && existsFile.isPublic && existsFile.getAcls) {
3664
+ // TODO: firestore emulator files seem to always be public and ACLs do not change?
3665
+ // let isPublic = await existsFile.isPublic();
3666
+ // expect(isPublic).toBe(false);
3667
+ // TODO: Not implemented in the emulator properly either
3668
+ // const acls = await existsFile.getAcls();
3669
+ // console.log({ acls });
3670
+ await existsFile.makePublic(true);
3671
+ // TODO: doesn't really test it properly since true is always returned by the emulator...
3672
+ const isPublic = await existsFile.isPublic();
3673
+ expect(isPublic).toBe(true);
3674
+ // TODO: Not implemented in the emulator
3675
+ // await existsFile.makePublic(false);
3676
+ // isPublic = await existsFile.isPublic();
3677
+ // expect(isPublic).toBe(false);
3678
+ }
3679
+ });
3680
+ });
3681
+ // TODO: getAcls() and related functions cannot be tested in the emulator currently
3682
+ describe('delete()', () => {
3683
+ itShouldFail('if the file does not exist.', async () => {
3684
+ await expectFail(() => doesNotExistFile.delete());
3685
+ });
3686
+ it('should delete the file at the path.', async () => {
3687
+ await existsFile.delete();
3688
+ const result = await existsFile.exists();
3689
+ expect(result).toBe(false);
3690
+ });
3691
+ describe('ignoreNotFound=true', () => {
3692
+ it('should not throw an error if the file does not exist.', async () => {
3693
+ await doesNotExistFile.delete({
3694
+ ignoreNotFound: true
3695
+ });
3696
+ });
3697
+ });
3698
+ });
3699
+ });
3700
+ describe('folder()', () => {
3701
+ const doesNotExistFolderPath = '/doesnotexist/';
3702
+ let doesNotExistFolder;
3703
+ const existsFolderPath = '/test/two/';
3704
+ let existsFolder;
3705
+ const existsFileName = 'exists.txt';
3706
+ const existsFilePath = existsFolderPath + existsFileName;
3707
+ const existsFileContent = 'Hello! \ud83d\ude0a';
3708
+ let existsFile;
3709
+ beforeEach(async () => {
3710
+ doesNotExistFolder = f.storageContext.folder(doesNotExistFolderPath);
3711
+ existsFolder = f.storageContext.folder(existsFolderPath);
3712
+ existsFile = f.storageContext.file(existsFilePath);
3713
+ await existsFile.upload(existsFileContent, {
3714
+ stringFormat: 'raw',
3715
+ contentType: 'text/plain'
3716
+ });
3717
+ });
3718
+ describe('exists()', () => {
3719
+ it('should return false if there are no items in the folder.', async () => {
3720
+ const exists = await doesNotExistFolder.exists();
3721
+ expect(exists).toBe(false);
3722
+ });
3723
+ it('should return true if there are items in the folder.', async () => {
3724
+ const exists = await existsFolder.exists();
3725
+ expect(exists).toBe(true);
3726
+ });
3727
+ });
3728
+ describe('list()', () => {
3729
+ const existsBFileName = 'a.txt';
3730
+ const existsBFilePath = existsFolderPath + existsBFileName;
3731
+ const existsCFolderPath = existsFolderPath + 'c/';
3732
+ const existsCFilePath = existsCFolderPath + 'c.txt';
3733
+ const otherFolderPath = '/other/';
3734
+ const otherFolderFilePath = otherFolderPath + 'other.txt';
3735
+ beforeEach(async () => {
3736
+ await f.storageContext.file(existsBFilePath).upload(existsFileContent, {
3737
+ stringFormat: 'raw',
3738
+ contentType: 'text/plain'
3739
+ });
3740
+ await f.storageContext.file(existsCFilePath).upload(existsFileContent, {
3741
+ stringFormat: 'raw',
3742
+ contentType: 'text/plain'
3743
+ });
3744
+ await f.storageContext.file(otherFolderFilePath).upload(existsFileContent, {
3745
+ stringFormat: 'raw',
3746
+ contentType: 'text/plain'
3747
+ });
3748
+ });
3749
+ describe('options', () => {
3750
+ describe('listAll', () => {
3751
+ describe('=false/unset', () => {
3752
+ it('should list all the direct files and folders that exist on the test path.', async () => {
3753
+ const result = await existsFolder.list();
3754
+ expect(result).toBeDefined();
3755
+ const files = result.files();
3756
+ expect(files.length).toBe(2);
3757
+ const fileNames = new Set(files.map(x => x.name));
3758
+ expect(fileNames).toContain(existsFileName);
3759
+ expect(fileNames).toContain(existsBFileName);
3760
+ const folders = result.folders();
3761
+ expect(folders.length).toBe(1);
3762
+ const folderNames = new Set(folders.map(x => x.name));
3763
+ expect(folderNames).toContain('c');
3764
+ });
3765
+ it('should list all the direct folders that exist at the root.', async () => {
3766
+ const rootFolder = await f.storageContext.folder('/');
3767
+ const result = await rootFolder.list();
3768
+ expect(result).toBeDefined();
3769
+ const files = result.files();
3770
+ expect(files.length).toBe(0); // files are under /test/ and /other/
3771
+ const folders = result.folders();
3772
+ expect(folders.length).toBe(2);
3773
+ const names = new Set(folders.map(x => x.name));
3774
+ expect(names).toContain('test');
3775
+ expect(names).toContain('other');
3776
+ });
3777
+ });
3778
+ describe('=true', () => {
3779
+ it('should list all files and folders that exist on the test path.', async () => {
3780
+ const result = await existsFolder.list({
3781
+ includeNestedResults: true
3782
+ });
3783
+ expect(result).toBeDefined();
3784
+ const files = result.files();
3785
+ expect(files.length).toBe(3);
3786
+ const filePaths = new Set(files.map(x => `${SLASH_PATH_SEPARATOR}${x.storagePath.pathString}`));
3787
+ expect(filePaths).toContain(existsFilePath);
3788
+ expect(filePaths).toContain(existsBFilePath);
3789
+ expect(filePaths).toContain(existsCFilePath);
3790
+ expect(filePaths).not.toContain(otherFolderFilePath);
3791
+ // folders are not counted/returned
3792
+ const folders = result.folders();
3793
+ expect(folders.length).toBe(0);
3794
+ });
3795
+ it('should list all the folders that exist at the root.', async () => {
3796
+ const rootFolder = await f.storageContext.folder('/');
3797
+ const result = await rootFolder.list({
3798
+ includeNestedResults: true
3799
+ });
3800
+ expect(result).toBeDefined();
3801
+ const files = result.files();
3802
+ expect(files.length).toBe(4); // all created files
3803
+ const folders = result.folders();
3804
+ expect(folders.length).toBe(0);
3805
+ });
3806
+ describe('maxResults', () => {
3807
+ it('should limit the number of results returned.', async () => {
3808
+ const rootFolder = await f.storageContext.folder('/');
3809
+ const limit = 2;
3810
+ const result = await rootFolder.list({
3811
+ includeNestedResults: true,
3812
+ maxResults: limit
3813
+ });
3814
+ expect(result).toBeDefined();
3815
+ if (f.storageContext.drivers.storageAccessorDriver.type === 'server') {
3816
+ // Currently only the server can properly limit the number of results returned.
3817
+ // The client-side will limit the results somewhat, but if folders are returned then it will return the results of those folders as well.
3818
+ const files = result.files();
3819
+ expect(files.length).toBe(limit);
3820
+ const nextPage = await result.next();
3821
+ const nextPageFiles = nextPage.files();
3822
+ expect(nextPageFiles.length).toBe(limit);
3823
+ }
3824
+ const folders = result.folders();
3825
+ expect(folders.length).toBe(0);
3826
+ });
3827
+ });
3828
+ });
3829
+ });
3830
+ });
3831
+ describe('file()', () => {
3832
+ it('should return the file for the result.', async () => {
3833
+ const result = await existsFolder.list();
3834
+ expect(result).toBeDefined();
3835
+ const files = result.files();
3836
+ const fileResult = files.find(x => x.name === existsFileName);
3837
+ const file = fileResult.file();
3838
+ const exists = await file.exists();
3839
+ expect(exists).toBe(true);
3840
+ });
3841
+ });
3842
+ describe('folder()', () => {
3843
+ it('should return the folder for the result.', async () => {
3844
+ const rootFolder = await f.storageContext.folder('/');
3845
+ const result = await rootFolder.list();
3846
+ expect(result).toBeDefined();
3847
+ const folders = result.folders();
3848
+ const folderResult = folders.find(x => x.name === 'test');
3849
+ const folder = folderResult.folder();
3850
+ const exists = await folder.exists();
3851
+ expect(exists).toBe(true);
3852
+ });
3853
+ });
3854
+ describe('next()', () => {
3855
+ it('should return the next set of results.', async () => {
3856
+ const maxResults = 1;
3857
+ const rootFolder = await f.storageContext.folder(existsFolderPath);
3858
+ const result = await rootFolder.list({
3859
+ maxResults
3860
+ });
3861
+ expect(result).toBeDefined();
3862
+ const files = result.files();
3863
+ expect(files.length).toBe(maxResults);
3864
+ const next = await result.next();
3865
+ expect(next).toBeDefined();
3866
+ const nextFiles = next.files();
3867
+ expect(nextFiles.length).toBe(maxResults);
3868
+ expect(nextFiles[0].storagePath.pathString).not.toBe(files[0].storagePath.pathString);
3869
+ expect(next.hasNext).toBe(false);
3870
+ });
3871
+ itShouldFail('if next() is called and hasNext was false.', async () => {
3872
+ const rootFolder = await f.storageContext.folder(existsFolderPath);
3873
+ const result = await rootFolder.list({});
3874
+ expect(result.hasNext).toBe(false);
3875
+ await expectFail(() => result.next());
3876
+ });
3877
+ });
3878
+ describe('maxResults', () => {
3879
+ it('should respect the max results.', async () => {
3880
+ const maxResults = 1;
3881
+ const rootFolder = await f.storageContext.folder(existsFolderPath);
3882
+ const result = await rootFolder.list({
3883
+ maxResults
3884
+ });
3885
+ expect(result).toBeDefined();
3886
+ const files = result.files();
3887
+ expect(files.length).toBe(maxResults);
3888
+ const folders = result.folders();
3889
+ expect(folders.length).toBe(1);
3890
+ const names = new Set(folders.map(x => x.name));
3891
+ expect(names).toContain('c');
3892
+ });
3893
+ it('prefixes/folders are unaffected by maxResults.', async () => {
3894
+ const maxResults = 1;
3895
+ const rootFolder = await f.storageContext.folder('/');
3896
+ const result = await rootFolder.list({
3897
+ maxResults
3898
+ });
3899
+ expect(result).toBeDefined();
3900
+ const files = result.files();
3901
+ expect(files.length).toBe(0); // files are under /test/ and /other/
3902
+ const folders = result.folders();
3903
+ expect(folders.length).toBe(2);
3904
+ const names = new Set(folders.map(x => x.name));
3905
+ expect(names).toContain('test');
3906
+ expect(names).toContain('other');
3907
+ });
3908
+ });
3909
+ describe('utilities', () => {
3910
+ describe('iterateStorageListFilesByEachFile()', () => {
3911
+ it('should iterate through all the files in the current folder one at a time', async () => {
3912
+ const visitedFiles = [];
3913
+ const result = await iterateStorageListFilesByEachFile({
3914
+ folder: existsFolder,
3915
+ readItemsFromPageResult: x => x.result.files(),
3916
+ iterateEachPageItem: async file => {
3917
+ visitedFiles.push(file);
3918
+ }
3919
+ });
3920
+ const visitedFilePathStrings = visitedFiles.map(x => `${SLASH_PATH_SEPARATOR}${x.storagePath.pathString}`);
3921
+ expect(visitedFilePathStrings).toContain(existsFilePath);
3922
+ expect(visitedFilePathStrings).toContain(existsBFilePath);
3923
+ expect(visitedFilePathStrings).not.toContain(existsCFilePath);
3924
+ expect(visitedFilePathStrings).not.toContain(otherFolderFilePath);
3925
+ expect(result).toBeDefined();
3926
+ expect(result.totalItemsLoaded).toBe(2);
3927
+ expect(result.totalItemsVisited).toBe(visitedFiles.length);
3928
+ });
3929
+ describe('includeNestedResults=true', () => {
3930
+ it('should iterate through all the files and nested files under the current folder one at a time', async () => {
3931
+ const visitedFiles = [];
3932
+ const result = await iterateStorageListFilesByEachFile({
3933
+ folder: existsFolder,
3934
+ includeNestedResults: true,
3935
+ readItemsFromPageResult: x => x.result.files(),
3936
+ iterateEachPageItem: async file => {
3937
+ visitedFiles.push(file);
3938
+ }
3939
+ });
3940
+ const visitedFilePathStrings = visitedFiles.map(x => `${SLASH_PATH_SEPARATOR}${x.storagePath.pathString}`);
3941
+ expect(result).toBeDefined();
3942
+ expect(result.totalItemsLoaded).toBe(3);
3943
+ expect(result.totalItemsVisited).toBe(visitedFiles.length);
3944
+ expect(visitedFilePathStrings).toContain(existsFilePath);
3945
+ expect(visitedFilePathStrings).toContain(existsBFilePath);
3946
+ expect(visitedFilePathStrings).toContain(existsCFilePath);
3947
+ expect(visitedFilePathStrings).not.toContain(otherFolderFilePath);
3948
+ });
3949
+ });
3950
+ });
3951
+ });
3952
+ });
3953
+ });
3954
+ });
3955
+ }
3956
+
3957
+ export { MOCK_FIREBASE_MODEL_SERVICE_FACTORIES, MOCK_SYSTEM_STATE_TYPE, MockItemCollectionFixture, MockItemCollectionFixtureInstance, MockItemCollections, MockItemDocument, MockItemPrivateDocument, MockItemSettingsItemEnum, MockItemStorageFixture, MockItemStorageFixtureInstance, MockItemSubItemDeepDocument, MockItemSubItemDocument, MockItemUserDocument, RulesUnitTestFirebaseTestingContextFixture, RulesUnitTestTestFirebaseInstance, TESTING_AUTHORIZED_FIREBASE_USER_ID, TestFirebaseContextFixture, TestFirebaseInstance, TestFirebaseStorageContextFixture, TestFirebaseStorageInstance, TestFirestoreContextFixture, TestFirestoreInstance, allChildMockItemSubItemDeepsWithinMockItem, authorizedFirebaseFactory, authorizedTestWithMockItemCollection, authorizedTestWithMockItemStorage, changeFirestoreLogLevelBeforeAndAfterTests, clearTestFirestoreContextCollections, describeFirebaseStorageAccessorDriverTests, describeFirestoreAccessorDriverTests, describeFirestoreDocumentAccessorTests, describeFirestoreIterationTests, describeFirestoreQueryDriverTests, firebaseRulesUnitTestBuilder, makeMockItemCollections, makeRulesTestFirebaseStorageContext, makeRulesTestFirestoreContext, makeTestingFirebaseStorageAccesorDriver, makeTestingFirebaseStorageDrivers, makeTestingFirestoreAccesorDriver, makeTestingFirestoreDrivers, mockFirebaseModelServices, mockItemCollectionReference, mockItemConverter, mockItemFirebaseModelServiceFactory, mockItemFirestoreCollection, mockItemIdentity, mockItemPrivateCollectionReference, mockItemPrivateCollectionReferenceFactory, mockItemPrivateConverter, mockItemPrivateFirebaseModelServiceFactory, mockItemPrivateFirestoreCollection, mockItemPrivateFirestoreCollectionGroup, mockItemPrivateIdentity, mockItemSettingsItemDencoder, mockItemSubItemCollectionReference, mockItemSubItemCollectionReferenceFactory, mockItemSubItemConverter, mockItemSubItemDeepCollectionReference, mockItemSubItemDeepCollectionReferenceFactory, mockItemSubItemDeepConverter, mockItemSubItemDeepFirebaseModelServiceFactory, mockItemSubItemDeepFirestoreCollection, mockItemSubItemDeepFirestoreCollectionGroup, mockItemSubItemDeepIdentity, mockItemSubItemFirebaseModelServiceFactory, mockItemSubItemFirestoreCollection, mockItemSubItemFirestoreCollectionGroup, mockItemSubItemIdentity, mockItemSystemDataConverter, mockItemSystemStateFirebaseModelServiceFactory, mockItemSystemStateStoredDataConverterMap, mockItemUserAccessorFactory, mockItemUserCollectionName, mockItemUserCollectionReference, mockItemUserCollectionReferenceFactory, mockItemUserConverter, mockItemUserFirebaseModelServiceFactory, mockItemUserFirestoreCollection, mockItemUserFirestoreCollectionGroup, mockItemUserIdentifier, mockItemUserIdentity, mockItemWithTestValue, mockItemWithValue, testWithMockItemCollectionFixture, testWithMockItemStorageFixture };