@itentialopensource/adapter-kafkav2 0.19.0 → 0.21.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.
@@ -0,0 +1,1603 @@
1
+ /* Required libraries. */
2
+ /* global g_redis log */
3
+ /* eslint consistent-return:warn */
4
+ /* eslint no-underscore-dangle: [2, { "allow": ["_id"] }] */
5
+ /* eslint no-unused-vars:warn */
6
+
7
+ const fs = require('fs-extra');
8
+ const uuid = require('uuid');
9
+
10
+ /* Fetch in the other needed components for the this Adaptor */
11
+ const {
12
+ MongoClient, MongoClientOptions, CreateIndexesOptions, CountDocumentsOptions,
13
+ DeleteOptions, DeleteResult, ReplaceOptions, Document, UpdateResult, WithId, Filter,
14
+ UpdateFilter, FindOneAndUpdateOptions, Sort
15
+ } = require('mongodb');
16
+
17
+ // Other global variables
18
+ let adapterDir = '';
19
+ let saveFS = false;
20
+ let storDir = `${adapterDir}/storage`;
21
+ let entityDir = `${adapterDir}/entities`;
22
+ let id = null;
23
+
24
+ const Storage = {
25
+ UNDEFINED: 0,
26
+ DBINFO: 1,
27
+ ADAPTERDB: 2,
28
+ FILESYSTEM: 3,
29
+ IAPDB: 4
30
+ };
31
+ Object.freeze(Storage);
32
+
33
+ /* DB UTILS INTERNAL FUNCTIONS */
34
+ /** getFromJson
35
+ * @summary returns information from a json file on the file system
36
+ */
37
+ function getFromJson(fileName, filter) {
38
+ const origin = `${id}-dbUtil-getFromJson`;
39
+ log.trace(origin);
40
+
41
+ // verify the required data has been provided
42
+ if (fileName === undefined || fileName === null || fileName === '') {
43
+ log.warn(`${origin}: Must provide a file name`);
44
+ return null;
45
+ }
46
+
47
+ // get the entity and the action if this is a metric
48
+ let ent = null;
49
+ let act = null;
50
+ let metric = false;
51
+ const useFilter = filter;
52
+ if (useFilter && useFilter.metric) {
53
+ metric = true;
54
+ if (useFilter.metric.entity) {
55
+ ent = useFilter.metric.entity;
56
+ }
57
+ if (useFilter.metric.action) {
58
+ act = useFilter.metric.action;
59
+ }
60
+ if (!ent || !act) {
61
+ log.warn(`${origin}: metric provided with one of entity/action, require both`);
62
+ return null;
63
+ }
64
+ delete useFilter.metric;
65
+ }
66
+
67
+ try {
68
+ if (fileName === 'adapter_configs') {
69
+ const retEntity = filter.entity;
70
+ if (!retEntity || !fs.existsSync(`${entityDir}`) || !fs.existsSync(`${entityDir}/${retEntity}`)) {
71
+ log.warn(`${origin}: Could not find adapter entity directory ${retEntity} - nothing to retrieve`);
72
+ return null;
73
+ }
74
+ if (!fs.existsSync(`${entityDir}/${retEntity}/action.json`)) {
75
+ log.warn(`${origin}: Could not find action.json for entity ${retEntity} - nothing to retrieve`);
76
+ return null;
77
+ }
78
+
79
+ // load the mockdatafiles
80
+ let files = fs.readdirSync(`${entityDir}/${retEntity}`);
81
+ const mockdatafiles = {};
82
+ if (files.includes('mockdatafiles') && fs.lstatSync(`${entityDir}/${retEntity}/mockdatafiles`).isDirectory()) {
83
+ fs.readdirSync(`${entityDir}/${retEntity}/mockdatafiles`).forEach((file) => {
84
+ if (file.split('.').pop() === 'json') {
85
+ const mockpath = `${entityDir}/${retEntity}/mockdatafiles/${file}`;
86
+ let data = {};
87
+ try {
88
+ data = JSON.parse(fs.readFileSync(mockpath));
89
+ } catch (ex) {
90
+ log.warn(`could not parse mockdata file ${file}, using empty object!`);
91
+ }
92
+ mockdatafiles[mockpath.split('/').pop()] = data;
93
+ } else if (file.split('.').pop() === 'xml') {
94
+ const mockpath = `${entityDir}/${retEntity}/mockdatafiles/${file}`;
95
+ mockdatafiles[mockpath.split('/').pop()] = fs.readFileSync(mockpath);
96
+ }
97
+ });
98
+ }
99
+
100
+ // load the action data
101
+ let actions;
102
+ if (files.includes('action.json')) {
103
+ actions = JSON.parse(fs.readFileSync(`${entityDir}/${retEntity}/action.json`));
104
+ }
105
+
106
+ // Load schema.json and other schemas in remaining json files
107
+ files = files.filter((f) => (f !== 'action.json') && f.endsWith('.json'));
108
+ const schema = [];
109
+ files.forEach((file) => {
110
+ const data = JSON.parse(fs.readFileSync(`${entityDir}/${retEntity}/${file}`));
111
+ schema.push({
112
+ name: file,
113
+ schema: data
114
+ });
115
+ });
116
+
117
+ // return the data
118
+ return {
119
+ actions: actions.actions,
120
+ schema,
121
+ mockdatafiles
122
+ };
123
+ }
124
+
125
+ // make sure what we need exists and has been provided
126
+ if (!fs.existsSync(`${storDir}`)) {
127
+ log.warn(`${origin}: Could not find adapter storage directory - nothing to retrieve`);
128
+ return null;
129
+ }
130
+
131
+ // determine if the file exists so we retrieve the data
132
+ if (fs.existsSync(`${storDir}/${fileName}.json`)) {
133
+ const content = JSON.parse(fs.readFileSync(`${storDir}/${fileName}.json`, 'utf-8'));
134
+ const toReturn = [];
135
+
136
+ if (metric) {
137
+ // if metric need to match the entity and action
138
+ content.table.forEach((item) => {
139
+ if (ent === item.entity && act === item.action) {
140
+ toReturn.push(item);
141
+ }
142
+ });
143
+ } else {
144
+ // if not metric must match the items in the filter
145
+ // if no filter, match everything
146
+ let key = [];
147
+ if (useFilter && useFilter.length > 0) {
148
+ key = Object.keys(useFilter);
149
+ }
150
+ content.table.forEach((item) => {
151
+ let push = true;
152
+ // check each key - if it does not match a key do not add to return data
153
+ key.forEach((fil) => {
154
+ if (useFilter[fil] !== item[fil]) push = false;
155
+ });
156
+ if (push) toReturn.push(item);
157
+ });
158
+ }
159
+ // not sure why we do this - a little different than above.
160
+ const filtered = content.table.filter((el) => {
161
+ if (useFilter) {
162
+ Object.keys(useFilter).forEach((obj) => {
163
+ if (el[obj] !== useFilter[obj]) {
164
+ return false;
165
+ }
166
+ });
167
+ }
168
+ return true;
169
+ });
170
+ return toReturn;
171
+ }
172
+
173
+ // if no file, nothing to return
174
+ return null;
175
+ } catch (ex) {
176
+ log.warn(`${origin}: Caught Exception ${ex}`);
177
+ return null;
178
+ }
179
+ }
180
+
181
+ /** countJSON
182
+ * @summary returns a count from a json storage file
183
+ */
184
+ function countJSON(fileName, filter) {
185
+ const origin = `${id}-dbUtil-countJSON`;
186
+ log.trace(origin);
187
+
188
+ // verify the required data has been provided
189
+ if (fileName === undefined || fileName === null || fileName === '') {
190
+ log.warn(`${origin}: Must provide a file name`);
191
+ return null;
192
+ }
193
+
194
+ try {
195
+ // make sure what we need exists and has been provided
196
+ if (!fs.existsSync(`${storDir}`)) {
197
+ log.warn(`${origin}: Could not find adapter storage directory - nothing to count`);
198
+ return null;
199
+ }
200
+
201
+ // determine if the file exists so we can count data from it
202
+ if (fs.existsSync(`${storDir}/${fileName}.json`)) {
203
+ const data = getFromJson(fileName, filter);
204
+ if (data) {
205
+ return data.length;
206
+ }
207
+ return -1;
208
+ }
209
+ } catch (ex) {
210
+ log.warn(`${origin}: Caught Exception ${ex}`);
211
+ return null;
212
+ }
213
+ }
214
+
215
+ /** saveAsJson
216
+ * @summary saves information into a json storage file
217
+ */
218
+ function saveAsJson(fileName, data) {
219
+ const origin = `${id}-dbUtil-saveAsJson`;
220
+ log.trace(origin);
221
+
222
+ // verify the required data has been provided
223
+ if (fileName === undefined || fileName === null || fileName === '') {
224
+ log.warn(`${origin}: Must provide a file name`);
225
+ return null;
226
+ }
227
+
228
+ // get the entity and the action if this is a metric
229
+ let ent = null;
230
+ let act = null;
231
+ let metric = false;
232
+ const useData = data;
233
+ if (useData && useData.metric) {
234
+ metric = true;
235
+ if (useData.metric.entity) {
236
+ ent = useData.metric.entity;
237
+ }
238
+ if (useData.metric.action) {
239
+ act = useData.metric.action;
240
+ }
241
+ if (!ent || !act) {
242
+ log.warn(`${origin}: metric provided with one of entity/action, require both`);
243
+ return null;
244
+ }
245
+ delete useData.metric;
246
+ }
247
+
248
+ try {
249
+ // make sure what we need exists and has been provided
250
+ if (!fs.existsSync(`${storDir}`)) {
251
+ // need to make the storage directory if it does not exist
252
+ fs.mkdirSync(`${storDir}`);
253
+ }
254
+
255
+ // determine if the file exists so we add data to it
256
+ if (fs.existsSync(`${storDir}/${fileName}.json`)) {
257
+ // have to read, append & save
258
+ const content = JSON.parse(fs.readFileSync(`${storDir}/${fileName}.json`, 'utf-8'));
259
+ let exists = false;
260
+
261
+ content.table.forEach((item) => {
262
+ const toPush = item;
263
+ if (metric) {
264
+ // if it is a metric and we already have entity and action need to edit the data
265
+ if (ent === item.entity && act === item.action) {
266
+ exists = true;
267
+ Object.keys(useData).forEach((key) => {
268
+ if (key === '$inc') {
269
+ Object.keys(useData[key]).forEach((inc) => {
270
+ if (!toPush[inc]) {
271
+ toPush[inc] = useData[key][inc];
272
+ }
273
+ toPush[inc] += useData[key][inc];
274
+ });
275
+ } else if (key === '$set') {
276
+ Object.keys(useData[key]).forEach((set) => {
277
+ toPush[set] = useData[key][set];
278
+ });
279
+ }
280
+ });
281
+ }
282
+ }
283
+ });
284
+ if (!exists) {
285
+ // push new thing to table.
286
+ const toPush = {};
287
+ const keysArray = Object.keys(useData);
288
+ keysArray.forEach((i) => {
289
+ const newKeys = Object.keys(useData[i]);
290
+ newKeys.forEach((j) => {
291
+ toPush[j] = useData[i][j];
292
+ });
293
+ });
294
+ content.table.push(toPush);
295
+ }
296
+
297
+ // now that is updated, write the file back out
298
+ fs.writeFileSync(`${storDir}/${fileName}.json`, JSON.stringify(content, null, 2));
299
+ return useData;
300
+ }
301
+
302
+ // if the file has not been created yet
303
+ const obj = { table: [] };
304
+ const toPush = {};
305
+ const keysArray = Object.keys(data);
306
+ keysArray.forEach((outer) => {
307
+ const newKeys = Object.keys(data[outer]);
308
+ newKeys.forEach((inner) => {
309
+ toPush[inner] = data[outer][inner];
310
+ });
311
+ });
312
+ obj.table.push(toPush);
313
+
314
+ // write the file out to the file system
315
+ fs.writeFileSync(`${storDir}/${fileName}.json`, JSON.stringify(obj, null, 2));
316
+ return data;
317
+ } catch (ex) {
318
+ log.warn(`${origin}: Caught Exception ${ex}`);
319
+ return null;
320
+ }
321
+ }
322
+
323
+ /** removeFromJSON
324
+ * @summary removes information from a json storage file
325
+ */
326
+ function removeFromJSON(fileName, filter, multiple) {
327
+ const origin = `${id}-dbUtil-removeFromJSON`;
328
+ log.trace(origin);
329
+
330
+ // verify the required data has been provided
331
+ if (fileName === undefined || fileName === null || fileName === '') {
332
+ log.warn(`${origin}: Must provide a file name`);
333
+ return null;
334
+ }
335
+
336
+ // get the entity and the action if this is a metric
337
+ let ent = null;
338
+ let act = null;
339
+ let metric = false;
340
+ const useFilter = filter;
341
+ if (useFilter && useFilter.metric) {
342
+ metric = true;
343
+ if (useFilter.metric.entity) {
344
+ ent = useFilter.metric.entity;
345
+ }
346
+ if (useFilter.metric.action) {
347
+ act = useFilter.metric.action;
348
+ }
349
+ if (!ent && !act) {
350
+ log.warn(`${origin}: metric provided with one of entity/action, require both`);
351
+ return null;
352
+ }
353
+ delete useFilter.metric;
354
+ }
355
+
356
+ try {
357
+ // make sure what we need exists and has been provided
358
+ if (!fs.existsSync(`${storDir}`)) {
359
+ log.warn(`${origin}: Could not find adapter storage directory - nothing to remove`);
360
+ return null;
361
+ }
362
+
363
+ // determine if the file exists so we remove data from it
364
+ if (fs.existsSync(`${storDir}/${fileName}.json`)) {
365
+ const content = JSON.parse(fs.readFileSync(`${storDir}/${fileName}.json`, 'utf-8'));
366
+ const toReturn = [];
367
+
368
+ // if this is a metric make sure the entity and action match
369
+ if (metric) {
370
+ content.table = content.table.filter((item) => {
371
+ if (ent === item.entity && act === item.action) {
372
+ toReturn.push(item);
373
+ return false;
374
+ }
375
+ return true;
376
+ });
377
+ } else {
378
+ // go through content to determine if item matches the filter
379
+ let ctr = 0;
380
+ // create the contents that are being removed
381
+ const removed = content.table.filter((el) => {
382
+ Object.keys(useFilter).forEach((obj) => {
383
+ if (el[obj] !== useFilter[obj]) {
384
+ return false;
385
+ }
386
+ });
387
+ ctr += 1;
388
+ if (!multiple && ctr > 1) {
389
+ return false;
390
+ }
391
+ return true;
392
+ });
393
+ let ctr1 = 0;
394
+ // remove the items from the contents
395
+ content.table = content.table.filter((el, i) => {
396
+ Object.keys(useFilter).forEach((obj) => {
397
+ if (el[obj] !== useFilter[obj]) {
398
+ return true;
399
+ }
400
+ });
401
+ ctr1 += 1;
402
+ if (!multiple && ctr1 > 1) {
403
+ return true;
404
+ }
405
+ return false;
406
+ });
407
+
408
+ // write the contents back to the file
409
+ fs.writeFileSync(`${storDir}/${fileName}.json`, JSON.stringify(content, null, 2));
410
+ return removed;
411
+ }
412
+
413
+ // write the contents back to the file
414
+ fs.writeFileSync(`${storDir}/${fileName}.json`, JSON.stringify(content, null, 2));
415
+ return toReturn;
416
+ }
417
+
418
+ log.error(`${origin}: Collection ${fileName} does not exist`);
419
+ return null;
420
+ } catch (ex) {
421
+ log.warn(`${origin}: Caught Exception ${ex}`);
422
+ return null;
423
+ }
424
+ }
425
+
426
+ /** deleteJSON
427
+ * @summary deletes a json storage file
428
+ */
429
+ function deleteJSON(fileName) {
430
+ const origin = `${id}-dbUtil-deleteJSON`;
431
+ log.trace(origin);
432
+
433
+ // verify the required data has been provided
434
+ if (fileName === undefined || fileName === null || fileName === '') {
435
+ log.warn(`${origin}: Must provide a file name`);
436
+ return null;
437
+ }
438
+
439
+ try {
440
+ // make sure what we need exists and has been provided
441
+ if (!fs.existsSync(`${storDir}`)) {
442
+ log.warn(`${origin}: Could not find adapter storage directory - nothing to delete`);
443
+ return null;
444
+ }
445
+
446
+ // determine if the file exists so we remove - also assume we add a .json suffix
447
+ if (fs.existsSync(`${storDir}/${fileName}.json`)) {
448
+ fs.remove(`${storDir}/${fileName}.json`).catch((some) => {
449
+ log.info(`${origin}: ${some}`);
450
+ fs.rmdirSync(`${storDir}/${fileName}.json`);
451
+ });
452
+ }
453
+
454
+ // successful -- return the fileName
455
+ return fileName;
456
+ } catch (ex) {
457
+ log.warn(`${origin}: Caught Exception ${ex}`);
458
+ return null;
459
+ }
460
+ }
461
+
462
+ class DBUtil {
463
+ /**
464
+ * These are database utilities that can be used by the adapter to interact with a
465
+ * mongo database
466
+ * @constructor
467
+ */
468
+ constructor(prongId, properties, directory) {
469
+ this.myid = prongId;
470
+ id = prongId;
471
+ this.baseDir = directory;
472
+ adapterDir = this.baseDir;
473
+ storDir = `${adapterDir}/storage`;
474
+ entityDir = `${adapterDir}/entities`;
475
+ this.props = properties;
476
+ this.adapterMongoClient = null;
477
+
478
+ // set up the properties I care about
479
+ this.refreshProperties(properties);
480
+ }
481
+
482
+ /**
483
+ * refreshProperties is used to set up all of the properties for the db utils.
484
+ * It allows properties to be changed later by simply calling refreshProperties rather
485
+ * than having to restart the db utils.
486
+ *
487
+ * @function refreshProperties
488
+ * @param {Object} properties - an object containing all of the properties
489
+ */
490
+ refreshProperties(properties) {
491
+ const origin = `${this.myid}-dbUtil-refreshProperties`;
492
+ log.trace(origin);
493
+ this.dburl = null;
494
+ this.dboptions = {};
495
+ this.database = this.myid;
496
+
497
+ // verify the necessary information was received
498
+ if (!properties) {
499
+ log.error(`${origin}: DB Utils received no properties!`);
500
+ return;
501
+ }
502
+ if (!properties.mongo || !properties.mongo.host) {
503
+ log.info(`${origin}: No default adapter database configured!`);
504
+ return;
505
+ }
506
+ this.dboptions = properties.mongo.dboptions || {};
507
+
508
+ // set the database port
509
+ let port = 27017;
510
+ if (properties.mongo.port) {
511
+ port = properties.mongo.port;
512
+ }
513
+ // set the database
514
+ if (properties.mongo.database) {
515
+ this.database = properties.mongo.database;
516
+ }
517
+ // set the user
518
+ let username = null;
519
+ if (properties.mongo.username) {
520
+ username = properties.mongo.username;
521
+ }
522
+ // set the password
523
+ let password = null;
524
+ if (properties.mongo.password) {
525
+ password = properties.mongo.password;
526
+ }
527
+
528
+ // format the database url
529
+ this.dburl = 'mongodb://';
530
+ log.info(`${origin}: Default adapter database at: ${properties.mongo.host}`);
531
+
532
+ if (username) {
533
+ this.dburl += `${encodeURIComponent(username)}:${encodeURIComponent(password)}@`;
534
+ log.info(`${origin}: Default adapter database will use authentication.`);
535
+ }
536
+ this.dburl += `${encodeURIComponent(properties.mongo.host)}:${encodeURIComponent(port)}/${encodeURIComponent(this.database)}`;
537
+
538
+ // are we using a replication set need to add it to the url
539
+ if (properties.mongo.replSet) {
540
+ this.dburl += `?${properties.mongo.replSet}`;
541
+ log.info(`${origin}: Default adapter database will use replica set.`);
542
+ }
543
+
544
+ // Do we need SSL to connect to the database
545
+ if (properties.mongo.db_ssl && properties.mongo.db_ssl.enabled === true) {
546
+ log.info(`${origin}: Default adapter database will use SSL.`);
547
+ this.dboptions.ssl = true;
548
+
549
+ // validate the server's certificate against a known certificate authority?
550
+ if (properties.mongo.db_ssl.accept_invalid_cert === false) {
551
+ this.dboptions.sslValidate = true;
552
+ log.info(`${origin}: Default adapter database will use Certificate based SSL.`);
553
+ // if validation is enabled, we need to read the CA file
554
+ if (properties.mongo.db_ssl.ca_file) {
555
+ try {
556
+ this.dboptions.sslCA = [fs.readFileSync(properties.mongo.db_ssl.ca_file)];
557
+ } catch (err) {
558
+ log.error(`${origin}: Error: unable to load Mongo CA file: ${err}`);
559
+ }
560
+ } else {
561
+ log.error(`${origin}: Error: Certificate validation is enabled but a CA is not specified.`);
562
+ }
563
+ if (properties.mongo.db_ssl.key_file) {
564
+ try {
565
+ this.dboptions.sslKey = [fs.readFileSync(properties.mongo.db_ssl.key_file)];
566
+ } catch (err) {
567
+ log.error(`${origin}: Error: Unable to load Mongo Key file: ${err}`);
568
+ }
569
+ }
570
+ if (properties.mongo.db_ssl.cert_file) {
571
+ try {
572
+ this.dboptions.sslCert = [fs.readFileSync(properties.mongo.db_ssl.cert_file)];
573
+ } catch (err) {
574
+ log.error(`${origin}: Error: Unable to load Mongo Certificate file: ${err}`);
575
+ }
576
+ }
577
+ } else {
578
+ this.dboptions.sslValidate = false;
579
+ log.info(`${origin}: Default adapter database not using Certificate based SSL.`);
580
+ }
581
+ } else {
582
+ log.info(`${origin}: Default adapter database not using SSL.`);
583
+ }
584
+
585
+ // if we were provided with a path to save metrics - instead of just a boolean
586
+ // will assume this is a place where all adapter stuff can go!
587
+ saveFS = this.props.save_metric || false;
588
+ if (saveFS && typeof saveFS === 'string' && saveFS !== '') {
589
+ storDir = saveFS;
590
+ }
591
+ }
592
+
593
+ /**
594
+ * @callback determineStorageCallback
595
+ * @param {string} err - the error message
596
+ * @param {int} storageType - the storage type
597
+ * @param {MongoClient} mongoClient - the mongo client
598
+ * @param {string} database - the database
599
+ */
600
+
601
+ /**
602
+ * @typedef DBInfo
603
+ * @property {string} dburl
604
+ * @property {MongoClientOptions} dboptions
605
+ * @property {string} database
606
+ */
607
+
608
+ /**
609
+ * @typedef StorageInfo
610
+ * @property {int} storageType
611
+ * @property {MongoClient} mongoClient
612
+ * @property {string} database
613
+ */
614
+
615
+ /**
616
+ * Call to determine the storage target. If the storage is a Mongo database, then
617
+ * the client connection and database is returned.
618
+ *
619
+ * @function determineStorage
620
+ * @param {DBInfo | null} dbInfo - the url for the database to connect to (dburl, dboptions, database). If null, tries to connect to adapter database, then the filesystem
621
+ * @param {determineStorageCallback | undefined} callback - the callback to invoke. If undefined, the function acts like a promise. If defined, the function returns void.
622
+ *
623
+ * @returns {Promise<StorageInfo>} - {storageType, mongoClient, database}. storageType is an enum
624
+ * @throws {string} - the error message
625
+ */
626
+ async determineStorage(dbInfo, callback = undefined) {
627
+ const origin = `${this.myid}-dbUtil-determineStorage`;
628
+ const useCallback = typeof callback === 'function';
629
+
630
+ const createReturnObj = (storageType, mongoClient, database) => ({
631
+ storageType,
632
+ mongoClient,
633
+ database
634
+ });
635
+
636
+ let mongoClient = null;
637
+ let storageType = null;
638
+ let database = null;
639
+
640
+ if (dbInfo) {
641
+ // priority 1 - use the dbInfo passed in
642
+ if (dbInfo.dburl && dbInfo.database) {
643
+ try {
644
+ mongoClient = await MongoClient.connect(dbInfo.dburl, dbInfo.dboptions);
645
+ log.debug('Using dbInfo');
646
+ storageType = Storage.DBINFO;
647
+ database = dbInfo.database;
648
+ if (useCallback) return callback(null, storageType, mongoClient, database);
649
+ return createReturnObj(storageType, mongoClient, database);
650
+ } catch (err) {
651
+ const msg = `${origin}: Error! Failed to connect to database: ${err}`;
652
+ log.error(msg);
653
+ if (useCallback) return callback(msg, null, null, null);
654
+ throw msg;
655
+ }
656
+ } else {
657
+ const msg = 'Error! Malformed dbInfo';
658
+ if (useCallback) return callback(msg, null, null, Storage.DBINFO); // Unknown why callback ends with Storage.DBINFO here, kept for legacy purposes
659
+ throw msg;
660
+ }
661
+ } else if (this.adapterMongoClient) {
662
+ // priority 2 - use the adapter database
663
+ log.debug('Using adapter db');
664
+ storageType = Storage.ADAPTERDB;
665
+ mongoClient = this.adapterMongoClient;
666
+ database = this.database;
667
+ if (useCallback) return callback(null, storageType, mongoClient, database);
668
+ return createReturnObj(storageType, mongoClient, database);
669
+ } else if (this.dburl === null && dbInfo === null) {
670
+ // priority 3 - use the filesystem
671
+ log.debug('Using filesystem');
672
+ storageType = Storage.FILESYSTEM;
673
+ if (useCallback) return callback(null, storageType, null, null);
674
+ return createReturnObj(storageType, mongoClient, database);
675
+ }
676
+ }
677
+
678
+ /**
679
+ * @callback connectCallback
680
+ * @param {boolean} isAlive
681
+ * @param {MongoClient} mongoClient
682
+ */
683
+
684
+ /**
685
+ * @typedef ConnectionInfo
686
+ * @property {boolean} isAlive
687
+ * @property {MongoClient} mongoClient
688
+ */
689
+
690
+ /**
691
+ * Call to connect and authenticate to the adapter database
692
+ *
693
+ * @function connect
694
+ * @param {connectCallback | undefined} callback - the callback to invoke. If undefined, the function acts like a promise. If defined, the function returns void.
695
+ *
696
+ * @returns {Promise<ConnectionInfo>}
697
+ */
698
+ async connect(callback = undefined) {
699
+ const origin = `${this.myid}-dbUtil-connect`;
700
+ const useCallback = typeof callback === 'function';
701
+ log.trace(origin);
702
+
703
+ let mongoClient = null;
704
+
705
+ // const options = (replSetEnabled === true) ? { replSet: opts } : { server: opts };
706
+ log.debug(`${origin}: Connecting to MongoDB with options ${JSON.stringify(this.dboptions)}`);
707
+
708
+ try {
709
+ // Now we will start the process of connecting to mongo db
710
+ mongoClient = await MongoClient.connect(this.dburl, this.dboptions);
711
+ mongoClient.on('close', () => {
712
+ this.alive = false;
713
+ log.error(`${origin}: MONGO CONNECTION LOST...`);
714
+ });
715
+
716
+ mongoClient.on('reconnect', async () => {
717
+ // we still need to check if we are properly authenticated
718
+ // so we just list collections to test it.
719
+ try {
720
+ await this.clientDB.listCollections().toArray();
721
+ log.info(`${origin}: MONGO CONNECTION BACK...`);
722
+ this.alive = true;
723
+ this.adapterMongoClient = mongoClient;
724
+ } catch (error) {
725
+ log.error(`${origin}: ${error}`);
726
+ this.alive = false;
727
+ }
728
+ });
729
+
730
+ log.info(`${origin}: mongo running @${this.dburl}/${this.database}`);
731
+ this.clientDB = mongoClient.db(this.database);
732
+ this.adapterMongoClient = mongoClient;
733
+
734
+ // we don't have authentication defined but we still need to check if Mongo does not
735
+ // require one, so we just list collections to test if it's doable.
736
+ try {
737
+ await this.clientDB.listCollections().toArray();
738
+ log.info(`${origin}: MongoDB connection has been established`);
739
+ this.alive = true;
740
+ this.adapterMongoClient = mongoClient;
741
+ } catch (error) {
742
+ log.error(`${origin}: ${error}`);
743
+ this.alive = false;
744
+ }
745
+ if (useCallback) return callback(this.alive, mongoClient);
746
+ return { isAlive: this.alive, mongoClient };
747
+ } catch (err) {
748
+ log.error(`${origin}: Error! Exiting... Must start MongoDB first ${err}`);
749
+ this.alive = false;
750
+ if (useCallback) return callback(this.alive, mongoClient);
751
+ return { isAlive: this.alive, mongoClient };
752
+ }
753
+ }
754
+
755
+ /**
756
+ * Call to disconnect from the adapter database
757
+ *
758
+ * @function disconnect
759
+ */
760
+ async disconnect() {
761
+ if (this.adapterMongoClient) {
762
+ await this.adapterMongoClient.close();
763
+ }
764
+ }
765
+
766
+ /**
767
+ * @callback collectionCallback
768
+ * @param {string} err - the error message
769
+ * @param {string} collectionName
770
+ */
771
+ /**
772
+ * createCollection creates the provided collection in the file system or database.
773
+ *
774
+ * @function createCollection
775
+ * @param {string} collectionName - the name of the collection to create
776
+ * @param {DBInfo} dbInfo - the url for the database to connect to (dburl, dboptions, database)
777
+ * @param {boolean} fsWrite - turn on write to the file system
778
+ * @param {collectionCallback | undefined} callback - the callback to invoke. If undefined, the function acts like a promise. If defined, the function returns void.
779
+ *
780
+ * @returns {Promise<string>} - the name of the collection
781
+ * @throws {string} - the error message
782
+ */
783
+ async createCollection(collectionName, dbInfo, fsWrite, callback = undefined) {
784
+ const origin = `${this.myid}-dbUtil-createCollection`;
785
+ const useCallback = typeof callback === 'function';
786
+ log.trace(origin);
787
+
788
+ // verify the required data has been provided
789
+ if (!collectionName || (typeof collectionName !== 'string')) {
790
+ const msg = `${origin}: Missing Collection Name or not string`;
791
+ log.warn(msg);
792
+ if (useCallback) return callback(msg, null);
793
+ throw msg;
794
+ }
795
+
796
+ let res;
797
+ try {
798
+ res = await this.determineStorage(dbInfo);
799
+ } catch (err) {
800
+ const msg = `${origin}: ${err}`;
801
+ if (useCallback) return callback(msg, null);
802
+ throw msg;
803
+ }
804
+
805
+ // if using file storage
806
+ if (res.storageType === Storage.FILESYSTEM) {
807
+ // if there is no adapter directory - can not do anything so error
808
+ if (!fs.existsSync(`${this.baseDir}`)) {
809
+ const msg = `${origin}: Not able to create storage - missing base directory!`;
810
+ log.warn(msg);
811
+ if (useCallback) return callback(msg, null);
812
+ throw msg;
813
+ }
814
+ // if there is no storage directory - create it
815
+ if (!fs.existsSync(`${this.baseDir}/storage`)) {
816
+ fs.mkdirSync(`${this.baseDir}/storage`);
817
+ }
818
+ // if the collection already exists - no need to create it
819
+ if (fs.existsSync(`${this.baseDir}/storage/${collectionName}`)) {
820
+ log.debug(`${origin}: storage file collection already exists`);
821
+ return collectionName;
822
+ }
823
+ // create the new collection on the file system
824
+ fs.mkdirSync(`${this.baseDir}/storage/${collectionName}`);
825
+ log.debug(`${origin}: storage file collection ${collectionName} created`);
826
+ return collectionName;
827
+ }
828
+
829
+ // if using MongoDB storage
830
+ if (res.storageType === Storage.DBINFO || res.storageType === Storage.ADAPTERDB) {
831
+ let collection;
832
+ try {
833
+ collection = await res.mongoClient.db(res.database).createCollection(collectionName);
834
+ } catch (err) {
835
+ // error we get back if the collection already existed - not a true error
836
+ if (err.codeName === 'NamespaceExists') {
837
+ log.debug(`${origin}: database collection already exists`);
838
+ if (useCallback) return callback(null, collectionName);
839
+ return collectionName;
840
+ }
841
+ const msg = `${origin}: Error creating collection ${err}`;
842
+ log.warn(msg);
843
+ if (useCallback) return callback(msg, null);
844
+ throw msg;
845
+ }
846
+
847
+ log.spam(`${origin}: db response ${res}`);
848
+ log.debug(`${origin}: database collection ${collectionName} created`);
849
+ if (useCallback) return callback(null, collectionName);
850
+ return collectionName;
851
+ }
852
+
853
+ log.warn(`${origin}: Unexpected end of function reached`);
854
+ }
855
+
856
+ /**
857
+ * removeCollection removes the provided collection from the file system or database.
858
+ *
859
+ * @function removeCollection
860
+ * @param {string} collectionName - the name of the collection to remove
861
+ * @param {DBInfo} dbInfo - the url for the database to connect to (dburl, dboptions, database)
862
+ * @param {boolean} fsWrite - turn on write to the file system
863
+ * @param {collectionCallback | undefined} callback - the callback to invoke. If undefined, the function acts like a promise. If defined, the function returns void.
864
+ *
865
+ * @returns {Promise<string>} - the name of the collection
866
+ * @throws {string} - the error message
867
+ */
868
+ async removeCollection(collectionName, dbInfo, fsWrite, callback = undefined) {
869
+ const origin = `${this.myid}-dbUtil-removeCollection`;
870
+ const useCallback = typeof callback === 'function';
871
+ log.trace(origin);
872
+
873
+ // verify the required data has been provided
874
+ if (!collectionName || (typeof collectionName !== 'string')) {
875
+ const msg = `${origin}: Missing Collection Name or not string`;
876
+ log.warn(msg);
877
+ if (useCallback) return callback(msg, null);
878
+ throw msg;
879
+ }
880
+
881
+ let res;
882
+ try {
883
+ res = await this.determineStorage(dbInfo);
884
+ } catch (err) {
885
+ const msg = `${origin}: ${err}`;
886
+ if (useCallback) return callback(msg, null);
887
+ throw msg;
888
+ }
889
+
890
+ // if using file storage
891
+ if (res.storageType === Storage.FILESYSTEM) {
892
+ const deld = deleteJSON(collectionName);
893
+ if (deld) {
894
+ log.debug(`${origin}: storage file collection ${collectionName} removed`);
895
+ if (useCallback) return callback(null, deld);
896
+ return deld;
897
+ }
898
+ const msg = `${origin}: could not remove storage file collection`;
899
+ log.debug(msg);
900
+ if (useCallback) return callback(msg, null);
901
+ throw msg;
902
+ }
903
+
904
+ // if using MongoDB storage
905
+ if (res.storageType === Storage.DBINFO || res.storageType === Storage.ADAPTERDB) {
906
+ // get the list of collections from the database - can we just drop?
907
+ let collections;
908
+ try {
909
+ collections = await res.mongoClient.db(res.database).listCollections().toArray();
910
+ } catch (err) {
911
+ const msg = `${origin}: Failed to get collections ${err}`;
912
+ log.warn(msg);
913
+ if (useCallback) return callback(msg, null);
914
+ throw msg;
915
+ }
916
+
917
+ // go through the collections to get the correct one for removal
918
+ const collectionToRemove = collections.find((collection) => collection.name === collectionName);
919
+ // If it doesn't exist, drop is implicit and successful
920
+ if (!collectionToRemove) {
921
+ if (useCallback) return callback(null, collectionName);
922
+ return collectionName;
923
+ }
924
+
925
+ // now that we found it, remove it
926
+ try {
927
+ const dropRes = await res.mongoClient.db(res.database).collection(collectionName).drop({});
928
+ log.spam(`${origin}: db response ${dropRes}`);
929
+ log.debug(`${origin}: database collection ${collectionName} removed`);
930
+ if (useCallback) return callback(null, collectionName);
931
+ return collectionName;
932
+ } catch (err) {
933
+ const msg = `${origin}: Failed to remove collection ${err}`;
934
+ if (useCallback) return callback(msg, null);
935
+ throw msg;
936
+ }
937
+ }
938
+
939
+ log.warn(`${origin}: Unexpected end of function reached`);
940
+ }
941
+
942
+ /**
943
+ * @callback createCallback
944
+ * @param {string} err - the error message
945
+ * @param {string} data - the modification made
946
+ */
947
+ /**
948
+ * Call to create an item in the database
949
+ *
950
+ * @function create
951
+ * @param {string} collectionName - the collection to save the item in. (required)
952
+ * @param {string} data - the modification to make. (required)
953
+ * @param {DBInfo} dbInfo - the url for the database to connect to (dburl, dboptions, database)
954
+ * @param {boolean} fsWrite - turn on write to the file system
955
+ * @param {createCallback | undefined} callback - the callback to invoke. If undefined, the function acts like a promise. If defined, the function returns void.
956
+ *
957
+ * @returns {Promise<string>} - the modification made
958
+ * @throws {string} - the error message
959
+ */
960
+ async create(collectionName, data, dbInfo, fsWrite, callback = undefined) {
961
+ const origin = `${this.myid}-dbUtil-create`;
962
+ const useCallback = typeof callback === 'function';
963
+ log.trace(origin);
964
+
965
+ // verify the required data has been provided
966
+ if (!collectionName) {
967
+ const msg = `${origin}: Missing Collection Name`;
968
+ log.warn(msg);
969
+ if (useCallback) return callback(msg, null);
970
+ throw msg;
971
+ } else if (!data) {
972
+ const msg = `${origin}: Missing data to add`;
973
+ log.warn(msg);
974
+ if (useCallback) return callback(msg, null);
975
+ throw msg;
976
+ } else if ((data.entity && !data.action) || (!data.entity && data.action)) {
977
+ const msg = `${origin}: Inconsistent entity/action set`;
978
+ log.warn(msg);
979
+ if (useCallback) return callback(msg, null);
980
+ throw msg;
981
+ }
982
+
983
+ // create the unique identifier (should we let mongo do this?)
984
+ const dataInfo = data;
985
+ if (!{}.hasOwnProperty.call(dataInfo, '_id')) {
986
+ dataInfo._id = uuid.v4();
987
+ }
988
+
989
+ let res;
990
+ try {
991
+ res = await this.determineStorage(dbInfo);
992
+ } catch (err) {
993
+ const msg = `${origin}: ${err}`;
994
+ if (useCallback) return callback(msg, null);
995
+ throw msg;
996
+ }
997
+
998
+ // if using file storage
999
+ if (res.storageType === Storage.FILESYSTEM) {
1000
+ // save it to file in the adapter storage directory
1001
+ const saved = saveAsJson(collectionName, data);
1002
+ if (!saved) {
1003
+ const msg = `${origin}: Data has not been saved to file storage`;
1004
+ log.warn(msg);
1005
+ if (useCallback) return callback(msg, null);
1006
+ throw msg;
1007
+ }
1008
+ log.debug(`${origin}: Data saved in file storage`);
1009
+ if (useCallback) return callback(null, saved);
1010
+ return saved;
1011
+ }
1012
+
1013
+ // if using MongoDB storage
1014
+ if (res.storageType === Storage.DBINFO || res.storageType === Storage.ADAPTERDB) {
1015
+ // Add the data to the database
1016
+ // insertOne has only 2 parameters: the data to be added & callback. Not an identifier.
1017
+ try {
1018
+ const result = await res.mongoClient.db(res.database).collection(collectionName).insertOne(dataInfo);
1019
+ if (res.storageType === Storage.DBINFO && res.mongoClient) res.mongoClient.close();
1020
+ log.spam(`${origin}: db response ${result}`);
1021
+ log.debug(`${origin}: Data saved in database`);
1022
+ if (useCallback) return callback(null, data);
1023
+ return data;
1024
+ } catch (err) {
1025
+ if (res.storageType === Storage.DBINFO && res.mongoClient) res.mongoClient.close();
1026
+ const msg = `${origin}: Failed to insert data in collection ${err}`;
1027
+ log.warn(msg);
1028
+ if (useCallback) return callback(msg, null);
1029
+ throw msg;
1030
+ }
1031
+ }
1032
+
1033
+ log.warn(`${origin}: Unexpected end of function reached`);
1034
+ }
1035
+
1036
+ /**
1037
+ * Call to create an index in the database
1038
+ *
1039
+ * @function createIndex
1040
+ * @param {string} collectionName - the collection to index. (required)
1041
+ * @param {string} fieldOrSpec - what to index. (required)
1042
+ * @param {CreateIndexesOptions | undefined} options - the creation options
1043
+ * @param {DBInfo} dbInfo - the url for the database to connect to (dburl, dboptions, database)
1044
+ * @param {createCallback | undefined} callback - the callback to invoke. If undefined, the function acts like a promise. If defined, the function returns void.
1045
+ *
1046
+ * @returns {Promise<string>} - the created index
1047
+ * @throws {string} - the error message
1048
+ */
1049
+ async createIndex(collectionName, fieldOrSpec, options, dbInfo, callback = undefined) {
1050
+ const origin = `${this.myid}-dbUtil-createIndex`;
1051
+ const useCallback = typeof callback === 'function';
1052
+ log.trace(origin);
1053
+
1054
+ // verify the required data has been provided
1055
+ if (!collectionName) {
1056
+ const msg = `${origin}: Missing Collection Name`;
1057
+ log.warn(msg);
1058
+ if (useCallback) return callback(msg, null);
1059
+ throw msg;
1060
+ }
1061
+ if (!fieldOrSpec) {
1062
+ const msg = `${origin}: Missing Specs`;
1063
+ log.warn(msg);
1064
+ if (useCallback) return callback(msg, null);
1065
+ throw msg;
1066
+ }
1067
+
1068
+ let res;
1069
+ try {
1070
+ res = await this.determineStorage(dbInfo);
1071
+ } catch (err) {
1072
+ const msg = `${origin}: ${err}`;
1073
+ if (useCallback) callback(msg, null);
1074
+ throw msg;
1075
+ }
1076
+
1077
+ // if using file storage
1078
+ if (res.storageType === Storage.FILESYSTEM) {
1079
+ // no database - no index
1080
+ const msg = `${origin}: No database - no index`;
1081
+ log.warn(msg);
1082
+ if (useCallback) return callback(msg, null);
1083
+ throw msg;
1084
+ }
1085
+
1086
+ // if using MongoDB storage
1087
+ if (res.storageType === Storage.DBINFO || res.storageType === Storage.ADAPTERDB) {
1088
+ // create the index on the collection
1089
+ let indexRes;
1090
+ try {
1091
+ indexRes = await res.mongoClient.db(res.database).collection(collectionName).createIndex(fieldOrSpec, options || {});
1092
+ if (res.storageType === Storage.DBINFO && res.mongoClient) res.mongoClient.close();
1093
+ log.debug(`${origin}: Data in collection indexed`);
1094
+ if (useCallback) return callback(null, indexRes);
1095
+ return indexRes;
1096
+ } catch (err) {
1097
+ if (res.storageType === Storage.DBINFO && res.mongoClient) res.mongoClient.close();
1098
+ const msg = `${origin}: Failed to index data in collection ${err}`;
1099
+ log.warn(msg);
1100
+ if (useCallback) return callback(msg, null);
1101
+ throw msg;
1102
+ }
1103
+ }
1104
+
1105
+ log.warn(`${origin}: Unexpected end of function reached`);
1106
+ }
1107
+
1108
+ /**
1109
+ * @callback countCallback
1110
+ * @param {string} err - the error message
1111
+ * @param {int} data - the count
1112
+ */
1113
+ /**
1114
+ * Call to count the documents in a collection
1115
+ *
1116
+ * @function countDocuments
1117
+ * @param {string} collectionName - the collection to count documents in. (required)
1118
+ * @param {object} query - the query to minimize documents you count. (required)
1119
+ * @param {CountDocumentsOptions | undefined} options
1120
+ * @param {DBInfo} dbInfo - the url for the database to connect to (dburl, dboptions, database)
1121
+ * @param {boolean} fsWrite - turn on write to the file system
1122
+ * @param {countCallback | undefined} callback - the callback to invoke. If undefined, the function acts like a promise. If defined, the function returns void.
1123
+ *
1124
+ * @returns {Promise<number>} - the count of the documents
1125
+ * @throws {string} - the error message
1126
+ */
1127
+ async countDocuments(collectionName, query, options, dbInfo, fsWrite, callback = undefined) {
1128
+ const origin = `${this.myid}-dbUtil-countDocuments`;
1129
+ const useCallback = typeof callback === 'function';
1130
+ log.trace(origin);
1131
+
1132
+ // verify the required data has been provided
1133
+ if (!collectionName) {
1134
+ const msg = `${origin}: Missing Collection Name`;
1135
+ log.warn(msg);
1136
+ if (useCallback) return callback(msg, null);
1137
+ throw msg;
1138
+ }
1139
+
1140
+ let res;
1141
+ try {
1142
+ res = await this.determineStorage(dbInfo);
1143
+ } catch (err) {
1144
+ const msg = `${origin}: ${err}`;
1145
+ if (useCallback) callback(msg, null);
1146
+ throw msg;
1147
+ }
1148
+
1149
+ // if using file storage
1150
+ if (res.storageType === Storage.FILESYSTEM) {
1151
+ // get a count from the JSON
1152
+ const data = countJSON(collectionName, query);
1153
+ if (!data || data === -1) {
1154
+ const msg = `${origin}: Could not count data from file storage`;
1155
+ log.warn(msg);
1156
+ if (useCallback) return callback(msg, null);
1157
+ throw msg;
1158
+ }
1159
+ log.debug(`${origin}: Count from file storage ${data}`);
1160
+ if (useCallback) return callback(null, data);
1161
+ return data;
1162
+ }
1163
+
1164
+ // if using MongoDB storage
1165
+ if (res.storageType === Storage.DBINFO || res.storageType === Storage.ADAPTERDB) {
1166
+ // get the count from mongo
1167
+ let count;
1168
+ try {
1169
+ count = await res.mongoClient.db(res.database).collection(collectionName).countDocuments(query, options || {});
1170
+ if (res.storageType === Storage.DBINFO && res.mongoClient) res.mongoClient.close();
1171
+ log.debug(`${origin}: Count from database ${res}`);
1172
+ if (useCallback) return callback(null, count);
1173
+ return count;
1174
+ } catch (err) {
1175
+ if (res.storageType === Storage.DBINFO && res.mongoClient) res.mongoClient.close();
1176
+ const msg = `${origin}: Failed to count collection ${err}`;
1177
+ log.warn(msg);
1178
+ if (useCallback) return callback(msg, null);
1179
+ throw msg;
1180
+ }
1181
+ }
1182
+
1183
+ log.warn(`${origin}: Unexpected end of function reached`);
1184
+ }
1185
+
1186
+ /**
1187
+ * Delete items from a collection
1188
+ *
1189
+ * @function delete
1190
+ * @param {string} collectionName - the collection to remove document from. (required)
1191
+ * @param {object} filter - the filter for the document(s) to remove. (required)
1192
+ * @param {DeleteOptions} options
1193
+ * @param {boolean} multiple
1194
+ * @param {DBInfo} dbInfo - the url for the database to connect to (dburl, dboptions, database)
1195
+ * @param {boolean} fsWrite - turn on write to the file system
1196
+ * @param {createCallback | undefined} callback - the callback to invoke. If undefined, the function acts like a promise. If defined, the function returns void.
1197
+ *
1198
+ * @returns {Promise<DeleteResult>}
1199
+ * @throws {string} - the error message
1200
+ */
1201
+ async delete(collectionName, filter, options, multiple, dbInfo, fsWrite, callback = undefined) {
1202
+ const origin = `${this.myid}-dbUtil-delete`;
1203
+ const useCallback = typeof callback === 'function';
1204
+ log.trace(origin);
1205
+
1206
+ // verify the required data has been provided
1207
+ if (!collectionName) {
1208
+ const msg = `${origin}: Missing Collection Name`;
1209
+ log.warn(msg);
1210
+ if (useCallback) return callback(msg, null);
1211
+ throw msg;
1212
+ }
1213
+ if (!filter) {
1214
+ const msg = `${origin}: Missing Filter`;
1215
+ log.warn(msg);
1216
+ if (useCallback) return callback(msg, null);
1217
+ throw msg;
1218
+ }
1219
+ if (multiple === undefined || multiple === null) {
1220
+ const msg = `${origin}: Missing Multiple flag`;
1221
+ log.warn(msg);
1222
+ if (useCallback) return callback(msg, null);
1223
+ throw msg;
1224
+ }
1225
+
1226
+ let res;
1227
+ try {
1228
+ res = await this.determineStorage(dbInfo);
1229
+ } catch (err) {
1230
+ const msg = `${origin}: ${err}`;
1231
+ if (useCallback) callback(msg, null);
1232
+ throw msg;
1233
+ }
1234
+
1235
+ // if using file storage
1236
+ if (res.storageType === Storage.FILESYSTEM) {
1237
+ // verify the collection exists
1238
+ if (!fs.existsSync(`${adapterDir}/storage/${collectionName}.json`)) {
1239
+ const msg = `${origin}: Collection ${collectionName} does not exist`;
1240
+ log.warn(msg);
1241
+ if (useCallback) return callback(msg, null);
1242
+ throw msg;
1243
+ }
1244
+ // remove the item from the collection
1245
+ const deld = removeFromJSON(collectionName, filter, multiple);
1246
+ if (!deld) {
1247
+ const msg = `${origin}: Data has not been deleted from file storage`;
1248
+ log.warn(msg);
1249
+ if (useCallback) return callback(msg, null);
1250
+ throw msg;
1251
+ }
1252
+ log.debug(`${origin}: Data has been deleted from file storage`);
1253
+ if (useCallback) return callback(null, deld);
1254
+ return deld;
1255
+ }
1256
+
1257
+ // if using MongoDB storage
1258
+ if (res.storageType === Storage.DBINFO || res.storageType === Storage.ADAPTERDB) {
1259
+ // Calls either 'collection.deleteOne' or 'collection.deleteMany'. Both methods take the same parameters
1260
+ const methodName = multiple ? 'deleteMany' : 'deleteOne';
1261
+ let delRes;
1262
+ try {
1263
+ delRes = await res.mongoClient.db(res.database).collection(collectionName)[methodName](filter, options);
1264
+ if (res.storageType === Storage.DBINFO && res.mongoClient) res.mongoClient.close();
1265
+ log.debug(`${origin}: Data has been deleted from database`);
1266
+ if (useCallback) return callback(null, delRes);
1267
+ return delRes;
1268
+ } catch (err) {
1269
+ if (res.storageType === Storage.DBINFO && res.mongoClient) res.mongoClient.close();
1270
+ const msg = `${origin}: Failed to delete data from database ${err}`;
1271
+ log.warn(msg);
1272
+ if (useCallback) return callback(msg, null);
1273
+ throw msg;
1274
+ }
1275
+ }
1276
+
1277
+ log.warn(`${origin}: Unexpected end of function reached`);
1278
+ }
1279
+
1280
+ /**
1281
+ * @callback replaceCallback
1282
+ * @param {string} err - the error message
1283
+ * @param {Document | UpdateResult<Document>} data - the update result
1284
+ */
1285
+
1286
+ /**
1287
+ * Replace an item in a collection
1288
+ *
1289
+ * @function replaceOne
1290
+ * @param {string} collectionName - the collection to replace document in. (required)
1291
+ * @param {object} filter - the filter for the document to replace. (required)
1292
+ * @param {object} doc - the document to insert. (required)
1293
+ * @param {ReplaceOptions} options
1294
+ * @param {DBInfo} dbInfo - the url for the database to connect to (dburl, dboptions, database)
1295
+ * @param {boolean} fsWrite - turn on write to the file system
1296
+ * @param {replaceCallback | undefined} callback - the callback to invoke. If undefined, the function acts like a promise. If defined, the function returns void.
1297
+ *
1298
+ * @returns {Promise<Document | UpdateResult<Document>>}
1299
+ * @throws {string} - the error message
1300
+ */
1301
+ async replaceOne(collectionName, filter, doc, options, dbInfo, fsWrite, callback = undefined) {
1302
+ const origin = `${this.myid}-dbUtil-replaceOne`;
1303
+ const useCallback = typeof callback === 'function';
1304
+ log.trace(origin);
1305
+
1306
+ // verify the required data has been provided
1307
+ if (!collectionName) {
1308
+ const msg = `${origin}: Missing Collection Name`;
1309
+ log.warn(msg);
1310
+ if (useCallback) return callback(msg, null);
1311
+ throw msg;
1312
+ }
1313
+ if (!filter) {
1314
+ const msg = `${origin}: Missing Filter`;
1315
+ log.warn(msg);
1316
+ if (useCallback) return callback(msg, null);
1317
+ throw msg;
1318
+ }
1319
+ if (!doc) {
1320
+ const msg = `${origin}: Missing Document`;
1321
+ log.warn(msg);
1322
+ if (useCallback) return callback(msg, null);
1323
+ throw msg;
1324
+ }
1325
+
1326
+ let res;
1327
+ try {
1328
+ res = await this.determineStorage(dbInfo);
1329
+ } catch (err) {
1330
+ const msg = `${origin}: ${err}`;
1331
+ if (useCallback) callback(msg, null);
1332
+ throw msg;
1333
+ }
1334
+
1335
+ // if using file storage
1336
+ if (res.storageType === Storage.FILESYSTEM) {
1337
+ // remove the data from the collection
1338
+ const rem = removeFromJSON(collectionName, filter, false);
1339
+ if (rem) {
1340
+ // add the data into the collection
1341
+ const sav = saveAsJson(collectionName, doc);
1342
+ if (sav) {
1343
+ log.debug(`${origin}: Data replaced in file storage`);
1344
+ if (useCallback) return callback(null, sav);
1345
+ return sav;
1346
+ }
1347
+ const msg = `${origin}: Could not save doc into file storage`;
1348
+ log.warn(msg);
1349
+ if (useCallback) return callback(msg, null);
1350
+ throw msg;
1351
+ }
1352
+ const msg = `${origin}: Could not delete from file storage`;
1353
+ log.warn(msg);
1354
+ if (useCallback) return callback(msg, null);
1355
+ throw msg;
1356
+ }
1357
+
1358
+ // if using MongoDB storage
1359
+ if (res.storageType === Storage.DBINFO || res.storageType === Storage.ADAPTERDB) {
1360
+ // replace an items in mongo
1361
+ let replaceRes;
1362
+ try {
1363
+ replaceRes = await res.mongoClient.db(res.database).collection(collectionName).replaceOne(filter, doc, options);
1364
+ if (res.storageType === Storage.DBINFO && res.mongoClient) res.mongoClient.close();
1365
+ log.debug(`${origin}: Data replaced in file storage`);
1366
+ if (useCallback) return callback(null, replaceRes);
1367
+ return replaceRes;
1368
+ } catch (err) {
1369
+ if (res.storageType === Storage.DBINFO && res.mongoClient) res.mongoClient.close();
1370
+ const msg = `${origin}: Failed to replace data in database ${err}`;
1371
+ log.warn(msg);
1372
+ if (useCallback) return callback(msg, null);
1373
+ throw msg;
1374
+ }
1375
+ }
1376
+
1377
+ log.warn(`${origin}: Unexpected end of function reached`);
1378
+ }
1379
+
1380
+ /**
1381
+ * @callback findCallback
1382
+ * @param {string} err - the error message
1383
+ * @param {Document[]} data - the found data
1384
+ */
1385
+
1386
+ /**
1387
+ * @typedef FindOptsParam
1388
+ * @property {Filter<Document>} filter
1389
+ * @property {Sort} sort
1390
+ * @property {number} start - start position
1391
+ * @property {number} limit - max number of results
1392
+ */
1393
+
1394
+ /**
1395
+ * Call to find items in the database
1396
+ *
1397
+ * @function find
1398
+ * @param {string} collectionName - the collection name to search in. (required)
1399
+ * @param {FindOptsParam} options - the options to use to find data.
1400
+ * @param {DBInfo} dbInfo - the url for the database to connect to (dburl, dboptions, database)
1401
+ * @param {boolean} fsWrite - turn on write to the file system
1402
+ * @param {findCallback | undefined} callback - the callback to invoke. If undefined, the function acts like a promise. If defined, the function returns void.
1403
+ *
1404
+ * @returns {Promise<Document[]>}
1405
+ * @throws {string} - the error message
1406
+ */
1407
+ async find(collectionName, options, dbInfo, fsWrite, callback = undefined) {
1408
+ const origin = `${this.myid}-dbUtil-find`;
1409
+ const useCallback = typeof callback === 'function';
1410
+ log.trace(origin);
1411
+
1412
+ // verify the required data has been provided
1413
+ if (!collectionName) {
1414
+ const msg = `${origin}: Missing Collection Name`;
1415
+ log.warn(msg);
1416
+ if (useCallback) return callback(msg, null);
1417
+ throw msg;
1418
+ }
1419
+
1420
+ // get the collection so we can run the remove on the collection
1421
+ let filter = {};
1422
+ let start = 0;
1423
+ let sort = {};
1424
+ let limit = 10;
1425
+ if (options) {
1426
+ filter = options.filter || {};
1427
+ start = options.start || 0;
1428
+ sort = options.sort || {};
1429
+ if (Object.hasOwnProperty.call(options, 'limit')) {
1430
+ ({ limit } = options);
1431
+ }
1432
+ }
1433
+
1434
+ // If limit is not specified, default to 10.
1435
+ // Note: limit may be 0, which is equivalent to setting no limit.
1436
+
1437
+ // Replace filter with regex to allow for substring lookup
1438
+ // TODO: Need to create a new filter object instead of mutating the exsisting one
1439
+ const filterKeys = Object.keys(filter).filter((key) => (key[0] !== '$' && typeof filter[key] === 'string'));
1440
+ filterKeys.map((key) => {
1441
+ try {
1442
+ const escapedFilter = filter[key].replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
1443
+ const regexedFilter = new RegExp(`.*${escapedFilter}.*`, 'i');
1444
+ filter[key] = {
1445
+ $regex: regexedFilter
1446
+ };
1447
+ } catch (e) {
1448
+ delete filter[key];
1449
+ }
1450
+ return key;
1451
+ });
1452
+
1453
+ let res;
1454
+ try {
1455
+ res = await this.determineStorage(dbInfo);
1456
+ } catch (err) {
1457
+ const msg = `${origin}: ${err}`;
1458
+ if (useCallback) callback(msg, null);
1459
+ throw msg;
1460
+ }
1461
+
1462
+ // if using file storage
1463
+ if (res.storageType === Storage.FILESYSTEM) {
1464
+ // Find it from file in the adapter
1465
+ let toReturn = getFromJson(collectionName, options);
1466
+ if (collectionName === 'adapter_configs') {
1467
+ log.debug(`${origin}: Data retrieved from file storage`);
1468
+ if (useCallback) return callback(null, [toReturn]);
1469
+ return [toReturn];
1470
+ }
1471
+ if (toReturn && toReturn.length > limit) {
1472
+ let curEnd = start + limit;
1473
+ if (curEnd < toReturn.length) {
1474
+ curEnd = toReturn.length;
1475
+ }
1476
+ toReturn = toReturn.slice(start, curEnd);
1477
+ }
1478
+ log.debug(`${origin}: Data retrieved from file storage`);
1479
+ if (useCallback) return callback(null, toReturn);
1480
+ return toReturn;
1481
+ }
1482
+
1483
+ // if using MongoDB storage
1484
+ if (res.storageType === Storage.DBINFO || res.storageType === Storage.ADAPTERDB) {
1485
+ // Find the data in the database
1486
+ let findRes;
1487
+ try {
1488
+ findRes = await res.mongoClient.db(res.database).collection(collectionName).find(filter)
1489
+ .sort(sort)
1490
+ .skip(start)
1491
+ .limit(limit)
1492
+ .toArray();
1493
+ log.debug(`${origin}: Data retrieved from database`);
1494
+ if (useCallback) return callback(null, findRes);
1495
+ return findRes;
1496
+ } catch (err) {
1497
+ const msg = `${origin}: Data could not be retrieved from database`;
1498
+ log.warn(msg);
1499
+ if (useCallback) return callback(msg, null);
1500
+ throw msg;
1501
+ } finally {
1502
+ if (res.storageType === Storage.DBINFO && res.mongoClient) res.mongoClient.close();
1503
+ }
1504
+ }
1505
+
1506
+ log.warn(`${origin}: Unexpected end of function reached`);
1507
+ }
1508
+
1509
+ /**
1510
+ * @callback findAndModifyCallback
1511
+ * @param {string} err - the error message
1512
+ * @param {WithId<Document>} data - the response
1513
+ */
1514
+
1515
+ /**
1516
+ * Call to find an item in a collection and modify it.
1517
+ *
1518
+ * @function findAndModify
1519
+ * @param {string} collectionName - the collection to find things from. (required)
1520
+ * @param {Filter<Document>} filter - the filter used to find objects. (optional)
1521
+ * @param {Array} sort - how to sort the items (first one in order will be modified). (optional)
1522
+ * @param {UpdateFilter<Document>} data - the modification to make. (required)
1523
+ * @param {boolean} upsert - option for the whether to insert new objects. (optional)
1524
+ * @param {DBInfo} dbInfo - the url for the database to connect to (dburl, dboptions, database) (optional)
1525
+ * @param {boolean} fsWrite - turn on write to the file system (optional)
1526
+ * @param {findAndModifyCallback | undefined} callback - the callback to invoke. If undefined, the function acts like a promise. If defined, the function returns void.
1527
+ *
1528
+ * @return {Promise<WithId<Document>>}
1529
+ * @throws {string} - the error message
1530
+ */
1531
+ async findAndModify(collectionName, filter, sort, data, upsert, dbInfo, fsWrite, callback = undefined) {
1532
+ const origin = `${this.myid}-dbUtil-findAndModify`;
1533
+ const useCallback = typeof callback === 'function';
1534
+ log.trace(origin);
1535
+
1536
+ // verify the required data has been provided
1537
+ if (collectionName === undefined || collectionName === null || collectionName === '') {
1538
+ const msg = `${origin}: Missing Collection Name`;
1539
+ log.warn(msg);
1540
+ if (useCallback) return callback(msg, null);
1541
+ throw msg;
1542
+ }
1543
+ if (data === undefined || data === null || typeof data !== 'object' || Object.keys(data).length === 0) {
1544
+ const msg = `${origin}: Missing data for modification`;
1545
+ log.warn(msg);
1546
+ if (useCallback) return callback(msg, null);
1547
+ throw msg;
1548
+ }
1549
+
1550
+ const options = {
1551
+ sort,
1552
+ upsert,
1553
+ returnOriginal: false
1554
+ };
1555
+
1556
+ let res;
1557
+ try {
1558
+ res = await this.determineStorage(dbInfo);
1559
+ } catch (err) {
1560
+ const msg = `${origin}: ${err}`;
1561
+ if (useCallback) callback(msg, null);
1562
+ throw msg;
1563
+ }
1564
+
1565
+ // if using file storage
1566
+ if (res.storageType === Storage.FILESYSTEM) {
1567
+ // save it to file in the adapter storage directory
1568
+ const saved = saveAsJson(collectionName, data);
1569
+ if (!saved) {
1570
+ const msg = `${origin}: Data has not been saved`;
1571
+ log.error(msg);
1572
+ if (useCallback) return callback(msg, null);
1573
+ throw msg;
1574
+ }
1575
+ log.debug(`${origin}: Data modified in file storage`);
1576
+ if (useCallback) return callback(null, saved);
1577
+ return saved;
1578
+ }
1579
+
1580
+ // if using MongoDB storage
1581
+ if (res.storageType === Storage.DBINFO || res.storageType === Storage.ADAPTERDB) {
1582
+ // find and modify the data in the database
1583
+ let findRes;
1584
+ try {
1585
+ findRes = await res.mongoClient.db(res.database).collection(collectionName).findOneAndUpdate((filter || {}), data, options);
1586
+ if (findRes) log.debug(`${origin}: Data modified in database`);
1587
+ if (useCallback) return callback(null, findRes);
1588
+ return findRes;
1589
+ } catch (err) {
1590
+ const msg = `${origin}: Failed to modified data in database ${err}`;
1591
+ log.warn(msg);
1592
+ if (useCallback) return callback(msg, null);
1593
+ throw msg;
1594
+ } finally {
1595
+ if (res.storageType === Storage.DBINFO && res.mongoClient) res.mongoClient.close();
1596
+ }
1597
+ }
1598
+
1599
+ log.warn(`${origin}: Unexpected end of function reached`);
1600
+ }
1601
+ }
1602
+
1603
+ module.exports = DBUtil;