@yarkivaev/scada 2.3.85 → 2.3.87
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 +20 -0
- package/index.js +5 -0
- package/package.json +4 -2
- package/src/application/mcp/analyticsMcp.js +376 -0
- package/src/application/mcp/coverageCursor.js +116 -0
- package/src/application/mcp/mcpCoverage.js +95 -0
- package/src/application/mcp/mcpJournal.js +102 -0
- package/src/application/mcp/mcpMetrics.js +69 -0
- package/src/application/mcp/mcpObserve.js +168 -0
- package/src/application/mcp/mcpQuery.js +80 -0
- package/src/application/mcp/mcpReads.js +221 -0
- package/src/application/mcp/mcpResources.js +106 -0
- package/src/application/mcp/mcpText.js +106 -0
- package/src/application/metricsPlant.js +19 -1
- package/src/application/shopWithTimeline.js +15 -6
- package/src/domain/namespace/namespaceTree.js +182 -0
- package/src/domain/plant/plant.js +2 -1
- package/src/domain/span/coverageSpans.js +229 -0
- package/src/domain/sql/sqlGuard.js +175 -0
- package/src/domain/sql/sqlShape.js +16 -0
- package/src/infrastructure/persistence/pg/alerts.js +37 -3
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parent path of a node, or an empty string at the root.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} path - namespace path
|
|
5
|
+
* @returns {string} parent path
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* parentOf('site/shop');
|
|
9
|
+
*/
|
|
10
|
+
function parentOf(path) {
|
|
11
|
+
const cut = path.lastIndexOf('/');
|
|
12
|
+
if (cut < 0) {
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
return path.slice(0, cut);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Topology resource body without source topics.
|
|
20
|
+
*
|
|
21
|
+
* @param {object} tree - namespaceTree
|
|
22
|
+
* @returns {Array<object>} nodes
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* topology(tree);
|
|
26
|
+
*/
|
|
27
|
+
function topology(tree) {
|
|
28
|
+
return tree.list().map((node) => {
|
|
29
|
+
return {
|
|
30
|
+
kind: node.kind,
|
|
31
|
+
liveness: node.silenceSeconds,
|
|
32
|
+
parent: parentOf(node.path),
|
|
33
|
+
path: node.path,
|
|
34
|
+
title: node.title
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Binding resource body: path, source topic, and unit.
|
|
41
|
+
*
|
|
42
|
+
* @param {object} tree - namespaceTree
|
|
43
|
+
* @returns {Array<object>} bindings
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* bindings(tree);
|
|
47
|
+
*/
|
|
48
|
+
function bindings(tree) {
|
|
49
|
+
return tree.list().filter((node) => {
|
|
50
|
+
return typeof node.source === 'string';
|
|
51
|
+
}).map((node) => {
|
|
52
|
+
return {
|
|
53
|
+
path: node.path,
|
|
54
|
+
topic: node.source,
|
|
55
|
+
unit: typeof node.unit === 'string' ? node.unit : ''
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* JSON resource contents for one URI.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} uri - resource URI
|
|
64
|
+
* @param {object} payload - JSON body
|
|
65
|
+
* @returns {object} read result
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* body('plant://topology', []);
|
|
69
|
+
*/
|
|
70
|
+
function body(uri, payload) {
|
|
71
|
+
return {
|
|
72
|
+
contents: [{
|
|
73
|
+
mimeType: 'application/json',
|
|
74
|
+
text: JSON.stringify(payload),
|
|
75
|
+
uri
|
|
76
|
+
}]
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Registers plant topology and source bindings.
|
|
82
|
+
*
|
|
83
|
+
* Topology has paths, kinds, titles, and liveness. Bindings map a path to a
|
|
84
|
+
* topic and a unit. Gateway topics are not part of topology.
|
|
85
|
+
*
|
|
86
|
+
* @param {object} server - McpServer
|
|
87
|
+
* @param {object} tree - namespaceTree
|
|
88
|
+
* @returns {void}
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* registerResources(server, tree);
|
|
92
|
+
*/
|
|
93
|
+
export default function registerResources(server, tree) {
|
|
94
|
+
server.registerResource('topology', 'plant://topology', {
|
|
95
|
+
description: 'Paths, kinds, titles, and liveness',
|
|
96
|
+
mimeType: 'application/json'
|
|
97
|
+
}, () => {
|
|
98
|
+
return body('plant://topology', topology(tree));
|
|
99
|
+
});
|
|
100
|
+
server.registerResource('bindings', 'plant://bindings', {
|
|
101
|
+
description: 'Source topic and unit for each bound path',
|
|
102
|
+
mimeType: 'application/json'
|
|
103
|
+
}, () => {
|
|
104
|
+
return body('plant://bindings', bindings(tree));
|
|
105
|
+
});
|
|
106
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caps a row list at the analytics response limit.
|
|
3
|
+
*
|
|
4
|
+
* @param {Array} rows - port rows
|
|
5
|
+
* @returns {{rows: Array, truncated: boolean}} capped rows
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* capRows([{ id: 1 }]);
|
|
9
|
+
*/
|
|
10
|
+
export function capRows(rows) {
|
|
11
|
+
const list = Array.isArray(rows) ? rows : [];
|
|
12
|
+
return {
|
|
13
|
+
rows: list.slice(0, 200),
|
|
14
|
+
truncated: list.length > 200
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Wraps a payload as MCP text content.
|
|
20
|
+
*
|
|
21
|
+
* @param {object} payload - JSON payload
|
|
22
|
+
* @returns {object} tool result
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* toolText({ rows: [] });
|
|
26
|
+
*/
|
|
27
|
+
export function toolText(payload) {
|
|
28
|
+
return {
|
|
29
|
+
content: [{
|
|
30
|
+
text: JSON.stringify(payload),
|
|
31
|
+
type: 'text'
|
|
32
|
+
}]
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Reads a point list from an array or a points field.
|
|
38
|
+
*
|
|
39
|
+
* @param {Array|object} raw - port payload
|
|
40
|
+
* @returns {Array} points
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* readPoints({ points: [1] });
|
|
44
|
+
*/
|
|
45
|
+
function readPoints(raw) {
|
|
46
|
+
if (Array.isArray(raw)) {
|
|
47
|
+
return raw;
|
|
48
|
+
}
|
|
49
|
+
if (raw && Array.isArray(raw.points)) {
|
|
50
|
+
return raw.points;
|
|
51
|
+
}
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Reads the step the port actually used.
|
|
57
|
+
*
|
|
58
|
+
* @param {Array|object} raw - port payload
|
|
59
|
+
* @param {number} step - requested step seconds
|
|
60
|
+
* @returns {number} step seconds
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* readStep({ points: [], step: 30 }, 15);
|
|
64
|
+
*/
|
|
65
|
+
function readStep(raw, step) {
|
|
66
|
+
if (raw && typeof raw.step === 'number') {
|
|
67
|
+
return raw.step;
|
|
68
|
+
}
|
|
69
|
+
return step;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Keeps a truncated flag supplied by the port.
|
|
74
|
+
*
|
|
75
|
+
* @param {Array|object} raw - port payload
|
|
76
|
+
* @returns {boolean} whether the port already truncated
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* flagged({ truncated: true });
|
|
80
|
+
*/
|
|
81
|
+
function flagged(raw) {
|
|
82
|
+
if (raw && raw.truncated === true) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Caps measurement points at 200 and reports the step.
|
|
90
|
+
*
|
|
91
|
+
* @param {Array|object} raw - port payload
|
|
92
|
+
* @param {number} step - requested step seconds
|
|
93
|
+
* @returns {object} points, step, truncated
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* capMeasurements([{ v: 1 }], 15);
|
|
97
|
+
*/
|
|
98
|
+
export function capMeasurements(raw, step) {
|
|
99
|
+
const points = readPoints(raw);
|
|
100
|
+
const truncated = points.length > 200 || flagged(raw);
|
|
101
|
+
return {
|
|
102
|
+
points: points.slice(0, 200),
|
|
103
|
+
step: readStep(raw, step),
|
|
104
|
+
truncated
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
import plant from '../domain/plant/plant.js';
|
|
2
2
|
import initialized from '../domain/shared/initialized.js';
|
|
3
|
+
import pgAlerts from '../infrastructure/persistence/pg/alerts.js';
|
|
3
4
|
import postgresPool from '../infrastructure/persistence/postgresPool.js';
|
|
4
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Plant extras: injected ports, or pgAlerts when a pool exists.
|
|
8
|
+
*
|
|
9
|
+
* @param {object} options - optional operations and alerts
|
|
10
|
+
* @param {object|undefined} pool - pg pool
|
|
11
|
+
* @returns {object} plant options
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* extras({ operations }, pool);
|
|
15
|
+
*/
|
|
16
|
+
function extras(options, pool) {
|
|
17
|
+
return {
|
|
18
|
+
operations: options.operations,
|
|
19
|
+
alerts: options.alerts || (pool ? pgAlerts(pool) : undefined)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
5
23
|
/**
|
|
6
24
|
* Generic plant factory wiring sensors and a shop factory over a connection.
|
|
7
25
|
*
|
|
@@ -32,5 +50,5 @@ export default async function metricsPlant(machines, options) {
|
|
|
32
50
|
segments: options.segments,
|
|
33
51
|
shopName: options.shopName
|
|
34
52
|
});
|
|
35
|
-
return plant(initialized({ [shop.name()]: shop }, Object.values));
|
|
53
|
+
return plant(initialized({ [shop.name()]: shop }, Object.values), extras(options, pool));
|
|
36
54
|
}
|
|
@@ -73,6 +73,13 @@ function writePort(name, decisions, owners) {
|
|
|
73
73
|
}, owners)(name);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
function demandWrite(name, decisions, owners) {
|
|
77
|
+
if (!decisions) {
|
|
78
|
+
throw new Error(`Timeline write requires userDecisions for ${name}`);
|
|
79
|
+
}
|
|
80
|
+
return writePort(name, decisions, owners);
|
|
81
|
+
}
|
|
82
|
+
|
|
76
83
|
function ownsLocal(owners, name) {
|
|
77
84
|
if (!owners) {
|
|
78
85
|
return true;
|
|
@@ -153,23 +160,23 @@ async function answer(scope, requestId, body, audit) {
|
|
|
153
160
|
}
|
|
154
161
|
|
|
155
162
|
/**
|
|
156
|
-
* Builds a machine timeline from PostgreSQL
|
|
163
|
+
* Builds a machine timeline from PostgreSQL when a pool exists.
|
|
157
164
|
*
|
|
158
|
-
*
|
|
165
|
+
* Reads do not need userDecisions. retag and respond throw without it.
|
|
166
|
+
* Optional owners registry routes those writes to an edge HTTP plant API.
|
|
159
167
|
*
|
|
160
168
|
* @param {string} name - machine id
|
|
161
|
-
* @param {object} options - pool, userDecisions
|
|
169
|
+
* @param {object} options - pool, optional userDecisions and owners
|
|
162
170
|
* @returns {object} timeline port with list, rowAt, pending, stream, retag, respond, bus
|
|
163
171
|
*
|
|
164
172
|
* @example
|
|
165
|
-
* const tl = shopWithTimeline('machine1', { pool
|
|
173
|
+
* const tl = shopWithTimeline('machine1', { pool });
|
|
166
174
|
*/
|
|
167
175
|
export default function shopWithTimeline(name, options) {
|
|
168
176
|
const { pool, userDecisions: decisions, owners, segments } = options;
|
|
169
|
-
if (pool
|
|
177
|
+
if (pool) {
|
|
170
178
|
const bus = pubsub();
|
|
171
179
|
const read = pgTimeline(pool, name);
|
|
172
|
-
const write = writePort(name, decisions, owners);
|
|
173
180
|
const port = timeline(read, bus);
|
|
174
181
|
return {
|
|
175
182
|
list: port.list,
|
|
@@ -179,6 +186,7 @@ export default function shopWithTimeline(name, options) {
|
|
|
179
186
|
stream: port.stream,
|
|
180
187
|
bus,
|
|
181
188
|
async retag(start, tags, properties, audit) {
|
|
189
|
+
const write = demandWrite(name, decisions, owners);
|
|
182
190
|
if (ownsLocal(owners, name)) {
|
|
183
191
|
await confirm(pool, {
|
|
184
192
|
machine: name,
|
|
@@ -192,6 +200,7 @@ export default function shopWithTimeline(name, options) {
|
|
|
192
200
|
bus.emit({ type: 'resolved', segment: resolvedStub(start, tags, properties), audit });
|
|
193
201
|
},
|
|
194
202
|
respond(requestId, body, audit) {
|
|
203
|
+
const write = demandWrite(name, decisions, owners);
|
|
195
204
|
return answer({ pool, owners, name, segments, write, bus }, requestId, body, audit);
|
|
196
205
|
}
|
|
197
206
|
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads a non-empty path.
|
|
3
|
+
*
|
|
4
|
+
* @param {object} node - candidate node
|
|
5
|
+
* @returns {string} path
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* pathOf({ path: 'site/unit' });
|
|
9
|
+
*/
|
|
10
|
+
function pathOf(node) {
|
|
11
|
+
if (typeof node.path === 'string' && node.path.length > 0) {
|
|
12
|
+
return node.path;
|
|
13
|
+
}
|
|
14
|
+
throw new Error('Namespace path is empty');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Requires a non-empty opaque kind.
|
|
19
|
+
*
|
|
20
|
+
* @param {object} node - candidate node
|
|
21
|
+
* @returns {string} kind
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* kindOf({ kind: 'metric', path: 'site/metric' });
|
|
25
|
+
*/
|
|
26
|
+
function kindOf(node) {
|
|
27
|
+
if (typeof node.kind === 'string' && node.kind.length > 0) {
|
|
28
|
+
return node.kind;
|
|
29
|
+
}
|
|
30
|
+
throw new Error(`Namespace kind is missing for ${pathOf(node)}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Requires a non-negative silence threshold in seconds.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} node - candidate node
|
|
37
|
+
* @returns {number} silence seconds
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* silenceOf({ path: 'site', silenceSeconds: 60 });
|
|
41
|
+
*/
|
|
42
|
+
function silenceOf(node) {
|
|
43
|
+
const value = node.silenceSeconds;
|
|
44
|
+
if (typeof value === 'number' && value >= 0) {
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`Namespace silence is missing for ${pathOf(node)}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Uses an explicit title or the last path segment.
|
|
52
|
+
*
|
|
53
|
+
* @param {object} node - candidate node
|
|
54
|
+
* @param {string} path - unique path
|
|
55
|
+
* @returns {string} display title
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* titled({ title: 'Unit' }, 'site/unit');
|
|
59
|
+
*/
|
|
60
|
+
function titled(node, path) {
|
|
61
|
+
if (typeof node.title === 'string' && node.title.length > 0) {
|
|
62
|
+
return node.title;
|
|
63
|
+
}
|
|
64
|
+
const parts = path.split('/');
|
|
65
|
+
return parts[parts.length - 1];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Copies one node into a frozen record.
|
|
70
|
+
*
|
|
71
|
+
* @param {object} node - candidate node
|
|
72
|
+
* @param {string} path - unique path
|
|
73
|
+
* @returns {object} frozen node
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* freezeNode({ kind: 'site', path: 'site', silenceSeconds: 30 }, 'site');
|
|
77
|
+
*/
|
|
78
|
+
function freezeNode(node, path) {
|
|
79
|
+
const frozen = {
|
|
80
|
+
kind: kindOf(node),
|
|
81
|
+
path,
|
|
82
|
+
silenceSeconds: silenceOf(node),
|
|
83
|
+
title: titled(node, path)
|
|
84
|
+
};
|
|
85
|
+
if (typeof node.source === 'string') {
|
|
86
|
+
frozen.source = node.source;
|
|
87
|
+
}
|
|
88
|
+
if (typeof node.unit === 'string') {
|
|
89
|
+
frozen.unit = node.unit;
|
|
90
|
+
}
|
|
91
|
+
return Object.freeze(frozen);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Builds a path map and rejects duplicates.
|
|
96
|
+
*
|
|
97
|
+
* @param {Array<object>} nodes - candidate nodes
|
|
98
|
+
* @returns {{ list: Array<object>, map: Map<string, object> }} index
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* indexNodes([{ kind: 'site', path: 'site', silenceSeconds: 30 }]);
|
|
102
|
+
*/
|
|
103
|
+
function indexNodes(nodes) {
|
|
104
|
+
const map = new Map();
|
|
105
|
+
for (const node of nodes) {
|
|
106
|
+
const path = pathOf(node);
|
|
107
|
+
if (map.has(path)) {
|
|
108
|
+
throw new Error(`Namespace path is duplicated: ${path}`);
|
|
109
|
+
}
|
|
110
|
+
map.set(path, freezeNode(node, path));
|
|
111
|
+
}
|
|
112
|
+
return { list: [...map.values()], map };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Returns the node for a path or throws when it is absent.
|
|
117
|
+
*
|
|
118
|
+
* @param {Map<string, object>} map - path index
|
|
119
|
+
* @param {string} path - requested path
|
|
120
|
+
* @returns {object} frozen node
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* lookup(new Map([['site', { path: 'site' }]]), 'site');
|
|
124
|
+
*/
|
|
125
|
+
function lookup(map, path) {
|
|
126
|
+
const node = map.get(path);
|
|
127
|
+
if (node) {
|
|
128
|
+
return node;
|
|
129
|
+
}
|
|
130
|
+
throw new Error(`Namespace path is missing: ${path}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Requires an array of nodes.
|
|
135
|
+
*
|
|
136
|
+
* @param {Array<object>} nodes - candidate nodes
|
|
137
|
+
* @returns {Array<object>} the same array when it is an array
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* nodesOf([{ kind: 'site', path: 'site', silenceSeconds: 30 }]);
|
|
141
|
+
*/
|
|
142
|
+
function nodesOf(nodes) {
|
|
143
|
+
if (Array.isArray(nodes)) {
|
|
144
|
+
return nodes;
|
|
145
|
+
}
|
|
146
|
+
throw new Error('Namespace nodes are missing');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Indexes plant nodes by a unique path for analytics reads.
|
|
151
|
+
*
|
|
152
|
+
* A node is an opaque kind, a silence threshold in seconds, and an optional
|
|
153
|
+
* source topic. Paths are the address. Gateway topics stay on the node and
|
|
154
|
+
* are not required.
|
|
155
|
+
*
|
|
156
|
+
* @param {Array<object>} nodes - path, kind, silenceSeconds, optional source, title, unit
|
|
157
|
+
* @returns {object} list, resolve, and below
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* const tree = namespaceTree([
|
|
161
|
+
* { path: 'site/shop/unit', kind: 'unit', silenceSeconds: 60 }
|
|
162
|
+
* ]);
|
|
163
|
+
* tree.resolve('site/shop/unit').kind;
|
|
164
|
+
*/
|
|
165
|
+
export default function namespaceTree(nodes) {
|
|
166
|
+
const index = indexNodes(nodesOf(nodes));
|
|
167
|
+
return Object.freeze({
|
|
168
|
+
below(path) {
|
|
169
|
+
const node = lookup(index.map, path);
|
|
170
|
+
const prefix = `${node.path}/`;
|
|
171
|
+
return index.list.filter((item) => {
|
|
172
|
+
return item.path.startsWith(prefix);
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
list() {
|
|
176
|
+
return index.list.slice();
|
|
177
|
+
},
|
|
178
|
+
resolve(path) {
|
|
179
|
+
return lookup(index.map, path);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Provides initialization for all contained shops.
|
|
4
4
|
*
|
|
5
5
|
* @param {object} shops - initialized list of shops
|
|
6
|
-
* @param {object} [options] - operations, topology
|
|
6
|
+
* @param {object} [options] - operations, alerts, topology
|
|
7
7
|
* @returns {object} plant with shops property and init method
|
|
8
8
|
*
|
|
9
9
|
* @example
|
|
@@ -14,6 +14,7 @@ export default function plant(shops, options = {}) {
|
|
|
14
14
|
return {
|
|
15
15
|
shops,
|
|
16
16
|
operations: options.operations,
|
|
17
|
+
alerts: options.alerts,
|
|
17
18
|
topology: options.topology,
|
|
18
19
|
init() {
|
|
19
20
|
shops.init();
|