Persist device row moves
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
buildCircuitDeviceRowMoveAssignments,
|
||||
} from "../src/frontend/utils/circuit-device-row-move-command.js";
|
||||
import type {
|
||||
CircuitTreeCircuitDto,
|
||||
CircuitTreeDeviceRowDto,
|
||||
} from "../src/frontend/types.js";
|
||||
|
||||
function row(
|
||||
id: string,
|
||||
sortOrder: number
|
||||
): CircuitTreeDeviceRowDto {
|
||||
return {
|
||||
id,
|
||||
sortOrder,
|
||||
name: id,
|
||||
displayName: id,
|
||||
quantity: 1,
|
||||
powerPerUnit: 0.1,
|
||||
simultaneityFactor: 1,
|
||||
rowTotalPower: 0.1,
|
||||
};
|
||||
}
|
||||
|
||||
function circuit(
|
||||
id: string,
|
||||
deviceRows: CircuitTreeDeviceRowDto[]
|
||||
): CircuitTreeCircuitDto {
|
||||
return {
|
||||
id,
|
||||
circuitListId: "list-1",
|
||||
sectionId: "section-1",
|
||||
equipmentIdentifier: id,
|
||||
sortOrder: 10,
|
||||
isReserve: deviceRows.length === 0,
|
||||
circuitTotalPower: 0,
|
||||
deviceRows,
|
||||
};
|
||||
}
|
||||
|
||||
describe("circuit device-row move command builder", () => {
|
||||
it("records exact sources and appends rows in selection order", () => {
|
||||
const targetRows = [row("target-row", 20)];
|
||||
const circuits = [
|
||||
circuit("source-1", [row("row-1", 30)]),
|
||||
circuit("source-2", [row("row-2", 10)]),
|
||||
circuit("target", targetRows),
|
||||
];
|
||||
|
||||
assert.deepEqual(
|
||||
buildCircuitDeviceRowMoveAssignments({
|
||||
circuits,
|
||||
rowIds: ["row-2", "row-1"],
|
||||
targetCircuitId: "target",
|
||||
targetDeviceRows: targetRows,
|
||||
}),
|
||||
[
|
||||
{
|
||||
rowId: "row-2",
|
||||
expectedCircuitId: "source-2",
|
||||
expectedSortOrder: 10,
|
||||
targetCircuitId: "target",
|
||||
targetSortOrder: 30,
|
||||
},
|
||||
{
|
||||
rowId: "row-1",
|
||||
expectedCircuitId: "source-1",
|
||||
expectedSortOrder: 30,
|
||||
targetCircuitId: "target",
|
||||
targetSortOrder: 40,
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it("starts a generated target circuit at sort order ten", () => {
|
||||
assert.deepEqual(
|
||||
buildCircuitDeviceRowMoveAssignments({
|
||||
circuits: [circuit("source", [row("row-1", 40)])],
|
||||
rowIds: ["row-1"],
|
||||
targetCircuitId: "generated-target",
|
||||
targetDeviceRows: [],
|
||||
}),
|
||||
[
|
||||
{
|
||||
rowId: "row-1",
|
||||
expectedCircuitId: "source",
|
||||
expectedSortOrder: 40,
|
||||
targetCircuitId: "generated-target",
|
||||
targetSortOrder: 10,
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it("omits unchanged assignments during a same-circuit reorder", () => {
|
||||
const rows = [row("row-1", 10), row("row-2", 20)];
|
||||
assert.deepEqual(
|
||||
buildCircuitDeviceRowMoveAssignments({
|
||||
circuits: [circuit("target", rows)],
|
||||
rowIds: ["row-1", "row-2"],
|
||||
targetCircuitId: "target",
|
||||
targetDeviceRows: rows,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects duplicate or missing selected rows", () => {
|
||||
const circuits = [circuit("source", [row("row-1", 10)])];
|
||||
assert.throws(
|
||||
() =>
|
||||
buildCircuitDeviceRowMoveAssignments({
|
||||
circuits,
|
||||
rowIds: ["row-1", "row-1"],
|
||||
targetCircuitId: "target",
|
||||
targetDeviceRows: [],
|
||||
}),
|
||||
/mehrfach/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
buildCircuitDeviceRowMoveAssignments({
|
||||
circuits,
|
||||
rowIds: ["missing"],
|
||||
targetCircuitId: "target",
|
||||
targetDeviceRows: [],
|
||||
}),
|
||||
/nicht gefunden/
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,529 +0,0 @@
|
||||
import path from "node:path";
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
||||
import {
|
||||
createDatabaseContext,
|
||||
type DatabaseContext,
|
||||
} from "../src/db/database-context.js";
|
||||
import { CircuitDeviceRowTransactionRepository } from "../src/db/repositories/circuit-device-row-transaction.repository.js";
|
||||
import { DistributionBoardRepository } from "../src/db/repositories/distribution-board.repository.js";
|
||||
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
||||
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
||||
import { circuits } from "../src/db/schema/circuits.js";
|
||||
import { projects } from "../src/db/schema/projects.js";
|
||||
|
||||
function createTestDatabase(): DatabaseContext {
|
||||
const context = createDatabaseContext(":memory:");
|
||||
migrate(context.db, {
|
||||
migrationsFolder: path.resolve("src", "db", "migrations"),
|
||||
});
|
||||
context.db.insert(projects).values({ id: "project-1", name: "Test project" }).run();
|
||||
const board = new DistributionBoardRepository(
|
||||
context.db
|
||||
).createWithCircuitListAndDefaultSections(
|
||||
"project-1",
|
||||
"UV-01"
|
||||
);
|
||||
const [section] = context.db
|
||||
.select()
|
||||
.from(circuitSections)
|
||||
.where(eq(circuitSections.circuitListId, board.id))
|
||||
.limit(1)
|
||||
.all();
|
||||
context.db
|
||||
.insert(circuits)
|
||||
.values({
|
||||
id: "circuit-1",
|
||||
circuitListId: board.id,
|
||||
sectionId: section.id,
|
||||
equipmentIdentifier: "-1F1",
|
||||
displayName: "Reserve",
|
||||
sortOrder: 10,
|
||||
isReserve: 1,
|
||||
})
|
||||
.run();
|
||||
return context;
|
||||
}
|
||||
|
||||
function insertCircuit(
|
||||
context: DatabaseContext,
|
||||
input: {
|
||||
id: string;
|
||||
equipmentIdentifier: string;
|
||||
sortOrder: number;
|
||||
isReserve?: number;
|
||||
}
|
||||
) {
|
||||
const [seedCircuit] = context.db.select().from(circuits).limit(1).all();
|
||||
context.db
|
||||
.insert(circuits)
|
||||
.values({
|
||||
id: input.id,
|
||||
circuitListId: seedCircuit.circuitListId,
|
||||
sectionId: seedCircuit.sectionId,
|
||||
equipmentIdentifier: input.equipmentIdentifier,
|
||||
displayName: input.equipmentIdentifier,
|
||||
sortOrder: input.sortOrder,
|
||||
isReserve: input.isReserve ?? 1,
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
||||
function insertDeviceRow(
|
||||
context: DatabaseContext,
|
||||
input: {
|
||||
id?: string;
|
||||
circuitId?: string;
|
||||
sortOrder?: number;
|
||||
displayName?: string;
|
||||
} = {}
|
||||
) {
|
||||
const rowId = input.id ?? "row-1";
|
||||
const circuitId = input.circuitId ?? "circuit-1";
|
||||
context.db
|
||||
.insert(circuitDeviceRows)
|
||||
.values({
|
||||
id: rowId,
|
||||
circuitId,
|
||||
sortOrder: input.sortOrder ?? 10,
|
||||
name: "Leuchte",
|
||||
displayName: input.displayName ?? "Leuchte",
|
||||
quantity: 1,
|
||||
powerPerUnit: 0.1,
|
||||
simultaneityFactor: 1,
|
||||
})
|
||||
.run();
|
||||
context.db
|
||||
.update(circuits)
|
||||
.set({ isReserve: 0 })
|
||||
.where(eq(circuits.id, circuitId))
|
||||
.run();
|
||||
}
|
||||
|
||||
describe("circuit device-row transaction repository", () => {
|
||||
it("commits a new device row and clears the circuit reserve status together", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
const repository = new CircuitDeviceRowTransactionRepository(context.db);
|
||||
const rowId = repository.createInCircuit({
|
||||
circuitId: "circuit-1",
|
||||
name: "Steckdose",
|
||||
displayName: "Steckdose",
|
||||
quantity: 1,
|
||||
powerPerUnit: 0.2,
|
||||
simultaneityFactor: 1,
|
||||
});
|
||||
|
||||
const [row] = context.db
|
||||
.select()
|
||||
.from(circuitDeviceRows)
|
||||
.where(eq(circuitDeviceRows.id, rowId))
|
||||
.all();
|
||||
const [circuit] = context.db
|
||||
.select()
|
||||
.from(circuits)
|
||||
.where(eq(circuits.id, "circuit-1"))
|
||||
.all();
|
||||
|
||||
assert.equal(row.circuitId, "circuit-1");
|
||||
assert.equal(row.sortOrder, 10);
|
||||
assert.equal(circuit.isReserve, 0);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("rolls back the row insert when clearing the reserve status fails", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
context.sqlite.exec(`
|
||||
CREATE TRIGGER fail_reserve_clear
|
||||
BEFORE UPDATE OF is_reserve ON circuits
|
||||
WHEN NEW.is_reserve = 0
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'forced reserve clear failure');
|
||||
END;
|
||||
`);
|
||||
const repository = new CircuitDeviceRowTransactionRepository(context.db);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
repository.createInCircuit({
|
||||
circuitId: "circuit-1",
|
||||
name: "Steckdose",
|
||||
displayName: "Steckdose",
|
||||
quantity: 1,
|
||||
powerPerUnit: 0.2,
|
||||
simultaneityFactor: 1,
|
||||
}),
|
||||
/forced reserve clear failure/
|
||||
);
|
||||
|
||||
assert.equal(context.db.select().from(circuitDeviceRows).all().length, 0);
|
||||
assert.equal(context.db.select().from(circuits).all()[0].isReserve, 1);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("commits a new circuit and all initial device rows together", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
const [seedCircuit] = context.db.select().from(circuits).limit(1).all();
|
||||
const repository = new CircuitDeviceRowTransactionRepository(context.db);
|
||||
const created = repository.createCircuitWithDeviceRows({
|
||||
circuit: {
|
||||
circuitListId: seedCircuit.circuitListId,
|
||||
sectionId: seedCircuit.sectionId,
|
||||
equipmentIdentifier: "-1F2",
|
||||
displayName: "Beleuchtung",
|
||||
sortOrder: 20,
|
||||
},
|
||||
deviceRows: [
|
||||
{
|
||||
name: "Leuchte",
|
||||
displayName: "Leuchte 1",
|
||||
quantity: 1,
|
||||
powerPerUnit: 0.1,
|
||||
simultaneityFactor: 1,
|
||||
sortOrder: 15,
|
||||
},
|
||||
{
|
||||
name: "Leuchte",
|
||||
displayName: "Leuchte 2",
|
||||
quantity: 2,
|
||||
powerPerUnit: 0.1,
|
||||
simultaneityFactor: 0.8,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const [createdCircuit] = context.db
|
||||
.select()
|
||||
.from(circuits)
|
||||
.where(eq(circuits.id, created.circuitId))
|
||||
.all();
|
||||
const createdRows = context.db
|
||||
.select()
|
||||
.from(circuitDeviceRows)
|
||||
.where(eq(circuitDeviceRows.circuitId, created.circuitId))
|
||||
.all()
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder);
|
||||
|
||||
assert.equal(createdCircuit.equipmentIdentifier, "-1F2");
|
||||
assert.equal(createdCircuit.isReserve, 0);
|
||||
assert.deepEqual(
|
||||
createdRows.map((row) => [row.id, row.displayName, row.sortOrder]),
|
||||
[
|
||||
[created.rowIds[0], "Leuchte 1", 15],
|
||||
[created.rowIds[1], "Leuchte 2", 20],
|
||||
]
|
||||
);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("rolls back the circuit and earlier rows when an initial row insert fails", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
const [seedCircuit] = context.db.select().from(circuits).limit(1).all();
|
||||
context.sqlite.exec(`
|
||||
CREATE TRIGGER fail_initial_device_row
|
||||
BEFORE INSERT ON circuit_device_rows
|
||||
WHEN NEW.name = 'Fail'
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'forced initial row failure');
|
||||
END;
|
||||
`);
|
||||
const repository = new CircuitDeviceRowTransactionRepository(context.db);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
repository.createCircuitWithDeviceRows({
|
||||
circuit: {
|
||||
circuitListId: seedCircuit.circuitListId,
|
||||
sectionId: seedCircuit.sectionId,
|
||||
equipmentIdentifier: "-1F2",
|
||||
displayName: "Rollback",
|
||||
sortOrder: 20,
|
||||
},
|
||||
deviceRows: [
|
||||
{
|
||||
name: "First",
|
||||
displayName: "Erste Zeile",
|
||||
quantity: 1,
|
||||
powerPerUnit: 0.1,
|
||||
simultaneityFactor: 1,
|
||||
},
|
||||
{
|
||||
name: "Fail",
|
||||
displayName: "Fehlerhafte Zeile",
|
||||
quantity: 1,
|
||||
powerPerUnit: 0.1,
|
||||
simultaneityFactor: 1,
|
||||
},
|
||||
],
|
||||
}),
|
||||
/forced initial row failure/
|
||||
);
|
||||
|
||||
assert.equal(context.db.select().from(circuits).all().length, 1);
|
||||
assert.equal(context.db.select().from(circuitDeviceRows).all().length, 0);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("commits the last-row deletion and activates the circuit reserve status together", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
insertDeviceRow(context);
|
||||
const repository = new CircuitDeviceRowTransactionRepository(context.db);
|
||||
|
||||
repository.deleteFromCircuit("row-1", "circuit-1");
|
||||
|
||||
assert.equal(context.db.select().from(circuitDeviceRows).all().length, 0);
|
||||
assert.equal(context.db.select().from(circuits).all()[0].isReserve, 1);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("rolls back the row deletion when activating the reserve status fails", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
insertDeviceRow(context);
|
||||
context.sqlite.exec(`
|
||||
CREATE TRIGGER fail_reserve_activation
|
||||
BEFORE UPDATE OF is_reserve ON circuits
|
||||
WHEN NEW.is_reserve = 1
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'forced reserve activation failure');
|
||||
END;
|
||||
`);
|
||||
const repository = new CircuitDeviceRowTransactionRepository(context.db);
|
||||
|
||||
assert.throws(
|
||||
() => repository.deleteFromCircuit("row-1", "circuit-1"),
|
||||
/forced reserve activation failure/
|
||||
);
|
||||
|
||||
assert.equal(context.db.select().from(circuitDeviceRows).all().length, 1);
|
||||
assert.equal(context.db.select().from(circuits).all()[0].isReserve, 0);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("moves rows to an existing circuit and updates both reserve states together", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
insertCircuit(context, {
|
||||
id: "circuit-2",
|
||||
equipmentIdentifier: "-1F2",
|
||||
sortOrder: 20,
|
||||
});
|
||||
insertCircuit(context, {
|
||||
id: "circuit-3",
|
||||
equipmentIdentifier: "-1F3",
|
||||
sortOrder: 30,
|
||||
});
|
||||
insertDeviceRow(context);
|
||||
insertDeviceRow(context, {
|
||||
id: "row-2",
|
||||
circuitId: "circuit-2",
|
||||
sortOrder: 10,
|
||||
displayName: "Zweite Quellzeile",
|
||||
});
|
||||
insertDeviceRow(context, {
|
||||
id: "target-row",
|
||||
circuitId: "circuit-3",
|
||||
sortOrder: 10,
|
||||
displayName: "Bestehende Zielzeile",
|
||||
});
|
||||
const repository = new CircuitDeviceRowTransactionRepository(context.db);
|
||||
|
||||
const result = repository.moveRows({
|
||||
rows: [
|
||||
{ id: "row-1", expectedCircuitId: "circuit-1" },
|
||||
{ id: "row-2", expectedCircuitId: "circuit-2" },
|
||||
],
|
||||
targetCircuitId: "circuit-3",
|
||||
});
|
||||
|
||||
const movedRows = context.db
|
||||
.select()
|
||||
.from(circuitDeviceRows)
|
||||
.all()
|
||||
.filter((row) => row.id === "row-1" || row.id === "row-2")
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder);
|
||||
const persistedCircuits = context.db.select().from(circuits).all();
|
||||
const targetCircuit = persistedCircuits.find(
|
||||
(circuit) => circuit.id === "circuit-3"
|
||||
);
|
||||
|
||||
assert.deepEqual(result, {
|
||||
movedRowIds: ["row-1", "row-2"],
|
||||
targetCircuitId: "circuit-3",
|
||||
createdCircuitId: undefined,
|
||||
});
|
||||
assert.deepEqual(
|
||||
movedRows.map((row) => [row.id, row.circuitId, row.sortOrder]),
|
||||
[
|
||||
["row-1", "circuit-3", 20],
|
||||
["row-2", "circuit-3", 30],
|
||||
]
|
||||
);
|
||||
assert.equal(
|
||||
persistedCircuits.find((circuit) => circuit.id === "circuit-1")?.isReserve,
|
||||
1
|
||||
);
|
||||
assert.equal(
|
||||
persistedCircuits.find((circuit) => circuit.id === "circuit-2")?.isReserve,
|
||||
1
|
||||
);
|
||||
assert.equal(targetCircuit?.isReserve, 0);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("rolls back an existing-target move when a reserve update fails", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
insertCircuit(context, {
|
||||
id: "circuit-2",
|
||||
equipmentIdentifier: "-1F2",
|
||||
sortOrder: 20,
|
||||
});
|
||||
insertDeviceRow(context);
|
||||
context.sqlite.exec(`
|
||||
CREATE TRIGGER fail_source_reserve_update
|
||||
BEFORE UPDATE OF is_reserve ON circuits
|
||||
WHEN NEW.id = 'circuit-1' AND NEW.is_reserve = 1
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'forced source reserve failure');
|
||||
END;
|
||||
`);
|
||||
const repository = new CircuitDeviceRowTransactionRepository(context.db);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
repository.moveRows({
|
||||
rows: [{ id: "row-1", expectedCircuitId: "circuit-1" }],
|
||||
targetCircuitId: "circuit-2",
|
||||
}),
|
||||
/forced source reserve failure/
|
||||
);
|
||||
|
||||
const [row] = context.db.select().from(circuitDeviceRows).all();
|
||||
const persistedCircuits = context.db.select().from(circuits).all();
|
||||
assert.equal(row.circuitId, "circuit-1");
|
||||
assert.equal(
|
||||
persistedCircuits.find((circuit) => circuit.id === "circuit-1")?.isReserve,
|
||||
0
|
||||
);
|
||||
assert.equal(
|
||||
persistedCircuits.find((circuit) => circuit.id === "circuit-2")?.isReserve,
|
||||
1
|
||||
);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("creates a new target circuit and moves rows into it atomically", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
insertDeviceRow(context);
|
||||
const [sourceCircuit] = context.db
|
||||
.select()
|
||||
.from(circuits)
|
||||
.where(eq(circuits.id, "circuit-1"))
|
||||
.all();
|
||||
const repository = new CircuitDeviceRowTransactionRepository(context.db);
|
||||
|
||||
const result = repository.moveRows({
|
||||
rows: [{ id: "row-1", expectedCircuitId: "circuit-1" }],
|
||||
createTargetCircuit: {
|
||||
circuitListId: sourceCircuit.circuitListId,
|
||||
sectionId: sourceCircuit.sectionId,
|
||||
equipmentIdentifier: "-1F2",
|
||||
displayName: "Neues Ziel",
|
||||
sortOrder: 20,
|
||||
},
|
||||
});
|
||||
|
||||
const [movedRow] = context.db
|
||||
.select()
|
||||
.from(circuitDeviceRows)
|
||||
.where(eq(circuitDeviceRows.id, "row-1"))
|
||||
.all();
|
||||
const [createdCircuit] = context.db
|
||||
.select()
|
||||
.from(circuits)
|
||||
.where(eq(circuits.id, result.createdCircuitId!))
|
||||
.all();
|
||||
const [updatedSource] = context.db
|
||||
.select()
|
||||
.from(circuits)
|
||||
.where(eq(circuits.id, "circuit-1"))
|
||||
.all();
|
||||
|
||||
assert.equal(result.targetCircuitId, result.createdCircuitId);
|
||||
assert.equal(createdCircuit.equipmentIdentifier, "-1F2");
|
||||
assert.equal(createdCircuit.isReserve, 0);
|
||||
assert.equal(movedRow.circuitId, createdCircuit.id);
|
||||
assert.equal(updatedSource.isReserve, 1);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("rolls back a newly created target circuit when the move fails", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
insertDeviceRow(context);
|
||||
const [sourceCircuit] = context.db
|
||||
.select()
|
||||
.from(circuits)
|
||||
.where(eq(circuits.id, "circuit-1"))
|
||||
.all();
|
||||
context.sqlite.exec(`
|
||||
CREATE TRIGGER fail_new_target_source_reserve
|
||||
BEFORE UPDATE OF is_reserve ON circuits
|
||||
WHEN NEW.id = 'circuit-1' AND NEW.is_reserve = 1
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'forced new-target move failure');
|
||||
END;
|
||||
`);
|
||||
const repository = new CircuitDeviceRowTransactionRepository(context.db);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
repository.moveRows({
|
||||
rows: [{ id: "row-1", expectedCircuitId: "circuit-1" }],
|
||||
createTargetCircuit: {
|
||||
circuitListId: sourceCircuit.circuitListId,
|
||||
sectionId: sourceCircuit.sectionId,
|
||||
equipmentIdentifier: "-1F2",
|
||||
displayName: "Rollback-Ziel",
|
||||
sortOrder: 20,
|
||||
},
|
||||
}),
|
||||
/forced new-target move failure/
|
||||
);
|
||||
|
||||
const [row] = context.db.select().from(circuitDeviceRows).all();
|
||||
const persistedCircuits = context.db.select().from(circuits).all();
|
||||
assert.equal(row.circuitId, "circuit-1");
|
||||
assert.equal(persistedCircuits.length, 1);
|
||||
assert.equal(persistedCircuits[0].isReserve, 0);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -23,7 +23,10 @@ describe("circuit write service rules", () => {
|
||||
},
|
||||
} as never,
|
||||
circuitSectionTransactionStore: {
|
||||
updateEquipmentIdentifiers(_listId: string, updates: Array<{ id: string; equipmentIdentifier: string }>) {
|
||||
updateEquipmentIdentifiers(
|
||||
_listId: string,
|
||||
updates: Array<{ id: string; equipmentIdentifier: string }>
|
||||
) {
|
||||
safeUpdatePayload = updates;
|
||||
},
|
||||
} as never,
|
||||
@@ -58,7 +61,10 @@ describe("circuit write service rules", () => {
|
||||
},
|
||||
} as never,
|
||||
circuitSectionTransactionStore: {
|
||||
updateEquipmentIdentifiers(_listId: string, updates: Array<{ id: string; equipmentIdentifier: string }>) {
|
||||
updateEquipmentIdentifiers(
|
||||
_listId: string,
|
||||
updates: Array<{ id: string; equipmentIdentifier: string }>
|
||||
) {
|
||||
safeUpdatePayload = updates;
|
||||
},
|
||||
} as never,
|
||||
@@ -72,7 +78,7 @@ describe("circuit write service rules", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("renumber handles gaps and keeps device rows untouched by identifier-only update path", async () => {
|
||||
it("renumber handles gaps without touching device rows", async () => {
|
||||
let safeCalled = 0;
|
||||
const service = new CircuitWriteService({
|
||||
circuitSectionRepository: {
|
||||
@@ -97,292 +103,12 @@ describe("circuit write service rules", () => {
|
||||
safeCalled += 1;
|
||||
},
|
||||
} as never,
|
||||
deviceRowRepository: {
|
||||
async update() {
|
||||
throw new Error("device rows must not be touched");
|
||||
},
|
||||
} as never,
|
||||
});
|
||||
|
||||
await service.renumberSection("s1");
|
||||
assert.equal(safeCalled, 1);
|
||||
});
|
||||
|
||||
it("moving a device row to another circuit preserves row and toggles reserve flags", async () => {
|
||||
let transactionalMove:
|
||||
| {
|
||||
rows: Array<{ id: string; expectedCircuitId: string }>;
|
||||
targetCircuitId?: string;
|
||||
createTargetCircuit?: unknown;
|
||||
}
|
||||
| undefined;
|
||||
const service = new CircuitWriteService({
|
||||
deviceRowRepository: {
|
||||
async findById() {
|
||||
return { id: "r1", circuitId: "c1" } as never;
|
||||
},
|
||||
} as never,
|
||||
deviceRowTransactionStore: {
|
||||
moveRows(input: typeof transactionalMove) {
|
||||
transactionalMove = input;
|
||||
return {
|
||||
movedRowIds: input!.rows.map((row) => row.id),
|
||||
targetCircuitId: input!.targetCircuitId!,
|
||||
};
|
||||
},
|
||||
} as never,
|
||||
circuitRepository: {
|
||||
async findById(circuitId: string) {
|
||||
if (circuitId === "c1") {
|
||||
return {
|
||||
id: "c1",
|
||||
sectionId: "s1",
|
||||
circuitListId: "l1",
|
||||
equipmentIdentifier: "-1F1",
|
||||
sortOrder: 10,
|
||||
isReserve: 0,
|
||||
} as never;
|
||||
}
|
||||
return {
|
||||
id: "c2",
|
||||
sectionId: "s1",
|
||||
circuitListId: "l1",
|
||||
equipmentIdentifier: "-1F2",
|
||||
sortOrder: 20,
|
||||
isReserve: 1,
|
||||
} as never;
|
||||
},
|
||||
} as never,
|
||||
});
|
||||
|
||||
await service.moveDeviceRow("r1", { targetCircuitId: "c2" });
|
||||
assert.deepEqual(transactionalMove, {
|
||||
rows: [{ id: "r1", expectedCircuitId: "c1" }],
|
||||
targetCircuitId: "c2",
|
||||
createTargetCircuit: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("moving a device row to placeholder creates a new circuit in target section", async () => {
|
||||
let preparedTarget:
|
||||
| {
|
||||
circuitListId: string;
|
||||
sectionId: string;
|
||||
equipmentIdentifier: string;
|
||||
displayName: string;
|
||||
sortOrder: number;
|
||||
}
|
||||
| undefined;
|
||||
const service = new CircuitWriteService({
|
||||
deviceRowRepository: {
|
||||
async findById() {
|
||||
return { id: "r1", circuitId: "c1" } as never;
|
||||
},
|
||||
} as never,
|
||||
deviceRowTransactionStore: {
|
||||
moveRows(input: { rows: Array<{ id: string }>; createTargetCircuit?: typeof preparedTarget }) {
|
||||
preparedTarget = input.createTargetCircuit;
|
||||
return {
|
||||
movedRowIds: input.rows.map((row) => row.id),
|
||||
targetCircuitId: "c-new",
|
||||
createdCircuitId: "c-new",
|
||||
};
|
||||
},
|
||||
} as never,
|
||||
circuitRepository: {
|
||||
async findById(circuitId: string) {
|
||||
if (circuitId === "c1") {
|
||||
return {
|
||||
id: "c1",
|
||||
sectionId: "s1",
|
||||
circuitListId: "l1",
|
||||
equipmentIdentifier: "-1F1",
|
||||
sortOrder: 10,
|
||||
isReserve: 0,
|
||||
} as never;
|
||||
}
|
||||
return null as never;
|
||||
},
|
||||
async listBySection() {
|
||||
return [{ sortOrder: 40 }] as never[];
|
||||
},
|
||||
} as never,
|
||||
circuitSectionRepository: {
|
||||
async findById() {
|
||||
return { id: "s2", circuitListId: "l1", prefix: "-2F" } as never;
|
||||
},
|
||||
} as never,
|
||||
numberingService: {
|
||||
async getNextIdentifier() {
|
||||
return "-2F8";
|
||||
},
|
||||
} as never,
|
||||
});
|
||||
|
||||
await service.moveDeviceRow("r1", { targetSectionId: "s2", createNewCircuit: true });
|
||||
assert.deepEqual(preparedTarget, {
|
||||
circuitListId: "l1",
|
||||
sectionId: "s2",
|
||||
equipmentIdentifier: "-2F8",
|
||||
displayName: "Neuer Stromkreis",
|
||||
sortOrder: 50,
|
||||
});
|
||||
});
|
||||
|
||||
it("moving a device row to its current circuit remains a no-op", async () => {
|
||||
let transactionalMoveCount = 0;
|
||||
const service = new CircuitWriteService({
|
||||
deviceRowRepository: {
|
||||
async findById() {
|
||||
return { id: "r1", circuitId: "c1" } as never;
|
||||
},
|
||||
} as never,
|
||||
deviceRowTransactionStore: {
|
||||
moveRows() {
|
||||
transactionalMoveCount += 1;
|
||||
throw new Error("same-circuit move must not write");
|
||||
},
|
||||
} as never,
|
||||
circuitRepository: {
|
||||
async findById() {
|
||||
return {
|
||||
id: "c1",
|
||||
sectionId: "s1",
|
||||
circuitListId: "l1",
|
||||
equipmentIdentifier: "-1F1",
|
||||
sortOrder: 10,
|
||||
isReserve: 0,
|
||||
} as never;
|
||||
},
|
||||
} as never,
|
||||
});
|
||||
|
||||
const result = await service.moveDeviceRow("r1", { targetCircuitId: "c1" });
|
||||
assert.equal(transactionalMoveCount, 0);
|
||||
assert.equal(result?.id, "r1");
|
||||
});
|
||||
|
||||
it("moving multiple device rows to a circuit preserves input order and toggles reserve", async () => {
|
||||
let transactionalMove:
|
||||
| {
|
||||
rows: Array<{ id: string; expectedCircuitId: string }>;
|
||||
targetCircuitId?: string;
|
||||
createTargetCircuit?: unknown;
|
||||
}
|
||||
| undefined;
|
||||
const service = new CircuitWriteService({
|
||||
deviceRowRepository: {
|
||||
async findById(rowId: string) {
|
||||
if (rowId === "r1") {
|
||||
return { id: "r1", circuitId: "c1" } as never;
|
||||
}
|
||||
return { id: "r2", circuitId: "c2" } as never;
|
||||
},
|
||||
} as never,
|
||||
deviceRowTransactionStore: {
|
||||
moveRows(input: typeof transactionalMove) {
|
||||
transactionalMove = input;
|
||||
return {
|
||||
movedRowIds: input!.rows.map((row) => row.id),
|
||||
targetCircuitId: input!.targetCircuitId!,
|
||||
};
|
||||
},
|
||||
} as never,
|
||||
circuitRepository: {
|
||||
async findById(circuitId: string) {
|
||||
if (circuitId === "c1") {
|
||||
return { id: "c1", sectionId: "s1", circuitListId: "l1", equipmentIdentifier: "-1F1", sortOrder: 10, isReserve: 0 } as never;
|
||||
}
|
||||
if (circuitId === "c2") {
|
||||
return { id: "c2", sectionId: "s1", circuitListId: "l1", equipmentIdentifier: "-1F2", sortOrder: 20, isReserve: 0 } as never;
|
||||
}
|
||||
return { id: "c3", sectionId: "s1", circuitListId: "l1", equipmentIdentifier: "-1F3", sortOrder: 30, isReserve: 1 } as never;
|
||||
},
|
||||
} as never,
|
||||
});
|
||||
|
||||
const result = await service.moveDeviceRowsBulk({ rowIds: ["r1", "r2"], targetCircuitId: "c3" });
|
||||
assert.deepEqual(transactionalMove, {
|
||||
rows: [
|
||||
{ id: "r1", expectedCircuitId: "c1" },
|
||||
{ id: "r2", expectedCircuitId: "c2" },
|
||||
],
|
||||
targetCircuitId: "c3",
|
||||
createTargetCircuit: undefined,
|
||||
});
|
||||
assert.deepEqual(result, {
|
||||
movedRowIds: ["r1", "r2"],
|
||||
targetCircuitId: "c3",
|
||||
});
|
||||
});
|
||||
|
||||
it("moving multiple device rows to placeholder creates exactly one new circuit", async () => {
|
||||
let transactionCount = 0;
|
||||
let preparedTarget:
|
||||
| {
|
||||
circuitListId: string;
|
||||
sectionId: string;
|
||||
equipmentIdentifier: string;
|
||||
displayName: string;
|
||||
sortOrder: number;
|
||||
}
|
||||
| undefined;
|
||||
const service = new CircuitWriteService({
|
||||
deviceRowRepository: {
|
||||
async findById(rowId: string) {
|
||||
return { id: rowId, circuitId: rowId === "r1" ? "c1" : "c2" } as never;
|
||||
},
|
||||
} as never,
|
||||
deviceRowTransactionStore: {
|
||||
moveRows(input: { rows: Array<{ id: string }>; createTargetCircuit?: typeof preparedTarget }) {
|
||||
transactionCount += 1;
|
||||
preparedTarget = input.createTargetCircuit;
|
||||
return {
|
||||
movedRowIds: input.rows.map((row) => row.id),
|
||||
targetCircuitId: "c-new",
|
||||
createdCircuitId: "c-new",
|
||||
};
|
||||
},
|
||||
} as never,
|
||||
circuitRepository: {
|
||||
async findById(circuitId: string) {
|
||||
if (circuitId === "c1" || circuitId === "c2") {
|
||||
return { id: circuitId, sectionId: "s1", circuitListId: "l1", equipmentIdentifier: "-1F1", sortOrder: 10, isReserve: 0 } as never;
|
||||
}
|
||||
return null as never;
|
||||
},
|
||||
async listBySection() {
|
||||
return [{ sortOrder: 30 }] as never[];
|
||||
},
|
||||
} as never,
|
||||
circuitSectionRepository: {
|
||||
async findById() {
|
||||
return { id: "s2", circuitListId: "l1", prefix: "-2F" } as never;
|
||||
},
|
||||
} as never,
|
||||
numberingService: {
|
||||
async getNextIdentifier() {
|
||||
return "-2F8";
|
||||
},
|
||||
} as never,
|
||||
});
|
||||
|
||||
const result = await service.moveDeviceRowsBulk({
|
||||
rowIds: ["r1", "r2"],
|
||||
targetSectionId: "s2",
|
||||
createNewCircuit: true,
|
||||
});
|
||||
assert.equal(transactionCount, 1);
|
||||
assert.deepEqual(preparedTarget, {
|
||||
circuitListId: "l1",
|
||||
sectionId: "s2",
|
||||
equipmentIdentifier: "-2F8",
|
||||
displayName: "Neuer Stromkreis",
|
||||
sortOrder: 40,
|
||||
});
|
||||
assert.equal(result.createdCircuitId, "c-new");
|
||||
});
|
||||
|
||||
it("reorders circuits inside one section without renumbering identifiers", async () => {
|
||||
let safeReorder: { sectionId: string; circuitIds: string[] } | undefined;
|
||||
const service = new CircuitWriteService({
|
||||
@@ -407,14 +133,16 @@ describe("circuit write service rules", () => {
|
||||
} as never,
|
||||
});
|
||||
|
||||
await service.reorderCircuitsInSection("s1", { orderedCircuitIds: ["c3", "c1", "c2"] });
|
||||
await service.reorderCircuitsInSection("s1", {
|
||||
orderedCircuitIds: ["c3", "c1", "c2"],
|
||||
});
|
||||
assert.deepEqual(safeReorder, {
|
||||
sectionId: "s1",
|
||||
circuitIds: ["c3", "c1", "c2"],
|
||||
});
|
||||
});
|
||||
|
||||
it("safe section identifier bulk update validates full section set (undo safety)", async () => {
|
||||
it("validates the complete section set before safe identifier updates", async () => {
|
||||
let safeCalled = false;
|
||||
const service = new CircuitWriteService({
|
||||
circuitSectionRepository: {
|
||||
@@ -436,6 +164,7 @@ describe("circuit write service rules", () => {
|
||||
},
|
||||
} as never,
|
||||
});
|
||||
|
||||
await service.updateSectionEquipmentIdentifiers("s1", {
|
||||
identifiers: [
|
||||
{ circuitId: "c1", equipmentIdentifier: "-1F2" },
|
||||
@@ -444,5 +173,4 @@ describe("circuit write service rules", () => {
|
||||
});
|
||||
assert.equal(safeCalled, true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user