@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
@@ -17,7 +17,10 @@
17
17
  */
18
18
  import { FirestoreCollection, } from './FirestoreCollection.js';
19
19
  import { FirebaseBaseWrapper } from '../auth/FirebaseBaseWrapper.js';
20
- import { getFirestore } from 'firebase-admin/firestore';
20
+ import { Promise_all_sequentially } from '@nu-art/ts-common';
21
+ import { DocumentReference, DocumentSnapshot, getFirestore, QuerySnapshot, } from 'firebase-admin/firestore';
22
+ import { getActiveTransaction, MemKey_FirestoreTransaction } from './consts.js';
23
+ import { MemStorage } from '@nu-art/ts-common/mem-storage';
21
24
  export class FirestoreWrapperBE extends FirebaseBaseWrapper {
22
25
  firestore;
23
26
  collections = {};
@@ -28,20 +31,127 @@ export class FirestoreWrapperBE extends FirebaseBaseWrapper {
28
31
  else
29
32
  this.firestore = getFirestore(firebaseSession.app);
30
33
  }
31
- getCollection(name, uniqueKeys) {
32
- const collection = this.collections[name];
34
+ runTransaction = async (processor) => {
35
+ if (getActiveTransaction())
36
+ return processor();
37
+ const postTransactionActions = [];
38
+ const toRet = await this.firestore.runTransaction(async (transaction) => {
39
+ const writeActions = [];
40
+ const transactionUpdates = {};
41
+ // @ts-ignore
42
+ transaction.postTransaction = (action) => {
43
+ return postTransactionActions.push(action);
44
+ };
45
+ // @ts-ignore
46
+ transaction.__nu_art__WriteActions = writeActions;
47
+ const originSet = transaction.set.bind(transaction);
48
+ const originDelete = transaction.delete.bind(transaction);
49
+ const originCreate = transaction.create.bind(transaction);
50
+ const originGet = transaction.get.bind(transaction);
51
+ const originGetAll = transaction.getAll.bind(transaction);
52
+ const getMockDocumentSnapshot = (data, _id, exists = true) => {
53
+ return {
54
+ id: _id,
55
+ ref: {},
56
+ exists,
57
+ data: () => (exists ? data : undefined),
58
+ get: (fieldPath) => data[fieldPath],
59
+ metadata: {},
60
+ };
61
+ };
62
+ const updateTransactionUpdates = async (data, exists = true) => {
63
+ let _data;
64
+ let _id;
65
+ if (data instanceof DocumentReference) {
66
+ _id = data.id;
67
+ _data = (await data.get()).data();
68
+ }
69
+ else {
70
+ _id = data._id;
71
+ _data = data;
72
+ }
73
+ transactionUpdates[_id] = getMockDocumentSnapshot(_data, _id, exists);
74
+ };
75
+ transaction.set = (documentRef, data) => {
76
+ updateTransactionUpdates(data);
77
+ writeActions.push(() => originSet(documentRef, data));
78
+ return transaction;
79
+ };
80
+ transaction.create = (documentRef, data) => {
81
+ updateTransactionUpdates(data);
82
+ writeActions.push(() => originCreate(documentRef, data));
83
+ return transaction;
84
+ };
85
+ transaction.delete = (documentRef, precondition) => {
86
+ updateTransactionUpdates(documentRef, false);
87
+ writeActions.push(() => originDelete(documentRef, precondition));
88
+ return transaction;
89
+ };
90
+ // @ts-ignore
91
+ transaction.get = async (args) => {
92
+ // @ts-ignore
93
+ const doc = await originGet(args);
94
+ if (doc instanceof DocumentSnapshot) {
95
+ const document = doc.data();
96
+ if (!document)
97
+ return doc;
98
+ return transactionUpdates[document._id] ?? doc;
99
+ }
100
+ if (doc instanceof QuerySnapshot) {
101
+ const docs = doc.docs;
102
+ // @ts-ignore
103
+ return {
104
+ docs: docs.map(doc => {
105
+ const _doc = doc.data();
106
+ if (!_doc)
107
+ return doc;
108
+ return transactionUpdates[_doc._id] ?? doc;
109
+ })
110
+ };
111
+ }
112
+ return doc;
113
+ };
114
+ // @ts-ignore
115
+ transaction.getAll = async (...documentRefsOrReadOptions) => {
116
+ const docs = await originGetAll(...documentRefsOrReadOptions);
117
+ return docs.map(doc => {
118
+ const _doc = doc.data();
119
+ if (!_doc)
120
+ return doc;
121
+ return transactionUpdates[_doc._id] ?? doc;
122
+ });
123
+ };
124
+ const parentStorage = MemStorage.getStore();
125
+ return new MemStorage().init(async () => {
126
+ const wrapper = { transaction, active: true };
127
+ MemKey_FirestoreTransaction.set(wrapper);
128
+ try {
129
+ const toRet = await processor();
130
+ writeActions.forEach(action => action());
131
+ return toRet;
132
+ }
133
+ finally {
134
+ wrapper.active = false;
135
+ }
136
+ }, parentStorage);
137
+ });
138
+ await Promise_all_sequentially(postTransactionActions);
139
+ return toRet;
140
+ };
141
+ getCollection(dbDef, hooks) {
142
+ const collection = this.collections[dbDef.dbKey];
33
143
  if (collection)
34
144
  return collection;
35
- return this.collections[name] = new FirestoreCollection(name, this, uniqueKeys);
145
+ return this.collections[dbDef.dbKey] = new FirestoreCollection(this, dbDef, hooks);
36
146
  }
37
147
  listen(collection, doc) {
38
- collection.wrapper.firestore.doc(`${collection.name}/${doc}`).onSnapshot(() => {
39
- this.logInfo('recieved snapshot!');
148
+ collection.wrapper.firestore.doc(`${collection.collection.path}/${doc}`).onSnapshot(() => {
149
+ this.logInfo('received snapshot!');
40
150
  });
41
151
  }
42
- async deleteCollection(name) {
43
- return this.getCollection(name).deleteAll();
44
- }
152
+ // public async deleteCollection(name: string) {
153
+ // return this.getCollection(name).deleteAll();
154
+ // }
45
155
  async listCollections() {
46
156
  if (!this.firestore.listCollections)
47
157
  return [];
@@ -0,0 +1,81 @@
1
+ import { Database, DB_Prototype } from '@nu-art/db-api-shared';
2
+ import { InvalidResult, Logger, UniqueId } from '@nu-art/ts-common';
3
+ import { Clause_Where, FirestoreQuery } from '@nu-art/firebase-shared';
4
+ import { FirestoreCollectionHooks } from './FirestoreCollection.js';
5
+ import type { Collection as MongoDriverCollection, Db as MongoDriverDb } from 'mongodb';
6
+ export declare class MongoCollection<Proto extends DB_Prototype> extends Logger {
7
+ readonly db: MongoDriverDb;
8
+ readonly mongoCollection: MongoDriverCollection<Proto['dbType']>;
9
+ readonly dbDef: Database<Proto>;
10
+ readonly uniqueKeys: Proto['uniqueKeys'][] | string[];
11
+ private readonly validator;
12
+ readonly hooks?: FirestoreCollectionHooks<Proto['dbType']>;
13
+ constructor(db: MongoDriverDb, _dbDef: Database<Proto>, hooks?: FirestoreCollectionHooks<Proto['dbType']>);
14
+ private getSession;
15
+ private sessionOpts;
16
+ private assertUniqueId;
17
+ private _customQuery;
18
+ query: Readonly<{
19
+ unique: (_id: Proto["uniqueParam"]) => Promise<Proto["dbType"] | undefined>;
20
+ uniqueAssert: (_id: Proto["uniqueParam"]) => Promise<Proto["dbType"]>;
21
+ uniqueWhere: (where: Clause_Where<Proto["dbType"]>) => Promise<Proto["dbType"]>;
22
+ uniqueCustom: (query: FirestoreQuery<Proto["dbType"]>) => Promise<Proto["dbType"]>;
23
+ all: (_ids: (Proto["uniqueParam"])[]) => Promise<(Proto["dbType"] | undefined)[]>;
24
+ custom: (query: FirestoreQuery<Proto["dbType"]>) => Promise<Proto["dbType"][]>;
25
+ where: (where: Clause_Where<Proto["dbType"]>) => Promise<Proto["dbType"][]>;
26
+ unManipulatedQuery: (query: FirestoreQuery<Proto["dbType"]>) => Promise<Proto["dbType"][]>;
27
+ }>;
28
+ uniqueGetOrCreate: (where: Clause_Where<Proto["dbType"]>, toCreate: () => Promise<Proto["uiType"]>) => Promise<Proto["dbType"] | Proto["uiType"]>;
29
+ private prepareForCreate;
30
+ private prepareForSet;
31
+ create: Readonly<{
32
+ item: (preDBItem: Proto["uiType"]) => Promise<Proto["dbType"]>;
33
+ all: (preDBItems: Proto["uiType"][]) => Promise<Proto["dbType"][]>;
34
+ }>;
35
+ set: Readonly<{
36
+ item: (preDBItem: Proto["uiType"]) => Promise<Proto["dbType"]>;
37
+ all: (items: (Proto["uiType"] | Proto["dbType"])[]) => Promise<Proto["dbType"][]>;
38
+ multi: (items: (Proto["uiType"] | Proto["dbType"])[]) => Promise<Proto["dbType"][]>;
39
+ }>;
40
+ private _setAll;
41
+ private _deleteAll;
42
+ delete: Readonly<{
43
+ unique: (id: Proto["uniqueParam"]) => Promise<Proto["dbType"] | undefined>;
44
+ item: (item: Proto["uiType"]) => Promise<Proto["dbType"] | undefined>;
45
+ all: (ids: (Proto["uniqueParam"])[]) => Promise<Proto["dbType"][]>;
46
+ allItems: (items: Proto["uiType"][]) => Promise<Proto["dbType"][]>;
47
+ query: (query: FirestoreQuery<Proto["dbType"]>) => Promise<Proto["dbType"][]>;
48
+ allDocs: (_docs: any[]) => Promise<Proto["dbType"][]>;
49
+ where: (where: Clause_Where<Proto["dbType"]>) => Promise<Proto["dbType"][]>;
50
+ unManipulatedQuery: (query: FirestoreQuery<Proto["dbType"]>) => Promise<Proto["dbType"][]>;
51
+ multi: {
52
+ all: (ids: UniqueId[]) => Promise<Proto["dbType"][]>;
53
+ items: (items: Proto["uiType"][]) => Promise<Proto["dbType"][]>;
54
+ allDocs: (_docs: any[]) => Promise<Proto["dbType"][]>;
55
+ query: (query: FirestoreQuery<Proto["dbType"]>) => Promise<Proto["dbType"][]>;
56
+ };
57
+ yes: {
58
+ iam: {
59
+ sure: {
60
+ iwant: {
61
+ todelete: {
62
+ the: {
63
+ collection: {
64
+ delete: () => Promise<void>;
65
+ };
66
+ };
67
+ };
68
+ };
69
+ };
70
+ };
71
+ };
72
+ }>;
73
+ runTransaction: <ReturnType>(processor: () => Promise<ReturnType>, label?: string) => Promise<ReturnType>;
74
+ runTransactionInChunks: <T = any, R = any>(items: T[], processor: (chunk: typeof items) => Promise<R[]>, chunkSize?: number) => Promise<R[]>;
75
+ getVersion: () => any;
76
+ needsUpgrade: (version?: string) => boolean;
77
+ validateItem(dbItem: Proto['dbType']): void;
78
+ protected onValidationError(instance: Proto['dbType'], results: InvalidResult<Proto['dbType']>): void;
79
+ private assertNoDuplicatedIds;
80
+ composeDbObjectUniqueId: (item: Proto["uiType"]) => string;
81
+ }
@@ -0,0 +1,426 @@
1
+ /*
2
+ * Firebase is a simpler Typescript wrapper to all of firebase services.
3
+ *
4
+ * Copyright (C) 2020 Adam van der Kruk aka TacB0sS
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */
18
+ import { dbObjectToId } from '@nu-art/db-api-shared';
19
+ import { __stringify, _keys, ApiException, BadImplementationException, batchActionParallel, compare, Const_UniqueKeys, currentTimeMillis, DB_Object_validator, dbIdLength, deepClone, DefaultDBVersion, exists, filterDuplicates, filterInstances, generateHex, keepPartialObject, Logger, MUSTNeverHappenException, StaticLogger, tsValidateResult, ValidationException, } from '@nu-art/ts-common';
20
+ import { composeDbObjectUniqueId, _EmptyQuery, maxBatch } from '@nu-art/firebase-shared';
21
+ import { HttpCodes } from '@nu-art/ts-common/core/exceptions/http-codes';
22
+ import { addDeletedToTransaction, getActiveTransaction, markTransactionWrite, MemKey_FirestoreTransaction } from './consts.js';
23
+ import { MongoInterface } from './MongoInterface.js';
24
+ import { MemStorage } from '@nu-art/ts-common/mem-storage';
25
+ const getDbDefValidator = (dbDef) => {
26
+ if (typeof dbDef.modifiablePropsValidator === 'object' && typeof dbDef.generatedPropsValidator === 'object')
27
+ return { ...dbDef.generatedPropsValidator, ...dbDef.modifiablePropsValidator, ...DB_Object_validator };
28
+ if (typeof dbDef.modifiablePropsValidator === 'function' && typeof dbDef.generatedPropsValidator === 'function')
29
+ return [dbDef.modifiablePropsValidator, dbDef.generatedPropsValidator];
30
+ if (typeof dbDef.modifiablePropsValidator === 'function')
31
+ return [dbDef.modifiablePropsValidator, (instance) => {
32
+ const partialInstance = keepPartialObject(instance, _keys(dbDef.generatedPropsValidator));
33
+ return tsValidateResult(partialInstance, dbDef.generatedPropsValidator);
34
+ }];
35
+ return [dbDef.generatedPropsValidator, (instance) => {
36
+ return tsValidateResult(keepPartialObject(instance, _keys(dbDef.modifiablePropsValidator)), dbDef.modifiablePropsValidator);
37
+ }];
38
+ };
39
+ export class MongoCollection extends Logger {
40
+ db;
41
+ mongoCollection;
42
+ dbDef;
43
+ uniqueKeys;
44
+ validator;
45
+ hooks;
46
+ constructor(db, _dbDef, hooks) {
47
+ super();
48
+ this.db = db;
49
+ if (!/[a-z-]{3,}/.test(_dbDef.backend.name))
50
+ StaticLogger.logWarning('Please follow name pattern for collections /[a-z-]{3,}/');
51
+ this.mongoCollection = db.collection(_dbDef.backend.name);
52
+ this.dbDef = _dbDef;
53
+ this.uniqueKeys = this.dbDef.uniqueKeys || Const_UniqueKeys;
54
+ this.validator = getDbDefValidator(_dbDef);
55
+ this.hooks = hooks;
56
+ }
57
+ getSession() {
58
+ const tx = getActiveTransaction();
59
+ if (!tx)
60
+ return undefined;
61
+ if ('abortTransaction' in tx) {
62
+ const session = tx;
63
+ const txState = session.transaction?.state;
64
+ if (txState === 'TRANSACTION_COMMITTED' || txState === 'TRANSACTION_ABORTED')
65
+ this.logWarning(`SESSION-STALE [${this.dbDef.dbKey}] txState=${txState}`);
66
+ return session;
67
+ }
68
+ return undefined;
69
+ }
70
+ sessionOpts() {
71
+ const session = this.getSession();
72
+ return session ? { session } : {};
73
+ }
74
+ assertUniqueId(item) {
75
+ if (compare(this.uniqueKeys, Const_UniqueKeys))
76
+ return (item._id ?? generateHex(dbIdLength));
77
+ const _id = composeDbObjectUniqueId(item, this.uniqueKeys);
78
+ if (exists(item._id) && _id !== item._id)
79
+ throw new MUSTNeverHappenException(`Composed _id does not match existing _id! expected: ${_id}, actual: ${item._id}`);
80
+ return _id;
81
+ }
82
+ async _customQuery(tsQuery, canManipulateQuery) {
83
+ if (canManipulateQuery)
84
+ tsQuery = this.hooks?.manipulateQuery?.(deepClone(tsQuery)) ?? tsQuery;
85
+ const compiled = MongoInterface.buildQuery(tsQuery);
86
+ let cursor = this.mongoCollection.find(compiled.filter, this.sessionOpts());
87
+ if (compiled.sort)
88
+ cursor = cursor.sort(compiled.sort);
89
+ if (compiled.projection)
90
+ cursor = cursor.project(compiled.projection);
91
+ if (compiled.skip)
92
+ cursor = cursor.skip(compiled.skip);
93
+ if (compiled.limit)
94
+ cursor = cursor.limit(compiled.limit);
95
+ return await cursor.toArray();
96
+ }
97
+ query = Object.freeze({
98
+ unique: async (_id) => {
99
+ const idStr = typeof _id !== 'string' ? this.assertUniqueId(_id) : _id;
100
+ const result = await this.mongoCollection.findOne({ _id: idStr }, this.sessionOpts());
101
+ return result;
102
+ },
103
+ uniqueAssert: async (_id) => {
104
+ const result = await this.query.unique(_id);
105
+ if (!result)
106
+ throw new ApiException(404, `Could not find ${this.dbDef.entityName} with _id: ${__stringify(_id)}`);
107
+ return result;
108
+ },
109
+ uniqueWhere: async (where) => this.query.uniqueCustom({ where }),
110
+ uniqueCustom: async (query) => {
111
+ const results = await this.query.custom(query);
112
+ if (results.length === 0)
113
+ throw new ApiException(404, `Could not find ${this.dbDef.entityName} with unique query: ${JSON.stringify(query)}`);
114
+ if (results.length > 1)
115
+ throw new BadImplementationException(`Too many results (${results.length}) in collection (${this.dbDef.dbKey}) for query: ${__stringify(query)}`);
116
+ return results[0];
117
+ },
118
+ all: async (_ids) => {
119
+ const idStrs = _ids.map(id => typeof id !== 'string' ? this.assertUniqueId(id) : id);
120
+ const results = await this.mongoCollection.find({ _id: { $in: idStrs } }, this.sessionOpts()).toArray();
121
+ const resultMap = new Map(results.map(r => [r._id, r]));
122
+ return idStrs.map(id => resultMap.get(id));
123
+ },
124
+ custom: async (query) => {
125
+ return this._customQuery(query, true);
126
+ },
127
+ where: async (where) => {
128
+ return this.query.custom({ where });
129
+ },
130
+ unManipulatedQuery: async (query) => {
131
+ return this._customQuery(query, false);
132
+ },
133
+ });
134
+ uniqueGetOrCreate = async (where, toCreate) => {
135
+ try {
136
+ return await this.query.uniqueWhere(where);
137
+ }
138
+ catch (e) {
139
+ return toCreate();
140
+ }
141
+ };
142
+ prepareForCreate = async (item, upgrade = true) => {
143
+ const now = currentTimeMillis();
144
+ item._id = this.assertUniqueId(item);
145
+ item.__updated = item.__created = now;
146
+ if (upgrade) {
147
+ item._v = this.getVersion();
148
+ await this.hooks?.upgradeInstances([item]);
149
+ }
150
+ await this.hooks?.preWriteProcessing?.(item, undefined);
151
+ this.validateItem(item);
152
+ return item;
153
+ };
154
+ prepareForSet = async (updatedItem, dbItem, upgrade = true) => {
155
+ if (!dbItem)
156
+ return this.prepareForCreate(updatedItem);
157
+ updatedItem._id = this.assertUniqueId(updatedItem);
158
+ updatedItem.__created = dbItem.__created;
159
+ this.dbDef.lockKeys?.forEach(lockedKey => {
160
+ if (exists(dbItem[lockedKey]))
161
+ updatedItem[lockedKey] = dbItem[lockedKey];
162
+ });
163
+ updatedItem.__updated = currentTimeMillis();
164
+ if (this.needsUpgrade(updatedItem._v))
165
+ await this.hooks?.upgradeInstances([updatedItem]);
166
+ updatedItem._v = this.getVersion();
167
+ await this.hooks?.preWriteProcessing?.(updatedItem, dbItem);
168
+ this.validateItem(updatedItem);
169
+ return updatedItem;
170
+ };
171
+ create = Object.freeze({
172
+ item: async (preDBItem) => {
173
+ const dbItem = await this.prepareForCreate(preDBItem);
174
+ markTransactionWrite();
175
+ await this.mongoCollection.insertOne(dbItem, this.sessionOpts());
176
+ await this.hooks?.postWriteProcessing?.({ updated: dbItem }, 'create');
177
+ return dbItem;
178
+ },
179
+ all: async (preDBItems) => {
180
+ if (preDBItems.length === 1)
181
+ return [await this.create.item(preDBItems[0])];
182
+ const dbItems = await Promise.all(preDBItems.map(item => this.prepareForCreate(item)));
183
+ this.assertNoDuplicatedIds(dbItems, 'create.all');
184
+ markTransactionWrite();
185
+ await this.mongoCollection.insertMany(dbItems, this.sessionOpts());
186
+ await this.hooks?.postWriteProcessing?.({ updated: dbItems }, 'create');
187
+ return dbItems;
188
+ },
189
+ });
190
+ set = Object.freeze({
191
+ item: async (preDBItem) => {
192
+ const session = this.getSession();
193
+ if (!session)
194
+ return this.runTransaction(() => this.set.item(preDBItem), 'set.item');
195
+ preDBItem._id = this.assertUniqueId(preDBItem);
196
+ const currDBItem = await this.query.unique(preDBItem._id);
197
+ if ((currDBItem?.__updated || 0) > (preDBItem.__updated || currentTimeMillis()))
198
+ throw HttpCodes._4XX.ENTITY_IS_OUTDATED('Item is outdated', `${this.dbDef.backend.name}/${currDBItem?._id} is outdated`);
199
+ const dbItem = await this.prepareForSet(preDBItem, currDBItem, false);
200
+ markTransactionWrite();
201
+ await this.mongoCollection.replaceOne({ _id: dbItem._id }, dbItem, { upsert: true, ...this.sessionOpts() });
202
+ await this.hooks?.postWriteProcessing?.({ before: currDBItem, updated: dbItem }, 'set');
203
+ return dbItem;
204
+ },
205
+ all: async (items) => {
206
+ if (this.getSession())
207
+ return this._setAll(items);
208
+ return this.runTransactionInChunks(items, (chunk) => this._setAll(chunk));
209
+ },
210
+ multi: async (items) => {
211
+ return this._setAll(items);
212
+ },
213
+ });
214
+ _setAll = async (items) => {
215
+ const ids = items.map(item => {
216
+ item._id = this.assertUniqueId(item);
217
+ return item._id;
218
+ });
219
+ const existingItems = await this.query.all(ids);
220
+ const existingMap = new Map(filterInstances(existingItems).map(item => [item._id, item]));
221
+ const preparedItems = await Promise.all(items.map(async (item) => {
222
+ const existing = existingMap.get(item._id);
223
+ return !exists(existing)
224
+ ? await this.prepareForCreate(item)
225
+ : await this.prepareForSet(item, existing);
226
+ }));
227
+ this.assertNoDuplicatedIds(preparedItems, 'set.all');
228
+ const ops = preparedItems.map(item => ({
229
+ replaceOne: {
230
+ filter: { _id: item._id },
231
+ replacement: item,
232
+ upsert: true,
233
+ }
234
+ }));
235
+ markTransactionWrite();
236
+ await this.mongoCollection.bulkWrite(ops, this.sessionOpts());
237
+ if (preparedItems.length)
238
+ await this.hooks?.postWriteProcessing?.({ before: existingItems, updated: preparedItems }, 'set');
239
+ return preparedItems;
240
+ };
241
+ _deleteAll = async (ids) => {
242
+ const items = filterInstances(await this.query.all(ids));
243
+ addDeletedToTransaction({
244
+ dbKey: this.dbDef.dbKey,
245
+ ids: items.map(dbObjectToId)
246
+ });
247
+ await this.hooks?.canDeleteItems(items);
248
+ markTransactionWrite();
249
+ await this.mongoCollection.deleteMany({ _id: { $in: ids } }, this.sessionOpts());
250
+ await this.hooks?.postWriteProcessing?.({ deleted: items }, 'delete');
251
+ return items;
252
+ };
253
+ delete = Object.freeze({
254
+ unique: async (id) => {
255
+ const idStr = typeof id !== 'string' ? this.assertUniqueId(id) : id;
256
+ const session = this.getSession();
257
+ if (!session)
258
+ return this.runTransaction(() => this.delete.unique(id), 'delete.unique');
259
+ const dbItem = await this.query.unique(id);
260
+ if (!dbItem)
261
+ return;
262
+ addDeletedToTransaction({ dbKey: this.dbDef.entityName, ids: [dbItem._id] });
263
+ await this.hooks?.canDeleteItems([dbItem]);
264
+ markTransactionWrite();
265
+ await this.mongoCollection.deleteOne({ _id: idStr }, this.sessionOpts());
266
+ await this.hooks?.postWriteProcessing?.({ deleted: dbItem }, 'delete');
267
+ return dbItem;
268
+ },
269
+ item: async (item) => {
270
+ item._id = this.assertUniqueId(item);
271
+ return this.delete.unique(item._id);
272
+ },
273
+ all: async (ids) => {
274
+ const idStrs = ids.map(id => typeof id !== 'string' ? this.assertUniqueId(id) : id);
275
+ if (!this.getSession())
276
+ return this.runTransactionInChunks(idStrs, (chunk) => this._deleteAll(chunk));
277
+ return this._deleteAll(idStrs);
278
+ },
279
+ allItems: async (items) => {
280
+ const ids = items.map(item => {
281
+ item._id = this.assertUniqueId(item);
282
+ return item._id;
283
+ });
284
+ if (!this.getSession())
285
+ return this.runTransactionInChunks(ids, (chunk) => this._deleteAll(chunk));
286
+ return this._deleteAll(ids);
287
+ },
288
+ query: async (query) => {
289
+ if (!exists(query) || compare(query, _EmptyQuery))
290
+ throw new MUSTNeverHappenException('An empty query was passed to delete.query!');
291
+ const items = await this.query.custom(query);
292
+ const ids = filterInstances(items.map(dbObjectToId));
293
+ if (!this.getSession()) {
294
+ await this.runTransactionInChunks(ids, (chunk) => this._deleteAll(chunk));
295
+ return items;
296
+ }
297
+ return this._deleteAll(ids);
298
+ },
299
+ allDocs: async (_docs) => {
300
+ const ids = _docs.map((d) => d._id || d.ref?.id).filter(Boolean);
301
+ if (!this.getSession())
302
+ return this.runTransactionInChunks(ids, (chunk) => this._deleteAll(chunk));
303
+ return this._deleteAll(ids);
304
+ },
305
+ where: async (where) => {
306
+ return this.delete.query({ where });
307
+ },
308
+ unManipulatedQuery: async (query) => {
309
+ if (!exists(query) || compare(query, _EmptyQuery))
310
+ throw new MUSTNeverHappenException('An empty query was passed to delete.unManipulatedQuery!');
311
+ const items = await this._customQuery(query, false);
312
+ const ids = filterInstances(items.map(dbObjectToId));
313
+ if (!this.getSession()) {
314
+ await this.runTransactionInChunks(ids, (chunk) => this._deleteAll(chunk));
315
+ return items;
316
+ }
317
+ return this._deleteAll(ids);
318
+ },
319
+ multi: {
320
+ all: async (ids) => this._deleteAll(ids),
321
+ items: async (items) => {
322
+ const _ids = items.map(item => {
323
+ item._id = this.assertUniqueId(item);
324
+ return item._id;
325
+ });
326
+ return this._deleteAll(_ids);
327
+ },
328
+ allDocs: async (_docs) => {
329
+ const ids = _docs.map((d) => d._id || d.ref?.id).filter(Boolean);
330
+ return this._deleteAll(ids);
331
+ },
332
+ query: async (query) => {
333
+ const items = await this.query.custom(query);
334
+ const ids = filterInstances(items.map(dbObjectToId));
335
+ return this._deleteAll(ids);
336
+ }
337
+ },
338
+ yes: { iam: { sure: { iwant: { todelete: { the: { collection: {
339
+ delete: async () => {
340
+ markTransactionWrite();
341
+ await this.mongoCollection.deleteMany({}, this.sessionOpts());
342
+ await this.hooks?.postWriteProcessing?.({ deleted: null }, 'delete');
343
+ }
344
+ } } } } } } }
345
+ });
346
+ runTransaction = async (processor, label) => {
347
+ const existingSession = this.getSession();
348
+ if (existingSession)
349
+ return processor();
350
+ const tag = label ? `${this.dbDef.dbKey}:${label}` : this.dbDef.dbKey;
351
+ const client = this.db.client;
352
+ const session = client.startSession();
353
+ this.logDebug(`TX-START [${tag}]`);
354
+ try {
355
+ let result;
356
+ const wrapper = {
357
+ transaction: session,
358
+ active: true,
359
+ writeCount: 0,
360
+ beginTransaction: () => session.startTransaction(),
361
+ };
362
+ const parentStorage = MemStorage.getStore();
363
+ try {
364
+ await new MemStorage().init(async () => {
365
+ MemKey_FirestoreTransaction.set(wrapper);
366
+ result = await processor();
367
+ }, parentStorage);
368
+ }
369
+ catch (e) {
370
+ wrapper.active = false;
371
+ if (wrapper.writeCount > 0)
372
+ await session.abortTransaction();
373
+ throw e;
374
+ }
375
+ wrapper.active = false;
376
+ if (wrapper.writeCount > 0)
377
+ await session.commitTransaction();
378
+ this.logDebug(`TX-END [${tag}] writes=${wrapper.writeCount}`);
379
+ return result;
380
+ }
381
+ finally {
382
+ await session.endSession();
383
+ }
384
+ };
385
+ runTransactionInChunks = async (items, processor, chunkSize = maxBatch) => {
386
+ return batchActionParallel(items, chunkSize, (chunk) => this.runTransaction(() => processor(chunk), `chunk(${chunk.length})`));
387
+ };
388
+ getVersion = () => {
389
+ return this.dbDef.versions?.[0] || DefaultDBVersion;
390
+ };
391
+ needsUpgrade = (version) => {
392
+ const versions = this.dbDef.versions || [DefaultDBVersion];
393
+ if (!version)
394
+ return false;
395
+ const index = versions.indexOf(version);
396
+ if (index === -1)
397
+ 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)})' `);
398
+ return index !== 0;
399
+ };
400
+ validateItem(dbItem) {
401
+ const results = tsValidateResult(dbItem, this.validator);
402
+ if (results)
403
+ this.onValidationError(dbItem, results);
404
+ }
405
+ onValidationError(instance, results) {
406
+ StaticLogger.logError(`error validating ${this.dbDef.entityName}:`, instance, 'With Error: ', results);
407
+ const validationException = new ValidationException(`error validating ${this.dbDef.entityName}`, instance, results);
408
+ throw new ApiException(HttpCodes._4XX.FAILED_VALIDATION.code, `error validating ${this.dbDef.entityName}`).setErrorBody(validationException);
409
+ }
410
+ assertNoDuplicatedIds(items, originFunctionName) {
411
+ if (filterDuplicates(items, dbObjectToId).length === items.length)
412
+ return;
413
+ const idCountMap = items.reduce((countMap, item) => {
414
+ countMap[item._id] = !exists(countMap[item._id]) ? 1 : 1 + countMap[item._id];
415
+ return countMap;
416
+ }, {});
417
+ _keys(idCountMap).forEach(key => {
418
+ if (idCountMap[key] === 1)
419
+ delete idCountMap[key];
420
+ });
421
+ throw new BadImplementationException(`${originFunctionName} received the same _id twice: ${__stringify(idCountMap, true)}`);
422
+ }
423
+ composeDbObjectUniqueId = (item) => {
424
+ return composeDbObjectUniqueId(item, this.uniqueKeys);
425
+ };
426
+ }
@@ -0,0 +1,18 @@
1
+ import { FirestoreQuery } from '@nu-art/firebase-shared';
2
+ import { TS_Object } from '@nu-art/ts-common';
3
+ import type { Document, Filter, Sort } from 'mongodb';
4
+ export type MqlCompiledQuery<T extends TS_Object = TS_Object> = {
5
+ filter: Filter<T>;
6
+ sort?: Sort;
7
+ projection?: Document;
8
+ skip?: number;
9
+ limit?: number;
10
+ };
11
+ export declare class MongoInterface {
12
+ static buildQuery<T extends TS_Object>(query?: FirestoreQuery<T>): MqlCompiledQuery<T>;
13
+ private static buildFilter;
14
+ private static isQueryComparator;
15
+ private static buildProjection;
16
+ private static buildSort;
17
+ private static buildPagination;
18
+ }