@yarkivaev/scada 2.3.46 → 2.3.48

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.
@@ -1,2 +1,2 @@
1
- GRANT SELECT, INSERT, UPDATE ON operations TO supervisor_sink;
1
+ GRANT SELECT, INSERT, UPDATE, DELETE ON operations TO supervisor_sink;
2
2
  GRANT SELECT, INSERT, UPDATE ON ingest_checkpoints TO supervisor_sink;
@@ -0,0 +1 @@
1
+ GRANT DELETE ON operations TO supervisor_sink;
@@ -0,0 +1,2 @@
1
+ CREATE INDEX IF NOT EXISTS idx_operations_machine_kind_time
2
+ ON operations (machine, kind, occurred_at);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yarkivaev/scada",
3
- "version": "2.3.46",
3
+ "version": "2.3.48",
4
4
  "description": "SCADA domain objects, state persistence, and plant monitoring",
5
5
  "repository": {
6
6
  "type": "git",
@@ -6,9 +6,9 @@ import pubsub from '../domain/shared/pubsub.js';
6
6
  *
7
7
  * Optional kindSources inject non-PG kinds into listForMachine merges.
8
8
  *
9
- * @param {object} persistence - operations store with upsert, get, remove, listForMachine
9
+ * @param {object} persistence - operations store with upsert, get, remove, listForMachine, latestForMachine
10
10
  * @param {object} [kindSources] - map of kind to { list(machineId, range) }
11
- * @returns {object} operations with listForMachine, upsert, get, remove, and stream
11
+ * @returns {object} operations with listForMachine, latestForMachine, upsert, get, remove, and stream
12
12
  *
13
13
  * @example
14
14
  * const ops = plantOperations(dataAccess.operations, { temp: temperaturePort });
@@ -32,15 +32,17 @@ function resolveKinds(kinds, sources) {
32
32
  *
33
33
  * listForMachine merges requested kinds from injectable sources and Postgres,
34
34
  * sorted by occurred_at. Omitted kinds default to injectable source keys.
35
+ * latestForMachine reads only from persistence (single kind).
35
36
  *
36
- * @param {object} persistence - store with upsert, get, remove, listForMachine
37
+ * @param {object} persistence - store with upsert, get, remove, listForMachine, latestForMachine
37
38
  * @param {object} bus - pubsub instance with stream and emit methods
38
39
  * @param {object} [kindSources] - map of kind to { list(machineId, range) }
39
- * @returns {object} operations with listForMachine, upsert, get, remove, stream
40
+ * @returns {object} operations with listForMachine, latestForMachine, upsert, get, remove, stream
40
41
  *
41
42
  * @example
42
43
  * const ops = operations(store, bus, { temp: temperaturePort });
43
44
  * await ops.listForMachine('m1', ['chem', 'temp'], { from: new Date() });
45
+ * await ops.latestForMachine('m1', 'chem', { to: new Date() });
44
46
  * await ops.remove('m1', 'nb-1');
45
47
  */
46
48
  export default function operations(persistence, bus, kindSources) {
@@ -59,6 +61,9 @@ export default function operations(persistence, bus, kindSources) {
59
61
  });
60
62
  });
61
63
  },
