@itentialopensource/adapter-utils 4.49.0 → 5.0.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,968 @@
1
+ /* @copyright Itential, LLC 2023 */
2
+
3
+ // Set globals
4
+ /* global log */
5
+ /* eslint global-require:warn */
6
+ /* eslint import/no-dynamic-require: warn */
7
+
8
+ const AsyncLockCl = require('async-lock');
9
+ const path = require('path');
10
+ const jsonQuery = require('json-query');
11
+
12
+ const lock = new AsyncLockCl();
13
+ let id = null;
14
+
15
+ /**
16
+ * @summary Creates a Cache Entity object to organize the cache by entity type.
17
+ * @function createCacheEntity
18
+ * @param {String} entityName - type of entity
19
+ * @param {Array} entityList - array of data
20
+ * @param {Object} interval - id of interval
21
+ * @returns {Object} - created Cache Entity object
22
+ */
23
+ function createCacheEntity(entityName, entityList, interval, sortEntities = true) {
24
+ const origin = `${id}-cacheHandler-createCacheEntity`;
25
+ log.trace(origin);
26
+ return ({
27
+ lockKey: entityName,
28
+ entityType: entityName,
29
+ list: entityList,
30
+ intervalId: interval,
31
+ sort: sortEntities
32
+ });
33
+ }
34
+
35
+ /**
36
+ * @summary Generates name for data object. Currently non-unique
37
+ * @function generateName
38
+ * @returns {String} - generated name
39
+ */
40
+ function generateName() {
41
+ log.warn('Name for entity not found, generated name placeholder');
42
+ return 'GeneratedName';
43
+ }
44
+
45
+ /**
46
+ * @summary Deletes cache data and properties. Data deletion
47
+ * saves considerable memory.
48
+ * @function deleteCacheData
49
+ * @param {Object} cacheHandler - this class instance
50
+ */
51
+ function deleteCacheData(cacheHandler) {
52
+ const origin = `${id}-cacheHandler-deleteCacheData`;
53
+ log.trace(origin);
54
+
55
+ const handler = cacheHandler;
56
+
57
+ // get all keys to make sure nothing else is editing the cache at this time
58
+ const keysArr = [];
59
+ handler.cache.forEach((entity) => {
60
+ keysArr.push(entity.lockKey);
61
+ });
62
+
63
+ lock.acquire(keysArr, () => {
64
+ if (!handler.enabled) {
65
+ handler.cache.forEach((entity) => {
66
+ clearInterval(entity.intervalId);
67
+ });
68
+ handler.cache = [];
69
+ handler.propertiesMap.clear();
70
+ log.warn(`${origin}: Cache deletion confirmed`);
71
+ }
72
+ });
73
+ }
74
+
75
+ /**
76
+ * @summary changes the frequency of which to update an entity type
77
+ * @function changeUpdateFrequency
78
+ * @param {Array of Objects} currentCache - cache
79
+ * @param {String} entityType - entity type of which to update
80
+ * @param {Integer} newFrequency - how often to updaate (in minutes)
81
+ * @param {cacheHandler} cacheHandler - instance of class to edit
82
+ */
83
+ function changeUpdateFrequency(currentCache, entityType, newFrequency, cacheHandler) {
84
+ const origin = `${id}-cacheHandler-changeUpdateFrequency`;
85
+ log.trace(origin);
86
+
87
+ const cache = currentCache;
88
+ for (let i = 0; i < cache.length; i += 1) {
89
+ if (cache[i].entityType === entityType) {
90
+ cacheHandler.populateCache(entityType);
91
+ clearInterval(cache[i].intervalId);
92
+ cache[i].intervalId = setInterval(() => {
93
+ cacheHandler.populateCache(entityType);
94
+ }, newFrequency * 60000);
95
+ log.info(`Changed update frequency for ${entityType}.`);
96
+ return;
97
+ }
98
+ }
99
+ log.error(`Failed to change interval. ${entityType} does not exist in cache.`);
100
+ }
101
+
102
+ /**
103
+ * @summary Given an array of populate objects from a single entityType, initialize missing fields.
104
+ * @function validatePopulate
105
+ * @param {Array} populateArr - array of populate objects from properties
106
+ */
107
+ function validatePopulate(populateArr) {
108
+ const origin = `${id}-cacheHandler-validatePopulate`;
109
+ log.trace(origin);
110
+
111
+ const arr = populateArr;
112
+ for (let i = 0; i < arr.length; i += 1) {
113
+ if (!arr[i].path) {
114
+ log.error(`No path in populate in properties for cache! Path is ${path}`);
115
+ // let brokerHandler handle this
116
+ } else {
117
+ arr[i].method = arr[i].method || 'GET';
118
+ arr[i].query = arr[i].query || {};
119
+ arr[i].body = arr[i].body || {};
120
+ arr[i].headers = arr[i].headers || {};
121
+ arr[i].handleFailure = arr[i].handleFailure || 'ignore';
122
+ arr[i].requestFields = arr[i].requestFields || {};
123
+ arr[i].responseDatakey = arr[i].responseDatakey || '';
124
+ arr[i].responseFields = arr[i].responseFields || {};
125
+ }
126
+ }
127
+ }
128
+
129
+ /**
130
+ * @summary Checks the properties passed in for the cache are valid,
131
+ * giving them default values if possible and removing them otherwise.
132
+ * @function validateProperties
133
+ * @param {Object} obj - properties json object
134
+ * @returns - adjusted valid properties with defaults
135
+ */
136
+ function validateProperties(obj) {
137
+ const origin = `${id}-cacheHandler-validateProperties`;
138
+ log.trace(origin);
139
+
140
+ if (obj === null) {
141
+ log.error(`${origin}: Null properties!`);
142
+ throw new Error('Null properties!');
143
+ }
144
+
145
+ const props = obj;
146
+ // check cache, cache.enabled, cache.entities exist
147
+ if (!props.cache) {
148
+ // no cache, no props
149
+ props.cache = {
150
+ enabled: false,
151
+ entities: []
152
+ };
153
+ } else {
154
+ if (!props.cache.enabled) {
155
+ props.cache.enabled = false;
156
+ log.error(`${origin}: Passed in cache properties but did not specify enabled!`);
157
+ }
158
+ if (!props.cache.entities) {
159
+ props.cache.entities = [];
160
+ log.error(`${origin}: Passed in cache properties but did not specify entities (not even as empty array)!`);
161
+ }
162
+ }
163
+ // check each entity type in entities
164
+ for (let i = 0; i < props.cache.entities.length; i += 1) {
165
+ // must have entityType and populate
166
+ if (!props.cache.entities[i].entityType || !props.cache.entities[i].populate || props.cache.entities[i].populate.length === 0) {
167
+ log.warn(`${origin}: removed invalid cache property at index ${i}`);
168
+ // remove rather than throw error
169
+ props.cache.entities.splice(i, 1);
170
+ i -= 1; // already pointing at next after splice
171
+ break;
172
+ }
173
+
174
+ // default frequency = 24 hrs, caps if <60 or >10080
175
+ if (!props.cache.entities[i].frequency) {
176
+ props.cache.entities[i].frequency = 24 * 60; // 1 day in minutes
177
+ } else if (props.cache.entities[i].frequency < 15) { // 15 min minimum
178
+ log.warn(`${origin}: Passed in frequency of ${props.cache.entities[i].frequency} is too small! Set to 15 minute.`);
179
+ props.cache.entities[i].frequency = 15;
180
+ } else if (props.cache.entities[i].frequency > 10080) { // 1 week max
181
+ log.warn(`${origin}: Passed in frequency of ${props.cache.entities[i].frequency} is too large! Set to 1 week.`);
182
+ props.cache.entities[i].frequency = 10080;
183
+ }
184
+
185
+ // default flushOnFail = false
186
+ if (!props.cache.entities[i].flushOnFail) {
187
+ props.cache.entities[i].flushOnFail = false;
188
+ }
189
+
190
+ // Limits entity type to 1000 entities
191
+ if (!props.cache.entities[i].limit) {
192
+ props.cache.entities[i].limits = 1000;
193
+ }
194
+
195
+ if (!props.cache.entities[i].retryAttempts) {
196
+ props.cache.entities[i].retryAttempts = 5; // currently only used for startup
197
+ }
198
+
199
+ // make sure task and filterField exists for every object in cachedTasks
200
+ if (!props.cache.entities[i].cachedTasks) {
201
+ props.cache.entities[i].cachedTasks = [];
202
+ } else {
203
+ props.cache.entities[i].cachedTasks.forEach((cacheTask) => {
204
+ const curTask = cacheTask; // for lint
205
+ if (!curTask.name) {
206
+ log.error('Cached task has no task name!');
207
+ curTask.name = ''; // prevent errors, will not ever match a task name
208
+ }
209
+ if (!curTask.filterField) {
210
+ // if no filter then assume is a get all call, would need to explicitly check this
211
+ curTask.filterField = '';
212
+ }
213
+ });
214
+ }
215
+
216
+ // fill in missing populate fields
217
+ validatePopulate(props.cache.entities[i].populate);
218
+ }
219
+ return props;
220
+ }
221
+
222
+ /**
223
+ * @summary Compares two objects by name for sorting purposes.
224
+ * @function compareByName
225
+ * @param {Object} a - object to compare
226
+ * @param {Object} b - object to compare
227
+ * @returns {int} - specifying data order in relation to each other
228
+ */
229
+ function compareByName(a, b) {
230
+ if (a.name > b.name) {
231
+ return 1;
232
+ }
233
+ if (a.name < b.name) {
234
+ return -1;
235
+ }
236
+ return 0;
237
+ }
238
+
239
+ /**
240
+ * @summary tests if object is an options object. Defaults:
241
+ * - filter = ""
242
+ * - start = 0 and limit = MAX_SAFE_INTEGER
243
+ * - sort = false
244
+ * @function validateOptions
245
+ * @param {Object} obj - object to be tested
246
+ * @return {Object} - modified object with defaults
247
+ */
248
+ function validateOptions(obj) {
249
+ const origin = `${id}-cacheHandler-validateOptions`;
250
+ log.trace(origin);
251
+
252
+ const opt = obj || {};
253
+
254
+ const keys = Object.keys(opt);
255
+ if (!keys.includes('filter')) {
256
+ opt.filter = {}; // filter for nothing means get everything
257
+ }
258
+ if (!keys.includes('start') && !keys.includes('limit')) {
259
+ opt.start = 0;
260
+ opt.limit = Number.MAX_SAFE_INTEGER; // return everything, maxed at max safe integer
261
+ } else if (!(keys.includes('start') && keys.includes('limit')) || opt.start < 0 || opt.limit < 0) {
262
+ log.error(`${origin}: incomplete or invalid options with start and limit!`);
263
+ throw new Error('Invalid options on accessing cache');
264
+ }
265
+
266
+ return opt;
267
+ }
268
+
269
+ /**
270
+ * @summary Helper method that retrieves entities from the cache
271
+ * @function retrieveCacheEntriesHelper
272
+ *
273
+ * @param {Object} cache - cache to search through
274
+ * @param {String} entityType - entity type to retrieve
275
+ * @param {Object} options - options on how to filter and return the data
276
+ */
277
+ function retrieveCacheEntriesHelper(cache, entityType, options, callback) {
278
+ // go through the cache to find the entity we care about
279
+ for (let i = 0; i < cache.length; i += 1) {
280
+ // check if entity type we care about
281
+ if (cache[i].entityType === entityType) {
282
+ // Lock the cache for the entity so we can take what is in the cache
283
+ // Lock and instant unlock as wait until lock is free
284
+ return lock.acquire(cache[i].lockKey, (done) => {
285
+ // return the entity's list and free the lock
286
+ done(cache[i].list);
287
+ }, (ret) => {
288
+ let arr = [];
289
+
290
+ // populate re-attempt fails
291
+ if (!ret) {
292
+ log.error(`${origin}: No list in retrieve helper, re-attempt populate failed!`);
293
+ return callback(null, 'No list in retrieve cache helper!');
294
+ }
295
+
296
+ // no cache data found
297
+ if (ret.length === 0) {
298
+ log.info('Cache data retrieved and found to be empty.');
299
+ return callback(arr);
300
+ }
301
+
302
+ // apply filter
303
+ if (!options.filter || Object.keys(options.filter).length === 0) {
304
+ // no filter, get everything
305
+ log.info('No filter applied to cache retrieved');
306
+ arr = ret;
307
+ } else {
308
+ // get cache data with substring in name
309
+ const filterField = Object.keys(options.filter)[0];
310
+ const filter = options.filter[filterField];
311
+ for (let j = 0; j < ret.length; j += 1) {
312
+ if (ret[j][filterField].includes(filter)) {
313
+ arr.push(ret[j]);
314
+ }
315
+ }
316
+ }
317
+
318
+ // don't need to sort if we know it's OOB
319
+ if (options.limit * options.start >= arr.length) {
320
+ log.warn(`${options.start} is not a valid page, too large!`);
321
+ return callback([]);
322
+ }
323
+
324
+ // pagination: get entities limit*start to limit*(start+1) - 1
325
+ if (options.start >= 0 && options.limit > 0) { // Probably should handle this in properties.
326
+ // if last "page" then will be returning fewer items
327
+ const end = Math.min(arr.length, options.limit * (options.start + 1));
328
+ arr = arr.slice(options.limit * options.start, end);
329
+ }
330
+ return callback(arr); // for linting
331
+ });
332
+ }
333
+ }
334
+ log.error(`${origin}: No entityType ${entityType} in retrieve helper!`);
335
+ return callback(null, `No ${entityType} in retrieve cache helper!`); // error not found
336
+ }
337
+
338
+ /**
339
+ * @summary Removes all data of a certain entity type
340
+ * @function removeCacheEntry
341
+ * @param {Array} cache - the cache to remove from
342
+ * @param {String} entityType - the entity type of which to remove
343
+ */
344
+ function removeCacheEntry(cache, entityType) {
345
+ const origin = `${id}-cacheHandler-removeCacheEntry`;
346
+ log.trace(origin);
347
+
348
+ for (let i = 0; i < cache.length; i += 1) {
349
+ if (cache[i].entityType === entityType) {
350
+ lock.acquire(cache[i].lockKey, () => {
351
+ clearInterval(cache[i].intervalId);
352
+ cache[i].splice(i, 1);
353
+ });
354
+ log.info(`Successfully removed ${entityType} from cache.`);
355
+ return;
356
+ }
357
+ }
358
+ log.error(`${origin}: Did not find cache type ${entityType} to remove!`);
359
+ }
360
+
361
+ /**
362
+ * @function getDataFromSources
363
+ * @summary INTERNAL FUNCTION: get data from source(s) - nested
364
+ * @param {*} loopField - fields
365
+ * @param {Array} sources - sources to look into
366
+ * @returns {*} - nested field value
367
+ */
368
+ function getDataFromSources(loopField, sources) {
369
+ let fieldValue = loopField;
370
+
371
+ // go through the sources to find the field
372
+ for (let s = 0; s < sources.length; s += 1) {
373
+ // find the field value using jsonquery
374
+ const nestedValue = jsonQuery(loopField, { data: sources[s] }).value;
375
+
376
+ // if we found in source - set and no need to check other sources
377
+ if (nestedValue) {
378
+ fieldValue = nestedValue;
379
+ break;
380
+ }
381
+ }
382
+
383
+ return fieldValue;
384
+ }
385
+
386
+ /**
387
+ * @summary Manipulates data from call to only represent the specified fields and id and name if they do not have one
388
+ *
389
+ * @function parseResponseFields
390
+ * @param {Boolean} responseFull - whether or not to take in all fields
391
+ * @param {Object of output to call result} responseFields - fields to take in
392
+ * - ie response field of "name": "id" will give the iap call output a name field
393
+ * with the value being the id
394
+ * @param {Array of Objects} allData - data returned from IAP Call
395
+ * @return {Array of Objects} - modified data
396
+ */
397
+ function parseResponseFields(responseFields, allData, end, requestFields) {
398
+ const origin = `${id}-cacheHandler-createCacheData`;
399
+ log.trace(origin);
400
+
401
+ const parsedData = [];
402
+ const rfKeys = Object.keys(responseFields);
403
+ log.debug(rfKeys);
404
+
405
+ let ostypePrefix = '';
406
+ if (responseFields.ostypePrefix) {
407
+ ostypePrefix = responseFields.ostypePrefix;
408
+ }
409
+
410
+ let statusValue = true;
411
+ if (responseFields.statusValue) {
412
+ statusValue = responseFields.statusValue;
413
+ }
414
+
415
+ allData.response.forEach((currData) => {
416
+ const newObj = currData;
417
+
418
+ for (let rf = 0; rf < rfKeys.length; rf += 1) {
419
+ if (rfKeys[rf] !== 'ostypePrefix') {
420
+ let fieldValue = getDataFromSources(responseFields[rfKeys[rf]], [currData, { fake: 'fakedata' }, requestFields]);
421
+
422
+ // if the field is ostype - need to add prefix
423
+ if (rfKeys[rf] === 'ostype' && typeof fieldValue === 'string') {
424
+ fieldValue = ostypePrefix + fieldValue;
425
+ }
426
+ // if there is a status to set, set it
427
+ if (rfKeys[rf] === 'status') {
428
+ // if really looking for just a good response
429
+ if (responseFields[rfKeys[rf]] === 'return2xx' && allData.icode === statusValue.toString()) {
430
+ newObj.isAlive = true;
431
+ } else if (fieldValue.toString() === statusValue.toString()) {
432
+ newObj.isAlive = true;
433
+ } else {
434
+ newObj.isAlive = false;
435
+ }
436
+ }
437
+ // if we found a good value
438
+ newObj[rfKeys[rf]] = fieldValue;
439
+ }
440
+ }
441
+
442
+ if (!Object.prototype.hasOwnProperty.call(currData, 'name')) {
443
+ newObj.name = generateName();
444
+ }
445
+
446
+ // no longer requiring id field
447
+
448
+ parsedData.push(newObj);
449
+ });
450
+ return parsedData;
451
+ }
452
+
453
+ /**
454
+ * @summary makes IAP Calls through Generic Handler
455
+ *
456
+ * @function makeIAPCall
457
+ * @param calls - calls to make
458
+ * @param requestHandler - instance of requestHandler to make call
459
+ * @param callback - data or error
460
+ */
461
+ function makeIAPCall(calls, requestHandler, callback) { // todo pass in properties from cacheHandler
462
+ const callPromises = [];
463
+ log.debug('Starting an iap call from cache.');
464
+ for (let i = 0; i < calls.length; i += 1) {
465
+ log.debug('Response :', calls[i].responseFields);
466
+ callPromises.push(new Promise((resolve, reject) => {
467
+ requestHandler.genericAdapterRequest(calls[i].path, calls[i].method, calls[i].query, calls[i].body, calls[i].headers, (callRet, callErr) => {
468
+ if (callErr) {
469
+ log.error('Make iap call failed with error');
470
+ log.error(callErr);
471
+ if (callErr.icode === 'AD.301') {
472
+ log.error(callErr.IAPerror.displayString);
473
+ return reject(callErr);
474
+ }
475
+ if (calls[i].handleFailure === 'ignore') {
476
+ log.info(`Call failed for path ${calls[i].path} with error code ${callErr.icode}. Ignoring.`);
477
+ return resolve([]);
478
+ }
479
+ log.warn(`Call failed for path ${calls[i].path} with error code ${callErr.icode}. Rejecting.`);
480
+ return reject(callErr);
481
+ }
482
+
483
+ // callRet is the object returned from that call
484
+ log.info(`Sucessful Call. Adding to cache from path ${calls[i].path}`);
485
+ const result = callRet;
486
+ if (calls[i].responseDatakey) {
487
+ result.response = jsonQuery(calls[i].responseDatakey, { data: result.response }).value;
488
+ }
489
+ return resolve(parseResponseFields(calls[i].responseFields, result, i, calls[i].requestFields));
490
+ });
491
+ }));
492
+ }
493
+
494
+ let returnData = [];
495
+ // Reworking the brokerHandler calls
496
+ return Promise.all(callPromises).then((results) => { // array of each Promise result
497
+ for (let i = 0; i < results.length; i += 1) {
498
+ returnData = returnData.concat(results[i]);
499
+ }
500
+ callback(returnData, null); // Returns data if all calls succeed
501
+ }, (error) => {
502
+ callback(null, error); // Throws error if at least one call failed
503
+ });
504
+ }
505
+
506
+ /**
507
+ * @summary Helper method that tries to populate the cache after 5 seconds
508
+ *
509
+ * @param {String} entityType - entity type to populate
510
+ * @param {CacheHandler} cacheHandler - instance of cacheHandler
511
+ * @param {Integer} attemptsRemaining - amount of attempts left to retry
512
+ * @param {function} callback - returns whether or not that cache was successfully populated
513
+ */
514
+ function populateTimeout(entityType, cacheHandler, attemptsRemaining, callback) {
515
+ let ar = attemptsRemaining;
516
+ log.info(`Retrying to populate cache for ${entityType}. Attempts Remaining: ${ar}`);
517
+ setTimeout(() => {
518
+ cacheHandler.populateCache(entityType).then((result) => {
519
+ if (result && result[0] === 'success') {
520
+ return callback(true);
521
+ }
522
+ ar -= 1;
523
+ if (ar === 0) {
524
+ return callback(false);
525
+ }
526
+ return populateTimeout(entityType, cacheHandler, ar, callback);
527
+ });
528
+ }, 10000);
529
+ }
530
+
531
+ /**
532
+ * @summary tries to populate the cache again after populate fails on startup
533
+ *
534
+ * @param {String} entityType - entity type to populate
535
+ * @param {CacheHandler} cacheHandler - instance of CacheHandler
536
+ * @param {Integer} attempts - amount of attempts to retry
537
+ */
538
+ async function retryPopulate(entityType, cacheHandler, attempts) {
539
+ log.info('Retrying populate...');
540
+ return new Promise((resolve) => {
541
+ populateTimeout(entityType, cacheHandler, attempts, (success) => {
542
+ if (success) {
543
+ return resolve(`Cache updated for ${entityType}.`);
544
+ }
545
+ return resolve(`Populate cache for ${entityType} failed.`);
546
+ });
547
+ });
548
+ }
549
+
550
+ // Exposed handler class
551
+ class CacheHandler {
552
+ /**
553
+ * Adapter Cache Handler
554
+ * @constructor
555
+ */
556
+ constructor(prongId, properties, directory, reqH) {
557
+ id = prongId; // accessable to non-exposed methods
558
+ this.baseDir = directory;
559
+ this.requestHandler = reqH; // Request Handler object with id, props, dir
560
+
561
+ this.cache = []; // array of CacheEntity objects
562
+
563
+ // set up the properties I care about
564
+ this.propertiesMap = new Map(); // entityType->properties map
565
+ this.refreshProperties(properties);
566
+ }
567
+
568
+ /**
569
+ * refreshProperties is used to set up all of the properties for the cache handler.
570
+ * It allows properties to be changed later by simply calling refreshProperties rather
571
+ * than having to restart the cache handler.
572
+ *
573
+ * @function refreshProperties
574
+ * @param {Object} properties - an object containing all of the properties
575
+ */
576
+ refreshProperties(properties) {
577
+ const origin = `${id}-cacheHandler-refreshProperties`;
578
+ log.trace(origin);
579
+
580
+ if (properties === null) {
581
+ log.error(`${origin}: Cache Handler received no properties!`);
582
+ return;
583
+ }
584
+
585
+ this.props = validateProperties(properties);
586
+
587
+ const wasEnabled = this.enabled || false; // in case undefined
588
+ this.enabled = this.props.cache.enabled;
589
+ if (!this.enabled) {
590
+ // destroy cache and properties for memory efficiency
591
+ if (wasEnabled) {
592
+ deleteCacheData(this);
593
+ }
594
+ log.warn('Cache was disabled from enabled, removed cache memory.');
595
+ return;
596
+ }
597
+
598
+ // check entityType was not deleted (if so flush that cache type)
599
+ this.propertiesMap.forEach((value, key) => {
600
+ for (let i = 0; i < this.props.cache.entities.length; i += 1) {
601
+ if (!this.props.cache.entities[i].name === key) { // currently in cache, but not props
602
+ removeCacheEntry(this.cache, key);
603
+ this.propertiesMap.delete(key);
604
+ log.warn(`${origin}: Removed cache entity type due to change in properties.`);
605
+ log.info(`Deleted props for entity type ${key} with val ${value}`);
606
+ return; // continue forEach
607
+ }
608
+ }
609
+ });
610
+
611
+ // set properties map
612
+ this.props.cache.entities.forEach((entity) => {
613
+ if (!this.propertiesMap.has(entity.entityType)) {
614
+ // entity does not exist yet
615
+ this.propertiesMap.set(entity.entityType, {});
616
+ this.propertiesMap.get(entity.entityType).frequency = entity.frequency;
617
+ this.propertiesMap.get(entity.entityType).flushOnFail = entity.flushOnFail;
618
+ this.propertiesMap.get(entity.entityType).populate = entity.populate;
619
+ this.propertiesMap.get(entity.entityType).limit = entity.limit;
620
+ this.propertiesMap.get(entity.entityType).retryAttempts = entity.retryAttempts;
621
+
622
+ const newIntervalId = setInterval(() => {
623
+ this.populateCache(entity.entityType);
624
+ }, entity.frequency * 60000);
625
+
626
+ const sort = Object.prototype.hasOwnProperty.call(entity, 'sort') ? entity.sort : true; // default true
627
+ this.cache.push(createCacheEntity(entity.entityType, null, newIntervalId, sort));
628
+ this.populateCache(entity.entityType).then((result) => {
629
+ if (result && result[0] === 'error') {
630
+ retryPopulate(entity.entityType, this, entity.retryAttempts).then((nextResult) => {
631
+ log.info(nextResult);
632
+ });
633
+ }
634
+ }).catch((error) => {
635
+ log.error('Failed populate cache!');
636
+ if (error.icode === 'AD.301') {
637
+ log.error(`Destroying interval, check path and call in properties for ${entity.entityType}`);
638
+ clearInterval(newIntervalId);
639
+ this.cache.removeCacheEntry(this.cache, entity.entityType);
640
+ }
641
+ });
642
+ } else {
643
+ // entity props frequency updated
644
+ if (entity.frequency !== this.propertiesMap.get(entity.entityType).frequency) {
645
+ changeUpdateFrequency(this.cache, entity.entityType, entity.frequency, this);
646
+ }
647
+
648
+ this.propertiesMap.get(entity.entityType).frequency = entity.frequency;
649
+ this.propertiesMap.get(entity.entityType).flushOnFail = entity.flushOnFail;
650
+ this.propertiesMap.get(entity.entityType).populate = entity.populate;
651
+ this.propertiesMap.get(entity.entityType).limit = entity.limit;
652
+ this.propertiesMap.get(entity.entityType).retryAttempts = entity.retryAttempts;
653
+ }
654
+ });
655
+ }
656
+
657
+ /**
658
+ * @summary Populates/updates cache with entities' data from IAP call
659
+ * @function populateCache
660
+ * @param {String/Array of Strings} entities - entities of which we want to store
661
+ * @param {Boolean} onlyIfNecessary - If true and cache has been populated after acquiring the lock, does nothing
662
+ * @return {Array of Strings} - whether each entity succeeded or errored
663
+ */
664
+ async populateCache(entities) {
665
+ const origin = `${id}-cacheHandler-populateCache`;
666
+ log.trace(origin);
667
+
668
+ if (!this.enabled) {
669
+ return Promise.reject(new Error('Cache is not enabled!'));
670
+ }
671
+
672
+ log.info('Populating cache.');
673
+
674
+ // support string and array input
675
+ let entityArr = entities;
676
+ if (!Array.isArray(entities)) {
677
+ entityArr = [entities];
678
+ }
679
+
680
+ // need all properties to make iap call
681
+ for (let i = 0; i < entityArr.length; i += 1) {
682
+ const entityType = entityArr[i];
683
+ if (!this.propertiesMap.has(entityType)) {
684
+ log.error(`${entityType} is an untracked entity type! Check properties.`);
685
+ return Promise.reject(new Error(`${entityType} is an untracked entity type! Check properties.`));
686
+ }
687
+ }
688
+
689
+ try {
690
+ const promiseArr = [];
691
+ // for each entityType need to populate
692
+ for (let j = 0; j < entityArr.length; j += 1) {
693
+ const entityType = entityArr[j];
694
+ // try to find current entityType in cache
695
+ for (let i = 0; i < this.cache.length; i += 1) {
696
+ if (this.cache[i].entityType === entityType) { // will always be found
697
+ promiseArr.push(new Promise((resolve, reject) => {
698
+ makeIAPCall(this.propertiesMap.get(entityType).populate, this.requestHandler, (cacheData, error) => {
699
+ // error message if applicable
700
+ let errorMsg = null;
701
+ if (error) {
702
+ errorMsg = `${origin}: ${entityType} failed with error ${error.IAPerror.displayString}.`;
703
+ } else if (cacheData.length > this.propertiesMap.get(entityType).limit) {
704
+ errorMsg = `${origin}: ${entityType} failed. Data surpassed the limit.`;
705
+ } else if (cacheData.length === 0) {
706
+ errorMsg = `${origin}: ${entityType} failed. Nothing found.`;
707
+ }
708
+ // handle error
709
+ if (errorMsg !== null) {
710
+ /**
711
+ * removed unnecessary check if cache[i].list is null here
712
+ * setting null var to null again is fine and shortens critical section
713
+ * */
714
+
715
+ if (this.propertiesMap.get(entityType).flushOnFail) {
716
+ lock.acquire(this.cache[i].lockKey, (done) => {
717
+ this.cache[i].list = null;
718
+ done(`${errorMsg} Flushed.`);
719
+ }, (ret) => {
720
+ log.error(ret);
721
+ });
722
+ } else {
723
+ log.error(`${errorMsg} Keeping old data.`);
724
+ }
725
+
726
+ // bad path
727
+ if (error && error.icode === 'AD.301') {
728
+ return reject(error);
729
+ }
730
+ return resolve('error');
731
+ }
732
+ // sort cache data, default true
733
+ if (this.cache[i].sort) {
734
+ cacheData.sort(compareByName);
735
+ }
736
+
737
+ // no error, edit cache
738
+ lock.acquire(this.cache[i].lockKey, (done) => {
739
+ if (this.enabled) { // in case properties changes during iap call
740
+ this.cache[i].list = cacheData;
741
+ done(`${entityType}: cache updated.`);
742
+ } else {
743
+ done('Populate cancelled due to disabled cache!');
744
+ }
745
+ }, (ret) => {
746
+ log.info(ret);
747
+ });
748
+ return resolve('success');
749
+ });
750
+ }));
751
+ break;
752
+ }
753
+ }
754
+ }
755
+ const arr = await Promise.allSettled(promiseArr);
756
+ return new Promise((resolve, reject) => {
757
+ const valueArray = [];
758
+ for (let p = 0; p < arr.length; p += 1) {
759
+ if (arr[p].status === 'rejected') {
760
+ reject(arr[p].reason);
761
+ }
762
+ valueArray.push(arr[p].value);
763
+ }
764
+ resolve(valueArray);
765
+ });
766
+ } catch (e) {
767
+ // re-throw any exception
768
+ throw new Error(e);
769
+ }
770
+ }
771
+
772
+ /**
773
+ * @summary Retrieves an entity's data from cache under specified filters
774
+ * @function retrieveCacheEntries
775
+ * @param {String} entityType - entity type that we want to retrieve items from
776
+ * @param {Object} opts - Options of what we want to search for and return
777
+ * Keys:
778
+ * Filter (String) - Substring in which to see which's data
779
+ * names contain
780
+ * Start (int) - Pagination Number (Not sure how to implement)
781
+ * Limit (int) - limit of how much data to send back
782
+ * Sort (Boolean) - whether we sort the data or not
783
+ * @return {Array} - array of data objects
784
+ */
785
+ retrieveCacheEntries(entityType, opts, callback) {
786
+ const origin = `${id}-cacheHandler-retrieveCacheEntries`;
787
+ log.trace(origin);
788
+
789
+ if (!this.enabled) {
790
+ log.warn(`${origin}: Cache is not enabled!`);
791
+ return callback(null, 'Cache is not enabled.');
792
+ }
793
+
794
+ if (typeof entityType !== 'string') {
795
+ log.error(`${entityType} is not of type String`);
796
+ return callback(null, `${entityType} is not of type String`);
797
+ }
798
+
799
+ try {
800
+ const options = validateOptions(opts); // may throw error
801
+ // go through the cache to find the entity we care about
802
+ for (let i = 0; i < this.cache.length; i += 1) {
803
+ // check if entity type we care about
804
+ if (this.cache[i].entityType === entityType) {
805
+ // Lock the cache for the entity so we can take what is in the cache
806
+ // Lock and instant unlock as wait until lock is free
807
+ return lock.acquire(this.cache[i].lockKey, (done) => {
808
+ // return the entity's list and free the lock
809
+ done(this.cache[i].list);
810
+ }, (ret) => {
811
+ // last iap call failed and flushed, attempt new iap call
812
+ if (!ret) {
813
+ // this returns a promise but below we return a callback...
814
+ return this.populateCache(entityType).then((result) => {
815
+ if (result && result[0] === 'success') {
816
+ return retrieveCacheEntriesHelper(this.cache, entityType, options, callback);
817
+ }
818
+ return callback(null, `Retrieve call for ${entityType} failed, and populate re-attempt failed`);
819
+ });
820
+ }
821
+
822
+ // normal retrieve
823
+ return retrieveCacheEntriesHelper(this.cache, entityType, options, callback);
824
+ });
825
+ }
826
+ }
827
+ log.error(`${origin}: Failed to find ${entityType} in retrieve!`);
828
+ return callback(null, `Retrieve call for ${entityType} failed.`);
829
+ } catch (e) {
830
+ // re-throw any exception
831
+ throw new Error(e);
832
+ }
833
+ }
834
+
835
+ /**
836
+ * @summary Method to check if the entity is in the cache. Faster than
837
+ * retrieveCacheEntries hence the seperation
838
+ *
839
+ * @function isEntityCached
840
+ * @param {String} entityType - the entity type to check for
841
+ * @param {Array of String} entityNames - the specific entities we are looking for of that type
842
+ *
843
+ * @return {Array of Boolean} - whether associated entity is found
844
+ */
845
+ isEntityCached(entityType, entityNames, callback) {
846
+ const origin = `${id}-cacheHandler-isEntityCached`;
847
+ log.trace(origin);
848
+ // Possibly supporting checking if entityType is cache? (set entityId = null)
849
+ if (!this.enabled) {
850
+ log.warn(`${origin}: Cache is not enabled!`);
851
+ return callback(null, 'Cache is not enabled!');
852
+ }
853
+
854
+ let names = entityNames;
855
+ if (!Array.isArray(entityNames)) {
856
+ names = [entityNames];
857
+ }
858
+
859
+ try {
860
+ for (let i = 0; i < this.cache.length; i += 1) {
861
+ if (this.cache[i].entityType === entityType) {
862
+ const returnVals = [];
863
+ return lock.acquire(this.cache[i].lockKey, (done) => {
864
+ done(this.cache[i].list);
865
+ }, (ret) => {
866
+ // null list means previously flushed or failed called
867
+ if (!ret) {
868
+ log.info(`Did not find ${entityType}, attempting populate in isEntityCached`);
869
+ return this.populateCache(entityType).then((result) => { // populate will re-acquire lock
870
+ if (result && result[0] === 'success') {
871
+ // re-acquire lock to read list
872
+ lock.acquire(this.cache[i].lockKey, (done) => {
873
+ names.forEach((name) => {
874
+ for (let j = 0; j < this.cache[i].list.length; j += 1) {
875
+ if (this.cache[i].list[j].name === name) {
876
+ returnVals.push('found');
877
+ return; // forEach
878
+ }
879
+ }
880
+ returnVals.push('notfound');
881
+ });
882
+ done(returnVals);
883
+ }, (retVals) => {
884
+ log.info(retVals);
885
+ return callback(retVals);
886
+ });
887
+ }
888
+ return callback(null, `isEntityCached call for ${entityType} failed.`);
889
+ });
890
+ }
891
+
892
+ names.forEach((curName) => {
893
+ for (let j = 0; j < ret.length; j += 1) {
894
+ if (ret[j].name === curName) {
895
+ returnVals.push('found');
896
+ return; // continue the forEach
897
+ }
898
+ }
899
+ returnVals.push('notfound');
900
+ });
901
+ log.info(returnVals);
902
+ return callback(returnVals);
903
+ }); // end of lock acquire
904
+ }
905
+ }
906
+ log.error(`${entityType} not found in cache.`);
907
+ return callback(false);
908
+ } catch (e) {
909
+ log.error(e);
910
+ return callback(false);
911
+ }
912
+ }
913
+
914
+ /**
915
+ * @function isEntityTypeToBeCached
916
+ * @summary Checks whether the cache properties for that entity type
917
+ * is set up, meaning that the entity type should be cached
918
+ * @param {String} entityType - the entity type to check for
919
+ * @returns {Boolean} - whether the entity type is set to be cached
920
+ */
921
+ isEntityTypeToBeCached(entityType, callback) {
922
+ const origin = `${id}-cacheHandler-isEntityTypeToBeCached`;
923
+ log.trace(origin);
924
+
925
+ if (!this.enabled) {
926
+ log.error('Cache is not enabled!');
927
+ return callback(null, 'Cache is not enabled!');
928
+ }
929
+
930
+ return callback(this.propertiesMap.has(entityType));
931
+ }
932
+
933
+ /**
934
+ * @function isTaskCached
935
+ * @summary Checks whether the task should be cached, for adapter
936
+ * (non-broker level) calls
937
+ * @param {String} entityType - entity type from identifyRequest
938
+ * @param {String} task - method name
939
+ */
940
+ isTaskCached(entityType, task) {
941
+ const origin = `${id}-cacheHandler-isTaskCached`;
942
+ log.trace(origin);
943
+
944
+ // do not bother with callbacks here
945
+ if (!this.enabled || !this.propertiesMap.has(entityType)) {
946
+ return false;
947
+ }
948
+
949
+ // find entity type
950
+ for (let i = 0; i < this.props.cache.entities.length; i += 1) {
951
+ if (this.props.cache.entities[i].entityType === entityType) {
952
+ // check if contains task
953
+ for (let c = 0; c < this.props.cache.entities[i].cachedTasks.length; c += 1) {
954
+ if (this.props.cache.entity[i].cachedTasks[c].name === task) {
955
+ return true;
956
+ }
957
+ }
958
+ // does not have task under that entityType
959
+ return false;
960
+ }
961
+ }
962
+ // entityType not cached, should never reach here
963
+ log.error(`${origin}: EntityType not found in cache, should never reach here.`);
964
+ return false;
965
+ }
966
+ }
967
+
968
+ module.exports = CacheHandler;