Skip to content

fix(rest): emit the projected export header on an empty result set (#3547)#3649

Merged
os-zhuang merged 1 commit into
mainfrom
claude/security-getreadablefields-query-9bcm4u
Jul 27, 2026
Merged

fix(rest): emit the projected export header on an empty result set (#3547)#3649
os-zhuang merged 1 commit into
mainfrom
claude/security-getreadablefields-query-9bcm4u

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

背景

#3547 的收尾。PR #3561 已落地 getReadableFields(object, context) 查询面,让 export 的列投影从「反推 masked 行」(#3498)改为「向 security 服务权威查询」。但 #3547 验收项里的「不受空结果影响」还差最后一步:

零行结果时,GET /data/:object/export 至今写出的是零字节文件——表头只在首个数据块旁边写一次(while 循环内),而首块为空就直接 break,表头从未落笔。

「导出列不依赖行内容」这句话,只有在零行时也成立才算真的成立。而列投影本来就由 schema + context 推导,零行时它照样知道该出哪些列。

改动

packages/rest/src/rest-server.ts —— 流式循环结束后,在列集权威时补写表头:

  • 权威 = 安全服务的可读投影readableProjected或显式 ?fields=(调用方自己点名的列,回显不泄露任何新信息)。空导出因此成为一份可用的导入模板
  • 非权威则保持无表头fieldsFromSchema && !readableProjected 时,masked 行回退路径没有行可收窄,此时写出完整 schema 表头会把 FLS 隐藏列的列名暴露出来——正是 跟踪:UI 操作按钮与 apiMethods 白名单一致性契约落地(#3026 设计定稿) #3391 要堵的泄露。该路径维持今天的行为。
  • header=false 在任何情况下仍然抑制表头。
  • json 无表头概念,[] 本就正确,不受影响。

契约 / 兼容

无契约变更。唯一的行为变化是「空结果 + 权威列集」从零字节文件变为一行表头;无 security 服务的部署行为逐字节不变

测试

packages/rest/src/export-integration.test.ts 新增 5 例(#3547 块):

  • 空结果集输出投影表头,且被掩码列仍不出现;
  • 空结果集 + getReadableFields → undefined(投影不可用)→ 保持无表头,不泄露 FLS 隐藏列名;
  • 空结果集 + 显式 ?fields= → 按请求回显;
  • 空结果集 + header=false → 无表头;
  • xlsx 空结果集携带表头行(rowCount === 1)。

全量:@objectstack/rest 26 文件 / 399 测试通过;pnpm turbo build --filter=@objectstack/rest CJS/ESM/DTS 全绿。

关联

Closes #3547(本 PR 补齐验收项「导出列不受 null 值/空结果影响」的空结果一半;null 值一半由 #3561 完成)。关联 #3391#3498#3561

🤖 Generated with Claude Code

https://claude.ai/code/session_01KZ2BGusRo58ZW8FMkDhGTb


Generated by Claude Code

…3547)

`GET /data/:object/export` wrote a zero-byte file whenever the query matched
no rows — the header was only ever written alongside the first data chunk.
The `getReadableFields` projection derives the readable column set from
schema + context, so it is known even with zero rows: "export columns don't
depend on row content" is only true if it also holds at zero rows.

The header is emitted only when the column set is AUTHORITATIVE — the
security service's readable projection, or an explicit `?fields=` request
(the caller named the columns, so echoing them discloses nothing new). When
the header is schema-derived and the projection was unavailable, the export
stays headerless as before: the masked-row fallback has no rows to narrow
with, and writing the full schema header would name FLS-hidden columns.
`header=false` still suppresses the header in every case.

Tests: empty result emits the projected header (csv + xlsx); no projection
stays headerless; explicit `?fields=` is echoed; `header=false` wins.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZ2BGusRo58ZW8FMkDhGTb
@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 27, 2026 12:45pm

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Jul 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/rest.

8 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/api/error-catalog.mdx (via @objectstack/rest)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest)
  • content/docs/api/index.mdx (via @objectstack/rest)
  • content/docs/plugins/index.mdx (via @objectstack/rest)
  • content/docs/plugins/packages.mdx (via @objectstack/rest)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/rest)
  • content/docs/releases/implementation-status.mdx (via @objectstack/rest)
  • content/docs/releases/v12.mdx (via @objectstack/rest)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review July 27, 2026 12:58
@os-zhuang
os-zhuang merged commit ce1f100 into main Jul 27, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/security-getreadablefields-query-9bcm4u branch July 27, 2026 12:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security 服务 getReadableFields 查询面(export 列投影的长期正解)

2 participants