@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,212 @@
1
+ import { ApiException, arrayToMap, BadImplementationException, Dispatcher, Minute, Module, MUSTNeverHappenException, RuntimeModules } from '@nu-art/ts-common';
2
+ import { ModuleBE_Firebase } from '@nu-art/firebase-backend';
3
+ import { addRoutes } from '../ModuleBE_APIs.js';
4
+ import { createBodyServerApi, createQueryServerApi } from '../../core/typed-api.js';
5
+ import { ApiDef_SyncEnv, HeaderKey_Authorization, HttpMethod } from '@nu-art/thunderstorm-shared';
6
+ import { AxiosHttpModule } from '../http/AxiosHttpModule.js';
7
+ import { MemKey_HttpRequest } from '../server/consts.js';
8
+ import { Storm } from '../../core/Storm.js';
9
+ import { ModuleBE_BackupDocDB } from '../../_entity/backup-doc/index.js';
10
+ import { Transform, Writable } from 'stream';
11
+ import { HttpCodes } from '@nu-art/ts-common/core/exceptions/http-codes';
12
+ const dispatch_OnSyncEnvCompleted = new Dispatcher('__onSyncEnvCompleted');
13
+ class ModuleBE_SyncEnv_Class extends Module {
14
+ constructor() {
15
+ super();
16
+ this.setDefaultConfig({ maxBatch: 500 });
17
+ }
18
+ init() {
19
+ super.init();
20
+ addRoutes([
21
+ createBodyServerApi(ApiDef_SyncEnv.vv1.syncToEnv, this.pushToEnv),
22
+ createBodyServerApi(ApiDef_SyncEnv.vv1.syncFromEnvBackup, this.syncFromEnvBackup),
23
+ createQueryServerApi(ApiDef_SyncEnv.vv1.getLatestBackup, this.getLatestBackupId),
24
+ createQueryServerApi(ApiDef_SyncEnv.vv1.createBackup, this.createBackup),
25
+ createQueryServerApi(ApiDef_SyncEnv.vv1.fetchBackupMetadata, this.fetchBackupMetadata),
26
+ createQueryServerApi(ApiDef_SyncEnv.vv1.syncFirebaseFromBackup, this.syncFirebaseFromBackup),
27
+ ]);
28
+ }
29
+ fetchBackupMetadata = async (queryParams) => {
30
+ const backupInfo = await this.getBackupInfo(queryParams);
31
+ if (!backupInfo)
32
+ throw new ApiException(404, 'backup file not found');
33
+ if (!backupInfo.metadata)
34
+ throw new ApiException(404, 'No metadata found on this backup');
35
+ return {
36
+ ...backupInfo.metadata,
37
+ remoteCollectionNames: (RuntimeModules()
38
+ .filter((module) => !!module.dbDef?.dbKey)).map(_module => _module.dbDef.dbKey)
39
+ };
40
+ };
41
+ async pushToEnv(body) {
42
+ const remoteUrls = {
43
+ dev: 'https://us-central1-shopify-manager-tool-dev.cloudfunctions.net/api',
44
+ prod: 'https://mng.be.petitfawn.com'
45
+ };
46
+ const url = remoteUrls[body.env];
47
+ const sessionId = MemKey_HttpRequest.get().headers[HeaderKey_Authorization];
48
+ const module = RuntimeModules().find((module) => module.dbModule?.dbDef?.dbKey === body.moduleName);
49
+ const upsertAll = module.apiDef.v1.upsertAll;
50
+ const response = await AxiosHttpModule
51
+ .createRequest({ ...upsertAll, fullUrl: url + '/' + upsertAll.path, timeout: 5 * Minute })
52
+ .setBody(body.items)
53
+ .setUrlParams(body.items)
54
+ .addHeader(HeaderKey_Authorization, sessionId)
55
+ .executeSync(true);
56
+ console.log(response);
57
+ }
58
+ createBackup = async () => {
59
+ return ModuleBE_BackupDocDB.initiateBackup(true);
60
+ };
61
+ getLatestBackupId = async () => {
62
+ const backups = await ModuleBE_BackupDocDB.collection.query.custom({ orderBy: [{ key: "__created", order: "desc" }], limit: 1 });
63
+ const latestBackup = backups[0];
64
+ if (!latestBackup)
65
+ throw HttpCodes._4XX.ENTITY_DOESNT_EXISTS("No backup found");
66
+ const latestBackupId = latestBackup?._id;
67
+ return { latestBackupId: latestBackupId };
68
+ };
69
+ syncFromEnvBackup = async (body) => {
70
+ if (!this.config.allowSyncEnv)
71
+ throw new MUSTNeverHappenException(`SyncEnv is disabled on this env- to sync into this env, add 'allowSyncEnv: true'.`);
72
+ //CleanSync means deleting collections before syncing them
73
+ if (!this.config.allowCleanSync && body.cleanSync)
74
+ throw new MUSTNeverHappenException(`CleanSync is disabled on this env- to CleanSync into this env, add 'allowCleanSync: true'.`);
75
+ if (Storm.getInstance().getEnvironment().toLowerCase() === 'prod' && body.env.toLowerCase() !== 'prod')
76
+ throw new MUSTNeverHappenException('MUST NEVER SYNC ENV THAT IS NOT PROD TO PROD!!');
77
+ if (this.config.allowedEnvsToSyncFrom && !this.config.allowedEnvsToSyncFrom.includes(body.env))
78
+ throw new MUSTNeverHappenException(`Env ${Storm.getInstance().getEnvironment()
79
+ .toLowerCase()} doesn't have env ${body.env} in it's allowedEnvsToSyncFrom list.`);
80
+ this.logInfoBold('Received API call Fetch From Env!');
81
+ this.logInfo(`Origin env: ${body.env}, backupId: ${body.backupId}`);
82
+ let startTime = undefined; // required for log
83
+ let endTime = undefined; // required for log
84
+ if (this.config.shouldBackupBeforeSync) {
85
+ this.logInfo(`---- Creating Backup... ----`);
86
+ startTime = performance.now(); // required for log
87
+ await this.createBackup();
88
+ endTime = performance.now(); // required for log
89
+ this.logInfo(`Backup took ${((endTime - startTime) / 1000).toFixed(3)} seconds`);
90
+ }
91
+ if (body.cleanSync) {
92
+ this.logInfo(`---- Cleaning Collections From DB... ----`);
93
+ //Delete all modules specified for syncing
94
+ const modulesToDelete = RuntimeModules().filter((module) => body.selectedModules.includes(module.dbDef?.dbKey));
95
+ for (const module of modulesToDelete) {
96
+ await module.collection.delete.yes.iam.sure.iwant.todelete.the.collection.delete();
97
+ this.logInfo(`---- Cleaned Collection ${module.dbDef.dbKey} ----`);
98
+ }
99
+ }
100
+ //Prepare Syncing data
101
+ const backupInfo = await this.getBackupInfo(body);
102
+ const stream = await ModuleBE_BackupDocDB.createBackupReadStream(backupInfo);
103
+ const collectionFilter = new SyncCollectionFilter(body.selectedModules);
104
+ const collectionWriter = new CollectionBatchWriter(body.chunkSize);
105
+ this.logInfo(`---- Syncing Collections From Backup... ----`);
106
+ startTime = performance.now();
107
+ await new Promise((resolve, reject) => {
108
+ stream
109
+ .pipe(collectionFilter)
110
+ .pipe(collectionWriter)
111
+ .on('finish', () => resolve());
112
+ });
113
+ endTime = performance.now();
114
+ this.logInfo(`Syncing Collections took ${((endTime - startTime) / 1000).toFixed(3)} seconds`);
115
+ this.logInfo(`---- Syncing Other Modules... ----`);
116
+ await dispatch_OnSyncEnvCompleted.dispatchModuleAsync(body.env, this.config.urlMap[body.env], this.config.sessionMap[body.env]);
117
+ this.logInfo(`---- DONE Syncing Other Modules----`);
118
+ if (this.config.shouldBackupBeforeSync && endTime !== undefined && startTime !== undefined)
119
+ this.logInfo(`(Backup took ${((endTime - startTime) / 1000).toFixed(3)} seconds)`);
120
+ };
121
+ async getBackupInfo(queryParams) {
122
+ const { backupId, env } = queryParams;
123
+ if (!env)
124
+ throw new BadImplementationException(`Did not receive env in the fetch from env api call!`);
125
+ return ModuleBE_BackupDocDB.getBackupInfo(backupId, this.config.urlMap[env], this.config.sessionMap[env]);
126
+ }
127
+ syncFirebaseFromBackup = async (queryParams) => {
128
+ try {
129
+ this.logDebug('Getting the firebase backup file');
130
+ const firebaseSessionAdmin = ModuleBE_Firebase.createAdminSession();
131
+ const backupInfo = await this.getBackupInfo(queryParams);
132
+ const database = firebaseSessionAdmin.getDatabase();
133
+ this.logDebug('Reading the file from storage');
134
+ const signedUrlDef = {
135
+ method: HttpMethod.GET,
136
+ path: '',
137
+ fullUrl: backupInfo.firebaseSignedUrl
138
+ };
139
+ const firebaseFile = await AxiosHttpModule
140
+ .createRequest(signedUrlDef)
141
+ .executeSync();
142
+ this.logDebug('Setting the file in firebase database');
143
+ await database.set('/', firebaseFile);
144
+ }
145
+ catch (err) {
146
+ throw new ApiException(500, err);
147
+ }
148
+ };
149
+ }
150
+ export const ModuleBE_SyncEnv = new ModuleBE_SyncEnv_Class();
151
+ class SyncCollectionFilter extends Transform {
152
+ allowedDbKeys;
153
+ constructor(allowedDbKeys) {
154
+ super({ objectMode: true });
155
+ this.allowedDbKeys = allowedDbKeys;
156
+ }
157
+ _transform(chunk, encoding, callback) {
158
+ if (this.allowedDbKeys.includes(chunk.dbKey)) {
159
+ this.push(chunk);
160
+ }
161
+ callback();
162
+ }
163
+ }
164
+ class CollectionBatchWriter extends Writable {
165
+ itemCount = 0;
166
+ paginationSize;
167
+ firestore;
168
+ batchWriter;
169
+ modules;
170
+ constructor(paginationSize) {
171
+ super({ objectMode: true });
172
+ this.paginationSize = paginationSize;
173
+ const firebaseSessionAdmin = ModuleBE_Firebase.createAdminSession();
174
+ this.firestore = firebaseSessionAdmin.getFirestoreV3().firestore;
175
+ this.batchWriter = this.firestore.batch();
176
+ this.modules = arrayToMap(RuntimeModules()
177
+ .filter((module) => !(!module || !module.dbDef)), module => module.dbDef.dbKey);
178
+ }
179
+ async _write(chunk, encoding, callback) {
180
+ try {
181
+ const module = this.modules[chunk.dbKey];
182
+ if (!module) {
183
+ ModuleBE_SyncEnv.logWarning(`Could not get module for chunk with dbKey ${chunk.dbKey}`);
184
+ callback();
185
+ }
186
+ const collectionName = module.dbDef.backend.name;
187
+ const docRef = this.firestore.doc(`${collectionName}/${chunk._id}`);
188
+ const data = JSON.parse(chunk.document);
189
+ this.batchWriter.set(docRef, data);
190
+ this.itemCount++;
191
+ if (this.itemCount === this.paginationSize) {
192
+ const prevBatchWriter = this.batchWriter;
193
+ this.batchWriter = this.firestore.batch();
194
+ this.itemCount = 0;
195
+ await prevBatchWriter.commit();
196
+ }
197
+ callback();
198
+ }
199
+ catch (error) {
200
+ callback(error instanceof Error ? error : new Error(String(error)));
201
+ }
202
+ }
203
+ async _final(callback) {
204
+ try {
205
+ await this.batchWriter.commit();
206
+ callback();
207
+ }
208
+ catch (err) {
209
+ callback(err);
210
+ }
211
+ }
212
+ }
@@ -0,0 +1,63 @@
1
+ import { FirestoreQuery } from '@nu-art/firebase-shared';
2
+ import { DB_Object, Module, ResolvableContent, TypedMap, UniqueId } from '@nu-art/ts-common';
3
+ import { firestore } from 'firebase-admin';
4
+ import { ModuleBE_BaseDB } from '../db-api-gen/ModuleBE_BaseDB.js';
5
+ import { DeltaSyncModule, FullSyncModule, NoNeedToSyncModule, SyncDataFirebaseState } from '@nu-art/thunderstorm-shared/sync-manager/types';
6
+ import { DBProto_DeletedDoc } from '@nu-art/thunderstorm-shared';
7
+ import { OnSyncEnvCompleted } from '../sync-env/ModuleBE_SyncEnv.js';
8
+ import { OnModuleCleanupV2 } from '../../_entity.js';
9
+ import { FirestoreCollectionV3 } from '@nu-art/firebase-backend/firestore-v3/FirestoreCollectionV3';
10
+ import Transaction = firestore.Transaction;
11
+ type DeletedDBItem = DB_Object & {
12
+ __collectionName: string;
13
+ __docId: UniqueId;
14
+ };
15
+ type Config = {
16
+ retainDeletedCount: number;
17
+ };
18
+ /**
19
+ * # ModuleBE_SyncManager
20
+ *
21
+ * ## <ins>Description:</ins>
22
+ * This module manages all the {@link BaseDB_Module} updates and deleted items in order to allow incremental sync of items with clients
23
+ *
24
+ * ## <ins>Config:</ins>
25
+ *
26
+ * ```json
27
+ * "ModuleBE_SyncManager" : {
28
+ * retainDeletedCount: 100
29
+ * }
30
+ * ```
31
+ */
32
+ export declare class ModuleBE_SyncManager_Class extends Module<Config> implements OnModuleCleanupV2, OnSyncEnvCompleted {
33
+ collection: FirestoreCollectionV3<DBProto_DeletedDoc>;
34
+ private database;
35
+ private dbModules;
36
+ smartSyncApi: import("../_imports.js")._ServerBodyApi<import("@nu-art/thunderstorm-shared").BodyApi<{
37
+ modules: (NoNeedToSyncModule | DeltaSyncModule | FullSyncModule)[];
38
+ }, {
39
+ modules: import("@nu-art/thunderstorm-shared/sync-manager/types").SyncDbData[];
40
+ }>>;
41
+ private resolvableFirebaseBasePath;
42
+ constructor();
43
+ __onSyncEnvCompleted(env: string, baseUrl: string, requiredHeaders: TypedMap<string>): Promise<void>;
44
+ init(): void;
45
+ private calculateSmartSync;
46
+ getOrCreateSyncData: () => Promise<SyncDataFirebaseState>;
47
+ private prepareItemToDelete;
48
+ onItemsDeleted(collectionName: string, items: DB_Object[], uniqueKeys?: string[], transaction?: Transaction): Promise<void>;
49
+ queryDeleted(collectionName: string, query: FirestoreQuery<DB_Object>): Promise<DeletedDBItem[]>;
50
+ __onCleanupInvokedV2: () => Promise<void>;
51
+ getFullSyncData: () => Promise<SyncDataFirebaseState>;
52
+ setLastUpdated(collectionName: string, lastUpdated: number): Promise<void>;
53
+ setOldestDeleted(collectionName: string, oldestDeleted: number): Promise<void>;
54
+ setModuleFilter: (filter: (modules: (ModuleBE_BaseDB<any>)[]) => Promise<(ModuleBE_BaseDB<any>)[]>) => void;
55
+ /**
56
+ * Set function that allows to set a custom resolver for the rtdb node path
57
+ * @param resolvablePath The resolver for the node path
58
+ */
59
+ setResolvablePath: (resolvablePath: ResolvableContent<string>) => void;
60
+ private filterModules;
61
+ }
62
+ export declare const ModuleBE_SyncManager: ModuleBE_SyncManager_Class;
63
+ export {};
@@ -0,0 +1,254 @@
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 { _EmptyQuery } from '@nu-art/firebase-shared';
22
+ import { ModuleBE_Firebase } from '@nu-art/firebase-backend';
23
+ import { __stringify, arrayToMap, currentTimeMillis, dispatch_onApplicationException, exists, filterDuplicates, filterInstances, LogLevel, Module, resolveContent, RuntimeModules } from '@nu-art/ts-common';
24
+ import { createBodyServerApi } from '../../core/typed-api.js';
25
+ import { addRoutes } from '../ModuleBE_APIs.js';
26
+ import { SmartSync_DeltaSync, SmartSync_FullSync, SmartSync_UpToDateSync } from '@nu-art/thunderstorm-shared/sync-manager/types';
27
+ import { DBDef_DeletedDoc } from '@nu-art/thunderstorm-shared';
28
+ import { ApiDef_SyncManager } from '@nu-art/thunderstorm-shared/sync-manager/apis';
29
+ /**
30
+ * # ModuleBE_SyncManager
31
+ *
32
+ * ## <ins>Description:</ins>
33
+ * This module manages all the {@link BaseDB_Module} updates and deleted items in order to allow incremental sync of items with clients
34
+ *
35
+ * ## <ins>Config:</ins>
36
+ *
37
+ * ```json
38
+ * "ModuleBE_SyncManager" : {
39
+ * retainDeletedCount: 100
40
+ * }
41
+ * ```
42
+ */
43
+ export class ModuleBE_SyncManager_Class extends Module {
44
+ collection;
45
+ database;
46
+ dbModules;
47
+ smartSyncApi;
48
+ resolvableFirebaseBasePath = `/state/${this.getName()}`;
49
+ constructor() {
50
+ super();
51
+ this.setMinLevel(LogLevel.Debug);
52
+ this.smartSyncApi = createBodyServerApi(ApiDef_SyncManager.v1.smartSync, this.calculateSmartSync);
53
+ this.setDefaultConfig({ retainDeletedCount: 1000 });
54
+ }
55
+ async __onSyncEnvCompleted(env, baseUrl, requiredHeaders) {
56
+ await this.database.delete(resolveContent(this.resolvableFirebaseBasePath));
57
+ }
58
+ init() {
59
+ const firestore = ModuleBE_Firebase.createAdminSession().getFirestoreV3();
60
+ this.collection = firestore.getCollection(DBDef_DeletedDoc);
61
+ this.dbModules = RuntimeModules().filter(module => (module.ModuleBE_BaseDBV2));
62
+ this.database = ModuleBE_Firebase.createAdminSession().getDatabase();
63
+ addRoutes([this.smartSyncApi]);
64
+ }
65
+ calculateSmartSync = async (body) => {
66
+ const frontendCollectionNames = body.modules.map(item => item.dbKey);
67
+ this.logVerbose(`Modules wanted: ${__stringify(frontendCollectionNames)}`);
68
+ const permissibleModules = await this.filterModules(this.dbModules.filter(dbModule => frontendCollectionNames.includes(dbModule.dbDef.dbKey)));
69
+ const modulesAllowed = filterInstances(permissibleModules.map(_module => _module.dbDef.dbKey));
70
+ this.logVerbose(`Modules found: ${__stringify(modulesAllowed)}`);
71
+ this.logVerbose(`Modules not found: ${frontendCollectionNames.filter(collectionName => modulesAllowed.includes(collectionName))}`);
72
+ const dbNameToModuleMap = arrayToMap(permissibleModules, (item) => item.dbDef.dbKey);
73
+ const syncDataResponse = [];
74
+ const upToDateSyncData = await this.getOrCreateSyncData();
75
+ // For each module, create the response, which says what type of sync it needs: none, delta or full.
76
+ await Promise.all(body.modules.map(async (syncRequest) => {
77
+ const moduleToCheck = dbNameToModuleMap[syncRequest.dbKey];
78
+ if (!moduleToCheck)
79
+ return this.logError(`Calculating collections to sync, failing to find dbKey: ${syncRequest.dbKey}`);
80
+ const remoteSyncData = upToDateSyncData[syncRequest.dbKey] ?? { lastUpdated: 0, oldestDeleted: 0 };
81
+ // Local has no sync data, or it's too old - tell local to send a full sync request for this module
82
+ if (syncRequest.lastUpdated === 0 && remoteSyncData.lastUpdated > 0 || exists(remoteSyncData.oldestDeleted) && remoteSyncData.oldestDeleted > syncRequest.lastUpdated) {
83
+ // full sync
84
+ syncDataResponse.push({
85
+ dbKey: syncRequest.dbKey,
86
+ sync: SmartSync_FullSync,
87
+ lastUpdated: remoteSyncData.lastUpdated,
88
+ });
89
+ return;
90
+ }
91
+ // Same lastUpdated timestamp in local and remote, no need to sync
92
+ if (syncRequest.lastUpdated === remoteSyncData.lastUpdated) {
93
+ // no sync
94
+ syncDataResponse.push({
95
+ dbKey: syncRequest.dbKey,
96
+ sync: SmartSync_UpToDateSync,
97
+ lastUpdated: remoteSyncData.lastUpdated
98
+ });
99
+ return;
100
+ }
101
+ // Different lastUpdated timestamp in local and remote - tell local to send a delta sync request for this module
102
+ if (syncRequest.lastUpdated !== remoteSyncData.lastUpdated) {
103
+ // delta sync
104
+ let toUpdate = [];
105
+ try {
106
+ toUpdate = await moduleToCheck.query.where({ __updated: { $gte: syncRequest.lastUpdated } });
107
+ }
108
+ catch (e) {
109
+ this.logWarningBold(`Module assumed to be normal DB module: ${moduleToCheck.getName()}, collection:${moduleToCheck.dbDef.dbKey}`);
110
+ throw e;
111
+ }
112
+ const itemsToReturn = {
113
+ toUpdate: toUpdate,
114
+ toDelete: await this.queryDeleted(syncRequest.dbKey, { where: { __updated: { $gte: syncRequest.lastUpdated } } })
115
+ };
116
+ syncDataResponse.push({
117
+ dbKey: syncRequest.dbKey,
118
+ sync: SmartSync_DeltaSync,
119
+ lastUpdated: remoteSyncData.lastUpdated,
120
+ items: itemsToReturn
121
+ });
122
+ }
123
+ }));
124
+ return {
125
+ modules: syncDataResponse
126
+ };
127
+ };
128
+ getOrCreateSyncData = async () => {
129
+ this.logVerbose('Current node path', `${resolveContent(this.resolvableFirebaseBasePath)}/syncData`);
130
+ const syncDataRef = this.database.ref(`${resolveContent(this.resolvableFirebaseBasePath)}/syncData`);
131
+ const rtdbSyncData = await syncDataRef.get({});
132
+ const dbModuleDbKeys = filterInstances(this.dbModules.map(module => module.dbDef.dbKey));
133
+ this.logVerbose(`BE DB Modules: ${__stringify(dbModuleDbKeys)}`);
134
+ const missingModules = this.dbModules.filter(dbModule => {
135
+ const dbBE_SyncData = rtdbSyncData[dbModule.dbDef.dbKey];
136
+ if (!dbBE_SyncData)
137
+ return true;
138
+ // const dbFE_SyncData = body.modules.find(module => module.dbName === dbModule.getCollectionName());
139
+ // if (!dbFE_SyncData)
140
+ return false;
141
+ // return dbFE_SyncData.lastUpdated > dbBE_SyncData.lastUpdated;
142
+ });
143
+ if (missingModules.length) {
144
+ this.logWarning(`Syncing missing modules: `, missingModules.map(module => module.dbDef.dbKey).sort());
145
+ const query = { limit: 1, orderBy: [{ key: '__updated', order: 'desc' }] };
146
+ const newestItems = (await Promise.all(missingModules.map(async (missingModule) => {
147
+ try {
148
+ return (await missingModule.query.unManipulatedQuery(query))[0];
149
+ }
150
+ catch (e) {
151
+ dispatch_onApplicationException.dispatchModule(e, this);
152
+ this.logError(e);
153
+ }
154
+ })));
155
+ newestItems.forEach((item, index) => rtdbSyncData[missingModules[index].dbDef.dbKey] = { lastUpdated: item?.__updated || 0 });
156
+ await syncDataRef.set(rtdbSyncData);
157
+ }
158
+ return rtdbSyncData;
159
+ };
160
+ prepareItemToDelete = (collectionName, item, uniqueKeys = ['_id']) => {
161
+ const { _id, __updated, __created, _v } = item;
162
+ const deletedItem = {
163
+ __docId: _id,
164
+ __updated,
165
+ __created,
166
+ _v,
167
+ __collectionName: collectionName
168
+ };
169
+ uniqueKeys.forEach(key => {
170
+ //Don't replace the _id, some items in the system have a calculated _id and can be deleted and created over and over.
171
+ if (key === '_id')
172
+ return;
173
+ // @ts-ignore
174
+ deletedItem[key] = item[key] || '';
175
+ });
176
+ return deletedItem;
177
+ };
178
+ async onItemsDeleted(collectionName, items, uniqueKeys = ['_id'], transaction) {
179
+ const toInsert = items.map(item => this.prepareItemToDelete(collectionName, item, uniqueKeys));
180
+ const now = currentTimeMillis();
181
+ toInsert.forEach(item => item.__updated = now);
182
+ await this.collection.create.all(toInsert, transaction);
183
+ const deletedCountRef = this.database.ref(`${resolveContent(this.resolvableFirebaseBasePath)}/deletedCount`);
184
+ let deletedCount = await deletedCountRef.get(0);
185
+ deletedCount += items.length;
186
+ await deletedCountRef.set(deletedCount);
187
+ }
188
+ async queryDeleted(collectionName, query) {
189
+ const finalQuery = {
190
+ ...query,
191
+ where: { ...query.where, __collectionName: collectionName }
192
+ };
193
+ const deletedItems = await this.collection.query.custom(finalQuery);
194
+ deletedItems.forEach(_item => _item._id = _item.__docId || _item._id);
195
+ return deletedItems;
196
+ }
197
+ __onCleanupInvokedV2 = async () => {
198
+ if (!this.config.retainDeletedCount)
199
+ return this.logWarning('Will not run cleanup of deleted values:\n No "retainDeletedCount" was specified in config..');
200
+ const deletedCountRef = this.database.ref(`${resolveContent(this.resolvableFirebaseBasePath)}/deletedCount`);
201
+ let deletedCount = await deletedCountRef.get();
202
+ if (deletedCount === undefined) {
203
+ deletedCount = (await this.collection.query.custom(_EmptyQuery)).length;
204
+ await deletedCountRef.set(deletedCount);
205
+ }
206
+ const toDeleteCount = deletedCount - this.config.retainDeletedCount;
207
+ if (toDeleteCount <= 0)
208
+ return;
209
+ this.logDebug('Docs to delete', deletedCount);
210
+ this.logDebug('Docs to retain', this.config.retainDeletedCount);
211
+ const deleted = await this.collection.delete.query({
212
+ limit: toDeleteCount,
213
+ orderBy: [{ key: '__updated', order: 'asc' }]
214
+ });
215
+ let newDeletedCount = deletedCount - deleted.length;
216
+ if (deleted.length !== toDeleteCount) {
217
+ this.logError(`Expected to delete ${toDeleteCount} but actually deleted ${deleted.length}`);
218
+ newDeletedCount = (await this.collection.query.custom(_EmptyQuery)).length;
219
+ }
220
+ await deletedCountRef.set(newDeletedCount);
221
+ const map = deleted.map(item => item.__collectionName);
222
+ const keys = filterDuplicates(map);
223
+ await Promise.all(keys.map(key => {
224
+ const newestDeletedItem = deleted.find(deletedItem => deletedItem.__collectionName === key);
225
+ this.logDebug(`setting oldest deleted timestamp ${key} = ${newestDeletedItem.__updated}`);
226
+ return this.setOldestDeleted(key, newestDeletedItem.__updated);
227
+ }));
228
+ };
229
+ getFullSyncData = async () => {
230
+ const syncDataRef = this.database.ref(`${resolveContent(this.resolvableFirebaseBasePath)}/syncData`);
231
+ return (await syncDataRef.get({}));
232
+ };
233
+ async setLastUpdated(collectionName, lastUpdated) {
234
+ return this.database.patch(`${resolveContent(this.resolvableFirebaseBasePath)}/syncData/${collectionName}`, { lastUpdated });
235
+ }
236
+ async setOldestDeleted(collectionName, oldestDeleted) {
237
+ return this.database.patch(`${resolveContent(this.resolvableFirebaseBasePath)}/deletedCount/${collectionName}`, { oldestDeleted });
238
+ }
239
+ setModuleFilter = (filter) => {
240
+ const previousFilter = this.filterModules;
241
+ this.filterModules = async (modules) => filter(await previousFilter(modules));
242
+ };
243
+ /**
244
+ * Set function that allows to set a custom resolver for the rtdb node path
245
+ * @param resolvablePath The resolver for the node path
246
+ */
247
+ setResolvablePath = (resolvablePath) => {
248
+ this.resolvableFirebaseBasePath = resolvablePath;
249
+ };
250
+ filterModules = async (modules) => {
251
+ return modules;
252
+ };
253
+ }
254
+ export const ModuleBE_SyncManager = new ModuleBE_SyncManager_Class();
package/package.json ADDED
@@ -0,0 +1,104 @@
1
+ {
2
+ "name": "@nu-art/thunderstorm-backend",
3
+ "version": "0.400.5",
4
+ "description": "Thunderstorm Backend",
5
+ "keywords": [
6
+ "TacB0sS",
7
+ "express",
8
+ "infra",
9
+ "nu-art",
10
+ "thunderstorm",
11
+ "typescript"
12
+ ],
13
+ "homepage": "https://github.com/nu-art-js/thunderstorm",
14
+ "bugs": {
15
+ "url": "https://github.com/nu-art-js/thunderstorm/issues"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+ssh://git@github.com:nu-art-js/thunderstorm.git"
20
+ },
21
+ "license": "Apache-2.0",
22
+ "author": "TacB0sS",
23
+ "scripts": {
24
+ "build": "tsc"
25
+ },
26
+ "contributors": [
27
+ {
28
+ "name": "TacB0sS"
29
+ },
30
+ {
31
+ "name": "Cipher",
32
+ "url": "https://www.linkedin.com/in/itay-leybovich-470b87229/"
33
+ }
34
+ ],
35
+ "publishConfig": {
36
+ "directory": "dist",
37
+ "linkDirectory": true
38
+ },
39
+ "dependencies": {
40
+ "@nu-art/thunderstorm-shared": "0.400.5",
41
+ "@nu-art/firebase-backend": "0.400.5",
42
+ "@nu-art/firebase-shared": "0.400.5",
43
+ "@nu-art/google-services-backend": "0.400.5",
44
+ "@nu-art/ts-common": "0.400.5",
45
+ "@nu-art/ts-styles": "0.400.5",
46
+ "abort-controller": "^3.0.0",
47
+ "axios": "^1.13.1",
48
+ "body-parser": "^1.19.0",
49
+ "browserify-zlib": "^0.2.0",
50
+ "buffer": "^6.0.3",
51
+ "compression": "^1.7.4",
52
+ "cors": "^2.8.5",
53
+ "crypto-browserify": "^3.12.0",
54
+ "csstype": "^3.0.0",
55
+ "express": "^4.18.2",
56
+ "firebase": "^11.9.0",
57
+ "firebase-admin": "13.4.0",
58
+ "firebase-functions": "6.3.2",
59
+ "history": "^4.9.0",
60
+ "moment": "^2.29.4",
61
+ "pako": "^2.1.0",
62
+ "papaparse": "^5.4.1",
63
+ "qs": "^6.6.0",
64
+ "react": "^18.0.0",
65
+ "react-dom": "^18.0.0",
66
+ "react-router-dom": "^6.9.0",
67
+ "react-virtualized-auto-sizer": "^1.0.12",
68
+ "react-window": "^1.8.8",
69
+ "request": "^2.88.0",
70
+ "stream-browserify": "^3.0.0",
71
+ "util": "^0.12.4"
72
+ },
73
+ "devDependencies": {
74
+ "@types/papaparse": "^5.3.14",
75
+ "@types/pako": "^2.0.0",
76
+ "@types/cors": "^2.8.17",
77
+ "@types/react": "^18.0.0",
78
+ "@types/react-dom": "^18.0.0",
79
+ "@types/react-router": "^5.1.20",
80
+ "@types/react-router-dom": "^5.3.3",
81
+ "@types/compression": "^1.7.0",
82
+ "@types/express": "^4.17.17",
83
+ "@types/history": "^4.7.2",
84
+ "@types/request": "^2.48.1",
85
+ "@types/chrome": "^0.0.202",
86
+ "@types/react-window": "^1.8.5",
87
+ "@types/react-virtualized-auto-sizer": "^1.0.1"
88
+ },
89
+ "unitConfig": {
90
+ "type": "typescript-lib"
91
+ },
92
+ "type": "module",
93
+ "exports": {
94
+ ".": {
95
+ "types": "./index.d.ts",
96
+ "import": "./index.js"
97
+ },
98
+ "./*": {
99
+ "types": "./*.d.ts",
100
+ "import": "./*.js"
101
+ },
102
+ "./styles": "./styles.scss"
103
+ }
104
+ }
package/shared.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from '@nu-art/thunderstorm-shared';
package/shared.js ADDED
@@ -0,0 +1,21 @@
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
+ export * from '@nu-art/thunderstorm-shared';