Packages
ash_graphql
0.26.4
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)
"""
defmacro mutation(do: block) do
empty? = !match?({:__block__, _, []}, block)
quote bind_quoted: [empty?: empty?, block: Macro.escape(block)] do
require Absinthe.Schema
if empty? ||
Enum.any?(
@ash_resources,
fn resource ->
!Enum.empty?(AshGraphql.Resource.Info.mutations(resource))
end
) do
Code.eval_quoted(
quote do
Absinthe.Schema.mutation do
unquote(block)
end
end,
[],
__ENV__
)
end
end
end
defmacro __using__(opts) do
quote bind_quoted: [
apis: opts[:apis],
api: opts[:api],
action_middleware: opts[:action_middleware] || []
],
generated: true do
require Ash.Api.Info
import Absinthe.Schema,
except: [
mutation: 1
]
import AshGraphql,
only: [
mutation: 1
]
apis =
api
|> List.wrap()
|> Kernel.++(List.wrap(apis))
apis =
apis
|> Enum.map(fn
{api, registry} ->
IO.warn("""
It is no longer required to list the registry along with an API when using `AshGraphql`
use AshGraphql, apis: [{My.App.Api, My.App.Registry}]
Can now be stated simply as
use AshGraphql, apis: [My.App.Api]
""")
api
api ->
api
end)
|> Enum.map(fn api -> {api, Ash.Api.Info.depend_on_resources(api), false} end)
|> List.update_at(0, fn {api, resources, _} -> {api, resources, true} end)
@ash_resources Enum.flat_map(apis, &elem(&1, 1))
ash_resources = @ash_resources
schema = __MODULE__
schema_env = __ENV__
for resource <- ash_resources do
resource
|> AshGraphql.Resource.get_auto_unions()
|> Enum.concat(resource |> AshGraphql.Resource.global_unions() |> Enum.map(&elem(&1, 1)))
|> Enum.map(fn attribute ->
if Ash.Type.NewType.new_type?(attribute.type) do
cond do
function_exported?(attribute.type, :graphql_type, 0) ->
attribute.type.graphql_type()
function_exported?(attribute.type, :graphql_type, 1) ->
attribute.type.graphql_type(attribute.constraints)
true ->
AshGraphql.Resource.atom_enum_type(resource, attribute.name)
end
else
AshGraphql.Resource.atom_enum_type(resource, attribute.name)
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 {api, resources, first?} <- apis do
defmodule Module.concat(api, 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
api = unquote(api)
action_middleware = unquote(action_middleware)
blueprint_with_queries =
api
|> AshGraphql.Api.queries(unquote(resources), action_middleware, __MODULE__)
|> Enum.reduce(blueprint, fn query, blueprint ->
Absinthe.Blueprint.add_field(blueprint, "RootQueryType", query)
end)
blueprint_with_mutations =
api
|> AshGraphql.Api.mutations(unquote(resources), action_middleware, __MODULE__)
|> Enum.reduce(blueprint_with_queries, fn mutation, blueprint ->
Absinthe.Blueprint.add_field(blueprint, "RootMutationType", mutation)
end)
type_definitions =
if unquote(first?) do
apis = unquote(Enum.map(apis, &elem(&1, 0)))
embedded_types =
AshGraphql.get_embedded_types(unquote(ash_resources), unquote(schema))
global_enums =
AshGraphql.global_enums(unquote(ash_resources), unquote(schema), __ENV__)
global_unions =
AshGraphql.global_unions(unquote(ash_resources), unquote(schema), __ENV__)
Enum.uniq_by(
AshGraphql.Api.global_type_definitions(unquote(schema), __ENV__) ++
AshGraphql.Api.type_definitions(
api,
unquote(resources),
unquote(schema),
__ENV__,
true
) ++
global_enums ++
global_unions ++
embedded_types,
& &1.identifier
)
else
AshGraphql.Api.type_definitions(
api,
unquote(resources),
unquote(schema),
__ENV__,
false
)
end
new_defs =
List.update_at(blueprint_with_mutations.schema_definitions, 0, fn schema_def ->
%{
schema_def
| type_definitions: schema_def.type_definitions ++ type_definitions
}
end)
{:ok, %{blueprint_with_mutations | schema_definitions: new_defs}}
end
end
if first? do
import_types(Absinthe.Type.Custom)
import_types(AshGraphql.Types.JSON)
import_types(AshGraphql.Types.JSONString)
end
@pipeline_modifier Module.concat(api, AshTypes)
end
end
end
def global_enums(resources, schema, env) do
resources
|> Enum.flat_map(&all_attributes_and_arguments/1)
|> 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
%Absinthe.Blueprint.Schema.EnumValueDefinition{
module: schema,
identifier: value,
__reference__: AshGraphql.Resource.ref(env),
name: String.upcase(to_string(name)),
value: value
}
end),
identifier: identifier,
__reference__: AshGraphql.Resource.ref(env)
}
end)
|> Enum.uniq_by(& &1.identifier)
end
def global_unions(resources, schema, env) do
resources
|> Enum.flat_map(fn resource ->
resource
|> AshGraphql.Resource.global_unions()
|> Enum.flat_map(fn {type, attribute} ->
type_name =
if function_exported?(type, :graphql_type, 0) do
type.graphql_type()
else
type.graphql_type(attribute.constraints)
end
input_type_name =
cond do
function_exported?(type, :graphql_input_type, 0) ->
type.graphql_input_type()
function_exported?(type, :graphql_input_type, 1) ->
type.graphql_input_type(attribute.constraints)
true ->
"#{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,
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))
|> Enum.concat(Ash.Resource.Info.calculations(resource))
|> 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, constraints, already_checked)
{[attr | nested] ++ acc, already_checked}
else
{[attr | acc], already_checked}
end
end)
|> then(fn {attrs, checked} ->
attrs = Enum.filter(attrs, &AshGraphql.Resource.Info.show_field?(resource, &1.name))
if return_new_checked? do
{attrs, checked}
else
attrs
end
end)
end
end
defp nested_attrs({:array, type}, constraints, already_checked) do
nested_attrs(type, constraints[:items] || [], already_checked)
end
defp nested_attrs(Ash.Type.Union, 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, config[:constraints][:items] || [], already_checked)
{attrs ++ new, already_checked}
type ->
{new, already_checked} =
nested_attrs(type, config[:constraints] || [], already_checked)
{attrs ++ new, already_checked}
end
end
)
end
defp nested_attrs(type, constraints, already_checked) do
cond do
Ash.Type.embedded_type?(type) ->
type
|> unwrap_type()
|> all_attributes_and_arguments(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, constraints, already_checked)
true ->
{[], already_checked}
end
end
def get_embed(type) do
if Ash.Type.NewType.new_type?(type) do
Ash.Type.NewType.subtype_of(type)
else
type
end
end
@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
Enum.flat_map(attributes, 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, 0) ||
function_exported?(type, :graphql_type, 1)) do
type
end
end
# sobelow_skip ["DOS.BinToAtom"]
def get_embedded_types(all_resources, schema) do
all_resources
|> Enum.flat_map(fn resource ->
resource
|> all_attributes_and_arguments()
|> 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
Ash.Type.Map ->
if attribute.constraints[:fields] do
{source_resource, attribute}
end
[]
Ash.Type.Union ->
attribute.constraints[:types]
|> Kernel.||([])
|> Enum.flat_map(fn {name, config} ->
if Ash.Type.embedded_type?(config[:type]) do
[
{source_resource,
%{
attribute
| type: config[:type],
constraints: config[:constraints],
name: :"#{attribute.name}_#{name}"
}}
]
else
[]
end
end)
other ->
if Ash.Type.embedded_type?(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
[
AshGraphql.Resource.type_definition(
embedded_type,
Module.concat(embedded_type, ShadowApi),
schema
),
AshGraphql.Resource.embedded_type_input(
source_resource,
attribute,
embedded_type,
schema
)
] ++
AshGraphql.Resource.enum_definitions(embedded_type, schema, __ENV__) ++
AshGraphql.Resource.map_definitions(embedded_type, schema, __ENV__)
else
[]
end
end)
|> Enum.uniq_by(& &1.identifier)
end
defp all_arguments(resource) do
action_arguments =
resource
|> Ash.Resource.Info.actions()
|> Enum.filter(&used_in_gql?(resource, &1))
|> 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}) 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)
queries = AshGraphql.Resource.Info.queries(resource)
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(&Ash.Type.embedded_type?(&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
@deprecated "add_context is no longer necessary"
def add_context(ctx, _apis, _options \\ []) do
ctx
end
end