Files
leistungsbilanz-ts/src/domain/models/circuit-group-renumber-project-command.model.ts
T

184 lines
6.1 KiB
TypeScript

import {
circuitGroupCategories,
type CircuitGroupCategory,
} from "../../shared/constants/circuit-group.js";
import {
formatCircuitGroupPrefix,
parseGroupedEquipmentIdentifier,
} from "../services/circuit-group-numbering.js";
import type { CircuitGroupRenumberPlan } from "../services/circuit-group-renumbering.js";
import type { SerializedProjectCommand } from "./project-command.model.js";
export const circuitGroupRenumberCommandType =
"circuit-group.renumber" as const;
export const circuitGroupRenumberCommandSchemaVersion = 1 as const;
export interface CircuitGroupRenumberCommandPayload
extends CircuitGroupRenumberPlan {
circuitListId: string;
}
export interface CircuitGroupRenumberProjectCommand
extends SerializedProjectCommand<CircuitGroupRenumberCommandPayload> {
schemaVersion: typeof circuitGroupRenumberCommandSchemaVersion;
type: typeof circuitGroupRenumberCommandType;
}
export function createCircuitGroupRenumberProjectCommand(
circuitListId: string,
plan: CircuitGroupRenumberPlan
): CircuitGroupRenumberProjectCommand {
const command: CircuitGroupRenumberProjectCommand = {
schemaVersion: circuitGroupRenumberCommandSchemaVersion,
type: circuitGroupRenumberCommandType,
payload: { circuitListId, groups: plan.groups },
};
assertCircuitGroupRenumberProjectCommand(command);
return command;
}
export function assertCircuitGroupRenumberProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is CircuitGroupRenumberProjectCommand {
if (
command.schemaVersion !== circuitGroupRenumberCommandSchemaVersion ||
command.type !== circuitGroupRenumberCommandType ||
!isPlainObject(command.payload) ||
Object.keys(command.payload).length !== 2 ||
typeof command.payload.circuitListId !== "string" ||
!command.payload.circuitListId.trim() ||
!Array.isArray(command.payload.groups) ||
command.payload.groups.length === 0
) {
throw new Error("Unsupported circuit-group renumber command.");
}
const groupIds = new Set<string>();
const entityIds = new Set<string>();
const targetGroupKeys = new Set<string>();
const targetIdentifiers = new Set<string>();
let hasChange = false;
for (const group of command.payload.groups) {
if (
!isPlainObject(group) ||
Object.keys(group).length !== 8 ||
typeof group.groupId !== "string" ||
!group.groupId.trim() ||
groupIds.has(group.groupId) ||
!circuitGroupCategories.includes(
group.category as CircuitGroupCategory
) ||
!positiveInteger(group.expectedGroupNumber) ||
!positiveInteger(group.targetGroupNumber) ||
group.expectedPrefix !==
formatCircuitGroupPrefix(
group.category as CircuitGroupCategory,
group.expectedGroupNumber as number
) ||
group.targetPrefix !==
formatCircuitGroupPrefix(
group.category as CircuitGroupCategory,
group.targetGroupNumber as number
) ||
!Array.isArray(group.circuits) ||
!Array.isArray(group.components)
) {
throw new Error("Circuit-group renumber assignment is invalid.");
}
const targetGroupKey = `${group.category}:${group.targetGroupNumber}`;
if (targetGroupKeys.has(targetGroupKey)) {
throw new Error(
"Circuit-group renumber contains duplicate target group numbers."
);
}
hasChange ||= group.expectedGroupNumber !== group.targetGroupNumber;
groupIds.add(group.groupId);
targetGroupKeys.add(targetGroupKey);
for (const circuit of group.circuits) {
assertEntityAssignment(
circuit,
"circuitId",
"circuit",
group.category as CircuitGroupCategory,
group.expectedGroupNumber as number,
group.targetGroupNumber as number,
entityIds,
targetIdentifiers
);
}
for (const component of group.components) {
assertEntityAssignment(
component,
"componentId",
"component",
group.category as CircuitGroupCategory,
group.expectedGroupNumber as number,
group.targetGroupNumber as number,
entityIds,
targetIdentifiers
);
}
}
if (!hasChange) {
throw new Error(
"Circuit-group renumber must change at least one group number."
);
}
}
function assertEntityAssignment(
value: unknown,
idField: "circuitId" | "componentId",
kind: "circuit" | "component",
category: CircuitGroupCategory,
expectedGroupNumber: number,
targetGroupNumber: number,
entityIds: Set<string>,
targetIdentifiers: Set<string>
) {
if (
!isPlainObject(value) ||
Object.keys(value).length !== 3 ||
typeof value[idField] !== "string" ||
!(value[idField] as string).trim() ||
typeof value.expectedEquipmentIdentifier !== "string" ||
typeof value.targetEquipmentIdentifier !== "string" ||
entityIds.has(value[idField] as string) ||
targetIdentifiers.has(value.targetEquipmentIdentifier)
) {
throw new Error(`Circuit-group renumber ${kind} assignment is invalid.`);
}
const expected = parseGroupedEquipmentIdentifier(
value.expectedEquipmentIdentifier
);
const target = parseGroupedEquipmentIdentifier(
value.targetEquipmentIdentifier
);
if (
!expected ||
!target ||
expected.kind !== target.kind ||
expected.category !== category ||
target.category !== category ||
expected.groupNumber !== expectedGroupNumber ||
target.groupNumber !== targetGroupNumber ||
(kind === "circuit" &&
(expected.kind !== "circuit" ||
expected.circuitNumber !== target.circuitNumber)) ||
(kind === "component" && expected.kind === "circuit")
) {
throw new Error(
`Circuit-group renumber ${kind} identifiers are incompatible.`
);
}
entityIds.add(value[idField] as string);
targetIdentifiers.add(value.targetEquipmentIdentifier);
}
function positiveInteger(value: unknown): value is number {
return Number.isInteger(value) && (value as number) > 0;
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}