Add placeholder move history
This commit is contained in:
@@ -226,8 +226,9 @@ eligibility for the first commands. Public command, undo and redo endpoints
|
||||
exist for Circuit and CircuitDeviceRow field updates as well as
|
||||
Circuit/CircuitDeviceRow insertion and deletion. Complete command coverage and
|
||||
the UI cutover remain future work. CircuitDeviceRow moves between existing
|
||||
circuits are also persisted; moves that create a target circuit remain future
|
||||
work.
|
||||
circuits and moves that create one new placeholder target circuit are also
|
||||
persisted. The latter stores the complete empty target snapshot so undo can
|
||||
restore the rows and remove only the unchanged generated circuit.
|
||||
|
||||
Required operations:
|
||||
|
||||
|
||||
@@ -26,18 +26,22 @@ concurrency checks.
|
||||
The public dispatcher currently supports `circuit.update`, `circuit.insert`,
|
||||
`circuit.delete`, `circuit-device-row.update`,
|
||||
`circuit-device-row.insert`, `circuit-device-row.delete` and
|
||||
`circuit-device-row.move`, all with schema version `1`. Other command types are
|
||||
rejected. Insert commands contain the complete entity or circuit block with
|
||||
stable ids; delete commands include the expected parent identity. Circuit
|
||||
snapshots contain zero, one or multiple complete device rows. Move commands
|
||||
contain each row's expected and target circuit plus its exact expected and
|
||||
target sort order. This makes deletion and moves undoable without generating
|
||||
replacement identities or renumbering circuits. The editor does not consume
|
||||
these endpoints yet.
|
||||
`circuit-device-row.move` plus
|
||||
`circuit-device-row.move-with-new-circuit`, all with schema version `1`. Other
|
||||
command types are rejected. Insert commands contain the complete entity or
|
||||
circuit block with stable ids; delete commands include the expected parent
|
||||
identity. Circuit snapshots contain zero, one or multiple complete device
|
||||
rows. Move commands contain each row's expected and target circuit plus its
|
||||
exact expected and target sort order. This makes deletion and moves undoable
|
||||
without generating replacement identities or renumbering circuits. The editor
|
||||
does not consume these endpoints yet.
|
||||
|
||||
`circuit-device-row.move` currently targets existing circuits in the same
|
||||
circuit list. Creating a new target circuit while moving rows is not yet part
|
||||
of the persistent command API.
|
||||
`circuit-device-row.move` targets existing circuits in the same circuit list.
|
||||
`circuit-device-row.move-with-new-circuit` atomically creates exactly one
|
||||
explicitly identified empty target circuit and moves one or multiple existing
|
||||
rows into it. Its inverse restores every row to its exact prior position and
|
||||
deletes the generated circuit only if its fields and complete row set still
|
||||
match the recorded state.
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
@@ -91,9 +91,13 @@ sichtbare Historie bleibt daher sitzungslokal.
|
||||
zwischen vorhandenen Stromkreisen derselben Liste. Erwartete und neue
|
||||
Stromkreis-/Sortierpositionen machen Forward und Inverse deterministisch;
|
||||
Reservewerte aller beteiligten Stromkreise werden atomar neu abgeleitet. Das
|
||||
Verschieben auf einen freien Platz mit gleichzeitiger Stromkreiserzeugung sowie
|
||||
Circuit-Sortierung, Neunummerierung und Synchronisierung müssen vor der
|
||||
Frontend-Umstellung noch als persistente Commands modelliert werden.
|
||||
Kompositkommando `circuit-device-row.move-with-new-circuit` bildet auch das
|
||||
Verschieben auf einen freien Platz ab: Ein Zielstromkreis mit stabiler UUID und
|
||||
BMK wird zusammen mit allen Zeilenbewegungen erzeugt. Undo stellt die exakten
|
||||
Quellpositionen wieder her und löscht den erzeugten Stromkreis nur, wenn dessen
|
||||
Felder und vollständiger Zeilenbestand unverändert sind. Circuit-Sortierung,
|
||||
Neunummerierung und Synchronisierung müssen vor der Frontend-Umstellung noch
|
||||
als persistente Commands modelliert werden.
|
||||
|
||||
## Projektgeräte
|
||||
|
||||
|
||||
@@ -182,13 +182,17 @@ Completed foundation:
|
||||
- `circuit-device-row.move` stores exact expected and target circuit/sort
|
||||
positions for one or multiple rows, recalculates all affected reserve states
|
||||
and reconstructs a deterministic inverse across multiple source circuits
|
||||
- `circuit-device-row.move-with-new-circuit` additionally records the complete
|
||||
empty target-circuit snapshot; forward creation and all row moves share one
|
||||
transaction, while undo restores the source positions and deletes only an
|
||||
unchanged generated target
|
||||
|
||||
Remaining constraints before completing persistent history:
|
||||
|
||||
- extend the concrete project-scoped command union and executors beyond the
|
||||
Circuit field/insert/delete and CircuitDeviceRow
|
||||
field/insert/delete/existing-target-move slices; the generic versioned
|
||||
command envelope is complete
|
||||
field/insert/delete/move slices; the generic versioned command envelope is
|
||||
complete
|
||||
- route the remaining structural, synchronization and destructive domain
|
||||
writes through the shared command, revision and stack transaction boundary
|
||||
- replace the session-local frontend command stack only after server-side
|
||||
|
||||
@@ -398,6 +398,9 @@ Implemented foundation:
|
||||
- typed CircuitDeviceRow move commands persist exact old/new circuit and sort
|
||||
positions for single or multi-row moves between existing circuits and derive
|
||||
affected reserve states in the same transaction
|
||||
- placeholder-target moves persist one complete generated circuit identity and
|
||||
all row positions atomically; undo restores every source and removes only an
|
||||
unchanged generated circuit, without renumbering
|
||||
- revision metadata, change-set payloads and the project counter are committed
|
||||
in one SQLite transaction
|
||||
- a stale expected revision produces no history writes
|
||||
|
||||
@@ -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."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import type { SerializedProjectCommand } from "./project-command.model.js";
|
||||
import {
|
||||
assertCircuitInsertProjectCommand,
|
||||
circuitInsertCommandType,
|
||||
circuitStructureCommandSchemaVersion,
|
||||
type CircuitSnapshot,
|
||||
} from "./circuit-structure-project-command.model.js";
|
||||
|
||||
export const circuitDeviceRowMoveCommandType =
|
||||
"circuit-device-row.move" as const;
|
||||
export const circuitDeviceRowMoveWithNewCircuitCommandType =
|
||||
"circuit-device-row.move-with-new-circuit" as const;
|
||||
export const circuitDeviceRowMoveCommandSchemaVersion = 1 as const;
|
||||
|
||||
export interface CircuitDeviceRowMoveAssignment {
|
||||
@@ -22,6 +30,26 @@ export interface CircuitDeviceRowMoveProjectCommand
|
||||
type: typeof circuitDeviceRowMoveCommandType;
|
||||
}
|
||||
|
||||
export type CircuitDeviceRowMoveTargetCircuitAction =
|
||||
| "create"
|
||||
| "delete";
|
||||
|
||||
export interface CircuitDeviceRowMoveWithNewCircuitCommandPayload {
|
||||
targetCircuitAction: CircuitDeviceRowMoveTargetCircuitAction;
|
||||
targetCircuit: CircuitSnapshot;
|
||||
moves: CircuitDeviceRowMoveAssignment[];
|
||||
}
|
||||
|
||||
export interface CircuitDeviceRowMoveWithNewCircuitProjectCommand
|
||||
extends SerializedProjectCommand<CircuitDeviceRowMoveWithNewCircuitCommandPayload> {
|
||||
schemaVersion: typeof circuitDeviceRowMoveCommandSchemaVersion;
|
||||
type: typeof circuitDeviceRowMoveWithNewCircuitCommandType;
|
||||
}
|
||||
|
||||
export type CircuitDeviceRowMoveHistoryProjectCommand =
|
||||
| CircuitDeviceRowMoveProjectCommand
|
||||
| CircuitDeviceRowMoveWithNewCircuitProjectCommand;
|
||||
|
||||
export function createCircuitDeviceRowMoveProjectCommand(
|
||||
moves: CircuitDeviceRowMoveAssignment[]
|
||||
): CircuitDeviceRowMoveProjectCommand {
|
||||
@@ -34,6 +62,24 @@ export function createCircuitDeviceRowMoveProjectCommand(
|
||||
return command;
|
||||
}
|
||||
|
||||
export function createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
||||
targetCircuitAction: CircuitDeviceRowMoveTargetCircuitAction,
|
||||
targetCircuit: CircuitSnapshot,
|
||||
moves: CircuitDeviceRowMoveAssignment[]
|
||||
): CircuitDeviceRowMoveWithNewCircuitProjectCommand {
|
||||
const command: CircuitDeviceRowMoveWithNewCircuitProjectCommand = {
|
||||
schemaVersion: circuitDeviceRowMoveCommandSchemaVersion,
|
||||
type: circuitDeviceRowMoveWithNewCircuitCommandType,
|
||||
payload: {
|
||||
targetCircuitAction,
|
||||
targetCircuit,
|
||||
moves,
|
||||
},
|
||||
};
|
||||
assertCircuitDeviceRowMoveWithNewCircuitProjectCommand(command);
|
||||
return command;
|
||||
}
|
||||
|
||||
export function assertCircuitDeviceRowMoveProjectCommand(
|
||||
command: SerializedProjectCommand<unknown>
|
||||
): asserts command is CircuitDeviceRowMoveProjectCommand {
|
||||
@@ -91,6 +137,76 @@ export function assertCircuitDeviceRowMoveProjectCommand(
|
||||
}
|
||||
}
|
||||
|
||||
export function assertCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
||||
command: SerializedProjectCommand<unknown>
|
||||
): asserts command is CircuitDeviceRowMoveWithNewCircuitProjectCommand {
|
||||
if (
|
||||
command.schemaVersion !== circuitDeviceRowMoveCommandSchemaVersion ||
|
||||
command.type !== circuitDeviceRowMoveWithNewCircuitCommandType
|
||||
) {
|
||||
throw new Error(
|
||||
"Unsupported circuit device-row move-with-new-circuit command."
|
||||
);
|
||||
}
|
||||
if (!isPlainObject(command.payload)) {
|
||||
throw new Error(
|
||||
"Circuit device-row move-with-new-circuit payload must be an object."
|
||||
);
|
||||
}
|
||||
const { targetCircuitAction, targetCircuit, moves } =
|
||||
command.payload;
|
||||
if (
|
||||
targetCircuitAction !== "create" &&
|
||||
targetCircuitAction !== "delete"
|
||||
) {
|
||||
throw new Error(
|
||||
"Target circuit action must be create or delete."
|
||||
);
|
||||
}
|
||||
|
||||
assertCircuitInsertProjectCommand({
|
||||
schemaVersion: circuitStructureCommandSchemaVersion,
|
||||
type: circuitInsertCommandType,
|
||||
payload: { circuit: targetCircuit },
|
||||
});
|
||||
const validatedTargetCircuit = targetCircuit as CircuitSnapshot;
|
||||
if (
|
||||
validatedTargetCircuit.isReserve !== true ||
|
||||
validatedTargetCircuit.deviceRows.length !== 0
|
||||
) {
|
||||
throw new Error(
|
||||
"Move target circuit snapshot must describe an empty reserve circuit."
|
||||
);
|
||||
}
|
||||
|
||||
assertCircuitDeviceRowMoveProjectCommand({
|
||||
schemaVersion: circuitDeviceRowMoveCommandSchemaVersion,
|
||||
type: circuitDeviceRowMoveCommandType,
|
||||
payload: { moves },
|
||||
});
|
||||
const validatedMoves = moves as CircuitDeviceRowMoveAssignment[];
|
||||
for (const move of validatedMoves) {
|
||||
if (
|
||||
targetCircuitAction === "create" &&
|
||||
(move.expectedCircuitId === validatedTargetCircuit.id ||
|
||||
move.targetCircuitId !== validatedTargetCircuit.id)
|
||||
) {
|
||||
throw new Error(
|
||||
"Create-target moves must move rows into the new circuit."
|
||||
);
|
||||
}
|
||||
if (
|
||||
targetCircuitAction === "delete" &&
|
||||
(move.expectedCircuitId !== validatedTargetCircuit.id ||
|
||||
move.targetCircuitId === validatedTargetCircuit.id)
|
||||
) {
|
||||
throw new Error(
|
||||
"Delete-target moves must move rows out of the created circuit."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertNonEmptyString(
|
||||
value: unknown,
|
||||
field: string
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CircuitDeviceRowMoveProjectCommand } from "../models/circuit-device-row-move-project-command.model.js";
|
||||
import type { CircuitDeviceRowMoveHistoryProjectCommand } from "../models/circuit-device-row-move-project-command.model.js";
|
||||
import type {
|
||||
AppendedProjectRevision,
|
||||
ProjectRevisionSource,
|
||||
@@ -11,12 +11,12 @@ export interface ExecuteCircuitDeviceRowMoveCommandInput {
|
||||
description?: string;
|
||||
actorId?: string;
|
||||
historyTargetChangeSetId?: string;
|
||||
command: CircuitDeviceRowMoveProjectCommand;
|
||||
command: CircuitDeviceRowMoveHistoryProjectCommand;
|
||||
}
|
||||
|
||||
export interface ExecutedCircuitDeviceRowMoveCommand {
|
||||
revision: AppendedProjectRevision;
|
||||
inverse: CircuitDeviceRowMoveProjectCommand;
|
||||
inverse: CircuitDeviceRowMoveHistoryProjectCommand;
|
||||
}
|
||||
|
||||
export interface CircuitDeviceRowMoveProjectCommandStore {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { ProjectHistoryOperationUnavailableError } from "../errors/project-history-operation-unavailable.error.js";
|
||||
import {
|
||||
assertCircuitDeviceRowMoveProjectCommand,
|
||||
assertCircuitDeviceRowMoveWithNewCircuitProjectCommand,
|
||||
circuitDeviceRowMoveCommandType,
|
||||
circuitDeviceRowMoveWithNewCircuitCommandType,
|
||||
} from "../models/circuit-device-row-move-project-command.model.js";
|
||||
import {
|
||||
assertCircuitDeviceRowUpdateProjectCommand,
|
||||
@@ -161,6 +163,15 @@ export class ProjectCommandService implements ProjectCommandExecutor {
|
||||
command: input.command,
|
||||
}).revision;
|
||||
}
|
||||
case circuitDeviceRowMoveWithNewCircuitCommandType: {
|
||||
assertCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
||||
input.command
|
||||
);
|
||||
return this.deviceRowMoveStore.execute({
|
||||
...input,
|
||||
command: input.command,
|
||||
}).revision;
|
||||
}
|
||||
case circuitInsertCommandType: {
|
||||
assertCircuitInsertProjectCommand(input.command);
|
||||
return this.circuitStructureStore.execute({
|
||||
|
||||
@@ -15,7 +15,11 @@ import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
||||
import { circuits } from "../src/db/schema/circuits.js";
|
||||
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
||||
import { projects } from "../src/db/schema/projects.js";
|
||||
import { createCircuitDeviceRowMoveProjectCommand } from "../src/domain/models/circuit-device-row-move-project-command.model.js";
|
||||
import {
|
||||
createCircuitDeviceRowMoveProjectCommand,
|
||||
createCircuitDeviceRowMoveWithNewCircuitProjectCommand,
|
||||
} from "../src/domain/models/circuit-device-row-move-project-command.model.js";
|
||||
import type { CircuitSnapshot } from "../src/domain/models/circuit-structure-project-command.model.js";
|
||||
|
||||
interface TestFixture {
|
||||
context: DatabaseContext;
|
||||
@@ -164,6 +168,35 @@ function isReserve(context: DatabaseContext, circuitId: string) {
|
||||
);
|
||||
}
|
||||
|
||||
function createMoveTargetSnapshot(
|
||||
fixture: TestFixture,
|
||||
overrides: Partial<CircuitSnapshot> = {}
|
||||
): CircuitSnapshot {
|
||||
return {
|
||||
id: "circuit-new",
|
||||
circuitListId: fixture.circuitListId,
|
||||
sectionId: fixture.sectionId,
|
||||
equipmentIdentifier: "-1F4",
|
||||
displayName: "Neuer Stromkreis",
|
||||
sortOrder: 40,
|
||||
protectionType: null,
|
||||
protectionRatedCurrent: null,
|
||||
protectionCharacteristic: null,
|
||||
cableType: null,
|
||||
cableCrossSection: null,
|
||||
cableLength: null,
|
||||
rcdAssignment: null,
|
||||
terminalDesignation: null,
|
||||
voltage: null,
|
||||
controlRequirement: null,
|
||||
status: null,
|
||||
isReserve: true,
|
||||
remark: null,
|
||||
deviceRows: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("circuit device-row move project-command repository", () => {
|
||||
it("moves multiple rows and supports persisted undo and redo", () => {
|
||||
const fixture = createTestDatabase();
|
||||
@@ -432,4 +465,245 @@ describe("circuit device-row move project-command repository", () => {
|
||||
fixture.context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("creates one target circuit and supports persisted undo and redo", () => {
|
||||
const fixture = createTestDatabase();
|
||||
try {
|
||||
const store = new CircuitDeviceRowMoveProjectCommandRepository(
|
||||
fixture.context.db
|
||||
);
|
||||
const targetCircuit = createMoveTargetSnapshot(fixture);
|
||||
const command =
|
||||
createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
||||
"create",
|
||||
targetCircuit,
|
||||
[
|
||||
{
|
||||
rowId: "row-1",
|
||||
expectedCircuitId: "circuit-1",
|
||||
expectedSortOrder: 10,
|
||||
targetCircuitId: targetCircuit.id,
|
||||
targetSortOrder: 10,
|
||||
},
|
||||
{
|
||||
rowId: "row-2",
|
||||
expectedCircuitId: "circuit-2",
|
||||
expectedSortOrder: 10,
|
||||
targetCircuitId: targetCircuit.id,
|
||||
targetSortOrder: 20,
|
||||
},
|
||||
]
|
||||
);
|
||||
const moved = store.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 0,
|
||||
source: "user",
|
||||
command,
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
[
|
||||
getRow(fixture.context, "row-1"),
|
||||
getRow(fixture.context, "row-2"),
|
||||
].map((row) => [row?.circuitId, row?.sortOrder]),
|
||||
[
|
||||
["circuit-new", 10],
|
||||
["circuit-new", 20],
|
||||
]
|
||||
);
|
||||
assert.equal(
|
||||
fixture.context.db
|
||||
.select({ equipmentIdentifier: circuits.equipmentIdentifier })
|
||||
.from(circuits)
|
||||
.where(eq(circuits.id, "circuit-new"))
|
||||
.get()?.equipmentIdentifier,
|
||||
"-1F4"
|
||||
);
|
||||
assert.equal(isReserve(fixture.context, "circuit-new"), false);
|
||||
assert.equal(isReserve(fixture.context, "circuit-1"), false);
|
||||
assert.equal(isReserve(fixture.context, "circuit-2"), true);
|
||||
|
||||
store.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 1,
|
||||
source: "undo",
|
||||
historyTargetChangeSetId: moved.revision.changeSetId,
|
||||
command: moved.inverse,
|
||||
});
|
||||
assert.equal(
|
||||
fixture.context.db
|
||||
.select({ id: circuits.id })
|
||||
.from(circuits)
|
||||
.where(eq(circuits.id, "circuit-new"))
|
||||
.get(),
|
||||
undefined
|
||||
);
|
||||
assert.deepEqual(
|
||||
[
|
||||
getRow(fixture.context, "row-1"),
|
||||
getRow(fixture.context, "row-2"),
|
||||
].map((row) => [row?.circuitId, row?.sortOrder]),
|
||||
[
|
||||
["circuit-1", 10],
|
||||
["circuit-2", 10],
|
||||
]
|
||||
);
|
||||
assert.equal(isReserve(fixture.context, "circuit-1"), false);
|
||||
assert.equal(isReserve(fixture.context, "circuit-2"), false);
|
||||
|
||||
store.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 2,
|
||||
source: "redo",
|
||||
historyTargetChangeSetId: moved.revision.changeSetId,
|
||||
command,
|
||||
});
|
||||
assert.equal(
|
||||
getRow(fixture.context, "row-2")?.circuitId,
|
||||
"circuit-new"
|
||||
);
|
||||
assert.deepEqual(
|
||||
new ProjectHistoryRepository(fixture.context.db).getState(
|
||||
"project-1"
|
||||
),
|
||||
{
|
||||
projectId: "project-1",
|
||||
currentRevision: 3,
|
||||
undoDepth: 1,
|
||||
redoDepth: 0,
|
||||
undoChangeSetId: moved.revision.changeSetId,
|
||||
redoChangeSetId: null,
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
fixture.context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects deletion when the created target changed outside history", () => {
|
||||
const fixture = createTestDatabase();
|
||||
try {
|
||||
const store = new CircuitDeviceRowMoveProjectCommandRepository(
|
||||
fixture.context.db
|
||||
);
|
||||
const targetCircuit = createMoveTargetSnapshot(fixture);
|
||||
const moved = store.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 0,
|
||||
source: "user",
|
||||
command:
|
||||
createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
||||
"create",
|
||||
targetCircuit,
|
||||
[
|
||||
{
|
||||
rowId: "row-2",
|
||||
expectedCircuitId: "circuit-2",
|
||||
expectedSortOrder: 10,
|
||||
targetCircuitId: targetCircuit.id,
|
||||
targetSortOrder: 10,
|
||||
},
|
||||
]
|
||||
),
|
||||
});
|
||||
fixture.context.db
|
||||
.update(circuits)
|
||||
.set({ displayName: "Außerhalb geändert" })
|
||||
.where(eq(circuits.id, targetCircuit.id))
|
||||
.run();
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
store.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 1,
|
||||
source: "undo",
|
||||
historyTargetChangeSetId:
|
||||
moved.revision.changeSetId,
|
||||
command: moved.inverse,
|
||||
}),
|
||||
/target circuit changed/
|
||||
);
|
||||
assert.equal(
|
||||
getRow(fixture.context, "row-2")?.circuitId,
|
||||
targetCircuit.id
|
||||
);
|
||||
assert.ok(
|
||||
fixture.context.db
|
||||
.select({ id: circuits.id })
|
||||
.from(circuits)
|
||||
.where(eq(circuits.id, targetCircuit.id))
|
||||
.get()
|
||||
);
|
||||
assert.equal(
|
||||
new ProjectHistoryRepository(fixture.context.db).getState(
|
||||
"project-1"
|
||||
)?.currentRevision,
|
||||
1
|
||||
);
|
||||
} finally {
|
||||
fixture.context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("rolls back target creation and moves for late history failures", () => {
|
||||
const fixture = createTestDatabase();
|
||||
try {
|
||||
fixture.context.sqlite.exec(`
|
||||
CREATE TRIGGER fail_composite_move_history
|
||||
BEFORE INSERT ON project_history_stack_entries
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'forced composite move history failure');
|
||||
END;
|
||||
`);
|
||||
const targetCircuit = createMoveTargetSnapshot(fixture);
|
||||
const store = new CircuitDeviceRowMoveProjectCommandRepository(
|
||||
fixture.context.db
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
store.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 0,
|
||||
source: "user",
|
||||
command:
|
||||
createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
||||
"create",
|
||||
targetCircuit,
|
||||
[
|
||||
{
|
||||
rowId: "row-2",
|
||||
expectedCircuitId: "circuit-2",
|
||||
expectedSortOrder: 10,
|
||||
targetCircuitId: targetCircuit.id,
|
||||
targetSortOrder: 10,
|
||||
},
|
||||
]
|
||||
),
|
||||
}),
|
||||
/forced composite move history failure/
|
||||
);
|
||||
assert.equal(
|
||||
fixture.context.db
|
||||
.select({ id: circuits.id })
|
||||
.from(circuits)
|
||||
.where(eq(circuits.id, targetCircuit.id))
|
||||
.get(),
|
||||
undefined
|
||||
);
|
||||
assert.equal(
|
||||
getRow(fixture.context, "row-2")?.circuitId,
|
||||
"circuit-2"
|
||||
);
|
||||
assert.equal(isReserve(fixture.context, "circuit-2"), false);
|
||||
assert.equal(
|
||||
fixture.context.db.select().from(projectRevisions).all()
|
||||
.length,
|
||||
0
|
||||
);
|
||||
} finally {
|
||||
fixture.context.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,7 +20,9 @@ import {
|
||||
} from "../src/domain/models/circuit-device-row-structure-project-command.model.js";
|
||||
import {
|
||||
assertCircuitDeviceRowMoveProjectCommand,
|
||||
assertCircuitDeviceRowMoveWithNewCircuitProjectCommand,
|
||||
createCircuitDeviceRowMoveProjectCommand,
|
||||
createCircuitDeviceRowMoveWithNewCircuitProjectCommand,
|
||||
} from "../src/domain/models/circuit-device-row-move-project-command.model.js";
|
||||
import {
|
||||
assertCircuitInsertProjectCommand,
|
||||
@@ -251,6 +253,29 @@ describe("circuit device-row structure project commands", () => {
|
||||
});
|
||||
|
||||
describe("circuit device-row move project commands", () => {
|
||||
const targetCircuit = {
|
||||
id: "circuit-new",
|
||||
circuitListId: "list-1",
|
||||
sectionId: "section-1",
|
||||
equipmentIdentifier: "-1F3",
|
||||
displayName: "Neuer Stromkreis",
|
||||
sortOrder: 30,
|
||||
protectionType: null,
|
||||
protectionRatedCurrent: null,
|
||||
protectionCharacteristic: null,
|
||||
cableType: null,
|
||||
cableCrossSection: null,
|
||||
cableLength: null,
|
||||
rcdAssignment: null,
|
||||
terminalDesignation: null,
|
||||
voltage: null,
|
||||
controlRequirement: null,
|
||||
status: null,
|
||||
isReserve: true,
|
||||
remark: null,
|
||||
deviceRows: [],
|
||||
};
|
||||
|
||||
it("captures deterministic source and target positions", () => {
|
||||
const command = createCircuitDeviceRowMoveProjectCommand([
|
||||
{
|
||||
@@ -313,6 +338,87 @@ describe("circuit device-row move project commands", () => {
|
||||
/no-op/
|
||||
);
|
||||
});
|
||||
|
||||
it("captures creation and deletion of a deterministic move target", () => {
|
||||
const create =
|
||||
createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
||||
"create",
|
||||
targetCircuit,
|
||||
[
|
||||
{
|
||||
rowId: "row-1",
|
||||
expectedCircuitId: "circuit-1",
|
||||
expectedSortOrder: 10,
|
||||
targetCircuitId: targetCircuit.id,
|
||||
targetSortOrder: 10,
|
||||
},
|
||||
]
|
||||
);
|
||||
const remove =
|
||||
createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
||||
"delete",
|
||||
targetCircuit,
|
||||
[
|
||||
{
|
||||
rowId: "row-1",
|
||||
expectedCircuitId: targetCircuit.id,
|
||||
expectedSortOrder: 10,
|
||||
targetCircuitId: "circuit-1",
|
||||
targetSortOrder: 10,
|
||||
},
|
||||
]
|
||||
);
|
||||
assert.doesNotThrow(() =>
|
||||
assertCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
||||
create
|
||||
)
|
||||
);
|
||||
assert.equal(remove.payload.targetCircuitAction, "delete");
|
||||
});
|
||||
|
||||
it("rejects inconsistent move targets and non-empty snapshots", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
||||
"create",
|
||||
targetCircuit,
|
||||
[
|
||||
{
|
||||
rowId: "row-1",
|
||||
expectedCircuitId: "circuit-1",
|
||||
expectedSortOrder: 10,
|
||||
targetCircuitId: "other-circuit",
|
||||
targetSortOrder: 10,
|
||||
},
|
||||
]
|
||||
),
|
||||
/move rows into the new circuit/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
assertCircuitDeviceRowMoveWithNewCircuitProjectCommand({
|
||||
schemaVersion: 1,
|
||||
type: "circuit-device-row.move-with-new-circuit",
|
||||
payload: {
|
||||
targetCircuitAction: "create",
|
||||
targetCircuit: {
|
||||
...targetCircuit,
|
||||
isReserve: false,
|
||||
},
|
||||
moves: [
|
||||
{
|
||||
rowId: "row-1",
|
||||
expectedCircuitId: "circuit-1",
|
||||
expectedSortOrder: 10,
|
||||
targetCircuitId: targetCircuit.id,
|
||||
targetSortOrder: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
/reserve state/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("circuit structure project commands", () => {
|
||||
|
||||
@@ -23,7 +23,10 @@ import { projects } from "../src/db/schema/projects.js";
|
||||
import { ProjectHistoryOperationUnavailableError } from "../src/domain/errors/project-history-operation-unavailable.error.js";
|
||||
import { ProjectRevisionConflictError } from "../src/domain/errors/project-revision-conflict.error.js";
|
||||
import { createCircuitDeviceRowUpdateProjectCommand } from "../src/domain/models/circuit-device-row-project-command.model.js";
|
||||
import { createCircuitDeviceRowMoveProjectCommand } from "../src/domain/models/circuit-device-row-move-project-command.model.js";
|
||||
import {
|
||||
createCircuitDeviceRowMoveProjectCommand,
|
||||
createCircuitDeviceRowMoveWithNewCircuitProjectCommand,
|
||||
} from "../src/domain/models/circuit-device-row-move-project-command.model.js";
|
||||
import { createCircuitDeviceRowInsertProjectCommand } from "../src/domain/models/circuit-device-row-structure-project-command.model.js";
|
||||
import { createCircuitUpdateProjectCommand } from "../src/domain/models/circuit-project-command.model.js";
|
||||
import { createCircuitInsertProjectCommand } from "../src/domain/models/circuit-structure-project-command.model.js";
|
||||
@@ -387,6 +390,91 @@ describe("project command service", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("dispatches placeholder moves that create and remove a circuit", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
const sourceCircuit = context.db
|
||||
.select()
|
||||
.from(circuits)
|
||||
.where(eq(circuits.id, "circuit-1"))
|
||||
.get();
|
||||
assert.ok(sourceCircuit);
|
||||
const targetCircuit = {
|
||||
id: "circuit-placeholder-target",
|
||||
circuitListId: sourceCircuit.circuitListId,
|
||||
sectionId: sourceCircuit.sectionId,
|
||||
equipmentIdentifier: "-1F2",
|
||||
displayName: "Neuer Stromkreis",
|
||||
sortOrder: 20,
|
||||
protectionType: null,
|
||||
protectionRatedCurrent: null,
|
||||
protectionCharacteristic: null,
|
||||
cableType: null,
|
||||
cableCrossSection: null,
|
||||
cableLength: null,
|
||||
rcdAssignment: null,
|
||||
terminalDesignation: null,
|
||||
voltage: null,
|
||||
controlRequirement: null,
|
||||
status: null,
|
||||
isReserve: true,
|
||||
remark: null,
|
||||
deviceRows: [],
|
||||
};
|
||||
|
||||
const moved = createService(context).executeUser({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 0,
|
||||
command:
|
||||
createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
||||
"create",
|
||||
targetCircuit,
|
||||
[
|
||||
{
|
||||
rowId: "row-1",
|
||||
expectedCircuitId: "circuit-1",
|
||||
expectedSortOrder: 10,
|
||||
targetCircuitId: targetCircuit.id,
|
||||
targetSortOrder: 10,
|
||||
},
|
||||
]
|
||||
),
|
||||
});
|
||||
assert.equal(moved.history.undoDepth, 1);
|
||||
assert.equal(
|
||||
context.db
|
||||
.select()
|
||||
.from(circuitDeviceRows)
|
||||
.where(eq(circuitDeviceRows.id, "row-1"))
|
||||
.get()?.circuitId,
|
||||
targetCircuit.id
|
||||
);
|
||||
|
||||
createService(context).undo({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 1,
|
||||
});
|
||||
assert.equal(
|
||||
context.db
|
||||
.select({ id: circuits.id })
|
||||
.from(circuits)
|
||||
.where(eq(circuits.id, targetCircuit.id))
|
||||
.get(),
|
||||
undefined
|
||||
);
|
||||
assert.equal(
|
||||
context.db
|
||||
.select()
|
||||
.from(circuitDeviceRows)
|
||||
.where(eq(circuitDeviceRows.id, "row-1"))
|
||||
.get()?.circuitId,
|
||||
"circuit-1"
|
||||
);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects unavailable history directions without writing a revision", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user