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:
Riccardo Binetti 2024-03-28 16:16:32 +01:00 committed by GitHub
parent 234f2d6d61
commit 9674614b62
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 145 additions and 70 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View file

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

View file

@ -451,5 +451,7 @@ defmodule AshGraphql.Test.Post do
manual(RelatedPosts)
no_attributes?(true)
end
has_one(:no_graphql, AshGraphql.Test.NoGraphql)
end
end