Skip to content

Commit 8464380

Browse files
fix(spec, objectql): a tree field with no reference is a self-reference, so its deleteBehavior takes effect (#23092)
Fixes #23065 Clause-②: yes (widening) Widens: `@objectstack/spec/data` gains the export `materializeTreeSelfReferences`, and `ObjectSchema` / `ObjectExtensionSchema` parse output now writes `reference` (the declaring object's name) on a `tree` field that omits it. The accept set is unchanged: nothing that parsed before is refused. ## What was wrong (measured at the door on `origin/main` `275ad4229a`) The contract already reads a `tree` with no `reference` as a self-reference: `refuseForeignTreeReference` tells authors "Drop `reference` (it is optional on a `tree`)", and `FieldSchema` materializes `set_null` on the field. But `referenceTargetOf` reads one field alone and cannot know the declaring object, so every reader answered "no target". Booted with `@objectstack/verify`'s `bootStack`, one object with `parent: { type: 'tree' }`, through the REST door as the signed-in admin: | reader | referenceless `tree`, before | same field with `reference` = own object (control) | referenceless `tree`, this PR | |---|---|---|---| | `DELETE` parent, `set_null` (default) | 200, child keeps the deleted id | 200, child `parent: null` | 200, child `parent: null` | | `DELETE` parent, `restrict` | 200, child keeps the id | 409 `DELETE_RESTRICTED` | 409 `DELETE_RESTRICTED`, `dependentObject` = the tree's own object | | `DELETE` parent, `cascade` | 200, child and grandchild kept | subtree deleted | subtree deleted (child and grandchild 404) | | `PUT /meta/object/:name` then `DELETE` parent | 200, child keeps the id | n/a | 200, child `parent: null` | | write check, `POST` child with a parent id that does not exist | 201 accepted | 400 `reference_not_found` | 400 `reference_not_found` | | `GET /data/:object/:id?expand=parent` | 404 "declares no target object" | 200, parent expanded | 200, parent expanded | | nested relation filter `{ parent: { title: 'p' } }` | 400 "declares no related object" | 200, rows | 200, rows | | dangling-reference audit (`inspectDanglingReferences`) | not audited (no target) | reported | reported, target = own object | | `os verify --delete-behaviors` case derivation | excluded: "names no target object" | probed | probed, proven through both doors | Readings taken with a scratch suite on both a raw config and a `defineStack` config (same answers), before and after; logs kept in the dev's scratch directory, not committed. ## Route: both measured, (ii) chosen and completed at the registry - **(i) thread the owner into the readers.** `referenceTargetOf` has 26 production call sites in 18 files across 8 packages (`core`, `lint`, `metadata-protocol`, `objectql`, `plugin-approvals`, `plugin-audit`, `service-analytics`, `verify`; counted at `275ad4229a`), and the delete walk itself reads the carrier (`referenceCarrierOf`, twice), not the target. Each would have to pass the owner, a caller that forgets gets the old silent `undefined`, and any reader that reads `def.reference` directly is still blind. That is one reading that has to be repeated correctly at every call site. - **(ii) write the self-reference where the object is known.** Measured gap: `ObjectSchema`'s parse alone does not reach the runtime for every door. `PUT /meta/object/:name` stores the authored body verbatim (registry entry measured: `{"type":"tree","label":"Parent"}`, no `reference`, no `deleteBehavior`), and a raw `registerObject` / raw `bootStack` config is never parsed. So the registry, the one door every registration passes (it already runs `objectFieldTypeRefusal` and `applySystemFields`), runs the same spec helper. Every reader, whether it reads the target or the carrier, then sees an ordinary self-reference, and no reader changes. I chose (ii) with the registry door. It is one reading, applied by one helper at the two places that know the object, with no reader-side fallback. The triage direction ("the engine's reference readers take the field's own object") holds without editing a reader. The parse output changes are byte-identical to a field that wrote its own name: the key lands at its `FieldSchema` shape position, and a re-parse is a no-op. `refuseForeignTreeReference`'s guidance still reads true, because dropping `reference` now yields exactly the self-reference. The authorable surface and the conversion ledger are unchanged (`check:authorable-surface` green; no ADR-0087 entry). ## What changed - `packages/spec/src/data/object.zod.ts`: new export `materializeTreeSelfReferences(ownName, fields)`. Absence is the two spellings a contract admits for "names no target": `undefined` (`FieldSchema`'s `.optional()`) and `null` (the blueprint's `StrictField`). Every other value is returned untouched. `reference: ''` is NOT absence here: the parse refuses it on a `tree` as a foreign target, so the registry leaves it as written instead of repairing it unparsed, and the carrier reader reads it as naming nothing, as before (patch round 1, review record 6131807682 item 10a). A foreign name stays `refuseForeignTreeReference`'s to refuse, and a non-string value stays the carrier reader's to refuse loudly. It returns the same object when nothing needs writing. `ObjectSchema` and `ObjectExtensionSchema` (own name = `extend`) run it as an `.overwrite` after their refinements, so the schema stays a `ZodObject`. - `packages/objectql/src/registry.ts`: `registerObject` runs the helper right after the field-type door, on a copy, so the caller's definition is not mutated. An extension registers under `name: ext.extend`. - `packages/spec/src/data/field-value.zod.ts`: the `referenceTargetOf` docblock no longer calls a referenceless `tree` "an authoring bug". It says where the target is written and why the function cannot derive it. - `packages/spec/src/data/field.zod.ts` (prose only): the `reference` TSDoc and `.describe()` ("…and writes the own name when it is left out"); the `deleteBehavior` TSDoc's #9784 paragraph and the `.overwrite` block now say the delete walk reads the key on `master_detail`, `lookup` and `tree` (the `statesDeleteSemantics` family). The `user` sentence names that family: `user` is still outside it, so the substance was true and the wording was stale. - Generated: `packages/spec/api-surface/data.json`, `packages/spec/export-origins/data.json`, `content/docs/references/{data/field,data/object,system/migration}.mdx`. - `.changeset/23065-referenceless-tree.md`: `minor` for `@objectstack/spec` (export added), `patch` for `@objectstack/objectql`. ## Merge with `origin/main` (patch round 1) `origin/main` was merged at `0511341408` (9 commits past the merge base `275ad4229a`) through `scripts/pm/os-regen-merge.sh`, by merge commit `f77dd94819`. Step 2 kept the branch's bytes of the four artifacts only the branch moved, and took main's side of `content/docs/references/data/object.mdx`, the one both sides moved. Step 3's commit was refused by the `os-regen` pre-commit as designed. Then `gen:schema && gen:docs` regenerated `object.mdx` on the merged tree, and the staged diff, read before committing, held both sides' rows: main's `target` row for `type: 'url'` and this PR's two `reference` rows. That was committed as `b390b3cbae`, and the hook printed `content/docs/references/data/object.mdx — current … marker cleared`. On the merged head, after a rebuild, `check:generated --fix` reported all 14 generated artifacts up to date, `check:api-surface`, `check:authorable-surface` and `check:docs` included, with nothing left to write. `git diff --stat 0511341 HEAD` is exactly this PR's 13 files. Since the merge, `main` has gained two commits, `6662e3f590` (runtime `app-plugin.ts`) and `1badd31838` (`ui/app.zod.ts` docs). Neither touches a file in this diff. ## Tests (at `d09dffd8bf`, the merged head) - `packages/spec/src/data/tree-reference-self-only.test.ts`: +7 pins. The parse writes the own name byte-identical to a written one and re-parses as a no-op. `create()` and `defineStack` write it. The extension door writes `extend`. The refusal's remedy holds. CONTROL: a written self-reference, `lookup`, `master_detail` and `text` parse exactly as `FieldSchema` alone parses them. Helper semantics: `undefined` and `null` are written; `''`, a non-string and a foreign value are untouched; nothing to write allocates nothing. `reference: ''` on a tree has one reading: the parse refuses it at `fields.f_tree.reference`. One existing assertion (`reference` toBeUndefined after parse) pinned exactly the behaviour this card changes and now reads the own name. 14/14. - `packages/objectql/src/engine-cascade-delete-tree.test.ts`: +10 pins on a real engine with a snapshot-rollback driver. The registry writes the self-reference and leaves the caller's object alone. An extension's tree references the extended object. Also pinned: `set_null` (absent, field-parsed, authored), the object-parsed door, `restrict` (envelope), `cascade` (subtree, one unit of work). An empty-string `reference` reaches the registry as written and `referenceTargetOf` answers no target. CONTROL: a written self-reference. 28/28. - `packages/verify/src/delete-behavior-referenceless-tree.test.ts`: the door pins on one `bootStack`. Through `DELETE /data`: `set_null`, `restrict` and `cascade`. The metadata save door. `runDeleteBehaviorProofs` proves all referenceless cases through both doors. The write check: a parent id that names no record answers 400 `VALIDATION_FAILED` with `fields[]` `{ field: 'parent', code: 'reference_not_found' }` (it was 201), and CONTROL: an existing parent is accepted and stored. `expand=parent` resolves the parent (it was 404). The nested relation condition `{ parent: { title } }` filters through it (it was 400). CONTROLS: a written self-reference, `lookup`, `master_detail`. 8/8. - Package suites on the merged head: `@objectstack/spec` 714 files / 20898 passed. `@objectstack/objectql` 409 files / 7989 passed. `@objectstack/verify` 35 files / 278 passed, run after the gates had finished: the previous round's concurrent-build "Failed to resolve entry" flake did not recur. `typecheck` is green for objectql and verify, test layers included; spec's test layer is OK through `check:generated`. ## Ablation (one-time proofs, not committed) Each leg was run against the committed fix, with the mutation landed through `scripts/ablation-replace.mjs` (marker 1, anchor 0), spec dist rebuilt with the marker confirmed by `ablation-dist-preflight`, and a `trap` restore. - Round 0 (at `bd8eff9eb6`): the helper writes nothing. Red: spec 6/13, objectql 8/27, verify 5/6. Green: every CONTROL and the #14892 / #22957 pins. - Round 1 leg A (at `d09dffd8bf`): the helper writes nothing. Verify is red on 7/8: every delete pin, the meta save door, `os verify`, the write check, and expand plus nested filter. Green: the CONTROLS test. - Round 1 leg B: `''` read as absence again. Red: the spec helper pin (1/14) and the objectql empty-string registry pin (1/28). Green: the spec "parse refuses `''`" pin, which is the parse's own refusal, and every other pin. - Restore, each leg: blob == HEAD, `git diff HEAD` empty, spec rebuilt in full, `ablation-dist-preflight --absent` clean for both markers, whole tree clean. ## Gates Union of the dispatch's 84-command list and the list re-derived on the merged head (115, the same set as round 0): 117 commands at `d09dffd8bf`, every exit captured before any pipe. All 117 exit 0. `check:dual-build-cjs-loads` first refused its prerequisite (exit 3, packages without dist), and its re-run after the later gates had built them exited 0. Among them, `check:generated` ("All 14 generated artifacts are up to date"), `check:api-surface`, `check:authorable-surface` and `check:docs` ("226 generated files in sync"). `dispatch-gates --ran` with exit codes: 115 derived, 115 run, 0 NOT-MEASURED, 0 UNRUN. ## Acceptance notes - Author-time lint on the metadata save door judges the authored body before any materialization, as it does for every parse default. So a rule that traverses a referenceless `tree` inside the very object being saved sees no target there, while `os validate` / `os lint` read `defineStack` output, which carries it. Not measured at a door; noted, carrier: none. - `ObjectSchema.create()` is typed off the authored literal, so TypeScript does not see the written `reference` on its result (the runtime value is there). This is the same property every parse default already has. - A stored `sys_metadata` row keeps its body verbatim (ADR-0005 appendix (c)). Only the registered definition carries the self-reference, and a round trip through `GET`/`PUT /meta/object` stores the own name, which re-parses clean. - objectui's use of `referenceTargetOf` / `referenceCarrierOf` is NOT MEASURED: repository-scoped search was refused from this session. No reader's signature or behaviour changes for a field that names a target, and the shipped corpus already carries a written self-reference on a tree (the showcase's `f_tree`). --- _Generated by [Claude Code](https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 36b2889 commit 8464380

13 files changed

Lines changed: 643 additions & 24 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/objectql': patch
4+
---
5+
6+
fix(spec, objectql): a `tree` field with no `reference` points at its own object, so its `deleteBehavior` takes effect when a parent is deleted
7+
8+
Clause-②: yes (widening)
9+
10+
A `tree` field is a record's parent in a hierarchy on its own object. `reference` is optional on it, and when you write one it must be the object's own name — the object schema refuses any other target and tells you to drop the key. But a `tree` with no `reference` behaved as if it pointed at nothing. Deleting a parent answered success and left every child holding the deleted id, whatever `deleteBehavior` said (`set_null`, `restrict` or `cascade`). The same field also could not be expanded (`expand=parent` answered "declares no target object"), could not be filtered through (`{ parent: { title: … } }` was refused), and accepted a parent id that does not exist.
11+
12+
Now a `tree` with no `reference` is a reference to the object that declares it, exactly as if you had written the object's name:
13+
14+
- **`@objectstack/spec`.** `ObjectSchema` and `ObjectExtensionSchema` write the object's own name (an extension's `extend`) into such a field's `reference` when they parse it. `defineStack` and `ObjectSchema.create()` output, and built artifacts, now carry the object's name in `reference` on those fields, byte-identical to a field that wrote it. Nothing you could write before is refused now. A new export, `materializeTreeSelfReferences(objectName, fields)`, is the one helper both the parse and the engine use.
15+
- **`@objectstack/objectql`.** The object registry writes the same self-reference when an object is registered. That covers an object saved through the metadata API (`PUT /meta/object/:name` stores the body as written) and a direct `registerObject` call, neither of which goes through the parse.
16+
17+
What changes on a parent delete:
18+
19+
- `set_null` (the default) clears the children's parent field and keeps the children.
20+
- `restrict` refuses the delete with `DELETE_RESTRICTED` (409), naming the tree's own object as the dependent.
21+
- `cascade` deletes the whole subtree.
22+
23+
`expand`, nested relation filters, the write-time check that a parent id exists, the dangling-reference audit and `os verify --delete-behaviors` now treat the field as a self-reference too. A `tree` that already wrote its own name, and every `lookup` and `master_detail`, behave exactly as before.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ const result = CurrencyConfigSchema.parse(data);
7676
| **maxSize** | `integer` | optional | Maximum permitted file size in BYTES for media fields. Enforced on write against the stored file size, not just checked in the browser. |
7777
| **options** | `{ label: string; value: string; description?: string; color?: string; … }[]` | optional | Static options for the option types. A `select` / `radio` field needs a non-empty list here or a `picklist` — with neither (or `options: []`) it is refused at parse. |
7878
| **picklist** | `string` | optional | Name of a shared `picklist` whose options this field offers — instead of `options`, never with it. Option types only (select, radio, multiselect, checkboxes, tags). A select / radio declares this or a non-empty `options`; with neither it is refused at parse. |
79-
| **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. 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. |
79+
| **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. 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. |
8080
| **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. |
8181
| **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. |
8282
| **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted |

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ const result = ApiMethod.parse(data);
241241
| **maxSize** | `integer` | optional | Maximum permitted file size in BYTES for media fields. Enforced on write against the stored file size, not just checked in the browser. |
242242
| **options** | `{ label: string; value: string; description?: string; color?: string; … }[]` | optional | Static options for the option types. A `select` / `radio` field needs a non-empty list here or a `picklist` — with neither (or `options: []`) it is refused at parse. |
243243
| **picklist** | `string` | optional | Name of a shared `picklist` whose options this field offers — instead of `options`, never with it. Option types only (select, radio, multiselect, checkboxes, tags). A select / radio declares this or a non-empty `options`; with neither it is refused at parse. |
244-
| **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. 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. |
244+
| **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. 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. |
245245
| **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. |
246246
| **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. |
247247
| **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted |
@@ -583,7 +583,7 @@ const result = ApiMethod.parse(data);
583583
| **maxSize** | `integer` | optional | Maximum permitted file size in BYTES for media fields. Enforced on write against the stored file size, not just checked in the browser. |
584584
| **options** | `{ label: string; value: string; description?: string; color?: string; … }[]` | optional | Static options for the option types. A `select` / `radio` field needs a non-empty list here or a `picklist` — with neither (or `options: []`) it is refused at parse. |
585585
| **picklist** | `string` | optional | Name of a shared `picklist` whose options this field offers — instead of `options`, never with it. Option types only (select, radio, multiselect, checkboxes, tags). A select / radio declares this or a non-empty `options`; with neither it is refused at parse. |
586-
| **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. 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. |
586+
| **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. 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. |
587587
| **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. |
588588
| **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. |
589589
| **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted |

