iotagent-node-lib 2.26.0 → 3.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.
Files changed (60) hide show
  1. package/lib/fiware-iotagent-lib.js +0 -5
  2. package/lib/plugins/compressTimestamp.js +2 -7
  3. package/lib/plugins/expressionParser.js +0 -47
  4. package/lib/plugins/expressionPlugin.js +6 -78
  5. package/lib/plugins/jexlParser.js +0 -36
  6. package/lib/plugins/pluginUtils.js +0 -119
  7. package/lib/services/ngsi/entities-NGSI-LD.js +600 -199
  8. package/lib/services/ngsi/entities-NGSI-v2.js +620 -298
  9. package/lib/services/ngsi/ngsiUtils.js +15 -22
  10. package/package.json +1 -1
  11. package/test/unit/general/loglevel-api_test.js +0 -2
  12. package/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin1.json +0 -5
  13. package/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast10.json +1 -1
  14. package/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast8.json +1 -1
  15. package/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast9.json +1 -1
  16. package/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin32.json +16 -15
  17. package/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin4.json +8 -8
  18. package/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin4a.json +34 -34
  19. package/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin5.json +8 -8
  20. package/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin6.json +0 -20
  21. package/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin7.json +0 -5
  22. package/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin8.json +10 -10
  23. package/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityTimestampPlugin1.json +8 -7
  24. package/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityTimestampPlugin2.json +5 -4
  25. package/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityTimestampPlugin3.json +1 -1
  26. package/test/unit/ngsi-ld/examples/contextRequests/updateContextProcessTimestamp.json +4 -4
  27. package/test/unit/ngsi-ld/examples/contextRequests/updateContextTimestampOverride.json +4 -3
  28. package/test/unit/ngsi-ld/examples/contextRequests/updateContextTimestampOverrideWithoutMilis.json +4 -3
  29. package/test/unit/ngsi-ld/expressions/expressionBasedTransformations-test.js +2 -5
  30. package/test/unit/ngsi-ld/expressions/jexlBasedTransformations-test.js +1 -7
  31. package/test/unit/ngsi-ld/ngsiService/autocast-test.js +15 -3
  32. package/test/unit/ngsi-ld/plugins/alias-plugin_test.js +0 -2
  33. package/test/unit/ngsi-ld/plugins/compress-timestamp-plugin_test.js +0 -31
  34. package/test/unit/ngsi-ld/plugins/multientity-plugin_test.js +18 -18
  35. package/test/unit/ngsi-ld/plugins/timestamp-processing-plugin_test.js +19 -6
  36. package/test/unit/ngsiv2/examples/contextRequests/updateContextAliasPlugin1.json +2 -2
  37. package/test/unit/ngsiv2/examples/contextRequests/updateContextMultientityPlugin11.json +5 -1
  38. package/test/unit/ngsiv2/examples/contextRequests/updateContextMultientityPlugin5.json +4 -4
  39. package/test/unit/ngsiv2/examples/contextRequests/updateContextMultientityPlugin6.json +0 -16
  40. package/test/unit/ngsiv2/examples/contextRequests/updateContextMultientityPlugin7.json +0 -4
  41. package/test/unit/ngsiv2/examples/contextRequests/updateContextMultientityPlugin8.json +8 -8
  42. package/test/unit/ngsiv2/examples/contextRequests/updateContextMultientityTimestampPlugin1.json +42 -42
  43. package/test/unit/ngsiv2/examples/contextRequests/updateContextMultientityTimestampPlugin2.json +7 -7
  44. package/test/unit/ngsiv2/examples/contextRequests/updateContextMultientityTimestampPlugin4.json +0 -5
  45. package/test/unit/ngsiv2/examples/contextRequests/updateContextProcessTimestamp.json +1 -7
  46. package/test/unit/ngsiv2/expressions/expressionBasedTransformations-test.js +0 -3
  47. package/test/unit/ngsiv2/expressions/expressionCombinedTransformations-test.js +0 -6
  48. package/test/unit/ngsiv2/expressions/jexlBasedTransformations-test.js +1 -7
  49. package/test/unit/ngsiv2/plugins/alias-plugin_test.js +0 -2
  50. package/test/unit/ngsiv2/plugins/bidirectional-plugin_test.js +0 -4
  51. package/test/unit/ngsiv2/plugins/compress-timestamp-plugin_test.js +0 -32
  52. package/test/unit/ngsiv2/plugins/multientity-plugin_test.js +5 -15
  53. package/test/unit/ngsiv2/plugins/timestamp-processing-plugin_test.js +1 -1
  54. package/lib/plugins/addEvent.js +0 -32
  55. package/lib/plugins/attributeAlias.js +0 -107
  56. package/lib/plugins/multiEntity.js +0 -255
  57. package/lib/plugins/timestampProcessPlugin.js +0 -95
  58. package/test/unit/ngsi-ld/plugins/event-plugin_test.js +0 -116
  59. package/test/unit/ngsiv2/plugins/event-plugin_test.js +0 -118
  60. package/test/unit/ngsiv2/plugins/translation-inPlugins_test.js +0 -262
@@ -26,20 +26,20 @@
26
26
  /* eslint-disable consistent-return */
27
27
 
28
28
  const request = require('../../request-shim');
29
- const statsService = require('./../stats/statsRegistry');
30
- const async = require('async');
31
- const apply = async.apply;
32
29
  const alarms = require('../common/alarmManagement');
