Add logical distribution board feed links
This commit is contained in:
parent
b50c00c0ac
commit
b893c72534
23 changed files with 3732 additions and 45 deletions
4
src/db/migrations/0012_good_gateway.sql
Normal file
4
src/db/migrations/0012_good_gateway.sql
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
ALTER TABLE `distribution_boards` ADD `feeding_distribution_board_id` text REFERENCES distribution_boards(id);--> statement-breakpoint
|
||||||
|
ALTER TABLE `distribution_boards` ADD `feeding_circuit_id` text REFERENCES circuits(id);--> statement-breakpoint
|
||||||
|
CREATE INDEX `distribution_boards_feeding_board_idx` ON `distribution_boards` (`feeding_distribution_board_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `distribution_boards_feeding_circuit_idx` ON `distribution_boards` (`feeding_circuit_id`);
|
||||||
2986
src/db/migrations/meta/0012_snapshot.json
Normal file
2986
src/db/migrations/meta/0012_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -85,6 +85,13 @@
|
||||||
"when": 1790531351492,
|
"when": 1790531351492,
|
||||||
"tag": "0011_glossy_norman_osborn",
|
"tag": "0011_glossy_norman_osborn",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 12,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1790540142918,
|
||||||
|
"tag": "0012_good_gateway",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -325,6 +325,16 @@ export class DistributionBoardStructureProjectCommandRepository
|
||||||
patch.supplyType
|
patch.supplyType
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
this.assertFeedingReference(database, input.projectId, current.id, {
|
||||||
|
feedingDistributionBoardId:
|
||||||
|
patch.feedingDistributionBoardId === undefined
|
||||||
|
? current.feedingDistributionBoardId
|
||||||
|
: patch.feedingDistributionBoardId,
|
||||||
|
feedingCircuitId:
|
||||||
|
patch.feedingCircuitId === undefined
|
||||||
|
? current.feedingCircuitId
|
||||||
|
: patch.feedingCircuitId,
|
||||||
|
});
|
||||||
const inversePatch = Object.fromEntries(
|
const inversePatch = Object.fromEntries(
|
||||||
input.command.payload.changes.map((change) => [
|
input.command.payload.changes.map((change) => [
|
||||||
change.field,
|
change.field,
|
||||||
|
|
@ -333,7 +343,8 @@ export class DistributionBoardStructureProjectCommandRepository
|
||||||
) as DistributionBoardUpdatePatch;
|
) as DistributionBoardUpdatePatch;
|
||||||
const inverse = createDistributionBoardUpdateProjectCommand(
|
const inverse = createDistributionBoardUpdateProjectCommand(
|
||||||
current.id,
|
current.id,
|
||||||
inversePatch
|
inversePatch,
|
||||||
|
input.command.schemaVersion
|
||||||
);
|
);
|
||||||
const updated = database
|
const updated = database
|
||||||
.update(distributionBoards)
|
.update(distributionBoards)
|
||||||
|
|
@ -352,6 +363,77 @@ export class DistributionBoardStructureProjectCommandRepository
|
||||||
}
|
}
|
||||||
return inverse;
|
return inverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private assertFeedingReference(
|
||||||
|
database: AppDatabase,
|
||||||
|
projectId: string,
|
||||||
|
distributionBoardId: string,
|
||||||
|
reference: {
|
||||||
|
feedingDistributionBoardId: string | null;
|
||||||
|
feedingCircuitId: string | null;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { feedingDistributionBoardId, feedingCircuitId } = reference;
|
||||||
|
if (feedingDistributionBoardId === null) {
|
||||||
|
if (feedingCircuitId !== null) {
|
||||||
|
throw new Error(
|
||||||
|
"Ein speisender Stromkreis erfordert eine speisende Verteilung."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (feedingDistributionBoardId === distributionBoardId) {
|
||||||
|
throw new Error("Eine Verteilung kann sich nicht selbst speisen.");
|
||||||
|
}
|
||||||
|
const projectBoards = database
|
||||||
|
.select({
|
||||||
|
id: distributionBoards.id,
|
||||||
|
feedingDistributionBoardId:
|
||||||
|
distributionBoards.feedingDistributionBoardId,
|
||||||
|
})
|
||||||
|
.from(distributionBoards)
|
||||||
|
.where(eq(distributionBoards.projectId, projectId))
|
||||||
|
.all();
|
||||||
|
const boardById = new Map(projectBoards.map((board) => [board.id, board]));
|
||||||
|
if (!boardById.has(feedingDistributionBoardId)) {
|
||||||
|
throw new Error(
|
||||||
|
"Die speisende Verteilung gehört nicht zum Projekt."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const visited = new Set<string>([distributionBoardId]);
|
||||||
|
let currentId: string | null = feedingDistributionBoardId;
|
||||||
|
while (currentId !== null) {
|
||||||
|
if (visited.has(currentId)) {
|
||||||
|
throw new Error("Die Einspeisungsverknüpfung würde einen Zyklus erzeugen.");
|
||||||
|
}
|
||||||
|
visited.add(currentId);
|
||||||
|
currentId = boardById.get(currentId)?.feedingDistributionBoardId ?? null;
|
||||||
|
}
|
||||||
|
if (feedingCircuitId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const feedingCircuit = database
|
||||||
|
.select({
|
||||||
|
projectId: circuitLists.projectId,
|
||||||
|
distributionBoardId: circuitLists.distributionBoardId,
|
||||||
|
})
|
||||||
|
.from(circuits)
|
||||||
|
.innerJoin(
|
||||||
|
circuitLists,
|
||||||
|
eq(circuitLists.id, circuits.circuitListId)
|
||||||
|
)
|
||||||
|
.where(eq(circuits.id, feedingCircuitId))
|
||||||
|
.get();
|
||||||
|
if (
|
||||||
|
!feedingCircuit ||
|
||||||
|
feedingCircuit.projectId !== projectId ||
|
||||||
|
feedingCircuit.distributionBoardId !== feedingDistributionBoardId
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Der speisende Stromkreis gehört nicht zur gewählten Verteilung."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertSupplyTypeEnabled(
|
function assertSupplyTypeEnabled(
|
||||||
|
|
@ -392,11 +474,20 @@ function structureMatches(
|
||||||
location,
|
location,
|
||||||
fedFrom,
|
fedFrom,
|
||||||
remark,
|
remark,
|
||||||
|
feedingDistributionBoardId,
|
||||||
|
feedingCircuitId,
|
||||||
...actualDistributionBoard
|
...actualDistributionBoard
|
||||||
} = actual.board;
|
} = actual.board;
|
||||||
if (
|
if (
|
||||||
simultaneityFactor !== 1 ||
|
simultaneityFactor !== 1 ||
|
||||||
[boardNumber, location, fedFrom, remark].some((value) => value !== null) ||
|
[
|
||||||
|
boardNumber,
|
||||||
|
location,
|
||||||
|
fedFrom,
|
||||||
|
remark,
|
||||||
|
feedingDistributionBoardId,
|
||||||
|
feedingCircuitId,
|
||||||
|
].some((value) => value !== null) ||
|
||||||
!sameRecord(
|
!sameRecord(
|
||||||
expected.distributionBoard,
|
expected.distributionBoard,
|
||||||
actualDistributionBoard
|
actualDistributionBoard
|
||||||
|
|
|
||||||
|
|
@ -300,6 +300,69 @@ export class DistributionBoardSubtreeProjectCommandRepository
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const feedingDistributionBoardId =
|
||||||
|
snapshot.distributionBoard.feedingDistributionBoardId;
|
||||||
|
const feedingCircuitId = snapshot.distributionBoard.feedingCircuitId;
|
||||||
|
if (feedingDistributionBoardId === null) {
|
||||||
|
if (feedingCircuitId !== null) {
|
||||||
|
throw new Error(
|
||||||
|
"Ein speisender Stromkreis erfordert eine speisende Verteilung."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (feedingDistributionBoardId === snapshot.distributionBoard.id) {
|
||||||
|
throw new Error("Eine Verteilung kann sich nicht selbst speisen.");
|
||||||
|
}
|
||||||
|
const projectBoards = database
|
||||||
|
.select({
|
||||||
|
id: distributionBoards.id,
|
||||||
|
feedingDistributionBoardId:
|
||||||
|
distributionBoards.feedingDistributionBoardId,
|
||||||
|
})
|
||||||
|
.from(distributionBoards)
|
||||||
|
.where(eq(distributionBoards.projectId, projectId))
|
||||||
|
.all();
|
||||||
|
const boardById = new Map(projectBoards.map((board) => [board.id, board]));
|
||||||
|
if (!boardById.has(feedingDistributionBoardId)) {
|
||||||
|
throw new Error(
|
||||||
|
"Die speisende Verteilung gehört nicht zum Projekt."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const visited = new Set<string>([snapshot.distributionBoard.id]);
|
||||||
|
let currentId: string | null = feedingDistributionBoardId;
|
||||||
|
while (currentId !== null) {
|
||||||
|
if (visited.has(currentId)) {
|
||||||
|
throw new Error(
|
||||||
|
"Die Einspeisungsverknüpfung würde einen Zyklus erzeugen."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
visited.add(currentId);
|
||||||
|
currentId = boardById.get(currentId)?.feedingDistributionBoardId ?? null;
|
||||||
|
}
|
||||||
|
if (feedingCircuitId !== null) {
|
||||||
|
const feedingCircuit = database
|
||||||
|
.select({
|
||||||
|
projectId: circuitLists.projectId,
|
||||||
|
distributionBoardId: circuitLists.distributionBoardId,
|
||||||
|
})
|
||||||
|
.from(circuits)
|
||||||
|
.innerJoin(
|
||||||
|
circuitLists,
|
||||||
|
eq(circuitLists.id, circuits.circuitListId)
|
||||||
|
)
|
||||||
|
.where(eq(circuits.id, feedingCircuitId))
|
||||||
|
.get();
|
||||||
|
if (
|
||||||
|
!feedingCircuit ||
|
||||||
|
feedingCircuit.projectId !== projectId ||
|
||||||
|
feedingCircuit.distributionBoardId !== feedingDistributionBoardId
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Der speisende Stromkreis gehört nicht zur gewählten Verteilung."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const linkedProjectDeviceIds = uniqueStrings(
|
const linkedProjectDeviceIds = uniqueStrings(
|
||||||
snapshot.circuits.flatMap((circuit) =>
|
snapshot.circuits.flatMap((circuit) =>
|
||||||
circuit.deviceRows.flatMap((row) =>
|
circuit.deviceRows.flatMap((row) =>
|
||||||
|
|
|
||||||
|
|
@ -270,6 +270,14 @@ function replaceProjectState(
|
||||||
database: AppDatabase,
|
database: AppDatabase,
|
||||||
state: ProjectStateSnapshot
|
state: ProjectStateSnapshot
|
||||||
) {
|
) {
|
||||||
|
database
|
||||||
|
.update(distributionBoards)
|
||||||
|
.set({
|
||||||
|
feedingDistributionBoardId: null,
|
||||||
|
feedingCircuitId: null,
|
||||||
|
})
|
||||||
|
.where(eq(distributionBoards.projectId, state.project.id))
|
||||||
|
.run();
|
||||||
database
|
database
|
||||||
.delete(externalModelSources)
|
.delete(externalModelSources)
|
||||||
.where(eq(externalModelSources.projectId, state.project.id))
|
.where(eq(externalModelSources.projectId, state.project.id))
|
||||||
|
|
@ -330,7 +338,15 @@ function replaceProjectState(
|
||||||
}
|
}
|
||||||
insertMany(database, rooms, state.rooms);
|
insertMany(database, rooms, state.rooms);
|
||||||
insertMany(database, projectDevices, state.projectDevices);
|
insertMany(database, projectDevices, state.projectDevices);
|
||||||
insertMany(database, distributionBoards, state.distributionBoards);
|
insertMany(
|
||||||
|
database,
|
||||||
|
distributionBoards,
|
||||||
|
state.distributionBoards.map((board) => ({
|
||||||
|
...board,
|
||||||
|
feedingDistributionBoardId: null,
|
||||||
|
feedingCircuitId: null,
|
||||||
|
}))
|
||||||
|
);
|
||||||
insertMany(database, circuitLists, state.circuitLists);
|
insertMany(database, circuitLists, state.circuitLists);
|
||||||
insertMany(database, circuitSections, state.circuitSections);
|
insertMany(database, circuitSections, state.circuitSections);
|
||||||
insertMany(
|
insertMany(
|
||||||
|
|
@ -341,6 +357,7 @@ function replaceProjectState(
|
||||||
isReserve: circuit.isReserve ? 1 : 0,
|
isReserve: circuit.isReserve ? 1 : 0,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
restoreDistributionBoardFeedingReferences(database, state);
|
||||||
insertMany(
|
insertMany(
|
||||||
database,
|
database,
|
||||||
distributionBoardComponents,
|
distributionBoardComponents,
|
||||||
|
|
@ -378,6 +395,31 @@ function replaceProjectState(
|
||||||
insertMany(database, externalModelObjects, state.externalModel.objects);
|
insertMany(database, externalModelObjects, state.externalModel.objects);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function restoreDistributionBoardFeedingReferences(
|
||||||
|
database: AppDatabase,
|
||||||
|
state: ProjectStateSnapshot
|
||||||
|
) {
|
||||||
|
for (const board of state.distributionBoards) {
|
||||||
|
if (
|
||||||
|
board.feedingDistributionBoardId === null &&
|
||||||
|
board.feedingCircuitId === null
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const updated = database
|
||||||
|
.update(distributionBoards)
|
||||||
|
.set({
|
||||||
|
feedingDistributionBoardId: board.feedingDistributionBoardId,
|
||||||
|
feedingCircuitId: board.feedingCircuitId,
|
||||||
|
})
|
||||||
|
.where(eq(distributionBoards.id, board.id))
|
||||||
|
.run();
|
||||||
|
if (updated.changes !== 1) {
|
||||||
|
throw new Error("Distribution-board feeding reference could not be restored.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function insertMany<TTable extends Parameters<AppDatabase["insert"]>[0]>(
|
function insertMany<TTable extends Parameters<AppDatabase["insert"]>[0]>(
|
||||||
database: AppDatabase,
|
database: AppDatabase,
|
||||||
table: TTable,
|
table: TTable,
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,15 @@ function insertProjectState(
|
||||||
insertMany(database, floors, state.floors);
|
insertMany(database, floors, state.floors);
|
||||||
insertMany(database, rooms, state.rooms);
|
insertMany(database, rooms, state.rooms);
|
||||||
insertMany(database, projectDevices, state.projectDevices);
|
insertMany(database, projectDevices, state.projectDevices);
|
||||||
insertMany(database, distributionBoards, state.distributionBoards);
|
insertMany(
|
||||||
|
database,
|
||||||
|
distributionBoards,
|
||||||
|
state.distributionBoards.map((board) => ({
|
||||||
|
...board,
|
||||||
|
feedingDistributionBoardId: null,
|
||||||
|
feedingCircuitId: null,
|
||||||
|
}))
|
||||||
|
);
|
||||||
insertMany(database, circuitLists, state.circuitLists);
|
insertMany(database, circuitLists, state.circuitLists);
|
||||||
insertMany(database, circuitSections, state.circuitSections);
|
insertMany(database, circuitSections, state.circuitSections);
|
||||||
insertMany(
|
insertMany(
|
||||||
|
|
@ -184,6 +192,25 @@ function insertProjectState(
|
||||||
isReserve: circuit.isReserve ? 1 : 0,
|
isReserve: circuit.isReserve ? 1 : 0,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
for (const board of state.distributionBoards) {
|
||||||
|
if (
|
||||||
|
board.feedingDistributionBoardId === null &&
|
||||||
|
board.feedingCircuitId === null
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const updated = database
|
||||||
|
.update(distributionBoards)
|
||||||
|
.set({
|
||||||
|
feedingDistributionBoardId: board.feedingDistributionBoardId,
|
||||||
|
feedingCircuitId: board.feedingCircuitId,
|
||||||
|
})
|
||||||
|
.where(eq(distributionBoards.id, board.id))
|
||||||
|
.run();
|
||||||
|
if (updated.changes !== 1) {
|
||||||
|
throw new Error("Distribution-board feeding reference could not be imported.");
|
||||||
|
}
|
||||||
|
}
|
||||||
insertMany(
|
insertMany(
|
||||||
database,
|
database,
|
||||||
distributionBoardComponents,
|
distributionBoardComponents,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { sqliteTable, text, unique } from "drizzle-orm/sqlite-core";
|
import { sqliteTable, text, unique, type AnySQLiteColumn } from "drizzle-orm/sqlite-core";
|
||||||
import { distributionBoards } from "./distribution-boards.js";
|
import { distributionBoards } from "./distribution-boards.js";
|
||||||
import { projects } from "./projects.js";
|
import { projects } from "./projects.js";
|
||||||
|
|
||||||
|
|
@ -11,7 +11,7 @@ export const circuitLists = sqliteTable(
|
||||||
.references(() => projects.id, { onDelete: "cascade" }),
|
.references(() => projects.id, { onDelete: "cascade" }),
|
||||||
distributionBoardId: text("distribution_board_id")
|
distributionBoardId: text("distribution_board_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => distributionBoards.id, { onDelete: "cascade" }),
|
.references((): AnySQLiteColumn => distributionBoards.id, { onDelete: "cascade" }),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
},
|
},
|
||||||
(table) => [unique("circuit_lists_distribution_board_id_unique").on(table.distributionBoardId)]
|
(table) => [unique("circuit_lists_distribution_board_id_unique").on(table.distributionBoardId)]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { index, integer, real, sqliteTable, text, unique } from "drizzle-orm/sqlite-core";
|
import { index, integer, real, sqliteTable, text, unique, type AnySQLiteColumn } from "drizzle-orm/sqlite-core";
|
||||||
import { circuitLists } from "./circuit-lists.js";
|
import { circuitLists } from "./circuit-lists.js";
|
||||||
import { circuitSections } from "./circuit-sections.js";
|
import { circuitSections } from "./circuit-sections.js";
|
||||||
|
|
||||||
|
|
@ -8,7 +8,7 @@ export const circuits = sqliteTable(
|
||||||
id: text("id").primaryKey(),
|
id: text("id").primaryKey(),
|
||||||
circuitListId: text("circuit_list_id")
|
circuitListId: text("circuit_list_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => circuitLists.id, { onDelete: "cascade" }),
|
.references((): AnySQLiteColumn => circuitLists.id, { onDelete: "cascade" }),
|
||||||
sectionId: text("section_id")
|
sectionId: text("section_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => circuitSections.id, { onDelete: "cascade" }),
|
.references(() => circuitSections.id, { onDelete: "cascade" }),
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
import { real, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
import { index, real, sqliteTable, text, type AnySQLiteColumn } from "drizzle-orm/sqlite-core";
|
||||||
|
import { circuits } from "./circuits.js";
|
||||||
import { floors } from "./floors.js";
|
import { floors } from "./floors.js";
|
||||||
import { projects } from "./projects.js";
|
import { projects } from "./projects.js";
|
||||||
import type { DistributionBoardSupplyType } from "../../shared/constants/distribution-board.js";
|
import type { DistributionBoardSupplyType } from "../../shared/constants/distribution-board.js";
|
||||||
|
|
||||||
export const distributionBoards = sqliteTable("distribution_boards", {
|
export const distributionBoards = sqliteTable(
|
||||||
|
"distribution_boards",
|
||||||
|
{
|
||||||
id: text("id").primaryKey(),
|
id: text("id").primaryKey(),
|
||||||
projectId: text("project_id")
|
projectId: text("project_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
|
|
@ -20,5 +23,22 @@ export const distributionBoards = sqliteTable("distribution_boards", {
|
||||||
location: text("location"),
|
location: text("location"),
|
||||||
fedFrom: text("fed_from"),
|
fedFrom: text("fed_from"),
|
||||||
remark: text("remark"),
|
remark: text("remark"),
|
||||||
});
|
feedingDistributionBoardId: text("feeding_distribution_board_id").references(
|
||||||
|
(): AnySQLiteColumn => distributionBoards.id,
|
||||||
|
{ onDelete: "restrict" }
|
||||||
|
),
|
||||||
|
feedingCircuitId: text("feeding_circuit_id").references(
|
||||||
|
(): AnySQLiteColumn => circuits.id,
|
||||||
|
{ onDelete: "restrict" }
|
||||||
|
),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
index("distribution_boards_feeding_board_idx").on(
|
||||||
|
table.feedingDistributionBoardId
|
||||||
|
),
|
||||||
|
index("distribution_boards_feeding_circuit_idx").on(
|
||||||
|
table.feedingCircuitId
|
||||||
|
),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,16 +9,18 @@ import type { SerializedProjectCommand } from "./project-command.model.js";
|
||||||
|
|
||||||
export const distributionBoardUpdateCommandType =
|
export const distributionBoardUpdateCommandType =
|
||||||
"distribution-board.update" as const;
|
"distribution-board.update" as const;
|
||||||
// The info-card fields were added additively to schema version 1: every
|
// Version 2 adds logical feeding references. Version-1 commands remain
|
||||||
// previously stored command remains valid, and the inverse is still derived
|
// executable and can only change the fields they originally knew.
|
||||||
// from the persisted row for exactly the changed fields.
|
export const distributionBoardUpdateCommandSchemaVersion = 2 as const;
|
||||||
export const distributionBoardUpdateCommandSchemaVersion = 1 as const;
|
const legacyDistributionBoardUpdateCommandSchemaVersion = 1 as const;
|
||||||
|
|
||||||
export interface DistributionBoardUpdateValues
|
export interface DistributionBoardUpdateValues
|
||||||
extends Record<DistributionBoardInfoField, string | null> {
|
extends Record<DistributionBoardInfoField, string | null> {
|
||||||
floorId: string | null;
|
floorId: string | null;
|
||||||
supplyType: DistributionBoardSupplyType | null;
|
supplyType: DistributionBoardSupplyType | null;
|
||||||
simultaneityFactor: number;
|
simultaneityFactor: number;
|
||||||
|
feedingDistributionBoardId: string | null;
|
||||||
|
feedingCircuitId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DistributionBoardUpdateField =
|
export type DistributionBoardUpdateField =
|
||||||
|
|
@ -41,16 +43,20 @@ export interface DistributionBoardUpdateCommandPayload {
|
||||||
|
|
||||||
export interface DistributionBoardUpdateProjectCommand
|
export interface DistributionBoardUpdateProjectCommand
|
||||||
extends SerializedProjectCommand<DistributionBoardUpdateCommandPayload> {
|
extends SerializedProjectCommand<DistributionBoardUpdateCommandPayload> {
|
||||||
schemaVersion: typeof distributionBoardUpdateCommandSchemaVersion;
|
schemaVersion:
|
||||||
|
| typeof legacyDistributionBoardUpdateCommandSchemaVersion
|
||||||
|
| typeof distributionBoardUpdateCommandSchemaVersion;
|
||||||
type: typeof distributionBoardUpdateCommandType;
|
type: typeof distributionBoardUpdateCommandType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createDistributionBoardUpdateProjectCommand(
|
export function createDistributionBoardUpdateProjectCommand(
|
||||||
distributionBoardId: string,
|
distributionBoardId: string,
|
||||||
patch: DistributionBoardUpdatePatch
|
patch: DistributionBoardUpdatePatch,
|
||||||
|
schemaVersion: DistributionBoardUpdateProjectCommand["schemaVersion"] =
|
||||||
|
distributionBoardUpdateCommandSchemaVersion
|
||||||
): DistributionBoardUpdateProjectCommand {
|
): DistributionBoardUpdateProjectCommand {
|
||||||
const command: DistributionBoardUpdateProjectCommand = {
|
const command: DistributionBoardUpdateProjectCommand = {
|
||||||
schemaVersion: distributionBoardUpdateCommandSchemaVersion,
|
schemaVersion,
|
||||||
type: distributionBoardUpdateCommandType,
|
type: distributionBoardUpdateCommandType,
|
||||||
payload: {
|
payload: {
|
||||||
distributionBoardId,
|
distributionBoardId,
|
||||||
|
|
@ -68,7 +74,8 @@ export function assertDistributionBoardUpdateProjectCommand(
|
||||||
command: SerializedProjectCommand<unknown>
|
command: SerializedProjectCommand<unknown>
|
||||||
): asserts command is DistributionBoardUpdateProjectCommand {
|
): asserts command is DistributionBoardUpdateProjectCommand {
|
||||||
if (
|
if (
|
||||||
command.schemaVersion !== distributionBoardUpdateCommandSchemaVersion ||
|
(command.schemaVersion !== legacyDistributionBoardUpdateCommandSchemaVersion &&
|
||||||
|
command.schemaVersion !== distributionBoardUpdateCommandSchemaVersion) ||
|
||||||
command.type !== distributionBoardUpdateCommandType ||
|
command.type !== distributionBoardUpdateCommandType ||
|
||||||
!isPlainObject(command.payload)
|
!isPlainObject(command.payload)
|
||||||
) {
|
) {
|
||||||
|
|
@ -93,7 +100,12 @@ export function assertDistributionBoardUpdateProjectCommand(
|
||||||
(change.field !== "floorId" &&
|
(change.field !== "floorId" &&
|
||||||
change.field !== "supplyType" &&
|
change.field !== "supplyType" &&
|
||||||
change.field !== "simultaneityFactor" &&
|
change.field !== "simultaneityFactor" &&
|
||||||
|
change.field !== "feedingDistributionBoardId" &&
|
||||||
|
change.field !== "feedingCircuitId" &&
|
||||||
!isInfoField(change.field)) ||
|
!isInfoField(change.field)) ||
|
||||||
|
(command.schemaVersion === legacyDistributionBoardUpdateCommandSchemaVersion &&
|
||||||
|
(change.field === "feedingDistributionBoardId" ||
|
||||||
|
change.field === "feedingCircuitId")) ||
|
||||||
seen.has(change.field)
|
seen.has(change.field)
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
@ -105,7 +117,9 @@ export function assertDistributionBoardUpdateProjectCommand(
|
||||||
} else if (change.field === "floorId") {
|
} else if (change.field === "floorId") {
|
||||||
if (
|
if (
|
||||||
change.value !== null &&
|
change.value !== null &&
|
||||||
(typeof change.value !== "string" || !change.value.trim())
|
(typeof change.value !== "string" ||
|
||||||
|
!change.value.trim() ||
|
||||||
|
change.value !== change.value.trim())
|
||||||
) {
|
) {
|
||||||
throw new Error("floorId must be a non-empty string or null.");
|
throw new Error("floorId must be a non-empty string or null.");
|
||||||
}
|
}
|
||||||
|
|
@ -118,6 +132,16 @@ export function assertDistributionBoardUpdateProjectCommand(
|
||||||
) {
|
) {
|
||||||
throw new Error("supplyType is invalid.");
|
throw new Error("supplyType is invalid.");
|
||||||
}
|
}
|
||||||
|
} else if (
|
||||||
|
change.field === "feedingDistributionBoardId" ||
|
||||||
|
change.field === "feedingCircuitId"
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
change.value !== null &&
|
||||||
|
(typeof change.value !== "string" || !change.value.trim())
|
||||||
|
) {
|
||||||
|
throw new Error(`${change.field} must be a non-empty string or null.`);
|
||||||
|
}
|
||||||
} else if (
|
} else if (
|
||||||
typeof change.value !== "number" ||
|
typeof change.value !== "number" ||
|
||||||
!Number.isFinite(change.value) ||
|
!Number.isFinite(change.value) ||
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,8 @@ export interface DistributionBoardSubtreeSnapshot {
|
||||||
floorId: string | null;
|
floorId: string | null;
|
||||||
supplyType: DistributionBoardSupplyType | null;
|
supplyType: DistributionBoardSupplyType | null;
|
||||||
simultaneityFactor: number;
|
simultaneityFactor: number;
|
||||||
|
feedingDistributionBoardId: string | null;
|
||||||
|
feedingCircuitId: string | null;
|
||||||
} & Record<DistributionBoardInfoField, string | null>;
|
} & Record<DistributionBoardInfoField, string | null>;
|
||||||
circuitList: {
|
circuitList: {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -184,6 +186,9 @@ export function normalizeDistributionBoardSubtreeSnapshot(
|
||||||
for (const field of distributionBoardInfoFields) {
|
for (const field of distributionBoardInfoFields) {
|
||||||
board[field] = board[field] ?? null;
|
board[field] = board[field] ?? null;
|
||||||
}
|
}
|
||||||
|
board.feedingDistributionBoardId =
|
||||||
|
board.feedingDistributionBoardId ?? null;
|
||||||
|
board.feedingCircuitId = board.feedingCircuitId ?? null;
|
||||||
return { ...snapshot, distributionBoard: board };
|
return { ...snapshot, distributionBoard: board };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -227,6 +232,18 @@ export function assertDistributionBoardSubtreeSnapshot(
|
||||||
assertNullableString(board[field], `distributionBoard.${field}`);
|
assertNullableString(board[field], `distributionBoard.${field}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (board.feedingDistributionBoardId !== undefined) {
|
||||||
|
assertNullableString(
|
||||||
|
board.feedingDistributionBoardId,
|
||||||
|
"distributionBoard.feedingDistributionBoardId"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (board.feedingCircuitId !== undefined) {
|
||||||
|
assertNullableString(
|
||||||
|
board.feedingCircuitId,
|
||||||
|
"distributionBoard.feedingCircuitId"
|
||||||
|
);
|
||||||
|
}
|
||||||
for (const field of [
|
for (const field of [
|
||||||
"id",
|
"id",
|
||||||
"projectId",
|
"projectId",
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,8 @@ import {
|
||||||
type ExternalModelStateSnapshot,
|
type ExternalModelStateSnapshot,
|
||||||
} from "../../external-model/domain/external-model-contracts.js";
|
} from "../../external-model/domain/external-model-contracts.js";
|
||||||
|
|
||||||
export const projectStateSnapshotSchemaVersion = 8 as const;
|
export const projectStateSnapshotSchemaVersion = 9 as const;
|
||||||
|
const roomAreaProjectStateSnapshotSchemaVersion = 8 as const;
|
||||||
const buildingProjectStateSnapshotSchemaVersion = 7 as const;
|
const buildingProjectStateSnapshotSchemaVersion = 7 as const;
|
||||||
const coverProjectStateSnapshotSchemaVersion = 6 as const;
|
const coverProjectStateSnapshotSchemaVersion = 6 as const;
|
||||||
const boardInfoProjectStateSnapshotSchemaVersion = 5 as const;
|
const boardInfoProjectStateSnapshotSchemaVersion = 5 as const;
|
||||||
|
|
@ -112,7 +113,7 @@ const distributionBoardInfoValueSchema = (field: DistributionBoardInfoField) =>
|
||||||
.max(distributionBoardInfoFieldMaxLengths[field])
|
.max(distributionBoardInfoFieldMaxLengths[field])
|
||||||
.nullable();
|
.nullable();
|
||||||
|
|
||||||
const distributionBoardSchema = legacyDistributionBoardSchema
|
const distributionBoardWithoutFeedingSchema = legacyDistributionBoardSchema
|
||||||
.extend({
|
.extend({
|
||||||
boardNumber: distributionBoardInfoValueSchema("boardNumber"),
|
boardNumber: distributionBoardInfoValueSchema("boardNumber"),
|
||||||
location: distributionBoardInfoValueSchema("location"),
|
location: distributionBoardInfoValueSchema("location"),
|
||||||
|
|
@ -121,6 +122,13 @@ const distributionBoardSchema = legacyDistributionBoardSchema
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
const distributionBoardSchema = distributionBoardWithoutFeedingSchema
|
||||||
|
.extend({
|
||||||
|
feedingDistributionBoardId: idSchema.nullable(),
|
||||||
|
feedingCircuitId: idSchema.nullable(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
function withEmptyDistributionBoardInfo(
|
function withEmptyDistributionBoardInfo(
|
||||||
boards: Array<z.infer<typeof legacyDistributionBoardSchema>>
|
boards: Array<z.infer<typeof legacyDistributionBoardSchema>>
|
||||||
) {
|
) {
|
||||||
|
|
@ -133,6 +141,16 @@ function withEmptyDistributionBoardInfo(
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function withEmptyDistributionBoardFeeding(
|
||||||
|
boards: Array<z.infer<typeof distributionBoardWithoutFeedingSchema>>
|
||||||
|
) {
|
||||||
|
return boards.map((board) => ({
|
||||||
|
...board,
|
||||||
|
feedingDistributionBoardId: null,
|
||||||
|
feedingCircuitId: null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
const circuitListSchema = z
|
const circuitListSchema = z
|
||||||
.object({
|
.object({
|
||||||
id: idSchema,
|
id: idSchema,
|
||||||
|
|
@ -553,11 +571,22 @@ export const projectStateSnapshotSchema = z
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
const roomAreaProjectStateSnapshotSchema = z
|
||||||
|
.object({
|
||||||
|
schemaVersion: z.literal(roomAreaProjectStateSnapshotSchemaVersion),
|
||||||
|
project: projectSchema,
|
||||||
|
distributionBoards: z.array(distributionBoardWithoutFeedingSchema),
|
||||||
|
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
|
||||||
|
externalModel: externalModelStateSchema,
|
||||||
|
...projectStateSnapshotContents,
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
const buildingProjectStateSnapshotSchema = z
|
const buildingProjectStateSnapshotSchema = z
|
||||||
.object({
|
.object({
|
||||||
schemaVersion: z.literal(buildingProjectStateSnapshotSchemaVersion),
|
schemaVersion: z.literal(buildingProjectStateSnapshotSchemaVersion),
|
||||||
project: projectSchema,
|
project: projectSchema,
|
||||||
distributionBoards: z.array(distributionBoardSchema),
|
distributionBoards: z.array(distributionBoardWithoutFeedingSchema),
|
||||||
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
|
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
|
||||||
externalModel: externalModelStateSchema,
|
externalModel: externalModelStateSchema,
|
||||||
...buildingProjectStateSnapshotContents,
|
...buildingProjectStateSnapshotContents,
|
||||||
|
|
@ -568,7 +597,7 @@ const coverProjectStateSnapshotSchema = z
|
||||||
.object({
|
.object({
|
||||||
schemaVersion: z.literal(coverProjectStateSnapshotSchemaVersion),
|
schemaVersion: z.literal(coverProjectStateSnapshotSchemaVersion),
|
||||||
project: projectSchema,
|
project: projectSchema,
|
||||||
distributionBoards: z.array(distributionBoardSchema),
|
distributionBoards: z.array(distributionBoardWithoutFeedingSchema),
|
||||||
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
|
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
|
||||||
externalModel: externalModelStateSchema,
|
externalModel: externalModelStateSchema,
|
||||||
...previousProjectStateSnapshotContents,
|
...previousProjectStateSnapshotContents,
|
||||||
|
|
@ -655,6 +684,27 @@ export function parseProjectStateSnapshot(
|
||||||
}
|
}
|
||||||
|
|
||||||
function upgradePreviousProjectStateSnapshot(value: unknown): unknown {
|
function upgradePreviousProjectStateSnapshot(value: unknown): unknown {
|
||||||
|
const roomArea = upgradeToRoomAreaProjectStateSnapshot(value);
|
||||||
|
if (
|
||||||
|
roomArea !== null &&
|
||||||
|
typeof roomArea === "object" &&
|
||||||
|
!Array.isArray(roomArea) &&
|
||||||
|
(roomArea as { schemaVersion?: unknown }).schemaVersion ===
|
||||||
|
roomAreaProjectStateSnapshotSchemaVersion
|
||||||
|
) {
|
||||||
|
const previous = roomAreaProjectStateSnapshotSchema.parse(roomArea);
|
||||||
|
return {
|
||||||
|
...previous,
|
||||||
|
schemaVersion: projectStateSnapshotSchemaVersion,
|
||||||
|
distributionBoards: withEmptyDistributionBoardFeeding(
|
||||||
|
previous.distributionBoards
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return roomArea;
|
||||||
|
}
|
||||||
|
|
||||||
|
function upgradeToRoomAreaProjectStateSnapshot(value: unknown): unknown {
|
||||||
const cover = upgradeToCoverProjectStateSnapshot(value);
|
const cover = upgradeToCoverProjectStateSnapshot(value);
|
||||||
if (
|
if (
|
||||||
cover !== null &&
|
cover !== null &&
|
||||||
|
|
@ -666,7 +716,7 @@ function upgradePreviousProjectStateSnapshot(value: unknown): unknown {
|
||||||
const previous = buildingProjectStateSnapshotSchema.parse(cover);
|
const previous = buildingProjectStateSnapshotSchema.parse(cover);
|
||||||
return {
|
return {
|
||||||
...previous,
|
...previous,
|
||||||
schemaVersion: projectStateSnapshotSchemaVersion,
|
schemaVersion: roomAreaProjectStateSnapshotSchemaVersion,
|
||||||
rooms: previous.rooms.map((room) => ({ ...room, areaM2: null })),
|
rooms: previous.rooms.map((room) => ({ ...room, areaM2: null })),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -683,7 +733,7 @@ function upgradePreviousProjectStateSnapshot(value: unknown): unknown {
|
||||||
// building. Build version 7 first so the room-area upgrade above remains a
|
// building. Build version 7 first so the room-area upgrade above remains a
|
||||||
// single exact transition.
|
// single exact transition.
|
||||||
const previous = coverProjectStateSnapshotSchema.parse(cover);
|
const previous = coverProjectStateSnapshotSchema.parse(cover);
|
||||||
return upgradePreviousProjectStateSnapshot({
|
return upgradeToRoomAreaProjectStateSnapshot({
|
||||||
...previous,
|
...previous,
|
||||||
schemaVersion: buildingProjectStateSnapshotSchemaVersion,
|
schemaVersion: buildingProjectStateSnapshotSchemaVersion,
|
||||||
buildings: [],
|
buildings: [],
|
||||||
|
|
@ -873,6 +923,15 @@ function assertProjectStateSnapshotRelations(
|
||||||
const sectionById = new Map(
|
const sectionById = new Map(
|
||||||
snapshot.circuitSections.map((entry) => [entry.id, entry])
|
snapshot.circuitSections.map((entry) => [entry.id, entry])
|
||||||
);
|
);
|
||||||
|
const boardById = new Map(
|
||||||
|
snapshot.distributionBoards.map((entry) => [entry.id, entry])
|
||||||
|
);
|
||||||
|
const circuitListById = new Map(
|
||||||
|
snapshot.circuitLists.map((entry) => [entry.id, entry])
|
||||||
|
);
|
||||||
|
const circuitById = new Map(
|
||||||
|
snapshot.circuits.map((entry) => [entry.id, entry])
|
||||||
|
);
|
||||||
const componentById = new Map(
|
const componentById = new Map(
|
||||||
snapshot.distributionBoardComponents.map((entry) => [
|
snapshot.distributionBoardComponents.map((entry) => [
|
||||||
entry.id,
|
entry.id,
|
||||||
|
|
@ -901,6 +960,47 @@ function assertProjectStateSnapshotRelations(
|
||||||
"Snapshot distribution board uses a disabled supply type."
|
"Snapshot distribution board uses a disabled supply type."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (board.feedingDistributionBoardId === board.id) {
|
||||||
|
throw new Error("Snapshot distribution board cannot feed itself.");
|
||||||
|
}
|
||||||
|
if (board.feedingDistributionBoardId !== null) {
|
||||||
|
assertReference(
|
||||||
|
boardIds,
|
||||||
|
board.feedingDistributionBoardId,
|
||||||
|
"feeding distribution board"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (board.feedingCircuitId !== null) {
|
||||||
|
if (board.feedingDistributionBoardId === null) {
|
||||||
|
throw new Error(
|
||||||
|
"Snapshot feeding circuit requires a feeding distribution board."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assertReference(circuitIds, board.feedingCircuitId, "feeding circuit");
|
||||||
|
const feedingCircuit = circuitById.get(board.feedingCircuitId);
|
||||||
|
const feedingList = feedingCircuit
|
||||||
|
? circuitListById.get(feedingCircuit.circuitListId)
|
||||||
|
: undefined;
|
||||||
|
if (
|
||||||
|
!feedingList ||
|
||||||
|
feedingList.distributionBoardId !== board.feedingDistributionBoardId
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Snapshot feeding circuit belongs to another distribution board."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const board of snapshot.distributionBoards) {
|
||||||
|
const visited = new Set<string>([board.id]);
|
||||||
|
let currentId = board.feedingDistributionBoardId;
|
||||||
|
while (currentId !== null) {
|
||||||
|
if (visited.has(currentId)) {
|
||||||
|
throw new Error("Snapshot distribution-board feeding graph contains a cycle.");
|
||||||
|
}
|
||||||
|
visited.add(currentId);
|
||||||
|
currentId = boardById.get(currentId)?.feedingDistributionBoardId ?? null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const list of snapshot.circuitLists) {
|
for (const list of snapshot.circuitLists) {
|
||||||
assertProjectOwnership(list.projectId, projectId, "circuit list");
|
assertProjectOwnership(list.projectId, projectId, "circuit list");
|
||||||
|
|
|
||||||
|
|
@ -223,6 +223,14 @@ export function remapProjectState(
|
||||||
...board,
|
...board,
|
||||||
id: requiredId(boardIds, board.id),
|
id: requiredId(boardIds, board.id),
|
||||||
projectId: targetProjectId,
|
projectId: targetProjectId,
|
||||||
|
feedingDistributionBoardId:
|
||||||
|
board.feedingDistributionBoardId === null
|
||||||
|
? null
|
||||||
|
: requiredId(boardIds, board.feedingDistributionBoardId),
|
||||||
|
feedingCircuitId:
|
||||||
|
board.feedingCircuitId === null
|
||||||
|
? null
|
||||||
|
: requiredId(circuitIds, board.feedingCircuitId),
|
||||||
floorId:
|
floorId:
|
||||||
board.floorId === null
|
board.floorId === null
|
||||||
? null
|
? null
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,8 @@ export interface DistributionBoardDto extends DistributionBoardInfoDto {
|
||||||
floorId: string | null;
|
floorId: string | null;
|
||||||
supplyType: DistributionBoardSupplyType | null;
|
supplyType: DistributionBoardSupplyType | null;
|
||||||
simultaneityFactor: number;
|
simultaneityFactor: number;
|
||||||
|
feedingDistributionBoardId: string | null;
|
||||||
|
feedingCircuitId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentLogoMetadataDto {
|
export interface DocumentLogoMetadataDto {
|
||||||
|
|
|
||||||
|
|
@ -372,7 +372,13 @@ export function updateDistributionBoard(
|
||||||
floorId: string | null;
|
floorId: string | null;
|
||||||
supplyType: NonNullable<DistributionBoardDto["supplyType"]>;
|
supplyType: NonNullable<DistributionBoardDto["supplyType"]>;
|
||||||
simultaneityFactor: number;
|
simultaneityFactor: number;
|
||||||
} & Partial<DistributionBoardInfoDto>,
|
} & Partial<DistributionBoardInfoDto> &
|
||||||
|
Partial<
|
||||||
|
Pick<
|
||||||
|
DistributionBoardDto,
|
||||||
|
"feedingDistributionBoardId" | "feedingCircuitId"
|
||||||
|
>
|
||||||
|
>,
|
||||||
expectedRevision: number
|
expectedRevision: number
|
||||||
) {
|
) {
|
||||||
return request<DistributionBoardCommandResultDto>(
|
return request<DistributionBoardCommandResultDto>(
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,8 @@ export async function createDistributionBoard(req: Request, res: Response) {
|
||||||
location: null,
|
location: null,
|
||||||
fedFrom: null,
|
fedFrom: null,
|
||||||
remark: null,
|
remark: null,
|
||||||
|
feedingDistributionBoardId: null,
|
||||||
|
feedingCircuitId: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -98,6 +100,17 @@ export async function updateDistributionBoard(
|
||||||
.filter((field) => parsed.data[field] !== undefined)
|
.filter((field) => parsed.data[field] !== undefined)
|
||||||
.map((field) => [field, parsed.data[field]])
|
.map((field) => [field, parsed.data[field]])
|
||||||
);
|
);
|
||||||
|
const feedingPatch = {
|
||||||
|
...(parsed.data.feedingDistributionBoardId === undefined
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
feedingDistributionBoardId:
|
||||||
|
parsed.data.feedingDistributionBoardId,
|
||||||
|
}),
|
||||||
|
...(parsed.data.feedingCircuitId === undefined
|
||||||
|
? {}
|
||||||
|
: { feedingCircuitId: parsed.data.feedingCircuitId }),
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
const result = projectCommandService.executeUser({
|
const result = projectCommandService.executeUser({
|
||||||
projectId,
|
projectId,
|
||||||
|
|
@ -110,6 +123,7 @@ export async function updateDistributionBoard(
|
||||||
supplyType: parsed.data.supplyType,
|
supplyType: parsed.data.supplyType,
|
||||||
simultaneityFactor: parsed.data.simultaneityFactor,
|
simultaneityFactor: parsed.data.simultaneityFactor,
|
||||||
...infoPatch,
|
...infoPatch,
|
||||||
|
...feedingPatch,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,8 @@ export const updateDistributionBoardSchema = z
|
||||||
location: distributionBoardInfoValueSchema("location"),
|
location: distributionBoardInfoValueSchema("location"),
|
||||||
fedFrom: distributionBoardInfoValueSchema("fedFrom"),
|
fedFrom: distributionBoardInfoValueSchema("fedFrom"),
|
||||||
remark: distributionBoardInfoValueSchema("remark"),
|
remark: distributionBoardInfoValueSchema("remark"),
|
||||||
|
feedingDistributionBoardId: z.string().trim().min(1).nullable().optional(),
|
||||||
|
feedingCircuitId: z.string().trim().min(1).nullable().optional(),
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -491,6 +491,159 @@ describe("distribution-board structure project command", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("links a feeding board and circuit while preserving version-1 updates", () => {
|
||||||
|
const context = createTestDatabase();
|
||||||
|
try {
|
||||||
|
const repository =
|
||||||
|
new DistributionBoardStructureProjectCommandRepository(context.db);
|
||||||
|
const upstream = createDistributionBoardStructureSnapshot(
|
||||||
|
"project-1",
|
||||||
|
"NSHV",
|
||||||
|
{ supplyType: "AV" }
|
||||||
|
);
|
||||||
|
const downstream = createDistributionBoardStructureSnapshot(
|
||||||
|
"project-1",
|
||||||
|
"UV EG",
|
||||||
|
{ supplyType: "AV" }
|
||||||
|
);
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command: createDistributionBoardInsertProjectCommand(upstream),
|
||||||
|
});
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 1,
|
||||||
|
source: "user",
|
||||||
|
command: createDistributionBoardInsertProjectCommand(downstream),
|
||||||
|
});
|
||||||
|
context.db.insert(circuits).values([
|
||||||
|
{
|
||||||
|
id: "feed-circuit",
|
||||||
|
circuitListId: upstream.circuitList.id,
|
||||||
|
sectionId: upstream.sections[2]!.id,
|
||||||
|
equipmentIdentifier: "-3F1.1",
|
||||||
|
sortOrder: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "downstream-circuit",
|
||||||
|
circuitListId: downstream.circuitList.id,
|
||||||
|
sectionId: downstream.sections[2]!.id,
|
||||||
|
equipmentIdentifier: "-3F1.1",
|
||||||
|
sortOrder: 10,
|
||||||
|
},
|
||||||
|
]).run();
|
||||||
|
|
||||||
|
const linked = repository.executeUpdate({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 2,
|
||||||
|
source: "user",
|
||||||
|
command: createDistributionBoardUpdateProjectCommand(
|
||||||
|
downstream.distributionBoard.id,
|
||||||
|
{
|
||||||
|
feedingDistributionBoardId: upstream.distributionBoard.id,
|
||||||
|
feedingCircuitId: "feed-circuit",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
context.db
|
||||||
|
.select({
|
||||||
|
feedingDistributionBoardId:
|
||||||
|
distributionBoards.feedingDistributionBoardId,
|
||||||
|
feedingCircuitId: distributionBoards.feedingCircuitId,
|
||||||
|
})
|
||||||
|
.from(distributionBoards)
|
||||||
|
.where(eq(distributionBoards.id, downstream.distributionBoard.id))
|
||||||
|
.get(),
|
||||||
|
{
|
||||||
|
feedingDistributionBoardId: upstream.distributionBoard.id,
|
||||||
|
feedingCircuitId: "feed-circuit",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
repository.executeUpdate({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 3,
|
||||||
|
source: "undo",
|
||||||
|
historyTargetChangeSetId: new ProjectHistoryRepository(
|
||||||
|
context.db
|
||||||
|
).getNextCommand("project-1", "undo")?.changeSetId,
|
||||||
|
command: linked.inverse,
|
||||||
|
});
|
||||||
|
repository.executeUpdate({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 4,
|
||||||
|
source: "user",
|
||||||
|
command: createDistributionBoardUpdateProjectCommand(
|
||||||
|
downstream.distributionBoard.id,
|
||||||
|
{
|
||||||
|
feedingDistributionBoardId: upstream.distributionBoard.id,
|
||||||
|
feedingCircuitId: null,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() => repository.executeUpdate({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 5,
|
||||||
|
source: "user",
|
||||||
|
command: createDistributionBoardUpdateProjectCommand(
|
||||||
|
upstream.distributionBoard.id,
|
||||||
|
{
|
||||||
|
feedingDistributionBoardId: downstream.distributionBoard.id,
|
||||||
|
feedingCircuitId: null,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
/Zyklus/
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => repository.executeUpdate({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 5,
|
||||||
|
source: "user",
|
||||||
|
command: createDistributionBoardUpdateProjectCommand(
|
||||||
|
downstream.distributionBoard.id,
|
||||||
|
{
|
||||||
|
feedingDistributionBoardId: upstream.distributionBoard.id,
|
||||||
|
feedingCircuitId: "downstream-circuit",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
/gehört nicht zur gewählten Verteilung/
|
||||||
|
);
|
||||||
|
|
||||||
|
repository.executeUpdate({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 5,
|
||||||
|
source: "user",
|
||||||
|
command: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
type: "distribution-board.update",
|
||||||
|
payload: {
|
||||||
|
distributionBoardId: downstream.distributionBoard.id,
|
||||||
|
changes: [{ field: "boardNumber", value: "UV-01" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const afterLegacyUpdate = context.db
|
||||||
|
.select()
|
||||||
|
.from(distributionBoards)
|
||||||
|
.where(eq(distributionBoards.id, downstream.distributionBoard.id))
|
||||||
|
.get();
|
||||||
|
assert.equal(afterLegacyUpdate?.boardNumber, "UV-01");
|
||||||
|
assert.equal(
|
||||||
|
afterLegacyUpdate?.feedingDistributionBoardId,
|
||||||
|
upstream.distributionBoard.id
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("sends distribution-board writes to the revision-safe API", async () => {
|
it("sends distribution-board writes to the revision-safe API", async () => {
|
||||||
const requests: Array<{ url: string; method: string; body: unknown }> = [];
|
const requests: Array<{ url: string; method: string; body: unknown }> = [];
|
||||||
const originalFetch = globalThis.fetch;
|
const originalFetch = globalThis.fetch;
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ function createSnapshot(): DistributionBoardSubtreeSnapshot {
|
||||||
floorId: "floor-1",
|
floorId: "floor-1",
|
||||||
supplyType: "AV",
|
supplyType: "AV",
|
||||||
simultaneityFactor: 0.8,
|
simultaneityFactor: 0.8,
|
||||||
|
feedingDistributionBoardId: "board-upstream",
|
||||||
|
feedingCircuitId: "circuit-upstream",
|
||||||
},
|
},
|
||||||
circuitList: {
|
circuitList: {
|
||||||
id: "list-1",
|
id: "list-1",
|
||||||
|
|
@ -157,6 +159,11 @@ describe("distribution-board subtree snapshot", () => {
|
||||||
assert.equal(clone.distributionBoard.id, "new-1");
|
assert.equal(clone.distributionBoard.id, "new-1");
|
||||||
assert.equal(clone.circuitList.id, "new-1");
|
assert.equal(clone.circuitList.id, "new-1");
|
||||||
assert.equal(clone.distributionBoard.name, "UV 1 Kopie");
|
assert.equal(clone.distributionBoard.name, "UV 1 Kopie");
|
||||||
|
assert.equal(
|
||||||
|
clone.distributionBoard.feedingDistributionBoardId,
|
||||||
|
"board-upstream"
|
||||||
|
);
|
||||||
|
assert.equal(clone.distributionBoard.feedingCircuitId, "circuit-upstream");
|
||||||
assert.equal(clone.circuitList.name, "UV 1 Kopie Stromkreisliste");
|
assert.equal(clone.circuitList.name, "UV 1 Kopie Stromkreisliste");
|
||||||
assert.equal(clone.sections[0].id, "new-2");
|
assert.equal(clone.sections[0].id, "new-2");
|
||||||
assert.equal(clone.circuits[0].id, "new-3");
|
assert.equal(clone.circuits[0].id, "new-3");
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@ it("upgrades a populated pre-print database through the current schema without c
|
||||||
for (const [key, value] of Object.entries(oldRoom)) assert.deepEqual(room[key], value);
|
for (const [key, value] of Object.entries(oldRoom)) assert.deepEqual(room[key], value);
|
||||||
for (const field of ["site_address", "work_stage", "project_lead", "project_editor"]) assert.equal(project[field], null);
|
for (const field of ["site_address", "work_stage", "project_lead", "project_editor"]) assert.equal(project[field], null);
|
||||||
for (const field of ["board_number", "location", "fed_from", "remark"]) assert.equal(board[field], null);
|
for (const field of ["board_number", "location", "fed_from", "remark"]) assert.equal(board[field], null);
|
||||||
|
assert.equal(board.feeding_distribution_board_id, null);
|
||||||
|
assert.equal(board.feeding_circuit_id, null);
|
||||||
assert.equal(floor.building_id, null);
|
assert.equal(floor.building_id, null);
|
||||||
assert.equal(room.area_m2, null);
|
assert.equal(room.area_m2, null);
|
||||||
assert.equal(context.sqlite.prepare("SELECT count(*) AS count FROM buildings").get()!.count, 0);
|
assert.equal(context.sqlite.prepare("SELECT count(*) AS count FROM buildings").get()!.count, 0);
|
||||||
|
|
|
||||||
|
|
@ -476,7 +476,14 @@ describe("project snapshot repository", () => {
|
||||||
delete payload.project[field];
|
delete payload.project[field];
|
||||||
}
|
}
|
||||||
for (const board of payload.distributionBoards) {
|
for (const board of payload.distributionBoards) {
|
||||||
for (const field of ["boardNumber", "location", "fedFrom", "remark"]) {
|
for (const field of [
|
||||||
|
"boardNumber",
|
||||||
|
"location",
|
||||||
|
"fedFrom",
|
||||||
|
"remark",
|
||||||
|
"feedingDistributionBoardId",
|
||||||
|
"feedingCircuitId",
|
||||||
|
]) {
|
||||||
delete board[field];
|
delete board[field];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,25 @@ import {
|
||||||
projectStateSnapshotSchemaVersion,
|
projectStateSnapshotSchemaVersion,
|
||||||
serializeProjectStateSnapshot,
|
serializeProjectStateSnapshot,
|
||||||
} from "../src/domain/models/project-state-snapshot.model.js";
|
} from "../src/domain/models/project-state-snapshot.model.js";
|
||||||
|
import { remapProjectState } from "../src/domain/models/project-transfer.model.js";
|
||||||
import { createNamedProjectSnapshotSchema } from "../src/shared/validation/project-snapshot.schemas.js";
|
import { createNamedProjectSnapshotSchema } from "../src/shared/validation/project-snapshot.schemas.js";
|
||||||
|
|
||||||
|
/** Removes the fields introduced by snapshot schema version 9. */
|
||||||
|
function withoutVersionNineFields<
|
||||||
|
T extends { distributionBoards: object[] },
|
||||||
|
>(snapshot: T): T {
|
||||||
|
for (const board of snapshot.distributionBoards) {
|
||||||
|
delete (board as Record<string, unknown>).feedingDistributionBoardId;
|
||||||
|
delete (board as Record<string, unknown>).feedingCircuitId;
|
||||||
|
}
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
/** Removes the fields introduced by snapshot schema version 7. */
|
/** Removes the fields introduced by snapshot schema version 7. */
|
||||||
function withoutVersionSevenFields<
|
function withoutVersionSevenFields<
|
||||||
T extends { floors: object[] },
|
T extends { floors: object[]; distributionBoards: object[] },
|
||||||
>(snapshot: T): T {
|
>(snapshot: T): T {
|
||||||
|
withoutVersionNineFields(snapshot);
|
||||||
delete (snapshot as Record<string, unknown>).buildings;
|
delete (snapshot as Record<string, unknown>).buildings;
|
||||||
for (const floor of snapshot.floors) {
|
for (const floor of snapshot.floors) {
|
||||||
delete (floor as Record<string, unknown>).buildingId;
|
delete (floor as Record<string, unknown>).buildingId;
|
||||||
|
|
@ -117,6 +130,8 @@ function protectedSnapshot() {
|
||||||
location: "Raum 003 Technik" as string | null,
|
location: "Raum 003 Technik" as string | null,
|
||||||
fedFrom: "HV 0.1" as string | null,
|
fedFrom: "HV 0.1" as string | null,
|
||||||
remark: null as string | null,
|
remark: null as string | null,
|
||||||
|
feedingDistributionBoardId: null as string | null,
|
||||||
|
feedingCircuitId: null as string | null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
circuitLists: [
|
circuitLists: [
|
||||||
|
|
@ -206,6 +221,96 @@ describe("project state snapshot model", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("upgrades snapshot version 8 with empty feeding references", () => {
|
||||||
|
const previous = withoutVersionNineFields(protectedSnapshot());
|
||||||
|
previous.schemaVersion = 8 as typeof previous.schemaVersion;
|
||||||
|
|
||||||
|
const upgraded = parseProjectStateSnapshot(previous);
|
||||||
|
|
||||||
|
assert.equal(upgraded.schemaVersion, projectStateSnapshotSchemaVersion);
|
||||||
|
assert.equal(
|
||||||
|
upgraded.distributionBoards[0]?.feedingDistributionBoardId,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
assert.equal(upgraded.distributionBoards[0]?.feedingCircuitId, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("validates logical feeding references and cycles", () => {
|
||||||
|
const linked = protectedSnapshot();
|
||||||
|
linked.distributionBoards.push({
|
||||||
|
id: "board-2",
|
||||||
|
projectId: "project-1",
|
||||||
|
name: "UV 2",
|
||||||
|
floorId: null,
|
||||||
|
supplyType: null,
|
||||||
|
simultaneityFactor: 1,
|
||||||
|
boardNumber: "UV 2.1",
|
||||||
|
location: null,
|
||||||
|
fedFrom: null,
|
||||||
|
remark: null,
|
||||||
|
feedingDistributionBoardId: "board-1",
|
||||||
|
feedingCircuitId: "circuit-1",
|
||||||
|
});
|
||||||
|
linked.circuitLists.push({
|
||||||
|
id: "list-2",
|
||||||
|
projectId: "project-1",
|
||||||
|
distributionBoardId: "board-2",
|
||||||
|
name: "UV 2 Stromkreisliste",
|
||||||
|
});
|
||||||
|
linked.circuitSections.push({
|
||||||
|
id: "section-2",
|
||||||
|
circuitListId: "list-2",
|
||||||
|
key: "three_phase",
|
||||||
|
displayName: "Dreiphasig 1",
|
||||||
|
prefix: "-3F1.",
|
||||||
|
sortOrder: 10,
|
||||||
|
category: "three_phase",
|
||||||
|
groupNumber: 1,
|
||||||
|
});
|
||||||
|
const parsed = parseProjectStateSnapshot(linked);
|
||||||
|
assert.equal(parsed.distributionBoards[1]?.feedingCircuitId, "circuit-1");
|
||||||
|
let nextId = 0;
|
||||||
|
const remapped = remapProjectState(
|
||||||
|
parsed,
|
||||||
|
"project-copy",
|
||||||
|
() => `copy-${++nextId}`
|
||||||
|
);
|
||||||
|
const remappedUpstream = remapped.distributionBoards.find(
|
||||||
|
(board) => board.name === "UV 1"
|
||||||
|
);
|
||||||
|
const remappedDownstream = remapped.distributionBoards.find(
|
||||||
|
(board) => board.name === "UV 2"
|
||||||
|
);
|
||||||
|
assert.ok(remappedUpstream);
|
||||||
|
assert.ok(remappedDownstream);
|
||||||
|
assert.equal(
|
||||||
|
remappedDownstream.feedingDistributionBoardId,
|
||||||
|
remappedUpstream.id
|
||||||
|
);
|
||||||
|
assert.notEqual(remappedDownstream.feedingCircuitId, "circuit-1");
|
||||||
|
assert.ok(
|
||||||
|
remapped.circuits.some(
|
||||||
|
(circuit) => circuit.id === remappedDownstream.feedingCircuitId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const missingBoard = structuredClone(linked);
|
||||||
|
missingBoard.distributionBoards[1]!.feedingDistributionBoardId = null;
|
||||||
|
assert.throws(
|
||||||
|
() => parseProjectStateSnapshot(missingBoard),
|
||||||
|
/requires a feeding distribution board/
|
||||||
|
);
|
||||||
|
|
||||||
|
const cycle = structuredClone(linked);
|
||||||
|
cycle.distributionBoards[0]!.feedingDistributionBoardId = "board-2";
|
||||||
|
cycle.distributionBoards[0]!.feedingCircuitId = null;
|
||||||
|
cycle.distributionBoards[1]!.feedingCircuitId = null;
|
||||||
|
assert.throws(
|
||||||
|
() => parseProjectStateSnapshot(cycle),
|
||||||
|
/contains a cycle/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("upgrades the supported baseline snapshot with a safe building default", () => {
|
it("upgrades the supported baseline snapshot with a safe building default", () => {
|
||||||
const baseline = withoutVersionSixFields(structuredClone(minimalSnapshot()));
|
const baseline = withoutVersionSixFields(structuredClone(minimalSnapshot()));
|
||||||
baseline.schemaVersion = 1 as typeof baseline.schemaVersion;
|
baseline.schemaVersion = 1 as typeof baseline.schemaVersion;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue