@yarkivaev/scada 2.3.90 → 2.3.112
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/README.md +6 -1
- package/charts/scada/Chart.yaml +6 -0
- package/charts/scada/templates/_helpers.tpl +63 -0
- package/charts/scada/templates/clickhouse.yaml +57 -0
- package/charts/scada/templates/collector.yaml +115 -0
- package/charts/scada/templates/postgres.yaml +70 -0
- package/charts/scada/templates/rabbitmq.yaml +150 -0
- package/charts/scada/templates/site-server.yaml +110 -0
- package/charts/scada/templates/supervisor-sink.yaml +64 -0
- package/charts/scada/values.yaml +30 -0
- package/index.js +10 -0
- package/package.json +3 -1
- package/src/application/deploy/appReleases.js +50 -0
- package/src/application/deploy/applyIncludes.js +31 -0
- package/src/application/deploy/expand.js +49 -0
- package/src/application/deploy/expandDevices.js +135 -0
- package/src/application/deploy/expandFlows.js +281 -0
- package/src/application/deploy/expandNamespace.js +193 -0
- package/src/application/deploy/expandPattern.js +207 -0
- package/src/application/deploy/expandProfiles.js +70 -0
- package/src/application/deploy/frozen.js +23 -0
- package/src/application/deploy/mergeDocuments.js +98 -0
- package/src/application/deploy/paths.js +86 -0
- package/src/application/deploy/pointPaths.js +20 -0
- package/src/application/deploy/readDocument.js +169 -0
- package/src/application/deploy/releaseEdges.js +185 -0
- package/src/application/deploy/releaseNodes.js +118 -0
- package/src/application/deploy/releaseValues.js +200 -0
- package/src/application/deploy/releases.js +115 -0
- package/src/application/deploy/withCollectors.js +28 -0
- package/src/bin/modbus-to-mqtt.js +57 -0
- package/src/infrastructure/ingest/mqtt/addressWindow.js +79 -0
- package/src/infrastructure/ingest/mqtt/modbusPublish.js +277 -0
- package/src/infrastructure/ingest/mqtt/registerPoint.js +90 -0
- package/src/infrastructure/ingest/mqtt/registerPublish.js +84 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rejects a patch that names one id twice.
|
|
3
|
+
*
|
|
4
|
+
* @param {object[]} patch - patch entries
|
|
5
|
+
* @returns {void}
|
|
6
|
+
* @example
|
|
7
|
+
* repeated([{ id: 'voltage' }]);
|
|
8
|
+
*/
|
|
9
|
+
function repeated(patch) {
|
|
10
|
+
const ids = patch.map((entry) => {
|
|
11
|
+
return entry.id;
|
|
12
|
+
});
|
|
13
|
+
ids.forEach((id, index) => {
|
|
14
|
+
if (ids.indexOf(id) !== index) {
|
|
15
|
+
throw new Error(`system document patch repeats ${id}`);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Copies patch fields onto one staying node.
|
|
22
|
+
*
|
|
23
|
+
* @param {object} node - template node
|
|
24
|
+
* @param {object} entry - patch entry
|
|
25
|
+
* @param {object} templates - reusable node lists
|
|
26
|
+
* @param {Function} apply - patch function
|
|
27
|
+
* @param {Function} expand - node function
|
|
28
|
+
* @returns {object} patched node
|
|
29
|
+
* @example
|
|
30
|
+
* revise({ id: 'electrical' }, { id: 'electrical' }, {}, () => { return []; }, () => { return {}; });
|
|
31
|
+
*/
|
|
32
|
+
function revise(node, entry, templates, apply, expand) {
|
|
33
|
+
const extra = {};
|
|
34
|
+
Object.keys(entry).forEach((key) => {
|
|
35
|
+
if (key !== 'enabled' && key !== 'id' && key !== 'nodes' && key !== 'template') {
|
|
36
|
+
extra[key] = entry[key];
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
const merged = { ...node, ...extra, id: node.id };
|
|
40
|
+
if (entry.nodes) {
|
|
41
|
+
merged.nodes = apply(node.nodes || [], entry.nodes, templates, expand);
|
|
42
|
+
}
|
|
43
|
+
if (!merged.nodes || merged.nodes.length === 0) {
|
|
44
|
+
delete merged.nodes;
|
|
45
|
+
}
|
|
46
|
+
return merged;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Applies one patch entry to an expanded child list.
|
|
51
|
+
*
|
|
52
|
+
* @param {object[]} current - children so far
|
|
53
|
+
* @param {object} entry - one patch entry
|
|
54
|
+
* @param {object} templates - reusable node lists
|
|
55
|
+
* @param {Function} apply - patch function
|
|
56
|
+
* @param {Function} expand - node function
|
|
57
|
+
* @returns {object[]} next children
|
|
58
|
+
* @example
|
|
59
|
+
* mergeOne([{ id: 'step' }], { enabled: false, id: 'step' }, {}, () => { return []; }, () => { return {}; });
|
|
60
|
+
*/
|
|
61
|
+
function mergeOne(current, entry, templates, apply, expand) {
|
|
62
|
+
const index = current.findIndex((node) => {
|
|
63
|
+
return node.id === entry.id;
|
|
64
|
+
});
|
|
65
|
+
if (index < 0) {
|
|
66
|
+
if (entry.enabled === false) {
|
|
67
|
+
return current;
|
|
68
|
+
}
|
|
69
|
+
return current.concat([expand(entry, templates)]);
|
|
70
|
+
}
|
|
71
|
+
if (entry.enabled === false) {
|
|
72
|
+
return current.filter((node) => {
|
|
73
|
+
return node.id !== entry.id;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const next = current.slice();
|
|
77
|
+
next[index] = revise(current[index], entry, templates, apply, expand);
|
|
78
|
+
return next;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Patches a template child list.
|
|
83
|
+
*
|
|
84
|
+
* An id the template lacks is added. An id the patch disables is removed.
|
|
85
|
+
* A staying entry may patch those children the same way.
|
|
86
|
+
*
|
|
87
|
+
* @param {object[]} seeded - template children
|
|
88
|
+
* @param {object[]} patch - node patch
|
|
89
|
+
* @param {object} templates - reusable node lists
|
|
90
|
+
* @param {Function} expand - node function
|
|
91
|
+
* @returns {object[]} patched children
|
|
92
|
+
* @example
|
|
93
|
+
* applyPatch([{ id: 'step' }], [{ enabled: false, id: 'step' }], {}, () => { return {}; });
|
|
94
|
+
*/
|
|
95
|
+
function applyPatch(seeded, patch, templates, expand) {
|
|
96
|
+
repeated(patch);
|
|
97
|
+
return patch.reduce((current, entry) => {
|
|
98
|
+
return mergeOne(current, entry, templates, applyPatch, expand);
|
|
99
|
+
}, seeded);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Expands the children of one namespace node.
|
|
104
|
+
*
|
|
105
|
+
* @param {object} node - namespace node
|
|
106
|
+
* @param {object} templates - reusable node lists
|
|
107
|
+
* @param {Function} expand - node function
|
|
108
|
+
* @returns {object[]} expanded children
|
|
109
|
+
* @example
|
|
110
|
+
* childrenOf({ id: 'unit', template: 'unit' }, { unit: [] }, () => { return {}; });
|
|
111
|
+
*/
|
|
112
|
+
function childrenOf(node, templates, expand) {
|
|
113
|
+
if (!node.template) {
|
|
114
|
+
return (node.nodes || []).map((child) => {
|
|
115
|
+
return expand(child, templates);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const base = templates[node.template];
|
|
119
|
+
if (!Array.isArray(base)) {
|
|
120
|
+
throw new Error(`system document has no template ${node.template}`);
|
|
121
|
+
}
|
|
122
|
+
const seeded = base.map((child) => {
|
|
123
|
+
return expand(child, templates);
|
|
124
|
+
});
|
|
125
|
+
return applyPatch(seeded, node.nodes || [], templates, expand);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Expands one namespace node, applying its template and patch.
|
|
130
|
+
*
|
|
131
|
+
* @param {object} node - namespace node
|
|
132
|
+
* @param {object} templates - reusable node lists
|
|
133
|
+
* @returns {object} expanded node
|
|
134
|
+
* @example
|
|
135
|
+
* expandNode({ id: 'mill' }, {});
|
|
136
|
+
*/
|
|
137
|
+
function expandNode(node, templates) {
|
|
138
|
+
if (!node || typeof node.id !== 'string' || node.id.length === 0) {
|
|
139
|
+
throw new Error('system document namespace id is empty');
|
|
140
|
+
}
|
|
141
|
+
const nodes = childrenOf(node, templates, expandNode);
|
|
142
|
+
const copy = { id: node.id };
|
|
143
|
+
Object.keys(node).forEach((key) => {
|
|
144
|
+
if (key !== 'enabled' && key !== 'id' && key !== 'nodes' && key !== 'template') {
|
|
145
|
+
copy[key] = node[key];
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
if (nodes.length > 0) {
|
|
149
|
+
copy.nodes = nodes;
|
|
150
|
+
}
|
|
151
|
+
return copy;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Expands a namespace tree.
|
|
156
|
+
*
|
|
157
|
+
* @param {object[]|undefined} nodes - root nodes
|
|
158
|
+
* @param {object} templates - reusable node lists
|
|
159
|
+
* @returns {object[]} expanded roots
|
|
160
|
+
* @example
|
|
161
|
+
* expandTree([{ id: 'mill' }], {});
|
|
162
|
+
*/
|
|
163
|
+
export function expandTree(nodes, templates) {
|
|
164
|
+
return (nodes || []).map((node) => {
|
|
165
|
+
return expandNode(node, templates);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Lists every node path from the root.
|
|
171
|
+
*
|
|
172
|
+
* @param {object[]|undefined} nodes - sibling nodes
|
|
173
|
+
* @param {string} [prefix] - parent path
|
|
174
|
+
* @returns {string[]} paths
|
|
175
|
+
* @example
|
|
176
|
+
* treePaths([{ id: 'mill', nodes: [{ id: 'hall' }] }]);
|
|
177
|
+
*/
|
|
178
|
+
export function treePaths(nodes, prefix = '') {
|
|
179
|
+
const found = [];
|
|
180
|
+
(nodes || []).forEach((node) => {
|
|
181
|
+
const path = prefix.length > 0 ? `${prefix}/${node.id}` : node.id;
|
|
182
|
+
if (path.split('/').some((part) => {
|
|
183
|
+
return part.length === 0;
|
|
184
|
+
})) {
|
|
185
|
+
throw new Error(`system document path ${path} has an empty segment`);
|
|
186
|
+
}
|
|
187
|
+
found.push(path);
|
|
188
|
+
treePaths(node.nodes, path).forEach((child) => {
|
|
189
|
+
found.push(child);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
return found;
|
|
193
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reports whether a token is a device address.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} value - flow source
|
|
5
|
+
* @returns {boolean} true when the token carries a scheme
|
|
6
|
+
* @example
|
|
7
|
+
* isAddress('modbus://192.0.2.10:502');
|
|
8
|
+
*/
|
|
9
|
+
export function isAddress(value) {
|
|
10
|
+
return typeof value === 'string' && value.includes('://');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Rejects an empty path segment.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} path - path or pattern
|
|
17
|
+
* @returns {void}
|
|
18
|
+
* @example
|
|
19
|
+
* assertSegments('mill/hall');
|
|
20
|
+
*/
|
|
21
|
+
function assertSegments(path) {
|
|
22
|
+
if (path.split('/').some((part) => {
|
|
23
|
+
return part.length === 0;
|
|
24
|
+
})) {
|
|
25
|
+
throw new Error(`system document path ${path} has an empty segment`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Lists brace groups in one path.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} text - path text
|
|
33
|
+
* @returns {{body: string, end: number, start: number}[]} groups
|
|
34
|
+
* @example
|
|
35
|
+
* groups('mill/{a,b}');
|
|
36
|
+
*/
|
|
37
|
+
function groups(text) {
|
|
38
|
+
return [...text.matchAll(/\{(?<body>[^{}]*)\}/gu)].map((match) => {
|
|
39
|
+
return {
|
|
40
|
+
body: match.groups.body,
|
|
41
|
+
end: match.index + match[0].length,
|
|
42
|
+
start: match.index
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Splits one brace body into segment alternatives.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} body - text inside braces
|
|
51
|
+
* @returns {string[]} alternatives
|
|
52
|
+
* @example
|
|
53
|
+
* alternatives('east,west');
|
|
54
|
+
*/
|
|
55
|
+
function alternatives(body) {
|
|
56
|
+
const parts = body.split(',').map((part) => {
|
|
57
|
+
return part.trim();
|
|
58
|
+
});
|
|
59
|
+
if (parts.some((part) => {
|
|
60
|
+
return part.length === 0 || part.includes('/');
|
|
61
|
+
})) {
|
|
62
|
+
throw new Error(`system document brace ${body} is not one segment`);
|
|
63
|
+
}
|
|
64
|
+
return parts;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Substitutes the index-th alternative of every brace group.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} text - original path
|
|
71
|
+
* @param {{end: number, start: number}[]} marks - brace groups
|
|
72
|
+
* @param {string[][]} choices - alternatives per group
|
|
73
|
+
* @param {number} index - shared alternative index
|
|
74
|
+
* @returns {string} one concrete path
|
|
75
|
+
* @example
|
|
76
|
+
* fill('mill/{a,b}', [], [], 0);
|
|
77
|
+
*/
|
|
78
|
+
function fill(text, marks, choices, index) {
|
|
79
|
+
let built = '';
|
|
80
|
+
let cursor = 0;
|
|
81
|
+
marks.forEach((group, place) => {
|
|
82
|
+
built += text.slice(cursor, group.start);
|
|
83
|
+
built += choices[place][index];
|
|
84
|
+
cursor = group.end;
|
|
85
|
+
});
|
|
86
|
+
return built + text.slice(cursor);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Builds one zipped row per shared brace index.
|
|
91
|
+
*
|
|
92
|
+
* @param {string} text - original path
|
|
93
|
+
* @param {{body: string, end: number, start: number}[]} marks - brace groups
|
|
94
|
+
* @returns {string[]} concrete paths
|
|
95
|
+
* @example
|
|
96
|
+
* rowsFrom('mill/{a,b}/{c,d}', []);
|
|
97
|
+
*/
|
|
98
|
+
function rowsFrom(text, marks) {
|
|
99
|
+
const choices = marks.map((group) => {
|
|
100
|
+
return alternatives(group.body);
|
|
101
|
+
});
|
|
102
|
+
const count = choices[0].length;
|
|
103
|
+
if (choices.some((choice) => {
|
|
104
|
+
return choice.length !== count;
|
|
105
|
+
})) {
|
|
106
|
+
throw new Error(`system document braces in ${text} differ in length`);
|
|
107
|
+
}
|
|
108
|
+
const rows = [];
|
|
109
|
+
for (let index = 0; index < count; index += 1) {
|
|
110
|
+
rows.push(fill(text, marks, choices, index));
|
|
111
|
+
}
|
|
112
|
+
return rows;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Expands `{a,b}` groups in one path together.
|
|
117
|
+
*
|
|
118
|
+
* A fixed string is returned as a single-element list. Brace groups share an
|
|
119
|
+
* index, so `{a,b}/{c,d}` becomes `a/c` and `b/d`.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} text - path, server id, or address
|
|
122
|
+
* @returns {string[]} expanded texts
|
|
123
|
+
* @example
|
|
124
|
+
* zipped('mill/{east,west}/{alpha,beta}');
|
|
125
|
+
*/
|
|
126
|
+
export function zipped(text) {
|
|
127
|
+
const marks = groups(text);
|
|
128
|
+
if (marks.length === 0) {
|
|
129
|
+
return [text];
|
|
130
|
+
}
|
|
131
|
+
return rowsFrom(text, marks);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Reports whether a value matches a star pattern.
|
|
136
|
+
*
|
|
137
|
+
* `*` matches any remainder, including `/`.
|
|
138
|
+
*
|
|
139
|
+
* @param {string} pattern - pattern that may contain `*`
|
|
140
|
+
* @param {string} value - concrete text
|
|
141
|
+
* @returns {boolean} true when the value fits
|
|
142
|
+
* @example
|
|
143
|
+
* fits('mill/*', 'mill/hall/unit');
|
|
144
|
+
*/
|
|
145
|
+
export function fits(pattern, value) {
|
|
146
|
+
const body = pattern.split('*').map((part) => {
|
|
147
|
+
return part.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
148
|
+
}).join('.*');
|
|
149
|
+
return new RegExp(`^${body}$`, 'u').test(value);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Collects pool entries that fit one concrete pattern.
|
|
154
|
+
*
|
|
155
|
+
* @param {string} item - pattern without braces
|
|
156
|
+
* @param {string[]} pool - existing values
|
|
157
|
+
* @param {string} noun - path or server
|
|
158
|
+
* @param {boolean} strict - whether a miss is an error
|
|
159
|
+
* @param {string[]} matched - accumulator
|
|
160
|
+
* @returns {void}
|
|
161
|
+
* @example
|
|
162
|
+
* collect('mill/*', ['mill/hall'], 'path', true, []);
|
|
163
|
+
*/
|
|
164
|
+
function collect(item, pool, noun, strict, matched) {
|
|
165
|
+
if (item.includes('*')) {
|
|
166
|
+
pool.forEach((entry) => {
|
|
167
|
+
if (fits(item, entry) && !matched.includes(entry)) {
|
|
168
|
+
matched.push(entry);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (!pool.includes(item)) {
|
|
174
|
+
if (strict) {
|
|
175
|
+
throw new Error(`system document has no ${noun} ${item}`);
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (!matched.includes(item)) {
|
|
180
|
+
matched.push(item);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Expands braces and `*` against values that already exist.
|
|
186
|
+
*
|
|
187
|
+
* @param {string} pattern - path or server pattern
|
|
188
|
+
* @param {string[]} pool - existing values
|
|
189
|
+
* @param {string} noun - path or server
|
|
190
|
+
* @param {boolean} strict - whether a miss is an error
|
|
191
|
+
* @returns {string[]} matches
|
|
192
|
+
* @example
|
|
193
|
+
* picked('mill/*', ['mill/hall/unit'], 'path', true);
|
|
194
|
+
*/
|
|
195
|
+
export function picked(pattern, pool, noun, strict) {
|
|
196
|
+
assertSegments(pattern);
|
|
197
|
+
const concrete = zipped(pattern);
|
|
198
|
+
const matched = [];
|
|
199
|
+
concrete.forEach((item) => {
|
|
200
|
+
assertSegments(item);
|
|
201
|
+
collect(item, pool, noun, strict, matched);
|
|
202
|
+
});
|
|
203
|
+
if (matched.length === 0 && strict) {
|
|
204
|
+
throw new Error(`system document has no ${noun} ${pattern}`);
|
|
205
|
+
}
|
|
206
|
+
return matched;
|
|
207
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overlays a child profile on its parent.
|
|
3
|
+
*
|
|
4
|
+
* The child keeps the parent's modules, command, variables, and apps.
|
|
5
|
+
* A child app id replaces that one app. A child command replaces the command.
|
|
6
|
+
*
|
|
7
|
+
* @param {object} parent - resolved parent profile
|
|
8
|
+
* @param {object} child - extending profile
|
|
9
|
+
* @returns {object} merged profile
|
|
10
|
+
* @example
|
|
11
|
+
* overlay({ command: ['node'] }, { extends: 'cell' });
|
|
12
|
+
*/
|
|
13
|
+
function overlay(parent, child) {
|
|
14
|
+
const merged = {
|
|
15
|
+
...parent,
|
|
16
|
+
...child,
|
|
17
|
+
apps: { ...(parent.apps || {}), ...(child.apps || {}) },
|
|
18
|
+
modules: { ...(parent.modules || {}), ...(child.modules || {}) },
|
|
19
|
+
variables: { ...(parent.variables || {}), ...(child.variables || {}) }
|
|
20
|
+
};
|
|
21
|
+
delete merged.extends;
|
|
22
|
+
return merged;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolves one profile, following `extends`.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} name - profile id
|
|
29
|
+
* @param {object} profiles - declared profiles
|
|
30
|
+
* @param {object} done - profiles already resolved
|
|
31
|
+
* @param {string[]} stack - profiles on the current chain
|
|
32
|
+
* @returns {object} resolved profile
|
|
33
|
+
* @example
|
|
34
|
+
* resolve('cell', { cell: {} }, {}, []);
|
|
35
|
+
*/
|
|
36
|
+
function resolve(name, profiles, done, stack) {
|
|
37
|
+
if (Object.hasOwn(done, name)) {
|
|
38
|
+
return done[name];
|
|
39
|
+
}
|
|
40
|
+
const profile = profiles[name];
|
|
41
|
+
if (!profile) {
|
|
42
|
+
throw new Error(`system document has no profile ${name}`);
|
|
43
|
+
}
|
|
44
|
+
if (stack.includes(name)) {
|
|
45
|
+
throw new Error(`system document profile ${name} extends itself`);
|
|
46
|
+
}
|
|
47
|
+
if (!profile.extends) {
|
|
48
|
+
done[name] = { ...profile };
|
|
49
|
+
return done[name];
|
|
50
|
+
}
|
|
51
|
+
const parent = resolve(profile.extends, profiles, done, stack.concat(name));
|
|
52
|
+
done[name] = overlay(parent, profile);
|
|
53
|
+
return done[name];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Merges every profile with the profile it extends.
|
|
58
|
+
*
|
|
59
|
+
* @param {object} profiles - declared profiles
|
|
60
|
+
* @returns {object} profiles with `extends` applied
|
|
61
|
+
* @example
|
|
62
|
+
* expandProfiles({ cell: {}, external: { extends: 'cell' } });
|
|
63
|
+
*/
|
|
64
|
+
export function expandProfiles(profiles) {
|
|
65
|
+
const done = {};
|
|
66
|
+
Object.keys(profiles).forEach((name) => {
|
|
67
|
+
resolve(name, profiles, done, []);
|
|
68
|
+
});
|
|
69
|
+
return done;
|
|
70
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep-freezes a document value.
|
|
3
|
+
*
|
|
4
|
+
* @param {unknown} value - any JSON-like value
|
|
5
|
+
* @returns {unknown} frozen value
|
|
6
|
+
* @example
|
|
7
|
+
* frozen({ id: 'mill' });
|
|
8
|
+
*/
|
|
9
|
+
export default function frozen(value) {
|
|
10
|
+
if (value === null || typeof value !== 'object' || Object.isFrozen(value)) {
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
if (Array.isArray(value)) {
|
|
14
|
+
return Object.freeze(value.map((item) => {
|
|
15
|
+
return frozen(item);
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
const copy = {};
|
|
19
|
+
Object.keys(value).forEach((key) => {
|
|
20
|
+
copy[key] = frozen(value[key]);
|
|
21
|
+
});
|
|
22
|
+
return Object.freeze(copy);
|
|
23
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reports whether a value is a plain map.
|
|
3
|
+
*
|
|
4
|
+
* @param {*} value - candidate
|
|
5
|
+
* @returns {boolean} true for a non-null object that is not an array
|
|
6
|
+
* @example
|
|
7
|
+
* isMap({ id: 'cell' });
|
|
8
|
+
*/
|
|
9
|
+
function isMap(value) {
|
|
10
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Deep-merges two maps. A null value removes that key.
|
|
15
|
+
*
|
|
16
|
+
* @param {object|undefined} left - base map
|
|
17
|
+
* @param {object|undefined} right - overlay
|
|
18
|
+
* @returns {object} merged map
|
|
19
|
+
* @example
|
|
20
|
+
* mergeMaps({ secret: 'pg' }, { image: 'plant:1' });
|
|
21
|
+
*/
|
|
22
|
+
function mergeMaps(left, right) {
|
|
23
|
+
const value = { ...(left || {}) };
|
|
24
|
+
Object.keys(right || {}).forEach((key) => {
|
|
25
|
+
const next = right[key];
|
|
26
|
+
if (next === null) {
|
|
27
|
+
delete value[key];
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (isMap(value[key]) && isMap(next)) {
|
|
31
|
+
value[key] = mergeMaps(value[key], next);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
value[key] = next;
|
|
35
|
+
});
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Merges namespace trees by node id.
|
|
41
|
+
*
|
|
42
|
+
* @param {object[]|undefined} left - base nodes
|
|
43
|
+
* @param {object[]|undefined} right - overlay nodes
|
|
44
|
+
* @returns {object[]} merged nodes
|
|
45
|
+
* @example
|
|
46
|
+
* mergeNodes([{ id: 'mill', nodes: [{ id: 'a' }] }], [{ id: 'mill', nodes: [{ id: 'b' }] }]);
|
|
47
|
+
*/
|
|
48
|
+
function mergeNodes(left, right) {
|
|
49
|
+
const value = new Map();
|
|
50
|
+
(left || []).forEach((node) => {
|
|
51
|
+
value.set(node.id, node);
|
|
52
|
+
});
|
|
53
|
+
(right || []).forEach((node) => {
|
|
54
|
+
const prior = value.get(node.id);
|
|
55
|
+
if (!prior) {
|
|
56
|
+
value.set(node.id, node);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const next = { ...prior, ...node };
|
|
60
|
+
const nodes = mergeNodes(prior.nodes, node.nodes);
|
|
61
|
+
if (nodes.length > 0) {
|
|
62
|
+
next.nodes = nodes;
|
|
63
|
+
} else {
|
|
64
|
+
delete next.nodes;
|
|
65
|
+
}
|
|
66
|
+
value.set(node.id, next);
|
|
67
|
+
});
|
|
68
|
+
return Array.from(value.values());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Overlays one system document on another.
|
|
73
|
+
*
|
|
74
|
+
* Maps and namespace trees merge. Listed servers replace. A `null`
|
|
75
|
+
* app drops that app. Omitted keys stay from the catalog.
|
|
76
|
+
*
|
|
77
|
+
* @param {object} base - catalog document
|
|
78
|
+
* @param {object} extra - contour overlay
|
|
79
|
+
* @returns {object} combined document
|
|
80
|
+
* @example
|
|
81
|
+
* mergeDocuments({ servers: [{ id: 'cell' }] }, { servers: [{ id: 'lab' }] });
|
|
82
|
+
*/
|
|
83
|
+
export default function mergeDocuments(base, extra) {
|
|
84
|
+
const document = { ...base, ...extra };
|
|
85
|
+
if (base.templates || extra.templates) {
|
|
86
|
+
document.templates = { ...(base.templates || {}), ...(extra.templates || {}) };
|
|
87
|
+
}
|
|
88
|
+
if (base.mappings || extra.mappings) {
|
|
89
|
+
document.mappings = mergeMaps(base.mappings, extra.mappings);
|
|
90
|
+
}
|
|
91
|
+
if (base.profiles || extra.profiles) {
|
|
92
|
+
document.profiles = mergeMaps(base.profiles, extra.profiles);
|
|
93
|
+
}
|
|
94
|
+
if (base.namespace || extra.namespace) {
|
|
95
|
+
document.namespace = mergeNodes(base.namespace, extra.namespace);
|
|
96
|
+
}
|
|
97
|
+
return document;
|
|
98
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Joins a child id onto a parent path.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} prefix - parent path
|
|
5
|
+
* @param {string} id - child id
|
|
6
|
+
* @returns {string} path
|
|
7
|
+
* @example
|
|
8
|
+
* joined('mill', 'unit');
|
|
9
|
+
*/
|
|
10
|
+
function joined(prefix, id) {
|
|
11
|
+
if (prefix.length === 0) {
|
|
12
|
+
return id;
|
|
13
|
+
}
|
|
14
|
+
return `${prefix}/${id}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Patch entries the template does not already name.
|
|
19
|
+
*
|
|
20
|
+
* @param {object[]} base - template children
|
|
21
|
+
* @param {object[]|undefined} patch - node patch
|
|
22
|
+
* @returns {object[]} added nodes
|
|
23
|
+
* @example
|
|
24
|
+
* extras([{ id: 'voltage' }], [{ id: 'rotation_on' }]);
|
|
25
|
+
*/
|
|
26
|
+
function extras(base, patch) {
|
|
27
|
+
return (patch || []).filter((entry) => {
|
|
28
|
+
return base.every((child) => {
|
|
29
|
+
return child.id !== entry.id;
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Children of one namespace node, expanding its template.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} node - namespace node
|
|
38
|
+
* @param {object} templates - reusable node lists
|
|
39
|
+
* @returns {object[]} children
|
|
40
|
+
* @example
|
|
41
|
+
* children({ id: 'unit', template: 'furnace' }, { furnace: [] });
|
|
42
|
+
*/
|
|
43
|
+
function children(node, templates) {
|
|
44
|
+
if (!node.template) {
|
|
45
|
+
return node.nodes || [];
|
|
46
|
+
}
|
|
47
|
+
const base = templates[node.template];
|
|
48
|
+
if (!Array.isArray(base)) {
|
|
49
|
+
throw new Error(`system document has no template ${node.template}`);
|
|
50
|
+
}
|
|
51
|
+
return base.concat(extras(base, node.nodes));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Walks a namespace and records every path.
|
|
56
|
+
*
|
|
57
|
+
* @param {object[]|undefined} nodes - sibling nodes
|
|
58
|
+
* @param {string} prefix - parent path
|
|
59
|
+
* @param {object} templates - reusable node lists
|
|
60
|
+
* @param {string[]} found - accumulator
|
|
61
|
+
* @returns {void}
|
|
62
|
+
* @example
|
|
63
|
+
* walk([{ id: 'mill' }], '', {}, []);
|
|
64
|
+
*/
|
|
65
|
+
function walk(nodes, prefix, templates, found) {
|
|
66
|
+
(nodes || []).forEach((node) => {
|
|
67
|
+
const next = joined(prefix, node.id);
|
|
68
|
+
found.push(next);
|
|
69
|
+
walk(children(node, templates), next, templates, found);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Lists namespace paths, expanding templates and added patch ids.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* paths({ namespace: [{ id: 'mill' }], templates: {} });
|
|
78
|
+
*
|
|
79
|
+
* @param {object} document - system document
|
|
80
|
+
* @returns {string[]} paths from the root
|
|
81
|
+
*/
|
|
82
|
+
export default function paths(document) {
|
|
83
|
+
const found = [];
|
|
84
|
+
walk(document.namespace, '', document.templates || {}, found);
|
|
85
|
+
return found;
|
|
86
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document path of each device point in a map.
|
|
3
|
+
*
|
|
4
|
+
* A point map key is the parameter. The published name is the flow path
|
|
5
|
+
* plus that parameter. The device point stays the map value.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* pointPaths('mill/electrical', { voltage: 'holding:4000:cdab' });
|
|
9
|
+
*
|
|
10
|
+
* @param {string} name - flow path
|
|
11
|
+
* @param {object} mapping - parameter id to device point
|
|
12
|
+
* @returns {object} device point to document path
|
|
13
|
+
*/
|
|
14
|
+
export default function pointPaths(name, mapping) {
|
|
15
|
+
const paths = {};
|
|
16
|
+
Object.keys(mapping || {}).forEach((key) => {
|
|
17
|
+
paths[String(mapping[key])] = `${name}/${key}`;
|
|
18
|
+
});
|
|
19
|
+
return paths;
|
|
20
|
+
}
|