@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
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { FirestoreCollection, } from './FirestoreCollection.js';
|
|
19
19
|
import { FirebaseBaseWrapper } from '../auth/FirebaseBaseWrapper.js';
|
|
20
|
-
import {
|
|
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
|
-
|
|
32
|
-
|
|
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[
|
|
145
|
+
return this.collections[dbDef.dbKey] = new FirestoreCollection(this, dbDef, hooks);
|
|
36
146
|
}
|
|
37
147
|
listen(collection, doc) {
|
|
38
|
-
collection.wrapper.firestore.doc(`${collection.
|
|
39
|
-
this.logInfo('
|
|
148
|
+
collection.wrapper.firestore.doc(`${collection.collection.path}/${doc}`).onSnapshot(() => {
|
|
149
|
+
this.logInfo('received snapshot!');
|
|
40
150
|
});
|
|
41
151
|
}
|
|
42
|
-
async deleteCollection(name) {
|
|
43
|
-
|
|
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
|
+
}
|