diff --git a/docs/generator-releases.md b/docs/generator-releases.md index 7e43adb..2489bb3 100644 --- a/docs/generator-releases.md +++ b/docs/generator-releases.md @@ -37,6 +37,7 @@ Newest first. An entry is **pending** until the kit release that pins it is publ **Spec authors** - **`aiAccess` is required (ERROR).** An `x-entity` without it fails `validate` and `generate` with `ENTITY_AIACCESS_MISSING`. To grant nothing, add `aiAccess: { operations: [], reason: "Tier 0 — no AI access" }`. `ENTITY_FIELDSETS_AI_ACCESS_MISSING` no longer exists. +- **A soft-delete parent's foreign keys stop cascading (clabonte/generator#3103).** They are `NoAction` from `0.24.0`, and a migration follows. Code that read ownership from `OnDelete` breaks without a validation error: read `belongsTo` instead (`API_Handbook.md`, "Never infer ownership from `OnDelete`"). - **Event headers must bind the envelope (ERROR).** A message that declares headers and binds no `eventId`, or no tenant or sub-tenant on a scoped event, fails with `ASYNC_ENVELOPE_HEADER_MISSING`. A global event whose headers require the tenant fails with `ASYNC_TENANT_HEADER_ON_GLOBAL_EVENT`. Mark the headers with `x-envelope`, and split a shared trait into a tenant-scoped trait and a tenant-optional one. Neither may layer on the other. - **Only `application/json` events (ERROR).** Any other resolved content type fails with `ASYNC_CONTENT_TYPE_UNSUPPORTED`. A message declaring none is unaffected. - **`outboxDialect: postgres` is removed.** It fails the load, and the message names the removal. Remove the key. diff --git a/handbooks/API_Handbook.md b/handbooks/API_Handbook.md index 8a99b93..092ba5c 100644 --- a/handbooks/API_Handbook.md +++ b/handbooks/API_Handbook.md @@ -1933,17 +1933,19 @@ x-entity: ## 9.5) Relationship classification: owning, association, opaque -`belongsTo` (§9.4) declares **composition** — the entity is a member of the target's aggregate, deletes cascade, and the parent owns the lifecycle. Not every foreign key means that. An entity frequently points at another entity it does not belong to, and the two cases generate very different code. +`belongsTo` (§9.4) declares **composition** — the entity is a member of the target's aggregate and the parent owns the lifecycle. It states ownership, not a delete behaviour. Not every foreign key means that. An entity frequently points at another entity it does not belong to, and the two cases generate very different code. Since implicit `{Entity}Id` → `belongsTo` inference was retired (kit `0.5.4`), a `format: uuid` property named after an entity carries **no relationship meaning on its own**. Every FK-shaped property is classified explicitly, exactly once: | Intent | Declaration | Delete behaviour | Aggregate membership | |---|---|---|---| -| Entity is part of the target's aggregate | `belongsTo: ` on `x-entity` | Cascade | Yes | +| Entity is part of the target's aggregate | `belongsTo: ` on `x-entity` | Follows the rule below | Yes | | Entity points at the target but is not owned | `x-references: ` on the property | NoAction | No | | Entity points at a target **another pillar owns**, with no local copy (from generator `0.15.0`) | `x-references: { entity: , resolve: remote }` on the property | n/a: plain uuid column, no FK | No | | The uuid is not a foreign key at all | `x-references: none` + justifying `description` | n/a | n/a | -| Owning FK under a name that is not `{Entity}Id` | `x-fk-for: ` on the property, bound to a declared `belongsTo` | Cascade | Yes | +| Owning FK under a name that is not `{Entity}Id` | `x-fk-for: ` on the property, bound to a declared `belongsTo` | Same as `belongsTo` | Yes | + +**Never infer ownership from `OnDelete`.** An owning foreign key is generated as `Cascade` only when the child has one `belongsTo` parent, the parent is not `delete: soft`, and the two entities do not belong to each other. Otherwise it is `NoAction`, so most owning foreign keys in a project with soft-delete roots are `NoAction`. Measured on generator `0.24.0`: a child of a soft-delete parent is `NoAction`, a child of a hard or undeclared parent is `Cascade`, and a child with two parents is `NoAction`. Through `0.23.0` a child of a soft-delete parent was `Cascade`. Code that walks ownership, such as a tenant check, must read `belongsTo` and the inverse collection it generates: code that read `OnDelete` stopped at its first hop after this change, and users got `403` on rows with no tenant column of their own. What a generated delete does to children is a separate question, answered by `x-entity.delete` ("Cascade Deletion"). Full extension reference: `Vendor_Extensions.md` §1.7 (`x-references`), §1.8 (`x-fk-for`), §1.9 (`x-expand-of` / `x-projection`). @@ -2916,7 +2918,7 @@ async function retryWithBackoff(operation, maxRetries = 3) { - **BilingualText**: Reference subfields in `searchableProperties` (e.g., `title.en`, `title.fr`). - **Parameter Naming**: Use `search` (not `q`) for free-text search, `filter` for OData filtering, `sort` (not `sortBy` or `sortOrder`) for sorting. - **Relationship classification**: Classify every `format: uuid` FK-shaped property exactly once — `belongsTo` for composition, `x-references: ` for a non-owning association, `x-references: { entity: , resolve: remote }` for a reference into another pillar with no replica (`Vendor_Extensions.md` §14.11), `x-references: none` (with a justifying description) for a uuid that is not a foreign key. Inference was retired; an unmarked property is an omission, not a default. See §9.5. -- **Legacy FK names**: When an owning FK cannot be named `{Entity}Id`, bind it with `x-fk-for: ` alongside the declared `belongsTo`, so composition and Cascade survive the naming exception. +- **Legacy FK names**: When an owning FK cannot be named `{Entity}Id`, bind it with `x-fk-for: ` alongside the declared `belongsTo`, so composition survives the naming exception. - **Projections**: Mark read-only embeds — `x-expand-of: ` for a scalar projection, `x-projection: true` for a non-owned collection. An unmarked projection embed is persisted as owned state. - **PATCH collections are complete sets**: a collection property present in an `Update{Resource}` body replaces the stored set — value-object arrays wholesale, child-entity arrays by identity reconcile (known `id` updates in place, no `id` creates, unknown `id` is `404`, omitted children are permanently deleted). Give `Update{Child}` an optional `id`, and say in the property description that omitted children are not recoverable. See §1.5.1. diff --git a/handbooks/Vendor_Extensions.md b/handbooks/Vendor_Extensions.md index 2d40ad4..0fc88b1 100644 --- a/handbooks/Vendor_Extensions.md +++ b/handbooks/Vendor_Extensions.md @@ -2215,7 +2215,7 @@ x-references: { entity: Customer, resolve: remote } # resolve: local | remote, **Semantics**: - **Association only.** The referencing entity is *not* a member of the target's aggregate. Contrast `belongsTo`, which declares composition and aggregate membership. -- **Delete behaviour is `NoAction`.** Deleting the target does not cascade to the referencing rows, because they are not owned. A composition FK (`belongsTo`) cascades; an association FK must not. +- **Delete behaviour is `NoAction`.** Deleting the target does not cascade to the referencing rows, because they are not owned. An association FK never cascades; whether a composition FK (`belongsTo`) does depends on its parent and is not a sign of ownership (`API_Handbook.md`, "Never infer ownership from `OnDelete`"). - **Nullability follows `required`.** The property is nullable unless listed in the schema's `required` array. There is no separate nullability marker. - Test-seed naming for the FK follows `TestSeed.Id`. @@ -2234,7 +2234,7 @@ The object form is only for a property. A `belongsTo` entry written as an object | Intent | Declaration | |---|---| -| This entity is part of the target's aggregate | `belongsTo: ` on `x-entity` (composition, Cascade, aggregate membership) | +| This entity is part of the target's aggregate | `belongsTo: ` on `x-entity` (composition, aggregate membership) | | This entity merely points at the target | `x-references: ` on the property (association, NoAction, no membership) | | The target is owned by another pillar and read through its API, with no local copy | `x-references: { entity: , resolve: remote }` on the property (plain uuid column; generator `0.15.0`, §14.11) | | The uuid is not a foreign key | `x-references: none` on the property + a justifying `description` | @@ -2306,7 +2306,7 @@ x-fk-for: Order # target entity name (PascalCase) **Semantics**: -- Composition is **preserved**: Cascade delete, aggregate membership, and every other consequence of `belongsTo` apply exactly as if the property had been named `orderId`. +- Composition is **preserved**: ownership, aggregate membership, delete behaviour and every other consequence of `belongsTo` apply exactly as if the property had been named `orderId`. - It is a *binding*, not a declaration. `x-fk-for` does not create a relationship; it names which declared `belongsTo` a misnamed column satisfies. - Binding **consumes** the `belongsTo`. Once `x-fk-for: ` has claimed it, a sibling property may carry `x-references: ` as a role association without tripping the mutual-exclusion rule — see §1.7 validation rule 2. @@ -2320,7 +2320,7 @@ x-fk-for: Order # target entity name (PascalCase) ```yaml # The column is `parentTicketRef` for historical reasons, but it is the owning -# FK to Ticket and must keep Cascade semantics. +# FK to Ticket and must stay part of its aggregate. TicketComment: type: object x-entity: