Persist circuit structure edits
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
buildCircuitDeviceRowInsertSnapshot,
|
||||
buildCircuitInsertSnapshot,
|
||||
} from "../src/frontend/utils/circuit-structure-command.js";
|
||||
import {
|
||||
deleteCircuitCommand,
|
||||
deleteCircuitDeviceRowCommand,
|
||||
insertCircuitCommand,
|
||||
insertCircuitDeviceRowCommand,
|
||||
} from "../src/frontend/utils/api.js";
|
||||
|
||||
describe("circuit structure command adapters", () => {
|
||||
it("builds a complete row snapshot without legacy identities", () => {
|
||||
const row = buildCircuitDeviceRowInsertSnapshot({
|
||||
id: "row-1",
|
||||
circuitId: "circuit-1",
|
||||
defaultSortOrder: 20,
|
||||
values: {
|
||||
linkedProjectDeviceId: "device-1",
|
||||
name: "Steckdose",
|
||||
displayName: "Steckdose Büro",
|
||||
quantity: 2,
|
||||
powerPerUnit: 0.4,
|
||||
simultaneityFactor: 0.8,
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(row, {
|
||||
id: "row-1",
|
||||
circuitId: "circuit-1",
|
||||
linkedProjectDeviceId: "device-1",
|
||||
legacyConsumerId: null,
|
||||
sortOrder: 20,
|
||||
name: "Steckdose",
|
||||
displayName: "Steckdose Büro",
|
||||
phaseType: null,
|
||||
connectionKind: null,
|
||||
costGroup: null,
|
||||
category: null,
|
||||
level: null,
|
||||
roomId: null,
|
||||
roomNumberSnapshot: null,
|
||||
roomNameSnapshot: null,
|
||||
quantity: 2,
|
||||
powerPerUnit: 0.4,
|
||||
simultaneityFactor: 0.8,
|
||||
cosPhi: null,
|
||||
remark: null,
|
||||
overriddenFields: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("derives reserve state from the complete device-row snapshot", () => {
|
||||
const reserve = buildCircuitInsertSnapshot({
|
||||
id: "circuit-1",
|
||||
circuitListId: "list-1",
|
||||
values: {
|
||||
sectionId: "section-1",
|
||||
equipmentIdentifier: "-2F1",
|
||||
sortOrder: 10,
|
||||
isReserve: false,
|
||||
voltage: 230,
|
||||
controlRequirement: "DALI",
|
||||
},
|
||||
deviceRows: [],
|
||||
});
|
||||
|
||||
assert.equal(reserve.isReserve, true);
|
||||
assert.equal(reserve.displayName, null);
|
||||
assert.equal(reserve.voltage, 230);
|
||||
assert.equal(reserve.controlRequirement, "DALI");
|
||||
|
||||
const row = buildCircuitDeviceRowInsertSnapshot({
|
||||
id: "row-1",
|
||||
circuitId: reserve.id,
|
||||
defaultSortOrder: 10,
|
||||
values: {
|
||||
name: "Gerät",
|
||||
displayName: "Gerät",
|
||||
quantity: 1,
|
||||
powerPerUnit: 0,
|
||||
simultaneityFactor: 1,
|
||||
cosPhi: 1,
|
||||
},
|
||||
});
|
||||
const populated = buildCircuitInsertSnapshot({
|
||||
id: reserve.id,
|
||||
circuitListId: reserve.circuitListId,
|
||||
values: {
|
||||
sectionId: reserve.sectionId,
|
||||
equipmentIdentifier: reserve.equipmentIdentifier,
|
||||
sortOrder: reserve.sortOrder,
|
||||
isReserve: true,
|
||||
},
|
||||
deviceRows: [row],
|
||||
});
|
||||
|
||||
assert.equal(populated.isReserve, false);
|
||||
assert.deepEqual(populated.deviceRows, [row]);
|
||||
});
|
||||
|
||||
it("sends structure envelopes through the project command route", async () => {
|
||||
const requests: Array<{ url: string; body: Record<string, unknown> }> = [];
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (input, init) => {
|
||||
requests.push({
|
||||
url: String(input),
|
||||
body: JSON.parse(String(init?.body)) as Record<string, unknown>,
|
||||
});
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
revision: {
|
||||
revisionId: "revision-1",
|
||||
changeSetId: "change-1",
|
||||
projectId: "project-1",
|
||||
revisionNumber: requests.length,
|
||||
createdAtIso: "2026-07-25T00:00:00.000Z",
|
||||
},
|
||||
history: {
|
||||
projectId: "project-1",
|
||||
currentRevision: requests.length,
|
||||
undoDepth: requests.length,
|
||||
redoDepth: 0,
|
||||
undoChangeSetId: "change-1",
|
||||
redoChangeSetId: null,
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
const row = buildCircuitDeviceRowInsertSnapshot({
|
||||
id: "row-1",
|
||||
circuitId: "circuit-1",
|
||||
defaultSortOrder: 10,
|
||||
values: {
|
||||
name: "Gerät",
|
||||
displayName: "Gerät",
|
||||
quantity: 1,
|
||||
powerPerUnit: 0,
|
||||
simultaneityFactor: 1,
|
||||
},
|
||||
});
|
||||
const circuit = buildCircuitInsertSnapshot({
|
||||
id: "circuit-1",
|
||||
circuitListId: "list-1",
|
||||
values: {
|
||||
sectionId: "section-1",
|
||||
equipmentIdentifier: "-2F1",
|
||||
sortOrder: 10,
|
||||
},
|
||||
deviceRows: [row],
|
||||
});
|
||||
|
||||
await insertCircuitCommand("project-1", 0, circuit, "insert circuit");
|
||||
await deleteCircuitCommand(
|
||||
"project-1",
|
||||
1,
|
||||
circuit.id,
|
||||
circuit.circuitListId,
|
||||
"delete circuit"
|
||||
);
|
||||
await insertCircuitDeviceRowCommand(
|
||||
"project-1",
|
||||
2,
|
||||
row,
|
||||
"insert row"
|
||||
);
|
||||
await deleteCircuitDeviceRowCommand(
|
||||
"project-1",
|
||||
3,
|
||||
row.id,
|
||||
row.circuitId,
|
||||
"delete row"
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
assert.deepEqual(
|
||||
requests.map((request) => request.url),
|
||||
Array(4).fill("/api/projects/project-1/commands")
|
||||
);
|
||||
assert.deepEqual(
|
||||
requests.map((request) => {
|
||||
const command = request.body.command as {
|
||||
schemaVersion: number;
|
||||
type: string;
|
||||
};
|
||||
return [
|
||||
request.body.expectedRevision,
|
||||
command.schemaVersion,
|
||||
command.type,
|
||||
];
|
||||
}),
|
||||
[
|
||||
[0, 1, "circuit.insert"],
|
||||
[1, 1, "circuit.delete"],
|
||||
[2, 1, "circuit-device-row.insert"],
|
||||
[3, 1, "circuit-device-row.delete"],
|
||||
]
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,265 +1,8 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import { CircuitWriteService } from "../src/domain/services/circuit-write.service.js";
|
||||
import { createCircuitSchema } from "../src/shared/validation/circuit.schemas.js";
|
||||
|
||||
describe("circuit write service rules", () => {
|
||||
it("accepts future sizing inputs without calculating sizing suggestions", () => {
|
||||
const parsed = createCircuitSchema.safeParse({
|
||||
sectionId: "s1",
|
||||
equipmentIdentifier: "-1F1",
|
||||
sortOrder: 10,
|
||||
voltage: 230,
|
||||
controlRequirement: "DALI",
|
||||
});
|
||||
|
||||
assert.equal(parsed.success, true);
|
||||
});
|
||||
|
||||
it("rejects duplicate equipment identifiers in same circuit list", async () => {
|
||||
const service = new CircuitWriteService({
|
||||
circuitListRepository: {
|
||||
async findById() {
|
||||
return { id: "list1", projectId: "p1" } as never;
|
||||
},
|
||||
} as never,
|
||||
circuitSectionRepository: {
|
||||
async findById() {
|
||||
return { id: "sec1", circuitListId: "list1" } as never;
|
||||
},
|
||||
} as never,
|
||||
circuitRepository: {
|
||||
async existsByEquipmentIdentifier() {
|
||||
return true;
|
||||
},
|
||||
} as never,
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
service.createCircuit("p1", "list1", {
|
||||
sectionId: "sec1",
|
||||
equipmentIdentifier: "-2F1",
|
||||
sortOrder: 10,
|
||||
}),
|
||||
/Duplicate equipmentIdentifier/
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects section and list mismatch", async () => {
|
||||
const service = new CircuitWriteService({
|
||||
circuitListRepository: {
|
||||
async findById() {
|
||||
return { id: "list1", projectId: "p1" } as never;
|
||||
},
|
||||
} as never,
|
||||
circuitSectionRepository: {
|
||||
async findById() {
|
||||
return { id: "sec1", circuitListId: "other" } as never;
|
||||
},
|
||||
} as never,
|
||||
circuitRepository: {
|
||||
async existsByEquipmentIdentifier() {
|
||||
return false;
|
||||
},
|
||||
} as never,
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
service.createCircuit("p1", "list1", {
|
||||
sectionId: "sec1",
|
||||
equipmentIdentifier: "-2F1",
|
||||
sortOrder: 10,
|
||||
}),
|
||||
/Section does not belong to circuit list/
|
||||
);
|
||||
});
|
||||
|
||||
it("deleting last device row keeps circuit and sets reserve", async () => {
|
||||
let transactionalDelete: { rowId: string; circuitId: string } | undefined;
|
||||
const service = new CircuitWriteService({
|
||||
deviceRowRepository: {
|
||||
async findById() {
|
||||
return { id: "r1", circuitId: "c1" } as never;
|
||||
},
|
||||
} as never,
|
||||
deviceRowTransactionStore: {
|
||||
deleteFromCircuit(rowId: string, circuitId: string) {
|
||||
transactionalDelete = { rowId, circuitId };
|
||||
},
|
||||
} as never,
|
||||
circuitRepository: {
|
||||
async findById() {
|
||||
return {
|
||||
id: "c1",
|
||||
sectionId: "s1",
|
||||
circuitListId: "l1",
|
||||
equipmentIdentifier: "-2F1",
|
||||
sortOrder: 10,
|
||||
isReserve: 0,
|
||||
} as never;
|
||||
},
|
||||
} as never,
|
||||
});
|
||||
|
||||
await service.deleteDeviceRow("r1");
|
||||
assert.deepEqual(transactionalDelete, { rowId: "r1", circuitId: "c1" });
|
||||
});
|
||||
|
||||
it("creating device row in reserve circuit clears reserve status", async () => {
|
||||
let transactionalCreate:
|
||||
| {
|
||||
circuitId: string;
|
||||
sortOrder?: number;
|
||||
name: string;
|
||||
displayName: string;
|
||||
}
|
||||
| undefined;
|
||||
const service = new CircuitWriteService({
|
||||
circuitRepository: {
|
||||
async findById() {
|
||||
return {
|
||||
id: "c1",
|
||||
sectionId: "s1",
|
||||
circuitListId: "l1",
|
||||
equipmentIdentifier: "-2F1",
|
||||
sortOrder: 10,
|
||||
isReserve: 1,
|
||||
} as never;
|
||||
},
|
||||
} as never,
|
||||
deviceRowRepository: {
|
||||
async findById() {
|
||||
return { id: "row1" } as never;
|
||||
},
|
||||
} as never,
|
||||
deviceRowTransactionStore: {
|
||||
createInCircuit(input: typeof transactionalCreate) {
|
||||
transactionalCreate = input;
|
||||
return "row1";
|
||||
},
|
||||
} as never,
|
||||
circuitListRepository: {} as never,
|
||||
circuitSectionRepository: {} as never,
|
||||
projectDeviceRepository: {
|
||||
async findById() {
|
||||
return { id: "pd1" } as never;
|
||||
},
|
||||
} as never,
|
||||
});
|
||||
|
||||
await service.createDeviceRow("c1", {
|
||||
name: "Load",
|
||||
displayName: "Load",
|
||||
quantity: 1,
|
||||
powerPerUnit: 1,
|
||||
simultaneityFactor: 1,
|
||||
});
|
||||
assert.deepEqual(transactionalCreate, {
|
||||
circuitId: "c1",
|
||||
linkedProjectDeviceId: undefined,
|
||||
sortOrder: undefined,
|
||||
name: "Load",
|
||||
displayName: "Load",
|
||||
phaseType: undefined,
|
||||
connectionKind: undefined,
|
||||
costGroup: undefined,
|
||||
category: undefined,
|
||||
level: undefined,
|
||||
roomId: undefined,
|
||||
roomNumberSnapshot: undefined,
|
||||
roomNameSnapshot: undefined,
|
||||
quantity: 1,
|
||||
powerPerUnit: 1,
|
||||
simultaneityFactor: 1,
|
||||
cosPhi: undefined,
|
||||
remark: undefined,
|
||||
overriddenFields: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("creates a circuit and all first device rows through one repository command", async () => {
|
||||
let transactionalCreate: {
|
||||
circuit: { circuitListId: string; sectionId: string; equipmentIdentifier: string };
|
||||
deviceRows: Array<{ linkedProjectDeviceId?: string; name: string }>;
|
||||
} | undefined;
|
||||
const service = new CircuitWriteService({
|
||||
circuitListRepository: {
|
||||
async findById() {
|
||||
return { id: "l1", projectId: "p1" } as never;
|
||||
},
|
||||
} as never,
|
||||
circuitSectionRepository: {
|
||||
async findById() {
|
||||
return { id: "s1", circuitListId: "l1" } as never;
|
||||
},
|
||||
} as never,
|
||||
circuitRepository: {
|
||||
async existsByEquipmentIdentifier() {
|
||||
return false;
|
||||
},
|
||||
async findById() {
|
||||
return { id: "c-new", circuitListId: "l1", sectionId: "s1" } as never;
|
||||
},
|
||||
} as never,
|
||||
projectDeviceRepository: {
|
||||
async findById(projectId: string, deviceId: string) {
|
||||
return projectId === "p1" && deviceId === "pd1" ? ({ id: "pd1" } as never) : null;
|
||||
},
|
||||
} as never,
|
||||
deviceRowRepository: {
|
||||
async findById(rowId: string) {
|
||||
return { id: rowId, circuitId: "c-new" } as never;
|
||||
},
|
||||
} as never,
|
||||
deviceRowTransactionStore: {
|
||||
createCircuitWithDeviceRows(input: typeof transactionalCreate) {
|
||||
transactionalCreate = input;
|
||||
return { circuitId: "c-new", rowIds: ["r1", "r2"] };
|
||||
},
|
||||
} as never,
|
||||
});
|
||||
|
||||
const created = await service.createCircuitWithDeviceRows("p1", "l1", {
|
||||
circuit: {
|
||||
sectionId: "s1",
|
||||
equipmentIdentifier: "-2F1",
|
||||
displayName: "Steckdosen",
|
||||
sortOrder: 10,
|
||||
},
|
||||
deviceRows: [
|
||||
{
|
||||
linkedProjectDeviceId: "pd1",
|
||||
name: "Socket",
|
||||
displayName: "Steckdose",
|
||||
quantity: 1,
|
||||
powerPerUnit: 0.2,
|
||||
simultaneityFactor: 1,
|
||||
},
|
||||
{
|
||||
name: "Manual",
|
||||
displayName: "Manuell",
|
||||
quantity: 2,
|
||||
powerPerUnit: 0.1,
|
||||
simultaneityFactor: 0.8,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(transactionalCreate?.circuit.circuitListId, "l1");
|
||||
assert.equal(transactionalCreate?.circuit.sectionId, "s1");
|
||||
assert.equal(transactionalCreate?.circuit.equipmentIdentifier, "-2F1");
|
||||
assert.deepEqual(
|
||||
transactionalCreate?.deviceRows.map((row) => row.name),
|
||||
["Socket", "Manual"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
created.deviceRows.map((row) => row?.id),
|
||||
["r1", "r2"]
|
||||
);
|
||||
});
|
||||
|
||||
it("renumber uses safe bulk identifier update for swapped identifiers", async () => {
|
||||
let safeUpdatePayload: Array<{ id: string; equipmentIdentifier: string }> = [];
|
||||
const service = new CircuitWriteService({
|
||||
|
||||
Reference in New Issue
Block a user