@yarkivaev/scada 2.3.52 → 2.3.53
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
|
@@ -2,7 +2,8 @@ import machineInPlant from '../../../../application/machineInPlant.js';
|
|
|
2
2
|
import operationJson from '../json/operationJson.js';
|
|
3
3
|
import timelineOperator from '../timelineOperator.js';
|
|
4
4
|
import operationWrites from './operationWrites.js';
|
|
5
|
-
import
|
|
5
|
+
import httpOperations from '../../../messaging/ownership/httpOperations.js';
|
|
6
|
+
import { jsonResponse, route, sendRouteError } from '@yarkivaev/simple-server';
|
|
6
7
|
|
|
7
8
|
function parseRange(query) {
|
|
8
9
|
const range = {};
|
|
@@ -29,11 +30,31 @@ function resolveKinds(query) {
|
|
|
29
30
|
return undefined;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
function edgePort(owners, machineId) {
|
|
34
|
+
if (!owners || typeof owners.resolve !== 'function') {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
const owner = owners.resolve(machineId);
|
|
38
|
+
if (!owner || owner.kind !== 'edge') {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
return httpOperations(owner, machineId);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function listLocal(plant, machineId, query) {
|
|
45
|
+
const rows = await plant.operations.listForMachine(
|
|
46
|
+
machineId,
|
|
47
|
+
resolveKinds(query),
|
|
48
|
+
parseRange(query)
|
|
49
|
+
);
|
|
50
|
+
return { items: rows.map(operationJson) };
|
|
51
|
+
}
|
|
52
|
+
|
|
32
53
|
/**
|
|
33
54
|
* Operations REST routes for machine-scoped reads and writes.
|
|
34
55
|
*
|
|
35
56
|
* Writes resolve operator via timelineOperator and stamp payload.operator.
|
|
36
|
-
* Edge-owned machines proxy create/update/delete to the owning plant API
|
|
57
|
+
* Edge-owned machines proxy list/create/update/delete to the owning plant API
|
|
37
58
|
* (no local upsert or decision insert). Optional owners registry mirrors timeline.
|
|
38
59
|
*
|
|
39
60
|
* @param {string} basePath - base URL path
|
|
@@ -60,12 +81,16 @@ export default function operationRoute(basePath, plant, operatorOptions, decisio
|
|
|
60
81
|
jsonResponse({ items: [] }).send(res);
|
|
61
82
|
return;
|
|
62
83
|
}
|
|
63
|
-
|
|
64
|
-
params.machineId
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
84
|
+
try {
|
|
85
|
+
const port = edgePort(owners, params.machineId);
|
|
86
|
+
const listed = port
|
|
87
|
+
? await port.list(query)
|
|
88
|
+
: await listLocal(plant, params.machineId, query);
|
|
89
|
+
const items = Array.isArray(listed && listed.items) ? listed.items : [];
|
|
90
|
+
jsonResponse({ items }).send(res);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
sendRouteError(res, err);
|
|
93
|
+
}
|
|
69
94
|
}),
|
|
70
95
|
route('POST', `${basePath}/machines/:machineId/operations`, async (req, res, params) => {
|
|
71
96
|
await writes.writeCreate(params.machineId, req, res);
|
|
@@ -2,6 +2,18 @@ function trimBase(url) {
|
|
|
2
2
|
return String(url).replace(/\/$/u, '');
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
+
function listPath(root, query) {
|
|
6
|
+
const params = new URLSearchParams();
|
|
7
|
+
Object.keys(query || {}).forEach((name) => {
|
|
8
|
+
const value = query[name];
|
|
9
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
10
|
+
params.set(name, String(value));
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
const suffix = params.toString();
|
|
14
|
+
return suffix.length > 0 ? `${root}?${suffix}` : root;
|
|
15
|
+
}
|
|
16
|
+
|
|
5
17
|
function authHeaders(token) {
|
|
6
18
|
const headers = { 'Content-Type': 'application/json' };
|
|
7
19
|
if (token) {
|
|
@@ -33,65 +45,77 @@ async function readJson(res) {
|
|
|
33
45
|
return JSON.parse(text);
|
|
34
46
|
}
|
|
35
47
|
|
|
48
|
+
function ownerContext(site, machineId) {
|
|
49
|
+
return {
|
|
50
|
+
base: trimBase(site.baseUrl),
|
|
51
|
+
fetcher: site.fetch || fetch,
|
|
52
|
+
token: site.token,
|
|
53
|
+
machineId
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function send(ctx, method, path, body) {
|
|
58
|
+
const url = `${ctx.base}${path}`;
|
|
59
|
+
let res;
|
|
60
|
+
try {
|
|
61
|
+
res = await ctx.fetcher(url, {
|
|
62
|
+
method,
|
|
63
|
+
headers: authHeaders(ctx.token),
|
|
64
|
+
body: body === undefined ? undefined : JSON.stringify(body)
|
|
65
|
+
});
|
|
66
|
+
} catch (cause) {
|
|
67
|
+
throw ownerError(
|
|
68
|
+
`owner operations unreachable for ${ctx.machineId}: ${cause.message}`,
|
|
69
|
+
'SERVICE_UNAVAILABLE',
|
|
70
|
+
503,
|
|
71
|
+
cause
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
throw ownerError(
|
|
76
|
+
`owner operations ${method} ${path} for ${ctx.machineId} failed: ${res.status} ${await readError(res)}`,
|
|
77
|
+
'BAD_GATEWAY',
|
|
78
|
+
502
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return readJson(res);
|
|
82
|
+
}
|
|
83
|
+
|
|
36
84
|
/**
|
|
37
|
-
* HTTP operations
|
|
85
|
+
* HTTP operations port that proxies list/create/update/delete to an edge plant API.
|
|
38
86
|
*
|
|
39
87
|
* Matches the owning-edge contract: central never upserts locally for edge machines.
|
|
40
88
|
* Failures surface as route errors (503 unreachable, 502 non-ok).
|
|
41
89
|
*
|
|
42
90
|
* @param {object} site - edge owner with baseUrl and optional token/fetch
|
|
43
91
|
* @param {string} machineId - machine identifier
|
|
44
|
-
* @returns {object} operations
|
|
92
|
+
* @returns {object} operations port
|
|
45
93
|
*
|
|
46
94
|
* @example
|
|
47
95
|
* const port = httpOperations({ baseUrl: 'http://edge/api/v1' }, 'm2');
|
|
48
96
|
* await port.create({ kind: 'load', payload: {}, operatorId: 2 });
|
|
49
97
|
*/
|
|
50
98
|
export default function httpOperations(site, machineId) {
|
|
51
|
-
const
|
|
52
|
-
const fetcher = site.fetch || fetch;
|
|
53
|
-
async function send(method, path, body) {
|
|
54
|
-
const url = `${base}${path}`;
|
|
55
|
-
let res;
|
|
56
|
-
try {
|
|
57
|
-
res = await fetcher(url, {
|
|
58
|
-
method,
|
|
59
|
-
headers: authHeaders(site.token),
|
|
60
|
-
body: body === undefined ? undefined : JSON.stringify(body)
|
|
61
|
-
});
|
|
62
|
-
} catch (cause) {
|
|
63
|
-
throw ownerError(
|
|
64
|
-
`owner operations unreachable for ${machineId}: ${cause.message}`,
|
|
65
|
-
'SERVICE_UNAVAILABLE',
|
|
66
|
-
503,
|
|
67
|
-
cause
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
if (!res.ok) {
|
|
71
|
-
throw ownerError(
|
|
72
|
-
`owner operations ${method} ${path} for ${machineId} failed: ${res.status} ${await readError(res)}`,
|
|
73
|
-
'BAD_GATEWAY',
|
|
74
|
-
502
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
return readJson(res);
|
|
78
|
-
}
|
|
99
|
+
const ctx = ownerContext(site, machineId);
|
|
79
100
|
const root = `/machines/${encodeURIComponent(machineId)}/operations`;
|
|
80
101
|
return Object.freeze({
|
|
102
|
+
list(query) {
|
|
103
|
+
return send(ctx, 'GET', listPath(root, query));
|
|
104
|
+
},
|
|
81
105
|
create(body) {
|
|
82
|
-
return send('POST', root, body);
|
|
106
|
+
return send(ctx, 'POST', root, body);
|
|
83
107
|
},
|
|
84
108
|
createMany(body) {
|
|
85
|
-
return send('POST', `${root}/batch`, body);
|
|
109
|
+
return send(ctx, 'POST', `${root}/batch`, body);
|
|
86
110
|
},
|
|
87
111
|
update(key, body) {
|
|
88
|
-
return send('PUT', `${root}/${encodeURIComponent(key)}`, body);
|
|
112
|
+
return send(ctx, 'PUT', `${root}/${encodeURIComponent(key)}`, body);
|
|
89
113
|
},
|
|
90
114
|
remove(key, body) {
|
|
91
|
-
return send('DELETE', `${root}/${encodeURIComponent(key)}`, body);
|
|
115
|
+
return send(ctx, 'DELETE', `${root}/${encodeURIComponent(key)}`, body);
|
|
92
116
|
},
|
|
93
117
|
decisions(key) {
|
|
94
|
-
return send('GET', `${root}/${encodeURIComponent(key)}/decisions`).then((body) => {
|
|
118
|
+
return send(ctx, 'GET', `${root}/${encodeURIComponent(key)}/decisions`).then((body) => {
|
|
95
119
|
return Array.isArray(body && body.items) ? body.items : [];
|
|
96
120
|
});
|
|
97
121
|
}
|