Files
leistungsbilanz-ts/src/domain/models/circuit-device-row-move-project-command.model.ts
T
2026-07-23 22:39:59 +02:00

231 lines
7.1 KiB
TypeScript

import type { SerializedProjectCommand } from "./project-command.model.js";
import {
assertCircuitInsertProjectCommand,
circuitInsertCommandType,
circuitStructureCommandSchemaVersion,
type CircuitSnapshot,
} from "./circuit-structure-project-command.model.js";
export const circuitDeviceRowMoveCommandType =
"circuit-device-row.move" as const;
export const circuitDeviceRowMoveWithNewCircuitCommandType =
"circuit-device-row.move-with-new-circuit" as const;
export const circuitDeviceRowMoveCommandSchemaVersion = 1 as const;
export interface CircuitDeviceRowMoveAssignment {
rowId: string;
expectedCircuitId: string;
expectedSortOrder: number;
targetCircuitId: string;
targetSortOrder: number;
}
export interface CircuitDeviceRowMoveCommandPayload {
moves: CircuitDeviceRowMoveAssignment[];
}
export interface CircuitDeviceRowMoveProjectCommand
extends SerializedProjectCommand<CircuitDeviceRowMoveCommandPayload> {
schemaVersion: typeof circuitDeviceRowMoveCommandSchemaVersion;
type: typeof circuitDeviceRowMoveCommandType;
}
export type CircuitDeviceRowMoveTargetCircuitAction =
| "create"
| "delete";
export interface CircuitDeviceRowMoveWithNewCircuitCommandPayload {
targetCircuitAction: CircuitDeviceRowMoveTargetCircuitAction;
targetCircuit: CircuitSnapshot;
moves: CircuitDeviceRowMoveAssignment[];
}
export interface CircuitDeviceRowMoveWithNewCircuitProjectCommand
extends SerializedProjectCommand<CircuitDeviceRowMoveWithNewCircuitCommandPayload> {
schemaVersion: typeof circuitDeviceRowMoveCommandSchemaVersion;
type: typeof circuitDeviceRowMoveWithNewCircuitCommandType;
}
export type CircuitDeviceRowMoveHistoryProjectCommand =
| CircuitDeviceRowMoveProjectCommand
| CircuitDeviceRowMoveWithNewCircuitProjectCommand;
export function createCircuitDeviceRowMoveProjectCommand(
moves: CircuitDeviceRowMoveAssignment[]
): CircuitDeviceRowMoveProjectCommand {
const command: CircuitDeviceRowMoveProjectCommand = {
schemaVersion: circuitDeviceRowMoveCommandSchemaVersion,
type: circuitDeviceRowMoveCommandType,
payload: { moves },
};
assertCircuitDeviceRowMoveProjectCommand(command);
return command;
}
export function createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
targetCircuitAction: CircuitDeviceRowMoveTargetCircuitAction,
targetCircuit: CircuitSnapshot,
moves: CircuitDeviceRowMoveAssignment[]
): CircuitDeviceRowMoveWithNewCircuitProjectCommand {
const command: CircuitDeviceRowMoveWithNewCircuitProjectCommand = {
schemaVersion: circuitDeviceRowMoveCommandSchemaVersion,
type: circuitDeviceRowMoveWithNewCircuitCommandType,
payload: {
targetCircuitAction,
targetCircuit,
moves,
},
};
assertCircuitDeviceRowMoveWithNewCircuitProjectCommand(command);
return command;
}
export function assertCircuitDeviceRowMoveProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is CircuitDeviceRowMoveProjectCommand {
if (
command.schemaVersion !== circuitDeviceRowMoveCommandSchemaVersion ||
command.type !== circuitDeviceRowMoveCommandType
) {
throw new Error("Unsupported circuit device-row move command.");
}
if (!isPlainObject(command.payload)) {
throw new Error(
"Circuit device-row move payload must be an object."
);
}
const moves = command.payload.moves;
if (!Array.isArray(moves) || moves.length === 0) {
throw new Error(
"Circuit device-row move command requires at least one move."
);
}
const rowIds = new Set<string>();
for (const move of moves) {
if (!isPlainObject(move)) {
throw new Error(
"Circuit device-row move command contains an invalid move."
);
}
assertNonEmptyString(move.rowId, "rowId");
assertNonEmptyString(
move.expectedCircuitId,
"expectedCircuitId"
);
assertFiniteNumber(
move.expectedSortOrder,
"expectedSortOrder"
);
assertNonEmptyString(move.targetCircuitId, "targetCircuitId");
assertFiniteNumber(move.targetSortOrder, "targetSortOrder");
if (rowIds.has(move.rowId)) {
throw new Error(
"Circuit device-row move command contains duplicate row ids."
);
}
if (
move.expectedCircuitId === move.targetCircuitId &&
move.expectedSortOrder === move.targetSortOrder
) {
throw new Error(
"Circuit device-row move command contains a no-op move."
);
}
rowIds.add(move.rowId);
}
}
export function assertCircuitDeviceRowMoveWithNewCircuitProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is CircuitDeviceRowMoveWithNewCircuitProjectCommand {
if (
command.schemaVersion !== circuitDeviceRowMoveCommandSchemaVersion ||
command.type !== circuitDeviceRowMoveWithNewCircuitCommandType
) {
throw new Error(
"Unsupported circuit device-row move-with-new-circuit command."
);
}
if (!isPlainObject(command.payload)) {
throw new Error(
"Circuit device-row move-with-new-circuit payload must be an object."
);
}
const { targetCircuitAction, targetCircuit, moves } =
command.payload;
if (
targetCircuitAction !== "create" &&
targetCircuitAction !== "delete"
) {
throw new Error(
"Target circuit action must be create or delete."
);
}
assertCircuitInsertProjectCommand({
schemaVersion: circuitStructureCommandSchemaVersion,
type: circuitInsertCommandType,
payload: { circuit: targetCircuit },
});
const validatedTargetCircuit = targetCircuit as CircuitSnapshot;
if (
validatedTargetCircuit.isReserve !== true ||
validatedTargetCircuit.deviceRows.length !== 0
) {
throw new Error(
"Move target circuit snapshot must describe an empty reserve circuit."
);
}
assertCircuitDeviceRowMoveProjectCommand({
schemaVersion: circuitDeviceRowMoveCommandSchemaVersion,
type: circuitDeviceRowMoveCommandType,
payload: { moves },
});
const validatedMoves = moves as CircuitDeviceRowMoveAssignment[];
for (const move of validatedMoves) {
if (
targetCircuitAction === "create" &&
(move.expectedCircuitId === validatedTargetCircuit.id ||
move.targetCircuitId !== validatedTargetCircuit.id)
) {
throw new Error(
"Create-target moves must move rows into the new circuit."
);
}
if (
targetCircuitAction === "delete" &&
(move.expectedCircuitId !== validatedTargetCircuit.id ||
move.targetCircuitId === validatedTargetCircuit.id)
) {
throw new Error(
"Delete-target moves must move rows out of the created circuit."
);
}
}
}
function assertNonEmptyString(
value: unknown,
field: string
): asserts value is string {
if (typeof value !== "string" || !value.trim()) {
throw new Error(`${field} must be a non-empty string.`);
}
}
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 isPlainObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}