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 { circuitProtectionDevices } from "../schema/circuit-protection-devices.js"; import { circuitSections } from "../schema/circuit-sections.js"; import { circuits } from "../schema/circuits.js"; import { distributionBoardComponentProtectionDevices } from "../schema/distribution-board-component-protection-devices.js"; import { distributionBoardComponents } from "../schema/distribution-board-components.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"; import { externalCsvConfigurations } from "../schema/external-csv-configurations.js"; import { ExternalModelStateRepository } from "./external-model-state.repository.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, internalProjectNumber: projects.internalProjectNumber, externalProjectNumber: projects.externalProjectNumber, buildingOwner: projects.buildingOwner, description: projects.description, isPublicBuilding: projects.isPublicBuilding, singlePhaseVoltageV: projects.singlePhaseVoltageV, threePhaseVoltageV: projects.threePhaseVoltageV, enabledDistributionBoardSupplyTypes: projects.enabledDistributionBoardSupplyTypes, 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 externalCsvConfiguration = database .select() .from(externalCsvConfigurations) .where(eq(externalCsvConfigurations.projectId, projectId)) .get() ?? null; const externalModel = new ExternalModelStateRepository(database) .getByProject(projectId).state; 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, category: circuitSections.category, groupNumber: circuitSections.groupNumber, }) .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 componentRows = database .select({ component: distributionBoardComponents }) .from(distributionBoardComponents) .innerJoin( circuitLists, eq(circuitLists.id, distributionBoardComponents.circuitListId) ) .where(eq(circuitLists.projectId, projectId)) .orderBy( asc(distributionBoardComponents.circuitListId), asc(distributionBoardComponents.placement), asc(distributionBoardComponents.sortOrder), asc(distributionBoardComponents.id) ) .all() .map((row) => row.component); const circuitProtectionRows = database .select({ protection: circuitProtectionDevices }) .from(circuitProtectionDevices) .innerJoin( circuits, eq(circuits.id, circuitProtectionDevices.circuitId) ) .innerJoin(circuitLists, eq(circuitLists.id, circuits.circuitListId)) .where(eq(circuitLists.projectId, projectId)) .orderBy(asc(circuitProtectionDevices.circuitId)) .all() .map((row) => row.protection); const componentProtectionRows = database .select({ protection: distributionBoardComponentProtectionDevices, }) .from(distributionBoardComponentProtectionDevices) .innerJoin( distributionBoardComponents, eq( distributionBoardComponents.id, distributionBoardComponentProtectionDevices.componentId ) ) .innerJoin( circuitLists, eq(circuitLists.id, distributionBoardComponents.circuitListId) ) .where(eq(circuitLists.projectId, projectId)) .orderBy( asc(distributionBoardComponentProtectionDevices.componentId) ) .all() .map((row) => row.protection); const circuitRows = database .select({ id: circuits.id, circuitListId: circuits.circuitListId, sectionId: circuits.sectionId, equipmentIdentifier: circuits.equipmentIdentifier, displayName: circuits.displayName, sortOrder: circuits.sortOrder, 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, 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, manualQuantity: circuitDeviceRows.manualQuantity, 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, internalProjectNumber: project.internalProjectNumber, externalProjectNumber: project.externalProjectNumber, buildingOwner: project.buildingOwner, description: project.description, isPublicBuilding: project.isPublicBuilding, singlePhaseVoltageV: project.singlePhaseVoltageV, threePhaseVoltageV: project.threePhaseVoltageV, enabledDistributionBoardSupplyTypes: project.enabledDistributionBoardSupplyTypes, }, distributionBoards: boardRows, externalCsvConfiguration, externalModel, circuitLists: listRows, circuitSections: sectionRows, distributionBoardComponents: componentRows, circuitProtectionDevices: circuitProtectionRows, distributionBoardComponentProtectionDevices: componentProtectionRows, 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"); }