Packages
ash_graphql
0.22.13
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 {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__)
AshGraphql.Api.global_type_definitions(unquote(schema), __ENV__) ++
AshGraphql.Api.type_definitions(
api,
unquote(resources),
unquote(schema),
__ENV__,
true
) ++
global_enums ++
embedded_types
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 ->
%Absinthe.Blueprint.Schema.EnumValueDefinition{
module: schema,
identifier: value,
__reference__: AshGraphql.Resource.ref(env),
name: String.upcase(to_string(value)),
value: value
}
end),
identifier: identifier,
__reference__: AshGraphql.Resource.ref(env)
}
end)
|> Enum.uniq_by(& &1.identifier)
end
@doc false
def all_attributes_and_arguments(resource, already_checked \\ [], nested? \\ true) do
if resource in already_checked do
[]
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.flat_map(fn %{type: type} = attr ->
if Ash.Type.embedded_type?(type) && nested? do
type = Ash.Type.NewType.subtype_of(type)
[
attr
| type
|> unwrap_type()
|> all_attributes_and_arguments(already_checked, nested?)
]
else
[attr]
end
end)
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
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
# 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} ->
attribute = %{
attribute
| type:
attribute.type
|> unwrap_type()
|> Ash.Type.NewType.subtype_of(),
constraints: Ash.Type.NewType.constraints(attribute.type, attribute.constraints)
}
attribute_type = unwrap_type(attribute.type)
case attribute_type do
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__)
else
[]
end
end)
|> Enum.uniq_by(& &1.identifier)
end
defp all_arguments(resource) do
action_arguments =
resource
|> Ash.Resource.Info.actions()
|> Enum.flat_map(& &1.arguments)
calculation_arguments =
resource
|> Ash.Resource.Info.public_calculations()
|> Enum.flat_map(& &1.arguments)
action_arguments ++ calculation_arguments
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
def add_context(ctx, apis, options \\ []) do
options = Keyword.put(options, :get_policy, :tuples)
empty_dataloader = Dataloader.new(options)
dataloader =
apis
|> List.wrap()
|> Enum.map(fn
{api, _registry} ->
api
api ->
api
end)
|> Enum.reduce(empty_dataloader, fn api, dataloader ->
Dataloader.add_source(
dataloader,
api,
AshGraphql.Dataloader.new(api)
)
end)
Map.put(ctx, :loader, dataloader)
end
end