Files
leistungsbilanz-ts/src/db/repositories/circuit-device-row-move-project-command.repository.ts
T

552 lines
16 KiB
TypeScript

import { and, eq, inArray } from "drizzle-orm";
import {
assertCircuitDeviceRowMoveProjectCommand,
assertCircuitDeviceRowMoveWithNewCircuitProjectCommand,
circuitDeviceRowMoveCommandType,
circuitDeviceRowMoveWithNewCircuitCommandType,
createCircuitDeviceRowMoveProjectCommand,
createCircuitDeviceRowMoveWithNewCircuitProjectCommand,
type CircuitDeviceRowMoveAssignment,
type CircuitDeviceRowMoveHistoryProjectCommand,
} from "../../domain/models/circuit-device-row-move-project-command.model.js";
import type { CircuitSnapshot } from "../../domain/models/circuit-structure-project-command.model.js";
import type {
CircuitDeviceRowMoveProjectCommandStore,
ExecuteCircuitDeviceRowMoveCommandInput,
} from "../../domain/ports/circuit-device-row-move-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 { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js";
import { resolveCircuitVoltage } from "./project-voltage.persistence.js";
interface PersistedMoveRow {
id: string;
circuitId: string;
sortOrder: number;
phaseType: string | null;
}
export class CircuitDeviceRowMoveProjectCommandRepository
implements CircuitDeviceRowMoveProjectCommandStore
{
constructor(private readonly database: AppDatabase) {}
execute(input: ExecuteCircuitDeviceRowMoveCommandInput) {
this.assertSupportedCommand(input.command);
return executeProjectCommandTransaction(
this.database,
input,
(tx) =>
this.applyCommand(tx, input.projectId, input.command)
);
}
private assertSupportedCommand(
command: CircuitDeviceRowMoveHistoryProjectCommand
) {
if (command.type === circuitDeviceRowMoveCommandType) {
assertCircuitDeviceRowMoveProjectCommand(command);
return;
}
if (
command.type ===
circuitDeviceRowMoveWithNewCircuitCommandType
) {
assertCircuitDeviceRowMoveWithNewCircuitProjectCommand(
command
);
return;
}
throw new Error("Unsupported circuit device-row move command.");
}
private applyCommand(
database: AppDatabase,
projectId: string,
command: CircuitDeviceRowMoveHistoryProjectCommand
): CircuitDeviceRowMoveHistoryProjectCommand {
if (command.type === circuitDeviceRowMoveCommandType) {
const rowsById = this.loadExpectedRows(
database,
command.payload.moves
);
const circuitIds = this.collectCircuitIds(
command.payload.moves
);
this.assertExistingCircuitsInOneList(
database,
projectId,
circuitIds
);
const inverse = createCircuitDeviceRowMoveProjectCommand(
this.reverseMoves(command.payload.moves, rowsById)
);
this.applyMoves(database, command.payload.moves);
this.updateReserveStates(database, circuitIds);
this.updateVoltages(database, projectId, circuitIds);
return inverse;
}
assertCircuitDeviceRowMoveWithNewCircuitProjectCommand(
command
);
const { targetCircuit, targetCircuitAction, moves } =
command.payload;
const rowsById = this.loadExpectedRows(database, moves);
const appliedTargetCircuit = {
...targetCircuit,
voltage: resolveCircuitVoltage(
database,
projectId,
targetCircuit.sectionId,
moves.map((move) => rowsById.get(move.rowId)?.phaseType)
),
};
this.assertCircuitLocation(database, projectId, appliedTargetCircuit);
if (targetCircuitAction === "create") {
const sourceCircuitIds = [
...new Set(moves.map((move) => move.expectedCircuitId)),
];
this.assertExistingCircuitsInOneList(
database,
projectId,
sourceCircuitIds,
targetCircuit.circuitListId
);
this.assertTargetCircuitAvailable(database, appliedTargetCircuit);
this.insertTargetCircuit(database, appliedTargetCircuit);
const inverse =
createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
"delete",
appliedTargetCircuit,
this.reverseMoves(moves, rowsById)
);
this.applyMoves(database, moves);
this.updateReserveStates(database, [
...sourceCircuitIds,
targetCircuit.id,
]);
this.updateVoltages(database, projectId, [
...sourceCircuitIds,
targetCircuit.id,
]);
return inverse;
}
this.assertTargetCircuitUnchanged(
database,
appliedTargetCircuit,
moves.map((move) => move.rowId)
);
const destinationCircuitIds = [
...new Set(moves.map((move) => move.targetCircuitId)),
];
this.assertExistingCircuitsInOneList(
database,
projectId,
destinationCircuitIds,
targetCircuit.circuitListId
);
const inverse =
createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
"create",
appliedTargetCircuit,
this.reverseMoves(moves, rowsById)
);
this.applyMoves(database, moves);
this.updateReserveStates(database, [
targetCircuit.id,
...destinationCircuitIds,
]);
this.updateVoltages(
database,
projectId,
destinationCircuitIds
);
const deleted = database
.delete(circuits)
.where(
and(
eq(circuits.id, targetCircuit.id),
eq(
circuits.circuitListId,
targetCircuit.circuitListId
),
eq(circuits.isReserve, 1)
)
)
.run();
if (deleted.changes !== 1) {
throw new Error(
"Created target circuit changed before deletion."
);
}
return inverse;
}
private updateVoltages(
database: AppDatabase,
projectId: string,
circuitIds: string[]
) {
for (const circuitId of new Set(circuitIds)) {
updateDerivedCircuitVoltage(database, projectId, circuitId);
}
}
private loadExpectedRows(
database: AppDatabase,
moves: CircuitDeviceRowMoveAssignment[]
) {
const rowIds = moves.map((move) => move.rowId);
const persistedRows = database
.select({
id: circuitDeviceRows.id,
circuitId: circuitDeviceRows.circuitId,
sortOrder: circuitDeviceRows.sortOrder,
phaseType: circuitDeviceRows.phaseType,
})
.from(circuitDeviceRows)
.where(inArray(circuitDeviceRows.id, rowIds))
.all();
if (persistedRows.length !== rowIds.length) {
throw new Error(
"One or more circuit device rows do not exist."
);
}
const rowsById = new Map(
persistedRows.map((row) => [row.id, row])
);
for (const move of moves) {
const row = rowsById.get(move.rowId);
if (
!row ||
row.circuitId !== move.expectedCircuitId ||
row.sortOrder !== move.expectedSortOrder
) {
throw new Error(
"Circuit device row changed before move execution."
);
}
}
return rowsById;
}
private collectCircuitIds(
moves: CircuitDeviceRowMoveAssignment[]
) {
return [
...new Set(
moves.flatMap((move) => [
move.expectedCircuitId,
move.targetCircuitId,
])
),
];
}
private assertExistingCircuitsInOneList(
database: AppDatabase,
projectId: string,
circuitIds: string[],
expectedCircuitListId?: string
) {
const participatingCircuits = database
.select({
id: circuits.id,
circuitListId: circuits.circuitListId,
projectId: circuitLists.projectId,
})
.from(circuits)
.innerJoin(
circuitLists,
eq(circuitLists.id, circuits.circuitListId)
)
.where(inArray(circuits.id, circuitIds))
.all();
if (participatingCircuits.length !== circuitIds.length) {
throw new Error(
"One or more move circuits do not exist."
);
}
if (
participatingCircuits.some(
(circuit) => circuit.projectId !== projectId
)
) {
throw new Error("Move circuit does not belong to project.");
}
const circuitListIds = new Set(
participatingCircuits.map(
(circuit) => circuit.circuitListId
)
);
if (
circuitListIds.size !== 1 ||
(expectedCircuitListId !== undefined &&
!circuitListIds.has(expectedCircuitListId))
) {
throw new Error(
"All moved rows and targets must belong to one circuit list."
);
}
}
private assertCircuitLocation(
database: AppDatabase,
projectId: string,
snapshot: CircuitSnapshot
) {
const list = database
.select({ id: circuitLists.id })
.from(circuitLists)
.where(
and(
eq(circuitLists.id, snapshot.circuitListId),
eq(circuitLists.projectId, projectId)
)
)
.get();
if (!list) {
throw new Error("Circuit list does not belong to project.");
}
const section = database
.select({ id: circuitSections.id })
.from(circuitSections)
.where(
and(
eq(circuitSections.id, snapshot.sectionId),
eq(
circuitSections.circuitListId,
snapshot.circuitListId
)
)
)
.get();
if (!section) {
throw new Error("Section does not belong to circuit list.");
}
}
private assertTargetCircuitAvailable(
database: AppDatabase,
snapshot: CircuitSnapshot
) {
const existingCircuit = database
.select({ id: circuits.id })
.from(circuits)
.where(eq(circuits.id, snapshot.id))
.get();
if (existingCircuit) {
throw new Error("Move target circuit id already exists.");
}
const duplicateIdentifier = database
.select({ id: circuits.id })
.from(circuits)
.where(
and(
eq(circuits.circuitListId, snapshot.circuitListId),
eq(
circuits.equipmentIdentifier,
snapshot.equipmentIdentifier
)
)
)
.get();
if (duplicateIdentifier) {
throw new Error(
"Duplicate equipmentIdentifier in circuit list."
);
}
}
private insertTargetCircuit(
database: AppDatabase,
snapshot: CircuitSnapshot
) {
database
.insert(circuits)
.values({
id: snapshot.id,
circuitListId: snapshot.circuitListId,
sectionId: snapshot.sectionId,
equipmentIdentifier: snapshot.equipmentIdentifier,
displayName: snapshot.displayName,
sortOrder: snapshot.sortOrder,
cableType: snapshot.cableType,
cableCrossSection: snapshot.cableCrossSection,
cableLength: snapshot.cableLength,
rcdAssignment: snapshot.rcdAssignment,
terminalDesignation: snapshot.terminalDesignation,
voltage: snapshot.voltage,
controlRequirement: snapshot.controlRequirement,
status: snapshot.status,
isReserve: 1,
remark: snapshot.remark,
})
.run();
if (snapshot.protectionDevice) {
database
.insert(circuitProtectionDevices)
.values(snapshot.protectionDevice)
.run();
}
}
private assertTargetCircuitUnchanged(
database: AppDatabase,
snapshot: CircuitSnapshot,
expectedRowIds: string[]
) {
const current = database
.select()
.from(circuits)
.where(eq(circuits.id, snapshot.id))
.get();
if (
!current ||
current.circuitListId !== snapshot.circuitListId ||
current.sectionId !== snapshot.sectionId ||
current.equipmentIdentifier !==
snapshot.equipmentIdentifier ||
current.displayName !== snapshot.displayName ||
current.sortOrder !== snapshot.sortOrder ||
current.cableType !== snapshot.cableType ||
current.cableCrossSection !== snapshot.cableCrossSection ||
current.cableLength !== snapshot.cableLength ||
current.rcdAssignment !== snapshot.rcdAssignment ||
current.terminalDesignation !==
snapshot.terminalDesignation ||
current.voltage !== snapshot.voltage ||
current.controlRequirement !==
snapshot.controlRequirement ||
current.status !== snapshot.status ||
current.remark !== snapshot.remark ||
Boolean(current.isReserve)
) {
throw new Error(
"Created target circuit changed before command execution."
);
}
const protectionDevice = database
.select()
.from(circuitProtectionDevices)
.where(eq(circuitProtectionDevices.circuitId, snapshot.id))
.get();
if (
snapshot.protectionDevice === undefined
? protectionDevice !== undefined
: snapshot.protectionDevice === null
? protectionDevice !== undefined
: !protectionDevice ||
Object.entries(snapshot.protectionDevice).some(
([key, value]) =>
(protectionDevice as Record<string, unknown>)[key] !==
value
)
) {
throw new Error(
"Created target circuit protection changed before command execution."
);
}
const currentRowIds = database
.select({ id: circuitDeviceRows.id })
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.circuitId, snapshot.id))
.all()
.map((row) => row.id);
const expectedRowIdSet = new Set(expectedRowIds);
if (
currentRowIds.length !== expectedRowIds.length ||
currentRowIds.some((id) => !expectedRowIdSet.has(id))
) {
throw new Error(
"Created target circuit rows changed before command execution."
);
}
}
private reverseMoves(
moves: CircuitDeviceRowMoveAssignment[],
rowsById: Map<string, PersistedMoveRow>
) {
return moves.map((move) => {
const current = rowsById.get(move.rowId)!;
return {
rowId: move.rowId,
expectedCircuitId: move.targetCircuitId,
expectedSortOrder: move.targetSortOrder,
targetCircuitId: current.circuitId,
targetSortOrder: current.sortOrder,
};
});
}
private applyMoves(
database: AppDatabase,
moves: CircuitDeviceRowMoveAssignment[]
) {
for (const move of moves) {
const result = database
.update(circuitDeviceRows)
.set({
circuitId: move.targetCircuitId,
sortOrder: move.targetSortOrder,
})
.where(
and(
eq(circuitDeviceRows.id, move.rowId),
eq(
circuitDeviceRows.circuitId,
move.expectedCircuitId
),
eq(
circuitDeviceRows.sortOrder,
move.expectedSortOrder
)
)
)
.run();
if (result.changes !== 1) {
throw new Error(
"Circuit device row changed during move execution."
);
}
}
}
private updateReserveStates(
database: AppDatabase,
circuitIds: string[]
) {
for (const circuitId of new Set(circuitIds)) {
const remainingRow = database
.select({ id: circuitDeviceRows.id })
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.circuitId, circuitId))
.limit(1)
.get();
const updated = database
.update(circuits)
.set({ isReserve: remainingRow ? 0 : 1 })
.where(eq(circuits.id, circuitId))
.run();
if (updated.changes !== 1) {
throw new Error(
"Circuit changed during device-row move execution."
);
}
}
}
}