Uh oh!
There was an error while loading. Please reload this page.
fix(rest): POST /datasources/:name/external/validate does URL-scoped work - #10960
Conversation
) The route called validateAll() — every federated object on every federated datasource, each validation driving a live introspect(datasource) — and then kept only the rows matching :name. The rows were right; the work was not scoped. Measured at the branch point: a request for one of three federated datasources introspected all three. ExternalDatasourceService gains validateDatasource(datasource), the scoped twin of the sweep composed from the same primitives (listObjects -> filter -> validateObject) with the same per-object catch; the REST registrar probes for it and answers 503 when the wired service has no scoped spelling, rather than falling back to the fan-out. Response is unchanged, pinned against the sweep-then-filter answer in both packages. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019bmVFqoQPq63zhKrxdYG1r
📓 Docs Drift Check9 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. ✅ What this run could not see
Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 33a86460f34f76ed3b247910fb34039707d55cd1 && git checkout 33a86460f34f76ed3b247910fb34039707d55cd1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 926778bce01620ae56a9b0eecbf0400c43a64aa8 5163a56bea70dc8f6ad43c4c87cecfe75932250c && git checkout -B drift-repro 926778bce01620ae56a9b0eecbf0400c43a64aa8 && git merge --no-ff 5163a56bea70dc8f6ad43c4c87cecfe75932250c
node scripts/docs-audit/affected-docs.mjs --json 926778bce01620ae56a9b0eecbf0400c43a64aa8 |
Uh oh!
There was an error while loading. Please reload this page.
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 32542973474 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Fixes#10537
POST /api/v1/datasources/:name/external/validateasked theexternal-datasourceservice for
validateAll()— every federated object on every federated datasource,each validation driving a live
introspect(datasource)remote-schema read — and thenkept only the rows whose
datasourcematched the URL. The rows were correct; thework was not scoped.
The fan-out, re-derived at the branch point
Measured through the real Hono adapter and the real
ExternalDatasourceServiceover arecording introspector, on a fixture with three federated datasources
(
external-datasource-validate-scope.test.ts, run before the production change):Three remotes dialled for a one-datasource request; three remotes dialled to answer the
empty report an unknown name already answered. Same run,
Tests 3 failed | 3 passed—the three that passed are the body-shape cases, which is the point below.
What changed
ExternalDatasourceServicegainsvalidateDatasource(datasource): the scoped twin ofthe sweep, composed from the same primitives (
listObjects→ filter →validateObject),the same federation predicate — now spelled once and shared with
validateAll()— andthe same per-object catch. The REST registrar calls it. The same request now dials
['wh_a']; the unknown name dials nothing.Response unchanged: same rows, same diffs, same
data.okverdict, same200, thesame
400 EXTERNAL_DATASOURCE_ERRORon a service refusal, the same503 SERVICE_UNAVAILABLEwhen federation is not wired in, and an unknown:namestillanswers an empty, vacuously
okreport rather than a404. The selection is keyed ono.datasource ?? 'default', which is exactly the valuevalidateObjectreports back asresult.datasource— so "the rows the post-filter kept" and "the objects this selects"are the same set. Every case pins that equivalence by computing the sweep-then-filter
answer live from a second service instance and deep-comparing, rather than against a
remembered body.
validateDatasourceis deliberately not onIExternalDatasourceService: triageauthorized the composition, not a contract-surface expansion. The service is the only
registrant of the
external-datasourceslot, and the registrar probes for the method;a wired service without it takes the same
503arm the family already takes when theservice cannot serve a route — not a silent fallback to
validateAll(), which wouldleave the fan-out reachable on a path no test drives.
How the work — not the output — was proved to narrow
The output was already right, so a test comparing response bodies would pass on both
sides of this change. The load-bearing pins are the call record:
introspected— which datasources were dialled — asserted as an exact list, over afixture with three federated datasources so the assertion can fail;
validateAll()asserted never called on the scoped path (vi.spyOnon the realservice instance);
wh_bunreachable) asserted never dialled for awh_arequest.
Service-side,
validateDatasourceis pinned the same way plus the equivalence: for eachof the three datasources, the scoped rows deep-equal the sweep-then-filter rows while the
sweep's own introspection count is asserted greater than one — otherwise the equivalence
line would be vacuous. The
default-bound-external-object edge is pinned too: it is thecase where "bound to
:name" and "federated" come apart, and it is why the scoped filteris not simply
datasource === name.Reverse verification
Predicted before running: reverting only the route production change turns the three
call-record cases RED while the body-shape cases stay GREEN, and additionally reddens the
two fixtures whose fakes now carry only the scoped spelling — i.e. more diagnostics,
across three files, not one.
Leg 1 —
git restore --source=origin/main -- packages/rest/src/external-datasource-routes.ts(the fix was committed first, so the restore point is a commit, and the service helper and
fixtures stayed in place):
Leg 2 —
git checkout HEAD -- packages/rest/src/external-datasource-routes.ts(
git status --porcelainempty afterwards, so the restored bytes are the committed ones):No rebuild mediates either leg: the mutated file is the test's own package source,
imported relatively, and the one cross-package import
(
@objectstack/service-datasource) is aliased to that package'ssrc/bypackages/rest/vitest.config.ts— an alias that predates this PR, for the reason its owncomment gives.
@objectstack/service-datasourcewas rebuilt anyway before typechecking,since
packages/rest'stscreads itsdist/*.d.ts.Verification
Dependency closure built first, then, at this PR's final commit
5163a56be:@objectstack/service-datasourcetestsTest Files 22 passed (22)·Tests 519 passed (519)@objectstack/resttestsTest Files 134 passed (134)·Tests 2179 passed (2179)@objectstack/service-datasourcetypecheck@objectstack/resttypecheckGate union derived at that commit with
node scripts/pm/dispatch-gates.mjs(no pathspassed — the script derives the change set itself from the merge base). All 20 green,
each exit code captured before any pipe:
The ratchet, in its own words — it first refused on an unbuilt closure
(
--re-measure cannot run: 31 workspace dependencies ... have no built type entry point on disk), which is a NOT-MEASURED verdict rather than a pass, so the closure was builtexactly as
lint.ymldoes and it was re-run:That one matters here rather than being ceremony:
@objectstack/restis aTEST_DEBTledger entry, so the new test file is exactly what it measures. No gate was weakened, no
threshold moved, no ledger or baseline edited.
Not in this PR
ExternalValidationPlugin.runDriftCheck(ctx, datasource)inpackages/runtimecarriesthe same shape —
validateAll()then a filter onr.datasource— andvalidateObjectre-introspects once per object rather than once per datasource. Both aredifferent packages / different consumers with their own verification surface, so they are
filed separately rather than ridden in here; neither is addressed by this PR. Issue
numbers are in the dev report on #10537.
Generated by Claude Code