mirror of
https://github.com/ash-project/ash_graphql.git
synced 2024-09-19 21:03:09 +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 |
|
| 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_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. |
|
| [`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 |
|
| [`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
|
resources
|
||||||
|> Enum.reject(&Ash.Resource.Info.embedded?/1)
|
|> Enum.reject(&Ash.Resource.Info.embedded?/1)
|
||||||
|> Enum.flat_map(fn resource ->
|
|> Enum.flat_map(fn resource ->
|
||||||
if AshGraphql.Resource in Spark.extensions(resource) &&
|
if AshGraphql.Resource in Spark.extensions(resource) do
|
||||||
AshGraphql.Resource.Info.type(resource) do
|
|
||||||
AshGraphql.Resource.type_definitions(resource, api, schema, relay_ids?) ++
|
AshGraphql.Resource.type_definitions(resource, api, schema, relay_ids?) ++
|
||||||
AshGraphql.Resource.mutation_types(resource, schema)
|
AshGraphql.Resource.mutation_types(resource, schema)
|
||||||
else
|
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."
|
@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
|
def generate_object?(resource) do
|
||||||
Extension.get_opt(resource, [:graphql], :generate_object?, nil)
|
Extension.get_opt(resource, [:graphql], :generate_object?, true)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "Fields that may be filtered on"
|
@doc "Fields that may be filtered on"
|
||||||
|
|
|
@ -281,8 +281,8 @@ defmodule AshGraphql.Resource do
|
||||||
schema: [
|
schema: [
|
||||||
type: [
|
type: [
|
||||||
type: :atom,
|
type: :atom,
|
||||||
required: true,
|
doc:
|
||||||
doc: "The type to use for this entity in the graphql schema"
|
"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?: [
|
derive_filter?: [
|
||||||
type: :boolean,
|
type: :boolean,
|
||||||
|
@ -499,71 +499,74 @@ defmodule AshGraphql.Resource do
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
def queries(api, resource, action_middleware, schema, relay_ids?, as_mutations? \\ false) do
|
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
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||||
resource
|
arguments: generic_action_args(query_action, resource, schema),
|
||||||
|> queries()
|
identifier: name,
|
||||||
|> Enum.filter(&(Map.get(&1, :as_mutation?, false) == as_mutations?))
|
middleware:
|
||||||
|> Enum.map(fn
|
action_middleware ++
|
||||||
%{type: :action, name: name, action: action} = query ->
|
api_middleware(api) ++
|
||||||
query_action =
|
id_translation_middleware(query.relay_id_translations, relay_ids?) ++
|
||||||
Ash.Resource.Info.action(resource, action) ||
|
[
|
||||||
raise "No such action #{action} on #{resource}"
|
{{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{
|
query ->
|
||||||
arguments: generic_action_args(query_action, resource, schema),
|
query_action =
|
||||||
identifier: name,
|
Ash.Resource.Info.action(resource, query.action) ||
|
||||||
middleware:
|
raise "No such action #{query.action} on #{resource}"
|
||||||
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 ->
|
type =
|
||||||
query_action =
|
AshGraphql.Resource.Info.type(resource) ||
|
||||||
Ash.Resource.Info.action(resource, query.action) ||
|
raise """
|
||||||
raise "No such action #{query.action} on #{resource}"
|
Resource #{inspect(resource)} is trying to define the query #{inspect(query.name)}
|
||||||
|
which requires a GraphQL type to be defined.
|
||||||
|
|
||||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
You should define the type of your resource with `type :my_resource_type`.
|
||||||
arguments:
|
"""
|
||||||
args(
|
|
||||||
query.type,
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||||
resource,
|
arguments:
|
||||||
query_action,
|
args(
|
||||||
schema,
|
query.type,
|
||||||
query.identity,
|
resource,
|
||||||
query.hide_inputs,
|
query_action,
|
||||||
query
|
schema,
|
||||||
),
|
query.identity,
|
||||||
identifier: query.name,
|
query.hide_inputs,
|
||||||
middleware:
|
query
|
||||||
action_middleware ++
|
),
|
||||||
api_middleware(api) ++
|
identifier: query.name,
|
||||||
id_translation_middleware(query.relay_id_translations, relay_ids?) ++
|
middleware:
|
||||||
[
|
action_middleware ++
|
||||||
{{AshGraphql.Graphql.Resolver, :resolve}, {api, resource, query, relay_ids?}}
|
api_middleware(api) ++
|
||||||
],
|
id_translation_middleware(query.relay_id_translations, relay_ids?) ++
|
||||||
complexity: {AshGraphql.Graphql.Resolver, :query_complexity},
|
[
|
||||||
module: schema,
|
{{AshGraphql.Graphql.Resolver, :resolve}, {api, resource, query, relay_ids?}}
|
||||||
name: to_string(query.name),
|
],
|
||||||
description: Ash.Resource.Info.action(resource, query.action).description,
|
complexity: {AshGraphql.Graphql.Resolver, :query_complexity},
|
||||||
type: query_type(query, resource, query_action, type),
|
module: schema,
|
||||||
__reference__: ref(__ENV__)
|
name: to_string(query.name),
|
||||||
}
|
description: Ash.Resource.Info.action(resource, query.action).description,
|
||||||
end)
|
type: query_type(query, resource, query_action, type),
|
||||||
else
|
__reference__: ref(__ENV__)
|
||||||
[]
|
}
|
||||||
end
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
# sobelow_skip ["DOS.StringToAtom"]
|
# sobelow_skip ["DOS.StringToAtom"]
|
||||||
|
@ -845,9 +848,20 @@ defmodule AshGraphql.Resource do
|
||||||
@doc false
|
@doc false
|
||||||
# sobelow_skip ["DOS.StringToAtom"]
|
# sobelow_skip ["DOS.StringToAtom"]
|
||||||
def mutation_types(resource, schema) do
|
def mutation_types(resource, schema) do
|
||||||
|
resource_type = AshGraphql.Resource.Info.type(resource)
|
||||||
|
|
||||||
resource
|
resource
|
||||||
|> mutations()
|
|> mutations()
|
||||||
|> Enum.flat_map(fn mutation ->
|
|> 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 = %{
|
||||||
mutation
|
mutation
|
||||||
| action:
|
| action:
|
||||||
|
@ -868,7 +882,7 @@ defmodule AshGraphql.Resource do
|
||||||
identifier: :result,
|
identifier: :result,
|
||||||
module: schema,
|
module: schema,
|
||||||
name: "result",
|
name: "result",
|
||||||
type: AshGraphql.Resource.Info.type(resource),
|
type: resource_type,
|
||||||
__reference__: ref(__ENV__)
|
__reference__: ref(__ENV__)
|
||||||
},
|
},
|
||||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||||
|
@ -3601,7 +3615,22 @@ defmodule AshGraphql.Resource do
|
||||||
actual_resource = Ash.Type.NewType.subtype_of(resource)
|
actual_resource = Ash.Type.NewType.subtype_of(resource)
|
||||||
|
|
||||||
if generate_object?(resource) do
|
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
|
resource = actual_resource
|
||||||
|
|
||||||
|
|
|
@ -8,4 +8,19 @@ defmodule AshGraphql.ResourceTest do
|
||||||
|
|
||||||
assert nil == Absinthe.Schema.lookup_type(AshGraphql.Test.Schema, :no_object)
|
assert nil == Absinthe.Schema.lookup_type(AshGraphql.Test.Schema, :no_object)
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -11,6 +11,7 @@ defmodule AshGraphql.Test.Registry do
|
||||||
entry(AshGraphql.Test.MapTypes)
|
entry(AshGraphql.Test.MapTypes)
|
||||||
entry(AshGraphql.Test.MultitenantPostTag)
|
entry(AshGraphql.Test.MultitenantPostTag)
|
||||||
entry(AshGraphql.Test.MultitenantTag)
|
entry(AshGraphql.Test.MultitenantTag)
|
||||||
|
entry(AshGraphql.Test.NoGraphql)
|
||||||
entry(AshGraphql.Test.NoObject)
|
entry(AshGraphql.Test.NoObject)
|
||||||
entry(AshGraphql.Test.NonIdPrimaryKey)
|
entry(AshGraphql.Test.NonIdPrimaryKey)
|
||||||
entry(AshGraphql.Test.Post)
|
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]
|
extensions: [AshGraphql.Resource]
|
||||||
|
|
||||||
graphql do
|
graphql do
|
||||||
type :no_object
|
|
||||||
generate_object? false
|
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
|
end
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
|
|
|
@ -451,5 +451,7 @@ defmodule AshGraphql.Test.Post do
|
||||||
manual(RelatedPosts)
|
manual(RelatedPosts)
|
||||||
no_attributes?(true)
|
no_attributes?(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
has_one(:no_graphql, AshGraphql.Test.NoGraphql)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue