@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.
- package/README.md +196 -33
- package/db/migrations/C0001__central_drop_metrics.sql +1 -0
- package/db/migrations/C0002__central_revoke_delete.sql +12 -0
- package/db/migrations/C0003__central_operators.sql +7 -0
- package/db/migrations/C0004__central_operations_grants.sql +2 -0
- package/db/migrations/C0005__central_operators_grants.sql +1 -0
- package/db/migrations/C0006__central_operators_registration.sql +16 -0
- package/db/migrations/E0001__edge_metrics.sql +8 -0
- package/db/migrations/E0002__edge_retention_delete.sql +8 -0
- package/db/migrations/E0003__edge_operations_grants.sql +2 -0
- package/db/migrations/V0001__baseline.sql +25 -0
- package/db/migrations/V0002__sink_extension_tables.sql +6 -0
- package/db/migrations/V0003__operations.sql +10 -0
- package/db/migrations/V0004__ingest_checkpoints.sql +6 -0
- package/db/migrations/V0005__user_decisions_operator.sql +2 -0
- package/index.js +75 -45
- package/package.json +28 -8
- package/src/application/bindSilentStreams.js +20 -0
- package/src/application/edgeOperatorCatalog.js +45 -0
- package/src/application/export/exportJob.js +118 -0
- package/src/application/export/exportQuery.js +32 -0
- package/src/application/export/exportSink.js +25 -0
- package/src/application/export/exportStream.js +33 -0
- package/src/application/machineInPlant.js +20 -0
- package/src/application/metricsPlant.js +35 -0
- package/src/application/plantApi.js +49 -0
- package/src/application/plantOperations.js +19 -0
- package/src/application/plantServer.js +107 -0
- package/src/application/shopWithTimeline.js +112 -0
- package/src/application/siteOperatorCatalog.js +78 -0
- package/src/application/siteServer.js +191 -0
- package/src/application/supervisorSink.js +97 -0
- package/src/application/timelineOperatorFromEnv.js +19 -0
- package/src/bin/supervisor-sink.js +28 -0
- package/src/{alert.js → domain/alerting/alert.js} +2 -2
- package/src/{alerts.js → domain/alerting/alerts.js} +3 -3
- package/src/domain/alerting/ingest.js +18 -0
- package/src/domain/ingest/ingestCursor.js +26 -0
- package/src/domain/operation/operation.js +23 -0
- package/src/domain/operation/operations.js +81 -0
- package/src/domain/operator/operator.js +23 -0
- package/src/domain/plant/machine.js +32 -0
- package/src/domain/plant/plant.js +20 -0
- package/src/domain/plant/shop.js +24 -0
- package/src/domain/segment/dispatch.js +31 -0
- package/src/domain/segment/normalize.js +31 -0
- package/src/domain/segment/silenceBudget.js +16 -0
- package/src/{initialized.js → domain/shared/initialized.js} +3 -4
- package/src/{pubsub.js → domain/shared/pubsub.js} +2 -3
- package/src/domain/timeline/timeline.js +25 -0
- package/src/infrastructure/catalog/tag-catalog.json +27 -0
- package/src/infrastructure/catalog/tagCatalog.js +65 -0
- package/src/infrastructure/client/index.js +20 -0
- package/src/infrastructure/client/machineClient.js +159 -0
- package/src/infrastructure/client/machineOperationsClient.js +159 -0
- package/src/infrastructure/client/scadaClient.js +67 -0
- package/src/infrastructure/client/sseConnection.js +42 -0
- package/src/infrastructure/http/edge/edgeApi.js +54 -0
- package/src/infrastructure/http/edge/httpMetricsRead.js +34 -0
- package/src/infrastructure/http/edge/metricsSensor.js +15 -0
- package/src/infrastructure/http/edge/parseStateHttpTimeoutMs.js +20 -0
- package/src/infrastructure/http/edge/routes/checkpoint/checkpointRoutes.js +79 -0
- package/src/infrastructure/http/edge/routes/metrics/metricsBatch.js +59 -0
- package/src/infrastructure/http/edge/routes/metrics/metricsCurrent.js +29 -0
- package/src/infrastructure/http/edge/routes/metrics/metricsPoll.js +25 -0
- package/src/infrastructure/http/edge/routes/metrics/metricsRange.js +26 -0
- package/src/infrastructure/http/edge/routes/metrics/metricsRoutes.js +21 -0
- package/src/infrastructure/http/edge/routes/retention/retentionRoutes.js +41 -0
- package/src/infrastructure/http/edge/startTestEdgeApi.js +39 -0
- package/src/infrastructure/http/edge/stateAccess.js +13 -0
- package/src/infrastructure/http/edge/stateHttpClient.js +106 -0
- package/src/infrastructure/http/edge/stateHttpTimeoutError.js +13 -0
- package/src/infrastructure/http/plant/json/decisionJson.js +44 -0
- package/src/infrastructure/http/plant/json/operationJson.js +28 -0
- package/src/infrastructure/http/plant/json/operatorJson.js +22 -0
- package/src/infrastructure/http/plant/json/segmentJson.js +27 -0
- package/src/infrastructure/http/plant/operationAudit.js +56 -0
- package/src/infrastructure/http/plant/routes/alertRoute.js +93 -0
- package/src/infrastructure/http/plant/routes/catalogRoute.js +21 -0
- package/src/infrastructure/http/plant/routes/decisionRoute.js +60 -0
- package/src/infrastructure/http/plant/routes/machineRoute.js +35 -0
- package/src/infrastructure/http/plant/routes/measurementRoute.js +60 -0
- package/src/infrastructure/http/plant/routes/operationDrafts.js +79 -0
- package/src/infrastructure/http/plant/routes/operationRoute.js +80 -0
- package/src/infrastructure/http/plant/routes/operationWrites.js +186 -0
- package/src/infrastructure/http/plant/routes/operatorRoute.js +143 -0
- package/src/infrastructure/http/plant/routes/simulationRoute.js +61 -0
- package/src/infrastructure/http/plant/routes/timelineRoute.js +112 -0
- package/src/infrastructure/http/plant/streams/alertStream.js +68 -0
- package/src/infrastructure/http/plant/streams/heartbeatStream.js +34 -0
- package/src/infrastructure/http/plant/streams/measurementStream.js +90 -0
- package/src/infrastructure/http/plant/streams/operationStream.js +42 -0
- package/src/infrastructure/http/plant/streams/timelineStream.js +97 -0
- package/src/infrastructure/http/plant/timelineOperator.js +87 -0
- package/src/infrastructure/ingest/activity/activityTracking.js +74 -0
- package/src/infrastructure/ingest/activity/activityTransformer.js +45 -0
- package/src/infrastructure/ingest/codecs/alertCodec.js +56 -0
- package/src/infrastructure/ingest/codecs/segmentCodec.js +30 -0
- package/src/infrastructure/ingest/codecs/userDecisionCodec.js +78 -0
- package/src/infrastructure/ingest/cooldown.js +24 -0
- package/src/infrastructure/ingest/db/migrate.js +78 -0
- package/src/infrastructure/ingest/db/migrationProfile.js +35 -0
- package/src/infrastructure/ingest/db/runRetention.js +58 -0
- package/src/infrastructure/ingest/ingestCheckpoint.js +71 -0
- package/src/infrastructure/ingest/modbus/mx210Tcp.js +81 -0
- package/src/infrastructure/ingest/modbus/silentStreams.js +152 -0
- package/src/infrastructure/ingest/mqtt/metricsTransformer.js +54 -0
- package/src/infrastructure/ingest/mqtt/modbusDeviceSpec.js +112 -0
- package/src/infrastructure/ingest/mqtt/modbusMqtt.js +204 -0
- package/src/infrastructure/ingest/mqtt/mqttMetrics.js +78 -0
- package/src/infrastructure/ingest/parseRequestTimeoutMs.js +20 -0
- package/src/infrastructure/ingest/pipelines/alertPipeline.js +48 -0
- package/src/infrastructure/ingest/pipelines/decisionPipeline.js +45 -0
- package/src/infrastructure/ingest/pipelines/segmentPipeline.js +60 -0
- package/src/infrastructure/ingest/processingErrorLog.js +25 -0
- package/src/infrastructure/ingest/silentOpenWatch.js +50 -0
- package/src/infrastructure/ingest/sinks/alertSink.js +43 -0
- package/src/infrastructure/ingest/sinks/closeOrphanOpen.js +32 -0
- package/src/infrastructure/ingest/sinks/closeSilentOpen.js +39 -0
- package/src/infrastructure/ingest/sinks/retagSink.js +43 -0
- package/src/infrastructure/ingest/sinks/userDecisionSink.js +41 -0
- package/src/infrastructure/ingest/telemetry/amqpMetricsIngest.js +94 -0
- package/src/infrastructure/ingest/telemetry/amqpMqttRelay.js +71 -0
- package/src/infrastructure/ingest/telemetry/deliverToMqttRecord.js +19 -0
- package/src/infrastructure/ingest/telemetry/streamNameFromTopic.js +21 -0
- package/src/infrastructure/messaging/ownership/httpOperations.js +96 -0
- package/src/infrastructure/messaging/ownership/httpTimeline.js +99 -0
- package/src/infrastructure/messaging/ownership/machineOwners.js +39 -0
- package/src/infrastructure/messaging/ownership/ownerTimeline.js +28 -0
- package/src/infrastructure/messaging/stomp/alerts/stompAlerts.js +21 -0
- package/src/infrastructure/messaging/stomp/alerts/stompAlertsCollection.js +51 -0
- package/src/infrastructure/messaging/stomp/alerts/stompAlertsInit.js +24 -0
- package/src/infrastructure/messaging/stomp/alerts/stompAlertsLogic.js +51 -0
- package/src/infrastructure/messaging/stomp/stompTimelineSegments.js +80 -0
- package/src/infrastructure/messaging/stomp/timeline.js +21 -0
- package/src/infrastructure/messaging/stomp/userDecisionBody.js +25 -0
- package/src/infrastructure/messaging/stomp/userDecisions.js +42 -0
- package/src/infrastructure/operators/centralOperators.js +64 -0
- package/src/infrastructure/operators/edgeOperators.js +39 -0
- package/src/infrastructure/operators/operatorById.js +33 -0
- package/src/infrastructure/operators/operatorExtras.js +41 -0
- package/src/infrastructure/operators/operators.js +32 -0
- package/src/infrastructure/operators/operatorsFromSeed.js +18 -0
- package/src/infrastructure/operators/operatorsSync.js +57 -0
- package/src/infrastructure/persistence/clickhouse/connection.js +58 -0
- package/src/infrastructure/persistence/clickhouse/pollTopicCursors.js +48 -0
- package/src/{clickhouseSensor.js → infrastructure/persistence/clickhouse/sensor.js} +9 -27
- package/src/infrastructure/persistence/clickhouse/streamHub.js +165 -0
- package/src/infrastructure/persistence/memory/alerts.js +21 -0
- package/src/infrastructure/persistence/memory/checkpoints.js +98 -0
- package/src/infrastructure/persistence/memory/metrics.js +69 -0
- package/src/infrastructure/persistence/memory/metricsDisabled.js +19 -0
- package/src/infrastructure/persistence/memory/operations.js +87 -0
- package/src/infrastructure/persistence/memory/segments.js +83 -0
- package/src/infrastructure/persistence/memory/timeline.js +56 -0
- package/src/infrastructure/persistence/metricsSensor.js +112 -0
- package/src/infrastructure/persistence/pg/alerts.js +25 -0
- package/src/infrastructure/persistence/pg/checkpoints.js +115 -0
- package/src/infrastructure/persistence/pg/metrics.js +73 -0
- package/src/infrastructure/persistence/pg/operations.js +95 -0
- package/src/infrastructure/persistence/pg/operators.js +118 -0
- package/src/infrastructure/persistence/pg/segments.js +48 -0
- package/src/infrastructure/persistence/pg/timeline.js +53 -0
- package/src/infrastructure/persistence/pg/userDecisions.js +72 -0
- package/src/infrastructure/persistence/postgresPool.js +24 -0
- package/src/infrastructure/persistence/stateDataFromMemory.js +28 -0
- package/src/infrastructure/persistence/stateDataFromPool.js +18 -0
- package/src/infrastructure/sync/operationCodec.js +75 -0
- package/src/infrastructure/sync/operationSyncIngest.js +82 -0
- package/src/infrastructure/sync/operationSyncSink.js +40 -0
- package/src/activeMelting.js +0 -54
- package/src/completedMelting.js +0 -42
- package/src/event.js +0 -24
- package/src/events.js +0 -51
- package/src/interval.js +0 -18
- package/src/machineChronology.js +0 -79
- package/src/meltingChronology.js +0 -37
- package/src/meltingMachine.js +0 -53
- package/src/meltingRuleEngine.js +0 -37
- package/src/meltingShop.js +0 -32
- package/src/meltings.js +0 -97
- package/src/monitoredMeltingMachine.js +0 -33
- package/src/plant.js +0 -23
- package/src/requests.js +0 -44
- package/src/rule.js +0 -33
- package/src/rules.js +0 -23
- package/src/scyllaSensor.js +0 -54
- package/src/segments.js +0 -64
- 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
|
+
}
|