552 lines
16 KiB
TypeScript
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."
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|