mirror of
https://github.com/ash-project/ash_graphql.git
synced 2024-09-19 12:53:40 +12:00
feat: initial POC release
This commit is contained in:
parent
759c92553c
commit
cd16030324
14 changed files with 1113 additions and 675 deletions
146
lib/api/api.ex
146
lib/api/api.ex
|
@ -1,23 +1,137 @@
|
|||
defmodule AshGraphql.Api do
|
||||
defmacro __using__(opts) do
|
||||
quote bind_quoted: [opts: opts] do
|
||||
@extensions AshGraphql.Api
|
||||
@authorize Keyword.get(opts, :authorize?, true)
|
||||
@max_complexity Keyword.get(opts, :max_complexity, 50)
|
||||
end
|
||||
@moduledoc "The entrypoint for adding graphql behavior to an Ash API"
|
||||
|
||||
@graphql %Ash.Dsl.Section{
|
||||
name: :graphql,
|
||||
describe: """
|
||||
Global configuration for graphql
|
||||
""",
|
||||
schema: [
|
||||
authorize?: [
|
||||
type: :boolean,
|
||||
doc: "Whether or not to perform authorization for this API",
|
||||
default: true
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
use Ash.Dsl.Extension, sections: [@graphql]
|
||||
|
||||
def authorize?(api) do
|
||||
Extension.get_opt(api, :api, :authorize?, true)
|
||||
end
|
||||
|
||||
def before_compile_hook(_env) do
|
||||
quote do
|
||||
use AshGraphql.Api.Schema, resources: @resources, api: __MODULE__
|
||||
|
||||
def graphql_authorize? do
|
||||
@authorize
|
||||
@doc false
|
||||
def queries(api, schema) do
|
||||
api
|
||||
|> Ash.Api.resources()
|
||||
|> Enum.flat_map(&AshGraphql.Resource.queries(api, &1, schema))
|
||||
end
|
||||
|
||||
def graphql_max_complexity() do
|
||||
@max_complexity
|
||||
end
|
||||
end
|
||||
@doc false
|
||||
def mutations(api, schema) do
|
||||
api
|
||||
|> Ash.Api.resources()
|
||||
|> Enum.filter(fn resource ->
|
||||
AshGraphql.Resource in Ash.Resource.extensions(resource)
|
||||
end)
|
||||
|> Enum.flat_map(&AshGraphql.Resource.mutations(api, &1, schema))
|
||||
end
|
||||
|
||||
@doc false
|
||||
def type_definitions(api, schema) do
|
||||
resource_types =
|
||||
api
|
||||
|> Ash.Api.resources()
|
||||
|> Enum.filter(fn resource ->
|
||||
AshGraphql.Resource in Ash.Resource.extensions(resource)
|
||||
end)
|
||||
|> Enum.flat_map(fn resource ->
|
||||
AshGraphql.Resource.type_definitions(resource, schema) ++
|
||||
AshGraphql.Resource.mutation_types(resource, schema)
|
||||
end)
|
||||
|
||||
[mutation_error(schema), relationship_change(schema)] ++ resource_types
|
||||
end
|
||||
|
||||
defp relationship_change(schema) do
|
||||
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{
|
||||
description: "A set of changes to apply to a relationship",
|
||||
fields: relationship_change_fields(schema),
|
||||
identifier: :relationship_change,
|
||||
module: schema,
|
||||
name: "RelationshipChange"
|
||||
}
|
||||
end
|
||||
|
||||
defp relationship_change_fields(schema) do
|
||||
[
|
||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||
description: "Ids to add to the relationship",
|
||||
identifier: :add,
|
||||
module: schema,
|
||||
name: "add",
|
||||
type: %Absinthe.Blueprint.TypeReference.List{
|
||||
of_type: :id
|
||||
}
|
||||
},
|
||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||
description: "Ids to remove from the relationship",
|
||||
identifier: :remove,
|
||||
module: schema,
|
||||
name: "remove",
|
||||
type: %Absinthe.Blueprint.TypeReference.List{
|
||||
of_type: :id
|
||||
}
|
||||
},
|
||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||
description:
|
||||
"Ids to replace the relationship with. Takes precendence over removal and addition",
|
||||
identifier: :replace,
|
||||
module: schema,
|
||||
name: "replace",
|
||||
type: %Absinthe.Blueprint.TypeReference.List{
|
||||
of_type: :id
|
||||
}
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
defp mutation_error(schema) do
|
||||
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{
|
||||
description: "An error generated by a failed mutation",
|
||||
fields: error_fields(schema),
|
||||
identifier: :mutation_error,
|
||||
module: schema,
|
||||
name: "MutationError"
|
||||
}
|
||||
end
|
||||
|
||||
defp error_fields(schema) do
|
||||
[
|
||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||
description: "The human readable error message",
|
||||
identifier: :message,
|
||||
module: schema,
|
||||
name: "message",
|
||||
type: :string
|
||||
},
|
||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||
description: "An error code for the given error",
|
||||
identifier: :code,
|
||||
module: schema,
|
||||
name: "code",
|
||||
type: :string
|
||||
},
|
||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||
description: "The field or fields that produced the error",
|
||||
identifier: :fields,
|
||||
module: schema,
|
||||
name: "fields",
|
||||
type: %Absinthe.Blueprint.TypeReference.List{
|
||||
of_type: :string
|
||||
}
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,532 +0,0 @@
|
|||
defmodule AshGraphql.Api.Schema do
|
||||
defmacro __using__(opts) do
|
||||
quote bind_quoted: [api: opts[:api]] do
|
||||
defmodule __MODULE__.Schema do
|
||||
# use Absinthe.Schema
|
||||
|
||||
@api api
|
||||
|
||||
def __absinthe_lookup__(type) do
|
||||
__absinthe_type__(type)
|
||||
end
|
||||
|
||||
def __absinthe_types__() do
|
||||
AshGraphql.Api.Schema.absinthe_types(@api)
|
||||
end
|
||||
|
||||
def __absinthe_type__(type) do
|
||||
AshGraphql.Api.Schema.absinthe_type(@api, type)
|
||||
end
|
||||
|
||||
def __absinthe_directives__() do
|
||||
AshGraphql.Api.Schema.Base.__absinthe_directives__()
|
||||
end
|
||||
|
||||
def __absinthe_directive__(dir) do
|
||||
AshGraphql.Api.Schema.Base.__absinthe_directive__(dir)
|
||||
end
|
||||
|
||||
def context(context) do
|
||||
context
|
||||
end
|
||||
|
||||
def plugins() do
|
||||
[Absinthe.Middleware.Batch, Absinthe.Middleware.Async]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Base do
|
||||
use Absinthe.Schema
|
||||
|
||||
query do
|
||||
end
|
||||
|
||||
# mutation do
|
||||
# end
|
||||
|
||||
# subscription do
|
||||
# end
|
||||
end
|
||||
|
||||
def absinthe_types(api, _), do: absinthe_types(api)
|
||||
|
||||
def absinthe_types(api) do
|
||||
base_types = %{
|
||||
__directive: "__Directive",
|
||||
__directive_location: "__DirectiveLocation",
|
||||
__enumvalue: "__EnumValue",
|
||||
__field: "__Field",
|
||||
__inputvalue: "__InputValue",
|
||||
__schema: "__Schema",
|
||||
__type: "__Type",
|
||||
boolean: "Boolean",
|
||||
id: "ID",
|
||||
query: "RootQueryType",
|
||||
# mutation: "RootMutationType",
|
||||
# subscription: "RootSusbcriptionType",
|
||||
string: "String"
|
||||
}
|
||||
|
||||
api
|
||||
|> resource_types()
|
||||
|> Enum.reduce(base_types, fn %{identifier: identifier, name: name}, acc ->
|
||||
Map.put(acc, identifier, name)
|
||||
end)
|
||||
end
|
||||
|
||||
def absinthe_type(api, :query) do
|
||||
%Absinthe.Type.Object{
|
||||
__private__: [__absinthe_referenced__: true],
|
||||
__reference__: %{
|
||||
location: %{file: "nofile", line: 1},
|
||||
module: Module.concat(api, Schema)
|
||||
},
|
||||
definition: Module.concat(api, Schema),
|
||||
description: nil,
|
||||
fields:
|
||||
%{
|
||||
__schema: schema_type(),
|
||||
__type: type_type(),
|
||||
__typename: type_name_type()
|
||||
}
|
||||
|> add_query_fields(api),
|
||||
identifier: :query,
|
||||
interfaces: [],
|
||||
is_type_of: :object,
|
||||
name: "RootQueryType"
|
||||
}
|
||||
end
|
||||
|
||||
def absinthe_type(api, type) do
|
||||
api
|
||||
|> resource_types()
|
||||
|> Enum.find(fn %{identifier: identifier} ->
|
||||
identifier == type
|
||||
end)
|
||||
|> case do
|
||||
nil ->
|
||||
AshGraphql.Api.Schema.Base.__absinthe_type__(type)
|
||||
|
||||
type ->
|
||||
type
|
||||
end
|
||||
end
|
||||
|
||||
defp resource_types(api) do
|
||||
api
|
||||
|> Ash.resources()
|
||||
|> Enum.filter(&(AshGraphql.GraphqlResource in &1.extensions))
|
||||
|> Enum.flat_map(&resource_types(api, &1))
|
||||
end
|
||||
|
||||
defp add_query_fields(acc, api) do
|
||||
api
|
||||
|> Ash.resources()
|
||||
|> Enum.filter(&(AshGraphql.GraphqlResource in &1.extensions))
|
||||
|> Enum.flat_map(&query_fields(api, &1))
|
||||
|> Enum.reduce(acc, fn query_field, acc ->
|
||||
Map.put(acc, query_field.identifier, query_field)
|
||||
end)
|
||||
end
|
||||
|
||||
defp query_fields(api, resource) do
|
||||
resource
|
||||
|> AshGraphql.fields()
|
||||
|> Enum.map(fn field ->
|
||||
get_one_identifier = AshGraphql.type(resource)
|
||||
|
||||
case field.type do
|
||||
:get ->
|
||||
%Absinthe.Type.Field{
|
||||
__private__: [],
|
||||
__reference__: %{
|
||||
location: %{file: "nofile", line: 1},
|
||||
module: AshExample.Api.Schema
|
||||
},
|
||||
args: %{
|
||||
id: %Absinthe.Type.Argument{
|
||||
__reference__: nil,
|
||||
default_value: nil,
|
||||
definition: nil,
|
||||
deprecation: nil,
|
||||
description: nil,
|
||||
identifier: :id,
|
||||
name: "id",
|
||||
type: %Absinthe.Type.NonNull{of_type: :id}
|
||||
}
|
||||
},
|
||||
# TODO: DO THIS
|
||||
complexity: 2,
|
||||
config: %{},
|
||||
default_value: nil,
|
||||
definition: AshExample.Api.Schema,
|
||||
deprecation: nil,
|
||||
description: nil,
|
||||
identifier: field.name,
|
||||
middleware: [
|
||||
{{AshGraphql.Graphql.Resolver, :resolve}, {api, resource, :get, field.action}}
|
||||
],
|
||||
name: Atom.to_string(field.name),
|
||||
triggers: [],
|
||||
type: get_one_identifier
|
||||
}
|
||||
|
||||
:read ->
|
||||
%Absinthe.Type.Field{
|
||||
__private__: [],
|
||||
__reference__: %{
|
||||
location: %{file: "nofile", line: 1},
|
||||
module: AshExample.Api.Schema
|
||||
},
|
||||
args: %{
|
||||
limit: %Absinthe.Type.Argument{
|
||||
identifier: :limit,
|
||||
type: :integer,
|
||||
name: "limit"
|
||||
},
|
||||
offset: %Absinthe.Type.Argument{
|
||||
identifier: :offset,
|
||||
default_value: 0,
|
||||
type: :integer,
|
||||
name: "offset"
|
||||
}
|
||||
# TODO: Generate types for the filter, sort, and paginate args
|
||||
# Also figure out graphql pagination
|
||||
# filter: %Absinthe.Type.Argument
|
||||
# id: %Absinthe.Type.Argument{
|
||||
# __reference__: nil,
|
||||
# default_value: nil,
|
||||
# definition: nil,
|
||||
# deprecation: nil,
|
||||
# description: nil,
|
||||
# identifier: :id,
|
||||
# name: "id",
|
||||
# type: %Absinthe.Type.NonNull{of_type: :id}
|
||||
# }
|
||||
},
|
||||
complexity: 1,
|
||||
config: %{},
|
||||
default_value: nil,
|
||||
definition: AshExample.Api.Schema,
|
||||
deprecation: nil,
|
||||
description: nil,
|
||||
identifier: field.name,
|
||||
middleware: [
|
||||
{{AshGraphql.Graphql.Resolver, :resolve}, {api, resource, :read, field.action}}
|
||||
],
|
||||
name: Atom.to_string(field.name),
|
||||
triggers: [],
|
||||
type: String.to_atom("page_of_#{get_one_identifier}")
|
||||
}
|
||||
end
|
||||
end)
|
||||
|
||||
# for field <- AshGraphql.fields(resource) do
|
||||
|
||||
# end
|
||||
# resource
|
||||
# |> Ash.actions()
|
||||
# |> Enum.flat_map(fn action ->
|
||||
# case action do
|
||||
# %{type: :read, primary?: true} ->
|
||||
# read_action(api, resource, action)
|
||||
|
||||
# _ ->
|
||||
# # TODO: Only support reads
|
||||
# []
|
||||
# end
|
||||
# end)
|
||||
end
|
||||
|
||||
# defp resource_types(api, resource) do
|
||||
# resource_type(api, resource)
|
||||
# # |> add_get_type(api, resource)
|
||||
# end
|
||||
|
||||
defp resource_types(api, resource) do
|
||||
# NOT DONE
|
||||
pkey_field = :id
|
||||
|
||||
# pkey_field =
|
||||
# case Ash.primary_key(resource) do
|
||||
# [field] ->
|
||||
# field
|
||||
|
||||
# primary_key ->
|
||||
# raise "Invalid primary key #{primary_key} for graphql resource"
|
||||
# end
|
||||
|
||||
get_one_identifier = AshGraphql.type(resource)
|
||||
|
||||
[
|
||||
%Absinthe.Type.Object{
|
||||
__private__: [__absinthe_referenced__: true],
|
||||
__reference__: %{
|
||||
location: %{file: "nofile", line: 1},
|
||||
module: AshExample.Api.Schema
|
||||
},
|
||||
definition: AshExample.Api.Schema,
|
||||
description: Ash.describe(resource),
|
||||
fields:
|
||||
%{
|
||||
:__typename => type_name_type(),
|
||||
pkey_field => id_type(pkey_field)
|
||||
}
|
||||
|> add_fields(api, resource),
|
||||
identifier: get_one_identifier,
|
||||
interfaces: [],
|
||||
is_type_of: :object,
|
||||
name: String.capitalize(Atom.to_string(get_one_identifier))
|
||||
},
|
||||
page_of(get_one_identifier)
|
||||
]
|
||||
end
|
||||
|
||||
defp add_fields(fields, _api, _resource) do
|
||||
fields
|
||||
# name: %Absinthe.Type.Field{
|
||||
# __private__: [],
|
||||
# __reference__: %{
|
||||
# location: %{file: "nofile", line: 1},
|
||||
# module: AshExample.Api.Schema
|
||||
# },
|
||||
# args: %{},
|
||||
# complexity:
|
||||
# {:ref, AshExample.Api.Schema,
|
||||
# {Absinthe.Blueprint.Schema.FieldDefinition, {:item, :name}}},
|
||||
# config:
|
||||
# {:ref, AshExample.Api.Schema,
|
||||
# {Absinthe.Blueprint.Schema.FieldDefinition, {:item, :name}}},
|
||||
# default_value: nil,
|
||||
# definition: AshExample.Api.Schema,
|
||||
# deprecation: nil,
|
||||
# description: nil,
|
||||
# identifier: :name,
|
||||
# middleware: [{Absinthe.Middleware.MapGet, :name}],
|
||||
# name: "name",
|
||||
# triggers:
|
||||
# {:ref, AshExample.Api.Schema,
|
||||
# {Absinthe.Blueprint.Schema.FieldDefinition, {:item, :name}}},
|
||||
# type: :string
|
||||
# }
|
||||
end
|
||||
|
||||
defp page_of(get_one_identifier) do
|
||||
%Absinthe.Type.Object{
|
||||
__private__: [__absinthe_referenced__: true],
|
||||
__reference__: %{
|
||||
location: %{file: "nofile", line: 1},
|
||||
module: AshExample.Api.Schema
|
||||
},
|
||||
definition: AshExample.Api.Schema,
|
||||
description: "A page of #{get_one_identifier}",
|
||||
fields: %{
|
||||
__typename: type_name_type(),
|
||||
offset: %Absinthe.Type.Field{
|
||||
__private__: [],
|
||||
__reference__: %{
|
||||
location: %{file: "nofile", line: 1},
|
||||
module: AshExample.Api.Schema
|
||||
},
|
||||
args: %{},
|
||||
# TODO: DO THIS
|
||||
complexity: 1,
|
||||
config: %{},
|
||||
default_value: nil,
|
||||
definition: AshExample.Api.Schema,
|
||||
deprecation: nil,
|
||||
description: nil,
|
||||
identifier: :offset,
|
||||
middleware: [{Absinthe.Middleware.MapGet, :offset}],
|
||||
name: "offset",
|
||||
triggers: [],
|
||||
type: :integer
|
||||
},
|
||||
limit: %Absinthe.Type.Field{
|
||||
__private__: [],
|
||||
__reference__: %{
|
||||
location: %{file: "nofile", line: 1},
|
||||
module: AshExample.Api.Schema
|
||||
},
|
||||
args: %{},
|
||||
# TODO: DO THIS
|
||||
complexity: 1,
|
||||
config: %{},
|
||||
default_value: nil,
|
||||
definition: AshExample.Api.Schema,
|
||||
deprecation: nil,
|
||||
description: nil,
|
||||
identifier: :limit,
|
||||
middleware: [{Absinthe.Middleware.MapGet, :limit}],
|
||||
name: "limit",
|
||||
triggers: [],
|
||||
type: :integer
|
||||
},
|
||||
results: %Absinthe.Type.Field{
|
||||
__private__: [],
|
||||
__reference__: %{
|
||||
location: %{file: "nofile", line: 1},
|
||||
module: AshExample.Api.Schema
|
||||
},
|
||||
args: %{},
|
||||
# TODO: DO THIS
|
||||
complexity: 1,
|
||||
config: %{},
|
||||
default_value: nil,
|
||||
definition: AshExample.Api.Schema,
|
||||
deprecation: nil,
|
||||
description: nil,
|
||||
identifier: :results,
|
||||
middleware: [{Absinthe.Middleware.MapGet, :results}],
|
||||
name: "results",
|
||||
triggers: [],
|
||||
type: %Absinthe.Type.NonNull{
|
||||
of_type: %Absinthe.Type.List{
|
||||
of_type: %Absinthe.Type.NonNull{of_type: get_one_identifier}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
identifier: String.to_atom("page_of_#{get_one_identifier}"),
|
||||
interfaces: [],
|
||||
is_type_of: :object,
|
||||
name: "pageOf#{String.capitalize(Atom.to_string(get_one_identifier))}"
|
||||
}
|
||||
end
|
||||
|
||||
defp id_type(field) do
|
||||
%Absinthe.Type.Field{
|
||||
__private__: [],
|
||||
__reference__: %{
|
||||
location: %{file: "nofile", line: 1},
|
||||
module: AshExample.Api.Schema
|
||||
},
|
||||
args: %{},
|
||||
# TODO: do this
|
||||
complexity: 1,
|
||||
config: %{},
|
||||
default_value: nil,
|
||||
definition: AshExample.Api.Schema,
|
||||
deprecation: nil,
|
||||
description: nil,
|
||||
identifier: :id,
|
||||
middleware: [{Absinthe.Middleware.MapGet, field}],
|
||||
name: "id",
|
||||
triggers: [],
|
||||
type: :id
|
||||
}
|
||||
end
|
||||
|
||||
defp schema_type() do
|
||||
%Absinthe.Type.Field{
|
||||
__private__: [],
|
||||
__reference__: %{
|
||||
location: %{
|
||||
file:
|
||||
"/Users/zachdaniel/dev/ash/ash_example/deps/absinthe/lib/absinthe/phase/schema/introspection.ex",
|
||||
line: 116
|
||||
},
|
||||
module: Absinthe.Phase.Schema.Introspection
|
||||
},
|
||||
args: %{},
|
||||
complexity: nil,
|
||||
config: nil,
|
||||
default_value: nil,
|
||||
definition: Absinthe.Phase.Schema.Introspection,
|
||||
deprecation: nil,
|
||||
description: "Represents the schema",
|
||||
identifier: :__schema,
|
||||
middleware: [
|
||||
{{Absinthe.Middleware, :shim},
|
||||
{:query, :__schema, [{:ref, Absinthe.Phase.Schema.Introspection, :schema}]}}
|
||||
],
|
||||
name: "__schema",
|
||||
triggers: %{},
|
||||
type: :__schema
|
||||
}
|
||||
end
|
||||
|
||||
def type_name_type() do
|
||||
%Absinthe.Type.Field{
|
||||
__private__: [],
|
||||
__reference__: %{
|
||||
location: %{
|
||||
file:
|
||||
"/Users/zachdaniel/dev/ash/ash_example/deps/absinthe/lib/absinthe/phase/schema/introspection.ex",
|
||||
line: 74
|
||||
},
|
||||
module: Absinthe.Phase.Schema.Introspection
|
||||
},
|
||||
args: %{},
|
||||
complexity: 0,
|
||||
config: 0,
|
||||
default_value: nil,
|
||||
definition: Absinthe.Phase.Schema.Introspection,
|
||||
deprecation: nil,
|
||||
description: "The name of the object type currently being queried.",
|
||||
identifier: :__typename,
|
||||
middleware: [
|
||||
{{Absinthe.Middleware, :shim},
|
||||
{:query, :__typename, [{:ref, Absinthe.Phase.Schema.Introspection, :typename}]}}
|
||||
],
|
||||
name: "__typename",
|
||||
triggers: %{},
|
||||
type: :string
|
||||
}
|
||||
end
|
||||
|
||||
def type_type() do
|
||||
%Absinthe.Type.Field{
|
||||
__private__: [],
|
||||
__reference__: %{
|
||||
location: %{
|
||||
file:
|
||||
"/Users/zachdaniel/dev/ash/ash_example/deps/absinthe/lib/absinthe/phase/schema/introspection.ex",
|
||||
line: 80
|
||||
},
|
||||
module: Absinthe.Phase.Schema.Introspection
|
||||
},
|
||||
args: %{
|
||||
name: %Absinthe.Type.Argument{
|
||||
__reference__: nil,
|
||||
default_value: nil,
|
||||
definition: nil,
|
||||
deprecation: nil,
|
||||
description: "The name of the type to introspect",
|
||||
identifier: :name,
|
||||
name: "name",
|
||||
type: %Absinthe.Type.NonNull{of_type: :string}
|
||||
}
|
||||
},
|
||||
complexity: nil,
|
||||
config: nil,
|
||||
default_value: nil,
|
||||
definition: Absinthe.Phase.Schema.Introspection,
|
||||
deprecation: nil,
|
||||
description: "Represents scalars, interfaces, object types, unions, enums in the system",
|
||||
identifier: :__type,
|
||||
middleware: [
|
||||
{{Absinthe.Middleware, :shim},
|
||||
{:query, :__type, [{:ref, Absinthe.Phase.Schema.Introspection, :type}]}}
|
||||
],
|
||||
name: "__type",
|
||||
triggers: %{},
|
||||
type: :__type
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# defmacro __using__(opts \\ []) do
|
||||
# quoted =
|
||||
# quote do
|
||||
# for resource <- Ash.resources(unquote(opts[:api])) do
|
||||
# end
|
||||
# end
|
||||
|
||||
# quote do
|
||||
# defmodule(__MODULE__.Schema, do: unquote(quoted))
|
||||
# end
|
||||
# end
|
||||
# end
|
|
@ -3,11 +3,52 @@ defmodule AshGraphql do
|
|||
Documentation for `AshGraphql`.
|
||||
"""
|
||||
|
||||
def fields(resource) do
|
||||
resource.graphql_fields()
|
||||
defmacro __using__(opts) do
|
||||
quote bind_quoted: [api: opts[:api]] do
|
||||
defmodule AshTypes do
|
||||
alias Absinthe.{Phase, Pipeline, Blueprint}
|
||||
|
||||
def pipeline(pipeline) do
|
||||
Pipeline.insert_before(
|
||||
pipeline,
|
||||
Phase.Schema.Validation.QueryTypeMustBeObject,
|
||||
__MODULE__
|
||||
)
|
||||
end
|
||||
|
||||
def type(resource) do
|
||||
resource.graphql_type()
|
||||
def run(blueprint, _opts) do
|
||||
api = unquote(api)
|
||||
Code.ensure_compiled(api)
|
||||
|
||||
blueprint_with_queries =
|
||||
api
|
||||
|> AshGraphql.Api.queries(__MODULE__)
|
||||
|> Enum.reduce(blueprint, fn query, blueprint ->
|
||||
Absinthe.Blueprint.add_field(blueprint, "RootQueryType", query)
|
||||
end)
|
||||
|
||||
blueprint_with_mutations =
|
||||
api
|
||||
|> AshGraphql.Api.mutations(__MODULE__)
|
||||
|> Enum.reduce(blueprint_with_queries, fn mutation, blueprint ->
|
||||
Absinthe.Blueprint.add_field(blueprint, "RootMutationType", mutation)
|
||||
end)
|
||||
|
||||
new_defs =
|
||||
List.update_at(blueprint_with_mutations.schema_definitions, 0, fn schema_def ->
|
||||
%{
|
||||
schema_def
|
||||
| type_definitions:
|
||||
schema_def.type_definitions ++
|
||||
AshGraphql.Api.type_definitions(api, __MODULE__)
|
||||
}
|
||||
end)
|
||||
|
||||
{:ok, %{blueprint_with_mutations | schema_definitions: new_defs}}
|
||||
end
|
||||
end
|
||||
|
||||
@pipeline_modifier AshTypes
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,37 +4,66 @@ defmodule AshGraphql.Graphql.Resolver do
|
|||
{api, resource, :get, action}
|
||||
) do
|
||||
opts =
|
||||
if api.graphql_authorize?() do
|
||||
if AshGraphql.Api.authorize?(api) do
|
||||
[actor: Map.get(context, :actor), action: action]
|
||||
else
|
||||
[action: action]
|
||||
end
|
||||
|
||||
opts = Keyword.put(opts, :load, load_nested(resource, resolution.definition.selections))
|
||||
|
||||
result = api.get(resource, id, opts)
|
||||
|
||||
Absinthe.Resolution.put_result(resolution, to_resolution(result))
|
||||
end
|
||||
|
||||
def resolve(
|
||||
%{arguments: %{limit: limit, offset: offset}, context: context} = resolution,
|
||||
{api, resource, :read, action}
|
||||
%{arguments: %{limit: limit, offset: offset} = args, context: context} = resolution,
|
||||
{api, resource, :list, action}
|
||||
) do
|
||||
opts =
|
||||
if api.graphql_authorize?() do
|
||||
if AshGraphql.Api.authorize?(api) do
|
||||
[actor: Map.get(context, :actor), action: action]
|
||||
else
|
||||
[action: action]
|
||||
end
|
||||
|
||||
result =
|
||||
selections =
|
||||
case Enum.find(resolution.definition.selections, &(&1.schema_node.identifier == :results)) do
|
||||
nil ->
|
||||
[]
|
||||
|
||||
field ->
|
||||
field.selections
|
||||
end
|
||||
|
||||
query =
|
||||
resource
|
||||
|> api.query
|
||||
|> Ash.Query.limit(limit)
|
||||
|> Ash.Query.offset(offset)
|
||||
|> Ash.Query.load(load_nested(resource, selections))
|
||||
|
||||
query =
|
||||
case Map.fetch(args, :filter) do
|
||||
{:ok, filter} ->
|
||||
case Jason.decode(filter) do
|
||||
{:ok, decoded} ->
|
||||
Ash.Query.filter(query, to_snake_case(decoded))
|
||||
|
||||
{:error, error} ->
|
||||
raise "Error parsing filter: #{inspect(error)}"
|
||||
end
|
||||
|
||||
_ ->
|
||||
query
|
||||
end
|
||||
|
||||
result =
|
||||
query
|
||||
|> api.read(opts)
|
||||
|> case do
|
||||
{:ok, results} ->
|
||||
{:ok, %AshGraphql.Paginator{results: results, limit: limit, offset: offset}}
|
||||
{:ok, %AshGraphql.Paginator{results: results, count: Enum.count(results)}}
|
||||
|
||||
error ->
|
||||
error
|
||||
|
@ -43,9 +72,233 @@ defmodule AshGraphql.Graphql.Resolver do
|
|||
Absinthe.Resolution.put_result(resolution, to_resolution(result))
|
||||
end
|
||||
|
||||
def resolve(resolution, _),
|
||||
do: Absinthe.Resolution.put_result(resolution, {:error, :unknown_request})
|
||||
def mutate(
|
||||
%{arguments: %{input: input}, context: context} = resolution,
|
||||
{api, resource, :create, action}
|
||||
) do
|
||||
{attributes, relationships} =
|
||||
Enum.reduce(input, {%{}, %{}}, fn {key, value}, {attrs, rels} ->
|
||||
if Ash.Resource.attribute(resource, key) do
|
||||
{Map.put(attrs, key, value), rels}
|
||||
else
|
||||
{attrs, Map.put(rels, key, value)}
|
||||
end
|
||||
end)
|
||||
|
||||
selections =
|
||||
case Enum.find(resolution.definition.selections, &(&1.schema_node.identifier == :result)) do
|
||||
nil ->
|
||||
[]
|
||||
|
||||
field ->
|
||||
field.selections
|
||||
end
|
||||
|
||||
load = load_nested(resource, selections)
|
||||
|
||||
changeset = Ash.Changeset.new(resource, attributes)
|
||||
|
||||
changeset_with_relationships =
|
||||
Enum.reduce(relationships, changeset, fn {relationship, replacement}, changeset ->
|
||||
Ash.Changeset.replace_relationship(changeset, relationship, replacement)
|
||||
end)
|
||||
|
||||
opts =
|
||||
if AshGraphql.Api.authorize?(api) do
|
||||
[actor: Map.get(context, :actor), action: action]
|
||||
else
|
||||
[action: action]
|
||||
end
|
||||
|
||||
result =
|
||||
with {:ok, value} <- api.create(changeset_with_relationships, opts),
|
||||
{:ok, value} <- api.load(value, load) do
|
||||
{:ok, %{result: value, errors: []}}
|
||||
else
|
||||
{:error, error} ->
|
||||
{:ok, %{result: nil, errors: to_errors(error)}}
|
||||
end
|
||||
|
||||
Absinthe.Resolution.put_result(resolution, to_resolution(result))
|
||||
end
|
||||
|
||||
def mutate(
|
||||
%{arguments: %{id: id, input: input}, context: context} = resolution,
|
||||
{api, resource, :update, action}
|
||||
) do
|
||||
case api.get(resource, id) do
|
||||
nil ->
|
||||
{:ok, %{result: nil, errors: [to_errors("not found")]}}
|
||||
|
||||
initial ->
|
||||
{attributes, relationships} =
|
||||
Enum.reduce(input, {%{}, %{}}, fn {key, value}, {attrs, rels} ->
|
||||
if Ash.Resource.attribute(resource, key) do
|
||||
{Map.put(attrs, key, value), rels}
|
||||
else
|
||||
{attrs, Map.put(rels, key, value)}
|
||||
end
|
||||
end)
|
||||
|
||||
changeset = Ash.Changeset.new(initial, attributes)
|
||||
|
||||
changeset_with_relationships =
|
||||
Enum.reduce(relationships, changeset, fn {relationship, replacement}, changeset ->
|
||||
Ash.Changeset.replace_relationship(changeset, relationship, replacement)
|
||||
end)
|
||||
|
||||
opts =
|
||||
if AshGraphql.Api.authorize?(api) do
|
||||
[actor: Map.get(context, :actor), action: action]
|
||||
else
|
||||
[action: action]
|
||||
end
|
||||
|
||||
selections =
|
||||
case Enum.find(
|
||||
resolution.definition.selections,
|
||||
&(&1.schema_node.identifier == :result)
|
||||
) do
|
||||
nil ->
|
||||
[]
|
||||
|
||||
field ->
|
||||
field.selections
|
||||
end
|
||||
|
||||
load = load_nested(resource, selections)
|
||||
|
||||
result =
|
||||
with {:ok, value} <- api.update(changeset_with_relationships, opts),
|
||||
{:ok, value} <- api.load(value, load) do
|
||||
{:ok, %{result: value, errors: []}}
|
||||
else
|
||||
{:error, error} ->
|
||||
{:ok, %{result: nil, errors: List.wrap(error)}}
|
||||
end
|
||||
|
||||
Absinthe.Resolution.put_result(resolution, to_resolution(result))
|
||||
end
|
||||
end
|
||||
|
||||
def mutate(%{arguments: %{id: id}, context: context} = resolution, {api, resource, action}) do
|
||||
case api.get(resource, id) do
|
||||
nil ->
|
||||
{:ok, %{result: nil, errors: [to_errors("not found")]}}
|
||||
|
||||
initial ->
|
||||
opts =
|
||||
if AshGraphql.Api.authorize?(api) do
|
||||
[actor: Map.get(context, :actor), action: action]
|
||||
else
|
||||
[action: action]
|
||||
end
|
||||
|
||||
result =
|
||||
case api.destroy(initial, opts) do
|
||||
:ok -> {:ok, %{result: initial, errors: []}}
|
||||
{:error, error} -> {:ok, %{result: nil, errors: to_errors(error)}}
|
||||
end
|
||||
|
||||
Absinthe.Resolution.put_result(resolution, to_resolution(result))
|
||||
end
|
||||
end
|
||||
|
||||
defp to_errors(errors) do
|
||||
errors
|
||||
|> List.wrap()
|
||||
|> Enum.map(fn error ->
|
||||
cond do
|
||||
is_binary(error) ->
|
||||
%{message: error}
|
||||
|
||||
Exception.exception?(error) ->
|
||||
%{
|
||||
message: Exception.message(error)
|
||||
}
|
||||
|
||||
true ->
|
||||
%{message: "something went wrong"}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def resolve_assoc(%{source: parent} = resolution, {:one, name}) do
|
||||
Absinthe.Resolution.put_result(resolution, {:ok, Map.get(parent, name)})
|
||||
end
|
||||
|
||||
def resolve_assoc(%{source: parent} = resolution, {:many, name}) do
|
||||
values = Map.get(parent, name)
|
||||
paginator = %AshGraphql.Paginator{results: values, count: Enum.count(values)}
|
||||
|
||||
Absinthe.Resolution.put_result(resolution, {:ok, paginator})
|
||||
end
|
||||
|
||||
defp load_nested(resource, fields) do
|
||||
Enum.map(fields, fn field ->
|
||||
relationship = Ash.Resource.relationship(resource, field.schema_node.identifier)
|
||||
|
||||
cond do
|
||||
!relationship ->
|
||||
field.schema_node.identifier
|
||||
|
||||
relationship.cardinality == :many ->
|
||||
trimmed_nested =
|
||||
Enum.flat_map(field.selections, fn nested ->
|
||||
if nested.schema_node.identifier == :results do
|
||||
nested.selections
|
||||
else
|
||||
[nested]
|
||||
end
|
||||
end)
|
||||
|
||||
nested_loads = load_nested(relationship.destination, trimmed_nested)
|
||||
|
||||
query = Ash.Query.load(relationship.destination, nested_loads)
|
||||
|
||||
query =
|
||||
Enum.reduce(field.arguments, query, fn
|
||||
%{name: "limit", value: value}, query ->
|
||||
Ash.Query.limit(query, value)
|
||||
|
||||
%{name: "offset", value: value}, query ->
|
||||
Ash.Query.offset(query, value)
|
||||
|
||||
%{name: "filter", value: value}, query ->
|
||||
case Jason.decode(value) do
|
||||
{:ok, decoded} ->
|
||||
Ash.Query.filter(query, to_snake_case(decoded))
|
||||
|
||||
{:error, error} ->
|
||||
raise "Error parsing filter: #{inspect(error)}"
|
||||
end
|
||||
end)
|
||||
|
||||
{field.schema_node.identifier, query}
|
||||
|
||||
true ->
|
||||
nested_loads = load_nested(relationship.destination, field.selections)
|
||||
|
||||
query = Ash.Query.load(relationship.destination, nested_loads)
|
||||
{field.schema_node.identifier, query}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp to_snake_case(map) when is_map(map) do
|
||||
Enum.into(map, %{}, fn {key, value} ->
|
||||
{Macro.underscore(key), to_snake_case(value)}
|
||||
end)
|
||||
end
|
||||
|
||||
defp to_snake_case(list) when is_list(list) do
|
||||
Enum.map(list, &to_snake_case/1)
|
||||
end
|
||||
|
||||
defp to_snake_case(other), do: other
|
||||
|
||||
defp to_resolution({:ok, value}), do: {:ok, value}
|
||||
defp to_resolution({:error, error}), do: {:error, List.wrap(error)}
|
||||
|
||||
defp to_resolution({:error, error}),
|
||||
do: {:error, error |> List.wrap() |> Enum.map(&Exception.message(&1))}
|
||||
end
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
defmodule AshGraphql.GraphqlResource do
|
||||
@callback graphql_fields() :: [%AshGraphql.GraphqlResource.Field{}]
|
||||
@callback graphql_type() :: atom
|
||||
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
@extensions AshGraphql.GraphqlResource
|
||||
@behaviour AshGraphql.GraphqlResource
|
||||
@graphql_type nil
|
||||
Module.register_attribute(__MODULE__, :graphql_fields, accumulate: true)
|
||||
|
||||
import AshGraphql.GraphqlResource, only: [graphql: 1]
|
||||
end
|
||||
end
|
||||
|
||||
defmacro graphql(do: body) do
|
||||
quote do
|
||||
import AshGraphql.GraphqlResource, only: [fields: 1, type: 1]
|
||||
unquote(body)
|
||||
import AshGraphql.GraphqlResource, only: [graphql: 1]
|
||||
end
|
||||
end
|
||||
|
||||
defmacro fields(do: body) do
|
||||
quote do
|
||||
import AshGraphql.GraphqlResource, only: [field: 2, field: 3]
|
||||
|
||||
unquote(body)
|
||||
|
||||
import AshGraphql.GraphqlResource, only: [fields: 1, type: 1]
|
||||
end
|
||||
end
|
||||
|
||||
defmacro type(type) do
|
||||
quote do
|
||||
@graphql_type unquote(type)
|
||||
end
|
||||
end
|
||||
|
||||
defmacro field(name, action, opts \\ []) do
|
||||
quote do
|
||||
field = AshGraphql.GraphqlResource.Field.new(unquote(name), unquote(action), unquote(opts))
|
||||
@graphql_fields field
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def before_compile_hook(_env) do
|
||||
quote do
|
||||
unless @graphql_type do
|
||||
raise "Must set graphql type for #{__MODULE__}"
|
||||
end
|
||||
|
||||
def graphql_type() do
|
||||
@graphql_type
|
||||
end
|
||||
|
||||
def graphql_fields() do
|
||||
@graphql_fields
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,15 +0,0 @@
|
|||
defmodule AshGraphql.GraphqlResource.Field do
|
||||
defstruct [:name, :action, :type]
|
||||
|
||||
def new(name, action, opts) do
|
||||
if opts[:type] && opts[:type] not in [:read, :get] do
|
||||
raise "Can only specify `read` or `get` for `type`"
|
||||
end
|
||||
|
||||
%__MODULE__{
|
||||
name: name,
|
||||
action: action,
|
||||
type: opts[:type] || :read
|
||||
}
|
||||
end
|
||||
end
|
|
@ -1,27 +0,0 @@
|
|||
# defmodule AshGraphql.GraphqlResource.ResourceTypes do
|
||||
# defmacro define_types(name, attributes, relationships) do
|
||||
# quote do
|
||||
# name = unquote(name)
|
||||
# attributes = Enum.map(unquote(attributes), &Map.to_list/1)
|
||||
# relationships = Enum.map(unquote(relationships), &Map.to_list/1)
|
||||
|
||||
# defmodule __MODULE__.GraphqlTypes do
|
||||
# use Absinthe.Schema.Notation
|
||||
|
||||
# quote do
|
||||
# object unquote(String.to_atom(name)) do
|
||||
# for attribute <- unquote(attributes) do
|
||||
# if attribute[:name] == :id and attribute[:primary_key?] do
|
||||
# field :id, :id
|
||||
# else
|
||||
# quote do
|
||||
# field unquote(attribute[:name]), unquote(attribute[:type])
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# end
|
|
@ -1,3 +1,3 @@
|
|||
defmodule AshGraphql.Paginator do
|
||||
defstruct [:limit, :results, :total, offset: 0]
|
||||
defstruct [:results, :count]
|
||||
end
|
||||
|
|
46
lib/resource/mutation.ex
Normal file
46
lib/resource/mutation.ex
Normal file
|
@ -0,0 +1,46 @@
|
|||
defmodule AshGraphql.Resource.Mutation do
|
||||
defstruct [:name, :action, :type]
|
||||
|
||||
@create_schema [
|
||||
name: [
|
||||
type: :atom,
|
||||
doc: "The name to use for the mutation.",
|
||||
default: :get
|
||||
],
|
||||
action: [
|
||||
type: :atom,
|
||||
doc: "The action to use for the mutation.",
|
||||
required: true
|
||||
]
|
||||
]
|
||||
|
||||
@update_schema [
|
||||
name: [
|
||||
type: :atom,
|
||||
doc: "The name to use for the mutation.",
|
||||
default: :get
|
||||
],
|
||||
action: [
|
||||
type: :atom,
|
||||
doc: "The action to use for the mutation.",
|
||||
required: true
|
||||
]
|
||||
]
|
||||
|
||||
@destroy_schema [
|
||||
name: [
|
||||
type: :atom,
|
||||
doc: "The name to use for the mutation.",
|
||||
default: :get
|
||||
],
|
||||
action: [
|
||||
type: :atom,
|
||||
doc: "The action to use for the mutation.",
|
||||
required: true
|
||||
]
|
||||
]
|
||||
|
||||
def create_schema, do: @create_schema
|
||||
def update_schema, do: @update_schema
|
||||
def destroy_schema, do: @destroy_schema
|
||||
end
|
32
lib/resource/query.ex
Normal file
32
lib/resource/query.ex
Normal file
|
@ -0,0 +1,32 @@
|
|||
defmodule AshGraphql.Resource.Query do
|
||||
defstruct [:name, :action, :type]
|
||||
|
||||
@get_schema [
|
||||
name: [
|
||||
type: :atom,
|
||||
doc: "The name to use for the query.",
|
||||
default: :get
|
||||
],
|
||||
action: [
|
||||
type: :atom,
|
||||
doc: "The action to use for the query.",
|
||||
required: true
|
||||
]
|
||||
]
|
||||
|
||||
@list_schema [
|
||||
name: [
|
||||
type: :atom,
|
||||
doc: "The name to use for the query.",
|
||||
default: :list
|
||||
],
|
||||
action: [
|
||||
type: :atom,
|
||||
doc: "The action to use for the query.",
|
||||
required: true
|
||||
]
|
||||
]
|
||||
|
||||
def get_schema, do: @get_schema
|
||||
def list_schema, do: @list_schema
|
||||
end
|
555
lib/resource/resource.ex
Normal file
555
lib/resource/resource.ex
Normal file
|
@ -0,0 +1,555 @@
|
|||
defmodule AshGraphql.Resource do
|
||||
@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: AshGraphql.Resource.Query.get_schema(),
|
||||
target: AshGraphql.Resource.Query,
|
||||
auto_set_fields: [
|
||||
type: :get
|
||||
]
|
||||
}
|
||||
|
||||
@list %Ash.Dsl.Entity{
|
||||
name: :list,
|
||||
schema: AshGraphql.Resource.Query.list_schema(),
|
||||
args: [:name, :action],
|
||||
describe: "A query to fetch a list of records",
|
||||
examples: [
|
||||
"list :list_posts, :default"
|
||||
],
|
||||
target: AshGraphql.Resource.Query,
|
||||
auto_set_fields: [
|
||||
type: :list
|
||||
]
|
||||
}
|
||||
|
||||
@create %Ash.Dsl.Entity{
|
||||
name: :create,
|
||||
schema: AshGraphql.Resource.Mutation.create_schema(),
|
||||
args: [:name, :action],
|
||||
describe: "A mutation to create a record",
|
||||
examples: [
|
||||
"create :create_post, :default"
|
||||
],
|
||||
target: AshGraphql.Resource.Mutation,
|
||||
auto_set_fields: [
|
||||
type: :create
|
||||
]
|
||||
}
|
||||
|
||||
@update %Ash.Dsl.Entity{
|
||||
name: :update,
|
||||
schema: AshGraphql.Resource.Mutation.update_schema(),
|
||||
args: [:name, :action],
|
||||
describe: "A mutation to update a record",
|
||||
examples: [
|
||||
"update :update_post, :default"
|
||||
],
|
||||
target: AshGraphql.Resource.Mutation,
|
||||
auto_set_fields: [
|
||||
type: :update
|
||||
]
|
||||
}
|
||||
|
||||
@destroy %Ash.Dsl.Entity{
|
||||
name: :destroy,
|
||||
schema: AshGraphql.Resource.Mutation.destroy_schema(),
|
||||
args: [:name, :action],
|
||||
describe: "A mutation to destroy a record",
|
||||
examples: [
|
||||
"destroy :destroy_post, :default"
|
||||
],
|
||||
target: AshGraphql.Resource.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 [
|
||||
AshJsonApi.Resource.Transformers.RequireIdPkey
|
||||
]
|
||||
|
||||
use Ash.Dsl.Extension, sections: [@graphql], transformers: @transformers
|
||||
|
||||
def queries(resource) do
|
||||
Ash.Dsl.Extension.get_entities(resource, [:graphql, :queries])
|
||||
end
|
||||
|
||||
def mutations(resource) do
|
||||
Ash.Dsl.Extension.get_entities(resource, [:graphql, :mutations])
|
||||
end
|
||||
|
||||
def type(resource) do
|
||||
Ash.Dsl.Extension.get_opt(resource, [:graphql], :type, nil)
|
||||
end
|
||||
|
||||
def fields(resource) do
|
||||
Ash.Dsl.Extension.get_opt(resource, [:graphql], :fields, [])
|
||||
end
|
||||
|
||||
@doc false
|
||||
def queries(api, resource, schema) do
|
||||
type = AshGraphql.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
|
||||
|
||||
@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
|
||||
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: AshGraphql.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 = AshGraphql.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 ->
|
||||
AshGraphql.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
|
||||
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
|
||||
|
||||
defp page_of(resource, schema) do
|
||||
type = AshGraphql.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 = AshGraphql.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 = AshGraphql.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
|
||||
|
||||
defp relationships(resource, schema, fields) do
|
||||
resource
|
||||
|> Ash.Resource.relationships()
|
||||
|> Enum.filter(&(&1.name in fields))
|
||||
|> Enum.filter(fn relationship ->
|
||||
AshGraphql.Resource in Ash.Resource.extensions(relationship.destination)
|
||||
end)
|
||||
|> Enum.map(fn
|
||||
%{cardinality: :one} = relationship ->
|
||||
type = AshGraphql.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 = AshGraphql.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} = Ash.Query.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
|
20
lib/resource/transformers/require_id_pkey.ex
Normal file
20
lib/resource/transformers/require_id_pkey.ex
Normal file
|
@ -0,0 +1,20 @@
|
|||
defmodule AshGraphql.Resource.Transformers.RequireIdPkey do
|
||||
@moduledoc "Ensures that the resource has a primary key called `id`"
|
||||
use Ash.Dsl.Transformer
|
||||
|
||||
alias Ash.Dsl.Transformer
|
||||
|
||||
def transform(_resource, dsl) do
|
||||
primary_key =
|
||||
dsl
|
||||
|> Transformer.get_entities([:attributes])
|
||||
|> Enum.filter(& &1.primary_key?)
|
||||
|> Enum.map(& &1.name)
|
||||
|
||||
unless primary_key == [:id] do
|
||||
raise "AshGraphql currently requires the primary key to be a field called `id`"
|
||||
end
|
||||
|
||||
{:ok, dsl}
|
||||
end
|
||||
end
|
15
mix.exs
15
mix.exs
|
@ -21,8 +21,21 @@ defmodule AshGraphql.MixProject do
|
|||
# Run "mix help deps" to learn about dependencies.
|
||||
defp deps do
|
||||
[
|
||||
{:absinthe, "~> 1.5.0-rc.5"},
|
||||
{:absinthe, "~> 1.5.2"},
|
||||
{:jason, "~> 1.2"},
|
||||
{:ash, path: "../ash"}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
# defmodule Foo do
|
||||
# use Absinthe.Schema.Notation
|
||||
|
||||
# object :foo do
|
||||
# field :item, :string do
|
||||
# resolve fn _, _, _ ->
|
||||
# "hello"
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
|
|
11
mix.lock
11
mix.lock
|
@ -1,13 +1,14 @@
|
|||
%{
|
||||
"absinthe": {:hex, :absinthe, "1.5.0-rc.5", "90b6335d452bfe72532257bb60c54793f6b8c4992b35b6861dc9adea5d01f463", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7caf5b2d7d4be5a401dc4a17d219d44650af5dce4fe635687ca543c1823047a6"},
|
||||
"absinthe": {:hex, :absinthe, "1.5.2", "2f9449b0c135ea61c09c11968d3d4fe6abd5bed38cf9be1c6d6b7c5ec858cfa0", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "669c84879629b7fffdc6cda9361ab9c81c9c7691e65418ba089b912a227963ac"},
|
||||
"ashton": {:hex, :ashton, "0.4.1", "d0f7782ac44fa22da7ce544028ee3d2078592a834d8adf3e5b4b6aeb94413a55", [:mix], [], "hexpm", "24db667932517fdbc3f2dae777f28b8d87629271387d4490bc4ae8d9c46ff3d3"},
|
||||
"decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"},
|
||||
"ecto": {:hex, :ecto, "3.4.3", "3a14c2500c3964165245a4f24a463e080762f7ccd0c632c763ea589f75ca205f", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b6f18dea95f2004d0369f6a8346513ca3f706614f4ede219a5f3fe5db5dd962"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"},
|
||||
"ets": {:hex, :ets, "0.8.0", "90153faafd289bb0801a537d5b05661f46d5e70b2bb55cccf5ab7f0d41d07832", [:mix], [], "hexpm", "bda4e05b16eada36798cfda16db551dc5243c0adc9a6dfe655b1bc1279b99cb8"},
|
||||
"jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"},
|
||||
"machinery": {:hex, :machinery, "1.0.0", "df6968d84c651b9971a33871c78c10157b6e13e4f3390b0bee5b0e8bdea8c781", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "4f6eb4185a48e7245360bedf653af4acc6fa6ae8ff4690619395543fa1a8395f"},
|
||||
"nimble_options": {:hex, :nimble_options, "0.2.1", "7eac99688c2544d4cc3ace36ee8f2bf4d738c14d031bd1e1193aab096309d488", [:mix], [], "hexpm", "ca48293609306791ce2634818d849b7defe09330adb7e4e1118a0bc59bed1cf4"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"},
|
||||
"picosat_elixir": {:hex, :picosat_elixir, "0.1.3", "1e4eab27786b7dc7764c307555d8943cbba82912ed943737372760377be05ec8", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3add4d5ea1afa49f51bb7576bae000fe88091969804cc25baf47ffec48a9c626"},
|
||||
"telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"},
|
||||
"nimble_options": {:hex, :nimble_options, "0.3.0", "1872911bf50a048f04da26e02704e6aeafc362c2daa7636b6dbfda9492ccfcfa", [:mix], [], "hexpm", "180790a8644fea402452bc15bb54b9bf2c8e5c1fdeb6b39d8072e59c324edf7f"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"},
|
||||
"picosat_elixir": {:hex, :picosat_elixir, "0.1.4", "d259219ae27148c07c4aa3fdee61b1a14f4bc7f83b0ebdf2752558d06b302c62", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "eb41cb16053a45c8556de32f065084af98ea0b13a523fb46dfb4f9cff4152474"},
|
||||
"telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue