Add Revit CSV preview API
This commit is contained in:
@@ -389,3 +389,22 @@ inverse command. The former direct restore/reconnect endpoints are removed.
|
||||
|
||||
`displayName` is included in the comparison but is not selected by default in the UI. Updating a
|
||||
project device never triggers synchronization implicitly.
|
||||
|
||||
## Revit CSV Foundation
|
||||
|
||||
- `GET /projects/:projectId/external-csv/configuration`
|
||||
- returns the complete versioned project configuration or `null`
|
||||
- `PUT /projects/:projectId/external-csv/configuration`
|
||||
- request: `expectedRevision` and the complete validated configuration
|
||||
- executes `external-csv-configuration.update`
|
||||
- returns configuration, revision and project history state
|
||||
- `POST /projects/:projectId/external-csv/preview`
|
||||
- request: file name and Base64-encoded CSV content up to 18 MB
|
||||
- requires a stored project configuration
|
||||
- returns transport metadata, SHA-256, row classifications, suspect row
|
||||
numbers and mapped source values for every recognized IFC object
|
||||
- does not persist a draft, mutate project state or create a revision
|
||||
|
||||
Parser errors expose stable codes such as `header-not-found`,
|
||||
`ambiguous-header`, `invalid-ifc-guid` and `duplicate-ifc-guid`. A later
|
||||
confirmed import must upload and parse the file again and verify its hash.
|
||||
|
||||
@@ -502,6 +502,13 @@ Entfernung zusammen mit Revision und persistentem Undo/Redo. Snapshot-Schema 3,
|
||||
Restore und portabler Projekttransfer enthalten diesen Zustand; beim Duplizieren
|
||||
werden Konfigurations-UUID und Projektlink remapped. Importvorschau, persistente
|
||||
externe Objekte und die Revit-GUI sind noch nicht implementiert.
|
||||
`GET` und `PUT /api/projects/:projectId/external-csv/configuration` lesen oder
|
||||
ändern die Konfiguration; der PUT plant Identität und nächsten
|
||||
Konfigurationsstand serverseitig und verwendet den typisierten Command.
|
||||
`POST /api/projects/:projectId/external-csv/preview` nimmt Dateiname und
|
||||
Base64-kodierte CSV bis 18 MB entgegen. Der zustandsfreie Service liefert Hash,
|
||||
Dialekt, Header, Klassifikationszahlen, Verdachtszeilen und gemappte
|
||||
Objektquellwerte. Er schreibt weder Entwurf noch Projektdaten.
|
||||
|
||||
## Noch nicht unterstützt
|
||||
|
||||
|
||||
@@ -249,9 +249,11 @@ keine Projektrevision und laufen über einen eigenen Audit-Store.
|
||||
synthetische Testdaten; die Referenzdatei steht noch aus.**
|
||||
3. Konfiguration über Migration, Snapshot v3, Transfer und den Command
|
||||
`external-csv-configuration.update` persistieren. **Erledigt.**
|
||||
4. Vorschau-Endpunkt und deutschen Projekt-Wizard ergänzen. Der Server speichert
|
||||
keinen Entwurf: Bei Bestätigung wird die Datei erneut übertragen, erneut
|
||||
geparst und ihr Hash gegen die Vorschau geprüft.
|
||||
4. Vorschau-Endpunkt und deutschen Projekt-Wizard ergänzen. **Erledigt:**
|
||||
Konfigurations-Read/Write- und zustandsfreier Vorschau-Endpunkt. **Offen:**
|
||||
deutscher Projekt-Wizard. Der Server speichert keinen Entwurf: Bei
|
||||
Bestätigung wird die Datei erneut übertragen, erneut geparst und ihr Hash
|
||||
gegen die Vorschau geprüft.
|
||||
|
||||
Abnahme: fokussierte Tests, vollständige Tests und alle vorgeschriebenen
|
||||
Build-/Typecheck-Schritte. Die lokal bereitgestellte Referenz-CSV wird mit
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
parseExternalCsv,
|
||||
serializeExternalCsv,
|
||||
} from "../src/external-model/csv/external-csv-transport.js";
|
||||
import { createExternalCsvPreview } from "../src/external-model/application/external-csv-preview.js";
|
||||
|
||||
const filePath = resolve(
|
||||
process.argv[2] ?? "docs/spec/ELT Stromkreisnummernvergabe-Check_DIV.csv"
|
||||
@@ -45,8 +46,14 @@ const nonEmptyPassthroughRows = document.rows.filter(
|
||||
row.cells.some((cell) => cell.value !== "")
|
||||
).length;
|
||||
const serialized = serializeExternalCsv(document);
|
||||
const preview = createExternalCsvPreview({
|
||||
fileName: filePath,
|
||||
bytes: source,
|
||||
configuration,
|
||||
});
|
||||
|
||||
assert.equal(counts.object, 897, "Expected 897 object rows.");
|
||||
assert.equal(preview.objectCount, 897, "Expected 897 preview objects.");
|
||||
assert.equal(nonEmptyPassthroughRows, 211, "Expected 211 non-empty passthrough rows.");
|
||||
assert.equal(counts["suspect-object"], 0, "Expected no suspect object rows.");
|
||||
assert.equal(
|
||||
@@ -66,6 +73,7 @@ process.stdout.write(
|
||||
rows: counts,
|
||||
nonEmptyPassthroughRows,
|
||||
roundTripByteIdentical: true,
|
||||
previewObjectCount: preview.objectCount,
|
||||
},
|
||||
null,
|
||||
2
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { ExternalCsvConfigurationReader } from "../../domain/ports/external-csv-configuration.reader.js";
|
||||
import type { AppDatabase } from "../database-context.js";
|
||||
import { externalCsvConfigurations } from "../schema/external-csv-configurations.js";
|
||||
import { projects } from "../schema/projects.js";
|
||||
|
||||
export class ExternalCsvConfigurationRepository
|
||||
implements ExternalCsvConfigurationReader
|
||||
{
|
||||
constructor(private readonly database: AppDatabase) {}
|
||||
|
||||
getByProject(projectId: string) {
|
||||
const project = this.database
|
||||
.select({ id: projects.id })
|
||||
.from(projects)
|
||||
.where(eq(projects.id, projectId))
|
||||
.get();
|
||||
if (!project) {
|
||||
return { projectExists: false, configuration: null };
|
||||
}
|
||||
const configuration = this.database
|
||||
.select()
|
||||
.from(externalCsvConfigurations)
|
||||
.where(eq(externalCsvConfigurations.projectId, projectId))
|
||||
.get() ?? null;
|
||||
return { projectExists: true, configuration };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { ExternalCsvConfigurationSnapshot } from "../models/external-csv-configuration-project-command.model.js";
|
||||
|
||||
export interface ExternalCsvConfigurationReadResult {
|
||||
projectExists: boolean;
|
||||
configuration: ExternalCsvConfigurationSnapshot | null;
|
||||
}
|
||||
|
||||
export interface ExternalCsvConfigurationReader {
|
||||
getByProject(projectId: string): ExternalCsvConfigurationReadResult;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import type { ExternalCsvConfiguration } from "../csv/external-csv-contracts.js";
|
||||
import { parseExternalCsv } from "../csv/external-csv-transport.js";
|
||||
|
||||
export interface ExternalCsvPreviewObject {
|
||||
rowNumber: number;
|
||||
ifcGuid: string;
|
||||
roomNumber: string;
|
||||
roomName: string;
|
||||
familyAndType: string;
|
||||
selectionMarker: string;
|
||||
circuitIdentifier: string;
|
||||
power: string;
|
||||
quantity: string | null;
|
||||
additionalSourceValues: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ExternalCsvPreview {
|
||||
fileName: string;
|
||||
byteLength: number;
|
||||
sha256: string;
|
||||
dialect: ReturnType<typeof parseExternalCsv>["dialect"];
|
||||
headerRowNumber: number;
|
||||
headerColumns: string[];
|
||||
rowCount: number;
|
||||
objectCount: number;
|
||||
passthroughCount: number;
|
||||
nonEmptyPassthroughCount: number;
|
||||
suspectObjectCount: number;
|
||||
objects: ExternalCsvPreviewObject[];
|
||||
suspectRowNumbers: number[];
|
||||
}
|
||||
|
||||
export function createExternalCsvPreview(input: {
|
||||
fileName: string;
|
||||
bytes: Uint8Array;
|
||||
configuration: ExternalCsvConfiguration;
|
||||
}): ExternalCsvPreview {
|
||||
const document = parseExternalCsv(input.bytes, input.configuration);
|
||||
const header = document.rows[document.headerRowIndex];
|
||||
const headerIndex = new Map(
|
||||
header.cells.map((cell, index) => [cell.value, index])
|
||||
);
|
||||
const valueAt = (row: (typeof document.rows)[number], column: string) =>
|
||||
row.cells[headerIndex.get(column)!]?.value ?? "";
|
||||
const objects = document.rows
|
||||
.filter((row) => row.classification === "object")
|
||||
.map((row): ExternalCsvPreviewObject => ({
|
||||
rowNumber: row.index + 1,
|
||||
ifcGuid: valueAt(row, input.configuration.columns.ifcGuid),
|
||||
roomNumber: valueAt(row, input.configuration.columns.roomNumber),
|
||||
roomName: valueAt(row, input.configuration.columns.roomName),
|
||||
familyAndType: valueAt(row, input.configuration.columns.familyAndType),
|
||||
selectionMarker: valueAt(row, input.configuration.columns.selectionMarker),
|
||||
circuitIdentifier: valueAt(row, input.configuration.columns.circuitIdentifier),
|
||||
power: valueAt(row, input.configuration.columns.power),
|
||||
quantity:
|
||||
input.configuration.columns.quantity === null
|
||||
? null
|
||||
: valueAt(row, input.configuration.columns.quantity),
|
||||
additionalSourceValues: Object.fromEntries(
|
||||
input.configuration.additionalSourceMappings.map((mapping) => [
|
||||
mapping.targetField,
|
||||
valueAt(row, mapping.sourceColumn),
|
||||
])
|
||||
),
|
||||
}));
|
||||
const passthroughRows = document.rows.filter(
|
||||
(row) => row.classification === "passthrough"
|
||||
);
|
||||
const suspectRows = document.rows.filter(
|
||||
(row) => row.classification === "suspect-object"
|
||||
);
|
||||
return {
|
||||
fileName: input.fileName,
|
||||
byteLength: input.bytes.byteLength,
|
||||
sha256: createHash("sha256").update(input.bytes).digest("hex"),
|
||||
dialect: document.dialect,
|
||||
headerRowNumber: document.headerRowIndex + 1,
|
||||
headerColumns: header.cells.map((cell) => cell.value),
|
||||
rowCount: document.rows.length,
|
||||
objectCount: objects.length,
|
||||
passthroughCount: passthroughRows.length,
|
||||
nonEmptyPassthroughCount: passthroughRows.filter((row) =>
|
||||
row.cells.some((cell) => cell.value !== "")
|
||||
).length,
|
||||
suspectObjectCount: suspectRows.length,
|
||||
objects,
|
||||
suspectRowNumbers: suspectRows.map((row) => row.index + 1),
|
||||
};
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { GlobalDeviceRepository } from "../../db/repositories/global-device.repo
|
||||
import { ProjectDeviceRepository } from "../../db/repositories/project-device.repository.js";
|
||||
import { ProjectRepository } from "../../db/repositories/project.repository.js";
|
||||
import { RoomRepository } from "../../db/repositories/room.repository.js";
|
||||
import { ExternalCsvConfigurationRepository } from "../../db/repositories/external-csv-configuration.repository.js";
|
||||
|
||||
export const circuitDeviceRowRepository =
|
||||
new CircuitDeviceRowRepository(db);
|
||||
@@ -28,3 +29,5 @@ export const globalDeviceRepository = new GlobalDeviceRepository(db);
|
||||
export const projectDeviceRepository = new ProjectDeviceRepository(db);
|
||||
export const projectRepository = new ProjectRepository(db);
|
||||
export const roomRepository = new RoomRepository(db);
|
||||
export const externalCsvConfigurationRepository =
|
||||
new ExternalCsvConfigurationRepository(db);
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
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 { createExternalCsvConfigurationUpdateProjectCommand } from "../../domain/models/external-csv-configuration-project-command.model.js";
|
||||
import {
|
||||
previewExternalCsvSchema,
|
||||
updateExternalCsvConfigurationSchema,
|
||||
} from "../../shared/validation/external-csv.schemas.js";
|
||||
import { externalCsvConfigurationRepository } 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.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -35,6 +35,11 @@ import {
|
||||
importProjectTransfer,
|
||||
importProjectTransferAsNewProject,
|
||||
} from "../controllers/project-transfer.controller.js";
|
||||
import {
|
||||
getExternalCsvConfiguration,
|
||||
previewExternalCsv,
|
||||
updateExternalCsvConfiguration,
|
||||
} from "../controllers/external-csv.controller.js";
|
||||
|
||||
export const projectRouter = Router();
|
||||
|
||||
@@ -44,6 +49,9 @@ projectRouter.post("/import", importProjectTransferAsNewProject);
|
||||
projectRouter.get("/:projectId", getProject);
|
||||
projectRouter.get("/:projectId/export", exportProjectTransfer);
|
||||
projectRouter.post("/:projectId/import", importProjectTransfer);
|
||||
projectRouter.get("/:projectId/external-csv/configuration", getExternalCsvConfiguration);
|
||||
projectRouter.put("/:projectId/external-csv/configuration", updateExternalCsvConfiguration);
|
||||
projectRouter.post("/:projectId/external-csv/preview", previewExternalCsv);
|
||||
projectRouter.get("/:projectId/history", getProjectHistory);
|
||||
projectRouter.get("/:projectId/history/revisions", listProjectRevisions);
|
||||
projectRouter.post("/:projectId/commands", executeProjectCommand);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const updateExternalCsvConfigurationSchema = z
|
||||
.object({
|
||||
expectedRevision: z.number().int().nonnegative(),
|
||||
configuration: z.unknown(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const previewExternalCsvSchema = z
|
||||
.object({
|
||||
fileName: z.string().trim().min(1).max(255),
|
||||
contentBase64: z.string().min(1).max(24_000_000),
|
||||
})
|
||||
.strict();
|
||||
@@ -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 {
|
||||
|
||||
@@ -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" },
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user