Add portable project transfer API
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
import crypto from "node:crypto";
|
||||
import { eq } from "drizzle-orm";
|
||||
import {
|
||||
createProjectTransferEnvelope,
|
||||
parseProjectTransferEnvelope,
|
||||
remapProjectState,
|
||||
} from "../../domain/models/project-transfer.model.js";
|
||||
import { createProjectStateRestoreCommand } from "../../domain/models/project-state-restore-command.model.js";
|
||||
import type { ProjectTransferStore } from "../../domain/ports/project-transfer.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 { distributionBoards } from "../schema/distribution-boards.js";
|
||||
import { floors } from "../schema/floors.js";
|
||||
import { projectDevices } from "../schema/project-devices.js";
|
||||
import { projects } from "../schema/projects.js";
|
||||
import { rooms } from "../schema/rooms.js";
|
||||
import {
|
||||
hashProjectStatePayload,
|
||||
readProjectStateSnapshot,
|
||||
} from "./project-state-snapshot.persistence.js";
|
||||
import { serializeProjectStateSnapshot } from "../../domain/models/project-state-snapshot.model.js";
|
||||
|
||||
export class ProjectTransferRepository implements ProjectTransferStore {
|
||||
constructor(private readonly database: AppDatabase) {}
|
||||
|
||||
exportProject(projectId: string) {
|
||||
const snapshot = readProjectStateSnapshot(this.database, projectId);
|
||||
if (!snapshot) {
|
||||
return null;
|
||||
}
|
||||
return createProjectTransferEnvelope(
|
||||
new Date().toISOString(),
|
||||
snapshot.payloadSha256,
|
||||
snapshot.state
|
||||
);
|
||||
}
|
||||
|
||||
prepareReplace(projectId: string, value: unknown) {
|
||||
const transfer = this.parseVerified(value);
|
||||
const current = readProjectStateSnapshot(this.database, projectId);
|
||||
if (!current) {
|
||||
return null;
|
||||
}
|
||||
const targetState =
|
||||
transfer.projectState.project.id === projectId
|
||||
? transfer.projectState
|
||||
: remapProjectState(
|
||||
transfer.projectState,
|
||||
projectId,
|
||||
() => crypto.randomUUID()
|
||||
);
|
||||
return {
|
||||
command: createProjectStateRestoreCommand(
|
||||
current.payloadSha256,
|
||||
targetState
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
importDuplicate(value: unknown) {
|
||||
const transfer = this.parseVerified(value);
|
||||
const projectId = crypto.randomUUID();
|
||||
const state = remapProjectState(
|
||||
transfer.projectState,
|
||||
projectId,
|
||||
() => crypto.randomUUID(),
|
||||
`${transfer.projectState.project.name} (Kopie)`
|
||||
);
|
||||
this.database.transaction((tx) => insertProjectState(tx, state));
|
||||
return { projectId, name: state.project.name };
|
||||
}
|
||||
|
||||
private parseVerified(value: unknown) {
|
||||
const transfer = parseProjectTransferEnvelope(value);
|
||||
const payloadJson = serializeProjectStateSnapshot(
|
||||
transfer.projectState
|
||||
);
|
||||
if (
|
||||
hashProjectStatePayload(payloadJson) !== transfer.payloadSha256
|
||||
) {
|
||||
throw new Error("Project transfer checksum verification failed.");
|
||||
}
|
||||
return transfer;
|
||||
}
|
||||
}
|
||||
|
||||
function insertProjectState(
|
||||
database: AppDatabase,
|
||||
state: ReturnType<typeof remapProjectState>
|
||||
) {
|
||||
const existing = database
|
||||
.select({ id: projects.id })
|
||||
.from(projects)
|
||||
.where(eq(projects.id, state.project.id))
|
||||
.get();
|
||||
if (existing) {
|
||||
throw new Error("Project transfer target already exists.");
|
||||
}
|
||||
database
|
||||
.insert(projects)
|
||||
.values({ ...state.project, currentRevision: 0 })
|
||||
.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)
|
||||
);
|
||||
}
|
||||
|
||||
function insertMany<TTable extends Parameters<AppDatabase["insert"]>[0]>(
|
||||
database: AppDatabase,
|
||||
table: TTable,
|
||||
values: unknown[]
|
||||
) {
|
||||
if (values.length > 0) {
|
||||
database.insert(table).values(values as never).run();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user