@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.
@@ -0,0 +1,1175 @@
1
+ /* @copyright Itential, LLC 2018 */
2
+
3
+ // Set globals
4
+ /* global log g_redis */
5
+ /* eslint consistent-return: warn */
6
+ /* eslint global-require: warn */
7
+ /* eslint import/no-dynamic-require: warn */
8
+
9
+ /* NodeJS internal utilities */
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const AsyncLockCl = require('async-lock');
13
+
14
+ // The schema validator
15
+ const AjvCl = require('ajv');
16
+
17
+ /* Fetch in the other needed components for the this Class */
18
+ const RestHandlerCl = require(path.join(__dirname, '/restHandler.js'));
19
+ const PropUtilCl = require(path.join(__dirname, '/propertyUtil.js'));
20
+ const ConnectorCl = require(path.join(__dirname, '/connectorRest.js'));
21
+ const TransUtilCl = require(path.join(__dirname, '/translatorUtil.js'));
22
+ const DBUtilCl = require(path.join(__dirname, '/dbUtil.js'));
23
+
24
+ let id = null;
25
+ const allowFailover = 'AD.300';
26
+ const noFailover = 'AD.500';
27
+ let dbUtilInst = null;
28
+ let propUtilInst = null;
29
+ let transUtilInst = null;
30
+ const NS_PER_SEC = 1e9;
31
+ let username = null;
32
+
33
+ // used for local cache or a temp if using redis
34
+ let cache = {};
35
+ const cachelock = 0;
36
+ let clock = null;
37
+
38
+ // INTERNAL FUNCTIONS
39
+ /**
40
+ * @summary Validate the properties have been provided for the libraries
41
+ *
42
+ * @function validateProperties
43
+ * @param {String} entityName - the name of the entity (required)
44
+ * @param {String} actionName - the name of the action to take (required)
45
+ *
46
+ * @return {Object} entitySchema - the entity schema object
47
+ */
48
+ function validateProperties(properties) {
49
+ const origin = `${id}-requestHandler-validateProperties`;
50
+ log.trace(origin);
51
+
52
+ try {
53
+ // get the path for the specific action file
54
+ const propertyFile = path.join(__dirname, '/../propertiesSchema.json');
55
+
56
+ // Read the action from the file system
57
+ const propertySchema = JSON.parse(fs.readFileSync(propertyFile, 'utf-8'));
58
+
59
+ // add any defaults to the data
60
+ const combinedProps = propUtilInst.mergeProperties(properties, propUtilInst.setDefaults(propertySchema));
61
+
62
+ // validate the entity against the schema
63
+ const ajvInst = new AjvCl();
64
+ const validate = ajvInst.compile(propertySchema);
65
+ const result = validate(combinedProps);
66
+
67
+ // if invalid properties throw an error
68
+ if (!result) {
69
+ // create the generic part of an error object
70
+ const errorObj = {
71
+ origin,
72
+ type: 'Schema Validation Failure',
73
+ vars: [validate.errors[0].message]
74
+ };
75
+
76
+ // log and throw the error
77
+ log.trace(`${origin}: Schema validation failure ${validate.errors[0].message}`);
78
+ throw new Error(JSON.stringify(errorObj));
79
+ }
80
+
81
+ // need to decode/decrypt the static token if it is encoded/encrypted
82
+ if (combinedProps.authentication && combinedProps.authentication.token
83
+ && (combinedProps.authentication.token.indexOf('{code}') === 0
84
+ || combinedProps.authentication.token.indexOf('{crypt}') === 0)) {
85
+ combinedProps.authentication.token = propUtilInst.decryptProperty(combinedProps.authentication.token);
86
+ }
87
+
88
+ // need to decode/decrypt the password if it is encoded/encrypted
89
+ if (combinedProps.authentication && combinedProps.authentication.password
90
+ && (combinedProps.authentication.password.indexOf('{code}') === 0
91
+ || combinedProps.authentication.password.indexOf('{crypt}') === 0)) {
92
+ combinedProps.authentication.password = propUtilInst.decryptProperty(combinedProps.authentication.password);
93
+ }
94
+
95
+ // return the resulting properties --- add any necessary defaults
96
+ return combinedProps;
97
+ } catch (e) {
98
+ return transUtilInst.checkAndThrow(e, origin, 'Issue validating properties');
99
+ }
100
+ }
101
+
102
+ /**
103
+ * @summary Walk through the entities and make sure they have an action file
104
+ * and that file format is validated against actionSchema.json
105
+ *
106
+ * @function walkThroughActionFiles
107
+ * @param {String} directory - the directory for the adapter (required)
108
+ */
109
+ function walkThroughActionFiles(directory) {
110
+ const origin = `${id}-requestHandler-walkThroughActionFiles`;
111
+ log.trace(origin);
112
+ const clean = [];
113
+
114
+ try {
115
+ // Read the action schema from the file system
116
+ const actionSchemaFile = path.join(__dirname, '/../actionSchema.json');
117
+ const actionSchema = JSON.parse(fs.readFileSync(actionSchemaFile, 'utf-8'));
118
+ const entitydir = `${directory}/entities`;
119
+
120
+ // if there is an entity directory
121
+ if (fs.statSync(directory).isDirectory() && fs.statSync(entitydir).isDirectory()) {
122
+ const entities = fs.readdirSync(entitydir);
123
+
124
+ // need to go through each entity in the entities directory
125
+ for (let e = 0; e < entities.length; e += 1) {
126
+ // make sure the entity is a directory - do not care about extra files
127
+ // only entities (dir)
128
+ if (fs.statSync(`${entitydir}/${entities[e]}`).isDirectory()) {
129
+ // see if the action file exists in the entity
130
+ if (fs.existsSync(`${entitydir}/${entities[e]}/action.json`)) {
131
+ // Read the entity actions from the file system
132
+ const actions = JSON.parse(fs.readFileSync(`${entitydir}/${entities[e]}/action.json`, 'utf-8'));
133
+
134
+ // add any defaults to the data
135
+ const defActions = propUtilInst.setDefaults(actionSchema);
136
+ const allActions = propUtilInst.mergeProperties(actions, defActions);
137
+
138
+ // validate the entity against the schema
139
+ const ajvInst = new AjvCl();
140
+ const validate = ajvInst.compile(actionSchema);
141
+ const result = validate(allActions);
142
+
143
+ // if invalid properties throw an error
144
+ if (!result) {
145
+ // Get the action and component from the first ajv error
146
+ let action = 'checkErrorDetails';
147
+ let component = 'checkErrorDetails';
148
+ const temp = validate.errors[0].dataPath;
149
+ const actStInd = temp.indexOf('[');
150
+ const actEndInd = temp.indexOf(']');
151
+ // if we have indexes for the action we can get specifics
152
+ if (actStInd >= 0 && actEndInd > actStInd) {
153
+ const actNum = temp.substring(actStInd + 1, actEndInd);
154
+ // get the action name from the number
155
+ if (actions.actions.length >= actNum) {
156
+ action = actions.actions[actNum].name;
157
+ }
158
+ // get the component that failed for the action
159
+ if (temp.length > actEndInd + 1) {
160
+ component = temp.substring(actEndInd + 2);
161
+ }
162
+ }
163
+ let msg = `${origin}: Error on validation of actions for entity `;
164
+ msg += `${entities[e]} - ${action} - ${component} Details: ${JSON.stringify(validate.errors)}`;
165
+ clean.push(msg);
166
+ log.warn(msg);
167
+ }
168
+
169
+ for (let a = 0; a < actions.actions.length; a += 1) {
170
+ const act = actions.actions[a];
171
+ let reqSchema = null;
172
+ let respSchema = null;
173
+
174
+ // Check that the request schema file defined for the action exist
175
+ if (act.requestSchema) {
176
+ if (!fs.existsSync(`${entitydir}/${entities[e]}/${act.requestSchema}`)) {
177
+ let msg = `${origin}: Error on validation of actions for entity `;
178
+ msg += `${entities[e]}: ${act.name} - missing request schema file - ${act.requestSchema}`;
179
+ clean.push(msg);
180
+ log.warn(msg);
181
+ } else {
182
+ reqSchema = JSON.parse(fs.readFileSync(`${entitydir}/${entities[e]}/${act.requestSchema}`, 'utf-8'));
183
+ }
184
+ } else if (act.schema && fs.existsSync(`${entitydir}/${entities[e]}/${act.schema}`)) {
185
+ reqSchema = JSON.parse(fs.readFileSync(`${entitydir}/${entities[e]}/${act.schema}`, 'utf-8'));
186
+ } else {
187
+ let msg = `${origin}: Error on validation of actions for entity `;
188
+ msg += `${entities[e]}: ${act.name} - missing request schema file - ${act.schema}`;
189
+ clean.push(msg);
190
+ log.warn(msg);
191
+ }
192
+
193
+ // Check that the response schema file defined for the action exist
194
+ if (act.responseSchema) {
195
+ if (!fs.existsSync(`${entitydir}/${entities[e]}/${act.responseSchema}`)) {
196
+ let msg = `${origin}: Error on validation of actions for entity `;
197
+ msg += `${entities[e]}: ${act.name} - missing response schema file - ${act.responseSchema}`;
198
+ clean.push(msg);
199
+ log.warn(msg);
200
+ } else {
201
+ respSchema = JSON.parse(fs.readFileSync(`${entitydir}/${entities[e]}/${act.responseSchema}`, 'utf-8'));
202
+ }
203
+ } else if (act.schema && fs.existsSync(`${entitydir}/${entities[e]}/${act.schema}`)) {
204
+ respSchema = JSON.parse(fs.readFileSync(`${entitydir}/${entities[e]}/${act.schema}`, 'utf-8'));
205
+ } else {
206
+ let msg = `${origin}: Error on validation of actions for entity `;
207
+ msg += `${entities[e]}: ${act.name} - missing response schema file - ${act.schema}`;
208
+ clean.push(msg);
209
+ log.warn(msg);
210
+ }
211
+
212
+ // check that the action is in the schemas
213
+ if (!reqSchema || !reqSchema.properties || !reqSchema.properties.ph_request_type
214
+ || !reqSchema.properties.ph_request_type.enum || !reqSchema.properties.ph_request_type.enum.includes(act.name)) {
215
+ let msg = `${origin}: Error on validation of actions for entity `;
216
+ msg += `${entities[e]}: ${act.name} - missing from ph_request_type in request schema`;
217
+ clean.push(msg);
218
+ log.warn(msg);
219
+ }
220
+ if (!respSchema || !respSchema.properties || !respSchema.properties.ph_request_type
221
+ || !respSchema.properties.ph_request_type.enum || !respSchema.properties.ph_request_type.enum.includes(act.name)) {
222
+ let msg = `${origin}: Error on validation of actions for entity `;
223
+ msg += `${entities[e]}: ${act.name} - missing from ph_request_type in response schema`;
224
+ clean.push(msg);
225
+ log.warn(msg);
226
+ }
227
+
228
+ // check that the mock data files exist
229
+ if (act.responseObjects) {
230
+ for (let m = 0; m < act.responseObjects.length; m += 1) {
231
+ if (act.responseObjects[m].mockFile) {
232
+ if (!fs.existsSync(`${entitydir}/${entities[e]}/${act.responseObjects[m].mockFile}`)) {
233
+ let msg = `${origin}: Error on validation of actions for entity `;
234
+ msg += `${entities[e]}: ${act} - missing mock data file - ${act.responseObjects[m].mockFile}`;
235
+ clean.push(msg);
236
+ log.warn(msg);
237
+ }
238
+ }
239
+ }
240
+ }
241
+ }
242
+ } else {
243
+ log.warn(`${origin}: Entity ${entities[e]} missing action file.`);
244
+ clean.push(`${origin}: Entity ${entities[e]} missing action file.`);
245
+ }
246
+ } else {
247
+ log.warn(`${origin}: Entity ${entities[e]} missing entity directory.`);
248
+ clean.push(`${origin}: Entity ${entities[e]} missing entity directory.`);
249
+ }
250
+ }
251
+ }
252
+
253
+ return clean;
254
+ } catch (e) {
255
+ return transUtilInst.checkAndThrow(e, origin, 'Issue validating actions');
256
+ }
257
+ }
258
+
259
+ /**
260
+ * @summary Method to check if the entity is in the cache
261
+ *
262
+ * @function isEntityCached
263
+ * @param {String} entityType - the entity type to check for
264
+ * @param {String/Array} entityId - the specific entity we are looking for
265
+ *
266
+ * @return {Array of Enumeration} - whether the entity was
267
+ * 'found' - entity was found
268
+ * 'notfound' - entity was not found
269
+ * 'needupdate' - update cache and try again
270
+ */
271
+ function isEntityCached(entityType, entityId) {
272
+ const origin = `${id}-requestHandler-isEntityCached`;
273
+ log.trace(origin);
274
+ let entityIds = entityId;
275
+ const results = [];
276
+
277
+ // go through the cache
278
+ if (cache[entityType]) {
279
+ const now = new Date().getTime();
280
+
281
+ // see if the cache is valid
282
+ if ((cache[entityType].updated) && (cache[entityType].updated >= now - 300000)) {
283
+ // entityId is not an Array, make it one
284
+ if (!Array.isArray(entityIds)) {
285
+ entityIds = [entityId];
286
+ }
287
+
288
+ for (let e = 0; e < entityIds.length; e += 1) {
289
+ // see if the device is in the cache
290
+ if (cache[entityType].list.includes(entityIds[e])) {
291
+ log.trace(`${origin}: Entity ${entityIds[e]} found in cache`);
292
+ results.push('found');
293
+ } else {
294
+ log.trace(`${origin}: Entity ${entityIds[e]} not found in cache`);
295
+ results.push('notfound');
296
+ }
297
+ }
298
+
299
+ return results;
300
+ }
301
+
302
+ log.warn(`${origin}: Entity Cache out of date`);
303
+ return ['needupdate'];
304
+ }
305
+
306
+ // Entity not found in cache
307
+ log.warn(`${origin}: Entity not in cache`);
308
+ return ['needupdate'];
309
+ }
310
+
311
+ /**
312
+ * @summary Method for metric db calls and tests
313
+ *
314
+ * @function dbCalls
315
+ * @param {String} entity - the entity to use. (required)
316
+ * @param {String} action - the action to use. (required)
317
+ * @param {Object} data - anything the user provides goes here. Possible tags:
318
+ * data = {
319
+ * code : <Number or String>,
320
+ * numRetries : <Number>,
321
+ * numRedirects : <Number>,
322
+ * isThrottling : <Boolean>,
323
+ * timeouts : <Number>,
324
+ * queueTime : <Number>,
325
+ * capabilityTime : <Number>,
326
+ * tripTime : <Number>,
327
+ * overallEnd : <Number>
328
+ * }
329
+ *
330
+ */
331
+ function dbCalls(collectionName, entity, action, data, callback) {
332
+ try {
333
+ // template_entity.createEntity logs a 201 response code which we don't track so code count doesn't show up in db.
334
+ const filter = { entity, action }; // hypothetically each doc has unique entity+action.
335
+ const edits = {
336
+ $inc: {
337
+ num_called: (data.code && (data.tripTime || data.adapterTime || data.capabilityTime)) ? 1 : 0,
338
+ numRetries: data.retries || 0,
339
+ numRedirects: data.redirects || 0,
340
+ throttleCount: (data.queueTime) ? 1 : 0, // separate thing to keep count of # throttles. may not be necessary.
341
+ timeouts: data.timeouts || 0,
342
+ tot_queue_time: parseFloat(data.queueTime) || 0,
343
+ tot_rnd_trip: parseFloat(data.tripTime) || 0, // = tot_rnd_trip
344
+ tot_library: parseFloat(data.capabilityTime) || 0, // = tot_library, overall things
345
+ tot_overall: parseFloat(data.overallTime) || 0, // = tot_library, overall things
346
+ ['results.'.concat(data.code)]: 1 // Note: this results in the JSON recording differing from DB record: JSON doesn't make a nested document, just "results.xxx".
347
+ // adapterTime: 0 // not accessible in this file, go to adapter.js later.tripTime
348
+ },
349
+ $set: {
350
+ time_units: 'ms',
351
+ entity,
352
+ action,
353
+ isThrottling: data.isThrottling
354
+ },
355
+ metric: {
356
+ entity,
357
+ action
358
+ }
359
+ };
360
+ // were are we writing? fs or db
361
+ return dbUtilInst.findAndModify(collectionName, filter, null, edits, true, null, true, (err, dbres) => {
362
+ if (err && !dbres) {
363
+ return callback(false);
364
+ }
365
+ return callback(true);
366
+ });
367
+ } catch (e) {
368
+ return callback(false);
369
+ }
370
+ }
371
+
372
+ class RequestHandler {
373
+ /**
374
+ * Request Handler
375
+ * @constructor
376
+ */
377
+ constructor(prongId, properties, directory) {
378
+ try {
379
+ this.myid = prongId;
380
+ id = prongId;
381
+ this.props = properties;
382
+ this.clean = [];
383
+ this.directory = directory;
384
+ this.suspend = false;
385
+ this.suspendInterval = 60000;
386
+
387
+ // need the db utilities before validation
388
+ this.dbUtil = new DBUtilCl(this.myid, properties, directory);
389
+ dbUtilInst = this.dbUtil;
390
+
391
+ // need the property utilities before validation
392
+ this.propUtil = new PropUtilCl(this.myid, directory, this.dbUtil);
393
+ propUtilInst = this.propUtil;
394
+
395
+ // reference to the needed classes for specific protocol handlers
396
+ this.transUtil = new TransUtilCl(prongId, this.propUtil);
397
+ transUtilInst = this.transUtil;
398
+
399
+ // validate the action files for the adapter
400
+ this.clean = walkThroughActionFiles(this.directory);
401
+
402
+ // save the adapter base directory
403
+ this.adapterBaseDir = directory;
404
+ this.clockInst = new AsyncLockCl();
405
+ clock = this.clockInst;
406
+
407
+ // set up the properties I care about
408
+ this.refreshProperties(properties);
409
+
410
+ // instantiate other runtime components
411
+ this.connector = new ConnectorCl(this.myid, this.props, this.transUtil, this.propUtil, this.dbUtil);
412
+ this.restHandler = new RestHandlerCl(this.myid, this.props, this.connector, this.transUtil);
413
+ } catch (e) {
414
+ // handle any exception
415
+ const origin = `${this.myid}-requestHandler-constructor`;
416
+ return this.transUtil.checkAndThrow(e, origin, 'Could not start Adapter Runtime Library');
417
+ }
418
+ }
419
+
420
+ /**
421
+ * @callback Callback
422
+ * @param {Object} result - the result of the get request
423
+ * @param {String} error - any error that occured
424
+ */
425
+
426
+ /**
427
+ * refreshProperties is used to set up all of the properties for the request handler.
428
+ * It allows properties to be changed later by simply calling refreshProperties rather
429
+ * than having to restart the request handler.
430
+ *
431
+ * @function refreshProperties
432
+ * @param {Object} properties - an object containing all of the properties
433
+ */
434
+ refreshProperties(properties) {
435
+ const origin = `${this.myid}-requestHandler-refreshProperties`;
436
+ log.trace(origin);
437
+
438
+ try {
439
+ // validate the properties that came in against library property schema
440
+ this.props = validateProperties(properties);
441
+
442
+ // get the list of failover codes
443
+ this.failoverCodes = [];
444
+
445
+ if (this.props.request && this.props.request.failover_codes
446
+ && Array.isArray(this.props.request.failover_codes)) {
447
+ this.failoverCodes = this.props.request.failover_codes;
448
+ }
449
+
450
+ // get the cache location
451
+ this.cacheLocation = 'local';
452
+
453
+ if (this.props.cache_location) {
454
+ this.cacheLocation = this.props.cache_location;
455
+ }
456
+
457
+ this.saveMetric = this.props.save_metric || false;
458
+
459
+ // set the username (required - default is null)
460
+ if (typeof this.props.authentication.username === 'string') {
461
+ username = this.props.authentication.username;
462
+ }
463
+
464
+ // if this is truly a refresh and we have a connector or rest handler, refresh them
465
+ if (this.connector) {
466
+ this.connector.refreshProperties(properties);
467
+ }
468
+ if (this.restHandler) {
469
+ this.restHandler.refreshProperties(properties);
470
+ }
471
+ } catch (e) {
472
+ // handle any exception
473
+ return this.transUtil.checkAndThrow(e, origin, 'Properties may not have been updated properly');
474
+ }
475
+ }
476
+
477
+ /**
478
+ * checkActionFiles is used to update the validation of the action files.
479
+ *
480
+ * @function checkActionFiles
481
+ */
482
+ checkActionFiles() {
483
+ const origin = `${this.myid}-requestHandler-checkActionFiles`;
484
+ log.trace(origin);
485
+
486
+ try {
487
+ // validate the action files for the adapter
488
+ this.clean = walkThroughActionFiles(this.directory);
489
+ return this.clean;
490
+ } catch (e) {
491
+ return ['Exception increase log level'];
492
+ }
493
+ }
494
+
495
+ /**
496
+ * checkProperties is used to validate the adapter properties.
497
+ *
498
+ * @function checkProperties
499
+ * @param {Object} properties - an object containing all of the properties
500
+ */
501
+ checkProperties(properties) {
502
+ const origin = `${this.myid}-requestHandler-checkProperties`;
503
+ log.trace(origin);
504
+
505
+ try {
506
+ // validate the action files for the adapter
507
+ this.testPropResult = validateProperties(properties);
508
+ return this.testPropResult;
509
+ } catch (e) {
510
+ return { exception: 'Exception increase log level' };
511
+ }
512
+ }
513
+
514
+ /**
515
+ * exposeDB is used to update the adapter metrics with the overall adapter time
516
+ *
517
+ * @function exposeDB
518
+ * @param {String} entity - the name of the entity for this request.
519
+ * (required)
520
+ * @param {String} action - the name of the action being executed. (required)
521
+ * @param {String} overallTime - the overall time in milliseconds (required)
522
+ */
523
+ exposeDB(entity, action, overallTime) {
524
+ const origin = `${this.myid}-requestHandler-exposeDB`;
525
+ log.trace(origin);
526
+
527
+ try {
528
+ // only allow the adapter.js to update the overallTime
529
+ const allowedData = {
530
+ overallTime
531
+ };
532
+ dbCalls('metrics', entity, action, allowedData, (status) => {
533
+ log.trace(`${origin}: ${status}`);
534
+ });
535
+
536
+ return true;
537
+ } catch (e) {
538
+ return false;
539
+ }
540
+ }
541
+
542
+ /**
543
+ * @summary Method that identifies the actual request to be made and then
544
+ * makes the call through the appropriate Handler
545
+ *
546
+ * @function identifyRequest
547
+ * @param {String} entity - the name of the entity for this request.
548
+ * (required)
549
+ * @param {String} action - the name of the action being executed. (required)
550
+ * @param {Object} requestObj - an object that contains all of the possible
551
+ * parts of the request (payload, uriPathVars,
552
+ * uriQuery, uriOptions and addlHeaders
553
+ * (optional). Can be a stringified Object.
554
+ * @param {Boolean} translate - whether to translate the response. Defaults
555
+ * to true. If no translation will just return
556
+ * 'success' or an error message
557
+ * @param {Callback} callback - a callback function to return the result
558
+ * Data/Status or the Error
559
+ */
560
+ identifyRequest(entity, action, requestObj, translate, callback) {
561
+ const meth = 'requestHandler-identifyRequest';
562
+ const origin = `${this.myid}-${meth}`;
563
+ log.trace(`${origin}: ${entity}-${action}`);
564
+ const overallTime = process.hrtime();
565
+
566
+ try {
567
+ // verify parameters passed are valid
568
+ if (entity === null || entity === '') {
569
+ const errorObj = this.formatErrorObject(this.myid, meth, 'Missing Data', ['entity'], null, null, null);
570
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
571
+ return callback(null, errorObj);
572
+ }
573
+ if (action === null || action === '') {
574
+ const errorObj = this.formatErrorObject(this.myid, meth, 'Missing Data', ['action'], null, null, null);
575
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
576
+ return callback(null, errorObj);
577
+ }
578
+
579
+ // Get the entity schema from the file system
580
+ return this.propUtil.getEntitySchema(entity, action, this.dbUtil, (entitySchema, entityError) => {
581
+ // verify protocol for call
582
+ if (entityError) {
583
+ const errorObj = this.transUtil.checkAndReturn(entityError, origin, 'Issue identifiying request');
584
+ return callback(null, errorObj);
585
+ }
586
+
587
+ // verify protocol for call
588
+ if (!Object.hasOwnProperty.call(entitySchema, 'protocol')) {
589
+ const errorObj = this.formatErrorObject(this.myid, meth, 'Missing Data', ['action protocol'], null, null, null);
590
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
591
+ return callback(null, errorObj);
592
+ }
593
+
594
+ // Determine the Protocol so the appropriate handler can be called
595
+ if (entitySchema.protocol.toUpperCase() === 'REST') {
596
+ return this.restHandler.genericRestRequest(entity, action, entitySchema, requestObj, translate, (result, error) => {
597
+ const overallDiff = process.hrtime(overallTime);
598
+ const overallEnd = `${Math.round(((overallDiff[0] * NS_PER_SEC) + overallDiff[1]) / 1000000)}ms`;
599
+ if (error) {
600
+ const newError = error;
601
+ if (!newError.metrics) {
602
+ newError.metrics = {};
603
+ }
604
+
605
+ newError.metrics.capabilityTime = overallEnd;
606
+ // console.log(newError); //can't test b/c no errors built into tests rn.
607
+ // will call from adapterFunction.ejs only eventually since it will have all metrics here + the 2 missing ones.
608
+ if (this.saveMetric) {
609
+ dbCalls('metrics', entity, action, newError.metrics, (saved) => {
610
+ if (saved) {
611
+ log.info(`${origin}: Metrics Saved`);
612
+ }
613
+ });
614
+ }
615
+ return callback(null, newError);
616
+ }
617
+
618
+ let newRes = result;
619
+ if (!newRes) {
620
+ newRes = {
621
+ metrics: {
622
+ }
623
+ };
624
+ } else if (!newRes.metrics) {
625
+ newRes.metrics = {};
626
+ }
627
+
628
+ newRes.metrics.capabilityTime = overallEnd;
629
+ // overallEnd is from start of identifyRequest to right before dbCalls is called (error or good).
630
+ // will call from adapterFunction.ejs only eventually since it will have all metrics here + the 2 missing ones.
631
+ if (this.saveMetric) {
632
+ dbCalls('metrics', entity, action, newRes.metrics, (saved) => {
633
+ if (saved) {
634
+ log.info(`${origin}: Metrics Saved`);
635
+ }
636
+ });
637
+ }
638
+ return callback(newRes);
639
+ });
640
+ }
641
+
642
+ // Unsupported protocols
643
+ const errorObj = this.formatErrorObject(this.myid, meth, 'Unsupported Protocol', [entitySchema.protocol], null, null, null);
644
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
645
+ return callback(null, errorObj);
646
+ });
647
+ } catch (e) {
648
+ // handle any exception
649
+ const errorObj = this.transUtil.checkAndReturn(e, origin, 'Issue identifiying request');
650
+ return callback(null, errorObj);
651
+ }
652
+ }
653
+
654
+ /**
655
+ * @summary Method that identifies the protocol for the healthcheck and then
656
+ * takes the appropriate action.
657
+ *
658
+ * @function identifyHealthcheck
659
+ * @param {Object} requestObj - an object that contains all of the possible
660
+ * parts of the request (payload, uriPathVars,
661
+ * uriQuery, uriOptions and addlHeaders
662
+ * (optional). Can be a stringified Object.
663
+ * @param {Callback} callback - a callback function to return the result of
664
+ * the Healthcheck
665
+ */
666
+ identifyHealthcheck(requestObj, callback) {
667
+ const meth = 'requestHandler-identifyHealthcheck';
668
+ const origin = `${this.myid}-${meth}`;
669
+ log.trace(origin);
670
+ const overallTime = process.hrtime();
671
+
672
+ try {
673
+ let prot = this.props.healthcheck.protocol;
674
+
675
+ // Get the entity schema from the file system
676
+ return this.propUtil.getEntitySchema('.system', 'healthcheck', this.dbUtil, (healthSchema, healthError) => {
677
+ if (healthError || !healthSchema || Object.keys(healthSchema).length === 0) {
678
+ log.debug(`${origin}: Using adapter properties for healthcheck information`);
679
+ } else {
680
+ log.debug(`${origin}: Using action and schema for healthcheck information`);
681
+ }
682
+
683
+ if (healthSchema && healthSchema.protocol) {
684
+ prot = healthSchema.protocol;
685
+ }
686
+
687
+ // Determine the Protocol so the appropriate handler can be called
688
+ if (prot.toUpperCase() === 'REST') {
689
+ return this.restHandler.healthcheckRest(healthSchema, requestObj, (result, error) => {
690
+ const overallDiff = process.hrtime(overallTime);
691
+ const overallEnd = `${Math.round(((overallDiff[0] * NS_PER_SEC) + overallDiff[1]) / 1000000)}ms`;
692
+ if (error) {
693
+ const newError = error;
694
+ if (!newError.metrics) {
695
+ newError.metrics = {};
696
+ }
697
+
698
+ newError.metrics.capabilityTime = overallEnd;
699
+ // console.log(newError); //can't test b/c no errors built into tests rn.
700
+ // will call from adapterFunction.ejs only eventually since it will have all metrics here + the 2 missing ones.
701
+ if (this.saveMetric) {
702
+ dbCalls('metrics', '.system', 'healthcheck', newError.metrics, (saved) => {
703
+ if (saved) {
704
+ log.info(`${origin}: Metrics Saved`);
705
+ }
706
+ });
707
+ }
708
+ return callback(null, newError);
709
+ }
710
+
711
+ let newRes = result;
712
+ if (!newRes) {
713
+ newRes = {
714
+ metrics: {
715
+ }
716
+ };
717
+ } else if (!newRes.metrics) {
718
+ newRes.metrics = {};
719
+ }
720
+
721
+ newRes.metrics.capabilityTime = overallEnd;
722
+ // overallEnd is from start of identifyRequest to right before dbCalls is called (error or good).
723
+ // will call from adapterFunction.ejs only eventually since it will have all metrics here + the 2 missing ones.
724
+ if (this.saveMetric) {
725
+ dbCalls('metrics', '.system', 'healthcheck', newRes.metrics, (saved) => {
726
+ if (saved) {
727
+ log.info(`${origin}: Metrics Saved`);
728
+ }
729
+ });
730
+ }
731
+ return callback(newRes);
732
+ });
733
+ }
734
+
735
+ // Unsupported protocols
736
+ const errorObj = this.formatErrorObject(this.myid, meth, 'Unsupported Protocol', [prot], null, null, null);
737
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
738
+ return callback(null, errorObj);
739
+ });
740
+ } catch (e) {
741
+ // handle any exception
742
+ const errorObj = this.transUtil.checkAndReturn(e, origin, 'Issue identifiying healthcheck');
743
+ return callback(null, errorObj);
744
+ }
745
+ }
746
+
747
+ /**
748
+ * getQueue is used to get information for all of the requests currently in the queue.
749
+ *
750
+ * @function getQueue
751
+ * @param {queueCallback} callback - a callback function to return the result (Queue)
752
+ * or the error
753
+ */
754
+ getQueue(callback) {
755
+ const meth = 'requestHandler-getQueue';
756
+ const origin = `${this.myid}-${meth}`;
757
+ log.trace(origin);
758
+
759
+ try {
760
+ // Determine the Protocol so the appropriate handler can be called
761
+ if (this.props.healthcheck.protocol.toUpperCase() === 'REST') {
762
+ return this.restHandler.getQueue(callback);
763
+ }
764
+
765
+ // Unsupported protocols
766
+ const errorObj = this.formatErrorObject(this.myid, meth, 'Unsupported Protocol', [this.props.healthcheck.protocol], null, null, null);
767
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
768
+ return callback(null, errorObj);
769
+ } catch (e) {
770
+ // handle any exception
771
+ const errorObj = this.transUtil.checkAndReturn(e, origin, 'Issue getting queue');
772
+ return callback(null, errorObj);
773
+ }
774
+ }
775
+
776
+ /**
777
+ * @summary Takes in property text and an encoding/encryption and returns
778
+ * the resulting encoded/encrypted string
779
+ *
780
+ * @function encryptProperty
781
+ * @param {String} property - the property to encrypt
782
+ * @param {String} technique - the technique to use to encrypt
783
+ *
784
+ * @param {Callback} callback - a callback function to return the result
785
+ * Encrypted String or the Error
786
+ */
787
+ encryptProperty(property, technique, callback) {
788
+ const meth = 'requestHandler-encryptProperty';
789
+ const origin = `${this.myid}-${meth}`;
790
+ log.trace(origin);
791
+
792
+ try {
793
+ const returnObj = {
794
+ icode: 'AD.200'
795
+ };
796
+
797
+ returnObj.response = this.propUtil.encryptProperty(property, technique);
798
+ return callback(returnObj);
799
+ } catch (e) {
800
+ // handle any exception
801
+ const errorObj = this.transUtil.checkAndReturn(e, origin, 'Issue encrypting property');
802
+ return callback(null, errorObj);
803
+ }
804
+ }
805
+
806
+ /**
807
+ * @summary Build a standard error object from the data provided
808
+ *
809
+ * @function formatErrorObject
810
+ * @param {String} adaptId - the id of the adapter (required).
811
+ * @param {String} origin - the originator of the error (required).
812
+ * @param {String} failCode - the internal IAP error code or error type (required).
813
+ * @param {Integer} sysCode - the error code from the other system (optional).
814
+ * @param {Object} sysRes - the raw response from the other system (optional).
815
+ * @param {Exception} stack - tany available stack trace from the issue (optional).
816
+ *
817
+ * @return {Object} the error object, null if missing pertinent information
818
+ */
819
+ formatErrorObject(adaptId, origin, failCode, variables, sysCode, sysRes, stack) {
820
+ const morigin = `${this.myid}-requestHandler-formatErrorObject`;
821
+ log.trace(morigin);
822
+
823
+ // this is just a pass through for clients to use - rather then expose translator
824
+ return this.transUtil.formatErrorObject(`${adaptId}-${origin}`, failCode, variables, sysCode, sysRes, stack);
825
+ }
826
+
827
+ /**
828
+ * @summary Determines whether the error is one that can be failed over to
829
+ * the next adapter to handle.
830
+ *
831
+ * @function setFailover
832
+ * @param {Object} errorObj - the error object
833
+ *
834
+ * @return {Enumeration} failoverCode - a string containing the failover code
835
+ * 'AD.300' - failover OK
836
+ * 'AD.500' - no failover
837
+ * code from response if can not decide
838
+ */
839
+ setFailover(errorObj) {
840
+ const origin = `${this.myid}-requestHandler-setFailover`;
841
+ log.trace(origin);
842
+
843
+ try {
844
+ // if the errorMsg exists and has a code, check it
845
+ if (errorObj && errorObj.code) {
846
+ if (this.failoverCodes) {
847
+ // is it a code that we allow failover?
848
+ for (let f = 0; f < this.failoverCodes.length; f += 1) {
849
+ if (errorObj.code === this.failoverCodes[f]) {
850
+ return allowFailover;
851
+ }
852
+ }
853
+ }
854
+ }
855
+
856
+ // if not resolved based on code, return what was in the error if provided
857
+ if (errorObj && errorObj.icode) {
858
+ return errorObj.icode;
859
+ }
860
+
861
+ // return the default result
862
+ return noFailover;
863
+ } catch (e) {
864
+ log.error(`${origin}: Caught Exception ${e}`);
865
+ return noFailover;
866
+ }
867
+ }
868
+
869
+ /**
870
+ * @summary Check the current cache to see if we know about a specific entity
871
+ *
872
+ * @function isEntityCached
873
+ * @param {String} entityType - the entity type to check for
874
+ * @param {String/Array} entityId - the specific entity we are looking for
875
+ *
876
+ * @return {Array of Enumeration} - whether the entity was
877
+ * 'found' - entity was found
878
+ * 'notfound' - entity was not found
879
+ * 'error' - there was an error - check logs
880
+ * 'needupdate' - update cache and try again
881
+ */
882
+ checkEntityCached(entityType, entityId, callback) {
883
+ const origin = `${this.myid}-requestHandler-checkEntityCached`;
884
+ log.trace(origin);
885
+
886
+ try {
887
+ if (this.cacheLocation === 'redis') {
888
+ const ckey = `${this.myid}__%%__cache`;
889
+
890
+ // get the cache from redis
891
+ return g_redis.get(ckey, (err, res) => {
892
+ if (err) {
893
+ log.error(`${origin}: Error on retrieve cache for ${ckey}`);
894
+ return callback(['error']);
895
+ }
896
+
897
+ // if there was no cache returned
898
+ if (!res) {
899
+ log.error(`${origin}: No cache for ${ckey}`);
900
+ return callback(['needupdate']);
901
+ }
902
+
903
+ // set the local cache to what we got from redis (temp storage)
904
+ cache = res;
905
+ return callback(isEntityCached(entityType, entityId));
906
+ });
907
+ }
908
+
909
+ return callback(isEntityCached(entityType, entityId));
910
+ } catch (e) {
911
+ log.error(`${origin}: Caught Exception ${e}`);
912
+ return callback(['error']);
913
+ }
914
+ }
915
+
916
+ /**
917
+ * @summary Adds the provided entity list to the cache
918
+ *
919
+ * @function addEntityCache
920
+ * @param {String} entityType - the entity type for the list
921
+ * @param {Array} entities - the list of entities to be added
922
+ *
923
+ * @param {Callback} callback - whether the cache was updated
924
+ */
925
+ addEntityCache(entityType, entities, callback) {
926
+ const meth = 'requestHandler-addEntityCache';
927
+ const origin = `${this.myid}-${meth}`;
928
+ log.trace(origin);
929
+ let storeEnt = entities;
930
+
931
+ if (!Array.isArray(entities)) {
932
+ storeEnt = [entities];
933
+ }
934
+
935
+ const entityEntry = {
936
+ list: storeEnt,
937
+ updated: new Date().getTime()
938
+ };
939
+
940
+ try {
941
+ // Lock the cache while adding items to it
942
+ return clock.acquire(cachelock, (done) => {
943
+ // if only storing locally, done
944
+ if (this.cacheLocation === 'local') {
945
+ // add the entities to the cache
946
+ cache[entityType] = entityEntry;
947
+ done(true, null);
948
+ } else {
949
+ // set the redis key
950
+ const ckey = `${this.myid}__%%__cache`;
951
+
952
+ // get the cache from redis
953
+ g_redis.get(ckey, (err, res) => {
954
+ if (err) {
955
+ log.error(`${origin}: Error on retrieve cache for ${ckey}`);
956
+ done(false, null);
957
+ } else {
958
+ cache = res;
959
+
960
+ // if no cache was found
961
+ if (!cache) {
962
+ cache = {};
963
+ }
964
+
965
+ // add the entities to the cache
966
+ cache[entityType] = entityEntry;
967
+
968
+ // store the cache in redis
969
+ g_redis.set(ckey, JSON.stringify(cache), (error) => {
970
+ if (error) {
971
+ log.error(`${origin}: Cache: ${ckey} not stored in redis`);
972
+ done(false, null);
973
+ } else {
974
+ done(true, null);
975
+ }
976
+ });
977
+ }
978
+ });
979
+ }
980
+ }, (ret, error) => {
981
+ if (error) {
982
+ log.error(`${origin}: Error from retrieving entity cache: ${error}`);
983
+ }
984
+
985
+ return callback(ret, error);
986
+ });
987
+ } catch (e) {
988
+ // handle any exception
989
+ const errorObj = this.transUtil.checkAndReturn(e, origin, 'Issue adding entity to cache');
990
+ return callback(null, errorObj);
991
+ }
992
+ }
993
+
994
+ /**
995
+ * @summary Provides a way for the adapter to tell north bound integrations
996
+ * whether the adapter supports type and specific entity
997
+ *
998
+ * @function verifyCapability
999
+ * @param {String} entityType - the entity type to check for
1000
+ * @param {String} actionType - the action type to check for
1001
+ * @param {String/Array} entityId - the specific entity we are looking for
1002
+ *
1003
+ * @return {Array of Enumeration} - whether the entity was
1004
+ * 'found' - entity was found
1005
+ * 'notfound' - entity was not found
1006
+ * 'error' - there was an error - check logs
1007
+ * 'needupdate' - update cache and try again
1008
+ */
1009
+ verifyCapability(entityType, actionType, entityId, callback) {
1010
+ const origin = `${this.myid}-requestHandler-verifyCapability`;
1011
+ log.trace(origin);
1012
+ const entitiesD = `${this.adapterBaseDir}/entities`;
1013
+
1014
+ try {
1015
+ // verify the entities directory exists
1016
+ if (!fs.existsSync(entitiesD)) {
1017
+ log.error(`${origin}: Missing ${entitiesD} directory - Aborting!`);
1018
+ return callback(['error']);
1019
+ }
1020
+
1021
+ // get the entities this adapter supports
1022
+ const entities = fs.readdirSync(entitiesD);
1023
+
1024
+ // go through the entities
1025
+ for (let e = 0; e < entities.length; e += 1) {
1026
+ // did we find the entity type?
1027
+ if (entities[e] === entityType) {
1028
+ // if we are only interested in the entity
1029
+ if (!actionType && !entityId) {
1030
+ return callback(['found']);
1031
+ }
1032
+ // if we do not have an action, check for the specific entity
1033
+ if (!actionType) {
1034
+ return this.checkEntityCached(entityType, entityId, callback);
1035
+ }
1036
+
1037
+ // get the entity actions from the action file
1038
+ const actionFile = `${entitiesD}/${entities[e]}/action.json`;
1039
+ if (fs.existsSync(actionFile)) {
1040
+ const actionJson = require(actionFile);
1041
+ const { actions } = actionJson;
1042
+
1043
+ // go through the actions for a match
1044
+ for (let a = 0; a < actions.length; a += 1) {
1045
+ if (actions[a].name === actionType) {
1046
+ // if we are not interested in a specific entity
1047
+ if (!entityId) {
1048
+ return callback(['found']);
1049
+ }
1050
+ return this.checkEntityCached(entityType, entityId, callback);
1051
+ }
1052
+ }
1053
+
1054
+ log.warn(`${origin}: Action ${actionType} not found on entity ${entityType}`);
1055
+
1056
+ // return an array for the entityids since can not act on any
1057
+ const result = ['notfound'];
1058
+ if (entityId && Array.isArray(entityId)) {
1059
+ // add not found for each entity, (already added the first)
1060
+ for (let r = 1; r < entityId.length; r += 1) {
1061
+ result.push('notfound');
1062
+ }
1063
+ }
1064
+ return callback(result);
1065
+ }
1066
+
1067
+ log.error(`${origin}: Action ${actionType} on entity ${entityType} missing action file`);
1068
+ return callback(['error']);
1069
+ }
1070
+ }
1071
+
1072
+ log.error(`${origin}: Entity ${entityType} not found in adapter`);
1073
+
1074
+ // return an array for the entityids since can not act on any
1075
+ const result = ['notfound'];
1076
+ if (entityId && Array.isArray(entityId)) {
1077
+ // add not found for each entity, (already added the first)
1078
+ for (let r = 1; r < entityId.length; r += 1) {
1079
+ result.push('notfound');
1080
+ }
1081
+ }
1082
+
1083
+ return callback(result);
1084
+ } catch (e) {
1085
+ log.error(`${origin}: Caught Exception ${e}`);
1086
+ return callback(['error']);
1087
+ }
1088
+ }
1089
+
1090
+ /**
1091
+ * @summary Provides a way for the adapter to tell north bound integrations
1092
+ * all of the capabilities for the current adapter
1093
+ *
1094
+ * @function getAllCapabilities
1095
+ *
1096
+ * @return {Array} - containing the entities and the actions available on each entity
1097
+ */
1098
+ getAllCapabilities() {
1099
+ const origin = `${this.myid}-requestHandler-getAllCapabilities`;
1100
+ log.trace(origin);
1101
+ const entitiesD = `${this.adapterBaseDir}/entities`;
1102
+ const capabilities = [];
1103
+
1104
+ try {
1105
+ // verify the entities directory exists
1106
+ if (!fs.existsSync(entitiesD)) {
1107
+ log.error(`${origin}: Missing ${entitiesD} directory`);
1108
+ return capabilities;
1109
+ }
1110
+
1111
+ // get the entities this adapter supports
1112
+ const entities = fs.readdirSync(entitiesD);
1113
+
1114
+ // go through the entities
1115
+ for (let e = 0; e < entities.length; e += 1) {
1116
+ // get the entity actions from the action file
1117
+ const actionFile = `${entitiesD}/${entities[e]}/action.json`;
1118
+ const entityActions = [];
1119
+
1120
+ if (fs.existsSync(actionFile)) {
1121
+ const actionJson = require(actionFile);
1122
+ const { actions } = actionJson;
1123
+
1124
+ // go through the actions for a match
1125
+ for (let a = 0; a < actions.length; a += 1) {
1126
+ entityActions.push(actions[a].name);
1127
+ }
1128
+ }
1129
+
1130
+ const newEntity = {
1131
+ entity: entities[e],
1132
+ actions: entityActions
1133
+ };
1134
+
1135
+ capabilities.push(newEntity);
1136
+ }
1137
+
1138
+ return capabilities;
1139
+ } catch (e) {
1140
+ log.error(`${origin}: Caught Exception ${e}`);
1141
+ return capabilities;
1142
+ }
1143
+ }
1144
+
1145
+ /**
1146
+ * @summary get a token with the provide parameters
1147
+ *
1148
+ * @function makeTokenRequest
1149
+ * @param {Object} reqBody - Any data to add to the body of the token request
1150
+ * @param {Object} callProperties - Properties that override the default properties
1151
+ *
1152
+ * @return {Object} - containing the token(s)
1153
+ */
1154
+ makeTokenRequest(reqBody, callProperties, callback) {
1155
+ const origin = `${this.myid}-requestHandler-makeTokenRequest`;
1156
+ log.trace(origin);
1157
+
1158
+ try {
1159
+ // set up the right credentials - passed in overrides default
1160
+ let useUser = username;
1161
+ if (callProperties && callProperties.authentication && callProperties.authentication.username) {
1162
+ useUser = callProperties.authentication.username;
1163
+ }
1164
+
1165
+ // validate the action files for the adapter
1166
+ return this.connector.makeTokenRequest('/none/token/path', useUser, reqBody, null, callProperties, callback);
1167
+ } catch (e) {
1168
+ // handle any exception
1169
+ const errorObj = this.transUtil.checkAndReturn(e, origin, 'Issue getting token');
1170
+ return callback(null, errorObj);
1171
+ }
1172
+ }
1173
+ }
1174
+
1175
+ module.exports = RequestHandler;