Packages
ash_graphql
0.2.0
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
@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}
@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.
""",
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"
],
fields: [
type: {:custom, __MODULE__, :__fields, []},
required: true,
doc: "The fields from this entity to include in the graphql"
]
],
sections: [
@queries,
@mutations
]
}
@doc false
def __fields(fields) do
fields = List.wrap(fields)
if Enum.all?(fields, &is_atom/1) do
{:ok, fields}
else
{:error, "Expected `fields` to be a list of atoms"}
end
end
@transformers [
AshGraphql.Resource.Transformers.RequireIdPkey
]
use Extension, sections: [@graphql], 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 fields(resource) do
Extension.get_opt(resource, [:graphql], :fields, [])
end
@doc false
def queries(api, resource, schema) do
type = Resource.type(resource)
resource
|> queries()
|> Enum.map(fn query ->
%Absinthe.Blueprint.Schema.FieldDefinition{
arguments: args(query.type),
identifier: query.name,
middleware: [
{{AshGraphql.Graphql.Resolver, :resolve}, {api, resource, query.type, query.action}}
],
module: schema,
name: to_string(query.name),
type: query_type(query.type, type)
}
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: [
%Absinthe.Blueprint.Schema.InputValueDefinition{
identifier: :id,
module: schema,
name: "id",
placement: :argument_definition,
type: :id
}
],
identifier: mutation.name,
middleware: [
{{AshGraphql.Graphql.Resolver, :mutate},
{api, resource, mutation.type, mutation.action}}
],
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.type, mutation.action}}
],
module: schema,
name: to_string(mutation.name),
type: String.to_atom("#{mutation.name}_result")
}
mutation ->
%Absinthe.Blueprint.Schema.FieldDefinition{
arguments: [
%Absinthe.Blueprint.Schema.InputValueDefinition{
identifier: :id,
module: schema,
name: "id",
placement: :argument_definition,
type: :id
},
%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.type, mutation.action}}
],
module: schema,
name: to_string(mutation.name),
type: String.to_atom("#{mutation.name}_result")
}
end)
end
@doc false
# sobelow_skip ["DOS.StringToAtom"]
def mutation_types(resource, schema) do
resource
|> mutations()
|> Enum.flat_map(fn mutation ->
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),
identifier: String.to_atom("#{mutation.name}_input"),
module: schema,
name: Macro.camelize("#{mutation.name}_input")
}
[input, result]
end
end)
end
defp mutation_fields(resource, schema, query) do
fields = Resource.fields(resource)
attribute_fields =
resource
|> Ash.Resource.attributes()
|> Enum.filter(&(&1.name in fields))
|> Enum.filter(& &1.writable?)
|> Enum.map(fn attribute ->
type = field_type(attribute.type)
field_type =
if attribute.allow_nil? || query.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.relationships()
|> Enum.filter(&(&1.name in fields))
|> Enum.filter(fn relationship ->
Resource in Ash.Resource.extensions(relationship.destination)
end)
|> Enum.map(fn
%{cardinality: :one} = relationship ->
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: relationship.name,
module: schema,
name: to_string(relationship.name),
type: :id
}
%{cardinality: :many} = relationship ->
case query.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)
attribute_fields ++ relationship_fields
end
defp query_type(:get, type), do: type
# sobelow_skip ["DOS.StringToAtom"]
defp query_type(:list, type), do: String.to_atom("page_of_#{type}")
defp args(:get) do
[
%Absinthe.Blueprint.Schema.InputValueDefinition{
name: "id",
identifier: :id,
type: :id,
description: "The id of the record"
}
]
end
defp args(:list) do
[
%Absinthe.Blueprint.Schema.InputValueDefinition{
name: "limit",
identifier: :limit,
type: :integer,
description: "The limit of records to return",
default_value: 20
},
%Absinthe.Blueprint.Schema.InputValueDefinition{
name: "offset",
identifier: :offset,
type: :integer,
description: "The count of records to skip",
default_value: 0
},
%Absinthe.Blueprint.Schema.InputValueDefinition{
name: "filter",
identifier: :filter,
type: :string,
description: "A json encoded filter to apply"
}
]
end
@doc false
def type_definitions(resource, schema) do
[
type_definition(resource, schema),
page_of(resource, schema)
]
end
# sobelow_skip ["DOS.StringToAtom"]
defp page_of(resource, schema) do
type = Resource.type(resource)
%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: 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}")
}
end
defp type_definition(resource, schema) do
type = Resource.type(resource)
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{
description: Ash.Resource.description(resource),
fields: fields(resource, schema),
identifier: type,
module: schema,
name: Macro.camelize(to_string(type))
}
end
defp fields(resource, schema) do
fields = Resource.fields(resource)
attributes(resource, schema, fields) ++
relationships(resource, schema, fields) ++
aggregates(resource, schema, fields)
end
defp attributes(resource, schema, fields) do
resource
|> Ash.Resource.attributes()
|> Enum.filter(&(&1.name in fields))
|> Enum.map(fn
%{name: :id} = attribute ->
%Absinthe.Blueprint.Schema.FieldDefinition{
description: attribute.description,
identifier: :id,
module: schema,
name: "id",
type: :id
}
attribute ->
%Absinthe.Blueprint.Schema.FieldDefinition{
description: attribute.description,
identifier: attribute.name,
module: schema,
name: to_string(attribute.name),
type: field_type(attribute.type)
}
end)
end
# sobelow_skip ["DOS.StringToAtom"]
defp relationships(resource, schema, fields) do
resource
|> Ash.Resource.relationships()
|> Enum.filter(&(&1.name in fields))
|> Enum.filter(fn relationship ->
Resource in Ash.Resource.extensions(relationship.destination)
end)
|> Enum.map(fn
%{cardinality: :one} = relationship ->
type = Resource.type(relationship.destination)
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: relationship.name,
module: schema,
name: to_string(relationship.name),
middleware: [
{{AshGraphql.Graphql.Resolver, :resolve_assoc}, {:one, relationship.name}}
],
arguments: [],
type: type
}
%{cardinality: :many} = relationship ->
type = Resource.type(relationship.destination)
query_type = String.to_atom("page_of_#{type}")
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: relationship.name,
module: schema,
name: to_string(relationship.name),
middleware: [
{{AshGraphql.Graphql.Resolver, :resolve_assoc}, {:many, relationship.name}}
],
arguments: args(:list),
type: query_type
}
end)
end
defp aggregates(resource, schema, fields) do
resource
|> Ash.Resource.aggregates()
|> Enum.filter(&(&1.name in fields))
|> Enum.map(fn aggregate ->
{:ok, type} = Aggregate.kind_to_type(aggregate.kind)
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: aggregate.name,
module: schema,
name: to_string(aggregate.name),
type: field_type(type)
}
end)
end
defp field_type(Ash.Type.String), do: :string
defp field_type(Ash.Type.UUID), do: :string
defp field_type(Ash.Type.Integer), do: :integer
defp field_type({:array, type}) do
%Absinthe.Blueprint.TypeReference.List{
of_type: field_type(type)
}
end
end