import { parseProjectStateSnapshot, projectStateSnapshotSchemaVersion, type ProjectStateSnapshot, } from "./project-state-snapshot.model.js"; export const projectTransferFormat = "leistungsbilanz.project" as const; export const projectTransferFormatVersion = 1 as const; export interface ProjectTransferEnvelope { format: typeof projectTransferFormat; formatVersion: typeof projectTransferFormatVersion; exportedAtIso: string; payloadSha256: string; projectState: ProjectStateSnapshot; } export function createProjectTransferEnvelope( exportedAtIso: string, payloadSha256: string, projectState: ProjectStateSnapshot ): ProjectTransferEnvelope { return parseProjectTransferEnvelope({ format: projectTransferFormat, formatVersion: projectTransferFormatVersion, exportedAtIso, payloadSha256, projectState, }); } export function parseProjectTransferEnvelope( value: unknown ): ProjectTransferEnvelope { if (!isPlainObject(value)) { throw new Error("Project transfer file must be an object."); } if ( value.format !== projectTransferFormat || value.formatVersion !== projectTransferFormatVersion ) { throw new Error("Project transfer format is not supported."); } if ( typeof value.exportedAtIso !== "string" || !Number.isFinite(Date.parse(value.exportedAtIso)) ) { throw new Error("Project transfer timestamp is invalid."); } if ( typeof value.payloadSha256 !== "string" || !/^[a-f0-9]{64}$/.test(value.payloadSha256) ) { throw new Error("Project transfer checksum is invalid."); } const projectState = parseProjectStateSnapshot(value.projectState); if (projectState.schemaVersion !== projectStateSnapshotSchemaVersion) { throw new Error("Project transfer snapshot version is not supported."); } if (Object.keys(value).length !== 5) { throw new Error("Project transfer file contains unknown fields."); } return { format: projectTransferFormat, formatVersion: projectTransferFormatVersion, exportedAtIso: value.exportedAtIso, payloadSha256: value.payloadSha256, projectState, }; } export function remapProjectState( source: ProjectStateSnapshot, targetProjectId: string, createId: () => string, name: string = source.project.name ): ProjectStateSnapshot { const boardIds = idMap(source.distributionBoards, createId); const listIds = idMap(source.circuitLists, createId); const sectionIds = idMap(source.circuitSections, createId); const circuitIds = idMap(source.circuits, createId); const componentIds = idMap( source.distributionBoardComponents, createId ); const deviceIds = idMap(source.projectDevices, createId); const floorIds = idMap(source.floors, createId); const roomIds = idMap(source.rooms, createId); const rowIds = new Map( source.circuits.flatMap((circuit) => circuit.deviceRows.map((row) => [row.id, createId()] as const) ) ); const externalSourceId = source.externalModel.source ? createId() : null; const externalBatchIds = idMap(source.externalModel.importBatches, createId); const externalRoomMappingIds = idMap( source.externalModel.roomMappings, createId ); const externalObjectIds = idMap(source.externalModel.objects, createId); return parseProjectStateSnapshot({ ...source, project: { ...source.project, id: targetProjectId, name }, externalCsvConfiguration: source.externalCsvConfiguration === null ? null : { ...source.externalCsvConfiguration, id: createId(), projectId: targetProjectId, }, externalModel: { source: source.externalModel.source === null ? null : { ...source.externalModel.source, id: externalSourceId!, projectId: targetProjectId, }, importBatches: source.externalModel.importBatches.map((batch) => ({ ...batch, id: requiredId(externalBatchIds, batch.id), projectId: targetProjectId, sourceId: externalSourceId!, })), roomMappings: source.externalModel.roomMappings.map((mapping) => ({ ...mapping, id: requiredId(externalRoomMappingIds, mapping.id), projectId: targetProjectId, sourceId: externalSourceId!, roomId: mapping.roomId === null ? null : requiredId(roomIds, mapping.roomId), defaultDistributionBoardId: mapping.defaultDistributionBoardId === null ? null : requiredId(boardIds, mapping.defaultDistributionBoardId), })), objects: source.externalModel.objects.map((object) => ({ ...object, id: requiredId(externalObjectIds, object.id), projectId: targetProjectId, sourceId: externalSourceId!, lastSeenImportBatchId: requiredId( externalBatchIds, object.lastSeenImportBatchId ), lastAcceptedImportBatchId: requiredId( externalBatchIds, object.lastAcceptedImportBatchId ), externalRoomMappingId: object.externalRoomMappingId === null ? null : requiredId( externalRoomMappingIds, object.externalRoomMappingId ), distributionBoardId: object.distributionBoardId === null ? null : requiredId(boardIds, object.distributionBoardId), linkedProjectDeviceId: object.linkedProjectDeviceId === null ? null : requiredId(deviceIds, object.linkedProjectDeviceId), circuitDeviceRowId: object.circuitDeviceRowId === null ? null : requiredId(rowIds, object.circuitDeviceRowId), })), }, distributionBoards: source.distributionBoards.map((board) => ({ ...board, id: requiredId(boardIds, board.id), projectId: targetProjectId, floorId: board.floorId === null ? null : requiredId(floorIds, board.floorId), })), circuitLists: source.circuitLists.map((list) => ({ ...list, id: requiredId(listIds, list.id), projectId: targetProjectId, distributionBoardId: requiredId(boardIds, list.distributionBoardId), })), circuitSections: source.circuitSections.map((section) => ({ ...section, id: requiredId(sectionIds, section.id), circuitListId: requiredId(listIds, section.circuitListId), })), distributionBoardComponents: source.distributionBoardComponents.map((component) => ({ ...component, id: requiredId(componentIds, component.id), circuitListId: requiredId( listIds, component.circuitListId ), sectionId: component.sectionId === null ? null : requiredId(sectionIds, component.sectionId), })), circuitProtectionDevices: source.circuitProtectionDevices.map( (protection) => ({ ...protection, circuitId: requiredId(circuitIds, protection.circuitId), }) ), distributionBoardComponentProtectionDevices: source.distributionBoardComponentProtectionDevices.map( (protection) => ({ ...protection, componentId: requiredId( componentIds, protection.componentId ), }) ), circuits: source.circuits.map((circuit) => ({ ...circuit, id: requiredId(circuitIds, circuit.id), circuitListId: requiredId(listIds, circuit.circuitListId), sectionId: requiredId(sectionIds, circuit.sectionId), deviceRows: circuit.deviceRows.map((row) => ({ ...row, id: requiredId(rowIds, row.id), circuitId: requiredId(circuitIds, circuit.id), linkedProjectDeviceId: row.linkedProjectDeviceId === null ? null : requiredId(deviceIds, row.linkedProjectDeviceId), roomId: row.roomId === null ? null : requiredId(roomIds, row.roomId), })), })), projectDevices: source.projectDevices.map((device) => ({ ...device, id: requiredId(deviceIds, device.id), projectId: targetProjectId, })), floors: source.floors.map((floor) => ({ ...floor, id: requiredId(floorIds, floor.id), projectId: targetProjectId, })), rooms: source.rooms.map((room) => ({ ...room, id: requiredId(roomIds, room.id), projectId: targetProjectId, floorId: room.floorId === null ? null : requiredId(floorIds, room.floorId), })), }); } function idMap( entries: ReadonlyArray<{ id: string }>, createId: () => string ) { return new Map(entries.map((entry) => [entry.id, createId()])); } function requiredId(ids: ReadonlyMap, sourceId: string) { const targetId = ids.get(sourceId); if (!targetId) { throw new Error(`Project transfer reference "${sourceId}" is invalid.`); } return targetId; } function isPlainObject(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); }