@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.
- package/ModuleBE_Firebase.d.ts +15 -1
- package/ModuleBE_Firebase.js +39 -3
- package/auth/firebase-session.d.ts +12 -8
- package/auth/firebase-session.js +53 -15
- package/firestore/DocWrapper.d.ts +30 -0
- package/{firestore-v3/DocWrapperV3.js → firestore/DocWrapper.js} +30 -32
- package/firestore/FirestoreCollection.d.ts +141 -59
- package/firestore/FirestoreCollection.js +419 -147
- package/firestore/FirestoreInterface.d.ts +2 -3
- package/firestore/FirestoreInterface.js +1 -5
- package/firestore/FirestoreWrapperBE.d.ts +5 -6
- package/firestore/FirestoreWrapperBE.js +119 -9
- package/firestore/MongoCollection.d.ts +81 -0
- package/firestore/MongoCollection.js +426 -0
- package/firestore/MongoInterface.d.ts +18 -0
- package/firestore/MongoInterface.js +132 -0
- package/firestore/MongoWrapperBE.d.ts +18 -0
- package/firestore/MongoWrapperBE.js +95 -0
- package/firestore/consts.d.ts +23 -0
- package/firestore/consts.js +34 -0
- package/firestore/types.d.ts +6 -1
- package/firestore/types.js +0 -24
- package/{functions-v2 → functions}/ModuleBE_BaseFunction.d.ts +6 -3
- package/{functions-v2 → functions}/ModuleBE_BaseFunction.js +1 -0
- package/{functions-v2/ModuleBE_ExpressFunction_V2.d.ts → functions/ModuleBE_ExpressFunction_Class.d.ts} +5 -3
- package/{functions-v2/ModuleBE_ExpressFunction_V2.js → functions/ModuleBE_ExpressFunction_Class.js} +7 -3
- package/{functions-v2 → functions}/ModuleBE_FirebaseDBListener.d.ts +2 -1
- package/{functions-v2 → functions}/ModuleBE_FirebaseDBListener.js +1 -0
- package/{functions-v2 → functions}/ModuleBE_FirebaseScheduler.js +1 -0
- package/{functions-v2 → functions}/ModuleBE_FirestoreListener.d.ts +5 -1
- package/{functions-v2 → functions}/ModuleBE_FirestoreListener.js +1 -0
- package/{functions-v2 → functions}/ModuleBE_PubSubFunction.d.ts +5 -2
- package/{functions-v2 → functions}/ModuleBE_PubSubFunction.js +1 -0
- package/{functions-v2 → functions}/ModuleBE_StorageListener.js +1 -0
- package/index.d.ts +16 -13
- package/index.js +16 -13
- package/package.json +10 -7
- package/firestore/FirestoreTransaction.d.ts +0 -30
- package/firestore/FirestoreTransaction.js +0 -153
- package/firestore-v3/DocWrapperV3.d.ts +0 -32
- package/firestore-v3/FirestoreCollectionV3.d.ts +0 -154
- package/firestore-v3/FirestoreCollectionV3.js +0 -470
- package/firestore-v3/FirestoreInterfaceV3.d.ts +0 -10
- package/firestore-v3/FirestoreInterfaceV3.js +0 -107
- package/firestore-v3/FirestoreWrapperBEV3.d.ts +0 -16
- package/firestore-v3/FirestoreWrapperBEV3.js +0 -154
- package/firestore-v3/consts.d.ts +0 -13
- package/firestore-v3/consts.js +0 -18
- package/firestore-v3/types.d.ts +0 -6
- package/firestore-v3/types.js +0 -1
- package/functions/firebase-function.d.ts +0 -38
- package/functions/firebase-function.js +0 -53
- package/v1.d.ts +0 -21
- package/v1.js +0 -38
- /package/{functions-v2 → functions}/ModuleBE_FirebaseScheduler.d.ts +0 -0
- /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 {
|
|
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 {
|
|
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
|
-
*
|
|
28
|
+
* # <ins>FirestoreBulkException</ins>
|
|
29
|
+
* @category Exceptions
|
|
23
30
|
*/
|
|
24
|
-
export class
|
|
25
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
67
|
-
@param
|
|
68
|
-
@
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
77
|
-
@param
|
|
78
|
-
@
|
|
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
|
|
81
|
-
|
|
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
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return this.
|
|
129
|
-
}
|
|
130
|
-
async
|
|
131
|
-
return this.
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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 {
|
|
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<
|
|
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
|
|
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 {
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
}
|