You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit c708de6
Browse filesBrowse the repository at this point in the historyBrowse files
feat(rest,console): the record page payload serves the 动态 count, so the activity part asks only the panel's rows (#28991)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} -->
_Requested by **Steedos** · [project
thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbNLXLNMXwrkkwwQqRkBQHM5)_Fixes#28737
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: none — no file under `skills/**` teaches the record page
payload's parts; `skills/objectstack-ui/rules/record-page.md` teaches
the page's layout, which this does not change. The hand-written
`content/docs/frontend/plugins/plugin-detail.mdx` Console Integration
section is updated.
## 维护者速读
- **改了什么**:记录页请求 `POST /data/:object/:id/page` 新增可选部件
`timeline`,由服务端统计这条记录完整 动态 流的条数(评论 + 动态 + 字段变更,去掉面板不画的类型,有字段变更时
`updated` 动态让位)。控制台用它显示 动态 计数徽标、「查看全部 N 条」和 动态 标签页计数,动态 部件只请求面板要画的 5
行,不再一次取 200 行。
- **为什么改**:#28682 里动态部件为了让计数正确只能取 200 行,这是它的合约审查 should-fix ③-2
记下的偏差。现在计数由服务端给出,行数可以按面板来取。
- **风险与回滚**:协议只增不减(Clause-② 加宽,Metadata-protocol:
added,`@objectstack/spec` 与 `@objectstack/rest` 定为
minor),新成员全部可选,旧客户端不受影响。`RECORD_PAGE_MAX_ACTIVITY_ROWS`(200)和
`RecordPageActivityRequestSchema` 的上下限都没动。评论或动态读取被拒时整个 `timeline`
部件被拒,字段变更读取被拒时记 0 条。回滚就是还原本 PR。
- **先看**:`packages/spec/src/api/record-page.zod.ts`(`timeline`
的来源表和共享的类型排除常量),`packages/rest/src/record-page-routes.ts` 的
`answerTimeline`,`ui/packages/app-shell/src/views/RecordDetailView.tsx`(什么时候只请求
5 行、什么时候读完整窗口)。
- **两分钟验证**:`cd ui && pnpm exec vitest run
packages/app-shell/src/views/RecordDetailView.timelineCount-28737.test.tsx`,四条通过:超过
200 条的记录显示服务端给的 341,请求只要 5 行;没有 动态 的记录显示 0,也没有回退读取;直接打开 动态 标签页时请求 200
行;去掉服务端计数的消融测试会退回本地计数 6。
- **需要你做的**:总监做合约审查(CONTRACT_REVIEW_TIER),由席位转交。PR 保持草稿。
## What changes
The record page payload counts the record's 动态 stream on the server, so
the console no longer reads 200 activity rows to name a count. The 动态
panel draws its newest 3 or 5 entries, and the page now asks only those
rows.
### 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()`):
- `timeline?: { discussion: boolean; activity: boolean; fieldChanges:
boolean }` (`RecordPageTimelineRequestSchema`, strict, every source
required): ask for the count of the 动态 stream drawn from the sources
named.
Response (`RecordPageResponseSchema`):
- `timeline?: { total: int ≥ 0 } | { error }`
(`RecordPageTimelineCountSchema` | `RecordPageStreamRefusalSchema`).
Constants:
- `RECORD_TIMELINE_UNDRAWN_ACTIVITY_TYPES = ['commented', 'mentioned',
'login', 'logout']`: the `sys_activity` types the stream draws no entry
for (`satisfies readonly SysActivityBuiltinType[]`).
- `RECORD_TIMELINE_UPDATE_ACTIVITY_TYPE = 'updated'`: the activity type
that steps aside once field changes are in the stream.
New exports: those two schemas and two constants, and for each schema
its `X` (input) and `XParsed` (output) type.
`RECORD_PAGE_MAX_ACTIVITY_ROWS` stays 200 and
`RecordPageActivityRequestSchema` keeps its bounds (ruling: not narrowed
here).
`@object-ui/types` (`ui/packages/types/src/data.ts`):
`RecordPagePayload.timeline?: { total: number } | { error:
RecordPagePartRefusal }`. The request type is the spec's
`RecordPageRequest`, so it gains `timeline` with no separate
declaration.
`@object-ui/plugin-detail`: exports `RecordTimelineTotalContext` (the
host's count of the whole stream, `number | null`),
`RECORD_TIMELINE_PANEL_MAX_ENTRIES` (5, the wide panel's rows) and
`RECORD_PAGE_TIMELINE_PANEL`.
### What the count is
| Source | Counted as the caller's read | When named |
| --- | --- | --- |
| `discussion` | `sys_comment` where `thread_id` = `{object}:{id}`
(`$count`, `$top=0`) | the panel draws the discussion |
| `activity` | `sys_activity` for the record, `type` not in
`RECORD_TIMELINE_UNDRAWN_ACTIVITY_TYPES` | the panel draws activity |
| `fieldChanges` | `sys_audit_log` for the record, `action` in `create`,
`update`, `delete` | field history is on for the object |
The total is their sum, less the activity `updated` rows when field
changes are counted and there is at least one (the stream's own
`mergeTimelineStream` rule). Each read goes through `findData` as the
caller, behind the stream object's API exposure gate, like the parts
#28682 added. A refused comment or activity read refuses the part. A
refused `sys_audit_log` read counts 0, because the console draws no
field change it cannot read. A source the boot does not register
(`OBJECT_NOT_FOUND`) holds no entry.
### One definition for the dropped types (ruling: name which)
**One shared definition.** `RECORD_TIMELINE_UNDRAWN_ACTIVITY_TYPES`
lives in the spec. The server's count excludes it (`$nin`), and the
console's activity reading
(`ui/packages/plugin-detail/src/renderers/recordActivityFeed.ts`) builds
its "no entry" rows of `ACTIVITY_TYPE_TO_FEED_TYPE` from the same
constant. `mergeTimelineStream` uses
`RECORD_TIMELINE_UPDATE_ACTIVITY_TYPE`. A test pins it as well: the
undrawn keys of `ACTIVITY_TYPE_TO_FEED_TYPE` equal the constant exactly
(`RecordTimelineTotal-28737.test.tsx`), and the spec test pins the
constants as builtin activity types.
### Console
- The page asks `timeline` whenever it draws the 动态 count, which is the
synthesized columns page, with the sources the panel draws. `activity`
asks `RECORD_TIMELINE_PANEL_MAX_ENTRIES` (5) rows, or the 200-row window
when the page opens on its 动态 tab (`?related=timeline`).
- The count chip, 「查看全部 N 条」, the 动态 tab's count and the tab row's probe
name the served total (`RecordTimelineTotalContext`). They still name
nothing while the feed loads or when a read is refused. Without a served
total (an authored page, a refused part, a data source without
`getRecordPage`) each counts the rows it holds, as before.
- Comments written on the page after it opened stay counted: the total
swaps the payload's comment count for the comments the page holds now.
- Opening the 动态 tab later reads the 200-row activity window once
(`sys_activity`, `$top=200`, `$expand=actor_id`), and only when the
payload asked the panel's rows and the record holds more activity rows
than that. A record with none makes no extra read.
### Docs
`content/docs/frontend/plugins/plugin-detail.mdx`, Console Integration:
the request snippet now asks `activity: { top: 5 }` and `timeline`, and
the prose says when the tab reads the window. The reference page
`content/docs/references/api/record-page.mdx` and the other spec
artifacts are regenerated.
## Tests
- `packages/spec/src/api/record-page.test.ts` `[#28737]`: the request
member is strict (every source required, booleans only, no extra key);
the response is a count or a refusal; the constants are builtin activity
types and leave `updated` out.
- `packages/rest/src/record-page-routes.test.ts` `[#28737]` (8): the sum
and the exact reads asked; only the named sources are read; zero; a
refused comment or activity read refuses the part; a refused ledger read
counts 0; not asked means not served; the exposure gate refuses without
reading; the body stays strict.
- `packages/qa/dogfood/test/record-page-timeline.dogfood.test.ts` (3,
real HTTP + security stack, plugin-audit's real writers, org-bound
boot): the served total equals the sum of the separate `$count` list
reads asked as the same member, with the updates stepping aside; without
field changes the updates stay; only the named sources are counted.
Armed controls: the stores hold the stream (3 comments, ≥ 3 ledger rows,
2 updates) and the member reads each source.
-
`ui/packages/plugin-detail/src/__tests__/RecordTimelineTotal-28737.test.tsx`:
the chip and 「查看全部」 show the served 341 while 5 rows are held; a served
0 shows 0; no count while loading or refused; control without a served
count shows the held 6; the full stream and the probe report the served
count; the shared-definition pin; the update steps aside.
-
`ui/packages/app-shell/src/views/RecordDetailView.timelineCount-28737.test.tsx`
(the card's two tests and more): a record with more than 200 merged
entries shows the served 341, the page request asks `activity: { top: 5
}` and `timeline`, and opening the 动态 tab reads the window once; a
record with none shows 0 and after opening the tab only the page request
went out; opening on `?related=timeline` asks 200 rows and makes no
activity read; ablation without the served count shows the held 6.
- Updated expectations, same behaviour otherwise:
`RecordDetailView.recordOpenOneRequest-28470`, `recordOpenWaves-13010`
and `recordPagePayload-27627` now expect `activity: { top: 5 }` plus
`timeline` on the synthesized page.
## Acceptance notes
- **Ruled sources only.** The count is comments + drawn activity +
ledger writes, less the updates. Known places where the drawn stream can
still differ from the count, all toward the count being the larger: the
console draws a ledger row only when it has a change it can display
(`collectAuditChanges`), and it reads the newest 50 ledger rows. The
count names every ledger write the caller can read.
- **The tab pages through what it holds.** The 动态 tab can page through
the 200 activity rows and 50 ledger rows it holds while the count names
the whole stream, as it did before this change for a record with more
than 200 rows.
- **A refused ledger read counts 0** by design: the console draws no
field change it cannot read. Pinned in the rest unit test only, because
on the dogfood boot the platform's default sets let a member read the
ledger rows of records they read.
- **Serial constraint.** Seat 1's #28483 (RecordDetailView guard states)
is already on `main`, and this branch has merged it (clean merge,
re-verified at the new head), so this PR lands second.
- **Size.** 1366 changed lines (1281 + / 85 −), under 3000, so not Tier
H.
## Four axes
- **实际业务需求**:动态 面板只画 3 或 5 条,但每次打开记录都要取 200 行动态来算计数,这是 #28682
合约审查记下的实测偏差(should-fix ③-2)。打开任何有 动态 的记录都会走这条路径。
- **项目长远合理性**:计数是服务端对同一个流的读,契约先在 spec
里声明(来源表、拒绝规则、共享常量),实现兑现声明。没有在消费端截断或猜计数,也没有收窄已发布的
`RecordPageActivityRequestSchema`。
- **防 AI 写错**:被丢弃的类型只有一个定义,服务端和控制台都从 spec
常量取,另有测试钉住两边完全一致,以后加一个不画的类型不会只改一边。请求体保持 strict,三个来源都必须明确给出布尔值,拼错或漏写会得到
400。
-
**创业阶段不扩散**:只加一个可选的计数部件,消费者就是记录页本身。没有新门禁,没有分页或按类型细分的计数,`RECORD_PAGE_MAX_ACTIVITY_ROWS`
不变。
## Verification
At the head after merging `main`:
- `@objectstack/spec` `record-page.test.ts` 10/10; `@objectstack/rest`
`record-page-routes.test.ts` 28/28; rest `typecheck` (incl.
`check:test-typecheck`) green; spec `check:test-typecheck` green.
- dogfood `record-page-timeline.dogfood.test.ts` 3/3.
- `ui` plugin-detail, whole package: 361 files passed, 1 skipped; 3197
tests passed.
- `ui` app-shell `RecordDetailView*` + the #28483 header test + the new
plugin-detail test after the merge: 98 files, 610 tests passed. Before
the merge, the 151 other ui test files that import `RecordDetailView`,
`recordPagePayload` or `getRecordPage` passed (1397 tests).
- `turbo run type-check` for `@object-ui/types`,
`@object-ui/plugin-detail`, `@object-ui/app-shell`: 67/67.
- `pnpm --filter @objectstack/spec check:generated`: all 14 artifacts
current, against a dist this run built.
- `node scripts/check-sdui-manifest.mjs`: intact.
- `node scripts/pm/dispatch-gates.mjs --commands` sweep: see the
os-dev-report on the card.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01QsR2jSrQVdYaC3GSLbQCFd
_Generated by [Claude
Code](https://claude.ai/code/session_01QsR2jSrQVdYaC3GSLbQCFd)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
feat(rest): the record page payload serves the record's 动态 count, so the activity part asks only the panel's rows (#28737)
7
+
8
+
Clause-②: yes (widening)
9
+
Metadata-protocol: added
10
+
11
+
`POST /api/v1/data/{object}/{id}/page` counts the record's 动态 stream on the server, so a record page no longer reads 200 activity rows to name a count.
12
+
13
+
-**`timeline: { discussion, activity, fieldChanges }`** (request) → **`timeline`** (response): `{ total }` — the number of entries the record's 动态 stream holds for the caller, from the sources named: the comment thread (`sys_comment`), the activity rows the stream draws (`sys_activity` without the types in `RECORD_TIMELINE_UNDRAWN_ACTIVITY_TYPES`) and the ledger's field changes (`sys_audit_log` writes). With field changes counted, the activity's `updated` rows step aside for them, as the stream does. Each source is counted as the caller's own read, behind its object's exposure gate; a refused comment or activity read refuses the part (`RecordPageStreamRefusalSchema`), a refused ledger read counts no field change, and a source the boot does not register holds no entry.
14
+
15
+
`RECORD_PAGE_MAX_ACTIVITY_ROWS` stays 200 and `RecordPageActivityRequestSchema` keeps its bounds: the 动态 tab still asks the window it draws. New exports in `@objectstack/spec/api`: `RecordPageTimelineRequestSchema`, `RecordPageTimelineCountSchema`, `RECORD_TIMELINE_UNDRAWN_ACTIVITY_TYPES`, `RECORD_TIMELINE_UPDATE_ACTIVITY_TYPE`, and their types. The console's activity reading maps exactly those undrawn types to no entry, so the server's count and the drawn stream share one definition.
Copy file name to clipboardExpand all lines: content/docs/references/api/record-page.mdx
+53-4Lines changed: 53 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,7 +1,7 @@
1
1
---
2
2
title: Record Page schema — API Protocol reference
3
3
navTitle: Record Page
4
-
description: "Record page payload — POST /api/v1/data/{object}/{id}/page. Reference for RecordPageActivityRequest and 16 more: every property with its type and default."
4
+
description: "Record page payload — POST /api/v1/data/{object}/{id}/page. Reference for RecordPageActivityRequest and 18 more: every property with its type and default."
5
5
---
6
6
7
7
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
@@ -39,7 +39,10 @@ The three stream parts are each that read exactly as the table on
39
39
`RecordPageActivityRequestSchema` spells it: the route composes the record's
40
40
scope and the order, and the request names only whether a stream is wanted
41
41
and how many rows. Paging further, posting, reacting and uploading keep
42
-
their own routes. Object permissions, row-level security, sharing and
42
+
their own routes. [#28737] The `timeline` part is the size of the record's
43
+
动态 stream — the comments, the activity and the field changes it merges —
44
+
counted by those reads as the caller (`RecordPageTimelineRequestSchema`), so
45
+
a page draws the stream's count without holding every row of it. Object permissions, row-level security, sharing and
43
46
field-level security therefore apply exactly as they apply to those reads.
44
47
A part whose read refuses carries that refusal in place of an answer — a
45
48
related list the caller may not read carries neither a count nor rows — and
@@ -54,8 +57,8 @@ no part discloses a record the caller could not open.
const result =RecordPageActivityRequestSchema.parse(data);
@@ -397,6 +400,7 @@ This schema accepts one of the following structures:
397
400
|**discussion**|`boolean`| optional | Return the record's discussion: its comment thread and those comments' reactions (`RecordPageDiscussionSchema`). |
398
401
|**activity**|`{ top: integer }`| optional | Return the record's newest activity rows and their count (`RecordPageStreamRowsSchema`). |
399
402
|**attachments**|`{ top: integer }`| optional | Return the record's newest attachments and their count (`RecordPageStreamRowsSchema`). |
403
+
|**timeline**|`{ discussion: boolean; activity: boolean; fieldChanges: boolean }`| optional | Return the size of the record's timeline stream over the sources named (`RecordPageTimelineCountSchema`). |
@@ -421,6 +425,14 @@ This schema accepts one of the following structures:
421
425
| :--- | :--- | :--- | :--- |
422
426
|**top**|`integer`| ✅ | How many of the record's newest attachments to return with their count; 0 returns the count alone. |
423
427
428
+
### Nested Shape: `RecordPageRequest.timeline`
429
+
430
+
| Property | Type | Required | Description |
431
+
| :--- | :--- | :--- | :--- |
432
+
|**discussion**|`boolean`| ✅ | Count the record's comment thread: every comment and reply. |
433
+
|**activity**|`boolean`| ✅ | Count the record's activity rows the stream draws an entry for (not `RECORD_TIMELINE_UNDRAWN_ACTIVITY_TYPES`). |
434
+
|**fieldChanges**|`boolean`| ✅ | Count the record's field changes (its `sys_audit_log` writes); with any, its `updated` activity rows step aside. |
435
+
424
436
425
437
---
426
438
@@ -440,6 +452,7 @@ This schema accepts one of the following structures:
440
452
|**discussion**|`{ comments: Record<string, any>[]; reactions: object \| object } \| { error: object }`| optional | The discussion part, when the request asked for it: the thread and its reactions, or the refusal of the comment read. |
441
453
|**activity**|`{ total: integer; records: Record<string, any>[] } \| { error: object }`| optional | The activity part, when the request asked for it: the newest rows and their count, or the refusal of their read. |
442
454
|**attachments**|`{ total: integer; records: Record<string, any>[] } \| { error: object }`| optional | The attachments part, when the request asked for it: the newest rows and their count, or the refusal of their read. |
455
+
|**timeline**|`{ total: integer } \| { error: object }`| optional | The timeline part, when the request asked for it: the stream's size, or the refusal of a comment or activity read. |
|[Automation Protocol](/docs/references/automation)| 15 | 78 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. |
26
26
|[Data Protocol](/docs/references/data)| 31 | 174 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. |
0 commit comments