@villedemontreal/mongo 6.7.0

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 (69) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +226 -0
  3. package/dist/src/config/configs.d.ts +16 -0
  4. package/dist/src/config/configs.js +26 -0
  5. package/dist/src/config/configs.js.map +1 -0
  6. package/dist/src/config/constants.d.ts +85 -0
  7. package/dist/src/config/constants.js +104 -0
  8. package/dist/src/config/constants.js.map +1 -0
  9. package/dist/src/config/init.d.ts +9 -0
  10. package/dist/src/config/init.js +24 -0
  11. package/dist/src/config/init.js.map +1 -0
  12. package/dist/src/config/mongooseConfigs.d.ts +73 -0
  13. package/dist/src/config/mongooseConfigs.js +107 -0
  14. package/dist/src/config/mongooseConfigs.js.map +1 -0
  15. package/dist/src/index.d.ts +6 -0
  16. package/dist/src/index.js +24 -0
  17. package/dist/src/index.js.map +1 -0
  18. package/dist/src/mongoClient.d.ts +19 -0
  19. package/dist/src/mongoClient.js +111 -0
  20. package/dist/src/mongoClient.js.map +1 -0
  21. package/dist/src/mongoUpdater.d.ts +103 -0
  22. package/dist/src/mongoUpdater.js +297 -0
  23. package/dist/src/mongoUpdater.js.map +1 -0
  24. package/dist/src/mongoUpdater.test.d.ts +1 -0
  25. package/dist/src/mongoUpdater.test.js +232 -0
  26. package/dist/src/mongoUpdater.test.js.map +1 -0
  27. package/dist/src/mongoUtils.d.ts +68 -0
  28. package/dist/src/mongoUtils.js +280 -0
  29. package/dist/src/mongoUtils.js.map +1 -0
  30. package/dist/src/mongoUtils.test.d.ts +1 -0
  31. package/dist/src/mongoUtils.test.js +24 -0
  32. package/dist/src/mongoUtils.test.js.map +1 -0
  33. package/dist/src/plugins/pagination/index.d.ts +11 -0
  34. package/dist/src/plugins/pagination/index.js +79 -0
  35. package/dist/src/plugins/pagination/index.js.map +1 -0
  36. package/dist/src/plugins/pagination/index.test.d.ts +1 -0
  37. package/dist/src/plugins/pagination/index.test.js +129 -0
  38. package/dist/src/plugins/pagination/index.test.js.map +1 -0
  39. package/dist/src/plugins/pagination/specs/IPaginateOptions.d.ts +51 -0
  40. package/dist/src/plugins/pagination/specs/IPaginateOptions.js +3 -0
  41. package/dist/src/plugins/pagination/specs/IPaginateOptions.js.map +1 -0
  42. package/dist/src/utils/logger.d.ts +11 -0
  43. package/dist/src/utils/logger.js +54 -0
  44. package/dist/src/utils/logger.js.map +1 -0
  45. package/dist/src/utils/testingConfigurations.d.ts +8 -0
  46. package/dist/src/utils/testingConfigurations.js +17 -0
  47. package/dist/src/utils/testingConfigurations.js.map +1 -0
  48. package/dist/tests/testingMongoUpdates/1.0.0.d.ts +5 -0
  49. package/dist/tests/testingMongoUpdates/1.0.0.js +27 -0
  50. package/dist/tests/testingMongoUpdates/1.0.0.js.map +1 -0
  51. package/dist/tests/testingMongoUpdates/1.0.1.d.ts +5 -0
  52. package/dist/tests/testingMongoUpdates/1.0.1.js +22 -0
  53. package/dist/tests/testingMongoUpdates/1.0.1.js.map +1 -0
  54. package/package.json +63 -0
  55. package/src/config/configs.ts +27 -0
  56. package/src/config/constants.ts +122 -0
  57. package/src/config/init.ts +23 -0
  58. package/src/config/mongooseConfigs.ts +178 -0
  59. package/src/index.ts +13 -0
  60. package/src/mongoClient.ts +122 -0
  61. package/src/mongoUpdater.test.ts +286 -0
  62. package/src/mongoUpdater.ts +423 -0
  63. package/src/mongoUtils.test.ts +23 -0
  64. package/src/mongoUtils.ts +322 -0
  65. package/src/plugins/pagination/index.test.ts +140 -0
  66. package/src/plugins/pagination/index.ts +96 -0
  67. package/src/plugins/pagination/specs/IPaginateOptions.ts +51 -0
  68. package/src/utils/logger.ts +53 -0
  69. package/src/utils/testingConfigurations.ts +13 -0
