Add named project snapshots
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
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.`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user