@machinemetrics/io-adapter-lib 2.36.0 → 2.38.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.
@@ -27,31 +27,45 @@ jobs:
27
27
  - lib
28
28
  - index.js
29
29
  - package.json
30
-
30
+
31
+ # Publish jobs use npm trusted publishing (OIDC): no stored NPM token.
32
+ # npm >= 11.5.1 reads NPM_ID_TOKEN and exchanges it for a short-lived publish
33
+ # token. Provenance is not generated on CircleCI, and is unavailable for
34
+ # private repos regardless of CI provider.
31
35
  publish:
32
36
  docker:
33
- - image: cimg/node:22.13.1
37
+ - image: cimg/node:22.14
34
38
  steps:
35
39
  - checkout
36
40
  - attach_workspace:
37
41
  at: .
42
+ - run:
43
+ name: Update npm for trusted publishing
44
+ command: sudo npm install -g npm@latest
38
45
  - run:
39
46
  name: Publish to NPM
40
47
  command: |
41
- echo "//registry.npmjs.org/:_authToken=${NPM_PUBLISH_TOKEN}" > .npmrc
48
+ echo "node $(node --version) / npm $(npm --version)"
49
+ export NPM_ID_TOKEN="$(circleci run oidc get --claims '{"aud": "npm:registry.npmjs.org"}')"
50
+ test -n "$NPM_ID_TOKEN" || { echo "ERROR: empty OIDC token (NPM_ID_TOKEN) — cannot use trusted publishing"; exit 1; }
42
51
  npm publish --access public
43
52
 
44
53
  publish-beta:
45
54
  docker:
46
- - image: cimg/node:22.13.1
55
+ - image: cimg/node:22.14
47
56
  steps:
48
57
  - checkout
49
58
  - attach_workspace:
50
59
  at: .
60
+ - run:
61
+ name: Update npm for trusted publishing
62
+ command: sudo npm install -g npm@latest
51
63
  - run:
52
64
  name: Publish Beta to NPM
53
65
  command: |
54
- echo "//registry.npmjs.org/:_authToken=${NPM_PUBLISH_TOKEN}" > .npmrc
66
+ echo "node $(node --version) / npm $(npm --version)"
67
+ export NPM_ID_TOKEN="$(circleci run oidc get --claims '{"aud": "npm:registry.npmjs.org"}')"
68
+ test -n "$NPM_ID_TOKEN" || { echo "ERROR: empty OIDC token (NPM_ID_TOKEN) — cannot use trusted publishing"; exit 1; }
55
69
  npm publish --access public --tag beta
56
70
 
57
71
  validate-version:
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.38.0]
4
+ - Added object-keys and object-values transforms
5
+
6
+ ## [2.37.0]
7
+ - Brother config changes in support of d00 (c00<->d00 may not be the exact cutoff)
8
+
3
9
  ## [2.36.0]
4
10
  - Added if / else-if / else branch transform
5
11
 
@@ -16,7 +16,10 @@ class BrotherHTTPConfig {
16
16
  };
17
17
 
18
18
  const mode = expressionService.config['collection-mode'];
19
- const defaultMode = this.DataCollectionModes.MIXED;
19
+ const deviceName = expressionService.config.device;
20
+ const defaultMode = deviceName === 'brother-ftp'
21
+ ? this.DataCollectionModes.FTP
22
+ : this.DataCollectionModes.MIXED;
20
23
  if (mode === undefined) this.dataCollectionMode = defaultMode;
21
24
  else if (mode.toLowerCase() === 'http') this.dataCollectionMode = this.DataCollectionModes.HTTP;
22
25
  else if (mode.toLowerCase() === 'ftp') this.dataCollectionMode = this.DataCollectionModes.FTP;
