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

View file

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

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

View file

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

View file

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

View file

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

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

View file

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