AshDispatch.Helpers.RecordReader (AshDispatch v0.5.0)

View Source

Defensive field-reading helpers for post-action notifier hooks.

Why this exists

Ash actions can use narrowed select clauses to return only a subset of attributes. The excluded attributes are populated as %Ash.NotLoaded{} sentinels rather than raising or returning nil.

Notifier hooks (counter broadcasts, locale extraction, dispatch context building) read fields off the action's resulting record to decide what to do next. A bare Map.get(record, field) returns the sentinel for narrowed fields, which then cascades into:

  • Ecto.Query.CastError when used in a query filter
  • ArgumentError "Actor field is not loaded" from Ash.Query.relates_to_actor_via checks
  • Protocol.UndefinedError{protocol: String.Chars} when interpolated into a topic string or log message
  • Truthy-check bugs: if Map.get(record, :foo) is true for a sentinel because it's a struct, not nil/false

The contract

Notifier side effects are best-effort — they must degrade silently when the record can't yield the field, not crash the request or spam the log. These helpers treat %Ash.NotLoaded{} as nil so callers' existing nil-checks make the right decision automatically.

Single debug log per call so production diagnostics are still available without polluting test output.

Summary

Functions

Returns true if the field is present on the record AND has been loaded (i.e. is not the %Ash.NotLoaded{} sentinel).

Like Map.get/2 but treats %Ash.NotLoaded{} as nil.

safe_get/2 with a default returned when the field is nil or %Ash.NotLoaded{}.

Functions

loaded?(record, field)

@spec loaded?(map(), atom() | String.t()) :: boolean()

Returns true if the field is present on the record AND has been loaded (i.e. is not the %Ash.NotLoaded{} sentinel).

safe_get(record, field)

@spec safe_get(map(), atom() | String.t()) :: term() | nil

Like Map.get/2 but treats %Ash.NotLoaded{} as nil.

Emits a single debug-level log line when the sentinel is observed, to aid diagnosing select-narrowed actions in production.

safe_get(record, field, default)

@spec safe_get(map(), atom() | String.t(), term()) :: term()

safe_get/2 with a default returned when the field is nil or %Ash.NotLoaded{}.