2020-08-14 09:39:59 +12:00
|
|
|
defmodule AshGraphql.Resource do
|
2020-08-15 02:20:47 +12:00
|
|
|
@moduledoc """
|
|
|
|
This Ash resource extension adds configuration for exposing a resource in a graphql.
|
|
|
|
|
|
|
|
See `graphql/1` for more information
|
|
|
|
"""
|
|
|
|
|
|
|
|
alias Ash.Dsl.Extension
|
|
|
|
alias Ash.Query.Aggregate
|
|
|
|
alias AshGraphql.Resource
|
|
|
|
alias AshGraphql.Resource.{Mutation, Query}
|
|
|
|
|
2020-08-14 09:39:59 +12:00
|
|
|
@get %Ash.Dsl.Entity{
|
|
|
|
name: :get,
|
|
|
|
args: [:name, :action],
|
|
|
|
describe: "A query to fetch a record by primary key",
|
|
|
|
examples: [
|
|
|
|
"get :get_post, :default"
|
|
|
|
],
|
2020-08-15 02:20:47 +12:00
|
|
|
schema: Query.get_schema(),
|
|
|
|
target: Query,
|
2020-08-14 09:39:59 +12:00
|
|
|
auto_set_fields: [
|
|
|
|
type: :get
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
@list %Ash.Dsl.Entity{
|
|
|
|
name: :list,
|
2020-08-15 02:20:47 +12:00
|
|
|
schema: Query.list_schema(),
|
2020-08-14 09:39:59 +12:00
|
|
|
args: [:name, :action],
|
|
|
|
describe: "A query to fetch a list of records",
|
|
|
|
examples: [
|
|
|
|
"list :list_posts, :default"
|
|
|
|
],
|
2020-08-15 02:20:47 +12:00
|
|
|
target: Query,
|
2020-08-14 09:39:59 +12:00
|
|
|
auto_set_fields: [
|
|
|
|
type: :list
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
@create %Ash.Dsl.Entity{
|
|
|
|
name: :create,
|
2020-08-15 02:20:47 +12:00
|
|
|
schema: Mutation.create_schema(),
|
2020-08-14 09:39:59 +12:00
|
|
|
args: [:name, :action],
|
|
|
|
describe: "A mutation to create a record",
|
|
|
|
examples: [
|
|
|
|
"create :create_post, :default"
|
|
|
|
],
|
2020-08-15 02:20:47 +12:00
|
|
|
target: Mutation,
|
2020-08-14 09:39:59 +12:00
|
|
|
auto_set_fields: [
|
|
|
|
type: :create
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
@update %Ash.Dsl.Entity{
|
|
|
|
name: :update,
|
2020-08-15 02:20:47 +12:00
|
|
|
schema: Mutation.update_schema(),
|
2020-08-14 09:39:59 +12:00
|
|
|
args: [:name, :action],
|
|
|
|
describe: "A mutation to update a record",
|
|
|
|
examples: [
|
|
|
|
"update :update_post, :default"
|
|
|
|
],
|
2020-08-15 02:20:47 +12:00
|
|
|
target: Mutation,
|
2020-08-14 09:39:59 +12:00
|
|
|
auto_set_fields: [
|
|
|
|
type: :update
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
@destroy %Ash.Dsl.Entity{
|
|
|
|
name: :destroy,
|
2020-08-15 02:20:47 +12:00
|
|
|
schema: Mutation.destroy_schema(),
|
2020-08-14 09:39:59 +12:00
|
|
|
args: [:name, :action],
|
|
|
|
describe: "A mutation to destroy a record",
|
|
|
|
examples: [
|
|
|
|
"destroy :destroy_post, :default"
|
|
|
|
],
|
2020-08-15 02:20:47 +12:00
|
|
|
target: Mutation,
|
2020-08-14 09:39:59 +12:00
|
|
|
auto_set_fields: [
|
|
|
|
type: :destroy
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
@queries %Ash.Dsl.Section{
|
|
|
|
name: :queries,
|
|
|
|
describe: """
|
|
|
|
Queries (read actions) to expose for the resource.
|
|
|
|
""",
|
|
|
|
entities: [
|
|
|
|
@get,
|
|
|
|
@list
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
@mutations %Ash.Dsl.Section{
|
|
|
|
name: :mutations,
|
|
|
|
describe: """
|
|
|
|
Mutations (create/update/destroy actions) to expose for the resource.
|
|
|
|
""",
|
|
|
|
entities: [
|
|
|
|
@create,
|
|
|
|
@update,
|
|
|
|
@destroy
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
@graphql %Ash.Dsl.Section{
|
|
|
|
name: :graphql,
|
|
|
|
describe: """
|
|
|
|
Configuration for a given resource in graphql
|
|
|
|
""",
|
|
|
|
schema: [
|
|
|
|
type: [
|
|
|
|
type: :atom,
|
|
|
|
required: true,
|
|
|
|
doc: "The type to use for this entity in the graphql schema"
|
|
|
|
]
|
|
|
|
],
|
|
|
|
sections: [
|
|
|
|
@queries,
|
|
|
|
@mutations
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
@transformers [
|
2020-08-15 02:20:47 +12:00
|
|
|
AshGraphql.Resource.Transformers.RequireIdPkey
|
2020-08-14 09:39:59 +12:00
|
|
|
]
|
|
|
|
|
2020-08-15 02:20:47 +12:00
|
|
|
use Extension, sections: [@graphql], transformers: @transformers
|
2020-08-14 09:39:59 +12:00
|
|
|
|
|
|
|
def queries(resource) do
|
2020-08-15 02:20:47 +12:00
|
|
|
Extension.get_entities(resource, [:graphql, :queries])
|
2020-08-14 09:39:59 +12:00
|
|
|
end
|
|
|
|
|
|
|
|
def mutations(resource) do
|
2020-08-15 02:20:47 +12:00
|
|
|
Extension.get_entities(resource, [:graphql, :mutations])
|
2020-08-14 09:39:59 +12:00
|
|
|
end
|
|
|
|
|
|
|
|
def type(resource) do
|
2020-08-15 02:20:47 +12:00
|
|
|
Extension.get_opt(resource, [:graphql], :type, nil)
|
2020-08-14 09:39:59 +12:00
|
|
|
end
|
|
|
|
|
|
|
|
@doc false
|
|
|
|
def queries(api, resource, schema) do
|
2020-08-15 02:20:47 +12:00
|
|
|
type = Resource.type(resource)
|
2020-08-14 09:39:59 +12:00
|
|
|
|
|
|
|
resource
|
|
|
|
|> queries()
|
|
|
|
|> Enum.map(fn query ->
|
2020-11-06 14:59:06 +13:00
|
|
|
query_action = Ash.Resource.action(resource, query.action, :read)
|
|
|
|
|
2020-08-14 09:39:59 +12:00
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
2020-11-18 20:14:33 +13:00
|
|
|
arguments: args(query.type, resource, query_action, query.identity),
|
2020-08-14 09:39:59 +12:00
|
|
|
identifier: query.name,
|
|
|
|
middleware: [
|
2020-11-18 20:14:33 +13:00
|
|
|
{{AshGraphql.Graphql.Resolver, :resolve}, {api, resource, query}}
|
2020-08-14 09:39:59 +12:00
|
|
|
],
|
|
|
|
module: schema,
|
|
|
|
name: to_string(query.name),
|
2020-11-06 14:59:06 +13:00
|
|
|
type: query_type(query.type, query_action, type)
|
2020-08-14 09:39:59 +12:00
|
|
|
}
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
2020-08-15 02:20:47 +12:00
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
2020-08-14 09:39:59 +12:00
|
|
|
@doc false
|
|
|
|
def mutations(api, resource, schema) do
|
|
|
|
resource
|
|
|
|
|> mutations()
|
|
|
|
|> Enum.map(fn
|
|
|
|
%{type: :destroy} = mutation ->
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
2020-11-18 20:14:33 +13:00
|
|
|
arguments: mutation_args(mutation, resource, schema),
|
2020-08-14 09:39:59 +12:00
|
|
|
identifier: mutation.name,
|
|
|
|
middleware: [
|
2020-11-18 20:14:33 +13:00
|
|
|
{{AshGraphql.Graphql.Resolver, :mutate}, {api, resource, mutation}}
|
2020-08-14 09:39:59 +12:00
|
|
|
],
|
|
|
|
module: schema,
|
|
|
|
name: to_string(mutation.name),
|
|
|
|
type: String.to_atom("#{mutation.name}_result")
|
|
|
|
}
|
|
|
|
|
|
|
|
%{type: :create} = mutation ->
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
|
|
arguments: [
|
|
|
|
%Absinthe.Blueprint.Schema.InputValueDefinition{
|
|
|
|
identifier: :input,
|
|
|
|
module: schema,
|
|
|
|
name: "input",
|
|
|
|
placement: :argument_definition,
|
|
|
|
type: String.to_atom("#{mutation.name}_input")
|
|
|
|
}
|
|
|
|
],
|
|
|
|
identifier: mutation.name,
|
|
|
|
middleware: [
|
2020-11-18 20:14:33 +13:00
|
|
|
{{AshGraphql.Graphql.Resolver, :mutate}, {api, resource, mutation}}
|
2020-08-14 09:39:59 +12:00
|
|
|
],
|
|
|
|
module: schema,
|
|
|
|
name: to_string(mutation.name),
|
|
|
|
type: String.to_atom("#{mutation.name}_result")
|
|
|
|
}
|
|
|
|
|
|
|
|
mutation ->
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
2020-11-18 20:14:33 +13:00
|
|
|
arguments:
|
|
|
|
mutation_args(mutation, resource, schema) ++
|
|
|
|
[
|
|
|
|
%Absinthe.Blueprint.Schema.InputValueDefinition{
|
|
|
|
identifier: :input,
|
|
|
|
module: schema,
|
|
|
|
name: "input",
|
|
|
|
placement: :argument_definition,
|
|
|
|
type: String.to_atom("#{mutation.name}_input")
|
|
|
|
}
|
|
|
|
],
|
2020-08-14 09:39:59 +12:00
|
|
|
identifier: mutation.name,
|
|
|
|
middleware: [
|
2020-11-18 20:14:33 +13:00
|
|
|
{{AshGraphql.Graphql.Resolver, :mutate}, {api, resource, mutation}}
|
2020-08-14 09:39:59 +12:00
|
|
|
],
|
|
|
|
module: schema,
|
|
|
|
name: to_string(mutation.name),
|
|
|
|
type: String.to_atom("#{mutation.name}_result")
|
|
|
|
}
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
2020-11-18 20:14:33 +13:00
|
|
|
defp mutation_args(%{identity: identity}, resource, _schema) when not is_nil(identity) do
|
|
|
|
resource
|
|
|
|
|> Ash.Resource.identities()
|
|
|
|
|> Enum.find(&(&1.name == identity))
|
|
|
|
|> Map.get(:keys)
|
|
|
|
|> Enum.map(fn key ->
|
|
|
|
attribute = Ash.Resource.attribute(resource, key)
|
|
|
|
|
|
|
|
%Absinthe.Blueprint.Schema.InputValueDefinition{
|
|
|
|
name: to_string(key),
|
|
|
|
identifier: key,
|
|
|
|
type: %Absinthe.Blueprint.TypeReference.NonNull{
|
|
|
|
of_type: field_type(attribute.type, attribute, resource)
|
|
|
|
},
|
|
|
|
description: attribute.description || ""
|
|
|
|
}
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
|
|
|
defp mutation_args(_, _, schema) do
|
|
|
|
[
|
|
|
|
%Absinthe.Blueprint.Schema.InputValueDefinition{
|
|
|
|
identifier: :id,
|
|
|
|
module: schema,
|
|
|
|
name: "id",
|
|
|
|
placement: :argument_definition,
|
|
|
|
type: :id
|
|
|
|
}
|
|
|
|
]
|
|
|
|
end
|
|
|
|
|
2020-08-14 09:39:59 +12:00
|
|
|
@doc false
|
2020-08-15 02:20:47 +12:00
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
2020-08-14 09:39:59 +12:00
|
|
|
def mutation_types(resource, schema) do
|
|
|
|
resource
|
|
|
|
|> mutations()
|
|
|
|
|> Enum.flat_map(fn mutation ->
|
2020-09-24 12:54:57 +12:00
|
|
|
mutation = %{
|
|
|
|
mutation
|
|
|
|
| action: Ash.Resource.action(resource, mutation.action, mutation.type)
|
|
|
|
}
|
|
|
|
|
2020-08-14 09:39:59 +12:00
|
|
|
description =
|
|
|
|
if mutation.type == :destroy do
|
|
|
|
"The record that was successfully deleted"
|
|
|
|
else
|
|
|
|
"The successful result of the mutation"
|
|
|
|
end
|
|
|
|
|
|
|
|
result = %Absinthe.Blueprint.Schema.ObjectTypeDefinition{
|
|
|
|
description: "The result of the #{inspect(mutation.name)} mutation",
|
|
|
|
fields: [
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
|
|
description: description,
|
|
|
|
identifier: :result,
|
|
|
|
module: schema,
|
|
|
|
name: "result",
|
2020-08-15 02:20:47 +12:00
|
|
|
type: Resource.type(resource)
|
2020-08-14 09:39:59 +12:00
|
|
|
},
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
|
|
description: "Any errors generated, if the mutation failed",
|
|
|
|
identifier: :errors,
|
|
|
|
module: schema,
|
|
|
|
name: "errors",
|
|
|
|
type: %Absinthe.Blueprint.TypeReference.List{
|
|
|
|
of_type: :mutation_error
|
|
|
|
}
|
|
|
|
}
|
|
|
|
],
|
|
|
|
identifier: String.to_atom("#{mutation.name}_result"),
|
|
|
|
module: schema,
|
|
|
|
name: Macro.camelize("#{mutation.name}_result")
|
|
|
|
}
|
|
|
|
|
|
|
|
if mutation.type == :destroy do
|
|
|
|
[result]
|
|
|
|
else
|
|
|
|
input = %Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
|
|
|
|
fields: mutation_fields(resource, schema, mutation),
|
|
|
|
identifier: String.to_atom("#{mutation.name}_input"),
|
|
|
|
module: schema,
|
|
|
|
name: Macro.camelize("#{mutation.name}_input")
|
|
|
|
}
|
|
|
|
|
|
|
|
[input, result]
|
|
|
|
end
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
2020-09-24 12:54:57 +12:00
|
|
|
defp mutation_fields(resource, schema, mutation) do
|
2020-08-14 09:39:59 +12:00
|
|
|
attribute_fields =
|
|
|
|
resource
|
2020-11-06 14:59:06 +13:00
|
|
|
|> Ash.Resource.public_attributes()
|
2020-09-24 12:54:57 +12:00
|
|
|
|> Enum.filter(fn attribute ->
|
|
|
|
is_nil(mutation.action.accept) || attribute.name in mutation.action.accept
|
|
|
|
end)
|
2020-08-14 09:39:59 +12:00
|
|
|
|> Enum.filter(& &1.writable?)
|
|
|
|
|> Enum.map(fn attribute ->
|
2020-11-06 14:59:06 +13:00
|
|
|
type = field_type(attribute.type, attribute, resource)
|
2020-08-14 09:39:59 +12:00
|
|
|
|
|
|
|
field_type =
|
2020-12-01 14:00:55 +13:00
|
|
|
if attribute.allow_nil? || attribute.default || mutation.type == :update do
|
2020-08-14 09:39:59 +12:00
|
|
|
type
|
|
|
|
else
|
|
|
|
%Absinthe.Blueprint.TypeReference.NonNull{
|
|
|
|
of_type: type
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
|
|
description: attribute.description,
|
|
|
|
identifier: attribute.name,
|
|
|
|
module: schema,
|
|
|
|
name: to_string(attribute.name),
|
|
|
|
type: field_type
|
|
|
|
}
|
|
|
|
end)
|
|
|
|
|
|
|
|
relationship_fields =
|
|
|
|
resource
|
2020-11-06 14:59:06 +13:00
|
|
|
|> Ash.Resource.public_relationships()
|
2020-08-14 09:39:59 +12:00
|
|
|
|> Enum.filter(fn relationship ->
|
2020-08-15 02:20:47 +12:00
|
|
|
Resource in Ash.Resource.extensions(relationship.destination)
|
2020-08-14 09:39:59 +12:00
|
|
|
end)
|
|
|
|
|> Enum.map(fn
|
|
|
|
%{cardinality: :one} = relationship ->
|
2020-11-12 16:57:48 +13:00
|
|
|
type =
|
|
|
|
if relationship.type == :belongs_to and relationship.required? do
|
|
|
|
%Absinthe.Blueprint.TypeReference.NonNull{
|
|
|
|
of_type: :id
|
|
|
|
}
|
|
|
|
else
|
|
|
|
:id
|
|
|
|
end
|
|
|
|
|
2020-08-14 09:39:59 +12:00
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
|
|
identifier: relationship.name,
|
|
|
|
module: schema,
|
|
|
|
name: to_string(relationship.name),
|
2020-11-12 16:57:48 +13:00
|
|
|
type: type
|
2020-08-14 09:39:59 +12:00
|
|
|
}
|
|
|
|
|
|
|
|
%{cardinality: :many} = relationship ->
|
2020-09-24 12:54:57 +12:00
|
|
|
case mutation.type do
|
2020-08-14 09:39:59 +12:00
|
|
|
:update ->
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
|
|
identifier: relationship.name,
|
|
|
|
module: schema,
|
|
|
|
name: to_string(relationship.name),
|
|
|
|
type: :relationship_change
|
|
|
|
}
|
|
|
|
|
|
|
|
:create ->
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
|
|
identifier: relationship.name,
|
|
|
|
module: schema,
|
|
|
|
name: to_string(relationship.name),
|
|
|
|
type: %Absinthe.Blueprint.TypeReference.List{
|
|
|
|
of_type: :id
|
|
|
|
}
|
|
|
|
}
|
|
|
|
end
|
|
|
|
end)
|
|
|
|
|
2020-12-02 18:07:15 +13:00
|
|
|
argument_fields =
|
|
|
|
Enum.map(mutation.action.arguments, fn argument ->
|
|
|
|
type =
|
|
|
|
if argument.allow_nil? do
|
|
|
|
%Absinthe.Blueprint.TypeReference.NonNull{
|
|
|
|
of_type: field_type(argument.type, argument, resource)
|
|
|
|
}
|
|
|
|
else
|
|
|
|
field_type(argument.type, argument, resource)
|
|
|
|
end
|
|
|
|
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
|
|
identifier: argument.name,
|
|
|
|
module: schema,
|
|
|
|
name: to_string(argument.name),
|
|
|
|
type: type
|
|
|
|
}
|
|
|
|
end)
|
|
|
|
|
|
|
|
attribute_fields ++ relationship_fields ++ argument_fields
|
2020-08-14 09:39:59 +12:00
|
|
|
end
|
|
|
|
|
2020-11-06 14:59:06 +13:00
|
|
|
defp query_type(:get, _, type), do: type
|
2020-08-15 02:20:47 +12:00
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
2020-11-06 14:59:06 +13:00
|
|
|
defp query_type(:list, action, type) do
|
|
|
|
if action.pagination do
|
|
|
|
String.to_atom("page_of_#{type}")
|
|
|
|
else
|
|
|
|
%Absinthe.Blueprint.TypeReference.NonNull{
|
|
|
|
of_type: %Absinthe.Blueprint.TypeReference.List{
|
|
|
|
of_type: %Absinthe.Blueprint.TypeReference.NonNull{
|
|
|
|
of_type: type
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
end
|
|
|
|
end
|
2020-08-14 09:39:59 +12:00
|
|
|
|
2020-11-18 20:14:33 +13:00
|
|
|
defp args(action_type, resource, action, identity \\ nil)
|
|
|
|
|
|
|
|
defp args(:get, _resource, _action, nil) do
|
2020-08-14 09:39:59 +12:00
|
|
|
[
|
|
|
|
%Absinthe.Blueprint.Schema.InputValueDefinition{
|
|
|
|
name: "id",
|
|
|
|
identifier: :id,
|
2020-11-12 16:53:53 +13:00
|
|
|
type: %Absinthe.Blueprint.TypeReference.NonNull{of_type: :id},
|
2020-08-14 09:39:59 +12:00
|
|
|
description: "The id of the record"
|
|
|
|
}
|
|
|
|
]
|
|
|
|
end
|
|
|
|
|
2020-11-18 20:14:33 +13:00
|
|
|
defp args(:get, resource, _action, identity) do
|
|
|
|
resource
|
|
|
|
|> Ash.Resource.identities()
|
|
|
|
|> Enum.find(&(&1.name == identity))
|
|
|
|
|> Map.get(:keys)
|
|
|
|
|> Enum.map(fn key ->
|
|
|
|
attribute = Ash.Resource.attribute(resource, key)
|
|
|
|
|
|
|
|
%Absinthe.Blueprint.Schema.InputValueDefinition{
|
|
|
|
name: to_string(key),
|
|
|
|
identifier: key,
|
|
|
|
type: %Absinthe.Blueprint.TypeReference.NonNull{
|
|
|
|
of_type: field_type(attribute.type, attribute, resource)
|
|
|
|
},
|
|
|
|
description: attribute.description || ""
|
|
|
|
}
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
|
|
|
defp args(:list, resource, action, _) do
|
2020-08-14 09:39:59 +12:00
|
|
|
[
|
|
|
|
%Absinthe.Blueprint.Schema.InputValueDefinition{
|
|
|
|
name: "filter",
|
|
|
|
identifier: :filter,
|
|
|
|
type: :string,
|
|
|
|
description: "A json encoded filter to apply"
|
2020-11-06 14:59:06 +13:00
|
|
|
},
|
|
|
|
%Absinthe.Blueprint.Schema.InputValueDefinition{
|
|
|
|
name: "sort",
|
|
|
|
identifier: :sort,
|
2020-11-12 16:41:54 +13:00
|
|
|
type: %Absinthe.Blueprint.TypeReference.List{
|
|
|
|
of_type: resource_sort_type(resource)
|
|
|
|
},
|
2020-11-06 14:59:06 +13:00
|
|
|
description: "How to sort the records in the response"
|
2020-08-14 09:39:59 +12:00
|
|
|
}
|
2020-11-06 14:59:06 +13:00
|
|
|
] ++
|
|
|
|
pagination_args(action)
|
|
|
|
end
|
|
|
|
|
|
|
|
defp pagination_args(action) do
|
|
|
|
if action.pagination do
|
|
|
|
max_message =
|
|
|
|
if action.pagination.max_page_size do
|
|
|
|
" Maximum #{action.pagination.max_page_size}"
|
|
|
|
else
|
|
|
|
""
|
|
|
|
end
|
|
|
|
|
|
|
|
limit_type =
|
2020-11-06 15:44:33 +13:00
|
|
|
if action.pagination.required? && is_nil(action.pagination.default_limit) do
|
2020-11-06 14:59:06 +13:00
|
|
|
%Absinthe.Blueprint.TypeReference.NonNull{
|
|
|
|
of_type: :integer
|
|
|
|
}
|
|
|
|
else
|
|
|
|
:integer
|
|
|
|
end
|
|
|
|
|
|
|
|
[
|
|
|
|
%Absinthe.Blueprint.Schema.InputValueDefinition{
|
|
|
|
name: "limit",
|
|
|
|
identifier: :limit,
|
|
|
|
type: limit_type,
|
2020-11-06 15:44:33 +13:00
|
|
|
default_value: action.pagination.default_limit,
|
2020-11-06 14:59:06 +13:00
|
|
|
description: "The number of records to return." <> max_message
|
|
|
|
}
|
|
|
|
] ++ keyset_pagination_args(action) ++ offset_pagination_args(action)
|
|
|
|
else
|
|
|
|
[]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
|
|
|
defp resource_sort_type(resource) do
|
|
|
|
String.to_atom(to_string(AshGraphql.Resource.type(resource)) <> "_sort_input")
|
|
|
|
end
|
|
|
|
|
|
|
|
defp keyset_pagination_args(action) do
|
|
|
|
if action.pagination.keyset? do
|
|
|
|
[
|
|
|
|
%Absinthe.Blueprint.Schema.InputValueDefinition{
|
|
|
|
name: "before",
|
|
|
|
identifier: :before,
|
|
|
|
type: :string,
|
|
|
|
description: "Show records before the specified keyset."
|
|
|
|
},
|
|
|
|
%Absinthe.Blueprint.Schema.InputValueDefinition{
|
|
|
|
name: "after",
|
|
|
|
identifier: :after,
|
|
|
|
type: :string,
|
|
|
|
description: "Show records after the specified keyset."
|
|
|
|
}
|
|
|
|
]
|
|
|
|
else
|
|
|
|
[]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp offset_pagination_args(action) do
|
|
|
|
if action.pagination.offset? do
|
|
|
|
[
|
|
|
|
%Absinthe.Blueprint.Schema.InputValueDefinition{
|
|
|
|
name: "offset",
|
|
|
|
identifier: :offset,
|
|
|
|
type: :integer,
|
|
|
|
description: "The number of records to skip."
|
|
|
|
}
|
|
|
|
]
|
|
|
|
else
|
|
|
|
[]
|
|
|
|
end
|
2020-08-14 09:39:59 +12:00
|
|
|
end
|
|
|
|
|
|
|
|
@doc false
|
2020-09-24 12:54:57 +12:00
|
|
|
def type_definitions(resource, api, schema) do
|
2020-08-14 09:39:59 +12:00
|
|
|
[
|
2020-09-24 12:54:57 +12:00
|
|
|
type_definition(resource, api, schema),
|
2020-11-06 14:59:06 +13:00
|
|
|
sort_input(resource, schema)
|
|
|
|
] ++ List.wrap(page_of(resource, schema)) ++ enum_definitions(resource, schema)
|
|
|
|
end
|
|
|
|
|
|
|
|
defp sort_input(resource, schema) do
|
|
|
|
%Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
|
2020-11-12 16:41:54 +13:00
|
|
|
fields: [
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
|
|
identifier: :order,
|
|
|
|
module: schema,
|
|
|
|
name: "order",
|
|
|
|
default_value: :asc,
|
|
|
|
type: :sort_order
|
|
|
|
},
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
|
|
identifier: :field,
|
|
|
|
module: schema,
|
|
|
|
name: "field",
|
|
|
|
type: resource_sort_field_type(resource)
|
|
|
|
}
|
|
|
|
],
|
|
|
|
identifier: resource_sort_type(resource),
|
2020-11-06 14:59:06 +13:00
|
|
|
module: schema,
|
2020-11-12 16:41:54 +13:00
|
|
|
name: resource |> resource_sort_type() |> to_string() |> Macro.camelize()
|
2020-11-06 14:59:06 +13:00
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2020-11-12 17:01:48 +13:00
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
2020-11-12 16:41:54 +13:00
|
|
|
defp resource_sort_field_type(resource) do
|
|
|
|
type = AshGraphql.Resource.type(resource)
|
|
|
|
String.to_atom(to_string(type) <> "_sort_field")
|
2020-11-06 14:59:06 +13:00
|
|
|
end
|
|
|
|
|
|
|
|
defp enum_definitions(resource, schema) do
|
2020-11-12 16:41:54 +13:00
|
|
|
atom_enums =
|
|
|
|
resource
|
|
|
|
|> Ash.Resource.public_attributes()
|
|
|
|
|> Enum.filter(&(&1.type == Ash.Type.Atom))
|
|
|
|
|> Enum.filter(&is_list(&1.constraints[:one_of]))
|
|
|
|
|> Enum.map(fn attribute ->
|
|
|
|
type_name = atom_enum_type(resource, attribute.name)
|
2020-11-06 14:59:06 +13:00
|
|
|
|
2020-11-12 16:41:54 +13:00
|
|
|
%Absinthe.Blueprint.Schema.EnumTypeDefinition{
|
|
|
|
module: schema,
|
|
|
|
name: type_name |> to_string() |> Macro.camelize(),
|
|
|
|
values:
|
|
|
|
Enum.map(attribute.constraints[:one_of], fn value ->
|
|
|
|
%Absinthe.Blueprint.Schema.EnumValueDefinition{
|
|
|
|
module: schema,
|
|
|
|
identifier: value,
|
|
|
|
name: String.upcase(to_string(value)),
|
|
|
|
value: value
|
|
|
|
}
|
|
|
|
end),
|
|
|
|
identifier: type_name
|
|
|
|
}
|
|
|
|
end)
|
|
|
|
|
|
|
|
attribute_sort_values = Enum.map(Ash.Resource.attributes(resource), & &1.name)
|
|
|
|
aggregate_sort_values = Enum.map(Ash.Resource.aggregates(resource), & &1.name)
|
|
|
|
|
|
|
|
sort_values = attribute_sort_values ++ aggregate_sort_values
|
|
|
|
|
|
|
|
sort_order = %Absinthe.Blueprint.Schema.EnumTypeDefinition{
|
|
|
|
module: schema,
|
|
|
|
name: resource |> resource_sort_field_type() |> to_string() |> Macro.camelize(),
|
|
|
|
identifier: resource_sort_field_type(resource),
|
|
|
|
values:
|
|
|
|
Enum.map(sort_values, fn sort_value ->
|
|
|
|
%Absinthe.Blueprint.Schema.EnumValueDefinition{
|
|
|
|
module: schema,
|
|
|
|
identifier: sort_value,
|
|
|
|
name: String.upcase(to_string(sort_value)),
|
|
|
|
value: sort_value
|
|
|
|
}
|
|
|
|
end)
|
|
|
|
}
|
|
|
|
|
|
|
|
[sort_order | atom_enums]
|
2020-08-14 09:39:59 +12:00
|
|
|
end
|
|
|
|
|
2020-08-15 02:20:47 +12:00
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
2020-08-14 09:39:59 +12:00
|
|
|
defp page_of(resource, schema) do
|
2020-08-15 02:20:47 +12:00
|
|
|
type = Resource.type(resource)
|
2020-08-14 09:39:59 +12:00
|
|
|
|
2020-11-06 14:59:06 +13:00
|
|
|
paginatable? =
|
|
|
|
resource
|
|
|
|
|> Ash.Resource.actions()
|
|
|
|
|> Enum.any?(fn action ->
|
|
|
|
action.type == :read && action.pagination
|
|
|
|
end)
|
|
|
|
|
|
|
|
if paginatable? do
|
|
|
|
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{
|
|
|
|
description: "A page of #{inspect(type)}",
|
|
|
|
fields: [
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
|
|
description: "The records contained in the page",
|
|
|
|
identifier: :results,
|
|
|
|
module: schema,
|
|
|
|
name: "results",
|
|
|
|
type: %Absinthe.Blueprint.TypeReference.List{
|
|
|
|
of_type: %Absinthe.Blueprint.TypeReference.NonNull{
|
|
|
|
of_type: type
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
|
|
description: "The count of records",
|
|
|
|
identifier: :count,
|
|
|
|
module: schema,
|
|
|
|
name: "count",
|
|
|
|
type: :integer
|
2020-08-14 09:39:59 +12:00
|
|
|
}
|
2020-11-06 14:59:06 +13:00
|
|
|
],
|
|
|
|
identifier: String.to_atom("page_of_#{type}"),
|
|
|
|
module: schema,
|
|
|
|
name: Macro.camelize("page_of_#{type}")
|
|
|
|
}
|
|
|
|
else
|
|
|
|
nil
|
|
|
|
end
|
2020-08-14 09:39:59 +12:00
|
|
|
end
|
|
|
|
|
2020-09-24 12:54:57 +12:00
|
|
|
defp type_definition(resource, api, schema) do
|
2020-08-15 02:20:47 +12:00
|
|
|
type = Resource.type(resource)
|
2020-08-14 09:39:59 +12:00
|
|
|
|
|
|
|
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{
|
|
|
|
description: Ash.Resource.description(resource),
|
2020-09-24 12:54:57 +12:00
|
|
|
fields: fields(resource, api, schema),
|
2020-08-14 09:39:59 +12:00
|
|
|
identifier: type,
|
|
|
|
module: schema,
|
|
|
|
name: Macro.camelize(to_string(type))
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2020-09-24 12:54:57 +12:00
|
|
|
defp fields(resource, api, schema) do
|
2020-11-06 14:59:06 +13:00
|
|
|
attributes(resource, schema) ++
|
|
|
|
relationships(resource, api, schema) ++
|
|
|
|
aggregates(resource, schema) ++
|
|
|
|
calculations(resource, schema)
|
2020-08-14 09:39:59 +12:00
|
|
|
end
|
|
|
|
|
2020-11-06 14:59:06 +13:00
|
|
|
defp attributes(resource, schema) do
|
2020-08-14 09:39:59 +12:00
|
|
|
resource
|
2020-11-06 14:59:06 +13:00
|
|
|
|> Ash.Resource.public_attributes()
|
2020-08-14 09:39:59 +12:00
|
|
|
|> Enum.map(fn
|
|
|
|
%{name: :id} = attribute ->
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
|
|
description: attribute.description,
|
|
|
|
identifier: :id,
|
|
|
|
module: schema,
|
|
|
|
name: "id",
|
2020-11-12 16:53:53 +13:00
|
|
|
type: %Absinthe.Blueprint.TypeReference.NonNull{of_type: :id}
|
2020-08-14 09:39:59 +12:00
|
|
|
}
|
|
|
|
|
|
|
|
attribute ->
|
2020-11-12 16:41:54 +13:00
|
|
|
field_type = field_type(attribute.type, attribute, resource)
|
|
|
|
|
|
|
|
field_type =
|
|
|
|
if attribute.allow_nil? do
|
|
|
|
field_type
|
|
|
|
else
|
|
|
|
%Absinthe.Blueprint.TypeReference.NonNull{
|
|
|
|
of_type: field_type
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2020-08-14 09:39:59 +12:00
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
|
|
description: attribute.description,
|
|
|
|
identifier: attribute.name,
|
|
|
|
module: schema,
|
|
|
|
name: to_string(attribute.name),
|
2020-11-12 16:41:54 +13:00
|
|
|
type: field_type
|
2020-08-14 09:39:59 +12:00
|
|
|
}
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
2020-08-15 02:20:47 +12:00
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
2020-11-06 14:59:06 +13:00
|
|
|
defp relationships(resource, api, schema) do
|
2020-08-14 09:39:59 +12:00
|
|
|
resource
|
2020-11-06 14:59:06 +13:00
|
|
|
|> Ash.Resource.public_relationships()
|
2020-08-14 09:39:59 +12:00
|
|
|
|> Enum.filter(fn relationship ->
|
2020-08-15 02:20:47 +12:00
|
|
|
Resource in Ash.Resource.extensions(relationship.destination)
|
2020-08-14 09:39:59 +12:00
|
|
|
end)
|
|
|
|
|> Enum.map(fn
|
|
|
|
%{cardinality: :one} = relationship ->
|
2020-11-12 16:53:53 +13:00
|
|
|
type =
|
|
|
|
if relationship.type == :belongs_to && relationship.required? do
|
|
|
|
%Absinthe.Blueprint.TypeReference.NonNull{
|
|
|
|
of_type: Resource.type(relationship.destination)
|
|
|
|
}
|
|
|
|
else
|
|
|
|
Resource.type(relationship.destination)
|
|
|
|
end
|
2020-08-14 09:39:59 +12:00
|
|
|
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
|
|
identifier: relationship.name,
|
|
|
|
module: schema,
|
|
|
|
name: to_string(relationship.name),
|
|
|
|
middleware: [
|
2020-09-24 12:54:57 +12:00
|
|
|
{{AshGraphql.Graphql.Resolver, :resolve_assoc}, {api, relationship}}
|
2020-08-14 09:39:59 +12:00
|
|
|
],
|
|
|
|
arguments: [],
|
|
|
|
type: type
|
|
|
|
}
|
|
|
|
|
|
|
|
%{cardinality: :many} = relationship ->
|
2020-11-06 14:59:06 +13:00
|
|
|
read_action = Ash.Resource.primary_action!(relationship.destination, :read)
|
|
|
|
|
2020-08-15 02:20:47 +12:00
|
|
|
type = Resource.type(relationship.destination)
|
2020-11-06 14:59:06 +13:00
|
|
|
|
|
|
|
query_type = %Absinthe.Blueprint.TypeReference.NonNull{
|
|
|
|
of_type: %Absinthe.Blueprint.TypeReference.List{
|
|
|
|
of_type: %Absinthe.Blueprint.TypeReference.NonNull{
|
|
|
|
of_type: type
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-08-14 09:39:59 +12:00
|
|
|
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
|
|
identifier: relationship.name,
|
|
|
|
module: schema,
|
|
|
|
name: to_string(relationship.name),
|
|
|
|
middleware: [
|
2020-09-24 12:54:57 +12:00
|
|
|
{{AshGraphql.Graphql.Resolver, :resolve_assoc}, {api, relationship}}
|
2020-08-14 09:39:59 +12:00
|
|
|
],
|
2020-11-06 14:59:06 +13:00
|
|
|
arguments: args(:list, relationship.destination, read_action),
|
2020-08-14 09:39:59 +12:00
|
|
|
type: query_type
|
|
|
|
}
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
2020-11-06 14:59:06 +13:00
|
|
|
defp aggregates(resource, schema) do
|
2020-08-14 09:39:59 +12:00
|
|
|
resource
|
2020-11-06 14:59:06 +13:00
|
|
|
|> Ash.Resource.public_aggregates()
|
2020-08-14 09:39:59 +12:00
|
|
|
|> Enum.map(fn aggregate ->
|
2020-08-15 02:20:47 +12:00
|
|
|
{:ok, type} = Aggregate.kind_to_type(aggregate.kind)
|
2020-08-14 09:39:59 +12:00
|
|
|
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
|
|
identifier: aggregate.name,
|
|
|
|
module: schema,
|
|
|
|
name: to_string(aggregate.name),
|
2020-11-06 14:59:06 +13:00
|
|
|
type: field_type(type, nil, resource)
|
|
|
|
}
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
|
|
|
defp calculations(resource, schema) do
|
|
|
|
resource
|
|
|
|
|> Ash.Resource.public_calculations()
|
|
|
|
|> Enum.map(fn calculation ->
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
|
|
identifier: calculation.name,
|
|
|
|
module: schema,
|
|
|
|
name: to_string(calculation.name),
|
|
|
|
type: field_type(calculation.type, nil, resource)
|
2020-08-14 09:39:59 +12:00
|
|
|
}
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
2020-11-06 14:59:06 +13:00
|
|
|
defp field_type({:array, type}, attribute, resource) do
|
|
|
|
new_attribute =
|
|
|
|
if attribute do
|
|
|
|
new_constraints = attribute.constraints[:items] || []
|
|
|
|
%{attribute | constraints: new_constraints, type: type}
|
|
|
|
end
|
2020-08-14 09:39:59 +12:00
|
|
|
|
2020-11-06 14:59:06 +13:00
|
|
|
if attribute.constraints[:nil_items?] do
|
|
|
|
%Absinthe.Blueprint.TypeReference.List{
|
|
|
|
of_type: field_type(type, new_attribute, resource)
|
|
|
|
}
|
|
|
|
else
|
|
|
|
%Absinthe.Blueprint.TypeReference.List{
|
|
|
|
of_type: %Absinthe.Blueprint.TypeReference.NonNull{
|
|
|
|
of_type: field_type(type, new_attribute, resource)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp field_type(type, attribute, resource) do
|
|
|
|
if Ash.Type.builtin?(type) do
|
|
|
|
do_field_type(type, attribute, resource)
|
|
|
|
else
|
|
|
|
type.graphql_type(attribute, resource)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp do_field_type(Ash.Type.Atom, %{constraints: constraints, name: name}, resource) do
|
|
|
|
if is_list(constraints[:one_of]) do
|
|
|
|
atom_enum_type(resource, name)
|
|
|
|
else
|
|
|
|
:string
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp do_field_type(Ash.Type.Map, _, _), do: :json
|
|
|
|
defp do_field_type(Ash.Type.Term, _, _), do: :string
|
|
|
|
defp do_field_type(Ash.Type.String, _, _), do: :string
|
|
|
|
defp do_field_type(Ash.Type.Integer, _, _), do: :integer
|
|
|
|
defp do_field_type(Ash.Type.Boolean, _, _), do: :boolean
|
|
|
|
defp do_field_type(Ash.Type.UUID, _, _), do: :string
|
|
|
|
defp do_field_type(Ash.Type.Date, _, _), do: :date
|
|
|
|
defp do_field_type(Ash.Type.UtcDatetime, _, _), do: :naive_datetime
|
|
|
|
|
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
|
|
|
defp atom_enum_type(resource, attribute_name) do
|
|
|
|
resource
|
|
|
|
|> AshGraphql.Resource.type()
|
|
|
|
|> to_string()
|
|
|
|
|> Kernel.<>("_")
|
|
|
|
|> Kernel.<>(to_string(attribute_name))
|
|
|
|
|> String.to_atom()
|
2020-08-14 09:39:59 +12:00
|
|
|
end
|
|
|
|
end
|