From 7af48897d817da00716b154a70d9ad2cdd5ed9ef Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 14:05:51 +0000 Subject: [PATCH 1/3] fix(rest): read IMPORT_JOB_MAX_ROWS from the spec export instead of a mirrored literal Refs #6535 --- .../rest/src/import-job-integration.test.ts | 25 ++++++++++++++++--- packages/rest/src/rest-server.ts | 6 +++-- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/rest/src/import-job-integration.test.ts b/packages/rest/src/import-job-integration.test.ts index 709612436a..99dcccae6d 100644 --- a/packages/rest/src/import-job-integration.test.ts +++ b/packages/rest/src/import-job-integration.test.ts @@ -25,6 +25,9 @@ import { ObjectQL } from '@objectstack/objectql'; import { SqlDriver } from '@objectstack/driver-sql'; import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol'; import { SysImportJob } from '@objectstack/platform-objects/audit'; +// #6535: the ONE definition of the async-import row ceiling. The pin below reads it +// from here so that moving it is observable at the enforcement point. +import { IMPORT_JOB_MAX_ROWS } from '@objectstack/spec/api'; import { RestServer } from './rest-server'; // The real backend: better-sqlite3 `:memory:`, constructed the canonical way @@ -172,12 +175,28 @@ describe('async import job — real engine + protocol integration', () => { expect(results._json.results.find((r: any) => !r.ok)).toMatchObject({ field: 'score', code: 'invalid_number' }); }); - it('rejects a payload above the 50k async ceiling with 413', async () => { - const rows = Array.from({ length: 50_001 }, (_, i) => ({ id: `x${i}`, title: 't' })); + // #6535: the ceiling has ONE definition — the spec export — and rest is its only + // enforcer. So this case derives everything it knows about the ceiling from that + // export: how big a payload must be to breach it, and the number the 413 copy is + // required to name. Re-spelling 50_000 here would just move the duplicated literal + // into a third place and re-open the drift surface the fix closes. + // + // What this pin buys, precisely: while spec and rest agree (they do today, both + // 50_000) it is green against BOTH implementations — there is no live defect to + // catch. It goes red the moment they DISAGREE. With a rest that re-declares its own + // literal, moving the spec constant down makes this send `spec + 1` rows at a route + // still enforcing the stale, larger number: the payload is accepted and no 413 is + // produced at all. Under the imported constant the two move together and stay green. + it('rejects a payload above the async ceiling the spec declares, with 413', async () => { + const rows = Array.from({ length: IMPORT_JOB_MAX_ROWS + 1 }, (_, i) => ({ id: `x${i}`, title: 't' })); const res = await callCreate(ctx.create, { format: 'json', rows }); + // Rejection-class: assert the ADR-0112 envelope (status AND code), never a bare throw. expect(res._status).toBe(413); expect(res._json.code).toBe('PAYLOAD_TOO_LARGE'); - expect(String(res._json.error)).toMatch(/50000/); + // The wording is contract here — the 413 tells the caller what to split the file + // into — so the interpolated number must be the spec's, on top of the envelope. + expect(String(res._json.error)).toContain(String(IMPORT_JOB_MAX_ROWS)); + expect(String(res._json.error)).toContain(`batches of ${IMPORT_JOB_MAX_ROWS}`); }); it('lists jobs in history and filters by status', async () => { diff --git a/packages/rest/src/rest-server.ts b/packages/rest/src/rest-server.ts index f0d4387c58..73961e217d 100644 --- a/packages/rest/src/rest-server.ts +++ b/packages/rest/src/rest-server.ts @@ -35,6 +35,10 @@ import type { DirectMountedRoute, MountedRouteSource } from './direct-mount.js'; import { RestServerConfig, RestApiConfig, CrudEndpointsConfig, MetadataEndpointsConfig, BatchEndpointsConfig, RouteGenerationConfig } from '@objectstack/spec/api'; import { DataProtocol, MetadataProtocol } from '@objectstack/spec/api'; import type { FieldErrorCode } from '@objectstack/spec/api'; +// The async-import row ceiling has exactly one definition, in the spec, whose +// TSDoc is its public statement (#6535). rest is the only enforcer, so it reads +// that export rather than re-declaring the literal beside a "mirrors spec" comment. +import { IMPORT_JOB_MAX_ROWS } from '@objectstack/spec/api'; import { PUBLIC_FORM_SERVER_MANAGED_FIELDS } from '@objectstack/spec/security'; import { PLURAL_TO_SINGULAR } from '@objectstack/spec/shared'; import { stripReadDecorations } from '@objectstack/spec/kernel'; @@ -1398,8 +1402,6 @@ export function apiAccessDenialFromEnable( /** Platform object backing async import jobs (see sys-import-job.object.ts). */ const IMPORT_JOB_OBJECT = 'sys_import_job'; -/** Hard ceiling on rows per async import job (mirrors spec IMPORT_JOB_MAX_ROWS). */ -const IMPORT_JOB_MAX_ROWS = 50_000; /** Cap on per-row results persisted on the job (failures first). */ const IMPORT_JOB_RESULTS_CAP = 500; /** Undo (logical rollback) is only recorded for jobs at or under this row From 6e90f1db39d072dc6df3c649585530fbf77563b9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 17:25:40 +0000 Subject: [PATCH 2/3] docs(changeset): patch note for the single-definition convergence (#6535) --- .../import-job-max-rows-single-definition.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .changeset/import-job-max-rows-single-definition.md diff --git a/.changeset/import-job-max-rows-single-definition.md b/.changeset/import-job-max-rows-single-definition.md new file mode 100644 index 0000000000..50bc6de9ac --- /dev/null +++ b/.changeset/import-job-max-rows-single-definition.md @@ -0,0 +1,15 @@ +--- +'@objectstack/rest': patch +--- + +rest 的异步导入行数上限改为直接读取 spec 的 `IMPORT_JOB_MAX_ROWS` 导出,不再自己声明一份同值字面量(#6535)。 + +**行为没有任何变化**:两处此前都是 `50_000`,改后仍是 `50_000`,上限、`413` 文案、拒绝边界全部不动。 +这是一次一致性收敛,不是缺陷修复——因此按 patch 计。 + +收敛掉的是一处漂移面:`packages/spec/src/api/export.zod.ts` 的那份导出带着 TSDoc,会进 +`content/docs/references/`,是这个上限的**对外说明**;而真正执行拒绝的是 `packages/rest`, +它此前读的是自己那份本地 `const`,两者之间只有一句 "mirrors spec" 注释相连。没有任何 gate +比较这两个数,所以把 spec 那份改掉、执行侧纹丝不动,全部检查依然全绿——文档说一套、系统做 +一套,而 `413` 文案里内插的又是 rest 那一份,连报错都会自洽地说谎。现在一处定义、两处读点 +(`maxRows:` 与 `413` 文案)同源。 From c14125a528eca68b9ac76e56f94a4373fa1f66a4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 00:18:10 +0000 Subject: [PATCH 3/3] docs(changeset): record the measured drift-blindness in the note (#6535) The changeset argued the convergence from the comment alone. Name the mechanism the PR actually measured: api-surface/api.json records the export's NAME, not its VALUE, so moving spec's 50_000 to 20_000 leaves that baseline byte-identical and every gate green while the enforced ceiling never budges. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017uFVNMmTxLpmfQYiuKM1Yx --- .changeset/import-job-max-rows-single-definition.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.changeset/import-job-max-rows-single-definition.md b/.changeset/import-job-max-rows-single-definition.md index 50bc6de9ac..7435e85161 100644 --- a/.changeset/import-job-max-rows-single-definition.md +++ b/.changeset/import-job-max-rows-single-definition.md @@ -7,9 +7,10 @@ rest 的异步导入行数上限改为直接读取 spec 的 `IMPORT_JOB_MAX_ROWS **行为没有任何变化**:两处此前都是 `50_000`,改后仍是 `50_000`,上限、`413` 文案、拒绝边界全部不动。 这是一次一致性收敛,不是缺陷修复——因此按 patch 计。 -收敛掉的是一处漂移面:`packages/spec/src/api/export.zod.ts` 的那份导出带着 TSDoc,会进 -`content/docs/references/`,是这个上限的**对外说明**;而真正执行拒绝的是 `packages/rest`, -它此前读的是自己那份本地 `const`,两者之间只有一句 "mirrors spec" 注释相连。没有任何 gate -比较这两个数,所以把 spec 那份改掉、执行侧纹丝不动,全部检查依然全绿——文档说一套、系统做 -一套,而 `413` 文案里内插的又是 rest 那一份,连报错都会自洽地说谎。现在一处定义、两处读点 -(`maxRows:` 与 `413` 文案)同源。 +收敛掉的是一处漂移面:`packages/spec/src/api/export.zod.ts` 的那份导出带着 TSDoc,是这个 +上限的**对外说明**(喂给生成的 reference 表面);而真正执行拒绝的是 `packages/rest`,它此前 +读的是自己那份本地 `const`,两者之间只有一句 "mirrors spec" 注释相连。没有任何 gate 比较这 +两个数——`api-surface/api.json` 只记下 `"IMPORT_JOB_MAX_ROWS (const)"` 这个**名字**,不记它的 +**值**——所以把 spec 那份改成 20_000、执行侧纹丝不动,`pnpm test` 与全部 `check:*` 依然全绿 +(本 PR 实测过)。失效方向是文档说一套、系统做一套,而 `413` 文案里内插的又是 rest 那一份, +连报错都会自洽地说谎。现在一处定义、两处读点(`maxRows:` 与 `413` 文案)同源。