@yarkivaev/scada 2.3.53 → 2.3.54
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/package.json
CHANGED
|
@@ -12,12 +12,16 @@ import simulationRoute from '../infrastructure/http/plant/routes/simulationRoute
|
|
|
12
12
|
import catalogRoute from '../infrastructure/http/plant/routes/catalogRoute.js';
|
|
13
13
|
import { routes } from '@yarkivaev/simple-server';
|
|
14
14
|
|
|
15
|
+
function pass(_id, rows) {
|
|
16
|
+
return rows;
|
|
17
|
+
}
|
|
18
|
+
|
|
15
19
|
/**
|
|
16
20
|
* Composable plant HTTP API factory.
|
|
17
21
|
*
|
|
18
22
|
* @param {string} basePath - base URL path
|
|
19
23
|
* @param {object} plant - plant domain object with operations (optional kindSources at construction)
|
|
20
|
-
* @param {object} [config] - clock, extraRoutes, requestTimeoutMs, heartbeat
|
|
24
|
+
* @param {object} [config] - clock, extraRoutes, requestTimeoutMs, heartbeat, decorateTimeline
|
|
21
25
|
* @returns {object} routes with list() and handle()
|
|
22
26
|
*
|
|
23
27
|
* @example
|
|
@@ -30,6 +34,7 @@ export default function plantApi(basePath, plant, config) {
|
|
|
30
34
|
return new Date();
|
|
31
35
|
});
|
|
32
36
|
const extra = opts.extraRoutes || [];
|
|
37
|
+
const decorate = opts.decorateTimeline || pass;
|
|
33
38
|
const routeList = [
|
|
34
39
|
...catalogRoute(basePath, opts.tagCatalog),
|
|
35
40
|
...machineRoute(basePath, plant),
|
|
@@ -37,8 +42,8 @@ export default function plantApi(basePath, plant, config) {
|
|
|
37
42
|
...measurementRoute(basePath, plant, time),
|
|
38
43
|
...alertStream(basePath, plant, time),
|
|
39
44
|
...alertRoute(basePath, plant),
|
|
40
|
-
...timelineRoute(basePath, plant, opts.timelineOperator),
|
|
41
|
-
...timelineStream(basePath, plant, time),
|
|
45
|
+
...timelineRoute(basePath, plant, opts.timelineOperator, decorate),
|
|
46
|
+
...timelineStream(basePath, plant, time, decorate),
|
|
42
47
|
...operationRoute(basePath, plant, opts.timelineOperator, opts.operationDecisions, opts.owners),
|
|
43
48
|
...operationStream(basePath, plant, time),
|
|
44
49
|
...heartbeatStream(basePath, time, opts.heartbeat),
|
|
@@ -72,7 +72,7 @@ async function initStomp(stomp, translations, requirePool) {
|
|
|
72
72
|
* Multi-kind operations come from plant.operations built with kindSources
|
|
73
73
|
* (e.g. plantOperations(persistence, { temp })).
|
|
74
74
|
*
|
|
75
|
-
* @param {object} config - port, basePath, plantFactory, extraRoutes, translations, stomp, requirePool
|
|
75
|
+
* @param {object} config - port, basePath, plantFactory, extraRoutes, translations, stomp, requirePool, decorateTimeline
|
|
76
76
|
* @returns {Promise<object>} server, plant, api, segments
|
|
77
77
|
*
|
|
78
78
|
* @example
|
|
@@ -95,7 +95,8 @@ export default async function plantServer(config) {
|
|
|
95
95
|
extraRoutes: extra,
|
|
96
96
|
timelineOperator: config.timelineOperator,
|
|
97
97
|
operationDecisions: config.operationDecisions,
|
|
98
|
-
owners: config.owners
|
|
98
|
+
owners: config.owners,
|
|
99
|
+
decorateTimeline: config.decorateTimeline
|
|
99
100
|
});
|
|
100
101
|
const server = http.createServer((req, res) => {
|
|
101
102
|
return api.handle(req, res);
|
|
@@ -142,7 +142,7 @@ function siteExtraRoutes(catalog, extraRoutes) {
|
|
|
142
142
|
/**
|
|
143
143
|
* Unified site process: supervisor-sink HTTP, plant API, and optional MQTT ingest.
|
|
144
144
|
*
|
|
145
|
-
* @param {object} config - port, basePath, translations, requirePool, plantFactory, extraRoutes, operatorCatalog, kindSources, streams, env
|
|
145
|
+
* @param {object} config - port, basePath, translations, requirePool, plantFactory, extraRoutes, operatorCatalog, kindSources, streams, env, decorateTimeline
|
|
146
146
|
* @returns {Promise<object>} sink, plant, mqtt pipeline
|
|
147
147
|
*
|
|
148
148
|
* @example
|
|
@@ -185,7 +185,8 @@ export default async function siteServer(config) {
|
|
|
185
185
|
extraRoutes: siteExtraRoutes(catalog, config.extraRoutes),
|
|
186
186
|
timelineOperator: timelineOperatorFromEnv(catalog, env, config),
|
|
187
187
|
operationDecisions: catalog.decisions,
|
|
188
|
-
owners: config.owners
|
|
188
|
+
owners: config.owners,
|
|
189
|
+
decorateTimeline: config.decorateTimeline
|
|
189
190
|
});
|
|
190
191
|
return { sink, plant, mqtt, telemetry, operationSync, operatorsSync: catalog.sync };
|
|
191
192
|
}
|
|
@@ -9,12 +9,16 @@ async function handleOperatorWrite(gate, parsed, write) {
|
|
|
9
9
|
await write(audit);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
async function rejectUnknownTags(timeline, parsed, machineId, res) {
|
|
12
|
+
async function rejectUnknownTags(timeline, parsed, machineId, res, decorate) {
|
|
13
13
|
if (typeof timeline.rowAt !== 'function') {
|
|
14
14
|
return false;
|
|
15
15
|
}
|
|
16
16
|
const row = await timeline.rowAt(new Date(parsed.start));
|
|
17
|
-
if (!row
|
|
17
|
+
if (!row) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const [gated] = await decorate(machineId, [row]);
|
|
21
|
+
if (allowedSegmentTags(gated.options, gated.tags, parsed.tags)) {
|
|
18
22
|
return false;
|
|
19
23
|
}
|
|
20
24
|
errorResponse('BAD_REQUEST', `Tag is not in segment options for ${machineId}`, 400).send(res);
|
|
@@ -41,12 +45,13 @@ async function respondToRequest(gate, timeline, requestId, req, res) {
|
|
|
41
45
|
* @param {string} basePath - base URL path
|
|
42
46
|
* @param {object} plant - plant domain object
|
|
43
47
|
* @param {object} [operatorOptions] - provider, requireOperator, defaultUser
|
|
48
|
+
* @param {function} decorate - (machineId, rows) => rows, applied before JSON and PATCH gate
|
|
44
49
|
* @returns {array} route objects
|
|
45
50
|
*
|
|
46
51
|
* @example
|
|
47
52
|
* timelineRoute('/api/v1', plant, { provider, requireOperator: true, defaultUser: 'hmi-kiosk' });
|
|
48
53
|
*/
|
|
49
|
-
export default function timelineRoute(basePath, plant, operatorOptions) {
|
|
54
|
+
export default function timelineRoute(basePath, plant, operatorOptions, decorate) {
|
|
50
55
|
const gate = timelineOperator(operatorOptions);
|
|
51
56
|
return [
|
|
52
57
|
route('GET', `${basePath}/machines/:machineId/segments`, async (req, res, params, query) => {
|
|
@@ -62,7 +67,7 @@ export default function timelineRoute(basePath, plant, operatorOptions) {
|
|
|
62
67
|
if (query.to) {
|
|
63
68
|
options.to = query.to;
|
|
64
69
|
}
|
|
65
|
-
const rows = await result.machine.timeline.list(options);
|
|
70
|
+
const rows = await decorate(params.machineId, await result.machine.timeline.list(options));
|
|
66
71
|
jsonResponse({ items: rows.map(segmentJson) }).send(res);
|
|
67
72
|
}),
|
|
68
73
|
route('PATCH', `${basePath}/machines/:machineId/segments`, async (req, res, params) => {
|
|
@@ -74,7 +79,7 @@ export default function timelineRoute(basePath, plant, operatorOptions) {
|
|
|
74
79
|
try {
|
|
75
80
|
const raw = await readBody(req);
|
|
76
81
|
const parsed = JSON.parse(raw);
|
|
77
|
-
if (await rejectUnknownTags(result.machine.timeline, parsed, params.machineId, res)) {
|
|
82
|
+
if (await rejectUnknownTags(result.machine.timeline, parsed, params.machineId, res, decorate)) {
|
|
78
83
|
return;
|
|
79
84
|
}
|
|
80
85
|
await handleOperatorWrite(gate, parsed, async (audit) => {
|
|
@@ -94,7 +99,7 @@ export default function timelineRoute(basePath, plant, operatorOptions) {
|
|
|
94
99
|
jsonResponse({ items: [] }).send(res);
|
|
95
100
|
return;
|
|
96
101
|
}
|
|
97
|
-
const rows = await result.machine.timeline.pending();
|
|
102
|
+
const rows = await decorate(params.machineId, await result.machine.timeline.pending());
|
|
98
103
|
const items = rows.map(({ id, name, start_time: startTime, end_time: endTime, duration, options }) => {
|
|
99
104
|
return {
|
|
100
105
|
id,
|
|
@@ -22,18 +22,53 @@ function segmentPayload(segment) {
|
|
|
22
22
|
return data;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
async function emitSegment(sse, event, decorate, machineId) {
|
|
26
|
+
if (event.type === 'created' && event.segment) {
|
|
27
|
+
const [row] = await decorate(machineId, [event.segment]);
|
|
28
|
+
sse.emit('segment_created', segmentPayload(row));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (event.type === 'resolved' && event.segment) {
|
|
32
|
+
const [row] = await decorate(machineId, [event.segment]);
|
|
33
|
+
sse.emit('segment_resolved', segmentPayload(row));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function emitRequest(sse, event, decorate, machineId) {
|
|
38
|
+
if (event.type === 'created' && event.request) {
|
|
39
|
+
const [row] = await decorate(machineId, [event.request]);
|
|
40
|
+
const start = row.start_time || row.startTime;
|
|
41
|
+
const end = row.end_time || row.endTime;
|
|
42
|
+
sse.emit('request_created', {
|
|
43
|
+
id: row.id,
|
|
44
|
+
segment: {
|
|
45
|
+
name: row.name,
|
|
46
|
+
start: start.toISOString(),
|
|
47
|
+
end: end.toISOString(),
|
|
48
|
+
duration: row.duration
|
|
49
|
+
},
|
|
50
|
+
options: row.options
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (event.type === 'resolved' && event.request) {
|
|
55
|
+
sse.emit('request_resolved', { id: event.request.id });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
25
59
|
/**
|
|
26
60
|
* Timeline SSE routes for segments and label requests.
|
|
27
61
|
*
|
|
28
62
|
* @param {string} basePath - base URL path
|
|
29
63
|
* @param {object} plant - plant domain object
|
|
30
64
|
* @param {function} clock - time provider
|
|
65
|
+
* @param {function} decorate - (machineId, rows) => rows
|
|
31
66
|
* @returns {array} route objects
|
|
32
67
|
*
|
|
33
68
|
* @example
|
|
34
69
|
* timelineStream('/api/v1', plant, clock);
|
|
35
70
|
*/
|
|
36
|
-
export default function timelineStream(basePath, plant, clock) {
|
|
71
|
+
export default function timelineStream(basePath, plant, clock, decorate) {
|
|
37
72
|
return [
|
|
38
73
|
route('GET', `${basePath}/machines/:machineId/segments/stream`, (req, res, params) => {
|
|
39
74
|
const sse = sseResponse(res, clock);
|
|
@@ -44,11 +79,7 @@ export default function timelineStream(basePath, plant, clock) {
|
|
|
44
79
|
return;
|
|
45
80
|
}
|
|
46
81
|
const subscription = result.machine.timeline.stream((event) => {
|
|
47
|
-
|
|
48
|
-
sse.emit('segment_created', segmentPayload(event.segment));
|
|
49
|
-
} else if (event.type === 'resolved' && event.segment) {
|
|
50
|
-
sse.emit('segment_resolved', segmentPayload(event.segment));
|
|
51
|
-
}
|
|
82
|
+
return emitSegment(sse, event, decorate, params.machineId);
|
|
52
83
|
});
|
|
53
84
|
const heartbeat = setInterval(() => {
|
|
54
85
|
sse.heartbeat();
|
|
@@ -67,23 +98,7 @@ export default function timelineStream(basePath, plant, clock) {
|
|
|
67
98
|
return;
|
|
68
99
|
}
|
|
69
100
|
const subscription = result.machine.timeline.stream((event) => {
|
|
70
|
-
|
|
71
|
-
const reqItem = event.request;
|
|
72
|
-
const start = reqItem.start_time || reqItem.startTime;
|
|
73
|
-
const end = reqItem.end_time || reqItem.endTime;
|
|
74
|
-
sse.emit('request_created', {
|
|
75
|
-
id: reqItem.id,
|
|
76
|
-
segment: {
|
|
77
|
-
name: reqItem.name,
|
|
78
|
-
start: start.toISOString(),
|
|
79
|
-
end: end.toISOString(),
|
|
80
|
-
duration: reqItem.duration
|
|
81
|
-
},
|
|
82
|
-
options: reqItem.options
|
|
83
|
-
});
|
|
84
|
-
} else if (event.type === 'resolved' && event.request) {
|
|
85
|
-
sse.emit('request_resolved', { id: event.request.id });
|
|
86
|
-
}
|
|
101
|
+
return emitRequest(sse, event, decorate, params.machineId);
|
|
87
102
|
});
|
|
88
103
|
const heartbeat = setInterval(() => {
|
|
89
104
|
sse.heartbeat();
|