Add project device update history

This commit is contained in:
2026-07-24 08:57:57 +02:00
parent 2668fc2f16
commit 0bc6c7372f
14 changed files with 785 additions and 13 deletions
@@ -0,0 +1,194 @@
import type { SerializedProjectCommand } from "./project-command.model.js";
export const projectDeviceUpdateCommandType =
"project-device.update" as const;
export const projectDeviceUpdateCommandSchemaVersion = 1 as const;
export interface ProjectDeviceUpdateValues {
name: string;
displayName: string;
phaseType: "single_phase" | "three_phase";
connectionKind: string | null;
costGroup: string | null;
category: string | null;
quantity: number;
powerPerUnit: number;
simultaneityFactor: number;
cosPhi: number | null;
remark: string | null;
voltageV: number | null;
}
export type ProjectDeviceUpdateField =
keyof ProjectDeviceUpdateValues;
export type ProjectDeviceUpdatePatch =
Partial<ProjectDeviceUpdateValues>;
export interface ProjectDeviceUpdateFieldChange<
TField extends ProjectDeviceUpdateField = ProjectDeviceUpdateField,
> {
field: TField;
value: ProjectDeviceUpdateValues[TField];
}
export interface ProjectDeviceUpdateCommandPayload {
projectDeviceId: string;
changes: ProjectDeviceUpdateFieldChange[];
}
export interface ProjectDeviceUpdateProjectCommand
extends SerializedProjectCommand<ProjectDeviceUpdateCommandPayload> {
schemaVersion: typeof projectDeviceUpdateCommandSchemaVersion;
type: typeof projectDeviceUpdateCommandType;
}
export function createProjectDeviceUpdateProjectCommand(
projectDeviceId: string,
patch: ProjectDeviceUpdatePatch
): ProjectDeviceUpdateProjectCommand {
const command: ProjectDeviceUpdateProjectCommand = {
schemaVersion: projectDeviceUpdateCommandSchemaVersion,
type: projectDeviceUpdateCommandType,
payload: {
projectDeviceId,
changes: Object.entries(patch).map(([field, value]) => ({
field: field as ProjectDeviceUpdateField,
value,
})) as ProjectDeviceUpdateFieldChange[],
},
};
assertProjectDeviceUpdateProjectCommand(command);
return command;
}
export function assertProjectDeviceUpdateProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is ProjectDeviceUpdateProjectCommand {
if (
command.schemaVersion !== projectDeviceUpdateCommandSchemaVersion ||
command.type !== projectDeviceUpdateCommandType
) {
throw new Error("Unsupported project-device update command.");
}
if (!isPlainObject(command.payload)) {
throw new Error("Project-device update payload must be an object.");
}
const { projectDeviceId, changes } = command.payload;
if (
typeof projectDeviceId !== "string" ||
!projectDeviceId.trim()
) {
throw new Error(
"Project-device update command requires a project device id."
);
}
if (!Array.isArray(changes) || changes.length === 0) {
throw new Error(
"Project-device update command requires at least one change."
);
}
const seenFields = new Set<string>();
for (const change of changes) {
if (!isPlainObject(change) || typeof change.field !== "string") {
throw new Error(
"Project-device update command contains an invalid change."
);
}
if (
!isProjectDeviceUpdateField(change.field) ||
seenFields.has(change.field)
) {
throw new Error(
"Project-device update command contains an invalid or duplicate field."
);
}
assertProjectDeviceUpdateFieldValue(
change.field,
change.value
);
seenFields.add(change.field);
}
}
function assertProjectDeviceUpdateFieldValue(
field: ProjectDeviceUpdateField,
value: unknown
) {
if (field === "name" || field === "displayName") {
if (typeof value !== "string" || !value.trim()) {
throw new Error(`${field} must be a non-empty string.`);
}
return;
}
if (field === "phaseType") {
if (value !== "single_phase" && value !== "three_phase") {
throw new Error("phaseType is invalid.");
}
return;
}
if (
field === "quantity" ||
field === "powerPerUnit" ||
field === "simultaneityFactor"
) {
if (
typeof value !== "number" ||
!Number.isFinite(value) ||
value < 0 ||
(field === "simultaneityFactor" && value > 1)
) {
throw new Error(`${field} is outside its allowed range.`);
}
return;
}
if (field === "cosPhi") {
if (
value !== null &&
(typeof value !== "number" ||
!Number.isFinite(value) ||
value < 0 ||
value > 1)
) {
throw new Error("cosPhi must be between zero and one or null.");
}
return;
}
if (field === "voltageV") {
if (
value !== null &&
(typeof value !== "number" ||
!Number.isFinite(value) ||
value <= 0)
) {
throw new Error("voltageV must be positive or null.");
}
return;
}
if (value !== null && typeof value !== "string") {
throw new Error(`${field} must be a string or null.`);
}
}
function isProjectDeviceUpdateField(
value: string
): value is ProjectDeviceUpdateField {
return [
"name",
"displayName",
"phaseType",
"connectionKind",
"costGroup",
"category",
"quantity",
"powerPerUnit",
"simultaneityFactor",
"cosPhi",
"remark",
"voltageV",
].includes(value);
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}