Add placeholder move history
This commit is contained in:
@@ -1,8 +1,15 @@
|
||||
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,
|
||||
@@ -10,156 +17,31 @@ import type {
|
||||
import type { AppDatabase } from "../database-context.js";
|
||||
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
||||
import { circuitLists } from "../schema/circuit-lists.js";
|
||||
import { circuitSections } from "../schema/circuit-sections.js";
|
||||
import { circuits } from "../schema/circuits.js";
|
||||
import { applyProjectHistoryTransition } from "./project-history.persistence.js";
|
||||
import { appendProjectRevision } from "./project-revision.persistence.js";
|
||||
|
||||
interface PersistedMoveRow {
|
||||
id: string;
|
||||
circuitId: string;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
export class CircuitDeviceRowMoveProjectCommandRepository
|
||||
implements CircuitDeviceRowMoveProjectCommandStore
|
||||
{
|
||||
constructor(private readonly database: AppDatabase) {}
|
||||
|
||||
execute(input: ExecuteCircuitDeviceRowMoveCommandInput) {
|
||||
assertCircuitDeviceRowMoveProjectCommand(input.command);
|
||||
this.assertSupportedCommand(input.command);
|
||||
|
||||
return this.database.transaction((tx) => {
|
||||
const rowIds = input.command.payload.moves.map(
|
||||
(move) => move.rowId
|
||||
const inverse = this.applyCommand(
|
||||
tx,
|
||||
input.projectId,
|
||||
input.command
|
||||
);
|
||||
const persistedRows = tx
|
||||
.select({
|
||||
id: circuitDeviceRows.id,
|
||||
circuitId: circuitDeviceRows.circuitId,
|
||||
sortOrder: circuitDeviceRows.sortOrder,
|
||||
})
|
||||
.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 input.command.payload.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."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const circuitIds = [
|
||||
...new Set(
|
||||
input.command.payload.moves.flatMap((move) => [
|
||||
move.expectedCircuitId,
|
||||
move.targetCircuitId,
|
||||
])
|
||||
),
|
||||
];
|
||||
const participatingCircuits = tx
|
||||
.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 !== input.projectId
|
||||
)
|
||||
) {
|
||||
throw new Error("Move circuit does not belong to project.");
|
||||
}
|
||||
const circuitListIds = new Set(
|
||||
participatingCircuits.map(
|
||||
(circuit) => circuit.circuitListId
|
||||
)
|
||||
);
|
||||
if (circuitListIds.size !== 1) {
|
||||
throw new Error(
|
||||
"All moved rows and targets must belong to one circuit list."
|
||||
);
|
||||
}
|
||||
|
||||
const inverse = createCircuitDeviceRowMoveProjectCommand(
|
||||
input.command.payload.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,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
for (const move of input.command.payload.moves) {
|
||||
const result = tx
|
||||
.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."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const circuitId of circuitIds) {
|
||||
const remainingRow = tx
|
||||
.select({ id: circuitDeviceRows.id })
|
||||
.from(circuitDeviceRows)
|
||||
.where(eq(circuitDeviceRows.circuitId, circuitId))
|
||||
.limit(1)
|
||||
.get();
|
||||
const updated = tx
|
||||
.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."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const revision = appendProjectRevision(tx, {
|
||||
projectId: input.projectId,
|
||||
expectedRevision: input.expectedRevision,
|
||||
@@ -178,4 +60,457 @@ export class CircuitDeviceRowMoveProjectCommandRepository
|
||||
return { revision, inverse };
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
return inverse;
|
||||
}
|
||||
|
||||
assertCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
||||
command
|
||||
);
|
||||
const { targetCircuit, targetCircuitAction, moves } =
|
||||
command.payload;
|
||||
const rowsById = this.loadExpectedRows(database, moves);
|
||||
this.assertCircuitLocation(database, projectId, targetCircuit);
|
||||
|
||||
if (targetCircuitAction === "create") {
|
||||
const sourceCircuitIds = [
|
||||
...new Set(moves.map((move) => move.expectedCircuitId)),
|
||||
];
|
||||
this.assertExistingCircuitsInOneList(
|
||||
database,
|
||||
projectId,
|
||||
sourceCircuitIds,
|
||||
targetCircuit.circuitListId
|
||||
);
|
||||
this.assertTargetCircuitAvailable(database, targetCircuit);
|
||||
this.insertTargetCircuit(database, targetCircuit);
|
||||
|
||||
const inverse =
|
||||
createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
||||
"delete",
|
||||
targetCircuit,
|
||||
this.reverseMoves(moves, rowsById)
|
||||
);
|
||||
this.applyMoves(database, moves);
|
||||
this.updateReserveStates(database, [
|
||||
...sourceCircuitIds,
|
||||
targetCircuit.id,
|
||||
]);
|
||||
return inverse;
|
||||
}
|
||||
|
||||
this.assertTargetCircuitUnchanged(
|
||||
database,
|
||||
targetCircuit,
|
||||
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",
|
||||
targetCircuit,
|
||||
this.reverseMoves(moves, rowsById)
|
||||
);
|
||||
this.applyMoves(database, moves);
|
||||
this.updateReserveStates(database, [
|
||||
targetCircuit.id,
|
||||
...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 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,
|
||||
})
|
||||
.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,
|
||||
protectionType: snapshot.protectionType,
|
||||
protectionRatedCurrent: snapshot.protectionRatedCurrent,
|
||||
protectionCharacteristic: snapshot.protectionCharacteristic,
|
||||
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();
|
||||
}
|
||||
|
||||
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.protectionType !== snapshot.protectionType ||
|
||||
current.protectionRatedCurrent !==
|
||||
snapshot.protectionRatedCurrent ||
|
||||
current.protectionCharacteristic !==
|
||||
snapshot.protectionCharacteristic ||
|
||||
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 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."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user