@nu-art/firebase-backend 0.401.9 → 0.500.6

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 (56) hide show
  1. package/ModuleBE_Firebase.d.ts +15 -1
  2. package/ModuleBE_Firebase.js +39 -3
  3. package/auth/firebase-session.d.ts +12 -8
  4. package/auth/firebase-session.js +53 -15
  5. package/firestore/DocWrapper.d.ts +30 -0
  6. package/{firestore-v3/DocWrapperV3.js → firestore/DocWrapper.js} +30 -32
  7. package/firestore/FirestoreCollection.d.ts +141 -59
  8. package/firestore/FirestoreCollection.js +419 -147
  9. package/firestore/FirestoreInterface.d.ts +2 -3
  10. package/firestore/FirestoreInterface.js +1 -5
  11. package/firestore/FirestoreWrapperBE.d.ts +5 -6
  12. package/firestore/FirestoreWrapperBE.js +119 -9
  13. package/firestore/MongoCollection.d.ts +81 -0
  14. package/firestore/MongoCollection.js +426 -0
  15. package/firestore/MongoInterface.d.ts +18 -0
  16. package/firestore/MongoInterface.js +132 -0
  17. package/firestore/MongoWrapperBE.d.ts +18 -0
  18. package/firestore/MongoWrapperBE.js +95 -0
  19. package/firestore/consts.d.ts +23 -0
  20. package/firestore/consts.js +34 -0
  21. package/firestore/types.d.ts +6 -1
  22. package/firestore/types.js +0 -24
  23. package/{functions-v2 → functions}/ModuleBE_BaseFunction.d.ts +6 -3
  24. package/{functions-v2 → functions}/ModuleBE_BaseFunction.js +1 -0
  25. package/{functions-v2/ModuleBE_ExpressFunction_V2.d.ts → functions/ModuleBE_ExpressFunction_Class.d.ts} +5 -3
  26. package/{functions-v2/ModuleBE_ExpressFunction_V2.js → functions/ModuleBE_ExpressFunction_Class.js} +7 -3
  27. package/{functions-v2 → functions}/ModuleBE_FirebaseDBListener.d.ts +2 -1
  28. package/{functions-v2 → functions}/ModuleBE_FirebaseDBListener.js +1 -0
  29. package/{functions-v2 → functions}/ModuleBE_FirebaseScheduler.js +1 -0
  30. package/{functions-v2 → functions}/ModuleBE_FirestoreListener.d.ts +5 -1
  31. package/{functions-v2 → functions}/ModuleBE_FirestoreListener.js +1 -0
  32. package/{functions-v2 → functions}/ModuleBE_PubSubFunction.d.ts +5 -2
  33. package/{functions-v2 → functions}/ModuleBE_PubSubFunction.js +1 -0
  34. package/{functions-v2 → functions}/ModuleBE_StorageListener.js +1 -0
  35. package/index.d.ts +16 -13
  36. package/index.js +16 -13
  37. package/package.json +10 -7
  38. package/firestore/FirestoreTransaction.d.ts +0 -30
  39. package/firestore/FirestoreTransaction.js +0 -153
  40. package/firestore-v3/DocWrapperV3.d.ts +0 -32
  41. package/firestore-v3/FirestoreCollectionV3.d.ts +0 -154
  42. package/firestore-v3/FirestoreCollectionV3.js +0 -470
  43. package/firestore-v3/FirestoreInterfaceV3.d.ts +0 -10
  44. package/firestore-v3/FirestoreInterfaceV3.js +0 -107
  45. package/firestore-v3/FirestoreWrapperBEV3.d.ts +0 -16
  46. package/firestore-v3/FirestoreWrapperBEV3.js +0 -154
  47. package/firestore-v3/consts.d.ts +0 -13
  48. package/firestore-v3/consts.js +0 -18
  49. package/firestore-v3/types.d.ts +0 -6
  50. package/firestore-v3/types.js +0 -1
  51. package/functions/firebase-function.d.ts +0 -38
  52. package/functions/firebase-function.js +0 -53
  53. package/v1.d.ts +0 -21
  54. package/v1.js +0 -38
  55. /package/{functions-v2 → functions}/ModuleBE_FirebaseScheduler.d.ts +0 -0
  56. /package/{functions-v2 → functions}/ModuleBE_StorageListener.d.ts +0 -0
@@ -15,170 +15,442 @@
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */
18
- import { BadImplementationException, batchAction, exists, generateHex, StaticLogger } from '@nu-art/ts-common';
18
+ import { dbObjectToId } from '@nu-art/db-api-shared';
19
+ import { __stringify, _keys, ApiException, BadImplementationException, batchActionParallel, compare, Const_UniqueKeys, CustomException, DB_Object_validator, dbIdLength, deepClone, DefaultDBVersion, Exception, exists, filterDuplicates, filterInstances, generateHex, keepPartialObject, Logger, MUSTNeverHappenException, StaticLogger, tsValidateResult, tsValidateUniqueId, ValidationException } from '@nu-art/ts-common';
19
20
  import { FirestoreInterface } from './FirestoreInterface.js';