64
+ latestForMachine(machineId, kind, bound) {
65
+ return persistence.latestForMachine(machineId, kind, bound);
66
+ },
62
67
  upsert(item) {
63
68
  const updatedAt = new Date();
64
69
  const row = stampRow(item, updatedAt);
@@ -21,7 +21,8 @@ const privilegedOnly = new Set([
21
21
  'C0005__central_operators_grants.sql',
22
22
  'C0006__central_operators_registration.sql',
23
23
  'E0002__edge_retention_delete.sql',
24
- 'E0003__edge_operations_grants.sql'
24
+ 'E0003__edge_operations_grants.sql',
25
+ 'E0004__edge_operations_delete.sql'
25
26
  ]);
26
27
 
27
28
  /**
@@ -37,6 +37,41 @@ function filterList(store, machineId, kind, range) {
37
37
  });
38
38
  }
39
39
 
40
+ /**
41
+ * Picks the latest matching row for a machine and kind at a time bound.
42
+ *
43
+ * @param {object} store - shared mutable store with operations array
44
+ * @param {string} machineId - machine identifier
45
+ * @param {string} kind - operation kind
46
+ * @param {{ to?: Date, before?: Date }} bound - inclusive to or exclusive before
47
+ * @returns {object|null} latest row or null
48
+ */
49
+ function filterLatest(store, machineId, kind, bound) {
50
+ const range = bound || {};
51
+ const before = range.before ? new Date(range.before).getTime() : null;
52
+ const to = range.to ? new Date(range.to).getTime() : null;
53
+ const matches = store.operations.filter((row) => {
54
+ if (row.machine !== machineId || row.kind !== kind) {
55
+ return false;
56
+ }
57
+ const at = new Date(row.occurred_at).getTime();
58
+ if (before !== null && at >= before) {
59
+ return false;
60
+ }
61
+ if (to !== null && at > to) {
62
+ return false;
63
+ }
64
+ return true;
65
+ }).sort((left, right) => {
66
+ const delta = new Date(right.occurred_at) - new Date(left.occurred_at);
67
+ if (delta !== 0) {
68
+ return delta;
69
+ }
70
+ return String(right.key).localeCompare(String(left.key));
71
+ });
72
+ return matches[0] || null;
73
+ }
74
+
40
75
  /**
41
76
  * In-memory operations state port for tests and local runs.
42
77
  *
@@ -82,6 +117,9 @@ export default function operationStateMemory(store) {
82
117
  },
83
118
  listForMachine(machineId, kind, range) {
84
119
  return Promise.resolve(filterList(store, machineId, kind, range));
120
+ },
121
+ latestForMachine(machineId, kind, bound) {
122
+ return Promise.resolve(filterLatest(store, machineId, kind, bound));
85
123
  }
86
124
  };
87
125
  }
@@ -38,6 +38,33 @@ function listForMachine(pool, machineId, kind, range) {
38
38
  });
39
39
  }
40
40
 
41
+ /**
42
+ * Loads the latest operation for a machine and kind at or before a bound.
43
+ *
44
+ * @param {object} pool - pg pool
45
+ * @param {string} machineId - machine identifier
46
+ * @param {string} kind - operation kind
47
+ * @param {{ to?: Date, before?: Date }} bound - inclusive to or exclusive before
48
+ * @returns {Promise<object|null>} latest row or null
49
+ */
50
+ function latestForMachine(pool, machineId, kind, bound) {
51
+ const range = bound || {};
52
+ let sql = `SELECT machine, occurred_at, kind, key, payload
53
+ FROM operations WHERE machine = $1 AND kind = $2`;
54
+ const prm = [machineId, kind];
55
+ if (range.before) {
56
+ prm.push(range.before);
57
+ sql += ` AND occurred_at < $${prm.length}`;
58
+ } else if (range.to) {
59
+ prm.push(range.to);
60
+ sql += ` AND occurred_at <= $${prm.length}`;
61
+ }
62
+ sql += ' ORDER BY occurred_at DESC, key DESC LIMIT 1';
63
+ return pool.query(sql, prm).then((result) => {
64
+ return result.rows[0] || null;
65
+ });
66
+ }
67
+
41
68
  function missing(machineId, key) {
42
69
  return new Error(`operation '${key}' not found for machine '${machineId}'`);
43
70
  }
@@ -70,11 +97,12 @@ async function removeOperation(pool, machineId, key) {
70
97
  * PostgreSQL operations persistence port for generic machine operations.
71
98
  *
72
99
  * @param {object} pool - pg pool
73
- * @returns {object} operations port with upsert, get, remove, and listForMachine
100
+ * @returns {object} operations port with upsert, get, remove, listForMachine, latestForMachine
74
101
  *
75
102
  * @example
76
103
  * const store = operationStatePg(pool);
77
104
  * await store.upsert({ machine: 'm1', key: 'nb-1', kind: 'chem', ... });
105
+ * await store.latestForMachine('m1', 'chem', { to: new Date() });
78
106
  * await store.remove('m1', 'nb-1');
79
107
  */
80
108
  export default function operationStatePg(pool) {
@@ -90,6 +118,9 @@ export default function operationStatePg(pool) {
90
118
  },
91
119
  listForMachine(machineId, kind, range) {
92
120
  return listForMachine(pool, machineId, kind, range);
121
+ },
122
+ latestForMachine(machineId, kind, bound) {
123
+ return latestForMachine(pool, machineId, kind, bound);
93
124
  }
94
125
  };
95
126
  }