diff --git a/CHANGES_NEXT_RELEASE b/CHANGES_NEXT_RELEASE index b12cf170a..97aac23cf 100644 --- a/CHANGES_NEXT_RELEASE +++ b/CHANGES_NEXT_RELEASE @@ -1,2 +1,3 @@ +- Fix: null values arithmetics in JEXL expressions (#1440) - Fix: remove mongo `DeprecationWarning: current Server Discovery and Monitoring engine is deprecated` by setting `useUnifiedTopology = true` - Upgrade mongodb dev dep from 4.17.0 to 4.17.1 diff --git a/lib/plugins/jexlParser.js b/lib/plugins/jexlParser.js index 56a4a329d..59c423da9 100644 --- a/lib/plugins/jexlParser.js +++ b/lib/plugins/jexlParser.js @@ -125,12 +125,24 @@ function extractContext(attributeList) { function applyExpression(expression, context, typeInformation) { logContext = fillService(logContext, typeInformation); + // Delete null values from context. Related: + // https://github.com/telefonicaid/iotagent-node-lib/issues/1440 + // https://github.com/TomFrost/Jexl/issues/133 + deleteNulls(context); const result = parse(expression, context); logger.debug(logContext, 'applyExpression "[%j]" over "[%j]" result "[%j]" ', expression, context, result); const expressionResult = result !== undefined ? result : expression; return expressionResult; } +function deleteNulls(object) { + for (let key in object) { + if (object[key] === null) { + delete object[key]; + } + } +} + function isTransform(identifier) { return jexl.getTransform(identifier) !== (null || undefined); } diff --git a/test/unit/ngsiv2/expressions/jexlBasedTransformations-test.js b/test/unit/ngsiv2/expressions/jexlBasedTransformations-test.js index 1611546e0..e6d3c0f30 100644 --- a/test/unit/ngsiv2/expressions/jexlBasedTransformations-test.js +++ b/test/unit/ngsiv2/expressions/jexlBasedTransformations-test.js @@ -455,6 +455,140 @@ const iotAgentConfig = { skipValue: null } ] + }, + testNull: { + commands: [], + type: 'testNull', + lazy: [], + active: [ + { + name: 'a', + type: 'Number', + expression: 'v' + }, + { + name: 'b', + type: 'Number', + expression: 'v*3' + }, + { + name: 'c', + type: 'Boolean', + expression: 'v==null' + }, + { + name: 'd', + type: 'Text', + expression: "v?'no soy null':'soy null'" + }, + { + name: 'e', + type: 'Text', + expression: "v==null?'soy null':'no soy null'" + }, + { + name: 'f', + type: 'Text', + expression: "(v*3)==null?'soy null':'no soy null'" + }, + { + name: 'g', + type: 'Boolean', + expression: 'v == undefined' + } + ] + }, + testNullSkip: { + commands: [], + type: 'testNullSkip', + lazy: [], + active: [ + { + name: 'a', + type: 'Number', + expression: 'v', + skipValue: 'avoidNull' + }, + { + name: 'b', + type: 'Number', + expression: 'v*3', + skipValue: 'avoidNull' + }, + { + name: 'c', + type: 'Boolean', + expression: 'v==null', + skipValue: 'avoidNull' + }, + { + name: 'd', + type: 'Text', + expression: "v?'no soy null':'soy null'", + skipValue: 'avoidNull' + }, + { + name: 'e', + type: 'Text', + expression: "v==null?'soy null':'no soy null'", + skipValue: 'avoidNull' + }, + { + name: 'f', + type: 'Text', + expression: "(v*3)==null?'soy null':'no soy null'", + skipValue: 'avoidNull' + }, + { + name: 'g', + type: 'Boolean', + expression: 'v == undefined', + skipValue: 'avoidNull' + } + ] + }, + testNullExplicit: { + type: 'testNullExplicit', + explicitAttrs: true, + commands: [], + lazy: [], + active: [ + { + name: 'a', + type: 'Number', + expression: 'v' + }, + { + name: 'b', + type: 'Number', + expression: 'v*3' + }, + { + name: 'c', + type: 'Boolean', + expression: 'v==null' + }, + { + name: 'd', + type: 'Text', + expression: "v?'no soy null':'soy null'" + }, + { + name: 'e', + type: 'Text', + expression: "v==null?'soy null':'no soy null'" + }, + { + name: 'f', + type: 'Text', + expression: "(v*3)==null?'soy null':'no soy null'" + }, + { + name: 'g', + type: 'Boolean', + expression: 'v == undefined' + } + ] } }, service: 'smartgondor', @@ -598,6 +732,298 @@ describe('Java expression language (JEXL) based transformations plugin', functio }); }); + describe('When applying expressions with null values', function () { + // Case: Update for an attribute with bad expression + const values = [ + { + name: 'v', + type: 'Number', + value: null + } + ]; + + beforeEach(function () { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartgondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities?options=upsert', { + id: 'testNull1', + type: 'testNull', + v: { + value: null, + type: 'Number' + }, + c: { + value: true, + type: 'Boolean' + }, + d: { + value: 'soy null', + type: 'Text' + }, + e: { + value: 'soy null', + type: 'Text' + }, + f: { + value: 'no soy null', + type: 'Text' + }, + g: { + value: true, + type: 'Boolean' + } + }) + .reply(204); + }); + + it('it should be handled properly', function (done) { + iotAgentLib.update('testNull1', 'testNull', '', values, function (error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When applying expressions without values (NaN)', function () { + // Case: Update for an attribute with bad expression + const values = [ + { + name: 'z', + type: 'Number', + value: null + } + ]; + + beforeEach(function () { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartgondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities?options=upsert', { + id: 'testNull2', + type: 'testNull', + z: { + value: null, + type: 'Number' + }, + c: { + value: true, + type: 'Boolean' + }, + d: { + value: 'soy null', + type: 'Text' + }, + e: { + value: 'soy null', + type: 'Text' + }, + f: { + value: 'no soy null', + type: 'Text' + }, + g: { + value: true, + type: 'Boolean' + } + }) + .reply(204); + }); + + it('it should be handled properly', function (done) { + iotAgentLib.update('testNull2', 'testNull', '', values, function (error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When applying expressions with null values - Skip values disabled', function () { + // Case: Update for an attribute with bad expression + const values = [ + { + name: 'v', + type: 'Number', + value: null + } + ]; + + beforeEach(function () { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartgondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities?options=upsert', { + id: 'testNullSkip1', + type: 'testNullSkip', + v: { + value: null, + type: 'Number' + }, + a: { + value: null, + type: 'Number' + }, + b: { + value: null, + type: 'Number' + }, + c: { + value: true, + type: 'Boolean' + }, + d: { + value: 'soy null', + type: 'Text' + }, + e: { + value: 'soy null', + type: 'Text' + }, + f: { + value: 'no soy null', + type: 'Text' + }, + g: { + value: true, + type: 'Boolean' + } + }) + .reply(204); + }); + + it('it should be handled properly', function (done) { + iotAgentLib.update('testNullSkip1', 'testNullSkip', '', values, function (error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When applying expressions without values (NaN) - Skip values disabled', function () { + // Case: Update for an attribute with bad expression + const values = [ + { + name: 'z', + type: 'Number', + value: null + } + ]; + + beforeEach(function () { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartgondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities?options=upsert', { + id: 'testNullSkip2', + type: 'testNullSkip', + z: { + value: null, + type: 'Number' + }, + a: { + value: null, + type: 'Number' + }, + b: { + value: null, + type: 'Number' + }, + c: { + value: true, + type: 'Boolean' + }, + d: { + value: 'soy null', + type: 'Text' + }, + e: { + value: 'soy null', + type: 'Text' + }, + f: { + value: 'no soy null', + type: 'Text' + }, + g: { + value: true, + type: 'Boolean' + } + }) + .reply(204); + }); + + it('it should be handled properly', function (done) { + iotAgentLib.update('testNullSkip2', 'testNullSkip', '', values, function (error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When applying expressions with not explicit measures - explicitAttrs = true', function () { + // Case: Update for an attribute with bad expression + const values = [ + { + name: 'v', + type: 'Number', + value: null + } + ]; + + beforeEach(function () { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartgondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities?options=upsert', { + id: 'testNullExplicit1', + type: 'testNullExplicit', + c: { + value: true, + type: 'Boolean' + }, + d: { + value: 'soy null', + type: 'Text' + }, + e: { + value: 'soy null', + type: 'Text' + }, + f: { + value: 'no soy null', + type: 'Text' + }, + g: { + value: true, + type: 'Boolean' + } + }) + .reply(204); + }); + + it('it should be handled properly', function (done) { + iotAgentLib.update('testNullExplicit1', 'testNullExplicit', '', values, function (error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + describe('When there are expression attributes that are just calculated (not sent by the device)', function () { // Case: Expression which results is sent as a new attribute const values = [ @@ -1078,6 +1504,7 @@ describe('Java expression language (JEXL) based transformations plugin', functio }); }); }); + describe('When a measure arrives and there is not enough information to calculate an expression', function () { const values = [ {