@yarkivaev/scada 1.4.0 → 2.3.45

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.
Files changed (189) hide show
  1. package/README.md +196 -33
  2. package/db/migrations/C0001__central_drop_metrics.sql +1 -0
  3. package/db/migrations/C0002__central_revoke_delete.sql +12 -0
  4. package/db/migrations/C0003__central_operators.sql +7 -0
  5. package/db/migrations/C0004__central_operations_grants.sql +2 -0
  6. package/db/migrations/C0005__central_operators_grants.sql +1 -0
  7. package/db/migrations/C0006__central_operators_registration.sql +16 -0
  8. package/db/migrations/E0001__edge_metrics.sql +8 -0
  9. package/db/migrations/E0002__edge_retention_delete.sql +8 -0
  10. package/db/migrations/E0003__edge_operations_grants.sql +2 -0
  11. package/db/migrations/V0001__baseline.sql +25 -0
  12. package/db/migrations/V0002__sink_extension_tables.sql +6 -0
  13. package/db/migrations/V0003__operations.sql +10 -0
  14. package/db/migrations/V0004__ingest_checkpoints.sql +6 -0
  15. package/db/migrations/V0005__user_decisions_operator.sql +2 -0
  16. package/index.js +75 -45
  17. package/package.json +28 -8
  18. package/src/application/bindSilentStreams.js +20 -0
  19. package/src/application/edgeOperatorCatalog.js +45 -0
  20. package/src/application/export/exportJob.js +118 -0
  21. package/src/application/export/exportQuery.js +32 -0
  22. package/src/application/export/exportSink.js +25 -0
  23. package/src/application/export/exportStream.js +33 -0
  24. package/src/application/machineInPlant.js +20 -0
  25. package/src/application/metricsPlant.js +35 -0
  26. package/src/application/plantApi.js +49 -0
  27. package/src/application/plantOperations.js +19 -0
  28. package/src/application/plantServer.js +107 -0
  29. package/src/application/shopWithTimeline.js +112 -0
  30. package/src/application/siteOperatorCatalog.js +78 -0
  31. package/src/application/siteServer.js +191 -0
  32. package/src/application/supervisorSink.js +97 -0
  33. package/src/application/timelineOperatorFromEnv.js +19 -0
  34. package/src/bin/supervisor-sink.js +28 -0
  35. package/src/{alert.js → domain/alerting/alert.js} +2 -2
  36. package/src/{alerts.js → domain/alerting/alerts.js} +3 -3
  37. package/src/domain/alerting/ingest.js +18 -0
  38. package/src/domain/ingest/ingestCursor.js +26 -0
  39. package/src/domain/operation/operation.js +23 -0
  40. package/src/domain/operation/operations.js +81 -0
  41. package/src/domain/operator/operator.js +23 -0
  42. package/src/domain/plant/machine.js +32 -0
  43. package/src/domain/plant/plant.js +20 -0
  44. package/src/domain/plant/shop.js +24 -0
  45. package/src/domain/segment/dispatch.js +31 -0
  46. package/src/domain/segment/normalize.js +31 -0
  47. package/src/domain/segment/silenceBudget.js +16 -0
  48. package/src/{initialized.js → domain/shared/initialized.js} +3 -4
  49. package/src/{pubsub.js → domain/shared/pubsub.js} +2 -3
  50. package/src/domain/timeline/timeline.js +25 -0
  51. package/src/infrastructure/catalog/tag-catalog.json +27 -0
  52. package/src/infrastructure/catalog/tagCatalog.js +65 -0
  53. package/src/infrastructure/client/index.js +20 -0
  54. package/src/infrastructure/client/machineClient.js +159 -0
  55. package/src/infrastructure/client/machineOperationsClient.js +159 -0
  56. package/src/infrastructure/client/scadaClient.js +67 -0
  57. package/src/infrastructure/client/sseConnection.js +42 -0
  58. package/src/infrastructure/http/edge/edgeApi.js +54 -0
  59. package/src/infrastructure/http/edge/httpMetricsRead.js +34 -0
  60. package/src/infrastructure/http/edge/metricsSensor.js +15 -0
  61. package/src/infrastructure/http/edge/parseStateHttpTimeoutMs.js +20 -0
  62. package/src/infrastructure/http/edge/routes/checkpoint/checkpointRoutes.js +79 -0
  63. package/src/infrastructure/http/edge/routes/metrics/metricsBatch.js +59 -0
  64. package/src/infrastructure/http/edge/routes/metrics/metricsCurrent.js +29 -0
  65. package/src/infrastructure/http/edge/routes/metrics/metricsPoll.js +25 -0
  66. package/src/infrastructure/http/edge/routes/metrics/metricsRange.js +26 -0
  67. package/src/infrastructure/http/edge/routes/metrics/metricsRoutes.js +21 -0
  68. package/src/infrastructure/http/edge/routes/retention/retentionRoutes.js +41 -0
  69. package/src/infrastructure/http/edge/startTestEdgeApi.js +39 -0
  70. package/src/infrastructure/http/edge/stateAccess.js +13 -0
  71. package/src/infrastructure/http/edge/stateHttpClient.js +106 -0
  72. package/src/infrastructure/http/edge/stateHttpTimeoutError.js +13 -0
  73. package/src/infrastructure/http/plant/json/decisionJson.js +44 -0
  74. package/src/infrastructure/http/plant/json/operationJson.js +28 -0
  75. package/src/infrastructure/http/plant/json/operatorJson.js +22 -0
  76. package/src/infrastructure/http/plant/json/segmentJson.js +27 -0
  77. package/src/infrastructure/http/plant/operationAudit.js +56 -0
  78. package/src/infrastructure/http/plant/routes/alertRoute.js +93 -0
  79. package/src/infrastructure/http/plant/routes/catalogRoute.js +21 -0
  80. package/src/infrastructure/http/plant/routes/decisionRoute.js +60 -0
  81. package/src/infrastructure/http/plant/routes/machineRoute.js +35 -0
  82. package/src/infrastructure/http/plant/routes/measurementRoute.js +60 -0
  83. package/src/infrastructure/http/plant/routes/operationDrafts.js +79 -0
  84. package/src/infrastructure/http/plant/routes/operationRoute.js +80 -0
  85. package/src/infrastructure/http/plant/routes/operationWrites.js +186 -0
  86. package/src/infrastructure/http/plant/routes/operatorRoute.js +143 -0
  87. package/src/infrastructure/http/plant/routes/simulationRoute.js +61 -0
  88. package/src/infrastructure/http/plant/routes/timelineRoute.js +112 -0
  89. package/src/infrastructure/http/plant/streams/alertStream.js +68 -0
  90. package/src/infrastructure/http/plant/streams/heartbeatStream.js +34 -0
  91. package/src/infrastructure/http/plant/streams/measurementStream.js +90 -0
  92. package/src/infrastructure/http/plant/streams/operationStream.js +42 -0
  93. package/src/infrastructure/http/plant/streams/timelineStream.js +97 -0
  94. package/src/infrastructure/http/plant/timelineOperator.js +87 -0
  95. package/src/infrastructure/ingest/activity/activityTracking.js +74 -0
  96. package/src/infrastructure/ingest/activity/activityTransformer.js +45 -0
  97. package/src/infrastructure/ingest/codecs/alertCodec.js +56 -0
  98. package/src/infrastructure/ingest/codecs/segmentCodec.js +30 -0
  99. package/src/infrastructure/ingest/codecs/userDecisionCodec.js +78 -0
  100. package/src/infrastructure/ingest/cooldown.js +24 -0
  101. package/src/infrastructure/ingest/db/migrate.js +78 -0
  102. package/src/infrastructure/ingest/db/migrationProfile.js +35 -0
  103. package/src/infrastructure/ingest/db/runRetention.js +58 -0
  104. package/src/infrastructure/ingest/ingestCheckpoint.js +71 -0
  105. package/src/infrastructure/ingest/modbus/mx210Tcp.js +81 -0
  106. package/src/infrastructure/ingest/modbus/silentStreams.js +152 -0
  107. package/src/infrastructure/ingest/mqtt/metricsTransformer.js +54 -0
  108. package/src/infrastructure/ingest/mqtt/modbusDeviceSpec.js +112 -0
  109. package/src/infrastructure/ingest/mqtt/modbusMqtt.js +204 -0
  110. package/src/infrastructure/ingest/mqtt/mqttMetrics.js +78 -0
  111. package/src/infrastructure/ingest/parseRequestTimeoutMs.js +20 -0
  112. package/src/infrastructure/ingest/pipelines/alertPipeline.js +48 -0
  113. package/src/infrastructure/ingest/pipelines/decisionPipeline.js +45 -0
  114. package/src/infrastructure/ingest/pipelines/segmentPipeline.js +60 -0
  115. package/src/infrastructure/ingest/processingErrorLog.js +25 -0
  116. package/src/infrastructure/ingest/silentOpenWatch.js +50 -0
  117. package/src/infrastructure/ingest/sinks/alertSink.js +43 -0
  118. package/src/infrastructure/ingest/sinks/closeOrphanOpen.js +32 -0
  119. package/src/infrastructure/ingest/sinks/closeSilentOpen.js +39 -0
  120. package/src/infrastructure/ingest/sinks/retagSink.js +43 -0
  121. package/src/infrastructure/ingest/sinks/userDecisionSink.js +41 -0
  122. package/src/infrastructure/ingest/telemetry/amqpMetricsIngest.js +94 -0
  123. package/src/infrastructure/ingest/telemetry/amqpMqttRelay.js +71 -0
  124. package/src/infrastructure/ingest/telemetry/deliverToMqttRecord.js +19 -0
  125. package/src/infrastructure/ingest/telemetry/streamNameFromTopic.js +21 -0
  126. package/src/infrastructure/messaging/ownership/httpOperations.js +96 -0
  127. package/src/infrastructure/messaging/ownership/httpTimeline.js +99 -0
  128. package/src/infrastructure/messaging/ownership/machineOwners.js +39 -0
  129. package/src/infrastructure/messaging/ownership/ownerTimeline.js +28 -0
  130. package/src/infrastructure/messaging/stomp/alerts/stompAlerts.js +21 -0
  131. package/src/infrastructure/messaging/stomp/alerts/stompAlertsCollection.js +51 -0
  132. package/src/infrastructure/messaging/stomp/alerts/stompAlertsInit.js +24 -0
  133. package/src/infrastructure/messaging/stomp/alerts/stompAlertsLogic.js +51 -0
  134. package/src/infrastructure/messaging/stomp/stompTimelineSegments.js +80 -0
  135. package/src/infrastructure/messaging/stomp/timeline.js +21 -0
  136. package/src/infrastructure/messaging/stomp/userDecisionBody.js +25 -0
  137. package/src/infrastructure/messaging/stomp/userDecisions.js +42 -0
  138. package/src/infrastructure/operators/centralOperators.js +64 -0
  139. package/src/infrastructure/operators/edgeOperators.js +39 -0
  140. package/src/infrastructure/operators/operatorById.js +33 -0
  141. package/src/infrastructure/operators/operatorExtras.js +41 -0
  142. package/src/infrastructure/operators/operators.js +32 -0
  143. package/src/infrastructure/operators/operatorsFromSeed.js +18 -0
  144. package/src/infrastructure/operators/operatorsSync.js +57 -0
  145. package/src/infrastructure/persistence/clickhouse/connection.js +58 -0
  146. package/src/infrastructure/persistence/clickhouse/pollTopicCursors.js +48 -0
  147. package/src/{clickhouseSensor.js → infrastructure/persistence/clickhouse/sensor.js} +9 -27
  148. package/src/infrastructure/persistence/clickhouse/streamHub.js +165 -0
  149. package/src/infrastructure/persistence/memory/alerts.js +21 -0
  150. package/src/infrastructure/persistence/memory/checkpoints.js +98 -0
  151. package/src/infrastructure/persistence/memory/metrics.js +69 -0
  152. package/src/infrastructure/persistence/memory/metricsDisabled.js +19 -0
  153. package/src/infrastructure/persistence/memory/operations.js +87 -0
  154. package/src/infrastructure/persistence/memory/segments.js +83 -0
  155. package/src/infrastructure/persistence/memory/timeline.js +56 -0
  156. package/src/infrastructure/persistence/metricsSensor.js +112 -0
  157. package/src/infrastructure/persistence/pg/alerts.js +25 -0
  158. package/src/infrastructure/persistence/pg/checkpoints.js +115 -0
  159. package/src/infrastructure/persistence/pg/metrics.js +73 -0
  160. package/src/infrastructure/persistence/pg/operations.js +95 -0
  161. package/src/infrastructure/persistence/pg/operators.js +118 -0
  162. package/src/infrastructure/persistence/pg/segments.js +48 -0
  163. package/src/infrastructure/persistence/pg/timeline.js +53 -0
  164. package/src/infrastructure/persistence/pg/userDecisions.js +72 -0
  165. package/src/infrastructure/persistence/postgresPool.js +24 -0
  166. package/src/infrastructure/persistence/stateDataFromMemory.js +28 -0
  167. package/src/infrastructure/persistence/stateDataFromPool.js +18 -0
  168. package/src/infrastructure/sync/operationCodec.js +75 -0
  169. package/src/infrastructure/sync/operationSyncIngest.js +82 -0
  170. package/src/infrastructure/sync/operationSyncSink.js +40 -0
  171. package/src/activeMelting.js +0 -54
  172. package/src/completedMelting.js +0 -42
  173. package/src/event.js +0 -24
  174. package/src/events.js +0 -51
  175. package/src/interval.js +0 -18
  176. package/src/machineChronology.js +0 -79
  177. package/src/meltingChronology.js +0 -37
  178. package/src/meltingMachine.js +0 -53
  179. package/src/meltingRuleEngine.js +0 -37
  180. package/src/meltingShop.js +0 -32
  181. package/src/meltings.js +0 -97
  182. package/src/monitoredMeltingMachine.js +0 -33
  183. package/src/plant.js +0 -23
  184. package/src/requests.js +0 -44
  185. package/src/rule.js +0 -33
  186. package/src/rules.js +0 -23
  187. package/src/scyllaSensor.js +0 -54
  188. package/src/segments.js +0 -64
  189. package/src/sqliteSensor.js +0 -106
