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 94ea60b
Browse filesBrowse the repository at this point in the historyBrowse files
fix(lint)!: a `lookup` or `master_detail` field whose `reference` is a stored-metadata-body table is refused where it is declared
7
+
8
+
Clause-②: no (narrowing)
9
+
10
+
<!-- adr-0087: not-required (no-migration-prescription) No spec key, stored shape or authorable spelling is added, removed or renamed. The expression rule's object pass now refuses one field declaration it accepted before: a `lookup` or `master_detail` whose `reference` is `sys_metadata` or `sys_metadata_history`, which the engine never hydrates. Nothing stored is rewritten, so there is nothing for `objectstack migrate meta` to convert; the `@objectstack/spec` change is the text of the `reference` describe only. -->
11
+
12
+
**BREAKING** (an authoring narrowing), shipped as `minor` under the launch-window convention for breaking changes.
13
+
14
+
A `lookup` or `master_detail` whose `reference` names a table of the stored-metadata-body family (`STORED_METADATA_BODY_OBJECTS`: `sys_metadata`, `sys_metadata_history`) is an error. The engine never hydrates such a field: `$expand` keeps the stored id and never nests the row, a formula or a validation rule never reads through it, and a master-detail child's `parent` root stays unbound, so every write a `parent` rule judges is refused. `os validate`, `os build` and the object save door (`PUT /api/v1/meta/object/:name`, and a draft's promotion) run the same pass, under the existing `EXPRESSION_INVALID` rule, located at the field's `reference`.
15
+
16
+
- FROM: `meta: { type: 'lookup', reference: 'sys_metadata' }` (or a `master_detail`, or `sys_metadata_history`) saved with a 200 and built with exit 0, and then only ever showed an id. TO: refused, with `422 INVALID_METADATA` at the save door and an error at `os validate` / `os build`. The message names the field, its type and the target, and ends on the family's one prescription.
17
+
- A reference to any other table, platform tables such as `sys_user` included, is unchanged.
18
+
- A `user` field whose authored `reference` names a family table is refused the same way: the parse accepts an authored `reference` on a `user` field, and it is the target every reader resolves.
19
+
- The refusal of a formula or a validation rule that reads through such a field is unchanged, and is still reported at that expression.
20
+
- A stored object row that already declares such a field keeps loading: no authoring rule runs when a stored row is rehydrated, and the engine guards keep refusing every relationship read into the family. Its next active save, or a draft's promotion, through the metadata API is refused until the field is removed or retargeted. The engine guards stay, for every path that runs no authoring rule.
21
+
-`@objectstack/spec` (`patch`): the `reference` describe states the convention, so the generated field reference says it too.
22
+
23
+
What to do. Remove the field, or retarget it. To point a record at a metadata item, store the item's type and name in `text` fields and read the item through the metadata API (`GET /api/v1/meta/:type/:name`), where its body is projected.
Copy file name to clipboardExpand all lines: content/docs/data-modeling/field-types.mdx
+6-2Lines changed: 6 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -307,7 +307,7 @@ Reference to a record in another object (foreign key).
307
307
308
308
| Property | Type | Default | Description |
309
309
|:---|:---|:---|:---|
310
-
|`reference`|`string`|**required**| Target object name (snake_case) |
310
+
|`reference`|`string`|**required**| Target object name (snake_case). A stored-metadata table (`sys_metadata`, `sys_metadata_history`) is refused — see [below](#a-relationship-never-targets-a-stored-metadata-table)|
311
311
|`referenceFilters`|`string[]`| — |**Removed** (#2377, ADR-0049) — no longer a recognized field property. `FieldSchema` is a strict object, so an unknown key is **rejected with guidance**, not silently stripped (ADR-0078): the error echoes the offending key and prescribes the replacement. Use structured `lookupFilters` + `dependsOn` instead; see [Relationships](/docs/data-modeling/relationships)|
312
312
|`deleteBehavior`|`'restrict' \| 'cascade' \| 'set_null'`|`'set_null'`| Behavior when referenced record is deleted. On a *required* lookup `set_null` is escalated to `restrict`, since a NOT NULL foreign key cannot be cleared — **whether the `set_null` was defaulted or written out explicitly**. On a `multiple: true` required lookup the escalation is judged per referencing row: only a row the member removal would leave EMPTY is refused. `cascade` and `restrict` are the values honored as written. Where `set_null` does run, a `multiple: true` lookup loses only the deleted **member** — the other members are kept, and a set emptied that way is stored as `[]`, never `null`|
313
313
@@ -329,7 +329,7 @@ Parent-child relationship (cascading delete by default).
329
329
330
330
| Property | Type | Default | Description |
331
331
|:---|:---|:---|:---|
332
-
|`reference`|`string`|**required**| Target (master) object name |
332
+
|`reference`|`string`|**required**| Target (master) object name. A stored-metadata table is refused — see [below](#a-relationship-never-targets-a-stored-metadata-table)|
333
333
|`referenceFilters`|`string[]`| — |**Removed** (#2377, ADR-0049) — no longer a recognized field property. `FieldSchema` is a strict object, so an unknown key is **rejected with guidance**, not silently stripped (ADR-0078): the error echoes the offending key and prescribes the replacement. Use structured `lookupFilters` + `dependsOn` instead; see [Relationships](/docs/data-modeling/relationships)|
334
334
|`deleteBehavior`|`'restrict' \| 'cascade'`|`'cascade'`| Behavior when parent is deleted. `restrict` refuses the parent delete while children exist; `cascade` (and omitting the key) deletes the children with the parent. An explicit `set_null` is a **parse-time rejection** on this type (#9689) — a detail row cannot outlive its master; use a `lookup` if children must survive the parent |
335
335
|`inlineEdit`|`boolean \| 'grid' \| 'form'`| — | Edit child records inline on the parent create/edit form (`true` = auto-pick, `'grid'`, or `'form'`) |
@@ -347,6 +347,10 @@ Parent-child relationship (cascading delete by default).
347
347
}
348
348
```
349
349
350
+
#### A relationship never targets a stored-metadata table
351
+
352
+
A `lookup` or `master_detail` whose `reference` is `sys_metadata` or `sys_metadata_history` is refused by `os validate`, `os build` and the object save door alike. Those rows carry serialized metadata bodies, which are served only through the metadata API (`GET /api/v1/meta/:type/:name`), where the body is projected; the engine never hydrates a reference into them, so `$expand` would keep the stored id and never nest the row, and a master-detail child's `parent` root would stay unbound, refusing every write a `parent` rule judges. To point a record at a metadata item, store its type and name in `text` fields and read the item through the metadata API.
Copy file name to clipboardExpand all lines: content/docs/references/data/field.mdx
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -76,7 +76,7 @@ const result = CurrencyConfigSchema.parse(data);
76
76
|**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. |
77
77
|**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. |
78
78
|**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, and writes the own name when it is left out. |
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. 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. |
80
80
|**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. |
81
81
|**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. |
82
82
|**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. |
Copy file name to clipboardExpand all lines: content/docs/references/data/object.mdx
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -242,7 +242,7 @@ const result = ApiMethod.parse(data);
242
242
|**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. |
243
243
|**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. |
244
244
|**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. |
245
-
|**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. |
245
+
|**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. |
246
246
|**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. |
247
247
|**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. |
248
248
|**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. |
@@ -598,7 +598,7 @@ const result = ApiMethod.parse(data);
598
598
|**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. |
599
599
|**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. |
600
600
|**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. |
601
-
|**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. |
601
+
|**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. |
602
602
|**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. |
603
603
|**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. |
604
604
|**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. |
0 commit comments