Add component persistence foundation
This commit is contained in:
@@ -0,0 +1,466 @@
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user