Packages

ash

3.27.5
3.29.3 3.29.2 3.29.1 3.29.0 3.28.0 3.27.8 3.27.7 3.27.6 3.27.5 3.27.4 3.27.3 3.27.2 3.27.1 3.27.0 3.26.0 3.25.2 3.25.1 3.25.0 retired 3.24.7 3.24.6 3.24.5 3.24.4 3.24.3 3.24.2 3.24.1 3.24.0 3.23.1 3.23.0 3.22.2 3.22.1 3.22.0 3.21.3 3.21.2 3.21.1 3.21.0 3.20.0 3.19.3 3.19.2 3.19.1 3.19.0 3.18.0 3.17.1 3.17.0 3.16.0 3.15.0 3.14.1 3.14.0 retired 3.13.2 3.13.1 3.13.0 3.12.0 3.11.3 3.11.2 3.11.1 3.11.0 3.10.1 3.10.0 3.9.0 3.8.0 3.7.6 3.7.5 3.7.4 3.7.3 3.7.2 3.7.1 3.7.0 retired 3.6.3 retired 3.6.2 3.6.1 3.6.0 3.5.43 3.5.42 3.5.41 3.5.40 3.5.39 3.5.38 3.5.37 3.5.36 3.5.35 3.5.34 3.5.33 3.5.32 3.5.31 3.5.30 3.5.29 3.5.28 3.5.27 3.5.26 3.5.25 3.5.24 3.5.23 3.5.22 3.5.21 3.5.20 3.5.19 3.5.18 3.5.17 3.5.16 3.5.15 3.5.14 3.5.13 3.5.12 3.5.11 3.5.10 3.5.9 3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.74 retired 3.4.73 3.4.72 3.4.71 3.4.70 3.4.69 3.4.68 3.4.67 3.4.66 3.4.65 3.4.64 3.4.63 3.4.62 3.4.61 3.4.60 3.4.59 3.4.58 3.4.57 3.4.56 3.4.55 3.4.54 3.4.53 3.4.52 3.4.51 3.4.50 3.4.49 3.4.48 3.4.47 3.4.46 3.4.45 3.4.44 3.4.43 3.4.42 3.4.41 3.4.40 3.4.39 3.4.38 3.4.37 3.4.36 3.4.35 3.4.34 3.4.33 3.4.32 3.4.31 3.4.30 3.4.29 3.4.28 3.4.27 3.4.26 3.4.25 3.4.24 3.4.23 3.4.22 3.4.21 3.4.20 3.4.19 3.4.18 3.4.17 3.4.16 3.4.15 3.4.14 3.4.13 3.4.12 3.4.11 3.4.10 3.4.9 3.4.8 3.4.7 3.4.6 3.4.5 3.4.4 3.4.3 3.4.2 3.4.1 3.4.0 3.3.3 3.3.2 3.3.1 3.3.0 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 3.2.0 3.1.8 3.1.7 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.16 3.0.15 3.0.14 3.0.13 3.0.12 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.4 3.0.3 3.0.2 3.0.1 3.0.0 3.0.0-rc.46 3.0.0-rc.45 3.0.0-rc.44 3.0.0-rc.43 3.0.0-rc.42 3.0.0-rc.41 3.0.0-rc.40 3.0.0-rc.39 3.0.0-rc.38 3.0.0-rc.37 3.0.0-rc.36 3.0.0-rc.35 3.0.0-rc.34 3.0.0-rc.33 3.0.0-rc.32 3.0.0-rc.31 3.0.0-rc.29 3.0.0-rc.28 3.0.0-rc.27 3.0.0-rc.26 3.0.0-rc.25 3.0.0-rc.24 3.0.0-rc.23 3.0.0-rc.22 3.0.0-rc.21 3.0.0-rc.20 3.0.0-rc.19 3.0.0-rc.18 3.0.0-rc.17 3.0.0-rc.16 3.0.0-rc.15 3.0.0-rc.14 3.0.0-rc.13 3.0.0-rc.12 3.0.0-rc.11 3.0.0-rc.10 3.0.0-rc.9 3.0.0-rc.8 3.0.0-rc.7 3.0.0-rc.6 3.0.0-rc.5 3.0.0-rc.4 3.0.0-rc.3 3.0.0-rc.1 3.0.0-rc.0 2.21.15 2.21.14 2.21.13 2.21.12 2.21.11 2.21.10 2.21.9 2.21.8 2.21.7 2.21.6 2.21.5 2.21.4 2.21.3 2.21.2 2.21.1 2.21.0 2.20.3 2.20.2 2.20.1 2.20.0 2.19.14 2.19.13 2.19.12 2.19.11 2.19.10 2.19.9 2.19.8 2.19.7 2.19.6 2.19.5 2.19.4 2.19.3 retired 2.19.2 retired 2.19.1 retired 2.19.0 retired 2.18.2 2.18.1 2.18.0 2.17.24 2.17.23 2.17.22 2.17.21 2.17.20 2.17.19 2.17.18 2.17.17 2.17.16 2.17.15 2.17.14 2.17.13 2.17.12 2.17.11 2.17.10 2.17.9 2.17.8 2.17.7 2.17.6 2.17.5 2.17.4 2.17.3 2.17.2 2.17.1 2.17.0 2.16.1 2.16.0 2.15.20 2.15.19 2.15.18 2.15.17 2.15.16 2.15.15 2.15.14 2.15.13 2.15.12 2.15.11 2.15.10 2.15.9 2.15.8 2.15.7 2.15.6 2.15.5 2.15.4 2.15.2 2.15.1 2.15.0 2.14.21 2.14.20 2.14.19 2.14.18 2.14.17 2.14.16 2.14.15 2.14.14 2.14.13 2.14.12 2.14.11 2.14.10 2.14.9 2.14.8 2.14.7 2.14.6 2.14.5 2.14.4 2.14.3 2.14.2 2.14.1 2.14.0 2.13.4 retired 2.13.3 2.13.2 2.13.1 2.13.0 2.12.1 2.12.0 2.11.11 2.11.10 2.11.9 2.11.8 2.11.7 2.11.6 2.11.5 2.11.4 2.11.3 2.11.2 2.11.1 2.11.0 2.11.0-rc.3 2.11.0-rc.2 2.11.0-rc.1 2.11.0-rc.0 2.10.2 2.10.1 2.10.0 2.9.29 2.9.28 2.9.27 2.9.26 2.9.25 2.9.24 2.9.23 2.9.22 2.9.21 2.9.20 2.9.19 2.9.18 2.9.17 2.9.16 2.9.15 2.9.14 2.9.13 2.9.12 2.9.11 2.9.10 2.9.9 2.9.8 2.9.7 2.9.6 2.9.5 2.9.4 2.9.3 2.9.2 2.9.1 2.9.0 2.8.1 2.8.0 2.7.1 2.7.0 2.6.31 2.6.30 2.6.29 2.6.28 2.6.27 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 2.6.2 2.6.1 2.6.0 2.5.16 2.5.15 2.5.14 2.5.13 2.5.12 2.5.11 2.5.10 2.5.9 2.5.8 2.5.7 2.5.6 2.5.5 2.5.4 2.5.3 2.5.2 2.5.1 2.5.0 2.5.0-rc.6 2.5.0-rc.5 2.5.0-rc.4 2.5.0-rc.3 2.5.0-rc.2 2.5.0-rc.1 2.5.0-rc.0 2.4.30 2.4.29 2.4.28 2.4.27 2.4.26 2.4.25 2.4.24 2.4.23 2.4.22 2.4.21 2.4.20 2.4.19 2.4.18 2.4.17 2.4.16 2.4.15 2.4.14 2.4.13 2.4.12 2.4.11 2.4.10 2.4.9 2.4.8 2.4.7 2.4.6 2.4.5 2.4.4 2.4.3 2.4.2 2.4.1 2.4.0 2.3.0 2.2.0 2.1.0 2.0.0 2.0.0-rc.15 2.0.0-rc.14 2.0.0-rc.13 2.0.0-rc.12 2.0.0-rc.11 2.0.0-rc.10 2.0.0-rc.9 2.0.0-rc.8 2.0.0-rc.7 2.0.0-rc.6 2.0.0-rc.5 2.0.0-rc.4 2.0.0-rc.3 2.0.0-rc.2 2.0.0-rc.1 2.0.0-rc.0 2.0.0-pre.8 2.0.0-pre.7 2.0.0-pre.6 2.0.0-pre.5 2.0.0-pre.4 2.0.0-pre.3 2.0.0-pre.2 2.0.0-pre.1 2.0.0-pre.0 1.53.3 1.53.2 1.53.0 1.52.0-rc.22 1.52.0-rc.21 1.52.0-rc.20 1.52.0-rc.19 1.52.0-rc.18 1.52.0-rc.17 1.52.0-rc.16 1.52.0-rc.15 1.52.0-rc.14 1.52.0-rc.13 1.52.0-rc.12 1.52.0-rc.11 1.52.0-rc.10 1.52.0-rc.9 1.52.0-rc.8 1.52.0-rc.7 1.52.0-rc.6 1.52.0-rc.5 1.52.0-rc.4 1.52.0-rc.3 1.52.0-rc.2 1.52.0-rc.1 1.52.0-rc.0 1.51.2 1.51.1 retired 1.51.0 1.50.21 1.50.20 1.50.19 1.50.18 1.50.17 1.50.16 1.50.15 1.50.14 1.50.13 1.50.12 1.50.11 1.50.10 1.50.9 1.50.8 1.50.7 1.50.6 1.50.5 1.50.4 1.50.3 1.50.2 1.50.1 1.50.0 1.49.0 1.48.0-rc.30 1.48.0-rc.29 1.48.0-rc.28 1.48.0-rc.27 1.48.0-rc.26 1.48.0-rc.25 1.48.0-rc.24 1.48.0-rc.23 1.48.0-rc.22 1.48.0-rc.21 1.48.0-rc.20 1.48.0-rc.19 1.48.0-rc.18 1.48.0-rc.17 1.48.0-rc.16 1.48.0-rc.15 1.48.0-rc.14 1.48.0-rc.13 1.48.0-rc.12 1.48.0-rc.11 1.48.0-rc.10 1.48.0-rc.9 1.48.0-rc.8 1.48.0-rc.7 1.48.0-rc.6 1.48.0-rc.5 1.48.0-rc.4 1.48.0-rc.3 1.48.0-rc.2 1.48.0-rc.1 1.48.0-rc.0 1.47.12 1.47.11 1.47.10 1.47.9 1.47.8 1.47.7 1.47.6 1.47.5 1.47.4 1.47.3 1.47.2 1.47.1 1.47.0 1.46.13 1.46.12 1.46.11 1.46.10 1.46.9 1.46.8 1.46.7 1.46.6 1.46.5 1.46.4 1.46.3 1.46.2 1.46.1 1.46.0 1.45.0-rc9 1.45.0-rc8 1.45.0-rc7 1.45.0-rc6 1.45.0-rc5 1.45.0-rc4 1.45.0-rc3 1.45.0-rc20 1.45.0-rc2 1.45.0-rc19 1.45.0-rc18 1.45.0-rc17 1.45.0-rc16 1.45.0-rc15 1.45.0-rc14 1.45.0-rc13 1.45.0-rc12 1.45.0-rc11 1.45.0-rc10 1.45.0-rc1 1.45.0-rc0 1.44.13 1.44.12 1.44.11 1.44.10 1.44.9 1.44.8 1.44.7 1.44.6 1.44.5 1.44.4 1.44.3 1.44.2 1.44.1 1.44.0 1.43.12 1.43.11 1.43.10 1.43.9 1.43.8 1.43.7 1.43.6 1.43.5 1.43.4 1.43.3 1.43.2 1.43.1 1.43.0 1.42.0 1.41.12 1.41.11 1.41.10 1.41.9 1.41.8 1.41.7 1.41.6 1.41.5 1.41.4 1.41.3 1.41.2 1.41.1 1.41.0 1.40.0 1.39.7 1.39.6 1.39.5 1.39.4 1.39.3 1.39.2 1.39.1 1.39.0 1.38.0 1.37.2 1.37.1 1.37.0 1.36.22 1.36.21 1.36.19 1.36.18 1.36.17 1.36.16 1.36.15 1.36.14 1.36.13 1.36.12 1.36.11 1.36.10 1.36.9 1.36.8 1.36.7 1.36.6 1.36.5 1.36.4 1.36.3 1.36.2 1.36.0 1.35.1 1.35.0 1.34.9 1.34.8 1.34.7 1.34.6 1.34.5 1.34.4 1.34.3 1.34.2 1.34.1 1.34.0 1.33.0 1.32.2 1.32.1 1.32.0 1.31.1 1.31.0 1.30.2 1.30.1 1.29.0-rc1 1.29.0-rc0 1.28.1 1.28.0 1.27.1 1.27.0 1.26.13 1.26.12 1.26.11 1.26.10 1.26.9 1.26.8 1.26.7 1.26.6 1.26.5 1.26.4 1.26.2 1.26.1 1.26.0 1.25.8 1.25.7 1.25.6 1.25.5 1.25.4 1.25.3 1.25.2 1.25.1 1.25.0 1.24.2 1.24.1 1.24.0 1.23.3 1.23.2 1.23.1 1.23.0 1.22.1 1.22.0 1.20.1 1.20.0 1.19.1 1.19.0 1.18.1 1.18.0 1.17.1 1.17.0 1.16.2 1.15.1 1.15.0 1.14.0 1.13.4 1.13.3 1.13.2 1.13.1 1.13.0 1.12.0 1.11.1 1.11.0 1.10.0 1.9.0 1.8.0 1.7.0 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.1 1.5.0 1.4.1 1.4.0 1.3.1 1.3.0 1.2.1 1.2.0 1.1.3 1.1.2 1.1.0 1.0.3 1.0.2 1.0.1 1.0.0 0.13.1 0.13.0 0.12.0 0.10.0 0.9.1 0.9.0 0.8.0 0.7.0 0.6.5 0.6.4 0.6.3 0.6.2 0.6.1 0.6.0 0.5.2 0.5.1 0.5.0 0.4.0 0.3.0 0.2.0 0.1.9 0.1.8 0.1.3 0.1.1 0.1.0

