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 @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