@itentialopensource/adapter-utils 4.44.9

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/lib/dbUtil.js ADDED
@@ -0,0 +1,1300 @@
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 { MongoClient } = require('mongodb');
12
+
13
+ // Other global variables
14
+ let adapterDir = '';
15
+ let saveFS = false;
16
+ let storDir = `${adapterDir}/storage`;
17
+ let id = null;
18
+
19
+ const Storage = {
20
+ UNDEFINED: 0,
21
+ DBINFO: 1,
22
+ ADAPTERDB: 2,
23
+ FILESYSTEM: 3,
24
+ IAPDB: 4
25
+ };
26
+ Object.freeze(Storage);
27
+
28
+ /* DB UTILS INTERNAL FUNCTIONS */
29
+ /** getFromJson
30
+ * @summary returns information from a json file on the file system
31
+ */
32
+ function getFromJson(fileName, filter) {
33
+ const origin = `${id}-dbUtil-getFromJson`;
34
+ log.trace(origin);
35
+
36
+ // verify the required data has been provided
37
+ if (fileName === undefined || fileName === null || fileName === '') {
38
+ log.warn(`${origin}: Must provide a file name`);
39
+ return null;
40
+ }
41
+
42
+ // get the entity and the action if this is a metric
43
+ let ent = null;
44
+ let act = null;
45
+ let metric = false;
46
+ const useFilter = filter;
47
+ if (useFilter && useFilter.metric) {
48
+ metric = true;
49
+ if (useFilter.metric.entity) {
50
+ ent = useFilter.metric.entity;
51
+ }
52
+ if (useFilter.metric.action) {
53
+ act = useFilter.metric.action;
54
+ }
55
+ if (!ent || !act) {
56
+ log.warn(`${origin}: metric provided with one of entity/action, require both`);
57
+ return null;
58
+ }
59
+ delete useFilter.metric;
60
+ }
61
+
62
+ try {
63
+ // make sure what we need exists and has been provided
64
+ if (!fs.existsSync(`${storDir}`)) {
65
+ log.warn(`${origin}: Could not find adapter storage directory - nothing to retrieve`);
66
+ return null;
67
+ }
68
+
69
+ // determine if the file exists so we retrieve the data
70
+ if (fs.existsSync(`${storDir}/${fileName}.json`)) {
71
+ const content = JSON.parse(fs.readFileSync(`${storDir}/${fileName}.json`, 'utf-8'));
72
+ const toReturn = [];
73
+
74
+ if (metric) {
75
+ // if metric need to match the entity and action
76
+ content.table.forEach((item) => {
77
+ if (ent === item.entity && act === item.action) {
78
+ toReturn.push(item);
79
+ }
80
+ });
81
+ } else {
82
+ // if not metric must match the items in the filter
83
+ const key = Object.keys(useFilter);
84
+ content.table.forEach((item) => {
85
+ let push = true;
86
+ key.forEach((fil) => {
87
+ if (useFilter[fil] !== item[fil]) push = false;
88
+ });
89
+ if (push) toReturn.push(item);
90
+ });
91
+ }
92
+ const filtered = content.table.filter((el) => {
93
+ Object.keys(useFilter).forEach((obj) => {
94
+ if (el[obj] !== useFilter[obj]) {
95
+ return false;
96
+ }
97
+ });
98
+ return true;
99
+ });
100
+ return toReturn;
101
+ }
102
+
103
+ // if no file, nothing to return
104
+ return null;
105
+ } catch (ex) {
106
+ log.warn(`${origin}: Caught Exception ${ex}`);
107
+ return null;
108
+ }
109
+ }
110
+
111
+ /** countJSON
112
+ * @summary returns a count from a json storage file
113
+ */
114
+ function countJSON(fileName, filter) {
115
+ const origin = `${id}-dbUtil-countJSON`;
116
+ log.trace(origin);
117
+
118
+ // verify the required data has been provided
119
+ if (fileName === undefined || fileName === null || fileName === '') {
120
+ log.warn(`${origin}: Must provide a file name`);
121
+ return null;
122
+ }
123
+
124
+ try {
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 count`);
128
+ return null;
129
+ }
130
+
131
+ // determine if the file exists so we can count data from it
132
+ if (fs.existsSync(`${storDir}/${fileName}.json`)) {
133
+ const data = getFromJson(fileName, filter);
134
+ if (data) {
135
+ return data.length;
136
+ }
137
+ return -1;
138
+ }
139
+ } catch (ex) {
140
+ log.warn(`${origin}: Caught Exception ${ex}`);
141
+ return null;
142
+ }
143
+ }
144
+
145
+ /** saveAsJson
146
+ * @summary saves information into a json storage file
147
+ */
148
+ function saveAsJson(fileName, data) {
149
+ const origin = `${id}-dbUtil-saveAsJson`;
150
+ log.trace(origin);
151
+
152
+ // verify the required data has been provided
153
+ if (fileName === undefined || fileName === null || fileName === '') {
154
+ log.warn(`${origin}: Must provide a file name`);
155
+ return null;
156
+ }
157
+
158
+ // get the entity and the action if this is a metric
159
+ let ent = null;
160
+ let act = null;
161
+ let metric = false;
162
+ const useData = data;
163
+ if (useData && useData.metric) {
164
+ metric = true;
165
+ if (useData.metric.entity) {
166
+ ent = useData.metric.entity;
167
+ }
168
+ if (useData.metric.action) {
169
+ act = useData.metric.action;
170
+ }
171
+ if (!ent || !act) {
172
+ log.warn(`${origin}: metric provided with one of entity/action, require both`);
173
+ return null;
174
+ }
175
+ delete useData.metric;
176
+ }
177
+
178
+ try {
179
+ // make sure what we need exists and has been provided
180
+ if (!fs.existsSync(`${storDir}`)) {
181
+ // need to make the storage directory if it does not exist
182
+ fs.mkdirSync(`${storDir}`);
183
+ }
184
+
185
+ // determine if the file exists so we add data to it
186
+ if (fs.existsSync(`${storDir}/${fileName}.json`)) {
187
+ // have to read, append & save
188
+ const content = JSON.parse(fs.readFileSync(`${storDir}/${fileName}.json`, 'utf-8'));
189
+ let exists = false;
190
+
191
+ content.table.forEach((item) => {
192
+ const toPush = item;
193
+ if (metric) {
194
+ // if it is a metric and we already have entity and action need to edit the data
195
+ if (ent === item.entity && act === item.action) {
196
+ exists = true;
197
+ Object.keys(useData).forEach((key) => {
198
+ if (key === '$inc') {
199
+ Object.keys(useData[key]).forEach((inc) => {
200
+ if (!toPush[inc]) {
201
+ toPush[inc] = useData[key][inc];
202
+ }
203
+ toPush[inc] += useData[key][inc];
204
+ });
205
+ } else if (key === '$set') {
206
+ Object.keys(useData[key]).forEach((set) => {
207
+ toPush[set] = useData[key][set];
208
+ });
209
+ }
210
+ });
211
+ }
212
+ }
213
+ });
214
+ if (!exists) {
215
+ // push new thing to table.
216
+ const toPush = {};
217
+ const keysArray = Object.keys(useData);
218
+ keysArray.forEach((i) => {
219
+ const newKeys = Object.keys(useData[i]);
220
+ newKeys.forEach((j) => {
221
+ toPush[j] = useData[i][j];
222
+ });
223
+ });
224
+ content.table.push(toPush);
225
+ }
226
+
227
+ // now that is updated, write the file back out
228
+ fs.writeFileSync(`${storDir}/${fileName}.json`, JSON.stringify(content, null, 2));
229
+ return useData;
230
+ }
231
+
232
+ // if the file has not been created yet
233
+ const obj = { table: [] };
234
+ const toPush = {};
235
+ const keysArray = Object.keys(data);
236
+ keysArray.forEach((outer) => {
237
+ const newKeys = Object.keys(data[outer]);
238
+ newKeys.forEach((inner) => {
239
+ toPush[inner] = data[outer][inner];
240
+ });
241
+ });
242
+ obj.table.push(toPush);
243
+
244
+ // write the file out to the file system
245
+ fs.writeFileSync(`${storDir}/${fileName}.json`, JSON.stringify(obj, null, 2));
246
+ return data;
247
+ } catch (ex) {
248
+ log.warn(`${origin}: Caught Exception ${ex}`);
249
+ return null;
250
+ }
251
+ }
252
+
253
+ /** removeFromJSON
254
+ * @summary removes information from a json storage file
255
+ */
256
+ function removeFromJSON(fileName, filter, multiple) {
257
+ const origin = `${id}-dbUtil-removeFromJSON`;
258
+ log.trace(origin);
259
+
260
+ // verify the required data has been provided
261
+ if (fileName === undefined || fileName === null || fileName === '') {
262
+ log.warn(`${origin}: Must provide a file name`);
263
+ return null;
264
+ }
265
+
266
+ // get the entity and the action if this is a metric
267
+ let ent = null;
268
+ let act = null;
269
+ let metric = false;
270
+ const useFilter = filter;
271
+ if (useFilter && useFilter.metric) {
272
+ metric = true;
273
+ if (useFilter.metric.entity) {
274
+ ent = useFilter.metric.entity;
275
+ }
276
+ if (useFilter.metric.action) {
277
+ act = useFilter.metric.action;
278
+ }
279
+ if (!ent && !act) {
280
+ log.warn(`${origin}: metric provided with one of entity/action, require both`);
281
+ return null;
282
+ }
283
+ delete useFilter.metric;
284
+ }
285
+
286
+ try {
287
+ // make sure what we need exists and has been provided
288
+ if (!fs.existsSync(`${storDir}`)) {
289
+ log.warn(`${origin}: Could not find adapter storage directory - nothing to remove`);
290
+ return null;
291
+ }
292
+
293
+ // determine if the file exists so we remove data from it
294
+ if (fs.existsSync(`${storDir}/${fileName}.json`)) {
295
+ const content = JSON.parse(fs.readFileSync(`${storDir}/${fileName}.json`, 'utf-8'));
296
+ const toReturn = [];
297
+
298
+ // if this is a metric make sure the entity and action match
299
+ if (metric) {
300
+ content.table = content.table.filter((item) => {
301
+ if (ent === item.entity && act === item.action) {
302
+ toReturn.push(item);
303
+ return false;
304
+ }
305
+ return true;
306
+ });
307
+ } else {
308
+ // go through content to determine if item matches the filter
309
+ let ctr = 0;
310
+ // create the contents that are being removed
311
+ const removed = content.table.filter((el) => {
312
+ Object.keys(useFilter).forEach((obj) => {
313
+ if (el[obj] !== useFilter[obj]) {
314
+ return false;
315
+ }
316
+ });
317
+ ctr += 1;
318
+ if (!multiple && ctr > 1) {
319
+ return false;
320
+ }
321
+ return true;
322
+ });
323
+ let ctr1 = 0;
324
+ // remove the items from the contents
325
+ content.table = content.table.filter((el, i) => {
326
+ Object.keys(useFilter).forEach((obj) => {
327
+ if (el[obj] !== useFilter[obj]) {
328
+ return true;
329
+ }
330
+ });
331
+ ctr1 += 1;
332
+ if (!multiple && ctr1 > 1) {
333
+ return true;
334
+ }
335
+ return false;
336
+ });
337
+
338
+ // write the contents back to the file
339
+ fs.writeFileSync(`${storDir}/${fileName}.json`, JSON.stringify(content, null, 2));
340
+ return removed;
341
+ }
342
+
343
+ // write the contents back to the file
344
+ fs.writeFileSync(`${storDir}/${fileName}.json`, JSON.stringify(content, null, 2));
345
+ return toReturn;
346
+ }
347
+
348
+ log.error(`${origin}: Collection ${fileName} does not exist`);
349
+ return null;
350
+ } catch (ex) {
351
+ log.warn(`${origin}: Caught Exception ${ex}`);
352
+ return null;
353
+ }
354
+ }
355
+
356
+ /** deleteJSON
357
+ * @summary deletes a json storage file
358
+ */
359
+ function deleteJSON(fileName) {
360
+ const origin = `${id}-dbUtil-deleteJSON`;
361
+ log.trace(origin);
362
+
363
+ // verify the required data has been provided
364
+ if (fileName === undefined || fileName === null || fileName === '') {
365
+ log.warn(`${origin}: Must provide a file name`);
366
+ return null;
367
+ }
368
+
369
+ try {
370
+ // make sure what we need exists and has been provided
371
+ if (!fs.existsSync(`${storDir}`)) {
372
+ log.warn(`${origin}: Could not find adapter storage directory - nothing to delete`);
373
+ return null;
374
+ }
375
+
376
+ // determine if the file exists so we remove - also assume we add a .json suffix
377
+ if (fs.existsSync(`${storDir}/${fileName}.json`)) {
378
+ fs.remove(`${storDir}/${fileName}.json`).catch((some) => {
379
+ log.info(`${origin}: ${some}`);
380
+ fs.rmdirSync(`${storDir}/${fileName}.json`);
381
+ });
382
+ }
383
+
384
+ // successful -- return the fileName
385
+ return fileName;
386
+ } catch (ex) {
387
+ log.warn(`${origin}: Caught Exception ${ex}`);
388
+ return null;
389
+ }
390
+ }
391
+
392
+ class DBUtil {
393
+ /**
394
+ * These are database utilities that can be used by the adapter to interact with a
395
+ * mongo database
396
+ * @constructor
397
+ */
398
+ constructor(prongId, properties, directory) {
399
+ this.myid = prongId;
400
+ id = prongId;
401
+ this.baseDir = directory;
402
+ adapterDir = this.baseDir;
403
+ storDir = `${adapterDir}/storage`;
404
+ this.props = properties;
405
+ this.adapterMongoClient = null;
406
+
407
+ // set up the properties I care about
408
+ this.refreshProperties(properties);
409
+ }
410
+
411
+ /**
412
+ * refreshProperties is used to set up all of the properties for the db utils.
413
+ * It allows properties to be changed later by simply calling refreshProperties rather
414
+ * than having to restart the db utils.
415
+ *
416
+ * @function refreshProperties
417
+ * @param {Object} properties - an object containing all of the properties
418
+ */
419
+ refreshProperties(properties) {
420
+ const origin = `${this.myid}-dbUtil-refreshProperties`;
421
+ log.trace(origin);
422
+ this.dburl = null;
423
+ this.dboptions = {};
424
+ this.database = this.myid;
425
+
426
+ // verify the necessary information was received
427
+ if (!properties) {
428
+ log.error(`${origin}: DB Utils received no properties!`);
429
+ return;
430
+ }
431
+ if (!properties.mongo || !properties.mongo.host) {
432
+ log.info(`${origin}: No default adapter database configured!`);
433
+ return;
434
+ }
435
+
436
+ // set the database port
437
+ let port = 27017;
438
+ if (properties.mongo.port) {
439
+ port = properties.mongo.port;
440
+ }
441
+ // set the database
442
+ if (properties.mongo.database) {
443
+ this.database = properties.mongo.database;
444
+ }
445
+ // set the user
446
+ let username = null;
447
+ if (properties.mongo.username) {
448
+ username = properties.mongo.username;
449
+ }
450
+ // set the password
451
+ let password = null;
452
+ if (properties.mongo.password) {
453
+ password = properties.mongo.password;
454
+ }
455
+
456
+ // format the database url
457
+ this.dburl = 'mongodb://';
458
+ log.info(`${origin}: Default adapter database at: ${properties.mongo.host}`);
459
+
460
+ if (username) {
461
+ this.dburl += `${encodeURIComponent(username)}:${encodeURIComponent(password)}@`;
462
+ log.info(`${origin}: Default adapter database will use authentication.`);
463
+ }
464
+ this.dburl += `${encodeURIComponent(properties.mongo.host)}:${encodeURIComponent(port)}/${encodeURIComponent(this.database)}`;
465
+
466
+ // are we using a replication set need to add it to the url
467
+ if (properties.mongo.replSet) {
468
+ this.dburl += `?${properties.mongo.replSet}`;
469
+ log.info(`${origin}: Default adapter database will use replica set.`);
470
+ }
471
+
472
+ // Do we need SSL to connect to the database
473
+ if (properties.mongo.db_ssl && properties.mongo.db_ssl.enabled === true) {
474
+ log.info(`${origin}: Default adapter database will use SSL.`);
475
+ this.dboptions.ssl = true;
476
+
477
+ // validate the server's certificate against a known certificate authority?
478
+ if (properties.mongo.db_ssl.accept_invalid_cert === false) {
479
+ this.dboptions.sslValidate = true;
480
+ log.info(`${origin}: Default adapter database will use Certificate based SSL.`);
481
+ // if validation is enabled, we need to read the CA file
482
+ if (properties.mongo.db_ssl.ca_file) {
483
+ try {
484
+ this.dboptions.sslCA = [fs.readFileSync(properties.mongo.db_ssl.ca_file)];
485
+ } catch (err) {
486
+ log.error(`${origin}: Error: unable to load Mongo CA file: ${err}`);
487
+ }
488
+ } else {
489
+ log.error(`${origin}: Error: Certificate validation is enabled but a CA is not specified.`);
490
+ }
491
+ if (properties.mongo.db_ssl.key_file) {
492
+ try {
493
+ this.dboptions.sslKey = [fs.readFileSync(properties.mongo.db_ssl.key_file)];
494
+ } catch (err) {
495
+ log.error(`${origin}: Error: Unable to load Mongo Key file: ${err}`);
496
+ }
497
+ }
498
+ if (properties.mongo.db_ssl.cert_file) {
499
+ try {
500
+ this.dboptions.sslCert = [fs.readFileSync(properties.mongo.db_ssl.cert_file)];
501
+ } catch (err) {
502
+ log.error(`${origin}: Error: Unable to load Mongo Certificate file: ${err}`);
503
+ }
504
+ }
505
+ } else {
506
+ this.dboptions.sslValidate = false;
507
+ log.info(`${origin}: Default adapter database not using Certificate based SSL.`);
508
+ }
509
+ } else {
510
+ log.info(`${origin}: Default adapter database not using SSL.`);
511
+ }
512
+
513
+ // if we were provided with a path to save metrics - instead of just a boolean
514
+ // will assume this is a place where all adapter stuff can go!
515
+ saveFS = this.props.save_metric || false;
516
+ if (saveFS && typeof saveFS === 'string' && saveFS !== '') {
517
+ storDir = saveFS;
518
+ }
519
+ }
520
+
521
+ /**
522
+ * Call to determine the storage target. If the storage is a Mongo database, then
523
+ * the client connection and database is returned.
524
+ *
525
+ * @function determineStorage
526
+ * @param {object} dbInfo - the url for the database to connect to (dburl, dboptions, database)
527
+ * @param {callback} callback - the error, the storage, the Mongo client connection, the Mongo database
528
+ */
529
+ determineStorage(dbInfo, callback) {
530
+ const origin = `${this.myid}-dbUtil-determineStorage`;
531
+
532
+ if (dbInfo) {
533
+ // priority 1 - use the dbInfo passed in
534
+ if (dbInfo.dburl && dbInfo.database) {
535
+ MongoClient.connect(dbInfo.dburl, dbInfo.dboptions, (err, mongoClient) => {
536
+ if (err) {
537
+ log.error(`${origin}: Error! Failed to connect to database: ${err}`);
538
+ return callback(err, null, null, null);
539
+ }
540
+ log.debug('using dbinfo');
541
+ return callback(null, Storage.DBINFO, mongoClient, dbInfo.database);
542
+ });
543
+ } else {
544
+ const err = 'Error! Marlformed dbInfo';
545
+ return callback(err, null, null, Storage.DBINFO);
546
+ }
547
+ } else if (this.adapterMongoClient) {
548
+ // priority 2 - use the adapter database
549
+ log.debug('using adapter db');
550
+ return callback(null, Storage.ADAPTERDB, this.adapterMongoClient, this.database);
551
+ } else if (this.dburl === null && dbInfo === null) {
552
+ // priority 3 - use the filesystem
553
+ log.debug('using filesystem');
554
+ return callback(null, Storage.FILESYSTEM, null, null);
555
+ }
556
+ }
557
+
558
+ /**
559
+ * Call to connect and authenticate to the adapter database
560
+ *
561
+ * @function connect
562
+ */
563
+ connect(callback) {
564
+ // callback format: (this.alive, mongoClient); generally mongoClient should only be used if this.alive is true.
565
+ const origin = `${this.myid}-dbUtil-connect`;
566
+ log.trace(origin);
567
+
568
+ // const options = (replSetEnabled === true) ? { replSet: opts } : { server: opts };
569
+ log.debug(`${origin}: Connecting to MongoDB with options ${JSON.stringify(this.dboptions)}`);
570
+
571
+ // Now we will start the process of connecting to mongo db
572
+ return MongoClient.connect(this.dburl, this.dboptions, (err, mongoClient) => {
573
+ if (!mongoClient) {
574
+ log.error(`${origin}: Error! Exiting... Must start MongoDB first ${err}`);
575
+ this.alive = false;
576
+ return callback(this.alive);
577
+ }
578
+ mongoClient.on('close', () => {
579
+ this.alive = false;
580
+ log.error(`${origin}: MONGO CONNECTION LOST...`);
581
+ });
582
+
583
+ mongoClient.on('reconnect', () => {
584
+ // we still need to check if we are properly authenticated
585
+ // so we just list collections to test it.
586
+ this.clientDB.listCollections().toArray((error) => {
587
+ if (error) {
588
+ log.error(`${origin}: ${error}`);
589
+ this.alive = false;
590
+ } else {
591
+ log.info(`${origin}: MONGO CONNECTION BACK...`);
592
+ this.alive = true;
593
+ this.adapterMongoClient = mongoClient;
594
+ }
595
+ });
596
+ });
597
+
598
+ mongoClient.db(this.database).serverConfig.on('left', (type) => {
599
+ if (type === 'primary') {
600
+ this.alive = false;
601
+ log.info(`${origin}: MONGO PRIMARY CONNECTION LOST...`);
602
+ } else if (type === 'secondary') {
603
+ log.info(`${origin}: MONGO SECONDARY CONNECTION LOST...`);
604
+ }
605
+ });
606
+
607
+ mongoClient.db(this.database).serverConfig.on('joined', (type) => {
608
+ if (type === 'primary') {
609
+ log.info(`${origin}: MONGO PRIMARY CONNECTION BACK...`);
610
+ } else if (type === 'secondary') {
611
+ log.info(`${origin}: MONGO SECONDARY CONNECTION BACK...`);
612
+ }
613
+ });
614
+
615
+ log.info(`${origin}: mongo running @${this.dburl}/${this.database}`);
616
+ this.clientDB = mongoClient.db(this.database);
617
+ this.adapterMongoClient = mongoClient;
618
+
619
+ // we don't have authentication defined but we still need to check if Mongo does not
620
+ // require one, so we just list collections to test if it's doable.
621
+ return this.clientDB.listCollections().toArray((error) => {
622
+ if (error) {
623
+ log.error(`${origin}: ${error}`);
624
+ this.alive = false;
625
+ } else {
626
+ log.info(`${origin}: MongoDB connection has been established`);
627
+ this.alive = true;
628
+ this.adapterMongoClient = mongoClient;
629
+ }
630
+ return callback(this.alive, mongoClient);
631
+ });
632
+ });
633
+ }
634
+
635
+ /**
636
+ * Call to disconnect from the adapter database
637
+ *
638
+ * @function disconnect
639
+ */
640
+ disconnect() {
641
+ if (this.adapterMongoClient) {
642
+ this.adapterMongoClient.close();
643
+ }
644
+ }
645
+
646
+ /**
647
+ * createCollection creates the provided collection in the file system or database.
648
+ *
649
+ * @function createCollection
650
+ * @param {string} collectionName - the name of the collection to create
651
+ * @param {object} dbInfo - the url for the database to connect to (dburl, dboptions, database)
652
+ * @param {boolean} fsWrite - turn on write to the file system
653
+ */
654
+ createCollection(collectionName, dbInfo, fsWrite, callback) {
655
+ const origin = `${this.myid}-dbUtil-createCollection`;
656
+ log.trace(origin);
657
+
658
+ try {
659
+ // verify the required data has been provided
660
+ if (!collectionName || (typeof collectionName !== 'string')) {
661
+ log.warn(`${origin}: Missing Collection Name or not string`);
662
+ return callback(`${origin}: Missing Collection Name or not string`, null);
663
+ }
664
+
665
+ return this.determineStorage(dbInfo, (storageError, storage, mongoClient, database) => {
666
+ if (storageError) {
667
+ return callback(`${origin}: ${storageError}`, null);
668
+ }
669
+
670
+ // if using file storage
671
+ if (storage === Storage.FILESYSTEM) {
672
+ // if there is no adapter directory - can not do anything so error
673
+ if (!fs.existsSync(`${this.baseDir}`)) {
674
+ log.warn(`${origin}: Not able to create storage - missing base directory!`);
675
+ return callback(`${origin}: Not able to create storage - missing base directory!`, null);
676
+ }
677
+ // if there is no storage directory - create it
678
+ if (!fs.existsSync(`${this.baseDir}/storage`)) {
679
+ fs.mkdirSync(`${this.baseDir}/storage`);
680
+ }
681
+ // if the collection already exists - no need to create it
682
+ if (fs.existsSync(`${this.baseDir}/storage/${collectionName}`)) {
683
+ log.debug(`${origin}: storage file collection already exists`);
684
+ return callback(null, collectionName);
685
+ }
686
+ // create the new collection on the file system
687
+ fs.mkdirSync(`${this.baseDir}/storage/${collectionName}`);
688
+ log.debug(`${origin}: storage file collection ${collectionName} created`);
689
+ return callback(null, collectionName);
690
+ }
691
+
692
+ // if using MongoDB storage
693
+ if (storage === Storage.DBINFO || storage === Storage.ADAPTERDB) {
694
+ return mongoClient.db(database).createCollection(collectionName, (err, res) => {
695
+ if (storage === Storage.DBINFO && mongoClient) mongoClient.close();
696
+ if (err) {
697
+ // error we get back if the collection already existed - not a true error
698
+ if (err.codeName === 'NamespaceExists') {
699
+ log.debug(`${origin}: database collection already exists`);
700
+ return callback(null, collectionName);
701
+ }
702
+ log.warn(`${origin}: Error creating collection ${err}`);
703
+ return callback(`${origin}: Error creating collection ${err}`, null);
704
+ }
705
+ log.spam(`${origin}: db response ${res}`);
706
+ log.debug(`${origin}: database collection ${collectionName} created`);
707
+ return callback(null, collectionName);
708
+ });
709
+ }
710
+ });
711
+ } catch (ex) {
712
+ log.warn(`${origin}: Caught Exception - ${ex}`);
713
+ return callback(`${origin}: Caught Exception - ${ex}`, null);
714
+ }
715
+ }
716
+
717
+ /**
718
+ * removeCollection removes the provided collection from the file system or database.
719
+ *
720
+ * @function removeCollection
721
+ * @param {string} collectionName - the name of the collection to remove
722
+ * @param {object} dbInfo - the url for the database to connect to (dburl, dboptions, database)
723
+ * @param {boolean} fsWrite - turn on write to the file system
724
+ */
725
+ removeCollection(collectionName, dbInfo, fsWrite, callback) {
726
+ const origin = `${this.myid}-dbUtil-removeCollection`;
727
+ log.trace(origin);
728
+
729
+ try {
730
+ // verify the required data has been provided
731
+ if (!collectionName || (typeof collectionName !== 'string')) {
732
+ log.warn(`${origin}: Missing Collection Name or not string`);
733
+ return callback(`${origin}: Missing Collection Name or not string`, null);
734
+ }
735
+
736
+ return this.determineStorage(dbInfo, (storageError, storage, mongoClient, database) => {
737
+ if (storageError) {
738
+ return callback(`${origin}: ${storageError}`, null);
739
+ }
740
+
741
+ // if using file storage
742
+ if (storage === Storage.FILESYSTEM) {
743
+ const deld = deleteJSON(collectionName);
744
+ if (deld) {
745
+ log.debug(`${origin}: storage file collection ${collectionName} removed`);
746
+ return callback(null, deld);
747
+ }
748
+ log.debug(`${origin}: could not remove storage file collection`);
749
+ return callback(`${origin}: could not remove storage file collection`, null);
750
+ }
751
+
752
+ // if using MongoDB storage
753
+ if (storage === Storage.DBINFO || storage === Storage.ADAPTERDB) {
754
+ // get the list of collections from the database - can we just drop?
755
+ return mongoClient.db(database).listCollections().toArray((err, result) => {
756
+ if (err) {
757
+ log.warn(`${origin}: Failed to get collections ${err}`);
758
+ return callback(`${origin}: Failed to get collections ${err}`, null);
759
+ }
760
+ // go through the collections to get the correct one for removal
761
+ for (let e = 0; e < result.length; e += 1) {
762
+ if (result[e].name === collectionName) {
763
+ // now that we found it, remove it
764
+ return mongoClient.db(database).collection(collectionName).drop({}, (err1, res) => {
765
+ if (storage === Storage.DBINFO && mongoClient) mongoClient.close();
766
+ if (err1) {
767
+ log.warn(`${origin}: Failed to remove collection ${err1}`);
768
+ return callback(`${origin}: Failed to remove collection ${err1}`, null);
769
+ }
770
+ log.spam(`${origin}: db response ${res}`);
771
+ log.debug(`${origin}: database collection ${collectionName} removed`);
772
+ return callback(null, collectionName);
773
+ });
774
+ }
775
+ }
776
+ return callback(null, collectionName);
777
+ });
778
+ }
779
+ });
780
+ } catch (ex) {
781
+ log.warn(`${origin}: Caught Exception - ${ex}`);
782
+ return callback(`${origin}: Caught Exception - ${ex}`, null);
783
+ }
784
+ }
785
+
786
+ /**
787
+ * Call to create an item in the database
788
+ *
789
+ * @function create
790
+ * @param {string} collectionName - the collection to save the item in. (required)
791
+ * @param {string} data - the modification to make. (required)
792
+ * @param {object} dbInfo - the url for the database to connect to (dburl, dboptions, database)
793
+ * @param {boolean} fsWrite - turn on write to the file system
794
+ */
795
+ create(collectionName, data, dbInfo, fsWrite, callback) {
796
+ const origin = `${this.myid}-dbUtil-create`;
797
+ log.trace(origin);
798
+
799
+ try {
800
+ // verify the required data has been provided
801
+ if (!collectionName) {
802
+ log.warn(`${origin}: Missing Collection Name`);
803
+ return callback(`${origin}: Missing Collection Name`, null);
804
+ }
805
+ if (!data) {
806
+ log.warn(`${origin}: Missing data to add`);
807
+ return callback(`${origin}: Missing data to add`, null);
808
+ }
809
+ if ((data.entity && !data.action) || (!data.entity && data.action)) {
810
+ log.warn(`${origin}: Inconsistent entity/action set`);
811
+ return callback(`${origin}: Inconsistent entity/action set`, null);
812
+ }
813
+
814
+ // create the unique identifier (should we let mongo do this?)
815
+ const dataInfo = data;
816
+ if (!{}.hasOwnProperty.call(dataInfo, '_id')) {
817
+ dataInfo._id = uuid.v4();
818
+ }
819
+
820
+ return this.determineStorage(dbInfo, (storageError, storage, mongoClient, database) => {
821
+ if (storageError) {
822
+ return callback(`${origin}: ${storageError}`, null);
823
+ }
824
+
825
+ // if using file storage
826
+ if (storage === Storage.FILESYSTEM) {
827
+ // save it to file in the adapter storage directory
828
+ const saved = saveAsJson(collectionName, data);
829
+ if (!saved) {
830
+ log.warn(`${origin}: Data has not been saved to file storage`);
831
+ return callback(`${origin}: Data has not been saved to file storage`, null);
832
+ }
833
+ log.debug(`${origin}: Data saved in file storage`);
834
+ return callback(null, saved);
835
+ }
836
+
837
+ // if using MongoDB storage
838
+ if (storage === Storage.DBINFO || storage === Storage.ADAPTERDB) {
839
+ // Add the data to the database
840
+ // insertOne has only 2 parameters: the data to be added & callback. Not an identifier.
841
+ return mongoClient.db(database).collection(collectionName).insertOne(dataInfo, (err, result) => {
842
+ if (storage === Storage.DBINFO && mongoClient) mongoClient.close();
843
+ if (err) {
844
+ log.warn(`${origin}: Failed to insert data in collection ${err}`);
845
+ return callback(`${origin}: Failed to insert data in collection ${err}`, null);
846
+ }
847
+ log.spam(`${origin}: db response ${result}`);
848
+ log.debug(`${origin}: Data saved in database`);
849
+ return callback(null, data);
850
+ });
851
+ }
852
+ });
853
+ } catch (ex) {
854
+ log.warn(`${origin}: Caught Exception - ${ex}`);
855
+ return callback(`${origin}: Caught Exception - ${ex}`, null);
856
+ }
857
+ }
858
+
859
+ /**
860
+ * Call to create an index in the database
861
+ *
862
+ * @function createIndex
863
+ * @param {string} collectionName - the collection to index. (required)
864
+ * @param {string} fieldOrSpec - what to index. (required)
865
+ * @param {object} dbInfo - the url for the database to connect to (dburl, dboptions, database)
866
+ */
867
+ createIndex(collectionName, fieldOrSpec, options, dbInfo, callback) {
868
+ const origin = `${this.myid}-dbUtil-createIndex`;
869
+ log.trace(origin);
870
+
871
+ try {
872
+ // verify the required data has been provided
873
+ if (!collectionName) {
874
+ log.warn(`${origin}: Missing Collection Name`);
875
+ return callback(`${origin}: Missing Collection Name`, null);
876
+ }
877
+ if (!fieldOrSpec) {
878
+ log.warn(`${origin}: Missing Specs`);
879
+ return callback(`${origin}: Missing Specs`, null);
880
+ }
881
+
882
+ return this.determineStorage(dbInfo, (storageError, storage, mongoClient, database) => {
883
+ if (storageError) {
884
+ return callback(`${origin}: ${storageError}`, null);
885
+ }
886
+
887
+ // if using file storage
888
+ if (storage === Storage.FILESYSTEM) {
889
+ // no database - no index
890
+ log.warn(`${origin}: No database - no index`);
891
+ return callback(`${origin}: No database - no index`, null);
892
+ }
893
+
894
+ // if using MongoDB storage
895
+ if (storage === Storage.DBINFO || storage === Storage.ADAPTERDB) {
896
+ // create the index on the collection
897
+ return mongoClient.db(database).collection(collectionName).createIndex(fieldOrSpec, options || {}, (err, res) => {
898
+ if (storage === Storage.DBINFO && mongoClient) mongoClient.close();
899
+ if (err) {
900
+ log.warn(`${origin}: Failed to index data in collection ${err}`);
901
+ return callback(`${origin}: Failed to index data in collection ${err}`, null);
902
+ }
903
+ log.debug(`${origin}: Data in collection indexed`);
904
+ return callback(null, res);
905
+ });
906
+ }
907
+ });
908
+ } catch (ex) {
909
+ log.warn(`${origin}: Caught Exception - ${ex}`);
910
+ return callback(`${origin}: Caught Exception - ${ex}`, null);
911
+ }
912
+ }
913
+
914
+ /**
915
+ * Call to count the documents in a collection
916
+ *
917
+ * @function countDocuments
918
+ * @param {string} collectionName - the collection to count documents in. (required)
919
+ * @param {object} query - the query to minimize documents you count. (required)
920
+ * @param {object} dbInfo - the url for the database to connect to (dburl, dboptions, database)
921
+ * @param {boolean} fsWrite - turn on write to the file system
922
+ */
923
+ countDocuments(collectionName, query, options, dbInfo, fsWrite, callback) {
924
+ const origin = `${this.myid}-dbUtil-countDocuments`;
925
+ log.trace(origin);
926
+
927
+ try {
928
+ // verify the required data has been provided
929
+ if (!collectionName) {
930
+ log.warn(`${origin}: Missing Collection Name`);
931
+ return callback(`${origin}: Missing Collection Name`, null);
932
+ }
933
+
934
+ return this.determineStorage(dbInfo, (storageError, storage, mongoClient, database) => {
935
+ if (storageError) {
936
+ return callback(`${origin}: ${storageError}`, null);
937
+ }
938
+
939
+ // if using file storage
940
+ if (storage === Storage.FILESYSTEM) {
941
+ // get a count from the JSON
942
+ const data = countJSON(collectionName, query);
943
+ if (!data || data === -1) {
944
+ log.warn(`${origin}: Could not count data from file storage`);
945
+ return callback(`${origin}: Could not count data from file storage`, null);
946
+ }
947
+ log.debug(`${origin}: Count from file storage ${data}`);
948
+ return callback(null, data);
949
+ }
950
+
951
+ // if using MongoDB storage
952
+ if (storage === Storage.DBINFO || storage === Storage.ADAPTERDB) {
953
+ // get the count from mongo
954
+ return mongoClient.db(database).collection(collectionName).count(query, options, (err, res) => {
955
+ if (storage === Storage.DBINFO && mongoClient) mongoClient.close();
956
+ if (err) {
957
+ log.warn(`${origin}: Failed to count collection ${err}`);
958
+ return callback(`${origin}: Failed to count collection ${err}`, null);
959
+ }
960
+ log.debug(`${origin}: Count from database ${res}`);
961
+ return callback(null, res);
962
+ });
963
+ }
964
+ });
965
+ } catch (ex) {
966
+ log.warn(`${origin}: Caught Exception - ${ex}`);
967
+ return callback(`${origin}: Caught Exception - ${ex}`, null);
968
+ }
969
+ }
970
+
971
+ /**
972
+ * Delete items from a collection
973
+ *
974
+ * @function delete
975
+ * @param {string} collectionName - the collection to remove document from. (required)
976
+ * @param {object} filter - the filter for the document(s) to remove. (required)
977
+ * @param {object} dbInfo - the url for the database to connect to (dburl, dboptions, database)
978
+ * @param {boolean} fsWrite - turn on write to the file system
979
+ */
980
+ delete(collectionName, filter, options, multiple, dbInfo, fsWrite, callback) {
981
+ const origin = `${this.myid}-dbUtil-delete`;
982
+ log.trace(origin);
983
+
984
+ try {
985
+ // verify the required data has been provided
986
+ if (!collectionName) {
987
+ log.warn(`${origin}: Missing Collection Name`);
988
+ return callback(`${origin}: Missing Collection Name`, null);
989
+ }
990
+ if (!filter) {
991
+ log.warn(`${origin}: Missing Filter`);
992
+ return callback(`${origin}: Missing Filter`, null);
993
+ }
994
+ if (multiple === undefined || multiple === null) {
995
+ log.warn(`${origin}: Missing Multiple flag`);
996
+ return callback(`${origin}: Missing Multiple flag`, null);
997
+ }
998
+
999
+ return this.determineStorage(dbInfo, (storageError, storage, mongoClient, database) => {
1000
+ if (storageError) {
1001
+ return callback(`${origin}: ${storageError}`, null);
1002
+ }
1003
+
1004
+ // if using file storage
1005
+ if (storage === Storage.FILESYSTEM) {
1006
+ // verify the collection exists
1007
+ if (!fs.existsSync(`${adapterDir}/storage/${collectionName}.json`)) {
1008
+ log.warn(`${origin}: Collection ${collectionName} does not exist`);
1009
+ return callback(null, `${origin}: Collection ${collectionName} does not exist`);
1010
+ }
1011
+ // remove the item from the collection
1012
+ const deld = removeFromJSON(collectionName, filter, multiple);
1013
+ if (!deld) {
1014
+ log.warn(`${origin}: Data has not been deleted from file storage`);
1015
+ return callback(`${origin}: Data has not been deleted from file storage`, null);
1016
+ }
1017
+ log.debug(`${origin}: Data has been deleted from file storage`);
1018
+ return callback(null, deld);
1019
+ }
1020
+
1021
+ // if using MongoDB storage
1022
+ if (storage === Storage.DBINFO || storage === Storage.ADAPTERDB) {
1023
+ if (!multiple) {
1024
+ // delete the single item from mongo
1025
+ return mongoClient.db(database).collection(collectionName).deleteOne(filter, options, (err, res) => {
1026
+ if (storage === Storage.DBINFO && mongoClient) mongoClient.close();
1027
+ if (err) {
1028
+ log.warn(`${origin}: Failed to delete data from database ${err}`);
1029
+ return callback(`${origin}: Failed delete data from database ${err}`, null);
1030
+ }
1031
+ log.debug(`${origin}: Data has been deleted from database`);
1032
+ return callback(null, res);
1033
+ });
1034
+ }
1035
+
1036
+ // delete the multiple items from mongo
1037
+ return mongoClient.db(database).collection(collectionName).deleteMany(filter, options, (err, res) => {
1038
+ if (storage === Storage.DBINFO && mongoClient) mongoClient.close();
1039
+ if (err) {
1040
+ log.warn(`${origin}: Failed to delete data from database ${err}`);
1041
+ return callback(`${origin}: Failed delete data from database ${err}`, null);
1042
+ }
1043
+ log.debug(`${origin}: Data has been deleted from database`);
1044
+ return callback(null, res);
1045
+ });
1046
+ }
1047
+ });
1048
+ } catch (ex) {
1049
+ log.warn(`${origin}: Caught Exception - ${ex}`);
1050
+ return callback(`${origin}: Caught Exception - ${ex}`, null);
1051
+ }
1052
+ }
1053
+
1054
+ /**
1055
+ * Replace an item in a collection
1056
+ *
1057
+ * @function replaceOne
1058
+ * @param {string} collectionName - the collection to replace document in. (required)
1059
+ * @param {object} filter - the filter for the document(s) to replace. (required)
1060
+ * @param {object} doc - the filter for the document(s) to replace. (required)
1061
+ * @param {object} dbInfo - the url for the database to connect to (dburl, dboptions, database)
1062
+ * @param {boolean} fsWrite - turn on write to the file system
1063
+ */
1064
+ replaceOne(collectionName, filter, doc, options, dbInfo, fsWrite, callback) {
1065
+ const origin = `${this.myid}-dbUtil-replaceOne`;
1066
+ log.trace(origin);
1067
+
1068
+ try {
1069
+ // verify the required data has been provided
1070
+ if (!collectionName) {
1071
+ log.warn(`${origin}: Missing Collection Name`);
1072
+ return callback(`${origin}: Missing Collection Name`, null);
1073
+ }
1074
+ if (!filter) {
1075
+ log.warn(`${origin}: Missing Filter`);
1076
+ return callback(`${origin}: Missing Filter`, null);
1077
+ }
1078
+ if (!doc) {
1079
+ log.warn(`${origin}: Missing Document`);
1080
+ return callback(`${origin}: Missing Document`, null);
1081
+ }
1082
+
1083
+ return this.determineStorage(dbInfo, (storageError, storage, mongoClient, database) => {
1084
+ if (storageError) {
1085
+ return callback(`${origin}: ${storageError}`, null);
1086
+ }
1087
+
1088
+ // if using file storage
1089
+ if (storage === Storage.FILESYSTEM) {
1090
+ // remove the data from the collection
1091
+ const rem = removeFromJSON(collectionName, filter, false);
1092
+ if (rem) {
1093
+ // add the data into the collection
1094
+ const sav = saveAsJson(collectionName, doc);
1095
+ if (sav) {
1096
+ log.debug(`${origin}: Data replaced in file storage`);
1097
+ return callback(null, sav);
1098
+ }
1099
+ log.warn(`${origin}: Could not save doc into file storage`);
1100
+ return callback(`${origin}: Could not save doc into file storage`, null);
1101
+ }
1102
+ log.warn(`${origin}: Could not delete from file storage`);
1103
+ return callback(`${origin}: Could not delete from file storage`, null);
1104
+ }
1105
+
1106
+ // if using MongoDB storage
1107
+ if (storage === Storage.DBINFO || storage === Storage.ADAPTERDB) {
1108
+ // replace an items in mongo
1109
+ return mongoClient.db(database).collection(collectionName).replaceOne(filter, doc, options, (err, res) => {
1110
+ if (storage === Storage.DBINFO && mongoClient) mongoClient.close();
1111
+ if (err) {
1112
+ log.warn(`${origin}: Failed to replace data in database ${err}`);
1113
+ return callback(`${origin}: Failed replace data in database ${err}`, null);
1114
+ }
1115
+ log.debug(`${origin}: Data replaced in file storage`);
1116
+ return callback(null, res);
1117
+ });
1118
+ }
1119
+ });
1120
+ } catch (ex) {
1121
+ log.warn(`${origin}: Caught Exception - ${ex}`);
1122
+ return callback(`${origin}: Caught Exception - ${ex}`, null);
1123
+ }
1124
+ }
1125
+
1126
+ /**
1127
+ * Call to find items in the database
1128
+ *
1129
+ * @function find
1130
+ * @param {string} collectionName - the collection name to search in. (required)
1131
+ * @param {object} options - the options to use to find data.
1132
+ * options = {
1133
+ * filter : <filter Obj>,
1134
+ * sort : <sort Obj>,
1135
+ * start : <start position>,
1136
+ * limit : <limit of results>,
1137
+ * }
1138
+ * @param {object} dbInfo - the url for the database to connect to (dburl, dboptions, database)
1139
+ * @param {boolean} fsWrite - turn on write to the file system
1140
+ */
1141
+ find(collectionName, options, dbInfo, fsWrite, callback) {
1142
+ const origin = `${this.myid}-dbUtil-find`;
1143
+ log.trace(origin);
1144
+
1145
+ try {
1146
+ // verify the required data has been provided
1147
+ if (!collectionName) {
1148
+ log.warn(`${origin}: Missing Collection Name`);
1149
+ return callback(`${origin}: Missing Collection Name`, null);
1150
+ }
1151
+
1152
+ // get the collection so we can run the remove on the collection
1153
+ let filter = {};
1154
+ let start = 0;
1155
+ let sort = {};
1156
+ let limit = 10;
1157
+ if (options) {
1158
+ filter = options.filter || {};
1159
+ start = options.start || 0;
1160
+ sort = options.sort || {};
1161
+ if (Object.hasOwnProperty.call(options, 'limit')) {
1162
+ ({ limit } = options);
1163
+ }
1164
+ }
1165
+
1166
+ // If limit is not specified, default to 10.
1167
+ // Note: limit may be 0, which is equivalent to setting no limit.
1168
+
1169
+ // Replace filter with regex to allow for substring lookup
1170
+ // TODO: Need to create a new filter object instead of mutating the exsisting one
1171
+ const filterKeys = Object.keys(filter).filter((key) => (key[0] !== '$' && typeof filter[key] === 'string'));
1172
+ filterKeys.map((key) => {
1173
+ try {
1174
+ const escapedFilter = filter[key].replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
1175
+ const regexedFilter = new RegExp(`.*${escapedFilter}.*`, 'i');
1176
+ filter[key] = {
1177
+ $regex: regexedFilter
1178
+ };
1179
+ } catch (e) {
1180
+ delete filter[key];
1181
+ }
1182
+ return key;
1183
+ });
1184
+
1185
+ return this.determineStorage(dbInfo, (storageError, storage, mongoClient, database) => {
1186
+ if (storageError) {
1187
+ return callback(`${origin}: ${storageError}`, null);
1188
+ }
1189
+
1190
+ // if using file storage
1191
+ if (storage === Storage.FILESYSTEM) {
1192
+ // Find it from file in the adapter
1193
+ let toReturn = getFromJson(collectionName, filter);
1194
+ if (toReturn && toReturn.length > limit) {
1195
+ let curEnd = start + limit;
1196
+ if (curEnd < toReturn.length) {
1197
+ curEnd = toReturn.length;
1198
+ }
1199
+ toReturn = toReturn.slice(start, curEnd);
1200
+ }
1201
+ log.trace(`${origin}: Data retrieved from file storage`);
1202
+ return callback(null, toReturn);
1203
+ }
1204
+
1205
+ // if using MongoDB storage
1206
+ if (storage === Storage.DBINFO || storage === Storage.ADAPTERDB) {
1207
+ // Find the data in the database
1208
+ return mongoClient.db(database).collection(collectionName).find(filter).sort(sort)
1209
+ .skip(start)
1210
+ .limit(limit)
1211
+ .toArray()
1212
+ .then((value) => {
1213
+ log.debug(`${origin}: Data retrieved from database`);
1214
+ return callback(null, value);
1215
+ })
1216
+ .finally(() => {
1217
+ if (storage === Storage.DBINFO && mongoClient) mongoClient.close();
1218
+ });
1219
+ }
1220
+ });
1221
+ } catch (ex) {
1222
+ log.warn(`${origin}: Caught Exception - ${ex}`);
1223
+ return callback(`${origin}: Caught Exception - ${ex}`, null);
1224
+ }
1225
+ }
1226
+
1227
+ /**
1228
+ * Call to find an item in a collection and modify it.
1229
+ *
1230
+ * @function findAndModify
1231
+ * @param {string} collectionName - the collection to find things from. (required)
1232
+ * @param {object} filter - the filter used to find objects. (optional)
1233
+ * @param {array} sort - how to sort the items (first one in order will be modified). (optional)
1234
+ * @param {object} data - the modification to make. (required)
1235
+ * @param {boolean} upsert - option for the whether to insert new objects. (optional)
1236
+ * @param {object} dbInfo - the url for the database to connect to (dburl, dboptions, database) (optional)
1237
+ * @param {boolean} fsWrite - turn on write to the file system (optional)
1238
+ * @param {updateCallback} callback - a callback function to return a result
1239
+ * (the new object) or the error
1240
+ */
1241
+ findAndModify(collectionName, filter, sort, data, upsert, dbInfo, fsWrite, callback) {
1242
+ const origin = `${this.myid}-dbUtil-findAndModify`;
1243
+ log.trace(origin);
1244
+
1245
+ try {
1246
+ // verify the required data has been provided
1247
+ if (collectionName === undefined || collectionName === null || collectionName === '') {
1248
+ log.warn(`${origin}: Missing Collection Name`);
1249
+ return callback(`${origin}: Missing Collection Name`, null);
1250
+ }
1251
+ if (data === undefined || data === null || typeof data !== 'object' || Object.keys(data).length === 0) {
1252
+ log.warn(`${origin}: Missing data for modification`);
1253
+ return callback(`${origin}: Missing data for modification`, null);
1254
+ }
1255
+
1256
+ const options = {
1257
+ sort,
1258
+ upsert,
1259
+ returnOriginal: false
1260
+ };
1261
+
1262
+ return this.determineStorage(dbInfo, (storageError, storage, mongoClient, database) => {
1263
+ if (storageError) {
1264
+ return callback(`${origin}: ${storageError}`, null);
1265
+ }
1266
+
1267
+ // if using file storage
1268
+ if (storage === Storage.FILESYSTEM) {
1269
+ // save it to file in the adapter storage directory
1270
+ const saved = saveAsJson(collectionName, data);
1271
+ if (!saved) {
1272
+ log.error(`${origin}: Data has not been saved`);
1273
+ return callback(`${origin}: Data has not been saved`, null);
1274
+ }
1275
+ log.debug(`${origin}: Data modified in file storage`);
1276
+ return callback(null, saved);
1277
+ }
1278
+
1279
+ // if using MongoDB storage
1280
+ if (storage === Storage.DBINFO || storage === Storage.ADAPTERDB) {
1281
+ // find and modify the data in the database
1282
+ return mongoClient.db(database).collection(collectionName).findOneAndUpdate((filter || {}), data, options, (err, result) => {
1283
+ if (storage === Storage.DBINFO && mongoClient) mongoClient.close();
1284
+ if (err) {
1285
+ log.warn(`${origin}: Failed to modified data in database ${err}`);
1286
+ return callback(`${origin}: Failed modified data in database ${err}`, null);
1287
+ }
1288
+ log.debug(`${origin}: Data modified in database`);
1289
+ return callback(null, result);
1290
+ });
1291
+ }
1292
+ });
1293
+ } catch (ex) {
1294
+ log.warn(`${origin}: Caught Exception - ${ex}`);
1295
+ return callback(`${origin}: Caught Exception - ${ex}`, null);
1296
+ }
1297
+ }
1298
+ }
1299
+
1300
+ module.exports = DBUtil;