@machinemetrics/io-adapter-lib 2.32.2 → 2.33.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.
- package/CHANGELOG.md +7 -0
- package/lib/config/device/opcuaConfig.js +25 -0
- package/lib/expressionService.js +19 -3
- package/lib/transform/map.js +2 -2
- package/package.json +1 -1
- package/test/config/opcua.test.js +22 -5
- package/test/configFiles/device/opcua-conditions-default.yml +5 -0
- package/test/configFiles/device/opcua-conditions.yml +9 -0
- package/test/configFiles/transform/map.yml +6 -0
- package/test/expressionService.test.js +36 -0
- package/test/transform/map.test.js +15 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.33.0]
|
|
4
|
+
- Added support for numeric indexing in expressions
|
|
5
|
+
- Fixed map not allowing any transforms to be chained beyond it
|
|
6
|
+
|
|
7
|
+
## [2.32.3]
|
|
8
|
+
- Added opcua-conditions options for code format and allow/deny nodes
|
|
9
|
+
|
|
3
10
|
## [2.32.2]
|
|
4
11
|
- Add support to reclassify conditions to NORMAL
|
|
5
12
|
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
const _ = require('lodash');
|
|
4
4
|
const ConfigError = require('../configError');
|
|
5
|
+
const AllowDenyList = require('../common/allowDenyList');
|
|
6
|
+
|
|
7
|
+
const allowDenyList = new AllowDenyList();
|
|
5
8
|
|
|
6
9
|
// Pattern to match valid OPC-UA node ID paths, as supported by node-opcua
|
|
7
10
|
// Our validation may be slightly stricter on some value validation.
|
|
@@ -144,6 +147,28 @@ class OpcuaConfig {
|
|
|
144
147
|
result.warningThreshold = warningThreshold;
|
|
145
148
|
}
|
|
146
149
|
|
|
150
|
+
result.codeFormat = '{conditionName}_{conditionIdHash}';
|
|
151
|
+
if (_.has(conditions, 'code-format')) {
|
|
152
|
+
const codeFormat = conditions['code-format'];
|
|
153
|
+
if (_.isEmpty(codeFormat) || !_.isString(codeFormat)) {
|
|
154
|
+
throw new ConfigError('code-format cannot be empty').atAttribute('code-format');
|
|
155
|
+
}
|
|
156
|
+
result.codeFormat = codeFormat;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (_.has(conditions, 'allow-condition-nodes')) {
|
|
160
|
+
result.allowConditionNodes = allowDenyList.parseWildcardKeyList(conditions['allow-condition-nodes'], 'allow-condition-nodes');
|
|
161
|
+
}
|
|
162
|
+
if (_.has(conditions, 'deny-condition-nodes')) {
|
|
163
|
+
result.denyConditionNodes = allowDenyList.parseWildcardKeyList(conditions['deny-condition-nodes'], 'deny-condition-nodes');
|
|
164
|
+
}
|
|
165
|
+
if (_.has(conditions, 'allow-source-nodes')) {
|
|
166
|
+
result.allowSourceNodes = allowDenyList.parseWildcardKeyList(conditions['allow-source-nodes'], 'allow-source-nodes');
|
|
167
|
+
}
|
|
168
|
+
if (_.has(conditions, 'deny-source-nodes')) {
|
|
169
|
+
result.denySourceNodes = allowDenyList.parseWildcardKeyList(conditions['deny-source-nodes'], 'deny-source-nodes');
|
|
170
|
+
}
|
|
171
|
+
|
|
147
172
|
return result;
|
|
148
173
|
} catch (error) {
|
|
149
174
|
if (error instanceof ConfigError) {
|
package/lib/expressionService.js
CHANGED
|
@@ -196,7 +196,7 @@ class ExpressionService {
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
findCompoundIdentifiers(str) {
|
|
199
|
-
const matches = str.match(/[A-Za-z_][A-Za-z0-9_-]*(?:(?:\.[A-Za-z][A-Za-z0-9_-]*)|(?:\['[^'"]+'\]))+/g);
|
|
199
|
+
const matches = str.match(/[A-Za-z_][A-Za-z0-9_-]*(?:(?:\.[A-Za-z][A-Za-z0-9_-]*)|(?:\[('[^'"]+'|\d+)\]))+/g);
|
|
200
200
|
return matches ?? [];
|
|
201
201
|
}
|
|
202
202
|
|
|
@@ -216,7 +216,13 @@ class ExpressionService {
|
|
|
216
216
|
rest.push(match[1]);
|
|
217
217
|
compound = compound.substr(match[0].length);
|
|
218
218
|
} else {
|
|
219
|
-
|
|
219
|
+
match = compound.match(/^\[(\d+)\]/);
|
|
220
|
+
if (match) {
|
|
221
|
+
rest.push(+match[1]);
|
|
222
|
+
compound = compound.substr(match[0].length);
|
|
223
|
+
} else {
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
220
226
|
}
|
|
221
227
|
}
|
|
222
228
|
}
|
|
@@ -249,6 +255,12 @@ class ExpressionService {
|
|
|
249
255
|
const first = tokens[0];
|
|
250
256
|
if (first === 'this' || this.channelsLookup[first]) {
|
|
251
257
|
accum[first] = _.reduceRight(_.tail(tokens), (sub, item) => {
|
|
258
|
+
if (_.isNumber(item)) {
|
|
259
|
+
return [
|
|
260
|
+
..._.times(Math.max(0, item - 1), () => 0),
|
|
261
|
+
sub,
|
|
262
|
+
];
|
|
263
|
+
}
|
|
252
264
|
return { [item]: sub };
|
|
253
265
|
}, 0);
|
|
254
266
|
} else {
|
|
@@ -261,7 +273,11 @@ class ExpressionService {
|
|
|
261
273
|
try {
|
|
262
274
|
return this.compileInnerExpression(expression, { ...scope, ...addedScope });
|
|
263
275
|
} catch (err) {
|
|
264
|
-
|
|
276
|
+
let message = err.message;
|
|
277
|
+
if (message.startsWith('Index out')) {
|
|
278
|
+
message = `${message}. Indexes in expressions start at 1`;
|
|
279
|
+
}
|
|
280
|
+
throw new Error(`Problem evaluating expression: ${message}`);
|
|
265
281
|
}
|
|
266
282
|
}
|
|
267
283
|
|
package/lib/transform/map.js
CHANGED
|
@@ -47,7 +47,7 @@ class MapFilter extends TransformState {
|
|
|
47
47
|
constructor({ engine, /* path, rootPath, baseTransform, */ args: { transforms } }, builder) {
|
|
48
48
|
super();
|
|
49
49
|
|
|
50
|
-
this.
|
|
50
|
+
this.innerChain = builder.createTransformChain(engine, transforms);
|
|
51
51
|
|
|
52
52
|
this.makeChain = () => {
|
|
53
53
|
return new MapChain(builder.createTransformChain(engine, transforms));
|
|
@@ -78,7 +78,7 @@ class MapFilter extends TransformState {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
supportsPendingChanges() {
|
|
81
|
-
return this.
|
|
81
|
+
return this.innerChain.supportsPendingChanges();
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
updateChainList(size) {
|
package/package.json
CHANGED
|
@@ -8,11 +8,13 @@ describe('OPC-UA config tests', function () {
|
|
|
8
8
|
let config;
|
|
9
9
|
let defaultConfig;
|
|
10
10
|
let conditionsConfig;
|
|
11
|
+
let conditionsDefaultConfig;
|
|
11
12
|
|
|
12
13
|
before(async () => {
|
|
13
14
|
defaultConfig = await testUtils.loadConfig('device/opcua-default.yml');
|
|
14
15
|
config = await testUtils.loadConfig('device/opcua-std.yml');
|
|
15
16
|
conditionsConfig = await testUtils.loadConfig('device/opcua-conditions.yml');
|
|
17
|
+
conditionsDefaultConfig = await testUtils.loadConfig('device/opcua-conditions-default.yml');
|
|
16
18
|
});
|
|
17
19
|
|
|
18
20
|
it('loads minimal config with defaults', async function () {
|
|
@@ -104,12 +106,27 @@ describe('OPC-UA config tests', function () {
|
|
|
104
106
|
testPath('nsu=http://test.org/UA/Data/;i=10853');
|
|
105
107
|
});
|
|
106
108
|
|
|
109
|
+
it('loads opcua-conditions with default properties', async function () {
|
|
110
|
+
expect(conditionsDefaultConfig.device.opcuaConditions.key).to.eq('system');
|
|
111
|
+
expect(conditionsDefaultConfig.device.opcuaConditions.faultThreshold).to.eq(undefined);
|
|
112
|
+
expect(conditionsDefaultConfig.device.opcuaConditions.warningThreshold).to.eq(undefined);
|
|
113
|
+
expect(conditionsDefaultConfig.device.opcuaConditions.codeFormat).to.eq('{conditionName}_{conditionIdHash}');
|
|
114
|
+
expect(conditionsDefaultConfig.device.opcuaConditions.allowConditionNodes).to.eq(undefined);
|
|
115
|
+
expect(conditionsDefaultConfig.device.opcuaConditions.denyConditionNodes).to.eq(undefined);
|
|
116
|
+
expect(conditionsDefaultConfig.device.opcuaConditions.allowSourceNodes).to.eq(undefined);
|
|
117
|
+
expect(conditionsDefaultConfig.device.opcuaConditions.denySourceNodes).to.eq(undefined);
|
|
118
|
+
expect(conditionsDefaultConfig.device.conditionKey).to.eq('system');
|
|
119
|
+
});
|
|
120
|
+
|
|
107
121
|
it('loads opcua-conditions with all properties', async function () {
|
|
108
|
-
expect(conditionsConfig.device.opcuaConditions).to.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
122
|
+
expect(conditionsConfig.device.opcuaConditions.key).to.eq('system');
|
|
123
|
+
expect(conditionsConfig.device.opcuaConditions.faultThreshold).to.eq(100);
|
|
124
|
+
expect(conditionsConfig.device.opcuaConditions.warningThreshold).to.eq(50);
|
|
125
|
+
expect(conditionsConfig.device.opcuaConditions.codeFormat).to.eq('{conditionName}');
|
|
126
|
+
expect(conditionsConfig.device.opcuaConditions.allowConditionNodes).to.have.lengthOf(1);
|
|
127
|
+
expect(conditionsConfig.device.opcuaConditions.denyConditionNodes).to.have.lengthOf(1);
|
|
128
|
+
expect(conditionsConfig.device.opcuaConditions.allowSourceNodes).to.have.lengthOf(1);
|
|
129
|
+
expect(conditionsConfig.device.opcuaConditions.denySourceNodes).to.have.lengthOf(1);
|
|
113
130
|
expect(conditionsConfig.device.conditionKey).to.eq('system');
|
|
114
131
|
});
|
|
115
132
|
});
|
|
@@ -5,3 +5,12 @@ opcua-conditions:
|
|
|
5
5
|
key: system
|
|
6
6
|
fault-threshold: 100
|
|
7
7
|
warning-threshold: 50
|
|
8
|
+
code-format: "{conditionName}"
|
|
9
|
+
allow-condition-nodes:
|
|
10
|
+
- ns=2;s=Flames/*
|
|
11
|
+
deny-condition-nodes:
|
|
12
|
+
- ns=2;s=Flames/ControlledFacilityComponents/*
|
|
13
|
+
allow-source-nodes:
|
|
14
|
+
- ns=2;s=Flames/*
|
|
15
|
+
deny-source-nodes:
|
|
16
|
+
- ns=2;s=Flames/ControlledFacilityComponents/*
|
|
@@ -7,6 +7,7 @@ declare-keys:
|
|
|
7
7
|
- counter
|
|
8
8
|
- program
|
|
9
9
|
- complex
|
|
10
|
+
- complex2
|
|
10
11
|
variables:
|
|
11
12
|
var1:
|
|
12
13
|
- source: counter
|
|
@@ -17,3 +18,8 @@ variables:
|
|
|
17
18
|
- source: complex
|
|
18
19
|
- map:
|
|
19
20
|
- expression: this.subkey
|
|
21
|
+
var3:
|
|
22
|
+
- source: complex2
|
|
23
|
+
- map:
|
|
24
|
+
- expression: this.subkey
|
|
25
|
+
- expression: this[1]
|
|
@@ -80,6 +80,42 @@ describe('ExpressionService tests', function () {
|
|
|
80
80
|
expect(() => exprsvc.compileExpression('this + 1')).to.throw('Undefined symbol this');
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
+
it('parses expressions with compound identifiers', function () {
|
|
84
|
+
const exprsvc = new ExpressionService();
|
|
85
|
+
|
|
86
|
+
const context1 = { this: 5 };
|
|
87
|
+
const expr1 = exprsvc.compileExpression('this', context1);
|
|
88
|
+
expect(expr1.evaluate(context1)).to.eq(5);
|
|
89
|
+
|
|
90
|
+
const context2 = { this: { subkey: 5 } };
|
|
91
|
+
const expr2 = exprsvc.compileExpression('this.subkey', context2);
|
|
92
|
+
expect(expr2.evaluate(context2)).to.eq(5);
|
|
93
|
+
|
|
94
|
+
const context3 = { this: { subkey: { secondary: 5 } } };
|
|
95
|
+
const expr3 = exprsvc.compileExpression('this.subkey.secondary', context3);
|
|
96
|
+
expect(expr3.evaluate(context3)).to.eq(5);
|
|
97
|
+
|
|
98
|
+
const context4 = { this: { subkey: { secondary: 5 } } };
|
|
99
|
+
const expr4 = exprsvc.compileExpression('this.subkey[\'secondary\']', context4);
|
|
100
|
+
expect(expr4.evaluate(context4)).to.eq(5);
|
|
101
|
+
|
|
102
|
+
const context5 = { this: { subkey: [0, 0, 5] } };
|
|
103
|
+
const expr5 = exprsvc.compileExpression('this.subkey[3]', context5);
|
|
104
|
+
expect(expr5.evaluate(context5)).to.eq(5);
|
|
105
|
+
|
|
106
|
+
const context6 = { this: [0, 0, 5] };
|
|
107
|
+
const expr6 = exprsvc.compileExpression('this[3]', context6);
|
|
108
|
+
expect(expr6.evaluate(context6)).to.eq(5);
|
|
109
|
+
|
|
110
|
+
const context7 = { this: { subkey: [0, 0, { bar: 5 }] } };
|
|
111
|
+
const expr7 = exprsvc.compileExpression('this.subkey[3].bar', context7);
|
|
112
|
+
expect(expr7.evaluate(context7)).to.eq(5);
|
|
113
|
+
|
|
114
|
+
const context8 = { this: { subkey: [0, 0, { bar: [0, { baz: 5 }] }] } };
|
|
115
|
+
const expr8 = exprsvc.compileExpression('this.subkey[3].bar[2].baz', context8);
|
|
116
|
+
expect(expr8.evaluate(context8)).to.eq(5);
|
|
117
|
+
});
|
|
118
|
+
|
|
83
119
|
/* eslint-disable no-template-curly-in-string */
|
|
84
120
|
|
|
85
121
|
it('parses string expressions', function () {
|
|
@@ -39,4 +39,19 @@ describe('map full engine config file tests', function () {
|
|
|
39
39
|
[[10, 20], 0],
|
|
40
40
|
]);
|
|
41
41
|
});
|
|
42
|
+
|
|
43
|
+
it('transform chained after map', function () {
|
|
44
|
+
const engine = new EngineV2(config);
|
|
45
|
+
const builder = new Builder(config);
|
|
46
|
+
builder.build(engine);
|
|
47
|
+
|
|
48
|
+
const source = testUtils.valueSource();
|
|
49
|
+
testUtils.attachEngineTransformValidator(engine, engine.variablePool.var3, source);
|
|
50
|
+
|
|
51
|
+
source.sendValue('complex2', [{ subkey: 10, xx: 15 }, { subkey: 20, xx: 30 }], 0);
|
|
52
|
+
|
|
53
|
+
engine.validateFilter([
|
|
54
|
+
[10, 0],
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
42
57
|
});
|