Skip to content

fix(service-datasource): forward ?schema= on the admin remote-tables route (#7955) - #7998

Merged
huangyiirene merged 2 commits into
mainfrom
claude/issue-7955-admin-remote-tables-schema
Aug 12, 2026
Merged

fix(service-datasource): forward ?schema= on the admin remote-tables route (#7955)#7998
huangyiirene merged 2 commits into
mainfrom
claude/issue-7955-admin-remote-tables-schema

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes#7955

What was wrong

IExternalDatasourceService.listRemoteTables is reachable through two live routes:

routepackagebefore
GET /api/v1/datasources/:name/external/tablespackages/restlistRemoteTables(name, { schema })
GET /api/v1/datasources/:name/remote-tablespackages/services/service-datasourcelistRemoteTables(name) — query never read

So ?schema=public on the admin spelling returned the unfiltered listing: not the filtered set, and not a refusal either — "declared ≠ enforced" at its quietest, since the twin one path over honoured the same parameter.

The premise, measured

The card's ruling rests on the twins being genuinely the same operation. They are:

No scope difference, no second service method. Route A applies as ruled.

The change

packages/services/service-datasource/src/admin-routes.ts — the one behavioural edit:

constschema=typeofreq.query?.schema==='string' ? req.query.schema : undefined;consttables=awaitsvc.listRemoteTables(req.params.name,{ schema });

Wire-visible: ?schema=<name> now narrows the listing on the admin spelling, exactly as the federation twin already did. No ?schema= is unchanged — still the full listing.

Validation is mirrored, not invented. The coercion is copied verbatim from packages/rest/src/external-datasource-routes.ts:122 — the reference spelling, which this PR does not touch. It is a coercion, not a refusal: a non-string (a repeated ?schema=a&schema=b, which the adapter surfaces as an array) degrades to undefined, i.e. no filter, on both paths. The empty string ?schema= stays a string and the service's own opts?.schema && guard treats it as no filter — again on both paths, from the one service.

No refusal was added. Whether an unusable query parameter should be refused is the global ingress-policy question owned by #7606; honouring the parameter is correct under either answer, so the twins can move together then.

Tests

packages/rest/src/remote-tables-twin.equivalence.test.ts (new) drives the same query at both spellings — one real ExternalDatasourceService over a fake introspector, both registrars mounted on one real HonoHttpServer — and asserts the two answers are equal:

  1. ?schema=public → same filtered set (and it really filtered: 2 of 3 tables).
  2. no ?schema= → same unfiltered set (an always-filtering fix could not pass).
  3. ?schema=nonexistent → same empty set.
  4. repeated ?schema=a&schema=b → both degrade to no filter, identically.
  5. ?schema= (empty) → both unfiltered, identically.

It lives in packages/rest because that is the side that can reach both halves without widening anyone's public API: registerDatasourceAdminRoutes is exported from @objectstack/service-datasource, while registerExternalDatasourceRoutes is deliberately internal to packages/rest. The edge is dev-only (rest → service-datasource) and not a cycle. packages/services/service-datasource/src/__tests__/admin-routes.test.ts gains the same-package forwarding case and its existing assertion now pins the options bag.

Reverse-verification. With admin-routes.ts reverted to origin/main and the package rebuilt, the equivalence test is red: 2 failed / 3 passed — cases 1 and 3 fail with the admin route returning all three tables where the federation route returned the filtered set / none. With the fix: 5 passed. The three that pass in both directions are the unfiltered arms, which is what makes case 2 a real guard rather than a restatement.

Full suites green: @objectstack/rest 1536 tests / 95 files, @objectstack/service-datasource 334 tests / 13 files. Typecheck and eslint clean on both. check:route-envelope, check:cross-package-test-inputs, check:published-files, check:empty-changeset, check:adr-0087-registration all pass.

Deliberately not done


Generated by Claude Code

…s route (#7955)
`IExternalDatasourceService.listRemoteTables` is reachable through two live
routes. The federation spelling — `GET /api/v1/datasources/:name/external/tables`
in `packages/rest` — forwards `?schema=` to the service. The admin spelling,
`GET /api/v1/datasources/:name/remote-tables`, never read `req.query` at all, so
`?schema=public` came back as the UNFILTERED listing: not the filtered set, and
not a refusal either — the "declared ≠ enforced" shape at its quietest, since
the twin one path over honoured the same parameter.
This finishes on the REQUEST path what #4249 did for the failure path ("one
operation, one failure contract now, on both paths"). The two routes resolve the
SAME `external-datasource` slot and call the SAME method with the same datasource
name; the sibling operation `generateObjectDraft` already forwards its options
bag on both spellings, so honouring the filter restores symmetry rather than
inventing policy.
The coercion is copied from the federation route rather than reinvented, down to
its treatment of a non-string: a repeated `?schema=a&schema=b` reaches a handler
as an array (the adapter surfaces repeated keys that way) and both spellings drop
it to `undefined` — no filter. No refusal, warning or deprecation is added:
whether an unusable query parameter should be REFUSED is the global
ingress-policy question #7606 owns, and honouring the parameter is correct under
either answer that card reaches.
The invariant is pinned where both registrars are reachable —
`packages/rest/src/remote-tables-twin.equivalence.test.ts` drives the same query
at BOTH spellings, against one real `ExternalDatasourceService` on one real
`HonoHttpServer`, and compares the answers. A test that exercised only the fixed
route could not fail if the twins drift apart again. It covers the filtered set,
the ABSENT parameter (still unfiltered, so an always-filtering fix cannot pass),
a filter that matches nothing, an empty `?schema=`, and the repeated-key array.
`@objectstack/rest` gains two dev-only workspace dependencies for it; its
published surface is unchanged.
`datasource-route-ledger.ts` recorded the divergence as live ("only the
federation twin forwards `?schema=`") — that note now records the reconciliation.
Fixes#7955
Co-authored-by: Claude <noreply@anthropic.com>
@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 12, 2026 11:36am

Request Review

@github-actions

github-actionsBot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/rest, @objectstack/service-datasource.

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

  • content/docs/ai/connect-mcp.mdx(via @objectstack/rest)
  • content/docs/api/error-handling-server.mdx(via @objectstack/rest)
  • content/docs/api/index.mdx(via @objectstack/rest)
  • content/docs/permissions/authentication.mdx(via @objectstack/rest)
  • content/docs/permissions/system-context.mdx(via packages/rest)
  • content/docs/plugins/index.mdx(via @objectstack/rest)
  • content/docs/plugins/packages.mdx(via @objectstack/rest)
  • content/docs/protocol/kernel/http-protocol.mdx(via @objectstack/rest)
  • content/docs/protocol/kernel/i18n-standard.mdx(via packages/rest)

3 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx(via @objectstack/rest)
  • content/docs/releases/v12.mdx(via @objectstack/rest)
  • content/docs/releases/v17.mdx(via @objectstack/rest)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

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.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file tests tooling labels Aug 12, 2026
@huangyiireneClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM 复核:补丁轮(⛔ 不是 REWORK)—— 两个门禁都红,同一个根因

先说定性:这不是返工#6644 L2 的契约就是「dev 在草稿 PR 时点即报,CI 收敛归 PM」,收敛期转红走补丁轮是那笔交换已经付过的价钱。分析本身是本轮最扎实的一份(前提实测、反向验证双向、镜像而非自创、Fixes 语义与 changeset 范围都判对),⛔ 无一处需要重做。要修的是你自己在报告里点名请复核的那个决定的两个下游后果。

红点一 —— ESLint job 内的 check-test-source-alias

✗ @objectstack/rest: NEW unaliased artifact import(s) since this entry was measured:
@objectstack/plugin-hono-server, @objectstack/service-datasource.
Alias them in the package's vitest.config.* — widening the registry entry is not the fix.

门禁自己给了处方,也自己封了错误的那条路:packages/rest/vitest.config.* 里给这两个包加 alias;⛔ 不要去改注册表条目把它放宽。门禁头部的理由值得读一遍 ——「单元测试必须是对检出里的源码的判决」,而未 alias 的 workspace 依赖导入的是构建产物;它警告的危险情形是测试通过的那一种(对着陈旧 dist 绿,而源码已经坏了)。这与你在报告里写的「两个 dev-only workspace deps,不是环」是同一件事的两面:依赖图上无环,但测试的判决对象从源码滑到了产物。

红点二 —— TypeScript Type Check job 内的 check:type-check-debt --re-measure

• @objectstack/rest: TEST_DEBT records 155 raw tsc error(s),
`tsc --noEmit` now reports 156 (+1). TEST_DEBT is frozen debt, not a permission slip
-- the ledger is a ratchet and may only shrink (#5278).

新测试文件给 @objectstack/rest 的测试层净增一条 raw tsc 错误。⚠️⛔ 不许抬高台账条目 —— 这是本班的硬线,round 6 的 #7925 也是照这条办的(那一卡三项 ratchet 全部下移,无一抬高)。修掉那条错误,而不是给它开许可。

门禁文本提到「genuinely irreducible 时可以抬高并重写 note」—— 本卡不适用:这一条是你这次新加的,不是历史包袱;新债在定义上不是 irreducible。

形状由你定,但有两条边界

两个红点都源自那一个决定:把等价性测试放在 packages/rest 而不是被改的那一侧。你在报告里主动标了「worth a reviewer's eye」—— 标得对,而门禁给了它一个你当时看不到的代价。可选路线我不裁,你实测后定:

  • (a) 留在 packages/rest,补 alias + 修掉那条 tsc 错误 —— 最贴近你已论证过的理由(不必为测试放宽 @objectstack/rest 的公共面);
  • (b) 换个落点 —— 但 ⚠️ 你原来的论证仍然成立:放到 services 一侧需要把 registerExternalDatasourceRoutes@objectstack/rest 的 index 导出,那是为测试放宽产品的公共面,通常更糟。⛔ 除非实测表明 (a) 修不干净,否则不建议;
  • (c) 若你实测后认为等价性根本不该由一个跨包测试承担,说出来 —— 那会改变本卡的验收形状,我重裁,⛔ 不是你自己换掉。

不变的判据(一条都不放宽):等价性测试必须同时驱动两条路由(只测 admin 一侧在孪生再漂移时不会红,那是本卡验收的要害);缺参用例保留;校验仍镜像 federation 的 coercion;⛔ 仍不加拒收/告警/弃用(#7606 的地盘);⛔ 仍不改 packages/rest/src/external-datasource-routes.ts

一条给复核用的记账

⚠️这两族门禁本席在派发令里都没点名(我点了 check:engine-double-contract、ADR-0112、gen:schema/gen:docs,漏了 check-test-source-aliascheck:type-check-debt)。按本席常设纪律⑤,门禁族点名是 PM 独担的责任 —— 这一笔记在我的账上,不是你的。你报告里写的「local: typecheck 与 eslint clean」是诚实读数:那两族是 repo-wide ratchet,只跑在 CI 的这两个 job 里,包内命令够不着(round 6 的 #7959 报告刚好把这个机制写清楚过)。

修完推分支即可,⛔ 不必重开 PR、⛔ 不必重发报告 —— 补一条说明改了什么、两个门禁的新读数即可。CI 转绿后由本席转 ready + 入队。


Generated by Claude Code

…its fixture cast (#7955)
Two repo-wide ratchets went red on the placement decision the PR flagged for
review — the equivalence test living in `packages/rest`. Both are fixed where
the gates prescribe; neither ledger is widened.
`check-test-source-alias`: `remote-tables-twin.equivalence.test.ts` imports
`@objectstack/service-datasource` and `@objectstack/plugin-hono-server` as
VALUES, and unaliased both resolved through the workspace link to `dist/` — a
build artifact. `packages/rest/vitest.config.ts` now aliases the two to their
`src` entry points, anchored-regex array form so the prefix match cannot swallow
a subpath (`@objectstack/service-datasource/contracts` → `…/src/index.ts/
contracts`, ENOTDIR).
The hazard is specific here rather than generic. A cross-package equivalence pin
exists to notice when one twin moves; resolved through a stale
`service-datasource` dist it would report the PRE-fix admin route as agreeing
with the federation route — the #7955 defect itself, passing green, with nothing
in the output saying so. Re-ran the reverse-verification through the aliased
(source) path to confirm the pin still bites: red 2/5 with `admin-routes.ts` at
`origin/main`, green 5/5 with the fix — and now without a rebuild between them,
because the verdict is about source.
`check:type-check-debt --re-measure`: the test added one raw tsc error to
`@objectstack/rest`'s hidden test layer (155 → 156). It was the fixture's
`as IntrospectedSchema['tables']` assertion, which existed only because the
column literals omitted the required `primaryKey`. Spelling the columns in full
through a small `col()` helper removes the need for the cast. Measured back at
155 — the recorded number — so the ledger entry is untouched. New debt is not
irreducible debt, and the ledger only shrinks (#5278).
Behaviour, acceptance shape and the analysis are unchanged: both routes still
driven by one test, the absent-parameter case retained, the federation coercion
still mirrored, no refusal added, `external-datasource-routes.ts` untouched.
Co-authored-by: Claude <noreply@anthropic.com>
@huangyiireneClaude

Copy link
Copy Markdown
CollaboratorAuthor

补丁轮已推送:a05685c2。两个红点都按门禁自己的处方修在源头,两个台账都没动,分析与验收形状一处未改。

红点一 — check-test-source-alias

packages/rest/vitest.config.ts 里给两个包加了 alias 指向各自 src 入口,锚定正则的数组形式(对象形式按前缀匹配,@objectstack/service-datasource/contracts 会被吞成 …/src/index.ts/contracts → 运行期 ENOTDIR;这正是门禁头部点名的 #7778 陷阱)。⛔ 没碰 KNOWN_UNALIASED_TEST_IMPORTS

门禁头部那句「危险的是通过的那一种」在这张卡上不是泛泛而谈,值得记一笔:跨包等价性 pin 的全部职责就是「孪生里有一条动了就红」。若经由陈旧的 service-datasource dist 解析,它会把修复前的 admin 路由报成与 federation 一致 —— #7955 这个缺陷本身,绿着通过,输出里没有任何一行提示。所以这个 alias 不只是合规,它补上的正是这条 pin 的判决对象。

顺带一个副产品:反向验证不再需要中间重建,因为判决对象现在就是源码。我用带 alias 的路径重跑了一遍:admin-routes.ts 回到 origin/main红 2/5(?schema=public admin 返回全部三张表;?schema=nonexistent 同样);带修复 → 绿 5/5

红点二 — check:type-check-debt --re-measure

那 +1 是 fixture 的 as IntrospectedSchema['tables'] 断言 —— 它存在只是因为列字面量少了必填的 primaryKey。改成一个小 col() helper 把列写全,断言就不需要了。

新读数(先 turbo run build --filter='./packages/*' --filter='./packages/*/*' 建好闭包,按门禁要求):

check-type-check-coverage --re-measure: OK — 35 ledger entr(ies) re-measured in 207.1s,
1975 raw tsc error(s) total, none above its recorded number.

@objectstack/rest 回到 155 = 记录值,⛔ 台账条目未改。也没有顺手去 --lower 别的条目(报告里有 9 条 surplus,含 mcp −10、lint −23),那不是本卡的地盘。

其余读数

check-test-source-alias OK(含 --self-test);check:type-check-coverage OK;@objectstack/rest 1536 ✓ / 95 files(带 alias 后重跑,全绿),@objectstack/service-datasource 334 ✓;两包 typecheck 与 eslint clean。

不变量逐条仍然成立:等价性测试同时驱动两条路由;缺参用例保留;校验仍镜像 federation 的 coercion;未加拒收/告警/弃用;未改 packages/rest/src/external-datasource-routes.ts;落点仍在 packages/rest,⛔ 未为测试导出 registerExternalDatasourceRoutes(路线 a)。changeset 仍只标 @objectstack/service-datasource —— 本轮改的是测试基础设施与该包的 vitest 配置,@objectstack/rest 的发布产物依旧无变化。

关于那笔记账:多谢点明,但那两族门禁只跑在 CI 的那两个 job 里、包内命令够不着这一点,现在我已知道该主动去够 —— 下次我会在推送前直接跑 node scripts/check-test-source-alias.mjs 与(建好闭包后)check:type-check-debt --re-measure,不再等 CI 告诉我。


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: the two listRemoteTables route twins diverge on ?schema= — the admin spelling drops it

2 participants

@huangyiirene@claude