import { asc, eq } from "drizzle-orm"; import { circuitGroupDeleteSubtreeCommandType, circuitGroupRestoreSubtreeCommandType, assertCircuitGroupDeleteSubtreeProjectCommand, assertCircuitGroupRestoreSubtreeProjectCommand, createCircuitGroupDeleteSubtreeProjectCommand, createCircuitGroupRestoreSubtreeProjectCommand, type CircuitGroupSubtreeProjectCommand, } from "../../domain/models/circuit-group-subtree-project-command.model.js"; import type { CircuitGroupSubtreeSnapshot, CircuitProtectionDeviceSnapshot, } from "../../domain/models/circuit-group-subtree-snapshot.model.js"; import type { CircuitGroupSubtreeProjectCommandStore, ExecuteCircuitGroupSubtreeCommandInput, } from "../../domain/ports/circuit-group-subtree-project-command.store.js"; import type { AppDatabase } from "../database-context.js"; import { circuitDeviceRows } from "../schema/circuit-device-rows.js"; import { circuitLists } from "../schema/circuit-lists.js"; import { circuitProtectionDevices } from "../schema/circuit-protection-devices.js"; import { circuitSections } from "../schema/circuit-sections.js"; import { circuits } from "../schema/circuits.js"; import { distributionBoardComponentProtectionDevices } from "../schema/distribution-board-component-protection-devices.js"; 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"; export class CircuitGroupSubtreeProjectCommandRepository implements CircuitGroupSubtreeProjectCommandStore { constructor(private readonly database: AppDatabase) {} capture(projectId: string, groupId: string) { return this.captureFrom(this.database, projectId, groupId); } execute(input: ExecuteCircuitGroupSubtreeCommandInput) { if (input.command.type === circuitGroupDeleteSubtreeCommandType) { assertCircuitGroupDeleteSubtreeProjectCommand(input.command); } else { assertCircuitGroupRestoreSubtreeProjectCommand(input.command); } return executeProjectCommandTransaction( this.database, input, (tx) => this.applyCommand(tx, input.projectId, input.command) ); } private applyCommand( database: AppDatabase, projectId: string, command: CircuitGroupSubtreeProjectCommand ) { if (command.type === circuitGroupDeleteSubtreeCommandType) { const current = this.captureFrom( database, projectId, command.payload.snapshot.group.id ); if (!sameSnapshot(current, command.payload.snapshot)) { throw new Error( "Circuit-group subtree changed before deletion." ); } const deleted = database .delete(circuitSections) .where(eq(circuitSections.id, current.group.id)) .run(); if (deleted.changes !== 1) { throw new Error("Circuit-group subtree could not be deleted."); } return createCircuitGroupRestoreSubtreeProjectCommand( command.payload.snapshot ); } if (command.type === circuitGroupRestoreSubtreeCommandType) { this.restore(database, projectId, command.payload.snapshot); return createCircuitGroupDeleteSubtreeProjectCommand( command.payload.snapshot ); } throw new Error("Unsupported circuit-group subtree command."); } private captureFrom( database: AppDatabase, projectId: string, groupId: string ): CircuitGroupSubtreeSnapshot { const group = database .select() .from(circuitSections) .where(eq(circuitSections.id, groupId)) .get(); if (!group || group.category === null || group.groupNumber === null) { throw new Error("Circuit group not found."); } this.assertListOwnership(database, projectId, group.circuitListId); const components = database .select() .from(distributionBoardComponents) .where(eq(distributionBoardComponents.sectionId, group.id)) .orderBy( asc(distributionBoardComponents.sortOrder), asc(distributionBoardComponents.id) ) .all() .map((component) => ({ component: { ...component, role: component.role as | "group_upstream_protection" | "group_residual_current_protection", }, protectionDevice: database .select() .from(distributionBoardComponentProtectionDevices) .where( eq( distributionBoardComponentProtectionDevices.componentId, component.id ) ) .get() ?? null, })); const circuitSnapshots = database .select() .from(circuits) .where(eq(circuits.sectionId, group.id)) .orderBy(asc(circuits.sortOrder), asc(circuits.id)) .all() .map((circuit) => { const { isReserve, ...values } = circuit; return { circuit: { ...values, isReserve: Boolean(isReserve), deviceRows: database .select() .from(circuitDeviceRows) .where(eq(circuitDeviceRows.circuitId, circuit.id)) .orderBy( asc(circuitDeviceRows.sortOrder), asc(circuitDeviceRows.id) ) .all(), }, protectionDevice: (database .select() .from(circuitProtectionDevices) .where(eq(circuitProtectionDevices.circuitId, circuit.id)) .get() as CircuitProtectionDeviceSnapshot | undefined) ?? null, }; }); return { group: { ...group, category: group.category, groupNumber: group.groupNumber, }, components, circuits: circuitSnapshots, }; } private restore( database: AppDatabase, projectId: string, snapshot: CircuitGroupSubtreeSnapshot ) { this.assertListOwnership( database, projectId, snapshot.group.circuitListId ); const existing = database .select({ id: circuitSections.id }) .from(circuitSections) .where(eq(circuitSections.id, snapshot.group.id)) .get(); if (existing) { throw new Error("Circuit group already exists before restoration."); } database.insert(circuitSections).values(snapshot.group).run(); for (const component of snapshot.components) { database .insert(distributionBoardComponents) .values(component.component) .run(); if (component.protectionDevice !== null) { database .insert(distributionBoardComponentProtectionDevices) .values(component.protectionDevice) .run(); } } for (const entry of snapshot.circuits) { const { deviceRows, isReserve, ...circuit } = entry.circuit; database .insert(circuits) .values({ ...circuit, isReserve: isReserve ? 1 : 0 }) .run(); if (entry.protectionDevice !== null) { database .insert(circuitProtectionDevices) .values(entry.protectionDevice) .run(); } if (deviceRows.length > 0) { database .insert(circuitDeviceRows) .values(deviceRows.map(toCircuitDeviceRowInsertValues)) .run(); } } } private assertListOwnership( database: AppDatabase, projectId: string, circuitListId: string ) { const list = database .select({ projectId: circuitLists.projectId }) .from(circuitLists) .where(eq(circuitLists.id, circuitListId)) .get(); if (!list || list.projectId !== projectId) { throw new Error("Circuit-group list does not belong to project."); } } } function sameSnapshot( left: CircuitGroupSubtreeSnapshot, right: CircuitGroupSubtreeSnapshot ) { return canonicalJson(left) === canonicalJson(right); } function canonicalJson(value: unknown): string { if (Array.isArray(value)) { return `[${value.map(canonicalJson).join(",")}]`; } if (value !== null && typeof value === "object") { return `{${Object.entries(value) .sort(([left], [right]) => left.localeCompare(right)) .map( ([key, child]) => `${JSON.stringify(key)}:${canonicalJson(child)}` ) .join(",")}}`; } return JSON.stringify(value) ?? "null"; }