@@ -0,0 +1,112 @@
1
+ function parseTimestamp(ts) {
2
+ return ts instanceof Date ? ts : new Date(ts);
3
+ }
4
+
5
+ function pollStream(spec) {
6
+ const { read, topic, since, step, callback, clock, unit } = spec;
7
+ const time = clock || (() => {
8
+ return new Date();
9
+ });
10
+ let lastTs = since;
11
+ const timer = setInterval(async () => {
12
+ try {
13
+ const rows = await read.poll(topic, lastTs.toISOString(), time().toISOString());
14
+ rows.forEach((row) => {
15
+ const timestamp = parseTimestamp(row.ts);
16
+ callback({ timestamp, value: row.value, unit });
17
+ lastTs = timestamp;
18
+ });
19
+ } catch {
20
+ /* next poll retries */
21
+ }
22
+ }, step);
23
+ return {
24
+ cancel() {
25
+ clearInterval(timer);
26
+ }
27
+ };
28
+ }
29
+
30
+ /**
31
+ * Read port adapter for PostgreSQL metrics state.
32
+ *
33
+ * @param {object} metrics - metrics state port from metricsStatePg
34
+ * @returns {object} read port for metricsSensor
35
+ */
36
+ export function pgMetricsRead(metrics) {
37
+ return {
38
+ async current(topic) {
39
+ const row = await metrics.latestForTopic(topic);
40
+ if (!row) {
41
+ return { found: false };
42
+ }
43
+ return { found: true, ts: row.ts, value: row.value };
44
+ },
45
+ range(topic, startIso, endIso, stepMs) {
46
+ return metrics.rangeForTopic(topic, startIso, endIso, stepMs);
47
+ },
48
+ poll(topic, afterIso, untilIso) {
49
+ return metrics.pollTopic(topic, afterIso, untilIso);
50
+ }
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Sensor reading metrics through a generic read port.
56
+ *
57
+ * @param {object} read - port with current, range, poll
58
+ * @param {string} topic - metrics topic key
59
+ * @param {string} displayName - label
60
+ * @param {string} unit - unit string
61
+ * @returns {object} sensor with name current measurements stream
62
+ */
63
+ export default function metricsSensor(read, topic, displayName, unit) {
64
+ return {
65
+ name() {
66
+ return displayName;
67
+ },
68
+ async current() {
69
+ const row = await read.current(topic);
70
+ if (!row.found) {
71
+ return { found: false };
72
+ }
73
+ return {
74
+ found: true,
75
+ timestamp: parseTimestamp(row.ts),
76
+ value: row.value,
77
+ unit
78
+ };
79
+ },
80
+ async measurements(range, step) {
81
+ const rows = await read.range(
82
+ topic,
83
+ range.start.toISOString(),
84
+ range.end.toISOString(),
85
+ step
86
+ );
87
+ return rows.map((row) => {
88
+ return {
89
+ timestamp: parseTimestamp(row.ts),
90
+ value: row.value,
91
+ unit
92
+ };
93
+ });
94
+ },
95
+ stream(since, step, callback, clock) {
96
+ return pollStream({ read, topic, since, step, callback, clock, unit });
97
+ }
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Sensor reading metrics from PostgreSQL metrics table in-process.
103
+ *
104
+ * @param {object} metrics - metrics state port with latestForTopic, rangeForTopic, pollTopic
105
+ * @param {string} topic - metrics topic key
106
+ * @param {string} displayName - label
107
+ * @param {string} unit - unit string
108
+ * @returns {object} sensor with name current measurements stream
109
+ */
110
+ export function pgMetricsSensor(metrics, topic, displayName, unit) {
111
+ return metricsSensor(pgMetricsRead(metrics), topic, displayName, unit);
112
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * PostgreSQL alert hydration port for supervisor-sink and plant STOMP alerts.
3
+ *
4
+ * @param {object} pool - pg pool
5
+ * @returns {object} alerts port with listUnacknowledged
6
+ *
7
+ * @example
8
+ * const store = alertsStatePg(pool);
9
+ * const rows = await store.listUnacknowledged({ machine: 'm1' });
10
+ */
11
+ export default function alertsStatePg(pool) {
12
+ return {
13
+ async listUnacknowledged(filters) {
14
+ let sql = 'SELECT * FROM alerts WHERE acknowledged = FALSE';
15
+ const prm = [];
16
+ if (filters.machine) {
17
+ prm.push(filters.machine);
18
+ sql += ` AND machine = $${prm.length}`;
19
+ }
20
+ sql += ' ORDER BY id';
21
+ const result = await pool.query(sql, prm);
22
+ return result.rows;
23
+ }
24
+ };
25
+ }
@@ -0,0 +1,115 @@
1
+ import segmentStatePg from './segments.js';
2
+
3
+ function parseJsonField(raw) {
4
+ if (!raw) {
5
+ return null;
6
+ }
7
+ return JSON.parse(raw);
8
+ }
9
+
10
+ function segmentItem(row, machineId) {
11
+ if (!row) {
12
+ return null;
13
+ }
14
+ return {
15
+ machine: machineId,
16
+ name: row.name,
17
+ start: new Date(row.start_time).getTime() / 1000,
18
+ end: new Date(row.end_time).getTime() / 1000,
19
+ duration: row.duration,
20
+ tags: parseJsonField(row.tags),
21
+ options: parseJsonField(row.options),
22
+ properties: parseJsonField(row.properties)
23
+ };
24
+ }
25
+
26
+ function segmentAt(pool, machineId, startEpoch) {
27
+ const segments = segmentStatePg(pool);
28
+ const start = new Date(startEpoch * 1000);
29
+ return segments.rowAt(machineId, start).then((row) => {
30
+ return segmentItem(row, machineId);
31
+ });
32
+ }
33
+
34
+ function replayCursor(pool, machineId) {
35
+ return pool.query(
36
+ `WITH closed AS (
37
+ SELECT MAX(EXTRACT(EPOCH FROM end_time)) AS ts
38
+ FROM segments
39
+ WHERE machine = $1 AND duration > 0
40
+ ),
41
+ pending AS (
42
+ SELECT MAX(EXTRACT(EPOCH FROM start_time)) AS ts
43
+ FROM segments
44
+ WHERE machine = $1 AND duration = 0
45
+ )
46
+ SELECT COALESCE(LEAST(closed.ts, pending.ts), pending.ts, closed.ts, 0.0) AS cursor
47
+ FROM closed, pending`,
48
+ [machineId]
49
+ ).then((result) => {
50
+ return Number(result.rows[0].cursor || 0);
51
+ });
52
+ }
53
+
54
+ function pendingSegments(pool) {
55
+ return pool.query(
56
+ `SELECT machine, name,
57
+ EXTRACT(EPOCH FROM start_time) AS start,
58
+ EXTRACT(EPOCH FROM end_time) AS end
59
+ FROM segments
60
+ WHERE duration = 0`
61
+ ).then((result) => {
62
+ return result.rows.map((row) => {
63
+ return {
64
+ machine: row.machine,
65
+ name: row.name,
66
+ start: Number(row.start),
67
+ end: Number(row.end)
68
+ };
69
+ });
70
+ });
71
+ }
72
+
73
+ function readings(pool, topics, from) {
74
+ if (topics.length === 0) {
75
+ return Promise.resolve([]);
76
+ }
77
+ const placeholders = topics.map((item, index) => {
78
+ return `$${index + 1}`;
79
+ }).join(',');
80
+ return pool.query(
81
+ `SELECT topic, EXTRACT(EPOCH FROM ts) AS ts, value
82
+ FROM metrics
83
+ WHERE topic IN (${placeholders}) AND ts > to_timestamp($${topics.length + 1})
84
+ ORDER BY ts ASC`,
85
+ [...topics, from]
86
+ ).then((result) => {
87
+ return result.rows.map((row) => {
88
+ return {
89
+ topic: row.topic,
90
+ timestamp: Number(row.ts),
91
+ value: Number(row.value)
92
+ };
93
+ });
94
+ });
95
+ }
96
+
97
+ export default function checkpointStatePg(pool, metricsEnabled = true) {
98
+ return {
99
+ replayCursor(machineId) {
100
+ return replayCursor(pool, machineId);
101
+ },
102
+ pendingSegments() {
103
+ return pendingSegments(pool);
104
+ },
105
+ readings(topics, from) {
106
+ if (!metricsEnabled) {
107
+ return Promise.resolve([]);
108
+ }
109
+ return readings(pool, topics, from);
110
+ },
111
+ segment(machineId, startEpoch) {
112
+ return segmentAt(pool, machineId, startEpoch);
113
+ }
114
+ };
115
+ }
@@ -0,0 +1,73 @@
1
+ export default function metricsStatePg(pool) {
2
+ return {
3
+ async latestForTopic(topic) {
4
+ const result = await pool.query(
5
+ 'SELECT ts, value FROM metrics WHERE topic = $1 ORDER BY ts DESC LIMIT 1',
6
+ [topic]
7
+ );
8
+ return result.rows[0] ?? null;
9
+ },
10
+ async rangeForTopic(topic, startIso, endIso, stepMs) {
11
+ const seconds = Math.max(1, Math.floor(stepMs / 1000));
12
+ const sql = `SELECT bucket AS ts, value FROM (
13
+ SELECT date_bin($1::interval, ts, '1970-01-01'::timestamptz) AS bucket, value,
14
+ ROW_NUMBER() OVER (PARTITION BY date_bin($1::interval, ts, '1970-01-01'::timestamptz) ORDER BY ts DESC) AS rn
15
+ FROM metrics WHERE topic = $2 AND ts >= $3 AND ts <= $4
16
+ ) sub WHERE rn = 1 ORDER BY ts`;
17
+ const prm = [`${seconds} seconds`, topic, startIso, endIso];
18
+ const result = await pool.query(sql, prm);
19
+ return result.rows.map((row) => {
20
+ return { ts: row.ts, value: row.value };
21
+ });
22
+ },
23
+ async pollTopic(topic, afterIso, untilIso) {
24
+ const result = await pool.query(
25
+ 'SELECT ts, value FROM metrics WHERE topic = $1 AND ts > $2 AND ts <= $3 ORDER BY ts LIMIT 100',
26
+ [topic, afterIso, untilIso]
27
+ );
28
+ return result.rows.map((row) => {
29
+ return { ts: row.ts, value: row.value };
30
+ });
31
+ },
32
+ async insertRows(items) {
33
+ const topics = items.map((row) => {
34
+ return row.topic;
35
+ });
36
+ const tss = items.map((row) => {
37
+ return row.ts;
38
+ });
39
+ const values = items.map((row) => {
40
+ return row.value;
41
+ });
42
+ await pool.query(
43
+ 'INSERT INTO metrics (topic, ts, value) SELECT * FROM unnest($1::text[], $2::timestamptz[], $3::float8[])',
44
+ [topics, tss, values]
45
+ );
46
+ }
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Sink adapter for mqttMetrics writing into supervisor-sink metrics table.
52
+ *
53
+ * @param {object} pool - PostgreSQL pool
54
+ * @returns {object} sink with write(records)
55
+ */
56
+ export function metricsSinkFromPool(pool) {
57
+ const metrics = metricsStatePg(pool);
58
+ return {
59
+ async write(records) {
60
+ if (records.length === 0) {
61
+ return;
62
+ }
63
+ const items = records.map((row) => {
64
+ return {
65
+ topic: row.topic,
66
+ ts: row.ts instanceof Date ? row.ts : new Date(row.ts),
67
+ value: row.value
68
+ };
69
+ });
70
+ await metrics.insertRows(items);
71
+ }
72
+ };
73
+ }
@@ -0,0 +1,95 @@
1
+ async function upsertOperation(pool, item) {
2
+ const existing = await pool.query('SELECT 1 FROM operations WHERE key = $1', [item.key]);
3
+ await pool.query(
4
+ `INSERT INTO operations (
5
+ machine, occurred_at, kind, key, payload
6
+ ) VALUES ($1, $2, $3, $4, $5)
7
+ ON CONFLICT (key) DO UPDATE SET
8
+ machine = EXCLUDED.machine,
9
+ occurred_at = EXCLUDED.occurred_at,
10
+ kind = EXCLUDED.kind,
11
+ payload = EXCLUDED.payload`,
12
+ [
13
+ item.machine,
14
+ item.occurred_at,
15
+ item.kind,
16
+ item.key,
17
+ item.payload
18
+ ]
19
+ );
20
+ return { created: existing.rows.length === 0 };
21
+ }
22
+
23
+ function listForMachine(pool, machineId, kind, range) {
24
+ let sql = `SELECT machine, occurred_at, kind, key, payload
25
+ FROM operations WHERE machine = $1 AND kind = $2`;
26
+ const prm = [machineId, kind];
27
+ if (range.from) {
28
+ prm.push(range.from);
29
+ sql += ` AND occurred_at >= $${prm.length}`;
30
+ }
31
+ if (range.to) {
32
+ prm.push(range.to);
33
+ sql += ` AND occurred_at <= $${prm.length}`;
34
+ }
35
+ sql += ' ORDER BY occurred_at';
36
+ return pool.query(sql, prm).then((result) => {
37
+ return result.rows;
38
+ });
39
+ }
40
+
41
+ function missing(machineId, key) {
42
+ return new Error(`operation '${key}' not found for machine '${machineId}'`);
43
+ }
44
+
45
+ async function getOperation(pool, machineId, key) {
46
+ const result = await pool.query(
47
+ `SELECT machine, occurred_at, kind, key, payload
48
+ FROM operations WHERE machine = $1 AND key = $2`,
49
+ [machineId, key]
50
+ );
51
+ if (result.rows.length === 0) {
52
+ throw missing(machineId, key);
53
+ }
54
+ return result.rows[0];
55
+ }
56
+
57
+ async function removeOperation(pool, machineId, key) {
58
+ const result = await pool.query(
59
+ `DELETE FROM operations WHERE machine = $1 AND key = $2
60
+ RETURNING machine, occurred_at, kind, key, payload`,
61
+ [machineId, key]
62
+ );
63
+ if (result.rows.length === 0) {
64
+ throw missing(machineId, key);
65
+ }
66
+ return result.rows[0];
67
+ }
68
+
69
+ /**
70
+ * PostgreSQL operations persistence port for generic machine operations.
71
+ *
72
+ * @param {object} pool - pg pool
73
+ * @returns {object} operations port with upsert, get, remove, and listForMachine
74
+ *
75
+ * @example
76
+ * const store = operationStatePg(pool);
77
+ * await store.upsert({ machine: 'm1', key: 'nb-1', kind: 'chem', ... });
78
+ * await store.remove('m1', 'nb-1');
79
+ */
80
+ export default function operationStatePg(pool) {
81
+ return {
82
+ upsert(item) {
83
+ return upsertOperation(pool, item);
84
+ },
85
+ get(machineId, key) {
86
+ return getOperation(pool, machineId, key);
87
+ },
88
+ remove(machineId, key) {
89
+ return removeOperation(pool, machineId, key);
90
+ },
91
+ listForMachine(machineId, kind, range) {
92
+ return listForMachine(pool, machineId, kind, range);
93
+ }
94
+ };
95
+ }
@@ -0,0 +1,118 @@
1
+ import operator from '../../../domain/operator/operator.js';
2
+
3
+ function conflict(uid) {
4
+ const err = new Error(`operator cardUid '${uid}' already exists`);
5
+ err.routeCode = 'CONFLICT';
6
+ err.routeStatus = 409;
7
+ return err;
8
+ }
9
+
10
+ function logFailure(logger, message, err) {
11
+ if (logger && typeof logger.error === 'function') {
12
+ logger.error(message, err);
13
+ return;
14
+ }
15
+ console.error(message, err); // eslint-disable-line no-console
16
+ }
17
+
18
+ async function listRows(pool, logger) {
19
+ try {
20
+ const result = await pool.query(
21
+ 'SELECT id, card_uid, first_name, last_name, display_name FROM operators ORDER BY id'
22
+ );
23
+ return result.rows.map((row) => {
24
+ return operator(row.id, row.card_uid, row.first_name, row.last_name, row.display_name);
25
+ });
26
+ } catch (err) {
27
+ const message = `operatorsFromPg list failed: ${err && err.message ? err.message : 'unknown error'}`;
28
+ logFailure(logger, message, err);
29
+ throw err;
30
+ }
31
+ }
32
+
33
+ async function insertRow(pool, logger, fields) {
34
+ try {
35
+ const result = await pool.query(
36
+ `INSERT INTO operators (card_uid, first_name, last_name, display_name)
37
+ VALUES ($1, $2, $3, $4)
38
+ RETURNING id, card_uid, first_name, last_name, display_name`,
39
+ [fields.cardUid, fields.firstName, fields.lastName, fields.displayName]
40
+ );
41
+ const row = result.rows[0];
42
+ return operator(row.id, row.card_uid, row.first_name, row.last_name, row.display_name);
43
+ } catch (err) {
44
+ if (err && err.code === '23505') {
45
+ throw conflict(fields.cardUid);
46
+ }
47
+ const message = `operatorsFromPg create failed: ${err && err.message ? err.message : 'unknown error'}`;
48
+ logFailure(logger, message, err);
49
+ throw err;
50
+ }
51
+ }
52
+
53
+ async function readFlag(pool, logger) {
54
+ try {
55
+ const result = await pool.query(
56
+ 'SELECT enabled FROM operators_registration WHERE singleton = 1'
57
+ );
58
+ if (!result.rows.length) {
59
+ throw new Error('operators_registration singleton row missing');
60
+ }
61
+ return result.rows[0].enabled === true;
62
+ } catch (err) {
63
+ const message = `operatorsFromPg enabled failed: ${err && err.message ? err.message : 'unknown error'}`;
64
+ logFailure(logger, message, err);
65
+ throw err;
66
+ }
67
+ }
68
+
69
+ async function writeFlag(pool, logger, flag) {
70
+ try {
71
+ const result = await pool.query(
72
+ `UPDATE operators_registration SET enabled = $1 WHERE singleton = 1
73
+ RETURNING enabled`,
74
+ [flag === true]
75
+ );
76
+ if (!result.rows.length) {
77
+ throw new Error('operators_registration singleton row missing');
78
+ }
79
+ return result.rows[0].enabled === true;
80
+ } catch (err) {
81
+ const message = `operatorsFromPg permit failed: ${err && err.message ? err.message : 'unknown error'}`;
82
+ logFailure(logger, message, err);
83
+ throw err;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * PostgreSQL operators port for central site-server.
89
+ * Implements list(), create(fields), enabled(), and permit(flag).
90
+ *
91
+ * @param {object} pool - pg pool
92
+ * @param {object} [logger] - optional logger with error(message, err)
93
+ * @returns {object} provider with async list, create, enabled, permit
94
+ *
95
+ * @example
96
+ * const provider = operatorsFromPg(pool);
97
+ * const rows = await provider.list();
98
+ * const created = await provider.create({
99
+ * cardUid: 'AB12', firstName: 'Ivan', lastName: 'Petrov', displayName: 'Ivan Petrov'
100
+ * });
101
+ * await provider.permit(true);
102
+ */
103
+ export default function operatorsFromPg(pool, logger) {
104
+ return {
105
+ list() {
106
+ return listRows(pool, logger);
107
+ },
108
+ create(fields) {
109
+ return insertRow(pool, logger, fields);
110
+ },
111
+ enabled() {
112
+ return readFlag(pool, logger);
113
+ },
114
+ permit(flag) {
115
+ return writeFlag(pool, logger, flag);
116
+ }
117
+ };
118
+ }
@@ -0,0 +1,48 @@
1
+ export default function segmentStatePg(pool) {
2
+ return {
3
+ async listForMachine(machineId, range) {
4
+ let sql = `SELECT s.name, s.start_time, s.end_time, s.duration, s.options, s.tags, s.properties
5
+ FROM segments s WHERE s.machine = $1`;
6
+ const prm = [machineId];
7
+ if (range.from) {
8
+ prm.push(range.from);
9
+ sql += ` AND (s.end_time >= $${prm.length} OR s.duration = 0)`;
10
+ }
11
+ if (range.to) {
12
+ prm.push(range.to);
13
+ sql += ` AND s.start_time <= $${prm.length}`;
14
+ }
15
+ sql += ' ORDER BY s.start_time';
16
+ const result = await pool.query(sql, prm);
17
+ return result.rows;
18
+ },
19
+ async rowAt(machineId, start) {
20
+ const result = await pool.query(
21
+ `SELECT name, start_time, end_time, duration, options, tags, properties
22
+ FROM segments WHERE machine = $1 AND start_time = $2`,
23
+ [machineId, start]
24
+ );
25
+ return result.rows[0] ?? null;
26
+ },
27
+ async pendingRequestsForMachine(machineId) {
28
+ const result = await pool.query(
29
+ `SELECT start_time AS id, name, start_time, end_time, duration, options
30
+ FROM segments WHERE machine = $1 AND resolved = FALSE ORDER BY start_time`,
31
+ [machineId]
32
+ );
33
+ return result.rows;
34
+ },
35
+ async retag(machineId, start, tagsJson, propertiesJson) {
36
+ await pool.query(
37
+ 'UPDATE segments SET tags = $1, properties = $2 WHERE machine = $3 AND start_time = $4',
38
+ [tagsJson, propertiesJson, machineId, start]
39
+ );
40
+ },
41
+ async resolveRequest(machineId, startKey, tagsJson, propertiesJson) {
42
+ await pool.query(
43
+ 'UPDATE segments SET tags = $1, properties = $2, resolved = TRUE, consumed = FALSE WHERE machine = $3 AND start_time = $4',
44
+ [tagsJson, propertiesJson, machineId, startKey]
45
+ );
46
+ }
47
+ };
48
+ }
@@ -0,0 +1,53 @@
1
+ import segmentStatePg from './segments.js';
2
+
3
+ function mapRow(item) {
4
+ const tags = item.tags === undefined || item.tags === null ? null : item.tags;
5
+ const properties = item.properties === undefined || item.properties === null ? null : item.properties;
6
+ return {
7
+ name: item.name,
8
+ start_time: new Date(item.start_time),
9
+ end_time: new Date(item.end_time),
10
+ duration: item.duration,
11
+ options: item.options,
12
+ tags,
13
+ properties
14
+ };
15
+ }
16
+
17
+ /**
18
+ * Postgres-backed timeline read port for one machine.
19
+ *
20
+ * @param {object} pool - pg pool
21
+ * @param {string} machineId - machine identifier
22
+ * @returns {object} timeline read port with list, rowAt, pending
23
+ *
24
+ * @example
25
+ * const pg = pgTimeline(pool, 'm1');
26
+ * await pg.list({ from: '2024-01-01' });
27
+ */
28
+ export default function pgTimeline(pool, machineId) {
29
+ const segments = segmentStatePg(pool);
30
+ return {
31
+ async list(range) {
32
+ const rows = await segments.listForMachine(machineId, range || {});
33
+ return rows.map(mapRow);
34
+ },
35
+ async rowAt(start) {
36
+ const row = await segments.rowAt(machineId, start);
37
+ return row ? mapRow(row) : null;
38
+ },
39
+ async pending() {
40
+ const rows = await segments.pendingRequestsForMachine(machineId);
41
+ return rows.map((item) => {
42
+ return {
43
+ id: item.id,
44
+ name: item.name,
45
+ start_time: new Date(item.start_time),
46
+ end_time: new Date(item.end_time),
47
+ duration: item.duration,
48
+ options: item.options
49
+ };
50
+ });
51
+ }
52
+ };
53
+ }