iotagent-node-lib 4.3.0 → 4.5.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 (30) hide show
  1. package/config.js +2 -1
  2. package/doc/admin.md +16 -7
  3. package/doc/api.md +230 -45
  4. package/doc/requirements.txt +1 -1
  5. package/docker/Mosquitto/Dockerfile +1 -1
  6. package/lib/commonConfig.js +25 -1
  7. package/lib/errors.js +4 -1
  8. package/lib/services/devices/deviceRegistryMemory.js +1 -1
  9. package/lib/services/devices/deviceRegistryMongoDB.js +3 -10
  10. package/lib/services/devices/deviceService.js +5 -3
  11. package/lib/services/devices/devices-NGSI-LD.js +5 -5
  12. package/lib/services/devices/devices-NGSI-mixed.js +3 -3
  13. package/lib/services/devices/devices-NGSI-v2.js +5 -5
  14. package/lib/services/ngsi/entities-NGSI-LD.js +1 -1
  15. package/lib/services/ngsi/entities-NGSI-v2.js +324 -268
  16. package/lib/services/ngsi/ngsiService.js +2 -2
  17. package/lib/services/ngsi/subscription-NGSI-LD.js +2 -2
  18. package/lib/services/ngsi/subscription-NGSI-v2.js +2 -2
  19. package/lib/services/northBound/deviceProvisioningServer.js +5 -4
  20. package/lib/services/northBound/northboundServer.js +2 -2
  21. package/package.json +2 -2
  22. package/scripts/legacy_expression_tool/requirements.txt +1 -1
  23. package/test/functional/README.md +60 -37
  24. package/test/functional/testCases.js +2206 -722
  25. package/test/functional/testUtils.js +53 -20
  26. package/test/unit/examples/deviceProvisioningRequests/updateProvisionDeviceWithApikey.json +4 -0
  27. package/test/unit/ngsi-ld/provisioning/device-update-registration_test.js +5 -5
  28. package/test/unit/ngsiv2/plugins/multientity-plugin_test.js +34 -2
  29. package/test/unit/ngsiv2/provisioning/device-update-registration_test.js +6 -6
  30. package/test/unit/ngsiv2/provisioning/updateProvisionedDevices-test.js +35 -0
