improvement: generic action support

docs: better docs
This commit is contained in:
Zach Daniel 2023-09-15 13:45:12 -04:00
parent 0ec762507f
commit 49c6534d73
24 changed files with 1111 additions and 229 deletions

View file

@ -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"}},

View file

@ -1,4 +1,6 @@
spark_locals_without_parens = [
action: 2,
action: 3,
allow_nil?: 1,
argument_names: 1,
as_mutation?: 1,

View file

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

View file

@ -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 `<action_type>_<resource>_<argument_name>_input` |
| `types` | ``any`` | | A keyword list of field names to their graphql type identifiers. |
### Introspection
Target: `AshGraphql.Resource.ManagedRelationship`

View file

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

View file

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

View file

@ -53,19 +53,6 @@ defmodule AshGraphql.Api do
@moduledoc """
The entrypoint for adding graphql behavior to an Ash API
<!--- ash-hq-hide-start --> <!--- -->
## DSL Documentation
### Index
#{Spark.Dsl.Extension.doc_index(@sections)}
### Docs
#{Spark.Dsl.Extension.doc(@sections)}
<!--- ash-hq-hide-stop --> <!--- -->
"""
require Ash.Api.Info

View file

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

View file

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

View file

@ -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 `<action_type>_<resource>_<argument_name>_input`
Because multiple actions could potentially be managing the same relationship, it isn't suficcient to
default to something like `<resource>_<relationship>_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.
"""
]
]

View file

@ -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.
"""
]
]

View file

@ -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.
"""
]
]

View file

@ -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.
<!--- ash-hq-hide-start --> <!--- -->
## DSL Documentation
### Index
#{Spark.Dsl.Extension.doc_index(@sections)}
### Docs
#{Spark.Dsl.Extension.doc(@sections)}
<!--- ash-hq-hide-stop --> <!--- -->
"""
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}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

71
mix.exs
View file

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

View file

@ -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"},
}

View file

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

View file

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

View file

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