@dereekb/firebase 12.7.0 → 13.0.1

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