‎content/docs/references/system/migration.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ Add a new field to an existing object
7777
| **maxSize** | `integer` | optional | Maximum permitted file size in BYTES for media fields. Enforced on write against the stored file size, not just checked in the browser. |
7878
| **options** | `{ label: string; value: string; description?: string; color?: string; … }[]` | optional | Static options for the option types. A `select` / `radio` field needs a non-empty list here or a `picklist` — with neither (or `options: []`) it is refused at parse. |
7979
| **picklist** | `string` | optional | Name of a shared `picklist` whose options this field offers — instead of `options`, never with it. Option types only (select, radio, multiselect, checkboxes, tags). A select / radio declares this or a non-empty `options`; with neither it is refused at parse. |
80-
| **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. 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. |
80+
| **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. 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. |
8181
| **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. |
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 |
@@ -504,7 +504,7 @@ Add a new field to an existing object
504504
| **maxSize** | `integer` | optional | Maximum permitted file size in BYTES for media fields. Enforced on write against the stored file size, not just checked in the browser. |
505505
| **options** | `{ label: string; value: string; description?: string; color?: string; … }[]` | optional | Static options for the option types. A `select` / `radio` field needs a non-empty list here or a `picklist` — with neither (or `options: []`) it is refused at parse. |
506506
| **picklist** | `string` | optional | Name of a shared `picklist` whose options this field offers — instead of `options`, never with it. Option types only (select, radio, multiselect, checkboxes, tags). A select / radio declares this or a non-empty `options`; with neither it is refused at parse. |
507-
| **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. 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. |
507+
| **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. 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. |
508508
| **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. |
509509
| **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. |
510510
| **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted |

0 commit comments

Comments
 (0)