@@ -260,7 +260,7 @@ function sendQueryValueNgsi2(entityName, attributes, typeInformation, token, cal
260
260
  * @param {Object} typeInformation Configuration information for the device.
261
261
  * @param {String} token User token to identify against the PEP Proxies (optional).
262
262
  */
263
- function sendUpdateValueNgsi2(entityName, measures, typeInformation, token, callback) {
263
+ function sendUpdateValueNgsi2(entityName, originMeasures, originTypeInformation, token, callback) {
264
264
  //aux function used to builf JEXL context.
265
265
  //it returns a flat object from an Attr array
266
266
  function reduceAttrToPlainObject(attrs, initObj = {}) {
@@ -273,315 +273,354 @@ function sendUpdateValueNgsi2(entityName, measures, typeInformation, token, call
273
273
  return initObj;
274
274
  }
275
275
  }
276
-
277
- let entities = {}; //{entityName:{entityType:[attrs]}} //SubGoal Populate entoties data striucture
278
- let jexlctxt = {}; //will store the whole context (not just for JEXL)
279
- let payload = {}; //will store the final payload
280
- let timestamp = { type: constants.TIMESTAMP_TYPE_NGSI2 }; //timestamp scafold-attr for insertions.
281
- let plainMeasures = null; //will contain measures POJO
282
- let idTypeSSSList = pluginUtils.getIdTypeServSubServiceFromDevice(typeInformation);
283
-
284
276
  //Make a clone and overwrite
285
- typeInformation = JSON.parse(JSON.stringify(typeInformation));
277
+ let idTypeSSSList = pluginUtils.getIdTypeServSubServiceFromDevice(originTypeInformation);
286
278
 
287
279
  //Check mandatory information: type
288
- if (!typeInformation || !typeInformation.type) {
289
- callback(new errors.TypeNotFound(null, entityName, typeInformation));
280
+ if (!originTypeInformation || !originTypeInformation.type) {
281
+ callback(new errors.TypeNotFound(null, entityName, originTypeInformation));
290
282
  return;
291
283
  }
292
- //Rename all measures with matches with id and type to measure_id and measure_type
293
- for (let measure of measures) {
294
- if (measure.name === 'id' || measure.name === 'type') {
295
- measure.name = constants.MEASURE + measure.name;
296
- }
297
- }
298
-
299
- //Make a copy of measures in an plain object: plainMeasures
300
- plainMeasures = reduceAttrToPlainObject(measures);
301
284
 
302
- //Build the initital JEXL Context
303
- //All the measures (avoid references make another copy instead)
304
- jexlctxt = reduceAttrToPlainObject(measures);
305
- //All the static
306
- jexlctxt = reduceAttrToPlainObject(typeInformation.staticAttributes, jexlctxt);
307
- //id type Service and Subservice
308
- jexlctxt = reduceAttrToPlainObject(idTypeSSSList, jexlctxt);
285
+ let payload = {}; //will store the final payload
286
+ let entities = {};
287
+ payload.actionType = 'append';
288
+ payload.entities = [];
309
289
 
290
+ const currentIsoDate = new Date().toISOString();
291
+ const currentMoment = moment(currentIsoDate);
310
292
  //Managing timestamp (mustInsertTimeInstant flag to decide if we should insert Timestamp later on)
311
- const mustInsertTimeInstant =
312
- typeInformation.timestamp !== undefined
313
- ? typeInformation.timestamp
314
- : false;
315
-
316
- if (mustInsertTimeInstant) {
317
- //remove TimeInstant from measures
318
- measures = measures.filter((item) => item.name !== constants.TIMESTAMP_ATTRIBUTE);
319
-
320
- if (plainMeasures[constants.TIMESTAMP_ATTRIBUTE]) {
321
- //if it comes from a measure
322
- if (moment(plainMeasures[constants.TIMESTAMP_ATTRIBUTE], moment.ISO_8601, true).isValid()) {
323
- timestamp.value = plainMeasures[constants.TIMESTAMP_ATTRIBUTE];
324
- } else {
325
- callback(
326
- new errors.BadTimestamp(plainMeasures[constants.TIMESTAMP_ATTRIBUTE], entityName, typeInformation)
327
- );
328
- return;
329
- }
330
- } else if (!typeInformation.timezone) {
331
- timestamp.value = new Date().toISOString();
332
- jexlctxt[constants.TIMESTAMP_ATTRIBUTE] = timestamp.value;
333
- } else {
334
- timestamp.value = moment().tz(typeInformation.timezone).format('YYYY-MM-DD[T]HH:mm:ss.SSSZ');
335
- jexlctxt[constants.TIMESTAMP_ATTRIBUTE] = timestamp.value;
336
- }
337
- }
338
-
339
- logger.debug(
340
- context,
341
- 'sendUpdateValueNgsi2 called with: entityName=%s, measures=%j, typeInformation=%j, initial jexlContext=%j, timestamp=%j with value=%j',
342
- entityName,
343
- plainMeasures,
344
- typeInformation,
345
- jexlctxt,
346
- mustInsertTimeInstant,
347
- timestamp.value
348
- );
293
+ const mustInsertTimeInstant =
294
+ originTypeInformation.timestamp !== undefined ? originTypeInformation.timestamp : false;
349
295
 
350
- //Now we can calculate the EntityName of primary entity
351
- let entityNameCalc = null;
352
- if (typeInformation.entityNameExp !== undefined && typeInformation.entityNameExp !== '') {
353
- try {
354
- logger.debug(context, 'sendUpdateValueNgsi2 entityNameExp %j', typeInformation.entityNameExp);
355
- entityNameCalc = expressionPlugin.applyExpression(typeInformation.entityNameExp, jexlctxt, typeInformation);
356
- } catch (e) {
357
- logger.debug(
358
- context,
359
- 'Error evaluating expression for entityName: %j with context: %j',
360
- typeInformation.entityNameExp,
361
- jexlctxt
362
- );
363
- }
296
+ // Check if measures is a single measure or a array of measures (a multimeasure)
297
+ if (originMeasures[0] && !originMeasures[0][0]) {
298
+ originMeasures = [originMeasures];
364
299
  }
300
+ for (let measures of originMeasures) {
301
+ entities = {}; //{entityName:{entityType:[attrs]}} //SubGoal Populate entities data structure
302
+ let jexlctxt = {}; //will store the whole context (not just for JEXL)
303
+
304
+ let plainMeasures = null; //will contain measures POJO
305
+ //Make a clone and overwrite
306
+ let typeInformation = JSON.parse(JSON.stringify(originTypeInformation));
307
+
308
+ //Rename all measures with matches with id and type to measure_id and measure_type
309
+ for (let measure of measures) {
310
+ if (measure.name === 'id' || measure.name === 'type') {
311
+ measure.name = constants.MEASURE + measure.name;
312
+ }
313
+ }
365
314
 
366
- entityName = entityNameCalc ? entityNameCalc : entityName;
367
- //enrich JEXL context
368
- jexlctxt['entity_name'] = entityName;
315
+ //Make a copy of measures in an plain object: plainMeasures
316
+ plainMeasures = reduceAttrToPlainObject(measures);
317
+ //Build the initital JEXL Context
318
+ //All the measures (avoid references make another copy instead)
319
+ jexlctxt = reduceAttrToPlainObject(measures);
320
+ //All the static
321
+ jexlctxt = reduceAttrToPlainObject(typeInformation.staticAttributes, jexlctxt);
322
+ //id type Service and Subservice
323
+ jexlctxt = reduceAttrToPlainObject(idTypeSSSList, jexlctxt);
369
324
 
370
- let preprocessedAttr = [];
371
- //Add Raw Static, Lazy, Command and Actives attr attributes
372
- if (typeInformation && typeInformation.staticAttributes) {
373
- preprocessedAttr = preprocessedAttr.concat(typeInformation.staticAttributes);
374
- }
375
- if (typeInformation && typeInformation.lazy) {
376
- preprocessedAttr = preprocessedAttr.concat(typeInformation.lazy);
377
- }
378
- if (typeInformation && typeInformation.active) {
379
- preprocessedAttr = preprocessedAttr.concat(typeInformation.active);
380
- }
325
+ logger.debug(
326
+ context,
327
+ 'sendUpdateValueNgsi2 loop with: entityName=%s, measures=%j, typeInformation=%j, initial jexlContext=%j, timestamp=%j',
328
+ entityName,
329
+ plainMeasures,
330
+ typeInformation,
331
+ jexlctxt,
332
+ mustInsertTimeInstant
333
+ );
381
334
 
382
- //Proccess every proto Attribute to populate entities data steuture
383
- entities[entityName] = {};
384
- entities[entityName][typeInformation.type] = [];
385
-
386
- for (let currentAttr of preprocessedAttr) {
387
- let hitted = false; //any measure, expressiom or value hit the attr (avoid propagate "silent attr" with null values )
388
- let attrEntityName = entityName;
389
- let attrEntityType = typeInformation.type;
390
- let valueExpression = null;
391
- //manage active attr without object__id (name by default)
392
- currentAttr.object_id = currentAttr.object_id ? currentAttr.object_id : currentAttr.name;
393
- //Enrich the attr (skip, hit, value, meta-timeInstant)
394
- currentAttr.skipValue = currentAttr.skipValue ? currentAttr.skipValue : null;
395
-
396
- //determine AttrEntityName for multientity
397
- if (
398
- currentAttr.entity_name !== null &&
399
- currentAttr.entity_name !== undefined &&
400
- currentAttr.entity_name !== '' &&
401
- typeof currentAttr.entity_name == 'string'
402
- ) {
335
+ //Now we can calculate the EntityName of primary entity
336
+ let entityNameCalc = null;
337
+ if (typeInformation.entityNameExp !== undefined && typeInformation.entityNameExp !== '') {
403
338
  try {
404
- logger.debug(
405
- context,
406
- 'Evaluating attribute: %j, for entity_name(exp):%j, with ctxt: %j',
407
- currentAttr.name,
408
- currentAttr.entity_name,
409
- jexlctxt
339
+ logger.debug(context, 'sendUpdateValueNgsi2 entityNameExp %j', typeInformation.entityNameExp);
340
+ entityNameCalc = expressionPlugin.applyExpression(
341
+ typeInformation.entityNameExp,
342
+ jexlctxt,
343
+ typeInformation
410
344
  );
411
- attrEntityName = jexlParser.applyExpression(currentAttr.entity_name, jexlctxt, typeInformation);
412
- if (!attrEntityName) {
413
- attrEntityName = currentAttr.entity_name;
414
- }
415
345
  } catch (e) {
416
346
  logger.debug(
417
347
  context,
418
- 'Exception evaluating entityNameExp:%j, with jexlctxt: %j',
419
- currentAttr.entity_name,
348
+ 'Error evaluating expression for entityName: %j with context: %j',
349
+ typeInformation.entityNameExp,
420
350
  jexlctxt
421
351
  );
422
- attrEntityName = currentAttr.entity_name;
423
352
  }
424
353
  }
425
354
 
426
- //determine AttrEntityType for multientity
427
- if (
428
- currentAttr.entity_type !== null &&
429
- currentAttr.entity_type !== undefined &&
430
- currentAttr.entity_type !== '' &&
431
- typeof currentAttr.entity_type === 'string'
432
- ) {
433
- attrEntityType = currentAttr.entity_type;
434
- }
355
+ entityName = entityNameCalc ? entityNameCalc : entityName;
356
+ //enrich JEXL context
357
+ jexlctxt['entity_name'] = entityName;
435
358
 
436
- //PRE POPULATE CONTEXT
437
- jexlctxt[currentAttr.name] = plainMeasures[currentAttr.object_id];
438
-
439
- //determine Value
440
- if (currentAttr.value !== undefined) {
441
- //static attributes already have a value
442
- hitted = true;
443
- valueExpression = currentAttr.value;
444
- } else if (plainMeasures[currentAttr.object_id] !== undefined) {
445
- //we have got a meaure for that Attr
446
- //actives ¿lazis?
447
- hitted = true;
448
- valueExpression = plainMeasures[currentAttr.object_id];
359
+ let preprocessedAttr = [];
360
+ //Add Raw Static, Lazy, Command and Actives attr attributes
361
+ if (typeInformation && typeInformation.staticAttributes) {
362
+ preprocessedAttr = preprocessedAttr.concat(typeInformation.staticAttributes);
449
363
  }
450
- //remove measures that has been shadowed by an alias (some may be left and managed later)
451
- //Maybe we must filter object_id if there is name == object_id
452
- measures = measures.filter((item) => item.name !== currentAttr.object_id && item.name !== currentAttr.name);
453
-
454
- if (
455
- currentAttr.expression !== undefined &&
456
- currentAttr.expression !== '' &&
457
- typeof currentAttr.expression == 'string'
458
- ) {
459
- try {
460
- hitted = true;
461
- valueExpression = jexlParser.applyExpression(currentAttr.expression, jexlctxt, typeInformation);
462
- //we fallback to null if anything unexpecte happend
463
- if (valueExpression === null || valueExpression === undefined || Number.isNaN(valueExpression)) {
464
- valueExpression = null;
465
- }
466
- } catch (e) {
467
- valueExpression = null;
468
- }
469
- logger.debug(
470
- context,
471
- 'Evaluated attr: %j, with expression: %j, and ctxt: %j resulting: %j',
472
- currentAttr.name,
473
- currentAttr.expression,
474
- jexlctxt,
475
- valueExpression
476
- );
364
+ if (typeInformation && typeInformation.lazy) {
365
+ preprocessedAttr = preprocessedAttr.concat(typeInformation.lazy);
366
+ }
367
+ if (typeInformation && typeInformation.active) {
368
+ preprocessedAttr = preprocessedAttr.concat(typeInformation.active);
477
369
  }
478
370
 
479
- currentAttr.hitted = hitted;
480
- currentAttr.value = valueExpression;
371
+ //Proccess every proto Attribute to populate entities data steuture
372
+ entities[entityName] = {};
373
+ entities[entityName][typeInformation.type] = [];
374
+
375
+ for (let currentAttr of preprocessedAttr) {
376
+ let hitted = false; //any measure, expressiom or value hit the attr (avoid propagate "silent attr" with null values )
377
+ let attrEntityName = entityName;
378
+ let attrEntityType = typeInformation.type;
379
+ let valueExpression = null;
380
+ //manage active attr without object__id (name by default)
381
+ currentAttr.object_id = currentAttr.object_id ? currentAttr.object_id : currentAttr.name;
382
+ //Enrich the attr (skip, hit, value, meta-timeInstant)
383
+ currentAttr.skipValue = currentAttr.skipValue ? currentAttr.skipValue : null;
384
+
385
+ //determine AttrEntityName for multientity
386
+ if (
387
+ currentAttr.entity_name !== null &&
388
+ currentAttr.entity_name !== undefined &&
389
+ currentAttr.entity_name !== '' &&
390
+ typeof currentAttr.entity_name == 'string'
391
+ ) {
392
+ try {
393
+ logger.debug(
394
+ context,
395
+ 'Evaluating attribute: %j, for entity_name(exp):%j, with ctxt: %j',
396
+ currentAttr.name,
397
+ currentAttr.entity_name,
398
+ jexlctxt
399
+ );
400
+ attrEntityName = jexlParser.applyExpression(currentAttr.entity_name, jexlctxt, typeInformation);
401
+ if (!attrEntityName) {
402
+ attrEntityName = currentAttr.entity_name;
403
+ }
404
+ } catch (e) {
405
+ logger.debug(
406
+ context,
407
+ 'Exception evaluating entityNameExp:%j, with jexlctxt: %j',
408
+ currentAttr.entity_name,
409
+ jexlctxt
410
+ );
411
+ attrEntityName = currentAttr.entity_name;
412
+ }
413
+ }
481
414
 
482
- //add TimeInstant to attr metadata
483
- if (mustInsertTimeInstant) {
484
- if (!currentAttr.metadata) {
485
- currentAttr.metadata = {};
415
+ //determine AttrEntityType for multientity
416
+ if (
417
+ currentAttr.entity_type !== null &&
418
+ currentAttr.entity_type !== undefined &&
419
+ currentAttr.entity_type !== '' &&
420
+ typeof currentAttr.entity_type === 'string'
421
+ ) {
422
+ attrEntityType = currentAttr.entity_type;
486
423
  }
487
- currentAttr.metadata[constants.TIMESTAMP_ATTRIBUTE] = timestamp;
488
- }
489
424
 
490
- //store de New Attributte in entity data structure
491
- if (hitted === true) {
492
- if (entities[attrEntityName] === undefined) {
493
- entities[attrEntityName] = {};
425
+ //PRE POPULATE CONTEXT
426
+ jexlctxt[currentAttr.name] = plainMeasures[currentAttr.object_id];
427
+
428
+ //determine Value
429
+ if (currentAttr.value !== undefined) {
430
+ //static attributes already have a value
431
+ hitted = true;
432
+ valueExpression = currentAttr.value;
433
+ } else if (plainMeasures[currentAttr.object_id] !== undefined) {
434
+ //we have got a meaure for that Attr
435
+ //actives ¿lazis?
436
+ hitted = true;
437
+ valueExpression = plainMeasures[currentAttr.object_id];
494
438
  }
495
- if (entities[attrEntityName][attrEntityType] === undefined) {
496
- entities[attrEntityName][attrEntityType] = [];
439
+ //remove measures that has been shadowed by an alias (some may be left and managed later)
440
+ //Maybe we must filter object_id if there is name == object_id
441
+ measures = measures.filter((item) => item.name !== currentAttr.object_id && item.name !== currentAttr.name);
442
+
443
+ if (
444
+ currentAttr.expression !== undefined &&
445
+ currentAttr.expression !== '' &&
446
+ typeof currentAttr.expression == 'string'
447
+ ) {
448
+ try {
449
+ hitted = true;
450
+ valueExpression = jexlParser.applyExpression(currentAttr.expression, jexlctxt, typeInformation);
451
+ //we fallback to null if anything unexpecte happend
452
+ if (valueExpression === null || valueExpression === undefined || Number.isNaN(valueExpression)) {
453
+ valueExpression = null;
454
+ }
455
+ } catch (e) {
456
+ valueExpression = null;
457
+ }
458
+ logger.debug(
459
+ context,
460
+ 'Evaluated attr: %j, with expression: %j, and ctxt: %j resulting: %j',
461
+ currentAttr.name,
462
+ currentAttr.expression,
463
+ jexlctxt,
464
+ valueExpression
465
+ );
497
466
  }
498
- //store de New Attributte
499
- entities[attrEntityName][attrEntityType].push(currentAttr);
500
- }
501
467
 
502
- //RE-Populate de JEXLcontext (except for null or NaN we preffer undefined)
503
- jexlctxt[currentAttr.name] = valueExpression;
504
- }
468
+ currentAttr.hitted = hitted;
469
+ currentAttr.value = valueExpression;
505
470
 
506
- //now we can compute explicit (Bool or Array) with the complete JexlContext
507
- let explicit = false;
508
- if (typeof typeInformation.explicitAttrs === 'string') {
509
- try {
510
- explicit = jexlParser.applyExpression(typeInformation.explicitAttrs, jexlctxt, typeInformation);
511
- if (explicit instanceof Array && mustInsertTimeInstant) {
512
- explicit.push(constants.TIMESTAMP_ATTRIBUTE);
471
+ //store de New Attributte in entity data structure
472
+ if (hitted === true) {
473
+ if (entities[attrEntityName] === undefined) {
474
+ entities[attrEntityName] = {};
475
+ }
476
+ if (entities[attrEntityName][attrEntityType] === undefined) {
477
+ entities[attrEntityName][attrEntityType] = [];
478
+ }
479
+ //store de New Attributte
480
+ entities[attrEntityName][attrEntityType].push(currentAttr);
513
481
  }
514
- logger.debug(
515
- context,
516
- 'Calculated explicitAttrs with expression: %j and ctxt: %j resulting: %j',
517
- typeInformation.explicitAttrs,
518
- jexlctxt,
519
- explicit
520
- );
521
- } catch (e) {
522
- // nothing to do: exception is already logged at info level
523
- }
524
- } else if (typeof typeInformation.explicitAttrs == 'boolean') {
525
- explicit = typeInformation.explicitAttrs;
526
- }
527
482
 
528
- //more mesures may be added to the attribute list (unnhandled/left mesaures) l
529
- if (explicit === false && Object.keys(measures).length > 0) {
530
- //add Timestamp to measures if needed
531
- if (mustInsertTimeInstant) {
532
- for (let currentMeasure of measures) {
533
- if (!currentMeasure.metadata) {
534
- currentMeasure.metadata = {};
483
+ //RE-Populate de JEXLcontext (except for null or NaN we preffer undefined)
484
+ jexlctxt[currentAttr.name] = valueExpression;
485
+
486
+ // Expand metadata value expression
487
+ if (currentAttr.metadata) {
488
+ for (var metaKey in currentAttr.metadata) {
489
+ if (currentAttr.metadata[metaKey].expression && metaKey !== constants.TIMESTAMP_ATTRIBUTE) {
490
+ let newAttrMeta = {};
491
+ if (currentAttr.metadata[metaKey].type) {
492
+ newAttrMeta['type'] = currentAttr.metadata[metaKey].type;
493
+ }
494
+ let metaValueExpression;
495
+ try {
496
+ metaValueExpression = jexlParser.applyExpression(
497
+ currentAttr.metadata[metaKey].expression,
498
+ jexlctxt,
499
+ typeInformation
500
+ );
501
+ //we fallback to null if anything unexpecte happend
502
+ if (
503
+ metaValueExpression === null ||
504
+ metaValueExpression === undefined ||
505
+ Number.isNaN(metaValueExpression)
506
+ ) {
507
+ metaValueExpression = null;
508
+ }
509
+ } catch (e) {
510
+ metaValueExpression = null;
511
+ }
512
+ newAttrMeta['value'] = metaValueExpression;
513
+ currentAttr.metadata[metaKey] = newAttrMeta;
514
+ }
535
515
  }
536
- currentMeasure.metadata[constants.TIMESTAMP_ATTRIBUTE] = timestamp;
537
516
  }
538
- //If just measures in the principal entity we missed the Timestamp.
539
517
  }
540
- entities[entityName][typeInformation.type] = entities[entityName][typeInformation.type].concat(measures);
541
- }
542
-
543
- //PRE-PROCESSING FINISHED
544
- //Explicit ATTRS and SKIPVALUES will be managed while we build NGSI payload
545
-
546
- //Get ready to build and send NGSI payload (entities-->payload)
547
- payload.actionType = 'append';
548
518
 
549
- payload.entities = [];
550
- for (let ename in entities) {
551
- for (let etype in entities[ename]) {
552
- let e = {};
553
- e.id = String(ename);
554
- e.type = String(etype);
555
- //extract attributes
556
- let isEmpty = true;
557
- for (let attr of entities[ename][etype]) {
558
- if (
559
- attr.name !== 'id' &&
560
- attr.name !== 'type' &&
561
- (attr.value !== attr.skipValue || attr.skipValue === undefined) &&
562
- (attr.hitted || attr.hitted === undefined) && //undefined is for pure measures
563
- (typeof explicit === 'boolean' || //true and false already handled
564
- (explicit instanceof Array && //check the array version
565
- (explicit.includes(attr.name) ||
566
- explicit.some(
567
- (item) => attr.object_id !== undefined && item.object_id === attr.object_id
568
- ))))
569
- ) {
570
- isEmpty = false;
571
- e[attr.name] = { type: attr.type, value: attr.value, metadata: attr.metadata };
519
+ //now we can compute explicit (Bool or Array) with the complete JexlContext
520
+ let explicit = false;
521
+ if (typeof typeInformation.explicitAttrs === 'string') {
522
+ try {
523
+ explicit = jexlParser.applyExpression(typeInformation.explicitAttrs, jexlctxt, typeInformation);
524
+ if (explicit instanceof Array && explicit.length > 0 && mustInsertTimeInstant) {
525
+ explicit.push(constants.TIMESTAMP_ATTRIBUTE);
572
526
  }
527
+ logger.debug(
528
+ context,
529
+ 'Calculated explicitAttrs with expression: %j and ctxt: %j resulting: %j',
530
+ typeInformation.explicitAttrs,
531
+ jexlctxt,
532
+ explicit
533
+ );
534
+ } catch (e) {
535
+ // nothing to do: exception is already logged at info level
573
536
  }
574
- if (!isEmpty) {
537
+ } else if (typeof typeInformation.explicitAttrs == 'boolean') {
538
+ explicit = typeInformation.explicitAttrs;
539
+ }
540
+
541
+ //more mesures may be added to the attribute list (unnhandled/left mesaures) l
542
+ if (explicit === false && Object.keys(measures).length > 0) {
543
+ entities[entityName][typeInformation.type] = entities[entityName][typeInformation.type].concat(measures);
544
+ }
545
+
546
+ //PRE-PROCESSING FINISHED
547
+ //Explicit ATTRS and SKIPVALUES will be managed while we build NGSI payload
548
+ //Get ready to build and send NGSI payload (entities-->payload)
549
+
550
+ for (let ename in entities) {
551
+ for (let etype in entities[ename]) {
552
+ let e = {};
553
+ e.id = String(ename);
554
+ e.type = String(etype);
555
+ let timestamp = { type: constants.TIMESTAMP_TYPE_NGSI2 }; //timestamp scafold-attr for insertions.
556
+ let timestampAttrs = null;
575
557
  if (mustInsertTimeInstant) {
576
- e[constants.TIMESTAMP_ATTRIBUTE] = timestamp;
558
+ // get timestamp for current entity
559
+
560
+ timestampAttrs = entities[ename][etype].filter(
561
+ (item) => item.name === constants.TIMESTAMP_ATTRIBUTE
562
+ );
563
+ if (timestampAttrs && timestampAttrs.length > 0) {
564
+ timestamp.value = timestampAttrs[0]['value'];
565
+ }
566
+
567
+ if (timestamp.value) {
568
+ if (!moment(timestamp.value, moment.ISO_8601, true).isValid()) {
569
+ callback(new errors.BadTimestamp(timestamp.value, entityName, typeInformation));
570
+ return;
571
+ }
572
+ } else {
573
+ if (!typeInformation.timezone) {
574
+ timestamp.value = currentIsoDate;
575
+ jexlctxt[constants.TIMESTAMP_ATTRIBUTE] = timestamp.value;
576
+ } else {
577
+ timestamp.value = currentMoment
578
+ .tz(typeInformation.timezone)
579
+ .format('YYYY-MM-DD[T]HH:mm:ss.SSSZ');
580
+ jexlctxt[constants.TIMESTAMP_ATTRIBUTE] = timestamp.value;
581
+ }
582
+ }
583
+ }
584
+ //extract attributes
585
+ let isEmpty = true;
586
+ for (let attr of entities[ename][etype]) {
587
+ if (
588
+ attr.name !== 'id' &&
589
+ attr.name !== 'type' &&
590
+ (attr.value !== attr.skipValue || attr.skipValue === undefined) &&
591
+ (attr.hitted || attr.hitted === undefined) && //undefined is for pure measures
592
+ (typeof explicit === 'boolean' || //true and false already handled
593
+ (explicit instanceof Array && //check the array version
594
+ (explicit.includes(attr.name) ||
595
+ explicit.some(
596
+ (item) => attr.object_id !== undefined && item.object_id === attr.object_id
597
+ ))))
598
+ ) {
599
+ isEmpty = false;
600
+ if (mustInsertTimeInstant) {
601
+ // Add TimeInstant to all attribute metadata of all entities
602
+ if (attr.name !== constants.TIMESTAMP_ATTRIBUTE) {
603
+ if (!attr.metadata) {
604
+ attr.metadata = {};
605
+ }
606
+ attr.metadata[constants.TIMESTAMP_ATTRIBUTE] = timestamp;
607
+ }
608
+ }
609
+ e[attr.name] = { type: attr.type, value: attr.value, metadata: attr.metadata };
610
+ }
611
+ }
612
+ if (!isEmpty) {
613
+ if (mustInsertTimeInstant) {
614
+ e[constants.TIMESTAMP_ATTRIBUTE] = timestamp;
615
+ }
616
+ payload.entities.push(e);
577
617
  }
578
- payload.entities.push(e);
579
618
  }
580
619
  }
581
- }
620
+ } // end for (let measures of originMeasures)
582
621
 
583
622
  let url = '/v2/op/update';
584
- let options = NGSIUtils.createRequestObject(url, typeInformation, token);
623
+ let options = NGSIUtils.createRequestObject(url, originTypeInformation, token);
585
624
  options.json = payload;
586
625
 
587
626
  // Prevent to update an entity with an empty payload: more than id and type
@@ -596,13 +635,17 @@ function sendUpdateValueNgsi2(entityName, measures, typeInformation, token, call
596
635
  // Note that the options object is prepared for the second case (multi entity), so we "patch" it
597
636
  // only in the first case
598
637
 
599
- //Multientity more than one name o more than one type at primary entity
600
- let multientity = Object.keys(entities).length > 1 || Object.keys(entities[entityName]).length > 1;
638
+ //Multi: multientity (more than one name o more than one type at primary entity)
639
+ // of multimeasure (originMeasures is an array of more than one element)
640
+ let multi =
641
+ Object.keys(entities).length > 1 ||
642
+ Object.keys(entities[entityName]).length > 1 ||
643
+ originMeasures.length > 1;
601
644
 
602
- if (!multientity) {
645
+ if (!multi) {
603
646
  // recreate options object to use single entity update
604
647
  url = '/v2/entities?options=upsert';
605
- options = NGSIUtils.createRequestObject(url, typeInformation, token);
648
+ options = NGSIUtils.createRequestObject(url, originTypeInformation, token);
606
649
  delete payload.actionType;
607
650
 
608
651
  let entityAttrs = payload.entities[0];
@@ -619,7 +662,20 @@ function sendUpdateValueNgsi2(entityName, measures, typeInformation, token, call
619
662
  transformedObject.type = entityAttrs.type;
620
663
  options.json = transformedObject;
621
664
  options.method = 'POST';
622
- } // else: keep current options object created for a batch update
665
+ } else if (payload.entities.every((entity) => 'TimeInstant' in entity)) {
666
+ // Try sort entities by TimeInstant
667
+ payload.entities.sort(
668
+ (a, b) => new Date(a.TimeInstant.value).getTime() - new Date(b.TimeInstant.value).getTime()
669
+ );
670
+ options.json = payload;
671
+ } else {
672
+ // keep current options object created for a batch update
673
+ logger.debug(
674
+ context,
675
+ "some entities lack the 'TimeInstant' key. Sorting is not feasible: %j ",
676
+ payload.entities
677
+ );
678
+ }
623
679
 
624
680
  //Send the NGSI request
625
681
  logger.debug(context, 'Updating device value in the Context Broker at: %j', options.url);
@@ -627,7 +683,7 @@ function sendUpdateValueNgsi2(entityName, measures, typeInformation, token, call
627
683
 
628
684
  request(
629
685
  options,
630
- generateNGSI2OperationHandler('update', entityName, typeInformation, token, options, callback)
686
+ generateNGSI2OperationHandler('update', entityName, originTypeInformation, token, options, callback)
631
687
  );
632
688
  } else {
633
689
  logger.debug(