Add component structure commands
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
import { distributionBoardComponentPlacements } from "../../shared/constants/distribution-board-component.js";
|
||||
import type { DistributionBoardComponentPlacement } from "../../shared/constants/distribution-board-component.js";
|
||||
import { protectionDeviceConfigurationSchema } from "../../shared/validation/protection-device.schemas.js";
|
||||
import type {
|
||||
BreakerTripCharacteristic,
|
||||
FuseUtilizationCategory,
|
||||
ProtectionDeviceType,
|
||||
RcdType,
|
||||
} from "../../shared/constants/protection-device.js";
|
||||
import type { SerializedProjectCommand } from "./project-command.model.js";
|
||||
|
||||
export const distributionBoardComponentInsertCommandType =
|
||||
"distribution-board-component.insert" as const;
|
||||
export const distributionBoardComponentDeleteCommandType =
|
||||
"distribution-board-component.delete" as const;
|
||||
export const distributionBoardComponentStructureCommandSchemaVersion =
|
||||
1 as const;
|
||||
|
||||
type MutableDistributionBoardComponentRole =
|
||||
| "group_upstream_protection"
|
||||
| "group_residual_current_protection"
|
||||
| "auxiliary";
|
||||
|
||||
export interface DistributionBoardComponentSnapshot {
|
||||
component: {
|
||||
id: string;
|
||||
circuitListId: string;
|
||||
sectionId: string | null;
|
||||
equipmentIdentifier: string;
|
||||
name: string;
|
||||
role: MutableDistributionBoardComponentRole;
|
||||
placement: DistributionBoardComponentPlacement;
|
||||
sortOrder: number;
|
||||
};
|
||||
protectionDevice: {
|
||||
componentId: string;
|
||||
type: ProtectionDeviceType;
|
||||
ratedCurrentA: number;
|
||||
fuseUtilizationCategory: FuseUtilizationCategory | null;
|
||||
tripCharacteristic: BreakerTripCharacteristic | null;
|
||||
rcdType: RcdType | null;
|
||||
ratedResidualCurrentMa: number | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface DistributionBoardComponentStructurePayload {
|
||||
snapshot: DistributionBoardComponentSnapshot;
|
||||
}
|
||||
|
||||
export interface DistributionBoardComponentInsertProjectCommand
|
||||
extends SerializedProjectCommand<DistributionBoardComponentStructurePayload> {
|
||||
schemaVersion: typeof distributionBoardComponentStructureCommandSchemaVersion;
|
||||
type: typeof distributionBoardComponentInsertCommandType;
|
||||
}
|
||||
|
||||
export interface DistributionBoardComponentDeleteProjectCommand
|
||||
extends SerializedProjectCommand<DistributionBoardComponentStructurePayload> {
|
||||
schemaVersion: typeof distributionBoardComponentStructureCommandSchemaVersion;
|
||||
type: typeof distributionBoardComponentDeleteCommandType;
|
||||
}
|
||||
|
||||
export type DistributionBoardComponentStructureProjectCommand =
|
||||
| DistributionBoardComponentInsertProjectCommand
|
||||
| DistributionBoardComponentDeleteProjectCommand;
|
||||
|
||||
export function createDistributionBoardComponentInsertProjectCommand(
|
||||
snapshot: DistributionBoardComponentSnapshot
|
||||
): DistributionBoardComponentInsertProjectCommand {
|
||||
const command: DistributionBoardComponentInsertProjectCommand = {
|
||||
schemaVersion:
|
||||
distributionBoardComponentStructureCommandSchemaVersion,
|
||||
type: distributionBoardComponentInsertCommandType,
|
||||
payload: { snapshot },
|
||||
};
|
||||
assertDistributionBoardComponentInsertProjectCommand(command);
|
||||
return command;
|
||||
}
|
||||
|
||||
export function createDistributionBoardComponentDeleteProjectCommand(
|
||||
snapshot: DistributionBoardComponentSnapshot
|
||||
): DistributionBoardComponentDeleteProjectCommand {
|
||||
const command: DistributionBoardComponentDeleteProjectCommand = {
|
||||
schemaVersion:
|
||||
distributionBoardComponentStructureCommandSchemaVersion,
|
||||
type: distributionBoardComponentDeleteCommandType,
|
||||
payload: { snapshot },
|
||||
};
|
||||
assertDistributionBoardComponentDeleteProjectCommand(command);
|
||||
return command;
|
||||
}
|
||||
|
||||
export function assertDistributionBoardComponentInsertProjectCommand(
|
||||
command: SerializedProjectCommand<unknown>
|
||||
): asserts command is DistributionBoardComponentInsertProjectCommand {
|
||||
assertStructureCommand(
|
||||
command,
|
||||
distributionBoardComponentInsertCommandType
|
||||
);
|
||||
}
|
||||
|
||||
export function assertDistributionBoardComponentDeleteProjectCommand(
|
||||
command: SerializedProjectCommand<unknown>
|
||||
): asserts command is DistributionBoardComponentDeleteProjectCommand {
|
||||
assertStructureCommand(
|
||||
command,
|
||||
distributionBoardComponentDeleteCommandType
|
||||
);
|
||||
}
|
||||
|
||||
function assertStructureCommand(
|
||||
command: SerializedProjectCommand<unknown>,
|
||||
type:
|
||||
| typeof distributionBoardComponentInsertCommandType
|
||||
| typeof distributionBoardComponentDeleteCommandType
|
||||
) {
|
||||
if (
|
||||
command.schemaVersion !==
|
||||
distributionBoardComponentStructureCommandSchemaVersion ||
|
||||
command.type !== type ||
|
||||
!isPlainObject(command.payload) ||
|
||||
Object.keys(command.payload).length !== 1
|
||||
) {
|
||||
throw new Error(
|
||||
"Unsupported distribution-board component structure command."
|
||||
);
|
||||
}
|
||||
assertDistributionBoardComponentSnapshot(command.payload.snapshot);
|
||||
}
|
||||
|
||||
export function assertDistributionBoardComponentSnapshot(
|
||||
value: unknown
|
||||
): asserts value is DistributionBoardComponentSnapshot {
|
||||
if (
|
||||
!isPlainObject(value) ||
|
||||
Object.keys(value).length !== 2 ||
|
||||
!isPlainObject(value.component)
|
||||
) {
|
||||
throw new Error(
|
||||
"Distribution-board component snapshot is invalid."
|
||||
);
|
||||
}
|
||||
const component = value.component;
|
||||
if (Object.keys(component).length !== 8) {
|
||||
throw new Error(
|
||||
"Distribution-board component snapshot is incomplete."
|
||||
);
|
||||
}
|
||||
for (const field of [
|
||||
"id",
|
||||
"circuitListId",
|
||||
"equipmentIdentifier",
|
||||
"name",
|
||||
] as const) {
|
||||
assertNonEmptyString(component[field], `component.${field}`);
|
||||
}
|
||||
if (
|
||||
component.role !== "group_upstream_protection" &&
|
||||
component.role !== "group_residual_current_protection" &&
|
||||
component.role !== "auxiliary"
|
||||
) {
|
||||
throw new Error(
|
||||
"Fixed distribution-board components cannot use component CRUD commands."
|
||||
);
|
||||
}
|
||||
if (
|
||||
!distributionBoardComponentPlacements.includes(
|
||||
component.placement as DistributionBoardComponentPlacement
|
||||
) ||
|
||||
!Number.isFinite(component.sortOrder)
|
||||
) {
|
||||
throw new Error(
|
||||
"Distribution-board component placement or sort order is invalid."
|
||||
);
|
||||
}
|
||||
const groupComponent = component.role !== "auxiliary";
|
||||
if (
|
||||
groupComponent !==
|
||||
(component.placement === "group" &&
|
||||
typeof component.sectionId === "string" &&
|
||||
Boolean(component.sectionId.trim()))
|
||||
) {
|
||||
throw new Error(
|
||||
"Group protection components require group placement and a section."
|
||||
);
|
||||
}
|
||||
if (
|
||||
!groupComponent &&
|
||||
(component.placement !== "footer" ||
|
||||
component.sectionId !== null)
|
||||
) {
|
||||
throw new Error(
|
||||
"Auxiliary components require footer placement without a section."
|
||||
);
|
||||
}
|
||||
|
||||
if (groupComponent) {
|
||||
if (!isPlainObject(value.protectionDevice)) {
|
||||
throw new Error(
|
||||
"Group protection components require protection data."
|
||||
);
|
||||
}
|
||||
assertProtectionDevice(
|
||||
value.protectionDevice,
|
||||
component.id as string
|
||||
);
|
||||
} else if (value.protectionDevice !== null) {
|
||||
throw new Error(
|
||||
"Auxiliary components cannot contain protection data."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function assertProtectionDevice(
|
||||
value: Record<string, unknown>,
|
||||
componentId: string
|
||||
) {
|
||||
if (
|
||||
Object.keys(value).length !== 7 ||
|
||||
value.componentId !== componentId
|
||||
) {
|
||||
throw new Error(
|
||||
"Distribution-board component protection snapshot is invalid."
|
||||
);
|
||||
}
|
||||
const result = protectionDeviceConfigurationSchema.safeParse({
|
||||
type: value.type,
|
||||
ratedCurrentA: value.ratedCurrentA,
|
||||
...(value.fuseUtilizationCategory === null
|
||||
? {}
|
||||
: { fuseUtilizationCategory: value.fuseUtilizationCategory }),
|
||||
...(value.tripCharacteristic === null
|
||||
? {}
|
||||
: { tripCharacteristic: value.tripCharacteristic }),
|
||||
...(value.rcdType === null ? {} : { rcdType: value.rcdType }),
|
||||
...(value.ratedResidualCurrentMa === null
|
||||
? {}
|
||||
: { ratedResidualCurrentMa: value.ratedResidualCurrentMa }),
|
||||
});
|
||||
if (!result.success) {
|
||||
throw new Error(
|
||||
"Distribution-board component protection configuration is invalid."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,24 @@
|
||||
import type { DistributionBoardComponentStructureProjectCommand } from "../models/distribution-board-component-structure-project-command.model.js";
|
||||
import type {
|
||||
AppendedProjectRevision,
|
||||
ProjectRevisionSource,
|
||||
} from "./project-revision.store.js";
|
||||
|
||||
export interface ExecuteDistributionBoardComponentStructureCommandInput {
|
||||
projectId: string;
|
||||
expectedRevision: number;
|
||||
source: ProjectRevisionSource;
|
||||
description?: string;
|
||||
actorId?: string;
|
||||
historyTargetChangeSetId?: string;
|
||||
command: DistributionBoardComponentStructureProjectCommand;
|
||||
}
|
||||
|
||||
export interface DistributionBoardComponentStructureProjectCommandStore {
|
||||
execute(
|
||||
input: ExecuteDistributionBoardComponentStructureCommandInput
|
||||
): {
|
||||
revision: AppendedProjectRevision;
|
||||
inverse: DistributionBoardComponentStructureProjectCommand;
|
||||
};
|
||||
}
|
||||
@@ -41,6 +41,12 @@ import {
|
||||
assertDistributionBoardUpdateProjectCommand,
|
||||
distributionBoardUpdateCommandType,
|
||||
} from "../models/distribution-board-project-command.model.js";
|
||||
import {
|
||||
assertDistributionBoardComponentDeleteProjectCommand,
|
||||
assertDistributionBoardComponentInsertProjectCommand,
|
||||
distributionBoardComponentDeleteCommandType,
|
||||
distributionBoardComponentInsertCommandType,
|
||||
} from "../models/distribution-board-component-structure-project-command.model.js";
|
||||
import {
|
||||
assertDistributionBoardDeleteProjectCommand,
|
||||
assertDistributionBoardInsertProjectCommand,
|
||||
@@ -91,6 +97,7 @@ import type { CircuitSectionReorderProjectCommandStore } from "../ports/circuit-
|
||||
import type { CircuitSectionRenumberProjectCommandStore } from "../ports/circuit-section-renumber-project-command.store.js";
|
||||
import type { CircuitStructureProjectCommandStore } from "../ports/circuit-structure-project-command.store.js";
|
||||
import type { DistributionBoardStructureProjectCommandStore } from "../ports/distribution-board-structure-project-command.store.js";
|
||||
import type { DistributionBoardComponentStructureProjectCommandStore } from "../ports/distribution-board-component-structure-project-command.store.js";
|
||||
import type {
|
||||
ExecuteProjectHistoryCommandInput,
|
||||
ExecutedProjectCommand,
|
||||
@@ -135,6 +142,7 @@ export class ProjectCommandService implements ProjectCommandExecutor {
|
||||
private readonly projectDeviceRowSyncStore: ProjectDeviceRowSyncProjectCommandStore,
|
||||
private readonly projectSettingsStore: ProjectSettingsProjectCommandStore,
|
||||
private readonly projectStateRestoreStore: ProjectStateRestoreCommandStore,
|
||||
private readonly distributionBoardComponentStructureStore: DistributionBoardComponentStructureProjectCommandStore,
|
||||
private readonly historyStore: ProjectHistoryStore
|
||||
) {}
|
||||
|
||||
@@ -296,6 +304,24 @@ export class ProjectCommandService implements ProjectCommandExecutor {
|
||||
command: input.command,
|
||||
}).revision;
|
||||
}
|
||||
case distributionBoardComponentInsertCommandType: {
|
||||
assertDistributionBoardComponentInsertProjectCommand(
|
||||
input.command
|
||||
);
|
||||
return this.distributionBoardComponentStructureStore.execute({
|
||||
...input,
|
||||
command: input.command,
|
||||
}).revision;
|
||||
}
|
||||
case distributionBoardComponentDeleteCommandType: {
|
||||
assertDistributionBoardComponentDeleteProjectCommand(
|
||||
input.command
|
||||
);
|
||||
return this.distributionBoardComponentStructureStore.execute({
|
||||
...input,
|
||||
command: input.command,
|
||||
}).revision;
|
||||
}
|
||||
case projectFloorInsertCommandType: {
|
||||
assertProjectFloorInsertProjectCommand(input.command);
|
||||
return this.projectLocationStructureStore.execute({
|
||||
|
||||
Reference in New Issue
Block a user