20
- import { FirestoreTransaction } from './FirestoreTransaction.js';
21
+ import { DocWrapper } from './DocWrapper.js';
22
+ import { composeDbObjectUniqueId } from '@nu-art/firebase-shared';
23
+ import { _EmptyQuery, maxBatch } from '@nu-art/firebase-shared';
24
+ import { HttpCodes } from '@nu-art/ts-common/core/exceptions/http-codes';
25
+ import { addDeletedToTransaction, getActiveTransaction } from './consts.js';
26
+ const defaultMultiWriteType = 'batch';
21
27
  /**
22
- * FirestoreCollection is a class for handling Firestore collections. It takes in the name, FirestoreWrapperBE instance, and uniqueKeys as parameters.
28
+ * # <ins>FirestoreBulkException</ins>
29
+ * @category Exceptions
23
30
  */
24
- export class FirestoreCollection {
25
- name;
31
+ export class FirestoreBulkException extends CustomException {
32
+ causes;
33
+ constructor(causes) {
34
+ super(FirestoreBulkException, __stringify(causes?.map(_err => _err.message)));
35
+ this.causes = causes;
36
+ }
37
+ }
38
+ /**
39
+ * If one of the validators is a function, returns an array of functions.
40
+ * If both validators are objects, returns a merged object.
41
+ */
42
+ const getDbDefValidator = (dbDef) => {
43
+ if (typeof dbDef.modifiablePropsValidator === 'object' && typeof dbDef.generatedPropsValidator === 'object')
44
+ return { ...dbDef.generatedPropsValidator, ...dbDef.modifiablePropsValidator, ...DB_Object_validator };
45
+ if (typeof dbDef.modifiablePropsValidator === 'function' && typeof dbDef.generatedPropsValidator === 'function')
46
+ return [dbDef.modifiablePropsValidator, dbDef.generatedPropsValidator];
47
+ if (typeof dbDef.modifiablePropsValidator === 'function')
48
+ return [dbDef.modifiablePropsValidator, (instance) => {
49
+ const partialInstance = keepPartialObject(instance, _keys(dbDef.generatedPropsValidator));
50
+ return tsValidateResult(partialInstance, dbDef.generatedPropsValidator);
51
+ }];
52
+ return [dbDef.generatedPropsValidator, (instance) => {
53
+ return tsValidateResult(keepPartialObject(instance, _keys(dbDef.modifiablePropsValidator)), dbDef.modifiablePropsValidator);
54
+ }];
55
+ };
56
+ /**
57
+ * FirestoreCollection is a class for handling Firestore collections.
58
+ */
59
+ export class FirestoreCollection extends Logger {
26
60
  wrapper;
27
61
  collection;
28
- /**
29
- * External unique as in there must never ever be two that answer the same query
30
- */
31
- externalUniqueFilter;
32
- /**
33
- * @param name
34
- * @param wrapper
35
- * @param uniqueKeys
36
- */
37
- constructor(name, wrapper, uniqueKeys) {
38
- this.name = name;
62
+ dbDef;
63
+ uniqueKeys;
64
+ validator;
65
+ hooks;
66
+ constructor(wrapper, _dbDef, hooks) {
67
+ super();
39
68
  this.wrapper = wrapper;
40
- if (!/[a-z-]{3,}/.test(name))
69
+ if (!/[a-z-]{3,}/.test(_dbDef.backend.name))
41
70
  StaticLogger.logWarning('Please follow name pattern for collections /[a-z-]{3,}/');
42
- this.collection = wrapper.firestore.collection(name);
43
- this.externalUniqueFilter = (instance) => {
44
- if (!uniqueKeys)
45
- throw new BadImplementationException('In order to use a unique query your collection MUST have a unique filter');
46
- return uniqueKeys.reduce((where, key) => {
47
- if (!exists(instance[key]))
48
- throw new BadImplementationException(`No where properties are allowed to be null or undefined.\nWhile querying collection '${this.name}' we found property '${String(key)}' to be '${where[key]}'`);
49
- where[key] = instance[key];
50
- return where;
51
- }, {});
52
- };
71
+ this.collection = wrapper.firestore.collection(_dbDef.backend.name);
72
+ this.dbDef = _dbDef;
73
+ this.uniqueKeys = this.dbDef.uniqueKeys || Const_UniqueKeys;
74
+ this.validator = getDbDefValidator(_dbDef);
75
+ this.hooks = hooks;
53
76
  }
54
- /**
55
- Executes a Firestore query on the collection.
56
- @param ourQuery - The query to execute.
57
- @returns A Promise that resolves to an array of FirestoreType_DocumentSnapshot objects.
58
- @private
59
- */
60
- async _query(ourQuery) {
61
- const myQuery = FirestoreInterface.buildQuery(this, ourQuery);
62
- return (await myQuery.get()).docs;
77
+ doc = Object.freeze({
78
+ _: (ref, data) => {
79
+ if (tsValidateResult(ref.id, tsValidateUniqueId))
80
+ throw new MUSTNeverHappenException(`Tackled a docRef with id that is an invalid UniqueId: '${ref.id}'`);
81
+ // @ts-ignore
82
+ return new DocWrapper(this, ref, data);
83
+ },
84
+ unique: (id) => {
85
+ if (!id)
86
+ throw new MUSTNeverHappenException('Did not receive an _id at doc.unique!');
87
+ let idStr;
88
+ if (typeof id !== 'string')
89
+ idStr = assertUniqueId(id, this.uniqueKeys);
90
+ else
91
+ idStr = id;
92
+ const doc = this.wrapper.firestore.doc(`${this.collection.path}/${idStr}`);
93
+ return this.doc._(doc);
94
+ },
95
+ item: (item) => {
96
+ item._id = assertUniqueId(item, this.uniqueKeys);
97
+ return this.doc.unique(item._id);
98
+ },
99
+ all: (_ids) => _ids.map(this.doc.unique),
100
+ allItems: (preDBItems) => {
101
+ return preDBItems.map(preDBItem => this.doc.item(preDBItem));
102
+ },
103
+ query: async (query) => {
104
+ return (await this._customQuery(query, true)).map(_snapshot => this.doc._(_snapshot.ref, _snapshot.data()));
105
+ },
106
+ unManipulatedQuery: async (query) => {
107
+ return (await this._customQuery(query, false)).map(_snapshot => this.doc._(_snapshot.ref, _snapshot.data()));
108
+ },
109
+ });
110
+ getAll = async (docs) => {
111
+ if (docs.length === 0)
112
+ return [];
113
+ const transaction = getActiveTransaction();
114
+ return (await (transaction ?? this.wrapper.firestore).getAll(...docs.map(_doc => _doc.ref))).map(_snapshot => _snapshot.data());
115
+ };
116
+ _customQuery = async (tsQuery, canManipulateQuery) => {
117
+ if (canManipulateQuery)
118
+ tsQuery = this.hooks?.manipulateQuery?.(deepClone(tsQuery)) ?? tsQuery;
119
+ const firestoreQuery = FirestoreInterface.buildQuery(this, tsQuery);
120
+ const transaction = getActiveTransaction();
121
+ if (transaction)
122
+ return (await transaction.get(firestoreQuery)).docs;
123
+ return (await firestoreQuery.get()).docs;
124
+ };
125
+ query = Object.freeze({
126
+ unique: async (_id) => await this.doc.unique(_id).get(),
127
+ uniqueAssert: async (_id) => {
128
+ const resultItem = await this.query.unique(_id);
129
+ if (!resultItem)
130
+ throw new ApiException(404, `Could not find ${this.dbDef.entityName} with _id: ${__stringify(_id)}`);
131
+ return resultItem;
132
+ },
133
+ uniqueWhere: async (where) => this.query.uniqueCustom({ where }),
134
+ uniqueCustom: async (query) => {
135
+ const thisShouldBeOnlyOne = await this.query.custom(query);
136
+ if (thisShouldBeOnlyOne.length === 0)
137
+ throw new ApiException(404, `Could not find ${this.dbDef.entityName} with unique query: ${JSON.stringify(query)}`);
138
+ if (thisShouldBeOnlyOne.length > 1)
139
+ throw new BadImplementationException(`Too many results (${thisShouldBeOnlyOne.length}) in collection (${this.dbDef.dbKey}) for query: ${__stringify(query)}`);
140
+ return thisShouldBeOnlyOne[0];
141
+ },
142
+ all: async (_ids) => await this.getAll(this.doc.all(_ids)),
143
+ custom: async (query) => {
144
+ return (await this._customQuery(query, true)).map(snapshot => snapshot.data());
145
+ },
146
+ where: async (where) => {
147
+ return this.query.custom({ where });
148
+ },
149
+ unManipulatedQuery: async (query) => {
150
+ return (await this._customQuery(query, false)).map(snapshot => snapshot.data());
151
+ },
152
+ });
153
+ uniqueGetOrCreate = async (where, toCreate) => {
154
+ try {
155
+ return await this.query.uniqueWhere(where);
156
+ }
157
+ catch (e) {
158
+ return toCreate();
159
+ }
160
+ };
161
+ _createAll = async (preDBItems, multiWriteType = defaultMultiWriteType) => {
162
+ if (preDBItems.length === 1)
163
+ return [await this.create.item(preDBItems[0])];
164
+ const transaction = getActiveTransaction();
165
+ const docs = this.doc.allItems(preDBItems);
166
+ const dbItems = await Promise.all(docs.map((doc, i) => doc.prepareForCreate(preDBItems[i])));
167
+ this.assertNoDuplicatedIds(dbItems, 'create.all');
168
+ if (transaction)
169
+ docs.forEach((doc, i) => transaction.create(doc.ref, dbItems[i]));
170
+ else
171
+ await this.multiWrite(multiWriteType, docs, 'create', dbItems);
172
+ await this.hooks?.postWriteProcessing?.({ updated: dbItems }, 'create');
173
+ return dbItems;
174
+ };
175
+ create = Object.freeze({
176
+ item: async (preDBItem) => await this.doc.item(preDBItem)
177
+ .create(preDBItem),
178
+ all: this._createAll,
179
+ });
180
+ _setAll = async (items, multiWriteType = defaultMultiWriteType, performUpgrade = true) => {
181
+ const transaction = getActiveTransaction();
182
+ const docs = this.doc.allItems(items);
183
+ const dbItems = await this.getAll(docs);
184
+ const preparedItems = await Promise.all(dbItems.map(async (_dbItem, i) => {
185
+ return !exists(_dbItem)
186
+ ? await docs[i].prepareForCreate(items[i], performUpgrade)
187
+ : await docs[i].prepareForSet(items[i], _dbItem, performUpgrade);
188
+ }));
189
+ this.assertNoDuplicatedIds(preparedItems, 'set.all');
190
+ if (transaction)
191
+ docs.map((doc, i) => transaction.set(doc.ref, preparedItems[i]));
192
+ else
193
+ await this.multiWrite(multiWriteType, docs, 'set', preparedItems);
194
+ if (preparedItems.length)
195
+ await this.hooks?.postWriteProcessing?.({ before: dbItems, updated: preparedItems }, 'set');
196
+ return preparedItems;
197
+ };
198
+ set = Object.freeze({
199
+ item: async (preDBItem) => {
200
+ return await this.doc.item(preDBItem).set(preDBItem);
201
+ },
202
+ all: (items) => {
203
+ if (getActiveTransaction())
204
+ return this._setAll(items);
205
+ return this.runTransactionInChunks(items, (chunk) => this._setAll(chunk));
206
+ },
207
+ /**
208
+ * Multi is a non atomic operation
209
+ */
210
+ multi: (items, multiWriteType = defaultMultiWriteType) => {
211
+ return this._setAll(items, multiWriteType);
212
+ },
213
+ });
214
+ // @ts-ignore
215
+ upgradeInstances = (items) => {
216
+ return this._setAll(items, defaultMultiWriteType, false);
217
+ };
218
+ _updateAll = async (updateData, multiWriteType = defaultMultiWriteType) => {
219
+ const docs = this.doc.all(updateData.map(_data => _data._id));
220
+ const toUpdate = await Promise.all(docs.map(async (_doc, i) => await _doc.prepareForUpdate(updateData[i])));
221
+ await this.multiWrite(multiWriteType, docs, 'update', toUpdate);
222
+ const dbItems = await this.getAll(docs);
223
+ await this.hooks?.postWriteProcessing?.({ updated: dbItems }, 'update');
224
+ return dbItems;
225
+ };
226
+ async validateUpdateData(updateData) {
63
227
  }
228
+ // update = Object.freeze({
229
+ // item: (updateData: UpdateObject<Proto['dbType']>) => this.doc.unique(updateData._id).update(updateData),
230
+ // all: this._updateAll,
231
+ // });
232
+ _deleteQuery = async (query, multiWriteType = defaultMultiWriteType) => {
233
+ if (!exists(query) || compare(query, _EmptyQuery))
234
+ throw new MUSTNeverHappenException('An empty query was passed to delete.query!');
235
+ const docsToBeDeleted = await this.doc.query(query);
236
+ const itemsToReturn = docsToBeDeleted.map(doc => doc.data);
237
+ await this._deleteAll(docsToBeDeleted, multiWriteType);
238
+ return itemsToReturn;
239
+ };
240
+ _deleteUnManipulatedQuery = async (query, multiWriteType = defaultMultiWriteType) => {
241
+ if (!exists(query) || compare(query, _EmptyQuery))
242
+ throw new MUSTNeverHappenException('An empty query was passed to delete.query!');
243
+ const docsToBeDeleted = await this.doc.unManipulatedQuery(query);
244
+ const itemsToReturn = docsToBeDeleted.map(doc => doc.data);
245
+ await this._deleteAll(docsToBeDeleted, multiWriteType);
246
+ return itemsToReturn;
247
+ };
248
+ _deleteAll = async (docsToBeDeleted, multiWriteType = defaultMultiWriteType) => {
249
+ const transaction = getActiveTransaction();
250
+ const dbItems = filterInstances(await this.getAll(docsToBeDeleted));
251
+ const itemsToCheck = dbItems.filter((item, index) => docsToBeDeleted[index].ref.id == item._id);
252
+ addDeletedToTransaction({
253
+ dbKey: this.dbDef.dbKey,
254
+ ids: dbItems.map(dbObjectToId)
255
+ });
256
+ await this.hooks?.canDeleteItems(itemsToCheck);
257
+ if (transaction)
258
+ docsToBeDeleted.map(async (doc) => transaction.delete(doc.ref));
259
+ else
260
+ await this.multiWrite(multiWriteType, docsToBeDeleted, 'delete');
261
+ await this.hooks?.postWriteProcessing?.({ deleted: dbItems }, 'delete');
262
+ return dbItems;
263
+ };
264
+ deleteCollection = async () => {
265
+ const refs = await this.collection.listDocuments();
266
+ const bulk = this.wrapper.firestore.bulkWriter();
267
+ refs.forEach(_ref => bulk.delete(_ref));
268
+ // deleted: null means that the whole collection has been deleted
269
+ await this.hooks?.postWriteProcessing?.({ deleted: null }, 'delete');
270
+ await bulk.close();
271
+ };
272
+ delete = Object.freeze({
273
+ unique: async (id) => await this.doc.unique(id).delete(),
274
+ item: async (item) => await this.doc.item(item).delete(),
275
+ all: async (ids) => {
276
+ if (!getActiveTransaction())
277
+ return this.runTransactionInChunks(ids, (chunk) => this.delete.all(chunk));
278
+ return this._deleteAll(ids.map(id => this.doc.unique(id)));
279
+ },
280
+ allDocs: async (docs) => {
281
+ if (!getActiveTransaction())
282
+ return this.runTransactionInChunks(docs, (chunk) => this.delete.allDocs(chunk));
283
+ return await this._deleteAll(docs);
284
+ },
285
+ allItems: async (items) => {
286
+ if (!getActiveTransaction())
287
+ return this.runTransactionInChunks(items, (chunk) => this.delete.allItems(chunk));
288
+ return await this._deleteAll(items.map(_item => this.doc.item(_item)));
289
+ },
290
+ query: async (query) => {
291
+ if (!getActiveTransaction()) {
292
+ if (!exists(query) || compare(query, _EmptyQuery))
293
+ throw new MUSTNeverHappenException('An empty query was passed to delete.query!');
294
+ const docs = await this.doc.query(query);
295
+ const items = docs.map(doc => doc.data);
296
+ await this.runTransactionInChunks(docs, (chunk) => this._deleteAll(chunk));
297
+ return items;
298
+ }
299
+ return await this._deleteQuery(query);
300
+ },
301
+ unManipulatedQuery: async (query) => {
302
+ if (!getActiveTransaction()) {
303
+ if (!exists(query) || compare(query, _EmptyQuery))
304
+ throw new MUSTNeverHappenException('An empty query was passed to delete.query!');
305
+ const docs = await this.doc.unManipulatedQuery(query);
306
+ const items = docs.map(doc => doc.data);
307
+ await this.runTransactionInChunks(docs, (chunk) => this._deleteAll(chunk));
308
+ return items;
309
+ }
310
+ return await this._deleteUnManipulatedQuery(query);
311
+ },
312
+ where: async (where) => {
313
+ return this.delete.query({ where });
314
+ },
315
+ /**
316
+ * Multi is a non atomic operation — doesn't use transactions. Use 'all' variants for transaction.
317
+ */
318
+ multi: {
319
+ all: async (ids, multiWriteType = defaultMultiWriteType) => await this._deleteAll(ids.map(id => this.doc.unique(id)), multiWriteType),
320
+ items: async (items, multiWriteType = defaultMultiWriteType) => await this._deleteAll(items.map(_item => this.doc.item(_item)), multiWriteType),
321
+ allDocs: async (docs, multiWriteType = defaultMultiWriteType) => await this._deleteAll(docs, multiWriteType),
322
+ query: async (query, multiWriteType = defaultMultiWriteType) => await this._deleteQuery(query, multiWriteType)
323
+ },
324
+ yes: { iam: { sure: { iwant: { todelete: { the: { collection: { delete: this.deleteCollection } } } } } } }
325
+ });
64
326
  /**
65
-
66
- Executes a unique Firestore query on the collection.
67
- @param ourQuery - The query to execute.
68
- @returns A Promise that resolves to a single FirestoreType_DocumentSnapshot object, or undefined if no match is found.
69
- @private
327
+ * @param writer Type of BulkWriter - can be Bulk writer or Batch writer
328
+ * @param doc
329
+ * @param operation create/update/set/delete
330
+ * @param item - mandatory for everything but delete
70
331
  */
71
- async _queryUnique(ourQuery) {
72
- const results = await this._query(ourQuery);
73
- return FirestoreInterface.assertUniqueDocument(results, ourQuery, this.name);
74
- }
332
+ addToMultiWrite = (writer, doc, operation, item) => {
333
+ switch (operation) {
334
+ case 'create':
335
+ writer.create(doc.ref, item);
336
+ break;
337
+ case 'set':
338
+ // @ts-ignore
339
+ writer.set(doc.ref, item);
340
+ break;
341
+ case 'update':
342
+ // @ts-ignore
343
+ writer.update(doc.ref, item);
344
+ break;
345
+ case 'delete':
346
+ writer.delete(doc.ref);
347
+ break;
348
+ }
349
+ return item;
350
+ };
351
+ multiWrite = async (type, docs, operation, items) => {
352
+ if (type === 'bulk')
353
+ return this.bulkWrite(docs, operation, items);
354
+ if (type === 'batch')
355
+ return this.batchWrite(docs, operation, items);
356
+ throw new Exception(`Unknown type passed to multiWrite: ${type}`);
357
+ };
358
+ bulkWrite = async (docs, operation, items) => {
359
+ const bulk = this.wrapper.firestore.bulkWriter();
360
+ const errors = [];
361
+ bulk.onWriteError(error => {
362
+ errors.push(error);
363
+ return false;
364
+ });
365
+ docs.forEach((doc, index) => this.addToMultiWrite(bulk, doc, operation, items?.[index]));
366
+ await bulk.close();
367
+ if (errors.length)
368
+ throw new FirestoreBulkException(errors);
369
+ };
75
370
  /**
76
- Executes a unique Firestore query on the collection and returns the matching object.
77
- @param ourQuery - The query to execute.
78
- @returns A Promise that resolves to the matching object, or undefined if no match is found.
371
+ * @param docs docs to write to
372
+ * @param operation create/update/set/delete
373
+ * @param items mandatory for everything but delete
79
374
  */
80
- async queryUnique(ourQuery) {
81
- const doc = await this._queryUnique(ourQuery);
82
- if (!doc)
83
- return;
84
- return doc.data();
85
- }
86
- async insert(instance, _id) {
87
- await this.createDocumentReference(_id).set(instance);
88
- return instance;
89
- }
90
- async insertAll(instances) {
91
- return await Promise.all(instances.map(instance => this.insert(instance)));
92
- }
93
- async query(ourQuery) {
94
- return (await this._query(ourQuery)).map(result => result.data());
95
- }
96
- async upsert(instance) {
97
- return this.runInTransaction((transaction) => transaction.upsert(this, instance));
98
- }
99
- async upsertAll(instances) {
100
- return batchAction(instances, 500, async (chunked) => this.runInTransaction(transaction => transaction.upsertAll(this, chunked)));
101
- }
102
- async patch(instance) {
103
- return this.runInTransaction(transaction => transaction.patch(this, instance));
104
- }
105
- async deleteItem(instance) {
106
- return this.runInTransaction((transaction) => transaction.deleteItem(this, instance));
107
- }
108
- async deleteUnique(query) {
109
- return this.runInTransaction((transaction) => transaction.deleteUnique(this, query));
110
- }
111
- async delete(query) {
112
- const docRefs = await this._query(query);
113
- return this.deleteBatch(docRefs);
114
- }
115
- async deleteBatch(docRefs) {
116
- return await batchAction(docRefs, 200, async (chunk) => {
375
+ batchWrite = async (docs, operation, items) => {
376
+ for (let batchIndex = 0; batchIndex < docs.length; batchIndex += maxBatch) {
117
377
  const batch = this.wrapper.firestore.batch();
118
- const toRet = [];
119
- await chunk.reduce((_batch, val) => {
120
- toRet.push(val.data());
121
- return _batch.delete(val.ref);
122
- }, batch).commit();
123
- return toRet;
124
- });
125
- }
126
- async deleteAll() {
127
- const docRefs = await this._query();
128
- return this.deleteBatch(docRefs);
129
- }
130
- async getAll(select) {
131
- return this.query({ select });
132
- }
133
- async runInTransaction(processor) {
134
- const firestore = this.wrapper.firestore;
135
- return firestore.runTransaction(async (transaction) => {
136
- return processor(new FirestoreTransaction(transaction));
137
- });
378
+ const chunk = docs.slice(batchIndex, batchIndex + maxBatch);
379
+ chunk.map((_doc, index) => this.addToMultiWrite(batch, _doc, operation, items?.[batchIndex + index]));
380
+ await batch.commit();
381
+ }
382
+ };
383
+ /**
384
+ * Runs the processor within a Firestore transaction scope. If already inside a transaction
385
+ * (detected via MemKey), the existing transaction is reused. Otherwise a new one is created.
386
+ */
387
+ runTransaction = async (processor) => {
388
+ return this.wrapper.runTransaction(processor);
389
+ };
390
+ runTransactionInChunks = async (items, processor, chunkSize = maxBatch) => {
391
+ return batchActionParallel(items, chunkSize, (chunk) => this.runTransaction(() => processor(chunk)));
392
+ };
393
+ getVersion = () => {
394
+ return this.dbDef.versions?.[0] || DefaultDBVersion;
395
+ };
396
+ needsUpgrade = (version) => {
397
+ const versions = this.dbDef.versions || [DefaultDBVersion];
398
+ if (!version)
399
+ return false;
400
+ const index = versions.indexOf(version);
401
+ if (index === -1)
402
+ throw HttpCodes._4XX.BAD_REQUEST('Invalid Object Version', `Provided item with version(${version}) which doesn't exist for collection '${this.dbDef.dbKey} (${__stringify(this.dbDef.versions)})' `);
403
+ return index !== 0;
404
+ };
405
+ validateItem(dbItem) {
406
+ const results = tsValidateResult(dbItem, this.validator);
407
+ if (results) {
408
+ this.onValidationError(dbItem, results);
409
+ }
138
410
  }
139
- createDocumentReference(_id) {
140
- const id = _id || generateHex(16);
141
- return this.wrapper.firestore.doc(`${this.name}/${id}`);
411
+ onValidationError(instance, results) {
412
+ StaticLogger.logError(`error validating ${this.dbDef.entityName}:`, instance, 'With Error: ', results);
413
+ // console.error(`error validating ${this.dbDef.entityName}:`, instance, 'With Error: ', results);
414
+ // const errorBody = {type: 'bad-input', body: {result: results, input: instance}};
415
+ const validationException = new ValidationException(`error validating ${this.dbDef.entityName}`, instance, results);
416
+ throw new ApiException(HttpCodes._4XX.FAILED_VALIDATION.code, `error validating ${this.dbDef.entityName}`).setErrorBody(validationException);
142
417
  }
143
- getUniqueFilter = () => this.externalUniqueFilter;
144
- async newQueryUnique(ourQuery) {
145
- const doc = await this._queryUnique(ourQuery);
146
- if (!doc || !doc.exists)
418
+ assertNoDuplicatedIds(items, originFunctionName) {
419
+ if (filterDuplicates(items, dbObjectToId).length === items.length)
147
420
  return;
148
- return new DocWrapper(this.wrapper, doc);
149
- }
150
- async newQuery(ourQuery) {
151
- const docs = await this._query(ourQuery);
152
- return docs.filter(doc => doc.exists).map(doc => new DocWrapper(this.wrapper, doc));
153
- }
154
- }
155
- export class DocWrapper {
156
- wrapper;
157
- doc;
158
- constructor(wrapper, doc) {
159
- this.wrapper = wrapper;
160
- this.doc = doc;
161
- }
162
- async runInTransaction(processor) {
163
- const firestore = this.wrapper.firestore;
164
- return firestore.runTransaction(processor);
421
+ const idCountMap = items.reduce((countMap, item) => {
422
+ // Count the number of appearances of each _id
423
+ countMap[item._id] = !exists(countMap[item._id]) ? 1 : 1 + countMap[item._id];
424
+ return countMap;
425
+ }, {});
426
+ // DEBUG - print the duplicate _ids
427
+ // _keys(idCountMap).forEach(key => {
428
+ // if (idCountMap[key] > 1)
429
+ // this.logWarning(`${idCountMap[key]} times ${key}`);
430
+ // });
431
+ // Throw exception if an _id appears more than once
432
+ _keys(idCountMap).forEach(key => {
433
+ if (idCountMap[key] === 1)
434
+ delete idCountMap[key];
435
+ });
436
+ throw new BadImplementationException(`${originFunctionName} received the same _id twice: ${__stringify(idCountMap, true)}`);
165
437
  }
166
- delete = async (transaction) => {
167
- if (!transaction) {
168
- // const item = this.doc.data(); TODO: TBD do we need a create and run in transaction for each delete??
169
- // this.doc.ref.delete();
170
- // return item;
171
- return this.runInTransaction(this.delete);
172
- }
173
- const item = this.get();
174
- transaction.delete(this.doc.ref);
175
- return item;
176
- };
177
- get = () => this.doc.data();
178
- set = async (instance, transaction) => {
179
- if (!transaction)
180
- return this.runInTransaction((_transaction) => this.set(instance, _transaction));
181
- transaction.set(this.doc.ref, instance);
182
- return instance;
438
+ composeDbObjectUniqueId = (item) => {
439
+ return composeDbObjectUniqueId(item, this.uniqueKeys);
183
440
  };
184
441
  }
442
+ /**
443
+ * If the collection has unique keys, assert they exist, and use them to generate the _id.
444
+ * In the case an _id already exists, verify it is not different from the uniqueKeys-generated _id.
445
+ */
446
+ export const assertUniqueId = (item, keys) => {
447
+ // If there are no specific uniqueKeys, generate a random _id.
448
+ if (compare(keys, Const_UniqueKeys))
449
+ return (item._id ?? generateHex(dbIdLength));
450
+ const _id = composeDbObjectUniqueId(item, keys);
451
+ // If the item has an _id, and it matches the uniqueKeys-generated _id, all is well.
452
+ // If the uniqueKeys-generated _id doesn't match the existing _id, this means someone had changed the uniqueKeys or _id which must never happen.
453
+ if (exists(item._id) && _id !== item._id)
454
+ throw new MUSTNeverHappenException(`When checking the existing _id, it did not match the _id composed from the unique keys! \n expected: ${_id} \n actual: ${item._id}`);
455
+ return _id;
456
+ };
@@ -1,11 +1,10 @@
1
1
  import { FirestoreQuery } from '@nu-art/firebase-shared';
2
2
  import { FirestoreType_DocumentSnapshot } from './types.js';
3
3
  import { FirestoreCollection } from './FirestoreCollection.js';
4
- import { TS_Object } from '@nu-art/ts-common';
4
+ import { DB_Prototype } from '@nu-art/db-api-shared';
5
5
  import { Query } from 'firebase-admin/firestore';
6
6
  export declare class FirestoreInterface {
7
- static buildQuery<Type extends TS_Object>(collection: FirestoreCollection<Type>, query?: FirestoreQuery<Type>): Query<FirebaseFirestore.DocumentData, FirebaseFirestore.DocumentData>;
7
+ static buildQuery<Proto extends DB_Prototype>(collection: FirestoreCollection<Proto>, query?: FirestoreQuery<Proto['dbType']>): Query<FirebaseFirestore.DocumentData, FirebaseFirestore.DocumentData>;
8
8
  private static isQueryObject;
9
9
  static assertUniqueDocument(results: FirestoreType_DocumentSnapshot[], query: FirestoreQuery<any>, collectionName: string): (FirestoreType_DocumentSnapshot | undefined);
10
- static buildUniqueQuery<Type extends TS_Object>(collection: FirestoreCollection<Type>, instance: Type): FirestoreQuery<Type>;
11
10
  }
@@ -73,7 +73,7 @@ export class FirestoreInterface {
73
73
  const page = query.limit.page || 0;
74
74
  // console.log(`limit: ${query.limit.itemsCount} * ${page}`);
75
75
  if (page > 0)
76
- myQuery = myQuery.offset(query.limit.itemsCount * page + 1);
76
+ myQuery = myQuery.offset(query.limit.itemsCount * page);
77
77
  myQuery = myQuery.limit(query.limit.itemsCount);
78
78
  }
79
79
  return myQuery;
@@ -104,8 +104,4 @@ export class FirestoreInterface {
104
104
  return;
105
105
  return results[0];
106
106
  }
107
- static buildUniqueQuery(collection, instance) {
108
- const where = collection.externalUniqueFilter(instance);
109
- return { where };
110
- }
111
107
  }
@@ -1,16 +1,15 @@
1
- import { FirestoreCollection } from './FirestoreCollection.js';
1
+ import { FirestoreCollectionHooks, FirestoreCollection } from './FirestoreCollection.js';
2
2
  import { FirestoreType, FirestoreType_Collection } from './types.js';
3
- import { FilterKeys } from '@nu-art/firebase-shared';
4
3
  import { FirebaseSession } from '../auth/firebase-session.js';
5
4
  import { FirebaseBaseWrapper } from '../auth/FirebaseBaseWrapper.js';
6
- import { DB_Object, TS_Object } from '@nu-art/ts-common';
5
+ import { Database, DB_Prototype } from '@nu-art/db-api-shared';
7
6
  export declare class FirestoreWrapperBE extends FirebaseBaseWrapper {
8
7
  readonly firestore: FirestoreType;
9
8
  private readonly collections;
10
9
  constructor(firebaseSession: FirebaseSession<any>, dbName?: string);
11
- getCollection<Type extends TS_Object>(name: string, uniqueKeys?: FilterKeys<Type>): FirestoreCollection<Type>;
12
- listen<Type extends DB_Object>(collection: FirestoreCollection<Type>, doc: string): void;
13
- deleteCollection(name: string): Promise<TS_Object[]>;
10
+ runTransaction: <ReturnType>(processor: () => Promise<ReturnType>) => Promise<ReturnType>;
11
+ getCollection<Proto extends DB_Prototype>(dbDef: Database<Proto>, hooks?: FirestoreCollectionHooks<Proto['dbType']>): FirestoreCollection<Proto>;
12
+ listen<Proto extends DB_Prototype>(collection: FirestoreCollection<Proto>, doc: string): void;
14
13
  listCollections(): Promise<FirestoreType_Collection[]>;
15
14
  isEmulator(): boolean;
16
15
  }