Add circuit history commands

This commit is contained in:
2026-07-23 22:18:36 +02:00
parent bd5f4f925f
commit ca13efbfcb
16 changed files with 1402 additions and 85 deletions
@@ -0,0 +1,220 @@
import {
assertCircuitDeviceRowInsertProjectCommand,
circuitDeviceRowInsertCommandType,
circuitDeviceRowStructureCommandSchemaVersion,
type CircuitDeviceRowSnapshot,
} from "./circuit-device-row-structure-project-command.model.js";
import type { SerializedProjectCommand } from "./project-command.model.js";
export const circuitInsertCommandType = "circuit.insert" as const;
export const circuitDeleteCommandType = "circuit.delete" as const;
export const circuitStructureCommandSchemaVersion = 1 as const;
export interface CircuitSnapshot {
id: string;
circuitListId: string;
sectionId: string;
equipmentIdentifier: string;
displayName: string | null;
sortOrder: number;
protectionType: string | null;
protectionRatedCurrent: number | null;
protectionCharacteristic: string | null;
cableType: string | null;
cableCrossSection: string | null;
cableLength: number | null;
rcdAssignment: string | null;
terminalDesignation: string | null;
voltage: number | null;
controlRequirement: string | null;
status: string | null;
isReserve: boolean;
remark: string | null;
deviceRows: CircuitDeviceRowSnapshot[];
}
export interface CircuitInsertCommandPayload {
circuit: CircuitSnapshot;
}
export interface CircuitDeleteCommandPayload {
circuitId: string;
expectedCircuitListId: string;
}
export interface CircuitInsertProjectCommand
extends SerializedProjectCommand<CircuitInsertCommandPayload> {
schemaVersion: typeof circuitStructureCommandSchemaVersion;
type: typeof circuitInsertCommandType;
}
export interface CircuitDeleteProjectCommand
extends SerializedProjectCommand<CircuitDeleteCommandPayload> {
schemaVersion: typeof circuitStructureCommandSchemaVersion;
type: typeof circuitDeleteCommandType;
}
export type CircuitStructureProjectCommand =
| CircuitInsertProjectCommand
| CircuitDeleteProjectCommand;
export function createCircuitInsertProjectCommand(
circuit: CircuitSnapshot
): CircuitInsertProjectCommand {
const command: CircuitInsertProjectCommand = {
schemaVersion: circuitStructureCommandSchemaVersion,
type: circuitInsertCommandType,
payload: { circuit },
};
assertCircuitInsertProjectCommand(command);
return command;
}
export function createCircuitDeleteProjectCommand(
circuitId: string,
expectedCircuitListId: string
): CircuitDeleteProjectCommand {
const command: CircuitDeleteProjectCommand = {
schemaVersion: circuitStructureCommandSchemaVersion,
type: circuitDeleteCommandType,
payload: { circuitId, expectedCircuitListId },
};
assertCircuitDeleteProjectCommand(command);
return command;
}
export function assertCircuitInsertProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is CircuitInsertProjectCommand {
if (
command.schemaVersion !== circuitStructureCommandSchemaVersion ||
command.type !== circuitInsertCommandType
) {
throw new Error("Unsupported circuit insert command.");
}
if (!isPlainObject(command.payload)) {
throw new Error("Circuit insert payload must be an object.");
}
const circuit = command.payload.circuit;
if (!isPlainObject(circuit)) {
throw new Error("Circuit insert command requires a circuit.");
}
assertNonEmptyString(circuit.id, "circuit.id");
assertNonEmptyString(circuit.circuitListId, "circuit.circuitListId");
assertNonEmptyString(circuit.sectionId, "circuit.sectionId");
assertNonEmptyString(
circuit.equipmentIdentifier,
"circuit.equipmentIdentifier"
);
assertNullableString(circuit.displayName, "circuit.displayName");
assertFiniteNumber(circuit.sortOrder, "circuit.sortOrder");
for (const field of [
"protectionType",
"protectionCharacteristic",
"cableType",
"cableCrossSection",
"rcdAssignment",
"terminalDesignation",
"controlRequirement",
"status",
"remark",
] as const) {
assertNullableString(circuit[field], `circuit.${field}`);
}
assertNullableNonNegativeNumber(
circuit.protectionRatedCurrent,
"circuit.protectionRatedCurrent"
);
assertNullableNonNegativeNumber(
circuit.cableLength,
"circuit.cableLength"
);
if (circuit.voltage !== null) {
assertFiniteNumber(circuit.voltage, "circuit.voltage");
if (circuit.voltage <= 0) {
throw new Error("circuit.voltage must be positive or null.");
}
}
if (typeof circuit.isReserve !== "boolean") {
throw new Error("circuit.isReserve must be a boolean.");
}
if (!Array.isArray(circuit.deviceRows)) {
throw new Error("circuit.deviceRows must be an array.");
}
if (circuit.isReserve !== (circuit.deviceRows.length === 0)) {
throw new Error(
"Circuit reserve state must match whether device rows exist."
);
}
const rowIds = new Set<string>();
for (const row of circuit.deviceRows) {
assertCircuitDeviceRowInsertProjectCommand({
schemaVersion: circuitDeviceRowStructureCommandSchemaVersion,
type: circuitDeviceRowInsertCommandType,
payload: { row },
});
if (row.circuitId !== circuit.id) {
throw new Error("Circuit device row belongs to a different circuit.");
}
if (rowIds.has(row.id)) {
throw new Error("Circuit insert command contains duplicate row ids.");
}
rowIds.add(row.id);
}
}
export function assertCircuitDeleteProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is CircuitDeleteProjectCommand {
if (
command.schemaVersion !== circuitStructureCommandSchemaVersion ||
command.type !== circuitDeleteCommandType
) {
throw new Error("Unsupported circuit delete command.");
}
if (!isPlainObject(command.payload)) {
throw new Error("Circuit delete payload must be an object.");
}
assertNonEmptyString(command.payload.circuitId, "circuitId");
assertNonEmptyString(
command.payload.expectedCircuitListId,
"expectedCircuitListId"
);
}
function assertNonEmptyString(value: unknown, field: string) {
if (typeof value !== "string" || !value.trim()) {
throw new Error(`${field} must be a non-empty string.`);
}
}
function assertNullableString(value: unknown, field: string) {
if (value !== null && typeof value !== "string") {
throw new Error(`${field} must be a string or null.`);
}
}
function assertFiniteNumber(
value: unknown,
field: string
): asserts value is number {
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new Error(`${field} must be a finite number.`);
}
}
function assertNullableNonNegativeNumber(value: unknown, field: string) {
if (value === null) {
return;
}
assertFiniteNumber(value, field);
if (value < 0) {
throw new Error(`${field} must not be negative.`);
}
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
@@ -0,0 +1,26 @@
import type { CircuitStructureProjectCommand } from "../models/circuit-structure-project-command.model.js";
import type {
AppendedProjectRevision,
ProjectRevisionSource,
} from "./project-revision.store.js";
export interface ExecuteCircuitStructureCommandInput {
projectId: string;
expectedRevision: number;
source: ProjectRevisionSource;
description?: string;
actorId?: string;
historyTargetChangeSetId?: string;
command: CircuitStructureProjectCommand;
}
export interface ExecutedCircuitStructureCommand {
revision: AppendedProjectRevision;
inverse: CircuitStructureProjectCommand;
}
export interface CircuitStructureProjectCommandStore {
execute(
input: ExecuteCircuitStructureCommandInput
): ExecutedCircuitStructureCommand;
}
@@ -13,6 +13,12 @@ import {
assertCircuitUpdateProjectCommand,
circuitUpdateCommandType,
} from "../models/circuit-project-command.model.js";
import {
assertCircuitDeleteProjectCommand,
assertCircuitInsertProjectCommand,
circuitDeleteCommandType,
circuitInsertCommandType,
} from "../models/circuit-structure-project-command.model.js";
import {
assertSerializedProjectCommand,
type SerializedProjectCommand,
@@ -20,6 +26,7 @@ import {
import type { CircuitDeviceRowProjectCommandStore } from "../ports/circuit-device-row-project-command.store.js";
import type { CircuitDeviceRowStructureProjectCommandStore } from "../ports/circuit-device-row-structure-project-command.store.js";
import type { CircuitProjectCommandStore } from "../ports/circuit-project-command.store.js";
import type { CircuitStructureProjectCommandStore } from "../ports/circuit-structure-project-command.store.js";
import type {
ExecuteProjectHistoryCommandInput,
ExecutedProjectCommand,
@@ -47,6 +54,7 @@ export class ProjectCommandService implements ProjectCommandExecutor {
private readonly circuitStore: CircuitProjectCommandStore,
private readonly deviceRowStore: CircuitDeviceRowProjectCommandStore,
private readonly deviceRowStructureStore: CircuitDeviceRowStructureProjectCommandStore,
private readonly circuitStructureStore: CircuitStructureProjectCommandStore,
private readonly historyStore: ProjectHistoryStore
) {}
@@ -140,6 +148,20 @@ export class ProjectCommandService implements ProjectCommandExecutor {
command: input.command,
}).revision;
}
case circuitInsertCommandType: {
assertCircuitInsertProjectCommand(input.command);
return this.circuitStructureStore.execute({
...input,
command: input.command,
}).revision;
}
case circuitDeleteCommandType: {
assertCircuitDeleteProjectCommand(input.command);
return this.circuitStructureStore.execute({
...input,
command: input.command,
}).revision;
}
default:
throw new Error(
`Unsupported project command type: ${input.command.type}.`