Create clean release database baseline
This commit is contained in:
@@ -180,9 +180,6 @@ function createMoveTargetSnapshot(
|
||||
equipmentIdentifier: "-1F4",
|
||||
displayName: "Neuer Stromkreis",
|
||||
sortOrder: 40,
|
||||
protectionType: null,
|
||||
protectionRatedCurrent: null,
|
||||
protectionCharacteristic: null,
|
||||
cableType: null,
|
||||
cableCrossSection: null,
|
||||
cableLength: null,
|
||||
|
||||
@@ -133,7 +133,6 @@ function createTestDatabase(): DatabaseContext {
|
||||
id: "row-1",
|
||||
circuitId: "circuit-1",
|
||||
linkedProjectDeviceId: "device-1",
|
||||
legacyConsumerId: "legacy-1",
|
||||
sortOrder: 10,
|
||||
name: "Leuchte",
|
||||
displayName: "Leuchte lokal",
|
||||
@@ -163,7 +162,6 @@ function createInsertSnapshot(
|
||||
id: "row-new",
|
||||
circuitId: "circuit-reserve",
|
||||
linkedProjectDeviceId: "device-1",
|
||||
legacyConsumerId: null,
|
||||
sortOrder: 10,
|
||||
name: "Steckdose",
|
||||
displayName: "Steckdose lokal",
|
||||
@@ -423,10 +421,6 @@ describe("circuit device-row structure project-command repository", () => {
|
||||
roomId: "room-foreign",
|
||||
}),
|
||||
createInsertSnapshot({ id: "row-1" }),
|
||||
createInsertSnapshot({
|
||||
id: "row-legacy",
|
||||
legacyConsumerId: "legacy-user-value",
|
||||
}),
|
||||
]) {
|
||||
assert.throws(() =>
|
||||
store.execute({
|
||||
|
||||
@@ -6,14 +6,12 @@ import {
|
||||
buildDeviceRowEditPatch,
|
||||
formatValue,
|
||||
getCircuitSectionLabel,
|
||||
getLegacyProjectColumnLayoutStorageKey,
|
||||
getProjectColumnLayoutStorageKey,
|
||||
isGridEditorControlTarget,
|
||||
getBlockSortValue,
|
||||
getCellKind,
|
||||
getCircuitValue,
|
||||
getDeviceValue,
|
||||
migrateLegacyColumnLayout,
|
||||
parseStoredColumnLayout,
|
||||
parseNumeric,
|
||||
} from "../src/frontend/utils/circuit-grid-model.js";
|
||||
@@ -41,9 +39,11 @@ const circuit: CircuitTreeCircuitDto = {
|
||||
equipmentIdentifier: "-1F1",
|
||||
displayName: "Lighting circuit",
|
||||
sortOrder: 10,
|
||||
protectionType: "MCB",
|
||||
protectionRatedCurrent: 16,
|
||||
protectionCharacteristic: "B",
|
||||
protectionDevice: {
|
||||
type: "LS",
|
||||
ratedCurrentA: 16,
|
||||
tripCharacteristic: "B",
|
||||
},
|
||||
cableType: "NYM-J",
|
||||
cableCrossSection: "1.5 mm²",
|
||||
cableLength: 20,
|
||||
@@ -83,10 +83,6 @@ describe("circuit grid model", () => {
|
||||
getProjectColumnLayoutStorageKey("project-1"),
|
||||
"circuitTreeEditor.columnLayout.v3.project-1"
|
||||
);
|
||||
assert.equal(
|
||||
getLegacyProjectColumnLayoutStorageKey("project-1"),
|
||||
"circuitTreeEditor.columnLayout.v2.project-1"
|
||||
);
|
||||
const layout = parseStoredColumnLayout(
|
||||
JSON.stringify({
|
||||
order: ["displayName", "equipmentIdentifier"],
|
||||
@@ -106,39 +102,6 @@ describe("circuit grid model", () => {
|
||||
assert.equal(parseStoredColumnLayout("{invalid"), null);
|
||||
});
|
||||
|
||||
it("adds the circuit protection column to legacy project layouts", () => {
|
||||
const legacyLayout = parseStoredColumnLayout(
|
||||
JSON.stringify({
|
||||
order: [
|
||||
"equipmentIdentifier",
|
||||
"displayName",
|
||||
"rowTotalPower",
|
||||
"remark",
|
||||
],
|
||||
visible: [
|
||||
"equipmentIdentifier",
|
||||
"displayName",
|
||||
"rowTotalPower",
|
||||
"remark",
|
||||
],
|
||||
})
|
||||
);
|
||||
assert.ok(legacyLayout);
|
||||
|
||||
const migrated = migrateLegacyColumnLayout(legacyLayout);
|
||||
assert.equal(
|
||||
migrated.order.indexOf("protectionSummary"),
|
||||
migrated.order.indexOf("rowTotalPower") + 1
|
||||
);
|
||||
assert.deepEqual(migrated.visible, [
|
||||
"equipmentIdentifier",
|
||||
"displayName",
|
||||
"rowTotalPower",
|
||||
"remark",
|
||||
"protectionSummary",
|
||||
]);
|
||||
});
|
||||
|
||||
it("shows stable circuit sections with German labels", () => {
|
||||
assert.equal(
|
||||
getCircuitSectionLabel({
|
||||
@@ -181,7 +144,7 @@ describe("circuit grid model", () => {
|
||||
it("projects circuit and device values without mixing ownership", () => {
|
||||
assert.equal(getDeviceValue(device, "roomSummary"), "1.01 Office");
|
||||
assert.equal(getDeviceValue(device, "rowTotalPower"), 0.08);
|
||||
assert.equal(getCircuitValue(circuit, "protectionSummary"), "MCB 16A B");
|
||||
assert.equal(getCircuitValue(circuit, "protectionSummary"), "LS · 16 A · B");
|
||||
assert.equal(
|
||||
getCircuitValue(
|
||||
{
|
||||
|
||||
@@ -424,7 +424,7 @@ describe("circuit group numbering", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects legacy, malformed and unsupported identifiers", () => {
|
||||
it("rejects outdated, malformed and unsupported identifiers", () => {
|
||||
for (const identifier of [
|
||||
"-1F1",
|
||||
"1F1.1",
|
||||
|
||||
@@ -90,7 +90,6 @@ function createTestDatabase(): DatabaseContext {
|
||||
id: "subtree-row",
|
||||
circuitId: "subtree-circuit",
|
||||
linkedProjectDeviceId: null,
|
||||
legacyConsumerId: null,
|
||||
sortOrder: 10,
|
||||
name: "Leuchte",
|
||||
displayName: "Leuchte",
|
||||
|
||||
@@ -71,9 +71,6 @@ function createSnapshot(): CircuitGroupSubtreeSnapshot {
|
||||
equipmentIdentifier: "-1F1.1",
|
||||
displayName: "Beleuchtung",
|
||||
sortOrder: 10,
|
||||
protectionType: null,
|
||||
protectionRatedCurrent: null,
|
||||
protectionCharacteristic: null,
|
||||
cableType: null,
|
||||
cableCrossSection: null,
|
||||
cableLength: null,
|
||||
@@ -89,7 +86,6 @@ function createSnapshot(): CircuitGroupSubtreeSnapshot {
|
||||
id: "row-1",
|
||||
circuitId: "circuit-1",
|
||||
linkedProjectDeviceId: "device-1",
|
||||
legacyConsumerId: null,
|
||||
sortOrder: 10,
|
||||
name: "Leuchte",
|
||||
displayName: "Leuchte Flur",
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
import {
|
||||
resolveCircuitPhaseType,
|
||||
resolveProjectVoltage,
|
||||
normalizeKnownElectricalPhaseType,
|
||||
} from "../src/domain/services/project-voltage.service.js";
|
||||
|
||||
describe("circuit power calculation", () => {
|
||||
@@ -74,16 +73,5 @@ describe("project voltage derivation", () => {
|
||||
assert.equal(resolveCircuitPhaseType("unassigned", [null]), "single_phase");
|
||||
});
|
||||
|
||||
it("normalizes known legacy phase labels without guessing unknown values", () => {
|
||||
assert.equal(normalizeKnownElectricalPhaseType("1phasig"), "single_phase");
|
||||
assert.equal(normalizeKnownElectricalPhaseType("1-phasig"), "single_phase");
|
||||
assert.equal(normalizeKnownElectricalPhaseType("3phase"), "three_phase");
|
||||
assert.equal(normalizeKnownElectricalPhaseType("3-phasig"), "three_phase");
|
||||
assert.equal(normalizeKnownElectricalPhaseType(null), null);
|
||||
assert.throws(
|
||||
() => normalizeKnownElectricalPhaseType("zweiphasig"),
|
||||
/Unknown electrical phase type/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ function createTestFixture(): TestFixture {
|
||||
equipmentIdentifier: "-1F1",
|
||||
displayName: "Licht Bestand",
|
||||
sortOrder: 10,
|
||||
protectionRatedCurrent: 10,
|
||||
cableLength: 10,
|
||||
voltage: 230,
|
||||
controlRequirement: "none",
|
||||
isReserve: 0,
|
||||
@@ -112,7 +112,7 @@ describe("circuit project-command repository", () => {
|
||||
const store = new CircuitProjectCommandRepository(fixture.context.db);
|
||||
const command = createCircuitUpdateProjectCommand("circuit-1", {
|
||||
displayName: null,
|
||||
protectionRatedCurrent: 16,
|
||||
cableLength: 16,
|
||||
voltage: 400,
|
||||
controlRequirement: "DALI",
|
||||
isReserve: true,
|
||||
@@ -128,7 +128,7 @@ describe("circuit project-command repository", () => {
|
||||
|
||||
const circuit = getCircuit(fixture.context);
|
||||
assert.equal(circuit.displayName, null);
|
||||
assert.equal(circuit.protectionRatedCurrent, 16);
|
||||
assert.equal(circuit.cableLength, 16);
|
||||
assert.equal(circuit.voltage, 230);
|
||||
assert.equal(circuit.controlRequirement, "DALI");
|
||||
assert.equal(circuit.isReserve, 1);
|
||||
@@ -137,7 +137,7 @@ describe("circuit project-command repository", () => {
|
||||
circuitId: "circuit-1",
|
||||
changes: [
|
||||
{ field: "displayName", value: "Licht Bestand" },
|
||||
{ field: "protectionRatedCurrent", value: 10 },
|
||||
{ field: "cableLength", value: 10 },
|
||||
{ field: "controlRequirement", value: "none" },
|
||||
{ field: "isReserve", value: false },
|
||||
],
|
||||
@@ -152,7 +152,7 @@ describe("circuit project-command repository", () => {
|
||||
deserializeProjectCommand(changeSet.forwardPayloadJson),
|
||||
createCircuitUpdateProjectCommand("circuit-1", {
|
||||
displayName: null,
|
||||
protectionRatedCurrent: 16,
|
||||
cableLength: 16,
|
||||
controlRequirement: "DALI",
|
||||
isReserve: true,
|
||||
})
|
||||
@@ -184,7 +184,7 @@ describe("circuit project-command repository", () => {
|
||||
source: "user",
|
||||
command: createCircuitUpdateProjectCommand("circuit-1", {
|
||||
displayName: "Licht Neu",
|
||||
protectionRatedCurrent: 16,
|
||||
cableLength: 16,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -199,7 +199,7 @@ describe("circuit project-command repository", () => {
|
||||
|
||||
const circuit = getCircuit(fixture.context);
|
||||
assert.equal(circuit.displayName, "Licht Bestand");
|
||||
assert.equal(circuit.protectionRatedCurrent, 10);
|
||||
assert.equal(circuit.cableLength, 10);
|
||||
assert.equal(undone.revision.revisionNumber, 2);
|
||||
assert.deepEqual(
|
||||
fixture.context.db
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from "../src/frontend/utils/api.js";
|
||||
|
||||
describe("circuit structure command adapters", () => {
|
||||
it("builds a complete row snapshot without legacy identities", () => {
|
||||
it("builds a complete row snapshot", () => {
|
||||
const row = buildCircuitDeviceRowInsertSnapshot({
|
||||
id: "row-1",
|
||||
circuitId: "circuit-1",
|
||||
@@ -31,7 +31,6 @@ describe("circuit structure command adapters", () => {
|
||||
id: "row-1",
|
||||
circuitId: "circuit-1",
|
||||
linkedProjectDeviceId: "device-1",
|
||||
legacyConsumerId: null,
|
||||
sortOrder: 20,
|
||||
name: "Steckdose",
|
||||
displayName: "Steckdose Büro",
|
||||
|
||||
@@ -117,9 +117,6 @@ function createTestDatabase(): TestFixture {
|
||||
equipmentIdentifier: "-1F1",
|
||||
displayName: "Bestand",
|
||||
sortOrder: 10,
|
||||
protectionType: "LS",
|
||||
protectionRatedCurrent: 16,
|
||||
protectionCharacteristic: "B",
|
||||
cableType: "NYM-J",
|
||||
cableCrossSection: "3x1,5",
|
||||
cableLength: 18.5,
|
||||
@@ -139,7 +136,6 @@ function createTestDatabase(): TestFixture {
|
||||
id: "row-existing-1",
|
||||
circuitId: "circuit-existing",
|
||||
linkedProjectDeviceId: "device-1",
|
||||
legacyConsumerId: "legacy-1",
|
||||
sortOrder: 10,
|
||||
name: "Leuchte A",
|
||||
displayName: "Leuchte A lokal",
|
||||
@@ -198,7 +194,6 @@ function createRow(
|
||||
id,
|
||||
circuitId,
|
||||
linkedProjectDeviceId: null,
|
||||
legacyConsumerId: null,
|
||||
sortOrder,
|
||||
name: `Gerät ${id}`,
|
||||
displayName: `Gerät ${id}`,
|
||||
@@ -241,9 +236,6 @@ function createCircuitSnapshot(
|
||||
equipmentIdentifier: "-1F2",
|
||||
displayName: "Neuer Stromkreis",
|
||||
sortOrder: 20,
|
||||
protectionType: null,
|
||||
protectionRatedCurrent: null,
|
||||
protectionCharacteristic: null,
|
||||
cableType: null,
|
||||
cableCrossSection: null,
|
||||
cableLength: null,
|
||||
@@ -569,14 +561,6 @@ describe("circuit structure project-command repository", () => {
|
||||
}),
|
||||
],
|
||||
}),
|
||||
createCircuitSnapshot(fixture, {
|
||||
id: "legacy-row",
|
||||
deviceRows: [
|
||||
createRow("legacy-row", "row-legacy", 10, {
|
||||
legacyConsumerId: "legacy-user-value",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
];
|
||||
for (const snapshot of invalidSnapshots) {
|
||||
assert.throws(() =>
|
||||
|
||||
@@ -1,466 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, it } from "node:test";
|
||||
import Database from "better-sqlite3";
|
||||
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
||||
import { createDatabaseContext } from "../src/db/database-context.js";
|
||||
|
||||
const migrationPath = path.resolve(
|
||||
"src",
|
||||
"db",
|
||||
"migrations",
|
||||
"0024_damp_mister_sinister.sql"
|
||||
);
|
||||
|
||||
function applyComponentSchemaMigration(sqlite: Database.Database): void {
|
||||
const migrationSql = fs.readFileSync(migrationPath, "utf8");
|
||||
for (const statement of migrationSql.split("--> statement-breakpoint")) {
|
||||
if (statement.trim()) {
|
||||
sqlite.exec(statement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createLegacySchema(sqlite: Database.Database): void {
|
||||
sqlite.pragma("foreign_keys = ON");
|
||||
sqlite.exec(`
|
||||
CREATE TABLE circuit_lists (
|
||||
id text PRIMARY KEY NOT NULL
|
||||
);
|
||||
CREATE TABLE circuit_sections (
|
||||
id text PRIMARY KEY NOT NULL,
|
||||
circuit_list_id text NOT NULL REFERENCES circuit_lists(id) ON DELETE CASCADE,
|
||||
key text NOT NULL,
|
||||
display_name text NOT NULL,
|
||||
prefix text NOT NULL,
|
||||
sort_order integer DEFAULT 0 NOT NULL
|
||||
);
|
||||
CREATE TABLE circuits (
|
||||
id text PRIMARY KEY NOT NULL,
|
||||
circuit_list_id text NOT NULL REFERENCES circuit_lists(id) ON DELETE CASCADE,
|
||||
equipment_identifier text NOT NULL,
|
||||
protection_type text,
|
||||
protection_rated_current real,
|
||||
protection_characteristic text
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
describe("distribution board component schema migration", () => {
|
||||
it("creates the additive schema on an empty database", () => {
|
||||
const context = createDatabaseContext(":memory:");
|
||||
try {
|
||||
migrate(context.db, {
|
||||
migrationsFolder: path.resolve("src", "db", "migrations"),
|
||||
});
|
||||
|
||||
const tables = (
|
||||
context.sqlite
|
||||
.prepare(
|
||||
`SELECT name
|
||||
FROM sqlite_master
|
||||
WHERE type = 'table'
|
||||
ORDER BY name`
|
||||
)
|
||||
.all() as Array<{ name: string }>
|
||||
).map((table) => table.name);
|
||||
|
||||
assert.equal(tables.includes("distribution_board_components"), true);
|
||||
assert.equal(tables.includes("circuit_protection_devices"), true);
|
||||
assert.deepEqual(
|
||||
tables.includes("distribution_board_component_protection_devices"),
|
||||
true
|
||||
);
|
||||
assert.deepEqual(
|
||||
tables.includes("circuit_list_equipment_identifiers"),
|
||||
true
|
||||
);
|
||||
|
||||
const sectionColumns = (
|
||||
context.sqlite
|
||||
.prepare("PRAGMA table_info(circuit_sections)")
|
||||
.all() as Array<{ name: string }>
|
||||
).map((column) => column.name);
|
||||
assert.equal(sectionColumns.includes("category"), true);
|
||||
assert.equal(sectionColumns.includes("group_number"), true);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("backfills groups and the BMK registry without rewriting existing planning data", () => {
|
||||
const sqlite = new Database(":memory:");
|
||||
try {
|
||||
createLegacySchema(sqlite);
|
||||
sqlite.exec(`
|
||||
INSERT INTO circuit_lists (id) VALUES ('list-1');
|
||||
INSERT INTO circuit_sections (
|
||||
id,
|
||||
circuit_list_id,
|
||||
key,
|
||||
display_name,
|
||||
prefix,
|
||||
sort_order
|
||||
) VALUES
|
||||
('lighting', 'list-1', 'lighting', 'Beleuchtung', '-1F', 10),
|
||||
('single', 'list-1', 'single_phase', '1-phasig', '-2F', 20),
|
||||
('three', 'list-1', 'three_phase', '3-phasig', '-3F', 30),
|
||||
('unassigned', 'list-1', 'unassigned', 'Nicht zugeordnet', '-9F', 40);
|
||||
INSERT INTO circuits (
|
||||
id,
|
||||
circuit_list_id,
|
||||
equipment_identifier,
|
||||
protection_type,
|
||||
protection_rated_current,
|
||||
protection_characteristic
|
||||
) VALUES
|
||||
('circuit-1', 'list-1', '-1F1', 'legacy fuse', 13, 'legacy'),
|
||||
('circuit-2', 'list-1', '-2F4', NULL, NULL, NULL);
|
||||
`);
|
||||
|
||||
applyComponentSchemaMigration(sqlite);
|
||||
|
||||
assert.deepEqual(
|
||||
sqlite
|
||||
.prepare(
|
||||
`SELECT
|
||||
id,
|
||||
category,
|
||||
group_number AS groupNumber
|
||||
FROM circuit_sections
|
||||
ORDER BY sort_order`
|
||||
)
|
||||
.all(),
|
||||
[
|
||||
{ id: "lighting", category: "lighting", groupNumber: 1 },
|
||||
{ id: "single", category: "single_phase", groupNumber: 1 },
|
||||
{ id: "three", category: "three_phase", groupNumber: 1 },
|
||||
{ id: "unassigned", category: null, groupNumber: null },
|
||||
]
|
||||
);
|
||||
assert.deepEqual(
|
||||
sqlite
|
||||
.prepare(
|
||||
`SELECT
|
||||
id,
|
||||
equipment_identifier AS equipmentIdentifier,
|
||||
protection_type AS protectionType,
|
||||
protection_rated_current AS protectionRatedCurrent,
|
||||
protection_characteristic AS protectionCharacteristic
|
||||
FROM circuits
|
||||
ORDER BY id`
|
||||
)
|
||||
.all(),
|
||||
[
|
||||
{
|
||||
id: "circuit-1",
|
||||
equipmentIdentifier: "-1F1",
|
||||
protectionType: "legacy fuse",
|
||||
protectionRatedCurrent: 13,
|
||||
protectionCharacteristic: "legacy",
|
||||
},
|
||||
{
|
||||
id: "circuit-2",
|
||||
equipmentIdentifier: "-2F4",
|
||||
protectionType: null,
|
||||
protectionRatedCurrent: null,
|
||||
protectionCharacteristic: null,
|
||||
},
|
||||
]
|
||||
);
|
||||
assert.deepEqual(
|
||||
sqlite
|
||||
.prepare(
|
||||
`SELECT
|
||||
owner_type AS ownerType,
|
||||
owner_id AS ownerId,
|
||||
equipment_identifier AS equipmentIdentifier
|
||||
FROM circuit_list_equipment_identifiers
|
||||
ORDER BY owner_id`
|
||||
)
|
||||
.all(),
|
||||
[
|
||||
{
|
||||
ownerType: "circuit",
|
||||
ownerId: "circuit-1",
|
||||
equipmentIdentifier: "-1F1",
|
||||
},
|
||||
{
|
||||
ownerType: "circuit",
|
||||
ownerId: "circuit-2",
|
||||
equipmentIdentifier: "-2F4",
|
||||
},
|
||||
]
|
||||
);
|
||||
assert.deepEqual(
|
||||
sqlite
|
||||
.prepare("SELECT count(*) AS count FROM circuit_protection_devices")
|
||||
.get(),
|
||||
{ count: 0 }
|
||||
);
|
||||
} finally {
|
||||
sqlite.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps the shared BMK registry synchronized and rejects normalized collisions", () => {
|
||||
const sqlite = new Database(":memory:");
|
||||
try {
|
||||
createLegacySchema(sqlite);
|
||||
sqlite.exec(`
|
||||
INSERT INTO circuit_lists (id) VALUES ('list-1');
|
||||
INSERT INTO circuit_sections (
|
||||
id,
|
||||
circuit_list_id,
|
||||
key,
|
||||
display_name,
|
||||
prefix
|
||||
) VALUES ('lighting', 'list-1', 'lighting', 'Beleuchtung', '-1F');
|
||||
INSERT INTO circuits (
|
||||
id,
|
||||
circuit_list_id,
|
||||
equipment_identifier
|
||||
) VALUES ('circuit-1', 'list-1', '-1F1');
|
||||
`);
|
||||
applyComponentSchemaMigration(sqlite);
|
||||
|
||||
sqlite
|
||||
.prepare(
|
||||
`INSERT INTO distribution_board_components (
|
||||
id,
|
||||
circuit_list_id,
|
||||
equipment_identifier,
|
||||
name,
|
||||
role,
|
||||
placement,
|
||||
sort_order
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)`
|
||||
)
|
||||
.run(
|
||||
"main-switch",
|
||||
"list-1",
|
||||
"-Q0",
|
||||
"Hauptschalter",
|
||||
"main_switch",
|
||||
"header",
|
||||
10
|
||||
);
|
||||
|
||||
assert.throws(() =>
|
||||
sqlite
|
||||
.prepare(
|
||||
`INSERT INTO distribution_board_components (
|
||||
id,
|
||||
circuit_list_id,
|
||||
equipment_identifier,
|
||||
name,
|
||||
role,
|
||||
placement
|
||||
) VALUES (?, ?, ?, ?, ?, ?)`
|
||||
)
|
||||
.run(
|
||||
"duplicate",
|
||||
"list-1",
|
||||
" -q0 ",
|
||||
"Duplikat",
|
||||
"auxiliary",
|
||||
"footer"
|
||||
)
|
||||
);
|
||||
assert.throws(() =>
|
||||
sqlite
|
||||
.prepare(
|
||||
`INSERT INTO distribution_board_components (
|
||||
id,
|
||||
circuit_list_id,
|
||||
equipment_identifier,
|
||||
name,
|
||||
role,
|
||||
placement
|
||||
) VALUES (?, ?, ?, ?, ?, ?)`
|
||||
)
|
||||
.run(
|
||||
"circuit-duplicate",
|
||||
"list-1",
|
||||
"-1f1",
|
||||
"Duplikat",
|
||||
"auxiliary",
|
||||
"footer"
|
||||
)
|
||||
);
|
||||
|
||||
sqlite
|
||||
.prepare(
|
||||
`INSERT INTO circuits (
|
||||
id,
|
||||
circuit_list_id,
|
||||
equipment_identifier
|
||||
) VALUES (?, ?, ?)`
|
||||
)
|
||||
.run("circuit-2", "list-1", "-1F2");
|
||||
sqlite
|
||||
.prepare(
|
||||
`UPDATE circuits
|
||||
SET equipment_identifier = ?
|
||||
WHERE id = ?`
|
||||
)
|
||||
.run("-1F3", "circuit-2");
|
||||
|
||||
assert.deepEqual(
|
||||
sqlite
|
||||
.prepare(
|
||||
`SELECT equipment_identifier AS equipmentIdentifier
|
||||
FROM circuit_list_equipment_identifiers
|
||||
WHERE owner_type = 'circuit' AND owner_id = 'circuit-2'`
|
||||
)
|
||||
.get(),
|
||||
{ equipmentIdentifier: "-1F3" }
|
||||
);
|
||||
|
||||
assert.throws(() =>
|
||||
sqlite
|
||||
.prepare(
|
||||
`UPDATE circuits
|
||||
SET equipment_identifier = ?
|
||||
WHERE id = ?`
|
||||
)
|
||||
.run("-q0", "circuit-2")
|
||||
);
|
||||
assert.deepEqual(
|
||||
sqlite
|
||||
.prepare(
|
||||
`SELECT equipment_identifier AS equipmentIdentifier
|
||||
FROM circuits
|
||||
WHERE id = 'circuit-2'`
|
||||
)
|
||||
.get(),
|
||||
{ equipmentIdentifier: "-1F3" }
|
||||
);
|
||||
|
||||
sqlite
|
||||
.prepare("DELETE FROM distribution_board_components WHERE id = ?")
|
||||
.run("main-switch");
|
||||
assert.deepEqual(
|
||||
sqlite
|
||||
.prepare(
|
||||
`SELECT count(*) AS count
|
||||
FROM circuit_list_equipment_identifiers
|
||||
WHERE owner_type = 'distribution_board_component'`
|
||||
)
|
||||
.get(),
|
||||
{ count: 0 }
|
||||
);
|
||||
} finally {
|
||||
sqlite.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("stores independent one-to-one circuit and component protection state", () => {
|
||||
const sqlite = new Database(":memory:");
|
||||
try {
|
||||
createLegacySchema(sqlite);
|
||||
sqlite.exec(`
|
||||
INSERT INTO circuit_lists (id) VALUES ('list-1');
|
||||
INSERT INTO circuit_sections (
|
||||
id,
|
||||
circuit_list_id,
|
||||
key,
|
||||
display_name,
|
||||
prefix
|
||||
) VALUES ('lighting', 'list-1', 'lighting', 'Beleuchtung', '-1F');
|
||||
INSERT INTO circuits (
|
||||
id,
|
||||
circuit_list_id,
|
||||
equipment_identifier
|
||||
) VALUES ('circuit-1', 'list-1', '-1F1');
|
||||
`);
|
||||
applyComponentSchemaMigration(sqlite);
|
||||
sqlite.exec(`
|
||||
INSERT INTO circuit_protection_devices (
|
||||
circuit_id,
|
||||
type,
|
||||
rated_current_a,
|
||||
trip_characteristic
|
||||
) VALUES ('circuit-1', 'LS', 10, 'B');
|
||||
INSERT INTO distribution_board_components (
|
||||
id,
|
||||
circuit_list_id,
|
||||
section_id,
|
||||
equipment_identifier,
|
||||
name,
|
||||
role,
|
||||
placement
|
||||
) VALUES (
|
||||
'group-rcd',
|
||||
'list-1',
|
||||
'lighting',
|
||||
'-1Q1.0',
|
||||
'Gruppen-FI',
|
||||
'group_residual_current_protection',
|
||||
'group'
|
||||
);
|
||||
INSERT INTO distribution_board_component_protection_devices (
|
||||
component_id,
|
||||
type,
|
||||
rated_current_a,
|
||||
rcd_type,
|
||||
rated_residual_current_ma
|
||||
) VALUES ('group-rcd', 'FI', 40, 'A', 30);
|
||||
`);
|
||||
|
||||
assert.deepEqual(
|
||||
sqlite
|
||||
.prepare("SELECT * FROM circuit_protection_devices")
|
||||
.get(),
|
||||
{
|
||||
circuit_id: "circuit-1",
|
||||
type: "LS",
|
||||
rated_current_a: 10,
|
||||
fuse_utilization_category: null,
|
||||
trip_characteristic: "B",
|
||||
rcd_type: null,
|
||||
rated_residual_current_ma: null,
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
sqlite
|
||||
.prepare(
|
||||
"SELECT * FROM distribution_board_component_protection_devices"
|
||||
)
|
||||
.get(),
|
||||
{
|
||||
component_id: "group-rcd",
|
||||
type: "FI",
|
||||
rated_current_a: 40,
|
||||
fuse_utilization_category: null,
|
||||
trip_characteristic: null,
|
||||
rcd_type: "A",
|
||||
rated_residual_current_ma: 30,
|
||||
}
|
||||
);
|
||||
|
||||
sqlite.prepare("DELETE FROM circuits WHERE id = ?").run("circuit-1");
|
||||
sqlite
|
||||
.prepare("DELETE FROM distribution_board_components WHERE id = ?")
|
||||
.run("group-rcd");
|
||||
assert.deepEqual(
|
||||
sqlite
|
||||
.prepare("SELECT count(*) AS count FROM circuit_protection_devices")
|
||||
.get(),
|
||||
{ count: 0 }
|
||||
);
|
||||
assert.deepEqual(
|
||||
sqlite
|
||||
.prepare(
|
||||
`SELECT count(*) AS count
|
||||
FROM distribution_board_component_protection_devices`
|
||||
)
|
||||
.get(),
|
||||
{ count: 0 }
|
||||
);
|
||||
} finally {
|
||||
sqlite.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -21,11 +21,9 @@ import { ProjectRevisionConflictError } from "../src/domain/errors/project-revis
|
||||
import {
|
||||
createDistributionBoardInsertProjectCommand,
|
||||
createDistributionBoardStructureSnapshot,
|
||||
type DistributionBoardStructureProjectCommand,
|
||||
} from "../src/domain/models/distribution-board-structure-project-command.model.js";
|
||||
import {
|
||||
createDistributionBoardUpdateProjectCommand,
|
||||
type DistributionBoardUpdateProjectCommand,
|
||||
} from "../src/domain/models/distribution-board-project-command.model.js";
|
||||
import {
|
||||
copyDistributionBoard,
|
||||
@@ -62,43 +60,6 @@ function getStructureCounts(context: DatabaseContext) {
|
||||
};
|
||||
}
|
||||
|
||||
function legacySections(circuitListId: string) {
|
||||
return [
|
||||
{
|
||||
id: "legacy-lighting",
|
||||
circuitListId,
|
||||
key: "lighting",
|
||||
displayName: "Lighting",
|
||||
prefix: "-1F",
|
||||
sortOrder: 10,
|
||||
},
|
||||
{
|
||||
id: "legacy-single-phase",
|
||||
circuitListId,
|
||||
key: "single_phase",
|
||||
displayName: "Single-phase circuits",
|
||||
prefix: "-2F",
|
||||
sortOrder: 20,
|
||||
},
|
||||
{
|
||||
id: "legacy-three-phase",
|
||||
circuitListId,
|
||||
key: "three_phase",
|
||||
displayName: "Three-phase circuits",
|
||||
prefix: "-3F",
|
||||
sortOrder: 30,
|
||||
},
|
||||
{
|
||||
id: "legacy-unassigned",
|
||||
circuitListId,
|
||||
key: "unassigned",
|
||||
displayName: "Unassigned",
|
||||
prefix: "-UF",
|
||||
sortOrder: 90,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
describe("distribution-board structure project command", () => {
|
||||
it("builds the fixed setup and sends the expected frontend revision", async () => {
|
||||
const structure = createDistributionBoardStructureSnapshot(
|
||||
@@ -308,104 +269,6 @@ describe("distribution-board structure project command", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("applies stored version-one setup commands with unassigned new fields", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
const repository =
|
||||
new DistributionBoardStructureProjectCommandRepository(
|
||||
context.db
|
||||
);
|
||||
const current = createDistributionBoardStructureSnapshot(
|
||||
"project-1",
|
||||
"UV Alt",
|
||||
{ floorId: null, supplyType: null }
|
||||
);
|
||||
const {
|
||||
floorId: _floorId,
|
||||
supplyType: _supplyType,
|
||||
...legacyBoard
|
||||
} = current.distributionBoard;
|
||||
const legacyCommand = {
|
||||
schemaVersion: 1,
|
||||
type: "distribution-board.insert",
|
||||
payload: {
|
||||
structure: {
|
||||
distributionBoard: legacyBoard,
|
||||
circuitList: current.circuitList,
|
||||
sections: legacySections(current.circuitList.id),
|
||||
},
|
||||
},
|
||||
} as unknown as DistributionBoardStructureProjectCommand;
|
||||
repository.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 0,
|
||||
source: "user",
|
||||
command: legacyCommand,
|
||||
});
|
||||
const inserted = context.db
|
||||
.select()
|
||||
.from(distributionBoards)
|
||||
.get();
|
||||
assert.equal(inserted?.floorId, null);
|
||||
assert.equal(inserted?.supplyType, null);
|
||||
assert.equal(
|
||||
context.db.select().from(circuitSections).all().length,
|
||||
4
|
||||
);
|
||||
assert.equal(
|
||||
context.db.select().from(distributionBoardComponents).all()
|
||||
.length,
|
||||
0
|
||||
);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps stored version-two setup commands component-free", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
const repository =
|
||||
new DistributionBoardStructureProjectCommandRepository(
|
||||
context.db
|
||||
);
|
||||
const current = createDistributionBoardStructureSnapshot(
|
||||
"project-1",
|
||||
"UV Version 2"
|
||||
);
|
||||
const command = {
|
||||
schemaVersion: 2,
|
||||
type: "distribution-board.insert",
|
||||
payload: {
|
||||
structure: {
|
||||
distributionBoard: current.distributionBoard,
|
||||
circuitList: current.circuitList,
|
||||
sections: legacySections(current.circuitList.id),
|
||||
},
|
||||
},
|
||||
} as DistributionBoardStructureProjectCommand;
|
||||
|
||||
const inserted = repository.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 0,
|
||||
source: "user",
|
||||
command,
|
||||
});
|
||||
assert.equal(inserted.inverse.schemaVersion, 2);
|
||||
assert.equal(
|
||||
context.db.select().from(circuitSections).all().length,
|
||||
4
|
||||
);
|
||||
assert.equal(
|
||||
context.db.select().from(distributionBoardComponents).all()
|
||||
.length,
|
||||
0
|
||||
);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("updates floor, supply type and simultaneity factor with persisted undo", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
@@ -524,52 +387,6 @@ describe("distribution-board structure project command", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps stored version-one board updates executable", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
const repository =
|
||||
new DistributionBoardStructureProjectCommandRepository(
|
||||
context.db
|
||||
);
|
||||
const structure = createDistributionBoardStructureSnapshot(
|
||||
"project-1",
|
||||
"UV Alt",
|
||||
{ supplyType: "AV" }
|
||||
);
|
||||
repository.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 0,
|
||||
source: "user",
|
||||
command:
|
||||
createDistributionBoardInsertProjectCommand(structure),
|
||||
});
|
||||
const legacyUpdate = {
|
||||
schemaVersion: 1,
|
||||
type: "distribution-board.update",
|
||||
payload: {
|
||||
distributionBoardId: structure.distributionBoard.id,
|
||||
changes: [{ field: "supplyType", value: "SV" }],
|
||||
},
|
||||
} as DistributionBoardUpdateProjectCommand;
|
||||
|
||||
repository.executeUpdate({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 1,
|
||||
source: "user",
|
||||
command: legacyUpdate,
|
||||
});
|
||||
|
||||
const board = context.db
|
||||
.select()
|
||||
.from(distributionBoards)
|
||||
.get();
|
||||
assert.equal(board?.supplyType, "SV");
|
||||
assert.equal(board?.simultaneityFactor, 1);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("sends distribution-board writes to the revision-safe API", async () => {
|
||||
const requests: Array<{ url: string; method: string; body: unknown }> = [];
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
@@ -82,9 +82,6 @@ function createSnapshot(): DistributionBoardSubtreeSnapshot {
|
||||
equipmentIdentifier: "-1F1.1",
|
||||
displayName: "Licht",
|
||||
sortOrder: 10,
|
||||
protectionType: null,
|
||||
protectionRatedCurrent: null,
|
||||
protectionCharacteristic: null,
|
||||
cableType: "NYM-J",
|
||||
cableCrossSection: "3x1,5 mm²",
|
||||
cableLength: 10,
|
||||
@@ -100,7 +97,6 @@ function createSnapshot(): DistributionBoardSubtreeSnapshot {
|
||||
id: "row-1",
|
||||
circuitId: "circuit-1",
|
||||
linkedProjectDeviceId: "device-1",
|
||||
legacyConsumerId: "legacy-1",
|
||||
sortOrder: 10,
|
||||
name: "Leuchte",
|
||||
displayName: "Leuchte",
|
||||
@@ -176,7 +172,6 @@ describe("distribution-board subtree snapshot", () => {
|
||||
"device-1"
|
||||
);
|
||||
assert.equal(clone.circuits[0].deviceRows[0].roomId, "room-1");
|
||||
assert.equal(clone.circuits[0].deviceRows[0].legacyConsumerId, null);
|
||||
assert.equal(clone.circuits[0].protectionDevice?.circuitId, "new-3");
|
||||
assert.doesNotThrow(() => assertDistributionBoardSubtreeSnapshot(clone));
|
||||
});
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
inferSectionKeyFromEquipmentIdentifier,
|
||||
inferSectionKeyFromLegacyInput,
|
||||
normalizeCircuitNumber,
|
||||
} from "../src/domain/services/legacy-consumer-migration-planner.js";
|
||||
|
||||
describe("legacy consumer migration planner", () => {
|
||||
it("normalizes valid circuit numbers and rejects invalid formats", () => {
|
||||
assert.equal(normalizeCircuitNumber(" -2f12 "), "-2F12");
|
||||
assert.equal(normalizeCircuitNumber("2F12"), null);
|
||||
assert.equal(normalizeCircuitNumber(""), null);
|
||||
assert.equal(normalizeCircuitNumber(null), null);
|
||||
});
|
||||
|
||||
it("infers section from equipment identifier prefix", () => {
|
||||
assert.equal(inferSectionKeyFromEquipmentIdentifier("-1F4"), "lighting");
|
||||
assert.equal(inferSectionKeyFromEquipmentIdentifier("-2F8"), "single_phase");
|
||||
assert.equal(inferSectionKeyFromEquipmentIdentifier("-3F1"), "three_phase");
|
||||
assert.equal(inferSectionKeyFromEquipmentIdentifier("-9F1"), null);
|
||||
});
|
||||
|
||||
it("infers section from category/phase when circuit number is missing", () => {
|
||||
assert.equal(
|
||||
inferSectionKeyFromLegacyInput({
|
||||
id: "1",
|
||||
circuitNumber: null,
|
||||
category: "Beleuchtung",
|
||||
phaseType: null,
|
||||
phaseCount: null,
|
||||
}),
|
||||
"lighting"
|
||||
);
|
||||
assert.equal(
|
||||
inferSectionKeyFromLegacyInput({
|
||||
id: "2",
|
||||
circuitNumber: null,
|
||||
category: null,
|
||||
phaseType: "three-phase",
|
||||
phaseCount: null,
|
||||
}),
|
||||
"three_phase"
|
||||
);
|
||||
assert.equal(
|
||||
inferSectionKeyFromLegacyInput({
|
||||
id: "3",
|
||||
circuitNumber: null,
|
||||
category: null,
|
||||
phaseType: null,
|
||||
phaseCount: null,
|
||||
}),
|
||||
null
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,245 +0,0 @@
|
||||
import path from "node:path";
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
||||
import {
|
||||
createDatabaseContext,
|
||||
type DatabaseContext,
|
||||
} from "../src/db/database-context.js";
|
||||
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
||||
import { LegacyConsumerMigrationRepository } from "../src/db/repositories/legacy-consumer-migration.repository.js";
|
||||
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
||||
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
||||
import { circuits } from "../src/db/schema/circuits.js";
|
||||
import { consumers } from "../src/db/schema/consumers.js";
|
||||
import { legacyConsumerCircuitMigrations } from "../src/db/schema/legacy-consumer-circuit-migrations.js";
|
||||
import { legacyConsumerMigrationReports } from "../src/db/schema/legacy-consumer-migration-report.js";
|
||||
import { projects } from "../src/db/schema/projects.js";
|
||||
|
||||
function createTestDatabase() {
|
||||
const context = createDatabaseContext(":memory:");
|
||||
migrate(context.db, {
|
||||
migrationsFolder: path.resolve("src", "db", "migrations"),
|
||||
});
|
||||
context.db.insert(projects).values({ id: "project-1", name: "Test project" }).run();
|
||||
const board = new DistributionBoardFixtureRepository(
|
||||
context.db
|
||||
).createWithCircuitListAndDefaultSections("project-1", "UV-01");
|
||||
const [section] = context.db
|
||||
.select()
|
||||
.from(circuitSections)
|
||||
.where(eq(circuitSections.circuitListId, board.id))
|
||||
.limit(1)
|
||||
.all();
|
||||
return { context, circuitListId: board.id, sectionId: section.id };
|
||||
}
|
||||
|
||||
function migrationInput(circuitListId: string, sectionId: string) {
|
||||
return {
|
||||
circuitListId,
|
||||
circuits: [
|
||||
{
|
||||
circuit: {
|
||||
circuitListId,
|
||||
sectionId,
|
||||
equipmentIdentifier: "-1F1",
|
||||
displayName: "Legacy lighting",
|
||||
sortOrder: 10,
|
||||
},
|
||||
deviceRows: [
|
||||
{
|
||||
legacyConsumerId: "consumer-1",
|
||||
sortOrder: 10,
|
||||
name: "Light 1",
|
||||
displayName: "Light 1",
|
||||
quantity: 1,
|
||||
powerPerUnit: 0.1,
|
||||
simultaneityFactor: 1,
|
||||
},
|
||||
{
|
||||
legacyConsumerId: "consumer-2",
|
||||
sortOrder: 20,
|
||||
name: "Light 2",
|
||||
displayName: "Light 2",
|
||||
quantity: 2,
|
||||
powerPerUnit: 0.2,
|
||||
simultaneityFactor: 0.8,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
report: {
|
||||
legacyConsumerCount: 2,
|
||||
createdCircuitCount: 1,
|
||||
createdDeviceRowCount: 2,
|
||||
duplicateGroupedCount: 1,
|
||||
generatedIdentifierCount: 0,
|
||||
unassignedRowCount: 0,
|
||||
warningsJson: "[]",
|
||||
generatedIdentifiersJson: "[]",
|
||||
duplicateGroupsJson:
|
||||
"[{\"normalizedCircuitNumber\":\"-1F1\",\"count\":2}]",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function assertNoMigrationWrites(context: DatabaseContext) {
|
||||
assert.equal(context.db.select().from(circuits).all().length, 0);
|
||||
assert.equal(context.db.select().from(circuitDeviceRows).all().length, 0);
|
||||
assert.equal(
|
||||
context.db.select().from(legacyConsumerCircuitMigrations).all().length,
|
||||
0
|
||||
);
|
||||
assert.equal(
|
||||
context.db.select().from(legacyConsumerMigrationReports).all().length,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
describe("legacy consumer migration repository", () => {
|
||||
it("reports every consumer without a completed migration mapping", async () => {
|
||||
const { context, circuitListId, sectionId } = createTestDatabase();
|
||||
try {
|
||||
context.db
|
||||
.insert(consumers)
|
||||
.values([
|
||||
{
|
||||
id: "consumer-1",
|
||||
projectId: "project-1",
|
||||
circuitListId,
|
||||
name: "Light 1",
|
||||
quantity: 1,
|
||||
installedPowerPerUnitKw: 0.1,
|
||||
demandFactor: 1,
|
||||
},
|
||||
{
|
||||
id: "consumer-2",
|
||||
projectId: "project-1",
|
||||
circuitListId: null,
|
||||
name: "Light 2",
|
||||
quantity: 1,
|
||||
installedPowerPerUnitKw: 0.2,
|
||||
demandFactor: 1,
|
||||
},
|
||||
])
|
||||
.run();
|
||||
const repository = new LegacyConsumerMigrationRepository(context.db);
|
||||
|
||||
assert.deepEqual(
|
||||
(await repository.listSourceConsumersByCircuitList(circuitListId)).map(
|
||||
(consumer) => consumer.id
|
||||
),
|
||||
["consumer-1"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
(await repository.listUnmigratedConsumers()).sort((left, right) =>
|
||||
left.id.localeCompare(right.id)
|
||||
),
|
||||
[
|
||||
{
|
||||
id: "consumer-1",
|
||||
projectId: "project-1",
|
||||
circuitListId,
|
||||
},
|
||||
{
|
||||
id: "consumer-2",
|
||||
projectId: "project-1",
|
||||
circuitListId: null,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
repository.persistCircuitListMigration(
|
||||
migrationInput(circuitListId, sectionId)
|
||||
);
|
||||
assert.deepEqual(await repository.listUnmigratedConsumers(), []);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("commits circuits, rows, mappings and report together", () => {
|
||||
const { context, circuitListId, sectionId } = createTestDatabase();
|
||||
try {
|
||||
const repository = new LegacyConsumerMigrationRepository(context.db);
|
||||
|
||||
repository.persistCircuitListMigration(
|
||||
migrationInput(circuitListId, sectionId)
|
||||
);
|
||||
|
||||
const persistedCircuits = context.db.select().from(circuits).all();
|
||||
const persistedRows = context.db
|
||||
.select()
|
||||
.from(circuitDeviceRows)
|
||||
.all()
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder);
|
||||
const persistedMappings = context.db
|
||||
.select()
|
||||
.from(legacyConsumerCircuitMigrations)
|
||||
.all()
|
||||
.sort((left, right) => left.consumerId.localeCompare(right.consumerId));
|
||||
const persistedReports = context.db
|
||||
.select()
|
||||
.from(legacyConsumerMigrationReports)
|
||||
.all();
|
||||
|
||||
assert.equal(persistedCircuits.length, 1);
|
||||
assert.equal(persistedRows.length, 2);
|
||||
assert.equal(persistedMappings.length, 2);
|
||||
assert.equal(persistedReports.length, 1);
|
||||
assert.deepEqual(
|
||||
persistedMappings.map((mapping) => [
|
||||
mapping.consumerId,
|
||||
mapping.circuitId,
|
||||
mapping.circuitDeviceRowId,
|
||||
mapping.circuitListId,
|
||||
]),
|
||||
[
|
||||
[
|
||||
"consumer-1",
|
||||
persistedCircuits[0].id,
|
||||
persistedRows[0].id,
|
||||
circuitListId,
|
||||
],
|
||||
[
|
||||
"consumer-2",
|
||||
persistedCircuits[0].id,
|
||||
persistedRows[1].id,
|
||||
circuitListId,
|
||||
],
|
||||
]
|
||||
);
|
||||
assert.equal(persistedReports[0].createdCircuitCount, 1);
|
||||
assert.equal(persistedReports[0].createdDeviceRowCount, 2);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("rolls back all migrated entities when the final report write fails", () => {
|
||||
const { context, circuitListId, sectionId } = createTestDatabase();
|
||||
try {
|
||||
context.sqlite.exec(`
|
||||
CREATE TRIGGER fail_migration_report
|
||||
BEFORE INSERT ON legacy_consumer_migration_reports
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'forced migration report failure');
|
||||
END;
|
||||
`);
|
||||
const repository = new LegacyConsumerMigrationRepository(context.db);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
repository.persistCircuitListMigration(
|
||||
migrationInput(circuitListId, sectionId)
|
||||
),
|
||||
/forced migration report failure/
|
||||
);
|
||||
|
||||
assertNoMigrationWrites(context);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,130 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import type {
|
||||
LegacyConsumerMigrationDependencies,
|
||||
LegacyMigrationCircuitInput,
|
||||
LegacyMigrationReportInput,
|
||||
} from "../src/domain/ports/legacy-consumer-migration.store.js";
|
||||
import { LegacyConsumerMigrationService } from "../src/domain/services/legacy-consumer-migration.service.js";
|
||||
|
||||
describe("legacy consumer migration service", () => {
|
||||
it("plans the migration through injected readers and persists one complete batch", async () => {
|
||||
let defaultsCreatedFor: string | null = null;
|
||||
let persisted:
|
||||
| {
|
||||
circuitListId: string;
|
||||
circuits: LegacyMigrationCircuitInput[];
|
||||
report: LegacyMigrationReportInput;
|
||||
}
|
||||
| undefined;
|
||||
const dependencies: LegacyConsumerMigrationDependencies = {
|
||||
circuitListReader: {
|
||||
async findById(projectId, circuitListId) {
|
||||
assert.equal(projectId, "project-1");
|
||||
assert.equal(circuitListId, "list-1");
|
||||
return { id: circuitListId };
|
||||
},
|
||||
},
|
||||
sectionStore: {
|
||||
async createDefaults(circuitListId) {
|
||||
defaultsCreatedFor = circuitListId;
|
||||
},
|
||||
async listByCircuitList() {
|
||||
return [
|
||||
{ id: "section-1", key: "single_phase", prefix: "-2F" },
|
||||
{ id: "section-2", key: "unassigned", prefix: "-XF" },
|
||||
];
|
||||
},
|
||||
},
|
||||
circuitReader: {
|
||||
async listByCircuitList() {
|
||||
return [
|
||||
{
|
||||
sectionId: "section-1",
|
||||
equipmentIdentifier: "-2F2",
|
||||
sortOrder: 10,
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
roomReader: {
|
||||
async listByProject() {
|
||||
return [
|
||||
{ id: "room-1", roomNumber: "1.01", roomName: "Besprechung" },
|
||||
];
|
||||
},
|
||||
},
|
||||
migrationStore: {
|
||||
async listSourceConsumersByCircuitList() {
|
||||
return [
|
||||
{
|
||||
id: "consumer-1",
|
||||
projectDeviceId: null,
|
||||
roomId: "room-1",
|
||||
circuitNumber: null,
|
||||
description: "Steckdosen Besprechung",
|
||||
name: "Steckdose",
|
||||
category: null,
|
||||
deviceType: null,
|
||||
phaseType: null,
|
||||
tradeOrCostGroup: "KG 440",
|
||||
protectionType: null,
|
||||
protectionRatedCurrent: null,
|
||||
protectionCharacteristic: null,
|
||||
cableType: null,
|
||||
cableCrossSection: null,
|
||||
comment: null,
|
||||
quantity: 2,
|
||||
installedPowerPerUnitKw: 0.2,
|
||||
demandFactor: 0.8,
|
||||
voltageV: 230,
|
||||
phaseCount: 1,
|
||||
powerFactor: 0.95,
|
||||
note: "Bestand",
|
||||
},
|
||||
];
|
||||
},
|
||||
async listMigratedConsumerIds() {
|
||||
return [];
|
||||
},
|
||||
persistCircuitListMigration(input) {
|
||||
persisted = input;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const report = await new LegacyConsumerMigrationService(
|
||||
dependencies
|
||||
).migrateCircuitList("project-1", "list-1");
|
||||
|
||||
assert.equal(defaultsCreatedFor, "list-1");
|
||||
assert.deepEqual(report.generatedIdentifiers, ["-2F3"]);
|
||||
assert.equal(report.createdCircuitCount, 1);
|
||||
assert.equal(report.createdDeviceRowCount, 1);
|
||||
assert.equal(persisted?.circuitListId, "list-1");
|
||||
assert.equal(persisted?.circuits[0].circuit.sectionId, "section-1");
|
||||
assert.equal(
|
||||
persisted?.circuits[0].circuit.equipmentIdentifier,
|
||||
"-2F3"
|
||||
);
|
||||
assert.deepEqual(persisted?.circuits[0].deviceRows[0], {
|
||||
linkedProjectDeviceId: undefined,
|
||||
legacyConsumerId: "consumer-1",
|
||||
sortOrder: 10,
|
||||
name: "Steckdose",
|
||||
displayName: "Steckdosen Besprechung",
|
||||
phaseType: undefined,
|
||||
connectionKind: undefined,
|
||||
costGroup: "KG 440",
|
||||
category: undefined,
|
||||
roomId: "room-1",
|
||||
roomNumberSnapshot: "1.01",
|
||||
roomNameSnapshot: "Besprechung",
|
||||
quantity: 2,
|
||||
powerPerUnit: 0.2,
|
||||
simultaneityFactor: 0.8,
|
||||
cosPhi: 0.95,
|
||||
remark: "Bestand",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -60,7 +60,7 @@ describe("serialized project commands", () => {
|
||||
circuitId: "circuit-1",
|
||||
changes: {
|
||||
displayName: "Werkstatt",
|
||||
protectionRatedCurrent: 16,
|
||||
cableLength: 16,
|
||||
isReserve: false,
|
||||
remark: null,
|
||||
},
|
||||
@@ -378,7 +378,6 @@ describe("project-device structure project commands", () => {
|
||||
id: "row-1",
|
||||
circuitId: "circuit-1",
|
||||
linkedProjectDeviceId: null,
|
||||
legacyConsumerId: null,
|
||||
sortOrder: 10,
|
||||
name: "Luminaire",
|
||||
displayName: "Local lighting",
|
||||
@@ -445,7 +444,6 @@ describe("circuit device-row structure project commands", () => {
|
||||
id: "row-1",
|
||||
circuitId: "circuit-1",
|
||||
linkedProjectDeviceId: null,
|
||||
legacyConsumerId: null,
|
||||
sortOrder: 10,
|
||||
name: "Leuchte",
|
||||
displayName: "Leuchte",
|
||||
@@ -517,9 +515,6 @@ describe("circuit device-row move project commands", () => {
|
||||
equipmentIdentifier: "-1F3",
|
||||
displayName: "Neuer Stromkreis",
|
||||
sortOrder: 30,
|
||||
protectionType: null,
|
||||
protectionRatedCurrent: null,
|
||||
protectionCharacteristic: null,
|
||||
cableType: null,
|
||||
cableCrossSection: null,
|
||||
cableLength: null,
|
||||
@@ -683,7 +678,6 @@ describe("circuit structure project commands", () => {
|
||||
id: "row-1",
|
||||
circuitId: "circuit-1",
|
||||
linkedProjectDeviceId: null,
|
||||
legacyConsumerId: null,
|
||||
sortOrder: 10,
|
||||
name: "Leuchte",
|
||||
displayName: "Leuchte",
|
||||
@@ -709,9 +703,6 @@ describe("circuit structure project commands", () => {
|
||||
equipmentIdentifier: "-1F1",
|
||||
displayName: "Beleuchtung",
|
||||
sortOrder: 10,
|
||||
protectionType: null,
|
||||
protectionRatedCurrent: null,
|
||||
protectionCharacteristic: null,
|
||||
cableType: null,
|
||||
cableCrossSection: null,
|
||||
cableLength: null,
|
||||
|
||||
@@ -555,7 +555,6 @@ describe("project command service", () => {
|
||||
id: "row-2",
|
||||
circuitId: "circuit-1",
|
||||
linkedProjectDeviceId: null,
|
||||
legacyConsumerId: null,
|
||||
sortOrder: 20,
|
||||
name: "Steckdose",
|
||||
displayName: "Steckdose",
|
||||
@@ -623,9 +622,6 @@ describe("project command service", () => {
|
||||
equipmentIdentifier: "-1F2",
|
||||
displayName: "Reserve",
|
||||
sortOrder: 20,
|
||||
protectionType: null,
|
||||
protectionRatedCurrent: null,
|
||||
protectionCharacteristic: null,
|
||||
cableType: null,
|
||||
cableCrossSection: null,
|
||||
cableLength: null,
|
||||
@@ -745,9 +741,6 @@ describe("project command service", () => {
|
||||
equipmentIdentifier: "-1F2",
|
||||
displayName: "Neuer Stromkreis",
|
||||
sortOrder: 20,
|
||||
protectionType: null,
|
||||
protectionRatedCurrent: null,
|
||||
protectionCharacteristic: null,
|
||||
cableType: null,
|
||||
cableCrossSection: null,
|
||||
cableLength: null,
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, it } from "node:test";
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
describe("project device canonical-field migration", () => {
|
||||
it("preserves canonical values while removing transitional mirror columns", () => {
|
||||
const sqlite = new Database(":memory:");
|
||||
try {
|
||||
sqlite.exec(`
|
||||
CREATE TABLE project_devices (
|
||||
id text PRIMARY KEY NOT NULL,
|
||||
power_per_unit real NOT NULL,
|
||||
simultaneity_factor real NOT NULL,
|
||||
phase_type text NOT NULL,
|
||||
cos_phi real,
|
||||
remark text,
|
||||
voltage_v real,
|
||||
installed_power_per_unit_kw real NOT NULL,
|
||||
demand_factor real NOT NULL,
|
||||
phase_count integer,
|
||||
power_factor real,
|
||||
note text
|
||||
);
|
||||
INSERT INTO project_devices VALUES (
|
||||
'device-1',
|
||||
0.25,
|
||||
0.8,
|
||||
'three_phase',
|
||||
0.95,
|
||||
'Canonical remark',
|
||||
400,
|
||||
0.25,
|
||||
0.8,
|
||||
3,
|
||||
0.95,
|
||||
'Canonical remark'
|
||||
);
|
||||
`);
|
||||
|
||||
const migrationSql = fs.readFileSync(
|
||||
path.resolve(
|
||||
"src",
|
||||
"db",
|
||||
"migrations",
|
||||
"0011_project_device_canonical_fields.sql"
|
||||
),
|
||||
"utf8"
|
||||
);
|
||||
for (const statement of migrationSql.split("--> statement-breakpoint")) {
|
||||
if (statement.trim()) {
|
||||
sqlite.exec(statement);
|
||||
}
|
||||
}
|
||||
|
||||
const columns = (
|
||||
sqlite.prepare("PRAGMA table_info(project_devices)").all() as Array<{
|
||||
name: string;
|
||||
}>
|
||||
).map((column) => column.name);
|
||||
assert.deepEqual(columns, [
|
||||
"id",
|
||||
"power_per_unit",
|
||||
"simultaneity_factor",
|
||||
"phase_type",
|
||||
"cos_phi",
|
||||
"remark",
|
||||
"voltage_v",
|
||||
]);
|
||||
assert.deepEqual(
|
||||
sqlite
|
||||
.prepare(
|
||||
`SELECT
|
||||
power_per_unit AS powerPerUnit,
|
||||
simultaneity_factor AS simultaneityFactor,
|
||||
phase_type AS phaseType,
|
||||
cos_phi AS cosPhi,
|
||||
remark,
|
||||
voltage_v AS voltageV
|
||||
FROM project_devices
|
||||
WHERE id = ?`
|
||||
)
|
||||
.get("device-1"),
|
||||
{
|
||||
powerPerUnit: 0.25,
|
||||
simultaneityFactor: 0.8,
|
||||
phaseType: "three_phase",
|
||||
cosPhi: 0.95,
|
||||
remark: "Canonical remark",
|
||||
voltageV: 400,
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
sqlite.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -30,8 +30,8 @@ describe("project device circuit-first schema", () => {
|
||||
|
||||
it("requires the circuit-first power and phase fields", () => {
|
||||
const result = createProjectDeviceSchema.safeParse({
|
||||
name: "Legacy device",
|
||||
displayName: "Legacy device",
|
||||
name: "Test device",
|
||||
displayName: "Test device",
|
||||
quantity: 1,
|
||||
installedPowerPerUnitKw: 0.1,
|
||||
demandFactor: 1,
|
||||
|
||||
@@ -27,7 +27,6 @@ function linkedRow() {
|
||||
id: "row1",
|
||||
circuitId: "c1",
|
||||
linkedProjectDeviceId: "pd1",
|
||||
legacyConsumerId: null,
|
||||
sortOrder: 10,
|
||||
name: "Old luminaire",
|
||||
displayName: "Local display name",
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
} from "../src/db/database-context.js";
|
||||
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
||||
import { ProjectLocationStructureProjectCommandRepository } from "../src/db/repositories/project-location-structure-project-command.repository.js";
|
||||
import { consumers } from "../src/db/schema/consumers.js";
|
||||
import { distributionBoards } from "../src/db/schema/distribution-boards.js";
|
||||
import { floors } from "../src/db/schema/floors.js";
|
||||
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
||||
@@ -423,53 +422,6 @@ describe("project-location structure project command", () => {
|
||||
floorContext.close();
|
||||
}
|
||||
|
||||
const roomContext = createTestDatabase();
|
||||
try {
|
||||
const repository =
|
||||
new ProjectLocationStructureProjectCommandRepository(
|
||||
roomContext.db
|
||||
);
|
||||
const room = createProjectRoomSnapshot("project-1", {
|
||||
roomNumber: "001",
|
||||
roomName: "Technik",
|
||||
});
|
||||
const inserted = repository.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 0,
|
||||
source: "user",
|
||||
command: createProjectRoomInsertProjectCommand(room),
|
||||
});
|
||||
roomContext.db
|
||||
.insert(consumers)
|
||||
.values({
|
||||
id: "legacy-consumer-1",
|
||||
projectId: "project-1",
|
||||
roomId: room.id,
|
||||
name: "Bestand",
|
||||
quantity: 1,
|
||||
installedPowerPerUnitKw: 1,
|
||||
demandFactor: 1,
|
||||
})
|
||||
.run();
|
||||
const undoStep = new ProjectHistoryRepository(
|
||||
roomContext.db
|
||||
).getNextCommand("project-1", "undo");
|
||||
assert.ok(undoStep);
|
||||
assert.throws(
|
||||
() =>
|
||||
repository.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 1,
|
||||
source: "undo",
|
||||
historyTargetChangeSetId: undoStep.changeSetId,
|
||||
command: inserted.inverse,
|
||||
}),
|
||||
/referenced project room/
|
||||
);
|
||||
} finally {
|
||||
roomContext.close();
|
||||
}
|
||||
|
||||
const rollbackContext = createTestDatabase();
|
||||
try {
|
||||
rollbackContext.sqlite.exec(`
|
||||
|
||||
@@ -365,37 +365,6 @@ describe("project settings project-command repository", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("applies persisted version-one voltage commands without clearing metadata", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
const repository = new ProjectSettingsProjectCommandRepository(
|
||||
context.db
|
||||
);
|
||||
repository.executeUpdate({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 0,
|
||||
source: "user",
|
||||
command: {
|
||||
schemaVersion: 1,
|
||||
type: "project.update-settings",
|
||||
payload: {
|
||||
singlePhaseVoltageV: 240,
|
||||
threePhaseVoltageV: 415,
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.deepEqual(getSettings(context), {
|
||||
...settings({
|
||||
singlePhaseVoltageV: 240,
|
||||
threePhaseVoltageV: 415,
|
||||
}),
|
||||
currentRevision: 1,
|
||||
});
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("persists the project supply-type selection and rejects disabling an in-use type", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
|
||||
@@ -172,18 +172,6 @@ function createTestDatabase(): DatabaseContext {
|
||||
return context;
|
||||
}
|
||||
|
||||
function removeVersionSevenFields(state: Record<string, unknown>) {
|
||||
delete state.distributionBoardComponents;
|
||||
delete state.circuitProtectionDevices;
|
||||
delete state.distributionBoardComponentProtectionDevices;
|
||||
for (const section of state.circuitSections as Array<
|
||||
Record<string, unknown>
|
||||
>) {
|
||||
delete section.category;
|
||||
delete section.groupNumber;
|
||||
}
|
||||
}
|
||||
|
||||
describe("project snapshot repository", () => {
|
||||
it("captures, hashes and lists a complete named logical snapshot", () => {
|
||||
const context = createTestDatabase();
|
||||
@@ -354,41 +342,23 @@ describe("project snapshot repository", () => {
|
||||
) as Record<string, unknown> & {
|
||||
project: Record<string, unknown>;
|
||||
};
|
||||
currentPayload.schemaVersion = 1;
|
||||
removeVersionSevenFields(currentPayload);
|
||||
delete currentPayload.project.internalProjectNumber;
|
||||
delete currentPayload.project.externalProjectNumber;
|
||||
delete currentPayload.project.buildingOwner;
|
||||
delete currentPayload.project.description;
|
||||
delete currentPayload.project.enabledDistributionBoardSupplyTypes;
|
||||
for (const board of currentPayload.distributionBoards as Array<
|
||||
Record<string, unknown>
|
||||
>) {
|
||||
delete board.floorId;
|
||||
delete board.supplyType;
|
||||
delete board.simultaneityFactor;
|
||||
}
|
||||
const legacyPayloadJson = JSON.stringify(currentPayload);
|
||||
currentPayload.schemaVersion = 99;
|
||||
const unsupportedPayloadJson = JSON.stringify(currentPayload);
|
||||
context.db
|
||||
.update(projectSnapshots)
|
||||
.set({
|
||||
schemaVersion: 1,
|
||||
payloadJson: legacyPayloadJson,
|
||||
schemaVersion: 99,
|
||||
payloadJson: unsupportedPayloadJson,
|
||||
payloadSha256: crypto
|
||||
.createHash("sha256")
|
||||
.update(legacyPayloadJson)
|
||||
.update(unsupportedPayloadJson)
|
||||
.digest("hex"),
|
||||
})
|
||||
.where(eq(projectSnapshots.id, snapshot.id))
|
||||
.run();
|
||||
const legacyPrepared = repository.prepareRestore(
|
||||
"project-1",
|
||||
snapshot.id
|
||||
);
|
||||
assert.ok(legacyPrepared);
|
||||
assert.equal(
|
||||
legacyPrepared.command.payload.targetState.project.buildingOwner,
|
||||
null
|
||||
assert.throws(
|
||||
() => repository.prepareRestore("project-1", snapshot.id),
|
||||
/schema version is not supported/
|
||||
);
|
||||
context.db
|
||||
.update(projectSnapshots)
|
||||
@@ -474,10 +444,6 @@ describe("portable project transfer", () => {
|
||||
copied.state.distributionBoards[0].simultaneityFactor,
|
||||
0.65
|
||||
);
|
||||
assert.equal(
|
||||
copied.state.circuits[0].deviceRows[0].legacyConsumerId,
|
||||
null
|
||||
);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
@@ -512,52 +478,6 @@ describe("portable project transfer", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("imports a checksum-valid version-two project transfer", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
const repository = new ProjectTransferRepository(context.db);
|
||||
const current = repository.exportProject("project-1");
|
||||
assert.ok(current);
|
||||
const legacyState = structuredClone(
|
||||
current.projectState
|
||||
) as unknown as Record<string, unknown> & {
|
||||
distributionBoards: Array<Record<string, unknown>>;
|
||||
};
|
||||
legacyState.schemaVersion = 2;
|
||||
removeVersionSevenFields(legacyState);
|
||||
delete (
|
||||
legacyState.project as Record<string, unknown>
|
||||
).enabledDistributionBoardSupplyTypes;
|
||||
for (const board of legacyState.distributionBoards) {
|
||||
delete board.floorId;
|
||||
delete board.supplyType;
|
||||
delete board.simultaneityFactor;
|
||||
}
|
||||
const legacyPayloadJson = JSON.stringify(legacyState);
|
||||
const duplicate = repository.importDuplicate({
|
||||
...current,
|
||||
projectState: legacyState,
|
||||
payloadSha256: crypto
|
||||
.createHash("sha256")
|
||||
.update(legacyPayloadJson)
|
||||
.digest("hex"),
|
||||
});
|
||||
const copied = readProjectStateSnapshot(
|
||||
context.db,
|
||||
duplicate.projectId
|
||||
);
|
||||
assert.ok(copied);
|
||||
assert.equal(copied.state.distributionBoards[0].floorId, null);
|
||||
assert.equal(copied.state.distributionBoards[0].supplyType, null);
|
||||
assert.equal(
|
||||
copied.state.distributionBoards[0].simultaneityFactor,
|
||||
1
|
||||
);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("rolls back a duplicate when a late project-state insert fails", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
|
||||
@@ -37,12 +37,9 @@ import { circuitLists } from "../src/db/schema/circuit-lists.js";
|
||||
import { circuitProtectionDevices } from "../src/db/schema/circuit-protection-devices.js";
|
||||
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
||||
import { circuits } from "../src/db/schema/circuits.js";
|
||||
import { consumers } from "../src/db/schema/consumers.js";
|
||||
import { distributionBoardComponentProtectionDevices } from "../src/db/schema/distribution-board-component-protection-devices.js";
|
||||
import { distributionBoardComponents } from "../src/db/schema/distribution-board-components.js";
|
||||
import { floors } from "../src/db/schema/floors.js";
|
||||
import { legacyConsumerCircuitMigrations } from "../src/db/schema/legacy-consumer-circuit-migrations.js";
|
||||
import { legacyConsumerMigrationReports } from "../src/db/schema/legacy-consumer-migration-report.js";
|
||||
import { projectDevices } from "../src/db/schema/project-devices.js";
|
||||
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
||||
import { projectSnapshots } from "../src/db/schema/project-snapshots.js";
|
||||
@@ -282,59 +279,6 @@ function changeCompleteProjectState(context: DatabaseContext) {
|
||||
).createWithCircuitListAndDefaultSections("project-1", "UV-02");
|
||||
}
|
||||
|
||||
function seedUpgradeOnlyData(context: DatabaseContext) {
|
||||
const circuit = context.db
|
||||
.select()
|
||||
.from(circuits)
|
||||
.where(eq(circuits.id, "circuit-1"))
|
||||
.get();
|
||||
assert.ok(circuit);
|
||||
context.db
|
||||
.insert(consumers)
|
||||
.values({
|
||||
id: "consumer-1",
|
||||
projectId: "project-1",
|
||||
distributionBoardId:
|
||||
context.db.select().from(circuitLists).get()!
|
||||
.distributionBoardId,
|
||||
circuitListId: circuit.circuitListId,
|
||||
projectDeviceId: "device-1",
|
||||
roomId: "room-1",
|
||||
name: "Legacy-Pumpe",
|
||||
quantity: 1,
|
||||
installedPowerPerUnitKw: 2.5,
|
||||
demandFactor: 0.8,
|
||||
})
|
||||
.run();
|
||||
context.db
|
||||
.insert(legacyConsumerCircuitMigrations)
|
||||
.values({
|
||||
consumerId: "consumer-1",
|
||||
circuitId: "circuit-1",
|
||||
circuitDeviceRowId: "row-1",
|
||||
circuitListId: circuit.circuitListId,
|
||||
createdAtIso: "2026-07-25T00:00:00.000Z",
|
||||
})
|
||||
.run();
|
||||
context.db
|
||||
.insert(legacyConsumerMigrationReports)
|
||||
.values({
|
||||
id: "report-1",
|
||||
circuitListId: circuit.circuitListId,
|
||||
legacyConsumerCount: 1,
|
||||
createdCircuitCount: 1,
|
||||
createdDeviceRowCount: 1,
|
||||
duplicateGroupedCount: 0,
|
||||
generatedIdentifierCount: 0,
|
||||
unassignedRowCount: 0,
|
||||
warningsJson: "[]",
|
||||
generatedIdentifiersJson: "[]",
|
||||
duplicateGroupsJson: "[]",
|
||||
createdAtIso: "2026-07-25T00:00:00.000Z",
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
||||
describe("project state restore command", () => {
|
||||
it("undoes a remapped cross-project import using the persisted target hash", () => {
|
||||
const context = createTestDatabase();
|
||||
@@ -360,7 +304,7 @@ describe("project state restore command", () => {
|
||||
description: "Projektimport wiederherstellen",
|
||||
command: prepared.command,
|
||||
});
|
||||
const legacyExpectedHash = hashProjectStatePayload(
|
||||
const serializedExpectedHash = hashProjectStatePayload(
|
||||
serializeProjectStateSnapshot(
|
||||
prepared.command.payload.targetState
|
||||
)
|
||||
@@ -372,7 +316,7 @@ describe("project state restore command", () => {
|
||||
.get();
|
||||
assert.ok(storedChangeSet);
|
||||
const inverse = JSON.parse(storedChangeSet.inversePayloadJson);
|
||||
inverse.payload.expectedStateSha256 = legacyExpectedHash;
|
||||
inverse.payload.expectedStateSha256 = serializedExpectedHash;
|
||||
context.db
|
||||
.update(projectChangeSets)
|
||||
.set({ inversePayloadJson: JSON.stringify(inverse) })
|
||||
@@ -397,7 +341,6 @@ describe("project state restore command", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
const snapshots = new ProjectSnapshotRepository(context.db);
|
||||
seedUpgradeOnlyData(context);
|
||||
const snapshot = snapshots.createNamed({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 0,
|
||||
@@ -443,28 +386,6 @@ describe("project state restore command", () => {
|
||||
context.db.select().from(projectSnapshots).all().length,
|
||||
1
|
||||
);
|
||||
assert.equal(
|
||||
context.db
|
||||
.select()
|
||||
.from(legacyConsumerCircuitMigrations)
|
||||
.all().length,
|
||||
1
|
||||
);
|
||||
assert.equal(
|
||||
context.db
|
||||
.select()
|
||||
.from(legacyConsumerMigrationReports)
|
||||
.all().length,
|
||||
1
|
||||
);
|
||||
assert.equal(
|
||||
context.db
|
||||
.select({ roomId: consumers.roomId })
|
||||
.from(consumers)
|
||||
.where(eq(consumers.id, "consumer-1"))
|
||||
.get()?.roomId,
|
||||
"room-1"
|
||||
);
|
||||
|
||||
const undone = service.undo({
|
||||
projectId: "project-1",
|
||||
|
||||
@@ -42,17 +42,6 @@ function minimalSnapshot() {
|
||||
};
|
||||
}
|
||||
|
||||
function legacyMinimalSnapshot() {
|
||||
const {
|
||||
distributionBoardComponents: _distributionBoardComponents,
|
||||
circuitProtectionDevices: _circuitProtectionDevices,
|
||||
distributionBoardComponentProtectionDevices:
|
||||
_distributionBoardComponentProtectionDevices,
|
||||
...snapshot
|
||||
} = minimalSnapshot();
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function protectedSnapshot() {
|
||||
return {
|
||||
...minimalSnapshot(),
|
||||
@@ -94,9 +83,6 @@ function protectedSnapshot() {
|
||||
equipmentIdentifier: "-1F1.1",
|
||||
displayName: "Reserve",
|
||||
sortOrder: 10,
|
||||
protectionType: null,
|
||||
protectionRatedCurrent: null,
|
||||
protectionCharacteristic: null,
|
||||
cableType: null,
|
||||
cableCrossSection: null,
|
||||
cableLength: null,
|
||||
@@ -148,234 +134,26 @@ function protectedSnapshot() {
|
||||
}
|
||||
|
||||
describe("project state snapshot model", () => {
|
||||
it("round-trips a versioned logical project state", () => {
|
||||
it("round-trips the current logical project state", () => {
|
||||
const snapshot = parseProjectStateSnapshot(minimalSnapshot());
|
||||
assert.deepEqual(
|
||||
deserializeProjectStateSnapshot(
|
||||
serializeProjectStateSnapshot(snapshot)
|
||||
),
|
||||
deserializeProjectStateSnapshot(serializeProjectStateSnapshot(snapshot)),
|
||||
snapshot
|
||||
);
|
||||
});
|
||||
|
||||
it("upgrades version-one snapshots with empty project metadata", () => {
|
||||
const legacy = {
|
||||
...legacyMinimalSnapshot(),
|
||||
schemaVersion: 1 as const,
|
||||
project: {
|
||||
id: "project-1",
|
||||
name: "Altprojekt",
|
||||
singlePhaseVoltageV: 230,
|
||||
threePhaseVoltageV: 400,
|
||||
},
|
||||
};
|
||||
const snapshot = parseProjectStateSnapshot(legacy);
|
||||
assert.equal(snapshot.schemaVersion, projectStateSnapshotSchemaVersion);
|
||||
assert.deepEqual(snapshot.project, {
|
||||
...legacy.project,
|
||||
internalProjectNumber: null,
|
||||
externalProjectNumber: null,
|
||||
buildingOwner: null,
|
||||
description: null,
|
||||
enabledDistributionBoardSupplyTypes: [
|
||||
"AV",
|
||||
"SV",
|
||||
"EV",
|
||||
"USV",
|
||||
"MSR",
|
||||
"SiBe",
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("upgrades version-two distribution boards without inferred assignments", () => {
|
||||
const previous = {
|
||||
...legacyMinimalSnapshot(),
|
||||
schemaVersion: 2 as const,
|
||||
project: {
|
||||
...minimalSnapshot().project,
|
||||
enabledDistributionBoardSupplyTypes: undefined,
|
||||
},
|
||||
distributionBoards: [
|
||||
{
|
||||
id: "board-1",
|
||||
projectId: "project-1",
|
||||
name: "UV 1",
|
||||
},
|
||||
],
|
||||
};
|
||||
delete previous.project.enabledDistributionBoardSupplyTypes;
|
||||
const snapshot = parseProjectStateSnapshot(previous);
|
||||
assert.deepEqual(snapshot.distributionBoards, [
|
||||
{
|
||||
...previous.distributionBoards[0],
|
||||
floorId: null,
|
||||
supplyType: null,
|
||||
simultaneityFactor: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("upgrades version-three snapshots with all project supply types", () => {
|
||||
const previous = {
|
||||
...legacyMinimalSnapshot(),
|
||||
schemaVersion: 3 as const,
|
||||
project: {
|
||||
...minimalSnapshot().project,
|
||||
enabledDistributionBoardSupplyTypes: undefined,
|
||||
},
|
||||
};
|
||||
delete previous.project.enabledDistributionBoardSupplyTypes;
|
||||
const snapshot = parseProjectStateSnapshot(previous);
|
||||
assert.deepEqual(snapshot.project.enabledDistributionBoardSupplyTypes, [
|
||||
"AV",
|
||||
"SV",
|
||||
"EV",
|
||||
"USV",
|
||||
"MSR",
|
||||
"SiBe",
|
||||
]);
|
||||
});
|
||||
|
||||
it("normalizes version-four device voltages to project settings", () => {
|
||||
const previous = {
|
||||
...legacyMinimalSnapshot(),
|
||||
schemaVersion: 4 as const,
|
||||
projectDevices: [
|
||||
{
|
||||
id: "device-1",
|
||||
projectId: "project-1",
|
||||
name: "Pumpe",
|
||||
displayName: "Pumpe",
|
||||
phaseType: "three_phase" as const,
|
||||
connectionKind: null,
|
||||
costGroup: null,
|
||||
category: null,
|
||||
quantity: 1,
|
||||
powerPerUnit: 2,
|
||||
simultaneityFactor: 1,
|
||||
cosPhi: null,
|
||||
remark: null,
|
||||
voltageV: 500,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const snapshot = parseProjectStateSnapshot(previous);
|
||||
|
||||
assert.equal(snapshot.schemaVersion, projectStateSnapshotSchemaVersion);
|
||||
assert.equal(snapshot.projectDevices[0]?.voltageV, 400);
|
||||
});
|
||||
|
||||
it("upgrades version-five distribution boards with a neutral factor", () => {
|
||||
const previous = {
|
||||
...legacyMinimalSnapshot(),
|
||||
schemaVersion: 5 as const,
|
||||
distributionBoards: [
|
||||
{
|
||||
id: "board-1",
|
||||
projectId: "project-1",
|
||||
name: "UV 1",
|
||||
floorId: null,
|
||||
supplyType: "AV" as const,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const snapshot = parseProjectStateSnapshot(previous);
|
||||
|
||||
assert.equal(snapshot.schemaVersion, projectStateSnapshotSchemaVersion);
|
||||
assert.equal(
|
||||
snapshot.distributionBoards[0]?.simultaneityFactor,
|
||||
1
|
||||
it("rejects pre-baseline snapshot shapes", () => {
|
||||
const outdated = structuredClone(minimalSnapshot());
|
||||
delete (outdated.project as Record<string, unknown>).description;
|
||||
assert.throws(() => parseProjectStateSnapshot(outdated));
|
||||
assert.throws(() =>
|
||||
parseProjectStateSnapshot({ ...minimalSnapshot(), schemaVersion: 7 })
|
||||
);
|
||||
});
|
||||
|
||||
it("upgrades version-six sections without inventing component data", () => {
|
||||
const previous = {
|
||||
...legacyMinimalSnapshot(),
|
||||
schemaVersion: 6 as const,
|
||||
circuitSections: [
|
||||
{
|
||||
id: "section-lighting",
|
||||
circuitListId: "list-1",
|
||||
key: "lighting",
|
||||
displayName: "Beleuchtung",
|
||||
prefix: "-1F",
|
||||
sortOrder: 10,
|
||||
},
|
||||
{
|
||||
id: "section-unassigned",
|
||||
circuitListId: "list-1",
|
||||
key: "unassigned",
|
||||
displayName: "Nicht zugeordnet",
|
||||
prefix: "-UF",
|
||||
sortOrder: 90,
|
||||
},
|
||||
],
|
||||
distributionBoards: [
|
||||
{
|
||||
id: "board-1",
|
||||
projectId: "project-1",
|
||||
name: "UV 1",
|
||||
floorId: null,
|
||||
supplyType: null,
|
||||
simultaneityFactor: 1,
|
||||
},
|
||||
],
|
||||
circuitLists: [
|
||||
{
|
||||
id: "list-1",
|
||||
projectId: "project-1",
|
||||
distributionBoardId: "board-1",
|
||||
name: "UV 1 Stromkreisliste",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const snapshot = parseProjectStateSnapshot(previous);
|
||||
|
||||
assert.deepEqual(
|
||||
snapshot.circuitSections.map(
|
||||
({ key, category, groupNumber }) => ({
|
||||
key,
|
||||
category,
|
||||
groupNumber,
|
||||
})
|
||||
),
|
||||
[
|
||||
{
|
||||
key: "lighting",
|
||||
category: "lighting",
|
||||
groupNumber: 1,
|
||||
},
|
||||
{
|
||||
key: "unassigned",
|
||||
category: null,
|
||||
groupNumber: null,
|
||||
},
|
||||
]
|
||||
);
|
||||
assert.deepEqual(snapshot.distributionBoardComponents, []);
|
||||
assert.deepEqual(snapshot.circuitProtectionDevices, []);
|
||||
assert.deepEqual(
|
||||
snapshot.distributionBoardComponentProtectionDevices,
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
it("validates the complete group, component and protection graph", () => {
|
||||
it("validates component, protection and equipment relations", () => {
|
||||
const snapshot = parseProjectStateSnapshot(protectedSnapshot());
|
||||
|
||||
assert.equal(snapshot.circuitSections[0]?.groupNumber, 1);
|
||||
assert.equal(
|
||||
snapshot.distributionBoardComponents[0]?.sectionId,
|
||||
"section-1"
|
||||
);
|
||||
assert.equal(
|
||||
snapshot.circuitProtectionDevices[0]?.tripCharacteristic,
|
||||
"B"
|
||||
);
|
||||
assert.equal(snapshot.circuitProtectionDevices[0]?.tripCharacteristic, "B");
|
||||
assert.equal(
|
||||
snapshot.distributionBoardComponentProtectionDevices[0]
|
||||
?.ratedResidualCurrentMa,
|
||||
@@ -401,113 +179,27 @@ describe("project state snapshot model", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects duplicate ids and cross-project ownership", () => {
|
||||
it("rejects duplicate ids and invalid ownership", () => {
|
||||
const duplicate = protectedSnapshot();
|
||||
duplicate.distributionBoards.push({
|
||||
...duplicate.distributionBoards[0]!,
|
||||
name: "UV 2",
|
||||
});
|
||||
assert.throws(
|
||||
() =>
|
||||
parseProjectStateSnapshot({
|
||||
...minimalSnapshot(),
|
||||
distributionBoards: [
|
||||
{
|
||||
id: "board-1",
|
||||
projectId: "project-1",
|
||||
name: "UV 1",
|
||||
floorId: null,
|
||||
supplyType: null,
|
||||
simultaneityFactor: 1,
|
||||
},
|
||||
{
|
||||
id: "board-1",
|
||||
projectId: "project-1",
|
||||
name: "UV 2",
|
||||
floorId: null,
|
||||
supplyType: null,
|
||||
simultaneityFactor: 1,
|
||||
},
|
||||
],
|
||||
}),
|
||||
() => parseProjectStateSnapshot(duplicate),
|
||||
/duplicate distribution board ids/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
parseProjectStateSnapshot({
|
||||
...minimalSnapshot(),
|
||||
floors: [
|
||||
{
|
||||
id: "floor-1",
|
||||
projectId: "project-2",
|
||||
name: "EG",
|
||||
sortOrder: 10,
|
||||
},
|
||||
],
|
||||
}),
|
||||
/floor belongs to a different project/
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects invalid references and inconsistent reserve circuits", () => {
|
||||
const wrongOwner = minimalSnapshot();
|
||||
wrongOwner.floors.push({
|
||||
id: "floor-1",
|
||||
projectId: "project-2",
|
||||
name: "EG",
|
||||
sortOrder: 10,
|
||||
});
|
||||
assert.throws(
|
||||
() =>
|
||||
parseProjectStateSnapshot({
|
||||
...minimalSnapshot(),
|
||||
distributionBoards: [
|
||||
{
|
||||
id: "board-1",
|
||||
projectId: "project-1",
|
||||
name: "UV 1",
|
||||
floorId: "missing",
|
||||
supplyType: "AV",
|
||||
simultaneityFactor: 1,
|
||||
},
|
||||
],
|
||||
}),
|
||||
/distribution board floor reference is invalid/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
parseProjectStateSnapshot({
|
||||
...minimalSnapshot(),
|
||||
rooms: [
|
||||
{
|
||||
id: "room-1",
|
||||
projectId: "project-1",
|
||||
floorId: "missing",
|
||||
roomNumber: "001",
|
||||
roomName: "Technik",
|
||||
},
|
||||
],
|
||||
}),
|
||||
/room floor reference is invalid/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
parseProjectStateSnapshot({
|
||||
...minimalSnapshot(),
|
||||
circuits: [
|
||||
{
|
||||
id: "circuit-1",
|
||||
circuitListId: "missing",
|
||||
sectionId: "missing",
|
||||
equipmentIdentifier: "-1F1",
|
||||
displayName: null,
|
||||
sortOrder: 10,
|
||||
protectionType: null,
|
||||
protectionRatedCurrent: null,
|
||||
protectionCharacteristic: null,
|
||||
cableType: null,
|
||||
cableCrossSection: null,
|
||||
cableLength: null,
|
||||
rcdAssignment: null,
|
||||
terminalDesignation: null,
|
||||
voltage: null,
|
||||
controlRequirement: null,
|
||||
status: null,
|
||||
isReserve: true,
|
||||
remark: null,
|
||||
deviceRows: [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
/circuit list reference is invalid/
|
||||
() => parseProjectStateSnapshot(wrongOwner),
|
||||
/floor belongs to a different project/
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -518,36 +210,29 @@ describe("named project snapshot request", () => {
|
||||
createNamedProjectSnapshotSchema.parse({
|
||||
expectedRevision: 12,
|
||||
name: " Freigabe ",
|
||||
description: " Stand vor Ausschreibung ",
|
||||
description: " Stand vor Änderung ",
|
||||
}),
|
||||
{
|
||||
expectedRevision: 12,
|
||||
name: "Freigabe",
|
||||
description: "Stand vor Ausschreibung",
|
||||
description: "Stand vor Änderung",
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects stale-shaped and excessive input", () => {
|
||||
for (const input of [
|
||||
{ expectedRevision: -1, name: "Stand" },
|
||||
{ expectedRevision: 0, name: " " },
|
||||
{ expectedRevision: 0, name: "x".repeat(101) },
|
||||
{
|
||||
expectedRevision: 0,
|
||||
name: "Stand",
|
||||
description: "x".repeat(501),
|
||||
},
|
||||
{
|
||||
expectedRevision: 0,
|
||||
name: "Stand",
|
||||
assert.throws(() =>
|
||||
createNamedProjectSnapshotSchema.parse({
|
||||
expectedRevision: -1,
|
||||
name: "",
|
||||
unexpected: true,
|
||||
},
|
||||
]) {
|
||||
assert.equal(
|
||||
createNamedProjectSnapshotSchema.safeParse(input).success,
|
||||
false
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
assert.throws(() =>
|
||||
createNamedProjectSnapshotSchema.parse({
|
||||
expectedRevision: 0,
|
||||
name: "x".repeat(201),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user