307 lines
8.5 KiB
TypeScript
307 lines
8.5 KiB
TypeScript
import { z } from "zod";
|
|
|
|
export const projectStateSnapshotSchemaVersion = 1 as const;
|
|
|
|
const idSchema = z.string().trim().min(1);
|
|
const nullableStringSchema = z.string().nullable();
|
|
const finiteNumberSchema = z.number().finite();
|
|
|
|
const projectSchema = z
|
|
.object({
|
|
id: idSchema,
|
|
name: z.string().trim().min(1),
|
|
singlePhaseVoltageV: finiteNumberSchema.positive(),
|
|
threePhaseVoltageV: finiteNumberSchema.positive(),
|
|
})
|
|
.strict();
|
|
|
|
const distributionBoardSchema = z
|
|
.object({
|
|
id: idSchema,
|
|
projectId: idSchema,
|
|
name: z.string().trim().min(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,
|
|
})
|
|
.strict();
|
|
|
|
const circuitDeviceRowSchema = z
|
|
.object({
|
|
id: idSchema,
|
|
circuitId: idSchema,
|
|
linkedProjectDeviceId: idSchema.nullable(),
|
|
legacyConsumerId: z.string().nullable(),
|
|
sortOrder: finiteNumberSchema,
|
|
name: z.string().trim().min(1),
|
|
displayName: z.string().trim().min(1),
|
|
phaseType: nullableStringSchema,
|
|
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,
|
|
protectionType: nullableStringSchema,
|
|
protectionRatedCurrent: finiteNumberSchema.nonnegative().nullable(),
|
|
protectionCharacteristic: nullableStringSchema,
|
|
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();
|
|
|
|
export const projectStateSnapshotSchema = z
|
|
.object({
|
|
schemaVersion: z.literal(projectStateSnapshotSchemaVersion),
|
|
project: projectSchema,
|
|
distributionBoards: z.array(distributionBoardSchema),
|
|
circuitLists: z.array(circuitListSchema),
|
|
circuitSections: z.array(circuitSectionSchema),
|
|
circuits: z.array(circuitSchema),
|
|
projectDevices: z.array(projectDeviceSchema),
|
|
floors: z.array(floorSchema),
|
|
rooms: z.array(roomSchema),
|
|
})
|
|
.strict();
|
|
|
|
export type ProjectStateSnapshot = z.infer<
|
|
typeof projectStateSnapshotSchema
|
|
>;
|
|
|
|
export function parseProjectStateSnapshot(
|
|
value: unknown
|
|
): ProjectStateSnapshot {
|
|
const snapshot = projectStateSnapshotSchema.parse(value);
|
|
assertProjectStateSnapshotRelations(snapshot);
|
|
return snapshot;
|
|
}
|
|
|
|
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;
|
|
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");
|
|
uniqueIds(snapshot.circuits, "circuit");
|
|
const sectionById = new Map(
|
|
snapshot.circuitSections.map((entry) => [entry.id, entry])
|
|
);
|
|
|
|
for (const board of snapshot.distributionBoards) {
|
|
assertProjectOwnership(board.projectId, projectId, "distribution board");
|
|
}
|
|
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"
|
|
);
|
|
}
|
|
for (const device of snapshot.projectDevices) {
|
|
assertProjectOwnership(device.projectId, projectId, "project device");
|
|
}
|
|
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."
|
|
);
|
|
}
|
|
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");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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.`);
|
|
}
|
|
}
|