Add placeholder move history
This commit is contained in:
@@ -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