Repository navigation
feat(rest,console): a record open is one request; the page payload carries the record's streams and the related rows' names - #28682
objectstack-fleet[bot] wants to merge 8 commits into
Conversation
… and the related rows' names The page names a related list's expand and asks for the discussion, the activity and the attachments; the route answers each as the read it replaces, behind that object's own exposure gate, refused on its own. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsR2jSSzaZAuzM1JMwdqpC
The record page asks its payload for the discussion and the activity when a block that draws the feed is on screen at open, and for the attachments when the page draws them. Each related list asks the lookups of its rows to resolve, as its own read expands them, and the related list's name fallback no longer resolves the parent record's own id. The mention roster is read on the composer's first focus, or by a reply or edit whose text names someone before then. Stream asks ride the payload the plan asks and never key it, so an assigned page landing after a synthesized one asks nothing again. Regenerated spec artifacts for the record page schema change; two changesets. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsR2jSSzaZAuzM1JMwdqpC
…on map The related list part and the activity part pass `expand` as the relation map the list read's normalizer lowers `$expand=a,b` to, so the rest package type-checks and the rows expand as that read's rows do. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsR2jSSzaZAuzM1JMwdqpC
Generated spec artifacts regenerated from the merged tree. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsR2jSSzaZAuzM1JMwdqpC
📓 Docs Drift CheckThis PR changes 2 package(s): 22 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 171 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 dc5eb91a85dbfd27385f1ccabf748b0f703b3d0e && git checkout dc5eb91a85dbfd27385f1ccabf748b0f703b3d0e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fa494d0538fe69e19be4a21ab13b92e8e6ae8d23 3b6fec5285b7b84331947ae2750103972241ac43 && git checkout -B drift-repro fa494d0538fe69e19be4a21ab13b92e8e6ae8d23 && git merge --no-ff 3b6fec5285b7b84331947ae2750103972241ac43
node scripts/docs-audit/affected-docs.mjs --json fa494d0538fe69e19be4a21ab13b92e8e6ae8d23
|
…-protocol added, spec minor The amended claim reads the 17 new api/RecordPage* authorable keys as a metadata-protocol addition, so @objectstack/spec moves to minor. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsR2jSSzaZAuzM1JMwdqpC
…ted query slot The stream reader's parameter no longer spells `query:`, so the REST query AST census counts one inline-literal slot for it; the record page module's floor moves 1 -> 2 and the registrar sum 5 -> 6. The approval request page's served-page test pins that the timeline asks no label read of its parent `request_id`, which no column draws, and that its actor lookup is still labelled. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsR2jSSzaZAuzM1JMwdqpC
… record page payload
The "Console Integration" section still showed the console reading a
record's comments with `dataSource.find('sys_comment', …)`, with field
names already stale. A record open now asks the discussion and the
activity in its one `POST /data/{object}/{id}/page` request; the section
says so, and when the console still reads `sys_comment` on its own.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jSSzaZAuzM1JMwdqpC
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract-review-claim: Taken under Generated by Claude Code |
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract reviewServed-tier: Inputs read: card #28470 (body and all 7 comments, including the three claim amendments and the os-dev-report), PR #28682 (body, 34-file list, the docs-drift comment, no review comments), the net diff Checks on the head: 69 check-runs, none failed, none in progress. The skipped ones are opt-in or matrix placeholders (Packed-tarball smoke, 18.x upgrade harness, Temporal Conformance, Dogfood Verify CLI, the Dogfood matrix stub, UI Published Artifacts, Schedule gate) plus Auto Label and Check PR Size on a second trigger of the same workflow, which succeeded on the first. PR author is ① Derived judgmentsRequest,
Response,
② Semver level
③ Boundary flagsDev deviations, from the os-dev-report (card comment 6558534355) and the PR body:
Flags the report did not raise:
④ Design judgmentThe hit file is judged against the card's Wanted list, #27627's rule "every part is the read it replaces, as the same caller", and its first consumer, the console's Read authority. The route reads the record first through
The approvals door. #28546 and #28657 were the same class, an approvals row read through a generic data-scoped door. This PR does not touch that line: Per-part refusal. A refused activity read leaves the record, the discussion, the attachments and the related lists answered (route test and console test, Response size. Per open the payload now carries up to 200 activity rows with the actor expanded, up to 100 attachment rows, the whole comment thread and its reactions, plus every related list's rows with their lookups expanded. These are the same bytes the console fetched in separate requests before; the one body is the card's point. The one unbounded term is the thread (flag 6). The neighbour prefetch now asks the same streams for the previous and next records, so a drawer open reads three records' streams rather than one. That is a console cost decision outside the hit file; recorded here for the epic seat's preview check, not a finding. Stale counts against the related-list filter. The attachments Strictness. The body is strict at every level; a window past its cap, an undeclared key in a part, a non-boolean flag and a non-array The first consumer. The console asks No must-fix. Three should-fixes: ② the Implemented-by: VERDICT: PASS Generated by Claude Code |
…ck/rest minor The route gains four accepted body members and three answer members, an additive feature on a published package; the contract review asked the level to read `minor`, as the door's first changeset graded it. Both packages sit in one fixed group, so the released version is unchanged. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsR2jSSzaZAuzM1JMwdqpC
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract-review-claim: The head moved past this seat's own record on this PR, so the delta re-review is this seat's ( Generated by Claude Code |
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract reviewServed-tier: Delta record. It re-reviews the interdiff from Inputs read: the compare The interdiff
Read authority, re-checkedThe delta touches no stream, no related block, no route and no schema. The spec hit file and The old clients' answer. The request and response schemas did not move. A request with none of ① Derived judgmentsUnchanged by the delta. The interdiff adds no member, removes none and changes no description. Every ① judgment of the previous record stands as written: the four request members, the three response members, the strict refusal shape, the exports and the generated surface artifacts. ② Semver level
③ Boundary flagsThe three should-fixes of the previous record:
No new dev flags. The os-dev-report was not re-issued; the PR body's own Verification lists the three fixes. The card #28470 has no comment since the previous record. PR author is ④ Design judgmentThe delta carries no design change. The hit file Checks on the head69 check-runs on Which judgments stand① stands whole. ② is re-rendered here: the one changed line is the grade, now right. ③ is re-rendered for the three should-fixes, all addressed; the previous record's ③ items 1, 3, 4, 5, 7 and 8 stand. ④ stands whole; the delta touches none of the code it judged. No must-fix. No should-fix open. Implemented-by: VERDICT: PASS Generated by Claude Code |
domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y
Requested by Steedos · project thread
Fixes #28470
维护者速读
POST /data/:object/:id/page一起返回;@ 提及候选人(sys_user?top=50)改为评论框第一次获得焦点时才读。打开记录不再单独发这些请求,也没有回退读取。/page之外还会发sys_comment、sys_activity、sys_user?top=50,以及每个被引用对象一次?top=1名称查询,共 8 个多余请求。@objectstack/specminor),新字段全部可选,旧客户端不受影响;每个新部件单独拒绝,不会连累整页。回滚就是还原本 PR。动态部件一次取 200 条,这是已命名的偏差,见下文。packages/spec/src/api/record-page.zod.ts(新成员和流表说明)、packages/rest/src/record-page-routes.ts(每个部件就是它替代的那次读)、ui/packages/app-shell/src/views/RecordDetailView.tsx(何时请求各部件);文档content/docs/frontend/plugins/plugin-detail.mdx的 Console Integration 一节改为讲记录页请求。cd ui && pnpm exec vitest run packages/app-shell/src/views/RecordDetailView.recordOpenOneRequest-28470.test.tsx,五条通过:一次/page,零次评论、动态、附件、提及候选和?top=1读取,含两条消融。Metadata-protocol: added,@objectstack/spec定为 minor。PR 保持草稿。What changes
One record open is one request. The parts a record page still read on its own after the page payload answered now ride in
POST /api/v1/data/{object}/{id}/page, and the console takes them from there with no fallback read.Contract members added (one complete list for the contract review)
@objectstack/spec(packages/spec/src/api/record-page.zod.ts, exported from@objectstack/spec/api):Request (
RecordPageRequestSchema, still.strict()):related[].expand?: string[]: the lookups of a related list's rows to resolve, as that list read's own$expand.discussion?: boolean: ask for the record's comment thread and its reactions.activity?: { top: int 1..200 }(RecordPageActivityRequestSchema, strict): ask for the newest activity rows and their count.attachments?: { top: int 0..100 }(RecordPageAttachmentsRequestSchema, strict): ask for the newest attachments and their count.Response (
RecordPageResponseSchema):discussion?: { comments, reactions } | { error }(RecordPageDiscussionSchema|RecordPageStreamRefusalSchema).reactionsis{ records } | { error }(RecordPageDiscussionReactionsSchema), so a refused reaction read leaves the comments answered.activity?: { total, records } | { error }(RecordPageStreamRowsSchema|RecordPageStreamRefusalSchema).attachments?: { total, records } | { error }(same pair).New exports:
RecordPageActivityRequestSchema,RecordPageAttachmentsRequestSchema,RecordPageStreamRefusalSchema,RecordPageStreamRowsSchema,RecordPageDiscussionReactionsSchema,RecordPageDiscussionSchema,RECORD_PAGE_MAX_ACTIVITY_ROWS(200),RECORD_PAGE_MAX_ATTACHMENT_ROWS(100), and for each schema itsX(input) andXParsed(output) type.@object-ui/types(ui/packages/types/src/data.ts, published):RecordPagePayload.discussion?: { comments: Record<string, any>[]; reactions: { records: Record<string, any>[] } | { error: RecordPagePartRefusal } } | { error: RecordPagePartRefusal }RecordPagePayload.activity?: { total: number; records: Record<string, any>[] } | { error: RecordPagePartRefusal }RecordPagePayload.attachments?: { total: number; records: Record<string, any>[] } | { error: RecordPagePartRefusal }RecordPageRequest, re-exported, so it gains the request members above with no separate declaration.@object-ui/react:DiscussionContextValue.onMentionSuggestionsWanted?: () => void.@object-ui/plugin-detail:RichTextCommentInput,RecordActivityTimelineandRecordChatterPaneltakeonMentionSuggestionsWanted?: () => void, called on the editor's focus.record:activityandrecord:chatterpass the context's hook.What each new part answers
Each part is the read it replaces, asked as the same caller, behind the stream object's own API exposure gate, and refused on its own:
related[].expand$expand(GET /data/{child}?$expand=…), spelled as the QueryAST relation map that read's normalizer lowers it todiscussion.commentsGET /data/sys_commentwherethread_id={object}:{id}, oldest first, no page sizediscussion.reactionsGET /data/sys_comment_reactionwherecomment_idin those comments, oldest first; not asked when the thread is emptyactivityGET /data/sys_activityfor the record, newest first,$top,$count,$expand=actor_idattachmentsGET /data/sys_attachmentfor the record, newest first,$top,$countA record the caller cannot read still refuses the whole request, its thread with it. The body stays strict and the query-parameter set stays empty.
Console
discussionandactivitywhen a block that draws the feed is on screen at open (the 动态 panel of a laid-out page, or a feed block on the tab the page opens on), andattachmentswhen the page draws them. The feed, the Attachments panel and the Attachments tab badge draw those parts. A refused part draws the refusal the panel draws today.RelatedList's name fallback no longer resolves the parent record's own id, which no column draws.sys_user, 50 rows) is read on the composer's first focus. A reply or an edit whose text holds an@before then reads it when written, so its mentions still resolve.Docs
content/docs/frontend/plugins/plugin-detail.mdx, the "Console Integration" section only (added to the file surface by claim amendment). It showed the console reading a record's comments withdataSource.find('sys_comment', …), with field names already stale onmain. It now says a record open takes the discussion and the activity from the/pagepayload, and names when the console still readssys_commentitself (a feed the open did not ask for, or a data source withoutgetRecordPage) and when it reads the mention list. Its snippet callsgetRecordPagewith the real types. The rest of the page is unchanged.The
?top=1probe callerRelatedList's reference-name fallback (ui/packages/plugin-detail/src/RelatedList.tsx) resolving the payload's bare ids in related rows, oneGET /data/{object}?top=1&filter=["id","in",[…]]per referenced object, the parent FK itself among them. The rows now arrive expanded, and the parent FK is skipped.Named deviation: the activity part asks 200 rows
The 动态 panel shows 3 or 5 rows, but its count chip and the 动态 tab count the whole merged stream (comments + activity + field changes, minus dropped types). A 5-row window would make that count wrong, so the part asks the same 200-row window the console's own read asked before. Shrinking it needs a server-side count of the merged stream: see
out_of_scope_findings.Follow-up: #28737 serves the merged 动态 count with the page payload, so the activity part can ask the panel's few rows. It owns
RECORD_PAGE_MAX_ACTIVITY_ROWS./pageonce per open (card item 4)Measured on a production build before this change:
/pagegoes out once per record. The two other/pageposts in a drawer open are the previous/next prefetch (#27056, #27545), different ids, by design. So item 4 changes nothing, and the seat recorded it out of scope.App-level reads a record open causes (out of scope, left to #28085 / #27626)
GET /api/v1/meta/object(the object list read again on open) andPATCH /api/v1/data/sys_user_preference/{id}(the recent-items write). Neither is the record's, and neither is changed here.meta/app,meta/view,meta/page,meta/object,meta/dashboard, the app bundle, packages, permissions, localization, settings, AI agents, the approvals inbox,sys_inbox_message,sys_user/{me}andmeta/object/sys_user) plus the same preference write. A record open does not cause them.Before / after
Measured on
examples/app-crm, production-built Console (pnpm build && pnpm console:build,pnpm dev:crm -- --fresh --ui --no-watch), Playwright, signed in as admin, every/api/request from the click (drawer) or navigation (record route) until network idle plus 3 s. HotCRM was not available, so app-crm stands in.Contact drawer (open "Ada Lovelace" from the Contacts list): 13 requests before, 4 after.
The after run has one neighbour prefetch instead of two only because the fresh seed listed Ada third of three (no next row); the prefetch is unchanged.
Account drawer (open "Acme Corp"): 13 requests before, 5 after:
GET /meta/object,POST /data/crm_account/{acme}/page, the two neighbour prefetches,PATCH /data/sys_user_preference/{id}. Gone:sys_user?top=50,sys_comment,sys_activity, and the five?top=1probes (sys_organization,sys_user,crm_contact,crm_account,crm_opportunity). The page request body:{"expand":["organization_id","created_by","updated_by","owner_id","owning_business_unit_id"],"related":[{"object":"crm_activity","field":"account","top":3,"expand":["organization_id","created_by","updated_by","owner_id","owning_business_unit_id","contact","opportunity"]},{"object":"crm_contact","field":"account","top":3,"expand":["organization_id","created_by","updated_by","owner_id","owning_business_unit_id"]},{"object":"crm_lead","field":"account","top":3,"expand":["organization_id","created_by","updated_by","owner_id","owning_business_unit_id","converted_opportunity"]},{"object":"crm_opportunity","field":"account","top":3,"expand":["organization_id","created_by","updated_by","owner_id","owning_business_unit_id","renewal_of"]}],"explain":["update","delete","transfer"],"fieldAccess":true,"approvals":true,"discussion":true,"activity":{"top":200}}Contact record route (navigate to
/apps/crm_app/crm_contact/record/{ada}, shell boot included): 37 requests before, 28 after. The nine gone:sys_user?top=50,sys_comment,sys_activity, the five?top=1probes, andGET /meta/object/sys_organization(the schema read the name fallback made for thesys_organizationprobe). Nothing was added.Composer focus (account drawer, then a click into "Write a note… @ to mention"): one
GET /data/sys_user?top=50&select=id,name,email,image, at the focus, and none before it.App-crm does not enable files on these objects, so
attachmentsis not asked here; the console test and the dogfood suite cover that part.Tests
packages/spec/src/api/record-page.test.ts: request strictness for the new members (unknown key inactivity/attachmentsrefused, bounds), and each stream part is its rows or its refusal, never both and never neither.packages/rest/src/record-page-routes.test.ts(7 new, 20 total): each part's read as replaced, for the same caller; an empty thread asks no reaction read; per-part refusal (a refused activity read leaves the others answered); a refused comment read; a refused reaction read keeps the comments; the stream object's exposure gate; strict body.packages/qa/dogfood/test/record-page-streams.dogfood.test.ts(new, real HTTP + security stack, non-admin member): discussion, reactions and activity equal the member's ownGETreads byte for byte, including atop: 1window with the whole count; a private record refuses the whole request asGETdoes; attachments count and list only the record's own (3), never another record's (2 secret).packages/qa/dogfood/test/record-page.dogfood.test.ts: a related list'sexpandis its read's own$expand.ui/packages/app-shell/src/views/RecordDetailView.recordOpenOneRequest-28470.test.tsx(new, realObjectStackAdapter, StrictMode): one/page; zerosys_comment,sys_comment_reaction,sys_activity,sys_attachment,sys_user?top=50and?top=1reads; the body asksdiscussion,activity200,attachments100 and the relatedexpand; the feed, the attachment and the badge draw the payload; the mention read goes out once, on the composer's focus; a refused activity drawsdata-denied-source="activity"and is not read again. Ablations: with nogetRecordPagethe detectors see thesys_activity,sys_attachmentandsys_commentreads; with the relatedexpandignored they see the?top=1name probe. Removing the parent-FK skip was checked to go red, then restored.RecordDetailView.recordPagePayload-27627,RecordDetailView.recordOpenWaves-13010,RecordDetailView.withheldMentions-28218(the composer reads the roster on focus; a reply reads it at the write).ui/apps/console/src/services/approvalRequestsDataSource.servedPage.test.tsx: it pinned the related timeline's label read of its parentrequest_id, which the relatedexpandretires. It now asserts no read of the parent request by id, and still that no read the page makes is refused.Acceptance notes
/page, zerosys_comment,sys_activity,sys_attachment,sys_user?top=50and?top=1reads, with ablations.Deviations
mainthe console read the whole thread (sys_commentwherethread_idis{object}:{id}, oldest first, no$top) and held all of it, and the comment thread has no paging door today. A page size needs that door first, which is outside this card.out_of_scope_findings
Verification
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 108 commands, all exit 0 without a PR body (check-plugin-teardown-shape --self-testafter fetching its pinned fixture commit on this shallow clone;pnpm --dir ui exec vitest run scripts/rerun alone after two 15 s timeouts under a concurrent spec suite: 169 files passed).@objectstack/spec:test782 files / 23126 tests passed;test:repo87 files / 1448 passed;check:generatedcurrent after the merge of main.@objectstack/rest:typecheck(incl. test typecheck) 0;record-page-routes.test.ts20 passed.packages/qa/dogfood:typecheck0; the two record page suites 8 passed against the rebuilt rest dist.--maxWorkers=2under the verify lock, the record-open files rerun after their updates);type-check0 for@object-ui/types,@object-ui/react,@object-ui/plugin-detail,@object-ui/app-shell; eslint 0 errors on every touched file (root config for the platform files);node scripts/check-sdui-manifest.mjs0.origin/main(no text conflicts; generated spec artifacts regenerated,check-regen-pendingclear). At the merged head: the five record-open ui files (42), the route suite (20) andrecord-page.test.ts(7) rerun green.Metadata-protocol: added,@objectstack/specatminorin.changeset/28470-record-page-streams.md, head579bcda1). PR-body gates with this body asGITHUB_EVENT_PATH:check-changeset-no-major --base origin/main0 (level axis: widening accounted for atminor),check-adr-0087-registration0,check-closing-keyword-parity0,check-empty-changeset0,check-partof-closing-keyword0;pnpm check:changeset-fixed0.579bcda1, fixed at2369096b. CI's Test Core 8/8 runs rest'stest:repo, whose query-slot census (rest-server-canonical-query-ast.test.ts) counts everyquery:token in a REST registrar: the stream read's parameter was spelledquery, so it is renamed andrecord-page-routes.tsis pinned at 2 slots (total 5 → 6). CI's Test UI 9/16 ran the console served-page test above. At2369096b: resttestandtest:repogreen, the 26 console test files that touch the record page green.2369096b: 65 check runs, 58 success, 7 skipped, none failed; Check Changeset green.f2e04ba8(no must-fix). Its should-fixes:@objectstack/restis gradedminorin.changeset/28470-record-page-streams.mdat3b6fec52, as AGENTS.md's Post-Task Checklist step 3 reads ("a kind takes at leastminoron a package the diff moves") and as the door's first changeset graded it (spec and rest share one fixed group, so the released version is unchanged;check-changeset-no-major --base origin/main,check:changeset-fixed,check-empty-changeset0); the whole-thread discussion is named under Deviations; the 200-row cap's follow-up card is spec+runtime+ui: the record page payload serves the merged 动态 count, so the activity part asks the panel's few rows instead of 200 (RECORD_PAGE_MAX_ACTIVITY_ROWS owner) #28737.f2e04ba8: the section'stsxsnippet type-checks against@object-ui/types(tscon the extracted fence, 0; a wrongactivity.toptype in it goes red, TS2322). The docs gates the derived list names, all exit 0:check-docs-section-name(and--self-test),check-affected-docs,check-drift-comment,check-doc-route-spelling --advisory,check-doc-authoring,check:docs-audit-scope,check:docs-redirects,check:docs-single-h1,check:docs-spec-enumerations,check:docs-transcript-drift,check:issue-citations,check:role-word,check:nul-bytes,check:org-identifier.Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: none — no file under
skills/**teaches the record page payload (POST /data/{object}/{id}/page), its request or response members, or when the console reads the mention roster (agit grepofskills/forRecordPage,getRecordPage,sys_comment,sys_attachmentandmentionSuggestionsnames nothing).skills/objectstack-datateachesenable.activities: falsehiding the timeline and the detail-page related lists by relationship flags; both stay true.Human Minimum
packages/specholds no runtime logic.pnpm lintpasses.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01QsR2jSSzaZAuzM1JMwdqpC