Skip to content

Commit b861525

Browse files
feat(objectql): a referenceNameVisible lookup or user field names a target the reader is refused, on $expand (#29460) (#29576)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi95v1bE93v4RhtAn6F7LSiDY)_ Fixes #29460 ## 维护者速读 - 改了什么:声明了 `referenceNameVisible: true` 的 lookup / user 字段,在 `$expand` 时,读者被拒读的目标记录现在答 `{ id, name, readRefused: true }`;此前它只答裸 id。 - 为什么改:这个键由 #29171 的裁决(6574398310,字母 B)声明在前、运行时未供;本卡兑现它,ui 卡 #29462 照这个形状把名字渲染成不带链接的纯文本。 - 风险与回滚:这是放宽读面(向被拒读者多给一个名字)。名字由引擎只按 id 与名称列去读,调用方的投影、嵌套 expand、过滤和排序都不进入那次读;名称列上的字段级规则(FLS、maskingRule、经引用的公式)照旧生效,此时给 `Record #<id>` 底名。不动存储形态,回滚即 revert 本 PR。 - 先看:`packages/objectql/src/engine-reference-name-visible.ts` 的两次读各自拿到什么,再看 `packages/rest/src/data-reference-name-visible.test.ts` 里的负向钉子与对照组。 - 两分钟验证:`pnpm --filter @objectstack/objectql build`,再在 `packages/rest` 下 `pnpm exec vitest run src/data-reference-name-visible.test.ts`,23 个全绿。 - 需要你做的:裁定下文 Open questions 第 1 条(名称列对读者 FLS 隐藏时,本 PR 给底名 `Record #<id>`,还是照给名字),其余无。 ## What changes `FieldSchema.referenceNameVisible` was declared ahead of its runtime. It is now served by objectql's `$expand`: - **A target the reader is refused** (the target object's read gate, its required permissions, or its row scope) answers `{ id, name, readRefused: true }` in place of the bare id. `id` is the id the referencing row holds. `name` is the target's display name as ADR-0079 resolves it (`nameField`, then `displayNameField`, then the derived title, with the floor `Record #<id>`). `readRefused: true` says in the value that the reader was refused the target (the `metadataRefused` precedent of a refused file value), so a renderer shows the name as text and links nothing. A `multiple: true` field is answered per element. - **Nothing else of the target.** Two reads, each handed only what it needs: 1. which of the ids the caller's own expansion left bare are refused is asked **as the caller**: the security service's object-level admission, then an id-only read for the row scope. None of the caller's `$expand` nesting is forwarded. 2. the name is an engine-owned read of the refused ids only, projected to `id` and the resolved name field, under the tenant-kept elevation (`{ ...caller, isSystem: true }` with operation-private keys stripped, so the driver still scopes it to the caller's organization). The `$expand` request's projection, nested expand, filter and order never reach it, and the field's own `displayField` does not choose the column it reads. - **A field-level rule on the name field is not lifted.** When the caller's field answer (`getReadableFields`) does not hold the name field, the field declares a `maskingRule`, it is a formula reading through another reference, it is not a title-typed column, or no field answer can be had, the name is the `Record #<id>` floor. See Open questions 1. - **Unchanged:** a target the reader can read is expanded exactly as before; an id no record has, a record of another organization, and every field without the key keep the bare id; an unexpanded read answers the stored bare id for every reader (ADR-0104 D1). `sys_user` names through a `user` field or a lookup to `sys_user` take the same path; there is no separate carve-out. - The key's `.describe()` drops its `[PLANNED — not yet served]` marker and states what is served; `packages/spec/liveness/field.json` records the key `live`, citing the server test; generated reference docs and the liveness counts are regenerated. Code: `packages/objectql/src/engine-reference-name-visible.ts` (new; the module-split host pattern, engine imported as a type only), one call site and one private method in `packages/objectql/src/engine.ts#expandRelatedRecords`, and `isReadRefusal` exported from `engine-file-reference-hydration.ts` so the two sub-reads that serve a refusal in the value read one predicate. ### Response shape for the ui card (#29462) After `$expand` of a `referenceNameVisible` field, each value is one of: | the reader and the target | the value | | --- | --- | | may read the target | the expanded record, exactly as without the key | | refused the target (gate, required permissions, row scope) | `{ id, name, readRefused: true }`; `name` may be the floor `Record #<id>` | | the id names no record, or a record of another organization | the bare id | | no key on the field, or no `$expand` | the bare id | ## Why Ruling 6574398310 (letter B) on #29171: a principal who can read the record carrying the field may see the referenced record's name and id even when it cannot read that record, and nothing else of it. The ui lane renders it (#29462). ### Readings this card starts from (`main` at a922779, real ObjectQL over SQLite, REST server) - Plain engine read, REST `GET` by id and REST list (no `$expand`): the lookup holds the bare FK id for every reader (readable, row-scope-hidden, gate-refused, dangling). - `$expand`: a readable target is the full record; a row-scope-hidden or gate-refused target, or a dangling id, keeps the bare id. A `sys_user` the reader's row scope admits is expanded; one it does not keeps the bare id. - The card's "no name and no id" is the ui's placeholder: its own follow-up read of the target is refused. No `sys_user` name carve-out exists in the engine or the plugins. ## Verification Clause-②: yes (widening) Metadata-protocol: none _Keep one value on each of the two lines above, each line starting with its key. [Human Minimum](https://github.com/objectstack-ai/objectstack/blob/main/CONTRIBUTING.md#human-minimum) item 5 says what they mean._ **Tests** — `packages/rest/src/data-reference-name-visible.test.ts` (new, 23 tests): real ObjectQL, the SQLite driver, the security plugin and the REST server; the record, list and query doors; a single lookup, a `multiple: true` lookup, a `user` field and a lookup to `sys_user`; the row-scope, gate and cross-organization cases; the floors; and a control for every negative pin. - Red on the old build, green on the new: before the engine change, the suite as it then stood read 13 failed, 8 passed, 1 skipped of 22 (every served case red, every control green); on this head 23/23. - `packages/spec` field and liveness suites: the field, field-reference-name-visible and liveness suites 3 files, 181 passed; on this head the package suites read objectql 534/534 files, 10391 tests passed; rest 374/374 files, 7080 passed, 332 skipped; spec 792/792 files, 23522 passed, 1 todo. **Forced test edit:** `packages/spec/src/data/field-reference-name-visible.test.ts` pinned the `[PLANNED` marker in the describe. It now pins the marker's absence and the served shape (the designed flip of a declared-ahead key); the bounds pins are unchanged. **Ablations** (each planted with `scripts/ablation-replace.mjs`, objectql rebuilt, presence proven in `dist/` with `scripts/ablation-dist-preflight.mjs`, the suite run, the source restored and proven equal to HEAD; after the last leg every marker proven absent from `dist/`, the tree clean, the suite 23/23): | leg | forbidden behaviour put back | red | | --- | --- | --- | | 1 | the caller's `$expand` projection, nested expand and nested filter and the field's `displayField` forwarded into the name read, and its row spread into the value | 15 failed (the nested projection, nested expand and `displayField` pins among them) | | 2 | the key gate removed (every reference field served) | 2 failed (the without-key controls) | | 3 | the caller's field answer on the name field ignored | 1 failed (the FLS floor) | | 4 | the name field's `maskingRule` ignored | 1 failed (the masking floor) | | 5 | a formula name reading through another reference computed under the elevation | 2 failed (the formula floor, the nested-expand pin) | | 6 | the name read loses the caller's organization | 1 failed (the other-organization pin) | | 7 | names served on the unexpanded read too | 1 failed (the unexpanded-read pin) | | 8 | a name invented for an id the name read did not return | 4 failed (dangling, per-element, list, other-organization) | | 9 | the caller probe's readable answer dropped | 1 failed (the readable-target-own-filter control) | **Local gates** on `7e8a7b30`: `dispatch-gates --commands` derived 109 commands after the last commit; 109 run, every one exit 0; `--ran` reconcile 109 derived, 109 run, 0 NOT-MEASURED, 0 UNRUN. `pnpm lint` exit 0; `typecheck` of objectql, rest and spec exit 0. **Dogfood battery** on `7e8a7b30`: `os-verify-lock` VERDICT command-exit 0 (held 29m15s, shared box) · Test Files 397 passed, 3 skipped (400) · Tests 3277 passed, 28 skipped (3305) · no failures Contract review: the paths put this on the schema face (`record-recognisers.mjs --paths`), at `CONTRACT_REVIEW_TIER`, scope `packages/spec/src/data/field.zod.ts`. ## Open questions 1. **The name field is itself kept from the reader by a field-level rule.** The describe as declared did not mention field-level rules; serving the name there would bypass the caller's field grant (or compute a mask or a reference hop under the elevation). Options: **A** (this PR) the name is the floor `Record #<id>` with the id, and the describe says so; **B** serve the name regardless, and amend the describe to say the key lifts field-level rules on the name field too (drop three checks in `servableNameField`). Recommendation A: it serves the ruling's "name and id" without widening any field grant, and B stays a contained follow-up if ruled. A reader who can read the target row but not its name field is served exactly what it is served without the key (the ruling's "can read the target ⇒ as today"). 2. **Plain reads are not served.** The delta asked to measure the plain read; ADR-0104 D1 has every unexpanded reference answer its stored form (the bare id, which is already the "id" half). Serving a name there would give a refused reader a richer value than a reader of the target gets on the same read. Served on `$expand` only; a ui that wants the name expands the field. 3. **A refused target is named even when the caller's own nested filter would have excluded it.** Filters are never forwarded into the name read, by design (forwarding them is ablation 1). A readable target the caller's nested filter excludes keeps today's bare id (control pinned). 4. **A named Zod schema for the served value** (`{ id, name, readRefused: true }` beside `FileRefusedValueSchema` in `field-value.zod.ts`) is spec-lane follow-up; this PR exports the TypeScript shape `ReferenceNameOnlyValue` from the engine module only. ## Human Minimum Each item is one rule from [CONTRIBUTING.md › Human Minimum](https://github.com/objectstack-ai/objectstack/blob/main/CONTRIBUTING.md#human-minimum), which links the AGENTS.md text that states it. - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (the 维护者速读 block above is Chinese by the lane seat's standing order). - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_01ADHkmRXbaXBLCJeJXfoh21)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2420778 commit b861525

13 files changed

Lines changed: 881 additions & 35 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/objectql': patch
3+
'@objectstack/spec': patch
4+
---
5+
6+
feat(objectql): a `referenceNameVisible` lookup or user field names the record it points at on `$expand` when the reader may not read that record
7+
8+
Clause-②: yes (widening)
9+
Metadata-protocol: none
10+
11+
`FieldSchema.referenceNameVisible` was declared ahead of its runtime. It is now served on the expanded read:
12+
13+
- **What a refused reader gets.** On `$expand` of a `lookup` or `user` field that declares `referenceNameVisible: true`, a target the reader is refused (the target object's read gate, its required permissions or its row scope) answers `{ id, name, readRefused: true }` in place of the bare id it answered before. `id` is the id the record holds; `name` is the target's display name as ADR-0079 resolves it (`nameField`, then `displayNameField`, then the derived title, with the floor `Record #` and the id). `readRefused: true` tells a renderer to show the name as text with no link. A `multiple: true` field is answered per element.
14+
- **Nothing else of the target.** The name is read by the engine from exactly the target's id and its name field. The `$expand` request's projection, nested expand, filter and order are never applied to that read, and the field's `displayField` does not change the column it reads.
15+
- **Field-level rules on the name field still hold.** When the reader's field-level security keeps the name field from it, the field declares a `maskingRule`, or the name is a formula reading through another reference, the name served is the `Record #` floor with the id.
16+
- **Unchanged.** A target the reader can read is expanded exactly as before. An id no record has, a record of another organization, and every field without the key keep the bare id. An unexpanded read still answers the stored id for every reader. The same key covers `sys_user` names through a `user` field or a lookup to `sys_user`.
17+
18+
The key's `.describe()` drops its planned marker and the liveness ledger records it as live.

‎content/docs/references/data/field.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ const result = CurrencyConfigSchema.parse(data);
7878
| **reference** | `string` | optional | Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects. Never a stored-metadata table (`sys_metadata`, `sys_metadata_history`): their rows are served only through the metadata API, so the engine never hydrates a reference into one, and `os validate`, `os build` and the object save door refuse it. On a `tree` field it is optional and, if given, must be the declaring object's own name — the object schema refuses any other target, and writes the own name when it is left out. |
7979
| **referenceVia** | `string` | optional | Declares this text field as the id half of a polymorphic pointer pair (ADR-0052 §5 ActivityPointer): the value is a record id of the object named by the SIBLING FIELD this key names — e.g. `record_id` with `referenceVia: 'object_name'`. The sibling must be a declared field on the same object holding an object machine name. Text fields only; mutually exclusive with `reference` (a static and a per-record target contradict). Enforced today at seed load: the value resolves as a natural key against the object the sibling column names, and an unresolvable pointer is refused loudly instead of stored verbatim. Adds no referential integrity or $expand behavior. |
8080
| **referenceDeletedWhen** | `{ field: string; value: string }` | optional | Marks the record a `referenceVia` pointer names as PLATFORM-DELETED on rows where a sibling `select` field holds one declared option — e.g. `record_id` with `{ field: 'cancel_reason', value: 'record_deleted' }`. A renderer then draws a tombstone from that option's own label, reads nothing and links nothing. Valid only beside `referenceVia`; `field` must be a single-value `select` on the same object declaring inline `options`, and `value` one of its option values — any other shape is refused at parse. |
81-
| **referenceNameVisible** | `boolean` | optional | [PLANNED — not yet served] Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField` value) and id even when that principal cannot read the referenced record. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. Declared ahead of its producer (ADR-0049): no runtime serves the name yet, so setting it changes no read today. |
81+
| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField` value) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }`, per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds: when the reader may not read that field, it is masked, or it is a formula reading through another reference, the name is the `Record #` floor with the id. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. |
8282
| **mailAssociation** | `boolean` | optional | Inbound mail association (`email` fields only): TRUE marks this field as identifying a correspondent. When a mailbox service ingests a message, it associates the message with every record of this object whose value in this field equals one of the message's participant addresses (From, Sender, Reply-To, To and Cc, plus those of a forwarded message it carries; ASCII case-insensitive; the receiving mailbox's own addresses excluded; only within the mailbox's organization). A message no record matches waits in the mailbox's unassociated pool. Refused on any other type. Takes effect only where a mailbox service is composed. |
8383
| **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted |
8484
| **inlineEdit** | `boolean \| Enum<'grid' \| 'form'>` | optional | Edit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form. |

