forked from jappel/leistungsbilanz-ts
184 lines
6.1 KiB
TypeScript
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);
|
|
}
|