Repository navigation
Commit 685d9f3
fix(runtime, plugin-sharing): a share link's resolve answer describes the fields it returns, so the shared-record page can show authored labels (#27107)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1X5AGM4y97vjVvqa9btSXiHi)_
Fixes #26991
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:分享链接的解析接口(`GET /api/v1/share-links/TOKEN/resolve`)在原有的
`record` / `link` / `redactFields` 之外,多返回一个
`fields`:只描述这条记录实际下发的字段,每个字段给出作者写的标签、类型、下拉选项的标签与颜色、货币设置、查找字段指向的对象和显示字段。两个出口(plugin-sharing
路由与 runtime 调度器)都经同一个
`buildShareLinkResolveAnswer`(`@objectstack/types`)生成答案。
- **为什么改**:打开分享链接的访客通常未登录,读不到对象元数据(`/meta/object` 对匿名返回
401),所以分享页只能把字段键名转成标题显示,作者写的中文标签、选项名、货币和查找名都显示不出来。
- **风险与回滚**:低。只新增一个响应键,原有三个键不变;被脱敏的字段在 `record` 和 `fields`
里都不出现,记录没下发的字段(如 `internal`)也不出现,不会多暴露对象的其它字段;`/meta` 的匿名拒绝不动。回滚即
revert。
-
**先看**:`packages/types/src/share-link-resolve-answer.ts`(`projectSharedRecordFields`
的边界:只看记录里的键);`packages/runtime/src/domains/share-links.ts` 与
`packages/plugins/plugin-sharing/src/share-link-routes.ts` 的 resolve 出口。
- **两分钟验证**:`pnpm --filter @objectstack/runtime exec vitest run
src/domains/share-links-field-projection.test.ts`,预期 12/12 通过(两个出口各 5
条,外加两条两出口答案相等)。
- **需要你做的**:无,席位落地。
## What changes
`GET /api/v1/share-links/TOKEN/resolve` answered `{ record, link,
redactFields }` on both mounts. It now answers `{ record, link,
redactFields, fields }`, built once by `buildShareLinkResolveAnswer` in
`@objectstack/types` for both the plugin-sharing routes
(`registerShareLinkRoutes`) and the runtime dispatcher's `/share-links`
domain.
`fields` is keyed by field name. Each entry holds only what a read-only
renderer needs:
| Key | When present |
|---|---|
| `label`, `type` | as the object schema holds them (authored labels;
the resolve read path has no caller-locale seam, and none is built) |
| `options` | a field declaring options: each option's `value`, `label`,
`color` (never `default` or visibility rules) |
| `multiple` | `true` only |
| `currencyConfig` | `currencyMode`, `defaultCurrency` |
| `reference`, `displayField` | a lookup's target object and authored
display field |
- **The boundary.** A field is projected only when the served record
carries its key after redaction AND the shared object declares it. So a
redacted field is absent from both `record` and `fields`, a field the
record read never returns (`internal: true`) is absent from both, and no
other field of the object is described. The meta door's anonymous
refusal (`GET /meta/object/NAME` answers 401) is unchanged; nothing new
reads object metadata for an anonymous caller beyond what the served
record's own keys already name.
- **Lookups.** Measured: the record read (`engine.find(object, { where:
{ id }, limit: 1 })` under a system context) expands nothing, so a
lookup's value is its stored id. The projection names `reference` and
the authored `displayField`; no second read expands names.
- **System columns.** The served record carries the registry-injected
columns (`created_at`, `updated_at`, `organization_id`, `created_by`,
`updated_by`, `owner_id`, `owning_business_unit_id`, `queue`), and the
projection describes them like any other served key (e.g. `owner_id` →
`{ label: 'Owner', type: 'lookup', reference: 'sys_user' }`). Measured
from a real resolve in the new test's harness.
### Placement of the one helper
`@objectstack/types`, as `share-link-resolve-answer.ts` beside
`share-password-header.ts`. `@objectstack/plugin-sharing` is a dev
dependency of `@objectstack/runtime`, so the plugin cannot be the home;
both packages already depend on `@objectstack/types` (and on
`@objectstack/spec`, which may hold no runtime logic), so the helper
adds no dependency edge. This is the same placement and reasoning the
shared password-header reading already uses.
The two `applyRedaction` copies are folded into it as
`redactSharedRecord`: the plugin's copy (it also stripped each object of
a top-level array). Redaction is unchanged for every input either mount
passes: both pass the one row object they read, where the two copies
were identical.
### Unchanged
The existing three keys and their values; refusal arms and their order;
response headers; the `/messages` route; the console's fold
(`ui/apps/console/src/pages/shared-record-shape.ts` reads `record`,
`link`, `redactFields` off `WireResolvedShare` and ignores any other
key, so the additive `fields` does not change what it parses). No
`packages/spec` schema: the resolve answer is not spec-declared today,
and this PR does not declare it. No `ui/**` change: the console reading
`fields` is the follow-up the card names.
## Tests
- `runtime` `src/domains/share-links-field-projection.test.ts` (new,
12): both mounts' production entries over one real `ObjectQL` +
`driver-sql` (better-sqlite3) + `ShareLinkService`, on an object with an
authored label, a select with option labels and a colour, a
fixed-currency field, a lookup with `displayField`, a per-link redacted
field and an `internal` field. Per mount: `fields` keys equal the
record's keys that the object declares; the redacted field is in
neither; control: with no redaction it is in both; the `internal` field
is in neither; the label/type/options/currency/lookup entries are exact
and the lookup's value is the stored id. Parity: for each link, the two
mounts' `data` are equal.
- `types` `src/share-link-resolve-answer.test.ts` (new, 7): the
boundary, the display-only keys, the nine link columns (never the hash,
allowlist or creator), an unreadable schema projects nothing, a redacted
key is skipped even if still present, `redactSharedRecord` shapes.
- `plugin-sharing` `share-link-envelope.conformance.test.ts`: the
resolve key-set pin now names `fields`.
- `runtime` `domain-handler-registry.test.ts`: the extracted share-links
domain's engine double answers `getSchema` (the route reads it off the
same engine the refusal probe already reads it from) and the case
asserts the redacted key is absent from `fields`.
<details><summary>Red first, ablation</summary>
- Red, at `d17dd70e` (the new test on `origin/main` `cbb3a1a1` code):
`Tests 10 failed | 2 passed (12)`; every per-mount case failed on the
absent `fields` (`the answer carries a field projection: expected
undefined to be type of 'object'`); the two parity cases passed, both
mounts lacking the key alike.
- Green at `653c867a`: `Tests 12 passed (12)`.
- Ablation (committed fix, `scripts/ablation-replace.mjs`): `for (const
key of Object.keys(record)) {` → `for (const key of
Object.keys(declared)) {` (project the object's whole field map). Anchor
1 → 0, blob `7c0a9303886d` → `19df7f482a58`; `pnpm --filter
@objectstack/types build`; `ablation-dist-preflight.mjs
@objectstack/types 'Object.keys(declared)'` hit `dist/index.js` and
`dist/index.mjs`. Run: `Tests 4 failed | 8 passed (12)`: "projects
exactly the served record keys…" and "a field the record read never
returns (internal)…" on both mounts. Restore: `git checkout HEAD --`,
`git diff HEAD` empty, hash equals the HEAD blob, rebuild, preflight
`--absent` exit 0 with the whole tree clean, run `Tests 12 passed (12)`.
</details>
## Acceptance notes
- `fields` labels are authored, untranslated: the resolve path has no
caller-locale seam today. A localized projection would be a new channel
for the anonymous caller, which the dispatch excluded.
- A `dynamic` currency field displays in the tenant default currency
(`localization.currency`), which an anonymous page cannot read; the
projection carries `currencyConfig` as declared and nothing more.
- The console still humanizes keys until its follow-up reads `fields`
(`SharedRecordFields.tsx`, `shared-record-shape.ts`); its comments
describing the answer as `{ record, link, redactFields }` move with that
follow-up.
## Verification
All local runs at `2804d207` unless noted.
<details><summary>Suites and typecheck</summary>
- `pnpm --filter @objectstack/types test && … typecheck`: `Test Files 30
passed (30)`, `Tests 796 passed (796)`, exit 0.
- `pnpm --filter @objectstack/plugin-sharing test && … typecheck`: `Test
Files 57 passed (57)`, `Tests 1278 passed (1278)`, exit 0 (at
`c1f772d0`; later commits touch only runtime tests).
- `pnpm --filter @objectstack/runtime test` (full `local` project, at
`c1f772d0`): `Tests 1 failed | 7609 passed | 19 skipped (7629)`; the one
failure was `domain-handler-registry.test.ts`'s share-links case, whose
engine double had no `getSchema` (500). Fixed in `2804d207`; re-run of
`src/domain-handler-registry.test.ts` plus `src/domains/`: `Test Files
156 passed (156)`, `Tests 4353 passed (4353)`. `pnpm --filter
@objectstack/runtime typecheck` (with its test typecheck): exit 0.
- `pnpm exec eslint --no-inline-config packages/types
packages/plugins/plugin-sharing packages/runtime`: exit 0.
- `test:repo` not run locally: the diff adds no repo-project test and no
derived family names it; CI `Test Core` runs it.
- Dogfood: n/a (no `!` in the title, no `(narrowing)` arm).
</details>
<details><summary>Derived gates (local), reconciled with --ran</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 64 families on this diff. Each was
run with its exit code captured before any pipe; the test-reading
families (`check:engine-double-contract`, `check:objectql-double-limit`,
`check:query-options-erasure`, `check:driver-memory-census`,
`check:where-matcher`, `check:cross-package-test-inputs`,
`check:test-source-alias`, `check:nul-bytes`,
`check:system-context-census`) and `check-plugin-teardown-shape
--self-test` were re-run on `2804d207`, all exit 0.
- 62 exit 0.
- NOT MEASURED, 2, both PREREQUISITE NOT MET (exit 3): `pnpm
check:dual-build-cjs-loads` and `pnpm check:i18n` read every package's
built `dist/`, which needs a full workspace build this worktree did not
run (the diff touches no translation bundle). CI's `Lint & Repo Gates`
runs both on a full build.
- `--ran` reconciliation: `64 derived famil(ies) accounted for — 62 run,
2 NOT-MEASURED (2 DERIVED from a recorded exit 3)`, exit 0.
- `check-plugin-teardown-shape --self-test` first exited 3 on the
shallow clone; after `git fetch --depth=1 origin
621a487` it exits 0 (48 cases).
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5os139Pfixtv7wjwGB)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 6817bf1 commit 685d9f3
9 files changed
Lines changed: 603 additions & 58 deletions
File tree
- .changeset
- packages
- plugins/plugin-sharing/src
- runtime/src
- domains
- types/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
24 | | - | |
| 24 | + | |
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| |||
500 | 500 | | |
501 | 501 | | |
502 | 502 | | |
503 | | - | |
504 | | - | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
505 | 506 | | |
506 | 507 | | |
507 | 508 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
| 10 | + | |
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| |||
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
29 | | - | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
30 | 32 | | |
31 | 33 | | |
32 | 34 | | |
| |||
47 | 49 | | |
48 | 50 | | |
49 | 51 | | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
50 | 55 | | |
51 | 56 | | |
52 | 57 | | |
| |||
186 | 191 | | |
187 | 192 | | |
188 | 193 | | |
189 | | - | |
| 194 | + | |
190 | 195 | | |
191 | 196 | | |
192 | 197 | | |
| |||
271 | 276 | | |
272 | 277 | | |
273 | 278 | | |
274 | | - | |
275 | | - | |
276 | | - | |
277 | | - | |
278 | | - | |
279 | | - | |
280 | | - | |
281 | | - | |
282 | | - | |
283 | | - | |
284 | | - | |
285 | | - | |
286 | 279 | | |
287 | 280 | | |
288 | 281 | | |
| |||
548 | 541 | | |
549 | 542 | | |
550 | 543 | | |
551 | | - | |
552 | | - | |
553 | | - | |
554 | | - | |
555 | | - | |
556 | | - | |
557 | | - | |
558 | | - | |
559 | | - | |
560 | | - | |
561 | | - | |
562 | | - | |
563 | | - | |
564 | | - | |
565 | | - | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
566 | 547 | | |
567 | 548 | | |
568 | 549 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
490 | 490 | | |
491 | 491 | | |
492 | 492 | | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
493 | 499 | | |
494 | 500 | | |
495 | 501 | | |
| |||
500 | 506 | | |
501 | 507 | | |
502 | 508 | | |
| 509 | + | |
| 510 | + | |
503 | 511 | | |
504 | 512 | | |
505 | 513 | | |
| |||
0 commit comments