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