Add project device structure history

This commit is contained in:
2026-07-24 09:09:59 +02:00
parent 0bc6c7372f
commit ac16cca77a
15 changed files with 1262 additions and 26 deletions
@@ -24,6 +24,21 @@ export type ProjectDeviceUpdateField =
export type ProjectDeviceUpdatePatch =
Partial<ProjectDeviceUpdateValues>;
export const projectDeviceUpdateFields = [
"name",
"displayName",
"phaseType",
"connectionKind",
"costGroup",
"category",
"quantity",
"powerPerUnit",
"simultaneityFactor",
"cosPhi",
"remark",
"voltageV",
] as const satisfies readonly ProjectDeviceUpdateField[];
export interface ProjectDeviceUpdateFieldChange<
TField extends ProjectDeviceUpdateField = ProjectDeviceUpdateField,
> {
@@ -173,20 +188,9 @@ function assertProjectDeviceUpdateFieldValue(
function isProjectDeviceUpdateField(
value: string
): value is ProjectDeviceUpdateField {
return [
"name",
"displayName",
"phaseType",
"connectionKind",
"costGroup",
"category",
"quantity",
"powerPerUnit",
"simultaneityFactor",
"cosPhi",
"remark",
"voltageV",
].includes(value);
return projectDeviceUpdateFields.includes(
value as ProjectDeviceUpdateField
);
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
@@ -0,0 +1,171 @@
import {
assertCircuitDeviceRowInsertProjectCommand,
circuitDeviceRowInsertCommandType,
circuitDeviceRowStructureCommandSchemaVersion,
type CircuitDeviceRowSnapshot,
} from "./circuit-device-row-structure-project-command.model.js";
import {
assertProjectDeviceUpdateProjectCommand,
projectDeviceUpdateCommandSchemaVersion,
projectDeviceUpdateCommandType,
projectDeviceUpdateFields,
type ProjectDeviceUpdateValues,
} from "./project-device-project-command.model.js";
import type { SerializedProjectCommand } from "./project-command.model.js";
export const projectDeviceInsertCommandType =
"project-device.insert" as const;
export const projectDeviceDeleteCommandType =
"project-device.delete" as const;
export const projectDeviceStructureCommandSchemaVersion = 1 as const;
export interface ProjectDeviceSnapshot
extends ProjectDeviceUpdateValues {
id: string;
projectId: string;
}
export interface ProjectDeviceInsertCommandPayload {
projectDevice: ProjectDeviceSnapshot;
linkedRows: CircuitDeviceRowSnapshot[];
}
export interface ProjectDeviceDeleteCommandPayload {
projectDeviceId: string;
expectedProjectId: string;
}
export interface ProjectDeviceInsertProjectCommand
extends SerializedProjectCommand<ProjectDeviceInsertCommandPayload> {
schemaVersion: typeof projectDeviceStructureCommandSchemaVersion;
type: typeof projectDeviceInsertCommandType;
}
export interface ProjectDeviceDeleteProjectCommand
extends SerializedProjectCommand<ProjectDeviceDeleteCommandPayload> {
schemaVersion: typeof projectDeviceStructureCommandSchemaVersion;
type: typeof projectDeviceDeleteCommandType;
}
export type ProjectDeviceStructureProjectCommand =
| ProjectDeviceInsertProjectCommand
| ProjectDeviceDeleteProjectCommand;
export function createProjectDeviceInsertProjectCommand(
projectDevice: ProjectDeviceSnapshot,
linkedRows: CircuitDeviceRowSnapshot[] = []
): ProjectDeviceInsertProjectCommand {
const command: ProjectDeviceInsertProjectCommand = {
schemaVersion: projectDeviceStructureCommandSchemaVersion,
type: projectDeviceInsertCommandType,
payload: { projectDevice, linkedRows },
};
assertProjectDeviceInsertProjectCommand(command);
return command;
}
export function createProjectDeviceDeleteProjectCommand(
projectDeviceId: string,
expectedProjectId: string
): ProjectDeviceDeleteProjectCommand {
const command: ProjectDeviceDeleteProjectCommand = {
schemaVersion: projectDeviceStructureCommandSchemaVersion,
type: projectDeviceDeleteCommandType,
payload: { projectDeviceId, expectedProjectId },
};
assertProjectDeviceDeleteProjectCommand(command);
return command;
}
export function assertProjectDeviceInsertProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is ProjectDeviceInsertProjectCommand {
if (
command.schemaVersion !== projectDeviceStructureCommandSchemaVersion ||
command.type !== projectDeviceInsertCommandType
) {
throw new Error("Unsupported project-device insert command.");
}
if (!isPlainObject(command.payload)) {
throw new Error("Project-device insert payload must be an object.");
}
const { projectDevice, linkedRows } = command.payload;
if (!isPlainObject(projectDevice)) {
throw new Error(
"Project-device insert command requires a project device."
);
}
assertNonEmptyString(projectDevice.id, "projectDevice.id");
assertNonEmptyString(
projectDevice.projectId,
"projectDevice.projectId"
);
assertProjectDeviceUpdateProjectCommand({
schemaVersion: projectDeviceUpdateCommandSchemaVersion,
type: projectDeviceUpdateCommandType,
payload: {
projectDeviceId: projectDevice.id,
changes: projectDeviceUpdateFields.map((field) => ({
field,
value: projectDevice[field],
})),
},
});
if (!Array.isArray(linkedRows)) {
throw new Error(
"Project-device insert linkedRows must be an array."
);
}
const rowIds = new Set<string>();
for (const row of linkedRows) {
assertCircuitDeviceRowInsertProjectCommand({
schemaVersion: circuitDeviceRowStructureCommandSchemaVersion,
type: circuitDeviceRowInsertCommandType,
payload: { row },
});
if (row.linkedProjectDeviceId !== null) {
throw new Error(
"Restored project-device rows must currently be disconnected."
);
}
if (rowIds.has(row.id)) {
throw new Error(
"Project-device insert command contains duplicate row ids."
);
}
rowIds.add(row.id);
}
}
export function assertProjectDeviceDeleteProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is ProjectDeviceDeleteProjectCommand {
if (
command.schemaVersion !== projectDeviceStructureCommandSchemaVersion ||
command.type !== projectDeviceDeleteCommandType
) {
throw new Error("Unsupported project-device delete command.");
}
if (!isPlainObject(command.payload)) {
throw new Error("Project-device delete payload must be an object.");
}
assertNonEmptyString(
command.payload.projectDeviceId,
"projectDeviceId"
);
assertNonEmptyString(
command.payload.expectedProjectId,
"expectedProjectId"
);
}
function assertNonEmptyString(value: unknown, field: string) {
if (typeof value !== "string" || !value.trim()) {
throw new Error(`${field} must be a non-empty string.`);
}
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
@@ -0,0 +1,26 @@
import type { ProjectDeviceStructureProjectCommand } from "../models/project-device-structure-project-command.model.js";
import type {
AppendedProjectRevision,
ProjectRevisionSource,
} from "./project-revision.store.js";
export interface ExecuteProjectDeviceStructureCommandInput {
projectId: string;
expectedRevision: number;
source: ProjectRevisionSource;
description?: string;
actorId?: string;
historyTargetChangeSetId?: string;
command: ProjectDeviceStructureProjectCommand;
}
export interface ExecutedProjectDeviceStructureCommand {
revision: AppendedProjectRevision;
inverse: ProjectDeviceStructureProjectCommand;
}
export interface ProjectDeviceStructureProjectCommandStore {
execute(
input: ExecuteProjectDeviceStructureCommandInput
): ExecutedProjectDeviceStructureCommand;
}
@@ -45,6 +45,12 @@ import {
assertProjectDeviceUpdateProjectCommand,
projectDeviceUpdateCommandType,
} from "../models/project-device-project-command.model.js";
import {
assertProjectDeviceDeleteProjectCommand,
assertProjectDeviceInsertProjectCommand,
projectDeviceDeleteCommandType,
projectDeviceInsertCommandType,
} from "../models/project-device-structure-project-command.model.js";
import type { CircuitDeviceRowProjectCommandStore } from "../ports/circuit-device-row-project-command.store.js";
import type { CircuitDeviceRowMoveProjectCommandStore } from "../ports/circuit-device-row-move-project-command.store.js";
import type { CircuitDeviceRowStructureProjectCommandStore } from "../ports/circuit-device-row-structure-project-command.store.js";
@@ -64,6 +70,7 @@ import type {
} from "../ports/project-history.store.js";
import type { ProjectDeviceProjectCommandStore } from "../ports/project-device-project-command.store.js";
import type { ProjectDeviceRowSyncProjectCommandStore } from "../ports/project-device-row-sync-project-command.store.js";
import type { ProjectDeviceStructureProjectCommandStore } from "../ports/project-device-structure-project-command.store.js";
import type { ProjectRevisionSource } from "../ports/project-revision.store.js";
interface DispatchProjectCommandInput {
@@ -85,6 +92,7 @@ export class ProjectCommandService implements ProjectCommandExecutor {
private readonly circuitStructureStore: CircuitStructureProjectCommandStore,
private readonly circuitSectionReorderStore: CircuitSectionReorderProjectCommandStore,
private readonly circuitSectionRenumberStore: CircuitSectionRenumberProjectCommandStore,
private readonly projectDeviceStructureStore: ProjectDeviceStructureProjectCommandStore,
private readonly projectDeviceStore: ProjectDeviceProjectCommandStore,
private readonly projectDeviceRowSyncStore: ProjectDeviceRowSyncProjectCommandStore,
private readonly historyStore: ProjectHistoryStore
@@ -238,6 +246,20 @@ export class ProjectCommandService implements ProjectCommandExecutor {
command: input.command,
}).revision;
}
case projectDeviceInsertCommandType: {
assertProjectDeviceInsertProjectCommand(input.command);
return this.projectDeviceStructureStore.execute({
...input,
command: input.command,
}).revision;
}
case projectDeviceDeleteCommandType: {
assertProjectDeviceDeleteProjectCommand(input.command);
return this.projectDeviceStructureStore.execute({
...input,
command: input.command,
}).revision;
}
default:
throw new Error(
`Unsupported project command type: ${input.command.type}.`