Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ The quick lookup surface: one line and a minimal snippet per construct. For rule
| [`hierarchy` / `leafOnly`](/spec/entities#hierarchy-leafonly-tree-entities) | tree entities, leaf-only references |
| [calculated fields](/spec/entities#calculated-fields) | server + UI-evaluated expressions, date helpers, call-outs |
| [`relations` / `composition`](/spec/relations#relations) | associations and master-detail compositions |
| [`relations` / `whenMasterDeleted`](/spec/relations#deleting-the-master-whenmasterdeleted) | whether deleting a master deletes the composition children it owns, or is refused while they exist |
| [`uses`](/spec/relations#multi-model-applications) | reuse entities owned by another intent model |
| [`related`](/spec/relations#related) | a read-only register of the records referencing this entity, on its own page |
| [`processes`](/spec/processes#processes) | workflows: user tasks, decisions, waits, boundary timers |
Expand Down
23 changes: 23 additions & 0 deletions docs/spec/relations.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,29 @@ Relation kinds: `oneToMany`, `manyToOne`, `oneToOne`, `manyToMany`. The foreign

Composition is **opt-in** — most required FKs are plain associations, and composition is explicit.

### Deleting the master — `whenMasterDeleted`

Composition is ownership: the child is a detail of its master, with a NOT NULL key because it cannot exist without it. So a delete of the master cannot leave those rows behind — an orphan is invisible in the UI (a detail is reached through its master's page, and the master is gone) while every report, roll-up and aggregate over the child keeps counting it. `whenMasterDeleted` chooses between the two sane outcomes, on the child that declares the composition:

```yaml
- name: SalesOrderItem
relations:
# deleted with the order — the default, so the key may be omitted
- { name: order, kind: manyToOne, to: SalesOrder, composition: true, whenMasterDeleted: cascade }

- name: SalesOrderCopy
relations:
# the order cannot be deleted while a copy of it exists
- { name: order, kind: manyToOne, to: SalesOrder, composition: true, whenMasterDeleted: refuse }
```

- **`cascade`** (the default) — the children go with the master, and each child's deletion is a deletion in full: its own composition children with it (a chain of any depth unwinds), its deletion observed by the reactions bound to it, so aggregates and roll-ups over the child relinquish what they counted. The master's deletion and the children's are one atomic unit.
- **`refuse`** — the master's delete is rejected while any child of that relation exists, naming both entities. Nothing is deleted; the master becomes deletable once the children have been removed deliberately.

::: info Normative
A conforming generator must, for every composition, either delete the children with the master or reject the master's deletion — the default is `cascade`, whether or not the key is authored, because it is what composition means. Both outcomes must hold for **every** writer that can delete the master, not only for a delete arriving over a generated interface: a reaction, a scheduled write and a cascade from a further master reach the same rows. `whenMasterDeleted` is valid only on a `composition: true` to-one, and only on the entity's first composition (its composition parent); anywhere else, and for any value other than `cascade` or `refuse`, it is an error.
:::

### Relation attributes

```yaml
Expand Down