ash_graphql/lib/resource/resource.ex

1411 lines
38 KiB
Elixir
Raw Normal View History

2020-08-14 09:39:59 +12:00
defmodule AshGraphql.Resource do
2020-08-15 02:20:47 +12:00
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.
""",
2020-12-27 19:49:35 +13:00
examples: [
"""
queries do
get :get_post, :default
list :list_posts, :default
end
"""
],
2020-08-14 09:39:59 +12:00
entities: [
@get,
@list
]
}
@mutations %Ash.Dsl.Section{
name: :mutations,
describe: """
Mutations (create/update/destroy actions) to expose for the resource.
""",
2020-12-27 19:49:35 +13:00
examples: [
"""
mutations do
create :create_post, :default
update :update_post, :default
destroy :destroy_post, :default
end
"""
],
2020-08-14 09:39:59 +12:00
entities: [
@create,
@update,
@destroy
]
}
@graphql %Ash.Dsl.Section{
name: :graphql,
describe: """
Configuration for a given resource in graphql
""",
2020-12-27 19:49:35 +13:00
examples: [
"""
graphql do
type :post
queries do
get :get_post, :default
list :list_posts, :default
end
mutations do
create :create_post, :default
update :update_post, :default
destroy :destroy_post, :default
end
end
"""
],
2020-08-14 09:39:59 +12:00
schema: [
type: [
type: :atom,
required: true,
doc: "The type to use for this entity in the graphql schema"
2021-01-13 09:14:35 +13:00
],
primary_key_delimiter: [
type: :string,
doc:
"If a composite primary key exists, this must be set to determine the `id` field value"
2020-08-14 09:39:59 +12:00
]
],
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-12-27 19:49:35 +13:00
@sections [@graphql]
@moduledoc """
This Ash resource extension adds configuration for exposing a resource in a graphql.
# Table of Contents
#{Ash.Dsl.Extension.doc_index(@sections)}
#{Ash.Dsl.Extension.doc(@sections)}
"""
use Extension, sections: @sections, 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
2021-01-13 09:14:35 +13:00
def primary_key_delimiter(resource) do
Extension.get_opt(resource, [:graphql], :primary_key_delimiter, [], false)
end
def encode_primary_key(%resource{} = record) do
case Ash.Resource.primary_key(resource) do
[field] ->
Map.get(record, field)
keys ->
delimiter = primary_key_delimiter(resource)
[_ | concatenated_keys] =
keys
|> Enum.reverse()
|> Enum.reduce([], fn key, acc -> [delimiter, to_string(Map.get(record, key)), acc] end)
IO.iodata_to_binary(concatenated_keys)
end
end
def decode_primary_key(resource, value) do
case Ash.Resource.primary_key(resource) do
[_field] ->
{:ok, value}
fields ->
delimiter = primary_key_delimiter(resource)
parts = String.split(value, delimiter)
if Enum.count(parts) == Enum.count(fields) do
{:ok, Enum.zip(fields, parts)}
else
{:error, "Invalid primary key"}
end
end
end
2020-08-14 09:39:59 +12:00
@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
2020-12-30 09:11:16 +13:00
if type do
resource
|> queries()
|> Enum.map(fn query ->
query_action = Ash.Resource.action(resource, query.action, :read)
2020-12-30 09:11:16 +13:00
%Absinthe.Blueprint.Schema.FieldDefinition{
2021-01-13 09:14:35 +13:00
arguments: args(query.type, resource, query_action, schema, query.identity),
2020-12-30 09:11:16 +13:00
identifier: query.name,
middleware: [
{{AshGraphql.Graphql.Resolver, :resolve}, {api, resource, query}}
],
module: schema,
name: to_string(query.name),
type: query_type(query.type, query_action, type)
}
end)
else
[]
end
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
@doc false
def mutations(api, resource, schema) do
resource
|> mutations()
|> Enum.map(fn
%{type: :destroy} = mutation ->
%Absinthe.Blueprint.Schema.FieldDefinition{
arguments: mutation_args(mutation, resource, schema),
2020-08-14 09:39:59 +12:00
identifier: mutation.name,
middleware: [
{{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: [
{{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{
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: [
{{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
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
2021-01-13 09:14:35 +13:00
@doc false
# sobelow_skip ["DOS.StringToAtom"]
def embedded_type_input(resource, schema) do
attribute_fields =
resource
|> Ash.Resource.public_attributes()
|> Enum.filter(& &1.writable?)
|> Enum.map(fn attribute ->
type = field_type(attribute.type, attribute, resource)
%Absinthe.Blueprint.Schema.FieldDefinition{
description: attribute.description,
identifier: attribute.name,
module: schema,
name: to_string(attribute.name),
type: type
}
end)
name = AshGraphql.Resource.type(resource)
%Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
fields: attribute_fields,
identifier: String.to_atom("#{name}_input"),
module: schema,
name: Macro.camelize("#{name}_input")
}
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
|> 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 ->
2021-01-13 09:14:35 +13:00
type = field_type(attribute.type, attribute, resource, true)
2020-08-14 09:39:59 +12:00
field_type =
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
|> 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 ->
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),
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 =
mutation.action.arguments
|> Enum.reject(& &1.private?)
|> Enum.map(fn argument ->
2020-12-02 18:07:15 +13:00
type =
if argument.allow_nil? do
%Absinthe.Blueprint.TypeReference.NonNull{
2021-01-13 09:14:35 +13:00
of_type: field_type(argument.type, argument, resource, true)
2020-12-02 18:07:15 +13:00
}
else
2021-01-13 09:14:35 +13:00
field_type(argument.type, argument, resource, true)
2020-12-02 18:07:15 +13:00
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
defp query_type(:get, _, type), do: type
2020-08-15 02:20:47 +12:00
# sobelow_skip ["DOS.StringToAtom"]
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
2021-01-13 09:14:35 +13:00
defp args(action_type, resource, action, schema, identity \\ nil)
defp args(:get, resource, action, schema, 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"
}
] ++ read_args(resource, action, schema)
2020-08-14 09:39:59 +12:00
end
defp args(:get, resource, action, schema, 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{
2021-01-13 09:14:35 +13:00
of_type: field_type(attribute.type, attribute, resource, true)
},
description: attribute.description || ""
}
end)
|> Enum.concat(read_args(resource, action, schema))
end
2021-01-13 09:14:35 +13:00
defp args(:list, resource, action, schema, _) do
args =
case resource_filter_fields(resource, schema) do
[] ->
[]
_ ->
[
%Absinthe.Blueprint.Schema.InputValueDefinition{
name: "filter",
identifier: :filter,
type: resource_filter_type(resource),
description: "A filter to limit the results"
}
]
end
args =
case sort_values(resource) do
[] ->
args
_ ->
[
%Absinthe.Blueprint.Schema.InputValueDefinition{
name: "sort",
identifier: :sort,
type: %Absinthe.Blueprint.TypeReference.List{
of_type: resource_sort_type(resource)
},
description: "How to sort the records in the response"
}
| args
]
end
args ++ pagination_args(action) ++ read_args(resource, action, schema)
end
defp read_args(resource, action, schema) do
action.arguments
|> Enum.reject(& &1.private?)
|> Enum.map(fn argument ->
type =
if argument.allow_nil? do
%Absinthe.Blueprint.TypeReference.NonNull{
of_type: field_type(argument.type, argument, resource, true)
}
else
field_type(argument.type, argument, resource, true)
end
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: argument.name,
module: schema,
name: to_string(argument.name),
type: type
}
end)
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 =
if action.pagination.required? && is_nil(action.pagination.default_limit) do
%Absinthe.Blueprint.TypeReference.NonNull{
of_type: :integer
}
else
:integer
end
[
%Absinthe.Blueprint.Schema.InputValueDefinition{
name: "limit",
identifier: :limit,
type: limit_type,
default_value: action.pagination.default_limit,
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
2020-12-30 09:11:16 +13:00
# sobelow_skip ["DOS.StringToAtom"]
defp resource_filter_type(resource) do
String.to_atom(to_string(AshGraphql.Resource.type(resource)) <> "_filter_input")
end
# sobelow_skip ["DOS.StringToAtom"]
defp attribute_filter_field_type(resource, attribute) do
String.to_atom(
to_string(AshGraphql.Resource.type(resource)) <> "_filter_" <> to_string(attribute.name)
)
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
[
2021-01-13 09:14:35 +13:00
type_definition(resource, api, schema)
2020-12-30 09:11:16 +13:00
] ++
2021-01-13 09:14:35 +13:00
List.wrap(sort_input(resource, schema)) ++
List.wrap(filter_input(resource, schema)) ++
2020-12-30 09:11:16 +13:00
filter_field_types(resource, schema) ++
List.wrap(page_of(resource, schema)) ++ enum_definitions(resource, schema)
end
defp filter_field_types(resource, schema) do
filter_attribute_types(resource, schema) ++ filter_aggregate_types(resource, schema)
end
defp filter_attribute_types(resource, schema) do
resource
|> Ash.Resource.public_attributes()
|> Enum.flat_map(&filter_type(&1, resource, schema))
end
defp filter_aggregate_types(resource, schema) do
resource
|> Ash.Resource.public_aggregates()
|> Enum.flat_map(&filter_type(&1, resource, schema))
end
defp attribute_or_aggregate_type(%Ash.Resource.Attribute{type: type}, _resource), do: type
defp attribute_or_aggregate_type(%Ash.Resource.Aggregate{kind: kind, field: field}, resource) do
field_type =
if field do
Ash.Resource.attribute(resource, field).type
end
{:ok, aggregate_type} = Ash.Query.Aggregate.kind_to_type(kind, field_type)
aggregate_type
end
defp filter_type(attribute_or_aggregate, resource, schema) do
type = attribute_or_aggregate_type(attribute_or_aggregate, resource)
fields =
Enum.flat_map(Ash.Filter.builtin_operators(), fn operator ->
expressable_types =
Enum.filter(operator.types(), fn
[:any, {:array, type}] when is_atom(type) ->
true
2020-12-30 09:11:16 +13:00
:same ->
2020-12-30 09:11:16 +13:00
true
:any ->
true
2020-12-30 09:11:16 +13:00
[:any, type] when is_atom(type) ->
2020-12-30 09:11:16 +13:00
true
_ ->
false
2020-12-30 09:11:16 +13:00
end)
if Enum.any?(expressable_types, &(&1 == :same)) do
2020-12-30 09:11:16 +13:00
[
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: operator.name(),
module: schema,
name: to_string(operator.name()),
2021-01-13 09:14:35 +13:00
type: field_type(type, attribute_or_aggregate, resource, true)
2020-12-30 09:11:16 +13:00
}
]
else
type =
case Enum.at(expressable_types, 0) do
[_, {:array, :same}] ->
{:array, type}
[_, :same] ->
type
[_, :any] ->
Ash.Type.String
[_, type] when is_atom(type) ->
case Ash.Type.get_type(type) do
nil ->
nil
type ->
type
end
_ ->
nil
end
if type do
2020-12-31 12:00:36 +13:00
attribute_or_aggregate = constraints_to_item_constraints(type, attribute_or_aggregate)
2020-12-30 09:11:16 +13:00
[
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: operator.name(),
module: schema,
name: to_string(operator.name()),
2021-01-13 09:14:35 +13:00
type: field_type(type, attribute_or_aggregate, resource, true)
2020-12-30 09:11:16 +13:00
}
]
else
[]
end
end
end)
if fields == [] do
[]
else
identifier = attribute_filter_field_type(resource, attribute_or_aggregate)
[
%Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
identifier: identifier,
fields: fields,
module: schema,
name: identifier |> to_string() |> Macro.camelize()
}
]
end
end
2020-12-31 12:00:36 +13:00
defp constraints_to_item_constraints(
{:array, _},
%Ash.Resource.Attribute{
constraints: constraints,
allow_nil?: allow_nil?
} = attribute
) do
%{
attribute
| constraints: [items: constraints, nil_items?: allow_nil?]
}
end
defp constraints_to_item_constraints(_, attribute_or_aggregate), do: attribute_or_aggregate
defp sort_input(resource, schema) do
2021-01-13 09:14:35 +13:00
case sort_values(resource) do
[] ->
nil
_ ->
%Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
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-12 16:41:54 +13:00
module: schema,
2021-01-13 09:14:35 +13:00
name: resource |> resource_sort_type() |> to_string() |> Macro.camelize()
2020-11-12 16:41:54 +13:00
}
2021-01-13 09:14:35 +13:00
end
end
2020-12-30 09:11:16 +13:00
defp filter_input(resource, schema) do
2021-01-13 09:14:35 +13:00
case resource_filter_fields(resource, schema) do
[] ->
nil
fields ->
%Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
identifier: resource_filter_type(resource),
module: schema,
name: resource |> resource_filter_type() |> to_string() |> Macro.camelize(),
fields: fields
}
end
2020-12-30 09:11:16 +13:00
end
defp resource_filter_fields(resource, schema) do
boolean_filter_fields(resource, schema) ++
attribute_filter_fields(resource, schema) ++
relationship_filter_fields(resource, schema) ++ aggregate_filter_fields(resource, schema)
end
defp attribute_filter_fields(resource, schema) do
resource
|> Ash.Resource.public_attributes()
2021-01-13 09:14:35 +13:00
|> Enum.reject(fn
{:array, _} ->
true
_ ->
false
end)
|> Enum.reject(&Ash.Type.embedded_type?/1)
2020-12-30 09:11:16 +13:00
|> Enum.flat_map(fn attribute ->
[
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: attribute.name,
module: schema,
name: to_string(attribute.name),
type: attribute_filter_field_type(resource, attribute)
}
]
end)
end
defp aggregate_filter_fields(resource, schema) do
resource
|> Ash.Resource.public_aggregates()
|> Enum.flat_map(fn aggregate ->
[
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: aggregate.name,
module: schema,
name: to_string(aggregate.name),
type: attribute_filter_field_type(resource, aggregate)
}
]
end)
end
defp relationship_filter_fields(resource, schema) do
resource
|> Ash.Resource.public_relationships()
|> Enum.filter(fn relationship ->
AshGraphql.Resource.type(relationship.destination)
end)
|> Enum.map(fn relationship ->
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: relationship.name,
module: schema,
name: to_string(relationship.name),
type: resource_filter_type(relationship.destination)
}
end)
end
defp boolean_filter_fields(resource, schema) do
if Ash.DataLayer.can?(:boolean_filter, resource) do
[
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: :and,
module: schema,
name: "and",
type: %Absinthe.Blueprint.TypeReference.List{
of_type: %Absinthe.Blueprint.TypeReference.NonNull{
of_type: resource_filter_type(resource)
}
}
},
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: :or,
module: schema,
name: "or",
type: %Absinthe.Blueprint.TypeReference.List{
of_type: %Absinthe.Blueprint.TypeReference.NonNull{
of_type: resource_filter_type(resource)
}
}
}
]
else
[]
end
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")
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-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)
2021-01-13 09:14:35 +13:00
sort_values = sort_values(resource)
2020-11-12 16:41:54 +13:00
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
2021-01-13 09:14:35 +13:00
defp sort_values(resource) do
attribute_sort_values =
resource
|> Ash.Resource.attributes()
|> Enum.reject(fn
%{type: {:array, _}} ->
false
_ ->
true
end)
|> Enum.reject(&Ash.Type.embedded_type?(&1.type))
|> Enum.map(& &1.name)
aggregate_sort_values =
resource
|> Ash.Resource.aggregates()
|> Enum.reject(fn aggregate ->
case Ash.Query.Aggregate.kind_to_type(aggregate.kind, nil) do
{:ok, {:array, _}} ->
true
{:ok, type} ->
Ash.Type.embedded_type?(type)
_ ->
true
end
end)
|> Enum.map(& &1.name)
attribute_sort_values ++ aggregate_sort_values
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
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
}
],
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
2021-01-13 09:14:35 +13:00
def 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
attributes(resource, schema) ++
relationships(resource, api, schema) ++
aggregates(resource, schema) ++
calculations(resource, schema)
2020-08-14 09:39:59 +12:00
end
defp attributes(resource, schema) do
2021-01-13 09:14:35 +13:00
non_id_attributes =
resource
|> Ash.Resource.public_attributes()
|> Enum.reject(& &1.primary_key?)
|> Enum.map(fn 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
}
2021-01-13 09:14:35 +13:00
end)
pkey_fields =
case Ash.Resource.primary_key(resource) do
[field] ->
attribute = Ash.Resource.attribute(resource, field)
if attribute.private? do
non_id_attributes
else
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
[
%Absinthe.Blueprint.Schema.FieldDefinition{
description: attribute.description,
identifier: attribute.name,
module: schema,
name: to_string(attribute.name),
type: field_type
}
]
end
fields ->
added_pkey_fields =
if :id in fields do
[]
else
for field <- fields do
attribute = Ash.Resource.attribute(resource, field)
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
%Absinthe.Blueprint.Schema.FieldDefinition{
description: attribute.description,
identifier: attribute.name,
module: schema,
name: to_string(attribute.name),
type: field_type
}
end
end
[
%Absinthe.Blueprint.Schema.FieldDefinition{
description: "The primary key of the resource",
identifier: :id,
module: schema,
name: "id",
type: :id
}
] ++ added_pkey_fields
end
non_id_attributes ++ pkey_fields
2020-08-14 09:39:59 +12:00
end
2020-08-15 02:20:47 +12:00
# sobelow_skip ["DOS.StringToAtom"]
defp relationships(resource, api, schema) do
2020-08-14 09:39:59 +12:00
resource
|> 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 ->
read_action = Ash.Resource.primary_action!(relationship.destination, :read)
2020-08-15 02:20:47 +12:00
type = Resource.type(relationship.destination)
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
],
2021-01-13 09:14:35 +13:00
arguments: args(:list, relationship.destination, read_action, schema),
2020-08-14 09:39:59 +12:00
type: query_type
}
end)
end
defp aggregates(resource, schema) do
2020-08-14 09:39:59 +12:00
resource
|> Ash.Resource.public_aggregates()
2020-08-14 09:39:59 +12:00
|> Enum.map(fn aggregate ->
2020-12-30 09:11:16 +13:00
field_type =
if aggregate.field do
Ash.Resource.attribute(resource, aggregate.field).type
end
{:ok, type} = Aggregate.kind_to_type(aggregate.kind, field_type)
2020-08-14 09:39:59 +12:00
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: aggregate.name,
module: schema,
name: to_string(aggregate.name),
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
2021-01-13 09:14:35 +13:00
defp field_type(type, field, resource, input? \\ false)
defp field_type({:array, type}, %Ash.Resource.Aggregate{} = aggregate, resource, input?) do
2020-12-30 09:11:16 +13:00
%Absinthe.Blueprint.TypeReference.List{
2021-01-13 09:14:35 +13:00
of_type: field_type(type, aggregate, resource, input?)
2020-12-30 09:11:16 +13:00
}
end
2021-01-13 09:14:35 +13:00
defp field_type({:array, type}, attribute, resource, input?) do
2020-12-30 09:11:16 +13:00
new_constraints = attribute.constraints[:items] || []
new_attribute = %{attribute | constraints: new_constraints, type: type}
2020-08-14 09:39:59 +12:00
if attribute.constraints[:nil_items?] do
%Absinthe.Blueprint.TypeReference.List{
2021-01-13 09:14:35 +13:00
of_type: field_type(type, new_attribute, resource, input?)
}
else
%Absinthe.Blueprint.TypeReference.List{
of_type: %Absinthe.Blueprint.TypeReference.NonNull{
2021-01-13 09:14:35 +13:00
of_type: field_type(type, new_attribute, resource, input?)
}
}
end
end
2021-01-13 09:14:35 +13:00
# sobelow_skip ["DOS.BinToAtom"]
defp field_type(type, attribute, resource, input?) do
if Ash.Type.builtin?(type) do
do_field_type(type, attribute, resource)
else
2021-01-13 09:14:35 +13:00
if Ash.Type.embedded_type?(type) do
case type(type) do
nil ->
:json
type ->
if input? do
:"#{type}_input"
else
type
end
end
else
type.graphql_type(attribute, resource)
end
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