Add Linear XLSX initial import
This commit is contained in:
parent
c633e78cd4
commit
bfdd048269
20 changed files with 932 additions and 85 deletions
|
|
@ -0,0 +1,87 @@
|
|||
import { createHash } from "node:crypto";
|
||||
import { eq } from "drizzle-orm";
|
||||
import {
|
||||
assertExternalInitialImportProjectCommand,
|
||||
createExternalInitialImportProjectCommand,
|
||||
isEmptyExternalModelState,
|
||||
} from "../../domain/models/external-initial-import-project-command.model.js";
|
||||
import type { ExternalModelConfigurationSnapshot } from "../../domain/models/external-model-configuration-project-command.model.js";
|
||||
import type { ExternalInitialImportProjectCommandStore } from "../../domain/ports/external-initial-import-project-command.store.js";
|
||||
import type { ExternalModelStateSnapshot } from "../../external-model/domain/external-model-contracts.js";
|
||||
import type { AppDatabase } from "../database-context.js";
|
||||
import { distributionBoards } from "../schema/distribution-boards.js";
|
||||
import { externalImportBatches } from "../schema/external-import-batches.js";
|
||||
import { externalModelConfigurations } from "../schema/external-model-configurations.js";
|
||||
import { externalModelObjects } from "../schema/external-model-objects.js";
|
||||
import { externalModelSources } from "../schema/external-model-sources.js";
|
||||
import { externalRoomMappings } from "../schema/external-room-mappings.js";
|
||||
import { projectDevices } from "../schema/project-devices.js";
|
||||
import { projects } from "../schema/projects.js";
|
||||
import { rooms } from "../schema/rooms.js";
|
||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||
import { ExternalModelStateRepository } from "./external-model-state.repository.js";
|
||||
|
||||
export class ExternalInitialImportProjectCommandRepository implements ExternalInitialImportProjectCommandStore {
|
||||
constructor(private readonly database: AppDatabase) {}
|
||||
|
||||
execute(input: Parameters<ExternalInitialImportProjectCommandStore["execute"]>[0]) {
|
||||
assertExternalInitialImportProjectCommand(input.command);
|
||||
return executeProjectCommandTransaction(this.database, input, (tx) => {
|
||||
const currentStateResult = new ExternalModelStateRepository(tx).getByProject(input.projectId);
|
||||
if (!currentStateResult.projectExists) throw new Error("Projekt nicht gefunden.");
|
||||
const currentConfiguration = tx.select().from(externalModelConfigurations).where(eq(externalModelConfigurations.projectId, input.projectId)).get() ?? null;
|
||||
const payload = input.command.payload;
|
||||
if (!same(payload.expectedState, currentStateResult.state) || !same(payload.expectedConfiguration, currentConfiguration)) {
|
||||
throw new Error("Revit-Zustand oder Konfiguration wurde zwischenzeitlich geändert.");
|
||||
}
|
||||
if (!isEmptyExternalModelState(payload.targetState)) validateTarget(tx, input.projectId, input.expectedRevision, input.source, payload.targetConfiguration, payload.targetState);
|
||||
replaceConfiguration(tx, input.projectId, payload.targetConfiguration);
|
||||
replaceState(tx, input.projectId, payload.targetState);
|
||||
const persistedState = new ExternalModelStateRepository(tx).getByProject(input.projectId).state;
|
||||
const persistedConfiguration = tx.select().from(externalModelConfigurations).where(eq(externalModelConfigurations.projectId, input.projectId)).get() ?? null;
|
||||
if (!same(payload.targetState, persistedState) || !same(payload.targetConfiguration, persistedConfiguration)) throw new Error("Gespeicherter Revit-Erstimport weicht vom Zielzustand ab.");
|
||||
return createExternalInitialImportProjectCommand(payload.targetConfiguration, currentConfiguration, payload.targetState, currentStateResult.state);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function validateTarget(database: AppDatabase, projectId: string, expectedRevision: number, source: string, configuration: ExternalModelConfigurationSnapshot | null, state: ExternalModelStateSnapshot) {
|
||||
if (!configuration || configuration.projectId !== projectId || state.source?.projectId !== projectId) throw new Error("Der Revit-Erstimport gehört zu einem anderen Projekt.");
|
||||
const batch = state.importBatches[0];
|
||||
if (source === "user" && batch.appliedProjectRevision !== expectedRevision + 1) throw new Error("Die Projektrevision des Revit-Erstimports ist ungültig.");
|
||||
if (!same(batch.configurationSnapshot, configuration.configuration)) throw new Error("Die Konfigurationskopie des Importbatches ist ungültig.");
|
||||
const bytes = decodeCanonicalBase64(batch.originalContentBase64!);
|
||||
if (createHash("sha256").update(bytes).digest("hex") !== batch.sha256) throw new Error("Die Prüfsumme des Revit-Erstimports ist ungültig.");
|
||||
const roomIds = ids(database.select({ id: rooms.id }).from(rooms).where(eq(rooms.projectId, projectId)).all());
|
||||
const boardIds = ids(database.select({ id: distributionBoards.id }).from(distributionBoards).where(eq(distributionBoards.projectId, projectId)).all());
|
||||
const projectDeviceIds = ids(database.select({ id: projectDevices.id }).from(projectDevices).where(eq(projectDevices.projectId, projectId)).all());
|
||||
const deviceTypeIds = new Set(configuration.configuration.deviceTypes.map((type) => type.id));
|
||||
for (const mapping of state.roomMappings) {
|
||||
if (mapping.roomId !== null && !roomIds.has(mapping.roomId)) throw new Error("Eine Raumzuordnung verweist auf ein anderes Projekt.");
|
||||
if (mapping.defaultDistributionBoardId !== null && !boardIds.has(mapping.defaultDistributionBoardId)) throw new Error("Ein Standardverteiler verweist auf ein anderes Projekt.");
|
||||
}
|
||||
for (const object of state.objects) {
|
||||
if (!deviceTypeIds.has(object.deviceTypeId)) throw new Error("Ein Revit-Objekt verweist auf eine unbekannte Geräteart.");
|
||||
if (object.distributionBoardId !== null && !boardIds.has(object.distributionBoardId)) throw new Error("Ein Revit-Objekt verweist auf einen fremden Verteiler.");
|
||||
if (object.linkedProjectDeviceId !== null && !projectDeviceIds.has(object.linkedProjectDeviceId)) throw new Error("Ein Revit-Objekt verweist auf ein fremdes Projektgerät.");
|
||||
}
|
||||
}
|
||||
|
||||
function replaceConfiguration(database: AppDatabase, projectId: string, target: ExternalModelConfigurationSnapshot | null) {
|
||||
database.delete(externalModelConfigurations).where(eq(externalModelConfigurations.projectId, projectId)).run();
|
||||
if (target) database.insert(externalModelConfigurations).values(target).run();
|
||||
}
|
||||
|
||||
function replaceState(database: AppDatabase, projectId: string, target: ExternalModelStateSnapshot) {
|
||||
database.delete(externalModelSources).where(eq(externalModelSources.projectId, projectId)).run();
|
||||
if (!target.source) return;
|
||||
database.insert(externalModelSources).values(target.source).run();
|
||||
database.insert(externalImportBatches).values(target.importBatches.map(({ originalContentBase64, ...batch }) => ({ ...batch, originalBytes: originalContentBase64 === null ? null : Buffer.from(originalContentBase64, "base64") }))).run();
|
||||
if (target.roomMappings.length) database.insert(externalRoomMappings).values(target.roomMappings).run();
|
||||
if (target.objects.length) database.insert(externalModelObjects).values(target.objects).run();
|
||||
}
|
||||
|
||||
function decodeCanonicalBase64(value: string) { const bytes = Buffer.from(value, "base64"); if (bytes.toString("base64") !== value) throw new Error("Der XLSX-Inhalt ist nicht kanonisch Base64-kodiert."); return bytes; }
|
||||
function ids(entries: Array<{ id: string }>) { return new Set(entries.map((entry) => entry.id)); }
|
||||
function same(left: unknown, right: unknown) { return canonical(left) === canonical(right); }
|
||||
function canonical(value: unknown): string { if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`; if (value !== null && typeof value === "object") { const record = value as Record<string, unknown>; return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonical(record[key])}`).join(",")}}`; } return JSON.stringify(value); }
|
||||
Loading…
Add table
Add a link
Reference in a new issue