mosquito-transport 1.9.2 → 1.9.3
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/bin/extract_backup.js +4 -2
- package/bin/install_backup.js +7 -3
- package/bin/utils.js +5 -0
- package/lib/helpers/variables.js +5 -1
- package/lib/index.d.ts +12 -10
- package/lib/index.js +26 -23
- package/lib/products/database/base.js +14 -11
- package/lib/products/database/index.js +83 -22
- package/package.json +1 -1
package/bin/extract_backup.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MongoClient } from "mongodb";
|
|
2
2
|
import { serialize } from 'mongodb/lib/bson.js';
|
|
3
|
-
import { BLOCKS_IDENTIFIERS, encryptData, isPath, isValidColName, isValidDbName, one_gb, resolvePath } from "./utils.js";
|
|
3
|
+
import { BLOCKS_IDENTIFIERS, encryptData, isPath, isValidColName, isValidDbName, one_gb, resolvePath, wait } from "./utils.js";
|
|
4
4
|
import { readdir, stat } from "fs/promises";
|
|
5
5
|
import { createReadStream } from "fs";
|
|
6
6
|
import { Validator } from "guard-object";
|
|
@@ -8,7 +8,7 @@ import { WritableBit } from "@deflexable/bit-stream";
|
|
|
8
8
|
import { join } from "path";
|
|
9
9
|
|
|
10
10
|
const BIT_SIZE = one_gb * .2;
|
|
11
|
-
const DOC_LIMITER =
|
|
11
|
+
const DOC_LIMITER = 300;
|
|
12
12
|
|
|
13
13
|
export const extractBackup = (config) => {
|
|
14
14
|
let { database, storage, password, onMongodbOption } = { ...config };
|
|
@@ -90,6 +90,7 @@ export const extractBackup = (config) => {
|
|
|
90
90
|
pushBuffer(Buffer.from(`${thisCol}`, 'utf8'));
|
|
91
91
|
|
|
92
92
|
while (canLoadMore) {
|
|
93
|
+
await wait(7); // pause for garbage collection
|
|
93
94
|
const data = await dbNameInstance.collection(thisCol).find({})
|
|
94
95
|
.skip(offset).limit(DOC_LIMITER).toArray();
|
|
95
96
|
offset += DOC_LIMITER;
|
|
@@ -149,6 +150,7 @@ export const extractBackup = (config) => {
|
|
|
149
150
|
reject(err);
|
|
150
151
|
});
|
|
151
152
|
});
|
|
153
|
+
await wait(1); // pause for garbage collection
|
|
152
154
|
} else {
|
|
153
155
|
const files = await readdir(dir);
|
|
154
156
|
if (files.length) {
|
package/bin/install_backup.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BLOCKS_IDENTIFIERS, decryptData, resolvePath } from "./utils.js";
|
|
1
|
+
import { BLOCKS_IDENTIFIERS, decryptData, resolvePath, wait } from "./utils.js";
|
|
2
2
|
import { MongoClient } from "mongodb";
|
|
3
3
|
import { deserialize } from 'mongodb/lib/bson.js';
|
|
4
4
|
import { mkdir } from "fs/promises";
|
|
@@ -100,7 +100,9 @@ export const installBackup = (config) => new Promise((callResolve, callReject) =
|
|
|
100
100
|
{ ...docRest },
|
|
101
101
|
{ upsert: true }
|
|
102
102
|
);
|
|
103
|
-
++installionStats.totalWrittenDocuments
|
|
103
|
+
if (!(++installionStats.totalWrittenDocuments % 300)) {
|
|
104
|
+
await wait(7); // pause for garbage collection
|
|
105
|
+
}
|
|
104
106
|
} else {
|
|
105
107
|
lastBlocks.database = INIT_BLOCKS.database;
|
|
106
108
|
|
|
@@ -135,7 +137,9 @@ export const installBackup = (config) => new Promise((callResolve, callReject) =
|
|
|
135
137
|
const writeStream = createWriteStream(lastBlocks.storage.path);
|
|
136
138
|
writeStream.write(thisElem);
|
|
137
139
|
lastBlocks.storage.file = writeStream;
|
|
138
|
-
++installionStats.totalWrittenFiles
|
|
140
|
+
if (!(++installionStats.totalWrittenFiles % 50)) {
|
|
141
|
+
await wait(3); // pause for garbage collection
|
|
142
|
+
};
|
|
139
143
|
};
|
|
140
144
|
} else throw `unknown block identifier "${prevHeader}" at block_id ${BLOCK_ID}`;
|
|
141
145
|
}
|
package/bin/utils.js
CHANGED
|
@@ -4,6 +4,11 @@ import { createCipheriv, createDecipheriv, createHash } from 'node:crypto';
|
|
|
4
4
|
export const one_mb = 1024 * 1024,
|
|
5
5
|
one_gb = one_mb * 1024;
|
|
6
6
|
|
|
7
|
+
export const wait = (ms = 1000) =>
|
|
8
|
+
new Promise(resolve => {
|
|
9
|
+
setTimeout(resolve, ms);
|
|
10
|
+
});
|
|
11
|
+
|
|
7
12
|
export const BLOCKS_IDENTIFIERS = {
|
|
8
13
|
DB_URL: '--->[DB_URL]:',
|
|
9
14
|
DB_NAME: '--->[DB_NAME]:',
|
package/lib/helpers/variables.js
CHANGED
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Db, Document, MongoClient, MongoClientOptions, SortDirection, UpdateDescription } from "mongodb";
|
|
1
|
+
import { ChangeStreamOptions, Db, Document, MongoClient, MongoClientOptions, SortDirection, UpdateDescription } from "mongodb";
|
|
2
2
|
import express from "express";
|
|
3
3
|
import { CorsOptions } from "cors";
|
|
4
4
|
import { Sort } from "mongodb";
|
|
@@ -882,22 +882,24 @@ interface MosquitoHttpOptions {
|
|
|
882
882
|
allowDisabledAuth?: boolean;
|
|
883
883
|
}
|
|
884
884
|
|
|
885
|
-
interface DatabaseListenerOption {
|
|
886
|
-
|
|
887
|
-
|
|
885
|
+
interface DatabaseListenerOption extends ChangeStreamOptions {
|
|
886
|
+
/**
|
|
887
|
+
* An array of {@link https://www.mongodb.com/docs/manual/reference/operator/aggregation-pipeline/|aggregation pipeline stages} through which to pass change stream documents. This allows for filtering (using $match) and manipulating the change stream documents.
|
|
888
|
+
*/
|
|
888
889
|
pipeline?: { pipeline?: Document[] }
|
|
889
890
|
}
|
|
890
891
|
|
|
891
892
|
interface DatabaseListenerCallbackData {
|
|
892
893
|
insertion?: { _id: string };
|
|
894
|
+
update?: UpdateDescription;
|
|
895
|
+
replacement?: { _id: string };
|
|
893
896
|
deletion?: string;
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
auth?: AuthData | undefined,
|
|
899
|
-
operation: 'insert' | 'delete' | 'update';
|
|
897
|
+
before?: Document;
|
|
898
|
+
after?: Document;
|
|
899
|
+
timestamp: number;
|
|
900
|
+
operation: 'insert' | 'update' | 'replace' | 'delete';
|
|
900
901
|
documentKey: string;
|
|
902
|
+
extras: any
|
|
901
903
|
}
|
|
902
904
|
|
|
903
905
|
interface StorageSnapshot {
|
package/lib/index.js
CHANGED
|
@@ -799,13 +799,14 @@ export default class MosquitoTransportServer {
|
|
|
799
799
|
if (normalizeRoute(route) === normalizeRoute(e))
|
|
800
800
|
throw `"${e}" is a reserved route used internally`;
|
|
801
801
|
});
|
|
802
|
+
const { logger } = this.config;
|
|
803
|
+
const hasLogger = logger.includes('all') || logger.includes('external-requests'),
|
|
804
|
+
hasErrorLogger = logger.includes('all') || logger.includes('error');
|
|
805
|
+
|
|
802
806
|
Scoped.expressInstances[this.port].use(
|
|
803
807
|
express.Router({ caseSensitive: true }).all(`/${normalizeRoute(route)}`, async (req, res) => {
|
|
804
808
|
const { mtoken, uglified } = req.headers;
|
|
805
|
-
const
|
|
806
|
-
const hasLogger = logger.includes('all') || logger.includes('external-requests'),
|
|
807
|
-
hasErrorLogger = logger.includes('all') || logger.includes('error'),
|
|
808
|
-
now = hasLogger && Date.now();
|
|
809
|
+
const now = hasLogger && Date.now();
|
|
809
810
|
|
|
810
811
|
if (hasLogger) console.log(`started route: /${req.url}`);
|
|
811
812
|
res.set(NO_CACHE_HEADER);
|
|
@@ -917,13 +918,13 @@ export default class MosquitoTransportServer {
|
|
|
917
918
|
|
|
918
919
|
listenDatabase = (path, callback, options) => {
|
|
919
920
|
if (typeof path !== 'string') throw `listenDatabase first argument must be a string but got ${path}`;
|
|
920
|
-
const { dbName, dbUrl } = options || {}
|
|
921
|
-
|
|
921
|
+
const { dbName, dbUrl } = options || {};
|
|
922
|
+
const { logger } = this.config;
|
|
923
|
+
const hasLogger = logger.includes('all') || logger.includes('database-snapshot'),
|
|
924
|
+
hasErrorLogger = logger.includes('all') || logger.includes('error');
|
|
922
925
|
|
|
923
926
|
return emitDatabase(path, async function () {
|
|
924
|
-
const
|
|
925
|
-
hasErrorLogger = logger.includes('all') || logger.includes('error'),
|
|
926
|
-
now = hasLogger && Date.now();
|
|
927
|
+
const now = hasLogger && Date.now();
|
|
927
928
|
if (hasLogger) console.log(`db-snapshot ${path}: `, arguments[0]);
|
|
928
929
|
try {
|
|
929
930
|
await callback?.(...arguments);
|
|
@@ -1006,11 +1007,11 @@ export default class MosquitoTransportServer {
|
|
|
1006
1007
|
|
|
1007
1008
|
listenStorage = (callback) => {
|
|
1008
1009
|
const { logger } = this.config;
|
|
1010
|
+
const hasLogger = logger.includes('all') || logger.includes('storage'),
|
|
1011
|
+
hasErrorLogger = logger.includes('all') || logger.includes('error');
|
|
1009
1012
|
|
|
1010
1013
|
return StorageListener.listenTo(this.projectName, async ({ dest, ...rest }) => {
|
|
1011
|
-
const
|
|
1012
|
-
hasErrorLogger = logger.includes('all') || logger.includes('error'),
|
|
1013
|
-
now = hasLogger && Date.now();
|
|
1014
|
+
const now = hasLogger && Date.now();
|
|
1014
1015
|
|
|
1015
1016
|
if (hasLogger) console.log(`started listenStorage ${dest}:`);
|
|
1016
1017
|
try {
|
|
@@ -1022,18 +1023,20 @@ export default class MosquitoTransportServer {
|
|
|
1022
1023
|
});
|
|
1023
1024
|
};
|
|
1024
1025
|
|
|
1025
|
-
listenNewUser = (callback) =>
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1026
|
+
listenNewUser = (callback) =>
|
|
1027
|
+
emitDatabase(EnginePath.userAcct, s => {
|
|
1028
|
+
if (s.insertion) {
|
|
1029
|
+
const j = { ...s.insertion };
|
|
1030
|
+
j.uid = j._id;
|
|
1031
|
+
if (j._id) delete j._id;
|
|
1032
|
+
callback?.(j);
|
|
1033
|
+
}
|
|
1034
|
+
}, this.projectName, ADMIN_DB_NAME, ADMIN_DB_URL);
|
|
1033
1035
|
|
|
1034
|
-
listenDeletedUser = (callback) =>
|
|
1035
|
-
|
|
1036
|
-
|
|
1036
|
+
listenDeletedUser = (callback) =>
|
|
1037
|
+
emitDatabase(EnginePath.userAcct, s => {
|
|
1038
|
+
if (s.deletion) callback?.(s.deletion);
|
|
1039
|
+
}, this.projectName, ADMIN_DB_NAME, ADMIN_DB_URL);
|
|
1037
1040
|
|
|
1038
1041
|
updateUserProfile = async (uid, profile) => {
|
|
1039
1042
|
if (!Validator.OBJECT(profile)) throw 'updateUserProfile() second argument must be an object';
|
|
@@ -6,19 +6,22 @@ import { Scoped } from "../../helpers/variables.js";
|
|
|
6
6
|
*/
|
|
7
7
|
export const getDB = (projectName, name, url = DEFAULT_DB) => {
|
|
8
8
|
if (!projectName) throw 'expected projectName in getDb()';
|
|
9
|
-
const {
|
|
10
|
-
|
|
11
|
-
if (name === ADMIN_DB_NAME) name = dbName;
|
|
12
|
-
if (!instance) throw `no MongoClient was found for database with dbRef "${url}"`;
|
|
13
|
-
// if (!name && !dbName) throw `no dbName found for database with dbRef "${dbUrl}"`;
|
|
14
|
-
|
|
15
|
-
return instance.db(name || dbName);
|
|
9
|
+
const { dbName, instance } = getDbNaming(projectName, name, url) || {};
|
|
10
|
+
return instance.instance.db(dbName);
|
|
16
11
|
};
|
|
17
12
|
|
|
18
|
-
export const
|
|
13
|
+
export const getDbNaming = (projectName, name, dbRef = DEFAULT_DB) => {
|
|
19
14
|
if (!projectName) throw 'expected projectName in getDb()';
|
|
20
|
-
if (
|
|
15
|
+
if (dbRef === 'admin' || dbRef === 'default') throw `reserved keyword dbRef: "${dbRef}"`;
|
|
16
|
+
|
|
17
|
+
dbRef = dbRef === ADMIN_DB_URL ? 'admin' : dbRef === DEFAULT_DB ? 'default' : dbRef;
|
|
18
|
+
const instance = Scoped.InstancesData[projectName].mongoInstances[dbRef];
|
|
19
|
+
|
|
20
|
+
if (!instance) throw `no MongoClient was found for database with dbRef "${dbRef}"`;
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
return {
|
|
23
|
+
dbRef,
|
|
24
|
+
instance,
|
|
25
|
+
dbName: name === ADMIN_DB_NAME ? instance.defaultName : (name || instance.defaultName)
|
|
26
|
+
};
|
|
24
27
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import express from "express";
|
|
2
2
|
import { deserializeE2E, encodeBinary, niceTry, serializeE2E } from "../../helpers/utils.js";
|
|
3
|
-
import { getDB,
|
|
3
|
+
import { getDB, getDbNaming } from "./base.js";
|
|
4
4
|
import { validateJWT } from "../auth/tokenizer.js";
|
|
5
5
|
import { Scoped } from "../../helpers/variables.js";
|
|
6
6
|
import { EngineRoutes, ERRORS, NO_CACHE_HEADER } from "../../helpers/values.js";
|
|
@@ -79,12 +79,11 @@ const deserializeWriteValue = (value) => {
|
|
|
79
79
|
} else return value;
|
|
80
80
|
};
|
|
81
81
|
|
|
82
|
-
const cleanseFind = (path, find, projectName,
|
|
83
|
-
const {
|
|
84
|
-
dbName = dbName || defaultName;
|
|
82
|
+
const cleanseFind = (path, find, projectName, name, dbUrl) => {
|
|
83
|
+
const { instance, dbName } = getDbNaming(projectName, name, dbUrl);
|
|
85
84
|
|
|
86
|
-
if (instance.__intercepted) {
|
|
87
|
-
const d = instance.interceptMap?.map?.[dbName]?.[path];
|
|
85
|
+
if (instance.instance.__intercepted) {
|
|
86
|
+
const d = instance.instance.interceptMap?.map?.[dbName]?.[path];
|
|
88
87
|
if (d?.fulltext) return find;
|
|
89
88
|
}
|
|
90
89
|
return cleanseFindCore(find);
|
|
@@ -289,33 +288,95 @@ const extractDocField = async (d, commands, projectName, dbName, dbUrl, doc_hold
|
|
|
289
288
|
};
|
|
290
289
|
|
|
291
290
|
export const emitDatabase = (path, callback, projectName, dbName, dbUrl, options) => {
|
|
292
|
-
const
|
|
291
|
+
const naming = getDbNaming(projectName, dbName, dbUrl);
|
|
293
292
|
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
293
|
+
const nodeId = `${path}:${projectName}:${naming.dbName}:${naming.dbRef}:${options && serializeToBase64(options)}`;
|
|
294
|
+
let instance = Scoped.AccumulatedDatabaseEmittions[nodeId];
|
|
295
|
+
|
|
296
|
+
if (!instance) {
|
|
297
|
+
const callers = new Map();
|
|
298
|
+
const destroy = internalEmitDatabase(path, (...args) => {
|
|
299
|
+
callers.forEach(value => {
|
|
300
|
+
value(...args);
|
|
301
|
+
});
|
|
302
|
+
}, projectName, dbName, dbUrl, options);
|
|
303
|
+
|
|
304
|
+
Scoped.AccumulatedDatabaseEmittions[nodeId] = (instance = { callers, destroy });
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const ref = {};
|
|
308
|
+
instance.callers.set(ref, callback);
|
|
309
|
+
|
|
310
|
+
return () => {
|
|
311
|
+
if (!instance.callers.has(ref)) return;
|
|
312
|
+
instance.callers.delete(ref);
|
|
313
|
+
if (!instance.callers.size) {
|
|
314
|
+
instance.destroy();
|
|
315
|
+
delete Scoped.AccumulatedDatabaseEmittions[nodeId];
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const requiredEvent = ['insert', 'update', 'replace', 'delete'];
|
|
321
|
+
|
|
322
|
+
const internalEmitDatabase = (path, callback, projectName, dbName, dbUrl, options, resumeAfter) => {
|
|
323
|
+
const { pipeline, ...restOptions } = options || {};
|
|
324
|
+
|
|
325
|
+
const col = getDB(projectName, dbName, dbUrl).collection(path);
|
|
326
|
+
const stream = col.watch(pipeline || [
|
|
327
|
+
{
|
|
328
|
+
$match: {
|
|
329
|
+
operationType: { $in: requiredEvent }
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
], {
|
|
333
|
+
...restOptions,
|
|
334
|
+
resumeAfter
|
|
335
|
+
});
|
|
299
336
|
|
|
300
337
|
stream.on('change', l => {
|
|
301
|
-
const { operationType: ops, fullDocument, fullDocumentBeforeChange, documentKey, updateDescription, clusterTime } = l;
|
|
338
|
+
const { operationType: ops, fullDocument, fullDocumentBeforeChange, documentKey, updateDescription, clusterTime, ...rest } = l;
|
|
339
|
+
|
|
340
|
+
if (!requiredEvent.includes(ops)) return;
|
|
302
341
|
|
|
303
|
-
if (ops !== 'insert' && ops !== 'delete' && ops !== 'update') return;
|
|
304
342
|
callback?.({
|
|
305
343
|
documentKey: documentKey._id,
|
|
306
344
|
insertion: ops === 'insert' ? fullDocument : undefined,
|
|
307
|
-
deletion: ops === 'delete' ? documentKey._id : undefined,
|
|
308
345
|
update: ops === 'update' ? { ...updateDescription } : undefined,
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
346
|
+
replacement: ops === 'replace' ? fullDocument : undefined,
|
|
347
|
+
deletion: ops === 'delete' ? documentKey._id : undefined,
|
|
348
|
+
before: fullDocumentBeforeChange,
|
|
349
|
+
after: fullDocument,
|
|
350
|
+
timestamp: clusterTime,
|
|
351
|
+
operation: ops,
|
|
352
|
+
extras: rest
|
|
314
353
|
});
|
|
315
354
|
});
|
|
316
355
|
|
|
356
|
+
stream.on('resumeTokenChanged', (token) => {
|
|
357
|
+
resumeAfter = token;
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
let closure = null;
|
|
361
|
+
|
|
362
|
+
stream.on('error', async (error) => {
|
|
363
|
+
await stream.close();
|
|
364
|
+
setTimeout(() => {
|
|
365
|
+
if (closure === null)
|
|
366
|
+
closure = internalEmitDatabase(path, callback, projectName, dbName, dbUrl, options, resumeAfter);
|
|
367
|
+
}, 7000);
|
|
368
|
+
process.emit('uncaughtException', `emitDatabase error: ${error}`);
|
|
369
|
+
});
|
|
370
|
+
|
|
317
371
|
return () => {
|
|
318
|
-
|
|
372
|
+
const thisClosure = closure;
|
|
373
|
+
closure = undefined;
|
|
374
|
+
|
|
375
|
+
if (thisClosure === null) {
|
|
376
|
+
stream.close();
|
|
377
|
+
} else if (typeof thisClosure === 'function') {
|
|
378
|
+
thisClosure();
|
|
379
|
+
}
|
|
319
380
|
}
|
|
320
381
|
};
|
|
321
382
|
|
|
@@ -658,7 +719,7 @@ export const databaseLiveRoutesHandler = ({
|
|
|
658
719
|
} catch (e) {
|
|
659
720
|
socket.emit('mSnapshot', [simplifyCaughtError(e), undefined]);
|
|
660
721
|
}
|
|
661
|
-
}, projectName, dbName, dbUrl
|
|
722
|
+
}, projectName, dbName, dbUrl);
|
|
662
723
|
} catch (e) {
|
|
663
724
|
if (hasErrorLoger) console.error(`errRoute /${route} err:`, e);
|
|
664
725
|
socket.emit('mSnapshot', [simplifyCaughtError(e), undefined]);
|
package/package.json
CHANGED