Repository navigation
fix(rest): the one-request record page accepts a related list keyed by a user field - #29965
Conversation
…y a user field The related-list check in record-page-routes.ts kept a private lookup/master_detail list and read `reference` raw, so a related list on sys_user keyed by a `user` field answered 400 invalid_relationship though its row query follows that field (18.3.0 regression, duly#185). It now asks the spec's one arbiter, referenceTargetOf, the judgement the engine's $expand and delete walk read. A field that does not reference the page's object is still a located 400. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzijwvEjcKQArbvuX9p
|
domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5 Contract reviewServed-tier: Face: declaration ( ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
|
domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5 席位核对Verdict: ACCEPT at head
|
📓 Docs Drift CheckThis PR changes 1 package(s): 10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 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 de9d7c528bd48e3cec59b118c598c4e9b9ade67b && git checkout de9d7c528bd48e3cec59b118c598c4e9b9ade67b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3b8c724cf21fe2b39b2c635c6ef2a2448fa952fb 3ca0ddde4314f0bbda3df23c6d92f14b70cb7891 && git checkout -B drift-repro 3b8c724cf21fe2b39b2c635c6ef2a2448fa952fb && git merge --no-ff 3ca0ddde4314f0bbda3df23c6d92f14b70cb7891
node scripts/docs-audit/affected-docs.mjs --json 3b8c724cf21fe2b39b2c635c6ef2a2448fa952fb
|
…nd-object read census record-page-routes.ts now calls referenceTargetOf, so the second-object read-exposure pin needs it classified. It reads no record of the target: the target is the addressed object, read behind the route's own get exposure gate. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzijwvEjcKQArbvuX9p
|
domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5 Contract reviewServed-tier: Face: declaration ( ① Derived judgments
② Semver levelUnchanged: ③ Boundary flags
Implemented-by: VERDICT: PASS |
|
domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5 席位核对Verdict: ACCEPT at head
|
Requested by Yibo Wang · project thread
Fixes #29946
维护者速读
POST /data/:object/:id/page的关联列表检查,不再用路由里私有的lookup/master_detail名单加裸读reference,改问 spec 的唯一目标裁判referenceTargetOf(引擎的$expand和删除级联读的就是它)。user字段按类型指向sys_user,所以sys_user页上按 owner/assignee 的关联列表重新返回 200 和行。invalid_relationship,而该列表自己的行查询{ [field]: id }本可以照常回答。user、指向别处的lookup、别的对象上的tree、读不了的reference);错误文案改为列出四种类型。@objectstack/restpatch。回滚 = revert 本 PR。packages/rest/src/record-page-routes.ts的referenceOf与invalid_relationship一处;测试record-page-routes.test.ts的[#29946]describe。pnpm --filter @objectstack/rest exec vitest run src/record-page-routes.test.ts,预期32 passed。What changes
@objectstack/rest,record-page-routes.ts. The related-list check drops the route's privateRELATIONSHIP_TYPESset (lookup,master_detail) and its rawreferenceread.referenceOfnow asks the spec's one arbiter,referenceTargetOffrom@objectstack/spec/data(the reference value typeslookup,master_detail,user,tree; auserfield's target is fixed tosys_userby its type, whether or not its carrier spells it). That is the same judgement the engine's$expandgate and referential delete walk read (engine.tsdeleteWalkTargetOfanswersreferenceTargetOffor auserfield), so the route and the related list's row query ({ [field]: id }, or$containson a multi-valued field) cannot disagree about which fields relate a child to its parent.referencecarrier (object- or array-valued, never parsed) throws inside the arbiter. The route catches that and keeps the located400 invalid_relationshipit answered before, not a thrown 500.'<field>' on '<child>' is not a lookup, master-detail, user or tree field that references '<object>'.Code, status and location are unchanged.tree: it comes with the arbiter. Atreefield relates a child to its own object only (its reference is self-referential by contract), and the row query follows it the same way, so a related list of a record's children on its own page is now accepted; atreeon another object's page is still refused. There is no separateownerfield type inFieldType(measuredfield.zod.ts), so nothing else changes.urlaction atrecord_headerfrom the card's second paragraph is a separatedomain:uicard and is not touched.Why
Card #29946, from duly#185: a record page on
sys_userwith a related list ofduly_taskkeyed byowner(aField.user) answered400 VALIDATION_FAILED: 'owner' on 'duly_task' is not a lookup or master-detail field that references 'sys_user'.on 18.3.0, so the console's full-page record view showed "could not load this record".Where it came from:
record-page-routes.tsdid not exist at@objectstack/rest@18.2.0(the contents API answers 404 at that tag). #27713 (0027c4a2b8, 2026-10-30) created the route withRELATIONSHIP_TYPES = new Set(['lookup', 'master_detail'])and the rawreferenceread; #28682 and #28991 kept the check as it was. On 18.2.0 the console read each related list through the ordinary list read of the child filtered by the field, which applies no relationship-type check, so the page rendered.Clause-②: no
Metadata-protocol: none
Verification
record-page-routes.test.ts, new[#29946]describe through the real route handler:sys_userkeyed by a bare{ type: 'user' }field, auserfield withreference: 'sys_user', and a multi-valueduserfield answers 200 with each list's rows; the reads are{ owner: 'u9' },{ assignee: 'u9' },{ reviewers: { $contains: 'u9' } };userfield on theaccountpage is still400invalid_relationshipatrelated.0.field, with the exact new message, and nothing is read;lookuprelates only to its own reference, and atreeonly to its own object;referencecarrier is the located 400, not a 500.title,owneras a lookup to another object,nopeare all 400) is unchanged and green.!new Set(['lookup','master_detail']).has(def.type) || …): 2 failed,a related list on sys_user keyed by a user field answers 200 …anda lookup keeps relating only to its own reference, and a tree to its own object, bothAssertionError: expected 400 to be 200.userfield regardless of target (… && def.type !== 'user'): 1 failed,a user field on a page that is not sys_user is still a located 400 …,AssertionError: expected 200 to be 400.@objectstack/rest^...closure build (26 tasks): exit 0.@objectstack/restpnpm test: 383 files, 7159 passed / 332 skipped, exit 0.tsc --noEmit: exit 0.check:test-typecheck: exit 0.pnpm lint: exit 0.node scripts/pm/dispatch-gates.mjs --commandsat the head rebased onorigin/main40fa4335: 59 derived commands, 58 exit 0, 1 exit 3 (prerequisite, nothing measured):pnpm check:dual-build-cjs-loadsreads a whole-repo build and only the rest closure was built; it reads no path this diff touches.check-plugin-teardown-shape.mjs --self-testfirst exited 3 (its fixture commit621a4876was not in the shallow clone) and is exit 0 (48 cases) after fetching that commit.dispatch-gates --ran:✓ 59 derived famil(ies) accounted for — 58 run, 1 NOT-MEASURED. CI's Build Core and type-check lanes are the jobs the tool lists as not measurable locally.packages/rest/src/record-page-routes.ts(scanned every open PR's file list before opening).record-page.dogfood.test.tssuite (the dogfood regression gate runs onmain's push battery).Human Minimum
packages/specholds no runtime logic. (spec untouched)pnpm lintpasses.main, one pull request.packages/spectouched: n/a.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.维护者速读section is the seat's Chinese summary for the maintainer).🤖 Generated with Claude Code
https://claude.ai/code/session_01WGufzijwvEjcKQArbvuX9p