import crypto from "node:crypto"; import { asc, eq } from "drizzle-orm"; import { parseProjectStateSnapshot, projectStateSnapshotSchemaVersion, serializeProjectStateSnapshot, type ProjectStateSnapshot, } from "../../domain/models/project-state-snapshot.model.js"; import type { AppDatabase } from "../database-context.js"; import { circuitDeviceRows } from "../schema/circuit-device-rows.js"; import { circuitLists } from "../schema/circuit-lists.js"; import { circuitSections } from "../schema/circuit-sections.js"; import { circuits } from "../schema/circuits.js"; import { distributionBoards } from "../schema/distribution-boards.js"; import { floors } from "../schema/floors.js"; import { projectDevices } from "../schema/project-devices.js"; import { projects } from "../schema/projects.js"; import { rooms } from "../schema/rooms.js"; export interface PersistedProjectStateSnapshot { currentRevision: number; state: ProjectStateSnapshot; payloadJson: string; payloadSha256: string; } export function readProjectStateSnapshot( database: AppDatabase, projectId: string ): PersistedProjectStateSnapshot | null { const project = database .select({ id: projects.id, name: projects.name, singlePhaseVoltageV: projects.singlePhaseVoltageV, threePhaseVoltageV: projects.threePhaseVoltageV, currentRevision: projects.currentRevision, }) .from(projects) .where(eq(projects.id, projectId)) .get(); if (!project) { return null; } const boardRows = database .select() .from(distributionBoards) .where(eq(distributionBoards.projectId, projectId)) .orderBy(asc(distributionBoards.name), asc(distributionBoards.id)) .all(); const listRows = database .select() .from(circuitLists) .where(eq(circuitLists.projectId, projectId)) .orderBy(asc(circuitLists.name), asc(circuitLists.id)) .all(); const sectionRows = database .select({ id: circuitSections.id, circuitListId: circuitSections.circuitListId, key: circuitSections.key, displayName: circuitSections.displayName, prefix: circuitSections.prefix, sortOrder: circuitSections.sortOrder, }) .from(circuitSections) .innerJoin( circuitLists, eq(circuitLists.id, circuitSections.circuitListId) ) .where(eq(circuitLists.projectId, projectId)) .orderBy( asc(circuitSections.circuitListId), asc(circuitSections.sortOrder), asc(circuitSections.id) ) .all(); const circuitRows = database .select({ id: circuits.id, circuitListId: circuits.circuitListId, sectionId: circuits.sectionId, equipmentIdentifier: circuits.equipmentIdentifier, displayName: circuits.displayName, sortOrder: circuits.sortOrder, protectionType: circuits.protectionType, protectionRatedCurrent: circuits.protectionRatedCurrent, protectionCharacteristic: circuits.protectionCharacteristic, cableType: circuits.cableType, cableCrossSection: circuits.cableCrossSection, cableLength: circuits.cableLength, rcdAssignment: circuits.rcdAssignment, terminalDesignation: circuits.terminalDesignation, voltage: circuits.voltage, controlRequirement: circuits.controlRequirement, status: circuits.status, isReserve: circuits.isReserve, remark: circuits.remark, }) .from(circuits) .innerJoin(circuitLists, eq(circuitLists.id, circuits.circuitListId)) .where(eq(circuitLists.projectId, projectId)) .orderBy( asc(circuits.circuitListId), asc(circuits.sortOrder), asc(circuits.id) ) .all(); const deviceRowRows = database .select({ id: circuitDeviceRows.id, circuitId: circuitDeviceRows.circuitId, linkedProjectDeviceId: circuitDeviceRows.linkedProjectDeviceId, legacyConsumerId: circuitDeviceRows.legacyConsumerId, sortOrder: circuitDeviceRows.sortOrder, name: circuitDeviceRows.name, displayName: circuitDeviceRows.displayName, phaseType: circuitDeviceRows.phaseType, connectionKind: circuitDeviceRows.connectionKind, costGroup: circuitDeviceRows.costGroup, category: circuitDeviceRows.category, level: circuitDeviceRows.level, roomId: circuitDeviceRows.roomId, roomNumberSnapshot: circuitDeviceRows.roomNumberSnapshot, roomNameSnapshot: circuitDeviceRows.roomNameSnapshot, quantity: circuitDeviceRows.quantity, powerPerUnit: circuitDeviceRows.powerPerUnit, simultaneityFactor: circuitDeviceRows.simultaneityFactor, cosPhi: circuitDeviceRows.cosPhi, remark: circuitDeviceRows.remark, overriddenFields: circuitDeviceRows.overriddenFields, }) .from(circuitDeviceRows) .innerJoin(circuits, eq(circuits.id, circuitDeviceRows.circuitId)) .innerJoin(circuitLists, eq(circuitLists.id, circuits.circuitListId)) .where(eq(circuitLists.projectId, projectId)) .orderBy( asc(circuitDeviceRows.circuitId), asc(circuitDeviceRows.sortOrder), asc(circuitDeviceRows.id) ) .all(); const projectDeviceRows = database .select() .from(projectDevices) .where(eq(projectDevices.projectId, projectId)) .orderBy(asc(projectDevices.displayName), asc(projectDevices.id)) .all(); const floorRows = database .select() .from(floors) .where(eq(floors.projectId, projectId)) .orderBy(asc(floors.sortOrder), asc(floors.id)) .all(); const roomRows = database .select() .from(rooms) .where(eq(rooms.projectId, projectId)) .orderBy(asc(rooms.roomNumber), asc(rooms.id)) .all(); const deviceRowsByCircuit = new Map(); for (const row of deviceRowRows) { const entries = deviceRowsByCircuit.get(row.circuitId) ?? []; entries.push(row); deviceRowsByCircuit.set(row.circuitId, entries); } const state = parseProjectStateSnapshot({ schemaVersion: projectStateSnapshotSchemaVersion, project: { id: project.id, name: project.name, singlePhaseVoltageV: project.singlePhaseVoltageV, threePhaseVoltageV: project.threePhaseVoltageV, }, distributionBoards: boardRows, circuitLists: listRows, circuitSections: sectionRows, circuits: circuitRows.map((circuit) => ({ ...circuit, isReserve: Boolean(circuit.isReserve), deviceRows: deviceRowsByCircuit.get(circuit.id) ?? [], })), projectDevices: projectDeviceRows, floors: floorRows, rooms: roomRows, }); const payloadJson = serializeProjectStateSnapshot(state); return { currentRevision: project.currentRevision, state, payloadJson, payloadSha256: hashProjectStatePayload(payloadJson), }; } export function hashProjectStatePayload(payloadJson: string) { return crypto.createHash("sha256").update(payloadJson).digest("hex"); }