Add device row history commands

This commit is contained in:
2026-07-23 22:09:53 +02:00
parent e4c7cf06e9
commit bd5f4f925f
14 changed files with 1109 additions and 13 deletions
@@ -0,0 +1,262 @@
import { and, eq } from "drizzle-orm";
import {
assertCircuitDeviceRowDeleteProjectCommand,
assertCircuitDeviceRowInsertProjectCommand,
circuitDeviceRowDeleteCommandType,
circuitDeviceRowInsertCommandType,
createCircuitDeviceRowDeleteProjectCommand,
createCircuitDeviceRowInsertProjectCommand,
type CircuitDeviceRowSnapshot,
type CircuitDeviceRowStructureProjectCommand,
} from "../../domain/models/circuit-device-row-structure-project-command.model.js";
import type {
CircuitDeviceRowStructureProjectCommandStore,
ExecuteCircuitDeviceRowStructureCommandInput,
} from "../../domain/ports/circuit-device-row-structure-project-command.store.js";
import type { AppDatabase } from "../database-context.js";
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
import { circuitLists } from "../schema/circuit-lists.js";
import { circuits } from "../schema/circuits.js";
import { projectDevices } from "../schema/project-devices.js";
import { rooms } from "../schema/rooms.js";
import { applyProjectHistoryTransition } from "./project-history.persistence.js";
import { appendProjectRevision } from "./project-revision.persistence.js";
export class CircuitDeviceRowStructureProjectCommandRepository
implements CircuitDeviceRowStructureProjectCommandStore
{
constructor(private readonly database: AppDatabase) {}
execute(input: ExecuteCircuitDeviceRowStructureCommandInput) {
return this.database.transaction((tx) => {
const inverse = this.applyCommand(
tx,
input.projectId,
input.source,
input.command
);
const revision = appendProjectRevision(tx, {
projectId: input.projectId,
expectedRevision: input.expectedRevision,
source: input.source,
description: input.description,
actorId: input.actorId,
forward: input.command,
inverse,
});
applyProjectHistoryTransition(tx, {
projectId: input.projectId,
source: input.source,
recordedChangeSetId: revision.changeSetId,
targetChangeSetId: input.historyTargetChangeSetId,
});
return { revision, inverse };
});
}
private applyCommand(
database: AppDatabase,
projectId: string,
source: ExecuteCircuitDeviceRowStructureCommandInput["source"],
command: CircuitDeviceRowStructureProjectCommand
): CircuitDeviceRowStructureProjectCommand {
if (command.type === circuitDeviceRowInsertCommandType) {
assertCircuitDeviceRowInsertProjectCommand(command);
return this.insert(
database,
projectId,
source,
command.payload.row
);
}
if (command.type === circuitDeviceRowDeleteCommandType) {
assertCircuitDeviceRowDeleteProjectCommand(command);
return this.delete(
database,
projectId,
command.payload.rowId,
command.payload.expectedCircuitId
);
}
throw new Error("Unsupported circuit device-row structure command.");
}
private insert(
database: AppDatabase,
projectId: string,
source: ExecuteCircuitDeviceRowStructureCommandInput["source"],
row: CircuitDeviceRowSnapshot
) {
if (source === "user" && row.legacyConsumerId !== null) {
throw new Error(
"User commands cannot assign legacy consumer ids."
);
}
this.assertCircuitInProject(database, projectId, row.circuitId);
this.assertReferencesInProject(database, projectId, row);
const existing = database
.select({ id: circuitDeviceRows.id })
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.id, row.id))
.get();
if (existing) {
throw new Error("Circuit device-row id already exists.");
}
database.insert(circuitDeviceRows).values(row).run();
const circuitUpdate = database
.update(circuits)
.set({ isReserve: 0 })
.where(eq(circuits.id, row.circuitId))
.run();
if (circuitUpdate.changes !== 1) {
throw new Error("Circuit changed before device-row insertion.");
}
return createCircuitDeviceRowDeleteProjectCommand(
row.id,
row.circuitId
);
}
private delete(
database: AppDatabase,
projectId: string,
rowId: string,
expectedCircuitId: string
) {
const row = database
.select()
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.id, rowId))
.get();
if (!row || row.circuitId !== expectedCircuitId) {
throw new Error(
"Circuit device row changed before command execution."
);
}
this.assertCircuitInProject(database, projectId, row.circuitId);
const result = database
.delete(circuitDeviceRows)
.where(
and(
eq(circuitDeviceRows.id, rowId),
eq(circuitDeviceRows.circuitId, expectedCircuitId)
)
)
.run();
if (result.changes !== 1) {
throw new Error("Circuit device row could not be deleted.");
}
const remainingRow = database
.select({ id: circuitDeviceRows.id })
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.circuitId, expectedCircuitId))
.limit(1)
.get();
const circuitUpdate = database
.update(circuits)
.set({ isReserve: remainingRow ? 0 : 1 })
.where(eq(circuits.id, expectedCircuitId))
.run();
if (circuitUpdate.changes !== 1) {
throw new Error("Circuit changed before device-row deletion.");
}
return createCircuitDeviceRowInsertProjectCommand(
toCircuitDeviceRowSnapshot(row)
);
}
private assertCircuitInProject(
database: AppDatabase,
projectId: string,
circuitId: string
) {
const circuit = database
.select({ id: circuits.id })
.from(circuits)
.innerJoin(
circuitLists,
eq(circuitLists.id, circuits.circuitListId)
)
.where(
and(
eq(circuits.id, circuitId),
eq(circuitLists.projectId, projectId)
)
)
.get();
if (!circuit) {
throw new Error("Circuit does not belong to project.");
}
}
private assertReferencesInProject(
database: AppDatabase,
projectId: string,
row: CircuitDeviceRowSnapshot
) {
if (row.linkedProjectDeviceId !== null) {
const device = database
.select({ id: projectDevices.id })
.from(projectDevices)
.where(
and(
eq(projectDevices.id, row.linkedProjectDeviceId),
eq(projectDevices.projectId, projectId)
)
)
.get();
if (!device) {
throw new Error("Invalid linked project device id.");
}
}
if (row.roomId !== null) {
const room = database
.select({ id: rooms.id })
.from(rooms)
.where(
and(
eq(rooms.id, row.roomId),
eq(rooms.projectId, projectId)
)
)
.get();
if (!room) {
throw new Error("Invalid room id.");
}
}
}
}
function toCircuitDeviceRowSnapshot(
row: typeof circuitDeviceRows.$inferSelect
): CircuitDeviceRowSnapshot {
return {
id: row.id,
circuitId: row.circuitId,
linkedProjectDeviceId: row.linkedProjectDeviceId,
legacyConsumerId: row.legacyConsumerId,
sortOrder: row.sortOrder,
name: row.name,
displayName: row.displayName,
phaseType: row.phaseType,
connectionKind: row.connectionKind,
costGroup: row.costGroup,
category: row.category,
level: row.level,
roomId: row.roomId,
roomNumberSnapshot: row.roomNumberSnapshot,
roomNameSnapshot: row.roomNameSnapshot,
quantity: row.quantity,
powerPerUnit: row.powerPerUnit,
simultaneityFactor: row.simultaneityFactor,
cosPhi: row.cosPhi,
remark: row.remark,
overriddenFields: row.overriddenFields,
};
}