@yarkivaev/scada 2.3.50 → 2.3.51

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yarkivaev/scada",
3
- "version": "2.3.50",
3
+ "version": "2.3.51",
4
4
  "description": "SCADA domain objects, state persistence, and plant monitoring",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Normalizes a tags or options field that may be an array or JSON string.
3
+ *
4
+ * @param {unknown} raw - stored field
5
+ * @returns {Array<string>} id list
6
+ */
7
+ function asIds(raw) {
8
+ if (Array.isArray(raw)) {
9
+ return raw.filter((id) => {
10
+ return typeof id === 'string';
11
+ });
12
+ }
13
+ if (typeof raw === 'string' && raw.length > 0) {
14
+ const parsed = JSON.parse(raw);
15
+ return asIds(parsed);
16
+ }
17
+ return [];
18
+ }
19
+
20
+ /**
21
+ * Returns whether requested tags are inside published options or already standing.
22
+ * Empty options mean no restriction so older publishers keep working.
23
+ *
24
+ * @param {unknown} options - published option ids
25
+ * @param {unknown} standing - tags already on the segment
26
+ * @param {unknown} tags - tags the operator wants to write
27
+ * @returns {boolean} true when every requested tag is allowed
28
+ * @example
29
+ * allowedSegmentTags(['load'], [], ['load'])
30
+ */
31
+ export default function allowedSegmentTags(options, standing, tags) {
32
+ const allow = asIds(options);
33
+ if (allow.length === 0) {
34
+ return true;
35
+ }
36
+ const held = new Set(asIds(standing));
37
+ const published = new Set(allow);
38
+ return asIds(tags).every((id) => {
39
+ return published.has(id) || held.has(id);
40
+ });
41
+ }
@@ -1,4 +1,5 @@
1
1
  import machineInPlant from '../../../../application/machineInPlant.js';
2
+ import allowedSegmentTags from '../allowedSegmentTags.js';
2
3
  import segmentJson from '../json/segmentJson.js';
3
4
  import timelineOperator from '../timelineOperator.js';
4
5
  import { errorResponse, jsonResponse, readBody, route, sendRouteError } from '@yarkivaev/simple-server';
@@ -8,6 +9,18 @@ async function handleOperatorWrite(gate, parsed, write) {
8
9
  await write(audit);
9
10
  }
10
11
 
12
+ async function rejectUnknownTags(timeline, parsed, machineId, res) {
13
+ if (typeof timeline.rowAt !== 'function') {
14
+ return false;
15
+ }
16
+ const row = await timeline.rowAt(new Date(parsed.start));
17
+ if (!row || allowedSegmentTags(row.options, row.tags, parsed.tags)) {
18
+ return false;
19
+ }
20
+ errorResponse('BAD_REQUEST', `Tag is not in segment options for ${machineId}`, 400).send(res);
21
+ return true;
22
+ }
23
+
11
24
  async function respondToRequest(gate, timeline, requestId, req, res) {
12
25
  const raw = await readBody(req);
13
26
  const parsed = JSON.parse(raw);
@@ -61,6 +74,9 @@ export default function timelineRoute(basePath, plant, operatorOptions) {
61
74
  try {
62
75
  const raw = await readBody(req);
63
76
  const parsed = JSON.parse(raw);
77
+ if (await rejectUnknownTags(result.machine.timeline, parsed, params.machineId, res)) {
78
+ return;
79
+ }
64
80
  await handleOperatorWrite(gate, parsed, async (audit) => {
65
81
  await result.machine.timeline.retag(new Date(parsed.start), parsed.tags, parsed.properties, audit);
66
82
  });