Repository navigation
fix(rest)!: refuse unknown keys on the dataset query body (#29156) - #29251
Conversation
…main) Claude-Session: https://claude.ai/code/session_01ADHkmQ6M1P18JzkN4aDNPb Co-authored-by: Claude <noreply@anthropic.com>
A runtimeFilter beside selection answered 200 with the unfiltered aggregate. The outer body is now a closed set measured from the handler's read points; dataset beside datasetName is refused too. Claude-Session: https://claude.ai/code/session_01ADHkmQ6M1P18JzkN4aDNPb Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmQ6M1P18JzkN4aDNPb Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 19 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 c7842c2ce05d9395a760aeaedb1095fc6ad75dfe && git checkout c7842c2ce05d9395a760aeaedb1095fc6ad75dfe
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f6a04e3af7756446bb7a2e73973f09e011090312 9b9d5b7b04d69e7dc908254deca27529d35a5b9d && git checkout -B drift-repro f6a04e3af7756446bb7a2e73973f09e011090312 && git merge --no-ff 9b9d5b7b04d69e7dc908254deca27529d35a5b9d
node scripts/docs-audit/affected-docs.mjs --json f6a04e3af7756446bb7a2e73973f09e011090312
|
|
domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Should-fix (not a FAIL):
Nothing here takes a declared capability away from a consumer: every body the census found a producer sending is served as before and reaches the service with the same arguments, and the only bodies refused are ones the spec's own Implemented-by: VERDICT: PASS |
…ity check A falsy datasetName beside dataset ran inline before the door and runs inline again; only a truthy name beside a truthy dataset is ambiguous. Claude-Session: https://claude.ai/code/session_01ADHkmQ6M1P18JzkN4aDNPb Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt Contract reviewServed-tier: This is the delta re-review of the findings-only commit on top of the full review at ① Derived judgments
② Semver levelUnchanged by the delta and still right: ③ Boundary flags
④ Design judgment
Nothing in the delta takes a declared capability away from a consumer; it returns one, and the only consumer-visible difference from Should-fix (not a FAIL): update the PR body's Tests sentence to 19 cases and name the two falsy-name controls in §2, so the Tests and Verification sections agree. Implemented-by: VERDICT: PASS |
|
domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt 席位核对ACCEPT · The maintainer's words for this seat, verbatim: 「接手所有开发,包括跟进之前未完成的开发落地。」
Generated by Claude Code |
|
domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt Landing record · PR #29251
Release: ships Generated by Claude Code |
domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt
Requested by Steve · project thread
Fixes #29156
Clause-②: no (narrowing)
Metadata-protocol: none
维护者速读
POST /analytics/dataset/query现在拒收请求体里它不认识的键(400 VALIDATION_FAILED,点名该键);把runtimeFilter放错在selection外面时,提示「did you meanselection.runtimeFilter」。同时给了dataset又给datasetName也拒收。analytics-routes.ts的一处调用。packages/rest/src/analytics-dataset-body-door.ts(判定逻辑与提示);packages/rest/src/analytics-routes.ts(接入点,在 selection 判定之前);packages/rest/src/analytics-dataset-body-door.test.ts。pnpm --filter @objectstack/rest exec vitest run src/analytics-dataset-body-door.test.ts,预期 19 条全过(其中 10 条在 main 上为红)。What changes
POST {basePath}/analytics/dataset/queryjudgedbody.selection(throughdatasetSelectionRefusal),body.datasetandbody.datasetName, and ignored every other top-level key. AruntimeFilterwritten besideselectioninstead of inside it answered 200 with the unfiltered aggregate;"bogus": 1besideselectionanswered 200 too. The sibling doorPOST /analytics/queryrefuses an unknown key, and this door's ownselectionrefuses one with a "did you mean" hint.packages/rest/src/analytics-dataset-body-door.ts:datasetQueryBodyRefusal(body)over the closed setDATASET_QUERY_BODY_KEYS=dataset,datasetName,previewDrafts,selection, measured from the handler's read points (the handler reads exactly these four offreq.body).400 VALIDATION_FAILED, messageUnrecognized key(s) on this dataset query: ...(the sibling door's opening), onedetails.fields[]entry per key withcode: unknown_field. The query never runs.runtimeFilter,filter,filters,wheresaydid you mean selection.runtimeFilter?; any other selection member saysdid you mean selection.KEY?. The member list is read offDatasetSelectionSchema.shape, not transcribed.datasetbeside a truthydatasetName(the handler's own truthiness) answers400 VALIDATION_FAILEDnaming both (invalid_shape). A falsydatasetNamebesidedatasetstill runs inline, and a falsydatasetbeside a name still runs the saved dataset, as before.analytics-routes.ts: one call, right after the body is read and before theselection.measurescheck, so a misplaced member is named before "selection.measures must be a non-empty array".{ code, message, details.fields }, the one an unknownselectionkey already answers here), not the nested{ error: {...} }the sibling dispatcher door writes. One door, one dialect..changeset/29156-dataset-query-body-unknown-keys.md(@objectstack/restpatch, BREAKING banner, Before / Now / Unchanged bullets and the remedy,adr-0087: not-required (no-migration-prescription)).content/docs/data-modeling/analytics.mdxstates the body shape, so it now says which keys the body takes and what is refused.No
packages/specorui/edit.Why
A dropped filter is a wider number with nothing to tell it apart from a filter that matched everything: the REST ingress policy's silent-widening case (
query-allowlist.ts), on the body instead of the query string.Producer census (before narrowing)
@objectstack/clientanalytics.queryDatasetdataset?,datasetName?,selection,previewDrafts?ui/packages/data-objectstackqueryDataset(dashboards, reports, charts, filter bar, dataset preview)datasetNameORdataset(a ternary),selection,previewDraftsonly when true@objectstack/verifydashboard-window.tsdatasetName,selectionanalytics.mdx, platform-checklistdashboards.json/studio-authoring.jsondatasetName,selectiondatasetordatasetName,selectionpackages/mcp,service-ai)No producer sends a key outside the four, and none sends both
datasetanddatasetName, so the pair is refused in this PR (delta acceptance 3).Tests
packages/rest/src/analytics-dataset-body-door.test.ts(new, 19 cases, through the real route): §1 refusals (top-levelruntimeFilteron a saved and an inline dataset,filter/filters/where, a misplaceddimensions,bogus, several keys sorted, the accepted keys listed), each asserting status,code, the named keys, the remedy, and thatqueryDatasetwas never called; §2 thedataset+datasetNamerefusal (no lookup, no query) and its three controls (a''and anulldatasetNamebesidedatasetrun inline; anulldatasetbeside a name runs the saved dataset); §3 controls asserting the arguments the service received (datasetName, inlinedataset,selection.runtimeFilter,previewDrafts, the unchanged selection-level refusal, the unchanged "Provide body.dataset" message).packages/qa/dogfood/test/analytics-dataset-body-door.dogfood.test.ts(new, 4 cases, showcase stack over HTTP): plain body 200 with a count above 0;selection.runtimeFilter200 and narrows to 0; top-levelruntimeFilter400 with the remedy;bogus400.Verification
Head
9b9d5b7b(a fast-forward ondbf091e2, the merge oforigin/main14208f2 into the fix), objectstack-ai/objectstack. The9b9d5b7bround aligned thedataset+datasetNamecheck to the handler's truthiness and added two pins (a''and anulldatasetNamebesidedatasetrun inline). At9b9d5b7b: the rest pin file 19 of 19; ablation of the old condition reds the''pin (1 failed, 18 passed) and restores to HEAD; rest suite 368 files, 6982 tests passed; typecheck exit 0; the same 90 derived gates all exit 0. The readings below are fromdbf091e2unless stated; the dogfood battery was not re-run, since no dogfood test sends both keys.Red first on main, green with the fix
692e0dc8, the test committed alone (e7b0ead3):pnpm --filter @objectstack/rest exec vitest run src/analytics-dataset-body-door.test.ts→ Tests 10 failed | 7 passed (17); every refusal case readexpected 200 to be 400with the body{"rows":[],"fields":[]}(the query ran), every control green.1d599e3e): Tests 17 passed (17); dogfood pin Tests 4 passed (4).Ablation of the refusal (scripts/ablation-replace.mjs, rebuilt, dist proven)
if (bodyRefusal) {→if (bodyRefusal && false) {inanalytics-routes.ts(anchor 1 → 0, blob 372cafb8f9a0 → 3142e06b6c1b);@objectstack/restrebuilt with--force;ablation-dist-preflightmarker present indist/index.cjsanddist/index.js.runtimeFiltercase read{"rows":[{"project_count":5}], …, "sql":"SELECT COUNT(*) AS \"project_count\" FROM \"showcase_project\""}: expected 200 to be 400: the unfiltered count, while theselection.runtimeFiltercontrol on the same stack answers 0.git diff HEADempty; rebuilt,--absentmarker absent from all 6 built files, tree clean; rest pin 17/17, dogfood pin 4/4.Package suites and lint
pnpm --filter @objectstack/rest test→ Test Files 368 passed (368) · Tests 6980 passed | 332 skipped, exit 0 (at6b02dae7; the merge brought nopackages/restor analytics path).pnpm --filter @objectstack/rest typecheck→ exit 0 (test layer 0 errors).eslint packages/rest packages/qa/dogfood --no-inline-config(root config) → exit 0.Full dogfood battery (narrowing), final head dbf091e
pnpm --filter @objectstack/dogfood testunder os-verify-lock →VERDICT command-exit 0· Test Files 389 passed | 3 skipped (392) · Tests 3212 passed | 28 skipped (3240). No failure.Derived gates at dbf091e
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack→ 90 commands; all run, all exit 0.check-plugin-teardown-shape.mjs --self-testfirst answered exit 3 (pinned commit 621a487 missing from the shallow clone); aftergit fetch --depth=1 origin 621a4876…it passed (48 cases).--ranreconciliation: 90 derived, 90 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero).check-changeset-no-major.mjs --base origin/main --event(this body) → exit 0:no (narrowing)+noneatpatch.check-adr-0087-registration.mjs→not-required (no-migration-prescription)notice.Human Minimum
packages/specholds no runtime logic. (no spec edit)pnpm lintpasses. (eslint over the touched packages, see Verification)main, one pull request.packages/spectouched: n/a, not touched.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Acceptance notes
Clause-②: no; this refuses bodies that answered 200 before, so the PR and changeset carryClause-②: no (narrowing)(the delta's narrowing rules:!title, BREAKING banner,adr-0087:marker, full dogfood battery, ablation).previewthroughrefuseRepeatedQueryParamsonly); the ingress policy converts routes per lane.Generated by Claude Code