Add Linear Excel transport
This commit is contained in:
parent
d4803a18d2
commit
52d3b1a270
10 changed files with 2131 additions and 83 deletions
267
tests/external-xlsx-transport.test.ts
Normal file
267
tests/external-xlsx-transport.test.ts
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import ExcelJS from "exceljs";
|
||||
import {
|
||||
createExternalXlsxReturn,
|
||||
ExternalXlsxError,
|
||||
parseExternalXlsx,
|
||||
} from "../src/external-model/xlsx/external-xlsx-transport.js";
|
||||
import {
|
||||
createRevitXlsxFixture,
|
||||
externalXlsxTestConfiguration,
|
||||
firstWorksharingId,
|
||||
secondWorksharingId,
|
||||
} from "./fixtures/revit-xlsx-fixtures.js";
|
||||
|
||||
describe("external XLSX transport", () => {
|
||||
it("reads typed cells and classifies ID-less rows without blocking summaries", async () => {
|
||||
const document = await parseExternalXlsx({
|
||||
fileName: "linear.xlsx",
|
||||
bytes: await createRevitXlsxFixture(),
|
||||
configuration: externalXlsxTestConfiguration,
|
||||
});
|
||||
|
||||
assert.equal(document.sheetName, "Linear Export");
|
||||
assert.equal(document.headerRowNumber, 1);
|
||||
assert.equal(document.firstDataRowNumber, 3);
|
||||
assert.equal(document.firstDataColumnNumber, 1);
|
||||
assert.deepEqual(
|
||||
document.rows.map((row) => row.classification),
|
||||
["object", "object", "suspect-object", "passthrough"]
|
||||
);
|
||||
assert.equal(document.rows[0].cells[5].value, 2);
|
||||
assert.equal(document.rows[0].cells[15].value, true);
|
||||
assert.equal(document.columns[0].comment, "Kommentar Worksharing-ID");
|
||||
assert.equal(document.columns[0].width, 12);
|
||||
assert.deepEqual(
|
||||
document.issues.map((issue) => issue.code),
|
||||
["suspect-object-skipped"]
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves editable layout positions without a separate ID column", async () => {
|
||||
const configuration = {
|
||||
...externalXlsxTestConfiguration,
|
||||
layout: { headerRow: 2, firstDataRow: 5, firstDataColumn: 2 },
|
||||
};
|
||||
const document = await parseExternalXlsx({
|
||||
fileName: "linear.xlsx",
|
||||
bytes: await createRevitXlsxFixture(configuration.layout),
|
||||
configuration,
|
||||
});
|
||||
|
||||
assert.equal(document.headerRowNumber, 2);
|
||||
assert.equal(document.firstDataRowNumber, 5);
|
||||
assert.equal(document.firstDataColumnNumber, 2);
|
||||
assert.equal(document.columns[1].header, "Worksharing-ID");
|
||||
assert.equal(document.rows[0].cells[1].value, firstWorksharingId);
|
||||
assert.deepEqual(
|
||||
document.rows.map((row) => row.classification),
|
||||
["object", "object", "suspect-object", "passthrough"]
|
||||
);
|
||||
});
|
||||
|
||||
it("compares Worksharing-ID and IfcGUID exactly and case-sensitively", async () => {
|
||||
const lowerCaseId = secondWorksharingId.toLowerCase();
|
||||
const document = await parseExternalXlsx({
|
||||
fileName: "linear.xlsx",
|
||||
bytes: await createRevitXlsxFixture({
|
||||
secondWorksharingId: lowerCaseId,
|
||||
firstIfcGuid: "IfcGUID",
|
||||
secondIfcGuid: "IFCGUID",
|
||||
}),
|
||||
configuration: externalXlsxTestConfiguration,
|
||||
});
|
||||
assert.equal(
|
||||
document.rows.filter((row) => row.classification === "object").length,
|
||||
2
|
||||
);
|
||||
assert.equal(
|
||||
document.issues.some((issue) => issue.code === "duplicate-ifc-guid"),
|
||||
false
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
parseExternalXlsx({
|
||||
fileName: "linear.xlsx",
|
||||
bytes: await createRevitXlsxFixture({ duplicateWorksharingId: true }),
|
||||
configuration: externalXlsxTestConfiguration,
|
||||
}),
|
||||
isXlsxError("duplicate-worksharing-id")
|
||||
);
|
||||
|
||||
const duplicateIfcDocument = await parseExternalXlsx({
|
||||
fileName: "linear.xlsx",
|
||||
bytes: await createRevitXlsxFixture({
|
||||
firstIfcGuid: "same",
|
||||
secondIfcGuid: "same",
|
||||
}),
|
||||
configuration: externalXlsxTestConfiguration,
|
||||
});
|
||||
assert.equal(
|
||||
duplicateIfcDocument.issues.filter(
|
||||
(issue) => issue.code === "duplicate-ifc-guid"
|
||||
).length,
|
||||
1
|
||||
);
|
||||
});
|
||||
|
||||
it("uses cached formula values without evaluating formulas", async () => {
|
||||
const document = await parseExternalXlsx({
|
||||
fileName: "linear.xlsx",
|
||||
bytes: await createRevitXlsxFixture({ includeFormula: true }),
|
||||
configuration: externalXlsxTestConfiguration,
|
||||
});
|
||||
const powerCell = document.rows[0].cells[11];
|
||||
assert.equal(powerCell.value, 250);
|
||||
assert.equal(powerCell.formula, "125*2");
|
||||
assert.equal(powerCell.usedCachedFormulaValue, true);
|
||||
assert.equal(
|
||||
document.issues.some(
|
||||
(issue) => issue.code === "cached-formula-value-used"
|
||||
),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it("blocks a required formula without a cached result", async () => {
|
||||
await assert.rejects(
|
||||
parseExternalXlsx({
|
||||
fileName: "linear.xlsx",
|
||||
bytes: await createRevitXlsxFixture({
|
||||
includeRequiredFormulaWithoutResult: true,
|
||||
}),
|
||||
configuration: externalXlsxTestConfiguration,
|
||||
}),
|
||||
isXlsxError("required-formula-result-missing")
|
||||
);
|
||||
});
|
||||
|
||||
it("writes selected object rows in source order and preserves workbook structure", async () => {
|
||||
const source = await createRevitXlsxFixture({ includeFormula: true });
|
||||
const result = await createExternalXlsxReturn({
|
||||
fileName: "linear.xlsx",
|
||||
bytes: source,
|
||||
configuration: externalXlsxTestConfiguration,
|
||||
selectedWorksharingIds: [secondWorksharingId, firstWorksharingId],
|
||||
updates: [
|
||||
{
|
||||
worksharingId: firstWorksharingId,
|
||||
values: {
|
||||
circuitCombined: "UV99-3F7",
|
||||
electricalPowerW: 321.5,
|
||||
voltageV: 400,
|
||||
currentA: 0.46,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(result.exportedObjectCount, 2);
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.load(result.bytes as never);
|
||||
const worksheet = workbook.worksheets[0];
|
||||
assert.equal(worksheet.name, "Linear Export");
|
||||
assert.equal(worksheet.rowCount, 4);
|
||||
assert.equal(worksheet.getCell("A3").value, firstWorksharingId);
|
||||
assert.equal(worksheet.getCell("A4").value, secondWorksharingId);
|
||||
assert.equal(worksheet.getCell("H3").value, "UV99-3F7");
|
||||
assert.equal(worksheet.getCell("L3").value, 321.5);
|
||||
assert.equal(worksheet.getCell("M3").value, 400);
|
||||
assert.equal(worksheet.getCell("N3").value, 0.46);
|
||||
assert.equal(worksheet.getCell("P4").value, "bleibt");
|
||||
assert.equal(worksheet.getCell("L3").numFmt, "0.00");
|
||||
assert.equal(worksheet.getColumn(1).width, 12);
|
||||
assert.equal(
|
||||
typeof worksheet.getCell("A1").note === "string"
|
||||
? worksheet.getCell("A1").note
|
||||
: worksheet.getCell("A1").note?.texts?.[0]?.text,
|
||||
"Kommentar Worksharing-ID"
|
||||
);
|
||||
const commentedCells: string[] = [];
|
||||
worksheet.eachRow({ includeEmpty: true }, (row) => {
|
||||
row.eachCell({ includeEmpty: true }, (cell) => {
|
||||
if (cell.note !== undefined) commentedCells.push(cell.address);
|
||||
});
|
||||
});
|
||||
assert.deepEqual(
|
||||
commentedCells,
|
||||
Array.from({ length: 16 }, (_, index) =>
|
||||
worksheet.getCell(1, index + 1).address
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it("warns for missing optional return columns and blocks missing circuit columns", async () => {
|
||||
const source = await createRevitXlsxFixture();
|
||||
const optionalMissing = {
|
||||
...externalXlsxTestConfiguration,
|
||||
columns: {
|
||||
...externalXlsxTestConfiguration.columns,
|
||||
designation: "Nicht vorhanden",
|
||||
},
|
||||
};
|
||||
const result = await createExternalXlsxReturn({
|
||||
fileName: "linear.xlsx",
|
||||
bytes: source,
|
||||
configuration: optionalMissing,
|
||||
});
|
||||
assert.equal(
|
||||
result.issues.some(
|
||||
(issue) =>
|
||||
issue.code === "optional-return-column-missing" &&
|
||||
issue.field === "designation"
|
||||
),
|
||||
true
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
createExternalXlsxReturn({
|
||||
fileName: "linear.xlsx",
|
||||
bytes: source,
|
||||
configuration: {
|
||||
...externalXlsxTestConfiguration,
|
||||
columns: {
|
||||
...externalXlsxTestConfiguration.columns,
|
||||
circuitCombined: "Nicht vorhanden",
|
||||
},
|
||||
},
|
||||
}),
|
||||
isXlsxError("return-circuit-column-missing")
|
||||
);
|
||||
});
|
||||
|
||||
it("enforces extension, byte size and post-read sheet limits", async () => {
|
||||
const source = await createRevitXlsxFixture();
|
||||
await assert.rejects(
|
||||
parseExternalXlsx({
|
||||
fileName: "linear.xlsm",
|
||||
bytes: source,
|
||||
configuration: externalXlsxTestConfiguration,
|
||||
}),
|
||||
isXlsxError("invalid-file-extension")
|
||||
);
|
||||
await assert.rejects(
|
||||
parseExternalXlsx({
|
||||
fileName: "linear.xlsx",
|
||||
bytes: new Uint8Array(10 * 1024 * 1024 + 1),
|
||||
configuration: externalXlsxTestConfiguration,
|
||||
}),
|
||||
isXlsxError("file-too-large")
|
||||
);
|
||||
await assert.rejects(
|
||||
parseExternalXlsx({
|
||||
fileName: "linear.xlsx",
|
||||
bytes: source,
|
||||
configuration: externalXlsxTestConfiguration,
|
||||
limits: { maxRows: 3, maxColumns: 100, maxCells: 1_000 },
|
||||
}),
|
||||
isXlsxError("worksheet-too-large")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function isXlsxError(code: ExternalXlsxError["code"]) {
|
||||
return (error: unknown) =>
|
||||
error instanceof ExternalXlsxError && error.code === code;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue