AshReferentialActions. Changes. EnsureTargetLive
(ash_referential_actions v0.1.1)
Copy Markdown
View Source
Runtime guard added to every create and update action of an
resource that declares restrict or nilify: writing the foreign key is rejected
unless the target exists and is live.
Without this, a user could be pointed at an already-archived (or, on data layers without foreign keys, missing) target — a ghost reference the destroy-side guard can never prevent, since it only fires on the target.
The check runs in two hooks:
before_action— catches direct attribute input early, before any work is done.after_action— re-checks the foreign keys that actually changed on the result record, catching values set afterbefore_action(notablymanage_relationship, which applies belongs_to keys in its own hooks). On transactional data layers the error rolls the write back; on non-transactional layers (e.g. ETS) the error is returned but the write is not undone — the same caveat as any after-action validation there.
Bulk batches without record hooks or managed relationships instead check
keys together in before_batch and after_batch, once per relationship,
domain and tenant. Batches with hooks retain the individual checks so keys
are checked at the original hook position. Global changes that only modify
attributes do not disable batching; global before_batch callbacks still
require individual checks because they run after this guard's before_batch.
Action after_batch callbacks also require individual checks so they cannot
run before a failing result guard.
Atomic updates that leave guarded keys alone do not run batch callbacks or
request result records for this guard.
The lookup takes a FOR SHARE lock on the target row where the data layer
supports one, so a concurrent archive of that target must wait: either it
commits first and this check sees the archived row, or this write commits
first and the archive's own guard sees this user. Without lock support
(e.g. ETS) the check stays a plain application-level read.
The target is looked up via the referential relationship's declared
read_action (or the referenced resource's primary read — a verifier rejects a
filtered primary read as the default), so action-level read filters cannot
silently hide a live target; archival's global filter still applies, so an
archived target counts as not live.