@@ -141,15 +144,23 @@ class BrotherHTTPConfig {
141
144
 
142
145
  /** Configuration indicates how to collect data:
143
146
  * HTTP only,
144
- * FTP only (default),
147
+ * FTP only (default for device brother-ftp),
145
148
  * HTTP + FTP for program headers
146
149
  */
147
- let mode = config['collection-mode'] ?? '';
148
- if (_.isString(mode)) {
149
- mode = mode.toLowerCase();
150
+ const isBrotherFtp = config.device === 'brother-ftp';
151
+ let mode = config['collection-mode'];
152
+ if (mode === undefined || mode === null || mode === '') {
153
+ mode = isBrotherFtp ? 'ftp' : '';
154
+ }
155
+ if (!_.isString(mode)) {
156
+ throw new ConfigError('collection-mode must be a string').atAttribute('collection-mode');
157
+ }
158
+ mode = mode.toLowerCase();
159
+ if (isBrotherFtp && mode !== 'ftp') {
160
+ throw new ConfigError('device brother-ftp requires collection-mode ftp').atAttribute('collection-mode');
150
161
  }
151
162
  if (!_.includes(['http', 'ftp', 'mixed'], mode)) {
152
- throw new ConfigError('collection-mode must be one of: {http, ftp, mixed}').atAttribute('macros');
163
+ throw new ConfigError('collection-mode must be one of: {http, ftp, mixed}').atAttribute('collection-mode');
153
164
  }
154
165
 
155
166
  let keysThatAlreadyExist = '';
@@ -191,6 +202,36 @@ class BrotherHTTPConfig {
191
202
  throw new ConfigError(`The value of macros key ${key} must be a valid integer`).atAttribute(key);
192
203
  }
193
204
  });
205
+
206
+ this.assignOptionalRelativePath(config, 'memory-path', 'memoryPath');
207
+ this.assignOptionalRelativePath(config, 'production-path', 'productionPath');
208
+ this.assignOptionalRelativePath(config, 'panel-path', 'panelPath');
209
+ this.assignOptionalRelativePath(config, 'position-path', 'positionPath');
210
+ this.assignOptionalRelativePath(config, 'work-counter-path', 'workCounterPath');
211
+ this.assignOptionalRelativePath(config, 'alarm-path', 'alarmPath');
212
+ this.assignOptionalRelativePath(config, 'macro-path', 'macroPath');
213
+
214
+ if (_.has(config, 'cnc-version')) {
215
+ const cncVersion = config['cnc-version'];
216
+ if (cncVersion !== 'legacy' && cncVersion !== 'd00') {
217
+ throw new ConfigError('cnc-version must be one of: legacy, d00').atAttribute('cnc-version');
218
+ }
219
+ this.cncVersion = cncVersion;
220
+ }
221
+ }
222
+
223
+ assignOptionalRelativePath(config, yamlKey, propertyName) {
224
+ if (!_.has(config, yamlKey)) {
225
+ return;
226
+ }
227
+ const value = config[yamlKey];
228
+ if (!_.isString(value) || value.trim() === '') {
229
+ throw new ConfigError(`${yamlKey} must be a non-empty string`).atAttribute(yamlKey);
230
+ }
231
+ if (value.includes('..')) {
232
+ throw new ConfigError(`${yamlKey} must not contain '..'`).atAttribute(yamlKey);
233
+ }
234
+ this[propertyName] = value;
194
235
  }
195
236
 
