Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions packages/rest/src/export-business-timezone.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@
*/

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import ExcelJS from 'exceljs';
import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server.js';
import { formatCellValue, formatRowCells, formatRowForJson } from './export-format.js';
import type { ExportFieldMeta } from './export-format.js';
import { loadXlsxWorkbook } from './xlsx-test-loader.js';

// The instant at the heart of the report: 2026-08-01 06:00 in +08 is
// 2026-07-31 22:00 in UTC — a different day, month and quarter-of-year.
Expand Down Expand Up @@ -238,8 +238,7 @@ async function xlsxRow(timezone?: string): Promise<string[]> {
const route = await boot(timezone);
const { res, getBuffer } = makeBinRes();
await route.handler({ params: { object: 'shift' }, query: { format: 'xlsx' } } as any, res);
const wb = new ExcelJS.Workbook();
await wb.xlsx.load(getBuffer() as any);
const wb = await loadXlsxWorkbook(getBuffer());
return (wb.worksheets[0].getRow(2).values as any[]).slice(1).map((v) => String(v));
}

Expand Down
17 changes: 6 additions & 11 deletions packages/rest/src/export-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@
*/

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import ExcelJS from 'exceljs';
import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { maskFieldValue } from '@objectstack/plugin-security';
import { RestServer } from './rest-server';
import { loadXlsxWorkbook } from './xlsx-test-loader.js';

