Packages
ash_graphql
0.12.4
1.10.0
1.9.4
1.9.3
1.9.2
1.9.1
1.9.0
1.8.5
1.8.4
1.8.3
1.8.2
1.8.1
1.8.0
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.0
1.5.1
1.5.0
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.4
1.3.3
1.3.2
1.3.1
1.3.0
1.2.1
1.2.0
1.1.1
1.1.0
1.0.1
1.0.0
1.0.0-rc.5
1.0.0-rc.4
retired
1.0.0-rc.3
1.0.0-rc.2
1.0.0-rc.1
1.0.0-rc.0
0.28.1
0.28.0
0.27.1
0.27.0
0.26.9
0.26.8
0.26.7
0.26.6
0.26.5
0.26.4
0.26.3
0.26.2
0.26.0
0.25.13
0.25.12
0.25.10
0.25.9
0.25.8
0.25.7
0.25.6
0.25.5
0.25.4
0.25.3
0.25.2
0.25.1
0.25.0
0.24.1
0.24.0
0.23.3
0.23.2
0.23.1
0.23.0
0.22.13
0.22.12
0.22.11
0.22.10
0.22.9
0.22.8
0.22.7
0.22.6
0.22.4
0.22.3
0.22.2
0.22.1
0.22.0
0.21.0
retired
0.20.5
0.20.4
0.20.3
0.20.2
0.20.1
0.20.0-rc.3
0.20.0-rc.2
0.20.0-rc.1
0.20.0-rc.0
0.19.0
0.18.0-rc0
0.17.5
0.17.5-rc0
0.17.4
0.17.2
0.17.1
0.17.0
0.16.28
0.16.27
0.16.26
0.16.25
0.16.24
0.16.23
0.16.22
0.16.21
0.16.20
0.16.18-rc5
0.16.18-rc4
0.16.18-rc3
0.16.18-rc2
0.16.18-rc1
0.16.18-rc0
0.16.17
0.16.16
0.16.15
0.16.14
0.16.13
0.16.12
0.16.11
0.16.10
0.16.9
0.16.8
0.16.7
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.10
0.15.9
0.15.8
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.1
0.14.0
0.13.1
0.13.0
0.12.5
0.12.4
0.12.3
0.12.1
0.12.0
0.10.0
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.0
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.0
0.4.0
0.3.2
0.3.1
0.3.0
0.2.1
0.2.0
0.1.3
0.1.2
The extension for building GraphQL APIs with Ash
Current section
Files
Jump to
Current section
Files
lib/resource/resource.ex
defmodule AshGraphql.Resource do
alias Ash.Dsl.Extension
alias Ash.Query.Aggregate
alias AshGraphql.Resource
alias AshGraphql.Resource.{Mutation, Query}
@get %Ash.Dsl.Entity{
name: :get,
args: [:name, :action],
describe: "A query to fetch a record by primary key",
examples: [
"get :get_post, :default"
],
schema: Query.get_schema(),
target: Query,
auto_set_fields: [
type: :get
]
}
@list %Ash.Dsl.Entity{
name: :list,
schema: Query.list_schema(),
args: [:name, :action],
describe: "A query to fetch a list of records",
examples: [
"list :list_posts, :default"
],
target: Query,
auto_set_fields: [
type: :list
]
}
@create %Ash.Dsl.Entity{
name: :create,
schema: Mutation.create_schema(),
args: [:name, :action],
describe: "A mutation to create a record",
examples: [
"create :create_post, :default"
],
target: Mutation,
auto_set_fields: [
type: :create
]
}
@update %Ash.Dsl.Entity{
name: :update,
schema: Mutation.update_schema(),
args: [:name, :action],
describe: "A mutation to update a record",
examples: [
"update :update_post, :default"
],
target: Mutation,
auto_set_fields: [
type: :update
]
}
@destroy %Ash.Dsl.Entity{
name: :destroy,
schema: Mutation.destroy_schema(),
args: [:name, :action],
describe: "A mutation to destroy a record",
examples: [
"destroy :destroy_post, :default"
],
target: Mutation,
auto_set_fields: [
type: :destroy
]
}
@queries %Ash.Dsl.Section{
name: :queries,
describe: """
Queries (read actions) to expose for the resource.
""",
examples: [
"""
queries do
get :get_post, :default
list :list_posts, :default
end
"""
],
entities: [
@get,
@list
]
}
@mutations %Ash.Dsl.Section{
name: :mutations,
describe: """
Mutations (create/update/destroy actions) to expose for the resource.
""",
examples: [
"""
mutations do
create :create_post, :default
update :update_post, :default
destroy :destroy_post, :default
end
"""
],
entities: [
@create,
@update,
@destroy
]
}
@graphql %Ash.Dsl.Section{
name: :graphql,
describe: """
Configuration for a given resource in graphql
""",
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
"""
],
schema: [
type: [
type: :atom,
required: true,
doc: "The type to use for this entity in the graphql schema"
],
primary_key_delimiter: [
type: :string,
doc:
"If a composite primary key exists, this must be set to determine the `id` field value"
]
],
sections: [
@queries,
@mutations
]
}
@transformers [
AshGraphql.Resource.Transformers.RequireIdPkey,
AshGraphql.Resource.Transformers.ValidateActions
]
@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
def queries(resource) do
Extension.get_entities(resource, [:graphql, :queries])
end
def mutations(resource) do
Extension.get_entities(resource, [:graphql, :mutations])
end
def type(resource) do
Extension.get_opt(resource, [:graphql], :type, nil)
end
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.Info.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.Info.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
@doc false
def queries(api, resource, schema) do
type = Resource.type(resource)
if type do
resource
|> queries()
|> Enum.map(fn query ->
query_action = Ash.Resource.Info.action(resource, query.action, :read)
%Absinthe.Blueprint.Schema.FieldDefinition{
arguments: args(query.type, resource, query_action, schema, query.identity),
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
end
# sobelow_skip ["DOS.StringToAtom"]
@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),
identifier: mutation.name,
middleware: [
{{AshGraphql.Graphql.Resolver, :mutate}, {api, resource, mutation}}
],
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}}
],
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")
}
],
identifier: mutation.name,
middleware: [
{{AshGraphql.Graphql.Resolver, :mutate}, {api, resource, mutation}}
],
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.Info.identities()
|> Enum.find(&(&1.name == identity))
|> Map.get(:keys)
|> Enum.map(fn key ->
attribute = Ash.Resource.Info.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
@doc false
# sobelow_skip ["DOS.StringToAtom"]
def mutation_types(resource, schema) do
resource
|> mutations()
|> Enum.flat_map(fn mutation ->
mutation = %{
mutation
| action: Ash.Resource.Info.action(resource, mutation.action, mutation.type)
}
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",
type: Resource.type(resource)
},
%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.action, mutation.type),
identifier: String.to_atom("#{mutation.name}_input"),
module: schema,
name: Macro.camelize("#{mutation.name}_input")
}
[input, result]
end
end)
end
@doc false
# sobelow_skip ["DOS.StringToAtom"]
def embedded_type_input(source_resource, attribute, resource, schema) do
create_action =
case attribute.constraints[:create_action] do
nil ->
Ash.Resource.Info.primary_action!(resource, :create)
name ->
Ash.Resource.Info.action(resource, name, :create)
end
update_action =
case attribute.constraints[:update_action] do
nil ->
Ash.Resource.Info.primary_action!(resource, :update)
name ->
Ash.Resource.Info.action(resource, name, :update)
end
fields =
mutation_fields(resource, schema, create_action, :create) ++
mutation_fields(resource, schema, update_action, :update)
fields =
fields
|> Enum.group_by(& &1.identifier)
# We only want one field per id. Right now we just take the first one
# If there are overlaps, and the field isn't `NonNull` in *all* cases, then
# we pick one and mark it explicitly as nullable (we unwrap the `NonNull`)
|> Enum.map(fn {_id, fields} ->
if Enum.all?(
fields,
&match?(%Absinthe.Blueprint.TypeReference.NonNull{}, &1.type)
) do
Enum.at(fields, 0)
else
fields
|> Enum.at(0)
|> case do
%{type: %Absinthe.Blueprint.TypeReference.NonNull{of_type: type}} = field ->
%{field | type: type}
field ->
field
end
end
end)
name = "#{AshGraphql.Resource.type(source_resource)}_#{attribute.name}_input"
%Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
fields: fields,
identifier: String.to_atom(name),
module: schema,
name: Macro.camelize(name)
}
end
defp mutation_fields(resource, schema, action, type) do
attribute_fields =
resource
|> Ash.Resource.Info.public_attributes()
|> Enum.filter(fn attribute ->
is_nil(action.accept) || attribute.name in action.accept
end)
|> Enum.filter(& &1.writable?)
|> Enum.map(fn attribute ->
type = field_type(attribute.type, attribute, resource, true)
field_type =
if attribute.allow_nil? || attribute.default || type == :update do
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.Info.public_relationships()
|> Enum.filter(fn relationship ->
Resource in Ash.Resource.Info.extensions(relationship.destination)
end)
|> Enum.map(fn
%{cardinality: :one} = relationship ->
type =
if relationship.type == :belongs_to and relationship.required? and type == :create do
%Absinthe.Blueprint.TypeReference.NonNull{
of_type: :id
}
else
:id
end
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: relationship.name,
module: schema,
name: to_string(relationship.name),
type: type
}
%{cardinality: :many} = relationship ->
case type do
: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)
argument_fields =
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)
attribute_fields ++ relationship_fields ++ argument_fields
end
defp query_type(:get, _, type), do: type
# 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
defp args(action_type, resource, action, schema, identity \\ nil)
defp args(:get, resource, action, schema, nil) do
[
%Absinthe.Blueprint.Schema.InputValueDefinition{
name: "id",
identifier: :id,
type: %Absinthe.Blueprint.TypeReference.NonNull{of_type: :id},
description: "The id of the record"
}
] ++ read_args(resource, action, schema)
end
defp args(:get, resource, action, schema, identity) do
resource
|> Ash.Resource.Info.identities()
|> Enum.find(&(&1.name == identity))
|> Map.get(:keys)
|> Enum.map(fn key ->
attribute = Ash.Resource.Info.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, true)
},
description: attribute.description || ""
}
end)
|> Enum.concat(read_args(resource, action, schema))
end
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
# 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
end
@doc false
def type_definitions(resource, api, schema) do
[
type_definition(resource, api, schema)
] ++
List.wrap(sort_input(resource, schema)) ++
List.wrap(filter_input(resource, schema)) ++
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.Info.public_attributes()
|> Enum.flat_map(&filter_type(&1, resource, schema))
end
defp filter_aggregate_types(resource, schema) do
resource
|> Ash.Resource.Info.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.Info.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
:same ->
true
:any ->
true
[:any, type] when is_atom(type) ->
true
_ ->
false
end)
if Enum.any?(expressable_types, &(&1 == :same)) do
[
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: operator.name(),
module: schema,
name: to_string(operator.name()),
type: field_type(type, attribute_or_aggregate, resource, true)
}
]
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
attribute_or_aggregate = constraints_to_item_constraints(type, attribute_or_aggregate)
[
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: operator.name(),
module: schema,
name: to_string(operator.name()),
type: field_type(type, attribute_or_aggregate, resource, true)
}
]
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
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
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),
module: schema,
name: resource |> resource_sort_type() |> to_string() |> Macro.camelize()
}
end
end
defp filter_input(resource, schema) do
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
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.Info.public_attributes()
|> Enum.reject(fn
{:array, _} ->
true
_ ->
false
end)
|> Enum.reject(&Ash.Type.embedded_type?/1)
|> 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.Info.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.Info.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
# sobelow_skip ["DOS.StringToAtom"]
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
atom_enums =
resource
|> Ash.Resource.Info.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)
%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)
sort_values = sort_values(resource)
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]
end
defp sort_values(resource) do
attribute_sort_values =
resource
|> Ash.Resource.Info.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.Info.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
# sobelow_skip ["DOS.StringToAtom"]
defp page_of(resource, schema) do
type = Resource.type(resource)
paginatable? =
resource
|> Ash.Resource.Info.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
}
],
identifier: String.to_atom("page_of_#{type}"),
module: schema,
name: Macro.camelize("page_of_#{type}")
}
else
nil
end
end
def type_definition(resource, api, schema) do
type = Resource.type(resource)
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{
description: Ash.Resource.Info.description(resource),
fields: fields(resource, api, schema),
identifier: type,
module: schema,
name: Macro.camelize(to_string(type))
}
end
defp fields(resource, api, schema) do
attributes(resource, schema) ++
relationships(resource, api, schema) ++
aggregates(resource, schema) ++
calculations(resource, schema)
end
defp attributes(resource, schema) do
non_id_attributes =
resource
|> Ash.Resource.Info.public_attributes()
|> Enum.reject(& &1.primary_key?)
|> Enum.map(fn attribute ->
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)
pkey_fields =
case Ash.Resource.Info.primary_key(resource) do
[field] ->
attribute = Ash.Resource.Info.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.Info.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
end
# sobelow_skip ["DOS.StringToAtom"]
defp relationships(resource, api, schema) do
resource
|> Ash.Resource.Info.public_relationships()
|> Enum.filter(fn relationship ->
Resource in Ash.Resource.Info.extensions(relationship.destination)
end)
|> Enum.map(fn
%{cardinality: :one} = relationship ->
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
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: relationship.name,
module: schema,
name: to_string(relationship.name),
middleware: [
{{AshGraphql.Graphql.Resolver, :resolve_assoc}, {api, relationship}}
],
arguments: [],
type: type
}
%{cardinality: :many} = relationship ->
read_action = Ash.Resource.Info.primary_action!(relationship.destination, :read)
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
}
}
}
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: relationship.name,
module: schema,
name: to_string(relationship.name),
middleware: [
{{AshGraphql.Graphql.Resolver, :resolve_assoc}, {api, relationship}}
],
arguments: args(:list, relationship.destination, read_action, schema),
type: query_type
}
end)
end
defp aggregates(resource, schema) do
resource
|> Ash.Resource.Info.public_aggregates()
|> Enum.map(fn aggregate ->
field_type =
if aggregate.field do
Ash.Resource.Info.attribute(resource, aggregate.field).type
end
{:ok, type} = Aggregate.kind_to_type(aggregate.kind, field_type)
%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.Info.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)
}
end)
end
defp field_type(type, field, resource, input? \\ false)
defp field_type({:array, type}, %Ash.Resource.Aggregate{} = aggregate, resource, input?) do
%Absinthe.Blueprint.TypeReference.List{
of_type: field_type(type, aggregate, resource, input?)
}
end
defp field_type({:array, type}, attribute, resource, input?) do
new_constraints = attribute.constraints[:items] || []
new_attribute = %{attribute | constraints: new_constraints, type: type}
if attribute.constraints[:nil_items?] do
%Absinthe.Blueprint.TypeReference.List{
of_type: field_type(type, new_attribute, resource, input?)
}
else
%Absinthe.Blueprint.TypeReference.List{
of_type: %Absinthe.Blueprint.TypeReference.NonNull{
of_type: field_type(type, new_attribute, resource, input?)
}
}
end
end
# 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
if Ash.Type.embedded_type?(type) do
if input? do
:"#{AshGraphql.Resource.type(resource)}_#{attribute.name}_input"
else
case type(type) do
nil ->
:json
type ->
type
end
end
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()
end
end