import { eq } from "drizzle-orm"; import { ProjectStateConflictError } from "../../domain/errors/project-state-conflict.error.js"; import { assertProjectStateRestoreCommand, createProjectStateRestoreCommand, } from "../../domain/models/project-state-restore-command.model.js"; import { serializeProjectStateSnapshot, type ProjectStateSnapshot, } from "../../domain/models/project-state-snapshot.model.js"; import type { ExecuteProjectStateRestoreCommandInput, ProjectStateRestoreCommandStore, } from "../../domain/ports/project-state-restore-command.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 { consumers } from "../schema/consumers.js"; import { distributionBoards } from "../schema/distribution-boards.js"; import { floors } from "../schema/floors.js"; import { legacyConsumerCircuitMigrations } from "../schema/legacy-consumer-circuit-migrations.js"; import { legacyConsumerMigrationReports } from "../schema/legacy-consumer-migration-report.js"; import { projectDevices } from "../schema/project-devices.js"; import { projects } from "../schema/projects.js"; import { rooms } from "../schema/rooms.js"; import { applyProjectHistoryTransition } from "./project-history.persistence.js"; import { appendProjectRevision } from "./project-revision.persistence.js"; import { hashProjectStatePayload, readProjectStateSnapshot, } from "./project-state-snapshot.persistence.js"; export class ProjectStateRestoreCommandRepository implements ProjectStateRestoreCommandStore { constructor(private readonly database: AppDatabase) {} execute(input: ExecuteProjectStateRestoreCommandInput) { assertProjectStateRestoreCommand(input.command); if (input.command.payload.targetState.project.id !== input.projectId) { throw new Error("Restore state belongs to a different project."); } return this.database.transaction((tx) => { const current = readProjectStateSnapshot(tx, input.projectId); if (!current) { throw new Error("Project not found."); } if ( current.payloadSha256 !== input.command.payload.expectedStateSha256 ) { throw new ProjectStateConflictError( input.projectId, input.command.payload.expectedStateSha256, current.payloadSha256 ); } const targetPayloadJson = serializeProjectStateSnapshot( input.command.payload.targetState ); const inverse = createProjectStateRestoreCommand( hashProjectStatePayload(targetPayloadJson), current.state ); replaceProjectState(tx, input.command.payload.targetState); const revision = appendProjectRevision(tx, { projectId: input.projectId, expectedRevision: input.expectedRevision, source: input.source, description: input.description, actorId: input.actorId, forward: input.command, inverse, }); applyProjectHistoryTransition(tx, { projectId: input.projectId, source: input.source, recordedChangeSetId: revision.changeSetId, targetChangeSetId: input.historyTargetChangeSetId, }); return { revision, inverse }; }); } } function replaceProjectState( database: AppDatabase, state: ProjectStateSnapshot ) { const upgradeOnlyData = captureUpgradeOnlyData( database, state.project.id ); database .delete(distributionBoards) .where(eq(distributionBoards.projectId, state.project.id)) .run(); database .delete(projectDevices) .where(eq(projectDevices.projectId, state.project.id)) .run(); database .delete(rooms) .where(eq(rooms.projectId, state.project.id)) .run(); database .delete(floors) .where(eq(floors.projectId, state.project.id)) .run(); database .update(projects) .set({ name: state.project.name, singlePhaseVoltageV: state.project.singlePhaseVoltageV, threePhaseVoltageV: state.project.threePhaseVoltageV, }) .where(eq(projects.id, state.project.id)) .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) ); restoreCompatibleUpgradeOnlyData(database, state, upgradeOnlyData); } function captureUpgradeOnlyData( database: AppDatabase, projectId: string ) { const consumerLinks = database .select({ id: consumers.id, distributionBoardId: consumers.distributionBoardId, circuitListId: consumers.circuitListId, projectDeviceId: consumers.projectDeviceId, roomId: consumers.roomId, }) .from(consumers) .where(eq(consumers.projectId, projectId)) .all(); const migrationMappings = database .select({ consumerId: legacyConsumerCircuitMigrations.consumerId, circuitId: legacyConsumerCircuitMigrations.circuitId, circuitDeviceRowId: legacyConsumerCircuitMigrations.circuitDeviceRowId, circuitListId: legacyConsumerCircuitMigrations.circuitListId, createdAtIso: legacyConsumerCircuitMigrations.createdAtIso, }) .from(legacyConsumerCircuitMigrations) .innerJoin( circuitLists, eq( circuitLists.id, legacyConsumerCircuitMigrations.circuitListId ) ) .where(eq(circuitLists.projectId, projectId)) .all(); const migrationReports = database .select({ id: legacyConsumerMigrationReports.id, circuitListId: legacyConsumerMigrationReports.circuitListId, legacyConsumerCount: legacyConsumerMigrationReports.legacyConsumerCount, createdCircuitCount: legacyConsumerMigrationReports.createdCircuitCount, createdDeviceRowCount: legacyConsumerMigrationReports.createdDeviceRowCount, duplicateGroupedCount: legacyConsumerMigrationReports.duplicateGroupedCount, generatedIdentifierCount: legacyConsumerMigrationReports.generatedIdentifierCount, unassignedRowCount: legacyConsumerMigrationReports.unassignedRowCount, warningsJson: legacyConsumerMigrationReports.warningsJson, generatedIdentifiersJson: legacyConsumerMigrationReports.generatedIdentifiersJson, duplicateGroupsJson: legacyConsumerMigrationReports.duplicateGroupsJson, createdAtIso: legacyConsumerMigrationReports.createdAtIso, }) .from(legacyConsumerMigrationReports) .innerJoin( circuitLists, eq(circuitLists.id, legacyConsumerMigrationReports.circuitListId) ) .where(eq(circuitLists.projectId, projectId)) .all(); return { consumerLinks, migrationMappings, migrationReports }; } function restoreCompatibleUpgradeOnlyData( database: AppDatabase, state: ProjectStateSnapshot, data: ReturnType ) { const boardIds = new Set( state.distributionBoards.map((entry) => entry.id) ); const listIds = new Set(state.circuitLists.map((entry) => entry.id)); const deviceIds = new Set( state.projectDevices.map((entry) => entry.id) ); const roomIds = new Set(state.rooms.map((entry) => entry.id)); const circuitIds = new Set(state.circuits.map((entry) => entry.id)); const rowIds = new Set( state.circuits.flatMap((circuit) => circuit.deviceRows.map((entry) => entry.id) ) ); for (const link of data.consumerLinks) { database .update(consumers) .set({ distributionBoardId: link.distributionBoardId && boardIds.has(link.distributionBoardId) ? link.distributionBoardId : null, circuitListId: link.circuitListId && listIds.has(link.circuitListId) ? link.circuitListId : null, projectDeviceId: link.projectDeviceId && deviceIds.has(link.projectDeviceId) ? link.projectDeviceId : null, roomId: link.roomId && roomIds.has(link.roomId) ? link.roomId : null, }) .where(eq(consumers.id, link.id)) .run(); } insertMany( database, legacyConsumerCircuitMigrations, data.migrationMappings.filter( (entry) => listIds.has(entry.circuitListId) && circuitIds.has(entry.circuitId) && rowIds.has(entry.circuitDeviceRowId) ) ); insertMany( database, legacyConsumerMigrationReports, data.migrationReports.filter((entry) => listIds.has(entry.circuitListId) ) ); } function insertMany[0]>( database: AppDatabase, table: TTable, values: unknown[] ) { if (values.length === 0) { return; } database .insert(table) .values(values as never) .run(); }