import crypto from "node:crypto"; import { eq } from "drizzle-orm"; import { createProjectTransferEnvelope, parseProjectTransferEnvelope, remapProjectState, } from "../../domain/models/project-transfer.model.js"; import { createProjectStateRestoreCommand } from "../../domain/models/project-state-restore-command.model.js"; import type { ProjectTransferStore } from "../../domain/ports/project-transfer.store.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"; import { hashProjectStatePayload, readProjectStateSnapshot, } from "./project-state-snapshot.persistence.js"; import { serializeProjectStateSnapshot } from "../../domain/models/project-state-snapshot.model.js"; export class ProjectTransferRepository implements ProjectTransferStore { constructor(private readonly database: AppDatabase) {} exportProject(projectId: string) { const snapshot = readProjectStateSnapshot(this.database, projectId); if (!snapshot) { return null; } return createProjectTransferEnvelope( new Date().toISOString(), snapshot.payloadSha256, snapshot.state ); } prepareReplace(projectId: string, value: unknown) { const transfer = this.parseVerified(value); const current = readProjectStateSnapshot(this.database, projectId); if (!current) { return null; } const targetState = transfer.projectState.project.id === projectId ? transfer.projectState : remapProjectState( transfer.projectState, projectId, () => crypto.randomUUID() ); return { command: createProjectStateRestoreCommand( current.payloadSha256, targetState ), }; } importDuplicate(value: unknown) { const transfer = this.parseVerified(value); const projectId = crypto.randomUUID(); const state = remapProjectState( transfer.projectState, projectId, () => crypto.randomUUID(), `${transfer.projectState.project.name} (Kopie)` ); this.database.transaction((tx) => insertProjectState(tx, state)); return { projectId, name: state.project.name }; } private parseVerified(value: unknown) { const rawPayloadSha256 = isPlainObject(value) && isPlainObject(value.projectState) ? hashProjectStatePayload(JSON.stringify(value.projectState)) : null; const transfer = parseProjectTransferEnvelope(value); const payloadJson = serializeProjectStateSnapshot( transfer.projectState ); if ( hashProjectStatePayload(payloadJson) !== transfer.payloadSha256 && rawPayloadSha256 !== transfer.payloadSha256 ) { throw new Error("Project transfer checksum verification failed."); } return transfer; } } function isPlainObject(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); } function insertProjectState( database: AppDatabase, state: ReturnType ) { const existing = database .select({ id: projects.id }) .from(projects) .where(eq(projects.id, state.project.id)) .get(); if (existing) { throw new Error("Project transfer target already exists."); } database .insert(projects) .values({ ...state.project, currentRevision: 0 }) .run(); insertMany(database, floors, state.floors); insertMany(database, rooms, state.rooms); insertMany(database, projectDevices, state.projectDevices); insertMany(database, distributionBoards, state.distributionBoards); insertMany(database, circuitLists, state.circuitLists); insertMany(database, circuitSections, state.circuitSections); insertMany( database, circuits, state.circuits.map(({ deviceRows: _deviceRows, ...circuit }) => ({ ...circuit, isReserve: circuit.isReserve ? 1 : 0, })) ); insertMany( database, circuitDeviceRows, state.circuits.flatMap((circuit) => circuit.deviceRows) ); } function insertMany[0]>( database: AppDatabase, table: TTable, values: unknown[] ) { if (values.length > 0) { database.insert(table).values(values as never).run(); } }