‎content/docs/references/data/object.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ const result = ApiMethod.parse(data);
247247
| **reference** | `string` | optional | Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects. Never a stored-metadata table (`sys_metadata`, `sys_metadata_history`): their rows are served only through the metadata API, so the engine never hydrates a reference into one, and `os validate`, `os build` and the object save door refuse it. On a `tree` field it is optional and, if given, must be the declaring object's own name — the object schema refuses any other target, and writes the own name when it is left out. |
248248
| **referenceVia** | `string` | optional | Declares this text field as the id half of a polymorphic pointer pair (ADR-0052 §5 ActivityPointer): the value is a record id of the object named by the SIBLING FIELD this key names — e.g. `record_id` with `referenceVia: 'object_name'`. The sibling must be a declared field on the same object holding an object machine name. Text fields only; mutually exclusive with `reference` (a static and a per-record target contradict). Enforced today at seed load: the value resolves as a natural key against the object the sibling column names, and an unresolvable pointer is refused loudly instead of stored verbatim. Adds no referential integrity or $expand behavior. |
249249
| **referenceDeletedWhen** | `{ field: string; value: string }` | optional | Marks the record a `referenceVia` pointer names as PLATFORM-DELETED on rows where a sibling `select` field holds one declared option — e.g. `record_id` with `{ field: 'cancel_reason', value: 'record_deleted' }`. A renderer then draws a tombstone from that option's own label, reads nothing and links nothing. Valid only beside `referenceVia`; `field` must be a single-value `select` on the same object declaring inline `options`, and `value` one of its option values — any other shape is refused at parse. |
250-
| **referenceNameVisible** | `boolean` | optional | [PLANNED — not yet served] Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField` value) and id even when that principal cannot read the referenced record. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. Declared ahead of its producer (ADR-0049): no runtime serves the name yet, so setting it changes no read today. |
250+
| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField` value) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }`, per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds: when the reader may not read that field, it is masked, or it is a formula reading through another reference, the name is the `Record #` floor with the id. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. |
251251
| **mailAssociation** | `boolean` | optional | Inbound mail association (`email` fields only): TRUE marks this field as identifying a correspondent. When a mailbox service ingests a message, it associates the message with every record of this object whose value in this field equals one of the message's participant addresses (From, Sender, Reply-To, To and Cc, plus those of a forwarded message it carries; ASCII case-insensitive; the receiving mailbox's own addresses excluded; only within the mailbox's organization). A message no record matches waits in the mailbox's unassociated pool. Refused on any other type. Takes effect only where a mailbox service is composed. |
252252
| **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted |
253253
| **inlineEdit** | `boolean \| Enum<'grid' \| 'form'>` | optional | Edit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form. |
@@ -618,7 +618,7 @@ const result = ApiMethod.parse(data);
618618
| **reference** | `string` | optional | Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects. Never a stored-metadata table (`sys_metadata`, `sys_metadata_history`): their rows are served only through the metadata API, so the engine never hydrates a reference into one, and `os validate`, `os build` and the object save door refuse it. On a `tree` field it is optional and, if given, must be the declaring object's own name — the object schema refuses any other target, and writes the own name when it is left out. |
619619
| **referenceVia** | `string` | optional | Declares this text field as the id half of a polymorphic pointer pair (ADR-0052 §5 ActivityPointer): the value is a record id of the object named by the SIBLING FIELD this key names — e.g. `record_id` with `referenceVia: 'object_name'`. The sibling must be a declared field on the same object holding an object machine name. Text fields only; mutually exclusive with `reference` (a static and a per-record target contradict). Enforced today at seed load: the value resolves as a natural key against the object the sibling column names, and an unresolvable pointer is refused loudly instead of stored verbatim. Adds no referential integrity or $expand behavior. |
620620
| **referenceDeletedWhen** | `{ field: string; value: string }` | optional | Marks the record a `referenceVia` pointer names as PLATFORM-DELETED on rows where a sibling `select` field holds one declared option — e.g. `record_id` with `{ field: 'cancel_reason', value: 'record_deleted' }`. A renderer then draws a tombstone from that option's own label, reads nothing and links nothing. Valid only beside `referenceVia`; `field` must be a single-value `select` on the same object declaring inline `options`, and `value` one of its option values — any other shape is refused at parse. |
621-
| **referenceNameVisible** | `boolean` | optional | [PLANNED — not yet served] Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField` value) and id even when that principal cannot read the referenced record. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. Declared ahead of its producer (ADR-0049): no runtime serves the name yet, so setting it changes no read today. |
621+
| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField` value) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }`, per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds: when the reader may not read that field, it is masked, or it is a formula reading through another reference, the name is the `Record #` floor with the id. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. |
622622
| **mailAssociation** | `boolean` | optional | Inbound mail association (`email` fields only): TRUE marks this field as identifying a correspondent. When a mailbox service ingests a message, it associates the message with every record of this object whose value in this field equals one of the message's participant addresses (From, Sender, Reply-To, To and Cc, plus those of a forwarded message it carries; ASCII case-insensitive; the receiving mailbox's own addresses excluded; only within the mailbox's organization). A message no record matches waits in the mailbox's unassociated pool. Refused on any other type. Takes effect only where a mailbox service is composed. |
623623
| **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted |
624624
| **inlineEdit** | `boolean \| Enum<'grid' \| 'form'>` | optional | Edit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form. |

0 commit comments

Comments
 (0)