467 lines
13 KiB
TypeScript
467 lines
13 KiB
TypeScript
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();
|
|
}
|
|
});
|
|
});
|