Packages
ash_referential_actions
0.1.0
Explicit cascade, restrict, nilify, and do-nothing semantics for Ash relationships.
Current section
Files
Jump to
Current section
Files
ash_referential_actions
README.md
README.md
# AshReferentialActions
Explicit referential actions for Ash relationships.
## Why
`belongs_to`, `has_many`, and `has_one` describe cardinality but not lifecycle. A related record may be owned, may prevent deletion, may lose its foreign key, or may require no lifecycle behavior. AshReferentialActions makes that choice mandatory and derives soft-archive and optional PostgreSQL behavior from it.
## DSL
Declare the same action on both sides of an attributable relationship:
```elixir
# A comment is owned by its post
cascade_belongs_to :post, Post, allow_nil?: false
cascade_has_many :comments, Comment
# A product cannot disappear while an order item references it
restrict_belongs_to :product, Product, allow_nil?: false
restrict_has_many :order_items, OrderItem
# A task remains when its assignee disappears
nilify_belongs_to :assignee, User, allow_nil?: true
nilify_has_many :assigned_tasks, Task, destination_attribute: :assignee_id
# Normal relationship with no lifecycle behavior
do_nothing_has_many :published_posts, Post, filter: expr(published)
```
Resources using the extension cannot declare plain attributable `belongs_to`, `has_many`, or `has_one`. Use one of `cascade_*`, `restrict_*`, `nilify_*`, or `do_nothing_*`.
Plain reverse relationships to destination resources that do not use AshReferentialActions are exempt. This primarily supports relationships generated by extensions such as AshPaperTrail.
Use normal Ash relationship options. `nilify_belongs_to` requires `allow_nil?: true`.
## Adapters
### Soft archive
```elixir
use Ash.Resource,
extensions: [AshReferentialActions.Archival]
```
The archival adapter also installs `AshArchival.Resource` and:
- generates `archive_related` from `cascade_has_many` and `cascade_has_one`
- rejects an archive while a live `restrict_has_many/has_one` record exists
- generates a private nilify update action and clears live `nilify_has_many/has_one` foreign keys
- rejects new cascade/restrict/nilify references to archived targets
- validates cascade destinations and ordering
### PostgreSQL physical delete
```elixir
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
extensions: [AshReferentialActions.Postgres]
```
The PostgreSQL adapter generates migration reference behavior:
- cascade -> `on_delete: :delete`
- restrict -> `on_delete: :restrict`
- nilify -> `on_delete: :nilify`
Do not enable this adapter merely because an application uses PostgreSQL. Applications that only soft-archive records should normally use the archival adapter alone and retain restrictive database foreign keys as a safety net.
The adapters can be combined when both soft archive and physical delete must share the same semantics.
## Generated nilify action
For:
```elixir
nilify_belongs_to :assignee, User, allow_nil?: true
```
AshReferentialActions generates a private update action named from the source attribute, for example:
```elixir
:__ash_referential_actions_nilify_assignee_id__
```
The action accepts no input and only sets `assignee_id` to nil. The target's archival change invokes it through the matching reverse relationship with Ash's `cascade_update` change.
## Guarantees
Compile-time verification rejects:
- unmarked attributable relationships
- missing or mismatched forward/reverse actions
- nilify relationships whose foreign key is non-nullable
- filtered or manual lifecycle reverse relationships
- lifecycle targets that do not use AshReferentialActions
- invalid cascade destinations or cascade order
- PostgreSQL reference behavior that conflicts with the declared action