AshDispatch.Helpers.ResourceIntrospection (AshDispatch v0.5.1)
View SourceHelpers for introspecting Ash resources to derive configuration automatically.
These helpers enable zero-configuration setups by examining resource relationships and attributes to infer settings that would otherwise require explicit configuration.
Usage
alias AshDispatch.Helpers.ResourceIntrospection
# Find the user_id field on a resource
ResourceIntrospection.derive_user_id_path(MyApp.Orders.Order)
#=> [:user_id]
# Find all user relationships
ResourceIntrospection.find_user_relationships(MyApp.Tickets.Ticket)
#=> [%{name: :user, source_attribute: :user_id}, %{name: :assigned_to, source_attribute: :assigned_to_id}]Ambiguity Handling
When a resource has multiple relationships to the user module, these helpers
return nil and log a warning. In such cases, explicit configuration is required.
Summary
Functions
Builds a filter expression for user_id based on a path.
Derives the user_id_path by introspecting the resource's relationships.
Derives user_id_path with audience-aware disambiguation.
Extracts just the filter from an audience config.
Finds all belongs_to relationships that point to the configured user module.
Returns the relationship name for a relationship-based audience.
Checks if a resource has any relationship to the user module.
Checks if an audience is configured as an MFA (Module, Function, Args) tuple.
Determines if an audience is relationship-based or filter-based.
Parses an audience config list into relationship path and filter components.
Resolves the user_id_path for counter scoping.
Functions
Builds a filter expression for user_id based on a path.
Converts a path like [:user_id] or [:cart, :user_id] into an Ash-compatible
filter keyword list that can be used with Ash.Query.filter/2.
Examples
# Simple path
build_user_filter([:user_id], "user-123")
#=> [user_id: "user-123"]
# Nested path (through relationship)
build_user_filter([:cart, :user_id], "user-123")
#=> [cart: [user_id: "user-123"]]
# Deeply nested
build_user_filter([:order, :cart, :user_id], "user-123")
#=> [order: [cart: [user_id: "user-123"]]]
Derives the user_id_path by introspecting the resource's relationships.
Finds the belongs_to relationship that points to the configured user_module
and returns its source_attribute as a single-element list (e.g., [:user_id]).
Return Values
[:field_name]- Single unambiguous user relationship foundnil- No user relationship, multiple relationships (ambiguous), or error
Examples
# Single user relationship
derive_user_id_path(MyApp.Notifications.Notification)
#=> [:user_id]
# Multiple relationships (logs warning)
derive_user_id_path(MyApp.Tickets.Ticket) # has :user and :assigned_admin
#=> nil
# No user relationship
derive_user_id_path(MyApp.Settings.Config)
#=> nilConfiguration
Requires :user_module to be configured:
config :ash_dispatch,
user_module: MyApp.Accounts.User
Derives user_id_path with audience-aware disambiguation.
When a resource has multiple user relationships, this function can auto-select the correct one if the audience name matches a relationship name.
Examples
# Ticket has :user, :started_by, :resolved_by, :closed_by
derive_user_id_path(Ticket, :user)
#=> [:user_id] # Auto-picks :user because audience matches
derive_user_id_path(Ticket, :resolved_by)
#=> [:resolved_by_id] # Auto-picks :resolved_by
derive_user_id_path(Ticket, :admin)
#=> nil # No match, warns with guidanceParameters
resource- The Ash resource moduleaudience- The audience atom (e.g.,:user,:admin). Passnilfor legacy behavior.
Return Values
[:field_name]- Relationship found (single or matched by audience)nil- No relationship, ambiguous without match, or error
Extracts just the filter from an audience config.
Convenience function that returns only the filter portion, discarding the relationship path. Useful for checking if a user matches an audience.
Examples
extract_audience_filter([:user, {:admin, true}])
#=> [admin: true]
extract_audience_filter([:user])
#=> []
Finds all belongs_to relationships that point to the configured user module.
Returns a list of relationship structs with their metadata.
Examples
find_user_relationships(MyApp.Tickets.Ticket)
#=> [
# %{name: :user, source_attribute: :user_id, type: :belongs_to},
# %{name: :assigned_to, source_attribute: :assigned_to_id, type: :belongs_to}
# ]
find_user_relationships(MyApp.Settings.Config)
#=> []
Returns the relationship name for a relationship-based audience.
For bare atom audiences, the audience name IS the relationship name. For filter-based audiences, extracts the first atom from the config path.
Examples
get_audience_relationship(:user)
#=> :user
get_audience_relationship(:admin) # config: {:admin, [:user, {:admin, true}]}
#=> :user (follows :user relationship, then filters)
get_audience_relationship(:partner) # config: {:partner, [:partner]}
#=> :partner
Checks if a resource has any relationship to the user module.
Examples
has_user_relationship?(MyApp.Orders.Order)
#=> true
has_user_relationship?(MyApp.Settings.Config)
#=> false
Checks if an audience is configured as an MFA (Module, Function, Args) tuple.
MFA-based audiences handle their own user resolution via the configured function,
so they don't need user_id_path for scoping. This is used to suppress
"ambiguous user relationships" warnings for resources with multiple user
relationships when using MFA audiences.
Audience Config Pattern
MFA audiences are configured as 3-tuples in the audiences config:
audiences: [
:user, # Bare atom - relationship-based
admin: [:user, admin: true], # List - filter-based
company_members: {MyApp.AudienceResolver, :company_members, [:resource]} # MFA tuple
]Examples
is_mfa_audience?(:company_members)
#=> true (configured as MFA tuple)
is_mfa_audience?(:user)
#=> false (bare atom)
is_mfa_audience?(:admin)
#=> false (filter-based list)
is_mfa_audience?(:unknown)
#=> false (not in config)
Determines if an audience is relationship-based or filter-based.
This distinction is important for counter recipient resolution:
- Relationship-based (bare atom in config): Extract recipient from the record itself
- Filter-based (tuple in config): Query all users matching the filter
Audience Config Pattern
The audience configuration in config :ash_dispatch, :audiences uses this convention:
audiences: [
:user, # Bare atom = relationship-based
{:admin, [:user, {:admin, true}]}, # Tuple = filter-based
{:partner, [:partner]} # Tuple with relationship path
]Examples
is_relationship_audience?(:user)
#=> true (extract from record's :user relationship)
is_relationship_audience?(:admin)
#=> false (query all users where admin: true)
is_relationship_audience?(:custom)
#=> true (not in config, assume relationship-based for backward compat)Use Cases
For counters:
- Relationship-based audience → broadcast to record owner only
- Filter-based audience → broadcast to ALL matching users
For events:
- Both use the same resolution, but relationship-based extracts from context
Parses an audience config list into relationship path and filter components.
This is the unified parsing logic used by both counter and event resolution.
Examples
# New format with relationship path
parse_audience_config([:user, {:admin, true}])
#=> {[:user], [admin: true]}
# Relationship chain (no filter)
parse_audience_config([:user, :associated_seller])
#=> {[:user, :associated_seller], []}
# Legacy format (filter only)
parse_audience_config([{:admin, true}])
#=> {[], [admin: true]}
# Empty config
parse_audience_config([])
#=> {[], []}Return Value
Returns a tuple {relationship_path, filter} where:
relationship_pathis a list of atoms representing relationships to followfilteris a keyword list of filter conditions
Resolves the user_id_path for counter scoping.
This function consolidates the logic for determining how to scope counter queries:
- scope provided → Scope expression takes precedence (returns nil, scope handles it)
- explicit user_id_path → Use the configured path
- auto-derive → Introspect resource relationships
Note: authorize? is used for Ash policy authorization but does NOT affect user scoping.
A counter can have authorize?: false (bypass policies) while still having user-scoped data
via user_id_path.
Parameters
resource- The Ash resource moduleopts- Options keyword list with::scope- Explicit scope expression (Ash.Expr):user_id_path- Explicit path to user_id field:audience- Audience atom for relationship disambiguation
Examples
# User counter with explicit path (authorize?: false bypasses policies but still scopes to user)
resolve_user_id_path_for_scoping(RoomMember, user_id_path: [:user_id])
#=> [:user_id]
# Auto-derive from relationship
resolve_user_id_path_for_scoping(Order, audience: :user)
#=> [:user_id] # if Order has belongs_to :user