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
@@ -28,9 +28,6 @@
28
28
  /* eslint-disable consistent-return */
29
29
 
30
30
  const request = require('../../request-shim');
31
- const statsService = require('./../stats/statsRegistry');
32
- const async = require('async');
33
- const apply = async.apply;
34
31
  const alarms = require('../common/alarmManagement');
35
32
  const errors = require('../../errors');
36
33
  const utils = require('../northBound/restUtils');
@@ -38,13 +35,14 @@ const pluginUtils = require('../../plugins/pluginUtils');
38
35
  const config = require('../../commonConfig');
39
36
  const constants = require('../../constants');
40
37
  const jexlParser = require('../../plugins/jexlParser');
38
+ const expressionPlugin = require('../../plugins/expressionPlugin');
39
+ const compressTimestampPlugin = require('../../plugins/compressTimestamp');
41
40
  const moment = require('moment-timezone');
42
41
  const NGSIUtils = require('./ngsiUtils');
43
42
  const logger = require('logops');
44
43
  const context = {
45
44
  op: 'IoTAgentNGSI.Entities-v2'
46
45
  };
47
- const _ = require('underscore');
48
46
 
49
47
  /**
50
48
  * Amends an NGSIv2 Geoattribute from String to GeoJSON format
@@ -96,16 +94,29 @@ function formatGeoAttrs(attr) {
96
94
  return obj;
97
95
  }
98
96
 
99
- function addTimestampNgsi2(payload, timezone) {
100
- function addTimestampEntity(entity, timezone) {
97
+ /**
98
+ * Adds timestamp to ngsiv2 payload entities accoding to timezone, and an optional timestampvalue.
99
+ *
100
+ * @param {Object} payload NGSIv2 payload with one or more entities
101
+ * @param String timezone TimeZone value (optional)
102
+ * @param String timestampValue Timestamp value (optional). If not provided current timestamp is used
103
+ * @param Boolean skipMetadataAtt An optional flag to indicate if timestamp should be added to each metadata attribute. Default is false
104
+ * @return {Object} NGSIv2 payload entities with timestamp
105
+ */
106
+ function addTimestampNgsi2(payload, timezone, timestampValue, skipMetadataAtt) {
107
+ function addTimestampEntity(entity, timezone, timestampValue) {
101
108
  const timestamp = {
102
109
  type: constants.TIMESTAMP_TYPE_NGSI2
103
110
  };
104
111
 
105
- if (!timezone) {
106
- timestamp.value = new Date().toISOString();
112
+ if (timestampValue) {
113
+ timestamp.value = timestampValue;
107
114
  } else {
108
- timestamp.value = moment().tz(timezone).format('YYYY-MM-DD[T]HH:mm:ss.SSSZ');
115
+ if (!timezone) {
116
+ timestamp.value = new Date().toISOString();
117
+ } else {
118
+ timestamp.value = moment().tz(timezone).format('YYYY-MM-DD[T]HH:mm:ss.SSSZ');
119
+ }
109
120
  }
110
121
 
111
122
  function addMetadata(attribute) {
@@ -137,7 +148,9 @@ function addTimestampNgsi2(payload, timezone) {
137
148
  for (const key in entity) {
138
149
  /* eslint-disable-next-line no-prototype-builtins */
139
150
  if (entity.hasOwnProperty(key) && key !== 'id' && key !== 'type') {
140
- addMetadata(entity[key]);
151
+ if (!skipMetadataAtt) {
152
+ addMetadata(entity[key]);
153
+ }
141
154
  keyCount += 1;
142
155
  }
143
156
  }
@@ -153,13 +166,13 @@ function addTimestampNgsi2(payload, timezone) {
153
166
  if (payload instanceof Array) {
154
167
  for (let i = 0; i < payload.length; i++) {
155
168
  if (!utils.isTimestampedNgsi2(payload[i])) {
156
- payload[i] = addTimestampEntity(payload[i], timezone);
169
+ payload[i] = addTimestampEntity(payload[i], timezone, timestampValue);
157
170
  }
158
171
  }
159
172
 
160
173
  return payload;
161
174
  }
162
- return addTimestampEntity(payload, timezone);
175
+ return addTimestampEntity(payload, timezone, timestampValue);
163
176
  }
164
177
 
165
178
  /**
@@ -314,151 +327,14 @@ function sendQueryValueNgsi2(entityName, attributes, typeInformation, token, cal
314
327
  );
315
328
  }
316
329
 
317
- function extractContextFromPayload(payload) {
318
- //{b: {value: 3,type: "string"},c: {value: false,type: "string"},d:{value: {g: 45},type: "string"},id: "tamcaysanto",type: "WaterTankMulti"}
319
- //to
320
- //{b:3,c:false,d:{g:45}}
321
-
322
- let ctx = {};
323
- for (const key in payload) {
324
- if (key !== 'type' && key !== 'id') {
325
- ctx[key] = payload[key].value;
326
- }
327
- }
328
- return ctx;
329
- }
330
330
  /**
331
- * Remove id, type and any hidden attrs after processing
331
+ * Determines if a value is of type float
332
332
  *
333
- * @param {Object} enities Unprocessed entities
334
- * @param {Object} typeInformation Configuration information for the device.
333
+ * @param {String} value Value to be analyzed
334
+ * @return {boolean} True if float, False otherwise.
335
335
  */
336
- function removeHiddenAttrsFromMultiEntity(entities, typeInformation, payload) {
337
- function filterForEntity(entity, explicitAttrsList, typeInformation) {
338
- let effectiveList = [];
339
- for (const attr of explicitAttrsList) {
340
- if (typeof attr === 'string') {
341
- effectiveList.push(attr);
342
- } else if (typeof attr === 'object' && attr.object_id) {
343
- //find objectId in active attributes
344
- for (const active of typeInformation.active) {
345
- if (active.object_id === attr.object_id) {
346
- if (
347
- active &&
348
- active.name &&
349
- active.entity_name &&
350
- active.entity_name === entity.id
351
- //if name is an expression it could fail... BUT IT WORKS!
352
- ) {
353
- effectiveList.push(active.name);
354
- }
355
- continue; //object_id must be unique
356
- }
357
- }
358
- }
359
- }
360
- return effectiveList;
361
- }
362
-
363
- let explicitAttrsList;
364
- if (typeInformation.explicitAttrs) {
365
- explicitAttrsList = [];
366
- explicitAttrsList.push('type');
367
- explicitAttrsList.push('id');
368
- if (typeof typeInformation.explicitAttrs === 'boolean') {
369
- if (typeInformation.timestamp) {
370
- explicitAttrsList.push(constants.TIMESTAMP_ATTRIBUTE);
371
- }
372
-
373
- if (typeInformation.active) {
374
- typeInformation.active.forEach((attr) => {
375
- explicitAttrsList.push(attr.name);
376
- });
377
- }
378
- if (typeInformation.staticAttributes) {
379
- typeInformation.staticAttributes.forEach((attr) => {
380
- explicitAttrsList.push(attr.name);
381
- });
382
- }
383
- } else if (typeof typeInformation.explicitAttrs === 'string') {
384
- entities.forEach((entity) => {
385
- const attsArray = pluginUtils.extractAttributesArrayFromNgsi2Entity(entity);
386
- const ctx = {
387
- ...jexlParser.extractContext(attsArray),
388
- ...extractContextFromPayload(payload)
389
- }; //maybe overlapping between this two objects.
390
- let res = null;
391
- try {
392
- res = jexlParser.applyExpression(typeInformation.explicitAttrs, ctx, typeInformation);
393
- } catch (e) {
394
- // nothing to do: exception is already logged at info level
395
- }
396
- explicitAttrsList = res ? explicitAttrsList.concat(res) : explicitAttrsList;
397
- });
398
- }
399
- }
400
- if (explicitAttrsList && explicitAttrsList.length >= 0) {
401
- entities.forEach((entity) => {
402
- //some attrs are object_ids {object_id:oid} and need to be resolved for this entity
403
- //"explicitAttrs" : "attrA?['attrA',{object_id:'foo'}]:['attrB',{object_id:'bar'}]",
404
- let efectiveAttrsList = filterForEntity(entity, explicitAttrsList, typeInformation);
405
- const hidden = _.difference(_.keys(entity), efectiveAttrsList);
406
- logger.debug(context, 'removeHiddenAttrsFromMultiEntity %s from entity %s', hidden, entity);
407
- hidden.forEach((attr) => {
408
- delete entity[attr];
409
- });
410
- });
411
- }
412
- return entities;
413
- }
414
-
415
- /**
416
- * Remove id, type and any hidden attrs after processing
417
- *
418
- * @param {Object} result An Unprocessed entity
419
- * @param {Object} typeInformation Configuration information for the device.
420
- */
421
- function removeHiddenAttrs(result, typeInformation, payload) {
422
- delete result.id;
423
- delete result.type;
424
- let explicitAttrsList;
425
- if (typeInformation.explicitAttrs && typeof typeInformation.explicitAttrs === 'boolean') {
426
- explicitAttrsList = [];
427
- if (typeInformation.timestamp) {
428
- explicitAttrsList.push(constants.TIMESTAMP_ATTRIBUTE);
429
- }
430
- if (typeInformation.active) {
431
- typeInformation.active.forEach((attr) => {
432
- explicitAttrsList.push(attr.name);
433
- });
434
- }
435
- if (typeInformation.staticAttributes) {
436
- typeInformation.staticAttributes.forEach((attr) => {
437
- explicitAttrsList.push(attr.name);
438
- });
439
- }
440
- } else if (typeInformation.explicitAttrs && typeof typeInformation.explicitAttrs === 'string') {
441
- const attsArray = pluginUtils.extractAttributesArrayFromNgsi2Entity(result);
442
- const ctx = {
443
- ...jexlParser.extractContext(attsArray),
444
- ...extractContextFromPayload(payload)
445
- }; //maybe overlapping between this two objects. Measures not in active attrs.
446
- let res = null;
447
- try {
448
- res = jexlParser.applyExpression(typeInformation.explicitAttrs, ctx, typeInformation);
449
- } catch (e) {
450
- // nothing to do: exception is already logged at info level
451
- }
452
- explicitAttrsList = res ? res : [];
453
- }
454
- if (explicitAttrsList && explicitAttrsList.length >= 0) {
455
- const hidden = _.difference(_.keys(result), explicitAttrsList);
456
- logger.debug(context, 'removeHiddenAttrs %s', hidden);
457
- hidden.forEach((attr) => {
458
- delete result[attr];
459
- });
460
- }
461
- return result;
336
+ function isFloat(value) {
337
+ return !isNaN(value) && value.toString().indexOf('.') !== -1;
462
338
  }
463
339
 
464
340
  /**
@@ -471,12 +347,31 @@ function removeHiddenAttrs(result, typeInformation, payload) {
471
347
  * @param {String} token User token to identify against the PEP Proxies (optional).
472
348
  */
473
349
  function sendUpdateValueNgsi2(entityName, attributes, typeInformation, token, callback) {
474
- let payload = {};
350
+ logger.debug(
351
+ context,
352
+ 'sendUpdateValueNgsi2 called with: \n entityName=%s \n attributes=%j \n typeInformation=%j',
353
+ entityName,
354
+ attributes,
355
+ typeInformation
356
+ );
357
+ let payload = {
358
+ entities: [
359
+ {
360
+ id: entityName
361
+ }
362
+ ]
363
+ };
475
364
 
476
- let url = '/v2/entities/' + entityName + '/attrs';
365
+ let url = '/v2/op/update';
477
366
 
478
367
  if (typeInformation && typeInformation.type) {
479
- url += '?type=' + typeInformation.type;
368
+ payload.entities[0].type = typeInformation.type;
369
+ }
370
+
371
+ if (config.getConfig().appendMode === true) {
372
+ payload.actionType = 'append';
373
+ } else {
374
+ payload.actionType = 'update';
480
375
  }
481
376
 
482
377
  let options = NGSIUtils.createRequestObject(url, typeInformation, token);
@@ -489,173 +384,600 @@ function sendUpdateValueNgsi2(entityName, attributes, typeInformation, token, ca
489
384
  callback(new errors.TypeNotFound(null, entityName));
490
385
  return;
491
386
  }
492
-
493
- for (let i = 0; i < attributes.length; i++) {
494
- if (attributes[i].name && attributes[i].type) {
495
- payload[attributes[i].name] = {
496
- value: attributes[i].value,
497
- type: attributes[i].type
498
- };
499
- const metadata = NGSIUtils.getMetaData(typeInformation, attributes[i].name, attributes[i].metadata);
500
- if (metadata) {
501
- payload[attributes[i].name].metadata = metadata;
387
+ let idTypeSSSList = pluginUtils.getIdTypeServSubServiceFromDevice(typeInformation);
388
+ logger.debug(context, 'sendUpdateValueNgsi2 \n idTypeSSS are %j ', idTypeSSSList);
389
+ let measureAttrsForCtxt = [];
390
+
391
+ // Check explicitAttrs: adds all final needed attributes to payload
392
+ if (
393
+ typeInformation.explicitAttrs === undefined ||
394
+ (typeof typeInformation.explicitAttrs === 'boolean' && !typeInformation.explicitAttrs)
395
+ // explicitAttrs is not defined => default case: all attrs should be included
396
+ ) {
397
+ // This loop adds all measure values (attributes) into payload entities (entity[0])
398
+ for (let i = 0; i < attributes.length; i++) {
399
+ if (attributes[i].name && attributes[i].type) {
400
+ payload.entities[0][attributes[i].name] = {
401
+ value: attributes[i].value,
402
+ type: attributes[i].type
403
+ };
404
+ const metadata = NGSIUtils.getMetaData(typeInformation, attributes[i].name, attributes[i].metadata);
405
+ if (metadata) {
406
+ payload.entities[0][attributes[i].name].metadata = metadata;
407
+ }
408
+ } else {
409
+ callback(new errors.BadRequest(null, entityName));
410
+ return;
502
411
  }
503
- } else {
504
- callback(new errors.BadRequest(null, entityName));
505
- return;
506
412
  }
507
- }
508
- //overwritte if id or type missnamed atributes reach this point
509
- payload.id = entityName;
510
- payload.type = typeInformation.type;
511
- payload = NGSIUtils.castJsonNativeAttributes(payload);
512
- async.waterfall(
513
- [
514
- apply(statsService.add, 'measureRequests', 1),
515
- apply(NGSIUtils.applyMiddlewares, NGSIUtils.updateMiddleware, payload, typeInformation)
516
- ],
517
- function (error, result) {
518
- if (error) {
519
- callback(error);
520
- } else {
521
- if (result) {
522
- // The payload has been transformed by multientity plugin. It is not a JSON object but an Array.
523
- if (result instanceof Array) {
524
- options = NGSIUtils.createRequestObject('/v2/op/update', typeInformation, token);
525
-
526
- if (
527
- 'timestamp' in typeInformation && typeInformation.timestamp !== undefined
528
- ? typeInformation.timestamp
529
- : config.getConfig().timestamp
530
- ) {
531
- // jshint maxdepth:5
532
- if (!utils.isTimestampedNgsi2(result)) {
533
- result = addTimestampNgsi2(result, typeInformation.timezone);
534
- // jshint maxdepth:5
535
- } else if (!utils.IsValidTimestampedNgsi2(result)) {
536
- logger.error(context, 'Invalid timestamp:%s', JSON.stringify(result));
537
- callback(new errors.BadTimestamp(result));
538
- return;
539
- }
540
- }
541
- options.json = {
542
- actionType: 'append',
543
- entities: removeHiddenAttrsFromMultiEntity(result, typeInformation, payload)
413
+ logger.debug(context, 'sendUpdateValueNgsi2 \n pre-initial non-explicitAttrs payload=%j', payload);
414
+ // Loop for add attrs from type.information.active (and lazys?) into payload entities (entity[0])
415
+ if (typeInformation.active) {
416
+ typeInformation.active.forEach((attr) => {
417
+ if (attr.expression) {
418
+ if (attr.object_id) {
419
+ payload.entities[0][attr.object_id] = {
420
+ value: payload.entities[0][attr.object_id]
421
+ ? payload.entities[0][attr.object_id].value
422
+ : undefined,
423
+ type: attr.type,
424
+ object_id: attr.object_id
544
425
  };
545
- if (config.getConfig().appendMode === true) {
546
- options.json.actionType = 'append';
547
- } else {
548
- options.json.actionType = 'update';
549
- }
550
426
  } else {
551
- options.json = removeHiddenAttrs(result, typeInformation, payload);
552
- logger.debug(context, 'typeInformation: %j', typeInformation);
553
- if (
554
- 'timestamp' in typeInformation && typeInformation.timestamp !== undefined
555
- ? typeInformation.timestamp
556
- : config.getConfig().timestamp
557
- ) {
558
- if (!utils.isTimestampedNgsi2(options.json)) {
559
- options.json = addTimestampNgsi2(options.json, typeInformation.timezone);
560
- } else if (!utils.IsValidTimestampedNgsi2(options.json)) {
561
- logger.error(context, 'Invalid timestamp:%s', JSON.stringify(options.json));
562
- callback(new errors.BadTimestamp(options.json));
563
- return;
564
- }
565
- }
566
- if (config.getConfig().appendMode === true) {
567
- options.method = 'POST';
568
- } else {
569
- options.method = 'PATCH';
427
+ payload.entities[0][attr.name] = {
428
+ value: payload.entities[0][attr.name] ? payload.entities[0][attr.name].value : undefined,
429
+ type: attr.type
430
+ };
431
+ }
432
+ }
433
+ });
434
+ }
435
+ } else {
436
+ let selectedAttrs = [];
437
+ if (typeof typeInformation.explicitAttrs === 'string') {
438
+ // explicitAttrs is a jexlExpression
439
+ // This ctxt should include all possible attrs
440
+ let attributesCtxt = [];
441
+ if (typeInformation.static) {
442
+ typeInformation.static.forEach(function (att) {
443
+ attributesCtxt.push(att);
444
+ });
445
+ }
446
+ // Measures
447
+ for (let i = 0; i < attributes.length; i++) {
448
+ if (attributes[i].name && attributes[i].type) {
449
+ let measureAttr = {
450
+ name: attributes[i].name,
451
+ value: attributes[i].value,
452
+ type: attributes[i].type
453
+ };
454
+ attributesCtxt.push(measureAttr);
455
+ }
456
+ }
457
+ // This context is just to calculate explicitAttrs when is an expression
458
+ let ctxt = expressionPlugin.extractContext(attributesCtxt.concat(idTypeSSSList), typeInformation);
459
+ // typeInformation.active attrs with expressions expanded by current ctxt
460
+ if (typeInformation.active) {
461
+ typeInformation.active.forEach(function (att) {
462
+ if (att.expression) {
463
+ if (expressionPlugin.contextAvailable(att.expression, ctxt, typeInformation)) {
464
+ let expandedAttr = {
465
+ name: att.name,
466
+ value: att.expression, // it doesn't matter final value here
467
+ type: att.type
468
+ };
469
+ attributesCtxt.push(expandedAttr);
470
+ ctxt = expressionPlugin.extractContext(
471
+ attributesCtxt.concat(idTypeSSSList),
472
+ typeInformation
473
+ );
570
474
  }
571
475
  }
476
+ });
477
+ }
478
+ // calculate expression for explicitAttrs
479
+ try {
480
+ let res = jexlParser.applyExpression(typeInformation.explicitAttrs, ctxt, typeInformation);
481
+ if (res === true) {
482
+ // like explicitAttrs == true
483
+ // selectAttrs should be measures which are defined attributes
484
+ typeInformation.active.forEach((attr) => {
485
+ selectedAttrs.push(attr.name);
486
+ selectedAttrs.push(attr.object_id);
487
+ });
488
+ } else if (res === false) {
489
+ // like explicitAttrs == false
490
+ // selectAttrs should be measures and defined attributes
491
+ typeInformation.active.forEach((attr) => {
492
+ selectedAttrs.push(attr.name);
493
+ selectedAttrs.push(attr.object_id);
494
+ });
495
+ for (let i = 0; i < attributes.length; i++) {
496
+ selectedAttrs.push(attributes[i].name);
497
+ }
572
498
  } else {
573
- delete payload.id;
574
- delete payload.type;
575
- options.json = payload;
499
+ selectedAttrs = res; // TBD: Check ensure is an array of strings
576
500
  }
577
- // Purge object_id from entities before sent to CB
578
- // object_id was added by createNgsi2Entity to allow multientity
579
- // with duplicate attribute names.
580
-
581
- //some entities may be empty if they had only wrong attributes id-type
582
- //we filter the array
583
- if (options.json.entities) {
584
- options.json.entities = options.json.entities.filter((value) => Object.keys(value).length > 0);
501
+ if (selectedAttrs.length === 0) {
502
+ // implies do nothing
503
+ logger.info(
504
+ context,
505
+ 'sendUpdateValueNgsi2 \n none selectedAttrs with %j and ctxt %j',
506
+ typeInformation.explicitAttrs,
507
+ ctxt
508
+ );
509
+ return callback(null);
585
510
  }
511
+ } catch (e) {
512
+ // nothing to do: exception is already logged at info level
513
+ }
586
514
 
587
- let att;
588
- if (options.json.entities) {
589
- for (let entity = 0; entity < options.json.entities.length; entity++) {
590
- for (att in options.json.entities[entity]) {
591
- /*jshint camelcase: false */
592
- if (options.json.entities[entity][att].object_id) {
593
- /*jshint camelcase: false */
594
- delete options.json.entities[entity][att].object_id;
595
- }
596
- if (options.json.entities[entity][att].multi) {
597
- delete options.json.entities[entity][att].multi;
598
- }
599
- if (options.json.entities[entity][att].name) {
600
- delete options.json.entities[entity][att].name;
601
- }
602
-
603
- try {
604
- // Format any GeoJSON attrs properly
605
- options.json.entities[entity][att] = formatGeoAttrs(options.json.entities[entity][att]);
606
- } catch (error) {
607
- return callback(new errors.BadGeocoordinates(JSON.stringify(options.json)));
608
- }
609
- }
515
+ typeInformation.active.forEach((attr) => {
516
+ if (selectedAttrs.includes(attr.name)) {
517
+ selectedAttrs.push(attr.object_id);
518
+ }
519
+ });
520
+ } else if (typeInformation.explicitAttrs && typeof typeInformation.explicitAttrs === 'boolean') {
521
+ // TBD: selectedAttrs could be a boolean as a result of applyExpression
522
+ // explicitAtts is true => Add measures which are defined attributes
523
+ typeInformation.active.forEach((attr) => {
524
+ selectedAttrs.push(attr.name);
525
+ selectedAttrs.push(attr.object_id);
526
+ });
527
+ }
528
+ // This loop adds selected measured values (attributes) into payload entities (entity[0])
529
+ for (let i = 0; i < attributes.length; i++) {
530
+ if (attributes[i].name && selectedAttrs.includes(attributes[i].name) && attributes[i].type) {
531
+ let attr = typeInformation.active.find((obj) => {
532
+ return obj.name === attributes[i].name;
533
+ });
534
+ payload.entities[0][attributes[i].name] = {
535
+ value: attributes[i].value,
536
+ type: attributes[i].type
537
+ };
538
+ // ensure payload has attr with proper object_id
539
+ if (attr && attr.object_id) {
540
+ payload.entities[0][attributes[i].name].object_id = attr.object_id;
541
+ }
542
+ const metadata = NGSIUtils.getMetaData(typeInformation, attributes[i].name, attributes[i].metadata);
543
+ if (metadata) {
544
+ payload.entities[0][attributes[i].name].metadata = metadata;
545
+ }
546
+ } else if (attributes[i].name && !selectedAttrs.includes(attributes[i].name) && attributes[i].type) {
547
+ let att = {
548
+ name: attributes[i].name,
549
+ type: attributes[i].type,
550
+ value: attributes[i].value
551
+ };
552
+ measureAttrsForCtxt.push(att);
553
+ }
554
+ }
555
+ logger.debug(
556
+ context,
557
+ 'sendUpdateValueNgsi2 \n pre-initial explicitAttrs payload=%j \n selectedAttrs',
558
+ payload,
559
+ selectedAttrs
560
+ );
561
+
562
+ // Loop for add seleted attrs from type.information.active into pyaload entities (entity[0])
563
+ if (typeInformation.active) {
564
+ typeInformation.active.forEach((attr) => {
565
+ if (selectedAttrs.includes(attr.name)) {
566
+ if (attr.object_id) {
567
+ payload.entities[0][attr.object_id] = {
568
+ value: payload.entities[0][attr.object_id]
569
+ ? payload.entities[0][attr.object_id].value
570
+ : payload.entities[0][attr.name]
571
+ ? payload.entities[0][attr.name].value
572
+ : undefined,
573
+ type: attr.type,
574
+ object_id: attr.object_id
575
+ };
576
+ } else {
577
+ payload.entities[0][attr.name] = {
578
+ value: payload.entities[0][attr.name] ? payload.entities[0][attr.name].value : undefined,
579
+ type: attr.type
580
+ };
610
581
  }
582
+ }
583
+ });
584
+ }
585
+ } // END check explicitAttrs
586
+ logger.debug(context, 'sendUpdateValueNgsi2 \n initial payload=%j', payload);
587
+
588
+ let currentEntity = payload.entities[0];
589
+
590
+ // Prepare attributes for expresionPlugin
591
+ const attsArray = pluginUtils.extractAttributesArrayFromNgsi2Entity(currentEntity);
592
+
593
+ // Exclude processing all attr expressions when current attr is of type 'commandStatus' or 'commandResult'
594
+ let attsArrayFiltered = [];
595
+ if (attsArray) {
596
+ attsArrayFiltered = attsArray.filter((obj) => {
597
+ return ![constants.COMMAND_STATUS, constants.COMMAND_RESULT].includes(obj.type);
598
+ });
599
+ }
600
+ let attributesCtxt = [...attsArrayFiltered]; // just copy
601
+ if (typeInformation.static) {
602
+ typeInformation.static.forEach(function (att) {
603
+ attributesCtxt.push(att);
604
+ });
605
+ }
606
+ if (measureAttrsForCtxt) {
607
+ measureAttrsForCtxt.forEach(function (att) {
608
+ attributesCtxt.push(att);
609
+ });
610
+ }
611
+ attributesCtxt = attributesCtxt.concat(idTypeSSSList);
612
+ let ctxt = expressionPlugin.extractContext(attributesCtxt, typeInformation);
613
+ logger.debug(context, 'sendUpdateValueNgsi2 \n initial ctxt %j ', ctxt);
614
+
615
+ // Sort currentEntity to get first attrs without expressions (checking attrs in typeInformation.active)
616
+ // attributes without expressions should be processed before
617
+ logger.debug(context, 'sendUpdateValueNgsi2 \n currentEntity %j ', currentEntity);
618
+ if (typeInformation.active && typeInformation.active.length > 0) {
619
+ for (const k in currentEntity) {
620
+ typeInformation.active.forEach(function (att) {
621
+ if (
622
+ (att.object_id && att.object_id === k && att.expression) ||
623
+ (att.name && att.name === k && att.expression)
624
+ ) {
625
+ let m = currentEntity[k];
626
+ delete currentEntity[k];
627
+ currentEntity[k] = m; // put into the end of currentEntity
628
+ }
629
+ });
630
+ }
631
+ }
632
+ logger.debug(context, 'sendUpdateValueNgsi2 \n currentEntity sorted %j ', currentEntity);
633
+ let timestampValue = undefined;
634
+ // Loop for each final attribute to apply alias, multientity and expressions
635
+ for (const j in currentEntity) {
636
+ // discard id and type
637
+ if (j !== 'id' || j !== 'type') {
638
+ // Apply Mapping Alias: object_id in attributes are in typeInformation.active
639
+ let attr;
640
+ let newAttr = payload.entities[0][j];
641
+ if (typeInformation.active) {
642
+ attr = typeInformation.active.find((obj) => {
643
+ return obj.object_id === j;
644
+ });
645
+ }
646
+ if (!attr) {
647
+ if (typeInformation.lazy) {
648
+ attr = typeInformation.lazy.find((obj) => {
649
+ return obj.object_id === j;
650
+ });
651
+ }
652
+ }
653
+ if (!attr) {
654
+ if (typeInformation.active) {
655
+ attr = typeInformation.active.find((obj) => {
656
+ return obj.name === j;
657
+ });
658
+ }
659
+ }
660
+ if (attr && attr.name) {
661
+ if (['id', 'type'].includes(attr.name)) {
662
+ // invalid mapping
663
+ logger.debug(
664
+ context,
665
+ 'sendUpdateValueNgsi2 \n invalid mapping for attr %j \n newAttr %j',
666
+ attr,
667
+ newAttr
668
+ );
669
+ delete payload.entities[0][attr.object_id];
670
+ attr = undefined; // stop processing attr
671
+ newAttr = undefined;
611
672
  } else {
612
- for (att in options.json) {
613
- /*jshint camelcase: false */
614
- if (options.json[att].object_id) {
615
- /*jshint camelcase: false */
616
- delete options.json[att].object_id;
617
- }
618
- if (options.json[att].multi) {
619
- delete options.json[att].multi;
620
- }
621
- if (options.json[att].name) {
622
- delete options[att].name;
623
- }
673
+ ctxt[attr.name] = payload.entities[0][j]['value'];
674
+ }
675
+ }
676
+ logger.debug(
677
+ context,
678
+ 'sendUpdateValueNgsi2 \n procesing j %j attr %j ctxt %j \n newAttr %j ',
679
+ j,
680
+ attr,
681
+ ctxt,
682
+ newAttr
683
+ );
684
+ if (attr && attr.type) {
685
+ newAttr.type = attr.type;
686
+ }
624
687
 
625
- try {
626
- // Format any GeoJSON attrs properly
627
- options.json[att] = formatGeoAttrs(options.json[att]);
628
- } catch (error) {
629
- return callback(new errors.BadGeocoordinates(JSON.stringify(options.json)));
688
+ // Apply expression
689
+ if (attr && attr.expression) {
690
+ logger.debug(
691
+ context,
692
+ 'sendUpdateValueNgsi2 \n apply expression %j \n over ctxt %j \n and device %j',
693
+ attr.expression,
694
+ ctxt,
695
+ typeInformation
696
+ );
697
+ let res = null;
698
+ try {
699
+ if (expressionPlugin.contextAvailable(attr.expression, ctxt, typeInformation)) {
700
+ res = expressionPlugin.applyExpression(attr.expression, ctxt, typeInformation);
701
+ } else {
702
+ logger.warn(
703
+ context,
704
+ 'sendUpdateValueNgsi2 \n no context available for apply expression %j \n',
705
+ attr.expression
706
+ );
707
+ res = newAttr.value; // keep newAttr value
708
+ }
709
+ } catch (e) {
710
+ logger.error(context, 'sendUpdateValueNgsi2 \n apply expression exception %j \n', e);
711
+ if (!expressionPlugin.checkJexl(typeInformation)) {
712
+ return callback(e); // just throw error with legacy parser for backward compatiblity
713
+ } else {
714
+ res = ctxt[attr.name]; // TBD: add reference to test
715
+ }
716
+ }
717
+ if (!expressionPlugin.checkJexl(typeInformation)) {
718
+ // legacy expression plugin: Involves some legacy checks performed by applierExpression
719
+ if (
720
+ res &&
721
+ res !== 'undefined' &&
722
+ res !== 'null' &&
723
+ (typeof res === 'string' || res instanceof String) &&
724
+ !res.includes('NaN')
725
+ ) {
726
+ newAttr.value = res;
727
+ if (newAttr.type === 'Number' && isFloat(newAttr.value)) {
728
+ newAttr.value = Number.parseFloat(newAttr.value);
729
+ } else if (newAttr.type === 'Number' && !Number.isNaN(Number.parseInt(newAttr.value))) {
730
+ newAttr.value = Number.parseInt(newAttr.value);
731
+ } else if (newAttr.type === 'Boolean') {
732
+ newAttr.value = newAttr.value === 'true' || newAttr.value === '1';
733
+ } else if (newAttr.type === 'None') {
734
+ newAttr.value = null;
735
+ }
736
+ } else if (isNaN(res) || res === 'undefined' || res === 'null') {
737
+ logger.debug(
738
+ context,
739
+ 'sendUpdateValueNgsi2 \n apply expression result: isNaN || undefined || null'
740
+ );
741
+ if (res === 'null' && newAttr.type === 'None') {
742
+ newAttr.value = null;
743
+ } else {
744
+ delete payload.entities[0][j]; // remove measure attr
745
+ attr = undefined; // stop process attr
630
746
  }
631
747
  }
748
+ } else {
749
+ // jexl expression plugin
750
+ newAttr.value = res;
632
751
  }
752
+ logger.debug(context, 'sendUpdateValueNgsi2 \n apply expression result %j \n newAttr %j', res, newAttr);
753
+ }
633
754
 
634
- // Prevent to update an entity with an empty payload
635
- if (Object.keys(options.json).length > 0) {
636
- logger.debug(context, 'Updating device value in the Context Broker at [%s]', options.url);
755
+ // Apply Multientity: entity_type and entity_name in attributes are in typeInformation.active
756
+ if (attr && (attr.entity_type || attr.entity_name)) {
757
+ // Create a newEntity for this attribute
758
+ let newEntityName = null;
759
+ if (attr.entity_name) {
760
+ try {
761
+ if (expressionPlugin.contextAvailable(attr.entity_name, ctxt, typeInformation)) {
762
+ newEntityName = expressionPlugin.applyExpression(attr.entity_name, ctxt, typeInformation);
763
+ } else {
764
+ logger.warn(
765
+ context,
766
+ 'sendUpdateValueNgsi2 \n MULTI no context available for apply expression %j \n',
767
+ attr.entity_name
768
+ );
769
+ newEntityName = attr.entity_name;
770
+ }
771
+ newEntityName = newEntityName ? newEntityName : attr.entity_name;
772
+ } catch (e) {
773
+ logger.error(context, 'sendUpdateValueNgsi2 \n MULTI apply expression exception %j \n', e);
774
+ newEntityName = attr.entity_name;
775
+ }
637
776
  logger.debug(
638
777
  context,
639
- 'Using the following NGSI v2 request:\n\n%s\n\n',
640
- JSON.stringify(options, null, 4)
778
+ 'sendUpdateValueNgsi2 \n MULTI apply expression %j \n result %j \n payload %j',
779
+ attr.entity_name,
780
+ newEntityName,
781
+ payload
641
782
  );
783
+ }
642
784
 
643
- request(
644
- options,
645
- generateNGSI2OperationHandler('update', entityName, typeInformation, token, options, callback)
646
- );
785
+ let newEntity = {
786
+ id: newEntityName ? newEntityName : payload.entities[0].id,
787
+ type: attr.entity_type ? attr.entity_type : payload.entities[0].type
788
+ };
789
+ // Check if there is already a newEntity created
790
+ let alreadyEntity = payload.entities.find((entity) => {
791
+ return entity.id === newEntity.id && entity.type === newEntity.type;
792
+ });
793
+ if (alreadyEntity) {
794
+ // Use alreadyEntity
795
+ alreadyEntity[attr.name] = newAttr;
647
796
  } else {
648
- logger.debug(
649
- context,
650
- 'Not updating device value in the Context Broker at [%s] due to empty payload \n\n[%s]\n\n',
651
- options.url,
652
- JSON.stringify(options, null, 4)
797
+ // Add newEntity to payload.entities
798
+ newEntity[attr.name] = newAttr;
799
+ if (
800
+ 'timestamp' in typeInformation && typeInformation.timestamp !== undefined
801
+ ? typeInformation.timestamp
802
+ : config.getConfig().timestamp !== undefined
803
+ ? config.getConfig().timestamp
804
+ : timestampValue !== undefined
805
+ ) {
806
+ newEntity = addTimestampNgsi2(newEntity, typeInformation.timezone, timestampValue);
807
+ logger.debug(context, 'sendUpdateValueNgsi2 \n timestamped newEntity=%j', newEntity);
808
+ }
809
+ payload.entities.push(newEntity);
810
+ }
811
+ if (attr && attr.name) {
812
+ if (attr.name !== j) {
813
+ logger.debug(
814
+ context,
815
+ 'sendUpdateValueNgsi2 \n MULTI remove measure attr %j keep alias j %j from %j \n',
816
+ j,
817
+ attr,
818
+ payload
819
+ );
820
+ delete payload.entities[0][j];
821
+ }
822
+ }
823
+ // if (attr && (attr.entity_type || attr.entity_name))
824
+ } else {
825
+ // Not a multientity attr
826
+ if (attr && attr.name) {
827
+ payload.entities[0][attr.name] = newAttr;
828
+ if (attr.name !== j) {
829
+ delete payload.entities[0][j]; // keep alias name, remove measure name
830
+ }
831
+ }
832
+ if (newAttr && newAttr.type === constants.TIMESTAMP_TYPE_NGSI2 && newAttr.value) {
833
+ let extendedTime = compressTimestampPlugin.fromBasicToExtended(newAttr.value);
834
+ if (extendedTime) {
835
+ // TBD: there is not flag about compressTimestamp in iotagent-node-lib,
836
+ // but there is one in agents
837
+ newAttr.value = extendedTime;
838
+ }
839
+ }
840
+ if (j === constants.TIMESTAMP_ATTRIBUTE) {
841
+ if (newAttr && newAttr.type === constants.TIMESTAMP_TYPE_NGSI2 && newAttr.value) {
842
+ timestampValue = newAttr.value;
843
+ logger.debug(
844
+ context,
845
+ 'sendUpdateValueNgsi2 \n newAttr is TimeInstant and new payload=%j',
846
+ payload
847
+ );
848
+ }
849
+ }
850
+ if (
851
+ newAttr &&
852
+ newAttr.metadata &&
853
+ newAttr.metadata[constants.TIMESTAMP_ATTRIBUTE] &&
854
+ newAttr.metadata[constants.TIMESTAMP_ATTRIBUTE].type === constants.TIMESTAMP_TYPE_NGSI2 &&
855
+ newAttr.metadata[constants.TIMESTAMP_ATTRIBUTE].value
856
+ ) {
857
+ let extendedTime = compressTimestampPlugin.fromBasicToExtended(
858
+ newAttr.metadata[constants.TIMESTAMP_ATTRIBUTE].value
859
+ );
860
+ if (extendedTime) {
861
+ newAttr.metadata[constants.TIMESTAMP_ATTRIBUTE].value = extendedTime;
862
+ }
863
+ }
864
+ }
865
+ } // if (j !== 'id' || j !== 'type')
866
+
867
+ // final attr loop
868
+ logger.debug(
869
+ context,
870
+ 'sendUpdateValueNgsi2 \n after procesing attr %j \n current entity %j \n current payload=%j',
871
+ j,
872
+ currentEntity,
873
+ payload
874
+ );
875
+ }
876
+ // for attr loop
877
+
878
+ // Add timestamp to paylaod
879
+ if (
880
+ 'timestamp' in typeInformation && typeInformation.timestamp !== undefined
881
+ ? typeInformation.timestamp
882
+ : config.getConfig().timestamp !== undefined
883
+ ? config.getConfig().timestamp
884
+ : timestampValue !== undefined
885
+ ) {
886
+ if (timestampValue) {
887
+ // timeInstant is provided as measure
888
+ if (payload.entities.length > 1) {
889
+ for (let n = 0; n < payload.entities.length; n++) {
890
+ // include metadata with TimeInstant in attrs when TimeInstant is provided as measure in all entities
891
+ payload.entities[n] = addTimestampNgsi2(
892
+ payload.entities[n],
893
+ typeInformation.timezone,
894
+ timestampValue,
895
+ false // skipMetadataAtt
653
896
  );
654
- callback(null);
897
+ }
898
+ } else {
899
+ // Do not include metadata with TimeInstant in attrs when TimeInstant is provided as measure
900
+ // and no more entities
901
+ payload.entities[0] = addTimestampNgsi2(
902
+ payload.entities[0],
903
+ typeInformation.timezone,
904
+ timestampValue,
905
+ true // skipMetadataAtt
906
+ );
907
+ }
908
+ } else {
909
+ // jshint maxdepth:5
910
+ for (let n = 0; n < payload.entities.length; n++) {
911
+ if (!utils.isTimestampedNgsi2(payload.entities[n])) {
912
+ // legacy check needed?
913
+ payload.entities[n] = addTimestampNgsi2(payload.entities[n], typeInformation.timezone);
914
+ // jshint maxdepth:5
915
+ } else if (!utils.IsValidTimestampedNgsi2(payload.entities[n])) {
916
+ // legacy check needed?
917
+ logger.error(context, 'Invalid timestamp:%s', JSON.stringify(payload.entities[0]));
918
+ callback(new errors.BadTimestamp(payload.entities));
919
+ return;
655
920
  }
656
921
  }
657
922
  }
658
- );
923
+ }
924
+ logger.debug(context, 'sendUpdateValueNgsi2 \n ending payload=%j', payload);
925
+
926
+ for (let m = 0; m < payload.entities.length; m++) {
927
+ for (var key in payload.entities[m]) {
928
+ // purge object_id from payload
929
+ if (payload.entities[m][key] && payload.entities[m][key].object_id) {
930
+ delete payload.entities[m][key].object_id;
931
+ }
932
+ }
933
+ payload.entities[m] = NGSIUtils.castJsonNativeAttributes(payload.entities[m]); // native types
934
+ }
935
+ logger.debug(context, 'sendUpdateValueNgsi2 \n payload with native types and without object_id %j', payload);
936
+
937
+ options.json = payload;
938
+
939
+ // Prevent to update an entity with an empty payload
940
+ if (
941
+ Object.keys(options.json).length > 0 &&
942
+ (options.json.entities.length > 1 ||
943
+ (options.json.entities.length === 1 && Object.keys(options.json.entities[0]).length > 2)) // more than id and type
944
+ ) {
945
+ // Final check: (to keep tests unchanged) before do CB requests
946
+ // one entity -> request /v2/entities/ + entityName + /atts ?type=typeInformation.type
947
+ // multi entities -> request /v2/op/update
948
+ // Note that the options object is prepared for the second case (multi entity), so we "patch" it
949
+ // only in the first case
950
+ if (options.json.entities.length === 1) {
951
+ // recreate options object to use single entity update
952
+ url = '/v2/entities/' + entityName + '/attrs';
953
+ if (typeInformation && typeInformation.type) {
954
+ url += '?type=' + typeInformation.type;
955
+ }
956
+ options = NGSIUtils.createRequestObject(url, typeInformation, token);
957
+ options.json = payload.entities[0];
958
+ delete options.json.id;
959
+ delete options.json.type;
960
+ if (config.getConfig().appendMode === true) {
961
+ options.method = 'POST';
962
+ } else {
963
+ options.method = 'PATCH';
964
+ }
965
+ } // else: keep current options object created for a batch update
966
+ logger.debug(context, 'Updating device value in the Context Broker at [%s]', options.url);
967
+ logger.debug(context, 'Using the following NGSI v2 request:\n\n%s\n\n', JSON.stringify(options, null, 4));
968
+ request(
969
+ options,
970
+ generateNGSI2OperationHandler('update', entityName, typeInformation, token, options, callback)
971
+ );
972
+ } else {
973
+ logger.debug(
974
+ context,
975
+ 'Not updating device value in the Context Broker at [%s] due to empty payload \n\n[%s]\n\n',
976
+ options.url,
977
+ JSON.stringify(options, null, 4)
978
+ );
979
+ callback(null);
980
+ }
659
981
  }
660
982
 
661
983
  exports.sendQueryValue = sendQueryValueNgsi2;