@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,1137 @@
1
+ /* @copyright Itential, LLC 2018-9 */
2
+
3
+ // Set globals
4
+ /* global log */
5
+ /* eslint global-require:warn */
6
+ /* eslint import/no-dynamic-require:warn */
7
+ /* eslint no-use-before-define: warn */
8
+ /* eslint prefer-object-spread:warn */
9
+ /* eslint prefer-destructuring:warn */
10
+
11
+ /* NodeJS internal utilities */
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ // The schema validator
16
+ const AjvCl = require('ajv');
17
+ const jsonQuery = require('json-query');
18
+ const cryptoJS = require('crypto-js');
19
+
20
+ let id = null;
21
+ let propUtilInst = null;
22
+ let translator = null;
23
+
24
+ /**
25
+ * @summary Takes in text and a key, encodes or if key then encrypts and returns the resulting
26
+ * encoded/encrypted string
27
+ *
28
+ * @function encrypt
29
+ * @param {String} value - the text to encrypt (required)
30
+ * @param {Object} eInfo - the encryption information (optional)
31
+ *
32
+ * @return {String} the encrypted/encoded string
33
+ */
34
+ function encrypt(value, eInfo) {
35
+ const origin = `${id}-translatorUtil-encrypt`;
36
+ log.trace(origin);
37
+
38
+ try {
39
+ // verify the input for the method
40
+ if (!value) {
41
+ return null;
42
+ }
43
+
44
+ // if encrypting, return the encrypted string
45
+ if (eInfo && eInfo.key) {
46
+ // this is being added to support different types of encryption
47
+ if (eInfo.type) {
48
+ return `${cryptoJS.AES.encrypt(value, eInfo.key)}`;
49
+ }
50
+
51
+ return `${cryptoJS.AES.encrypt(value, eInfo.key)}`;
52
+ }
53
+
54
+ // if encoding, return the encoded string
55
+ return `${Buffer.from(value).toString('base64')}`;
56
+ } catch (e) {
57
+ log.error(`${origin}: Encyrpt took exception: ${e}`);
58
+ return null;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * @summary Takes in encrypted or encoded text and decodes/decrypts it to return
64
+ * the actual text
65
+ *
66
+ * @function decrypt
67
+ * @param {String} value - the text to decrypt (required)
68
+ * @param {Object} eInfo - the encryption information (optional)
69
+ *
70
+ * @return {String} the string
71
+ */
72
+ function decrypt(value, eInfo) {
73
+ const origin = `${id}-translatorUtil-decrypt`;
74
+ log.trace(origin);
75
+
76
+ try {
77
+ // verify the input for the method
78
+ if (!value) {
79
+ return null;
80
+ }
81
+
82
+ // if decrypting, return the decrypted string
83
+ if (eInfo && eInfo.key) {
84
+ // this is being added to support different types of encryption
85
+ if (eInfo.type) {
86
+ return cryptoJS.AES.decrypt(value, eInfo.key).toString(cryptoJS.enc.Utf8);
87
+ }
88
+
89
+ return cryptoJS.AES.decrypt(value, eInfo.key).toString(cryptoJS.enc.Utf8);
90
+ }
91
+
92
+ // if decoding, return the decoded string
93
+ return `${Buffer.from(value, 'base64').toString('ascii')}`;
94
+ } catch (e) {
95
+ log.error(`${origin}: Decrypt took exception: ${e}`);
96
+ return null;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * @summary Takes in an object and returns the value. If it is an Array, it
102
+ * returns the first element in the array. It returns null if it can not get
103
+ * the value from the object
104
+ *
105
+ * @function getValueFromObject
106
+ * @param {Object} object - the object to extract the data from
107
+ * @param {Object} dataSchema - the schema of data to extract
108
+ * @param {boolean} request - whether this is an outbound request
109
+ * @param {Boolean} dynamicFields - do we show fields not in schema
110
+ *
111
+ * @return {Value} the value we want (String, Boolean or Number)
112
+ */
113
+ function getValueFromObject(object, dataSchema, request, dynamicFields) {
114
+ const origin = `${id}-translatorUtil-getValueFromObject`;
115
+ log.trace(origin);
116
+
117
+ // if no value found - no object
118
+ if (object === undefined || object === null) {
119
+ log.spam(`${origin}: No object returning null`);
120
+ return null;
121
+ }
122
+
123
+ // get the type of data we are working with
124
+ let type = null;
125
+
126
+ if (dataSchema && dataSchema.type && !Array.isArray(dataSchema.type)) {
127
+ type = dataSchema.type.toLowerCase();
128
+ } else if (dataSchema && dataSchema.type && Array.isArray(dataSchema.type)) {
129
+ if (dataSchema.type.length === 1) {
130
+ type = dataSchema.type[0].toLowerCase();
131
+ } else if (dataSchema.type.length > 1) {
132
+ if (typeof object === 'string' && dataSchema.type.toString().toLowerCase().indexOf('string') > -1) {
133
+ type = 'string';
134
+ } else if (typeof object === 'number' && dataSchema.type.toString().toLowerCase().indexOf('number') > -1) {
135
+ type = 'number';
136
+ } else if (typeof object === 'number' && dataSchema.type.toString().toLowerCase().indexOf('integer') > -1) {
137
+ type = 'integer';
138
+ } else if (typeof object === 'boolean' && dataSchema.type.toString().toLowerCase().indexOf('boolean') > -1) {
139
+ type = 'boolean';
140
+ } else if (Array.isArray(object) && dataSchema.type.toString().toLowerCase().indexOf('array') > -1) {
141
+ type = 'array';
142
+ } else if (typeof object === 'object' && dataSchema.type.toString().toLowerCase().indexOf('object') > -1) {
143
+ type = 'object';
144
+ }
145
+ }
146
+ }
147
+
148
+ // if there is no type on the data, just send it through
149
+ if (!type) {
150
+ return object;
151
+ }
152
+
153
+ // if we are supposed to extract a number, boolean or string
154
+ if (type === 'number' || type === 'integer' || type === 'boolean' || type === 'string') {
155
+ // if data is an array, just return the first object
156
+ if (Array.isArray(object)) {
157
+ if (object.length === 0 || object[0] === '') {
158
+ log.spam(`${origin}: No value returning null`);
159
+ return null;
160
+ }
161
+
162
+ return object[0];
163
+ }
164
+
165
+ if (typeof object === 'string' && object === '') {
166
+ log.spam(`${origin}: Empty value returning null`);
167
+ return null;
168
+ }
169
+
170
+ return object;
171
+ }
172
+
173
+ // if extracting an array, need recursion to handle elements in the array
174
+ if (type === 'array') {
175
+ // if data is not an array - then can not work an array
176
+ if (!Array.isArray(object)) {
177
+ log.spam(`${origin}: Data is not an array returning null`);
178
+ return null;
179
+ }
180
+
181
+ const returnArr = [];
182
+
183
+ // loop through all of the elements in the array
184
+ for (let k = 0; k < object.length; k += 1) {
185
+ // if the array is supposed to have items (obect within it)
186
+ if (dataSchema.items) {
187
+ // recursive call to get array elements
188
+ const fieldValue = getValueFromObject(object[k], dataSchema.items, request, dynamicFields);
189
+
190
+ // if data to return, add to the array being returned
191
+ if (fieldValue !== null) {
192
+ returnArr.push(fieldValue);
193
+ }
194
+ } else {
195
+ // just add elements to the array to be returned
196
+ returnArr.push(object[k]);
197
+ }
198
+ }
199
+
200
+ // if nothing in the array return null
201
+ if (returnArr.length === 0) {
202
+ log.spam(`${origin}: No data to return returning null`);
203
+ return null;
204
+ }
205
+
206
+ return returnArr;
207
+ }
208
+
209
+ // if extracting an object, need recursion to handle properties in the object
210
+ if (type === 'object') {
211
+ let returnObj = {};
212
+
213
+ if (request) {
214
+ returnObj = buildObject(object, dataSchema, dynamicFields);
215
+ } else {
216
+ returnObj = extractObject(object, dataSchema, dynamicFields);
217
+ }
218
+
219
+ // if nothing in the array return null
220
+ if (Object.keys(returnObj).length === 0) {
221
+ return {};
222
+ }
223
+
224
+ return returnObj;
225
+ }
226
+
227
+ // unsupported type - return null
228
+ log.spam(`${origin}: Unsupported data type returning null`);
229
+ return null;
230
+ }
231
+
232
+ /**
233
+ * INTERNAL FUNCTION
234
+ *
235
+ * @summary Takes in a JSON object containing an entity it then extracts the info
236
+ * IAP cares about into a IAP Entity
237
+ *
238
+ * @function extractObject
239
+ * @param {Object} dataObj - the object from the other system
240
+ * @param {String} entitySchema - the entity schema
241
+ * @param {Boolean} dynamicFields - do we show fields not in schema
242
+ *
243
+ * @return {Object} the IAP Entity
244
+ */
245
+ function extractObject(dataObj, entitySchema, dynamicFields) {
246
+ const origin = `${id}-translatorUtil-extractObject`;
247
+ log.trace(origin);
248
+ const returnObj = {};
249
+ let addFields = dynamicFields;
250
+
251
+ // if no translation needed - just return the object
252
+ if (Object.hasOwnProperty.call(entitySchema, 'translate')
253
+ && typeof entitySchema.translate === 'boolean' && entitySchema.translate === false) {
254
+ return dataObj;
255
+ }
256
+ // Should allow dymanic fields on this object? - change to inherited
257
+ if (Object.hasOwnProperty.call(entitySchema, 'dynamicfields')
258
+ && typeof entitySchema.dynamicfields === 'boolean') {
259
+ addFields = entitySchema.dynamicfields;
260
+ }
261
+ // if there are no properties - if addFields is true return object
262
+ if (!entitySchema.properties && (addFields)) {
263
+ return dataObj;
264
+ }
265
+ // if there are no properties and no dynamic fields, return null
266
+ if (!entitySchema.properties) {
267
+ return returnObj;
268
+ }
269
+
270
+ const schemaKeys = Object.keys(entitySchema.properties);
271
+
272
+ // loop through all of the properties in the schema
273
+ for (let k = 0; k < schemaKeys.length; k += 1) {
274
+ const field = entitySchema.properties[schemaKeys[k]];
275
+
276
+ // if this property has an external name, need to see if in the data object
277
+ if (Object.hasOwnProperty.call(field, 'external_name')) {
278
+ // if the external name is something get that field
279
+ if (field.external_name) {
280
+ // need to determine the field in the incoming object where the data is
281
+ const externalPath = field.external_name.split('.');
282
+ let location = dataObj;
283
+ let inField = null;
284
+
285
+ // get to the field in the object
286
+ for (let a = 0; a < externalPath.length; a += 1) {
287
+ // if we are at the point to get the data, get the data
288
+ if (a === externalPath.length - 1) {
289
+ inField = location[externalPath[a]];
290
+ } else if (location[externalPath[a]]) {
291
+ // walk down the object path, if it exists
292
+ location = location[externalPath[a]];
293
+ } else {
294
+ // if the path does not exist, break the loop and value is null
295
+ break;
296
+ }
297
+ }
298
+
299
+ // get the field value from the data so it can be put in the return
300
+ let fieldValue = getValueFromObject(inField, field, false, addFields);
301
+
302
+ // if we are decoding/decrypting the value
303
+ if (field.encode || (field.encrypt && field.encrypt.key)) {
304
+ fieldValue = decrypt(fieldValue, field.encrypt);
305
+ }
306
+
307
+ // if in the data object, add to the IAP entity
308
+ if (fieldValue !== null) {
309
+ if (field.respFilter) {
310
+ returnObj[schemaKeys[k]] = jsonQuery(field.respFilter, { data: fieldValue }).value;
311
+ } else {
312
+ returnObj[schemaKeys[k]] = fieldValue;
313
+ }
314
+ }
315
+ }
316
+ // if the external field is null or empty then we ignore the field!
317
+ } else {
318
+ // if the field does not have an external name then use field key
319
+ let fieldValue = getValueFromObject(dataObj[schemaKeys[k]], field, false, addFields);
320
+
321
+ // if we are decoding/decrypting the value
322
+ if (field.encode || (field.encrypt && field.encrypt.key)) {
323
+ fieldValue = decrypt(fieldValue, field.encrypt);
324
+ }
325
+
326
+ // if in the data object, add to the IAP entity
327
+ if (fieldValue !== null) {
328
+ if (field.respFilter) {
329
+ returnObj[schemaKeys[k]] = jsonQuery(field.respFilter, { data: fieldValue }).value;
330
+ } else {
331
+ returnObj[schemaKeys[k]] = fieldValue;
332
+ }
333
+ }
334
+ }
335
+ }
336
+
337
+ // if we should allow dymanic fields on this object, add the fields
338
+ if (addFields) {
339
+ const objectKeys = Object.keys(dataObj);
340
+
341
+ // loop through all of the fields in the object
342
+ for (let o = 0; o < objectKeys.length; o += 1) {
343
+ let found = false;
344
+
345
+ // loop through all of the properties in the schema
346
+ for (let en = 0; en < schemaKeys.length; en += 1) {
347
+ const field = entitySchema.properties[schemaKeys[en]];
348
+
349
+ // if the field is not in the schema - we need to add it
350
+ // using the field name that came in since no translation
351
+ if (field.external_name) {
352
+ const externalPath = field.external_name.split('.');
353
+
354
+ if (externalPath[externalPath.length - 1] === objectKeys[o]) {
355
+ found = true;
356
+ }
357
+ } else if (schemaKeys[en] === objectKeys[o]) {
358
+ found = true;
359
+ }
360
+ }
361
+
362
+ // if not found, add it
363
+ if (!found) {
364
+ returnObj[objectKeys[o]] = dataObj[objectKeys[o]];
365
+ }
366
+ }
367
+ }
368
+
369
+ // return the resulting IAP entity
370
+ return returnObj;
371
+ }
372
+
373
+ /**
374
+ * @summary Checks to see if any fields in the schema need to be parsed
375
+ *
376
+ * @function parseFields
377
+ * @param {Object} retObject - the object to extract the data from
378
+ * @param {Array} dataSchema - the schema of data to extract
379
+ *
380
+ * @return {Object} the return object with parsed data
381
+ */
382
+ function parseFields(retObject, dataSchema) {
383
+ const origin = `${id}-translatorUtil-parseFields`;
384
+ log.trace(origin);
385
+ const myReturn = retObject;
386
+ const schemaKeys = Object.keys(dataSchema);
387
+
388
+ // loop through all of the properties in the schema
389
+ for (let k = 0; k < schemaKeys.length; k += 1) {
390
+ const field = dataSchema[schemaKeys[k]];
391
+
392
+ // if object or array need rercursion
393
+ if (field.type === 'object' && field.properties && myReturn && myReturn[schemaKeys[k]]) {
394
+ myReturn[schemaKeys[k]] = parseFields(myReturn[schemaKeys[k]], field.properties);
395
+ }
396
+ if (field.type === 'array' && field.items && myReturn && myReturn[schemaKeys[k]]) {
397
+ myReturn[schemaKeys[k]] = parseFields(myReturn[schemaKeys[k]], field.items);
398
+ }
399
+
400
+ // if string and we need to parse this field
401
+ if (field.type === 'string' && field.parse && myReturn && myReturn[schemaKeys[k]]) {
402
+ try {
403
+ myReturn[schemaKeys[k]] = JSON.parse(myReturn[schemaKeys[k]]);
404
+ } catch (ex) {
405
+ log.warn(`${origin}: Could not parse data in field`);
406
+ }
407
+ }
408
+ }
409
+ return myReturn;
410
+ }
411
+
412
+ /**
413
+ * INTERNAL FUNCTION
414
+ *
415
+ * @summary Takes in a JSON object containing an entity it then extracts the info
416
+ * IAP cares about into a IAP Entity. This object is then merged with
417
+ * default data and validated against the provided entity schema.
418
+ *
419
+ * @function extractJSONEntity
420
+ * @param {Object} dataObj - the object from the other system
421
+ * @param {String} entitySchema - the entity schema
422
+ *
423
+ * @return {Object} the IAP Entity
424
+ */
425
+ function extractJSONEntity(dataObj, entitySchema) {
426
+ const origin = `${id}-translatorUtil-extractJSONEntity`;
427
+ log.trace(origin);
428
+ const returnObj = extractObject(dataObj, entitySchema, false);
429
+
430
+ try {
431
+ // add any defaults to the data
432
+ let combinedEntity = propUtilInst.mergeProperties(returnObj, propUtilInst.setDefaults(entitySchema));
433
+
434
+ // validate the entity against the schema
435
+ const ajvInst = new AjvCl();
436
+ const validate = ajvInst.compile(entitySchema);
437
+ const result = validate(combinedEntity);
438
+
439
+ // if invalid properties throw an error
440
+ if (!result) {
441
+ // create the generic part of an error object
442
+ const errorObj = {
443
+ origin,
444
+ type: 'Schema Validation Failure',
445
+ vars: [validate.errors[0].message]
446
+ };
447
+
448
+ // log and throw the error
449
+ log.error(`${origin}: Schema validation failure ${validate.errors[0].message}`);
450
+ throw new Error(JSON.stringify(errorObj));
451
+ }
452
+
453
+ // if IAP request type exists, remove it (internal use only)
454
+ if (combinedEntity.ph_request_type) {
455
+ delete combinedEntity.ph_request_type;
456
+ }
457
+
458
+ // see if we need to parse fields
459
+ // only if translating
460
+ if (entitySchema.translate) {
461
+ // must have properties
462
+ if (entitySchema.properties) {
463
+ // get the schema keys
464
+ combinedEntity = parseFields(combinedEntity, entitySchema.properties);
465
+ }
466
+ }
467
+
468
+ // return the resulting IAP entity
469
+ return combinedEntity;
470
+ } catch (e) {
471
+ return translator.checkAndThrow(e, origin, 'Issue extracting JSON');
472
+ }
473
+ }
474
+
475
+ /**
476
+ * INTERNAL FUNCTION
477
+ *
478
+ * @summary Takes in a IAP object containing an entity it then extracts the info
479
+ * the external system cares about into a System Entity
480
+ *
481
+ * @function buildJSONEntity
482
+ * @param {Object} dataObj - the object from IAP
483
+ * @param {String} entitySchema - the entity schema
484
+ * @param {Boolean} dynamicFields - do we show fields not in schema
485
+ *
486
+ * @return {Object} the Entity for the other system
487
+ */
488
+ function buildObject(dataObj, entitySchema, dynamicFields) {
489
+ const origin = `${id}-translatorUtil-buildObject`;
490
+ log.trace(origin);
491
+
492
+ const returnObj = {};
493
+ let addFields = dynamicFields;
494
+
495
+ // if no translation needed - just return the object
496
+ if (Object.hasOwnProperty.call(entitySchema, 'translate')
497
+ && typeof entitySchema.translate === 'boolean' && entitySchema.translate === false) {
498
+ return dataObj;
499
+ }
500
+ // Should allow dymanic fields on this object? - change to inherited
501
+ if (Object.hasOwnProperty.call(entitySchema, 'dynamicfields')
502
+ && typeof entitySchema.dynamicfields === 'boolean') {
503
+ addFields = entitySchema.dynamicfields;
504
+ }
505
+ // if there are no properties - if addFields is true return object
506
+ if (!entitySchema.properties && addFields) {
507
+ return dataObj;
508
+ }
509
+ // if there are no properties and no dynamic fields, return null
510
+ if (!entitySchema.properties) {
511
+ return returnObj;
512
+ }
513
+
514
+ const schemaKeys = Object.keys(entitySchema.properties);
515
+
516
+ // loop through all of the properties in the schema
517
+ for (let k = 0; k < schemaKeys.length; k += 1) {
518
+ const field = entitySchema.properties[schemaKeys[k]];
519
+
520
+ // if this property has an external name, need to see if in the data object
521
+ if (Object.hasOwnProperty.call(field, 'external_name')) {
522
+ // if the external name is something get that field
523
+ if (field.external_name) {
524
+ let fieldValue = getValueFromObject(dataObj[schemaKeys[k]], field, true, addFields);
525
+
526
+ // if we are encoding/encrypting the value
527
+ if (field.encode || (field.encrypt && field.encrypt.key)) {
528
+ fieldValue = encrypt(fieldValue, field.encrypt);
529
+ }
530
+
531
+ // if in the data object, add to the system entity
532
+ if (fieldValue !== null) {
533
+ // need to determine the field in the object where the data should go
534
+ const externalPath = field.external_name.split('.');
535
+ let location = returnObj;
536
+
537
+ // get to the field in the object
538
+ for (let a = 0; a < externalPath.length; a += 1) {
539
+ let isArray = false;
540
+ let tind = 0;
541
+ let epath = externalPath[a];
542
+ if (epath.indexOf('[') >= 0) {
543
+ isArray = true;
544
+ const epathparts = epath.split('[');
545
+ epath = epathparts[0];
546
+ const endI = epathparts[1].indexOf(']');
547
+ tind = Number(epathparts[1].substring(0, endI));
548
+ }
549
+
550
+ // if we are at the point to put the data - set it
551
+ if (a === externalPath.length - 1) {
552
+ // if we already have this key in the object, need to merge
553
+ if (location[epath] && typeof location[epath] === 'object') {
554
+ if (isArray) {
555
+ log.warn(`${epath} is already in the data as an object - check data for errors`);
556
+ }
557
+ const baseObj = {};
558
+ baseObj[epath] = fieldValue;
559
+
560
+ // this will merge the two objects - first object is default
561
+ this.mergeObjects(baseObj, location[epath]);
562
+ } else if (location[epath] && Array.isArray(location[epath])) {
563
+ if (!isArray) {
564
+ log.warn(`${epath} is already in the data as an array - check data for errors`);
565
+ }
566
+ // just push the data into the array
567
+ location[epath].push(fieldValue);
568
+ } else if (location[epath]) {
569
+ if (isArray) {
570
+ log.warn(`${epath} is already in the data as a field - check data for errors`);
571
+ }
572
+ location[epath] = fieldValue;
573
+ } else {
574
+ // just put the value in the return object
575
+ location[epath] = fieldValue;
576
+ }
577
+ } else if (location[epath]) {
578
+ // if type does not match then it is an issue - Array v Object
579
+ if ((isArray && typeof location[epath] === 'object') || (!isArray && Array.isArray(location[epath]))) {
580
+ log.warn(`${epath} is already in the data and the type does not match the desired item - check data for errors`);
581
+ }
582
+ // if an array need to make sure the index exists and walk down the array
583
+ if (isArray && Array.isArray(location[epath])) {
584
+ // if the index has not been created yet - create it
585
+ for (let i = location[epath].length; i <= tind; i += 1) {
586
+ location[epath].push({});
587
+ }
588
+ location = location[epath][tind];
589
+ } else {
590
+ // walk down the object path, if it exists
591
+ location = location[epath];
592
+ }
593
+ } else if (isArray) {
594
+ // if the array does not exist yet, create it
595
+ location[epath] = [];
596
+ for (let i = 0; i <= tind; i += 1) {
597
+ location[epath].push({});
598
+ }
599
+ location = location[epath][tind];
600
+ } else {
601
+ // if the sub object does not exist yet, create it
602
+ location[epath] = {};
603
+ location = location[epath];
604
+ }
605
+ }
606
+ }
607
+ }
608
+ // if the external field is null or empty then we ignore the field!
609
+ } else {
610
+ // if the field does not have an external name then use field key
611
+ let fieldValue = getValueFromObject(dataObj[schemaKeys[k]], field, true, addFields);
612
+
613
+ // if we are encoding/encrypting the value
614
+ if (field.encode || (field.encrypt && field.encrypt.key)) {
615
+ fieldValue = encrypt(fieldValue, field.encrypt);
616
+ }
617
+
618
+ // if in the data object, add to the IAP entity
619
+ if (fieldValue !== null) {
620
+ // if we already have this key in the object, need to merge
621
+ if (returnObj[schemaKeys[k]] && typeof returnObj[schemaKeys[k]] === 'object') {
622
+ const baseObj = {};
623
+ baseObj[schemaKeys[k]] = fieldValue;
624
+
625
+ // this will merge the two objects - first object is default
626
+ this.mergeObjects(baseObj, returnObj[schemaKeys[k]]);
627
+ } else {
628
+ // just put the value in the return object
629
+ returnObj[schemaKeys[k]] = fieldValue;
630
+ }
631
+ }
632
+ }
633
+ }
634
+
635
+ // if we should allow dymanic fields on this object, add the fields
636
+ if (addFields) {
637
+ const objectKeys = Object.keys(dataObj);
638
+
639
+ // loop through all of the fields in the object
640
+ for (let o = 0; o < objectKeys.length; o += 1) {
641
+ // if the field is not in the schema - we need to add it
642
+ // using the field name that came in since no translation
643
+ if (!schemaKeys.includes(objectKeys[o])) {
644
+ returnObj[objectKeys[o]] = dataObj[objectKeys[o]];
645
+ }
646
+ }
647
+ }
648
+
649
+ // return the resulting system entity
650
+ return returnObj;
651
+ }
652
+
653
+ /**
654
+ * INTERNAL FUNCTION
655
+ *
656
+ * @summary Takes in a IAP object containing an entity. This object is then merged with
657
+ * default data and validated against the provided entity schema. It then extracts the info
658
+ * the external system cares about into a System Entity
659
+ *
660
+ * @function buildJSONEntity
661
+ * @param {Object} dataObj - the object from IAP
662
+ * @param {String} entitySchema - the entity schema
663
+ *
664
+ * @return {Object} the Entity for the other system
665
+ */
666
+ function buildJSONEntity(dataObj, entitySchema) {
667
+ const origin = `${id}-translatorUtil-buildJSONEntity`;
668
+ log.trace(origin);
669
+
670
+ try {
671
+ // add any defaults to the data
672
+ const combinedEntity = propUtilInst.mergeProperties(dataObj, propUtilInst.setDefaults(entitySchema));
673
+
674
+ // validate the entity against the schema
675
+ const ajvInst = new AjvCl();
676
+ const validate = ajvInst.compile(entitySchema);
677
+ const result = validate(combinedEntity);
678
+
679
+ // if invalid properties throw an error
680
+ if (!result) {
681
+ // create the error object
682
+ const errorObj = {
683
+ origin,
684
+ type: 'Schema Validation Failure',
685
+ vars: [validate.errors[0].message]
686
+ };
687
+
688
+ // log and throw the error
689
+ log.error(`${origin}: Schema validation failure ${validate.errors[0].message}`);
690
+ throw new Error(JSON.stringify(errorObj));
691
+ }
692
+
693
+ // if IAP request type exists, remove it (internal use only)
694
+ if (combinedEntity.ph_request_type) {
695
+ delete combinedEntity.ph_request_type;
696
+ }
697
+
698
+ const returnObj = buildObject(combinedEntity, entitySchema, false);
699
+
700
+ // return the resulting system entity
701
+ return returnObj;
702
+ } catch (e) {
703
+ return translator.checkAndThrow(e, origin, 'Issue extracting JSON');
704
+ }
705
+ }
706
+
707
+ class AdapterTranslatorUtil {
708
+ /**
709
+ * Adapter Translator Utility
710
+ * @constructor
711
+ */
712
+ constructor(prongId, propUtilCl) {
713
+ id = prongId;
714
+ this.myid = prongId;
715
+ this.propUtil = propUtilCl;
716
+
717
+ // set globals (available to private functions)
718
+ propUtilInst = this.propUtil;
719
+ translator = this;
720
+
721
+ // get the path for the specific error file
722
+ const errorFile = path.join(this.propUtil.baseDir, '/error.json');
723
+
724
+ // if the file does not exist - error
725
+ if (!fs.existsSync(errorFile)) {
726
+ const origin = `${this.myid}-translatorUtil-constructor`;
727
+ log.warn(`${origin}: Could not locate ${errorFile} - errors will be missing details`);
728
+ }
729
+
730
+ // Read the action from the file system
731
+ this.errors = JSON.parse(fs.readFileSync(errorFile, 'utf-8'));
732
+ this.errors = this.errors.errors;
733
+ }
734
+
735
+ // GENERIC UTILITY CALLS USED BY VARIOUS TRANSLATORS
736
+ /**
737
+ * @summary Takes in a JSON object containing an entity it then extracts the info
738
+ * IAP cares about into a IAP Entity
739
+ *
740
+ * @function mapFromOutboundEntity
741
+ * @param {Object} inEntity - the entity from the other system
742
+ * @param {String} entitySchema - the entity schema
743
+ *
744
+ * @return {Object} the Entity for use in IAP
745
+ */
746
+ mapFromOutboundEntity(inEntity, entitySchema) {
747
+ const origin = `${this.myid}-translatorUtil-mapFromOutboundEntity`;
748
+ log.trace(origin);
749
+
750
+ // create the generic part of an error object
751
+ const errorObj = {
752
+ origin
753
+ };
754
+
755
+ try {
756
+ // if nothing passed in, nothing to do
757
+ if (inEntity === null) {
758
+ // add the specific pieces of the error object
759
+ errorObj.type = 'Missing Data';
760
+ errorObj.vars = ['Entity'];
761
+
762
+ // log and throw the error
763
+ log.error(`${origin}: No Entity for mapFromOutboundEntity`);
764
+ throw new Error(JSON.stringify(errorObj));
765
+ }
766
+ if (entitySchema === null) {
767
+ // add the specific pieces of the error object
768
+ errorObj.type = 'Missing Data';
769
+ errorObj.vars = ['Entity Schema'];
770
+
771
+ // log and throw the error
772
+ log.error(`${origin}: No Entity Schema for mapFromOutboundEntity`);
773
+ throw new Error(JSON.stringify(errorObj));
774
+ }
775
+
776
+ // if no translation needed on top object - just return the object
777
+ if (Object.hasOwnProperty.call(entitySchema, 'translate')
778
+ && typeof entitySchema.translate === 'boolean' && entitySchema.translate === false) {
779
+ const cleanEntity = inEntity;
780
+
781
+ // if IAP request type exists, remove it (internal use only)
782
+ if (cleanEntity.ph_request_type) {
783
+ delete cleanEntity.ph_request_type;
784
+ }
785
+
786
+ return cleanEntity;
787
+ }
788
+
789
+ // make sure we are working with a JSON object instead of strigified object
790
+ const transObj = this.formatInputData(inEntity);
791
+
792
+ // if an array of Entities, just translate the data (no objects)
793
+ if (Array.isArray(transObj)) {
794
+ const outEntities = [];
795
+
796
+ for (let i = 0; i < transObj.length; i += 1) {
797
+ // is this just an array of data or something that needs to be translated?
798
+ if (typeof transObj[i] === 'object') {
799
+ // move the fields we care about into a IAP Entity Object
800
+ outEntities.push(extractJSONEntity(transObj[i], entitySchema));
801
+ } else {
802
+ outEntities.push(transObj[i]);
803
+ }
804
+ }
805
+
806
+ return outEntities;
807
+ }
808
+
809
+ // if a single System Entity, should translate data and get value for objects
810
+ // move the fields we care about into a IAP Entity Object
811
+ return extractJSONEntity(transObj, entitySchema);
812
+ } catch (e) {
813
+ return this.checkAndThrow(e, origin, 'Issue mapping from outbound entity');
814
+ }
815
+ }
816
+
817
+ /**
818
+ * @summary Takes in a JSON object containing a IAP entity it then extracts the info
819
+ * the other system cares about into an Entity
820
+ *
821
+ * @function MapToJSONEntity
822
+ * @param {Object} outEntity - the entity to the other system
823
+ * @param {String} entitySchema - the entity schema
824
+ *
825
+ * @return {Object} the Entity for use in the other system
826
+ */
827
+ mapToOutboundEntity(outEntity, entitySchema) {
828
+ const origin = `${this.myid}-translatorUtil-mapToOutboundEntity`;
829
+ log.trace(origin);
830
+
831
+ // create the generic part of an error object
832
+ const errorObj = {
833
+ origin
834
+ };
835
+
836
+ try {
837
+ // if nothing passed in, nothing to do
838
+ if (outEntity === null) {
839
+ // add the specific pieces of the error object
840
+ errorObj.type = 'Missing Data';
841
+ errorObj.vars = ['Entity'];
842
+
843
+ // log and throw the error
844
+ log.error(`${origin}: No Entity for mapToOutboundEntity`);
845
+ throw new Error(JSON.stringify(errorObj));
846
+ }
847
+ if (!entitySchema) {
848
+ // add the specific pieces of the error object
849
+ errorObj.type = 'Missing Data';
850
+ errorObj.vars = ['Entity Schema'];
851
+
852
+ // log and throw the error
853
+ log.error(`${origin}: No Entity Schema for mapToOutboundEntity`);
854
+ throw new Error(JSON.stringify(errorObj));
855
+ }
856
+
857
+ // if no translation needed on top object - just return the object
858
+ if (Object.hasOwnProperty.call(entitySchema, 'translate')
859
+ && typeof entitySchema.translate === 'boolean' && entitySchema.translate === false) {
860
+ const cleanEntity = outEntity;
861
+
862
+ // if IAP request type exists, remove it (internal use only)
863
+ if (cleanEntity.ph_request_type) {
864
+ delete cleanEntity.ph_request_type;
865
+ }
866
+
867
+ return cleanEntity;
868
+ }
869
+
870
+ // make sure we are working with a JSON object instead of strigified object
871
+ const transObj = this.formatInputData(outEntity);
872
+
873
+ // if this is an array of Objects, translate each of the objects
874
+ if (Array.isArray(transObj)) {
875
+ const retObjects = [];
876
+
877
+ for (let i = 0; i < transObj.length; i += 1) {
878
+ // move the fields the other system cares about into the Object
879
+ retObjects.push(buildJSONEntity(transObj[i], entitySchema));
880
+ }
881
+
882
+ return retObjects;
883
+ }
884
+
885
+ // if a single Object, should translate data and get value for objects
886
+ // move the fields the other system cares about into the Object
887
+ return buildJSONEntity(transObj, entitySchema);
888
+ } catch (e) {
889
+ return this.checkAndThrow(e, origin, 'Issue mapping to outbound entity');
890
+ }
891
+ }
892
+
893
+ /**
894
+ * @summary Takes in two objects and merges them so the returned
895
+ * object has secondary only where no primary values were provided.
896
+ *
897
+ * @function mergeObjects
898
+ * @param {Object} object - the primary object
899
+ * @param {Object} secondary - the secondary object
900
+ *
901
+ * @return {Object} the properties with the merged in secondaries
902
+ */
903
+ mergeObjects(object, secondary) {
904
+ const origin = `${this.myid}-translatorUtil-mergeObjects`;
905
+ log.trace(origin);
906
+
907
+ // do not waste time merging if there is nothing to merge
908
+ if (!object || typeof object !== 'object') {
909
+ return Object.assign({}, secondary);
910
+ }
911
+ if (!secondary || typeof secondary !== 'object') {
912
+ return Object.assign({}, object);
913
+ }
914
+
915
+ const combinedObj = Object.assign({}, secondary);
916
+ const keys = Object.keys(object);
917
+
918
+ // loop through all of the primary object to insert them into the conbined data
919
+ for (let k = 0; k < keys.length; k += 1) {
920
+ const thisField = object[keys[k]];
921
+
922
+ // if this key is to an object
923
+ if (thisField && typeof thisField === 'object' && combinedObj[keys[k]]) {
924
+ // recursive call with primary and secondary object
925
+ combinedObj[keys[k]] = this.mergeObjects(thisField, combinedObj[keys[k]]);
926
+ } else if (thisField || !combinedObj[keys[k]]) {
927
+ // if no secondary or primary has value merge it - overriding the secondary
928
+ combinedObj[keys[k]] = thisField;
929
+ }
930
+ }
931
+
932
+ // return the merged object
933
+ return combinedObj;
934
+ }
935
+
936
+ /**
937
+ * @summary If the input is a stringified JSON object, return the JSON object. Otherwise,
938
+ * return the object that was provided.
939
+ *
940
+ * @function formatInputData
941
+ * @param {Object} input - the input that was recieved (optional).
942
+ * Can be a stringified Object.
943
+ *
944
+ * @return {Object} formatOutput - the query params as an object
945
+ */
946
+ formatInputData(input) {
947
+ const origin = `${this.myid}-translatorUtil-formatInputData`;
948
+ log.trace(origin);
949
+
950
+ // prepare the input we received
951
+ let formatOutput = input;
952
+
953
+ // if the input was passed in as a string parse the json into an object
954
+ if (input && typeof input === 'string') {
955
+ try {
956
+ // parse the query parameters object that was passed in
957
+ formatOutput = JSON.parse(input);
958
+ } catch (err) {
959
+ log.trace(`${origin}: Could not JSON parse the input: ${err}`);
960
+ return input;
961
+ }
962
+ }
963
+
964
+ // return the object
965
+ return formatOutput;
966
+ }
967
+
968
+ /**
969
+ * @summary Build a standard error object from the data provided
970
+ *
971
+ * @function formatErrorObject
972
+ * @param {String} origin - the originator of the error (optional).
973
+ * @param {String} type - the internal error type (optional).
974
+ * @param {Array} variables - the variables to put into the error message (optional).
975
+ * @param {Integer} sysCode - the error code from the other system (optional).
976
+ * @param {Object} sysRes - the raw response from the other system (optional).
977
+ * @param {Exception} stack - any available stack trace from the issue (optional).
978
+ *
979
+ * @return {Object} - the error object, null if missing pertinent information
980
+ */
981
+ formatErrorObject(origin, type, variables, sysCode, sysRes, stack) {
982
+ log.trace(`${this.myid}-translatorUtil-formatErrorObject`);
983
+
984
+ // add the required fields
985
+ const errorObject = {
986
+ icode: 'AD.999',
987
+ IAPerror: {
988
+ origin: `${this.myid}-unidentified`,
989
+ displayString: 'error not provided',
990
+ recommendation: 'report this issue to the adapter team!'
991
+ }
992
+ };
993
+
994
+ if (origin) {
995
+ errorObject.IAPerror.origin = origin;
996
+ }
997
+ if (type) {
998
+ errorObject.IAPerror.displayString = type;
999
+ }
1000
+
1001
+ // add the messages from the error.json
1002
+ for (let e = 0; e < this.errors.length; e += 1) {
1003
+ if (this.errors[e].key === type) {
1004
+ errorObject.icode = this.errors[e].icode;
1005
+ errorObject.IAPerror.displayString = this.errors[e].displayString;
1006
+ errorObject.IAPerror.recommendation = this.errors[e].recommendation;
1007
+ } else if (this.errors[e].icode === type) {
1008
+ errorObject.icode = this.errors[e].icode;
1009
+ errorObject.IAPerror.displayString = this.errors[e].displayString;
1010
+ errorObject.IAPerror.recommendation = this.errors[e].recommendation;
1011
+ }
1012
+ }
1013
+
1014
+ // replace the variables
1015
+ let varCnt = 0;
1016
+ while (errorObject.IAPerror.displayString.indexOf('$VARIABLE$') >= 0) {
1017
+ let curVar = '';
1018
+
1019
+ // get the current variable
1020
+ if (variables && Array.isArray(variables) && variables.length >= varCnt + 1) {
1021
+ curVar = variables[varCnt];
1022
+ }
1023
+ varCnt += 1;
1024
+ errorObject.IAPerror.displayString = errorObject.IAPerror.displayString.replace('$VARIABLE$', curVar);
1025
+ }
1026
+
1027
+ // add all of the optional fields
1028
+ if (sysCode) {
1029
+ errorObject.IAPerror.code = sysCode;
1030
+ }
1031
+ if (sysRes) {
1032
+ errorObject.IAPerror.raw_response = sysRes;
1033
+ }
1034
+ if (stack) {
1035
+ errorObject.IAPerror.stack = stack;
1036
+ }
1037
+
1038
+ // return the object
1039
+ return errorObject;
1040
+ }
1041
+
1042
+ /**
1043
+ * @summary Checks the type of error (internal v external). Then it either returns an
1044
+ * Exception object and returns the error object
1045
+ *
1046
+ * @function checkAndReturn
1047
+ * @param {Object} e - the exception to check.
1048
+ * @param {String} caller - the method that is calling this.
1049
+ * @param {String} logMsg - the message to be logged.
1050
+ *
1051
+ * @return - the error object
1052
+ */
1053
+ checkAndReturn(e, caller, logMsg) {
1054
+ log.trace(`${this.myid}-translatorUtil-checkAndReturn`);
1055
+ let internal = null;
1056
+ let origin = caller;
1057
+
1058
+ if (!caller) {
1059
+ origin = `${this.myid}-unidentified`;
1060
+ }
1061
+
1062
+ // create the generic part of an error object
1063
+ const errorObj = {
1064
+ origin,
1065
+ type: 'Caught Exception',
1066
+ vars: [],
1067
+ syscode: null,
1068
+ sysresp: null,
1069
+ exception: e
1070
+ };
1071
+
1072
+ // determine if we already had an internal message
1073
+ try {
1074
+ internal = JSON.parse(e.message);
1075
+ } catch (ex) {
1076
+ // message was not internal
1077
+ log.error(`${origin}: ${logMsg}: ${e}`);
1078
+ internal = null;
1079
+ }
1080
+
1081
+ // return the appropriate error message
1082
+ if (internal && internal.origin && internal.type) {
1083
+ return this.formatErrorObject(internal.origin, internal.type, internal.vars, internal.syscode, internal.sysresp, internal.exception);
1084
+ }
1085
+
1086
+ return this.formatErrorObject(errorObj.origin, errorObj.type, errorObj.vars, errorObj.syscode, errorObj.sysresp, errorObj.exception);
1087
+ }
1088
+
1089
+ /**
1090
+ * @summary Checks the type of error (internal v external). Then it either rethrows it or
1091
+ * creates an Exception object and throws that
1092
+ *
1093
+ * @function checkAndThrow
1094
+ * @param {Object} e - the exception to check.
1095
+ * @param {String} caller - the method that is calling this.
1096
+ * @param {String} logMsg - the message to be logged.
1097
+ *
1098
+ * @return - throws a new Error
1099
+ */
1100
+ checkAndThrow(e, caller, logMsg) {
1101
+ log.trace(`${this.myid}-translatorUtil-checkAndThrow`);
1102
+ let internal = null;
1103
+ let origin = caller;
1104
+
1105
+ if (!caller) {
1106
+ origin = `${this.myid}-unidentified`;
1107
+ }
1108
+
1109
+ // create the generic part of an error object
1110
+ const errorObj = {
1111
+ origin,
1112
+ type: 'Caught Exception',
1113
+ vars: [],
1114
+ syscode: null,
1115
+ sysresp: null,
1116
+ exception: e
1117
+ };
1118
+
1119
+ // determine if we already had an internal message
1120
+ try {
1121
+ internal = JSON.parse(e.message);
1122
+ } catch (ex) {
1123
+ // message was not internal
1124
+ log.error(`${origin}: ${logMsg}: ${e}`);
1125
+ internal = null;
1126
+ }
1127
+
1128
+ // return the appropriate error message
1129
+ if (internal && internal.origin && internal.type) {
1130
+ throw e;
1131
+ } else {
1132
+ throw new Error(JSON.stringify(errorObj));
1133
+ }
1134
+ }
1135
+ }
1136
+
1137
+ module.exports = AdapterTranslatorUtil;