leistungsbilanz-ts/src/domain/models/distribution-board-project-command.model.ts

184 lines
6.1 KiB
TypeScript

import {
distributionBoardInfoFieldMaxLengths,
distributionBoardInfoFields,
distributionBoardSupplyTypes,
type DistributionBoardInfoField,
type DistributionBoardSupplyType,
} from "../../shared/constants/distribution-board.js";
import type { SerializedProjectCommand } from "./project-command.model.js";
export const distributionBoardUpdateCommandType =
"distribution-board.update" as const;
// Version 2 adds logical feeding references. Version-1 commands remain
// executable and can only change the fields they originally knew.
export const distributionBoardUpdateCommandSchemaVersion = 2 as const;
const legacyDistributionBoardUpdateCommandSchemaVersion = 1 as const;
export interface DistributionBoardUpdateValues
extends Record<DistributionBoardInfoField, string | null> {
floorId: string | null;
supplyType: DistributionBoardSupplyType | null;
simultaneityFactor: number;
feedingDistributionBoardId: string | null;
feedingCircuitId: string | null;
}
export type DistributionBoardUpdateField =
keyof DistributionBoardUpdateValues;
export type DistributionBoardUpdatePatch =
Partial<DistributionBoardUpdateValues>;
export interface DistributionBoardUpdateFieldChange<
TField extends DistributionBoardUpdateField =
DistributionBoardUpdateField,
> {
field: TField;
value: DistributionBoardUpdateValues[TField];
}
export interface DistributionBoardUpdateCommandPayload {
distributionBoardId: string;
changes: DistributionBoardUpdateFieldChange[];
}
export interface DistributionBoardUpdateProjectCommand
extends SerializedProjectCommand<DistributionBoardUpdateCommandPayload> {
schemaVersion:
| typeof legacyDistributionBoardUpdateCommandSchemaVersion
| typeof distributionBoardUpdateCommandSchemaVersion;
type: typeof distributionBoardUpdateCommandType;
}
export function createDistributionBoardUpdateProjectCommand(
distributionBoardId: string,
patch: DistributionBoardUpdatePatch,
schemaVersion: DistributionBoardUpdateProjectCommand["schemaVersion"] =
distributionBoardUpdateCommandSchemaVersion
): DistributionBoardUpdateProjectCommand {
const command: DistributionBoardUpdateProjectCommand = {
schemaVersion,
type: distributionBoardUpdateCommandType,
payload: {
distributionBoardId,
changes: Object.entries(patch).map(([field, value]) => ({
field: field as DistributionBoardUpdateField,
value,
})) as DistributionBoardUpdateFieldChange[],
},
};
assertDistributionBoardUpdateProjectCommand(command);
return command;
}
export function assertDistributionBoardUpdateProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is DistributionBoardUpdateProjectCommand {
if (
(command.schemaVersion !== legacyDistributionBoardUpdateCommandSchemaVersion &&
command.schemaVersion !== distributionBoardUpdateCommandSchemaVersion) ||
command.type !== distributionBoardUpdateCommandType ||
!isPlainObject(command.payload)
) {
throw new Error("Unsupported distribution-board update command.");
}
const { distributionBoardId, changes } = command.payload;
if (
typeof distributionBoardId !== "string" ||
!distributionBoardId.trim() ||
!Array.isArray(changes) ||
changes.length === 0
) {
throw new Error(
"Distribution-board update command requires an id and changes."
);
}
const seen = new Set<string>();
for (const change of changes) {
if (
!isPlainObject(change) ||
typeof change.field !== "string" ||
(change.field !== "floorId" &&
change.field !== "supplyType" &&
change.field !== "simultaneityFactor" &&
change.field !== "feedingDistributionBoardId" &&
change.field !== "feedingCircuitId" &&
!isInfoField(change.field)) ||
(command.schemaVersion === legacyDistributionBoardUpdateCommandSchemaVersion &&
(change.field === "feedingDistributionBoardId" ||
change.field === "feedingCircuitId")) ||
seen.has(change.field)
) {
throw new Error(
"Distribution-board update contains an invalid or duplicate field."
);
}
if (isInfoField(change.field)) {
assertInfoFieldValue(change.field, change.value);
} else if (change.field === "floorId") {
if (
change.value !== null &&
(typeof change.value !== "string" ||
!change.value.trim() ||
change.value !== change.value.trim())
) {
throw new Error("floorId must be a non-empty string or null.");
}
} else if (change.field === "supplyType") {
if (
change.value !== null &&
!distributionBoardSupplyTypes.includes(
change.value as DistributionBoardSupplyType
)
) {
throw new Error("supplyType is invalid.");
}
} else if (
change.field === "feedingDistributionBoardId" ||
change.field === "feedingCircuitId"
) {
if (
change.value !== null &&
(typeof change.value !== "string" || !change.value.trim())
) {
throw new Error(`${change.field} must be a non-empty string or null.`);
}
} else if (
typeof change.value !== "number" ||
!Number.isFinite(change.value) ||
change.value < 0 ||
change.value > 1
) {
throw new Error(
"simultaneityFactor must be between zero and one."
);
}
seen.add(change.field);
}
}
function isInfoField(field: string): field is DistributionBoardInfoField {
return (distributionBoardInfoFields as readonly string[]).includes(field);
}
function assertInfoFieldValue(
field: DistributionBoardInfoField,
value: unknown
) {
if (value === null) {
return;
}
if (
typeof value !== "string" ||
!value.trim() ||
value !== value.trim() ||
value.length > distributionBoardInfoFieldMaxLengths[field]
) {
throw new Error(
`${field} must be a trimmed, non-empty string of at most ${distributionBoardInfoFieldMaxLengths[field]} characters or null.`
);
}
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}