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,186 @@
import type { SerializedProjectCommand } from "./project-command.model.js";
export const circuitDeviceRowInsertCommandType =
"circuit-device-row.insert" as const;
export const circuitDeviceRowDeleteCommandType =
"circuit-device-row.delete" as const;
export const circuitDeviceRowStructureCommandSchemaVersion = 1 as const;
export interface CircuitDeviceRowSnapshot {
id: string;
circuitId: string;
linkedProjectDeviceId: string | null;
legacyConsumerId: string | null;
sortOrder: number;
name: string;
displayName: string;
phaseType: string | null;
connectionKind: string | null;
costGroup: string | null;
category: string | null;
level: string | null;
roomId: string | null;
roomNumberSnapshot: string | null;
roomNameSnapshot: string | null;
quantity: number;
powerPerUnit: number;
simultaneityFactor: number;
cosPhi: number | null;
remark: string | null;
overriddenFields: string | null;
}
export interface CircuitDeviceRowInsertCommandPayload {
row: CircuitDeviceRowSnapshot;
}
export interface CircuitDeviceRowDeleteCommandPayload {
rowId: string;
expectedCircuitId: string;
}
export interface CircuitDeviceRowInsertProjectCommand
extends SerializedProjectCommand<CircuitDeviceRowInsertCommandPayload> {
schemaVersion: typeof circuitDeviceRowStructureCommandSchemaVersion;
type: typeof circuitDeviceRowInsertCommandType;
}
export interface CircuitDeviceRowDeleteProjectCommand
extends SerializedProjectCommand<CircuitDeviceRowDeleteCommandPayload> {
schemaVersion: typeof circuitDeviceRowStructureCommandSchemaVersion;
type: typeof circuitDeviceRowDeleteCommandType;
}
export type CircuitDeviceRowStructureProjectCommand =
| CircuitDeviceRowInsertProjectCommand
| CircuitDeviceRowDeleteProjectCommand;
export function createCircuitDeviceRowInsertProjectCommand(
row: CircuitDeviceRowSnapshot
): CircuitDeviceRowInsertProjectCommand {
const command: CircuitDeviceRowInsertProjectCommand = {
schemaVersion: circuitDeviceRowStructureCommandSchemaVersion,
type: circuitDeviceRowInsertCommandType,
payload: { row },
};
assertCircuitDeviceRowInsertProjectCommand(command);
return command;
}
export function createCircuitDeviceRowDeleteProjectCommand(
rowId: string,
expectedCircuitId: string
): CircuitDeviceRowDeleteProjectCommand {
const command: CircuitDeviceRowDeleteProjectCommand = {
schemaVersion: circuitDeviceRowStructureCommandSchemaVersion,
type: circuitDeviceRowDeleteCommandType,
payload: { rowId, expectedCircuitId },
};
assertCircuitDeviceRowDeleteProjectCommand(command);
return command;
}
export function assertCircuitDeviceRowInsertProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is CircuitDeviceRowInsertProjectCommand {
if (
command.schemaVersion !==
circuitDeviceRowStructureCommandSchemaVersion ||
command.type !== circuitDeviceRowInsertCommandType
) {
throw new Error("Unsupported circuit device-row insert command.");
}
if (!isPlainObject(command.payload)) {
throw new Error("Circuit device-row insert payload must be an object.");
}
const row = command.payload.row;
if (!isPlainObject(row)) {
throw new Error("Circuit device-row insert command requires a row.");
}
assertNonEmptyString(row.id, "row.id");
assertNonEmptyString(row.circuitId, "row.circuitId");
assertNullableString(row.linkedProjectDeviceId, "row.linkedProjectDeviceId");
assertNullableString(row.legacyConsumerId, "row.legacyConsumerId");
assertFiniteNumber(row.sortOrder, "row.sortOrder");
assertNonEmptyString(row.name, "row.name");
assertNonEmptyString(row.displayName, "row.displayName");
for (const field of [
"phaseType",
"connectionKind",
"costGroup",
"category",
"level",
"roomId",
"roomNumberSnapshot",
"roomNameSnapshot",
"remark",
"overriddenFields",
] as const) {
assertNullableString(row[field], `row.${field}`);
}
assertNonNegativeNumber(row.quantity, "row.quantity");
assertNonNegativeNumber(row.powerPerUnit, "row.powerPerUnit");
assertNonNegativeNumber(
row.simultaneityFactor,
"row.simultaneityFactor"
);
if (row.cosPhi !== null) {
assertFiniteNumber(row.cosPhi, "row.cosPhi");
if (row.cosPhi <= 0) {
throw new Error("row.cosPhi must be positive or null.");
}
}
}
export function assertCircuitDeviceRowDeleteProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is CircuitDeviceRowDeleteProjectCommand {
if (
command.schemaVersion !==
circuitDeviceRowStructureCommandSchemaVersion ||
command.type !== circuitDeviceRowDeleteCommandType
) {
throw new Error("Unsupported circuit device-row delete command.");
}
if (!isPlainObject(command.payload)) {
throw new Error("Circuit device-row delete payload must be an object.");
}
assertNonEmptyString(command.payload.rowId, "rowId");
assertNonEmptyString(
command.payload.expectedCircuitId,
"expectedCircuitId"
);
}
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 assertNonNegativeNumber(value: unknown, field: string) {
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 { CircuitDeviceRowStructureProjectCommand } from "../models/circuit-device-row-structure-project-command.model.js";
import type {
AppendedProjectRevision,
ProjectRevisionSource,
} from "./project-revision.store.js";
export interface ExecuteCircuitDeviceRowStructureCommandInput {
projectId: string;
expectedRevision: number;
source: ProjectRevisionSource;
description?: string;
actorId?: string;
historyTargetChangeSetId?: string;
command: CircuitDeviceRowStructureProjectCommand;
}
export interface ExecutedCircuitDeviceRowStructureCommand {
revision: AppendedProjectRevision;
inverse: CircuitDeviceRowStructureProjectCommand;
}
export interface CircuitDeviceRowStructureProjectCommandStore {
execute(
input: ExecuteCircuitDeviceRowStructureCommandInput
): ExecutedCircuitDeviceRowStructureCommand;
}
@@ -3,6 +3,12 @@ import {
assertCircuitDeviceRowUpdateProjectCommand,
circuitDeviceRowUpdateCommandType,
} from "../models/circuit-device-row-project-command.model.js";
import {
assertCircuitDeviceRowDeleteProjectCommand,
assertCircuitDeviceRowInsertProjectCommand,
circuitDeviceRowDeleteCommandType,
circuitDeviceRowInsertCommandType,
} from "../models/circuit-device-row-structure-project-command.model.js";
import {
assertCircuitUpdateProjectCommand,
circuitUpdateCommandType,
@@ -12,6 +18,7 @@ import {
type SerializedProjectCommand,
} from "../models/project-command.model.js";
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 {
ExecuteProjectHistoryCommandInput,
@@ -39,6 +46,7 @@ export class ProjectCommandService implements ProjectCommandExecutor {
constructor(
private readonly circuitStore: CircuitProjectCommandStore,
private readonly deviceRowStore: CircuitDeviceRowProjectCommandStore,
private readonly deviceRowStructureStore: CircuitDeviceRowStructureProjectCommandStore,
private readonly historyStore: ProjectHistoryStore
) {}
@@ -118,6 +126,20 @@ export class ProjectCommandService implements ProjectCommandExecutor {
command: input.command,
}).revision;
}
case circuitDeviceRowInsertCommandType: {
assertCircuitDeviceRowInsertProjectCommand(input.command);
return this.deviceRowStructureStore.execute({
...input,
command: input.command,
}).revision;
}
case circuitDeviceRowDeleteCommandType: {
assertCircuitDeviceRowDeleteProjectCommand(input.command);
return this.deviceRowStructureStore.execute({
...input,
command: input.command,
}).revision;
}
default:
throw new Error(
`Unsupported project command type: ${input.command.type}.`