278 lines
9.0 KiB
TypeScript
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);
|
|
}
|