758 lines
22 KiB
TypeScript
758 lines
22 KiB
TypeScript
import { z } from "zod";
|
|
import { distributionBoardSupplyTypes } from "../../shared/constants/distribution-board.js";
|
|
import {
|
|
circuitGroupCategories,
|
|
} from "../../shared/constants/circuit-group.js";
|
|
import {
|
|
distributionBoardComponentPlacements,
|
|
distributionBoardComponentRoles,
|
|
} from "../../shared/constants/distribution-board-component.js";
|
|
import { protectionDeviceConfigurationSchema } from "../../shared/validation/protection-device.schemas.js";
|
|
import {
|
|
breakerTripCharacteristics,
|
|
fuseUtilizationCategories,
|
|
protectionDeviceTypes,
|
|
rcdTypes,
|
|
} from "../../shared/constants/protection-device.js";
|
|
import {
|
|
resolveCircuitPhaseType,
|
|
resolveProjectVoltage,
|
|
} from "../services/project-voltage.service.js";
|
|
import { validateExternalCsvConfiguration } from "../../external-model/csv/external-csv-configuration.js";
|
|
import type { ExternalCsvConfiguration } from "../../external-model/csv/external-csv-contracts.js";
|
|
|
|
export const projectStateSnapshotSchemaVersion = 3 as const;
|
|
const previousProjectStateSnapshotSchemaVersion = 2 as const;
|
|
const baselineProjectStateSnapshotSchemaVersion = 1 as const;
|
|
|
|
const idSchema = z.string().trim().min(1);
|
|
const nullableStringSchema = z.string().nullable();
|
|
const finiteNumberSchema = z.number().finite();
|
|
|
|
const previousProjectSchema = z
|
|
.object({
|
|
id: idSchema,
|
|
name: z.string().trim().min(1),
|
|
internalProjectNumber: z.string().trim().min(1).max(100).nullable(),
|
|
externalProjectNumber: z.string().trim().min(1).max(100).nullable(),
|
|
buildingOwner: z.string().trim().min(1).max(200).nullable(),
|
|
description: z.string().trim().min(1).max(2000).nullable(),
|
|
singlePhaseVoltageV: finiteNumberSchema.positive(),
|
|
threePhaseVoltageV: finiteNumberSchema.positive(),
|
|
enabledDistributionBoardSupplyTypes: z
|
|
.array(z.enum(distributionBoardSupplyTypes))
|
|
.min(1)
|
|
.refine((values) => new Set(values).size === values.length),
|
|
})
|
|
.strict();
|
|
|
|
const projectSchema = previousProjectSchema
|
|
.extend({ isPublicBuilding: z.boolean() })
|
|
.strict();
|
|
|
|
const distributionBoardSchema = z
|
|
.object({
|
|
id: idSchema,
|
|
projectId: idSchema,
|
|
name: z.string().trim().min(1),
|
|
floorId: idSchema.nullable(),
|
|
supplyType: z.enum(distributionBoardSupplyTypes).nullable(),
|
|
simultaneityFactor: finiteNumberSchema.min(0).max(1),
|
|
})
|
|
.strict();
|
|
|
|
const circuitListSchema = z
|
|
.object({
|
|
id: idSchema,
|
|
projectId: idSchema,
|
|
distributionBoardId: idSchema,
|
|
name: z.string().trim().min(1),
|
|
})
|
|
.strict();
|
|
|
|
const circuitSectionSchema = z
|
|
.object({
|
|
id: idSchema,
|
|
circuitListId: idSchema,
|
|
key: z.string().trim().min(1),
|
|
displayName: z.string().trim().min(1),
|
|
prefix: z.string().trim().min(1),
|
|
sortOrder: finiteNumberSchema,
|
|
category: z.enum(circuitGroupCategories).nullable(),
|
|
groupNumber: z.number().int().positive().nullable(),
|
|
})
|
|
.strict()
|
|
.superRefine((section, context) => {
|
|
if ((section.category === null) !== (section.groupNumber === null)) {
|
|
context.addIssue({
|
|
code: "custom",
|
|
message:
|
|
"Circuit-section category and group number must both be set or both be null.",
|
|
});
|
|
}
|
|
});
|
|
|
|
const circuitDeviceRowSchema = z
|
|
.object({
|
|
id: idSchema,
|
|
circuitId: idSchema,
|
|
linkedProjectDeviceId: idSchema.nullable(),
|
|
sortOrder: finiteNumberSchema,
|
|
name: z.string().trim().min(1),
|
|
displayName: z.string().trim().min(1),
|
|
phaseType: z.enum(["single_phase", "three_phase"]).nullable(),
|
|
connectionKind: nullableStringSchema,
|
|
costGroup: nullableStringSchema,
|
|
category: nullableStringSchema,
|
|
level: nullableStringSchema,
|
|
roomId: idSchema.nullable(),
|
|
roomNumberSnapshot: nullableStringSchema,
|
|
roomNameSnapshot: nullableStringSchema,
|
|
quantity: finiteNumberSchema.nonnegative(),
|
|
powerPerUnit: finiteNumberSchema.nonnegative(),
|
|
simultaneityFactor: finiteNumberSchema.nonnegative(),
|
|
cosPhi: finiteNumberSchema.positive().nullable(),
|
|
remark: nullableStringSchema,
|
|
overriddenFields: nullableStringSchema,
|
|
})
|
|
.strict();
|
|
|
|
const circuitSchema = z
|
|
.object({
|
|
id: idSchema,
|
|
circuitListId: idSchema,
|
|
sectionId: idSchema,
|
|
equipmentIdentifier: z.string().trim().min(1),
|
|
displayName: nullableStringSchema,
|
|
sortOrder: finiteNumberSchema,
|
|
cableType: nullableStringSchema,
|
|
cableCrossSection: nullableStringSchema,
|
|
cableLength: finiteNumberSchema.nonnegative().nullable(),
|
|
rcdAssignment: nullableStringSchema,
|
|
terminalDesignation: nullableStringSchema,
|
|
voltage: finiteNumberSchema.positive().nullable(),
|
|
controlRequirement: nullableStringSchema,
|
|
status: nullableStringSchema,
|
|
isReserve: z.boolean(),
|
|
remark: nullableStringSchema,
|
|
deviceRows: z.array(circuitDeviceRowSchema),
|
|
})
|
|
.strict();
|
|
|
|
const projectDeviceSchema = z
|
|
.object({
|
|
id: idSchema,
|
|
projectId: idSchema,
|
|
name: z.string().trim().min(1),
|
|
displayName: z.string().trim().min(1),
|
|
phaseType: z.enum(["single_phase", "three_phase"]),
|
|
connectionKind: nullableStringSchema,
|
|
costGroup: nullableStringSchema,
|
|
category: nullableStringSchema,
|
|
quantity: finiteNumberSchema.nonnegative(),
|
|
powerPerUnit: finiteNumberSchema.nonnegative(),
|
|
simultaneityFactor: finiteNumberSchema.min(0).max(1),
|
|
cosPhi: finiteNumberSchema.min(0).max(1).nullable(),
|
|
remark: nullableStringSchema,
|
|
voltageV: finiteNumberSchema.positive().nullable(),
|
|
})
|
|
.strict();
|
|
|
|
const floorSchema = z
|
|
.object({
|
|
id: idSchema,
|
|
projectId: idSchema,
|
|
name: z.string().trim().min(1),
|
|
sortOrder: finiteNumberSchema,
|
|
})
|
|
.strict();
|
|
|
|
const roomSchema = z
|
|
.object({
|
|
id: idSchema,
|
|
projectId: idSchema,
|
|
floorId: idSchema.nullable(),
|
|
roomNumber: z.string().trim().min(1),
|
|
roomName: z.string().trim().min(1),
|
|
})
|
|
.strict();
|
|
|
|
const distributionBoardComponentSchema = z
|
|
.object({
|
|
id: idSchema,
|
|
circuitListId: idSchema,
|
|
sectionId: idSchema.nullable(),
|
|
equipmentIdentifier: z.string().trim().min(1),
|
|
name: z.string().trim().min(1),
|
|
role: z.enum(distributionBoardComponentRoles),
|
|
placement: z.enum(distributionBoardComponentPlacements),
|
|
sortOrder: finiteNumberSchema,
|
|
})
|
|
.strict();
|
|
|
|
const persistedProtectionDeviceFields = {
|
|
type: z.enum(protectionDeviceTypes),
|
|
ratedCurrentA: finiteNumberSchema.positive(),
|
|
fuseUtilizationCategory: z.enum(fuseUtilizationCategories).nullable(),
|
|
tripCharacteristic: z.enum(breakerTripCharacteristics).nullable(),
|
|
rcdType: z.enum(rcdTypes).nullable(),
|
|
ratedResidualCurrentMa: finiteNumberSchema.positive().nullable(),
|
|
};
|
|
|
|
function validatePersistedProtectionDevice(
|
|
value: {
|
|
type: (typeof protectionDeviceTypes)[number];
|
|
ratedCurrentA: number;
|
|
fuseUtilizationCategory:
|
|
| (typeof fuseUtilizationCategories)[number]
|
|
| null;
|
|
tripCharacteristic:
|
|
| (typeof breakerTripCharacteristics)[number]
|
|
| null;
|
|
rcdType: (typeof rcdTypes)[number] | null;
|
|
ratedResidualCurrentMa: number | null;
|
|
},
|
|
context: z.RefinementCtx
|
|
) {
|
|
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) {
|
|
context.addIssue({
|
|
code: "custom",
|
|
message: "Snapshot protection-device configuration is invalid.",
|
|
});
|
|
}
|
|
}
|
|
|
|
const circuitProtectionDeviceSchema = z
|
|
.object({
|
|
circuitId: idSchema,
|
|
...persistedProtectionDeviceFields,
|
|
})
|
|
.strict()
|
|
.superRefine(validatePersistedProtectionDevice);
|
|
|
|
const componentProtectionDeviceSchema = z
|
|
.object({
|
|
componentId: idSchema,
|
|
...persistedProtectionDeviceFields,
|
|
})
|
|
.strict()
|
|
.superRefine(validatePersistedProtectionDevice);
|
|
|
|
const externalCsvConfigurationSnapshotSchema = z
|
|
.object({
|
|
id: idSchema,
|
|
projectId: idSchema,
|
|
configurationVersion: z.number().int().positive(),
|
|
configuration: z.custom<ExternalCsvConfiguration>(
|
|
(value) => validateExternalCsvConfiguration(value).success
|
|
),
|
|
})
|
|
.strict();
|
|
|
|
const projectStateSnapshotContents = {
|
|
circuitLists: z.array(circuitListSchema),
|
|
circuitSections: z.array(circuitSectionSchema),
|
|
circuits: z.array(circuitSchema),
|
|
projectDevices: z.array(projectDeviceSchema),
|
|
floors: z.array(floorSchema),
|
|
rooms: z.array(roomSchema),
|
|
distributionBoardComponents: z.array(distributionBoardComponentSchema),
|
|
circuitProtectionDevices: z.array(circuitProtectionDeviceSchema),
|
|
distributionBoardComponentProtectionDevices: z.array(
|
|
componentProtectionDeviceSchema
|
|
),
|
|
};
|
|
|
|
export const projectStateSnapshotSchema = z
|
|
.object({
|
|
schemaVersion: z.literal(projectStateSnapshotSchemaVersion),
|
|
project: projectSchema,
|
|
distributionBoards: z.array(distributionBoardSchema),
|
|
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
|
|
...projectStateSnapshotContents,
|
|
})
|
|
.strict();
|
|
|
|
const previousProjectStateSnapshotSchema = z
|
|
.object({
|
|
schemaVersion: z.literal(previousProjectStateSnapshotSchemaVersion),
|
|
project: projectSchema,
|
|
distributionBoards: z.array(distributionBoardSchema),
|
|
...projectStateSnapshotContents,
|
|
})
|
|
.strict();
|
|
|
|
const baselineProjectStateSnapshotSchema = z
|
|
.object({
|
|
schemaVersion: z.literal(baselineProjectStateSnapshotSchemaVersion),
|
|
project: previousProjectSchema,
|
|
distributionBoards: z.array(distributionBoardSchema),
|
|
...projectStateSnapshotContents,
|
|
})
|
|
.strict();
|
|
|
|
export type ProjectStateSnapshot = z.infer<
|
|
typeof projectStateSnapshotSchema
|
|
>;
|
|
|
|
export function parseProjectStateSnapshot(
|
|
value: unknown
|
|
): ProjectStateSnapshot {
|
|
const parsedSnapshot = projectStateSnapshotSchema.parse(
|
|
upgradePreviousProjectStateSnapshot(value)
|
|
);
|
|
const snapshot = normalizeSnapshotPhaseTypes(parsedSnapshot);
|
|
assertProjectStateSnapshotRelations(snapshot);
|
|
return snapshot;
|
|
}
|
|
|
|
function upgradePreviousProjectStateSnapshot(value: unknown): unknown {
|
|
if (
|
|
value === null ||
|
|
typeof value !== "object" ||
|
|
Array.isArray(value) ||
|
|
typeof (value as { schemaVersion?: unknown }).schemaVersion !== "number"
|
|
) {
|
|
return value;
|
|
}
|
|
const schemaVersion = (value as { schemaVersion: number }).schemaVersion;
|
|
if (schemaVersion === previousProjectStateSnapshotSchemaVersion) {
|
|
const previous = previousProjectStateSnapshotSchema.parse(value);
|
|
return {
|
|
...previous,
|
|
schemaVersion: projectStateSnapshotSchemaVersion,
|
|
externalCsvConfiguration: null,
|
|
};
|
|
}
|
|
if (schemaVersion === baselineProjectStateSnapshotSchemaVersion) {
|
|
const baseline = baselineProjectStateSnapshotSchema.parse(value);
|
|
return {
|
|
...baseline,
|
|
schemaVersion: projectStateSnapshotSchemaVersion,
|
|
project: { ...baseline.project, isPublicBuilding: false },
|
|
externalCsvConfiguration: null,
|
|
};
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function normalizeSnapshotPhaseTypes(
|
|
snapshot: ProjectStateSnapshot
|
|
): ProjectStateSnapshot {
|
|
const sectionById = new Map(
|
|
snapshot.circuitSections.map((section) => [section.id, section])
|
|
);
|
|
const projectDeviceById = new Map(
|
|
snapshot.projectDevices.map((device) => [device.id, device])
|
|
);
|
|
return {
|
|
...snapshot,
|
|
circuits: snapshot.circuits.map((circuit) => {
|
|
const section = sectionById.get(circuit.sectionId);
|
|
const deviceRows = circuit.deviceRows.map((row) => {
|
|
const linkedPhaseType = row.linkedProjectDeviceId
|
|
? projectDeviceById.get(row.linkedProjectDeviceId)?.phaseType
|
|
: undefined;
|
|
return {
|
|
...row,
|
|
phaseType:
|
|
row.phaseType ??
|
|
linkedPhaseType ??
|
|
(section?.key === "three_phase"
|
|
? "three_phase"
|
|
: "single_phase"),
|
|
};
|
|
});
|
|
return {
|
|
...circuit,
|
|
voltage: section
|
|
? resolveProjectVoltage(
|
|
resolveCircuitPhaseType(
|
|
section.key,
|
|
deviceRows.map((row) => row.phaseType)
|
|
),
|
|
snapshot.project
|
|
)
|
|
: circuit.voltage,
|
|
deviceRows,
|
|
};
|
|
}),
|
|
};
|
|
}
|
|
|
|
export function serializeProjectStateSnapshot(
|
|
snapshot: ProjectStateSnapshot
|
|
): string {
|
|
return JSON.stringify(parseProjectStateSnapshot(snapshot));
|
|
}
|
|
|
|
export function deserializeProjectStateSnapshot(
|
|
serialized: string
|
|
): ProjectStateSnapshot {
|
|
return parseProjectStateSnapshot(JSON.parse(serialized) as unknown);
|
|
}
|
|
|
|
function assertProjectStateSnapshotRelations(
|
|
snapshot: ProjectStateSnapshot
|
|
) {
|
|
const projectId = snapshot.project.id;
|
|
if (
|
|
snapshot.externalCsvConfiguration !== null &&
|
|
snapshot.externalCsvConfiguration.projectId !== projectId
|
|
) {
|
|
throw new Error("Snapshot external CSV configuration belongs to a different project.");
|
|
}
|
|
const boardIds = uniqueIds(
|
|
snapshot.distributionBoards,
|
|
"distribution board"
|
|
);
|
|
const circuitListIds = uniqueIds(
|
|
snapshot.circuitLists,
|
|
"circuit list"
|
|
);
|
|
const sectionIds = uniqueIds(
|
|
snapshot.circuitSections,
|
|
"circuit section"
|
|
);
|
|
const projectDeviceIds = uniqueIds(
|
|
snapshot.projectDevices,
|
|
"project device"
|
|
);
|
|
const floorIds = uniqueIds(snapshot.floors, "floor");
|
|
const roomIds = uniqueIds(snapshot.rooms, "room");
|
|
const circuitIds = uniqueIds(snapshot.circuits, "circuit");
|
|
const componentIds = uniqueIds(
|
|
snapshot.distributionBoardComponents,
|
|
"distribution board component"
|
|
);
|
|
const sectionById = new Map(
|
|
snapshot.circuitSections.map((entry) => [entry.id, entry])
|
|
);
|
|
const componentById = new Map(
|
|
snapshot.distributionBoardComponents.map((entry) => [
|
|
entry.id,
|
|
entry,
|
|
])
|
|
);
|
|
const groupKeys = new Set<string>();
|
|
const equipmentIdentifiersByList = new Map<string, Set<string>>();
|
|
|
|
for (const board of snapshot.distributionBoards) {
|
|
assertProjectOwnership(board.projectId, projectId, "distribution board");
|
|
if (board.floorId !== null) {
|
|
assertReference(
|
|
floorIds,
|
|
board.floorId,
|
|
"distribution board floor"
|
|
);
|
|
}
|
|
if (
|
|
board.supplyType !== null &&
|
|
!snapshot.project.enabledDistributionBoardSupplyTypes.includes(
|
|
board.supplyType
|
|
)
|
|
) {
|
|
throw new Error(
|
|
"Snapshot distribution board uses a disabled supply type."
|
|
);
|
|
}
|
|
}
|
|
for (const list of snapshot.circuitLists) {
|
|
assertProjectOwnership(list.projectId, projectId, "circuit list");
|
|
assertReference(
|
|
boardIds,
|
|
list.distributionBoardId,
|
|
"circuit list distribution board"
|
|
);
|
|
}
|
|
for (const section of snapshot.circuitSections) {
|
|
assertReference(
|
|
circuitListIds,
|
|
section.circuitListId,
|
|
"circuit section list"
|
|
);
|
|
if (section.category !== null && section.groupNumber !== null) {
|
|
const groupKey = [
|
|
section.circuitListId,
|
|
section.category,
|
|
section.groupNumber,
|
|
].join(":");
|
|
if (groupKeys.has(groupKey)) {
|
|
throw new Error(
|
|
"Snapshot contains a duplicate circuit group number."
|
|
);
|
|
}
|
|
groupKeys.add(groupKey);
|
|
}
|
|
}
|
|
for (const device of snapshot.projectDevices) {
|
|
assertProjectOwnership(device.projectId, projectId, "project device");
|
|
if (
|
|
device.voltageV !==
|
|
resolveProjectVoltage(device.phaseType, snapshot.project)
|
|
) {
|
|
throw new Error(
|
|
"Snapshot project device voltage must match project settings."
|
|
);
|
|
}
|
|
}
|
|
for (const floor of snapshot.floors) {
|
|
assertProjectOwnership(floor.projectId, projectId, "floor");
|
|
}
|
|
for (const room of snapshot.rooms) {
|
|
assertProjectOwnership(room.projectId, projectId, "room");
|
|
if (room.floorId !== null) {
|
|
assertReference(floorIds, room.floorId, "room floor");
|
|
}
|
|
}
|
|
|
|
const deviceRowIds = new Set<string>();
|
|
for (const circuit of snapshot.circuits) {
|
|
assertReference(
|
|
circuitListIds,
|
|
circuit.circuitListId,
|
|
"circuit list"
|
|
);
|
|
assertReference(sectionIds, circuit.sectionId, "circuit section");
|
|
const section = sectionById.get(circuit.sectionId)!;
|
|
if (section.circuitListId !== circuit.circuitListId) {
|
|
throw new Error(
|
|
"Snapshot circuit section belongs to a different circuit list."
|
|
);
|
|
}
|
|
if (circuit.isReserve !== (circuit.deviceRows.length === 0)) {
|
|
throw new Error(
|
|
"Snapshot circuit reserve state must match its device rows."
|
|
);
|
|
}
|
|
const expectedVoltage = resolveProjectVoltage(
|
|
resolveCircuitPhaseType(
|
|
section.key,
|
|
circuit.deviceRows.map((row) => row.phaseType)
|
|
),
|
|
snapshot.project
|
|
);
|
|
if (circuit.voltage !== expectedVoltage) {
|
|
throw new Error(
|
|
"Snapshot circuit voltage must match project settings."
|
|
);
|
|
}
|
|
registerEquipmentIdentifier(
|
|
equipmentIdentifiersByList,
|
|
circuit.circuitListId,
|
|
circuit.equipmentIdentifier
|
|
);
|
|
for (const row of circuit.deviceRows) {
|
|
if (row.circuitId !== circuit.id) {
|
|
throw new Error(
|
|
"Snapshot device row belongs to a different circuit."
|
|
);
|
|
}
|
|
if (deviceRowIds.has(row.id)) {
|
|
throw new Error("Snapshot contains duplicate device row ids.");
|
|
}
|
|
deviceRowIds.add(row.id);
|
|
if (row.linkedProjectDeviceId !== null) {
|
|
assertReference(
|
|
projectDeviceIds,
|
|
row.linkedProjectDeviceId,
|
|
"device row project device"
|
|
);
|
|
}
|
|
if (row.roomId !== null) {
|
|
assertReference(roomIds, row.roomId, "device row room");
|
|
}
|
|
}
|
|
}
|
|
|
|
const fixedComponentRolesByList = new Set<string>();
|
|
const groupComponentRolesBySection = new Set<string>();
|
|
for (const component of snapshot.distributionBoardComponents) {
|
|
assertReference(
|
|
circuitListIds,
|
|
component.circuitListId,
|
|
"distribution board component circuit list"
|
|
);
|
|
if (component.sectionId !== null) {
|
|
assertReference(
|
|
sectionIds,
|
|
component.sectionId,
|
|
"distribution board component section"
|
|
);
|
|
if (
|
|
sectionById.get(component.sectionId)?.circuitListId !==
|
|
component.circuitListId
|
|
) {
|
|
throw new Error(
|
|
"Snapshot distribution board component section belongs to a different circuit list."
|
|
);
|
|
}
|
|
}
|
|
assertComponentPlacement(component);
|
|
registerEquipmentIdentifier(
|
|
equipmentIdentifiersByList,
|
|
component.circuitListId,
|
|
component.equipmentIdentifier
|
|
);
|
|
if (
|
|
component.role === "main_switch" ||
|
|
component.role === "surge_protective_device"
|
|
) {
|
|
assertUniqueKey(
|
|
fixedComponentRolesByList,
|
|
`${component.circuitListId}:${component.role}`,
|
|
"Snapshot contains duplicate fixed distribution board components."
|
|
);
|
|
}
|
|
if (
|
|
component.role === "group_upstream_protection" ||
|
|
component.role === "group_residual_current_protection"
|
|
) {
|
|
assertUniqueKey(
|
|
groupComponentRolesBySection,
|
|
`${component.sectionId}:${component.role}`,
|
|
"Snapshot contains duplicate group protection components."
|
|
);
|
|
}
|
|
}
|
|
|
|
const protectedCircuitIds = new Set<string>();
|
|
for (const protection of snapshot.circuitProtectionDevices) {
|
|
assertReference(
|
|
circuitIds,
|
|
protection.circuitId,
|
|
"circuit protection device circuit"
|
|
);
|
|
assertUniqueKey(
|
|
protectedCircuitIds,
|
|
protection.circuitId,
|
|
"Snapshot contains duplicate circuit protection devices."
|
|
);
|
|
}
|
|
|
|
const protectedComponentIds = new Set<string>();
|
|
for (const protection of snapshot.distributionBoardComponentProtectionDevices) {
|
|
assertReference(
|
|
componentIds,
|
|
protection.componentId,
|
|
"component protection device component"
|
|
);
|
|
assertUniqueKey(
|
|
protectedComponentIds,
|
|
protection.componentId,
|
|
"Snapshot contains duplicate component protection devices."
|
|
);
|
|
const component = componentById.get(protection.componentId)!;
|
|
if (
|
|
component.role !== "group_upstream_protection" &&
|
|
component.role !== "group_residual_current_protection"
|
|
) {
|
|
throw new Error(
|
|
"Snapshot protection data belongs to a non-protection component."
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function assertComponentPlacement(
|
|
component: ProjectStateSnapshot["distributionBoardComponents"][number]
|
|
) {
|
|
const isFixedHeader =
|
|
component.role === "main_switch" ||
|
|
component.role === "surge_protective_device";
|
|
const isGroupProtection =
|
|
component.role === "group_upstream_protection" ||
|
|
component.role === "group_residual_current_protection";
|
|
if (
|
|
(isFixedHeader &&
|
|
(component.placement !== "header" ||
|
|
component.sectionId !== null)) ||
|
|
(isGroupProtection &&
|
|
(component.placement !== "group" ||
|
|
component.sectionId === null)) ||
|
|
(component.role === "auxiliary" &&
|
|
(component.placement !== "footer" ||
|
|
component.sectionId !== null))
|
|
) {
|
|
throw new Error(
|
|
"Snapshot distribution board component placement is invalid."
|
|
);
|
|
}
|
|
}
|
|
|
|
function registerEquipmentIdentifier(
|
|
identifiersByList: Map<string, Set<string>>,
|
|
circuitListId: string,
|
|
equipmentIdentifier: string
|
|
) {
|
|
const identifiers =
|
|
identifiersByList.get(circuitListId) ?? new Set<string>();
|
|
const normalized = equipmentIdentifier.trim().toLocaleLowerCase("de");
|
|
if (identifiers.has(normalized)) {
|
|
throw new Error(
|
|
"Snapshot contains duplicate equipment identifiers in one circuit list."
|
|
);
|
|
}
|
|
identifiers.add(normalized);
|
|
identifiersByList.set(circuitListId, identifiers);
|
|
}
|
|
|
|
function assertUniqueKey(
|
|
keys: Set<string>,
|
|
key: string,
|
|
message: string
|
|
) {
|
|
if (keys.has(key)) {
|
|
throw new Error(message);
|
|
}
|
|
keys.add(key);
|
|
}
|
|
|
|
function uniqueIds(
|
|
entries: ReadonlyArray<{ id: string }>,
|
|
label: string
|
|
) {
|
|
const ids = new Set<string>();
|
|
for (const entry of entries) {
|
|
if (ids.has(entry.id)) {
|
|
throw new Error(`Snapshot contains duplicate ${label} ids.`);
|
|
}
|
|
ids.add(entry.id);
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
function assertProjectOwnership(
|
|
actualProjectId: string,
|
|
expectedProjectId: string,
|
|
label: string
|
|
) {
|
|
if (actualProjectId !== expectedProjectId) {
|
|
throw new Error(`Snapshot ${label} belongs to a different project.`);
|
|
}
|
|
}
|
|
|
|
function assertReference(
|
|
ids: ReadonlySet<string>,
|
|
id: string,
|
|
label: string
|
|
) {
|
|
if (!ids.has(id)) {
|
|
throw new Error(`Snapshot ${label} reference is invalid.`);
|
|
}
|
|
}
|