iotagent-node-lib 4.10.0 → 4.12.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.
@@ -1 +0,0 @@
1
-
package/Changelog CHANGED
@@ -1,3 +1,16 @@
1
+ 4.12.0 (Jan 28th, 2026)
2
+
3
+ - Fix: set default value for defaultTransport to allow set polling and transport for autoprovisioned devices when no IOTA_DEFAULT_TRANSPORT en var is used (#1751)
4
+ - Fix: allow match auto-provisioned devices and groups when the same type is used by several groups (which always uses different apikeys) (#1713)
5
+
6
+ 4.11.0 (Oct 23th, 2025)
7
+
8
+ - Add: IOTA_MONGO_URI env var
9
+ - Add: locale string number jexl function (#1737)
10
+ - Deprecated: IOTA_MONGO_ different from IOTA_MONGO_URI (use IOTA_MONGO_URI instead)
11
+ - Fix: broken notification command mode (it was not working without providing targetEntityId and targetEntityType in the notification, which belongs to another command mode: advancedNotification)
12
+ - Fix: use normalized in attrsFormat for commands subscriptions (#1743)
13
+
1
14
  4.10.0 (Oct 6th, 2025)
2
15
 
3
16
  - Add: command modes (new field cmdMode): legacy, notification and advancedNotification (#1732)
package/doc/admin.md CHANGED
@@ -248,15 +248,8 @@ the `mongob` section (as described bellow). E.g.:
248
248
 
249
249
  #### `mongodb`
250
250
 
251
- It configures the MongoDB driver for those repositories with 'mongodb' type. If the `host` parameter is a list of
252
- comma-separated IPs, they will be considered to be part of a Replica Set. In that case, the optional property
253
- `replicaSet` should contain the Replica Set name. If the database requires authentication, username (`user`), password
254
- (`password`) and authSource (`authSource`) can be set. If the database requires TLS/SSL connection but any validation of
255
- the certificate chain is not mandatory, all you need is to set the ssl (`ssl`) option as `true` to connect the database.
256
- If you need to add more complex option(s) such as `retryWrites=true` or `w=majority` when connection database, extraArgs
257
- (`extraArgs`) can be used to perform it. For The MongoBD driver will retry the connection at startup time `retries`
258
- times, waiting `retryTime` seconds between attempts, if those attributes are present (default values are 5 and 5
259
- respectively). E.g.:
251
+ It configures the MongoDB driver for those repositories with 'mongodb' type, using the `uri` parameter (which format is
252
+ available in [this reference](http://mongodb.github.io/node-mongodb-native/driver-articles/mongoclient.html)).
260
253
 
261
254
  ```javascript
262
255
  {
@@ -454,12 +447,14 @@ This flag is overwritten by `useCBflowControl` flag in group or device. This fla
454
447
 
455
448
  #### `cmdMode`
456
449
 
457
- Set command mode for the IoTAgent instance (it can be overriden by the `cmdMode` at group or device level). Possible values are:
450
+ Set command mode for the IoTAgent instance (it can be overriden by the `cmdMode` at group or device level). Possible
451
+ values are:
458
452
 
459
- * `legacy` (used as default if this setting is not defined): IoTAgent commands will use Context Broker registers
460
- mechanims.
461
- * `notification`: IoTAgent commands will use subscriptions to be notified for Context Broker commands.
462
- * `advancedNotification`: IoTAgent commands will use subscriptions to be notified for Context Broker commands (but in a different way as in `notification` mode)
453
+ - `legacy` (used as default if this setting is not defined): IoTAgent commands will use Context Broker registers
454
+ mechanims.
455
+ - `notification`: IoTAgent commands will use subscriptions to be notified for Context Broker commands.
456
+ - `advancedNotification`: IoTAgent commands will use subscriptions to be notified for Context Broker commands (but in
457
+ a different way as in `notification` mode)
463
458
 
464
459
  Have a look to [this document](devel/northboundinteractions.md) for more detail on how this modes work.
465
460
 
@@ -505,17 +500,7 @@ overrides.
505
500
  | IOTA_IOTAM_AGENTPATH | `iotManager.agentPath` |
506
501
  | IOTA_IOTAM_PROTOCOL | `iotManager.protocol` |
507
502
  | IOTA_IOTAM_DESCRIPTION | `iotManager.description` |
508
- | IOTA_MONGO_HOST | `mongodb.host` |
509
- | IOTA_MONGO_PORT | `mongodb.port` |
510
- | IOTA_MONGO_DB | `mongodb.db` |
511
- | IOTA_MONGO_REPLICASET | `mongodb.replicaSet` |
512
- | IOTA_MONGO_USER | `mongodb.user` |
513
- | IOTA_MONGO_PASSWORD | `mongodb.password` |
514
- | IOTA_MONGO_AUTH_SOURCE | `mongodb.authSource` |
515
- | IOTA_MONGO_RETRIES | `mongodb.retries` |
516
- | IOTA_MONGO_RETRY_TIME | `mongodb.retryTime` |
517
- | IOTA_MONGO_SSL | `mongodb.ssl` |
518
- | IOTA_MONGO_EXTRAARGS | `mongodb.extraArgs` |
503
+ | IOTA_MONGO_URI | `mongodb.uri` |
519
504
  | IOTA_POLLING_EXPIRATION | `pollingExpiration` |
520
505
  | IOTA_POLLING_DAEMON_FREQ | `pollingDaemonFrequency` |
521
506
  | IOTA_MULTI_CORE | `multiCore` |
package/doc/api.md CHANGED
@@ -279,12 +279,16 @@ IOTAgent, you can create it in advance using the Context Broker
279
279
 
280
280
  ## Entity creation when `cmdMode` is `notification`
281
281
 
282
- Even when an entity should not be created (see [above section](#device-autoprovision-and-entity-creation)), when the device uses `cmdMode` set to `notification` an entity is created. In particular:
282
+ Even when an entity should not be created (see [above section](#device-autoprovision-and-entity-creation)), when the
283
+ device uses `cmdMode` set to `notification` an entity is created. In particular:
283
284
 
284
- * An entity is created at device provision time
285
- * That entity is created with an attribute corresponding to each commands. The value of that attribute at creation time is `null` (specifying in some way that the command has not been triggered yet). Note that if the attribute doesn't have any command, the entity is not created (even if `cmdMode` is `notification`).
285
+ - An entity is created at device provision time
286
+ - That entity is created with an attribute corresponding to each commands. The value of that attribute at creation
287
+ time is `null` (specifying in some way that the command has not been triggered yet). Note that if the attribute
288
+ doesn't have any command, the entity is not created (even if `cmdMode` is `notification`).
286
289
 
287
- For instance, if device has commands `ping` and `switch` the entity corresponding to that device will be created at provising time with the following attributes:
290
+ For instance, if device has commands `ping` and `switch` the entity corresponding to that device will be created at
291
+ provising time with the following attributes:
288
292
 
289
293
  ```
290
294
  ...
@@ -300,7 +304,8 @@ For instance, if device has commands `ping` and `switch` the entity correspondin
300
304
  }
301
305
  ```
302
306
 
303
- **NOTE:** `command` is the usual type for attributes associated to commands, but the one used at provisioning time (`"command": [ ...]` field) will be actually used.
307
+ **NOTE:** `command` is the usual type for attributes associated to commands, but the one used at provisioning time
308
+ (`"command": [ ...]` field) will be actually used.
304
309
 
305
310
  ## Entity Name expression support
306
311
 
@@ -708,52 +713,53 @@ to incorporate new transformations from the IoT Agent configuration file in a fa
708
713
 
709
714
  Current common transformation set:
710
715
 
711
- | JEXL Transformation | Equivalent JavaScript Function |
712
- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
713
- | jsonparse: (str) | `JSON.parse(str);` |
714
- | jsonstringify: (obj) | `JSON.stringify(obj);` |
715
- | indexOf: (val, char) | `String(val).indexOf(char);` |
716
- | length: (val) | `String(val).length;` |
717
- | trim: (val) | `String(val).trim();` |
718
- | substr: (val, int1, int2) | `String(val).substr(int1, int2);` |
719
- | addreduce: (arr) | <code>arr.reduce((i, v) &vert; i + v));</code> |
720
- | lengtharray: (arr) | `arr.length;` |
721
- | typeof: (val) | `typeof val;` |
722
- | isarray: (arr) | `Array.isArray(arr);` |
723
- | isnan: (val) | `isNaN(val);` |
724
- | parseint: (val) | `parseInt(val);` |
725
- | parsefloat: (val) | `parseFloat(val);` |
726
- | toisodate: (val) | `new Date(val).toISOString();` |
727
- | timeoffset:(isostr) | `new Date(isostr).getTimezoneOffset();` |
728
- | tostring: (val) | `val.toString();` |
729
- | urlencode: (val) | `encodeURI(val);` |
730
- | urldecode: (val) | `decodeURI(val);` |
731
- | replacestr: (str, from, to) | `str.replace(from, to);` |
732
- | replaceregexp: (str, reg, to) | `str.replace(new RegExp(reg), to);` |
733
- | replaceallstr: (str, from, to) | `str.replaceAll(from, to);` |
734
- | replaceallregexp: (str, reg, to) | `str.replaceAll(new RegExp(reg,"g"), to);` |
735
- | split: (str, ch) | `str.split(ch);` |
736
- | joinarrtostr: (arr, ch) | `arr.join(ch);` |
737
- | concatarr: (arr, arr2) | `arr.concat(arr2);` |
738
- | mapper: (val, values, choices) | <code>choices[values.findIndex((target) &vert; target == val)]);</code> |
739
- | thmapper: (val, values, choices) | <code>choices[values.reduce((acc,curr,i,arr) &vert; (acc==0)&vert;&vert;acc?acc:val<=curr?acc=i:acc=null,null)];</code> |
740
- | bitwisemask: (i,mask,op,shf) | <code>(op==="&"?parseInt(i)&mask: op==="&vert;"?parseInt(i)&vert;mask: op==="^"?parseInt(i)^mask:i)>>shf;</code> |
741
- | slice: (arr, init, end) | `arr.slice(init,end);` |
742
- | addset: (arr, x) | <code>{ return Array.from((new Set(arr)).add(x)) }</code> |
743
- | removeset: (arr, x) | <code>{ let s = new Set(arr); s.delete(x); return Array.from(s) }</code> |
744
- | touppercase: (val) | `String(val).toUpperCase()` |
745
- | tolowercase: (val) | `String(val).toLowerCase()` |
746
- | round: (val) | `Math.round(val)` |
747
- | floor: (val) | `Math.floor(val)` |
748
- | ceil: (val) | `Math.ceil(val)` |
749
- | tofixed: (val, decimals) | `Number.parseFloat(val).toFixed(decimals)` |
750
- | gettime: (d) | `new Date(d).getTime()` |
751
- | toisostring: (d) | `new Date(d).toISOString()` |
752
- | localestring: (d, timezone, options) | `new Date(d).toLocaleString(timezone, options)` |
753
- | now: () | `Date.now()` |
754
- | hextostring: (val) | `new TextDecoder().decode(new Uint8Array(val.match(/.{1,2}/g).map(byte => parseInt(byte, 16))))` |
755
- | valuePicker: (val,pick) | <code>valuePicker: (val,pick) => Object.entries(val).filter(([_, v]) => v === pick).map(([k, _]) => k)</code> |
756
- | valuePickerMulti: (val,pick) | <code>valuePickerMulti: (val,pick) => Object.entries(val).filter(([_, v]) => pick.includes(v)).map(([k, _]) => k)</code> |
716
+ | JEXL Transformation | Equivalent JavaScript Function |
717
+ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
718
+ | jsonparse: (str) | `JSON.parse(str);` |
719
+ | jsonstringify: (obj) | `JSON.stringify(obj);` |
720
+ | indexOf: (val, char) | `String(val).indexOf(char);` |
721
+ | length: (val) | `String(val).length;` |
722
+ | trim: (val) | `String(val).trim();` |
723
+ | substr: (val, int1, int2) | `String(val).substr(int1, int2);` |
724
+ | addreduce: (arr) | <code>arr.reduce((i, v) &vert; i + v));</code> |
725
+ | lengtharray: (arr) | `arr.length;` |
726
+ | typeof: (val) | `typeof val;` |
727
+ | isarray: (arr) | `Array.isArray(arr);` |
728
+ | isnan: (val) | `isNaN(val);` |
729
+ | parseint: (val) | `parseInt(val);` |
730
+ | parsefloat: (val) | `parseFloat(val);` |
731
+ | toisodate: (val) | `new Date(val).toISOString();` |
732
+ | timeoffset:(isostr) | `new Date(isostr).getTimezoneOffset();` |
733
+ | tostring: (val) | `val.toString();` |
734
+ | urlencode: (val) | `encodeURI(val);` |
735
+ | urldecode: (val) | `decodeURI(val);` |
736
+ | replacestr: (str, from, to) | `str.replace(from, to);` |
737
+ | replaceregexp: (str, reg, to) | `str.replace(new RegExp(reg), to);` |
738
+ | replaceallstr: (str, from, to) | `str.replaceAll(from, to);` |
739
+ | replaceallregexp: (str, reg, to) | `str.replaceAll(new RegExp(reg,"g"), to);` |
740
+ | split: (str, ch) | `str.split(ch);` |
741
+ | joinarrtostr: (arr, ch) | `arr.join(ch);` |
742
+ | concatarr: (arr, arr2) | `arr.concat(arr2);` |
743
+ | mapper: (val, values, choices) | <code>choices[values.findIndex((target) &vert; target == val)]);</code> |
744
+ | thmapper: (val, values, choices) | <code>choices[values.reduce((acc,curr,i,arr) &vert; (acc==0)&vert;&vert;acc?acc:val<=curr?acc=i:acc=null,null)];</code> |
745
+ | bitwisemask: (i,mask,op,shf) | <code>(op==="&"?parseInt(i)&mask: op==="&vert;"?parseInt(i)&vert;mask: op==="^"?parseInt(i)^mask:i)>>shf;</code> |
746
+ | slice: (arr, init, end) | `arr.slice(init,end);` |
747
+ | addset: (arr, x) | <code>{ return Array.from((new Set(arr)).add(x)) }</code> |
748
+ | removeset: (arr, x) | <code>{ let s = new Set(arr); s.delete(x); return Array.from(s) }</code> |
749
+ | touppercase: (val) | `String(val).toUpperCase()` |
750
+ | tolowercase: (val) | `String(val).toLowerCase()` |
751
+ | round: (val) | `Math.round(val)` |
752
+ | floor: (val) | `Math.floor(val)` |
753
+ | ceil: (val) | `Math.ceil(val)` |
754
+ | tofixed: (val, decimals) | `Number.parseFloat(val).toFixed(decimals)` |
755
+ | gettime: (d) | `new Date(d).getTime()` |
756
+ | toisostring: (d) | `new Date(d).toISOString()` |
757
+ | localestring: (d, timezone, options) | `new Date(d).toLocaleString(timezone, options)` |
758
+ | localestringnumber: (n, locale, options) | `n.toLocaleStringNumber(locale, options)` |
759
+ | now: () | `Date.now()` |
760
+ | hextostring: (val) | `new TextDecoder().decode(new Uint8Array(val.match(/.{1,2}/g).map(byte => parseInt(byte, 16))))` |
761
+ | valuePicker: (val,pick) | <code>valuePicker: (val,pick) => Object.entries(val).filter(([_, v]) => v === pick).map(([k, _]) => k)</code> |
762
+ | valuePickerMulti: (val,pick) | <code>valuePickerMulti: (val,pick) => Object.entries(val).filter(([_, v]) => pick.includes(v)).map(([k, _]) => k)</code> |
757
763
 
758
764
  You have available this [JEXL interactive playground][99] with all the transformations already loaded, in which you can
759
765
  test all the functions described above.
@@ -1349,7 +1355,8 @@ as `command` in the [config group](#config-group-datamodel) or in the [device pr
1349
1355
  attributes are created using `command` as attribute type. Also, you can define the protocol you want the commands to be
1350
1356
  sent (HTTP/MQTT) with the `transport` parameter at the provisioning process.
1351
1357
 
1352
- **NOTE**: in `advancedNotification` mode the command is not triggered updating an attribute, but creating a command execution entity. However, this mode has not been implemented yet so details are to be clarified.
1358
+ **NOTE**: in `advancedNotification` mode the command is not triggered updating an attribute, but creating a command
1359
+ execution entity. However, this mode has not been implemented yet so details are to be clarified.
1353
1360
 
1354
1361
  For a given device provisioned with a `ping` command defined, any update on this attribute "ping" at the NGSI entity in
1355
1362
  the Context Broker will send a command to your device. For instance, to send the `ping` command with value
@@ -1812,7 +1819,7 @@ Config group is represented by a JSON object with the following fields:
1812
1819
  | `endpoint` | ✓ | `string` | | Endpoint where the group of device is going to receive commands, if any. |
1813
1820
  | `storeLastMeasure` | ✓ | `boolean` | | Store in device last measure received. See more info [in this section](admin.md#storelastmeasure). False by default |
1814
1821
  | `useCBflowControl` | ✓ | `boolean` | | Use Context Broker flow control. See more info [in this section](admin.md#useCBflowControl). False by default |
1815
- | `cmdMode` | ✓ | `string` | | Command mode that will use iotagent with CB: **legacy**, **notification** and **advancedNotification**. **Legacy** is based on registers. **notification** based on simplified schema of subscriptions. **Legacy** by default. More information on how the different modes work can be found in [the northbound interactions documents](devel/northboundinteractions.md). |
1822
+ | `cmdMode` | ✓ | `string` | | Command mode that will use iotagent with CB: **legacy**, **notification** and **advancedNotification**. **Legacy** is based on registers. **notification** based on simplified schema of subscriptions. **Legacy** by default. More information on how the different modes work can be found in [the northbound interactions documents](devel/northboundinteractions.md). |
1816
1823
 
1817
1824
  ### Config group operations
1818
1825
 
@@ -2036,7 +2043,7 @@ the API resource fields and the same fields in the database model.
2036
2043
  | `storeLastMeasure` | ✓ | `boolean` | | Store in device last measure received. Useful just for debugging purpose. See more info [in this section](admin.md#storelastmeasure). False by default. |
2037
2044
  | `lastMeasure` | ✓ | `object` | | last measure stored on device when `storeLastMeasure` is enabled. See more info [in this section](admin.md#storelastmeasure). This field can be cleared using `{}` in a device update request. In that case, `lastMeasure` is removed from device (until a next measure is received and `lastMesuare` gets created again). |
2038
2045
  | `useCBflowControl` | ✓ | `boolean` | | Use Context Broker flow control. See more info [in this section](admin.md#useCBflowControl). False by default. |
2039
- | `cmdMode` | ✓ | `string` | | Command mode that will use iotagent with CB: **legacy**, **notification** and **advancedNotification**. **Legacy** is based on registers. **notification** based on simplified schema of subscriptions. **Legacy** by default. More information on how the different modes work can be found in [the northbound interactions documents](devel/northboundinteractions.md). |
2046
+ | `cmdMode` | ✓ | `string` | | Command mode that will use iotagent with CB: **legacy**, **notification** and **advancedNotification**. **Legacy** is based on registers. **notification** based on simplified schema of subscriptions. **Legacy** by default. More information on how the different modes work can be found in [the northbound interactions documents](devel/northboundinteractions.md). |
2040
2047
 
2041
2048
  ### Device operations
2042
2049
 
@@ -2493,4 +2500,4 @@ updateEntityRequestsError 5
2493
2500
  [18]:
2494
2501
  https://czosel.github.io/jexl-playground/#/?context=%7B%0A%20%20%22value%22%20%3A%206%2C%0A%20%20%22ts%22%3A%201637245214901%2C%0A%20%22name%22%3A%20%22DevId629%22%2C%0A%20%22object%22%3A%7Bname%3A%20%22John%22%2C%20surname%3A%20%22Doe%22%7D%2C%0A%20%20%22array%22%3A%5B1%2C3%5D%0A%7D&input=name%7Csubstr(0%2Cname%7CindexOf(%22e%22)%2B1)&transforms=%7B%0A%20%20%20%20jsonparse%3A%20(str)%20%3D%3E%20JSON.parse(str)%2C%0A%20%20%20%20jsonstringify%3A%20(obj)%20%3D%3E%20JSON.stringify(obj)%2C%0A%20%20%20%20indexOf%3A%20(val%2C%20char)%20%3D%3E%20String(val).indexOf(char)%2C%0A%20%20%20%20length%3A%20(val)%20%3D%3E%20String(val).length%2C%0A%20%20%20%20trim%3A%20(val)%20%3D%3E%20String(val).trim()%2C%0A%20%20%20%20substr%3A%20(val%2C%20int1%2C%20int2)%20%3D%3E%20String(val).substr(int1%2C%20int2)%2C%0A%20%20%20%20addreduce%3A%20(arr)%20%3D%3E%20arr.reduce((i%2C%20v)%20%3D%3E%20i%20%2B%20v)%2C%0A%20%20%20%20lengtharray%3A%20(arr)%20%3D%3E%20arr.length%2C%0A%20%20%20%20typeof%3A%20(val)%20%3D%3E%20typeof%20val%2C%0A%20%20%20%20isarray%3A%20(arr)%20%3D%3E%20Array.isArray(arr)%2C%0A%20%20%20%20isnan%3A%20(val)%20%3D%3E%20isNaN(val)%2C%0A%20%20%20%20parseint%3A%20(val)%20%3D%3E%20parseInt(val)%2C%0A%20%20%20%20parsefloat%3A%20(val)%20%3D%3E%20parseFloat(val)%2C%0A%20%20%20%20toisodate%3A%20(val)%20%3D%3E%20new%20Date(val).toISOString()%2C%0A%20%20%20%20timeoffset%3A%20(isostr)%20%3D%3E%20new%20Date(isostr).getTimezoneOffset()%2C%0A%20%20%20%20tostring%3A%20(val)%20%3D%3E%20val.toString()%2C%0A%20%20%20%20urlencode%3A%20(val)%20%3D%3E%20encodeURI(val)%2C%0A%20%20%20%20urldecode%3A%20(val)%20%3D%3E%20decodeURI(val)%2C%0A%20%20%20%20replacestr%3A%20(str%2C%20from%2C%20to)%20%3D%3E%20str.replace(from%2C%20to)%2C%0A%20%20%20%20replaceregexp%3A%20(str%2C%20reg%2C%20to)%20%3D%3E%20str.replace(new%20RegExp(reg)%2C%20to)%2C%0A%20%20%20%20replaceallstr%3A%20(str%2C%20from%2C%20to)%20%3D%3E%20str.replaceAll(from%2C%20to)%2C%0A%20%20%20%20replaceallregexp%3A%20(str%2C%20reg%2C%20to)%20%3D%3E%20str.replaceAll(new%20RegExp(reg%2C%20'g')%2C%20to)%2C%0A%20%20%20%20split%3A%20(str%2C%20ch)%20%3D%3E%20str.split(ch)%2C%0A%20%20%20%20mapper%3A%20(val%2C%20values%2C%20choices)%20%3D%3E%20choices%5Bvalues.findIndex((target)%20%3D%3E%20target%20%3D%3D%3D%20val)%5D%2C%0A%20%20%20%20thmapper%3A%20(val%2C%20values%2C%20choices)%20%3D%3E%0A%20%20%20%20%20%20%20%20choices%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20values.reduce((acc%2C%20curr%2C%20i)%20%3D%3E%20(acc%20%3D%3D%3D%200%20%7C%7C%20acc%20%3F%20acc%20%3A%20val%20%3C%3D%20curr%20%3F%20(acc%20%3D%20i)%20%3A%20(acc%20%3D%20null))%2C%20null)%0A%20%20%20%20%20%20%20%20%5D%2C%0A%20%20%20%20bitwisemask%3A%20(i%2C%20mask%2C%20op%2C%20shf)%20%3D%3E%0A%20%20%20%20%20%20%20%20(op%20%3D%3D%3D%20'%26'%20%3F%20parseInt(i)%20%26%20mask%20%3A%20op%20%3D%3D%3D%20'%7C'%20%3F%20parseInt(i)%20%7C%20mask%20%3A%20op%20%3D%3D%3D%20'%5E'%20%3F%20parseInt(i)%20%5E%20mask%20%3A%20i)%20%3E%3E%0A%20%20%20%20%20%20%20%20shf%2C%0A%20%20%20%20slice%3A%20(arr%2C%20init%2C%20end)%20%3D%3E%20arr.slice(init%2C%20end)%0A%7D
2495
2502
  [99]:
2496
- https://czosel.github.io/jexl-playground/#/?context=%7B%0A%20%20%22text%22%20%3A%20%22%20%20foobar%7B%7D%20%20%22%0A%7D&input=text%20%7C%20replacestr(%22foo%22%2C%22FOO%22)%7Ctrim%7Curlencode&transforms=%20%7B%0A%20%20%20%20jsonparse%3A%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeOperation%20%3D%20(fn)%20%3D%3E%20(...args)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20try%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20fn(...args)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20null%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeOperation(JSON.parse)(val)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20jsonstringify%3A%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeOperation%20%3D%20(fn)%20%3D%3E%20(...args)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20try%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20fn(...args)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20null%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeOperation(JSON.stringify)(val)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20indexOf%3A%20(val%2C%20char)%20%3D%3E%20String(val).indexOf(char)%2C%0A%20%20%20%20length%3A%20(val)%20%3D%3E%20String(val).length%2C%0A%20%20%20%20trim%3A%20(val)%20%3D%3E%20String(val).trim()%2C%0A%20%20%20%20substr%3A%20(val%2C%20int1%2C%20int2)%20%3D%3E%20String(val).substr(int1%2C%20int2)%2C%0A%20%20%20%20addreduce%3A%20(arr)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeOperation%20%3D%20(fn)%20%3D%3E%20(...args)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20try%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20fn(...args)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20null%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeOperation((arr)%20%3D%3E%20arr.reduce((i%2C%20v)%20%3D%3E%20i%20%2B%20v))(arr)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20lengtharray%3A%20(arr)%20%3D%3E%20arr.length%2C%0A%20%20%20%20typeof%3A%20(val)%20%3D%3E%20typeof%20val%2C%0A%20%20%20%20isarray%3A%20Array.isArray%2C%0A%20%20%20%20isnan%3A%20isNaN%2C%0A%20%20%20%20parseint%3A%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeParseNumber%20%3D%20(fn)%20%3D%3E%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20result%20%3D%20fn(val)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20isNaN(result)%20%3F%20null%20%3A%20result%3B%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeParseNumber((val)%20%3D%3E%20parseInt(val%2C%2010))(val)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20parsefloat%3A%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeParseNumber%20%3D%20(fn)%20%3D%3E%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20result%20%3D%20fn(val)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20isNaN(result)%20%3F%20null%20%3A%20result%3B%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeParseNumber(parseFloat)(val)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20toisodate%3A%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeDateOperation%20%3D%20(fn)%20%3D%3E%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20date%20%3D%20new%20Date(val)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20isNaN(date.getTime())%20%3F%20null%20%3A%20fn(date)%3B%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeDateOperation((date)%20%3D%3E%20date.toISOString())(val)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20timeoffset%3A%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeDateOperation%20%3D%20(fn)%20%3D%3E%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20date%20%3D%20new%20Date(val)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20isNaN(date.getTime())%20%3F%20null%20%3A%20fn(date)%3B%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeDateOperation((date)%20%3D%3E%20date.getTimezoneOffset())(val)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20tostring%3A%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeOperation%20%3D%20(fn)%20%3D%3E%20(...args)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20try%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20fn(...args)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20null%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeOperation((val)%20%3D%3E%20val.toString())(val)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20urlencode%3A%20encodeURI%2C%0A%20%20%20%20urldecode%3A%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeOperation%20%3D%20(fn)%20%3D%3E%20(...args)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20try%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20fn(...args)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20null%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeOperation(decodeURI)(val)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20replacestr%3A%20(str%2C%20from%2C%20to)%20%3D%3E%20str.replace(from%2C%20to)%2C%0A%20%20%20%20replaceregexp%3A%20(str%2C%20reg%2C%20to)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeOperation%20%3D%20(fn)%20%3D%3E%20(...args)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20try%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20fn(...args)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20null%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeOperation((str%2C%20reg%2C%20to)%20%3D%3E%20str.replace(new%20RegExp(reg)%2C%20to))(str%2C%20reg%2C%20to)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20replaceallregexp%3A%20(str%2C%20reg%2C%20to)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeOperation%20%3D%20(fn)%20%3D%3E%20(...args)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20try%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20fn(...args)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20null%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeOperation((str%2C%20reg%2C%20to)%20%3D%3E%20str.replace(new%20RegExp(reg%2C%20'g')%2C%20to))(str%2C%20reg%2C%20to)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20split%3A%20(str%2C%20ch)%20%3D%3E%20str.split(ch)%2C%0A%20%20%20%20joinarrtostr%3A%20(arr%2C%20ch)%20%3D%3E%20arr.join(ch)%2C%0A%20%20%20%20concatarr%3A%20(arr%2C%20arr2)%20%3D%3E%20arr.concat(arr2)%2C%0A%20%20%20%20mapper%3A%20(val%2C%20values%2C%20choices)%20%3D%3E%20choices%5Bvalues.findIndex((target)%20%3D%3E%20target%20%3D%3D%3D%20val)%5D%2C%0A%20%20%20%20thmapper%3A%20(val%2C%20values%2C%20choices)%20%3D%3E%0A%20%20%20%20%20%20%20%20choices%5Bvalues.reduce((acc%2C%20curr%2C%20i)%20%3D%3E%20(acc%20!%3D%3D%20null%20%3F%20acc%20%3A%20val%20%3C%3D%20curr%20%3F%20i%20%3A%20null)%2C%20null)%5D%2C%0A%20%20%20%20bitwisemask%3A%20(i%2C%20mask%2C%20op%2C%20shf)%20%3D%3E%0A%20%20%20%20%20%20%20%20(op%20%3D%3D%3D%20'%26'%20%3F%20parseInt(i)%20%26%20mask%20%3A%20op%20%3D%3D%3D%20'%7C'%20%3F%20parseInt(i)%20%7C%20mask%20%3A%20op%20%3D%3D%3D%20'%5E'%20%3F%20parseInt(i)%20%5E%20mask%20%3A%20i)%20%3E%3E%0A%20%20%20%20%20%20%20%20shf%2C%0A%20%20%20%20slice%3A%20(arr%2C%20init%2C%20end)%20%3D%3E%20arr.slice(init%2C%20end)%2C%0A%20%20%20%20addset%3A%20(arr%2C%20x)%20%3D%3E%20Array.from(new%20Set(arr).add(x))%2C%0A%20%20%20%20removeset%3A%20(arr%2C%20x)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20s%20%3D%20new%20Set(arr)%3B%0A%20%20%20%20%20%20%20%20s.delete(x)%3B%0A%20%20%20%20%20%20%20%20return%20Array.from(s)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20touppercase%3A%20(val)%20%3D%3E%20String(val).toUpperCase()%2C%0A%20%20%20%20tolowercase%3A%20(val)%20%3D%3E%20String(val).toLowerCase()%2C%0A%20%20%20%20floor%3A%20Math.floor%2C%0A%20%20%20%20ceil%3A%20Math.ceil%2C%0A%20%20%20%20round%3A%20Math.round%2C%0A%20%20%20%20tofixed%3A%20(val%2C%20decimals)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20num%20%3D%20Number.parseFloat(val)%3B%0A%20%20%20%20%20%20%20%20const%20dec%20%3D%20Number.parseInt(decimals)%3B%0A%20%20%20%20%20%20%20%20return%20isNaN(num)%20%7C%7C%20isNaN(dec)%20%3F%20null%20%3A%20num.toFixed(dec)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20gettime%3A%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeDateOperation%20%3D%20(fn)%20%3D%3E%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20date%20%3D%20new%20Date(val)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20isNaN(date.getTime())%20%3F%20null%20%3A%20fn(date)%3B%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeDateOperation((date)%20%3D%3E%20date.getTime())(val)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20toisostring%3A%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeDateOperation%20%3D%20(fn)%20%3D%3E%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20date%20%3D%20new%20Date(val)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20isNaN(date.getTime())%20%3F%20null%20%3A%20fn(date)%3B%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeDateOperation((date)%20%3D%3E%20date.toISOString())(val)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20localestring%3A%20(d%2C%20timezone%2C%20options)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeOperation%20%3D%20(fn)%20%3D%3E%20(...args)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20try%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20fn(...args)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20null%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeOperation((d%2C%20timezone%2C%20options)%20%3D%3E%20new%20Date(d).toLocaleString(timezone%2C%20options))(d%2C%20timezone%2C%20options)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20now%3A%20Date.now%2C%0A%20%20%20%20hextostring%3A%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeOperation%20%3D%20(fn)%20%3D%3E%20(...args)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20try%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20fn(...args)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20null%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeOperation((val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20(typeof%20val%20!%3D%3D%20'string'%20%7C%7C%20!%2F%5E%5B0-9a-fA-F%5D%2B%24%2F.test(val)%20%7C%7C%20val.length%20%25%202%20!%3D%3D%200)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20null%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20new%20TextDecoder().decode(new%20Uint8Array(val.match(%2F.%7B1%2C2%7D%2Fg).map((byte)%20%3D%3E%20parseInt(byte%2C%2016))))%3B%0A%20%20%20%20%20%20%20%20%7D)(val)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20valuePicker%3A%20(val%2C%20pick)%20%3D%3E%0A%20%20%20%20%20%20%20%20Object.entries(val)%0A%20%20%20%20%20%20%20%20%20%20%20%20.filter((%5B%2C%20v%5D)%20%3D%3E%20v%20%3D%3D%3D%20pick)%0A%20%20%20%20%20%20%20%20%20%20%20%20.map((%5Bk%5D)%20%3D%3E%20k)%2C%0A%20%20%20%20valuePickerMulti%3A%20(val%2C%20pick)%20%3D%3E%0A%20%20%20%20%20%20%20%20Object.entries(val)%0A%20%20%20%20%20%20%20%20%20%20%20%20.filter((%5B%2C%20v%5D)%20%3D%3E%20pick.includes(v))%0A%20%20%20%20%20%20%20%20%20%20%20%20.map((%5Bk%5D)%20%3D%3E%20k)%0A%7D
2503
+ https://czosel.github.io/jexl-playground/#/?context=%7B%0A%20%20%22text%22%20%3A%20%22%20%20foobar%7B%7D%20%20%22%0A%7D&input=text%20%7C%20replacestr(%22foo%22%2C%22FOO%22)%7Ctrim%7Curlencode&transforms=%20%7B%0A%20%20%20%20jsonparse%3A%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeOperation%20%3D%20(fn)%20%3D%3E%20(...args)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20try%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20fn(...args)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20null%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeOperation(JSON.parse)(val)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20jsonstringify%3A%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeOperation%20%3D%20(fn)%20%3D%3E%20(...args)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20try%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20fn(...args)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20null%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeOperation(JSON.stringify)(val)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20indexOf%3A%20(val%2C%20char)%20%3D%3E%20String(val).indexOf(char)%2C%0A%20%20%20%20length%3A%20(val)%20%3D%3E%20String(val).length%2C%0A%20%20%20%20trim%3A%20(val)%20%3D%3E%20String(val).trim()%2C%0A%20%20%20%20substr%3A%20(val%2C%20int1%2C%20int2)%20%3D%3E%20String(val).substr(int1%2C%20int2)%2C%0A%20%20%20%20addreduce%3A%20(arr)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeOperation%20%3D%20(fn)%20%3D%3E%20(...args)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20try%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20fn(...args)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20null%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeOperation((arr)%20%3D%3E%20arr.reduce((i%2C%20v)%20%3D%3E%20i%20%2B%20v))(arr)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20lengtharray%3A%20(arr)%20%3D%3E%20arr.length%2C%0A%20%20%20%20typeof%3A%20(val)%20%3D%3E%20typeof%20val%2C%0A%20%20%20%20isarray%3A%20Array.isArray%2C%0A%20%20%20%20isnan%3A%20isNaN%2C%0A%20%20%20%20parseint%3A%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeParseNumber%20%3D%20(fn)%20%3D%3E%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20result%20%3D%20fn(val)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20isNaN(result)%20%3F%20null%20%3A%20result%3B%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeParseNumber((val)%20%3D%3E%20parseInt(val%2C%2010))(val)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20parsefloat%3A%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeParseNumber%20%3D%20(fn)%20%3D%3E%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20result%20%3D%20fn(val)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20isNaN(result)%20%3F%20null%20%3A%20result%3B%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeParseNumber(parseFloat)(val)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20toisodate%3A%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeDateOperation%20%3D%20(fn)%20%3D%3E%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20date%20%3D%20new%20Date(val)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20isNaN(date.getTime())%20%3F%20null%20%3A%20fn(date)%3B%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeDateOperation((date)%20%3D%3E%20date.toISOString())(val)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20timeoffset%3A%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeDateOperation%20%3D%20(fn)%20%3D%3E%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20date%20%3D%20new%20Date(val)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20isNaN(date.getTime())%20%3F%20null%20%3A%20fn(date)%3B%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeDateOperation((date)%20%3D%3E%20date.getTimezoneOffset())(val)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20tostring%3A%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeOperation%20%3D%20(fn)%20%3D%3E%20(...args)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20try%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20fn(...args)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20null%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeOperation((val)%20%3D%3E%20val.toString())(val)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20urlencode%3A%20encodeURI%2C%0A%20%20%20%20urldecode%3A%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeOperation%20%3D%20(fn)%20%3D%3E%20(...args)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20try%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20fn(...args)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20null%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeOperation(decodeURI)(val)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20replacestr%3A%20(str%2C%20from%2C%20to)%20%3D%3E%20str.replace(from%2C%20to)%2C%0A%20%20%20%20replaceregexp%3A%20(str%2C%20reg%2C%20to)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeOperation%20%3D%20(fn)%20%3D%3E%20(...args)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20try%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20fn(...args)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20null%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeOperation((str%2C%20reg%2C%20to)%20%3D%3E%20str.replace(new%20RegExp(reg)%2C%20to))(str%2C%20reg%2C%20to)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20replaceallregexp%3A%20(str%2C%20reg%2C%20to)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeOperation%20%3D%20(fn)%20%3D%3E%20(...args)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20try%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20fn(...args)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20null%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeOperation((str%2C%20reg%2C%20to)%20%3D%3E%20str.replace(new%20RegExp(reg%2C%20'g')%2C%20to))(str%2C%20reg%2C%20to)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20split%3A%20(str%2C%20ch)%20%3D%3E%20str.split(ch)%2C%0A%20%20%20%20joinarrtostr%3A%20(arr%2C%20ch)%20%3D%3E%20arr.join(ch)%2C%0A%20%20%20%20concatarr%3A%20(arr%2C%20arr2)%20%3D%3E%20arr.concat(arr2)%2C%0A%20%20%20%20mapper%3A%20(val%2C%20values%2C%20choices)%20%3D%3E%20choices%5Bvalues.findIndex((target)%20%3D%3E%20target%20%3D%3D%3D%20val)%5D%2C%0A%20%20%20%20thmapper%3A%20(val%2C%20values%2C%20choices)%20%3D%3E%0A%20%20%20%20%20%20%20%20choices%5Bvalues.reduce((acc%2C%20curr%2C%20i)%20%3D%3E%20(acc%20!%3D%3D%20null%20%3F%20acc%20%3A%20val%20%3C%3D%20curr%20%3F%20i%20%3A%20null)%2C%20null)%5D%2C%0A%20%20%20%20bitwisemask%3A%20(i%2C%20mask%2C%20op%2C%20shf)%20%3D%3E%0A%20%20%20%20%20%20%20%20(op%20%3D%3D%3D%20'%26'%20%3F%20parseInt(i)%20%26%20mask%20%3A%20op%20%3D%3D%3D%20'%7C'%20%3F%20parseInt(i)%20%7C%20mask%20%3A%20op%20%3D%3D%3D%20'%5E'%20%3F%20parseInt(i)%20%5E%20mask%20%3A%20i)%20%3E%3E%0A%20%20%20%20%20%20%20%20shf%2C%0A%20%20%20%20slice%3A%20(arr%2C%20init%2C%20end)%20%3D%3E%20arr.slice(init%2C%20end)%2C%0A%20%20%20%20addset%3A%20(arr%2C%20x)%20%3D%3E%20Array.from(new%20Set(arr).add(x))%2C%0A%20%20%20%20removeset%3A%20(arr%2C%20x)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20s%20%3D%20new%20Set(arr)%3B%0A%20%20%20%20%20%20%20%20s.delete(x)%3B%0A%20%20%20%20%20%20%20%20return%20Array.from(s)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20touppercase%3A%20(val)%20%3D%3E%20String(val).toUpperCase()%2C%0A%20%20%20%20tolowercase%3A%20(val)%20%3D%3E%20String(val).toLowerCase()%2C%0A%20%20%20%20floor%3A%20Math.floor%2C%0A%20%20%20%20ceil%3A%20Math.ceil%2C%0A%20%20%20%20round%3A%20Math.round%2C%0A%20%20%20%20tofixed%3A%20(val%2C%20decimals)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20num%20%3D%20Number.parseFloat(val)%3B%0A%20%20%20%20%20%20%20%20const%20dec%20%3D%20Number.parseInt(decimals)%3B%0A%20%20%20%20%20%20%20%20return%20isNaN(num)%20%7C%7C%20isNaN(dec)%20%3F%20null%20%3A%20num.toFixed(dec)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20gettime%3A%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeDateOperation%20%3D%20(fn)%20%3D%3E%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20date%20%3D%20new%20Date(val)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20isNaN(date.getTime())%20%3F%20null%20%3A%20fn(date)%3B%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeDateOperation((date)%20%3D%3E%20date.getTime())(val)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20toisostring%3A%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeDateOperation%20%3D%20(fn)%20%3D%3E%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20date%20%3D%20new%20Date(val)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20isNaN(date.getTime())%20%3F%20null%20%3A%20fn(date)%3B%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeDateOperation((date)%20%3D%3E%20date.toISOString())(val)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20localestring%3A%20(d%2C%20timezone%2C%20options)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeOperation%20%3D%20(fn)%20%3D%3E%20(...args)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20try%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20fn(...args)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20null%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeOperation((d%2C%20timezone%2C%20options)%20%3D%3E%20new%20Date(d).toLocaleString(timezone%2C%20options))(d%2C%20timezone%2C%20options)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20localestringnumber%3A%20(n%2C%20locale%2C%20options)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeOperation%20%3D%0A%20%20%20%20%20%20%20%20%20%20%20%20(fn)%20%3D%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20(...args)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20try%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20fn(...args)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20null%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeOperation((n%2C%20locale%2C%20options)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20(typeof%20n%20!%3D%3D%20'number'%20%7C%7C%20isNaN(n))%20return%20null%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20n.toLocaleString(locale%2C%20options)%3B%0A%20%20%20%20%20%20%20%20%7D)(n%2C%20locale%2C%20options)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20now%3A%20Date.now%2C%0A%20%20%20%20hextostring%3A%20(val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20safeOperation%20%3D%20(fn)%20%3D%3E%20(...args)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20try%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20fn(...args)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20null%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20return%20safeOperation((val)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20(typeof%20val%20!%3D%3D%20'string'%20%7C%7C%20!%2F%5E%5B0-9a-fA-F%5D%2B%24%2F.test(val)%20%7C%7C%20val.length%20%25%202%20!%3D%3D%200)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20null%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20new%20TextDecoder().decode(new%20Uint8Array(val.match(%2F.%7B1%2C2%7D%2Fg).map((byte)%20%3D%3E%20parseInt(byte%2C%2016))))%3B%0A%20%20%20%20%20%20%20%20%7D)(val)%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20valuePicker%3A%20(val%2C%20pick)%20%3D%3E%0A%20%20%20%20%20%20%20%20Object.entries(val)%0A%20%20%20%20%20%20%20%20%20%20%20%20.filter((%5B%2C%20v%5D)%20%3D%3E%20v%20%3D%3D%3D%20pick)%0A%20%20%20%20%20%20%20%20%20%20%20%20.map((%5Bk%5D)%20%3D%3E%20k)%2C%0A%20%20%20%20valuePickerMulti%3A%20(val%2C%20pick)%20%3D%3E%0A%20%20%20%20%20%20%20%20Object.entries(val)%0A%20%20%20%20%20%20%20%20%20%20%20%20.filter((%5B%2C%20v%5D)%20%3D%3E%20pick.includes(v))%0A%20%20%20%20%20%20%20%20%20%20%20%20.map((%5Bk%5D)%20%3D%3E%20k)%0A%7D
package/doc/deprecated.md CHANGED
@@ -12,12 +12,17 @@ longer. In particular:
12
12
 
13
13
  A list of deprecated features and the version in which they were deprecated follows:
14
14
 
15
+ - Config `mongodb.*` different from `mongodb.uri` (and associated env vars `IOTA_MONGO_*` except `IOTA_MONGO_URI`) in
16
+ 4.11.0
15
17
  - Support to NGSI v1 (finally removed in 2.18.0)
16
18
  - Support to Node.js v4 in iotagent-node-lib 2.8.1 (finally removed in 2.9.0)
17
19
  - Support to Node.js v6 in iotagent-node-lib 2.9.0 (finally removed in 2.10.0)
18
20
  - Support to Node.js v8 in iotagent-node-lib 2.12.0 (finally removed in 2.13.0)
19
21
  - Support to Node.js v10 in iotagent-node-lib 2.15.0 (finally removed in 2.16.0)
20
22
  - Support to Node.js v12 in iotagent-node-lib 2.24.0 (finally removed in 2.25.0)
23
+ - Support to Node.js v14 in iotagent-node-lib 3.0.0 (finally removed in 3.1.0)
24
+ - Support to Node.js v16 in iotagent-node-lib 4.9.0 (finally removed in 4.10.0)
25
+ - Support to Node.js v18 in iotagent-node-lib 4.9.0 (finally removed in 4.10.0)
21
26
  - Support to NGSI-LD v1.3 in iotagent-node-lib 2.25.0 (finally removed in 2.26.0)
22
27
  - Support groups (provision) statically defined by configuration
23
28
  - Support to in-memory registry (i.e.`deviceRegistry.type=memory`)
@@ -47,17 +52,31 @@ information in the case you want to use old versions:
47
52
 
48
53
  The following table provides information about the last iotagent-node-lib version supporting currently removed features:
49
54
 
50
- | **Removed feature** | **Last iotagent-node-lib version supporting feature** | **That version release date** |
51
- | ----------------------------------------------------- | ----------------------------------------------------- | ----------------------------- |
52
- | NGSI v1 API | 2.17.0 | August 30th, 2021 |
53
- | Support to Node.js v4 | 2.8.1 | December 19th, 2018 |
54
- | Support to Node.js v6 | 2.9.0 | May 22nd, 2019 |
55
- | Support to Node.js v8 | 2.12.0 | April 7th, 2020 |
56
- | Support to Node.js v10 | 2.15.0 | February 18th, 2021 |
57
- | Support to Node.js v12 | 2.24.0 | September 2nd, 2022 |
58
- | Support to NGSI-LD 1.3 | 2.25.0 | January 24th, 2023 |
59
- | eventType configuration | 2.26.0 | March 15th, 2023 |
60
- | Support to Legacy Expressions | 3.1.0 | April 25th, 2023 |
61
- | bidirectional plugin | 3.3.0 | August 24th, 2023 |
62
- | appendMode configuration (`IOTA_APPEND_MODE` env var) | 3.3.0 | August 24th, 2023 |
63
- | push-mode stats | 4.5.0 | June 11th, 2024 |
55
+ | **Removed feature** | **Last iotagent-node-lib version supporting feature** | **That version release date** |
56
+ | -------------------------------------------------------------- | ----------------------------------------------------- | ----------------------------- |
57
+ | NGSI v1 API | 2.17.0 | August 30th, 2021 |
58
+ | Support to Node.js v4 | 2.8.1 | December 19th, 2018 |
59
+ | Support to Node.js v6 | 2.9.0 | May 22nd, 2019 |
60
+ | Support to Node.js v8 | 2.12.0 | April 7th, 2020 |
61
+ | Support to Node.js v10 | 2.15.0 | February 18th, 2021 |
62
+ | Support to Node.js v12 | 2.24.0 | September 2nd, 2022 |
63
+ | Support to Node.js v14 | 3.0.0 | March 30th, 2023 |
64
+ | Support to Node.js v16 | 4.9.0 | August 22nd, 2025 |
65
+ | Support to Node.js v18 | 4.9.0 | August 22nd, 2025 |
66
+ | Support to NGSI-LD 1.3 | 2.25.0 | January 24th, 2023 |
67
+ | eventType configuration | 2.26.0 | March 15th, 2023 |
68
+ | Support to Legacy Expressions | 3.1.0 | April 25th, 2023 |
69
+ | bidirectional plugin | 3.3.0 | August 24th, 2023 |
70
+ | appendMode configuration (`IOTA_APPEND_MODE` env var) | 3.3.0 | August 24th, 2023 |
71
+ | push-mode stats | 4.5.0 | June 11th, 2024 |
72
+ | config `mongodb.host` (env var `IOTA_MONGO_HOST`) | Not yet | Not yet |
73
+ | config `mongodb.port` (env var `IOTA_MONGO_PORT`) | Not yet | Not yet |
74
+ | config `mongodb.db` (env var `IOTA_MONGO_DB`) | Not yet | Not yet |
75
+ | config `mongodb.replicaSet` (env var `IOTA_MONGO_REPLICASET`) | Not yet | Not yet |
76
+ | config `mongodb.user` (env var `IOTA_MONGO_USER`) | Not yet | Not yet |
77
+ | config `mongodb.password` (env var `IOTA_MONGO_PASSWORD`) | Not yet | Not yet |
78
+ | config `mongodb.authSource` (env var `IOTA_MONGO_AUTH_SOURCE`) | Not yet | Not yet |
79
+ | config `mongodb.retries` (env var `IOTA_MONGO_RETRIES`) | Not yet | Not yet |
80
+ | config `mongodb.retryTime` (env var `IOTA_MONGO_RETRY_TIME`) | Not yet | Not yet |
81
+ | config `mongodb.ssl` (env var `IOTA_MONGO_SSL`) | Not yet | Not yet |
82
+ | config `mongodb.extraArgs` (env var `IOTA_MONGO_EXTRAARGS`) | Not yet | Not yet |
@@ -346,7 +346,8 @@ queries (and thus P2 and R2 payloads).
346
346
 
347
347
  ![General ](./img/scenario3.png 'Scenario 3: commands')
348
348
 
349
- **FIXME:** this scenario describes the registration-based commanding mechanism, which is currently deprecated. It should be reworked
349
+ **FIXME:** this scenario describes the registration-based commanding mechanism, which is currently deprecated. It should
350
+ be reworked
350
351
 
351
352
  This scenario requires that the attributes that are going to be requested are marked as provided by the IoT Agent,
352
353
  through a registration process (NGSIv9). Examples of this registration process will be provided in the practical section
@@ -733,11 +734,14 @@ error, that error must follow the NGSI payloads described in the Scenario 1 erro
733
734
 
734
735
  The interactions depend on the command mode (`cmdMode`):
735
736
 
736
- * `legacy`
737
- * `notification`
738
- * `advancedNotification`
737
+ - `legacy`
738
+ - `notification`
739
+ - `advancedNotification`
739
740
 
740
- The way of setting up Context Broker to IotAgent communication and the interaction between Context Broker and IoTAgent when the command is executed depends on the mode (thus specific subsections about it are provided next for the three modes). However, the way in which the command result is provided is the same to all modes (so it is described in a [common section](#result-reporting)).
741
+ The way of setting up Context Broker to IotAgent communication and the interaction between Context Broker and IoTAgent
742
+ when the command is executed depends on the mode (thus specific subsections about it are provided next for the three
743
+ modes). However, the way in which the command result is provided is the same to all modes (so it is described in a
744
+ [common section](#result-reporting)).
741
745
 
742
746
  #### `legacy` mode
743
747
 
@@ -766,7 +770,8 @@ curl -X POST -H "Content-Type: application/json" -H "Accept: application/json" -
766
770
  }' "https://<platform-ip>:1026/v2/registrations"
767
771
  ```
768
772
 
769
- If everything has gone OK, the Context Broker will return 201 Created with the ID of the registration in the `Location` header:
773
+ If everything has gone OK, the Context Broker will return 201 Created with the ID of the registration in the `Location`
774
+ header:
770
775
 
771
776
  ```
772
777
  HTTP/1.1 201 Created
@@ -831,8 +836,8 @@ Fiware-Correlator: 9cae9496-8ec7-11e6-80fc-fa163e734aab
831
836
  }
832
837
  ```
833
838
 
834
- The IoT Agent detects the selected attribute is a command, and replies to the Context Broker with a 204 No Content (without
835
- payload).
839
+ The IoT Agent detects the selected attribute is a command, and replies to the Context Broker with a 204 No Content
840
+ (without payload).
836
841
 
837
842
  This response just indicates that the IoT Agent has received the command successfully, and gives no information about
838
843
  the requested information or command execution.
@@ -867,13 +872,14 @@ curl -X POST -H "Content-Type: application/json" -H "Accept: application/json" -
867
872
  "http": {
868
873
  "url": "<value of the IOTA_PROVIDER_URL>/notify"
869
874
  },
870
- "attrsFormat": "simplifiedNormalized",
875
+ "attrsFormat": "normalized",
871
876
  "attrs": [ "switch" ]
872
877
  }
873
878
  }' "https://<platform-ip>:1026/v2/subscriptions"
874
879
  ```
875
880
 
876
- If everything has gone OK, the Context Broker will return 201 Created with the ID of the subscription in the `Location` header:
881
+ If everything has gone OK, the Context Broker will return 201 Created with the ID of the subscription in the `Location`
882
+ header:
877
883
 
878
884
  ```
879
885
  HTTP/1.1 201 Created
@@ -908,8 +914,8 @@ curl -X POST -H "Content-Type: application/json" -H "Accept: application/json" -
908
914
  } ' "https://<platform-ip>:1026/v2/op/update"
909
915
  ```
910
916
 
911
- The Context Broker receives this update and detects that it triggers the subscription, so it
912
- notifies to the IoT Agent, as follows:
917
+ The Context Broker receives this update and detects that it triggers the subscription, so it notifies to the IoT Agent,
918
+ as follows:
913
919
 
914
920
  ```bash
915
921
  POST /notify HTTP/1.1
@@ -919,17 +925,20 @@ Fiware-ServicePath: /iota2ngsi
919
925
  Accept: application/json
920
926
  Content-length: ...
921
927
  Content-type: application/json; charset=utf-8
922
- Ngsiv2-Attrsformat: simplifiedNormalized
928
+ Ngsiv2-Attrsformat: normalized
923
929
  Fiware-Correlator: 9cae9496-8ec7-11e6-80fc-fa163e734aab
924
930
 
925
931
  {
926
- "id": "Dev0001",
927
- "type": "device",
928
- "switch": {
929
- "type": "command",
930
- "value": "54, 12",
931
- "metadata": {}
932
- }
932
+ "subscriptionId": "60b0cedd497e8b681d40b58e",
933
+ "data": [{
934
+ "id": "Dev0001",
935
+ "type": "device",
936
+ "switch": {
937
+ "type": "command",
938
+ "value": "54, 12",
939
+ "metadata": {}
940
+ }
941
+ }]
933
942
  }
934
943
  ```
935
944
 
@@ -946,7 +955,8 @@ request will be.
946
955
 
947
956
  ##### Set up ContextBroker to IOTA comunication mechanism
948
957
 
949
- The communication mechanism will be based on subscriptions, although a different one than the one used in `notification` mode. Note this mode has not been implemented yet, so following should be taken as a draft:
958
+ The communication mechanism will be based on subscriptions, although a different one than the one used in `notification`
959
+ mode. Note this mode has not been implemented yet, so following should be taken as a draft:
950
960
 
951
961
  ```bash
952
962
  curl -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "Fiware-Service: workshop" \
@@ -974,7 +984,8 @@ curl -X POST -H "Content-Type: application/json" -H "Accept: application/json" -
974
984
 
975
985
  ##### Command Execution
976
986
 
977
- Command execution involves to create a *command execution entity* (details are yet to be implemented), which in sequence triggers a notification to IoTAgent as this one (draft)
987
+ Command execution involves to create a _command execution entity_ (details are yet to be implemented), which in sequence
988
+ triggers a notification to IoTAgent as this one (draft)
978
989
 
979
990
  ```bash
980
991
  POST /notify HTTP/1.1
@@ -1072,7 +1083,8 @@ the requested information or command execution.
1072
1083
  At this point, the command has been issued to the IoTAgent and the User doesn't still know what the result of its
1073
1084
  request will be.
1074
1085
 
1075
- As mentioned before, advanced notification mode has not been yet implemented. The following considerations are gap in the current implementation at IoT Agent side that should be addressed:
1086
+ As mentioned before, advanced notification mode has not been yet implemented. The following considerations are gap in
1087
+ the current implementation at IoT Agent side that should be addressed:
1076
1088
 
1077
1089
  - Fields others than `targetEntityId`, `targetEntityType`, `cmd` and `params` (i.e. `execTs`, `status`, `info`,
1078
1090
  `onDelivered`, `onOk`, `onError`, `onInfo`, `cmdExecution` and `dataExpiration`), are not actually used. By the
@@ -173,6 +173,9 @@ function processEnvironmentVariables() {
173
173
  'IOTA_IOTAM_AGENTPATH'
174
174
  ];
175
175
  const mongoVariables = [
176
+ 'IOTA_MONGO_URI',
177
+ // FIXME: the following IOTA_MONGO_ env vars are deprecated and will
178
+ // be eventually removed (use IOTA_MONGO_URI)
176
179
  'IOTA_MONGO_HOST',
177
180
  'IOTA_MONGO_PORT',
178
181
  'IOTA_MONGO_DB',
@@ -365,6 +368,12 @@ function processEnvironmentVariables() {
365
368
  // Default transport
366
369
  if (process.env.IOTA_DEFAULT_TRANSPORT !== undefined) {
367
370
  config.defaultTransport = process.env.IOTA_DEFAULT_TRANSPORT;
371
+ } else {
372
+ // defaultTransport is "MQTT" by default even when no ENV VAR is used
373
+ // and should be available in iotagent-library to allow
374
+ // set polling and transport for autoprovisioned devices
375
+ // at lib/services/devices/deviceService.js#L302
376
+ config.defaultTransport = 'MQTT';
368
377
  }
369
378
 
370
379
  // Default explicitAttrs
@@ -409,6 +418,10 @@ function processEnvironmentVariables() {
409
418
  config.mongodb = {};
410
419
  }
411
420
 
421
+ if (process.env.IOTA_MONGO_URI) {
422
+ config.mongodb.uri = process.env.IOTA_MONGO_URI;
423
+ }
424
+
412
425
  if (process.env.IOTA_MONGO_HOST) {
413
426
  config.mongodb.host = process.env.IOTA_MONGO_HOST;
414
427
  }
@@ -204,6 +204,21 @@ const map = {
204
204
  options
205
205
  );
206
206
  },
207
+ localestringnumber: (n, locale, options) => {
208
+ const safeOperation =
209
+ (fn) =>
210
+ (...args) => {
211
+ try {
212
+ return fn(...args);
213
+ } catch (e) {
214
+ return null;
215
+ }
216
+ };
217
+ return safeOperation((n, locale, options) => {
218
+ if (typeof n !== 'number' || isNaN(n)) return null;
219
+ return n.toLocaleString(locale, options);
220
+ })(n, locale, options);
221
+ },
207
222
  now: Date.now,
208
223
  hextostring: (val) => {
209
224
  const safeOperation =
@@ -21,7 +21,7 @@
21
21
  * please contact with::daniel.moranjimenez@telefonica.com
22
22
  */
23
23
 
24
- /**
24
+ /*
25
25
  * This module sets up the connection with the mongodb through mongoose. This connection will be used
26
26
  * in mongoose schemas to persist objects.
27
27
  */
@@ -49,8 +49,7 @@ function loadModels() {
49
49
  *
50
50
  * @this Reference to the dbConn module itself.
51
51
  */
52
-
53
- function init(host, db, port, options, callback) {
52
+ function init(host, db, port, options, callback, fullUri = null) {
54
53
  let url;
55
54
  let retries = 0;
56
55
  let lastError;
@@ -60,21 +59,25 @@ function init(host, db, port, options, callback) {
60
59
  return `${item}:${port}`;
61
60
  }
62
61
 
63
- url = 'mongodb://';
62
+ if (fullUri) {
63
+ url = fullUri;
64
+ } else {
65
+ url = 'mongodb://';
64
66
 
65
- if (options.auth) {
66
- url += `${encodeURIComponent(options.auth.user)}:${encodeURIComponent(options.auth.password)}@`;
67
- }
67
+ if (options.auth) {
68
+ url += `${encodeURIComponent(options.auth.user)}:${encodeURIComponent(options.auth.password)}@`;
69
+ }
68
70
 
69
- const hosts = host.split(',').map(addPort).join(',');
70
- url += `${hosts}/${db}`;
71
+ const hosts = host.split(',').map(addPort).join(',');
72
+ url += `${hosts}/${db}`;
71
73
 
72
- if (options.extraArgs) {
73
- const query = new URLSearchParams(options.extraArgs).toString();
74
- if (query) {
75
- url += `?${query}`;
74
+ if (options.extraArgs) {
75
+ const query = new URLSearchParams(options.extraArgs).toString();
76
+ if (query) {
77
+ url += `?${query}`;
78
+ }
79
+ delete options.extraArgs;
76
80
  }
77
- delete options.extraArgs;
78
81
  }
79
82
 
80
83
  function connectionAttempt(callback) {
@@ -91,32 +94,17 @@ function init(host, db, port, options, callback) {
91
94
  lastError = error;
92
95
  alarms.raise(constants.MONGO_ALARM, error);
93
96
  });
94
- /* eslint-disable-next-line no-unused-vars */
95
- defaultDb.on('connecting', function (error) {
96
- logger.debug(context, 'Mongo Driver connecting');
97
- });
98
- defaultDb.on('connected', function () {
99
- logger.debug(context, 'Mongo Driver connected');
100
- });
101
- defaultDb.on('reconnected', function () {
102
- logger.debug(context, 'Mongo Driver reconnected');
103
- });
104
- defaultDb.on('disconnected', function () {
105
- logger.debug(context, 'Mongo Driver disconnected');
106
- });
107
- defaultDb.on('reconnectFailed', function () {
97
+ defaultDb.on('connecting', () => logger.debug(context, 'Mongo Driver connecting'));
98
+ defaultDb.on('connected', () => logger.debug(context, 'Mongo Driver connected'));
99
+ defaultDb.on('reconnected', () => logger.debug(context, 'Mongo Driver reconnected'));
100
+ defaultDb.on('disconnected', () => logger.debug(context, 'Mongo Driver disconnected'));
101
+ defaultDb.on('reconnectFailed', () => {
108
102
  logger.error(context, 'MONGODB-004: MongoDB connection was lost');
109
103
  process.exit(1);
110
104
  });
111
- defaultDb.on('disconnecting', function () {
112
- logger.debug(context, 'Mongo Driver disconnecting');
113
- });
114
- defaultDb.on('open', function () {
115
- logger.debug(context, 'Mongo Driver open');
116
- });
117
- defaultDb.on('close', function () {
118
- logger.debug(context, 'Mongo Driver close');
119
- });
105
+ defaultDb.on('disconnecting', () => logger.debug(context, 'Mongo Driver disconnecting'));
106
+ defaultDb.on('open', () => logger.debug(context, 'Mongo Driver open'));
107
+ defaultDb.on('close', () => logger.debug(context, 'Mongo Driver close'));
120
108
  callback();
121
109
  })
122
110
  .catch((err) => {
@@ -144,34 +132,45 @@ function init(host, db, port, options, callback) {
144
132
 
145
133
  function configureDb(callback) {
146
134
  const currentConfig = config.getConfig();
135
+
147
136
  if (currentConfig.deviceRegistry?.type === 'mongodb') {
148
- if (!currentConfig.mongodb?.host) {
149
- logger.fatal(context, 'MONGODB-003: No host found for MongoDB driver.');
150
- callback(new errors.BadConfiguration('No host found for MongoDB driver'));
151
- } else {
152
- const dbName = currentConfig.mongodb.db || DEFAULT_DB_NAME;
153
- const port = currentConfig.mongodb.port || 27017;
137
+ const mongoCfg = currentConfig.mongodb;
138
+
139
+ if (mongoCfg?.uri) {
154
140
  const options = {};
141
+ logger.info(context, `Using full MongoDB URI from configuration`);
142
+ init(null, null, null, options, callback, mongoCfg.uri);
143
+ return;
144
+ }
155
145
 
156
- if (currentConfig.mongodb.replicaSet) options.replicaSet = currentConfig.mongodb.replicaSet;
157
- if (currentConfig.mongodb.ssl) options.ssl = currentConfig.mongodb.ssl;
158
- if (currentConfig.mongodb.extraArgs) options.extraArgs = currentConfig.mongodb.extraArgs;
146
+ if (!mongoCfg?.host) {
147
+ logger.fatal(context, 'MONGODB-003: No host found for MongoDB driver.');
148
+ callback(new errors.BadConfiguration('No host found for MongoDB driver'));
149
+ return;
150
+ }
159
151
 
160
- if (currentConfig.mongodb.user && currentConfig.mongodb.password) {
161
- options.auth = {
162
- user: currentConfig.mongodb.user,
163
- password: currentConfig.mongodb.password
152
+ const dbName = mongoCfg.db || DEFAULT_DB_NAME;
153
+ const port = mongoCfg.port || 27017;
154
+ const options = {};
155
+
156
+ if (mongoCfg.replicaSet) options.replicaSet = mongoCfg.replicaSet;
157
+ if (mongoCfg.ssl) options.ssl = mongoCfg.ssl;
158
+ if (mongoCfg.extraArgs) options.extraArgs = mongoCfg.extraArgs;
159
+
160
+ if (mongoCfg.user && mongoCfg.password) {
161
+ options.auth = {
162
+ user: mongoCfg.user,
163
+ password: mongoCfg.password
164
+ };
165
+ if (mongoCfg.authSource) {
166
+ options.extraArgs = {
167
+ ...options.extraArgs,
168
+ authSource: mongoCfg.authSource
164
169
  };
165
- if (currentConfig.mongodb.authSource) {
166
- options.extraArgs = {
167
- ...options.extraArgs,
168
- authSource: currentConfig.mongodb.authSource
169
- };
170
- }
171
170
  }
172
-
173
- init(currentConfig.mongodb.host, dbName, port, options, callback);
174
171
  }
172
+
173
+ init(mongoCfg.host, dbName, port, options, callback);
175
174
  } else {
176
175
  callback();
177
176
  }
@@ -341,7 +341,7 @@ function sendSubscriptionsNgsi2(unregister, deviceData, callback) {
341
341
  }
342
342
  const trigger = attrs; // one subscription for all commands
343
343
  const content = attrs;
344
- const attrsFormat = 'simplifiedNormalized';
344
+ const attrsFormat = 'normalized';
345
345
 
346
346
  logger.debug(
347
347
  context,
@@ -168,7 +168,21 @@ function executeWithDeviceInformation(operationFunction) {
168
168
  deviceInformation
169
169
  );
170
170
  const currentType = type ? type : deviceInformation.type;
171
- config.getGroupRegistry().getTypeSilently(currentType, function (error, deviceGroup) {
171
+
172
+ function getConfiguration(currentType, deviceInformation, callback) {
173
+ config
174
+ .getGroupRegistry()
175
+ .getSilently(deviceInformation.resource, deviceInformation.apikey, function (error, deviceGroup) {
176
+ if (error) {
177
+ config.getGroupRegistry().getTypeSilently(currentType, function (error, deviceGroup) {
178
+ callback(error, deviceGroup);
179
+ });
180
+ } else {
181
+ callback(null, deviceGroup);
182
+ }
183
+ });
184
+ }
185
+ getConfiguration(currentType, deviceInformation, function (error, deviceGroup) {
172
186
  let typeInformation;
173
187
  const configDeviceInfo = config.getConfig().types[currentType];
174
188
  if (error) {
@@ -293,8 +293,10 @@ function handleNotificationNgsi2(req, res, next) {
293
293
  }
294
294
  }
295
295
  logger.debug(context, 'extracted atts %j from dataElement %j', atts, dataElement);
296
- var id = null;
297
- var type = null;
296
+ // FIXME: command execution entity for advancedNotification is still not implemented
297
+ var id = dataElement.id;
298
+ var type = dataElement.type;
299
+
298
300
  if (dataElement.targetEntityId && dataElement.targetEntityId.value) {
299
301
  id = dataElement.targetEntityId.value;
300
302
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "iotagent-node-lib",
3
3
  "license": "AGPL-3.0-only",
4
4
  "description": "IoT Agent library to interface with NGSI Context Broker",
5
- "version": "4.10.0",
5
+ "version": "4.12.0",
6
6
  "homepage": "https://github.com/telefonicaid/iotagent-node-lib",
7
7
  "keywords": [
8
8
  "fiware",
@@ -2235,6 +2235,79 @@ const testCases = [
2235
2235
  }
2236
2236
  ]
2237
2237
  },
2238
+ {
2239
+ describeName: '0201 - Simple groups with commands',
2240
+ provision: {
2241
+ url: 'http://localhost:' + config.iota.server.port + '/iot/services',
2242
+ method: 'POST',
2243
+ json: {
2244
+ services: [
2245
+ {
2246
+ resource: '/iot/json',
2247
+ apikey: globalEnv.apikey,
2248
+ entity_type: globalEnv.entity_type,
2249
+ commands: [
2250
+ {
2251
+ name: 'cmd1',
2252
+ type: 'command'
2253
+ }
2254
+ ],
2255
+ endpoint: 'http://myendpoint.com',
2256
+ transport: 'http',
2257
+ lazy: [],
2258
+ attributes: [],
2259
+ static_attributes: []
2260
+ },
2261
+ {
2262
+ resource: '/iot/json',
2263
+ apikey: globalEnv.apikey + 'OTHER',
2264
+ entity_type: globalEnv.entity_type,
2265
+ commands: [
2266
+ {
2267
+ name: 'cmd1',
2268
+ type: 'command'
2269
+ }
2270
+ ],
2271
+ transport: 'mqtt',
2272
+ lazy: [],
2273
+ attributes: [],
2274
+ static_attributes: []
2275
+ }
2276
+ ]
2277
+ },
2278
+ headers: {
2279
+ 'fiware-service': globalEnv.service,
2280
+ 'fiware-servicepath': globalEnv.servicePath
2281
+ }
2282
+ },
2283
+ should: [
2284
+ {
2285
+ shouldName:
2286
+ 'A - WHEN sending not provisioned object_ids (measures) through http IT should store commands into Context Broker',
2287
+ type: 'single',
2288
+ skip: '!lib', // there is not CB registration mock
2289
+ measure: {
2290
+ url: 'http://localhost:' + config.http.port + '/iot/json',
2291
+ method: 'POST',
2292
+ qs: {
2293
+ i: globalEnv.deviceId,
2294
+ k: globalEnv.apikey
2295
+ },
2296
+ json: {
2297
+ b: 10
2298
+ }
2299
+ },
2300
+ expectation: {
2301
+ id: globalEnv.entity_name,
2302
+ type: globalEnv.entity_type,
2303
+ b: {
2304
+ value: 10,
2305
+ type: 'Text'
2306
+ }
2307
+ }
2308
+ }
2309
+ ]
2310
+ },
2238
2311
  // 0300 - STATIC ATTRIBUTES TESTS
2239
2312
  {
2240
2313
  describeName:
@@ -398,6 +398,36 @@ describe('Jexl expression interpreter', function () {
398
398
  });
399
399
  });
400
400
 
401
+ describe('When number localization functions are executed', function () {
402
+ it('should return the localized string representation of the number', function (done) {
403
+ const scope = {
404
+ theNumber: 1234567.89,
405
+ locale: 'es-ES',
406
+ options: { minimumFractionDigits: 2, maximumFractionDigits: 2 }
407
+ };
408
+
409
+ expressionParser.parse('theNumber|localestringnumber(locale, options)', scope, function (error, result) {
410
+ should.not.exist(error);
411
+ result.should.equal('1.234.567,89');
412
+ done();
413
+ });
414
+ });
415
+
416
+ it('should return null for invalid number input', function (done) {
417
+ const scope = {
418
+ theNumber: 'not_a_number',
419
+ locale: 'en-US',
420
+ options: {}
421
+ };
422
+
423
+ expressionParser.parse('theNumber|localestringnumber(locale, options)', scope, function (error, result) {
424
+ should.not.exist(error);
425
+ should.equal(result, null);
426
+ done();
427
+ });
428
+ });
429
+ });
430
+
401
431
  // Check errors
402
432
  describe('When invalid inputs are used', function () {
403
433
  describe('When an invalid JSON string is parsed', function () {
@@ -243,7 +243,7 @@ describe('NGSI-v2 - In memory device registry', function () {
243
243
  should.exist(device.type);
244
244
  device.name.should.equal('name5');
245
245
  device.type.should.equal('Light5');
246
- Object.keys(device).length.should.equal(13);
246
+ Object.keys(device).length.should.equal(14);
247
247
  done();
248
248
  });
249
249
  });
@@ -15,7 +15,7 @@
15
15
  "url":"http://smartgondor.com/notify"
16
16
  },
17
17
  "attrs":["position"],
18
- "attrsFormat":"simplifiedNormalized",
18
+ "attrsFormat":"normalized",
19
19
  "onlyChangedAttrs":true
20
20
  }
21
21
  }
@@ -15,7 +15,7 @@
15
15
  "url":"http://smartgondor.com/notify"
16
16
  },
17
17
  "attrs":["position"],
18
- "attrsFormat":"simplifiedNormalized",
18
+ "attrsFormat":"normalized",
19
19
  "onlyChangedAttrs":true
20
20
  }
21
21
  }
@@ -15,7 +15,7 @@
15
15
  "url":"http://smartgondor.com/notify"
16
16
  },
17
17
  "attrs":["position"],
18
- "attrsFormat":"simplifiedNormalized",
18
+ "attrsFormat":"normalized",
19
19
  "onlyChangedAttrs":true
20
20
  }
21
21
  }
@@ -15,7 +15,7 @@
15
15
  "url":"http://smartgondor.com/notify"
16
16
  },
17
17
  "attrs":["reset"],
18
- "attrsFormat":"simplifiedNormalized",
18
+ "attrsFormat":"normalized",
19
19
  "onlyChangedAttrs":true
20
20
  }
21
21
  }