33
30
  const errors = require('../../errors');
34
31
  const utils = require('../northBound/restUtils');
32
+ const pluginUtils = require('../../plugins/pluginUtils');
35
33
  const config = require('../../commonConfig');
36
34
  const constants = require('../../constants');
37
35
  const jexlParser = require('../../plugins/jexlParser');
36
+ const expressionPlugin = require('../../plugins/expressionPlugin');
37
+ const compressTimestampPlugin = require('../../plugins/compressTimestamp');
38
38
  const moment = require('moment-timezone');
39
39
  const logger = require('logops');
40
40
  const _ = require('underscore');
41
41
  const context = {
42
- op: 'IoTAgentNGSI.Entities-LD'
42
+ op: 'IoTAgentNGSI-LD'
43
43
  };
44
44
  const NGSIv2 = require('./entities-NGSI-v2');
45
45
  const NGSIUtils = require('./ngsiUtils');
@@ -171,7 +171,7 @@ function convertAttrNGSILD(attr) {
171
171
  obj.value = { '@type': attr.type, '@value': attr.value };
172
172
  }
173
173
 
174
- if (attr.metadata) {
174
+ if (!!obj && attr.metadata) {
175
175
  let timestamp;
176
176
  Object.keys(attr.metadata).forEach(function (key) {
177
177
  switch (key) {
@@ -411,89 +411,13 @@ function addLinkedEntities(typeInformation, json) {
411
411
  }
412
412
 
413
413
  /**
414
- * Remove id, type and any hidden attrs after processing
414
+ * Determines if a value is of type float
415
415
  *
416
- * @param {Object} enities Unprocessed entities
417
- * @param {Object} typeInformation Configuration information for the device.
416
+ * @param {String} value Value to be analyzed
417
+ * @return {boolean} True if float, False otherwise.
418
418
  */
419
- function removeHiddenAttrsFromMultiEntity(entities, typeInformation) {
420
- let explicitAttrsList;
421
- if (typeInformation.explicitAttrs) {
422
- explicitAttrsList = [];
423
- explicitAttrsList.push('type');
424
- explicitAttrsList.push('id');
425
- if (typeof typeInformation.explicitAttrs === 'boolean') {
426
- if (typeInformation.timestamp) {
427
- explicitAttrsList.push(constants.TIMESTAMP_ATTRIBUTE);
428
- }
429
- if (typeInformation.active) {
430
- typeInformation.active.forEach((attr) => {
431
- explicitAttrsList.push(attr.name);
432
- });
433
- }
434
- if (typeInformation.staticAttributes) {
435
- typeInformation.staticAttributes.forEach((attr) => {
436
- explicitAttrsList.push(attr.name);
437
- });
438
- }
439
- } else if (typeof typeInformation.explicitAttrs === 'string') {
440
- entities.forEach((entity) => {
441
- const attsArray = utils.extractAttributesArrayFromNgsi2Entity(entity);
442
- const ctx = jexlParser.extractContext(attsArray);
443
- const res = jexlParser.parse(typeInformation.explicitAttrs, ctx);
444
- explicitAttrsList = explicitAttrsList.concat(res);
445
- });
446
- }
447
- }
448
- if (explicitAttrsList && explicitAttrsList.length >= 0) {
449
- entities.forEach((entity) => {
450
- const hidden = _.difference(_.keys(entity), explicitAttrsList);
451
- hidden.forEach((attr) => {
452
- delete entity[attr];
453
- });
454
- });
455
- }
456
- return entities;
457
- }
458
-
459
- /**
460
- * Remove id, type and any hidden attrs after processing
461
- *
462
- * @param {Object} result Unprocessed entity
463
- * @param {Object} typeInformation Configuration information for the device.
464
- */
465
- function removeHiddenAttrs(result, typeInformation) {
466
- delete result.id;
467
- delete result.type;
468
- let explicitAttrsList;
469
- if (typeInformation.explicitAttrs && typeof typeInformation.explicitAttrs === 'boolean') {
470
- explicitAttrsList = [];
471
- if (typeInformation.timestamp) {
472
- explicitAttrsList.push(constants.TIMESTAMP_ATTRIBUTE);
473
- }
474
- if (typeInformation.active) {
475
- typeInformation.active.forEach((attr) => {
476
- explicitAttrsList.push(attr.name);
477
- });
478
- }
479
- if (typeInformation.staticAttributes) {
480
- typeInformation.staticAttributes.forEach((attr) => {
481
- explicitAttrsList.push(attr.name);
482
- });
483
- }
484
- } else if (typeInformation.explicitAttrs && typeof typeInformation.explicitAttrs === 'string') {
485
- const attsArray = utils.extractAttributesArrayFromNgsi2Entity(result);
486
- const ctx = jexlParser.extractContext(attsArray);
487
- const res = jexlParser.parse(typeInformation.explicitAttrs, ctx);
488
- explicitAttrsList = res;
489
- }
490
- if (explicitAttrsList && explicitAttrsList.length >= 0) {
491
- const hidden = _.difference(_.keys(result), explicitAttrsList);
492
- hidden.forEach((attr) => {
493
- delete result[attr];
494
- });
495
- }
496
- return result;
419
+ function isFloat(value) {
420
+ return !isNaN(value) && value.toString().indexOf('.') !== -1;
497
421
  }
498
422
 
499
423
  /**
@@ -506,9 +430,31 @@ function removeHiddenAttrs(result, typeInformation) {
506
430
  * @param {String} token User token to identify against the PEP Proxies (optional).
507
431
  */
508
432
  function sendUpdateValueNgsiLD(entityName, attributes, typeInformation, token, callback) {
509
- let payload = {};
433
+ logger.debug(
434
+ context,
435
+ 'sendUpdateValueNgsiLD called with: \n entityName=%s \n attributes=%j \n typeInformation=%j',
436
+ entityName,
437
+ attributes,
438
+ typeInformation
439
+ );
440
+ const payload = [
441
+ {
442
+ id: entityName
443
+ }
444
+ ];
445
+
510
446
  const url = '/ngsi-ld/v1/entityOperations/upsert/?options=update';
511
447
 
448
+ if (typeInformation && typeInformation.type) {
449
+ payload[0].type = typeInformation.type;
450
+ }
451
+
452
+ if (config.getConfig().appendMode === true) {
453
+ payload.actionType = 'append';
454
+ } else {
455
+ payload.actionType = 'update';
456
+ }
457
+
512
458
  const options = NGSIUtils.createRequestObject(url, typeInformation, token);
513
459
  options.method = 'POST';
514
460
 
@@ -520,143 +466,598 @@ function sendUpdateValueNgsiLD(entityName, attributes, typeInformation, token, c
520
466
  callback(new errors.TypeNotFound(null, entityName));
521
467
  return;
522
468
  }
469
+ const idTypeSSSList = pluginUtils.getIdTypeServSubServiceFromDevice(typeInformation);
470
+ logger.debug(context, 'sendUpdateValueNgsiLD \n idTypeSSS are %j ', idTypeSSSList);
471
+ const measureAttrsForCtxt = [];
523
472
 
524
- payload.id = entityName;
525
- payload.type = typeInformation.type;
526
-
527
- for (let i = 0; i < attributes.length; i++) {
528
- if (attributes[i].name && attributes[i].type) {
529
- payload[attributes[i].name] = {
530
- value: attributes[i].value,
531
- type: attributes[i].type
532
- };
533
- const metadata = NGSIUtils.getMetaData(typeInformation, attributes[i].name, attributes[i].metadata);
534
- if (metadata) {
535
- payload[attributes[i].name].metadata = metadata;
473
+ // Check explicitAttrs: adds all final needed attributes to payload
474
+ if (
475
+ typeInformation.explicitAttrs === undefined ||
476
+ (typeof typeInformation.explicitAttrs === 'boolean' && !typeInformation.explicitAttrs)
477
+ // explicitAttrs is not defined => default case: all attrs should be included
478
+ ) {
479
+ // This loop adds all measure values (attributes) into payload entities (entity[0])
480
+ for (let i = 0; i < attributes.length; i++) {
481
+ if (attributes[i].name && attributes[i].type) {
482
+ payload[0][attributes[i].name] = {
483
+ value: attributes[i].value,
484
+ type: attributes[i].type
485
+ };
486
+ const metadata = NGSIUtils.getMetaData(typeInformation, attributes[i].name, attributes[i].metadata);
487
+ if (metadata) {
488
+ payload[0][attributes[i].name].metadata = metadata;
489
+ }
490
+ } else {
491
+ callback(new errors.BadRequest(null, entityName));
492
+ return;
536
493
  }
537
- } else {
538
- callback(new errors.BadRequest(null, entityName));
539
- return;
540
494
  }
541
- }
542
-
543
- payload = NGSIUtils.castJsonNativeAttributes(payload);
544
- async.waterfall(
545
- [
546
- apply(statsService.add, 'measureRequests', 1),
547
- apply(NGSIUtils.applyMiddlewares, NGSIUtils.updateMiddleware, payload, typeInformation)
548
- ],
549
- function (error, result) {
550
- if (error) {
551
- callback(error);
552
- } else {
553
- if (result) {
554
- // The payload has been transformed by multientity plugin. It is not a JSON object but an Array.
555
- if (result instanceof Array) {
556
- if (
557
- 'timestamp' in typeInformation && typeInformation.timestamp !== undefined
558
- ? typeInformation.timestamp
559
- : config.getConfig().timestamp
560
- ) {
561
- if (!utils.isTimestampedNgsi2(result)) {
562
- options.json = NGSIv2.addTimestamp(result, typeInformation.timezone);
563
- } else if (!utils.IsValidTimestampedNgsi2(result)) {
564
- logger.error(context, 'Invalid timestamp:%s', JSON.stringify(result));
565
- callback(new errors.BadTimestamp(result));
566
- return;
567
- }
568
- }
569
-
570
- options.json = removeHiddenAttrsFromMultiEntity(result, typeInformation);
495
+ logger.debug(context, 'sendUpdateValueNgsiLD \n pre-initial non-explicitAttrs payload=%j', payload);
496
+ // Loop for add attrs from type.information.active (and lazys?) into payload entities (entity[0])
497
+ if (typeInformation.active) {
498
+ typeInformation.active.forEach((attr) => {
499
+ if (attr.expression) {
500
+ if (attr.object_id) {
501
+ payload[0][attr.object_id] = {
502
+ value: payload[0][attr.object_id] ? payload[0][attr.object_id].value : undefined,
503
+ type: attr.type,
504
+ object_id: attr.object_id
505
+ };
571
506
  } else {
572
- // Remove id, type and any hidden attrs after processing
573
- options.json = removeHiddenAttrs(result, typeInformation);
574
- logger.debug(context, 'typeInformation: %j', typeInformation);
575
- if (
576
- 'timestamp' in typeInformation && typeInformation.timestamp !== undefined
577
- ? typeInformation.timestamp
578
- : config.getConfig().timestamp
579
- ) {
580
- if (!utils.isTimestampedNgsi2(options.json)) {
581
- options.json = NGSIv2.addTimestamp(options.json, typeInformation.timezone);
582
- } else if (!utils.IsValidTimestampedNgsi2(options.json)) {
583
- logger.error(context, 'Invalid timestamp:%s', JSON.stringify(options.json));
584
- callback(new errors.BadTimestamp(options.json));
585
- return;
586
- }
587
- }
507
+ payload[0][attr.name] = {
508
+ value: payload[0][attr.name] ? payload[0][attr.name].value : undefined,
509
+ type: attr.type
510
+ };
588
511
  }
589
- } else {
590
- delete payload.id;
591
- delete payload.type;
592
- options.json = payload;
593
512
  }
594
- // Purge object_id from entities before sent to CB
595
- // object_id was added by createNgsi2Entity to allow multientity
596
- // with duplicate attribute names.
597
- let att;
598
- if (options.json) {
599
- for (let entity = 0; entity < options.json.length; entity++) {
600
- for (att in options.json[entity]) {
601
- /*jshint camelcase: false */
602
- if (options.json[entity][att].object_id) {
603
- /*jshint camelcase: false */
604
- delete options.json[entity][att].object_id;
605
- }
606
- if (options.json[entity][att].multi) {
607
- delete options.json[entity][att].multi;
608
- }
609
- if (options.json[entity][att].name) {
610
- delete options.json[entity][att].name;
611
- }
513
+ });
514
+ }
515
+ } else {
516
+ let selectedAttrs = [];
517
+ if (typeof typeInformation.explicitAttrs === 'string') {
518
+ // explicitAttrs is a jexlExpression
519
+ // This ctxt should include all possible attrs
520
+ const attributesCtxt = [];
521
+ if (typeInformation.static) {
522
+ typeInformation.static.forEach(function (att) {
523
+ attributesCtxt.push(att);
524
+ });
525
+ }
526
+ // Measures
527
+ for (let i = 0; i < attributes.length; i++) {
528
+ if (attributes[i].name && attributes[i].type) {
529
+ const measureAttr = {
530
+ name: attributes[i].name,
531
+ value: attributes[i].value,
532
+ type: attributes[i].type
533
+ };
534
+ attributesCtxt.push(measureAttr);
535
+ }
536
+ }
537
+ // This context is just to calculate explicitAttrs when is an expression
538
+ let ctxt = expressionPlugin.extractContext(attributesCtxt.concat(idTypeSSSList), typeInformation);
539
+ // typeInformation.active attrs with expressions expanded by current ctxt
540
+ if (typeInformation.active) {
541
+ typeInformation.active.forEach(function (att) {
542
+ if (att.expression) {
543
+ if (expressionPlugin.contextAvailable(att.expression, ctxt, typeInformation)) {
544
+ const expandedAttr = {
545
+ name: att.name,
546
+ value: att.expression, // it doesn't matter final value here
547
+ type: att.type
548
+ };
549
+ attributesCtxt.push(expandedAttr);
550
+ ctxt = expressionPlugin.extractContext(
551
+ attributesCtxt.concat(idTypeSSSList),
552
+ typeInformation
553
+ );
612
554
  }
613
555
  }
614
- } else {
615
- for (att in options.json) {
616
- /*jshint camelcase: false */
617
- if (options.json[att].object_id) {
618
- /*jshint camelcase: false */
619
- delete options.json[att].object_id;
620
- }
621
- if (options.json[att].multi) {
622
- delete options.json[att].multi;
623
- }
624
- if (options.json[att].name) {
625
- delete options.json[att].name;
626
- }
556
+ });
557
+ }
558
+ // calculate expression for explicitAttrs
559
+ try {
560
+ const res = jexlParser.applyExpression(typeInformation.explicitAttrs, ctxt, typeInformation);
561
+ if (res === true) {
562
+ // like explicitAttrs == true
563
+ // selectAttrs should be measures which are defined attributes
564
+ typeInformation.active.forEach((attr) => {
565
+ selectedAttrs.push(attr.name);
566
+ selectedAttrs.push(attr.object_id);
567
+ });
568
+ } else if (res === false) {
569
+ // like explicitAttrs == false
570
+ // selectAttrs should be measures and defined attributes
571
+ typeInformation.active.forEach((attr) => {
572
+ selectedAttrs.push(attr.name);
573
+ selectedAttrs.push(attr.object_id);
574
+ });
575
+ for (let i = 0; i < attributes.length; i++) {
576
+ selectedAttrs.push(attributes[i].name);
627
577
  }
578
+ } else {
579
+ selectedAttrs = res; // TBD: Check ensure is an array of strings
580
+ }
581
+ if (selectedAttrs.length === 0) {
582
+ // implies do nothing
583
+ logger.info(
584
+ context,
585
+ 'sendUpdateValueNgsiLD \n none selectedAttrs with %j and ctxt %j',
586
+ typeInformation.explicitAttrs,
587
+ ctxt
588
+ );
589
+ return callback(null);
628
590
  }
591
+ } catch (e) {
592
+ // nothing to do: exception is already logged at info level
593
+ }
629
594
 
630
- try {
631
- if (result instanceof Array) {
632
- options.json = _.map(options.json, formatAsNGSILD);
595
+ typeInformation.active.forEach((attr) => {
596
+ if (selectedAttrs.includes(attr.name)) {
597
+ selectedAttrs.push(attr.object_id);
598
+ }
599
+ });
600
+ } else if (typeInformation.explicitAttrs && typeof typeInformation.explicitAttrs === 'boolean') {
601
+ // TBD: selectedAttrs could be a boolean as a result of applyExpression
602
+ // explicitAtts is true => Add measures which are defined attributes
603
+ typeInformation.active.forEach((attr) => {
604
+ selectedAttrs.push(attr.name);
605
+ selectedAttrs.push(attr.object_id);
606
+ });
607
+ }
608
+ // This loop adds selected measured values (attributes) into payload entities (entity[0])
609
+ for (let i = 0; i < attributes.length; i++) {
610
+ if (attributes[i].name && selectedAttrs.includes(attributes[i].name) && attributes[i].type) {
611
+ const attr = typeInformation.active.find((obj) => {
612
+ return obj.name === attributes[i].name;
613
+ });
614
+ payload[0][attributes[i].name] = {
615
+ value: attributes[i].value,
616
+ type: attributes[i].type
617
+ };
618
+ // ensure payload has attr with proper object_id
619
+ if (attr && attr.object_id) {
620
+ payload[0][attributes[i].name].object_id = attr.object_id;
621
+ }
622
+ const metadata = NGSIUtils.getMetaData(typeInformation, attributes[i].name, attributes[i].metadata);
623
+ if (metadata) {
624
+ payload[0][attributes[i].name].metadata = metadata;
625
+ }
626
+ } else if (attributes[i].name && !selectedAttrs.includes(attributes[i].name) && attributes[i].type) {
627
+ const att = {
628
+ name: attributes[i].name,
629
+ type: attributes[i].type,
630
+ value: attributes[i].value
631
+ };
632
+ measureAttrsForCtxt.push(att);
633
+ }
634
+ }
635
+ logger.debug(
636
+ context,
637
+ 'sendUpdateValueNgsiLD \n pre-initial explicitAttrs payload=%j \n selectedAttrs',
638
+ payload,
639
+ selectedAttrs
640
+ );
641
+
642
+ // Loop for add seleted attrs from type.information.active into pyaload entities (entity[0])
643
+ if (typeInformation.active) {
644
+ typeInformation.active.forEach((attr) => {
645
+ if (selectedAttrs.includes(attr.name)) {
646
+ if (attr.object_id) {
647
+ payload[0][attr.object_id] = {
648
+ value: payload[0][attr.object_id]
649
+ ? payload[0][attr.object_id].value
650
+ : payload[0][attr.name]
651
+ ? payload[0][attr.name].value
652
+ : undefined,
653
+ type: attr.type,
654
+ object_id: attr.object_id
655
+ };
633
656
  } else {
634
- options.json.id = entityName;
635
- options.json.type = typeInformation.type;
636
- options.json = [formatAsNGSILD(options.json)];
657
+ payload[0][attr.name] = {
658
+ value: payload[0][attr.name] ? payload[0][attr.name].value : undefined,
659
+ type: attr.type
660
+ };
637
661
  }
638
- } catch (error) {
639
- return callback(new errors.BadGeocoordinates(JSON.stringify(payload)));
640
662
  }
663
+ });
664
+ }
665
+ } // END check explicitAttrs
666
+ logger.debug(context, 'sendUpdateValueNgsiLD \n initial payload=%j', payload);
667
+
668
+ const currentEntity = payload[0];
641
669
 
670
+ // Prepare attributes for expresionPlugin
671
+ const attsArray = pluginUtils.extractAttributesArrayFromNgsi2Entity(currentEntity);
672
+
673
+ // Exclude processing all attr expressions when current attr is of type 'commandStatus' or 'commandResult'
674
+ let attsArrayFiltered = [];
675
+ if (attsArray) {
676
+ attsArrayFiltered = attsArray.filter((obj) => {
677
+ return ![constants.COMMAND_STATUS, constants.COMMAND_RESULT].includes(obj.type);
678
+ });
679
+ }
680
+ let attributesCtxt = [...attsArrayFiltered]; // just copy
681
+ if (typeInformation.static) {
682
+ typeInformation.static.forEach(function (att) {
683
+ attributesCtxt.push(att);
684
+ });
685
+ }
686
+ if (measureAttrsForCtxt) {
687
+ measureAttrsForCtxt.forEach(function (att) {
688
+ attributesCtxt.push(att);
689
+ });
690
+ }
691
+ attributesCtxt = attributesCtxt.concat(idTypeSSSList);
692
+ const ctxt = expressionPlugin.extractContext(attributesCtxt, typeInformation);
693
+ logger.debug(context, 'sendUpdateValueNgsiLD \n initial ctxt %j ', ctxt);
694
+
695
+ // Sort currentEntity to get first attrs without expressions (checking attrs in typeInformation.active)
696
+ // attributes without expressions should be processed before
697
+ logger.debug(context, 'sendUpdateValueNgsiLD \n currentEntity %j ', currentEntity);
698
+ if (typeInformation.active && typeInformation.active.length > 0) {
699
+ for (const k in currentEntity) {
700
+ typeInformation.active.forEach(function (att) {
701
+ if (
702
+ (att.object_id && att.object_id === k && att.expression) ||
703
+ (att.name && att.name === k && att.expression)
704
+ ) {
705
+ const m = currentEntity[k];
706
+ delete currentEntity[k];
707
+ currentEntity[k] = m; // put into the end of currentEntity
708
+ }
709
+ });
710
+ }
711
+ }
712
+ logger.debug(context, 'sendUpdateValueNgsiLD \n currentEntity sorted %j ', currentEntity);
713
+ let timestampValue;
714
+ // Loop for each final attribute to apply alias, multientity and expressions
715
+ for (const j in currentEntity) {
716
+ // discard id and type
717
+ if (j !== 'id' || j !== 'type') {
718
+ // Apply Mapping Alias: object_id in attributes are in typeInformation.active
719
+ let attr;
720
+ let newAttr = payload[0][j];
721
+ if (typeInformation.active) {
722
+ attr = typeInformation.active.find((obj) => {
723
+ return obj.object_id === j;
724
+ });
725
+ }
726
+ if (!attr) {
727
+ if (typeInformation.lazy) {
728
+ attr = typeInformation.lazy.find((obj) => {
729
+ return obj.object_id === j;
730
+ });
731
+ }
732
+ }
733
+ if (!attr) {
642
734
  if (typeInformation.active) {
643
- addLinkedEntities(typeInformation, options.json);
735
+ attr = typeInformation.active.find((obj) => {
736
+ return obj.name === j;
737
+ });
644
738
  }
739
+ }
740
+ if (attr && attr.name) {
741
+ if (['id', 'type'].includes(attr.name)) {
742
+ // invalid mapping
743
+ logger.debug(
744
+ context,
745
+ 'sendUpdateValueNgsiLD \n invalid mapping for attr %j \n newAttr %j',
746
+ attr,
747
+ newAttr
748
+ );
749
+ delete payload[0][attr.object_id];
750
+ attr = undefined; // stop processing attr
751
+ newAttr = undefined;
752
+ } else {
753
+ ctxt[attr.name] = payload[0][j].value;
754
+ }
755
+ }
756
+ logger.debug(
757
+ context,
758
+ 'sendUpdateValueNgsiLD \n procesing j %j attr %j ctxt %j \n newAttr %j ',
759
+ j,
760
+ attr,
761
+ ctxt,
762
+ newAttr
763
+ );
764
+
765
+ if (attr && attr.type) {
766
+ if (attr.type !== 'GeoProperty') {
767
+ newAttr.type = attr.type;
768
+ }
769
+ }
645
770
 
646
- logger.debug(context, 'Updating device value in the Context Broker at [%s]', options.url);
771
+ // Apply expression
772
+ if (attr && attr.expression) {
647
773
  logger.debug(
648
774
  context,
649
- 'Using the following NGSI-LD request:\n\n%s\n\n',
650
- JSON.stringify(options, null, 4)
775
+ 'sendUpdateValueNgsiLD \n apply expression %j \n over ctxt %j \n and device %j',
776
+ attr.expression,
777
+ ctxt,
778
+ typeInformation
651
779
  );
780
+ let res = null;
781
+ try {
782
+ if (expressionPlugin.contextAvailable(attr.expression, ctxt, typeInformation)) {
783
+ res = expressionPlugin.applyExpression(attr.expression, ctxt, typeInformation);
784
+ } else {
785
+ logger.warn(
786
+ context,
787
+ 'sendUpdateValueNgsiLD \n no context available for apply expression %j \n',
788
+ attr.expression
789
+ );
790
+ res = newAttr.value; // keep newAttr value
791
+ }
792
+ } catch (e) {
793
+ logger.error(context, 'sendUpdateValueNgsiLD \n apply expression exception %j \n', e);
794
+ if (!expressionPlugin.checkJexl(typeInformation)) {
795
+ return callback(e); // just throw error with legacy parser for backward compatiblity
796
+ } else {
797
+ res = ctxt[attr.name]; // TBD: add reference to test
798
+ }
799
+ }
800
+ if (!expressionPlugin.checkJexl(typeInformation)) {
801
+ // legacy expression plugin: Involves some legacy checks performed by applierExpression
802
+ if (
803
+ res &&
804
+ res !== 'undefined' &&
805
+ res !== 'null' &&
806
+ (typeof res === 'string' || res instanceof String) &&
807
+ !res.includes('NaN')
808
+ ) {
809
+ newAttr.value = res;
810
+ if (newAttr.type === 'Number' && isFloat(newAttr.value)) {
811
+ newAttr.value = Number.parseFloat(newAttr.value);
812
+ } else if (newAttr.type === 'Number' && !Number.isNaN(Number.parseInt(newAttr.value))) {
813
+ newAttr.value = Number.parseInt(newAttr.value);
814
+ } else if (newAttr.type === 'Boolean') {
815
+ newAttr.value = newAttr.value === 'true' || newAttr.value === '1';
816
+ } else if (newAttr.type === 'None') {
817
+ newAttr.value = null;
818
+ }
819
+ } else if (isNaN(res) || res === 'undefined' || res === 'null') {
820
+ logger.debug(
821
+ context,
822
+ 'sendUpdateValueNgsiLD \n apply expression result: isNaN || undefined || null'
823
+ );
824
+ if (res === 'null' && newAttr.type === 'None') {
825
+ newAttr.value = null;
826
+ } else {
827
+ delete payload[0][j]; // remove measure attr
828
+ attr = undefined; // stop process attr
829
+ }
830
+ }
831
+ } else {
832
+ // jexl expression plugin
833
+ newAttr.value = res;
834
+ }
835
+ logger.debug(
836
+ context,
837
+ 'sendUpdateValueNgsiLD \n apply expression result %j \n newAttr %j',
838
+ res,
839
+ newAttr
840
+ );
841
+ }
652
842
 
653
- request(
654
- options,
655
- generateNGSILDOperationHandler('update', entityName, typeInformation, token, options, callback)
843
+ // Apply Multientity: entity_type and entity_name in attributes are in typeInformation.active
844
+ if (attr && (attr.entity_type || attr.entity_name)) {
845
+ // Create a newEntity for this attribute
846
+ let newEntityName = null;
847
+ if (attr.entity_name) {
848
+ try {
849
+ if (expressionPlugin.contextAvailable(attr.entity_name, ctxt, typeInformation)) {
850
+ newEntityName = expressionPlugin.applyExpression(attr.entity_name, ctxt, typeInformation);
851
+ } else {
852
+ logger.warn(
853
+ context,
854
+ 'sendUpdateValueNgsiLD \n MULTI no context available for apply expression %j \n',
855
+ attr.entity_name
856
+ );
857
+ newEntityName = attr.entity_name;
858
+ }
859
+ newEntityName = newEntityName ? newEntityName : attr.entity_name;
860
+ } catch (e) {
861
+ logger.error(context, 'sendUpdateValueNgsiLD \n MULTI apply expression exception %j \n', e);
862
+ newEntityName = attr.entity_name;
863
+ }
864
+ logger.debug(
865
+ context,
866
+ 'sendUpdateValueNgsiLD \n MULTI apply expression %j \n result %j \n payload %j',
867
+ attr.entity_name,
868
+ newEntityName,
869
+ payload
870
+ );
871
+ }
872
+
873
+ let newEntity = {
874
+ id: newEntityName ? newEntityName : payload[0].id,
875
+ type: attr.entity_type ? attr.entity_type : payload[0].type
876
+ };
877
+ // Check if there is already a newEntity created
878
+ const alreadyEntity = payload.find((entity) => {
879
+ return entity.id === newEntity.id && entity.type === newEntity.type;
880
+ });
881
+ if (alreadyEntity) {
882
+ // Use alreadyEntity
883
+ alreadyEntity[attr.name] = newAttr;
884
+ } else {
885
+ // Add newEntity to payload
886
+ newEntity[attr.name] = newAttr;
887
+ if (
888
+ 'timestamp' in typeInformation && typeInformation.timestamp !== undefined
889
+ ? typeInformation.timestamp
890
+ : config.getConfig().timestamp !== undefined
891
+ ? config.getConfig().timestamp
892
+ : timestampValue !== undefined
893
+ ) {
894
+ newEntity = NGSIv2.addTimestamp(newEntity, typeInformation.timezone, timestampValue);
895
+ logger.debug(context, 'sendUpdateValueNgsiLD \n timestamped newEntity=%j', newEntity);
896
+ }
897
+ payload.push(newEntity);
898
+ }
899
+ if (attr && attr.name) {
900
+ if (attr.name !== j) {
901
+ logger.debug(
902
+ context,
903
+ 'sendUpdateValueNgsiLD \n MULTI remove measure attr %j keep alias j %j from %j \n',
904
+ j,
905
+ attr,
906
+ payload
907
+ );
908
+ delete payload[0][j];
909
+ }
910
+ }
911
+ // if (attr && (attr.entity_type || attr.entity_name))
912
+ } else {
913
+ // Not a multientity attr
914
+ if (attr && attr.name) {
915
+ payload[0][attr.name] = newAttr;
916
+ if (attr.name !== j) {
917
+ delete payload[0][j]; // keep alias name, remove measure name
918
+ }
919
+ }
920
+ if (newAttr && newAttr.type === constants.TIMESTAMP_TYPE_NGSI2 && newAttr.value) {
921
+ const extendedTime = compressTimestampPlugin.fromBasicToExtended(newAttr.value);
922
+ if (extendedTime) {
923
+ // TBD: there is not flag about compressTimestamp in iotagent-node-lib,
924
+ // but there is one in agents
925
+ newAttr.value = extendedTime;
926
+ }
927
+ }
928
+ if (j === constants.TIMESTAMP_ATTRIBUTE) {
929
+ if (newAttr && newAttr.type === constants.TIMESTAMP_TYPE_NGSI2 && newAttr.value) {
930
+ timestampValue = newAttr.value;
931
+ logger.debug(
932
+ context,
933
+ 'sendUpdateValueNgsiLD \n newAttr is TimeInstant and new payload=%j',
934
+ payload
935
+ );
936
+ }
937
+ }
938
+ if (
939
+ newAttr &&
940
+ newAttr.metadata &&
941
+ newAttr.metadata[constants.TIMESTAMP_ATTRIBUTE] &&
942
+ newAttr.metadata[constants.TIMESTAMP_ATTRIBUTE].type === constants.TIMESTAMP_TYPE_NGSI2 &&
943
+ newAttr.metadata[constants.TIMESTAMP_ATTRIBUTE].value
944
+ ) {
945
+ const extendedTime = compressTimestampPlugin.fromBasicToExtended(
946
+ newAttr.metadata[constants.TIMESTAMP_ATTRIBUTE].value
947
+ );
948
+ if (extendedTime) {
949
+ newAttr.metadata[constants.TIMESTAMP_ATTRIBUTE].value = extendedTime;
950
+ }
951
+ }
952
+ }
953
+ } // if (j !== 'id' || j !== 'type')
954
+
955
+ // final attr loop
956
+ logger.debug(
957
+ context,
958
+ 'sendUpdateValueNgsiLD \n after procesing attr %j \n current entity %j \n current payload=%j',
959
+ j,
960
+ currentEntity,
961
+ payload
962
+ );
963
+ }
964
+ // for attr loop
965
+
966
+ // Add timestamp to paylaod
967
+ if (
968
+ 'timestamp' in typeInformation && typeInformation.timestamp !== undefined
969
+ ? typeInformation.timestamp
970
+ : config.getConfig().timestamp !== undefined
971
+ ? config.getConfig().timestamp
972
+ : timestampValue !== undefined
973
+ ) {
974
+ if (timestampValue) {
975
+ // timeInstant is provided as measure
976
+ if (Object.keys(payload[0]).length > 3) {
977
+ // include metadata with TimeInstant in attrs when TimeInstant is provided as measure in all entities
978
+ payload[0] = NGSIv2.addTimestamp(
979
+ payload[0],
980
+ typeInformation.timezone,
981
+ timestampValue,
982
+ false // skipMetadataAtt
983
+ );
984
+ } else {
985
+ // Do not include metadata with TimeInstant in attrs when TimeInstant is provided as measure
986
+ // and no more entities
987
+ payload[0] = NGSIv2.addTimestamp(
988
+ payload[0],
989
+ typeInformation.timezone,
990
+ timestampValue,
991
+ true // skipMetadataAtt
656
992
  );
657
993
  }
994
+ } else {
995
+ // jshint maxdepth:5
996
+ for (let n = 0; n < payload.length; n++) {
997
+ if (!utils.isTimestampedNgsi2(payload[n])) {
998
+ // legacy check needed?
999
+ payload[n] = NGSIv2.addTimestamp(payload[n], typeInformation.timezone);
1000
+ // jshint maxdepth:5
1001
+ } else if (!utils.IsValidTimestampedNgsi2(payload[n])) {
1002
+ // legacy check needed?
1003
+ logger.error(context, 'Invalid timestamp:%s', JSON.stringify(payload[0]));
1004
+ callback(new errors.BadTimestamp(payload));
1005
+ return;
1006
+ }
1007
+ }
658
1008
  }
659
- );
1009
+ }
1010
+
1011
+ logger.debug(context, 'sendUpdateValueNgsiLD \n ending payload=%j', payload);
1012
+
1013
+ for (let m = 0; m < payload.length; m++) {
1014
+ for (const key in payload[m]) {
1015
+ // purge object_id from payload
1016
+ if (payload[m][key] && payload[m][key].object_id) {
1017
+ delete payload[m][key].object_id;
1018
+ }
1019
+ }
1020
+ payload[m] = NGSIUtils.castJsonNativeAttributes(payload[m]); // native types
1021
+ }
1022
+ logger.debug(context, 'sendUpdateValueNgsiLD \n payload with native types and without object_id %j', payload);
1023
+
1024
+ options.json = payload;
1025
+ try {
1026
+ if (payload instanceof Array) {
1027
+ options.json = _.map(options.json, formatAsNGSILD);
1028
+ } else {
1029
+ options.json.id = entityName;
1030
+ options.json.type = typeInformation.type;
1031
+ options.json = [formatAsNGSILD(options.json)];
1032
+ }
1033
+ } catch (error) {
1034
+ return callback(new errors.BadGeocoordinates(JSON.stringify(payload)));
1035
+ }
1036
+
1037
+ if (typeInformation.active) {
1038
+ addLinkedEntities(typeInformation, options.json);
1039
+ }
1040
+
1041
+ // Prevent to update an entity with an empty payload
1042
+ if (
1043
+ Object.keys(options.json).length > 0 &&
1044
+ (options.json.length > 1 || (options.json.length === 1 && Object.keys(options.json[0]).length > 2)) // more than id and type
1045
+ ) {
1046
+ logger.debug(context, 'Updating device value in the Context Broker at [%s]', options.url);
1047
+ logger.debug(context, 'Using the following NGSI LD request:\n\n%s\n\n', JSON.stringify(options, null, 4));
1048
+ request(
1049
+ options,
1050
+ generateNGSILDOperationHandler('update', entityName, typeInformation, token, options, callback)
1051
+ );
1052
+ } else {
1053
+ logger.debug(
1054
+ context,
1055
+ 'Not updating device value in the Context Broker at [%s] due to empty payload \n\n[%s]\n\n',
1056
+ options.url,
1057
+ JSON.stringify(options, null, 4)
1058
+ );
1059
+ callback(null);
1060
+ }
660
1061
  }
661
1062
 
662
1063
  exports.convertAttrNGSILD = convertAttrNGSILD;