@attraccess/plugin-wago 0.1.0-nightly.925.2 → 0.1.0-nightly.927.1
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/dist/index.js +852 -55
- package/dist/migrations.js +28 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -36,15 +36,17 @@ module.exports = __toCommonJS(plugin_exports);
|
|
|
36
36
|
|
|
37
37
|
// backend/wago.controller.ts
|
|
38
38
|
var import_common2 = require("@nestjs/common");
|
|
39
|
-
var
|
|
39
|
+
var import_plugins_backend_sdk6 = require("@attraccess/plugins-backend-sdk");
|
|
40
40
|
|
|
41
41
|
// backend/wago.service.ts
|
|
42
|
-
var
|
|
42
|
+
var import_node_crypto2 = require("node:crypto");
|
|
43
43
|
var import_common = require("@nestjs/common");
|
|
44
44
|
|
|
45
45
|
// backend/protocol.ts
|
|
46
46
|
var DISCOVERY_ROOT = "attraccess/wago/discovery";
|
|
47
|
-
var
|
|
47
|
+
var CONFIGURATION_PROTOCOL_VERSION = 1;
|
|
48
|
+
var CONFIGURATION_CAPABILITY = `configuration-v${CONFIGURATION_PROTOCOL_VERSION}`;
|
|
49
|
+
var REQUIRED_CAPABILITIES = ["claim", "heartbeat", CONFIGURATION_CAPABILITY];
|
|
48
50
|
var SUPPORTED_PROTOCOL_MAJOR = 1;
|
|
49
51
|
function parseAnnouncement(payload) {
|
|
50
52
|
let value;
|
|
@@ -85,8 +87,38 @@ function compatibilityError(announcement) {
|
|
|
85
87
|
function discoveryTopic(hardwareId) {
|
|
86
88
|
return `${DISCOVERY_ROOT}/${hardwareId}`;
|
|
87
89
|
}
|
|
88
|
-
function heartbeatTopic(hardwareId) {
|
|
89
|
-
return
|
|
90
|
+
function heartbeatTopic(prefix, hardwareId) {
|
|
91
|
+
return `${normalizeOperationalPrefix(prefix)}/v${CONFIGURATION_PROTOCOL_VERSION}/controllers/${hardwareId}/heartbeat`;
|
|
92
|
+
}
|
|
93
|
+
function configurationDesiredTopic(prefix, hardwareId) {
|
|
94
|
+
return `${normalizeOperationalPrefix(prefix)}/v${CONFIGURATION_PROTOCOL_VERSION}/controllers/${hardwareId}/configuration/desired`;
|
|
95
|
+
}
|
|
96
|
+
function configurationReportedTopic(prefix, hardwareId) {
|
|
97
|
+
return `${normalizeOperationalPrefix(prefix)}/v${CONFIGURATION_PROTOCOL_VERSION}/controllers/${hardwareId}/configuration/reported`;
|
|
98
|
+
}
|
|
99
|
+
function commandTopic(prefix, hardwareId) {
|
|
100
|
+
return `${normalizeOperationalPrefix(prefix)}/v${CONFIGURATION_PROTOCOL_VERSION}/controllers/${hardwareId}/commands`;
|
|
101
|
+
}
|
|
102
|
+
function configurationReportedWildcardTopic(prefix) {
|
|
103
|
+
return configurationReportedTopic(prefix, "+");
|
|
104
|
+
}
|
|
105
|
+
function configurationReportedHardwareId(prefix, topic) {
|
|
106
|
+
const topicPrefix = `${normalizeOperationalPrefix(prefix)}/v${CONFIGURATION_PROTOCOL_VERSION}/controllers/`;
|
|
107
|
+
const topicSuffix = "/configuration/reported";
|
|
108
|
+
if (!topic.startsWith(topicPrefix) || !topic.endsWith(topicSuffix)) return null;
|
|
109
|
+
const hardwareId = topic.slice(topicPrefix.length, -topicSuffix.length);
|
|
110
|
+
return hardwareId && !/[+/]/.test(hardwareId) ? hardwareId : null;
|
|
111
|
+
}
|
|
112
|
+
function normalizeOperationalPrefix(prefix) {
|
|
113
|
+
const trimmed = prefix.trim();
|
|
114
|
+
let start = 0;
|
|
115
|
+
let end = trimmed.length;
|
|
116
|
+
while (trimmed[start] === "/") start += 1;
|
|
117
|
+
while (trimmed[end - 1] === "/") end -= 1;
|
|
118
|
+
const normalized = trimmed.slice(start, end);
|
|
119
|
+
if (!normalized || normalized.split("/").some((segment) => !segment || /[+#]/.test(segment)))
|
|
120
|
+
throw new Error("MQTT prefix must contain non-empty segments without wildcards");
|
|
121
|
+
return normalized;
|
|
90
122
|
}
|
|
91
123
|
|
|
92
124
|
// backend/wago-controller.entity.ts
|
|
@@ -173,6 +205,7 @@ var WagoSettings = class {
|
|
|
173
205
|
constructor() {
|
|
174
206
|
__publicField(this, "id");
|
|
175
207
|
__publicField(this, "defaultMqttServerId");
|
|
208
|
+
__publicField(this, "operationalPrefix");
|
|
176
209
|
}
|
|
177
210
|
};
|
|
178
211
|
__decorateClass([
|
|
@@ -181,6 +214,9 @@ __decorateClass([
|
|
|
181
214
|
__decorateClass([
|
|
182
215
|
(0, import_plugins_backend_sdk2.Column)({ type: "integer", name: "default_mqtt_server_id", nullable: true })
|
|
183
216
|
], WagoSettings.prototype, "defaultMqttServerId", 2);
|
|
217
|
+
__decorateClass([
|
|
218
|
+
(0, import_plugins_backend_sdk2.Column)({ type: "varchar", name: "operational_prefix", default: "attraccess/wago" })
|
|
219
|
+
], WagoSettings.prototype, "operationalPrefix", 2);
|
|
184
220
|
WagoSettings = __decorateClass([
|
|
185
221
|
(0, import_plugins_backend_sdk2.Entity)({ name: "plugin_wago_settings" })
|
|
186
222
|
], WagoSettings);
|
|
@@ -231,9 +267,365 @@ WagoEnrollment = __decorateClass([
|
|
|
231
267
|
(0, import_plugins_backend_sdk3.Entity)({ name: "plugin_wago_enrollments" })
|
|
232
268
|
], WagoEnrollment);
|
|
233
269
|
|
|
270
|
+
// backend/configuration.ts
|
|
271
|
+
var import_node_crypto = require("node:crypto");
|
|
272
|
+
var HARDWARE_PROFILES = ["751-9301", "879-3000", "879-1300"];
|
|
273
|
+
var CHANNEL_PROFILES = [
|
|
274
|
+
"metered-switched-load",
|
|
275
|
+
"pulsed-lock-bank",
|
|
276
|
+
"guarded-enable-request",
|
|
277
|
+
"generic-digital-output",
|
|
278
|
+
"generic-monitored-input"
|
|
279
|
+
];
|
|
280
|
+
var CAPABILITIES = ["output", "input", "measurement", "pulse", "guard"];
|
|
281
|
+
function canonicalSnapshot(snapshot) {
|
|
282
|
+
if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot))
|
|
283
|
+
throw new Error("configuration snapshot must be an object");
|
|
284
|
+
return JSON.stringify(sortValue(snapshot));
|
|
285
|
+
}
|
|
286
|
+
function configurationHash(snapshot) {
|
|
287
|
+
return (0, import_node_crypto.createHash)("sha256").update(canonicalSnapshot(snapshot)).digest("hex");
|
|
288
|
+
}
|
|
289
|
+
function configurationDiff(previous, current) {
|
|
290
|
+
const changes = [];
|
|
291
|
+
diffValue("$", previous, current, changes);
|
|
292
|
+
return changes;
|
|
293
|
+
}
|
|
294
|
+
function parseConfigurationReport(value) {
|
|
295
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
296
|
+
const report = value;
|
|
297
|
+
if (!Number.isSafeInteger(report.revision) || report.revision < 1 || typeof report.contentHash !== "string" || !report.contentHash.trim() || report.errors !== void 0 && !Array.isArray(report.errors))
|
|
298
|
+
return null;
|
|
299
|
+
const errors = report.errors ?? [];
|
|
300
|
+
if (!errors.every(isConfigurationValidationError)) return null;
|
|
301
|
+
return { revision: report.revision, contentHash: report.contentHash, errors };
|
|
302
|
+
}
|
|
303
|
+
function validateSnapshot(snapshot) {
|
|
304
|
+
let value;
|
|
305
|
+
try {
|
|
306
|
+
value = JSON.parse(canonicalSnapshot(snapshot));
|
|
307
|
+
} catch (error) {
|
|
308
|
+
return [
|
|
309
|
+
{ path: "$", code: "invalid_snapshot", message: error instanceof Error ? error.message : "invalid snapshot" }
|
|
310
|
+
];
|
|
311
|
+
}
|
|
312
|
+
const errors = [];
|
|
313
|
+
exactKeys(value, "$", ["version", "physicalPoints", "logicalChannels"], errors);
|
|
314
|
+
if (value.version !== CONFIGURATION_PROTOCOL_VERSION)
|
|
315
|
+
errors.push({
|
|
316
|
+
path: "version",
|
|
317
|
+
code: "unsupported_version",
|
|
318
|
+
message: `version must be ${CONFIGURATION_PROTOCOL_VERSION}`
|
|
319
|
+
});
|
|
320
|
+
const points = collection(value.physicalPoints, "physicalPoints", errors);
|
|
321
|
+
const channels = collection(value.logicalChannels, "logicalChannels", errors);
|
|
322
|
+
const pointIds = /* @__PURE__ */ new Set();
|
|
323
|
+
const channelIds = /* @__PURE__ */ new Set();
|
|
324
|
+
points.forEach((point, index) => {
|
|
325
|
+
const path = `physicalPoints[${index}]`;
|
|
326
|
+
if (!record(point, path, errors)) return;
|
|
327
|
+
exactKeys(point, path, ["id", "hardwareProfile", "channel"], errors);
|
|
328
|
+
addId(point.id, `${path}.id`, pointIds, errors);
|
|
329
|
+
enumValue(point.hardwareProfile, `${path}.hardwareProfile`, HARDWARE_PROFILES, errors);
|
|
330
|
+
if (!Number.isSafeInteger(point.channel) || point.channel < 0)
|
|
331
|
+
errors.push({
|
|
332
|
+
path: `${path}.channel`,
|
|
333
|
+
code: "invalid_channel",
|
|
334
|
+
message: "channel must be a non-negative integer"
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
channels.forEach((channel, index) => {
|
|
338
|
+
if (channel && typeof channel === "object" && !Array.isArray(channel))
|
|
339
|
+
addId(channel.id, `logicalChannels[${index}].id`, channelIds, errors);
|
|
340
|
+
});
|
|
341
|
+
channels.forEach((channel, index) => {
|
|
342
|
+
const path = `logicalChannels[${index}]`;
|
|
343
|
+
if (!record(channel, path, errors)) return;
|
|
344
|
+
exactKeys(
|
|
345
|
+
channel,
|
|
346
|
+
path,
|
|
347
|
+
[
|
|
348
|
+
"id",
|
|
349
|
+
"physicalPointId",
|
|
350
|
+
"profile",
|
|
351
|
+
"capabilities",
|
|
352
|
+
"disconnectPolicy",
|
|
353
|
+
"range",
|
|
354
|
+
"pulse",
|
|
355
|
+
"guard",
|
|
356
|
+
"measurement"
|
|
357
|
+
],
|
|
358
|
+
errors
|
|
359
|
+
);
|
|
360
|
+
if (typeof channel.physicalPointId !== "string" || !pointIds.has(channel.physicalPointId))
|
|
361
|
+
errors.push(referenceError(`${path}.physicalPointId`, "physical point", channel.physicalPointId));
|
|
362
|
+
enumValue(channel.profile, `${path}.profile`, CHANNEL_PROFILES, errors);
|
|
363
|
+
const capabilities = capabilityList(channel.capabilities, `${path}.capabilities`, errors);
|
|
364
|
+
validateDisconnectPolicy(channel.disconnectPolicy, `${path}.disconnectPolicy`, errors);
|
|
365
|
+
validateRange(channel.range, `${path}.range`, capabilities, errors);
|
|
366
|
+
validatePulse(channel.pulse, `${path}.pulse`, capabilities, errors);
|
|
367
|
+
validateGuard(channel.guard, `${path}.guard`, capabilities, channelIds, errors);
|
|
368
|
+
validateMeasurement(channel.measurement, `${path}.measurement`, capabilities, errors);
|
|
369
|
+
validateProfile(channel.profile, capabilities, path, errors);
|
|
370
|
+
});
|
|
371
|
+
return errors;
|
|
372
|
+
}
|
|
373
|
+
function diffValue(path, previous, current, changes) {
|
|
374
|
+
if (Object.is(previous, current)) return;
|
|
375
|
+
if (Array.isArray(previous) && Array.isArray(current)) {
|
|
376
|
+
const length = Math.max(previous.length, current.length);
|
|
377
|
+
for (let index = 0; index < length; index += 1)
|
|
378
|
+
diffValue(`${path}[${index}]`, previous[index], current[index], changes);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (isRecord(previous) && isRecord(current)) {
|
|
382
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(previous), ...Object.keys(current)]);
|
|
383
|
+
for (const key of [...keys].sort()) diffValue(`${path}.${key}`, previous[key], current[key], changes);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
changes.push({ path, previous, current });
|
|
387
|
+
}
|
|
388
|
+
function isConfigurationValidationError(value) {
|
|
389
|
+
return isRecord(value) && typeof value.path === "string" && Boolean(value.path.trim()) && typeof value.code === "string" && Boolean(value.code.trim()) && typeof value.message === "string" && Boolean(value.message.trim());
|
|
390
|
+
}
|
|
391
|
+
function isRecord(value) {
|
|
392
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
393
|
+
}
|
|
394
|
+
function collection(value, path, errors) {
|
|
395
|
+
if (!Array.isArray(value)) {
|
|
396
|
+
errors.push({ path, code: "invalid_collection", message: `${path} must be an array` });
|
|
397
|
+
return [];
|
|
398
|
+
}
|
|
399
|
+
return value;
|
|
400
|
+
}
|
|
401
|
+
function record(value, path, errors) {
|
|
402
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return true;
|
|
403
|
+
errors.push({ path, code: "invalid_object", message: `${path} must be an object` });
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
function exactKeys(value, path, allowed, errors, optional = []) {
|
|
407
|
+
Object.keys(value).filter((key) => !allowed.includes(key)).forEach(
|
|
408
|
+
(key) => errors.push({
|
|
409
|
+
path: path === "$" ? key : `${path}.${key}`,
|
|
410
|
+
code: "unknown_field",
|
|
411
|
+
message: "field is not supported by configuration version 1"
|
|
412
|
+
})
|
|
413
|
+
);
|
|
414
|
+
allowed.filter((key) => !["range", "pulse", "guard", "measurement", ...optional].includes(key) && !(key in value)).forEach(
|
|
415
|
+
(key) => errors.push({
|
|
416
|
+
path: path === "$" ? key : `${path}.${key}`,
|
|
417
|
+
code: "required_field",
|
|
418
|
+
message: "field is required"
|
|
419
|
+
})
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
function addId(value, path, ids, errors) {
|
|
423
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
424
|
+
errors.push({ path, code: "invalid_id", message: "id must be a non-empty string" });
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (ids.has(value)) errors.push({ path, code: "duplicate_id", message: `duplicate id ${value}` });
|
|
428
|
+
ids.add(value);
|
|
429
|
+
}
|
|
430
|
+
function enumValue(value, path, options, errors) {
|
|
431
|
+
if (!options.includes(value))
|
|
432
|
+
errors.push({ path, code: "unsupported_value", message: `must be one of: ${options.join(", ")}` });
|
|
433
|
+
}
|
|
434
|
+
function capabilityList(value, path, errors) {
|
|
435
|
+
if (!Array.isArray(value) || !value.length) {
|
|
436
|
+
errors.push({ path, code: "invalid_capabilities", message: "capabilities must be a non-empty array" });
|
|
437
|
+
return /* @__PURE__ */ new Set();
|
|
438
|
+
}
|
|
439
|
+
const capabilities = /* @__PURE__ */ new Set();
|
|
440
|
+
value.forEach((item, index) => {
|
|
441
|
+
if (!CAPABILITIES.includes(item))
|
|
442
|
+
errors.push({
|
|
443
|
+
path: `${path}[${index}]`,
|
|
444
|
+
code: "unsupported_capability",
|
|
445
|
+
message: `must be one of: ${CAPABILITIES.join(", ")}`
|
|
446
|
+
});
|
|
447
|
+
else if (capabilities.has(item))
|
|
448
|
+
errors.push({ path: `${path}[${index}]`, code: "duplicate_capability", message: `duplicate capability ${item}` });
|
|
449
|
+
else capabilities.add(item);
|
|
450
|
+
});
|
|
451
|
+
return capabilities;
|
|
452
|
+
}
|
|
453
|
+
function validateDisconnectPolicy(value, path, errors) {
|
|
454
|
+
if (!record(value, path, errors)) return;
|
|
455
|
+
exactKeys(value, path, ["mode", "timeoutMs"], errors, ["timeoutMs"]);
|
|
456
|
+
if (!["hold", "immediate", "watchdog"].includes(value.mode))
|
|
457
|
+
errors.push({
|
|
458
|
+
path: `${path}.mode`,
|
|
459
|
+
code: "unsupported_value",
|
|
460
|
+
message: "mode must be hold, immediate, or watchdog"
|
|
461
|
+
});
|
|
462
|
+
if (value.mode === "watchdog" && (!Number.isSafeInteger(value.timeoutMs) || value.timeoutMs <= 0))
|
|
463
|
+
errors.push({
|
|
464
|
+
path: `${path}.timeoutMs`,
|
|
465
|
+
code: "required_field",
|
|
466
|
+
message: "watchdog timeoutMs must be a positive integer"
|
|
467
|
+
});
|
|
468
|
+
if (value.mode !== "watchdog" && value.timeoutMs !== void 0)
|
|
469
|
+
errors.push({
|
|
470
|
+
path: `${path}.timeoutMs`,
|
|
471
|
+
code: "unsupported_field",
|
|
472
|
+
message: "timeoutMs is only valid for watchdog policies"
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
function validateRange(value, path, capabilities, errors) {
|
|
476
|
+
if (value === void 0) return;
|
|
477
|
+
if (!record(value, path, errors)) return;
|
|
478
|
+
exactKeys(value, path, ["minimum", "maximum"], errors);
|
|
479
|
+
if (!Number.isFinite(value.minimum) || !Number.isFinite(value.maximum) || value.minimum >= value.maximum)
|
|
480
|
+
errors.push({
|
|
481
|
+
path,
|
|
482
|
+
code: "invalid_range",
|
|
483
|
+
message: "minimum and maximum must be finite numbers where minimum is less than maximum"
|
|
484
|
+
});
|
|
485
|
+
if (!capabilities.has("input") && !capabilities.has("measurement"))
|
|
486
|
+
errors.push({ path, code: "unsupported_field", message: "range requires input or measurement capability" });
|
|
487
|
+
}
|
|
488
|
+
function validatePulse(value, path, capabilities, errors) {
|
|
489
|
+
if (value === void 0) return;
|
|
490
|
+
if (!record(value, path, errors)) return;
|
|
491
|
+
exactKeys(value, path, ["durationMs"], errors);
|
|
492
|
+
if (!Number.isSafeInteger(value.durationMs) || value.durationMs <= 0)
|
|
493
|
+
errors.push({
|
|
494
|
+
path: `${path}.durationMs`,
|
|
495
|
+
code: "invalid_duration",
|
|
496
|
+
message: "durationMs must be a positive integer"
|
|
497
|
+
});
|
|
498
|
+
if (!capabilities.has("pulse"))
|
|
499
|
+
errors.push({ path, code: "unsupported_field", message: "pulse requires pulse capability" });
|
|
500
|
+
}
|
|
501
|
+
function validateGuard(value, path, capabilities, channelIds, errors) {
|
|
502
|
+
if (value === void 0) return;
|
|
503
|
+
if (!record(value, path, errors)) return;
|
|
504
|
+
exactKeys(value, path, ["channelId", "when"], errors);
|
|
505
|
+
if (typeof value.channelId !== "string" || !channelIds.has(value.channelId))
|
|
506
|
+
errors.push(referenceError(`${path}.channelId`, "logical channel", value.channelId));
|
|
507
|
+
if (!["on", "off"].includes(value.when))
|
|
508
|
+
errors.push({ path: `${path}.when`, code: "unsupported_value", message: "when must be on or off" });
|
|
509
|
+
if (!capabilities.has("guard"))
|
|
510
|
+
errors.push({ path, code: "unsupported_field", message: "guard requires guard capability" });
|
|
511
|
+
}
|
|
512
|
+
function validateMeasurement(value, path, capabilities, errors) {
|
|
513
|
+
if (value === void 0) return;
|
|
514
|
+
if (!record(value, path, errors)) return;
|
|
515
|
+
exactKeys(value, path, ["unit", "scale", "offset"], errors);
|
|
516
|
+
enumValue(value.unit, `${path}.unit`, ["ampere", "volt", "watt", "percent"], errors);
|
|
517
|
+
if (!Number.isFinite(value.scale) || !Number.isFinite(value.offset))
|
|
518
|
+
errors.push({ path, code: "invalid_measurement", message: "scale and offset must be finite numbers" });
|
|
519
|
+
if (!capabilities.has("measurement"))
|
|
520
|
+
errors.push({ path, code: "unsupported_field", message: "measurement requires measurement capability" });
|
|
521
|
+
}
|
|
522
|
+
function validateProfile(value, capabilities, path, errors) {
|
|
523
|
+
const required = {
|
|
524
|
+
"metered-switched-load": ["output", "measurement"],
|
|
525
|
+
"pulsed-lock-bank": ["output", "pulse"],
|
|
526
|
+
"guarded-enable-request": ["output", "guard"],
|
|
527
|
+
"generic-digital-output": ["output"],
|
|
528
|
+
"generic-monitored-input": ["input"]
|
|
529
|
+
};
|
|
530
|
+
required[value]?.filter((capability) => !capabilities.has(capability)).forEach(
|
|
531
|
+
(capability) => errors.push({
|
|
532
|
+
path: `${path}.capabilities`,
|
|
533
|
+
code: "missing_capability",
|
|
534
|
+
message: `${value} requires ${capability} capability`
|
|
535
|
+
})
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
function referenceError(path, type, id) {
|
|
539
|
+
return { path, code: "missing_reference", message: `${type} ${String(id)} does not exist in this snapshot` };
|
|
540
|
+
}
|
|
541
|
+
function sortValue(value) {
|
|
542
|
+
if (Array.isArray(value)) return value.map(sortValue);
|
|
543
|
+
if (!value || typeof value !== "object") return value;
|
|
544
|
+
return Object.fromEntries(
|
|
545
|
+
Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, sortValue(item)])
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// backend/wago-configuration-draft.entity.ts
|
|
550
|
+
var import_plugins_backend_sdk4 = require("@attraccess/plugins-backend-sdk");
|
|
551
|
+
var WagoConfigurationDraft = class {
|
|
552
|
+
constructor() {
|
|
553
|
+
__publicField(this, "id");
|
|
554
|
+
__publicField(this, "controllerId");
|
|
555
|
+
__publicField(this, "snapshot");
|
|
556
|
+
__publicField(this, "reviewedHash");
|
|
557
|
+
__publicField(this, "updatedAt");
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
__decorateClass([
|
|
561
|
+
(0, import_plugins_backend_sdk4.PrimaryGeneratedColumn)({ type: "integer" })
|
|
562
|
+
], WagoConfigurationDraft.prototype, "id", 2);
|
|
563
|
+
__decorateClass([
|
|
564
|
+
(0, import_plugins_backend_sdk4.Column)({ type: "integer", name: "controller_id", unique: true })
|
|
565
|
+
], WagoConfigurationDraft.prototype, "controllerId", 2);
|
|
566
|
+
__decorateClass([
|
|
567
|
+
(0, import_plugins_backend_sdk4.Column)({ type: "text" })
|
|
568
|
+
], WagoConfigurationDraft.prototype, "snapshot", 2);
|
|
569
|
+
__decorateClass([
|
|
570
|
+
(0, import_plugins_backend_sdk4.Column)({ type: "varchar", name: "reviewed_hash", nullable: true })
|
|
571
|
+
], WagoConfigurationDraft.prototype, "reviewedHash", 2);
|
|
572
|
+
__decorateClass([
|
|
573
|
+
(0, import_plugins_backend_sdk4.Column)({ type: "varchar", name: "updated_at" })
|
|
574
|
+
], WagoConfigurationDraft.prototype, "updatedAt", 2);
|
|
575
|
+
WagoConfigurationDraft = __decorateClass([
|
|
576
|
+
(0, import_plugins_backend_sdk4.Entity)({ name: "plugin_wago_configuration_drafts" })
|
|
577
|
+
], WagoConfigurationDraft);
|
|
578
|
+
|
|
579
|
+
// backend/wago-configuration-revision.entity.ts
|
|
580
|
+
var import_plugins_backend_sdk5 = require("@attraccess/plugins-backend-sdk");
|
|
581
|
+
var WagoConfigurationRevision = class {
|
|
582
|
+
constructor() {
|
|
583
|
+
__publicField(this, "id");
|
|
584
|
+
__publicField(this, "controllerId");
|
|
585
|
+
__publicField(this, "revision");
|
|
586
|
+
__publicField(this, "snapshot");
|
|
587
|
+
__publicField(this, "contentHash");
|
|
588
|
+
__publicField(this, "state");
|
|
589
|
+
__publicField(this, "rejectionErrors");
|
|
590
|
+
__publicField(this, "publishedAt");
|
|
591
|
+
__publicField(this, "reportedAt");
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
__decorateClass([
|
|
595
|
+
(0, import_plugins_backend_sdk5.PrimaryGeneratedColumn)({ type: "integer" })
|
|
596
|
+
], WagoConfigurationRevision.prototype, "id", 2);
|
|
597
|
+
__decorateClass([
|
|
598
|
+
(0, import_plugins_backend_sdk5.Column)({ type: "integer", name: "controller_id" })
|
|
599
|
+
], WagoConfigurationRevision.prototype, "controllerId", 2);
|
|
600
|
+
__decorateClass([
|
|
601
|
+
(0, import_plugins_backend_sdk5.Column)({ type: "integer" })
|
|
602
|
+
], WagoConfigurationRevision.prototype, "revision", 2);
|
|
603
|
+
__decorateClass([
|
|
604
|
+
(0, import_plugins_backend_sdk5.Column)({ type: "text" })
|
|
605
|
+
], WagoConfigurationRevision.prototype, "snapshot", 2);
|
|
606
|
+
__decorateClass([
|
|
607
|
+
(0, import_plugins_backend_sdk5.Column)({ type: "varchar", name: "content_hash" })
|
|
608
|
+
], WagoConfigurationRevision.prototype, "contentHash", 2);
|
|
609
|
+
__decorateClass([
|
|
610
|
+
(0, import_plugins_backend_sdk5.Column)({ type: "varchar" })
|
|
611
|
+
], WagoConfigurationRevision.prototype, "state", 2);
|
|
612
|
+
__decorateClass([
|
|
613
|
+
(0, import_plugins_backend_sdk5.Column)({ type: "text", name: "rejection_errors", nullable: true })
|
|
614
|
+
], WagoConfigurationRevision.prototype, "rejectionErrors", 2);
|
|
615
|
+
__decorateClass([
|
|
616
|
+
(0, import_plugins_backend_sdk5.Column)({ type: "varchar", name: "published_at" })
|
|
617
|
+
], WagoConfigurationRevision.prototype, "publishedAt", 2);
|
|
618
|
+
__decorateClass([
|
|
619
|
+
(0, import_plugins_backend_sdk5.Column)({ type: "varchar", name: "reported_at", nullable: true })
|
|
620
|
+
], WagoConfigurationRevision.prototype, "reportedAt", 2);
|
|
621
|
+
WagoConfigurationRevision = __decorateClass([
|
|
622
|
+
(0, import_plugins_backend_sdk5.Entity)({ name: "plugin_wago_configuration_revisions" })
|
|
623
|
+
], WagoConfigurationRevision);
|
|
624
|
+
|
|
234
625
|
// backend/wago.service.ts
|
|
235
626
|
var PLUGIN_CONTEXT = /* @__PURE__ */ Symbol.for("attraccess.plugin.context");
|
|
236
627
|
var STALE_AFTER_MS = 9e4;
|
|
628
|
+
var MAX_PENDING_CONFIGURATION_REPORTS = 100;
|
|
237
629
|
var ENROLLMENT_RETRY_MS = 6e4;
|
|
238
630
|
var WagoService = class {
|
|
239
631
|
constructor(context) {
|
|
@@ -241,9 +633,14 @@ var WagoService = class {
|
|
|
241
633
|
__publicField(this, "controllers");
|
|
242
634
|
__publicField(this, "settings");
|
|
243
635
|
__publicField(this, "enrollments");
|
|
636
|
+
__publicField(this, "drafts");
|
|
637
|
+
__publicField(this, "revisions");
|
|
244
638
|
__publicField(this, "subscriptions", []);
|
|
245
639
|
__publicField(this, "enrollmentExpiryTimers", /* @__PURE__ */ new Map());
|
|
246
640
|
__publicField(this, "claimLocks", /* @__PURE__ */ new Map());
|
|
641
|
+
__publicField(this, "configurationLocks", /* @__PURE__ */ new Map());
|
|
642
|
+
__publicField(this, "configurationReportQueues", /* @__PURE__ */ new Map());
|
|
643
|
+
__publicField(this, "claimConfigurationLock", Promise.resolve());
|
|
247
644
|
__publicField(this, "subscriptionRebuild", Promise.resolve());
|
|
248
645
|
__publicField(this, "subscriptionRetryTimer", null);
|
|
249
646
|
__publicField(this, "activeSubscriptionGeneration", 0);
|
|
@@ -251,6 +648,8 @@ var WagoService = class {
|
|
|
251
648
|
this.controllers = context.getRepository(WagoController);
|
|
252
649
|
this.settings = context.getRepository(WagoSettings);
|
|
253
650
|
this.enrollments = context.getRepository(WagoEnrollment);
|
|
651
|
+
this.drafts = context.getRepository(WagoConfigurationDraft);
|
|
652
|
+
this.revisions = context.getRepository(WagoConfigurationRevision);
|
|
254
653
|
}
|
|
255
654
|
async onModuleInit() {
|
|
256
655
|
const enrollments = await this.enrollments.createQueryBuilder("enrollment").where("enrollment.consumedAt IS NULL").getMany();
|
|
@@ -288,18 +687,144 @@ var WagoService = class {
|
|
|
288
687
|
async getSettings() {
|
|
289
688
|
const settings = await this.settings.findOneBy({ id: 1 });
|
|
290
689
|
if (settings) return settings;
|
|
291
|
-
await this.settings.createQueryBuilder().insert().values({ id: 1, defaultMqttServerId: null }).orIgnore().execute();
|
|
690
|
+
await this.settings.createQueryBuilder().insert().values({ id: 1, defaultMqttServerId: null, operationalPrefix: "attraccess/wago" }).orIgnore().execute();
|
|
292
691
|
return this.settings.findOneByOrFail({ id: 1 });
|
|
293
692
|
}
|
|
294
|
-
async
|
|
295
|
-
if (serverId !== null && !await this.context.getMqttServerConfig(serverId))
|
|
693
|
+
async setSettings(serverId, operationalPrefix) {
|
|
694
|
+
if (serverId !== void 0 && serverId !== null && !await this.context.getMqttServerConfig(serverId))
|
|
296
695
|
throw new import_common.NotFoundException(`MQTT server ${serverId} not found`);
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
696
|
+
const save = async () => {
|
|
697
|
+
const settings2 = await this.getSettings();
|
|
698
|
+
if (serverId !== void 0) settings2.defaultMqttServerId = serverId;
|
|
699
|
+
if (operationalPrefix !== void 0) {
|
|
700
|
+
const normalizedPrefix = normalizeOperationalPrefix(operationalPrefix);
|
|
701
|
+
if (normalizedPrefix !== settings2.operationalPrefix) {
|
|
702
|
+
const controllers = await this.controllers.find({ where: { trustState: "claimed" } });
|
|
703
|
+
if (controllers.length)
|
|
704
|
+
throw new import_common.ConflictException("operational MQTT prefix cannot change after a controller has been claimed");
|
|
705
|
+
settings2.operationalPrefix = normalizedPrefix;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
await this.settings.save(settings2);
|
|
709
|
+
return settings2;
|
|
710
|
+
};
|
|
711
|
+
const settings = operationalPrefix === void 0 ? await save() : await this.withClaimConfigurationLock(save);
|
|
300
712
|
await this.subscribeConfiguredServers();
|
|
301
713
|
return settings;
|
|
302
714
|
}
|
|
715
|
+
async setDefaultMqttServer(serverId) {
|
|
716
|
+
return this.setSettings(serverId);
|
|
717
|
+
}
|
|
718
|
+
async getDraft(controllerId) {
|
|
719
|
+
await this.claimedController(controllerId);
|
|
720
|
+
return this.drafts.findOneBy({ controllerId });
|
|
721
|
+
}
|
|
722
|
+
async saveDraft(controllerId, snapshot) {
|
|
723
|
+
return this.withConfigurationLock(controllerId, () => this.saveDraftWhileLocked(controllerId, snapshot));
|
|
724
|
+
}
|
|
725
|
+
async validateDraft(controllerId) {
|
|
726
|
+
const draft = await this.getDraft(controllerId);
|
|
727
|
+
if (!draft) throw new import_common.NotFoundException(`WAGO controller ${controllerId} has no configuration draft`);
|
|
728
|
+
const errors = validateSnapshot(JSON.parse(draft.snapshot));
|
|
729
|
+
return { valid: errors.length === 0, errors };
|
|
730
|
+
}
|
|
731
|
+
async revisionsFor(controllerId, offset = 0, limit = 20) {
|
|
732
|
+
await this.claimedController(controllerId);
|
|
733
|
+
const pageOffset = Number.isSafeInteger(offset) && offset > 0 ? offset : 0;
|
|
734
|
+
const pageLimit = Number.isSafeInteger(limit) && limit > 0 ? Math.min(limit, 100) : 20;
|
|
735
|
+
const revisions = await this.revisions.find({
|
|
736
|
+
where: { controllerId },
|
|
737
|
+
order: { revision: "DESC" },
|
|
738
|
+
select: [
|
|
739
|
+
"id",
|
|
740
|
+
"controllerId",
|
|
741
|
+
"revision",
|
|
742
|
+
"contentHash",
|
|
743
|
+
"state",
|
|
744
|
+
"rejectionErrors",
|
|
745
|
+
"publishedAt",
|
|
746
|
+
"reportedAt"
|
|
747
|
+
],
|
|
748
|
+
skip: pageOffset,
|
|
749
|
+
take: pageLimit
|
|
750
|
+
});
|
|
751
|
+
return { revisions, offset: pageOffset, limit: pageLimit };
|
|
752
|
+
}
|
|
753
|
+
async reviewDraft(controllerId) {
|
|
754
|
+
return this.withConfigurationLock(controllerId, () => this.reviewDraftWhileLocked(controllerId));
|
|
755
|
+
}
|
|
756
|
+
async publishDraft(controllerId) {
|
|
757
|
+
return this.withConfigurationLock(controllerId, () => this.publishDraftWhileLocked(controllerId));
|
|
758
|
+
}
|
|
759
|
+
async rollback(controllerId, revision) {
|
|
760
|
+
return this.withConfigurationLock(controllerId, async () => {
|
|
761
|
+
const source = await this.revisions.findOneBy({ controllerId, revision });
|
|
762
|
+
if (!source) throw new import_common.NotFoundException(`WAGO configuration revision ${revision} not found`);
|
|
763
|
+
await this.saveDraftWhileLocked(controllerId, JSON.parse(source.snapshot));
|
|
764
|
+
await this.reviewDraftWhileLocked(controllerId);
|
|
765
|
+
return this.publishDraftWhileLocked(controllerId);
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
async previewRevision(controllerId, revision) {
|
|
769
|
+
await this.claimedController(controllerId);
|
|
770
|
+
const selected = await this.revisions.findOneBy({ controllerId, revision });
|
|
771
|
+
if (!selected) throw new import_common.NotFoundException(`WAGO configuration revision ${revision} not found`);
|
|
772
|
+
const [current] = await this.revisions.find({ where: { controllerId }, order: { revision: "DESC" }, take: 1 });
|
|
773
|
+
return {
|
|
774
|
+
revision: selected,
|
|
775
|
+
current: current ?? null,
|
|
776
|
+
diff: configurationDiff(current ? JSON.parse(current.snapshot) : null, JSON.parse(selected.snapshot))
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
async saveDraftWhileLocked(controllerId, snapshot) {
|
|
780
|
+
await this.claimedController(controllerId);
|
|
781
|
+
const serialized = canonicalSnapshot(snapshot);
|
|
782
|
+
const existing = await this.drafts.findOneBy({ controllerId });
|
|
783
|
+
const draft = existing ?? this.drafts.create({ controllerId, snapshot: serialized, reviewedHash: null, updatedAt: "" });
|
|
784
|
+
draft.snapshot = serialized;
|
|
785
|
+
draft.reviewedHash = null;
|
|
786
|
+
draft.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
787
|
+
return this.drafts.save(draft);
|
|
788
|
+
}
|
|
789
|
+
async reviewDraftWhileLocked(controllerId) {
|
|
790
|
+
await this.claimedController(controllerId);
|
|
791
|
+
const draft = await this.drafts.findOneBy({ controllerId });
|
|
792
|
+
if (!draft) throw new import_common.NotFoundException(`WAGO controller ${controllerId} has no configuration draft`);
|
|
793
|
+
const previous = await this.latestRevision(controllerId);
|
|
794
|
+
draft.reviewedHash = configurationHash(JSON.parse(draft.snapshot));
|
|
795
|
+
await this.drafts.save(draft);
|
|
796
|
+
const diff = configurationDiff(previous ? JSON.parse(previous.snapshot) : null, JSON.parse(draft.snapshot));
|
|
797
|
+
return { draft, previous, changed: diff.length > 0, diff };
|
|
798
|
+
}
|
|
799
|
+
async publishDraftWhileLocked(controllerId) {
|
|
800
|
+
const controller = await this.claimedController(controllerId);
|
|
801
|
+
const draft = await this.drafts.findOneBy({ controllerId });
|
|
802
|
+
if (!draft) throw new import_common.NotFoundException(`WAGO controller ${controllerId} has no configuration draft`);
|
|
803
|
+
const validation = validateSnapshot(JSON.parse(draft.snapshot));
|
|
804
|
+
if (validation.length)
|
|
805
|
+
throw new import_common.ConflictException({ message: "configuration draft is invalid", errors: validation });
|
|
806
|
+
const contentHash = configurationHash(JSON.parse(draft.snapshot));
|
|
807
|
+
if (draft.reviewedHash !== contentHash)
|
|
808
|
+
throw new import_common.ConflictException("review the current configuration draft before publishing it");
|
|
809
|
+
const previous = await this.latestRevision(controllerId);
|
|
810
|
+
if (previous?.state === "pending" && previous.contentHash === contentHash)
|
|
811
|
+
return this.publishRevision(controller, previous);
|
|
812
|
+
const revision = this.revisions.create({
|
|
813
|
+
controllerId,
|
|
814
|
+
revision: (previous?.revision ?? 0) + 1,
|
|
815
|
+
snapshot: draft.snapshot,
|
|
816
|
+
contentHash,
|
|
817
|
+
state: "pending",
|
|
818
|
+
rejectionErrors: null,
|
|
819
|
+
publishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
820
|
+
reportedAt: null
|
|
821
|
+
});
|
|
822
|
+
return this.publishRevision(controller, await this.revisions.save(revision));
|
|
823
|
+
}
|
|
824
|
+
async latestRevision(controllerId) {
|
|
825
|
+
const [revision] = await this.revisions.find({ where: { controllerId }, order: { revision: "DESC" }, take: 1 });
|
|
826
|
+
return revision ?? null;
|
|
827
|
+
}
|
|
303
828
|
async createEnrollment(hardwareId, mqttServerId, manualCredentials) {
|
|
304
829
|
const normalizedHardwareId = hardwareId.trim();
|
|
305
830
|
if (!isValidHardwareId(normalizedHardwareId))
|
|
@@ -308,8 +833,8 @@ var WagoService = class {
|
|
|
308
833
|
if (!selectedServerId) throw new import_common.ConflictException("select an MQTT server before creating an enrollment package");
|
|
309
834
|
const server = await this.context.getMqttServerConfig(selectedServerId);
|
|
310
835
|
if (!server) throw new import_common.NotFoundException(`MQTT server ${selectedServerId} not found`);
|
|
311
|
-
const claimSecret = (0,
|
|
312
|
-
const identity = `wago-enrollment-${(0,
|
|
836
|
+
const claimSecret = (0, import_node_crypto2.randomBytes)(24).toString("base64url");
|
|
837
|
+
const identity = `wago-enrollment-${(0, import_node_crypto2.randomBytes)(8).toString("hex")}`;
|
|
313
838
|
const expiresAt = new Date(Date.now() + 15 * 6e4).toISOString();
|
|
314
839
|
const provisionedCredential = await this.context.getMqttCredentialProvisioning().provision({
|
|
315
840
|
mqttServerId: selectedServerId,
|
|
@@ -355,9 +880,37 @@ var WagoService = class {
|
|
|
355
880
|
};
|
|
356
881
|
}
|
|
357
882
|
async claim(id, name, verifier, mqttServerId) {
|
|
358
|
-
return this.withClaimLock(id, () =>
|
|
883
|
+
return this.withClaimLock(id, async () => {
|
|
884
|
+
const prepared = await this.withClaimConfigurationLock(() => this.prepareClaim(id, name, verifier, mqttServerId));
|
|
885
|
+
try {
|
|
886
|
+
await this.context.mqtt.publish(
|
|
887
|
+
prepared.mqttServerId,
|
|
888
|
+
`${discoveryTopic(prepared.controller.hardwareId)}/claim`,
|
|
889
|
+
JSON.stringify({
|
|
890
|
+
username: prepared.credential.username,
|
|
891
|
+
password: prepared.credential.password,
|
|
892
|
+
configuration: prepared.configuration
|
|
893
|
+
}),
|
|
894
|
+
{ qos: 1 }
|
|
895
|
+
);
|
|
896
|
+
prepared.credentialDelivered = true;
|
|
897
|
+
await this.context.mqtt.publish(prepared.mqttServerId, discoveryTopic(prepared.controller.hardwareId), "", {
|
|
898
|
+
qos: 1,
|
|
899
|
+
retain: true
|
|
900
|
+
});
|
|
901
|
+
return prepared.controller;
|
|
902
|
+
} catch (error) {
|
|
903
|
+
if (!prepared.credentialDelivered) await this.restoreUnclaimedController(prepared);
|
|
904
|
+
throw error;
|
|
905
|
+
} finally {
|
|
906
|
+
await this.subscribeConfiguredServers().catch((error) => {
|
|
907
|
+
this.context.logger.warn(`Could not refresh WAGO MQTT subscriptions after claim: ${String(error)}`);
|
|
908
|
+
this.scheduleSubscriptionRetry();
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
});
|
|
359
912
|
}
|
|
360
|
-
async
|
|
913
|
+
async prepareClaim(id, name, verifier, mqttServerId) {
|
|
361
914
|
const controller = await this.controllers.findOneBy({ id });
|
|
362
915
|
if (!controller) throw new import_common.NotFoundException(`WAGO controller ${id} not found`);
|
|
363
916
|
if (controller.trustState === "claimed") throw new import_common.ConflictException("controller has already been claimed");
|
|
@@ -377,20 +930,26 @@ var WagoService = class {
|
|
|
377
930
|
"the controller enrollment package has expired or was already consumed; create a new one"
|
|
378
931
|
);
|
|
379
932
|
const identity = `wago-controller-${controller.hardwareId}`;
|
|
933
|
+
const settings = await this.getSettings();
|
|
934
|
+
const namespace = normalizeOperationalPrefix(settings.operationalPrefix);
|
|
380
935
|
const credential = await this.context.getMqttCredentialProvisioning().provision({
|
|
381
936
|
mqttServerId: selectedServerId,
|
|
382
937
|
identity,
|
|
383
938
|
username: identity,
|
|
384
939
|
vhost: "/",
|
|
385
940
|
topicPolicy: {
|
|
386
|
-
publish: [
|
|
387
|
-
|
|
941
|
+
publish: [
|
|
942
|
+
`${namespace}/v${CONFIGURATION_PROTOCOL_VERSION}/controllers/${controller.hardwareId}/#`
|
|
943
|
+
],
|
|
944
|
+
subscribe: [
|
|
945
|
+
configurationDesiredTopic(namespace, controller.hardwareId),
|
|
946
|
+
commandTopic(namespace, controller.hardwareId)
|
|
947
|
+
]
|
|
388
948
|
}
|
|
389
949
|
});
|
|
390
950
|
if (!("password" in credential)) {
|
|
391
951
|
throw new import_common.ConflictException(`Manual credential provisioning is required: ${credential.instructions.join(" ")}`);
|
|
392
952
|
}
|
|
393
|
-
let credentialDelivered = false;
|
|
394
953
|
const previousController = {
|
|
395
954
|
trustState: controller.trustState,
|
|
396
955
|
name: controller.name,
|
|
@@ -404,36 +963,59 @@ var WagoService = class {
|
|
|
404
963
|
controller.mqttServerId = selectedServerId;
|
|
405
964
|
controller.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
406
965
|
await this.controllers.save(controller);
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
966
|
+
return {
|
|
967
|
+
controller,
|
|
968
|
+
mqttServerId: selectedServerId,
|
|
969
|
+
credential,
|
|
970
|
+
configuration: {
|
|
971
|
+
protocolVersion: CONFIGURATION_PROTOCOL_VERSION,
|
|
972
|
+
namespace,
|
|
973
|
+
desiredTopic: configurationDesiredTopic(namespace, controller.hardwareId),
|
|
974
|
+
reportedTopic: configurationReportedTopic(namespace, controller.hardwareId)
|
|
975
|
+
},
|
|
976
|
+
identity,
|
|
977
|
+
previousController,
|
|
978
|
+
credentialDelivered: false
|
|
979
|
+
};
|
|
419
980
|
} catch (error) {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
`Could not restore WAGO controller ${controller.id} after claim failure: ${String(rollbackError)}`
|
|
426
|
-
);
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
throw error;
|
|
430
|
-
} finally {
|
|
431
|
-
await this.subscribeConfiguredServers().catch((error) => {
|
|
432
|
-
this.context.logger.warn(`Could not refresh WAGO MQTT subscriptions after claim: ${String(error)}`);
|
|
433
|
-
this.scheduleSubscriptionRetry();
|
|
981
|
+
await this.restoreUnclaimedControllerWhileLocked({
|
|
982
|
+
controller,
|
|
983
|
+
mqttServerId: selectedServerId,
|
|
984
|
+
identity,
|
|
985
|
+
previousController
|
|
434
986
|
});
|
|
987
|
+
throw error;
|
|
435
988
|
}
|
|
436
989
|
}
|
|
990
|
+
async restoreUnclaimedController({
|
|
991
|
+
controller,
|
|
992
|
+
mqttServerId,
|
|
993
|
+
identity,
|
|
994
|
+
previousController
|
|
995
|
+
}) {
|
|
996
|
+
await this.withClaimConfigurationLock(
|
|
997
|
+
() => this.restoreUnclaimedControllerWhileLocked({
|
|
998
|
+
controller,
|
|
999
|
+
mqttServerId,
|
|
1000
|
+
identity,
|
|
1001
|
+
previousController
|
|
1002
|
+
})
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
async restoreUnclaimedControllerWhileLocked({
|
|
1006
|
+
controller,
|
|
1007
|
+
mqttServerId,
|
|
1008
|
+
identity,
|
|
1009
|
+
previousController
|
|
1010
|
+
}) {
|
|
1011
|
+
await this.context.getMqttCredentialProvisioning().revoke({ mqttServerId, identity, username: identity, vhost: "/" }).catch(() => void 0);
|
|
1012
|
+
Object.assign(controller, previousController);
|
|
1013
|
+
await this.controllers.save(controller).catch((rollbackError) => {
|
|
1014
|
+
this.context.logger.warn(
|
|
1015
|
+
`Could not restore WAGO controller ${controller.id} after claim failure: ${String(rollbackError)}`
|
|
1016
|
+
);
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
437
1019
|
async subscribeConfiguredServers() {
|
|
438
1020
|
if (this.destroyed) return;
|
|
439
1021
|
const rebuild = this.subscriptionRebuild.then(() => this.rebuildSubscriptions());
|
|
@@ -463,15 +1045,47 @@ var WagoService = class {
|
|
|
463
1045
|
await this.onDiscovery(serverId, message.topic, message.payload);
|
|
464
1046
|
})
|
|
465
1047
|
);
|
|
466
|
-
|
|
1048
|
+
if (this.destroyed) {
|
|
1049
|
+
replacements.forEach((subscription) => subscription.unsubscribe());
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
const claimedControllers = controllers.filter(
|
|
467
1053
|
(item) => item.trustState === "claimed" && (item.mqttServerId ?? settings.defaultMqttServerId) === serverId
|
|
468
|
-
)
|
|
469
|
-
|
|
470
|
-
|
|
1054
|
+
);
|
|
1055
|
+
const controllersByHardwareId = new Map(
|
|
1056
|
+
claimedControllers.map((controller) => [controller.hardwareId, controller])
|
|
1057
|
+
);
|
|
1058
|
+
replacements.push(
|
|
1059
|
+
await this.context.mqtt.subscribe(
|
|
1060
|
+
serverId,
|
|
1061
|
+
configurationReportedWildcardTopic(settings.operationalPrefix),
|
|
1062
|
+
(message) => {
|
|
471
1063
|
if (!this.isActiveSubscriptionGeneration(generation)) return;
|
|
472
|
-
|
|
473
|
-
|
|
1064
|
+
const hardwareId = configurationReportedHardwareId(settings.operationalPrefix, message.topic);
|
|
1065
|
+
const controller = hardwareId ? controllersByHardwareId.get(hardwareId) : void 0;
|
|
1066
|
+
if (controller) this.enqueueConfigurationReport(controller.id, message.payload);
|
|
1067
|
+
}
|
|
1068
|
+
)
|
|
1069
|
+
);
|
|
1070
|
+
if (this.destroyed) {
|
|
1071
|
+
replacements.forEach((subscription) => subscription.unsubscribe());
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
for (const controller of claimedControllers) {
|
|
1075
|
+
replacements.push(
|
|
1076
|
+
await this.context.mqtt.subscribe(
|
|
1077
|
+
serverId,
|
|
1078
|
+
heartbeatTopic(settings.operationalPrefix, controller.hardwareId),
|
|
1079
|
+
async (message) => {
|
|
1080
|
+
if (!this.isActiveSubscriptionGeneration(generation)) return;
|
|
1081
|
+
await this.onHeartbeat(controller.hardwareId, message.payload);
|
|
1082
|
+
}
|
|
1083
|
+
)
|
|
474
1084
|
);
|
|
1085
|
+
if (this.destroyed) {
|
|
1086
|
+
replacements.forEach((subscription) => subscription.unsubscribe());
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
475
1089
|
}
|
|
476
1090
|
}
|
|
477
1091
|
} catch (error) {
|
|
@@ -572,6 +1186,91 @@ var WagoService = class {
|
|
|
572
1186
|
controller.updatedAt = now;
|
|
573
1187
|
await this.controllers.save(controller);
|
|
574
1188
|
}
|
|
1189
|
+
async onConfigurationReported(controllerId, payload) {
|
|
1190
|
+
let report;
|
|
1191
|
+
try {
|
|
1192
|
+
report = parseConfigurationReport(JSON.parse(payload.toString("utf8")));
|
|
1193
|
+
} catch {
|
|
1194
|
+
this.context.logger.warn(`Ignoring invalid WAGO configuration report for controller ${controllerId}`);
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
if (!report) {
|
|
1198
|
+
this.context.logger.warn(`Ignoring malformed WAGO configuration report for controller ${controllerId}`);
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
await this.withConfigurationLock(controllerId, async () => {
|
|
1202
|
+
const revision = await this.revisions.findOneBy({ controllerId, revision: report.revision });
|
|
1203
|
+
if (!revision || revision.contentHash !== report.contentHash) return;
|
|
1204
|
+
if (revision.state !== "published") return;
|
|
1205
|
+
revision.state = report.errors.length ? "rejected" : "applied";
|
|
1206
|
+
revision.rejectionErrors = report.errors.length ? JSON.stringify(report.errors) : null;
|
|
1207
|
+
revision.reportedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1208
|
+
await this.revisions.save(revision);
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
enqueueConfigurationReport(controllerId, payload) {
|
|
1212
|
+
const queue = this.configurationReportQueues.get(controllerId) ?? { pending: /* @__PURE__ */ new Map(), processing: false };
|
|
1213
|
+
this.configurationReportQueues.set(controllerId, queue);
|
|
1214
|
+
if (queue.processing) {
|
|
1215
|
+
const revision = this.configurationReportRevision(payload);
|
|
1216
|
+
const key = revision ?? Number.NaN;
|
|
1217
|
+
if (queue.pending.has(key) || queue.pending.size < MAX_PENDING_CONFIGURATION_REPORTS)
|
|
1218
|
+
queue.pending.set(key, payload);
|
|
1219
|
+
else this.context.logger.warn(`Dropping excess WAGO configuration report for controller ${controllerId}`);
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
queue.processing = true;
|
|
1223
|
+
void this.processConfigurationReports(controllerId, payload, queue);
|
|
1224
|
+
}
|
|
1225
|
+
async processConfigurationReports(controllerId, payload, queue) {
|
|
1226
|
+
let next = payload;
|
|
1227
|
+
while (next) {
|
|
1228
|
+
try {
|
|
1229
|
+
await this.onConfigurationReported(controllerId, next);
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
this.context.logger.warn(`Could not process WAGO configuration report: ${String(error)}`);
|
|
1232
|
+
}
|
|
1233
|
+
const pending = queue.pending.entries().next();
|
|
1234
|
+
if (pending.done) next = null;
|
|
1235
|
+
else {
|
|
1236
|
+
const [revision, report] = pending.value;
|
|
1237
|
+
queue.pending.delete(revision);
|
|
1238
|
+
next = report;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
queue.processing = false;
|
|
1242
|
+
if (this.configurationReportQueues.get(controllerId) === queue) this.configurationReportQueues.delete(controllerId);
|
|
1243
|
+
}
|
|
1244
|
+
configurationReportRevision(payload) {
|
|
1245
|
+
try {
|
|
1246
|
+
const report = JSON.parse(payload.toString("utf8"));
|
|
1247
|
+
return Number.isSafeInteger(report.revision) && report.revision >= 1 ? report.revision : null;
|
|
1248
|
+
} catch {
|
|
1249
|
+
return null;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
async publishRevision(controller, revision) {
|
|
1253
|
+
if (!controller.mqttServerId) throw new import_common.ConflictException(`WAGO controller ${controller.id} has no MQTT server`);
|
|
1254
|
+
const incompatibility = compatibilityError({
|
|
1255
|
+
protocolVersion: controller.protocolVersion,
|
|
1256
|
+
capabilities: JSON.parse(controller.capabilities)
|
|
1257
|
+
});
|
|
1258
|
+
if (incompatibility) throw new import_common.ConflictException(`Cannot publish configuration: ${incompatibility}`);
|
|
1259
|
+
const settings = await this.getSettings();
|
|
1260
|
+
await this.context.mqtt.publish(
|
|
1261
|
+
controller.mqttServerId,
|
|
1262
|
+
configurationDesiredTopic(settings.operationalPrefix ?? "attraccess/wago", controller.hardwareId),
|
|
1263
|
+
JSON.stringify({
|
|
1264
|
+
protocolVersion: CONFIGURATION_PROTOCOL_VERSION,
|
|
1265
|
+
revision: revision.revision,
|
|
1266
|
+
contentHash: revision.contentHash,
|
|
1267
|
+
snapshot: JSON.parse(revision.snapshot)
|
|
1268
|
+
}),
|
|
1269
|
+
{ qos: 1, retain: true }
|
|
1270
|
+
);
|
|
1271
|
+
revision.state = "published";
|
|
1272
|
+
return this.revisions.save(revision);
|
|
1273
|
+
}
|
|
575
1274
|
connectivity(controller) {
|
|
576
1275
|
if (controller.trustState === "untrusted") return "untrusted";
|
|
577
1276
|
return controller.lastHeartbeatAt && Date.now() - Date.parse(controller.lastHeartbeatAt) <= STALE_AFTER_MS ? "online" : "stale";
|
|
@@ -653,6 +1352,43 @@ var WagoService = class {
|
|
|
653
1352
|
if (this.claimLocks.get(id) === lock) this.claimLocks.delete(id);
|
|
654
1353
|
}
|
|
655
1354
|
}
|
|
1355
|
+
async withClaimConfigurationLock(operation) {
|
|
1356
|
+
const previous = this.claimConfigurationLock;
|
|
1357
|
+
let release;
|
|
1358
|
+
const lock = new Promise((resolve) => {
|
|
1359
|
+
release = resolve;
|
|
1360
|
+
});
|
|
1361
|
+
this.claimConfigurationLock = lock;
|
|
1362
|
+
await previous;
|
|
1363
|
+
try {
|
|
1364
|
+
return await operation();
|
|
1365
|
+
} finally {
|
|
1366
|
+
release();
|
|
1367
|
+
if (this.claimConfigurationLock === lock) this.claimConfigurationLock = Promise.resolve();
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
async claimedController(id) {
|
|
1371
|
+
const controller = await this.controllers.findOneBy({ id });
|
|
1372
|
+
if (!controller || controller.trustState !== "claimed")
|
|
1373
|
+
throw new import_common.NotFoundException(`claimed WAGO controller ${id} not found`);
|
|
1374
|
+
if (!controller.mqttServerId) throw new import_common.ConflictException(`WAGO controller ${id} has no MQTT server`);
|
|
1375
|
+
return controller;
|
|
1376
|
+
}
|
|
1377
|
+
async withConfigurationLock(id, operation) {
|
|
1378
|
+
const previous = this.configurationLocks.get(id) ?? Promise.resolve();
|
|
1379
|
+
let release;
|
|
1380
|
+
const lock = new Promise((resolve) => {
|
|
1381
|
+
release = resolve;
|
|
1382
|
+
});
|
|
1383
|
+
this.configurationLocks.set(id, lock);
|
|
1384
|
+
await previous;
|
|
1385
|
+
try {
|
|
1386
|
+
return await operation();
|
|
1387
|
+
} finally {
|
|
1388
|
+
release();
|
|
1389
|
+
if (this.configurationLocks.get(id) === lock) this.configurationLocks.delete(id);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
656
1392
|
scheduleSubscriptionRetry() {
|
|
657
1393
|
if (this.destroyed || this.subscriptionRetryTimer) return;
|
|
658
1394
|
this.subscriptionRetryTimer = setTimeout(() => {
|
|
@@ -670,10 +1406,10 @@ WagoService = __decorateClass([
|
|
|
670
1406
|
__decorateParam(0, (0, import_common.Inject)(PLUGIN_CONTEXT))
|
|
671
1407
|
], WagoService);
|
|
672
1408
|
function hash(value) {
|
|
673
|
-
return (0,
|
|
1409
|
+
return (0, import_node_crypto2.createHash)("sha256").update(value).digest("hex");
|
|
674
1410
|
}
|
|
675
1411
|
function safeEqual(left, right) {
|
|
676
|
-
return left.length === right.length && (0,
|
|
1412
|
+
return left.length === right.length && (0, import_node_crypto2.timingSafeEqual)(Buffer.from(left), Buffer.from(right));
|
|
677
1413
|
}
|
|
678
1414
|
function isValidHardwareId(hardwareId) {
|
|
679
1415
|
return Boolean(hardwareId) && !/[/+#]/.test(hardwareId);
|
|
@@ -691,7 +1427,7 @@ var WagoControllerApi = class {
|
|
|
691
1427
|
return this.wago.getSettings();
|
|
692
1428
|
}
|
|
693
1429
|
setSettings(body) {
|
|
694
|
-
return this.wago.
|
|
1430
|
+
return this.wago.setSettings(body?.defaultMqttServerId, body?.operationalPrefix);
|
|
695
1431
|
}
|
|
696
1432
|
enrollment(body) {
|
|
697
1433
|
const manualCredentials = body?.manualUsername || body?.manualPassword ? { username: body.manualUsername ?? "", password: body.manualPassword ?? "" } : void 0;
|
|
@@ -700,6 +1436,30 @@ var WagoControllerApi = class {
|
|
|
700
1436
|
claim(id, body) {
|
|
701
1437
|
return this.wago.claim(id, body?.name ?? "", body?.verifier ?? "", body?.mqttServerId);
|
|
702
1438
|
}
|
|
1439
|
+
draft(id) {
|
|
1440
|
+
return this.wago.getDraft(id);
|
|
1441
|
+
}
|
|
1442
|
+
saveDraft(id, body) {
|
|
1443
|
+
return this.wago.saveDraft(id, body?.snapshot);
|
|
1444
|
+
}
|
|
1445
|
+
validateDraft(id) {
|
|
1446
|
+
return this.wago.validateDraft(id);
|
|
1447
|
+
}
|
|
1448
|
+
reviewDraft(id) {
|
|
1449
|
+
return this.wago.reviewDraft(id);
|
|
1450
|
+
}
|
|
1451
|
+
revisions(id, offset, limit) {
|
|
1452
|
+
return this.wago.revisionsFor(id, Number(offset), Number(limit));
|
|
1453
|
+
}
|
|
1454
|
+
publishDraft(id) {
|
|
1455
|
+
return this.wago.publishDraft(id);
|
|
1456
|
+
}
|
|
1457
|
+
rollback(id, revision) {
|
|
1458
|
+
return this.wago.rollback(id, revision);
|
|
1459
|
+
}
|
|
1460
|
+
previewRevision(id, revision) {
|
|
1461
|
+
return this.wago.previewRevision(id, revision);
|
|
1462
|
+
}
|
|
703
1463
|
};
|
|
704
1464
|
__decorateClass([
|
|
705
1465
|
(0, import_common2.Get)("controllers")
|
|
@@ -720,8 +1480,45 @@ __decorateClass([
|
|
|
720
1480
|
__decorateParam(0, (0, import_common2.Param)("id", import_common2.ParseIntPipe)),
|
|
721
1481
|
__decorateParam(1, (0, import_common2.Body)())
|
|
722
1482
|
], WagoControllerApi.prototype, "claim", 1);
|
|
1483
|
+
__decorateClass([
|
|
1484
|
+
(0, import_common2.Get)("controllers/:id/configuration/draft"),
|
|
1485
|
+
__decorateParam(0, (0, import_common2.Param)("id", import_common2.ParseIntPipe))
|
|
1486
|
+
], WagoControllerApi.prototype, "draft", 1);
|
|
1487
|
+
__decorateClass([
|
|
1488
|
+
(0, import_common2.Post)("controllers/:id/configuration/draft"),
|
|
1489
|
+
__decorateParam(0, (0, import_common2.Param)("id", import_common2.ParseIntPipe)),
|
|
1490
|
+
__decorateParam(1, (0, import_common2.Body)())
|
|
1491
|
+
], WagoControllerApi.prototype, "saveDraft", 1);
|
|
1492
|
+
__decorateClass([
|
|
1493
|
+
(0, import_common2.Post)("controllers/:id/configuration/validate"),
|
|
1494
|
+
__decorateParam(0, (0, import_common2.Param)("id", import_common2.ParseIntPipe))
|
|
1495
|
+
], WagoControllerApi.prototype, "validateDraft", 1);
|
|
1496
|
+
__decorateClass([
|
|
1497
|
+
(0, import_common2.Post)("controllers/:id/configuration/review"),
|
|
1498
|
+
__decorateParam(0, (0, import_common2.Param)("id", import_common2.ParseIntPipe))
|
|
1499
|
+
], WagoControllerApi.prototype, "reviewDraft", 1);
|
|
1500
|
+
__decorateClass([
|
|
1501
|
+
(0, import_common2.Get)("controllers/:id/configuration/revisions"),
|
|
1502
|
+
__decorateParam(0, (0, import_common2.Param)("id", import_common2.ParseIntPipe)),
|
|
1503
|
+
__decorateParam(1, (0, import_common2.Query)("offset")),
|
|
1504
|
+
__decorateParam(2, (0, import_common2.Query)("limit"))
|
|
1505
|
+
], WagoControllerApi.prototype, "revisions", 1);
|
|
1506
|
+
__decorateClass([
|
|
1507
|
+
(0, import_common2.Post)("controllers/:id/configuration/publish"),
|
|
1508
|
+
__decorateParam(0, (0, import_common2.Param)("id", import_common2.ParseIntPipe))
|
|
1509
|
+
], WagoControllerApi.prototype, "publishDraft", 1);
|
|
1510
|
+
__decorateClass([
|
|
1511
|
+
(0, import_common2.Post)("controllers/:id/configuration/rollback/:revision"),
|
|
1512
|
+
__decorateParam(0, (0, import_common2.Param)("id", import_common2.ParseIntPipe)),
|
|
1513
|
+
__decorateParam(1, (0, import_common2.Param)("revision", import_common2.ParseIntPipe))
|
|
1514
|
+
], WagoControllerApi.prototype, "rollback", 1);
|
|
1515
|
+
__decorateClass([
|
|
1516
|
+
(0, import_common2.Get)("controllers/:id/configuration/revisions/:revision/preview"),
|
|
1517
|
+
__decorateParam(0, (0, import_common2.Param)("id", import_common2.ParseIntPipe)),
|
|
1518
|
+
__decorateParam(1, (0, import_common2.Param)("revision", import_common2.ParseIntPipe))
|
|
1519
|
+
], WagoControllerApi.prototype, "previewRevision", 1);
|
|
723
1520
|
WagoControllerApi = __decorateClass([
|
|
724
|
-
(0,
|
|
1521
|
+
(0, import_plugins_backend_sdk6.Auth)("resources.update"),
|
|
725
1522
|
(0, import_common2.Controller)("wago"),
|
|
726
1523
|
__decorateParam(0, (0, import_common2.Inject)(WagoService))
|
|
727
1524
|
], WagoControllerApi);
|
|
@@ -731,7 +1528,7 @@ var PLUGIN_CONTEXT2 = /* @__PURE__ */ Symbol.for("attraccess.plugin.context");
|
|
|
731
1528
|
var WagoPluginModule = class {
|
|
732
1529
|
};
|
|
733
1530
|
var plugin = {
|
|
734
|
-
entities: [WagoController, WagoSettings, WagoEnrollment],
|
|
1531
|
+
entities: [WagoController, WagoSettings, WagoEnrollment, WagoConfigurationDraft, WagoConfigurationRevision],
|
|
735
1532
|
register(context) {
|
|
736
1533
|
return {
|
|
737
1534
|
module: WagoPluginModule,
|
package/dist/migrations.js
CHANGED
|
@@ -21,6 +21,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
21
21
|
// backend/migrations.ts
|
|
22
22
|
var migrations_exports = {};
|
|
23
23
|
__export(migrations_exports, {
|
|
24
|
+
AddWagoConfigurationRevisions1780000000002: () => AddWagoConfigurationRevisions1780000000002,
|
|
24
25
|
AddWagoEnrollmentRevocation1780000000001: () => AddWagoEnrollmentRevocation1780000000001,
|
|
25
26
|
CreateWagoControllers1780000000000: () => CreateWagoControllers1780000000000
|
|
26
27
|
});
|
|
@@ -71,8 +72,35 @@ var AddWagoEnrollmentRevocation1780000000001 = class {
|
|
|
71
72
|
await queryRunner.query('ALTER TABLE "plugin_wago_enrollments" DROP COLUMN "revoked_at"');
|
|
72
73
|
}
|
|
73
74
|
};
|
|
75
|
+
|
|
76
|
+
// backend/migrations/1780000000002-add-wago-configuration-revisions.ts
|
|
77
|
+
var AddWagoConfigurationRevisions1780000000002 = class {
|
|
78
|
+
constructor() {
|
|
79
|
+
__publicField(this, "name", "AddWagoConfigurationRevisions1780000000002");
|
|
80
|
+
}
|
|
81
|
+
async up(queryRunner) {
|
|
82
|
+
await queryRunner.query(`ALTER TABLE "plugin_wago_settings" ADD COLUMN "operational_prefix" varchar NOT NULL DEFAULT ('attraccess/wago')`);
|
|
83
|
+
await queryRunner.query(`CREATE TABLE "plugin_wago_configuration_drafts" (
|
|
84
|
+
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "controller_id" integer NOT NULL,
|
|
85
|
+
"snapshot" text NOT NULL, "reviewed_hash" varchar, "updated_at" varchar NOT NULL,
|
|
86
|
+
CONSTRAINT "UQ_plugin_wago_configuration_drafts_controller_id" UNIQUE ("controller_id"))`);
|
|
87
|
+
await queryRunner.query(`CREATE TABLE "plugin_wago_configuration_revisions" (
|
|
88
|
+
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "controller_id" integer NOT NULL,
|
|
89
|
+
"revision" integer NOT NULL, "snapshot" text NOT NULL, "content_hash" varchar NOT NULL,
|
|
90
|
+
"state" varchar NOT NULL, "rejection_errors" text, "published_at" varchar NOT NULL, "reported_at" varchar,
|
|
91
|
+
CONSTRAINT "UQ_plugin_wago_configuration_revisions_controller_revision" UNIQUE ("controller_id", "revision"))`);
|
|
92
|
+
await queryRunner.query('CREATE INDEX "IDX_plugin_wago_configuration_revisions_controller" ON "plugin_wago_configuration_revisions" ("controller_id", "revision")');
|
|
93
|
+
}
|
|
94
|
+
async down(queryRunner) {
|
|
95
|
+
await queryRunner.query('DROP INDEX "IDX_plugin_wago_configuration_revisions_controller"');
|
|
96
|
+
await queryRunner.query('DROP TABLE "plugin_wago_configuration_revisions"');
|
|
97
|
+
await queryRunner.query('DROP TABLE "plugin_wago_configuration_drafts"');
|
|
98
|
+
await queryRunner.query('ALTER TABLE "plugin_wago_settings" DROP COLUMN "operational_prefix"');
|
|
99
|
+
}
|
|
100
|
+
};
|
|
74
101
|
// Annotate the CommonJS export names for ESM import in node:
|
|
75
102
|
0 && (module.exports = {
|
|
103
|
+
AddWagoConfigurationRevisions1780000000002,
|
|
76
104
|
AddWagoEnrollmentRevocation1780000000001,
|
|
77
105
|
CreateWagoControllers1780000000000
|
|
78
106
|
});
|