Packages
ash_graphql
1.7.2
1.9.4
1.9.3
1.9.2
1.9.1
1.9.0
1.8.5
1.8.4
1.8.3
1.8.2
1.8.1
1.8.0
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.0
1.5.1
1.5.0
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.4
1.3.3
1.3.2
1.3.1
1.3.0
1.2.1
1.2.0
1.1.1
1.1.0
1.0.1
1.0.0
1.0.0-rc.5
1.0.0-rc.4
retired
1.0.0-rc.3
1.0.0-rc.2
1.0.0-rc.1
1.0.0-rc.0
0.28.1
0.28.0
0.27.1
0.27.0
0.26.9
0.26.8
0.26.7
0.26.6
0.26.5
0.26.4
0.26.3
0.26.2
0.26.0
0.25.13
0.25.12
0.25.10
0.25.9
0.25.8
0.25.7
0.25.6
0.25.5
0.25.4
0.25.3
0.25.2
0.25.1
0.25.0
0.24.1
0.24.0
0.23.3
0.23.2
0.23.1
0.23.0
0.22.13
0.22.12
0.22.11
0.22.10
0.22.9
0.22.8
0.22.7
0.22.6
0.22.4
0.22.3
0.22.2
0.22.1
0.22.0
0.21.0
retired
0.20.5
0.20.4
0.20.3
0.20.2
0.20.1
0.20.0-rc.3
0.20.0-rc.2
0.20.0-rc.1
0.20.0-rc.0
0.19.0
0.18.0-rc0
0.17.5
0.17.5-rc0
0.17.4
0.17.2
0.17.1
0.17.0
0.16.28
0.16.27
0.16.26
0.16.25
0.16.24
0.16.23
0.16.22
0.16.21
0.16.20
0.16.18-rc5
0.16.18-rc4
0.16.18-rc3
0.16.18-rc2
0.16.18-rc1
0.16.18-rc0
0.16.17
0.16.16
0.16.15
0.16.14
0.16.13
0.16.12
0.16.11
0.16.10
0.16.9
0.16.8
0.16.7
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.10
0.15.9
0.15.8
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.1
0.14.0
0.13.1
0.13.0
0.12.5
0.12.4
0.12.3
0.12.1
0.12.0
0.10.0
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.0
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.0
0.4.0
0.3.2
0.3.1
0.3.0
0.2.1
0.2.0
0.1.3
0.1.2
The extension for building GraphQL APIs with Ash
Current section
Files
Jump to
Current section
Files
lib/ash_graphql.ex
defmodule AshGraphql do
@moduledoc """
AshGraphql is a GraphQL extension for the Ash framework.
For more information, see the [getting started guide](/documentation/tutorials/getting-started-with-graphql.md)
"""
@doc false
defmacro mutation(do: block) do
empty? = !match?({:__block__, _, []}, block)
quote bind_quoted: [empty?: empty?, block: Macro.escape(block)], location: :keep do
require Absinthe.Schema
if empty? ||
Enum.any?(
@ash_resources,
fn resource ->
!Enum.empty?(AshGraphql.Resource.Info.mutations(resource, @all_domains))
end
) do
Code.eval_quoted(
quote do
Absinthe.Schema.mutation do
unquote(block)
end
end,
[],
__ENV__
)
end
end
end
defmacro subscription(do: block) do
empty? = !match?({:__block__, _, []}, block)
quote bind_quoted: [empty?: empty?, block: Macro.escape(block)], location: :keep do
require Absinthe.Schema
if empty? ||
Enum.any?(
@ash_resources,
fn resource ->
!Enum.empty?(AshGraphql.Resource.Info.subscriptions(resource, @all_domains))
end
) do
Code.eval_quoted(
quote do
Absinthe.Schema.subscription do
unquote(block)
end
end,
[],
__ENV__
)
end
end
end
defmacro __using__(opts) do
auto_import_types =
if Keyword.get(opts, :auto_import_absinthe_types?, true) do
quote do
import_types(Absinthe.Type.Custom)
import_types(AshGraphql.Types.JSON)
import_types(AshGraphql.Types.JSONString)
end
end
quote bind_quoted: [
domains: opts[:domains],
domain: opts[:domain],
generate_sdl_file: opts[:generate_sdl_file],
auto_generate_sdl_file?: opts[:auto_generate_sdl_file?],
action_middleware: opts[:action_middleware] || [],
define_relay_types?: Keyword.get(opts, :define_relay_types?, true),
relay_ids?: Keyword.get(opts, :relay_ids?, false),
auto_import_types: Macro.escape(auto_import_types)
],
location: :keep,
generated: true do
require Ash.Domain.Info
import Absinthe.Schema,
except: [
mutation: 1,
subscription: 1
]
import AshGraphql,
only: [
mutation: 1,
subscription: 1
]
@after_compile AshGraphql.Codegen
domains =
domain
|> List.wrap()
|> Kernel.++(List.wrap(domains))
|> Enum.uniq()
domains =
domains
|> Enum.map(fn
{domain, registry} ->
IO.warn("""
It is no longer required to list the registry along with a domain when using `AshGraphql`
use AshGraphql, domains: [{My.App.Domain, My.App.Registry}]
Can now be stated simply as
use AshGraphql, domains: [My.App.Domain]
""")
domain
domain ->
domain
end)
|> Enum.map(fn domain -> {domain, Ash.Domain.Info.resources(domain), false} end)
|> Enum.reduce({[], []}, fn {domain, resources, first?}, {acc, seen_resources} ->
resources = Enum.reject(resources, &(&1 in seen_resources))
{[{domain, resources, first?} | acc], seen_resources ++ resources}
end)
|> elem(0)
|> Enum.reverse()
|> List.update_at(0, fn {domain, resources, _} -> {domain, resources, true} end)
@generate_sdl_file generate_sdl_file
@auto_generate_sdl_file? auto_generate_sdl_file?
@doc false
def generate_sdl_file do
@generate_sdl_file
end
@doc false
def auto_generate_sdl_file? do
@auto_generate_sdl_file?
end
@doc false
def ash_graphql_schema?, do: true
@ash_resources Enum.flat_map(domains, &elem(&1, 1))
ash_resources = @ash_resources
@all_domains Enum.map(domains, &elem(&1, 0))
Enum.each(ash_resources, &Code.ensure_compiled!/1)
schema = __MODULE__
schema_env = __ENV__
for resource <- ash_resources do
resource
|> AshGraphql.Resource.global_unions(Enum.map(domains, &elem(&1, 0)))
|> Enum.map(&elem(&1, 1))
|> Enum.map(fn attribute ->
if function_exported?(attribute.type, :graphql_type, 1) do
attribute.type.graphql_type(attribute.constraints)
end
end)
|> Enum.uniq()
|> Enum.each(fn type_name ->
# sobelow_skip ["DOS.BinToAtom"]
def unquote(:"resolve_gql_union_#{type_name}")(%Ash.Union{type: type}, _) do
# sobelow_skip ["DOS.BinToAtom"]
:"#{unquote(type_name)}_#{type}"
end
def unquote(:"resolve_gql_union_#{type_name}")(value, _) do
value.__union_type__
end
end)
end
for {domain, resources, first?} <- domains do
defmodule Module.concat(domain, AshTypes) do
@moduledoc false
alias Absinthe.{Blueprint, Phase, Pipeline}
def pipeline(pipeline) do
Pipeline.insert_before(
pipeline,
Absinthe.Phase.Schema.ApplyDeclaration,
__MODULE__
)
end
@dialyzer {:nowarn_function, {:run, 2}}
def run(blueprint, _opts) do
domain = unquote(domain)
action_middleware = unquote(action_middleware)
all_domains = unquote(Enum.map(domains, &elem(&1, 0)))
domain_queries =
AshGraphql.Domain.queries(
domain,
all_domains,
unquote(resources),
action_middleware,
unquote(schema),
unquote(relay_ids?)
)
relay_queries =
if unquote(first?) and unquote(define_relay_types?) and unquote(relay_ids?) do
domains_with_resources = unquote(Enum.map(domains, &{elem(&1, 0), elem(&1, 1)}))
AshGraphql.relay_queries(
domains_with_resources,
all_domains,
unquote(schema),
__ENV__
)
else
[]
end
blueprint_with_queries =
(relay_queries ++ domain_queries)
|> Enum.reduce(blueprint, fn query, blueprint ->
Absinthe.Blueprint.add_field(blueprint, "RootQueryType", query)
end)
blueprint_with_mutations =
domain
|> AshGraphql.Domain.mutations(
all_domains,
unquote(resources),
action_middleware,
unquote(schema),
unquote(relay_ids?)
)
|> Enum.reduce(blueprint_with_queries, fn mutation, blueprint ->
Absinthe.Blueprint.add_field(blueprint, "RootMutationType", mutation)
end)
blueprint_with_subscriptions =
domain
|> AshGraphql.Domain.subscriptions(
all_domains,
unquote(resources),
action_middleware,
unquote(schema)
)
|> Enum.reduce(blueprint_with_mutations, fn subscription, blueprint ->
Absinthe.Blueprint.add_field(blueprint, "RootSubscriptionType", subscription)
end)
managed_relationship_types =
AshGraphql.Resource.managed_relationship_definitions(
Process.get(:managed_relationship_requirements, []),
unquote(schema)
)
|> Enum.uniq_by(& &1.identifier)
|> Enum.reject(fn type ->
existing_types =
case blueprint_with_subscriptions do
%{schema_definitions: [%{type_definitions: type_definitions}]} ->
type_definitions
_ ->
[]
end
Enum.any?(existing_types, fn existing_type ->
existing_type.identifier == type.identifier
end)
end)
domains = unquote(Enum.map(domains, &elem(&1, 0)))
type_definitions =
if unquote(first?) do
embedded_types =
AshGraphql.get_embedded_types(
unquote(ash_resources),
domains,
unquote(schema),
unquote(relay_ids?)
)
global_maps =
AshGraphql.global_maps(
unquote(ash_resources),
domains,
unquote(schema),
__ENV__
)
global_enums =
AshGraphql.global_enums(
unquote(ash_resources),
domains,
unquote(schema),
__ENV__
)
global_unions =
AshGraphql.global_unions(
unquote(ash_resources),
domains,
unquote(schema),
__ENV__
)
Enum.uniq_by(
AshGraphql.Domain.global_type_definitions(unquote(schema), __ENV__) ++
AshGraphql.Domain.type_definitions(
domain,
domains,
unquote(resources),
unquote(schema),
__ENV__,
true,
unquote(define_relay_types?),
unquote(relay_ids?)
) ++
global_maps ++
global_enums ++
global_unions ++
embedded_types,
& &1.identifier
)
else
AshGraphql.Domain.type_definitions(
domain,
domains,
unquote(resources),
unquote(schema),
__ENV__,
false,
false,
unquote(relay_ids?)
)
end
new_defs =
List.update_at(blueprint_with_subscriptions.schema_definitions, 0, fn schema_def ->
%{
schema_def
| type_definitions:
schema_def.type_definitions ++
type_definitions ++ managed_relationship_types
}
end)
{:ok, %{blueprint_with_subscriptions | schema_definitions: new_defs}}
end
end
if first? do
Code.eval_quoted(auto_import_types, [], __ENV__)
end
@pipeline_modifier Module.concat(domain, AshTypes)
end
end
end
@doc false
def global_maps(resources, all_domains, schema, env) do
resources
|> Enum.flat_map(&AshGraphql.Resource.map_definitions(&1, all_domains, schema, env))
|> Enum.uniq_by(& &1.identifier)
end
@doc false
def global_enums(resources, all_domains, schema, env) do
resources
|> Enum.flat_map(&all_attributes_and_arguments(&1, all_domains))
|> only_enum_types()
|> Enum.uniq()
|> Enum.map(fn type ->
{name, identifier} =
case type do
Ash.Type.DurationName ->
{"DurationName", :duration_name}
type ->
graphql_type = type.graphql_type([])
{graphql_type |> to_string() |> Macro.camelize(), graphql_type}
end
%Absinthe.Blueprint.Schema.EnumTypeDefinition{
module: schema,
name: name,
values:
Enum.map(type.values(), fn value ->
name =
if function_exported?(type, :graphql_rename_value, 1) do
type.graphql_rename_value(value)
else
value
end
description =
if function_exported?(type, :graphql_describe_enum_value, 1) do
type.graphql_describe_enum_value(value)
else
enum_type_description(type, value)
end
%Absinthe.Blueprint.Schema.EnumValueDefinition{
module: schema,
identifier: value,
__reference__: AshGraphql.Resource.ref(env),
description: description,
name: String.upcase(to_string(name)),
value: value
}
end),
identifier: identifier,
__reference__: AshGraphql.Resource.ref(env)
}
end)
|> Enum.uniq_by(& &1.identifier)
end
defp enum_type_description(type, value) do
if Spark.implements_behaviour?(type, Ash.Type.Enum) do
type.description(value)
else
nil
end
end
@doc false
def global_unions(resources, all_domains, schema, env) do
resources
|> Enum.flat_map(fn resource ->
resource
|> AshGraphql.Resource.global_unions(all_domains)
|> Enum.flat_map(fn {type, attribute} ->
type_name = type.graphql_type(attribute.constraints)
input_type_name =
if function_exported?(type, :graphql_input_type, 1) do
type.graphql_input_type(attribute.constraints)
else
"#{type_name}_input"
end
AshGraphql.Resource.union_type_definitions(
resource,
attribute,
type_name,
schema,
env,
input_type_name
)
end)
end)
|> Enum.uniq_by(& &1.identifier)
end
@doc false
def all_attributes_and_arguments(
resource,
all_domains,
already_checked \\ [],
nested? \\ true,
return_new_checked? \\ false
) do
if resource in already_checked do
if return_new_checked? do
{[], already_checked}
else
[]
end
else
already_checked = [resource | already_checked]
resource
|> Ash.Resource.Info.public_attributes()
|> Enum.concat(all_arguments(resource, all_domains))
|> Enum.concat(Ash.Resource.Info.calculations(resource))
|> Enum.concat(
resource
|> Ash.Resource.Info.actions()
|> Enum.filter(&(&1.type == :action && &1.returns))
|> Enum.map(fn action ->
%{
type: action.returns,
constraints: action.constraints,
name: action.name,
from_generic_action?: true
}
end)
)
|> Enum.reduce({[], already_checked}, fn %{type: type} = attr, {acc, already_checked} ->
if nested? do
constraints = Map.get(attr, :constraints, [])
{nested, already_checked} =
nested_attrs(type, all_domains, constraints, already_checked)
{[attr | nested] ++ acc, already_checked}
else
{[attr | acc], already_checked}
end
end)
|> then(fn {attrs, checked} ->
attrs =
Enum.filter(attrs, fn attr ->
if Map.get(attr, :from_generic_action?) do
true
else
AshGraphql.Resource.Info.show_field?(resource, attr.name)
end
end)
if return_new_checked? do
{attrs, checked}
else
attrs
end
end)
end
end
@doc false
def relay_queries(domains_with_resources, all_domains, schema, env) do
type_to_domain_and_resource_map =
domains_with_resources
|> Enum.flat_map(fn {domain, resources} ->
resources
|> Enum.flat_map(fn resource ->
type = AshGraphql.Resource.Info.type(resource)
if type do
[{type, {domain, resource}}]
else
[]
end
end)
end)
|> Enum.into(%{})
[
%Absinthe.Blueprint.Schema.FieldDefinition{
name: "node",
identifier: :node,
arguments: [
%Absinthe.Blueprint.Schema.InputValueDefinition{
name: "id",
identifier: :id,
type: %Absinthe.Blueprint.TypeReference.NonNull{
of_type: :id
},
description: "The Node unique identifier",
__reference__: AshGraphql.Resource.ref(env)
}
],
middleware: [
{{AshGraphql.Graphql.Resolver, :resolve_node},
{type_to_domain_and_resource_map, all_domains}}
],
complexity: {AshGraphql.Graphql.Resolver, :query_complexity},
module: schema,
description: "Retrieves a Node from its global id",
type: %Absinthe.Blueprint.TypeReference.NonNull{of_type: :node},
__reference__: AshGraphql.Resource.ref(__ENV__)
}
]
end
defp nested_attrs({:array, type}, domain, constraints, already_checked) do
nested_attrs(type, domain, constraints[:items] || [], already_checked)
end
defp nested_attrs(Ash.Type.Union, domain, constraints, already_checked) do
Enum.reduce(
constraints[:types] || [],
{[], already_checked},
fn {_, config}, {attrs, already_checked} ->
case config[:type] do
{:array, type} ->
{new, already_checked} =
nested_attrs(type, domain, config[:constraints][:items] || [], already_checked)
{attrs ++ new, already_checked}
type ->
{new, already_checked} =
nested_attrs(type, domain, config[:constraints] || [], already_checked)
{attrs ++ new, already_checked}
end
end
)
end
defp nested_attrs(type, all_domains, constraints, already_checked) do
cond do
AshGraphql.Resource.embedded?(type) ->
type
|> unwrap_type()
|> all_attributes_and_arguments(all_domains, already_checked, true, true)
Ash.Type.NewType.new_type?(type) ->
constraints = Ash.Type.NewType.constraints(type, constraints)
type = Ash.Type.NewType.subtype_of(type)
nested_attrs(type, all_domains, constraints, already_checked)
true ->
{[], already_checked}
end
end
@doc false
def get_embed(type) do
if Ash.Type.NewType.new_type?(type) do
Ash.Type.NewType.subtype_of(type)
else
type
end
end
@doc """
Use this to load any requested fields for a result when it is returned
from a custom resolver or mutation.
## Determining required fields
If you have a custom query/mutation that returns the record at a "path" in
the response, then specify the path. In the example below, `path` would be
`["record"]`. This is how we know what fields to load.
```elixir
query something() {
result {
record { # <- this is the instance
id
name
}
}
}
```
## Options
- `path`: The path to the record(s) in the response
- `domain`: The domain to use when loading the fields. Determined from the resource by default.
- `authorize?`: Whether to authorize access to fields. Defaults to the domain's setting (which defaults to `true`).
- `actor`: The actor to use when authorizing access to fields. Defaults to the actor in the resolution context.
- `tenant`: The tenant to use when authorizing access to fields. Defaults to the tenant in the resolution context.
"""
@spec load_fields(input, Ash.Resource.t(), Absinthe.Resolution.t(), opts :: Keyword.t()) ::
{:ok, input} | {:error, term()}
when input: Ash.Resource.record() | list(Ash.Resource.record()) | Ash.Page.page()
def load_fields(data, resource, resolution, opts \\ []) do
Ash.load(data, load_fields_on_query(resource, resolution, opts), resource: resource)
end
@doc """
The same as `load_fields/4`, but modifies the provided query to load the required fields.
This allows doing the loading in a single query rather than two separate queries.
"""
@spec load_fields_on_query(
query :: Ash.Query.t() | Ash.Resource.t(),
Absinthe.Resolution.t(),
Keyword.t()
) ::
Ash.Query.t()
def load_fields_on_query(query, resolution, opts \\ []) do
query =
query
|> Ash.Query.new()
resource = query.resource
domain =
opts[:domain] || Ash.Resource.Info.domain(resource) ||
raise ArgumentError,
"Could not determine domain for #{inspect(resource)}. Please specify the `domain` option."
tenant = Keyword.get(opts, :tenant, Map.get(resolution.context, :tenant))
authorize? = Keyword.get(opts, :authorize?, AshGraphql.Domain.Info.authorize?(domain))
actor = Keyword.get(opts, :actor, Map.get(resolution.context, :actor))
query
|> Ash.Query.set_tenant(tenant)
|> Ash.Query.set_context(AshGraphql.ContextHelpers.get_context(resolution.context))
|> AshGraphql.Graphql.Resolver.select_fields(
resource,
resolution,
nil,
opts[:path] || []
)
|> AshGraphql.Graphql.Resolver.load_fields(
[
domain: domain,
tenant: tenant,
authorize?: authorize?,
actor: actor
],
resource,
resolution,
resolution.path,
resolution.context,
nil
)
end
@doc """
Applies AshGraphql's error handling logic if the value is an `{:error, error}` tuple, otherwise returns the value
Useful for automatically handling errors in custom queries
## Options
- `domain`: The domain to use when loading the fields. Determined from the resource by default.
"""
@spec handle_errors(
result :: term,
resource :: Ash.Resource.t(),
resolution :: Absinthe.Resolution.t(),
opts :: Keyword.t()
) ::
term()
def handle_errors(result, resource, resolution, opts \\ [])
def handle_errors({:error, error}, resource, resolution, opts) do
domain =
Ash.Resource.Info.domain(resource) || opts[:domain] ||
raise ArgumentError,
"Could not determine domain for #{inspect(resource)}. Please specify the `domain` option."
AshGraphql.Graphql.Resolver.to_resolution(
{:error, List.wrap(error)},
resolution.context,
domain
)
end
def handle_errors(result, _, _, _), do: result
@doc false
def only_union_types(attributes) do
Enum.flat_map(attributes, fn attribute ->
attribute
|> only_union_type()
|> List.wrap()
end)
end
defp only_union_type(%{type: {:array, type}, constraints: constraints} = attribute) do
only_union_type(%{attribute | type: type, constraints: constraints[:items] || []})
end
defp only_union_type(attribute) do
this_union_type =
case union_type(attribute.type) do
nil ->
nil
type ->
{type, attribute}
end
attribute = %{
attribute
| type:
attribute.type
|> unwrap_type()
|> Ash.Type.NewType.subtype_of(),
constraints: Ash.Type.NewType.constraints(attribute.type, attribute.constraints)
}
case unwrap_type(attribute.type) do
Ash.Type.Union ->
attribute.constraints[:types]
|> Kernel.||([])
|> Enum.flat_map(fn {_name, config} ->
case union_type(config[:type]) do
nil ->
[]
type ->
[{type, attribute}]
end
end)
type ->
case union_type(type) do
nil ->
[]
type ->
[{type, attribute}]
end
end
|> Enum.concat(List.wrap(this_union_type))
end
defp only_enum_types(attributes) do
attributes
|> Enum.filter(&AshGraphql.Resource.define_type?(&1.type, &1.constraints))
|> Enum.flat_map(fn attribute ->
attribute = %{
type:
attribute.type
|> unwrap_type()
|> Ash.Type.NewType.subtype_of(),
constraints: Ash.Type.NewType.constraints(attribute.type, attribute.constraints)
}
case unwrap_type(attribute.type) do
Ash.Type.Union ->
Enum.flat_map(attribute.constraints[:types] || [], fn {_name, config} ->
case enum_type(config[:type]) do
nil ->
[]
type ->
[type]
end
end)
type ->
case enum_type(type) do
nil ->
[]
type ->
[type]
end
end
end)
end
defp union_type({:array, type}) do
union_type(type)
end
defp union_type(type) do
if Ash.Type.NewType.new_type?(type) &&
Ash.Type.NewType.subtype_of(type) == Ash.Type.Union &&
function_exported?(type, :graphql_type, 1) do
type
end
end
# sobelow_skip ["DOS.BinToAtom"]
@doc false
def get_embedded_types(all_resources, all_domains, schema, relay_ids?) do
all_resources
|> Enum.flat_map(fn resource ->
resource
|> all_attributes_and_arguments(all_domains)
|> Enum.map(&{resource, &1})
end)
|> Enum.flat_map(fn
{source_resource, attribute} ->
{type, constraints} =
case attribute.type do
{:array, type} ->
{type, attribute.constraints[:items] || []}
type ->
{type, attribute.constraints}
end
attribute = %{
attribute
| type:
type
|> Ash.Type.NewType.subtype_of(),
constraints: Ash.Type.NewType.constraints(type, constraints)
}
case attribute.type do
type when type in [Ash.Type.Map, Ash.Type.Keyword, Ash.Type.Struct] ->
if fields = attribute.constraints[:fields] do
Enum.flat_map(fields, fn {name, config} ->
if AshGraphql.Resource.embedded?(config[:type]) do
[
{source_resource,
%{
attribute
| type: config[:type],
constraints: config[:constraints],
name: :"#{attribute.name}_#{name}"
}}
]
else
[]
end
end)
else
[]
end
Ash.Type.Union ->
attribute.constraints[:types]
|> Kernel.||([])
|> Enum.flat_map(fn {name, config} ->
if AshGraphql.Resource.embedded?(config[:type]) do
[
{source_resource,
%{
attribute
| type: config[:type],
constraints: config[:constraints],
name: :"#{attribute.name}_#{name}"
}}
]
else
[]
end
end)
other ->
if AshGraphql.Resource.embedded?(other) do
[{source_resource, attribute}]
else
[]
end
end
end)
|> Enum.map(fn {source_resource, attribute} ->
type = unwrap_type(attribute.type)
Code.ensure_compiled!(type)
{source_resource, attribute, type}
end)
|> Enum.flat_map(fn {source_resource, attribute, embedded} ->
[{source_resource, attribute, embedded}] ++ get_nested_embedded_types(embedded)
end)
|> Enum.flat_map(fn {source_resource, attribute, embedded_type} ->
if AshGraphql.Resource.Info.type(embedded_type) do
Enum.filter(
[
AshGraphql.Resource.type_definition(
embedded_type,
Ash.EmbeddableType.ShadowDomain,
[Ash.EmbeddableType.ShadowDomain],
schema,
relay_ids?
),
AshGraphql.Resource.embedded_type_input(
source_resource,
attribute,
embedded_type,
schema
)
],
& &1
) ++
AshGraphql.Resource.enum_definitions(embedded_type, schema, __ENV__)
else
[]
end
end)
|> Enum.uniq_by(& &1.identifier)
end
defp all_arguments(resource, all_domains) do
action_arguments =
resource
|> Ash.Resource.Info.actions()
|> Enum.filter(&used_in_gql?(resource, &1, all_domains))
|> Enum.flat_map(& &1.arguments)
calculation_arguments =
resource
|> Ash.Resource.Info.public_calculations()
|> Enum.flat_map(& &1.arguments)
action_arguments ++ calculation_arguments
end
defp used_in_gql?(resource, %{name: name}, all_domains) do
if Ash.Resource.Info.embedded?(resource) do
# We should actually check if any resource refers to this action for this
true
else
mutations = AshGraphql.Resource.Info.mutations(resource, all_domains)
queries = AshGraphql.Resource.Info.queries(resource, all_domains)
Enum.any?(mutations, fn mutation ->
mutation.action == name || Map.get(mutation, :read_action) == name
end) || Enum.any?(queries, &(&1.action == name))
end
end
defp enum_type({:array, type}), do: enum_type(type)
defp enum_type(type) do
if is_atom(type) && ensure_compiled?(type) && function_exported?(type, :values, 0) &&
(function_exported?(type, :graphql_type, 0) || function_exported?(type, :graphql_type, 1)) do
type
end
end
defp ensure_compiled?(type) do
Code.ensure_compiled!(type)
rescue
_ ->
false
end
defp unwrap_type({:array, type}), do: unwrap_type(type)
defp unwrap_type(type), do: type
defp get_nested_embedded_types(embedded_type) do
embedded_type
|> Ash.Resource.Info.public_attributes()
|> Enum.filter(&AshGraphql.Resource.embedded?(&1.type))
|> Enum.map(fn attribute ->
{attribute, unwrap_type(attribute.type)}
end)
|> Enum.flat_map(fn {attribute, embedded} ->
[{embedded_type, attribute, embedded}] ++ get_nested_embedded_types(embedded)
end)
end
end