@nu-art/thunderstorm-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/_entity/app-config/ModuleBE_AppConfigAPI.d.ts +9 -0
- package/_entity/app-config/ModuleBE_AppConfigAPI.js +20 -0
- package/_entity/app-config/ModuleBE_AppConfigDB.d.ts +27 -0
- package/_entity/app-config/ModuleBE_AppConfigDB.js +91 -0
- package/_entity/app-config/index.d.ts +2 -0
- package/_entity/app-config/index.js +2 -0
- package/_entity/app-config/module-pack.d.ts +2 -0
- package/_entity/app-config/module-pack.js +3 -0
- package/_entity/backup-doc/ModuleBE_BackupDocDB.d.ts +52 -0
- package/_entity/backup-doc/ModuleBE_BackupDocDB.js +350 -0
- package/_entity/backup-doc/ModuleBE_BackupScheduler.d.ts +7 -0
- package/_entity/backup-doc/ModuleBE_BackupScheduler.js +14 -0
- package/_entity/backup-doc/index.d.ts +3 -0
- package/_entity/backup-doc/index.js +3 -0
- package/_entity/backup-doc/module-pack.d.ts +2 -0
- package/_entity/backup-doc/module-pack.js +3 -0
- package/_entity/editable-test/ModuleBE_EditableTestDB.d.ts +8 -0
- package/_entity/editable-test/ModuleBE_EditableTestDB.js +8 -0
- package/_entity/editable-test/index.d.ts +1 -0
- package/_entity/editable-test/index.js +1 -0
- package/_entity/editable-test/module-pack.d.ts +1 -0
- package/_entity/editable-test/module-pack.js +3 -0
- package/_entity.d.ts +3 -0
- package/_entity.js +3 -0
- package/core/BaseStorm.d.ts +17 -0
- package/core/BaseStorm.js +77 -0
- package/core/Storm.d.ts +15 -0
- package/core/Storm.js +93 -0
- package/core/db-def.d.ts +10 -0
- package/core/db-def.js +11 -0
- package/core/default-storm.d.ts +3 -0
- package/core/default-storm.js +30 -0
- package/core/storm-modulepack.d.ts +3 -0
- package/core/storm-modulepack.js +20 -0
- package/core/typed-api.d.ts +7 -0
- package/core/typed-api.js +46 -0
- package/exceptions.d.ts +1 -0
- package/exceptions.js +21 -0
- package/index.d.ts +27 -0
- package/index.js +48 -0
- package/modules/CleanupScheduler.d.ts +14 -0
- package/modules/CleanupScheduler.js +50 -0
- package/modules/ModuleBE_APIs.d.ts +11 -0
- package/modules/ModuleBE_APIs.js +19 -0
- package/modules/ModuleBE_CSVParser.d.ts +9 -0
- package/modules/ModuleBE_CSVParser.js +50 -0
- package/modules/ModuleBE_ForceUpgrade.d.ts +21 -0
- package/modules/ModuleBE_ForceUpgrade.js +70 -0
- package/modules/ModuleBE_ServerInfo.d.ts +20 -0
- package/modules/ModuleBE_ServerInfo.js +76 -0
- package/modules/_imports.d.ts +6 -0
- package/modules/_imports.js +26 -0
- package/modules/_tdb/service-accounts.d.ts +19 -0
- package/modules/_tdb/service-accounts.js +2 -0
- package/modules/action-processor/Action_SetupProject.d.ts +9 -0
- package/modules/action-processor/Action_SetupProject.js +23 -0
- package/modules/action-processor/ModuleBE_ActionProcessor.d.ts +11 -0
- package/modules/action-processor/ModuleBE_ActionProcessor.js +67 -0
- package/modules/action-processor/types.d.ts +10 -0
- package/modules/action-processor/types.js +1 -0
- package/modules/archiving/ModuleBE_Archiving.d.ts +119 -0
- package/modules/archiving/ModuleBE_Archiving.js +236 -0
- package/modules/collection-actions/ModuleBE_CollectionActions.d.ts +12 -0
- package/modules/collection-actions/ModuleBE_CollectionActions.js +69 -0
- package/modules/collection-actions/dispatcher.d.ts +7 -0
- package/modules/collection-actions/dispatcher.js +2 -0
- package/modules/db-api-gen/ModuleBE_BaseApi.d.ts +16 -0
- package/modules/db-api-gen/ModuleBE_BaseApi.js +74 -0
- package/modules/db-api-gen/ModuleBE_BaseDB.d.ts +78 -0
- package/modules/db-api-gen/ModuleBE_BaseDB.js +298 -0
- package/modules/http/AxiosHttpModule.d.ts +25 -0
- package/modules/http/AxiosHttpModule.js +132 -0
- package/modules/http/types.d.ts +6 -0
- package/modules/http/types.js +1 -0
- package/modules/proxy/ModuleBE_RemoteProxy.d.ts +35 -0
- package/modules/proxy/ModuleBE_RemoteProxy.js +86 -0
- package/modules/proxy/RemoteProxyCaller.d.ts +19 -0
- package/modules/proxy/RemoteProxyCaller.js +82 -0
- package/modules/proxy/assert-secret-middleware.d.ts +2 -0
- package/modules/proxy/assert-secret-middleware.js +24 -0
- package/modules/server/HeaderKey.d.ts +8 -0
- package/modules/server/HeaderKey.js +41 -0
- package/modules/server/HttpServer.d.ts +41 -0
- package/modules/server/HttpServer.js +223 -0
- package/modules/server/consts.d.ts +13 -0
- package/modules/server/consts.js +9 -0
- package/modules/server/route-resolvers/RouteResolver_Dummy.d.ts +7 -0
- package/modules/server/route-resolvers/RouteResolver_Dummy.js +34 -0
- package/modules/server/route-resolvers/RouteResolver_ModulePath.d.ts +22 -0
- package/modules/server/route-resolvers/RouteResolver_ModulePath.js +84 -0
- package/modules/server/route-resolvers/index.d.ts +7 -0
- package/modules/server/route-resolvers/index.js +21 -0
- package/modules/server/server-api.d.ts +85 -0
- package/modules/server/server-api.js +362 -0
- package/modules/server/server-errors.d.ts +4 -0
- package/modules/server/server-errors.js +79 -0
- package/modules/sync-env/ModuleBE_SyncEnv.d.ts +36 -0
- package/modules/sync-env/ModuleBE_SyncEnv.js +212 -0
- package/modules/sync-manager/ModuleBE_SyncManager.d.ts +63 -0
- package/modules/sync-manager/ModuleBE_SyncManager.js +254 -0
- package/package.json +104 -0
- package/shared.d.ts +1 -0
- package/shared.js +21 -0
- package/test/StormTest.d.ts +23 -0
- package/test/StormTest.js +49 -0
- package/utils/file.d.ts +2 -0
- package/utils/file.js +29 -0
- package/utils/promisify-request.d.ts +3 -0
- package/utils/promisify-request.js +33 -0
- package/utils/types.d.ts +11 -0
- package/utils/types.js +21 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Database API Generator is a utility library for Thunderstorm.
|
|
3
|
+
*
|
|
4
|
+
* Given proper configurations it will dynamically generate APIs to your Firestore
|
|
5
|
+
* collections, will assert uniqueness and restrict deletion... and more
|
|
6
|
+
*
|
|
7
|
+
* Copyright (C) 2020 Adam van der Kruk aka TacB0sS
|
|
8
|
+
*
|
|
9
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
10
|
+
* you may not use this file except in compliance with the License.
|
|
11
|
+
* You may obtain a copy of the License at
|
|
12
|
+
*
|
|
13
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
14
|
+
*
|
|
15
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
16
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
17
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
18
|
+
* See the License for the specific language governing permissions and
|
|
19
|
+
* limitations under the License.
|
|
20
|
+
*/
|
|
21
|
+
import { _keys, ApiException, asArray, BadImplementationException, batchActionParallel, currentTimeMillis, dbObjectToId, filterDuplicates, filterInstances, getDotNotatedValue, merge, Module } from '@nu-art/ts-common';
|
|
22
|
+
import { ModuleBE_Firebase, } from '@nu-art/firebase-backend';
|
|
23
|
+
import { getModuleBEConfig } from '../../core/db-def.js';
|
|
24
|
+
import { ModuleBE_SyncManager } from '../sync-manager/ModuleBE_SyncManager.js';
|
|
25
|
+
import { MemKey_DeletedDocs } from '@nu-art/firebase-backend/firestore-v3/consts';
|
|
26
|
+
import { dispatch_CollectEntityDependencies } from '../collection-actions/dispatcher.js';
|
|
27
|
+
const CONST_DefaultWriteChunkSize = 200;
|
|
28
|
+
/**
|
|
29
|
+
* An abstract base class used for implementing CRUD operations on a specific collection.
|
|
30
|
+
*
|
|
31
|
+
* By default, it exposes API endpoints for creating, deleting, updating, querying and querying for unique document.
|
|
32
|
+
*/
|
|
33
|
+
export class ModuleBE_BaseDB extends Module {
|
|
34
|
+
// @ts-ignore
|
|
35
|
+
ModuleBE_BaseDBV2 = true;
|
|
36
|
+
// private static DeleteHardLimit = 250;
|
|
37
|
+
collection;
|
|
38
|
+
dbDef;
|
|
39
|
+
query;
|
|
40
|
+
create;
|
|
41
|
+
set;
|
|
42
|
+
delete;
|
|
43
|
+
doc;
|
|
44
|
+
runTransaction;
|
|
45
|
+
constructor(dbDef, appConfig) {
|
|
46
|
+
super();
|
|
47
|
+
const config = getModuleBEConfig(dbDef);
|
|
48
|
+
const preConfig = { chunksSize: CONST_DefaultWriteChunkSize, ...config, ...appConfig };
|
|
49
|
+
// @ts-ignore
|
|
50
|
+
this.setDefaultConfig(preConfig);
|
|
51
|
+
this.dbDef = dbDef;
|
|
52
|
+
this.canDeleteItems.bind(this);
|
|
53
|
+
this._preWriteProcessing.bind(this);
|
|
54
|
+
this._postWriteProcessing.bind(this);
|
|
55
|
+
this.upgradeInstances.bind(this);
|
|
56
|
+
this.manipulateQuery.bind(this);
|
|
57
|
+
this.collectDependencies.bind(this);
|
|
58
|
+
}
|
|
59
|
+
__collectEntityDependencies = async (type, itemIds, transaction) => {
|
|
60
|
+
//Assert this collection has dependencies fields to go over
|
|
61
|
+
const dependencyDefs = (this.dbDef.dependencies ?? {});
|
|
62
|
+
const dependencyDefKeys = _keys(dependencyDefs).filter(key => dependencyDefs[key].dbKey === type);
|
|
63
|
+
if (!dependencyDefKeys.length)
|
|
64
|
+
return;
|
|
65
|
+
//Collect all conflicting item queries
|
|
66
|
+
const conflictItemQueries = dependencyDefKeys.reduce((acc, dependencyDefKey) => {
|
|
67
|
+
const dependencyDef = dependencyDefs[dependencyDefKey];
|
|
68
|
+
let whereClause;
|
|
69
|
+
switch (dependencyDef.fieldType) {
|
|
70
|
+
case 'string':
|
|
71
|
+
whereClause = ids => ({ [dependencyDefKey]: { $in: ids } });
|
|
72
|
+
break;
|
|
73
|
+
case 'string[]':
|
|
74
|
+
whereClause = ids => ({ [dependencyDefKey]: { $aca: ids } });
|
|
75
|
+
break;
|
|
76
|
+
default:
|
|
77
|
+
throw new BadImplementationException(`Proto Dependency fieldType is not 'string'/'string[]'. Cannot check for EntityDependency for collection '${this.dbDef.dbKey}'.`);
|
|
78
|
+
}
|
|
79
|
+
acc.push(batchActionParallel(itemIds, 10, async (ids) => this.query.unManipulatedQuery({ where: whereClause(ids) }, transaction)));
|
|
80
|
+
return acc;
|
|
81
|
+
}, []);
|
|
82
|
+
if (!conflictItemQueries.length)
|
|
83
|
+
return;
|
|
84
|
+
//Get all conflicting items
|
|
85
|
+
let conflictingItems = filterInstances((await Promise.all(conflictItemQueries)).flat());
|
|
86
|
+
conflictingItems = filterDuplicates(conflictingItems, dbObjectToId);
|
|
87
|
+
//Filter out conflicting items that were already deleted in this transaction
|
|
88
|
+
const ignoredInThisTransaction = MemKey_DeletedDocs.get([]).find(item => item.transaction === transaction);
|
|
89
|
+
if (ignoredInThisTransaction) {
|
|
90
|
+
//The key associated with this collection
|
|
91
|
+
const ignoredForThisCollection = ignoredInThisTransaction.deleted[this.dbDef.dbKey];
|
|
92
|
+
//Filter out all ids of items which were already deleted in this transaction
|
|
93
|
+
conflictingItems = conflictingItems.filter(object => !ignoredForThisCollection?.has(object._id));
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
dbKey: type,
|
|
97
|
+
dependencyMap: this.mapConflicts(conflictingItems, itemIds, dependencyDefKeys),
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
mapConflicts = (conflictItems, itemIds, conflictFields) => {
|
|
101
|
+
return itemIds.reduce((acc, itemId) => {
|
|
102
|
+
const conflictingItems = conflictItems.filter(item => {
|
|
103
|
+
for (const field of conflictFields) {
|
|
104
|
+
const value = getDotNotatedValue(field, item);
|
|
105
|
+
if (asArray(value).includes(itemId))
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
});
|
|
110
|
+
if (conflictingItems.length)
|
|
111
|
+
acc[itemId] = { [this.dbDef.dbKey]: conflictingItems.map(dbObjectToId) };
|
|
112
|
+
return acc;
|
|
113
|
+
}, {});
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Executed during the initialization of the module.
|
|
117
|
+
* The collection reference is set in this method.
|
|
118
|
+
*/
|
|
119
|
+
init() {
|
|
120
|
+
const firestore = ModuleBE_Firebase.createAdminSession(this.config?.projectId).getFirestoreV3();
|
|
121
|
+
this.collection = firestore.getCollection(this.dbDef, {
|
|
122
|
+
canDeleteItems: this.canDeleteItems.bind(this),
|
|
123
|
+
preWriteProcessing: this._preWriteProcessing.bind(this),
|
|
124
|
+
postWriteProcessing: this._postWriteProcessing.bind(this),
|
|
125
|
+
upgradeInstances: this.upgradeInstances.bind(this),
|
|
126
|
+
manipulateQuery: this.manipulateQuery.bind(this)
|
|
127
|
+
});
|
|
128
|
+
// ############################## API ##############################
|
|
129
|
+
this.runTransaction = this.collection.runTransaction;
|
|
130
|
+
const wrapInTryCatch = (input, path) => _keys(input).reduce((acc, key) => {
|
|
131
|
+
const value = input[key];
|
|
132
|
+
const newPath = path ? `${path}.${String(key)}` : String(key);
|
|
133
|
+
if (typeof value === 'function') {
|
|
134
|
+
acc[key] = (async (...args) => {
|
|
135
|
+
try {
|
|
136
|
+
return await value(...args);
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
this.logError(`Error while calling "${newPath}"`);
|
|
140
|
+
throw e;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
return acc;
|
|
144
|
+
}
|
|
145
|
+
if (typeof value === 'object' && value !== null) {
|
|
146
|
+
acc[key] = wrapInTryCatch(value, newPath);
|
|
147
|
+
return acc;
|
|
148
|
+
}
|
|
149
|
+
acc[key] = value;
|
|
150
|
+
return acc;
|
|
151
|
+
}, {});
|
|
152
|
+
this.query = wrapInTryCatch(this.collection.query, 'query');
|
|
153
|
+
this.create = wrapInTryCatch(this.collection.create, 'create');
|
|
154
|
+
this.set = wrapInTryCatch(this.collection.set, 'set');
|
|
155
|
+
this.delete = wrapInTryCatch(this.collection.delete, 'delete');
|
|
156
|
+
this.doc = wrapInTryCatch(this.collection.doc, 'doc');
|
|
157
|
+
}
|
|
158
|
+
querySync = async (syncQuery) => {
|
|
159
|
+
const items = await this.collection.query.custom(syncQuery);
|
|
160
|
+
const deletedItems = await ModuleBE_SyncManager.queryDeleted(this.dbDef.dbKey, syncQuery);
|
|
161
|
+
await this.upgradeInstances(items);
|
|
162
|
+
return { toUpdate: items, toDelete: deletedItems };
|
|
163
|
+
};
|
|
164
|
+
_preWriteProcessing = async (dbItem, originalDbInstance, transaction, upgrade = true) => {
|
|
165
|
+
await this.preWriteProcessing(dbItem, originalDbInstance, transaction);
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* Override this method to customize the processing that should be done before create, set or update.
|
|
169
|
+
*
|
|
170
|
+
* @param transaction - The transaction object.
|
|
171
|
+
* @param dbInstance - The DB entry for which the uniqueness is being asserted.
|
|
172
|
+
* @param originalDbInstance - The DB instance fetched from remote firestore.
|
|
173
|
+
*/
|
|
174
|
+
async preWriteProcessing(dbInstance, originalDbInstance, transaction) {
|
|
175
|
+
}
|
|
176
|
+
_postWriteProcessing = async (data, actionType, transaction) => {
|
|
177
|
+
const now = currentTimeMillis();
|
|
178
|
+
if (data.updated && !(Array.isArray(data.updated) && data.updated.length === 0)) {
|
|
179
|
+
const latestUpdated = Array.isArray(data.updated) ?
|
|
180
|
+
data.updated.reduce((toRet, current) => Math.max(toRet, current.__updated), data.updated[0].__updated) :
|
|
181
|
+
data.updated.__updated;
|
|
182
|
+
await ModuleBE_SyncManager.setLastUpdated(this.dbDef.dbKey, latestUpdated);
|
|
183
|
+
}
|
|
184
|
+
if (data.deleted && !(Array.isArray(data.updated) && data.updated.length === 0)) {
|
|
185
|
+
await ModuleBE_SyncManager.onItemsDeleted(this.dbDef.dbKey, asArray(data.deleted), this.config.uniqueKeys, transaction);
|
|
186
|
+
await ModuleBE_SyncManager.setLastUpdated(this.dbDef.dbKey, now);
|
|
187
|
+
}
|
|
188
|
+
else if (data.deleted === null)
|
|
189
|
+
// this means the whole collection has been deleted - setting the oldestDeleted to now will trigger a clean sync
|
|
190
|
+
await ModuleBE_SyncManager.setOldestDeleted(this.dbDef.dbKey, now);
|
|
191
|
+
await this.postWriteProcessing(data, actionType, transaction);
|
|
192
|
+
};
|
|
193
|
+
/**
|
|
194
|
+
* Override this method to customize processing that should be done after create, set, update or delete.
|
|
195
|
+
* @param data
|
|
196
|
+
* @param actionType create/set/update/delete
|
|
197
|
+
* @param transaction
|
|
198
|
+
*/
|
|
199
|
+
async postWriteProcessing(data, actionType, transaction) {
|
|
200
|
+
}
|
|
201
|
+
manipulateQuery(query) {
|
|
202
|
+
return query;
|
|
203
|
+
}
|
|
204
|
+
preUpsertProcessing;
|
|
205
|
+
/**
|
|
206
|
+
* Override this method to provide actions or assertions to be executed before the deletion happens.
|
|
207
|
+
* @param transaction - The transaction object
|
|
208
|
+
* @param dbItems - The DB entry that is going to be deleted.
|
|
209
|
+
*/
|
|
210
|
+
async canDeleteItems(dbItems, transaction) {
|
|
211
|
+
const dependencies = await this.collectDependencies(dbItems, transaction);
|
|
212
|
+
if (dependencies)
|
|
213
|
+
throw new ApiException(422, 'entity has dependencies').setErrorBody({
|
|
214
|
+
type: 'entity-has-dependencies',
|
|
215
|
+
data: dependencies
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
async collectDependencies(dbInstances, transaction) {
|
|
219
|
+
const dependencyResponses = await dispatch_CollectEntityDependencies.dispatchModuleAsync(this.dbDef.dbKey, dbInstances.map(dbObjectToId), transaction);
|
|
220
|
+
const filtered = filterInstances(dependencyResponses);
|
|
221
|
+
if (!filtered.length)
|
|
222
|
+
return undefined;
|
|
223
|
+
const merged = filtered.reduce((acc, dependency) => merge(acc, dependency));
|
|
224
|
+
return _keys(merged.dependencyMap).length ? merged : undefined;
|
|
225
|
+
}
|
|
226
|
+
versionUpgrades = {};
|
|
227
|
+
/**
|
|
228
|
+
* Upgrades the entity from the given version to the next one (to the same version if the given version is the latest)
|
|
229
|
+
* @param version - The version we start from
|
|
230
|
+
* @param processor
|
|
231
|
+
*/
|
|
232
|
+
registerVersionUpgradeProcessor(version, processor) {
|
|
233
|
+
this.versionUpgrades[version] = processor;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Check if the collection has at least one item without the latest version. Version[0] is the latest version.
|
|
237
|
+
*/
|
|
238
|
+
isCollectionUpToDate = async () => {
|
|
239
|
+
return (await this.query.unManipulatedQuery({
|
|
240
|
+
limit: 1,
|
|
241
|
+
where: { _v: { $neq: this.dbDef.versions[0] } }
|
|
242
|
+
})).length === 0;
|
|
243
|
+
};
|
|
244
|
+
upgradeCollection = async (force = false) => {
|
|
245
|
+
return this.processCollection(async (instances) => {
|
|
246
|
+
const instancesToSave = await this.upgradeInstances(instances, force);
|
|
247
|
+
// @ts-ignore
|
|
248
|
+
await this.collection.upgradeInstances(instancesToSave);
|
|
249
|
+
});
|
|
250
|
+
};
|
|
251
|
+
processCollection = async (processInstances) => {
|
|
252
|
+
let docs;
|
|
253
|
+
const itemsCount = this.config.chunksSize;
|
|
254
|
+
const query = {
|
|
255
|
+
limit: { page: 0, itemsCount },
|
|
256
|
+
};
|
|
257
|
+
while ((docs = await this.collection.doc.unManipulatedQuery(query)).length > 0) {
|
|
258
|
+
// this is old Backward compatible from before the assertion of unique ids where the doc ref is the _id of the doc
|
|
259
|
+
const toDelete = docs.filter(doc => {
|
|
260
|
+
return doc.ref.id !== doc.data._id;
|
|
261
|
+
});
|
|
262
|
+
const instances = docs.map(d => d.data);
|
|
263
|
+
this.logWarning(`Upgrading batch(${query.limit.page}) found instances(${instances.length}) for entity: "${this.dbDef.entityName}" ....`);
|
|
264
|
+
await processInstances(instances);
|
|
265
|
+
if (toDelete.length > 0) {
|
|
266
|
+
this.logWarning(`Need to delete docs: ${toDelete.length} ${this.dbDef.entityName}s ....`);
|
|
267
|
+
await this.collection.delete.multi.allDocs(toDelete);
|
|
268
|
+
}
|
|
269
|
+
query.limit.page++;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
async upgradeInstances(instances, force = false) {
|
|
273
|
+
let instancesToSave = [];
|
|
274
|
+
for (let i = this.config.versions.length - 1; i >= 0; i--) {
|
|
275
|
+
const version = this.config.versions[i];
|
|
276
|
+
const instancesToUpgrade = instances.filter(instance => instance._v === version);
|
|
277
|
+
const nextVersion = this.config.versions[i - 1] ?? version;
|
|
278
|
+
const versionTransition = `${version} => ${nextVersion}`;
|
|
279
|
+
if (instancesToUpgrade.length === 0) {
|
|
280
|
+
this.logVerbose(`No instances to upgrade from ${versionTransition}`);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const upgradeProcessor = this.versionUpgrades[version];
|
|
284
|
+
if (!upgradeProcessor) {
|
|
285
|
+
this.logVerbose(`Will not update ${instancesToUpgrade.length} instances of version ${versionTransition}`);
|
|
286
|
+
this.logVerbose(`No upgrade processor for: ${versionTransition}`);
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
this.logVerbose(`Upgrade instances(${instancesToUpgrade.length}): ${versionTransition}`);
|
|
290
|
+
await upgradeProcessor?.(instancesToUpgrade);
|
|
291
|
+
instancesToSave.push(...instancesToUpgrade);
|
|
292
|
+
}
|
|
293
|
+
instancesToSave = filterDuplicates(instancesToSave);
|
|
294
|
+
instancesToUpgrade.forEach(instance => instance._v = nextVersion);
|
|
295
|
+
}
|
|
296
|
+
return force ? instances : instancesToSave;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ApiDef, BaseHttpModule_Class, BaseHttpRequest, TypedApi } from '@nu-art/thunderstorm-shared';
|
|
2
|
+
import { ApiError_GeneralErrorMessage, ApiErrorResponse, ResponseError } from '@nu-art/ts-common/core/exceptions/types';
|
|
3
|
+
import { AxiosRequestConfig as Axios_RequestConfig } from 'axios';
|
|
4
|
+
export declare class AxiosHttpModule_Class extends BaseHttpModule_Class {
|
|
5
|
+
private requestOption;
|
|
6
|
+
init(): void;
|
|
7
|
+
createRequest<API extends TypedApi<any, any, any, any>>(apiDef: ApiDef<API>, data?: string): AxiosHttpRequest<API>;
|
|
8
|
+
setRequestOption(requestOption: Axios_RequestConfig): this;
|
|
9
|
+
}
|
|
10
|
+
export declare const AxiosHttpModule: AxiosHttpModule_Class;
|
|
11
|
+
declare class AxiosHttpRequest<API extends TypedApi<any, any, any, any>> extends BaseHttpRequest<API> {
|
|
12
|
+
private response?;
|
|
13
|
+
private cancelController;
|
|
14
|
+
protected status?: number;
|
|
15
|
+
private requestOption;
|
|
16
|
+
constructor(requestKey: string, requestData?: string, shouldCompress?: boolean);
|
|
17
|
+
getStatus(): number;
|
|
18
|
+
getResponse(): any;
|
|
19
|
+
protected abortImpl(): void;
|
|
20
|
+
getErrorResponse(): ApiErrorResponse<ResponseError | ApiError_GeneralErrorMessage>;
|
|
21
|
+
setRequestOption(requestOption: Axios_RequestConfig): this;
|
|
22
|
+
protected executeImpl(): Promise<void>;
|
|
23
|
+
_getResponseHeader(headerKey: string): string | string[] | undefined;
|
|
24
|
+
}
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Thunderstorm is a full web app framework!
|
|
3
|
+
*
|
|
4
|
+
* Typescript & Express backend infrastructure that natively runs on firebase function
|
|
5
|
+
* Typescript & React frontend infrastructure
|
|
6
|
+
*
|
|
7
|
+
* Copyright (C) 2020 Adam van der Kruk aka TacB0sS
|
|
8
|
+
*
|
|
9
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
10
|
+
* you may not use this file except in compliance with the License.
|
|
11
|
+
* You may obtain a copy of the License at
|
|
12
|
+
*
|
|
13
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
14
|
+
*
|
|
15
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
16
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
17
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
18
|
+
* See the License for the specific language governing permissions and
|
|
19
|
+
* limitations under the License.
|
|
20
|
+
*/
|
|
21
|
+
// noinspection TypeScriptPreferShortImport
|
|
22
|
+
import { BaseHttpModule_Class, BaseHttpRequest } from '@nu-art/thunderstorm-shared';
|
|
23
|
+
import { BadImplementationException, composeUrl, StaticLogger } from '@nu-art/ts-common';
|
|
24
|
+
// Axios v1+ import style
|
|
25
|
+
import axios, { CanceledError } from 'axios';
|
|
26
|
+
export class AxiosHttpModule_Class extends BaseHttpModule_Class {
|
|
27
|
+
requestOption = {};
|
|
28
|
+
init() {
|
|
29
|
+
super.init();
|
|
30
|
+
let origin = this.config.origin;
|
|
31
|
+
if (!origin)
|
|
32
|
+
return;
|
|
33
|
+
if (origin?.endsWith('/'))
|
|
34
|
+
origin = origin.substring(0, origin.length - 1);
|
|
35
|
+
this.origin = origin;
|
|
36
|
+
}
|
|
37
|
+
createRequest(apiDef, data) {
|
|
38
|
+
const request = new AxiosHttpRequest(apiDef.path, data, this.shouldCompress())
|
|
39
|
+
.setLogger(this)
|
|
40
|
+
.setMethod(apiDef.method)
|
|
41
|
+
.setTimeout(this.timeout)
|
|
42
|
+
.setRequestOption(this.requestOption)
|
|
43
|
+
.addHeaders(this.getDefaultHeaders());
|
|
44
|
+
if (apiDef.fullUrl)
|
|
45
|
+
request.setUrl(apiDef.fullUrl);
|
|
46
|
+
else
|
|
47
|
+
request.setOrigin(apiDef.baseUrl ?? this.origin).setRelativeUrl(apiDef.path);
|
|
48
|
+
return request;
|
|
49
|
+
}
|
|
50
|
+
setRequestOption(requestOption) {
|
|
51
|
+
this.requestOption = requestOption;
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export const AxiosHttpModule = new AxiosHttpModule_Class();
|
|
56
|
+
class AxiosHttpRequest extends BaseHttpRequest {
|
|
57
|
+
response;
|
|
58
|
+
cancelController;
|
|
59
|
+
status;
|
|
60
|
+
requestOption = {};
|
|
61
|
+
constructor(requestKey, requestData, shouldCompress) {
|
|
62
|
+
super(requestKey, requestData);
|
|
63
|
+
this.compress = shouldCompress === undefined ? false : shouldCompress;
|
|
64
|
+
this.cancelController = new AbortController();
|
|
65
|
+
}
|
|
66
|
+
getStatus() {
|
|
67
|
+
if (!this.status)
|
|
68
|
+
throw new BadImplementationException('Missing status..');
|
|
69
|
+
return this.status;
|
|
70
|
+
}
|
|
71
|
+
getResponse() {
|
|
72
|
+
return this.response?.data;
|
|
73
|
+
}
|
|
74
|
+
abortImpl() {
|
|
75
|
+
this.cancelController.abort();
|
|
76
|
+
}
|
|
77
|
+
getErrorResponse() {
|
|
78
|
+
return { debugMessage: this.getResponse() };
|
|
79
|
+
}
|
|
80
|
+
setRequestOption(requestOption) {
|
|
81
|
+
this.requestOption = requestOption;
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
executeImpl() {
|
|
85
|
+
const executor = async (resolve, reject) => {
|
|
86
|
+
if (this.aborted)
|
|
87
|
+
return resolve();
|
|
88
|
+
const fullUrl = composeUrl(this.url, this.params);
|
|
89
|
+
const body = this.body;
|
|
90
|
+
if (typeof body === 'string')
|
|
91
|
+
this.addHeader('Content-Length', `${body.length}`);
|
|
92
|
+
const headers = Object.keys(this.headers).reduce((carry, headerKey) => {
|
|
93
|
+
carry[headerKey] = this.headers[headerKey].join('; ');
|
|
94
|
+
return carry;
|
|
95
|
+
}, {});
|
|
96
|
+
const options = {
|
|
97
|
+
...this.requestOption,
|
|
98
|
+
url: fullUrl,
|
|
99
|
+
method: this.method,
|
|
100
|
+
headers,
|
|
101
|
+
timeout: this.timeout,
|
|
102
|
+
signal: this.cancelController.signal, // ✅ Axios v1 cancellation
|
|
103
|
+
};
|
|
104
|
+
if (body)
|
|
105
|
+
options.data = body;
|
|
106
|
+
if (this.responseType)
|
|
107
|
+
options.responseType = this.responseType;
|
|
108
|
+
this.logger?.logDebug(options);
|
|
109
|
+
try {
|
|
110
|
+
this.response = await axios.request(options);
|
|
111
|
+
this.status = this.response?.status ?? 200;
|
|
112
|
+
return resolve();
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
// cancellation path in v1
|
|
116
|
+
if (e instanceof CanceledError || (axios.isCancel && axios.isCancel(e)) || e?.code === 'ERR_CANCELED') {
|
|
117
|
+
this.aborted = true;
|
|
118
|
+
StaticLogger.logWarning('Api cancelled: ', e.message);
|
|
119
|
+
}
|
|
120
|
+
this.response = e?.response;
|
|
121
|
+
this.status = this.response?.status ?? 500;
|
|
122
|
+
return reject(e);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
return new Promise(executor);
|
|
126
|
+
}
|
|
127
|
+
_getResponseHeader(headerKey) {
|
|
128
|
+
if (!this.response)
|
|
129
|
+
throw new BadImplementationException(`axios didn't return yet`);
|
|
130
|
+
return this.response.headers?.[headerKey];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { AxiosRequestConfig, AxiosResponse, CancelTokenSource, Method, ResponseType } from 'axios';
|
|
2
|
+
export type Axios_RequestConfig = AxiosRequestConfig;
|
|
3
|
+
export type Axios_Response<T = any> = AxiosResponse<T>;
|
|
4
|
+
export type Axios_CancelTokenSource = CancelTokenSource;
|
|
5
|
+
export type Axios_Method = Method;
|
|
6
|
+
export type Axios_ResponseType = ResponseType;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Module, TS_Object } from '@nu-art/ts-common';
|
|
2
|
+
import { ServerApi } from '../server/server-api.js';
|
|
3
|
+
import { ServerApi_Middleware } from '../../utils/types.js';
|
|
4
|
+
import { TypedApi } from '@nu-art/thunderstorm-shared';
|
|
5
|
+
type ProxyConfig = {
|
|
6
|
+
extras?: TS_Object;
|
|
7
|
+
urls: string[];
|
|
8
|
+
secret: string;
|
|
9
|
+
};
|
|
10
|
+
export type RemoteProxyConfig = {
|
|
11
|
+
remotes: {
|
|
12
|
+
[proxyId: string]: ProxyConfig;
|
|
13
|
+
};
|
|
14
|
+
secretHeaderName?: string;
|
|
15
|
+
proxyHeaderName?: string;
|
|
16
|
+
};
|
|
17
|
+
export declare class ModuleBE_RemoteProxy_Class<Config extends RemoteProxyConfig> extends Module<Config> {
|
|
18
|
+
readonly Middleware: ServerApi_Middleware;
|
|
19
|
+
private secretHeader;
|
|
20
|
+
private proxyHeader;
|
|
21
|
+
protected init(): void;
|
|
22
|
+
assertSecret(): TS_Object | undefined;
|
|
23
|
+
}
|
|
24
|
+
export declare class ServerApi_Proxy<API extends TypedApi<any, any, any, any>> extends ServerApi<API> {
|
|
25
|
+
private readonly api;
|
|
26
|
+
constructor(api: ServerApi<API>);
|
|
27
|
+
protected process(): Promise<API['R']>;
|
|
28
|
+
}
|
|
29
|
+
export declare class ServerApi_Alternate<API extends TypedApi<any, any, any, any>> extends ServerApi<API> {
|
|
30
|
+
private readonly api;
|
|
31
|
+
constructor(api: ServerApi<API>, pathSuffix: string);
|
|
32
|
+
protected process(): Promise<API['R']>;
|
|
33
|
+
}
|
|
34
|
+
export declare const ModuleBE_RemoteProxy: ModuleBE_RemoteProxy_Class<RemoteProxyConfig>;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Thunderstorm is a full web app framework!
|
|
3
|
+
*
|
|
4
|
+
* Typescript & Express backend infrastructure that natively runs on firebase function
|
|
5
|
+
* Typescript & React frontend infrastructure
|
|
6
|
+
*
|
|
7
|
+
* Copyright (C) 2020 Adam van der Kruk aka TacB0sS
|
|
8
|
+
*
|
|
9
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
10
|
+
* you may not use this file except in compliance with the License.
|
|
11
|
+
* You may obtain a copy of the License at
|
|
12
|
+
*
|
|
13
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
14
|
+
*
|
|
15
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
16
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
17
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
18
|
+
* See the License for the specific language governing permissions and
|
|
19
|
+
* limitations under the License.
|
|
20
|
+
*/
|
|
21
|
+
import { ApiException, ImplementationMissingException, Module } from '@nu-art/ts-common';
|
|
22
|
+
import { ServerApi } from '../server/server-api.js';
|
|
23
|
+
import { HeaderKey } from '../server/HeaderKey.js';
|
|
24
|
+
import { MemKey_HttpRequestPath } from '../server/consts.js';
|
|
25
|
+
export class ModuleBE_RemoteProxy_Class extends Module {
|
|
26
|
+
Middleware = async () => {
|
|
27
|
+
ModuleBE_RemoteProxy.assertSecret();
|
|
28
|
+
};
|
|
29
|
+
secretHeader;
|
|
30
|
+
proxyHeader;
|
|
31
|
+
init() {
|
|
32
|
+
if (!this.config)
|
|
33
|
+
throw new ImplementationMissingException('MUST specify config for this module!!');
|
|
34
|
+
if (!this.config.secretHeaderName)
|
|
35
|
+
this.config.secretHeaderName = 'x-secret';
|
|
36
|
+
if (!this.config.proxyHeaderName)
|
|
37
|
+
this.config.proxyHeaderName = 'x-proxy';
|
|
38
|
+
this.secretHeader = new HeaderKey(this.config.secretHeaderName);
|
|
39
|
+
this.proxyHeader = new HeaderKey(this.config.proxyHeaderName);
|
|
40
|
+
}
|
|
41
|
+
assertSecret() {
|
|
42
|
+
if (!this.secretHeader || !this.proxyHeader)
|
|
43
|
+
throw new ImplementationMissingException('MUST add RemoteProxy to your module list!!!');
|
|
44
|
+
const secret = this.secretHeader.get();
|
|
45
|
+
const proxyId = this.proxyHeader.get();
|
|
46
|
+
const expectedSecret = this.config.remotes[proxyId];
|
|
47
|
+
if (!proxyId)
|
|
48
|
+
throw new ApiException(403, `Missing proxy declaration in config for ${proxyId} !!`);
|
|
49
|
+
if (!secret)
|
|
50
|
+
throw new ApiException(403, `Missing secret !!`);
|
|
51
|
+
if (!expectedSecret)
|
|
52
|
+
throw new ApiException(403, `ProxyId '${proxyId}' is not registered for remote access !!`);
|
|
53
|
+
if (expectedSecret.secret !== secret)
|
|
54
|
+
throw new ApiException(403, `Secret does not match for proxyId: ${proxyId}`);
|
|
55
|
+
const requestUrl = MemKey_HttpRequestPath.get();
|
|
56
|
+
if (!expectedSecret.urls?.find(urlPattern => requestUrl.match(urlPattern)))
|
|
57
|
+
throw new ApiException(403, `Requested url '${requestUrl}' is not allowed from proxyId: ${proxyId}`);
|
|
58
|
+
return expectedSecret.extras;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export class ServerApi_Proxy extends ServerApi {
|
|
62
|
+
api;
|
|
63
|
+
constructor(api) {
|
|
64
|
+
// super(api.method, `${api.relativePath}/proxy`);
|
|
65
|
+
super({ ...api.apiDef, path: `${api.apiDef.path}/proxy` });
|
|
66
|
+
this.api = api;
|
|
67
|
+
this.setMiddlewares(ModuleBE_RemoteProxy.Middleware);
|
|
68
|
+
}
|
|
69
|
+
async process() {
|
|
70
|
+
// @ts-ignore
|
|
71
|
+
return this.api.process();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export class ServerApi_Alternate extends ServerApi {
|
|
75
|
+
api;
|
|
76
|
+
constructor(api, pathSuffix) {
|
|
77
|
+
// super(api.method, `${api.relativePath}/proxy`);
|
|
78
|
+
super({ ...api.apiDef, path: `${api.apiDef.path}/${pathSuffix}` });
|
|
79
|
+
this.api = api;
|
|
80
|
+
}
|
|
81
|
+
async process() {
|
|
82
|
+
// @ts-ignore
|
|
83
|
+
return this.api.process();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export const ModuleBE_RemoteProxy = new ModuleBE_RemoteProxy_Class();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Module } from '@nu-art/ts-common';
|
|
2
|
+
import { BodyApi, QueryApi } from '@nu-art/thunderstorm-shared';
|
|
3
|
+
export type RemoteServerConfig = {
|
|
4
|
+
secretHeaderName: string;
|
|
5
|
+
proxyHeaderName: string;
|
|
6
|
+
proxyId: string;
|
|
7
|
+
secret: string;
|
|
8
|
+
url: string;
|
|
9
|
+
};
|
|
10
|
+
export declare class RemoteProxyCaller<Config extends RemoteServerConfig> extends Module<Config> {
|
|
11
|
+
protected init(): void;
|
|
12
|
+
protected executeGetRequest: <API extends QueryApi<any, any, any>>(url: string, _params: API["P"], _headers?: {
|
|
13
|
+
[key: string]: string;
|
|
14
|
+
}) => Promise<API["R"]>;
|
|
15
|
+
protected executePostRequest: <API extends BodyApi<any, any, any>>(url: string, body: API["B"], _headers?: {
|
|
16
|
+
[key: string]: string;
|
|
17
|
+
}) => Promise<API["R"]>;
|
|
18
|
+
private executeRequest;
|
|
19
|
+
}
|