Deprecated: This project has been superseded by ash_referential_actions, which declares cascade, restrict, nilify, and view semantics directly on relationships. New code should use
AshReferentialActions.Archival.
Automatically sets archive_related from ash_archival for all fully-contained child relationships (has_many and has_one).
Add ash_cascade_archival to your list of dependencies in mix.exs:
def deps do
[
{:ash_cascade_archival, "~> 0.6.0"}
]
endSimply add AshCascadeArchival to your resource's extensions:
defmodule MyApp.Post do
use Ash.Resource,
extensions: [AshCascadeArchival.Resource]
attributes do
uuid_primary_key :id
attribute :title, :string
end
relationships do
belongs_to :author, MyApp.Author
has_many :comments, MyApp.Comment
has_many :post_tags, MyApp.PostTag
many_to_many :tags, MyApp.Tag, through: MyApp.PostTag
end
actions do
defaults [:read, :destroy, create: :*, update: :*]
end
endFor the example above, archive_related is automatically set to:
archive do
archive_related [:comments, :post_tags]
endAshCascadeArchival automatically identifies fully-contained child relationships and adds them to archive_related. A relationship is considered fully-contained when:
- has_one:
no_attributes?: false,manual: nil,filters: [] - has_many:
no_attributes?: false,manual: nil,filters: []
Note: many_to_many relationships are excluded because archive_related would target the destination resource, not the through resource. Instead, define a has_many relationship to the through resource (e.g., has_many :post_tags, PostTag) to archive it.
Use the except option to exclude specific relationships:
defmodule MyApp.Post do
use Ash.Resource,
extensions: [AshCascadeArchival.Resource]
cascade_archive do
except [:post_tags]
end
relationships do
has_many :comments, MyApp.Comment
has_many :post_tags, MyApp.PostTag
end
endResult:
archive do
archive_related [:comments] # post_tags excluded
endUse the only option to include a specific set of relationships:
defmodule MyApp.Post do
use Ash.Resource,
extensions: [AshCascadeArchival.Resource]
cascade_archive do
only [:comments]
end
relationships do
has_many :comments, MyApp.Comment
has_many :post_tags, MyApp.PostTag
end
endResult:
archive do
archive_related [:comments] # post_tags not included
endonly and except cannot be used together.
When neither option is set, all fully-contained child relationships are archived.
Use only [] to archive no relationships.
archive_related is executed sequentially, so its order is the cascade
execution order. Since 0.6.0 the list is sorted alphabetically by default
(instead of following declaration order), so reordering relationships in the
source file can never silently change cascade behavior.
When the order matters — e.g. an ash_ownership
locked_by destination must be archived after everything that locks it — pin the
tail with archive_last:
cascade_archive do
archive_last [:post_tags, :comments] # ...everything else..., post_tags, comments
endOnly the tail needs stating. Independent children can be archived at any point, so the constraint is always "these go last, in this order" — never "this goes first". Named relationships are removed from the alphabetical base and appended in the order given, which is enough to express any order the cascade needs.
AshCascadeArchival.Verifier.LockOrder checks the result against the actual
locked_by edges and, when it fails, reports a ready-to-paste archive_last.
Cascading invokes each archive_related destination's primary destroy
action, so that action decides what really happens: a soft destroy runs as
an update (archiving when its changes set the archive attribute, as
ash_archival's do), a non-soft destroy hard-deletes, and a missing
destroy crashes at runtime. Since 0.6.0 the verifier classifies by that
actual action (not by extension presence, which exclude_destroy_actions
and custom soft destroys can contradict): a destination without a primary
destroy action, with a non-soft one, or with a no-op soft one (no changes
and no manual implementation) is rejected — make its destroys actually
archive, exclude the relationship with except, or opt into hard deletion
explicitly with hard_delete. The no-op check is a heuristic: any change or
manual implementation counts as evidence of archiving; the verifier cannot
prove your custom soft destroy really archives.
Note: Spark surfaces verifier errors as compiler warnings — the module still compiles. Treat warnings as errors in CI (
mix compile --warnings-as-errors) to make these checks enforcing.
Some children are worthless without their parent and need no soft-delete history (derived caches, computation results). Declare that the cascade really deletes them:
cascade_archive do
hard_delete [:derived_caches]
endhard_delete destinations must have a non-soft primary destroy action: a
soft one would archive (making the declaration misleading), and a missing
one would crash the cascade at runtime — both are verified at compile time.
Relationships marked by ash_ownership
are recognized on all three sides: locked_by is never included in
archive_related (it points at locking records, not contained children), a locks
edge is exempt from the reverse-relationship requirement (it is a
non-owning reference, not a containment chain), and a refers edge is
too (the real parent is a different relationship, and the cascade reaches
the record through it).
A relationship carrying a __cascade_skip__ map key is never treated as a
fully-contained child. Extensions that generate several relationships over
the same rows — differing only in a filter or a read-action argument — use
it to nominate one of them as the cascade path, so the parent does not walk
the same children once per view:
%{relationship | name: :visible_customers}
|> Map.put(:__cascade_skip__, true)The marker is a plain map key, so declaring it costs no dependency in either direction.
AshCascadeArchival verifies that parent resources with AshArchival have proper reverse relationships. If a child has a belongs_to to an archival parent, the parent must have a corresponding fully-contained relationship back to the child.
Example error:
AshArchival requires has_many or has_one to pair with belongs_to.
Parent MyApp.Author must have one of the following:
has_many :posts, MyApp.Post
has_one :post, MyApp.Post
- Transformer: Finds all fully-contained child relationships, sets
archive_related, and orders it (alphabetical base +archive_lasttail) - Verifiers: Ensure bidirectional relationships are properly configured for archival, and that every
archive_relateddestination is itself archival
archive_relatedis now sorted alphabetically instead of following relationship declaration order. If your code depended on the previous implicit order, declare it explicitly with thearchive_lastoption.- A relationship in
archive_relatedwhose destination has no primary destroy action, or a non-soft one, is now a compile error (previously the cascade silently hard-deleted or crashed on such destinations). Make the destination's destroys soft, useexcept, or declare the intent withhard_delete.
By default, AshCascadeArchival logs the configured archive_related relationships during compilation. You can disable this in your config:
# config/config.exs
config :ash_cascade_archival, :log, falseDefault: true (logging enabled)
You cannot use both cascade_archive and manually set archive_related. If you need to include or exclude specific relationships, use the only or except option in cascade_archive:
cascade_archive do
only [:relationship_name]
endcascade_archive do
except [:relationship_name]
endMIT