Files
leistungsbilanz-ts/src/domain/models/project-transfer.model.ts
T

278 lines
9.0 KiB
TypeScript

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<string, string>, 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<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}