196
237
  checkScanInterval(config) {
@@ -204,6 +204,7 @@ class Config {
204
204
  return new OPCUAConfig(expressionService);
205
205
  case 'brother':
206
206
  case 'brother-http':
207
+ case 'brother-ftp':
207
208
  return new BrotherHTTPConfig(expressionService);
208
209
  case 'adam-6052':
209
210
  return new AdamConfig(expressionService);
@@ -315,6 +316,7 @@ class Config {
315
316
  'opc-ua',
316
317
  'brother',
317
318
  'brother-http',
319
+ 'brother-ftp',
318
320
  'adam-6052',
319
321
  'mtconnect',
320
322
  'mtconnect-haas',
@@ -21,6 +21,8 @@ const MaxFilter = require('./max');
21
21
  const MaxLengthFilter = require('./maxLength');
22
22
  const MinDeltaFilter = require('./minDelta');
23
23
  const MinFilter = require('./min');
24
+ const ObjectKeysFilter = require('./objectKeys');
25
+ const ObjectValuesFilter = require('./objectValues');
24
26
  const OffDelayFilter = require('./offDelay');
25
27
  const OnDelayFilter = require('./onDelay');
26
28
  const PatternEscapeFilter = require('./patternEscape');
@@ -68,6 +70,8 @@ module.exports = {
68
70
  maxLength: MaxLengthFilter,
69
71
  min: MinFilter,
70
72
  minDelta: MinDeltaFilter,
73
+ objectKeys: ObjectKeysFilter,
74
+ objectValues: ObjectValuesFilter,
71
75
  offDelay: OffDelayFilter,
72
76
  onDelay: OnDelayFilter,
73
77
  patternEscape: PatternEscapeFilter,
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ const _ = require('lodash');
4
+ const TransformState = require('./transformState');
5
+
6
+ /**
7
+ * Returns the keys of an object sample. Arrays return their numeric indexes;
8
+ * other values pass through unchanged.
9
+ */
10
+ class ObjectKeysFilter extends TransformState {
11
+ static op = 'object-keys';
12
+
13
+ static create(_args) {
14
+ return new ObjectKeysFilter();
15
+ }
16
+
17
+ filter(context, value, time) {
18
+ if (_.isArray(value)) {
19
+ this.commitValue(context, _.range(value.length), time);
20
+ } else if (_.isObject(value)) {
21
+ this.commitValue(context, Object.keys(value), time);
22
+ } else {
23
+ this.commitValue(context, value, time);
24
+ }
25
+ }
26
+ }
27
+
28
+ module.exports = ObjectKeysFilter;
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ const _ = require('lodash');
4
+ const TransformState = require('./transformState');
5
+
6
+ /**
7
+ * Returns the values of an object sample. Arrays pass through unchanged;
8
+ * other non-object values also pass through unchanged.
9
+ */
10
+ class ObjectValuesFilter extends TransformState {
11
+ static op = 'object-values';
12
+
13
+ static create(_args) {
14
+ return new ObjectValuesFilter();
15
+ }
16
+
17
+ filter(context, value, time) {
18
+ if (_.isArray(value)) {
19
+ this.commitValue(context, value, time);
20
+ } else if (_.isObject(value)) {
21
+ this.commitValue(context, Object.values(value), time);
22
+ } else {
23
+ this.commitValue(context, value, time);
24
+ }
25
+ }
26
+ }
27
+
28
+ module.exports = ObjectValuesFilter;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@machinemetrics/io-adapter-lib",
3
- "version": "2.36.0",
3
+ "version": "2.38.0",
4
4
  "description": "Configuration and engine implementation for MachineMetrics AdapterScripts and adapters",
5
5
  "main": "index.js",
6
6
  "license": "UNLICENSED",
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ const expect = require('chai').expect;
4
+ const ConfigError = require('../../lib/config/configError');
5
+ const testUtils = require('../util/testUtils');
6
+
7
+ describe('Brother FTP device config', function () {
8
+ it('accepts brother-ftp without collection-mode (defaults to ftp)', async function () {
9
+ const cfg = await testUtils.loadConfig('device/brother-ftp-minimal.yml');
10
+ expect(cfg.deviceName).to.eq('brother-ftp');
11
+ expect(cfg.device.dataCollectionMode).to.eq(cfg.device.DataCollectionModes.FTP);
12
+ });
13
+
14
+ it('passes through optional FTP paths and cnc-version', async function () {
15
+ const cfg = await testUtils.loadConfig('device/brother-ftp-paths.yml');
16
+ expect(cfg.device.cncVersion).to.eq('d00');
17
+ expect(cfg.device.memoryPath).to.eq('_DAT/MEM.NC');
18
+ expect(cfg.device.productionPath).to.eq('_PRD/PRDD2.NC');
19
+ expect(cfg.device.panelPath).to.eq('_DAT/PANEL.NC');
20
+ expect(cfg.device.positionPath).to.eq('_DAT/PDSP.NC');
21
+ expect(cfg.device.workCounterPath).to.eq('_DAT/WKCNTR.NC');
22
+ expect(cfg.device.alarmPath).to.eq('_DAT/ALARM.NC');
23
+ expect(cfg.device.macroPath).to.eq('_DAT/MCRNI1.NC');
24
+ });
25
+
26
+ it('does not set path properties when keys are omitted', async function () {
27
+ const cfg = await testUtils.loadConfig('device/brother-ftp-minimal.yml');
28
+ expect(cfg.device.memoryPath).to.eq(undefined);
29
+ expect(cfg.device.cncVersion).to.eq(undefined);
30
+ });
31
+
32
+ it('accepts cnc-version legacy', async function () {
33
+ const cfg = await testUtils.loadConfig('device/brother-ftp-cnc-legacy.yml');
34
+ expect(cfg.device.cncVersion).to.eq('legacy');
35
+ });
36
+
37
+ it('rejects invalid cnc-version', async function () {
38
+ try {
39
+ await testUtils.loadConfig('device/brother-ftp-bad-cnc.yml');
40
+ expect.fail('Expected ConfigError');
41
+ } catch (err) {
42
+ expect(err).to.be.instanceof(ConfigError);
43
+ }
44
+ });
45
+
46
+ it('rejects paths containing ..', async function () {
47
+ try {
48
+ await testUtils.loadConfig('device/brother-ftp-bad-path.yml');
49
+ expect.fail('Expected ConfigError');
50
+ } catch (err) {
51
+ expect(err).to.be.instanceof(ConfigError);
52
+ }
53
+ });
54
+
55
+ it('rejects brother-ftp with non-ftp collection-mode', async function () {
56
+ try {
57
+ await testUtils.loadConfig('device/brother-ftp-bad-mode.yml');
58
+ expect.fail('Expected ConfigError');
59
+ } catch (err) {
60
+ expect(err).to.be.instanceof(ConfigError);
61
+ }
62
+ });
63
+ });
@@ -15,5 +15,7 @@ describe('Brother HTTP config tests', function () {
15
15
  expect(defaultConfig.device.port).to.eq(21);
16
16
  expect(defaultConfig.device.scanInterval).to.eq(1000);
17
17
  expect(defaultConfig.device.httpInterval).to.eq(50);
18
+ expect(defaultConfig.device.memoryPath).to.eq(undefined);
19
+ expect(defaultConfig.device.cncVersion).to.eq(undefined);
18
20
  });
19
21
  });
@@ -0,0 +1,3 @@
1
+ device: brother-ftp
2
+ endpoint: 192.168.1.50
3
+ cnc-version: bogus
@@ -0,0 +1,3 @@
1
+ device: brother-ftp
2
+ endpoint: 192.168.1.50
3
+ collection-mode: http
@@ -0,0 +1,3 @@
1
+ device: brother-ftp
2
+ endpoint: 192.168.1.50
3
+ memory-path: _DAT/../MEM.NC
@@ -0,0 +1,3 @@
1
+ device: brother-ftp
2
+ endpoint: 192.168.1.50
3
+ cnc-version: legacy
@@ -0,0 +1,2 @@
1
+ device: brother-ftp
2
+ endpoint: 192.168.1.50
@@ -0,0 +1,11 @@
1
+ device: brother-ftp
2
+ endpoint: 192.168.1.50
3
+ collection-mode: ftp
4
+ cnc-version: d00
5
+ memory-path: _DAT/MEM.NC
6
+ production-path: _PRD/PRDD2.NC
7
+ panel-path: _DAT/PANEL.NC
8
+ position-path: _DAT/PDSP.NC
9
+ work-counter-path: _DAT/WKCNTR.NC
10
+ alarm-path: _DAT/ALARM.NC
11
+ macro-path: _DAT/MCRNI1.NC
@@ -0,0 +1,15 @@
1
+ version: 2
2
+ device: mtconnect-adapter
3
+ endpoint: localhost:8001
4
+ mtconnect-port: 8002
5
+ declare-keys:
6
+ - cond1:
7
+ type: condition
8
+ variables:
9
+ activeCodes:
10
+ - source: cond1
11
+ - object-values
12
+ - reject:
13
+ expression: this.level != 'FAULT' and this.level != 'WARNING'
14
+ - map:
15
+ - expression: this.code
@@ -0,0 +1,14 @@
1
+ version: 2
2
+ device: mtconnect-adapter
3
+ endpoint: localhost:8001
4
+ mtconnect-port: 8002
5
+ declare-keys:
6
+ - obj
7
+ - arr
8
+ variables:
9
+ objKeys:
10
+ - source: obj
11
+ - object-keys
12
+ arrKeys:
13
+ - source: arr
14
+ - object-keys
@@ -0,0 +1,14 @@
1
+ version: 2
2
+ device: mtconnect-adapter
3
+ endpoint: localhost:8001
4
+ mtconnect-port: 8002
5
+ declare-keys:
6
+ - obj
7
+ - arr
8
+ variables:
9
+ objValues:
10
+ - source: obj
11
+ - object-values
12
+ arrValues:
13
+ - source: arr
14
+ - object-values
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ const EngineV2 = require('../../lib/engine/engineV2');
4
+ const Builder = require('../../lib/engine/transformBuilderV2');
5
+ const testUtils = require('../util/testUtils');
6
+
7
+ describe('repro active condition codes via object-values pipeline', async function () {
8
+ let config;
9
+ before(async () => {
10
+ config = await testUtils.loadConfig('repro/active-condition-codes-repro.yml');
11
+ });
12
+
13
+ it('emits the list of FAULT/WARNING codes as the condition table evolves', function () {
14
+ const engine = new EngineV2(config);
15
+ const builder = new Builder(config);
16
+ builder.build(engine);
17
+
18
+ const source = testUtils.mtconnectSource(config.device.keys);
19
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.activeCodes, source);
20
+
21
+ source.sendCondition('cond1', { level: 'NORMAL' }, 0);
22
+ source.sendCondition('cond1', { code: 'X01', level: 'WARNING', message: 'W X01' }, 1000);
23
+ source.sendCondition('cond1', { code: 'X02', level: 'FAULT', message: 'F X02' }, 2000);
24
+ source.sendCondition('cond1', { code: 'X01', level: 'NORMAL' }, 3000);
25
+ source.sendCondition('cond1', { code: 'X01', level: 'FAULT', message: 'F X01' }, 4000);
26
+ source.sendCondition('cond1', { level: 'UNAVAILABLE' }, 5000);
27
+ source.sendCondition('cond1', { code: 'X02', level: 'WARNING', message: 'W X02' }, 6000);
28
+ source.sendCondition('cond1', { level: 'UNAVAILABLE' }, 7000);
29
+
30
+ engine.validateFilter([
31
+ [[], 0],
32
+ [['X01'], 1],
33
+ [['X01', 'X02'], 2],
34
+ [['X02'], 3],
35
+ [['X01', 'X02'], 4],
36
+ [[], 5],
37
+ [['X02'], 6],
38
+ [[], 7],
39
+ ]);
40
+ });
41
+ });
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ const EngineV2 = require('../../lib/engine/engineV2');
4
+ const Builder = require('../../lib/engine/transformBuilderV2');
5
+ const ObjectKeys = require('../../lib/transform').objectKeys;
6
+ const testUtils = require('../util/testUtils');
7
+
8
+ describe('object-keys transform tests', function () {
9
+ it('returns keys for objects', async function () {
10
+ await testUtils.testValue(new ObjectKeys(), { a: 1, b: 2 }, ['a', 'b']);
11
+ await testUtils.testValue(new ObjectKeys(), { x: 'hello', y: 'world', z: '!' }, ['x', 'y', 'z']);
12
+ await testUtils.testValue(new ObjectKeys(), {}, []);
13
+ });
14
+
15
+ it('returns indexes for arrays', async function () {
16
+ await testUtils.testValue(new ObjectKeys(), ['a', 'b', 'c'], [0, 1, 2]);
17
+ await testUtils.testValue(new ObjectKeys(), [10, 20], [0, 1]);
18
+ await testUtils.testValue(new ObjectKeys(), [], []);
19
+ });
20
+
21
+ it('passes through non-object values', async function () {
22
+ await testUtils.testValue(new ObjectKeys(), 42, 42);
23
+ await testUtils.testValue(new ObjectKeys(), 'hello', 'hello');
24
+ await testUtils.testValue(new ObjectKeys(), null, null);
25
+ });
26
+ });
27
+
28
+ describe('object-keys full engine config file tests', function () {
29
+ let config;
30
+ before(async () => {
31
+ config = await testUtils.loadConfig('transform/object-keys.yml');
32
+ });
33
+
34
+ it('returns keys for objects', function () {
35
+ const engine = new EngineV2(config);
36
+ const builder = new Builder(config);
37
+ builder.build(engine);
38
+
39
+ const source = testUtils.valueSource();
40
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.objKeys, source);
41
+
42
+ source.sendValue('obj', { a: 1, b: 2, c: 3 }, 0);
43
+
44
+ engine.validateFilter([
45
+ [['a', 'b', 'c'], 0],
46
+ ]);
47
+ });
48
+
49
+ it('returns indexes for arrays', function () {
50
+ const engine = new EngineV2(config);
51
+ const builder = new Builder(config);
52
+ builder.build(engine);
53
+
54
+ const source = testUtils.valueSource();
55
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.arrKeys, source);
56
+
57
+ source.sendValue('arr', ['x', 'y', 'z'], 0);
58
+
59
+ engine.validateFilter([
60
+ [[0, 1, 2], 0],
61
+ ]);
62
+ });
63
+ });
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ const EngineV2 = require('../../lib/engine/engineV2');
4
+ const Builder = require('../../lib/engine/transformBuilderV2');
5
+ const ObjectValues = require('../../lib/transform').objectValues;
6
+ const testUtils = require('../util/testUtils');
7
+
8
+ describe('object-values transform tests', function () {
9
+ it('returns values for objects', async function () {
10
+ await testUtils.testValue(new ObjectValues(), { a: 1, b: 2 }, [1, 2]);
11
+ await testUtils.testValue(new ObjectValues(), { x: 'hello', y: 'world', z: '!' }, ['hello', 'world', '!']);
12
+ await testUtils.testValue(new ObjectValues(), {}, []);
13
+ });
14
+
15
+ it('returns array itself for arrays', async function () {
16
+ await testUtils.testValue(new ObjectValues(), ['a', 'b', 'c'], ['a', 'b', 'c']);
17
+ await testUtils.testValue(new ObjectValues(), [10, 20], [10, 20]);
18
+ await testUtils.testValue(new ObjectValues(), [], []);
19
+ });
20
+
21
+ it('passes through non-object values', async function () {
22
+ await testUtils.testValue(new ObjectValues(), 42, 42);
23
+ await testUtils.testValue(new ObjectValues(), 'hello', 'hello');
24
+ await testUtils.testValue(new ObjectValues(), null, null);
25
+ });
26
+ });
27
+
28
+ describe('object-values full engine config file tests', function () {
29
+ let config;
30
+ before(async () => {
31
+ config = await testUtils.loadConfig('transform/object-values.yml');
32
+ });
33
+
34
+ it('returns values for objects', function () {
35
+ const engine = new EngineV2(config);
36
+ const builder = new Builder(config);
37
+ builder.build(engine);
38
+
39
+ const source = testUtils.valueSource();
40
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.objValues, source);
41
+
42
+ source.sendValue('obj', { a: 1, b: 2, c: 3 }, 0);
43
+
44
+ engine.validateFilter([
45
+ [[1, 2, 3], 0],
46
+ ]);
47
+ });
48
+
49
+ it('returns the array itself for arrays', function () {
50
+ const engine = new EngineV2(config);
51
+ const builder = new Builder(config);
52
+ builder.build(engine);
53
+
54
+ const source = testUtils.valueSource();
55
+ testUtils.attachEngineTransformValidator(engine, engine.variablePool.arrValues, source);
56
+
57
+ source.sendValue('arr', ['x', 'y', 'z'], 0);
58
+
59
+ engine.validateFilter([
60
+ [['x', 'y', 'z'], 0],
61
+ ]);
62
+ });
63
+ });