forked from jappel/leistungsbilanz-ts
275 lines
10 KiB
TypeScript
275 lines
10 KiB
TypeScript
import crypto from "node:crypto";
|
|
import type { Request, Response } from "express";
|
|
import { assertExternalCsvConfiguration } from "../../external-model/csv/external-csv-configuration.js";
|
|
import { ExternalCsvParseError } from "../../external-model/csv/external-csv-transport.js";
|
|
import { createExternalCsvPreview } from "../../external-model/application/external-csv-preview.js";
|
|
import { createExternalInitialImportPlan } from "../../external-model/application/external-initial-import-plan.js";
|
|
import { buildExternalInitialImportTarget } from "../../external-model/application/external-initial-import-target.js";
|
|
import { parseExternalCsv } from "../../external-model/csv/external-csv-transport.js";
|
|
import {
|
|
createEmptyExternalModelState,
|
|
createExternalInitialImportProjectCommand,
|
|
} from "../../domain/models/external-initial-import-project-command.model.js";
|
|
import { createExternalCsvConfigurationUpdateProjectCommand } from "../../domain/models/external-csv-configuration-project-command.model.js";
|
|
import {
|
|
previewExternalCsvSchema,
|
|
planExternalInitialImportSchema,
|
|
applyExternalInitialImportSchema,
|
|
updateExternalCsvConfigurationSchema,
|
|
} from "../../shared/validation/external-csv.schemas.js";
|
|
import {
|
|
distributionBoardRepository,
|
|
externalCsvConfigurationRepository,
|
|
externalModelStateRepository,
|
|
floorRepository,
|
|
projectDeviceRepository,
|
|
roomRepository,
|
|
} from "../composition/application-repositories.js";
|
|
import { projectCommandService } from "../composition/project-command-stores.js";
|
|
import { respondWithProjectCommandError } from "./project-command.controller.js";
|
|
|
|
export function getExternalCsvConfiguration(req: Request, res: Response) {
|
|
const projectId = getProjectId(req, res);
|
|
if (!projectId) return;
|
|
const result = externalCsvConfigurationRepository.getByProject(projectId);
|
|
if (!result.projectExists) {
|
|
return res.status(404).json({ error: "Project not found" });
|
|
}
|
|
return res.json({ configuration: result.configuration });
|
|
}
|
|
|
|
export function updateExternalCsvConfiguration(req: Request, res: Response) {
|
|
const projectId = getProjectId(req, res);
|
|
if (!projectId) return;
|
|
const parsed = updateExternalCsvConfigurationSchema.safeParse(req.body);
|
|
if (!parsed.success) {
|
|
return res.status(400).json({ error: parsed.error.flatten() });
|
|
}
|
|
try {
|
|
assertExternalCsvConfiguration(parsed.data.configuration);
|
|
const current = externalCsvConfigurationRepository.getByProject(projectId);
|
|
if (!current.projectExists) {
|
|
return res.status(404).json({ error: "Project not found" });
|
|
}
|
|
const target = {
|
|
id: current.configuration?.id ?? crypto.randomUUID(),
|
|
projectId,
|
|
configurationVersion:
|
|
(current.configuration?.configurationVersion ?? 0) + 1,
|
|
configuration: parsed.data.configuration,
|
|
};
|
|
const result = projectCommandService.executeUser({
|
|
projectId,
|
|
expectedRevision: parsed.data.expectedRevision,
|
|
description: "Revit-CSV-Konfiguration bearbeiten",
|
|
command: createExternalCsvConfigurationUpdateProjectCommand(
|
|
current.configuration,
|
|
target
|
|
),
|
|
});
|
|
return res.json({ ...result, configuration: target });
|
|
} catch (error) {
|
|
return respondWithProjectCommandError(error, res);
|
|
}
|
|
}
|
|
|
|
export function previewExternalCsv(req: Request, res: Response) {
|
|
const projectId = getProjectId(req, res);
|
|
if (!projectId) return;
|
|
const parsed = previewExternalCsvSchema.safeParse(req.body);
|
|
if (!parsed.success) {
|
|
return res.status(400).json({ error: parsed.error.flatten() });
|
|
}
|
|
const stored = externalCsvConfigurationRepository.getByProject(projectId);
|
|
if (!stored.projectExists) {
|
|
return res.status(404).json({ error: "Project not found" });
|
|
}
|
|
if (!stored.configuration) {
|
|
return res.status(409).json({
|
|
error: "Für das Projekt ist noch keine Revit-CSV-Konfiguration gespeichert.",
|
|
code: "EXTERNAL_CSV_CONFIGURATION_REQUIRED",
|
|
});
|
|
}
|
|
try {
|
|
const bytes = decodeBase64(parsed.data.contentBase64);
|
|
return res.json(
|
|
createExternalCsvPreview({
|
|
fileName: parsed.data.fileName,
|
|
bytes,
|
|
configuration: stored.configuration.configuration,
|
|
})
|
|
);
|
|
} catch (error) {
|
|
if (error instanceof ExternalCsvParseError) {
|
|
return res.status(400).json({ error: error.message, code: error.code });
|
|
}
|
|
return res.status(400).json({
|
|
error: error instanceof Error ? error.message : "CSV-Vorschau fehlgeschlagen.",
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function planExternalInitialImport(req: Request, res: Response) {
|
|
const projectId = getProjectId(req, res);
|
|
if (!projectId) return;
|
|
const parsed = planExternalInitialImportSchema.safeParse(req.body);
|
|
if (!parsed.success) {
|
|
return res.status(400).json({ error: parsed.error.flatten() });
|
|
}
|
|
const stored = externalCsvConfigurationRepository.getByProject(projectId);
|
|
if (!stored.projectExists) {
|
|
return res.status(404).json({ error: "Project not found" });
|
|
}
|
|
if (!stored.configuration) {
|
|
return res.status(409).json({
|
|
error: "Für das Projekt ist noch keine Revit-CSV-Konfiguration gespeichert.",
|
|
code: "EXTERNAL_CSV_CONFIGURATION_REQUIRED",
|
|
});
|
|
}
|
|
const externalState = externalModelStateRepository.getByProject(projectId);
|
|
if (externalState.state.source !== null) {
|
|
return res.status(409).json({
|
|
error: "Für dieses Projekt wurde bereits ein Revit-Modell importiert.",
|
|
code: "EXTERNAL_INITIAL_IMPORT_ALREADY_APPLIED",
|
|
});
|
|
}
|
|
try {
|
|
const bytes = decodeBase64(parsed.data.contentBase64);
|
|
const [floors, rooms, distributionBoards, projectDevices] = await Promise.all([
|
|
floorRepository.listByProject(projectId),
|
|
roomRepository.listByProject(projectId),
|
|
distributionBoardRepository.listByProject(projectId),
|
|
projectDeviceRepository.listByProject(projectId),
|
|
]);
|
|
return res.json(createExternalInitialImportPlan({
|
|
fileName: parsed.data.fileName,
|
|
bytes,
|
|
configurationVersion: stored.configuration.configurationVersion,
|
|
configuration: stored.configuration.configuration,
|
|
floors,
|
|
rooms,
|
|
distributionBoards,
|
|
projectDevices,
|
|
}));
|
|
} catch (error) {
|
|
if (error instanceof ExternalCsvParseError) {
|
|
return res.status(400).json({ error: error.message, code: error.code });
|
|
}
|
|
return res.status(400).json({
|
|
error: error instanceof Error ? error.message : "Erstimport-Planung fehlgeschlagen.",
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function applyExternalInitialImport(req: Request, res: Response) {
|
|
const projectId = getProjectId(req, res);
|
|
if (!projectId) return;
|
|
const parsed = applyExternalInitialImportSchema.safeParse(req.body);
|
|
if (!parsed.success) {
|
|
return res.status(400).json({ error: parsed.error.flatten() });
|
|
}
|
|
const stored = externalCsvConfigurationRepository.getByProject(projectId);
|
|
if (!stored.projectExists) {
|
|
return res.status(404).json({ error: "Project not found" });
|
|
}
|
|
if (!stored.configuration) {
|
|
return res.status(409).json({
|
|
error: "Für das Projekt ist noch keine Revit-CSV-Konfiguration gespeichert.",
|
|
code: "EXTERNAL_CSV_CONFIGURATION_REQUIRED",
|
|
});
|
|
}
|
|
if (
|
|
stored.configuration.configurationVersion !==
|
|
parsed.data.expectedConfigurationVersion
|
|
) {
|
|
return res.status(409).json({
|
|
error: "Die Revit-CSV-Konfiguration wurde seit der Planung geändert.",
|
|
code: "EXTERNAL_CSV_CONFIGURATION_CHANGED",
|
|
});
|
|
}
|
|
try {
|
|
const bytes = decodeBase64(parsed.data.contentBase64);
|
|
const [floors, rooms, distributionBoards, projectDevices] = await Promise.all([
|
|
floorRepository.listByProject(projectId),
|
|
roomRepository.listByProject(projectId),
|
|
distributionBoardRepository.listByProject(projectId),
|
|
projectDeviceRepository.listByProject(projectId),
|
|
]);
|
|
const plan = createExternalInitialImportPlan({
|
|
fileName: parsed.data.fileName,
|
|
bytes,
|
|
configurationVersion: stored.configuration.configurationVersion,
|
|
configuration: stored.configuration.configuration,
|
|
floors,
|
|
rooms,
|
|
distributionBoards,
|
|
projectDevices,
|
|
});
|
|
if (plan.sha256 !== parsed.data.expectedSha256) {
|
|
return res.status(409).json({
|
|
error: "Die CSV-Datei stimmt nicht mehr mit der geprüften Planung überein.",
|
|
code: "EXTERNAL_CSV_FILE_CHANGED",
|
|
});
|
|
}
|
|
const target = buildExternalInitialImportTarget({
|
|
projectId,
|
|
expectedRevision: parsed.data.expectedRevision,
|
|
sourceName: parsed.data.sourceName,
|
|
importedAtIso: new Date().toISOString(),
|
|
configurationVersion: stored.configuration.configurationVersion,
|
|
configuration: stored.configuration.configuration,
|
|
originalContentBase64: parsed.data.contentBase64,
|
|
document: parseExternalCsv(bytes, stored.configuration.configuration),
|
|
plan,
|
|
roomDecisions: parsed.data.roomDecisions,
|
|
familyProjectDeviceDecisions:
|
|
parsed.data.familyProjectDeviceDecisions,
|
|
createId: () => crypto.randomUUID(),
|
|
});
|
|
const result = projectCommandService.executeUser({
|
|
projectId,
|
|
expectedRevision: parsed.data.expectedRevision,
|
|
description: `Revit-Erstimport ${parsed.data.fileName}`,
|
|
command: createExternalInitialImportProjectCommand(
|
|
createEmptyExternalModelState(),
|
|
target
|
|
),
|
|
});
|
|
return res.status(201).json({
|
|
...result,
|
|
import: {
|
|
sourceId: target.source!.id,
|
|
importBatchId: target.importBatches[0].id,
|
|
objectCount: target.objects.length,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof ExternalCsvParseError) {
|
|
return res.status(400).json({ error: error.message, code: error.code });
|
|
}
|
|
return respondWithProjectCommandError(error, res);
|
|
}
|
|
}
|
|
|
|
function decodeBase64(value: string) {
|
|
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(value) || value.length % 4 !== 0) {
|
|
throw new Error("CSV-Inhalt ist nicht gültig Base64-kodiert.");
|
|
}
|
|
const bytes = Buffer.from(value, "base64");
|
|
if (bytes.toString("base64") !== value) {
|
|
throw new Error("CSV-Inhalt ist nicht kanonisch Base64-kodiert.");
|
|
}
|
|
if (bytes.length > 18 * 1024 * 1024) {
|
|
throw new Error("CSV-Datei überschreitet die maximale Größe von 18 MB.");
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
function getProjectId(req: Request, res: Response) {
|
|
const { projectId } = req.params;
|
|
if (typeof projectId !== "string" || !projectId.trim()) {
|
|
res.status(400).json({ error: "Invalid projectId" });
|
|
return null;
|
|
}
|
|
return projectId;
|
|
}
|