mirror of
https://github.com/ash-project/ash_graphql.git
synced 2024-09-20 21:32:59 +12:00
435 lines
13 KiB
Elixir
435 lines
13 KiB
Elixir
defmodule AshGraphql.Domain do
|
|
@queries %Spark.Dsl.Section{
|
|
name: :queries,
|
|
describe: """
|
|
Queries to expose for the resource.
|
|
""",
|
|
examples: [
|
|
"""
|
|
queries do
|
|
get Post, :get_post, :read
|
|
read_one User, :current_user, :current_user
|
|
list Post, :list_posts, :read
|
|
end
|
|
"""
|
|
],
|
|
entities:
|
|
Enum.map(
|
|
AshGraphql.Resource.queries(),
|
|
&%{
|
|
&1
|
|
| args: [:resource | &1.args],
|
|
schema:
|
|
Keyword.put(&1.schema, :resource,
|
|
type: {:spark, Ash.Resource},
|
|
doc: "The resource that the action is defined on"
|
|
)
|
|
}
|
|
)
|
|
}
|
|
|
|
@mutations %Spark.Dsl.Section{
|
|
name: :mutations,
|
|
describe: """
|
|
Mutations (create/update/destroy actions) to expose for the resource.
|
|
""",
|
|
examples: [
|
|
"""
|
|
mutations do
|
|
create :create_post, :create
|
|
update :update_post, :update
|
|
destroy :destroy_post, :destroy
|
|
end
|
|
"""
|
|
],
|
|
entities:
|
|
Enum.map(
|
|
AshGraphql.Resource.mutations(),
|
|
&%{
|
|
&1
|
|
| args: [:resource | &1.args],
|
|
schema:
|
|
Keyword.put(&1.schema, :resource,
|
|
type: {:spark, Ash.Resource},
|
|
doc: "The resource that the action is defined on"
|
|
)
|
|
}
|
|
)
|
|
}
|
|
|
|
@graphql %Spark.Dsl.Section{
|
|
name: :graphql,
|
|
describe: """
|
|
Domain level configuration for GraphQL
|
|
""",
|
|
examples: [
|
|
"""
|
|
graphql do
|
|
authorize? false # To skip authorization for this domain
|
|
end
|
|
"""
|
|
],
|
|
sections: [
|
|
@queries,
|
|
@mutations
|
|
],
|
|
schema: [
|
|
authorize?: [
|
|
type: :boolean,
|
|
doc: "Whether or not to perform authorization for this domain",
|
|
default: true
|
|
],
|
|
tracer: [
|
|
type: :atom,
|
|
doc:
|
|
"A tracer to use to trace execution in the graphql. Will use `config :ash, :tracer` if it is set."
|
|
],
|
|
root_level_errors?: [
|
|
type: :boolean,
|
|
default: false,
|
|
doc:
|
|
"By default, mutation errors are shown in their result object's errors key, but this setting places those errors in the top level errors list"
|
|
],
|
|
error_handler: [
|
|
type: :mfa,
|
|
default: {AshGraphql.DefaultErrorHandler, :handle_error, []},
|
|
doc: """
|
|
Set an MFA to intercept/handle any errors that are generated.
|
|
"""
|
|
],
|
|
show_raised_errors?: [
|
|
type: :boolean,
|
|
default: false,
|
|
doc:
|
|
"For security purposes, if an error is *raised* then Ash simply shows a generic error. If you want to show those errors, set this to true."
|
|
]
|
|
]
|
|
}
|
|
|
|
@sections [@graphql]
|
|
|
|
@moduledoc """
|
|
The entrypoint for adding GraphQL behavior to an Ash domain
|
|
"""
|
|
|
|
require Ash.Domain.Info
|
|
|
|
use Spark.Dsl.Extension,
|
|
sections: @sections,
|
|
transformers: [
|
|
AshGraphql.Domain.Transformers.RequireKeysetForRelayQueries,
|
|
AshGraphql.Domain.Transformers.ValidateActions,
|
|
AshGraphql.Domain.Transformers.ValidateCompatibleNames
|
|
]
|
|
|
|
def install(igniter, module, Ash.Domain, _path, _argv) do
|
|
igniter
|
|
|> Spark.Igniter.add_extension(
|
|
module,
|
|
Ash.Domain,
|
|
:extensions,
|
|
AshGraphql.Domain
|
|
)
|
|
|> add_to_graphql_schema(module)
|
|
end
|
|
|
|
defp add_to_graphql_schema(igniter, domain) do
|
|
case AshGraphql.Igniter.find_schema(igniter, domain) do
|
|
{:ok, igniter, _} ->
|
|
igniter
|
|
|
|
{:error, igniter, []} ->
|
|
AshGraphql.Igniter.setup_absinthe_schema(igniter)
|
|
|
|
{:error, igniter, all_schemas} ->
|
|
schema =
|
|
case all_schemas do
|
|
[schema] ->
|
|
schema
|
|
|
|
schemas ->
|
|
Owl.IO.select(
|
|
schemas,
|
|
label: "Multiple Ash.Graphql modules found. Please select one to use:",
|
|
render_as: &inspect/1
|
|
)
|
|
end
|
|
|
|
Igniter.Code.Module.find_and_update_module!(igniter, schema, fn zipper ->
|
|
with {:ok, zipper} <- Igniter.Code.Module.move_to_use(zipper, AshGraphql),
|
|
{:ok, zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 1),
|
|
{:ok, zipper} <- Igniter.Code.Keyword.get_key(zipper, :domains),
|
|
{:ok, zipper} <- Igniter.Code.List.append_to_list(zipper, domain) do
|
|
{:ok, zipper}
|
|
else
|
|
_ ->
|
|
{:warning,
|
|
"""
|
|
Could not add #{inspect(domain)} to the list of domains in #{inspect(schema)}.
|
|
|
|
Please make that change manually.
|
|
"""}
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
@deprecated "See `AshGraphql.Domain.Info.authorize?/1`"
|
|
defdelegate authorize?(domain), to: AshGraphql.Domain.Info
|
|
|
|
@deprecated "See `AshGraphql.Domain.Info.root_level_errors?/1`"
|
|
defdelegate root_level_errors?(domain), to: AshGraphql.Domain.Info
|
|
|
|
@deprecated "See `AshGraphql.Domain.Info.show_raised_errors?/1`"
|
|
defdelegate show_raised_errors?(domain), to: AshGraphql.Domain.Info
|
|
|
|
@doc false
|
|
def queries(domain, all_domains, resources, action_middleware, schema, relay_ids?) do
|
|
Enum.flat_map(
|
|
resources,
|
|
&AshGraphql.Resource.queries(domain, all_domains, &1, action_middleware, schema, relay_ids?)
|
|
)
|
|
end
|
|
|
|
@doc false
|
|
def mutations(domain, all_domains, resources, action_middleware, schema, relay_ids?) do
|
|
resources
|
|
|> Enum.filter(fn resource ->
|
|
AshGraphql.Resource in Spark.extensions(resource)
|
|
end)
|
|
|> Enum.flat_map(
|
|
&AshGraphql.Resource.mutations(
|
|
domain,
|
|
all_domains,
|
|
&1,
|
|
action_middleware,
|
|
schema,
|
|
relay_ids?
|
|
)
|
|
)
|
|
end
|
|
|
|
def subscriptions(api, resources, action_middleware, schema) do
|
|
resources
|
|
|> IO.inspect(label: :subscriptions)
|
|
|> Enum.filter(fn resource ->
|
|
AshGraphql.Resource in Spark.extensions(resource)
|
|
end)
|
|
|> Enum.flat_map(&AshGraphql.Resource.subscriptions(api, &1, action_middleware, schema))
|
|
end
|
|
|
|
@doc false
|
|
def type_definitions(
|
|
domain,
|
|
all_domains,
|
|
resources,
|
|
schema,
|
|
env,
|
|
first?,
|
|
define_relay_types?,
|
|
relay_ids?
|
|
) do
|
|
resource_types =
|
|
resources
|
|
|> Enum.reject(&Ash.Resource.Info.embedded?/1)
|
|
|> Enum.flat_map(fn resource ->
|
|
if AshGraphql.Resource in Spark.extensions(resource) do
|
|
AshGraphql.Resource.type_definitions(resource, domain, all_domains, schema, relay_ids?) ++
|
|
AshGraphql.Resource.mutation_types(resource, all_domains, schema)
|
|
else
|
|
AshGraphql.Resource.no_graphql_types(resource, schema)
|
|
end
|
|
end)
|
|
|
|
if first? do
|
|
relay_types =
|
|
if define_relay_types? do
|
|
[
|
|
%Absinthe.Blueprint.Schema.InterfaceTypeDefinition{
|
|
description: "A relay node",
|
|
name: "Node",
|
|
fields: [
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
description: "A unique identifier",
|
|
identifier: :id,
|
|
module: schema,
|
|
name: "id",
|
|
__reference__: AshGraphql.Resource.ref(env),
|
|
type: %Absinthe.Blueprint.TypeReference.NonNull{of_type: :id}
|
|
}
|
|
],
|
|
identifier: :node,
|
|
resolve_type: &AshGraphql.Graphql.Resolver.resolve_node_type/2,
|
|
__reference__: AshGraphql.Resource.ref(env),
|
|
module: schema
|
|
},
|
|
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{
|
|
description: "A relay page info",
|
|
name: "PageInfo",
|
|
fields: [
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
description: "When paginating backwards, are there more items?",
|
|
identifier: :has_previous_page,
|
|
module: schema,
|
|
name: "has_previous_page",
|
|
__reference__: AshGraphql.Resource.ref(env),
|
|
type: %Absinthe.Blueprint.TypeReference.NonNull{of_type: :boolean}
|
|
},
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
description: "When paginating forwards, are there more items?",
|
|
identifier: :has_next_page,
|
|
module: schema,
|
|
name: "has_next_page",
|
|
__reference__: AshGraphql.Resource.ref(env),
|
|
type: %Absinthe.Blueprint.TypeReference.NonNull{of_type: :boolean}
|
|
},
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
description: "When paginating backwards, the cursor to continue",
|
|
identifier: :start_cursor,
|
|
module: schema,
|
|
name: "start_cursor",
|
|
__reference__: AshGraphql.Resource.ref(env),
|
|
type: :string
|
|
},
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
description: "When paginating forwards, the cursor to continue",
|
|
identifier: :end_cursor,
|
|
module: schema,
|
|
name: "end_cursor",
|
|
__reference__: AshGraphql.Resource.ref(env),
|
|
type: :string
|
|
}
|
|
# 'count' field is not compatible with keyset pagination
|
|
],
|
|
identifier: :page_info,
|
|
module: schema,
|
|
__reference__: AshGraphql.Resource.ref(env)
|
|
}
|
|
]
|
|
else
|
|
[]
|
|
end
|
|
|
|
relay_types ++ resource_types
|
|
else
|
|
resource_types
|
|
end
|
|
end
|
|
|
|
def global_type_definitions(schema, env) do
|
|
[mutation_error(schema, env), sort_order(schema, env)]
|
|
end
|
|
|
|
defp sort_order(schema, env) do
|
|
%Absinthe.Blueprint.Schema.EnumTypeDefinition{
|
|
module: schema,
|
|
name: "SortOrder",
|
|
values: [
|
|
%Absinthe.Blueprint.Schema.EnumValueDefinition{
|
|
module: schema,
|
|
identifier: :desc,
|
|
__reference__: AshGraphql.Resource.ref(env),
|
|
name: "DESC",
|
|
value: :desc
|
|
},
|
|
%Absinthe.Blueprint.Schema.EnumValueDefinition{
|
|
module: schema,
|
|
identifier: :desc_nils_first,
|
|
__reference__: AshGraphql.Resource.ref(env),
|
|
name: "DESC_NULLS_FIRST",
|
|
value: :des_nils_first
|
|
},
|
|
%Absinthe.Blueprint.Schema.EnumValueDefinition{
|
|
module: schema,
|
|
identifier: :desc_nils_last,
|
|
__reference__: AshGraphql.Resource.ref(env),
|
|
name: "DESC_NULLS_LAST",
|
|
value: :desc_nils_last
|
|
},
|
|
%Absinthe.Blueprint.Schema.EnumValueDefinition{
|
|
module: schema,
|
|
identifier: :asc,
|
|
__reference__: AshGraphql.Resource.ref(env),
|
|
name: "ASC",
|
|
value: :asc
|
|
},
|
|
%Absinthe.Blueprint.Schema.EnumValueDefinition{
|
|
module: schema,
|
|
identifier: :asc_nils_first,
|
|
__reference__: AshGraphql.Resource.ref(env),
|
|
name: "ASC_NULLS_FIRST",
|
|
value: :asc_nils_first
|
|
},
|
|
%Absinthe.Blueprint.Schema.EnumValueDefinition{
|
|
module: schema,
|
|
identifier: :asc_nils_last,
|
|
__reference__: AshGraphql.Resource.ref(env),
|
|
name: "ASC_NULLS_LAST",
|
|
value: :asc_nils_last
|
|
}
|
|
],
|
|
identifier: :sort_order,
|
|
__reference__: AshGraphql.Resource.ref(env)
|
|
}
|
|
end
|
|
|
|
defp mutation_error(schema, env) do
|
|
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{
|
|
description: "An error generated by a failed mutation",
|
|
fields: error_fields(schema, env),
|
|
identifier: :mutation_error,
|
|
module: schema,
|
|
name: "MutationError",
|
|
__reference__: AshGraphql.Resource.ref(env)
|
|
}
|
|
end
|
|
|
|
defp error_fields(schema, env) do
|
|
[
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
description: "The human readable error message",
|
|
identifier: :message,
|
|
module: schema,
|
|
__reference__: AshGraphql.Resource.ref(env),
|
|
name: "message",
|
|
type: :string
|
|
},
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
description: "A shorter error message, with vars not replaced",
|
|
identifier: :short_message,
|
|
module: schema,
|
|
__reference__: AshGraphql.Resource.ref(env),
|
|
name: "short_message",
|
|
type: :string
|
|
},
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
description: "Replacements for the short message",
|
|
identifier: :vars,
|
|
module: schema,
|
|
__reference__: AshGraphql.Resource.ref(env),
|
|
name: "vars",
|
|
type: :json
|
|
},
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
description: "An error code for the given error",
|
|
identifier: :code,
|
|
module: schema,
|
|
__reference__: AshGraphql.Resource.ref(env),
|
|
name: "code",
|
|
type: :string
|
|
},
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
description: "The field or fields that produced the error",
|
|
identifier: :fields,
|
|
module: schema,
|
|
__reference__: AshGraphql.Resource.ref(env),
|
|
name: "fields",
|
|
type: %Absinthe.Blueprint.TypeReference.List{
|
|
of_type: %Absinthe.Blueprint.TypeReference.NonNull{
|
|
of_type: :string
|
|
}
|
|
}
|
|
}
|
|
]
|
|
end
|
|
end
|