Merge origin/main into feature/user-management
Renumber the users migration to 0013_users so it follows the upstream 0012_good_gateway (logical board feeds); AGENTS.md deferred-work list keeps both the new feed items and the auth items. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
95bfba2f67
63 changed files with 5546 additions and 443 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`);
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "7c6307b6-2516-445c-a088-c8ad746b9fde",
|
||||
"id": "8029e6ab-56de-4345-8d38-0ca7462640de",
|
||||
"prevId": "63f27b3a-7378-462d-8119-b67d67903dbb",
|
||||
"tables": {
|
||||
"buildings": {
|
||||
|
|
@ -1010,9 +1010,38 @@
|
|||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"feeding_distribution_board_id": {
|
||||
"name": "feeding_distribution_board_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"feeding_circuit_id": {
|
||||
"name": "feeding_circuit_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"distribution_boards_feeding_board_idx": {
|
||||
"name": "distribution_boards_feeding_board_idx",
|
||||
"columns": [
|
||||
"feeding_distribution_board_id"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"distribution_boards_feeding_circuit_idx": {
|
||||
"name": "distribution_boards_feeding_circuit_idx",
|
||||
"columns": [
|
||||
"feeding_circuit_id"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"distribution_boards_project_id_projects_id_fk": {
|
||||
"name": "distribution_boards_project_id_projects_id_fk",
|
||||
|
|
@ -1039,6 +1068,32 @@
|
|||
],
|
||||
"onDelete": "set null",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"distribution_boards_feeding_distribution_board_id_distribution_boards_id_fk": {
|
||||
"name": "distribution_boards_feeding_distribution_board_id_distribution_boards_id_fk",
|
||||
"tableFrom": "distribution_boards",
|
||||
"tableTo": "distribution_boards",
|
||||
"columnsFrom": [
|
||||
"feeding_distribution_board_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "restrict",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"distribution_boards_feeding_circuit_id_circuits_id_fk": {
|
||||
"name": "distribution_boards_feeding_circuit_id_circuits_id_fk",
|
||||
"tableFrom": "distribution_boards",
|
||||
"tableTo": "circuits",
|
||||
"columnsFrom": [
|
||||
"feeding_circuit_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "restrict",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
|
|
@ -2916,92 +2971,6 @@
|
|||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"users": {
|
||||
"name": "users",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"password_hash": {
|
||||
"name": "password_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'user'"
|
||||
},
|
||||
"active": {
|
||||
"name": "active",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"must_change_password": {
|
||||
"name": "must_change_password",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch())"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch())"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"users_username_unique": {
|
||||
"name": "users_username_unique",
|
||||
"columns": [
|
||||
"username"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
|
|
|
|||
3072
src/db/migrations/meta/0013_snapshot.json
Normal file
3072
src/db/migrations/meta/0013_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -89,8 +89,15 @@
|
|||
{
|
||||
"idx": 12,
|
||||
"version": "6",
|
||||
"when": 1790617581434,
|
||||
"tag": "0012_lively_turbo",
|
||||
"when": 1790540142918,
|
||||
"tag": "0012_good_gateway",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 13,
|
||||
"version": "6",
|
||||
"when": 1790624623732,
|
||||
"tag": "0013_users",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { circuits } from "../schema/circuits.js";
|
|||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||
import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js";
|
||||
import { resolveCircuitVoltage } from "./project-voltage.persistence.js";
|
||||
import { assertCircuitsAreNotReferencedAsFeeders } from "./distribution-board-feeding-deletion-guard.persistence.js";
|
||||
|
||||
interface PersistedMoveRow {
|
||||
id: string;
|
||||
|
|
@ -171,6 +172,9 @@ export class CircuitDeviceRowMoveProjectCommandRepository
|
|||
projectId,
|
||||
destinationCircuitIds
|
||||
);
|
||||
assertCircuitsAreNotReferencedAsFeeders(database, projectId, [
|
||||
targetCircuit.id,
|
||||
]);
|
||||
const deleted = database
|
||||
.delete(circuits)
|
||||
.where(
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { distributionBoardComponentProtectionDevices } from "../schema/distribut
|
|||
import { distributionBoardComponents } from "../schema/distribution-board-components.js";
|
||||
import { toCircuitDeviceRowInsertValues } from "./circuit-device-row-structure.persistence.js";
|
||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||
import { assertCircuitsAreNotReferencedAsFeeders } from "./distribution-board-feeding-deletion-guard.persistence.js";
|
||||
|
||||
export class CircuitGroupSubtreeProjectCommandRepository
|
||||
implements CircuitGroupSubtreeProjectCommandStore
|
||||
|
|
@ -65,6 +66,11 @@ export class CircuitGroupSubtreeProjectCommandRepository
|
|||
"Circuit-group subtree changed before deletion."
|
||||
);
|
||||
}
|
||||
assertCircuitsAreNotReferencedAsFeeders(
|
||||
database,
|
||||
projectId,
|
||||
current.circuits.map((entry) => entry.circuit.id)
|
||||
);
|
||||
const deleted = database
|
||||
.delete(circuitSections)
|
||||
.where(eq(circuitSections.id, current.group.id))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import crypto from "node:crypto";
|
||||
import { and, asc, eq } from "drizzle-orm";
|
||||
import { and, asc, eq, inArray } from "drizzle-orm";
|
||||
import { defaultCircuitSectionDefinitions } from "../../domain/models/distribution-board-structure-project-command.model.js";
|
||||
import type { AppDatabase } from "../database-context.js";
|
||||
import { circuitSections } from "../schema/circuit-sections.js";
|
||||
|
|
@ -30,6 +30,15 @@ export class CircuitSectionRepository {
|
|||
.orderBy(asc(circuitSections.sortOrder));
|
||||
}
|
||||
|
||||
async listByCircuitLists(circuitListIds: string[]) {
|
||||
if (circuitListIds.length === 0) return [];
|
||||
return this.database
|
||||
.select()
|
||||
.from(circuitSections)
|
||||
.where(inArray(circuitSections.circuitListId, circuitListIds))
|
||||
.orderBy(asc(circuitSections.sortOrder), asc(circuitSections.id));
|
||||
}
|
||||
|
||||
async createDefaults(circuitListId: string) {
|
||||
const db = this.database;
|
||||
for (const entry of createDefaultCircuitSectionValues(circuitListId)) {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import {
|
|||
toCircuitDeviceRowSnapshot,
|
||||
} from "./circuit-device-row-structure.persistence.js";
|
||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||
import { assertCircuitsAreNotReferencedAsFeeders } from "./distribution-board-feeding-deletion-guard.persistence.js";
|
||||
import { resolveCircuitVoltage } from "./project-voltage.persistence.js";
|
||||
import { assertEquipmentIdentifierAvailable } from "./equipment-identifier-uniqueness.persistence.js";
|
||||
|
||||
|
|
@ -236,6 +237,8 @@ export class CircuitStructureProjectCommandRepository
|
|||
protectionDevice: protectionDevice ?? null,
|
||||
});
|
||||
|
||||
assertCircuitsAreNotReferencedAsFeeders(database, projectId, [circuit.id]);
|
||||
|
||||
const result = database
|
||||
.delete(circuits)
|
||||
.where(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { asc, eq } from "drizzle-orm";
|
||||
import { asc, eq, inArray } from "drizzle-orm";
|
||||
import type { AppDatabase } from "../database-context.js";
|
||||
import { circuits } from "../schema/circuits.js";
|
||||
|
||||
|
|
@ -14,6 +14,15 @@ export class CircuitRepository {
|
|||
.orderBy(asc(circuits.sortOrder), asc(circuits.equipmentIdentifier));
|
||||
}
|
||||
|
||||
async listByCircuitLists(circuitListIds: string[]) {
|
||||
if (circuitListIds.length === 0) return [];
|
||||
return this.database
|
||||
.select()
|
||||
.from(circuits)
|
||||
.where(inArray(circuits.circuitListId, circuitListIds))
|
||||
.orderBy(asc(circuits.sortOrder), asc(circuits.equipmentIdentifier));
|
||||
}
|
||||
|
||||
async listBySection(sectionId: string) {
|
||||
const db = this.database;
|
||||
return db
|
||||
|
|
|
|||
|
|
@ -19,6 +19,18 @@ export class DistributionBoardComponentRepository {
|
|||
);
|
||||
}
|
||||
|
||||
async listByCircuitLists(circuitListIds: string[]) {
|
||||
if (circuitListIds.length === 0) return [];
|
||||
return this.database
|
||||
.select()
|
||||
.from(distributionBoardComponents)
|
||||
.where(inArray(distributionBoardComponents.circuitListId, circuitListIds))
|
||||
.orderBy(
|
||||
asc(distributionBoardComponents.sortOrder),
|
||||
asc(distributionBoardComponents.id)
|
||||
);
|
||||
}
|
||||
|
||||
async listProtectionByComponentIds(componentIds: string[]) {
|
||||
if (componentIds.length === 0) {
|
||||
return [];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
import { and, asc, eq, inArray } from "drizzle-orm";
|
||||
import type { AppDatabase } from "../database-context.js";
|
||||
import { distributionBoards } from "../schema/distribution-boards.js";
|
||||
|
||||
export function assertBoardsAreNotReferencedAsFeeders(
|
||||
database: AppDatabase,
|
||||
projectId: string,
|
||||
distributionBoardIds: string[]
|
||||
) {
|
||||
if (distributionBoardIds.length === 0) return;
|
||||
const dependent = database
|
||||
.select({
|
||||
name: distributionBoards.name,
|
||||
boardNumber: distributionBoards.boardNumber,
|
||||
})
|
||||
.from(distributionBoards)
|
||||
.where(
|
||||
and(
|
||||
eq(distributionBoards.projectId, projectId),
|
||||
inArray(
|
||||
distributionBoards.feedingDistributionBoardId,
|
||||
[...new Set(distributionBoardIds)]
|
||||
)
|
||||
)
|
||||
)
|
||||
.orderBy(asc(distributionBoards.boardNumber), asc(distributionBoards.name))
|
||||
.limit(1)
|
||||
.get();
|
||||
if (dependent) throwFeedingReferenceError(dependent);
|
||||
}
|
||||
|
||||
export function assertCircuitsAreNotReferencedAsFeeders(
|
||||
database: AppDatabase,
|
||||
projectId: string,
|
||||
circuitIds: string[]
|
||||
) {
|
||||
if (circuitIds.length === 0) return;
|
||||
const dependent = database
|
||||
.select({
|
||||
name: distributionBoards.name,
|
||||
boardNumber: distributionBoards.boardNumber,
|
||||
})
|
||||
.from(distributionBoards)
|
||||
.where(
|
||||
and(
|
||||
eq(distributionBoards.projectId, projectId),
|
||||
inArray(
|
||||
distributionBoards.feedingCircuitId,
|
||||
[...new Set(circuitIds)]
|
||||
)
|
||||
)
|
||||
)
|
||||
.orderBy(asc(distributionBoards.boardNumber), asc(distributionBoards.name))
|
||||
.limit(1)
|
||||
.get();
|
||||
if (dependent) throwFeedingReferenceError(dependent);
|
||||
}
|
||||
|
||||
function throwFeedingReferenceError(dependent: {
|
||||
name: string;
|
||||
boardNumber: string | null;
|
||||
}): never {
|
||||
const label = dependent.boardNumber ?? dependent.name;
|
||||
throw new Error(`Speist ${label} – erst Einspeisung ändern.`);
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@ import { distributionBoards } from "../schema/distribution-boards.js";
|
|||
import { floors } from "../schema/floors.js";
|
||||
import { projects } from "../schema/projects.js";
|
||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||
import { assertBoardsAreNotReferencedAsFeeders } from "./distribution-board-feeding-deletion-guard.persistence.js";
|
||||
|
||||
export class DistributionBoardStructureProjectCommandRepository
|
||||
implements DistributionBoardStructureProjectCommandStore
|
||||
|
|
@ -248,6 +249,8 @@ export class DistributionBoardStructureProjectCommandRepository
|
|||
);
|
||||
}
|
||||
|
||||
assertBoardsAreNotReferencedAsFeeders(database, projectId, [board.id]);
|
||||
|
||||
const deleted = database
|
||||
.delete(distributionBoards)
|
||||
.where(
|
||||
|
|
@ -325,6 +328,16 @@ export class DistributionBoardStructureProjectCommandRepository
|
|||
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(
|
||||
input.command.payload.changes.map((change) => [
|
||||
change.field,
|
||||
|
|
@ -333,7 +346,8 @@ export class DistributionBoardStructureProjectCommandRepository
|
|||
) as DistributionBoardUpdatePatch;
|
||||
const inverse = createDistributionBoardUpdateProjectCommand(
|
||||
current.id,
|
||||
inversePatch
|
||||
inversePatch,
|
||||
input.command.schemaVersion
|
||||
);
|
||||
const updated = database
|
||||
.update(distributionBoards)
|
||||
|
|
@ -352,6 +366,77 @@ export class DistributionBoardStructureProjectCommandRepository
|
|||
}
|
||||
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(
|
||||
|
|
@ -392,11 +477,20 @@ function structureMatches(
|
|||
location,
|
||||
fedFrom,
|
||||
remark,
|
||||
feedingDistributionBoardId,
|
||||
feedingCircuitId,
|
||||
...actualDistributionBoard
|
||||
} = actual.board;
|
||||
if (
|
||||
simultaneityFactor !== 1 ||
|
||||
[boardNumber, location, fedFrom, remark].some((value) => value !== null) ||
|
||||
[
|
||||
boardNumber,
|
||||
location,
|
||||
fedFrom,
|
||||
remark,
|
||||
feedingDistributionBoardId,
|
||||
feedingCircuitId,
|
||||
].some((value) => value !== null) ||
|
||||
!sameRecord(
|
||||
expected.distributionBoard,
|
||||
actualDistributionBoard
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import { projects } from "../schema/projects.js";
|
|||
import { rooms } from "../schema/rooms.js";
|
||||
import { toCircuitDeviceRowInsertValues } from "./circuit-device-row-structure.persistence.js";
|
||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||
import { assertBoardsAreNotReferencedAsFeeders } from "./distribution-board-feeding-deletion-guard.persistence.js";
|
||||
|
||||
export class DistributionBoardSubtreeProjectCommandRepository
|
||||
implements DistributionBoardSubtreeProjectCommandStore
|
||||
|
|
@ -82,6 +83,9 @@ export class DistributionBoardSubtreeProjectCommandRepository
|
|||
"Distribution-board subtree changed before deletion."
|
||||
);
|
||||
}
|
||||
assertBoardsAreNotReferencedAsFeeders(database, projectId, [
|
||||
current.distributionBoard.id,
|
||||
]);
|
||||
const deleted = database
|
||||
.delete(distributionBoards)
|
||||
.where(
|
||||
|
|
@ -300,6 +304,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(
|
||||
snapshot.circuits.flatMap((circuit) =>
|
||||
circuit.deviceRows.flatMap((row) =>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import {
|
|||
snapshotsEqual,
|
||||
} from "./external-object-assignment.persistence.js";
|
||||
import { assertEquipmentIdentifierAvailable } from "./equipment-identifier-uniqueness.persistence.js";
|
||||
import { assertCircuitsAreNotReferencedAsFeeders } from "./distribution-board-feeding-deletion-guard.persistence.js";
|
||||
|
||||
export class ExternalObjectNewCircuitProjectCommandRepository
|
||||
implements ExternalObjectNewCircuitProjectCommandStore
|
||||
|
|
@ -162,6 +163,9 @@ export class ExternalObjectNewCircuitProjectCommandRepository
|
|||
objects,
|
||||
"External object changed during new-circuit assignment."
|
||||
);
|
||||
assertCircuitsAreNotReferencedAsFeeders(database, projectId, [
|
||||
expectedCircuit.id,
|
||||
]);
|
||||
const deleted = database.delete(circuits).where(and(
|
||||
eq(circuits.id, expectedCircuit.id),
|
||||
eq(circuits.circuitListId, expectedCircuit.circuitListId)
|
||||
|
|
|
|||
|
|
@ -270,6 +270,14 @@ function replaceProjectState(
|
|||
database: AppDatabase,
|
||||
state: ProjectStateSnapshot
|
||||
) {
|
||||
database
|
||||
.update(distributionBoards)
|
||||
.set({
|
||||
feedingDistributionBoardId: null,
|
||||
feedingCircuitId: null,
|
||||
})
|
||||
.where(eq(distributionBoards.projectId, state.project.id))
|
||||
.run();
|
||||
database
|
||||
.delete(externalModelSources)
|
||||
.where(eq(externalModelSources.projectId, state.project.id))
|
||||
|
|
@ -330,7 +338,15 @@ function replaceProjectState(
|
|||
}
|
||||
insertMany(database, rooms, state.rooms);
|
||||
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, circuitSections, state.circuitSections);
|
||||
insertMany(
|
||||
|
|
@ -341,6 +357,7 @@ function replaceProjectState(
|
|||
isReserve: circuit.isReserve ? 1 : 0,
|
||||
}))
|
||||
);
|
||||
restoreDistributionBoardFeedingReferences(database, state);
|
||||
insertMany(
|
||||
database,
|
||||
distributionBoardComponents,
|
||||
|
|
@ -378,6 +395,31 @@ function replaceProjectState(
|
|||
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]>(
|
||||
database: AppDatabase,
|
||||
table: TTable,
|
||||
|
|
|
|||
|
|
@ -173,7 +173,15 @@ function insertProjectState(
|
|||
insertMany(database, floors, state.floors);
|
||||
insertMany(database, rooms, state.rooms);
|
||||
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, circuitSections, state.circuitSections);
|
||||
insertMany(
|
||||
|
|
@ -184,6 +192,25 @@ function insertProjectState(
|
|||
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(
|
||||
database,
|
||||
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 { projects } from "./projects.js";
|
||||
|
||||
|
|
@ -11,7 +11,7 @@ export const circuitLists = sqliteTable(
|
|||
.references(() => projects.id, { onDelete: "cascade" }),
|
||||
distributionBoardId: text("distribution_board_id")
|
||||
.notNull()
|
||||
.references(() => distributionBoards.id, { onDelete: "cascade" }),
|
||||
.references((): AnySQLiteColumn => distributionBoards.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
},
|
||||
(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 { circuitSections } from "./circuit-sections.js";
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ export const circuits = sqliteTable(
|
|||
id: text("id").primaryKey(),
|
||||
circuitListId: text("circuit_list_id")
|
||||
.notNull()
|
||||
.references(() => circuitLists.id, { onDelete: "cascade" }),
|
||||
.references((): AnySQLiteColumn => circuitLists.id, { onDelete: "cascade" }),
|
||||
sectionId: text("section_id")
|
||||
.notNull()
|
||||
.references(() => circuitSections.id, { onDelete: "cascade" }),
|
||||
|
|
|
|||
|
|
@ -1,24 +1,44 @@
|
|||
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 { projects } from "./projects.js";
|
||||
import type { DistributionBoardSupplyType } from "../../shared/constants/distribution-board.js";
|
||||
|
||||
export const distributionBoards = sqliteTable("distribution_boards", {
|
||||
id: text("id").primaryKey(),
|
||||
projectId: text("project_id")
|
||||
.notNull()
|
||||
.references(() => projects.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
floorId: text("floor_id").references(() => floors.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
supplyType: text("supply_type").$type<DistributionBoardSupplyType>(),
|
||||
simultaneityFactor: real("simultaneity_factor")
|
||||
.notNull()
|
||||
.default(1),
|
||||
boardNumber: text("board_number"),
|
||||
location: text("location"),
|
||||
fedFrom: text("fed_from"),
|
||||
remark: text("remark"),
|
||||
});
|
||||
export const distributionBoards = sqliteTable(
|
||||
"distribution_boards",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
projectId: text("project_id")
|
||||
.notNull()
|
||||
.references(() => projects.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
floorId: text("floor_id").references(() => floors.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
supplyType: text("supply_type").$type<DistributionBoardSupplyType>(),
|
||||
simultaneityFactor: real("simultaneity_factor")
|
||||
.notNull()
|
||||
.default(1),
|
||||
boardNumber: text("board_number"),
|
||||
location: text("location"),
|
||||
fedFrom: text("fed_from"),
|
||||
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
|
||||
),
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue