mirror of
https://github.com/ash-project/ash_graphql.git
synced 2024-09-19 12:53:40 +12:00
feat: allow resources without types (#121)
* fix: regenerate spark formatter and cheatsheet Make CI pass again * feat: allow resources without types Ensure they only expose generic action queries. Add checks to ensure that either `type :resource_type` or `generate_object? false` is passed if it's needed. Close #119
This commit is contained in:
parent
234f2d6d61
commit
9674614b62
9 changed files with 145 additions and 70 deletions
|
@ -51,7 +51,7 @@ end
|
|||
|
||||
| Name | Type | Default | Docs |
|
||||
|------|------|---------|------|
|
||||
| [`type`](#graphql-type){: #graphql-type .spark-required} | `atom` | | The type to use for this entity in the graphql schema |
|
||||
| [`type`](#graphql-type){: #graphql-type } | `atom` | | The type to use for this entity in the graphql schema. If the resource doesn't have a type, it also needs to have `generate_object? false` and can only expose generic action queries. |
|
||||
| [`derive_filter?`](#graphql-derive_filter?){: #graphql-derive_filter? } | `boolean` | `true` | Set to false to disable the automatic generation of a filter input for read actions. |
|
||||
| [`derive_sort?`](#graphql-derive_sort?){: #graphql-derive_sort? } | `boolean` | `true` | Set to false to disable the automatic generation of a sort input for read actions. |
|
||||
| [`encode_primary_key?`](#graphql-encode_primary_key?){: #graphql-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 |
|
||||
|
|
|
@ -96,8 +96,7 @@ defmodule AshGraphql.Api do
|
|||
resources
|
||||
|> Enum.reject(&Ash.Resource.Info.embedded?/1)
|
||||
|> Enum.flat_map(fn resource ->
|
||||
if AshGraphql.Resource in Spark.extensions(resource) &&
|
||||
AshGraphql.Resource.Info.type(resource) do
|
||||
if AshGraphql.Resource in Spark.extensions(resource) do
|
||||
AshGraphql.Resource.type_definitions(resource, api, schema, relay_ids?) ++
|
||||
AshGraphql.Resource.mutation_types(resource, schema)
|
||||
else
|
||||
|
|
|
@ -135,7 +135,7 @@ defmodule AshGraphql.Resource.Info do
|
|||
|
||||
@doc "Wether or not an object should be generated, or if one with the type name for this resource should be used."
|
||||
def generate_object?(resource) do
|
||||
Extension.get_opt(resource, [:graphql], :generate_object?, nil)
|
||||
Extension.get_opt(resource, [:graphql], :generate_object?, true)
|
||||
end
|
||||
|
||||
@doc "Fields that may be filtered on"
|
||||
|
|
|
@ -281,8 +281,8 @@ defmodule AshGraphql.Resource do
|
|||
schema: [
|
||||
type: [
|
||||
type: :atom,
|
||||
required: true,
|
||||
doc: "The type to use for this entity in the graphql schema"
|
||||
doc:
|
||||
"The type to use for this entity in the graphql schema. If the resource doesn't have a type, it also needs to have `generate_object? false` and can only expose generic action queries."
|
||||
],
|
||||
derive_filter?: [
|
||||
type: :boolean,
|
||||
|
@ -499,71 +499,74 @@ defmodule AshGraphql.Resource do
|
|||
|
||||
@doc false
|
||||
def queries(api, resource, action_middleware, schema, relay_ids?, as_mutations? \\ false) do
|
||||
type = AshGraphql.Resource.Info.type(resource)
|
||||
resource
|
||||
|> queries()
|
||||
|> 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}"
|
||||
|
||||
if type do
|
||||
resource
|
||||
|> queries()
|
||||
|> 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: generic_action_args(query_action, resource, schema),
|
||||
identifier: name,
|
||||
middleware:
|
||||
action_middleware ++
|
||||
api_middleware(api) ++
|
||||
id_translation_middleware(query.relay_id_translations, relay_ids?) ++
|
||||
[
|
||||
{{AshGraphql.Graphql.Resolver, :resolve}, {api, resource, query, false}}
|
||||
],
|
||||
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__)
|
||||
}
|
||||
|
||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||
arguments: generic_action_args(query_action, resource, schema),
|
||||
identifier: name,
|
||||
middleware:
|
||||
action_middleware ++
|
||||
api_middleware(api) ++
|
||||
id_translation_middleware(query.relay_id_translations, relay_ids?) ++
|
||||
[
|
||||
{{AshGraphql.Graphql.Resolver, :resolve}, {api, resource, query, false}}
|
||||
],
|
||||
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}"
|
||||
|
||||
query ->
|
||||
query_action =
|
||||
Ash.Resource.Info.action(resource, query.action) ||
|
||||
raise "No such action #{query.action} on #{resource}"
|
||||
type =
|
||||
AshGraphql.Resource.Info.type(resource) ||
|
||||
raise """
|
||||
Resource #{inspect(resource)} is trying to define the query #{inspect(query.name)}
|
||||
which requires a GraphQL type to be defined.
|
||||
|
||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||
arguments:
|
||||
args(
|
||||
query.type,
|
||||
resource,
|
||||
query_action,
|
||||
schema,
|
||||
query.identity,
|
||||
query.hide_inputs,
|
||||
query
|
||||
),
|
||||
identifier: query.name,
|
||||
middleware:
|
||||
action_middleware ++
|
||||
api_middleware(api) ++
|
||||
id_translation_middleware(query.relay_id_translations, relay_ids?) ++
|
||||
[
|
||||
{{AshGraphql.Graphql.Resolver, :resolve}, {api, resource, query, relay_ids?}}
|
||||
],
|
||||
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
|
||||
[]
|
||||
end
|
||||
You should define the type of your resource with `type :my_resource_type`.
|
||||
"""
|
||||
|
||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||
arguments:
|
||||
args(
|
||||
query.type,
|
||||
resource,
|
||||
query_action,
|
||||
schema,
|
||||
query.identity,
|
||||
query.hide_inputs,
|
||||
query
|
||||
),
|
||||
identifier: query.name,
|
||||
middleware:
|
||||
action_middleware ++
|
||||
api_middleware(api) ++
|
||||
id_translation_middleware(query.relay_id_translations, relay_ids?) ++
|
||||
[
|
||||
{{AshGraphql.Graphql.Resolver, :resolve}, {api, resource, query, relay_ids?}}
|
||||
],
|
||||
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)
|
||||
end
|
||||
|
||||
# sobelow_skip ["DOS.StringToAtom"]
|
||||
|
@ -845,9 +848,20 @@ defmodule AshGraphql.Resource do
|
|||
@doc false
|
||||
# sobelow_skip ["DOS.StringToAtom"]
|
||||
def mutation_types(resource, schema) do
|
||||
resource_type = AshGraphql.Resource.Info.type(resource)
|
||||
|
||||
resource
|
||||
|> mutations()
|
||||
|> Enum.flat_map(fn mutation ->
|
||||
unless resource_type do
|
||||
raise """
|
||||
Resource #{inspect(resource)} is trying to define the mutation #{inspect(mutation.name)}
|
||||
which requires a GraphQL type to be defined.
|
||||
|
||||
You should define the type of your resource with `type :my_resource_type`.
|
||||
"""
|
||||
end
|
||||
|
||||
mutation = %{
|
||||
mutation
|
||||
| action:
|
||||
|
@ -868,7 +882,7 @@ defmodule AshGraphql.Resource do
|
|||
identifier: :result,
|
||||
module: schema,
|
||||
name: "result",
|
||||
type: AshGraphql.Resource.Info.type(resource),
|
||||
type: resource_type,
|
||||
__reference__: ref(__ENV__)
|
||||
},
|
||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||
|
@ -3601,7 +3615,22 @@ defmodule AshGraphql.Resource do
|
|||
actual_resource = Ash.Type.NewType.subtype_of(resource)
|
||||
|
||||
if generate_object?(resource) do
|
||||
type = AshGraphql.Resource.Info.type(resource)
|
||||
type =
|
||||
AshGraphql.Resource.Info.type(resource) ||
|
||||
raise """
|
||||
Resource #{inspect(resource)} needs to generate its GraphQL type but doesn't have a type
|
||||
configured in its `graphql` section.
|
||||
|
||||
To fix this do one of the following:
|
||||
|
||||
1) Define the type of your resource with `type :your_resource_type` to let Ash generate it.
|
||||
|
||||
2) Pass both `generate_object? false` and `type :your_resource_type` to manually define
|
||||
your resource type using Absinthe.
|
||||
|
||||
3) Pass only `generate_object? false` to skip the resource type entirely. This means that
|
||||
you can only use actions which don't require the type (e.g. `action` queries).
|
||||
"""
|
||||
|
||||
resource = actual_resource
|
||||
|
||||
|
|
|
@ -8,4 +8,19 @@ defmodule AshGraphql.ResourceTest do
|
|||
|
||||
assert nil == Absinthe.Schema.lookup_type(AshGraphql.Test.Schema, :no_object)
|
||||
end
|
||||
|
||||
test "resource with no type can execute generic queries" do
|
||||
resp =
|
||||
"""
|
||||
query NoObjectCount {
|
||||
noObjectCount
|
||||
}
|
||||
"""
|
||||
|> Absinthe.run(AshGraphql.Test.Schema)
|
||||
|
||||
assert {:ok, result} = resp
|
||||
|
||||
refute Map.has_key?(result, :errors)
|
||||
assert %{data: %{"noObjectCount" => [1, 2, 3, 4, 5]}} = result
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,6 +11,7 @@ defmodule AshGraphql.Test.Registry do
|
|||
entry(AshGraphql.Test.MapTypes)
|
||||
entry(AshGraphql.Test.MultitenantPostTag)
|
||||
entry(AshGraphql.Test.MultitenantTag)
|
||||
entry(AshGraphql.Test.NoGraphql)
|
||||
entry(AshGraphql.Test.NoObject)
|
||||
entry(AshGraphql.Test.NonIdPrimaryKey)
|
||||
entry(AshGraphql.Test.Post)
|
||||
|
|
16
test/support/resources/no_graphql.ex
Normal file
16
test/support/resources/no_graphql.ex
Normal file
|
@ -0,0 +1,16 @@
|
|||
defmodule AshGraphql.Test.NoGraphql do
|
||||
@moduledoc false
|
||||
|
||||
use Ash.Resource,
|
||||
data_layer: Ash.DataLayer.Ets
|
||||
|
||||
attributes do
|
||||
uuid_primary_key(:id)
|
||||
|
||||
attribute(:name, :string)
|
||||
end
|
||||
|
||||
relationships do
|
||||
belongs_to(:post, AshGraphql.Test.Post, allow_nil?: false)
|
||||
end
|
||||
end
|
|
@ -6,8 +6,21 @@ defmodule AshGraphql.Test.NoObject do
|
|||
extensions: [AshGraphql.Resource]
|
||||
|
||||
graphql do
|
||||
type :no_object
|
||||
generate_object? false
|
||||
|
||||
queries do
|
||||
action :no_object_count, :count
|
||||
end
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults([:read, :create])
|
||||
|
||||
action :count, {:array, :integer} do
|
||||
run(fn _input, _context ->
|
||||
{:ok, [1, 2, 3, 4, 5]}
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
|
|
|
@ -451,5 +451,7 @@ defmodule AshGraphql.Test.Post do
|
|||
manual(RelatedPosts)
|
||||
no_attributes?(true)
|
||||
end
|
||||
|
||||
has_one(:no_graphql, AshGraphql.Test.NoGraphql)
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue