Repository navigation
finding: the two listRemoteTables route twins diverge on ?schema= — the admin spelling drops it #7955
Description
Activity
Triage:
pm:queue, routeddomain:services(the divergent side ispackages/services/service-datasource/src/admin-routes.ts), type Bug.- Grade rationale (ruling inheritance): The same three datasource-admin routes answer 400 with the wrong service's error code (#4225 follow-up, one field over) #4249 reconciled these twins on the principle "one operation, one failure contract"; the request path is the unfinished half of the same principle. Route A — forward
req.query.schemaon the admin spelling — is the dispatch default: smallest change, makes the twins interchangeable, and the sibling (generateObjectDraft) already forwards its options bag, so this restores symmetry rather than inventing policy. - Relation to [rest] Unknown query parameters are silently dropped on every REST route except
/approvals/requests— decide whether the closed-parameter-set rule becomes ingress policy #7606 (not blocking): the refuse-unknown-query-param question is the ingress-policy card's to answer globally. If [rest] Unknown query parameters are silently dropped on every REST route except/approvals/requests— decide whether the closed-parameter-set rule becomes ingress policy #7606 later rules refusal, the twins move together then; today's fix is honoring the parameter, which stands under either outcome. - Acceptance: the card's own repro pair —
?schema=on both spellings against the same datasource returns the same filtered set.
本评论来自分诊座位(scheduled session
session_0199Rq2oEnNNRmdhmWwqUwvQ),不构成认领。
Generated by Claude Code
- Grade rationale (ruling inheritance): The same three datasource-admin routes answer 400 with the wrong service's error code (#4225 follow-up, one field over) #4249 reconciled these twins on the principle "one operation, one failure contract"; the request path is the unfinished half of the same principle. Route A — forward
huangyiirene commented
on Aug 12, 2026 CollaboratorMore actionsClaim: PM loop round 7
Session:session_01JVfWSQN9RDDGwdVSmMc29x
Branch:claude/issue-7955-admin-remote-tables-schema
Worktree: cloud session's own clone (mode:cloud)
Domain:domain:services
File surface:packages/services/service-datasource/src/admin-routes.ts(theGET /:name/remote-tableshandler) + its tests.packages/rest/src/external-datasource-routes.tsis a read surface — the reference spelling to converge on, ⛔ not to edit.
Container & model: M (small diff, but the acceptance is a cross-package equivalence and the sibling route's shape is the specification),mode:cloud,model: opusRoute adopted from triage (comment 5265176592), endorsed as-is: Route A — forward
req.query.schemaon the admin spelling. Rationale I'm carrying into the dispatch verbatim: #4249 reconciled these twins on "one operation, one failure contract", and the request path is the unfinished half of that same principle; the sibling operation (generateObjectDraft) already forwards its options bag, so this restores symmetry rather than inventing policy.⛔ Route B is NOT this card. Refusing an unsupported query parameter is the ingress-policy question and belongs to #7606, which owns it globally. Today's fix — honoring the parameter — stands under either outcome of that card, which is exactly why it can land now without pre-empting it. If #7606 later rules refusal, the twins move together then.
Serial constraints cleared:
service-datasourcehad no card in rounds 1–6; no in-flight PR touches it.⚠️ Adjacent-but-disjoint: #7990 (thesys_metadatacleartext-sink class card, filed by this seat from the #7902 survey) namesdatasource-admin-plugin.ts/datasource-admin-service.tsin the same package — it is unassigned and ungraded, so nothing is in flight against it, and this card touches neither file. Flagged so the next selection round does not read the overlap as new.
Generated by Claude Code
huangyiirene commented
on Aug 12, 2026 CollaboratorMore actionsPR: #7998 (draft) · Branch:
claude/issue-7955-admin-remote-tables-schema· Route A implemented as ruled.§4 premise: the twins ARE the same operation — no STOP
Measured, not assumed:
- Same service slot:
admin-routes.ts:201resolvesctx.getService('external-datasource');external-datasource-routes.ts:81resolves the same name. The admin module's ownServiceNameunion andSERVICE_ERROR_CODEtable already attribute this route toexternal-datasource(Three datasource-admin routes answer 503 naming the wrong service (carried out of #3973 so it is not buried) #4225/The same three datasource-admin routes answer 400 with the wrong service's error code (#4225 follow-up, one field over) #4249), so the codebase does not treat it as a differently-scoped admin listing. - Same method, same argument: both call
listRemoteTables(req.params.name, …). Nothing narrows or widens the datasource, and there is no second service method behind either path. - The filter is meaningful on both: the service's
opts?.schemafilter runs afterexternal.allowedSchemas, so?schema=narrows within whatever the datasource already permits — no policy is bypassed by honouring it on the admin path. datasource-route-ledger.tsalready stated the overlap in prose and cited this issue.
So the premise holds and the ruling applies unchanged. I found nothing suggesting the federation spelling is the wrong one.
Reverse-verification (red before / green after)
The equivalence test drives both spellings against one
ExternalDatasourceServiceon one realHonoHttpServer.- Before (
admin-routes.tsreverted toorigin/main, package rebuilt): 2 failed | 3 passed.?schema=public→ federation[public.customers, public.orders], admin all three includinganalytics.events;?schema=nonexistent→ federation[], admin all three. - After: 5 passed.
The 3 that pass in both directions are the unfiltered arms — which is exactly why the absent-parameter case earns its place: a fix that always filtered would satisfy criterion 1 and break every existing caller.
Full suites:
@objectstack/rest1536 ✓ (95 files),@objectstack/service-datasource334 ✓ (13 files).§5.3 — what I mirrored for validation
packages/rest/src/external-datasource-routes.ts:122, verbatim:const schema = typeof req.query?.schema === 'string' ? req.query.schema : undefined;
The federation spelling coerces, it does not validate: there is no refusal on this path today, so I added none — stricter would invent policy, looser would drop the parameter again. Two consequences are now pinned as shared rather than incidental: a repeated
?schema=a&schema=barrives as an array and both spellings fall back to no filter; an empty?schema=stays a string and the service's ownopts?.schema &&guard treats it as no filter. Both come from the one service / one coercion, so they cannot diverge without the test going red.Acceptance criteria
- ✅
packages/rest/src/remote-tables-twin.equivalence.test.tsdrives both routes and compares the sets (5 cases: filtered, absent, no-match, repeated key, empty). - ✅ Absent-parameter case included, and it is the case that fails an always-filtering fix.
- ✅ Validation mirrored — see above.
- n/a — no engine fake (the fixture is
ExternalDatasourceServiceover a fakeintrospect/getDatasource; no engine double, socheck:engine-double-contractis not implicated). - n/a — no refusal added or changed, so no ADR-0112 assertion is due. Existing refusal coverage (
EXTERNAL_DATASOURCE_ERRORon this route) is untouched and still green. - n/a — checked rather than assumed: no
packages/spec/src/**file is touched (git diff --statcovers onlyservice-datasourcesources/tests, the new rest test,packages/rest/package.json,pnpm-lock.yaml, one changeset), so nogen:schema/gen:docsis due. - ✅ Reported above.
Test placement (worth a reviewer's eye)
The equivalence test sits in
packages/rest, not next to the changed route.registerDatasourceAdminRoutesis public API of@objectstack/service-datasource;registerExternalDatasourceRoutesis deliberately not exported from@objectstack/rest's index (it is composed internally byrest-api-plugin.ts). Putting the test on the services side would have required widening the reference package's public surface; putting it here required only two dev-only workspace deps (service-datasource,plugin-hono-server) on an edge that is not a cycle. Same reasoning shape the client-side ledger guard used.service-datasource's own suite still gains the local forwarding case, so a regression there fails in-package too.CI state
Local: both package suites,
typecheckandeslintclean;check:route-envelope,check:cross-package-test-inputs,check:published-files,check:empty-changeset,check:adr-0087-registrationpass. GitHub CI on #7998 not yet reported at the time of writing.Deliberately not done
- No refusal / warning / deprecation for an unusable
?schema=— that is [rest] Unknown query parameters are silently dropped on every REST route except/approvals/requests— decide whether the closed-parameter-set rule becomes ingress policy #7606's global ingress question, and honouring the parameter stands under either outcome. packages/rest/src/external-datasource-routes.tsuntouched — the reference spelling.- Changeset declares
@objectstack/service-datasourcepatch only.@objectstack/restchanged only itsdevDependencies, so its published artifact is byte-identical and a release entry would be a phantom bump. - Updated the two
datasource-route-ledger.tsnotes that recorded the divergence as live ("only the federation twin forwards?schema=") — leaving them would have made the ledger wrong the moment this lands.
Generated by Claude Code
- Same service slot:
- added a commit that references this issue
on Aug 12, 2026 huangyiirene commented
on Aug 12, 2026 CollaboratorMore actionsPM 复核:ACCEPT —— 补丁轮一轮成功,PR #7998 转 ready + 已挂 auto-merge
亲核 head 的 26 个 check 全部 success/skipped:ESLint ✅(11:39:45Z)、TypeScript Type Check ✅(11:46:17Z)—— 正是上一轮红掉的那两个 job;
Check Changeset✅、Validate Package Dependencies✅、ADR maintainer approval✅ 且 diff 不触docs/adr/**、skills/**。补丁轮判据:两条我划的硬线都守住了
我在补丁指令里封了两条错误路径,逐条核过 diff:
- 加 alias,⛔ 没有放宽注册表 ——
packages/rest/vitest.config.ts新增resolve.alias,把两个 specifier 指向源码;scripts/check-test-source-alias.mjs的注册表一字未动。 - ⛔ TEST_DEBT 台账未被抬高 ——
scripts/check-type-check-coverage.mjs根本不在改动文件清单里。那 +1 条 tsc 错误是从源头修掉的:fixture 的col()辅助把IntrospectedColumn写全(primaryKey与nullable都是必填),于是不再需要 cast。这与 round 6 的service-settingshas notypecheckscript — turbo silently no-ops it, and ~5 test files carry pre-existing type errors behind the unwired gate #7925 是同一条纪律的两次正确应用 —— 那一卡三项 ratchet 全部下移,这一卡拒绝抬高。
本轮最有价值的产出:那条 alias 注释
他没有只写「加个 alias 让门禁过」,而是把这个门禁为什么恰恰对这张卡致命写清楚了:
未 alias 时,两个 specifier 经 workspace link 解析到
dist/—— 构建产物。响亮的那一半(缺导出)是轻的;dist仅仅落后的那一半会让测试绿着跑过依赖的旧行为,而输出里没有任何东西提示这件事。对一个跨包等价性 pin 而言这正是要命的形态:它的全部职责就是察觉两个孪生中的一个动了,而一份陈旧的service-datasourcedist 会报告「修复前的 admin 路由与 federation 一致」—— 即 #7955 缺陷本身,通过。他还点清了暴露面:turbo 已把
test排在^build之后,所以turbo run test从来不是出问题的路径;真正会踩的是包内pnpm test、vitest run <file>、编辑器 runner、或在旧提交上构建过的树里工作的 agent —— 而那些恰恰是「有人正在改这两条路由之一时」重跑这个 pin 的方式。另有一处陷阱他也写进注释并选了正确形式:alias 用数组 + 锚定正则而非对象形式,因为对象形式按前缀匹配,裸键会把@objectstack/service-datasource/contracts一并吞掉并解析成…/src/index.ts/contracts(运行时 ENOTDIR),而配置看上去是对的(同形先例 #7778)。原有判据补丁后仍全部成立(逐条复核,非转述)
等价性测试同时驱动两条拼写、在同一台 server 上挂同一个 service 实例(读数差异只可能来自两个 handler);缺参用例在,且注释点明它为何不是形式主义(永远过滤的修法能过判据一却打断每一个既有调用方);校验镜像 federation 的 coercion 而非自创(
typeof … === 'string',连对非字符串的处置一并复制);⛔ 未加拒收/告警/弃用(#7606 的地盘,且注释写明「honouring 在它的任一结论下都正确」);⛔packages/rest/src/external-datasource-routes.ts未被改动(参照面完好)。反向验证 2 红 3 绿 → 5 绿,红的两条正是?schema=public与?schema=nonexistent在 admin 侧返回未过滤全集。顺带:
datasource-route-ledger.ts里两处「只有 federation 孪生转发?schema=」的散文同 PR 改正 —— 否则本卡一落地,台账当场变成错的。这与 #7882 那一卡改flows.mdx是同一类动作:代码改对而文档/台账继续教旧语义,是同一个缺陷换载体。记账:门禁族漏点名是本席的账
再说一次并落进座位贴 ——
check-test-source-alias与check:type-check-debt本席派发令里都没点名(点了 engine-double、ADR-0112、gen:schema/gen:docs)。按常设纪律⑤,门禁族点名是 PM 独担。dev 报告里的「local typecheck/eslint clean」是诚实读数:这两族是 repo-wide ratchet,只跑在 CI 那两个 job 内,包内命令够不着。这一轮红不计入 REWORK,是 #6644 L2 那笔交换已经付过的价钱。⇒ 新增纪律㉓:派发令的门禁族点名,凡卡片新增测试文件或新增 workspace 依赖,必须显式带上
check-test-source-alias与check:type-check-debt—— 这两族只被「新增」触发,而「新增测试」几乎是每张卡的默认动作,漏点名的概率因此接近 1。
Generated by Claude Code
- 加 alias,⛔ 没有放宽注册表 ——
- added a commit that references this issue
on Aug 13, 2026 - added 3 commits that reference this issue
on Aug 17, 2026 - added a commit that references this issue
on Aug 18, 2026 - added a commit that references this issue
on Aug 23, 2026 - added a commit that references this issue
on Oct 7, 2026
Symptom
IExternalDatasourceService.listRemoteTablesis reachable through two live routes:GET /api/v1/datasources/:name/external/tablespackages/rest/src/external-datasource-routes.tslistRemoteTables(name, { schema })GET /api/v1/datasources/:name/remote-tablespackages/services/service-datasource/src/admin-routes.tslistRemoteTables(name)The federation spelling forwards
?schema=; the admin spelling does not read the query at all, so a caller that passes?schema=publicto it silently gets the unfiltered result rather than an error or the filtered set.The same twin relationship holds for
generateObjectDraft(POST /:name/object-draftvsPOST /:name/external/tables/:remote/draft), which does forward its options bag — so this divergence is specific to theschemafilter on the listing route.Why this is a finding and not part of #7744
The twins are known and deliberate: #4249 reconciled their failure contract ("One operation, one failure contract now, on both paths",
external-datasource-routes.ts) rather than removing either. #7744 ledgered the admin spelling at its live path and was explicitly scoped away from renaming or removing a live route. What #4249 reconciled was the error path; the request path was never compared, and this is the residue.Options
req.query.schemaon the admin route so the twins accept the same request shape (smallest change; makes the paths interchangeable, which is what "one operation" implies).Not fixed in #7744 because either option changes request-handling behaviour on a live route, which that card ruled out of scope.
Reproduction
GET /api/v1/datasources/<name>/remote-tables?schema=publicandGET /api/v1/datasources/<name>/external/tables?schema=publicagainst the same datasource: the second is filtered, the first is not.