@@ -0,0 +1,423 @@
1
+ import { Timer, utils } from '@villedemontreal/general-utils';
2
+ import * as fs from 'fs';
3
+ import { isFunction } from 'lodash';
4
+ import * as MongoDb from 'mongodb';
5
+ import * as semver from 'semver';
6
+ import { constants as mongodbConstants } from './config/constants';
7
+ import { createLogger } from './utils/logger';
8
+
9
+ const logger = createLogger('MongoUpdater');
10
+
11
+ /**
12
+ * Mongo updater
13
+ * Manages the updates of the mongo schemas
14
+ */
15
+ export interface IMongoUpdater {
16
+ /**
17
+ * Validates that the application has been installed.
18
+ * This involves creating a special "appSchema" collection
19
+ * and document to track the application version and being able
20
+ * to update its schemas and documents...
21
+ */
22
+ checkInstallation(): Promise<void>;
23
+
24
+ /**
25
+ * Checks if the application needs update or not. Installs the updates
26
+ * if so.
27
+ */
28
+ checkUpdates(): Promise<void>;
29
+
30
+ /**
31
+ * Locks the appSchema document.
32
+ *
33
+ * @returns true if the lock has been acquired succesfully
34
+ * or false if the document was already locked.
35
+ */
36
+ lockAppSchemaDocument(): Promise<boolean>;
37
+
38
+ /**
39
+ * Unlocks the appSchema document.
40
+ *
41
+ * @returns true if the lock has been removed succesfully
42
+ * or false if the document was not locked.
43
+ */
44
+ unlockAppSchemaDocument(): Promise<boolean>;
45
+
46
+ /**
47
+ * Updates the app schema version stored in mongo database
48
+ */
49
+ updateAppSchemaVersion(currentVersion: string, newVersion: string): Promise<void>;
50
+
51
+ /**
52
+ * Installs the appSchema collection.
53
+ */
54
+ installAppSchemaCollection(): Promise<any>;
55
+
56
+ /**
57
+ * Gets a list of available app schema update files.
58
+ */
59
+ getAppSchemaUpdateFiles(currentVersion: string, newVersion: string): Promise<string[]>;
60
+
61
+ /**
62
+ * Updates the app schema
63
+ */
64
+ applyAppSchemaUpdates(currentVersion: string, newVersion: string): Promise<any>;
65
+
66
+ /**
67
+ * Gets the appSchema collection
68
+ */
69
+ getAppSchemaCollection(): Promise<MongoDb.Collection>;
70
+
71
+ /**
72
+ * Gets the current version from the appSchema document.
73
+ */
74
+ getAppSchemaVersion(): Promise<string>;
75
+ }
76
+
77
+ export interface ISchemeInfo {
78
+ version: string;
79
+ lock: boolean;
80
+ lockTimestamp: number;
81
+ }
82
+
83
+ export class MongoUpdater implements IMongoUpdater {
84
+ constructor(
85
+ private mongoDb: MongoDb.Db,
86
+ /**
87
+ * The *relative* path to the directory where the
88
+ * update files are.
89
+ */
90
+ private mongoSchemaUpdatesDirPath: string,
91
+ private lockMaxAgeSeconds: number,
92
+ private appSchemaCollectionName: string
93
+ ) {}
94
+
95
+ public async installAppSchemaCollection(): Promise<any> {
96
+ try {
97
+ // Installing the "appSchema" collection.
98
+ // tslint:disable-next-line: prefer-template
99
+ logger.info(' > Installing the "' + this.appSchemaCollectionName + '" collection.');
100
+ const collection: MongoDb.Collection = await this.mongoDb.createCollection(this.appSchemaCollectionName);
101
+
102
+ // ==========================================
103
+ // Makes sure only one appSchema document exists.
104
+ // ==========================================
105
+ await collection.createIndexes([
106
+ {
107
+ key: {
108
+ name: 1
109
+ },
110
+ name: 'name_1',
111
+ unique: true
112
+ }
113
+ ]);
114
+
115
+ // ==========================================
116
+ // Inserts the first version of the AppSchema document
117
+ // ==========================================
118
+ await collection.insertOne({
119
+ name: 'singleton', // do not change!
120
+ version: '0.0.0',
121
+ lock: false,
122
+ lockTimestamp: 0
123
+ } as ISchemeInfo);
124
+ } catch (err) {
125
+ // ==========================================
126
+ // Maybe the error occured because another app
127
+ // was also trying to create the collection?
128
+ // ==========================================
129
+ const maxWaitMilliseconds = 10 * 1000;
130
+ const start = new Timer();
131
+ while (start.getMillisecondsElapsed() < maxWaitMilliseconds) {
132
+ await utils.sleep(1000);
133
+
134
+ const appSchemaCollection: MongoDb.Collection = await this.getAppSchemaCollection();
135
+ if (!appSchemaCollection) {
136
+ continue;
137
+ }
138
+
139
+ const document = await appSchemaCollection.findOne({});
140
+ if (!document) {
141
+ continue;
142
+ }
143
+
144
+ // ==========================================
145
+ // We ignore the error!
146
+ // ==========================================
147
+ return;
148
+ }
149
+
150
+ // ==========================================
151
+ // Throws the error...
152
+ // ==========================================
153
+ throw err;
154
+ }
155
+ }
156
+
157
+ public async updateAppSchemaVersion(currentVersion: string, newVersion: string): Promise<void> {
158
+ const appSchemaCollection: MongoDb.Collection = await this.getAppSchemaCollection();
159
+
160
+ await appSchemaCollection.updateOne({}, { $set: { version: newVersion } });
161
+ // tslint:disable-next-line: prefer-template
162
+ logger.info(' > MongoDB App Schema updagred from version ' + currentVersion + ' to version ' + newVersion);
163
+ }
164
+
165
+ public async getAppSchemaUpdateFiles(
166
+ currentAppSchemaVersion: string,
167
+ targetAppSchemaVersion: string
168
+ ): Promise<string[]> {
169
+ return new Promise<string[]>((resolve, reject) => {
170
+ fs.readdir(this.getAppSchemaFilesDirPath(), (err, files) => {
171
+ if (err) {
172
+ return reject(err);
173
+ }
174
+
175
+ let filesClean = files;
176
+
177
+ try {
178
+ filesClean = filesClean
179
+ .filter(name => {
180
+ return name.match(/\.js$/) !== null;
181
+ })
182
+ .map(name => {
183
+ return name.split('.js')[0];
184
+ })
185
+ .filter(updateFileVersion => {
186
+ if (
187
+ semver.gt(updateFileVersion, currentAppSchemaVersion) &&
188
+ semver.lte(updateFileVersion, targetAppSchemaVersion)
189
+ ) {
190
+ return true;
191
+ }
192
+ return false;
193
+ });
194
+ } catch (err2) {
195
+ return reject(err2);
196
+ }
197
+ return resolve(filesClean.sort(semver.compare));
198
+ });
199
+ });
200
+ }
201
+
202
+ public async applyAppSchemaUpdates(currentVersion: string, newVersion: string): Promise<void> {
203
+ const updateFileNames: string[] = await this.getAppSchemaUpdateFiles(currentVersion, newVersion);
204
+ if (updateFileNames.length > 0) {
205
+ for (const updateFileName of updateFileNames) {
206
+ logger.info(' > Pending app schema update: ' + updateFileName);
207
+
208
+ // tslint:disable-next-line: prefer-template
209
+ const updateFilePath = this.getAppSchemaFilesDirPath() + '/' + updateFileName;
210
+ let updateFunction: (db: MongoDb.Db) => Promise<void>;
211
+ try {
212
+ updateFunction = require(updateFilePath).default;
213
+ } catch (e) {
214
+ return Promise.reject(e);
215
+ }
216
+
217
+ if (!isFunction(updateFunction)) {
218
+ return Promise.reject(
219
+ 'The default export for an app schema update file must be a function! Was not for file : ' + updateFilePath
220
+ );
221
+ }
222
+
223
+ await updateFunction(this.mongoDb);
224
+ }
225
+ logger.info('All app schema updates done');
226
+ }
227
+ }
228
+
229
+ public async getAppSchemaCollection(): Promise<MongoDb.Collection> {
230
+ return await this.mongoDb.collection(this.appSchemaCollectionName);
231
+ }
232
+
233
+ public async getAppSchemaVersion(): Promise<string> {
234
+ const appSchemaCollection: MongoDb.Collection = await this.getAppSchemaCollection();
235
+
236
+ const documents: any[] = await appSchemaCollection.find().toArray();
237
+ if (documents.length > 0) {
238
+ return documents[0].version;
239
+ }
240
+
241
+ return '0.0.0';
242
+ }
243
+
244
+ /**
245
+ * Tries to get the lock to modify Mongo's schemas.
246
+ *
247
+ * If a lock already exists, checks if it is too old.
248
+ * If too old, will create a new one... This is to prevents
249
+ * situations where a lock would have been taken by an app
250
+ * but that app *crashed* while the lock was on. We don't want
251
+ * suck lock to be active forever...
252
+ *
253
+ */
254
+ public async lockAppSchemaDocument(): Promise<boolean> {
255
+ const appSchemaCollection: MongoDb.Collection = await this.getAppSchemaCollection();
256
+
257
+ let document = await appSchemaCollection.findOneAndUpdate(
258
+ { lock: false },
259
+ {
260
+ $set: {
261
+ lock: true,
262
+ lockTimestamp: new Date().getTime()
263
+ }
264
+ }
265
+ );
266
+
267
+ if (document.value !== null) {
268
+ logger.info(` > Succesfully locked the ${this.appSchemaCollectionName} document`);
269
+ return true;
270
+ }
271
+
272
+ document = await appSchemaCollection.findOne({ lock: true });
273
+ if (document === null) {
274
+ // try again!
275
+ return this.lockAppSchemaDocument();
276
+ }
277
+
278
+ // ==========================================
279
+ // Checks the existing lock's timestamp
280
+ // ==========================================
281
+ const lockTimestamp = (document as any).lockTimestamp;
282
+ const nowTimestamp = new Date().getTime();
283
+ const lockAgeMilliSeconds = nowTimestamp - lockTimestamp;
284
+
285
+ // ==========================================
286
+ // Lock is too old! We overwrite it....
287
+ // ==========================================
288
+ if (lockAgeMilliSeconds > this.lockMaxAgeSeconds * 1000) {
289
+ document = await appSchemaCollection.findOneAndUpdate(
290
+ { lockTimestamp },
291
+ {
292
+ $set: {
293
+ lock: true,
294
+ lockTimestamp: new Date().getTime()
295
+ }
296
+ }
297
+ );
298
+
299
+ // ==========================================
300
+ // *Just* taken by another app!
301
+ // ==========================================
302
+ if (document.value === null) {
303
+ return false;
304
+ }
305
+
306
+ return true;
307
+ }
308
+
309
+ // ==========================================
310
+ // The existing lock is still valid...
311
+ // We can't get it.
312
+ // ==========================================
313
+ return false;
314
+ }
315
+
316
+ public async unlockAppSchemaDocument(): Promise<boolean> {
317
+ const appSchemaCollection: MongoDb.Collection = await this.getAppSchemaCollection();
318
+
319
+ const document = await appSchemaCollection.findOneAndUpdate(
320
+ { lock: true },
321
+ {
322
+ $set: {
323
+ lock: false,
324
+ lockTimestamp: 0
325
+ }
326
+ }
327
+ );
328
+
329
+ if (document.value !== null) {
330
+ logger.info(` > Succesfully unlocked the ${this.appSchemaCollectionName} document`);
331
+ return true;
332
+ }
333
+
334
+ logger.info(`> The ${this.appSchemaCollectionName} document was not locked`);
335
+ return false;
336
+ }
337
+
338
+ public async checkInstallation(): Promise<void> {
339
+ logger.info(
340
+ `Validating that the "${this.appSchemaCollectionName}" collection required by the application has been installed.`
341
+ );
342
+ const collections: any[] = await this.mongoDb.listCollections({ name: this.appSchemaCollectionName }).toArray();
343
+
344
+ if (collections.length === 0) {
345
+ logger.info(` > The "${this.appSchemaCollectionName}" collection was not found... Starting a new installation.`);
346
+ await this.installAppSchemaCollection();
347
+ } else {
348
+ logger.info(` > The "${this.appSchemaCollectionName}" collection was found. No installation required.`);
349
+ }
350
+ }
351
+
352
+ public checkUpdates = async (): Promise<void> => {
353
+ logger.info('Checking for db app schema updates:');
354
+
355
+ let lockAcquired = false;
356
+ const targetVersion: string = this.findMongoAppSchemaTargetVersion();
357
+ try {
358
+ while (true) {
359
+ // ==========================================
360
+ // Checks if the appSchema version has to be
361
+ // updated.
362
+ // ==========================================
363
+ const currentAppSchemaVersion: string = await this.getAppSchemaVersion();
364
+ if (semver.gte(currentAppSchemaVersion, targetVersion)) {
365
+ // tslint:disable-next-line: prefer-template
366
+ logger.info(' > Current database app schema is up to date : ' + currentAppSchemaVersion + ').');
367
+ return;
368
+ }
369
+
370
+ if (lockAcquired) {
371
+ logger.info(` > Applying some required updates...`);
372
+ await this.applyAppSchemaUpdates(currentAppSchemaVersion, targetVersion);
373
+ await this.updateAppSchemaVersion(currentAppSchemaVersion, targetVersion);
374
+ return;
375
+ }
376
+
377
+ // ==========================================
378
+ // Tries to get the lock. Will do this as long
379
+ // as the lock can't be acquired (ie : it is
380
+ // released or is too old) or as long as the appSchema
381
+ // version still is not up to date.
382
+ // ==========================================
383
+ lockAcquired = await this.lockAppSchemaDocument();
384
+ if (!lockAcquired) {
385
+ const wait = 1000;
386
+ logger.warning(
387
+ `The lock can't be acquired. The maximum age it can be before being considered ` +
388
+ `to be too old is ${this.lockMaxAgeSeconds} seconds. Waiting for ${wait} milliseconds...`
389
+ );
390
+ await utils.sleep(wait);
391
+ } else {
392
+ logger.info(` > Lock acquired.`);
393
+ }
394
+ }
395
+ } finally {
396
+ if (lockAcquired) {
397
+ await this.unlockAppSchemaDocument();
398
+ }
399
+ }
400
+ };
401
+
402
+ protected getAppSchemaFilesDirPath(): string {
403
+ return mongodbConstants.appRoot + this.mongoSchemaUpdatesDirPath;
404
+ }
405
+
406
+ /**
407
+ * Finds the latest Mongo update file version.
408
+ */
409
+ protected findMongoAppSchemaTargetVersion(): string {
410
+ let targetVersion = '0.0.0';
411
+
412
+ fs.readdirSync(this.getAppSchemaFilesDirPath()).forEach(fileName => {
413
+ if (fileName.endsWith('.js')) {
414
+ const version = fileName.split('.js')[0];
415
+ if (semver.gt(version, targetVersion)) {
416
+ targetVersion = version;
417
+ }
418
+ }
419
+ });
420
+
421
+ return targetVersion;
422
+ }
423
+ }
@@ -0,0 +1,23 @@
1
+ import { assert } from 'chai';
2
+ import { MongoMemoryReplSet, MongoMemoryServer } from 'mongodb-memory-server-core';
3
+ import { MongoUtils } from './mongoUtils';
4
+
5
+ describe('MongoUtils - #mockMongoose', () => {
6
+ /*
7
+ It is mandatory to create a new instance of MongoUtils to test the function mockMongoose as the singleton will keep its parameters instanced if used again. The parameter mongoMemServer, if instanciated by a first call,
8
+ won't be changed on a second one, as if the memory server is already mocked, nothing happens.
9
+ */
10
+ it('should return a mongo memory server', async function() {
11
+ const mongoUtils = new MongoUtils();
12
+ const memServ = await mongoUtils.mockMongoose(this, null, false);
13
+ assert.isDefined(memServ);
14
+ assert.instanceOf(memServ, MongoMemoryServer);
15
+ });
16
+
17
+ it('should return a mongo memory replica set', async function() {
18
+ const mongoUtils = new MongoUtils();
19
+ const memServ = await mongoUtils.mockMongoose(this, null, true);
20
+ assert.isDefined(memServ);
21
+ assert.instanceOf(memServ, MongoMemoryReplSet);
22
+ });
23
+ });