diff --git a/.check.exs b/.check.exs index 865ff7e..4858901 100644 --- a/.check.exs +++ b/.check.exs @@ -11,7 +11,8 @@ ## ...or adjusted (e.g. use one-line formatter for more compact credo output) # {:credo, "mix credo --format oneline"}, - {:check_formatter, command: "mix spark.formatter --check"} + {:check_formatter, command: "mix spark.formatter --check"}, + {:doctor, false} ## custom new tools may be added (mix tasks or arbitrary commands) # {:my_mix_task, command: "mix release", env: %{"MIX_ENV" => "prod"}}, diff --git a/.formatter.exs b/.formatter.exs index 1dc8fa1..dfbae68 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,4 +1,6 @@ spark_locals_without_parens = [ + action: 2, + action: 3, allow_nil?: 1, argument_names: 1, as_mutation?: 1, diff --git a/documentation/dsls/DSL:-AshGraphql.Api.cheatmd b/documentation/dsls/DSL:-AshGraphql.Api.cheatmd new file mode 100644 index 0000000..447b4e1 --- /dev/null +++ b/documentation/dsls/DSL:-AshGraphql.Api.cheatmd @@ -0,0 +1,37 @@ +# DSL: AshGraphql.Api + +The entrypoint for adding graphql behavior to an Ash API + + +## graphql +Global configuration for graphql + + + + +### Examples +``` +graphql do + authorize? false # To skip authorization for this API +end + +``` + + + + +### Options +| Name | Type | Default | Docs | +| --- | --- | --- | --- | +| `authorize?` | `boolean` | true | Whether or not to perform authorization for this API | +| `tracer` | `atom` | | A tracer to use to trace execution in the graphql. Will use `config :ash, :tracer` if it is set. | +| `root_level_errors?` | `boolean` | false | 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` | `mfa` | {AshGraphql.DefaultErrorHandler, :handle_error, []} | Set an MFA to intercept/handle any errors that are generated. | +| `show_raised_errors?` | `boolean` | false | 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. | +| `debug?` | `boolean` | false | Whether or not to log (extremely verbose) debug information | + + + + + + diff --git a/documentation/dsls/DSL:-AshGraphql.Resource.cheatmd b/documentation/dsls/DSL:-AshGraphql.Resource.cheatmd new file mode 100644 index 0000000..ec1dafb --- /dev/null +++ b/documentation/dsls/DSL:-AshGraphql.Resource.cheatmd @@ -0,0 +1,523 @@ +# DSL: AshGraphql.Resource + +This Ash resource extension adds configuration for exposing a resource in a graphql. + + +## graphql +Configuration for a given resource in graphql + + +### Nested DSLs + * [queries](#graphql-queries) + * get + * read_one + * list + * action + * [mutations](#graphql-mutations) + * create + * update + * destroy + * action + * [managed_relationships](#graphql-managed_relationships) + * managed_relationship + + +### Examples +``` +graphql do + type :post + + queries do + get :get_post, :read + list :list_posts, :read + end + + mutations do + create :create_post, :create + update :update_post, :update + destroy :destroy_post, :destroy + end +end + +``` + + + + +### Options +| Name | Type | Default | Docs | +| --- | --- | --- | --- | +| `type`* | `atom` | | The type to use for this entity in the graphql schema | +| `derive_filter?` | `boolean` | true | Set to false to disable the automatic generation of a filter input for read actions. | +| `derive_sort?` | `boolean` | true | Set to false to disable the automatic generation of a sort input for read actions. | +| `encode_primary_key?` | `boolean` | true | For resources with composite primary keys, or primary keys not called `:id`, this will cause the id to be encoded as a single `id` attribute, both in the representation of the resource and in get requests | +| `relationships` | `list(atom)` | | A list of relationships to include on the created type. Defaults to all public relationships where the destination defines a graphql type. | +| `field_names` | `Keyword.t` | | A keyword list of name overrides for attributes. | +| `hide_fields` | `list(atom)` | | A list of attributes to hide from the api | +| `argument_names` | `Keyword.t` | | A nested keyword list of action names, to argument name remappings. i.e `create: [arg_name: :new_name]` | +| `keyset_field` | `atom` | | If set, the keyset will be displayed on all read actions in this field. It will be `nil` unless at least one of the read actions on a resource uses keyset pagination or it is the result of a mutation | +| `attribute_types` | `Keyword.t` | | A keyword list of type overrides for attributes. The type overrides should refer to types available in the graphql (absinthe) schema. `list_of/1` and `non_null/1` helpers can be used. | +| `attribute_input_types` | `Keyword.t` | | A keyword list of input type overrides for attributes. The type overrides should refer to types available in the graphql (absinthe) schema. `list_of/1` and `non_null/1` helpers can be used. | +| `primary_key_delimiter` | `String.t` | "~" | If a composite primary key exists, this can be set to determine delimiter used in the `id` field value. | +| `depth_limit` | `integer` | | A simple way to prevent massive queries. | +| `generate_object?` | `boolean` | true | Whether or not to create the GraphQL object, this allows you to manually create the GraphQL object. | +| `filterable_fields` | `list(atom)` | | A list of fields that are allowed to be filtered on. Defaults to all filterable fields for which a GraphQL type can be created. | + + +## graphql.queries +Queries (read actions) to expose for the resource. + + +### Nested DSLs + * [get](#graphql-queries-get) + * [read_one](#graphql-queries-read_one) + * [list](#graphql-queries-list) + * [action](#graphql-queries-action) + + +### Examples +``` +queries do + get :get_post, :read + read_one :current_user, :current_user + list :list_posts, :read +end + +``` + + + + +## graphql.queries.get +```elixir +get name, action +``` + + +A query to fetch a record by primary key + + + +### Examples +``` +get :get_post, :read +``` + + + +### Arguments +| Name | Type | Default | Docs | +| --- | --- | --- | --- | +| `name` | `atom` | :get | The name to use for the query. | +| `action`* | `atom` | | The action to use for the query. | +### Options +| Name | Type | Default | Docs | +| --- | --- | --- | --- | +| `identity` | `atom` | | The identity to use for looking up the record. Pass `false` to not use an identity. | +| `allow_nil?` | `boolean` | true | Whether or not the action can return nil. | +| `modify_resolution` | `mfa` | | An MFA that will be called with the resolution, the query, and the result of the action as the first three arguments. See the [the guide](/documentation/topics/modifying-the-resolution.html) for more. | +| `type_name` | `atom` | | Override the type name returned by this query. Must be set if the read action has `metadata` that is not hidden via the `show_metadata` key. | +| `metadata_names` | `Keyword.t` | [] | Name overrides for metadata fields on the read action. | +| `metadata_types` | `Keyword.t` | [] | Type overrides for metadata fields on the read action. | +| `show_metadata` | `list(atom)` | | The metadata attributes to show. Defaults to all. | +| `as_mutation?` | `boolean` | false | Places the query in the `mutations` key instead. Not typically necessary, but is often paired with `as_mutation?`. See the [the guide](/documentation/topics/modifying-the-resolution.html) for more. | + + + + + +### Introspection + +Target: `AshGraphql.Resource.Query` + +## graphql.queries.read_one +```elixir +read_one name, action +``` + + +A query to fetch a record + + + +### Examples +``` +read_one :current_user, :current_user +``` + + + +### Arguments +| Name | Type | Default | Docs | +| --- | --- | --- | --- | +| `name` | `atom` | :get | The name to use for the query. | +| `action`* | `atom` | | The action to use for the query. | +### Options +| Name | Type | Default | Docs | +| --- | --- | --- | --- | +| `allow_nil?` | `boolean` | true | Whether or not the action can return nil. | +| `type_name` | `atom` | | Override the type name returned by this query. Must be set if the read action has `metadata` that is not hidden via the `show_metadata` key. | +| `metadata_names` | `Keyword.t` | [] | Name overrides for metadata fields on the read action. | +| `metadata_types` | `Keyword.t` | [] | Type overrides for metadata fields on the read action. | +| `show_metadata` | `list(atom)` | | The metadata attributes to show. Defaults to all. | +| `as_mutation?` | `boolean` | false | Places the query in the `mutations` key instead. Not typically necessary, but is often paired with `as_mutation?`. See the [the guide](/documentation/topics/modifying-the-resolution.html) for more. | + + + + + +### Introspection + +Target: `AshGraphql.Resource.Query` + +## graphql.queries.list +```elixir +list name, action +``` + + +A query to fetch a list of records + + + +### Examples +``` +list :list_posts, :read +``` + +``` +list :list_posts_paginated, :read, relay?: true +``` + + + +### Arguments +| Name | Type | Default | Docs | +| --- | --- | --- | --- | +| `name` | `atom` | :get | The name to use for the query. | +| `action`* | `atom` | | The action to use for the query. | +### Options +| Name | Type | Default | Docs | +| --- | --- | --- | --- | +| `relay?` | `boolean` | false | If true, the graphql queries/resolvers for this resource will be built to honor the relay specification. See [the relay guide](/documentation/topics/relay.html) for more. | +| `type_name` | `atom` | | Override the type name returned by this query. Must be set if the read action has `metadata` that is not hidden via the `show_metadata` key. | +| `metadata_names` | `Keyword.t` | [] | Name overrides for metadata fields on the read action. | +| `metadata_types` | `Keyword.t` | [] | Type overrides for metadata fields on the read action. | +| `show_metadata` | `list(atom)` | | The metadata attributes to show. Defaults to all. | +| `as_mutation?` | `boolean` | false | Places the query in the `mutations` key instead. Not typically necessary, but is often paired with `as_mutation?`. See the [the guide](/documentation/topics/modifying-the-resolution.html) for more. | + + + + + +### Introspection + +Target: `AshGraphql.Resource.Query` + +## graphql.queries.action +```elixir +action name, action +``` + + +Runs a generic action + + + +### Examples +``` +action :check_status, :check_status +``` + + + +### Arguments +| Name | Type | Default | Docs | +| --- | --- | --- | --- | +| `name` | `atom` | :get | The name to use for the query. | +| `action`* | `atom` | | The action to use for the query. | + + + + + + +### Introspection + +Target: `AshGraphql.Resource.Action` + + +## graphql.mutations +Mutations (create/update/destroy actions) to expose for the resource. + + +### Nested DSLs + * [create](#graphql-mutations-create) + * [update](#graphql-mutations-update) + * [destroy](#graphql-mutations-destroy) + * [action](#graphql-mutations-action) + + +### Examples +``` +mutations do + create :create_post, :create + update :update_post, :update + destroy :destroy_post, :destroy +end + +``` + + + + +## graphql.mutations.create +```elixir +create name, action +``` + + +A mutation to create a record + + + +### Examples +``` +create :create_post, :create +``` + + + +### Arguments +| Name | Type | Default | Docs | +| --- | --- | --- | --- | +| `name` | `atom` | :get | The name to use for the mutation. | +| `action`* | `atom` | | The action to use for the mutation. | +### Options +| Name | Type | Default | Docs | +| --- | --- | --- | --- | +| `upsert?` | `boolean` | false | Whether or not to use the `upsert?: true` option when calling `YourApi.create/2`. | +| `upsert_identity` | `atom` | false | Which identity to use for the upsert | +| `modify_resolution` | `mfa` | | An MFA that will be called with the resolution, the query, and the result of the action as the first three arguments. See the [the guide](/documentation/topics/modifying-the-resolution.html) for more. | + + + + + +### Introspection + +Target: `AshGraphql.Resource.Mutation` + +## graphql.mutations.update +```elixir +update name, action +``` + + +A mutation to update a record + + + +### Examples +``` +update :update_post, :update +``` + + + +### Arguments +| Name | Type | Default | Docs | +| --- | --- | --- | --- | +| `name` | `atom` | :get | The name to use for the mutation. | +| `action`* | `atom` | | The action to use for the mutation. | +### Options +| Name | Type | Default | Docs | +| --- | --- | --- | --- | +| `identity` | `atom` | | The identity to use to fetch the record to be updated. Use `false` if no identity is required. | +| `read_action` | `atom` | | The read action to use to fetch the record to be updated. Defaults to the primary read action. | + + + + + +### Introspection + +Target: `AshGraphql.Resource.Mutation` + +## graphql.mutations.destroy +```elixir +destroy name, action +``` + + +A mutation to destroy a record + + + +### Examples +``` +destroy :destroy_post, :destroy +``` + + + +### Arguments +| Name | Type | Default | Docs | +| --- | --- | --- | --- | +| `name` | `atom` | :get | The name to use for the mutation. | +| `action`* | `atom` | | The action to use for the mutation. | +### Options +| Name | Type | Default | Docs | +| --- | --- | --- | --- | +| `read_action` | `atom` | | The read action to use to fetch the record to be destroyed. Defaults to the primary read action. | +| `identity` | `atom` | | The identity to use to fetch the record to be destroyed. Use `false` if no identity is required. | + + + + + +### Introspection + +Target: `AshGraphql.Resource.Mutation` + +## graphql.mutations.action +```elixir +action name, action +``` + + +Runs a generic action + + + +### Examples +``` +action :check_status, :check_status +``` + + + +### Arguments +| Name | Type | Default | Docs | +| --- | --- | --- | --- | +| `name` | `atom` | :get | The name to use for the query. | +| `action`* | `atom` | | The action to use for the query. | + + + + + + +### Introspection + +Target: `AshGraphql.Resource.Action` + + +## graphql.managed_relationships +Generates input objects for `manage_relationship` arguments on resource actions. + + +### Nested DSLs + * [managed_relationship](#graphql-managed_relationships-managed_relationship) + + +### Examples +``` +managed_relationships do + manage_relationship :create_post, :comments +end + +``` + + + + +### Options +| Name | Type | Default | Docs | +| --- | --- | --- | --- | +| `auto?` | `boolean` | | Automatically derive types for all arguments that have a `manage_relationship` call change. | + + + +## graphql.managed_relationships.managed_relationship +```elixir +managed_relationship action, argument +``` + + +Instructs ash_graphql that a given argument with a `manage_relationship` change should have its input objects derived automatically from the potential actions to be called. + +For example, given an action like: + +```elixir +actions do +create :create do +argument :comments, {:array, :map} + +change manage_relationship(:comments, type: :direct_control) # <- we look for this change with a matching argument name +end +end +``` + +You could add the following managed_relationship + +```elixir +graphql do +... + +managed_relationships do +managed_relationship :create, :comments +end +end +``` + +By default, the `{:array, :map}` would simply be a `json[]` type. If the argument name +is placed in this list, all of the potential actions that could be called will be combined +into a single input object. If there are type conflicts (for example, if the input could create +or update a record, and the create and update actions have an argument of the same name but with a different type), +a warning is emitted at compile time and the first one is used. If that is insufficient, you will need to do one of the following: + +1.) provide the `:types` option to the `managed_relationship` constructor (see that option for more) +2.) define a custom type, with a custom input object (see the custom types guide), and use that custom type instead of `:map` +3.) change your actions to not have overlapping inputs with different types + +Since managed relationships can ultimately call multiple actions, there is the possibility +of field type conflicts. Use the `types` option to determine the type of fields and remove the conflict warnings. + +For `non_null` use `{:non_null, type}`, and for a list, use `{:array, type}`, for example: + +`{:non_null, {:array, {:non_null, :string}}}` for a non null list of non null strings. + +To *remove* a key from the input object, simply pass `nil` as the type. + + + + + + +### Arguments +| Name | Type | Default | Docs | +| --- | --- | --- | --- | +| `action` | `atom` | | The action that accepts the argument | +| `argument`* | `atom` | | The argument for which an input object should be derived. | +### Options +| Name | Type | Default | Docs | +| --- | --- | --- | --- | +| `lookup_with_primary_key?` | `boolean` | | If the managed_relationship has `on_lookup` behavior, this option determines whether or not the primary key is provided in the input object for looking up. | +| `lookup_identities` | `list(atom)` | | Determines which identities are provided in the input object for looking up, if there is `on_lookup` behavior. Defalts to the `use_identities` option. | +| `type_name` | `atom` | | The name of the input object that will be derived. Defaults to `___input` | +| `types` | ``any`` | | A keyword list of field names to their graphql type identifiers. | + + + + + +### Introspection + +Target: `AshGraphql.Resource.ManagedRelationship` + + + + + + diff --git a/documentation/topics/modifying-the-resolution.md b/documentation/topics/modifying-the-resolution.md new file mode 100644 index 0000000..db63a39 --- /dev/null +++ b/documentation/topics/modifying-the-resolution.md @@ -0,0 +1,10 @@ +# Modifying the Resolution + +Using the `modify_resolution` option, you can alter the Absinthe resolution. + +`modify_resoltion` is an MFA that will be called with the resolution, the query, and the result of the action as the first three arguments. Must return a new absinthe resolution. + +This can be used to implement things like setting cookies based on resource actions. A method of using resolution context +for that is documented here: https://hexdocs.pm/absinthe_plug/Absinthe.Plug.html#module-before-send + +*Important* if you are modifying the context in a query, then you should also set `as_mutation?` to true and represent this in your graphql as a mutation. See `as_mutation?` for more. diff --git a/documentation/topics/relay.md b/documentation/topics/relay.md new file mode 100644 index 0000000..4a66418 --- /dev/null +++ b/documentation/topics/relay.md @@ -0,0 +1,8 @@ +# Relay + +Enabling relay for a resource sets it up to follow the [relay specification](https://relay.dev/graphql/connections.htm). + +The two changes that are made currently are: + +* the type for the resource will implement the `Node` interface +* pagination over that resource will behave as a Connection. diff --git a/lib/api/api.ex b/lib/api/api.ex index 384ffe1..f0f5d07 100644 --- a/lib/api/api.ex +++ b/lib/api/api.ex @@ -53,19 +53,6 @@ defmodule AshGraphql.Api do @moduledoc """ The entrypoint for adding graphql behavior to an Ash API - - - - ## DSL Documentation - - ### Index - - #{Spark.Dsl.Extension.doc_index(@sections)} - - ### Docs - - #{Spark.Dsl.Extension.doc(@sections)} - """ require Ash.Api.Info diff --git a/lib/api/info.ex b/lib/api/info.ex index 93457e2..ca98f89 100644 --- a/lib/api/info.ex +++ b/lib/api/info.ex @@ -10,7 +10,10 @@ defmodule AshGraphql.Api.Info do @doc "The tracer to use for the given schema" def tracer(api) do - Extension.get_opt(api, [:graphql], :tracer, Application.get_env(:ash, :tracer), true) + api + |> Extension.get_opt([:graphql], :tracer, nil, true) + |> List.wrap() + |> Enum.concat(List.wrap(Application.get_env(:ash, :tracer))) end @doc "Wether or not to surface errors to the root of the response" diff --git a/lib/graphql/resolver.ex b/lib/graphql/resolver.ex index 0a04354..3c3a53c 100644 --- a/lib/graphql/resolver.ex +++ b/lib/graphql/resolver.ex @@ -9,6 +9,110 @@ defmodule AshGraphql.Graphql.Resolver do def resolve(%Absinthe.Resolution{state: :resolved} = resolution, _), do: resolution + def resolve( + %{arguments: arguments, context: context} = resolution, + {api, resource, %{name: query_name, action: action}} + ) do + action = Ash.Resource.Info.action(resource, action) + + case handle_arguments(resource, action, arguments) do + {:ok, arguments} -> + metadata = %{ + api: api, + resource: resource, + resource_short_name: Ash.Resource.Info.short_name(resource), + actor: Map.get(context, :actor), + tenant: Map.get(context, :tenant), + action: action, + source: :graphql, + query: query_name, + authorize?: AshGraphql.Api.Info.authorize?(api) + } + + trace api, + resource, + :gql_query, + query_name, + metadata do + result = + %Ash.ActionInput{api: api, resource: resource} + |> Ash.ActionInput.set_context(get_context(context)) + |> Ash.ActionInput.for_action(action.name, arguments) + |> api.run_action() + |> case do + {:ok, result} -> + load_opts = + [ + actor: Map.get(context, :actor), + action: action, + api: api, + verbose?: AshGraphql.Api.Info.debug?(api), + authorize?: AshGraphql.Api.Info.authorize?(api), + tenant: Map.get(context, :tenant) + ] + + if Ash.Type.can_load?(action.returns, action.constraints) do + {fields, path} = nested_fields_and_path(resolution, [], []) + + loads = + type_loads( + fields, + action.returns, + action.constraints, + load_opts, + resource, + action.name, + resolution, + path, + hd(resolution.path), + nil + ) + + case loads do + [] -> + {:ok, result} + + loads -> + Ash.Type.load( + action.returns, + result, + loads, + action.constraints, + Map.new(load_opts) + ) + end + else + {:ok, result} + end + + {:error, error} -> + {:error, error} + end + + resolution + |> Absinthe.Resolution.put_result( + to_resolution( + result, + context, + api + ) + ) + |> add_root_errors(api, result) + end + + {:error, error} -> + {:error, error} + end + rescue + e -> + if AshGraphql.Api.Info.show_raised_errors?(api) do + error = Ash.Error.to_ash_error([e], __STACKTRACE__) + Absinthe.Resolution.put_result(resolution, to_resolution({:error, error}, context, api)) + else + something_went_wrong(resolution, e, api, __STACKTRACE__) + end + end + def resolve( %{arguments: arguments, context: context} = resolution, {api, resource, @@ -42,7 +146,9 @@ defmodule AshGraphql.Graphql.Resolver do opts = [ actor: Map.get(context, :actor), action: action, - verbose?: AshGraphql.Api.Info.debug?(api) + verbose?: AshGraphql.Api.Info.debug?(api), + authorize?: AshGraphql.Api.Info.authorize?(api), + tenant: Map.get(context, :tenant) ] filter = identity_filter(identity, resource, arguments) @@ -66,6 +172,7 @@ defmodule AshGraphql.Graphql.Resolver do api: api, tenant: Map.get(context, :tenant), authorize?: AshGraphql.Api.Info.authorize?(api), + tracer: AshGraphql.Api.Info.tracer(api), actor: Map.get(context, :actor) ], resource, @@ -96,6 +203,7 @@ defmodule AshGraphql.Graphql.Resolver do api: api, tenant: Map.get(context, :tenant), authorize?: AshGraphql.Api.Info.authorize?(api), + tracer: AshGraphql.Api.Info.tracer(api), actor: Map.get(context, :actor) ], resource, @@ -174,7 +282,9 @@ defmodule AshGraphql.Graphql.Resolver do opts = [ actor: Map.get(context, :actor), action: action, - verbose?: AshGraphql.Api.Info.debug?(api) + verbose?: AshGraphql.Api.Info.debug?(api), + authorize?: AshGraphql.Api.Info.authorize?(api), + tenant: Map.get(context, :tenant) ] query = @@ -188,6 +298,7 @@ defmodule AshGraphql.Graphql.Resolver do api: api, tenant: Map.get(context, :tenant), authorize?: AshGraphql.Api.Info.authorize?(api), + tracer: AshGraphql.Api.Info.tracer(api), actor: Map.get(context, :actor) ], resource, @@ -263,7 +374,9 @@ defmodule AshGraphql.Graphql.Resolver do opts = [ actor: Map.get(context, :actor), action: action, - verbose?: AshGraphql.Api.Info.debug?(api) + verbose?: AshGraphql.Api.Info.debug?(api), + authorize?: AshGraphql.Api.Info.authorize?(api), + tenant: Map.get(context, :tenant) ] pagination = Ash.Resource.Info.action(resource, action).pagination @@ -285,6 +398,7 @@ defmodule AshGraphql.Graphql.Resolver do api: api, tenant: Map.get(context, :tenant), authorize?: AshGraphql.Api.Info.authorize?(api), + tracer: AshGraphql.Api.Info.tracer(api), actor: Map.get(context, :actor) ], resource, @@ -880,6 +994,8 @@ defmodule AshGraphql.Graphql.Resolver do actor: Map.get(context, :actor), action: action, verbose?: AshGraphql.Api.Info.debug?(api), + authorize?: AshGraphql.Api.Info.authorize?(api), + tenant: Map.get(context, :tenant), upsert?: upsert? ] @@ -905,6 +1021,7 @@ defmodule AshGraphql.Graphql.Resolver do api: api, tenant: Map.get(context, :tenant), authorize?: AshGraphql.Api.Info.authorize?(api), + tracer: AshGraphql.Api.Info.tracer(api), actor: Map.get(context, :actor) ], resource, @@ -1028,7 +1145,9 @@ defmodule AshGraphql.Graphql.Resolver do opts = [ actor: Map.get(context, :actor), action: action, - verbose?: AshGraphql.Api.Info.debug?(api) + verbose?: AshGraphql.Api.Info.debug?(api), + authorize?: AshGraphql.Api.Info.authorize?(api), + tenant: Map.get(context, :tenant) ] changeset = @@ -1046,6 +1165,7 @@ defmodule AshGraphql.Graphql.Resolver do api: api, tenant: Map.get(context, :tenant), authorize?: AshGraphql.Api.Info.authorize?(api), + tracer: AshGraphql.Api.Info.tracer(api), actor: Map.get(context, :actor) ], resource, @@ -1181,7 +1301,13 @@ defmodule AshGraphql.Graphql.Resolver do |> add_root_errors(api, result) {:ok, initial} -> - opts = destroy_opts(api, context, action) + opts = [ + action: action, + verbose?: AshGraphql.Api.Info.debug?(api), + actor: Map.get(context, :actor), + authorize?: AshGraphql.Api.Info.authorize?(api), + tenant: Map.get(context, :tenant) + ] changeset = initial @@ -1264,9 +1390,7 @@ defmodule AshGraphql.Graphql.Resolver do defp something_went_wrong(resolution, e, api, stacktrace) do tracer = AshGraphql.Api.Info.tracer(api) - if tracer do - tracer.set_error(Ash.Error.to_ash_error(e)) - end + Ash.Tracer.set_error(tracer, e) uuid = log_exception(e, stacktrace) @@ -1703,7 +1827,17 @@ defmodule AshGraphql.Graphql.Resolver do already_expanded? ) - Ash.Type.embedded_type?(type) || Ash.Resource.Info.resource?(type) -> + Ash.Type.embedded_type?(type) || Ash.Resource.Info.resource?(type) || + (type in [Ash.Type.Struct, :struct] && constraints[:instance_of] && + (Ash.Type.embedded_type?(constraints[:instance_of]) || + Ash.Resource.Info.resource?(constraints[:instance_of]))) -> + type = + if type in [:struct, Ash.Type.Struct] do + constraints[:instance_of] + else + type + end + fields = if already_expanded? do selections @@ -1723,8 +1857,7 @@ defmodule AshGraphql.Graphql.Resolver do fields end - fields - |> resource_loads(type, resolution, load_opts, path) + resource_loads(fields, type, resolution, load_opts, path) type == Ash.Type.Union -> {global_selections, fragments} = @@ -1987,21 +2120,6 @@ defmodule AshGraphql.Graphql.Resolver do end) end - defp destroy_opts(api, context, action) do - if AshGraphql.Api.Info.authorize?(api) do - [ - actor: Map.get(context, :actor), - action: action, - verbose?: AshGraphql.Api.Info.debug?(api) - ] - else - [ - action: action, - verbose?: AshGraphql.Api.Info.debug?(api) - ] - end - end - defp add_root_errors(resolution, api, {:error, error_or_errors}) do do_root_errors(api, resolution, error_or_errors) end diff --git a/lib/resource/managed_relationship.ex b/lib/resource/managed_relationship.ex index a11e051..07aa089 100644 --- a/lib/resource/managed_relationship.ex +++ b/lib/resource/managed_relationship.ex @@ -24,43 +24,24 @@ defmodule AshGraphql.Resource.ManagedRelationship do type: :boolean, doc: """ If the managed_relationship has `on_lookup` behavior, this option determines whether or not the primary key is provided in the input object for looking up. - - This option is ignored if there is no `on_lookup`. """ ], lookup_identities: [ type: {:list, :atom}, doc: """ - If the managed_relationship has `on_lookup` behavior, this option determines which identities are provided in the input object for looking up. - - This option is ignored if there is no `on_lookup`. By default *all* identities are provided. + Determines which identities are provided in the input object for looking up, if there is `on_lookup` behavior. Defalts to the `use_identities` option. """ ], type_name: [ type: :atom, doc: """ The name of the input object that will be derived. Defaults to `___input` - - Because multiple actions could potentially be managing the same relationship, it isn't suficcient to - default to something like `__input`. Additionally, Ash doesn't expose resource - action names by default, meaning that there is no automatic way to ensure that all - of these have a default name that will always be unique. If you have multiple actions of the same - type that manage a relationship with an argument of the same name, you will get a compile-time error. """ ], types: [ type: :any, doc: """ A keyword list of field names to their graphql type identifiers. - - Since managed relationships can ultimately call multiple actions, there is the possibility - of field type conflicts. Use this to determine the type of fields and remove the conflict warnings. - - For `non_null` use `{:non_null, type}`, and for a list, use `{:array, type}`, for example: - - `{:non_null, {:array, {:non_null, :string}}}` for a non null list of non null strings. - - To *remove* a key from the input object, simply pass `nil` as the type. """ ] ] diff --git a/lib/resource/mutation.ex b/lib/resource/mutation.ex index 6c9467e..d8503ca 100644 --- a/lib/resource/mutation.ex +++ b/lib/resource/mutation.ex @@ -35,9 +35,7 @@ defmodule AshGraphql.Resource.Mutation do modify_resolution: [ type: :mfa, doc: """ - An MFA that will be called with the resolution, the changeset, and the result of the action as the first three arguments (followed by the arguments in the mfa). - Must return a new absinthe resolution. This can be used to implement things like setting cookies based on resource actions. A method of using resolution context - for that is documented here: https://hexdocs.pm/absinthe_plug/Absinthe.Plug.html#module-before-send + An MFA that will be called with the resolution, the query, and the result of the action as the first three arguments. See the [the guide](/documentation/topics/modifying-the-resolution.html) for more. """ ] ] @@ -56,9 +54,7 @@ defmodule AshGraphql.Resource.Mutation do identity: [ type: :atom, doc: """ - The identity to use to fetch the record to be updated. - - If no identity is required (e.g for a read action that already knows how to fetch the item to be updated), use `false`. + The identity to use to fetch the record to be updated. Use `false` if no identity is required. """ ], read_action: [ @@ -87,8 +83,7 @@ defmodule AshGraphql.Resource.Mutation do identity: [ type: :atom, doc: """ - The identity to use to fetch the record to be destroyed. - If no identity is required (e.g for a read action that already knows how to fetch the item to be updated), use `false`. + The identity to use to fetch the record to be destroyed. Use `false` if no identity is required. """ ] ] diff --git a/lib/resource/query.ex b/lib/resource/query.ex index 8ebcfdf..9a597da 100644 --- a/lib/resource/query.ex +++ b/lib/resource/query.ex @@ -29,11 +29,7 @@ defmodule AshGraphql.Resource.Query do type_name: [ type: :atom, doc: """ - Override the type name returned by this query. Must be set if the read action has `metadata`. - - To ignore any action metadata, set this to the same type the resource uses, or set `show_metadata` to `[]`. - To show metadata in the response, choose a new name here, like `:user_with_token` to get a response type that - includes the additional fields. + Override the type name returned by this query. Must be set if the read action has `metadata` that is not hidden via the `show_metadata` key. """ ], metadata_names: [ @@ -54,13 +50,7 @@ defmodule AshGraphql.Resource.Query do type: :boolean, default: false, doc: """ - Places the query in the `mutations` key instead. The use cases for this are likely very minimal. - - If you have a query that needs to modify the graphql context using `modify_resolution`, then you - should likely set this as well. A simple example might be a `log_in`, which could be a read - action on the user that accepts an email/password, and should then set some context in the graphql - inside of `modify_resolution`. Once in the context, you can see the guide referenced in `modify_resolution` - for more on setting the session or a cookie with an auth token. + Places the query in the `mutations` key instead. Not typically necessary, but is often paired with `as_mutation?`. See the [the guide](/documentation/topics/modifying-the-resolution.html) for more. """ ] ] @@ -80,12 +70,7 @@ defmodule AshGraphql.Resource.Query do modify_resolution: [ type: :mfa, doc: """ - An MFA that will be called with the resolution, the query, and the result of the action as the first three arguments (followed by the arguments in the mfa). - Must return a new absinthe resolution. This can be used to implement things like setting cookies based on resource actions. A method of using resolution context - for that is documented here: https://hexdocs.pm/absinthe_plug/Absinthe.Plug.html#module-before-send - - *Important* if you are modifying the context, then you should also set `as_mutation?` to true and represent - this in your graphql as a mutation. See `as_mutation?` for more. + An MFA that will be called with the resolution, the query, and the result of the action as the first three arguments. See the [the guide](/documentation/topics/modifying-the-resolution.html) for more. """ ] ] @@ -105,12 +90,7 @@ defmodule AshGraphql.Resource.Query do type: :boolean, default: false, doc: """ - If true, the graphql queries/resolvers for this resource will be built to honor the [relay specification](https://relay.dev/graphql/connections.htm). - - The two changes that are made currently are: - - * the type for the resource will implement the `Node` interface - * pagination over that resource will behave as a Connection. + If true, the graphql queries/resolvers for this resource will be built to honor the relay specification. See [the relay guide](/documentation/topics/relay.html) for more. """ ] ] diff --git a/lib/resource/resource.ex b/lib/resource/resource.ex index 38dd50c..c431e3d 100644 --- a/lib/resource/resource.ex +++ b/lib/resource/resource.ex @@ -47,6 +47,38 @@ defmodule AshGraphql.Resource do ] } + @action_schema [ + name: [ + type: :atom, + doc: "The name to use for the query.", + default: :get + ], + action: [ + type: :atom, + doc: "The action to use for the query.", + required: true + ] + ] + + defmodule Action do + @moduledoc "Represents a configured generic action" + defstruct [:type, :name, :action] + end + + @action %Spark.Dsl.Entity{ + name: :action, + schema: @action_schema, + args: [:name, :action], + describe: "Runs a generic action", + examples: [ + "action :check_status, :check_status" + ], + target: Action, + auto_set_fields: [ + type: :action + ] + } + @create %Spark.Dsl.Entity{ name: :create, schema: Mutation.create_schema(), @@ -106,7 +138,8 @@ defmodule AshGraphql.Resource do entities: [ @get, @read_one, - @list + @list, + @action ] } @@ -151,6 +184,15 @@ defmodule AshGraphql.Resource do 1.) provide the `:types` option to the `managed_relationship` constructor (see that option for more) 2.) define a custom type, with a custom input object (see the custom types guide), and use that custom type instead of `:map` 3.) change your actions to not have overlapping inputs with different types + + Since managed relationships can ultimately call multiple actions, there is the possibility + of field type conflicts. Use the `types` option to determine the type of fields and remove the conflict warnings. + + For `non_null` use `{:non_null, type}`, and for a list, use `{:array, type}`, for example: + + `{:non_null, {:array, {:non_null, :string}}}` for a non null list of non null strings. + + To *remove* a key from the input object, simply pass `nil` as the type. """ } @@ -195,7 +237,8 @@ defmodule AshGraphql.Resource do entities: [ @create, @update, - @destroy + @destroy, + @action ] } @@ -271,10 +314,7 @@ defmodule AshGraphql.Resource do keyset_field: [ type: :atom, doc: """ - If set, the keyset will be displayed on all read actions in this field. - - It will always be `nil` unless at least one of the read actions on a resource uses keyset pagination. - It will also be nil on any mutation results. + If set, the keyset will be displayed on all read actions in this field. It will be `nil` unless at least one of the read actions on a resource uses keyset pagination or it is the result of a mutation """ ], attribute_types: [ @@ -335,19 +375,6 @@ defmodule AshGraphql.Resource do @moduledoc """ This Ash resource extension adds configuration for exposing a resource in a graphql. - - - - ## DSL Documentation - - ### Index - - #{Spark.Dsl.Extension.doc_index(@sections)} - - ### Docs - - #{Spark.Dsl.Extension.doc(@sections)} - """ use Spark.Dsl.Extension, sections: @sections, transformers: @transformers, verifiers: @verifiers @@ -415,27 +442,49 @@ defmodule AshGraphql.Resource do if type do resource |> queries() - |> Enum.filter(&(&1.as_mutation? == as_mutations?)) - |> Enum.map(fn query -> - query_action = - Ash.Resource.Info.action(resource, query.action) || - raise "No such action #{query.action} on #{resource}" + |> Enum.filter(&(Map.get(&1, :as_mutation?, false) == as_mutations?)) + |> Enum.map(fn + %{type: :action, name: name, action: action} = query -> + query_action = + Ash.Resource.Info.action(resource, action) || + raise "No such action #{action} on #{resource}" - %Absinthe.Blueprint.Schema.FieldDefinition{ - arguments: args(query.type, resource, query_action, schema, query.identity), - identifier: query.name, - middleware: - action_middleware ++ - [ - {{AshGraphql.Graphql.Resolver, :resolve}, {api, resource, query}} - ], - complexity: {AshGraphql.Graphql.Resolver, :query_complexity}, - module: schema, - name: to_string(query.name), - description: Ash.Resource.Info.action(resource, query.action).description, - type: query_type(query, resource, query_action, type), - __reference__: ref(__ENV__) - } + %Absinthe.Blueprint.Schema.FieldDefinition{ + arguments: generic_action_args(query_action, resource, schema), + identifier: name, + middleware: + action_middleware ++ + [ + {{AshGraphql.Graphql.Resolver, :resolve}, {api, resource, query}} + ], + complexity: {AshGraphql.Graphql.Resolver, :query_complexity}, + module: schema, + name: to_string(name), + description: query_action.description, + type: generic_action_type(query_action, resource), + __reference__: ref(__ENV__) + } + + query -> + query_action = + Ash.Resource.Info.action(resource, query.action) || + raise "No such action #{query.action} on #{resource}" + + %Absinthe.Blueprint.Schema.FieldDefinition{ + arguments: args(query.type, resource, query_action, schema, query.identity), + identifier: query.name, + middleware: + action_middleware ++ + [ + {{AshGraphql.Graphql.Resolver, :resolve}, {api, resource, query}} + ], + complexity: {AshGraphql.Graphql.Resolver, :query_complexity}, + module: schema, + name: to_string(query.name), + description: Ash.Resource.Info.action(resource, query.action).description, + type: query_type(query, resource, query_action, type), + __reference__: ref(__ENV__) + } end) else [] @@ -448,6 +497,27 @@ defmodule AshGraphql.Resource do resource |> mutations() |> Enum.map(fn + %{type: :action, name: name, action: action} = query -> + query_action = + Ash.Resource.Info.action(resource, action) || + raise "No such action #{action} on #{resource}" + + %Absinthe.Blueprint.Schema.FieldDefinition{ + arguments: generic_action_args(query_action, resource, schema), + identifier: name, + middleware: + action_middleware ++ + [ + {{AshGraphql.Graphql.Resolver, :resolve}, {api, resource, query}} + ], + complexity: {AshGraphql.Graphql.Resolver, :query_complexity}, + module: schema, + name: to_string(name), + description: query_action.description, + type: generic_action_type(query_action, resource), + __reference__: ref(__ENV__) + } + %{type: :destroy} = mutation -> action = Ash.Resource.Info.action(resource, mutation.action) || @@ -836,39 +906,44 @@ defmodule AshGraphql.Resource do argument_names = AshGraphql.Resource.Info.argument_names(resource) attribute_fields = - if action.type == :destroy && !action.soft? do - [] - else - resource - |> Ash.Resource.Info.public_attributes() - |> Enum.filter(fn attribute -> - AshGraphql.Resource.Info.show_field?(resource, attribute.name) && - (is_nil(action.accept) || attribute.name in action.accept) && attribute.writable? - end) - |> Enum.map(fn attribute -> - allow_nil? = - attribute.allow_nil? || attribute.default != nil || type == :update || - attribute.generated? || - (type == :create && attribute.name in action.allow_nil_input) + cond do + action.type == :action -> + [] - explicitly_required = attribute.name in action.require_attributes + action.type == :destroy && !action.soft? -> + [] - field_type = - attribute.type - |> field_type(attribute, resource, true) - |> maybe_wrap_non_null(explicitly_required || not allow_nil?) + true -> + resource + |> Ash.Resource.Info.public_attributes() + |> Enum.filter(fn attribute -> + AshGraphql.Resource.Info.show_field?(resource, attribute.name) && + (is_nil(action.accept) || attribute.name in action.accept) && attribute.writable? + end) + |> Enum.map(fn attribute -> + allow_nil? = + attribute.allow_nil? || attribute.default != nil || type == :update || + attribute.generated? || + (type == :create && attribute.name in action.allow_nil_input) - name = field_names[attribute.name] || attribute.name + explicitly_required = attribute.name in action.require_attributes - %Absinthe.Blueprint.Schema.FieldDefinition{ - description: attribute.description, - identifier: attribute.name, - module: schema, - name: to_string(name), - type: field_type, - __reference__: ref(__ENV__) - } - end) + field_type = + attribute.type + |> field_type(attribute, resource, true) + |> maybe_wrap_non_null(explicitly_required || not allow_nil?) + + name = field_names[attribute.name] || attribute.name + + %Absinthe.Blueprint.Schema.FieldDefinition{ + description: attribute.description, + identifier: attribute.name, + module: schema, + name: to_string(name), + type: field_type, + __reference__: ref(__ENV__) + } + end) end argument_fields = @@ -1096,6 +1171,39 @@ defmodule AshGraphql.Resource do end end + defp generic_action_type(action, resource) do + fake_attribute = %{ + type: action.returns, + constraints: action.constraints, + allow_nil?: Map.get(action, :allow_nil?, false), + name: action.name + } + + fake_attribute.type + |> field_type(fake_attribute, resource, false) + |> maybe_wrap_non_null(argument_required?(fake_attribute)) + end + + defp generic_action_args(action, resource, schema) do + action.arguments + |> Enum.reject(& &1.private?) + |> Enum.map(fn argument -> + type = + argument.type + |> field_type(argument, resource, true) + |> maybe_wrap_non_null(argument_required?(argument)) + + %Absinthe.Blueprint.Schema.FieldDefinition{ + identifier: argument.name, + module: schema, + name: to_string(argument.name), + description: argument.description, + type: type, + __reference__: ref(__ENV__) + } + end) + end + defp args(action_type, resource, action, schema, identity \\ nil) defp args(:get, resource, action, schema, nil) do @@ -1791,8 +1899,11 @@ defmodule AshGraphql.Resource do end end) |> Enum.filter(fn identity -> - is_nil(managed_relationship.lookup_identities) || + if is_nil(managed_relationship.lookup_identities) do + identity.name in List.wrap(opts[:use_identities]) + else identity.name in managed_relationship.lookup_identities + end end) |> Enum.flat_map(fn identity -> identity @@ -2900,14 +3011,18 @@ defmodule AshGraphql.Resource do relay? = resource |> queries() - |> Enum.any?(& &1.relay?) + |> Enum.any?(&Map.get(&1, :relay?)) countable? = resource |> queries() - |> Enum.any?(fn query -> - action = Ash.Resource.Info.action(resource, query.action) - query.relay? && action.pagination && action.pagination.countable + |> Enum.any?(fn + %{relay?: true} = query -> + action = Ash.Resource.Info.action(resource, query.action) + action.pagination && action.pagination.countable + + _ -> + false end) if relay? do @@ -3119,7 +3234,7 @@ defmodule AshGraphql.Resource do resource |> AshGraphql.Resource.Info.queries() - |> Enum.filter(&(&1.type_name && &1.type_name != resource_type)) + |> Enum.filter(&(Map.get(&1, :type_name) && &1.type_name != resource_type)) |> Enum.map(fn query -> relay? = Map.get(query, :relay?) @@ -3161,7 +3276,7 @@ defmodule AshGraphql.Resource do relay? = resource |> queries() - |> Enum.any?(& &1.relay?) + |> Enum.any?(&Map.get(&1, :relay?)) interfaces = if relay? do @@ -3830,6 +3945,16 @@ defmodule AshGraphql.Resource do defp get_specific_field_type(Ash.Type.UUID, _, _, _), do: :id defp get_specific_field_type(Ash.Type.Float, _, _, _), do: :float + defp get_specific_field_type(Ash.Type.Struct, %{constraints: constraints}, resource, input?) do + type = + if !input? && constraints[:instance_of] && + Ash.Resource.Info.resource?(constraints[:instance_of]) do + AshGraphql.Resource.Info.type(constraints[:instance_of]) + end + + type || get_specific_field_type(Ash.Type.Map, %{constraints: constraints}, resource, input?) + end + defp get_specific_field_type(type, attribute, resource, _) do raise """ Could not determine graphql field type for #{inspect(type)} on #{inspect(resource)}.#{attribute.name} diff --git a/lib/resource/transformers/add_union_type_resolvers.ex b/lib/resource/transformers/add_union_type_resolvers.ex index bd4eefd..36dd26d 100644 --- a/lib/resource/transformers/add_union_type_resolvers.ex +++ b/lib/resource/transformers/add_union_type_resolvers.ex @@ -1,5 +1,6 @@ defmodule AshGraphql.Resource.Transformers.AddUnionTypeResolvers do - @moduledoc "Set the computation of resolving union types as functions" + # Set the computation of resolving union types as functions + @moduledoc false use Spark.Dsl.Transformer def after?(_), do: true diff --git a/lib/resource/transformers/require_keyset_for_relay_queries.ex b/lib/resource/transformers/require_keyset_for_relay_queries.ex index 4f31a6e..56c7805 100644 --- a/lib/resource/transformers/require_keyset_for_relay_queries.ex +++ b/lib/resource/transformers/require_keyset_for_relay_queries.ex @@ -1,7 +1,8 @@ defmodule AshGraphql.Resource.Transformers.RequireKeysetForRelayQueries do - @moduledoc "Ensures that all relay queries configure keyset pagination" - use Spark.Dsl.Transformer + # Ensures that all relay queries configure keyset pagination + @moduledoc false + use Spark.Dsl.Transformer alias Spark.Dsl.Transformer def after_compile?, do: true @@ -10,7 +11,7 @@ defmodule AshGraphql.Resource.Transformers.RequireKeysetForRelayQueries do dsl |> AshGraphql.Resource.Info.queries() |> Enum.each(fn query -> - if query.relay? do + if Map.get(query, :relay?) do action = Ash.Resource.Info.action(dsl, query.action) unless action.pagination && action.pagination.keyset? do diff --git a/lib/resource/transformers/require_pkey_delimiter.ex b/lib/resource/transformers/require_pkey_delimiter.ex index c8b8c03..5531b34 100644 --- a/lib/resource/transformers/require_pkey_delimiter.ex +++ b/lib/resource/transformers/require_pkey_delimiter.ex @@ -1,5 +1,7 @@ defmodule AshGraphql.Resource.Transformers.RequirePkeyDelimiter do - @moduledoc "Ensures that the resource has a primary key called `id`" + # Ensures that the resource has a primary key called `id` + @moduledoc false + use Spark.Dsl.Transformer alias Spark.Dsl.Transformer diff --git a/lib/resource/transformers/validate_actions.ex b/lib/resource/transformers/validate_actions.ex index 0fd9d73..ffbe799 100644 --- a/lib/resource/transformers/validate_actions.ex +++ b/lib/resource/transformers/validate_actions.ex @@ -1,5 +1,6 @@ defmodule AshGraphql.Resource.Transformers.ValidateActions do - @moduledoc "Ensures that all referenced actiosn exist" + # Ensures that all referenced actiosn exist + @moduledoc false use Spark.Dsl.Transformer alias Spark.Dsl.Transformer @@ -16,12 +17,24 @@ defmodule AshGraphql.Resource.Transformers.ValidateActions do %AshGraphql.Resource.Query{} -> :read + %AshGraphql.Resource.Action{} -> + nil + %AshGraphql.Resource.Mutation{type: type} -> type end available_actions = Transformer.get_entities(dsl, [:actions]) || [] + available_actions = + if type do + Enum.filter(available_actions, fn action -> + action.type == type + end) + else + available_actions + end + action = Enum.find(available_actions, fn action -> action.name == query_or_mutation.action diff --git a/lib/resource/transformers/validate_compatible_names.ex b/lib/resource/transformers/validate_compatible_names.ex index df29360..4d3df2e 100644 --- a/lib/resource/transformers/validate_compatible_names.ex +++ b/lib/resource/transformers/validate_compatible_names.ex @@ -1,5 +1,6 @@ defmodule AshGraphql.Resource.Transformers.ValidateCompatibleNames do - @moduledoc "Ensures that all field names are valid or remapped to something valid exist" + # Ensures that all field names are valid or remapped to something valid exist + @moduledoc false use Spark.Dsl.Transformer alias Spark.Dsl.Transformer diff --git a/lib/resource/verifiers/verify_query_metadata.ex b/lib/resource/verifiers/verify_query_metadata.ex index ea48079..8d4d6a7 100644 --- a/lib/resource/verifiers/verify_query_metadata.ex +++ b/lib/resource/verifiers/verify_query_metadata.ex @@ -1,5 +1,6 @@ defmodule AshGraphql.Resource.Verifiers.VerifyQueryMetadata do - @moduledoc "Ensures that queries for actions with metadata have a type set" + # Ensures that queries for actions with metadata have a type set + @moduledoc false use Spark.Dsl.Verifier alias Spark.Dsl.Transformer @@ -7,6 +8,7 @@ defmodule AshGraphql.Resource.Verifiers.VerifyQueryMetadata do def verify(dsl) do dsl |> AshGraphql.Resource.Info.queries() + |> Enum.reject(&(&1.type == :action)) |> Enum.each(fn query -> action = Ash.Resource.Info.action(dsl, query.action) show_metadata = query.show_metadata || Enum.map(Map.get(action, :metadata, []), & &1.name) diff --git a/mix.exs b/mix.exs index 40d6ad6..8871054 100644 --- a/mix.exs +++ b/mix.exs @@ -39,15 +39,16 @@ defmodule AshGraphql.MixProject do end defp extras() do - "documentation/**/*.md" + "documentation/**/*.{md,livemd,cheatmd}" |> Path.wildcard() |> Enum.map(fn path -> title = path |> Path.basename(".md") + |> Path.basename(".livemd") + |> Path.basename(".cheatmd") |> String.split(~r/[-_]/) - |> Enum.map(&String.capitalize/1) - |> Enum.join(" ") + |> Enum.map_join(" ", &capitalize/1) |> case do "F A Q" -> "FAQ" @@ -63,24 +64,29 @@ defmodule AshGraphql.MixProject do end) end - defp groups_for_extras() do - "documentation/*" - |> Path.wildcard() - |> Enum.map(fn folder -> - name = - folder - |> Path.basename() - |> String.split(~r/[-_]/) - |> Enum.map(&String.capitalize/1) - |> Enum.join(" ") - - {name, folder |> Path.join("**") |> Path.wildcard()} + defp capitalize(string) do + string + |> String.split(" ") + |> Enum.map(fn string -> + [hd | tail] = String.graphemes(string) + String.capitalize(hd) <> Enum.join(tail) end) end + defp groups_for_extras() do + [ + Tutorials: [ + ~r'documentation/tutorials' + ], + "How To": ~r'documentation/how_to', + Topics: ~r'documentation/topics', + DSLs: ~r'documentation/dsls' + ] + end + defp docs do [ - main: "AshGraphql", + main: "getting-started-with-graphql", source_ref: "v#{@version}", logo: "logos/small-logo.png", extra_section: "GUIDES", @@ -108,7 +114,17 @@ defmodule AshGraphql.MixProject do ], Introspection: [ AshGraphql.Resource.Info, - AshGraphql.Api.Info + AshGraphql.Api.Info, + AshGraphql.Resource, + AshGraphql.Api, + AshGraphql.Resource.Action, + AshGraphql.Resource.ManagedRelationship, + AshGraphql.Resource.Mutation, + AshGraphql.Resource.Query + ], + Errors: [ + AshGraphql.Error, + AshGraphql.Errors ], Miscellaneous: [ AshGraphql.Resource.Helpers @@ -140,18 +156,17 @@ defmodule AshGraphql.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:ash, ash_version("~> 2.11 and >= 2.11.8")}, - {:dataloader, "~> 1.0"}, + {:ash, ash_version("~> 2.14 and >= 2.14.17")}, {:absinthe_plug, "~> 1.4"}, {:absinthe, "~> 1.7"}, {:jason, "~> 1.2"}, {:ex_doc, "~> 0.22", only: [:dev, :test], runtime: false}, - {:ex_check, "~> 0.12.0", only: [:dev, :test]}, + {:ex_check, "~> 0.12", only: [:dev, :test]}, {:credo, ">= 0.0.0", only: [:dev, :test], runtime: false}, {:dialyxir, ">= 0.0.0", only: [:dev, :test], runtime: false}, {:sobelow, ">= 0.0.0", only: [:dev, :test], runtime: false}, - {:git_ops, "~> 2.5.1", only: [:dev, :test]}, - {:excoveralls, "~> 0.13.0", only: [:dev, :test]}, + {:git_ops, "~> 2.5", only: [:dev, :test]}, + {:excoveralls, "~> 0.13", only: [:dev, :test]}, {:mix_test_watch, "~> 1.0", only: :dev, runtime: false} ] end @@ -169,8 +184,16 @@ defmodule AshGraphql.MixProject do [ sobelow: "sobelow --skip", credo: "credo --strict", - docs: ["docs", "ash.replace_doc_links"], - "spark.formatter": "spark.formatter --extensions AshGraphql.Resource,AshGraphql.Api" + docs: [ + "spark.cheat_sheets", + "docs", + "ash.replace_doc_links", + "spark.cheat_sheets_in_search" + ], + "spark.formatter": "spark.formatter --extensions AshGraphql.Resource,AshGraphql.Api", + "spark.cheat_sheets_in_search": + "spark.cheat_sheets_in_search --extensions AshGraphql.Resource,AshGraphql.Api", + "spark.cheat_sheets": "spark.cheat_sheets --extensions AshGraphql.Resource,AshGraphql.Api" ] end end diff --git a/mix.lock b/mix.lock index a8606e9..d443803 100644 --- a/mix.lock +++ b/mix.lock @@ -1,47 +1,38 @@ %{ - "absinthe": {:hex, :absinthe, "1.7.1", "aca6f64994f0914628429ddbdfbf24212747b51780dae189dd98909da911757b", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c0c4dbd93881fa3bfbad255608234b104b877c2a901850c1fe8c53b408a72a57"}, + "absinthe": {:hex, :absinthe, "1.7.5", "a15054f05738e766f7cc7fd352887dfd5e61cec371fb4741cca37c3359ff74ac", [:mix], [{:dataloader, "~> 1.0.0 or ~> 2.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2.1", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "22a9a38adca26294ad0ee91226168f5d215b401efd770b8a1b8fd9c9b21ec316"}, "absinthe_plug": {:hex, :absinthe_plug, "1.5.8", "38d230641ba9dca8f72f1fed2dfc8abd53b3907d1996363da32434ab6ee5d6ab", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bbb04176647b735828861e7b2705465e53e2cf54ccf5a73ddd1ebd855f996e5a"}, - "ash": {:hex, :ash, "2.11.8", "f17d7032abdf7322c19ff32c231d695d6cc69d46c0803860c8cbe28cbbca7ee6", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: true]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: false]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:spark, ">= 1.1.20 and < 2.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:stream_data, "~> 0.5.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1dffdd3d16f5914b8ba45f2a6cdb9e7e584c4213d9ec25f517a0b91734fff907"}, + "ash": {:hex, :ash, "2.14.17", "d5b8f3a136d4ecd67645a0c539083c7ba0a35a388fce3f240f1cd16d06031b55", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: true]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: false]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:spark, ">= 1.1.20 and < 2.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:stream_data, "~> 0.5", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "334efc54934100c1437f4c869148bd29a0fdb4dc5b912b470fdeaf15a718de33"}, "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, - "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, "comparable": {:hex, :comparable, "1.0.0", "bb669e91cedd14ae9937053e5bcbc3c52bb2f22422611f43b6e38367d94a495f", [:mix], [{:typable, "~> 0.1", [hex: :typable, repo: "hexpm", optional: false]}], "hexpm", "277c11eeb1cd726e7cd41c6c199e7e52fa16ee6830b45ad4cdc62e51f62eb60c"}, "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, - "dataloader": {:hex, :dataloader, "1.0.10", "a42f07641b1a0572e0b21a2a5ae1be11da486a6790f3d0d14512d96ff3e3bbe9", [:mix], [{:ecto, ">= 3.4.3 and < 4.0.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "54cd70cec09addf4b2ace14cc186a283a149fd4d3ec5475b155951bf33cd963f"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, - "dialyxir": {:hex, :dialyxir, "1.2.0", "58344b3e87c2e7095304c81a9ae65cb68b613e28340690dfe1a5597fd08dec37", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "61072136427a851674cab81762be4dbeae7679f85b1272b6d25c3a839aff8463"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.31", "a93921cdc6b9b869f519213d5bc79d9e218ba768d7270d46fdcf1c01bacff9e2", [:mix], [], "hexpm", "317d367ee0335ef037a87e46c91a2269fef6306413f731e8ec11fc45a7efd059"}, + "dialyxir": {:hex, :dialyxir, "1.4.1", "a22ed1e7bd3a3e3f197b68d806ef66acb61ee8f57b3ac85fc5d57354c5482a93", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "84b795d6d7796297cca5a3118444b80c7d94f7ce247d49886e7c291e1ae49801"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.35", "437773ca9384edf69830e26e9e7b2e0d22d2596c4a6b17094a3b29f01ea65bb8", [:mix], [], "hexpm", "8652ba3cb85608d0d7aa2d21b45c6fad4ddc9a1f9a1f1b30ca3a246f0acc33f6"}, "ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"}, "elixir_make": {:hex, :elixir_make, "0.7.7", "7128c60c2476019ed978210c245badf08b03dbec4f24d05790ef791da11aa17c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5bc19fff950fad52bbe5f211b12db9ec82c6b34a9647da0c2224b8b8464c7e6c"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "ets": {:hex, :ets, "0.8.1", "8ff9bcda5682b98493f8878fc9dbd990e48d566cba8cce59f7c2a78130da29ea", [:mix], [], "hexpm", "6be41b50adb5bc5c43626f25ea2d0af1f4a242fb3fad8d53f0c67c20b78915cc"}, - "ex_check": {:hex, :ex_check, "0.12.0", "c0e2919ecc06afeaf62c52d64f3d91bd4bc7dd8deaac5f84becb6278888c967a", [:mix], [], "hexpm", "cfafa8ef97c2596d45a1f19b5794cb5c7f700f25d164d3c9f8d7ec17ee67cf42"}, - "ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"}, - "excoveralls": {:hex, :excoveralls, "0.13.4", "7b0baee01fe150ef81153e6ffc0fc68214737f54570dc257b3ca4da8e419b812", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "faae00b3eee35cdf0342c10b669a7c91f942728217d2a7c7f644b24d391e6190"}, + "ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"}, + "ex_check": {:hex, :ex_check, "0.15.0", "074b94c02de11c37bba1ca82ae5cc4926e6ccee862e57a485b6ba60fca2d8dc1", [:mix], [], "hexpm", "33848031a0c7e4209c3b4369ce154019788b5219956220c35ca5474299fb6a0e"}, + "ex_doc": {:hex, :ex_doc, "0.30.6", "5f8b54854b240a2b55c9734c4b1d0dd7bdd41f71a095d42a70445c03cf05a281", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bd48f2ddacf4e482c727f9293d9498e0881597eae6ddc3d9562bd7923375109f"}, + "excoveralls": {:hex, :excoveralls, "0.17.1", "83fa7906ef23aa7fc8ad7ee469c357a63b1b3d55dd701ff5b9ce1f72442b2874", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "95bc6fda953e84c60f14da4a198880336205464e75383ec0f570180567985ae0"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"}, - "git_ops": {:hex, :git_ops, "2.5.6", "dd01e0e4aedc69b532860ae72281902b00474f4f729b64193f5350daeec191e5", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "00ed67de1f83684424196823d66e0f15c26117ee2ea6a3174b84de5b030b4112"}, - "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, - "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "git_ops": {:hex, :git_ops, "2.6.0", "e0791ee1cf5db03f2c61b7ebd70e2e95cba2bb9b9793011f26609f22c0900087", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "b98fca849b18aaf490f4ac7d1dd8c6c469b0cc3e6632562d366cab095e666ffe"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, - "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, - "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "mix_test_watch": {:hex, :mix_test_watch, "1.1.0", "330bb91c8ed271fe408c42d07e0773340a7938d8a0d281d57a14243eae9dc8c3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "52b6b1c476cbb70fd899ca5394506482f12e5f6b0d6acff9df95c7f1e0812ec3"}, + "mix_test_watch": {:hex, :mix_test_watch, "1.1.1", "eee6fc570d77ad6851c7bc08de420a47fd1e449ef5ccfa6a77ef68b72e7e51ad", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "f82262b54dee533467021723892e15c3267349849f1f737526523ecba4e6baae"}, "nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.3.0", "9e18a119d9efc3370a3ef2a937bf0b24c088d9c4bf0ba9d7c3751d49d347d035", [:mix], [], "hexpm", "7977f183127a7cbe9346981e2f480dc04c55ffddaef746bd58debd566070eef8"}, - "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, "picosat_elixir": {:hex, :picosat_elixir, "0.2.3", "bf326d0f179fbb3b706bb2c15fbc367dacfa2517157d090fdfc32edae004c597", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f76c9db2dec9d2561ffaa9be35f65403d53e984e8cd99c832383b7ab78c16c66"}, "plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"}, "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, - "sobelow": {:hex, :sobelow, "0.12.2", "45f4d500e09f95fdb5a7b94c2838d6b26625828751d9f1127174055a78542cf5", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "2f0b617dce551db651145662b84c8da4f158e7abe049a76daaaae2282df01c5d"}, - "sourceror": {:hex, :sourceror, "0.12.3", "a2ad3a1a4554b486d8a113ae7adad5646f938cad99bf8bfcef26dc0c88e8fade", [:mix], [], "hexpm", "4d4e78010ca046524e8194ffc4683422f34a96f6b82901abbb45acc79ace0316"}, - "spark": {:hex, :spark, "1.1.21", "8d09983e628d26edf358e7e8c0fa922667a049c4cfea6895d4b0820fc9bc1261", [:mix], [{:nimble_options, "~> 0.5 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "588dda298d6ea0a1d3f06c7978145ee799281bf8bcb0b418b7dae4b3d0be2e59"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, - "stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"}, + "sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"}, + "sourceror": {:hex, :sourceror, "0.13.0", "c6ecc96ee3ae0e042e9082a9550a1989ea40182492dc29024a8d9d2b136e5014", [:mix], [], "hexpm", "d0a819491061cd26bfa4450d1c84301a410c19c1782a6577ce15853fc0e7e4e1"}, + "spark": {:hex, :spark, "1.1.36", "a4cc1168fe94c24c90fbb3521921c4d3f9ec86642f84adc6da5ae62426189edd", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.5 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "ce03b3167f85fdc4416c9c5cee4aac4cc0ca8715d115631b68669be5e2e890f7"}, + "stream_data": {:hex, :stream_data, "0.6.0", "e87a9a79d7ec23d10ff83eb025141ef4915eeb09d4491f79e52f2562b73e5f47", [:mix], [], "hexpm", "b92b5031b650ca480ced047578f1d57ea6dd563f5b57464ad274718c9c29501c"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, } diff --git a/test/generic_actions_test.exs b/test/generic_actions_test.exs new file mode 100644 index 0000000..1715db0 --- /dev/null +++ b/test/generic_actions_test.exs @@ -0,0 +1,51 @@ +defmodule AshGraphql.CreateTest do + use ExUnit.Case, async: false + + setup do + on_exit(fn -> + Application.delete_env(:ash_graphql, AshGraphql.Test.Api) + + AshGraphql.TestHelpers.stop_ets() + end) + end + + test "generic action queries can be run" do + resp = + """ + query { + postCount + } + """ + |> Absinthe.run(AshGraphql.Test.Schema) + + assert {:ok, result} = resp + + refute Map.has_key?(result, :errors) + + assert %{data: %{"postCount" => 0}} = result + end + + test "generic action mutations can be run" do + post = AshGraphql.Test.Api.create!(Ash.Changeset.new(AshGraphql.Test.Post, text: "foobar")) + + resp = + """ + mutation { + randomPost { + id + comments{ + id + } + } + } + """ + |> Absinthe.run(AshGraphql.Test.Schema) + + assert {:ok, result} = resp + + refute Map.has_key?(result, :errors) + post_id = post.id + + assert %{data: %{"randomPost" => %{"id" => ^post_id, "comments" => []}}} = result + end +end diff --git a/test/support/resources/post.ex b/test/support/resources/post.ex index 6a9632b..c76f85b 100644 --- a/test/support/resources/post.ex +++ b/test/support/resources/post.ex @@ -114,6 +114,7 @@ defmodule AshGraphql.Test.Post do list :keyset_paginated_posts, :keyset_paginated list :paginated_posts_without_limit, :paginated_without_limit list :paginated_posts_limit_not_required, :paginated_limit_not_required + action(:post_count, :count) end managed_relationships do @@ -149,6 +150,9 @@ defmodule AshGraphql.Test.Post do destroy :delete_post, :destroy destroy :delete_best_post, :destroy, read_action: :best_post, identity: false destroy :delete_post_with_error, :destroy_with_error + + # this is a mutation just for testing + action(:random_post, :random) end end @@ -176,6 +180,33 @@ defmodule AshGraphql.Test.Post do change(AshGraphql.Test.ForceChangeId) end + action :count, :integer do + argument(:published, :boolean) + + run(fn input, _ -> + query = + if input.arguments[:published] do + Ash.Query.filter(__MODULE__, published == true) + else + __MODULE__ + end + + input.api.count(query) + end) + end + + action :random, :struct do + constraints(instance_of: __MODULE__) + argument(:published, :boolean) + allow_nil? true + + run(fn input, _ -> + __MODULE__ + |> Ash.Query.limit(1) + |> input.api.read_one() + end) + end + create :create_confirm do argument(:confirmation, :string) validate(confirm(:text, :confirmation)) diff --git a/test/support/schema.ex b/test/support/schema.ex index 036ac31..6e0e1b4 100644 --- a/test/support/schema.ex +++ b/test/support/schema.ex @@ -27,8 +27,4 @@ defmodule AshGraphql.Test.Schema do value(:open, description: "The post is open") value(:closed, description: "The post is closed") end - - def plugins do - [Absinthe.Middleware.Dataloader | Absinthe.Plugin.defaults()] - end end