Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .changeset/28001-cascade-delete-objects-beside-item.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
---
'@objectstack/spec': minor
'@objectstack/objectql': patch
Expand All @@ -11,6 +11,6 @@
Metadata-protocol: added
Skills-docs: none — `cascadeDeleteObjects` is a served-only key beside the object read with no authorable spelling; no file under `skills/**` teaches the object read's envelope keys (a `git grep` of `skills/` for `cascadeDeleteObjects` and `disabledActions` names nothing), and every taught `deleteBehavior` use stays true.

- **Served beside the object** (`@objectstack/metadata-protocol`, `@objectstack/rest`). `GET /api/v1/meta/object/:name` serves `cascadeDeleteObjects` beside `item`: the names of the objects that deleting a record of this object can also delete. That is every object whose relation into it cascades (a `master_detail` by default, a `lookup` or `tree` that declares `deleteBehavior: 'cascade'`), and the objects that cascade into those in turn, each once. A `restrict` or `set_null` relation removes nothing and is not named. The default cached door carries it too, and the list is folded into that door's ETag, so an object registered later that cascades into this one changes the ETag. The key is absent where no engine answered, on a draft read, on the list read and on every type other than `object`; `[]` means nothing cascades. `item` is unchanged. A delete confirmation can now name what else a delete removes without keeping its own copy of the rule.
- **Served beside the object** (`@objectstack/metadata-protocol`, `@objectstack/rest`). `GET /api/v1/meta/object/:name` serves `cascadeDeleteObjects` beside `item`: the names of the objects that deleting a record of this object can also delete. That is every object whose relation into it cascades (a `master_detail` by default, a `lookup`, `tree` or `user` field that declares `deleteBehavior: 'cascade'`), and the objects that cascade into those in turn, each once. A `restrict` or `set_null` relation removes nothing and is not named. The default cached door carries it too, and the list is folded into that door's ETag, so an object registered later that cascades into this one changes the ETag. The key is absent where no engine answered, on a draft read, on the list read and on every type other than `object`; `[]` means nothing cascades. `item` is unchanged. A delete confirmation can now name what else a delete removes without keeping its own copy of the rule.
- **The fact it serves** (`@objectstack/objectql`). `ObjectQL.getCascadeDeleteObjects(object)` answers that list from the delete walk's own relation scan, which the delete and this read now share. It reads the registry and no row, so it never says which records or how many. It returns `undefined` for an object the registry does not hold.
- **The declaration** (`@objectstack/spec`). `GetMetaItemResponseSchema` and `GetMetaItemCachedResponseSchema` gain the optional `cascadeDeleteObjects` (an array of object names), and `IObjectQLEngine` gains the optional member `getCascadeDeleteObjects(objectName)`, which the metadata read calls through the kernel's service table. Nothing about it is authorable.
4 changes: 2 additions & 2 deletions .changeset/29253-user-field-delete-behavior.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
---
'@objectstack/objectql': minor
---
Expand All @@ -7,7 +7,7 @@
Clause-②: no (narrowing) — deleting a user that a `user` field declaring `restrict` (or `set_null` with `required: true`) holds is refused with `409 DELETE_RESTRICTED`, where it succeeded before; a declared `cascade` now deletes the rows holding the user, and a declared `set_null` now clears them. Nothing is widened.
Metadata-protocol: semantic

<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped, and no export or type changes: a `deleteBehavior` the spec already accepts on a `user` field is now enforced by the delete walk the way it is on a `lookup`. The refusal is the walk's own DELETE_RESTRICTED, so there is no stored value for a conversion to rewrite; an author who did not mean the declaration removes it or changes it. -->
<!-- adr-0087: registered user-field-delete-behavior-enforced -->

**BREAKING** for an app that declares `deleteBehavior` on a `user` field: the declared value now takes effect when the user is deleted.

Expand All @@ -17,6 +17,6 @@
- **Unchanged.** A `user` field that declares no `deleteBehavior` (the spec's parse adds none) stays out of the walk: deleting the user leaves the field as it was. `lookup`, `master_detail` and `tree` behave as before.
- **Which deletes.** Every user delete: better-auth deletes the user (admin remove-user, self delete-user) through the engine by id, and the generic data routes refuse a caller's own `sys_user` delete. A `restrict` refusal reaches the auth caller as `409 DELETE_RESTRICTED`, and the credential rows deleted before it are rolled back.

- **Rows stored before #9784.** Until then the spec's parse wrote `deleteBehavior: 'set_null'` on every field type, `user` included, so an object stored in that era can carry a `set_null` on a `user` field that no author wrote. That value now takes effect: the field is cleared when the user is deleted, and on a `required` field the delete is refused.
- **Rows stored before #9784.** Until then the spec's parse wrote `deleteBehavior: 'set_null'` on every field type, `user` included, so an object stored in that era can carry a `set_null` on a `user` field that no author wrote. That value now takes effect: the field is cleared when the user is deleted, and on a `required` field the delete is refused. No conversion rewrites it, because whether the value was meant is the author's to say: the semantic migration entry `user-field-delete-behavior-enforced` lists it for `objectstack migrate meta` and the upgrade guide. Save the object again without the key, or with the value you want.

**The fix.** If you declared `deleteBehavior` on a `user` field only as documentation, remove it or change it to what you want enforced. Nothing to do for a `user` field that declares no `deleteBehavior`.
14 changes: 14 additions & 0 deletions .changeset/29415-user-delete-behavior-entry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@objectstack/spec': patch
---

docs(spec): register the `user`-field `deleteBehavior` enforcement as a semantic migration entry, and stop the spec saying a `user` field never joins the delete walk (#29415)

Clause-②: no
Metadata-protocol: none

The enforcement itself shipped in `@objectstack/objectql` (#29253): a `deleteBehavior` declared on a `user` field now takes effect when the user is deleted. This change records it and corrects the spec text around it. Nothing an author can write, and nothing the runtime does, moves.

- **Migration entry.** A new semantic entry, `user-field-delete-behavior-enforced`, so `objectstack migrate meta` and the protocol upgrade guide reach the change. It names the one stored population that needs a look: objects saved while the parse still wrote `deleteBehavior: 'set_null'` on every field type can carry a `set_null` on a `user` field nobody wrote. On a field that is not `required` it clears the field when the user is deleted and is fine as it is; on a `required` field it now refuses the user delete with `409 DELETE_RESTRICTED`, so remove it or declare the outcome you want.
- **Spec text.** The `deleteBehavior` docblocks in `FieldSchema`, the `user`-field `reference` refusal message, the `user-field-foreign-reference-refused` entry, the `referentialFieldClear` and `referentialCascadeDelete` hook-context descriptions, the `getCascadeDeleteObjects` contract docblock and the `cascadeDeleteObjects` description on the metadata `object` read now say that a `user` field joins the delete walk when it declares a `deleteBehavior`, and stays out when it declares none.
- **Studio forms.** The field designer and the object form's field rows now show the `deleteBehavior` control on a `user` field, with all three outcomes, as they do on a `lookup`. A `user` field has no default, so the control writes a value only when you pick one.
Loading
Loading