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
|
exist for Circuit and CircuitDeviceRow field updates as well as
|
||||||
Circuit/CircuitDeviceRow insertion and deletion. Complete command coverage and
|
Circuit/CircuitDeviceRow insertion and deletion. Complete command coverage and
|
||||||
the UI cutover remain future work. CircuitDeviceRow moves between existing
|
the UI cutover remain future work. CircuitDeviceRow moves between existing
|
||||||
circuits are also persisted; moves that create a target circuit remain future
|
circuits and moves that create one new placeholder target circuit are also
|
||||||
work.
|
persisted. The latter stores the complete empty target snapshot so undo can
|
||||||
|
restore the rows and remove only the unchanged generated circuit.
|
||||||
|
|
||||||
Required operations:
|
Required operations:
|
||||||
|
|
||||||
|
|||||||
@@ -26,18 +26,22 @@ concurrency checks.
|
|||||||
The public dispatcher currently supports `circuit.update`, `circuit.insert`,
|
The public dispatcher currently supports `circuit.update`, `circuit.insert`,
|
||||||
`circuit.delete`, `circuit-device-row.update`,
|
`circuit.delete`, `circuit-device-row.update`,
|
||||||
`circuit-device-row.insert`, `circuit-device-row.delete` and
|
`circuit-device-row.insert`, `circuit-device-row.delete` and
|
||||||
`circuit-device-row.move`, all with schema version `1`. Other command types are
|
`circuit-device-row.move` plus
|
||||||
rejected. Insert commands contain the complete entity or circuit block with
|
`circuit-device-row.move-with-new-circuit`, all with schema version `1`. Other
|
||||||
stable ids; delete commands include the expected parent identity. Circuit
|
command types are rejected. Insert commands contain the complete entity or
|
||||||
snapshots contain zero, one or multiple complete device rows. Move commands
|
circuit block with stable ids; delete commands include the expected parent
|
||||||
contain each row's expected and target circuit plus its exact expected and
|
identity. Circuit snapshots contain zero, one or multiple complete device
|
||||||
target sort order. This makes deletion and moves undoable without generating
|
rows. Move commands contain each row's expected and target circuit plus its
|
||||||
replacement identities or renumbering circuits. The editor does not consume
|
exact expected and target sort order. This makes deletion and moves undoable
|
||||||
these endpoints yet.
|
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-device-row.move` targets existing circuits in the same circuit list.
|
||||||
circuit list. Creating a new target circuit while moving rows is not yet part
|
`circuit-device-row.move-with-new-circuit` atomically creates exactly one
|
||||||
of the persistent command API.
|
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:
|
Example:
|
||||||
|
|
||||||
|
|||||||
@@ -91,9 +91,13 @@ sichtbare Historie bleibt daher sitzungslokal.
|
|||||||
zwischen vorhandenen Stromkreisen derselben Liste. Erwartete und neue
|
zwischen vorhandenen Stromkreisen derselben Liste. Erwartete und neue
|
||||||
Stromkreis-/Sortierpositionen machen Forward und Inverse deterministisch;
|
Stromkreis-/Sortierpositionen machen Forward und Inverse deterministisch;
|
||||||
Reservewerte aller beteiligten Stromkreise werden atomar neu abgeleitet. Das
|
Reservewerte aller beteiligten Stromkreise werden atomar neu abgeleitet. Das
|
||||||
Verschieben auf einen freien Platz mit gleichzeitiger Stromkreiserzeugung sowie
|
Kompositkommando `circuit-device-row.move-with-new-circuit` bildet auch das
|
||||||
Circuit-Sortierung, Neunummerierung und Synchronisierung müssen vor der
|
Verschieben auf einen freien Platz ab: Ein Zielstromkreis mit stabiler UUID und
|
||||||
Frontend-Umstellung noch als persistente Commands modelliert werden.
|
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
|
## Projektgeräte
|
||||||
|
|
||||||
|
|||||||
@@ -182,13 +182,17 @@ Completed foundation:
|
|||||||
- `circuit-device-row.move` stores exact expected and target circuit/sort
|
- `circuit-device-row.move` stores exact expected and target circuit/sort
|
||||||
positions for one or multiple rows, recalculates all affected reserve states
|
positions for one or multiple rows, recalculates all affected reserve states
|
||||||
and reconstructs a deterministic inverse across multiple source circuits
|
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:
|
Remaining constraints before completing persistent history:
|
||||||
|
|
||||||
- extend the concrete project-scoped command union and executors beyond the
|
- extend the concrete project-scoped command union and executors beyond the
|
||||||
Circuit field/insert/delete and CircuitDeviceRow
|
Circuit field/insert/delete and CircuitDeviceRow
|
||||||
field/insert/delete/existing-target-move slices; the generic versioned
|
field/insert/delete/move slices; the generic versioned command envelope is
|
||||||
command envelope is complete
|
complete
|
||||||
- route the remaining structural, synchronization and destructive domain
|
- route the remaining structural, synchronization and destructive domain
|
||||||
writes through the shared command, revision and stack transaction boundary
|
writes through the shared command, revision and stack transaction boundary
|
||||||
- replace the session-local frontend command stack only after server-side
|
- 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
|
- typed CircuitDeviceRow move commands persist exact old/new circuit and sort
|
||||||
positions for single or multi-row moves between existing circuits and derive
|
positions for single or multi-row moves between existing circuits and derive
|
||||||
affected reserve states in the same transaction
|
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
|
- revision metadata, change-set payloads and the project counter are committed
|
||||||
in one SQLite transaction
|
in one SQLite transaction
|
||||||
- a stale expected revision produces no history writes
|
- a stale expected revision produces no history writes
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import { and, eq, inArray } from "drizzle-orm";
|
import { and, eq, inArray } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
assertCircuitDeviceRowMoveProjectCommand,
|
assertCircuitDeviceRowMoveProjectCommand,
|
||||||
|
assertCircuitDeviceRowMoveWithNewCircuitProjectCommand,
|
||||||
|
circuitDeviceRowMoveCommandType,
|
||||||
|
circuitDeviceRowMoveWithNewCircuitCommandType,
|
||||||
createCircuitDeviceRowMoveProjectCommand,
|
createCircuitDeviceRowMoveProjectCommand,
|
||||||
|
createCircuitDeviceRowMoveWithNewCircuitProjectCommand,
|
||||||
|
type CircuitDeviceRowMoveAssignment,
|
||||||
|
type CircuitDeviceRowMoveHistoryProjectCommand,
|
||||||
} from "../../domain/models/circuit-device-row-move-project-command.model.js";
|
} 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 {
|
import type {
|
||||||
CircuitDeviceRowMoveProjectCommandStore,
|
CircuitDeviceRowMoveProjectCommandStore,
|
||||||
ExecuteCircuitDeviceRowMoveCommandInput,
|
ExecuteCircuitDeviceRowMoveCommandInput,
|
||||||
@@ -10,156 +17,31 @@ import type {
|
|||||||
import type { AppDatabase } from "../database-context.js";
|
import type { AppDatabase } from "../database-context.js";
|
||||||
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
||||||
import { circuitLists } from "../schema/circuit-lists.js";
|
import { circuitLists } from "../schema/circuit-lists.js";
|
||||||
|
import { circuitSections } from "../schema/circuit-sections.js";
|
||||||
import { circuits } from "../schema/circuits.js";
|
import { circuits } from "../schema/circuits.js";
|
||||||
import { applyProjectHistoryTransition } from "./project-history.persistence.js";
|
import { applyProjectHistoryTransition } from "./project-history.persistence.js";
|
||||||
import { appendProjectRevision } from "./project-revision.persistence.js";
|
import { appendProjectRevision } from "./project-revision.persistence.js";
|
||||||
|
|
||||||
|
interface PersistedMoveRow {
|
||||||
|
id: string;
|
||||||
|
circuitId: string;
|
||||||
|
sortOrder: number;
|
||||||
|
}
|
||||||
|
|
||||||
export class CircuitDeviceRowMoveProjectCommandRepository
|
export class CircuitDeviceRowMoveProjectCommandRepository
|
||||||
implements CircuitDeviceRowMoveProjectCommandStore
|
implements CircuitDeviceRowMoveProjectCommandStore
|
||||||
{
|
{
|
||||||
constructor(private readonly database: AppDatabase) {}
|
constructor(private readonly database: AppDatabase) {}
|
||||||
|
|
||||||
execute(input: ExecuteCircuitDeviceRowMoveCommandInput) {
|
execute(input: ExecuteCircuitDeviceRowMoveCommandInput) {
|
||||||
assertCircuitDeviceRowMoveProjectCommand(input.command);
|
this.assertSupportedCommand(input.command);
|
||||||
|
|
||||||
return this.database.transaction((tx) => {
|
return this.database.transaction((tx) => {
|
||||||
const rowIds = input.command.payload.moves.map(
|
const inverse = this.applyCommand(
|
||||||
(move) => move.rowId
|
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, {
|
const revision = appendProjectRevision(tx, {
|
||||||
projectId: input.projectId,
|
projectId: input.projectId,
|
||||||
expectedRevision: input.expectedRevision,
|
expectedRevision: input.expectedRevision,
|
||||||
@@ -178,4 +60,457 @@ export class CircuitDeviceRowMoveProjectCommandRepository
|
|||||||
return { revision, inverse };
|
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 type { SerializedProjectCommand } from "./project-command.model.js";
|
||||||
|
import {
|
||||||
|
assertCircuitInsertProjectCommand,
|
||||||
|
circuitInsertCommandType,
|
||||||
|
circuitStructureCommandSchemaVersion,
|
||||||
|
type CircuitSnapshot,
|
||||||
|
} from "./circuit-structure-project-command.model.js";
|
||||||
|
|
||||||
export const circuitDeviceRowMoveCommandType =
|
export const circuitDeviceRowMoveCommandType =
|
||||||
"circuit-device-row.move" as const;
|
"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 const circuitDeviceRowMoveCommandSchemaVersion = 1 as const;
|
||||||
|
|
||||||
export interface CircuitDeviceRowMoveAssignment {
|
export interface CircuitDeviceRowMoveAssignment {
|
||||||
@@ -22,6 +30,26 @@ export interface CircuitDeviceRowMoveProjectCommand
|
|||||||
type: typeof circuitDeviceRowMoveCommandType;
|
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(
|
export function createCircuitDeviceRowMoveProjectCommand(
|
||||||
moves: CircuitDeviceRowMoveAssignment[]
|
moves: CircuitDeviceRowMoveAssignment[]
|
||||||
): CircuitDeviceRowMoveProjectCommand {
|
): CircuitDeviceRowMoveProjectCommand {
|
||||||
@@ -34,6 +62,24 @@ export function createCircuitDeviceRowMoveProjectCommand(
|
|||||||
return command;
|
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(
|
export function assertCircuitDeviceRowMoveProjectCommand(
|
||||||
command: SerializedProjectCommand<unknown>
|
command: SerializedProjectCommand<unknown>
|
||||||
): asserts command is CircuitDeviceRowMoveProjectCommand {
|
): 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(
|
function assertNonEmptyString(
|
||||||
value: unknown,
|
value: unknown,
|
||||||
field: string
|
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 {
|
import type {
|
||||||
AppendedProjectRevision,
|
AppendedProjectRevision,
|
||||||
ProjectRevisionSource,
|
ProjectRevisionSource,
|
||||||
@@ -11,12 +11,12 @@ export interface ExecuteCircuitDeviceRowMoveCommandInput {
|
|||||||
description?: string;
|
description?: string;
|
||||||
actorId?: string;
|
actorId?: string;
|
||||||
historyTargetChangeSetId?: string;
|
historyTargetChangeSetId?: string;
|
||||||
command: CircuitDeviceRowMoveProjectCommand;
|
command: CircuitDeviceRowMoveHistoryProjectCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExecutedCircuitDeviceRowMoveCommand {
|
export interface ExecutedCircuitDeviceRowMoveCommand {
|
||||||
revision: AppendedProjectRevision;
|
revision: AppendedProjectRevision;
|
||||||
inverse: CircuitDeviceRowMoveProjectCommand;
|
inverse: CircuitDeviceRowMoveHistoryProjectCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CircuitDeviceRowMoveProjectCommandStore {
|
export interface CircuitDeviceRowMoveProjectCommandStore {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { ProjectHistoryOperationUnavailableError } from "../errors/project-history-operation-unavailable.error.js";
|
import { ProjectHistoryOperationUnavailableError } from "../errors/project-history-operation-unavailable.error.js";
|
||||||
import {
|
import {
|
||||||
assertCircuitDeviceRowMoveProjectCommand,
|
assertCircuitDeviceRowMoveProjectCommand,
|
||||||
|
assertCircuitDeviceRowMoveWithNewCircuitProjectCommand,
|
||||||
circuitDeviceRowMoveCommandType,
|
circuitDeviceRowMoveCommandType,
|
||||||
|
circuitDeviceRowMoveWithNewCircuitCommandType,
|
||||||
} from "../models/circuit-device-row-move-project-command.model.js";
|
} from "../models/circuit-device-row-move-project-command.model.js";
|
||||||
import {
|
import {
|
||||||
assertCircuitDeviceRowUpdateProjectCommand,
|
assertCircuitDeviceRowUpdateProjectCommand,
|
||||||
@@ -161,6 +163,15 @@ export class ProjectCommandService implements ProjectCommandExecutor {
|
|||||||
command: input.command,
|
command: input.command,
|
||||||
}).revision;
|
}).revision;
|
||||||
}
|
}
|
||||||
|
case circuitDeviceRowMoveWithNewCircuitCommandType: {
|
||||||
|
assertCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
||||||
|
input.command
|
||||||
|
);
|
||||||
|
return this.deviceRowMoveStore.execute({
|
||||||
|
...input,
|
||||||
|
command: input.command,
|
||||||
|
}).revision;
|
||||||
|
}
|
||||||
case circuitInsertCommandType: {
|
case circuitInsertCommandType: {
|
||||||
assertCircuitInsertProjectCommand(input.command);
|
assertCircuitInsertProjectCommand(input.command);
|
||||||
return this.circuitStructureStore.execute({
|
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 { circuits } from "../src/db/schema/circuits.js";
|
||||||
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
||||||
import { projects } from "../src/db/schema/projects.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 {
|
interface TestFixture {
|
||||||
context: DatabaseContext;
|
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", () => {
|
describe("circuit device-row move project-command repository", () => {
|
||||||
it("moves multiple rows and supports persisted undo and redo", () => {
|
it("moves multiple rows and supports persisted undo and redo", () => {
|
||||||
const fixture = createTestDatabase();
|
const fixture = createTestDatabase();
|
||||||
@@ -432,4 +465,245 @@ describe("circuit device-row move project-command repository", () => {
|
|||||||
fixture.context.close();
|
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";
|
} from "../src/domain/models/circuit-device-row-structure-project-command.model.js";
|
||||||
import {
|
import {
|
||||||
assertCircuitDeviceRowMoveProjectCommand,
|
assertCircuitDeviceRowMoveProjectCommand,
|
||||||
|
assertCircuitDeviceRowMoveWithNewCircuitProjectCommand,
|
||||||
createCircuitDeviceRowMoveProjectCommand,
|
createCircuitDeviceRowMoveProjectCommand,
|
||||||
|
createCircuitDeviceRowMoveWithNewCircuitProjectCommand,
|
||||||
} from "../src/domain/models/circuit-device-row-move-project-command.model.js";
|
} from "../src/domain/models/circuit-device-row-move-project-command.model.js";
|
||||||
import {
|
import {
|
||||||
assertCircuitInsertProjectCommand,
|
assertCircuitInsertProjectCommand,
|
||||||
@@ -251,6 +253,29 @@ describe("circuit device-row structure project commands", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("circuit device-row move 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", () => {
|
it("captures deterministic source and target positions", () => {
|
||||||
const command = createCircuitDeviceRowMoveProjectCommand([
|
const command = createCircuitDeviceRowMoveProjectCommand([
|
||||||
{
|
{
|
||||||
@@ -313,6 +338,87 @@ describe("circuit device-row move project commands", () => {
|
|||||||
/no-op/
|
/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", () => {
|
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 { ProjectHistoryOperationUnavailableError } from "../src/domain/errors/project-history-operation-unavailable.error.js";
|
||||||
import { ProjectRevisionConflictError } from "../src/domain/errors/project-revision-conflict.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 { 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 { 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 { createCircuitUpdateProjectCommand } from "../src/domain/models/circuit-project-command.model.js";
|
||||||
import { createCircuitInsertProjectCommand } from "../src/domain/models/circuit-structure-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", () => {
|
it("rejects unavailable history directions without writing a revision", () => {
|
||||||
const context = createTestDatabase();
|
const context = createTestDatabase();
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user