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(); 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(); 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, id: string, label: string ) { if (!ids.has(id)) { throw new Error(`Snapshot ${label} reference is invalid.`); } }