Add portable project transfer API
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
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 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)
|
||||
)
|
||||
);
|
||||
return parseProjectStateSnapshot({
|
||||
...source,
|
||||
project: { ...source.project, id: targetProjectId, name },
|
||||
distributionBoards: source.distributionBoards.map((board) => ({
|
||||
...board,
|
||||
id: requiredId(boardIds, board.id),
|
||||
projectId: targetProjectId,
|
||||
})),
|
||||
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),
|
||||
})),
|
||||
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),
|
||||
legacyConsumerId: null,
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user