// ---------------------------------------------------------------------------
// The real backend: better-sqlite3 `:memory:`, constructed the canonical way
Expand Down Expand Up @@ -211,8 +211,7 @@ describe('export route — real engine + protocol integration', () => {
expect(headers['Content-Type']).toBe(
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
);
const wb = new ExcelJS.Workbook();
await wb.xlsx.load(getBuffer() as any);
const wb = await loadXlsxWorkbook(getBuffer());
const ws = wb.worksheets[0];
const header = (ws.getRow(1).values as any[]).slice(1).map((v) => String(v));
expect(header).toEqual(['ID', '标题', '完成', '优先级', '截止', '负责人']);
Expand All @@ -227,8 +226,7 @@ describe('export route — real engine + protocol integration', () => {
// Default limit (10000) is within the style cap, so colours are applied.
expect(headers['X-Export-Styles']).toBe('applied');

const wb = new ExcelJS.Workbook();
await wb.xlsx.load(getBuffer() as any);
const wb = await loadXlsxWorkbook(getBuffer());
const ws = wb.worksheets[0];
// priority is column 4 (ID, 标题, 完成, 优先级, ...).
const highCell = ws.getRow(2).getCell(4); // '高' → #e11d48
Expand All @@ -247,8 +245,7 @@ describe('export route — real engine + protocol integration', () => {
);

expect(headers['X-Export-Styles']).toBe('dropped');
const wb = new ExcelJS.Workbook();
await wb.xlsx.load(getBuffer() as any);
const wb = await loadXlsxWorkbook(getBuffer());
const ws = wb.worksheets[0];
// Data is intact...
const r1 = (ws.getRow(2).values as any[]).slice(1).map((v) => String(v));
Expand Down Expand Up @@ -528,8 +525,7 @@ describe('export route — FLS column projection via getReadableFields (#3547)',
});
const { res, getBuffer } = makeBinRes();
await route.handler({ params: { object: 'task' }, query: { format: 'xlsx' } } as any, res);
const wb = new ExcelJS.Workbook();
await wb.xlsx.load(getBuffer() as any);
const wb = await loadXlsxWorkbook(getBuffer());
const ws = wb.worksheets[0];
expect((ws.getRow(1).values as any[]).slice(1)).toEqual(['ID', '完成']);
expect(ws.rowCount).toBe(1); // header only
Expand Down Expand Up @@ -610,8 +606,7 @@ describe('export route — search', () => {
{ params: { object: 'task' }, query: { format: 'xlsx', search: '代码' } } as any,
res,
);
const wb = new ExcelJS.Workbook();
await wb.xlsx.load(getBuffer() as any);
const wb = await loadXlsxWorkbook(getBuffer());
expect(wb.worksheets[0].rowCount).toBe(2); // header + one match
});
});
Expand Down
5 changes: 2 additions & 3 deletions packages/rest/src/rest.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import ExcelJS from 'exceljs';
import { RouteManager } from './route-manager';
import { RestServer, mapDataError } from './rest-server';
import { createRestApiPlugin } from './rest-api-plugin';
import type { RestApiPluginConfig } from './rest-api-plugin';
import { loadXlsxWorkbook } from './xlsx-test-loader.js';

// ---------------------------------------------------------------------------
// Mocks & Helpers
Expand Down Expand Up @@ -1263,8 +1263,7 @@ describe('RestServer', () => {
// xlsx is a zip — verify the PK signature, then round-trip the content.
expect(buf.subarray(0, 2).toString('latin1')).toBe('PK');

const wb = new ExcelJS.Workbook();
await wb.xlsx.load(buf);
const wb = await loadXlsxWorkbook(buf);
const ws = wb.getWorksheet('Export');
expect(ws).toBeDefined();
// row.values is 1-indexed (values[0] is empty).
Expand Down
88 changes: 88 additions & 0 deletions packages/rest/src/xlsx-test-loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* The one place this package asserts around `exceljs`'s broken `load` signature
* (#13378). Test layer only — nothing in `src/index.ts` reaches it, so tsup
* (entry: `src/index.ts`) never emits it into `dist` and it is not published.
*
* ## Why the assertion below is unavoidable, in the dependency's own bytes
*
* `exceljs@4.4.0/index.d.ts` opens, at line 1, with:
*
* ```ts
* declare interface Buffer extends ArrayBuffer { }
* ```
*
* That file carries 106 top-level `export` declarations, so it **is a module** —
* which makes this `Buffer` module-local, and it therefore SHADOWS Node's global
* `Buffer` inside every exceljs signature. The one that matters here is the
* `Xlsx.load` at `index.d.ts:1490`:
*
* ```ts
* load(buffer: Buffer, options?: Partial<XlsxReadOptions>): Promise<Workbook>;
* ```
*
* ⇒ `Workbook.xlsx.load` does not ask for a Node `Buffer`. It asks for something
* structurally identical to `ArrayBuffer`. A Node `Buffer` is a `Uint8Array`, so
* it is not assignable, and tsc says so precisely:
*
* ```
* src/rest.test.ts(1267,26): error TS2345: Argument of type 'Buffer<ArrayBuffer>' is not assignable to parameter of type 'Buffer'.
* The types of 'slice(...)[Symbol.toStringTag]' are incompatible between these types.
* Type '"Uint8Array"' is not assignable to type '"ArrayBuffer"'.
* ```
*
* ⭐ There is NO Node `Buffer` value that satisfies that parameter. The defect is
* in the published declaration, not at any call site — so this is not laziness,
* and no amount of care at a call site can remove it. What a call site CAN do is
* not restate it: before #13378 the package paid this at 6 anonymous `as any`s
* and left a 7th site as a ledgered `TS2345`. Now it is stated once, here.
*
* ## Why option C (upgrade) is not the answer — measured 2026-08-30
*
* `exceljs` `dist-tags.latest` IS 4.4.0 (published 2023-10-19). The only publish
* after it in the package's whole 166-version history is `4.4.1-prerelease.0`
* (2024-12-20), and its `index.d.ts` carries the identical declaration at the
* identical lines: shim at line 1, 106 exports, `load(buffer: Buffer, …)` at
* 1490. The registry's `time.modified` is that same date. There is no later line
* to pin to. (4.3.0 ships the shim too, so this is not a 4.4.0 regression that a
* bump could undo.)
*
* ## Why not a declaration override (option B)
*
* The `Buffer` above is module-local, so it cannot be reached by interface
* augmentation from outside; an override would have to redeclare the module,
* replacing the package's entire typing surface. Far more surface than the one
* assertion it would remove.
*
* ## Runtime is untouched
*
* exceljs's `load` has always accepted the Node `Buffer` these tests hand it —
* that is what every one of those 6 `as any` sites was doing, and passing. This
* helper changes only what tsc is told; it does not convert, copy or reshape the
* bytes.
*/

import ExcelJS from 'exceljs';
import type { Workbook, Xlsx } from 'exceljs';

/**
* The parameter type `Xlsx.load` actually declares, read off the dependency's
* own signature instead of spelled by hand. Naming it this way means that if
* exceljs ever drops the shim, this alias resolves to Node's `Buffer` and the
* assertion below quietly becomes a no-op rather than a lie.
*/
type XlsxLoadInput = Parameters<Xlsx['load']>[0];

/**
* Load .xlsx bytes into a fresh {@link Workbook}.
*
* The single site in this package where the exceljs declaration defect above is
* asserted away. Callers pass the Node `Buffer` their fixture produced and get
* back a workbook; no call site needs to know about any of this.
*/
export async function loadXlsxWorkbook(bytes: Buffer): Promise<Workbook> {
const wb = new ExcelJS.Workbook();
await wb.xlsx.load(bytes as unknown as XlsxLoadInput);
return wb;
}
4 changes: 2 additions & 2 deletions packages/rest/test-typecheck-debt.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"_comment": "Per-file tsc error debt of the @objectstack/rest TEST layer (#5286). `tsconfig.test.json` compiles `src/**/*.test.ts` — which `tsconfig.json` excludes and therefore no gate ever read — and every file below still carries errors from before that gate existed. THIS FIELD IS GENERATED: every regeneration rewrites it from scripts/check-test-typecheck.mts, and the EXACT ratchet below requires a regeneration on every repair — so an edit made here is gone by the next one. Anything true of THIS package goes in the sibling `_note` field, which is authored, is preserved verbatim, and is never written by the generator (#12624). This comment states NO cause for the errors, deliberately: the classes differ per package and per file, they move as the debt is paid down, and a cause written here is rewritten verbatim into every ledger by every regeneration — so it outlives its own repair and cannot be corrected in the file where it is read. Measure instead, before repairing anything: `tsc --noEmit --pretty false -p tsconfig.test.json` in the package prints the real classes with their TS codes. EXACT ratchet, judged by re-running tsc: a file that gains errors is red, a file that loses them is red until its number is re-recorded, a file that reaches zero is red until its entry is deleted, and a file NOT listed here may have no errors at all. Regenerate with: pnpm --filter @objectstack/rest gen:test-typecheck-debt",
"_note": "Everything still recorded here is held by its own card, and none of it is an annotation repair. #13377 holds the request literals this package builds by hand against IHttpRequest, which omit members that interface requires. #13378 holds the exceljs call whose parameter type resolves to that package's own module-local Buffer declaration rather than Node's. Repairing either one here would mean changing a fixture's data, or adding an assertion this package's paydown rules out, so these entries shrink when those cards land and not before.",
"_note": "Everything still recorded here is held by its own card, and none of it is an annotation repair. #13377 holds every entry that remains: the request literals this package builds by hand against IHttpRequest, which omit members that interface requires. Repairing them here would mean changing a fixture's data, so these entries shrink when that card lands and not before. The exceljs call that #13378 held is no longer in this ledger: that dependency declares its own module-local Buffer, which shadows Node's inside every exceljs signature, so no Node Buffer can be passed to Workbook.xlsx.load — the assertion that costs is now stated once, in src/xlsx-test-loader.ts, and every xlsx-reading test in this package goes through it.",
"entries": {
"src/meta-public-book-grant.test.ts": 1,
"src/rest-batch-size-cap.test.ts": 1,
"src/rest.test.ts": 3
"src/rest.test.ts": 2
}
}
Loading