Add Revit CSV preview API

This commit is contained in:
2026-08-02 16:49:42 +02:00
parent 219da5ec3b
commit caa6caf99f
14 changed files with 437 additions and 3 deletions
+52
View File
@@ -0,0 +1,52 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
previewExternalCsvSchema,
updateExternalCsvConfigurationSchema,
} from "../src/shared/validation/external-csv.schemas.js";
import { createDefaultExternalCsvConfiguration } from "../src/external-model/csv/external-csv-contracts.js";
import { externalCsvTestColumns } from "./fixtures/revit-csv-fixtures.js";
describe("external CSV API contracts", () => {
it("accepts strict update and preview request envelopes", () => {
assert.equal(
updateExternalCsvConfigurationSchema.safeParse({
expectedRevision: 4,
configuration: createDefaultExternalCsvConfiguration(externalCsvTestColumns),
}).success,
true
);
assert.equal(
previewExternalCsvSchema.safeParse({
fileName: "revit.csv",
contentBase64: "YWJj",
}).success,
true
);
});
it("rejects stale-shaped, oversized and unknown request fields", () => {
assert.equal(
updateExternalCsvConfigurationSchema.safeParse({
expectedRevision: -1,
configuration: {},
}).success,
false
);
assert.equal(
previewExternalCsvSchema.safeParse({
fileName: "revit.csv",
contentBase64: "YWJj",
apply: true,
}).success,
false
);
assert.equal(
previewExternalCsvSchema.safeParse({
fileName: "revit.csv",
contentBase64: "a".repeat(24_000_001),
}).success,
false
);
});
});
@@ -8,6 +8,7 @@ import {
type DatabaseContext,
} from "../src/db/database-context.js";
import { ExternalCsvConfigurationProjectCommandRepository } from "../src/db/repositories/external-csv-configuration-project-command.repository.js";
import { ExternalCsvConfigurationRepository } from "../src/db/repositories/external-csv-configuration.repository.js";
import { ProjectTransferRepository } from "../src/db/repositories/project-transfer.repository.js";
import { ProjectStateRestoreCommandRepository } from "../src/db/repositories/project-state-restore-command.repository.js";
import { readProjectStateSnapshot } from "../src/db/repositories/project-state-snapshot.persistence.js";
@@ -169,6 +170,23 @@ describe("external CSV configuration project command", () => {
}
});
it("distinguishes projects without configuration from unknown projects", () => {
const context = createTestDatabase();
try {
const reader = new ExternalCsvConfigurationRepository(context.db);
assert.deepEqual(reader.getByProject("project-1"), {
projectExists: true,
configuration: null,
});
assert.deepEqual(reader.getByProject("missing"), {
projectExists: false,
configuration: null,
});
} finally {
context.close();
}
});
it("restores and undoes the complete configuration through snapshot history", () => {
const context = createTestDatabase();
try {
+56
View File
@@ -0,0 +1,56 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { createExternalCsvPreview } from "../src/external-model/application/external-csv-preview.js";
import {
externalCsvTestConfiguration,
firstIfcGuid,
quotedRevitCsv,
utf8Bytes,
} from "./fixtures/revit-csv-fixtures.js";
describe("external CSV preview", () => {
it("projects transport statistics and mapped object source values without writes", () => {
const configuration = structuredClone(externalCsvTestConfiguration);
configuration.additionalSourceMappings.push({
sourceColumn: "Raumname",
targetField: "sourceRoomLabel",
});
// The additional mapping intentionally needs a distinct source column in
// persisted configuration, so use a separately named synthetic header.
configuration.additionalSourceMappings[0].sourceColumn = "Zusatzwert";
const csv = quotedRevitCsv
.replace('"Anzahl";"IfcGUID"', '"Anzahl";"Zusatzwert";"IfcGUID"')
.replaceAll(';"2";"0Ab', ';"2";"A";"0Ab')
.replaceAll(';"1";"1Ab', ';"1";"B";"1Ab')
.replaceAll(';"1";""', ';"1";"C";""')
.replaceAll(';"";""\r\n', ';"";"";""\r\n');
const preview = createExternalCsvPreview({
fileName: "export.csv",
bytes: utf8Bytes(csv, true),
configuration,
});
assert.equal(preview.fileName, "export.csv");
assert.equal(preview.headerRowNumber, 2);
assert.equal(preview.rowCount, 7);
assert.equal(preview.objectCount, 2);
assert.equal(preview.passthroughCount, 2);
assert.equal(preview.nonEmptyPassthroughCount, 1);
assert.equal(preview.suspectObjectCount, 1);
assert.deepEqual(preview.suspectRowNumbers, [6]);
assert.match(preview.sha256, /^[a-f0-9]{64}$/);
assert.deepEqual(preview.objects[0], {
rowNumber: 4,
ifcGuid: firstIfcGuid,
roomNumber: "01/101",
roomName: "Technik",
familyAndType: "Steckdose: Doppelsteckdose",
selectionMarker: "Arbeitsplatz",
circuitIdentifier: "UV_AV_01-2F1",
power: "120,5",
quantity: "2",
additionalSourceValues: { sourceRoomLabel: "A" },
});
});
});