A declarative, extensible framework for building Elixir applications.

Security advisory: This version has known vulnerabilities. View advisories

Current section

Files

Jump to
ash lib ash info manifest generator reachability.ex
Raw

lib/ash/info/manifest/generator/reachability.ex

# SPDX-FileCopyrightText: 2025 ash contributors <https://github.com/ash-project/ash/graphs/contributors>
#
# SPDX-License-Identifier: MIT
defmodule Ash.Info.Manifest.Generator.Reachability do
@moduledoc """
Discovers all reachable resources and standalone types by traversing the type graph
starting from a set of root resources.
Returns results in depth-first discovery order: dependencies appear before the
resources that reference them. This ordering is important for consumers that need
to declare types before they are referenced (e.g., Zod schema generation).
Handles cycle detection via a visited set to prevent infinite recursion.
"""
alias Ash.Info.Manifest.Generator.TypeResolver
@doc """
Find all resources and standalone types reachable from the given resource modules.
Accepts either:
- A list of resource modules (traverses all fields, relationships, and public action arguments)
- A list of `{resource_module, [action_name]}` tuples (only traverses arguments of specified actions)
## Visibility Options
* `:include_private_attributes?` - Traverse private attributes (default: `false`)
* `:include_private_calculations?` - Traverse private calculations (default: `false`)
* `:include_private_aggregates?` - Traverse private aggregates (default: `false`)
* `:include_private_relationships?` - Traverse private relationships (default: `false`)
* `:include_private_arguments?` - Traverse private action arguments (default: `false`)
* `:include_private_actions?` - Traverse private actions (default: `false`).
Private actions are skipped during reachability even when their names are
explicitly listed, so types referenced only from them don't end up in the
manifest.
Returns `{reachable_resources, standalone_types}` where both are lists of modules
in depth-first discovery order (dependencies before dependents).
"""
@spec find_reachable([atom() | {atom(), [atom()]}], keyword()) :: {[atom()], [atom()]}
def find_reachable(resource_entries, opts \\ []) do
{resources, types, _visited} =
Enum.reduce(resource_entries, {[], [], MapSet.new()}, fn entry,
{resources, types, visited} ->
{resource, action_names} = normalize_entry(entry)
if MapSet.member?(visited, resource) do
{resources, types, visited}
else
{found_resources, found_types, new_visited} =
traverse_resource(
resource,
MapSet.put(visited, resource),
action_names,
opts
)
{
resources ++ found_resources ++ [resource],
types ++ found_types,
new_visited
}
end
end)
{Enum.uniq(resources), Enum.uniq(types)}
end
defp normalize_entry({resource, action_names}) when is_atom(resource) and is_list(action_names),
do: {resource, action_names}
defp normalize_entry(resource) when is_atom(resource),
do: {resource, nil}
# action_names: nil means "traverse all public actions", list means "only these actions"
defp traverse_resource(resource, visited, action_names, opts) do
if is_resource?(resource) do
# Traverse fields (respecting visibility options)
fields = get_fields(resource, opts)
# Traverse relationships (respecting visibility options)
relationships = get_relationships(resource, opts)
# Walk fields
{field_resources, field_types, visited} =
Enum.reduce(fields, {[], [], visited}, fn field, {resources, types, visited} ->
{type, constraints} = get_field_type_and_constraints(field)
{found_r, found_t, new_visited} =
traverse_type(type, constraints, visited, opts)
{resources ++ found_r, types ++ found_t, new_visited}
end)
# Walk relationship destinations
{rel_resources, rel_types, visited} =
Enum.reduce(
relationships,
{field_resources, field_types, visited},
fn rel, {resources, types, visited} ->
destination = rel.destination
if MapSet.member?(visited, destination) do
{resources, types, visited}
else
new_visited = MapSet.put(visited, destination)
# Discovered resources only traverse fields/relationships, not action arguments
{found_r, found_t, newer_visited} =
traverse_resource(destination, new_visited, [], opts)
{
resources ++ found_r ++ [destination],
types ++ found_t,
newer_visited
}
end
end
)
# Walk action arguments
{arg_resources, arg_types, visited} =
traverse_action_arguments(
resource,
action_names,
rel_resources,
rel_types,
visited,
opts
)
{arg_resources, arg_types, visited}
else
{[], [], visited}
end
end
defp traverse_action_arguments(
resource,
action_names,
resources,
types,
visited,
opts
) do
actions =
resource
|> get_actions_to_traverse(action_names)
|> filter_private_actions(opts)
include_private_args? = Keyword.get(opts, :include_private_arguments?, false)
Enum.reduce(actions, {resources, types, visited}, fn action, {resources, types, visited} ->
args =
if include_private_args? do
action.arguments
else
Enum.filter(action.arguments, & &1.public?)
end
# Walk argument types
{resources, types, visited} =
Enum.reduce(args, {resources, types, visited}, fn arg, {resources, types, visited} ->
{type, constraints} = {arg.type, arg.constraints || []}
{found_r, found_t, new_visited} =
traverse_type(type, constraints, visited, opts)
{resources ++ found_r, types ++ found_t, new_visited}
end)
# Walk accepted-attribute types. Public attributes are already reached
# via the resource's field set, but private attributes named in `accept`
# become action inputs and their types must be registered too.
{resources, types, visited} =
traverse_action_accepted_attributes(resource, action, resources, types, visited, opts)
# Walk return type (generic actions declare a custom return type via :returns;
# CRUD actions return the resource itself, which is already reachable)
{resources, types, visited} =
traverse_action_returns(action, resources, types, visited, opts)
# Walk metadata field types (custom types in metadata fields need to be reachable)
traverse_action_metadata(action, resources, types, visited, opts)
end)
end
defp traverse_action_accepted_attributes(resource, action, resources, types, visited, opts) do
accept = Map.get(action, :accept) || []
Enum.reduce(accept, {resources, types, visited}, fn name, {resources, types, visited} ->
case Ash.Resource.Info.attribute(resource, name) do
nil ->
{resources, types, visited}
attribute ->
{found_r, found_t, new_visited} =
traverse_type(attribute.type, attribute.constraints || [], visited, opts)
{resources ++ found_r, types ++ found_t, new_visited}
end
end)
end
defp traverse_action_returns(action, resources, types, visited, opts) do
case Map.get(action, :returns) do
nil ->
{resources, types, visited}
return_type ->
return_constraints = Map.get(action, :constraints) || []
{found_r, found_t, new_visited} =
traverse_type(return_type, return_constraints, visited, opts)
{resources ++ found_r, types ++ found_t, new_visited}
end
end
defp traverse_action_metadata(action, resources, types, visited, opts) do
metadata = Map.get(action, :metadata) || []
Enum.reduce(metadata, {resources, types, visited}, fn meta, {resources, types, visited} ->
meta_type = Map.get(meta, :type)
meta_constraints = Map.get(meta, :constraints) || []
if meta_type do
{found_r, found_t, new_visited} =
traverse_type(meta_type, meta_constraints, visited, opts)
{resources ++ found_r, types ++ found_t, new_visited}
else
{resources, types, visited}
end
end)
end
defp get_actions_to_traverse(resource, nil) do
# nil means traverse all public actions
Ash.Resource.Info.actions(resource)
|> Enum.filter(&Map.get(&1, :public?, false))
end
defp get_actions_to_traverse(resource, action_names) when is_list(action_names) do
Enum.flat_map(action_names, fn name ->
case Ash.Resource.Info.action(resource, name) do
nil -> []
action -> [action]
end
end)
end
defp filter_private_actions(actions, opts) do
if Keyword.get(opts, :include_private_actions?, false) do
actions
else
Enum.filter(actions, &Map.get(&1, :public?, true))
end
end
defp traverse_type(type, constraints, visited, opts) when is_list(constraints) do
{unwrapped_type, unwrapped_constraints} = TypeResolver.unwrap_new_type(type, constraints)
# Detect named type modules: NewTypes (type differs after unwrap) or enums
is_named = is_atom(type) and TypeResolver.named_type_module?(type)
if is_named and MapSet.member?(visited, type) do
# Cycle detected — stop traversal
{[], [], visited}
else
visited = if is_named, do: MapSet.put(visited, type), else: visited
{found_r, found_t, visited} =
traverse_unwrapped_type(unwrapped_type, unwrapped_constraints, visited, opts)
if is_named do
{found_r, [type | found_t], visited}
else
{found_r, found_t, visited}
end
end
end
defp traverse_type(_type, _constraints, visited, _opts) do
{[], [], visited}
end
defp traverse_unwrapped_type(unwrapped_type, constraints, visited, opts) do
case unwrapped_type do
{:array, inner_type} ->
items_constraints = Keyword.get(constraints, :items, [])
traverse_type(inner_type, items_constraints, visited, opts)
Ash.Type.Struct ->
instance_of = Keyword.get(constraints, :instance_of)
if instance_of && is_resource?(instance_of) do
traverse_resource_ref(instance_of, visited, opts)
else
traverse_field_constraints(constraints, visited, opts)
end
Ash.Type.Union ->
union_types = Keyword.get(constraints, :types, [])
Enum.reduce(union_types, {[], [], visited}, fn {_name, config},
{resources, types, visited} ->
member_type = Keyword.get(config, :type)
member_constraints = Keyword.get(config, :constraints, [])
if member_type do
{found_r, found_t, new_visited} =
traverse_type(member_type, member_constraints, visited, opts)
{resources ++ found_r, types ++ found_t, new_visited}
else
{resources, types, visited}
end
end)
type when type in [Ash.Type.Map, Ash.Type.Keyword, Ash.Type.Tuple] ->
traverse_field_constraints(constraints, visited, opts)
type when is_atom(type) ->
cond do
is_resource?(type) ->
traverse_resource_ref(type, visited, opts)
Code.ensure_loaded?(type) == true ->
traverse_field_constraints(constraints, visited, opts)
true ->
{[], [], visited}
end
_ ->
{[], [], visited}
end
end
defp traverse_resource_ref(resource, visited, opts) do
if MapSet.member?(visited, resource) do
{[], [], visited}
else
new_visited = MapSet.put(visited, resource)
# Discovered resources only traverse fields/relationships, not action arguments
{found_r, found_t, newer_visited} =
traverse_resource(resource, new_visited, [], opts)
{found_r ++ [resource], found_t, newer_visited}
end
end
defp traverse_field_constraints(constraints, visited, opts) do
fields = Keyword.get(constraints, :fields)
if fields && is_list(fields) do
Enum.reduce(fields, {[], [], visited}, fn {_name, config}, {resources, types, visited} ->
field_type = Keyword.get(config, :type)
field_constraints = Keyword.get(config, :constraints, [])
if field_type do
{found_r, found_t, new_visited} =
traverse_type(field_type, field_constraints, visited, opts)
{resources ++ found_r, types ++ found_t, new_visited}
else
{resources, types, visited}
end
end)
else
{[], [], visited}
end
end
defp get_fields(resource, opts) do
include_private_attrs? = Keyword.get(opts, :include_private_attributes?, false)
include_private_calcs? = Keyword.get(opts, :include_private_calculations?, false)
include_private_aggs? = Keyword.get(opts, :include_private_aggregates?, false)
# If all three are the same (all public or all include-private), fetch in one call
if include_private_attrs? == include_private_calcs? and
include_private_calcs? == include_private_aggs? do
if include_private_attrs? do
Ash.Resource.Info.fields(resource, [:attributes, :aggregates, :calculations])
else
resource
|> Ash.Resource.Info.fields([:attributes, :aggregates, :calculations])
|> Enum.filter(& &1.public?)
end
else
# Mix-and-match: fetch each kind separately
attrs =
if include_private_attrs?,
do: Ash.Resource.Info.attributes(resource),
else: Ash.Resource.Info.public_attributes(resource)
calcs =
if include_private_calcs?,
do: Ash.Resource.Info.calculations(resource),
else: Ash.Resource.Info.public_calculations(resource)
aggs =
if include_private_aggs?,
do: Ash.Resource.Info.aggregates(resource),
else: Ash.Resource.Info.public_aggregates(resource)
Enum.concat([attrs, calcs, aggs])
end
end
defp get_relationships(resource, opts) do
if Keyword.get(opts, :include_private_relationships?, false) do
Ash.Resource.Info.relationships(resource)
else
Ash.Resource.Info.public_relationships(resource)
end
end
defp get_field_type_and_constraints(field) do
{Map.get(field, :type), Map.get(field, :constraints, []) || []}
end
defp is_resource?(module) when is_atom(module) do
Code.ensure_loaded?(module) == true and Ash.Resource.Info.resource?(module)
end
defp is_resource?(_), do: false
end