@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.
Files changed (111) hide show
  1. package/_entity/app-config/ModuleBE_AppConfigAPI.d.ts +9 -0
  2. package/_entity/app-config/ModuleBE_AppConfigAPI.js +20 -0
  3. package/_entity/app-config/ModuleBE_AppConfigDB.d.ts +27 -0
  4. package/_entity/app-config/ModuleBE_AppConfigDB.js +91 -0
  5. package/_entity/app-config/index.d.ts +2 -0
  6. package/_entity/app-config/index.js +2 -0
  7. package/_entity/app-config/module-pack.d.ts +2 -0
  8. package/_entity/app-config/module-pack.js +3 -0
  9. package/_entity/backup-doc/ModuleBE_BackupDocDB.d.ts +52 -0
  10. package/_entity/backup-doc/ModuleBE_BackupDocDB.js +350 -0
  11. package/_entity/backup-doc/ModuleBE_BackupScheduler.d.ts +7 -0
  12. package/_entity/backup-doc/ModuleBE_BackupScheduler.js +14 -0
  13. package/_entity/backup-doc/index.d.ts +3 -0
  14. package/_entity/backup-doc/index.js +3 -0
  15. package/_entity/backup-doc/module-pack.d.ts +2 -0
  16. package/_entity/backup-doc/module-pack.js +3 -0
  17. package/_entity/editable-test/ModuleBE_EditableTestDB.d.ts +8 -0
  18. package/_entity/editable-test/ModuleBE_EditableTestDB.js +8 -0
  19. package/_entity/editable-test/index.d.ts +1 -0
  20. package/_entity/editable-test/index.js +1 -0
  21. package/_entity/editable-test/module-pack.d.ts +1 -0
  22. package/_entity/editable-test/module-pack.js +3 -0
  23. package/_entity.d.ts +3 -0
  24. package/_entity.js +3 -0
  25. package/core/BaseStorm.d.ts +17 -0
  26. package/core/BaseStorm.js +77 -0
  27. package/core/Storm.d.ts +15 -0
  28. package/core/Storm.js +93 -0
  29. package/core/db-def.d.ts +10 -0
  30. package/core/db-def.js +11 -0
  31. package/core/default-storm.d.ts +3 -0
  32. package/core/default-storm.js +30 -0
  33. package/core/storm-modulepack.d.ts +3 -0
  34. package/core/storm-modulepack.js +20 -0
  35. package/core/typed-api.d.ts +7 -0
  36. package/core/typed-api.js +46 -0
  37. package/exceptions.d.ts +1 -0
  38. package/exceptions.js +21 -0
  39. package/index.d.ts +27 -0
  40. package/index.js +48 -0
  41. package/modules/CleanupScheduler.d.ts +14 -0
  42. package/modules/CleanupScheduler.js +50 -0
  43. package/modules/ModuleBE_APIs.d.ts +11 -0
  44. package/modules/ModuleBE_APIs.js +19 -0
  45. package/modules/ModuleBE_CSVParser.d.ts +9 -0
  46. package/modules/ModuleBE_CSVParser.js +50 -0
  47. package/modules/ModuleBE_ForceUpgrade.d.ts +21 -0
  48. package/modules/ModuleBE_ForceUpgrade.js +70 -0
  49. package/modules/ModuleBE_ServerInfo.d.ts +20 -0
  50. package/modules/ModuleBE_ServerInfo.js +76 -0
  51. package/modules/_imports.d.ts +6 -0
  52. package/modules/_imports.js +26 -0
  53. package/modules/_tdb/service-accounts.d.ts +19 -0
  54. package/modules/_tdb/service-accounts.js +2 -0
  55. package/modules/action-processor/Action_SetupProject.d.ts +9 -0
  56. package/modules/action-processor/Action_SetupProject.js +23 -0
  57. package/modules/action-processor/ModuleBE_ActionProcessor.d.ts +11 -0
  58. package/modules/action-processor/ModuleBE_ActionProcessor.js +67 -0
  59. package/modules/action-processor/types.d.ts +10 -0
  60. package/modules/action-processor/types.js +1 -0
  61. package/modules/archiving/ModuleBE_Archiving.d.ts +119 -0
  62. package/modules/archiving/ModuleBE_Archiving.js +236 -0
  63. package/modules/collection-actions/ModuleBE_CollectionActions.d.ts +12 -0
  64. package/modules/collection-actions/ModuleBE_CollectionActions.js +69 -0
  65. package/modules/collection-actions/dispatcher.d.ts +7 -0
  66. package/modules/collection-actions/dispatcher.js +2 -0
  67. package/modules/db-api-gen/ModuleBE_BaseApi.d.ts +16 -0
  68. package/modules/db-api-gen/ModuleBE_BaseApi.js +74 -0
  69. package/modules/db-api-gen/ModuleBE_BaseDB.d.ts +78 -0
  70. package/modules/db-api-gen/ModuleBE_BaseDB.js +298 -0
  71. package/modules/http/AxiosHttpModule.d.ts +25 -0
  72. package/modules/http/AxiosHttpModule.js +132 -0
  73. package/modules/http/types.d.ts +6 -0
  74. package/modules/http/types.js +1 -0
  75. package/modules/proxy/ModuleBE_RemoteProxy.d.ts +35 -0
  76. package/modules/proxy/ModuleBE_RemoteProxy.js +86 -0
  77. package/modules/proxy/RemoteProxyCaller.d.ts +19 -0
  78. package/modules/proxy/RemoteProxyCaller.js +82 -0
  79. package/modules/proxy/assert-secret-middleware.d.ts +2 -0
  80. package/modules/proxy/assert-secret-middleware.js +24 -0
  81. package/modules/server/HeaderKey.d.ts +8 -0
  82. package/modules/server/HeaderKey.js +41 -0
  83. package/modules/server/HttpServer.d.ts +41 -0
  84. package/modules/server/HttpServer.js +223 -0
  85. package/modules/server/consts.d.ts +13 -0
  86. package/modules/server/consts.js +9 -0
  87. package/modules/server/route-resolvers/RouteResolver_Dummy.d.ts +7 -0
  88. package/modules/server/route-resolvers/RouteResolver_Dummy.js +34 -0
  89. package/modules/server/route-resolvers/RouteResolver_ModulePath.d.ts +22 -0
  90. package/modules/server/route-resolvers/RouteResolver_ModulePath.js +84 -0
  91. package/modules/server/route-resolvers/index.d.ts +7 -0
  92. package/modules/server/route-resolvers/index.js +21 -0
  93. package/modules/server/server-api.d.ts +85 -0
  94. package/modules/server/server-api.js +362 -0
  95. package/modules/server/server-errors.d.ts +4 -0
  96. package/modules/server/server-errors.js +79 -0
  97. package/modules/sync-env/ModuleBE_SyncEnv.d.ts +36 -0
  98. package/modules/sync-env/ModuleBE_SyncEnv.js +212 -0
  99. package/modules/sync-manager/ModuleBE_SyncManager.d.ts +63 -0
  100. package/modules/sync-manager/ModuleBE_SyncManager.js +254 -0
  101. package/package.json +104 -0
  102. package/shared.d.ts +1 -0
  103. package/shared.js +21 -0
  104. package/test/StormTest.d.ts +23 -0
  105. package/test/StormTest.js +49 -0
  106. package/utils/file.d.ts +2 -0
  107. package/utils/file.js +29 -0
  108. package/utils/promisify-request.d.ts +3 -0
  109. package/utils/promisify-request.js +33 -0
  110. package/utils/types.d.ts +11 -0
  111. 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
+ }