mirror of
https://github.com/ash-project/ash_graphql.git
synced 2024-09-19 21:03:09 +12:00
feat: support embedded resources
This commit is contained in:
parent
9b12983ef8
commit
a820beb474
8 changed files with 552 additions and 188 deletions
|
@ -11,6 +11,7 @@ locals_without_parens = [
|
|||
identity: 1,
|
||||
list: 2,
|
||||
list: 3,
|
||||
primary_key_delimiter: 1,
|
||||
type: 1,
|
||||
update: 2,
|
||||
update: 3
|
||||
|
|
2
.github/workflows/elixir.yml
vendored
2
.github/workflows/elixir.yml
vendored
|
@ -18,7 +18,7 @@ jobs:
|
|||
matrix:
|
||||
otp: ["23", "22"]
|
||||
elixir: ["1.10.3"]
|
||||
ash: ["master", "1.26.11"]
|
||||
ash: ["master", "1.28.0"]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ASH_VERSION: ${{matrix.ash}}
|
||||
|
|
|
@ -54,8 +54,11 @@ defmodule AshGraphql do
|
|||
|
||||
type_definitions =
|
||||
if unquote(first?) do
|
||||
embedded_types = AshGraphql.get_embedded_types(unquote(apis))
|
||||
|
||||
AshGraphql.Api.global_type_definitions(__MODULE__) ++
|
||||
AshGraphql.Api.type_definitions(api, __MODULE__)
|
||||
AshGraphql.Api.type_definitions(api, __MODULE__) ++
|
||||
embedded_types
|
||||
else
|
||||
AshGraphql.Api.type_definitions(api, __MODULE__)
|
||||
end
|
||||
|
@ -64,8 +67,7 @@ defmodule AshGraphql do
|
|||
List.update_at(blueprint_with_mutations.schema_definitions, 0, fn schema_def ->
|
||||
%{
|
||||
schema_def
|
||||
| imports: [{Absinthe.Type.Custom, []} | List.wrap(schema_def.imports)],
|
||||
type_definitions: schema_def.type_definitions ++ type_definitions
|
||||
| type_definitions: schema_def.type_definitions ++ type_definitions
|
||||
}
|
||||
end)
|
||||
|
||||
|
@ -73,11 +75,67 @@ defmodule AshGraphql do
|
|||
end
|
||||
end
|
||||
|
||||
if first? do
|
||||
import_types(Absinthe.Type.Custom)
|
||||
import_types(AshGraphql.Types.JSON)
|
||||
end
|
||||
|
||||
@pipeline_modifier Module.concat(api, AshTypes)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get_embedded_types(apis) do
|
||||
apis
|
||||
|> Enum.map(&elem(&1, 0))
|
||||
|> Enum.flat_map(&Ash.Api.resources/1)
|
||||
|> Enum.flat_map(&Ash.Resource.attributes/1)
|
||||
|> Enum.map(& &1.type)
|
||||
|> Enum.filter(&Ash.Type.embedded_type?/1)
|
||||
|> Enum.map(fn
|
||||
{:array, resource} ->
|
||||
resource
|
||||
|
||||
resource ->
|
||||
resource
|
||||
end)
|
||||
|> Enum.filter(&(AshGraphql.Resource in Ash.Resource.extensions(&1)))
|
||||
|> Enum.flat_map(fn type ->
|
||||
[type] ++ get_nested_embedded_types(type)
|
||||
end)
|
||||
|> Enum.flat_map(fn embedded_type ->
|
||||
[
|
||||
AshGraphql.Resource.type_definition(
|
||||
embedded_type,
|
||||
Module.concat(embedded_type, ShadowApi),
|
||||
__MODULE__
|
||||
),
|
||||
AshGraphql.Resource.embedded_type_input(
|
||||
embedded_type,
|
||||
__MODULE__
|
||||
)
|
||||
]
|
||||
end)
|
||||
end
|
||||
|
||||
defp get_nested_embedded_types(embedded_type) do
|
||||
embedded_type
|
||||
|> Ash.Resource.attributes()
|
||||
|> Enum.map(& &1.type)
|
||||
|> Enum.filter(&Ash.Type.embedded_type?/1)
|
||||
|> Enum.map(fn
|
||||
{:array, resource} ->
|
||||
resource
|
||||
|
||||
resource ->
|
||||
resource
|
||||
end)
|
||||
|> Enum.filter(&(AshGraphql.Resource in Ash.Resource.extensions(&1)))
|
||||
|> Enum.flat_map(fn type ->
|
||||
[type] ++ get_nested_embedded_types(type)
|
||||
end)
|
||||
end
|
||||
|
||||
def add_context(ctx, apis) do
|
||||
dataloader =
|
||||
apis
|
||||
|
|
|
@ -15,23 +15,33 @@ defmodule AshGraphql.Graphql.Resolver do
|
|||
|
||||
filter =
|
||||
if identity do
|
||||
resource
|
||||
|> Ash.Resource.identities()
|
||||
|> Enum.find(&(&1.name == identity))
|
||||
|> Map.get(:keys)
|
||||
|> Enum.map(fn key ->
|
||||
{key, Map.get(arguments, key)}
|
||||
end)
|
||||
{:ok,
|
||||
resource
|
||||
|> Ash.Resource.identities()
|
||||
|> Enum.find(&(&1.name == identity))
|
||||
|> Map.get(:keys)
|
||||
|> Enum.map(fn key ->
|
||||
{key, Map.get(arguments, key)}
|
||||
end)}
|
||||
else
|
||||
[id: Map.get(arguments, :id)]
|
||||
case AshGraphql.Resource.decode_primary_key(resource, Map.get(arguments, :id) || "") do
|
||||
{:ok, value} -> {:ok, [id: value]}
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
result =
|
||||
resource
|
||||
|> Ash.Query.new()
|
||||
|> Ash.Query.set_tenant(Map.get(context, :tenant))
|
||||
|> Ash.Query.filter(^filter)
|
||||
|> api.read_one(opts)
|
||||
case filter do
|
||||
{:ok, filter} ->
|
||||
resource
|
||||
|> Ash.Query.new()
|
||||
|> Ash.Query.set_tenant(Map.get(context, :tenant))
|
||||
|> Ash.Query.filter(^filter)
|
||||
|> api.read_one(opts)
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
|
||||
Absinthe.Resolution.put_result(resolution, to_resolution(result))
|
||||
end
|
||||
|
@ -118,10 +128,7 @@ defmodule AshGraphql.Graphql.Resolver do
|
|||
|
||||
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)
|
||||
changeset_with_relationships = changeset_with_relationships(relationships, changeset)
|
||||
|
||||
opts = [
|
||||
actor: Map.get(context, :actor),
|
||||
|
@ -151,54 +158,55 @@ defmodule AshGraphql.Graphql.Resolver do
|
|||
) do
|
||||
filter =
|
||||
if identity do
|
||||
resource
|
||||
|> Ash.Resource.identities()
|
||||
|> Enum.find(&(&1.name == identity))
|
||||
|> Map.get(:keys)
|
||||
|> Enum.map(fn key ->
|
||||
{key, Map.get(arguments, key)}
|
||||
end)
|
||||
{:ok,
|
||||
resource
|
||||
|> Ash.Resource.identities()
|
||||
|> Enum.find(&(&1.name == identity))
|
||||
|> Map.get(:keys)
|
||||
|> Enum.map(fn key ->
|
||||
{key, Map.get(arguments, key)}
|
||||
end)}
|
||||
else
|
||||
[id: Map.get(arguments, :id)]
|
||||
case AshGraphql.Resource.decode_primary_key(resource, Map.get(arguments, :id) || "") do
|
||||
{:ok, value} -> {:ok, [id: value]}
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
resource
|
||||
|> Ash.Query.filter(^filter)
|
||||
|> Ash.Query.set_tenant(Map.get(context, :tenant))
|
||||
|> api.read_one!()
|
||||
|> case do
|
||||
nil ->
|
||||
{:ok, %{result: nil, errors: [to_errors("not found")]}}
|
||||
case filter do
|
||||
{:ok, filter} ->
|
||||
resource
|
||||
|> Ash.Query.filter(^filter)
|
||||
|> Ash.Query.set_tenant(Map.get(context, :tenant))
|
||||
|> api.read_one!()
|
||||
|> case do
|
||||
nil ->
|
||||
{:ok, %{result: nil, errors: [to_errors("not found")]}}
|
||||
|
||||
initial ->
|
||||
{attributes, relationships, arguments} = split_attrs_rels_and_args(input, resource)
|
||||
changeset = Ash.Changeset.new(initial, attributes)
|
||||
initial ->
|
||||
{attributes, relationships, arguments} = split_attrs_rels_and_args(input, resource)
|
||||
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)
|
||||
changeset_with_relationships = changeset_with_relationships(relationships, changeset)
|
||||
|
||||
opts = [
|
||||
actor: Map.get(context, :actor),
|
||||
authorize?: AshGraphql.Api.authorize?(api),
|
||||
action: action
|
||||
]
|
||||
opts = [
|
||||
actor: Map.get(context, :actor),
|
||||
authorize?: AshGraphql.Api.authorize?(api),
|
||||
action: action
|
||||
]
|
||||
|
||||
result =
|
||||
changeset_with_relationships
|
||||
|> Ash.Changeset.set_tenant(Map.get(context, :tenant))
|
||||
|> Ash.Changeset.set_arguments(arguments)
|
||||
|> api.update(opts)
|
||||
|> case do
|
||||
{:ok, value} ->
|
||||
{:ok, %{result: value, errors: []}}
|
||||
result =
|
||||
changeset_with_relationships
|
||||
|> Ash.Changeset.set_tenant(Map.get(context, :tenant))
|
||||
|> Ash.Changeset.set_arguments(arguments)
|
||||
|> api.update(opts)
|
||||
|> update_result()
|
||||
|
||||
{:error, error} ->
|
||||
{:ok, %{result: nil, errors: List.wrap(error)}}
|
||||
end
|
||||
Absinthe.Resolution.put_result(resolution, to_resolution(result))
|
||||
end
|
||||
|
||||
Absinthe.Resolution.put_result(resolution, to_resolution(result))
|
||||
{:error, error} ->
|
||||
Absinthe.Resolution.put_result(resolution, to_resolution({:error, error}))
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -208,47 +216,111 @@ defmodule AshGraphql.Graphql.Resolver do
|
|||
) do
|
||||
filter =
|
||||
if identity do
|
||||
resource
|
||||
|> Ash.Resource.identities()
|
||||
|> Enum.find(&(&1.name == identity))
|
||||
|> Map.get(:keys)
|
||||
|> Enum.map(fn key ->
|
||||
{key, Map.get(arguments, key)}
|
||||
end)
|
||||
{:ok,
|
||||
resource
|
||||
|> Ash.Resource.identities()
|
||||
|> Enum.find(&(&1.name == identity))
|
||||
|> Map.get(:keys)
|
||||
|> Enum.map(fn key ->
|
||||
{key, Map.get(arguments, key)}
|
||||
end)}
|
||||
else
|
||||
[id: Map.get(arguments, :id)]
|
||||
case AshGraphql.Resource.decode_primary_key(resource, Map.get(arguments, :id) || "") do
|
||||
{:ok, value} -> {:ok, [id: value]}
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
resource
|
||||
|> Ash.Query.filter(^filter)
|
||||
|> Ash.Query.set_tenant(Map.get(context, :tenant))
|
||||
|> api.read_one!()
|
||||
|> case do
|
||||
nil ->
|
||||
{:ok, %{result: nil, errors: [to_errors("not found")]}}
|
||||
case filter do
|
||||
{:ok, filter} ->
|
||||
resource
|
||||
|> Ash.Query.filter(^filter)
|
||||
|> Ash.Query.set_tenant(Map.get(context, :tenant))
|
||||
|> api.read_one!()
|
||||
|> case 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
|
||||
initial ->
|
||||
opts = destroy_opts(api, context, action)
|
||||
|
||||
result =
|
||||
initial
|
||||
|> Ash.Changeset.new()
|
||||
|> Ash.Changeset.set_tenant(Map.get(context, :tenant))
|
||||
|> api.destroy(opts)
|
||||
|> case do
|
||||
:ok -> {:ok, %{result: initial, errors: []}}
|
||||
{:error, error} -> {:ok, %{result: nil, errors: to_errors(error)}}
|
||||
end
|
||||
result =
|
||||
initial
|
||||
|> Ash.Changeset.new()
|
||||
|> Ash.Changeset.set_tenant(Map.get(context, :tenant))
|
||||
|> api.destroy(opts)
|
||||
|> destroy_result(initial)
|
||||
|
||||
Absinthe.Resolution.put_result(resolution, to_resolution(result))
|
||||
Absinthe.Resolution.put_result(resolution, to_resolution(result))
|
||||
end
|
||||
|
||||
{:error, error} ->
|
||||
Absinthe.Resolution.put_result(resolution, to_resolution({:error, error}))
|
||||
end
|
||||
end
|
||||
|
||||
defp destroy_opts(api, context, action) do
|
||||
if AshGraphql.Api.authorize?(api) do
|
||||
[actor: Map.get(context, :actor), action: action]
|
||||
else
|
||||
[action: action]
|
||||
end
|
||||
end
|
||||
|
||||
defp update_result(result) do
|
||||
case result do
|
||||
{:ok, value} ->
|
||||
{:ok, %{result: value, errors: []}}
|
||||
|
||||
{:error, error} ->
|
||||
{:ok, %{result: nil, errors: List.wrap(error)}}
|
||||
end
|
||||
end
|
||||
|
||||
defp destroy_result(result, initial) do
|
||||
case result do
|
||||
:ok -> {:ok, %{result: initial, errors: []}}
|
||||
{:error, error} -> {:ok, %{result: nil, errors: to_errors(error)}}
|
||||
end
|
||||
end
|
||||
|
||||
defp changeset_with_relationships(relationships, changeset) do
|
||||
Enum.reduce(relationships, changeset, fn {relationship, replacement}, changeset ->
|
||||
case decode_related_pkeys(changeset, relationship, replacement) do
|
||||
{:ok, replacement} ->
|
||||
Ash.Changeset.replace_relationship(changeset, relationship, replacement)
|
||||
|
||||
{:error, _error} ->
|
||||
Ash.Changeset.add_error(changeset, "Invalid relationship primary keys")
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp decode_related_pkeys(changeset, relationship, primary_keys)
|
||||
when is_list(primary_keys) do
|
||||
primary_keys
|
||||
|> Enum.reduce_while({:ok, []}, fn pkey, {:ok, list} ->
|
||||
case AshGraphql.Resource.decode_primary_key(
|
||||
Ash.Resource.related(changeset.resource, relationship),
|
||||
pkey
|
||||
) do
|
||||
{:ok, value} -> {:cont, {:ok, [value | list]}}
|
||||
{:error, error} -> {:halt, {:error, error}}
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
{:ok, values} -> {:ok, Enum.reverse(values)}
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
defp decode_related_pkeys(changeset, relationship, primary_key) do
|
||||
AshGraphql.Resource.decode_primary_key(
|
||||
Ash.Resource.related(changeset.resource, relationship),
|
||||
primary_key
|
||||
)
|
||||
end
|
||||
|
||||
defp split_attrs_rels_and_args(input, resource) do
|
||||
Enum.reduce(input, {%{}, %{}, %{}}, fn {key, value}, {attrs, rels, args} ->
|
||||
cond do
|
||||
|
|
|
@ -142,6 +142,11 @@ defmodule AshGraphql.Resource do
|
|||
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: [
|
||||
|
@ -179,6 +184,44 @@ defmodule AshGraphql.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.primary_key(resource) do
|
||||
[field] ->
|
||||
Map.get(record, field)
|
||||
|
||||
keys ->
|
||||
delimiter = primary_key_delimiter(resource)
|
||||
|
||||
[_ | concatenated_keys] =
|
||||
keys
|
||||
|> Enum.reverse()
|
||||
|> Enum.reduce([], fn key, acc -> [delimiter, to_string(Map.get(record, key)), acc] end)
|
||||
|
||||
IO.iodata_to_binary(concatenated_keys)
|
||||
end
|
||||
end
|
||||
|
||||
def decode_primary_key(resource, value) do
|
||||
case Ash.Resource.primary_key(resource) do
|
||||
[_field] ->
|
||||
{:ok, value}
|
||||
|
||||
fields ->
|
||||
delimiter = primary_key_delimiter(resource)
|
||||
parts = String.split(value, delimiter)
|
||||
|
||||
if Enum.count(parts) == Enum.count(fields) do
|
||||
{:ok, Enum.zip(fields, parts)}
|
||||
else
|
||||
{:error, "Invalid primary key"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def queries(api, resource, schema) do
|
||||
type = Resource.type(resource)
|
||||
|
@ -190,7 +233,7 @@ defmodule AshGraphql.Resource do
|
|||
query_action = Ash.Resource.action(resource, query.action, :read)
|
||||
|
||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||
arguments: args(query.type, resource, query_action, query.identity),
|
||||
arguments: args(query.type, resource, query_action, schema, query.identity),
|
||||
identifier: query.name,
|
||||
middleware: [
|
||||
{{AshGraphql.Graphql.Resolver, :resolve}, {api, resource, query}}
|
||||
|
@ -356,6 +399,35 @@ defmodule AshGraphql.Resource do
|
|||
end)
|
||||
end
|
||||
|
||||
@doc false
|
||||
# sobelow_skip ["DOS.StringToAtom"]
|
||||
def embedded_type_input(resource, schema) do
|
||||
attribute_fields =
|
||||
resource
|
||||
|> Ash.Resource.public_attributes()
|
||||
|> Enum.filter(& &1.writable?)
|
||||
|> Enum.map(fn attribute ->
|
||||
type = field_type(attribute.type, attribute, resource)
|
||||
|
||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||
description: attribute.description,
|
||||
identifier: attribute.name,
|
||||
module: schema,
|
||||
name: to_string(attribute.name),
|
||||
type: type
|
||||
}
|
||||
end)
|
||||
|
||||
name = AshGraphql.Resource.type(resource)
|
||||
|
||||
%Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
|
||||
fields: attribute_fields,
|
||||
identifier: String.to_atom("#{name}_input"),
|
||||
module: schema,
|
||||
name: Macro.camelize("#{name}_input")
|
||||
}
|
||||
end
|
||||
|
||||
defp mutation_fields(resource, schema, mutation) do
|
||||
attribute_fields =
|
||||
resource
|
||||
|
@ -365,7 +437,7 @@ defmodule AshGraphql.Resource do
|
|||
end)
|
||||
|> Enum.filter(& &1.writable?)
|
||||
|> Enum.map(fn attribute ->
|
||||
type = field_type(attribute.type, attribute, resource)
|
||||
type = field_type(attribute.type, attribute, resource, true)
|
||||
|
||||
field_type =
|
||||
if attribute.allow_nil? || attribute.default || mutation.type == :update do
|
||||
|
@ -436,10 +508,10 @@ defmodule AshGraphql.Resource do
|
|||
type =
|
||||
if argument.allow_nil? do
|
||||
%Absinthe.Blueprint.TypeReference.NonNull{
|
||||
of_type: field_type(argument.type, argument, resource)
|
||||
of_type: field_type(argument.type, argument, resource, true)
|
||||
}
|
||||
else
|
||||
field_type(argument.type, argument, resource)
|
||||
field_type(argument.type, argument, resource, true)
|
||||
end
|
||||
|
||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||
|
@ -469,9 +541,9 @@ defmodule AshGraphql.Resource do
|
|||
end
|
||||
end
|
||||
|
||||
defp args(action_type, resource, action, identity \\ nil)
|
||||
defp args(action_type, resource, action, schema, identity \\ nil)
|
||||
|
||||
defp args(:get, _resource, _action, nil) do
|
||||
defp args(:get, _resource, _action, _schema, nil) do
|
||||
[
|
||||
%Absinthe.Blueprint.Schema.InputValueDefinition{
|
||||
name: "id",
|
||||
|
@ -482,7 +554,7 @@ defmodule AshGraphql.Resource do
|
|||
]
|
||||
end
|
||||
|
||||
defp args(:get, resource, _action, identity) do
|
||||
defp args(:get, resource, _action, _schema, identity) do
|
||||
resource
|
||||
|> Ash.Resource.identities()
|
||||
|> Enum.find(&(&1.name == identity))
|
||||
|
@ -494,31 +566,50 @@ defmodule AshGraphql.Resource do
|
|||
name: to_string(key),
|
||||
identifier: key,
|
||||
type: %Absinthe.Blueprint.TypeReference.NonNull{
|
||||
of_type: field_type(attribute.type, attribute, resource)
|
||||
of_type: field_type(attribute.type, attribute, resource, true)
|
||||
},
|
||||
description: attribute.description || ""
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
defp args(:list, resource, action, _) do
|
||||
[
|
||||
%Absinthe.Blueprint.Schema.InputValueDefinition{
|
||||
name: "filter",
|
||||
identifier: :filter,
|
||||
type: resource_filter_type(resource),
|
||||
description: "A filter to limit the results"
|
||||
},
|
||||
%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"
|
||||
}
|
||||
] ++
|
||||
pagination_args(action)
|
||||
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)
|
||||
end
|
||||
|
||||
defp pagination_args(action) do
|
||||
|
@ -609,10 +700,10 @@ defmodule AshGraphql.Resource do
|
|||
@doc false
|
||||
def type_definitions(resource, api, schema) do
|
||||
[
|
||||
type_definition(resource, api, schema),
|
||||
sort_input(resource, schema),
|
||||
filter_input(resource, schema)
|
||||
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
|
||||
|
@ -671,7 +762,7 @@ defmodule AshGraphql.Resource do
|
|||
identifier: operator.name(),
|
||||
module: schema,
|
||||
name: to_string(operator.name()),
|
||||
type: field_type(type, attribute_or_aggregate, resource)
|
||||
type: field_type(type, attribute_or_aggregate, resource, true)
|
||||
}
|
||||
]
|
||||
else
|
||||
|
@ -710,7 +801,7 @@ defmodule AshGraphql.Resource do
|
|||
identifier: operator.name(),
|
||||
module: schema,
|
||||
name: to_string(operator.name()),
|
||||
type: field_type(type, attribute_or_aggregate, resource)
|
||||
type: field_type(type, attribute_or_aggregate, resource, true)
|
||||
}
|
||||
]
|
||||
else
|
||||
|
@ -751,35 +842,47 @@ defmodule AshGraphql.Resource do
|
|||
defp constraints_to_item_constraints(_, attribute_or_aggregate), do: attribute_or_aggregate
|
||||
|
||||
defp sort_input(resource, schema) do
|
||||
%Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
|
||||
fields: [
|
||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||
identifier: :order,
|
||||
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: "order",
|
||||
default_value: :asc,
|
||||
type: :sort_order
|
||||
},
|
||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||
identifier: :field,
|
||||
module: schema,
|
||||
name: "field",
|
||||
type: resource_sort_field_type(resource)
|
||||
name: resource |> resource_sort_type() |> to_string() |> Macro.camelize()
|
||||
}
|
||||
],
|
||||
identifier: resource_sort_type(resource),
|
||||
module: schema,
|
||||
name: resource |> resource_sort_type() |> to_string() |> Macro.camelize()
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
defp filter_input(resource, schema) do
|
||||
%Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
|
||||
identifier: resource_filter_type(resource),
|
||||
module: schema,
|
||||
name: resource |> resource_filter_type() |> to_string() |> Macro.camelize(),
|
||||
fields: resource_filter_fields(resource, schema)
|
||||
}
|
||||
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
|
||||
|
@ -791,6 +894,14 @@ defmodule AshGraphql.Resource do
|
|||
defp attribute_filter_fields(resource, schema) do
|
||||
resource
|
||||
|> Ash.Resource.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{
|
||||
|
@ -894,10 +1005,7 @@ defmodule AshGraphql.Resource do
|
|||
}
|
||||
end)
|
||||
|
||||
attribute_sort_values = Enum.map(Ash.Resource.attributes(resource), & &1.name)
|
||||
aggregate_sort_values = Enum.map(Ash.Resource.aggregates(resource), & &1.name)
|
||||
|
||||
sort_values = attribute_sort_values ++ aggregate_sort_values
|
||||
sort_values = sort_values(resource)
|
||||
|
||||
sort_order = %Absinthe.Blueprint.Schema.EnumTypeDefinition{
|
||||
module: schema,
|
||||
|
@ -917,6 +1025,40 @@ defmodule AshGraphql.Resource do
|
|||
[sort_order | atom_enums]
|
||||
end
|
||||
|
||||
defp sort_values(resource) do
|
||||
attribute_sort_values =
|
||||
resource
|
||||
|> Ash.Resource.attributes()
|
||||
|> Enum.reject(fn
|
||||
%{type: {:array, _}} ->
|
||||
false
|
||||
|
||||
_ ->
|
||||
true
|
||||
end)
|
||||
|> Enum.reject(&Ash.Type.embedded_type?(&1.type))
|
||||
|> Enum.map(& &1.name)
|
||||
|
||||
aggregate_sort_values =
|
||||
resource
|
||||
|> Ash.Resource.aggregates()
|
||||
|> Enum.reject(fn aggregate ->
|
||||
case Ash.Query.Aggregate.kind_to_type(aggregate.kind, nil) do
|
||||
{:ok, {:array, _}} ->
|
||||
true
|
||||
|
||||
{:ok, type} ->
|
||||
Ash.Type.embedded_type?(type)
|
||||
|
||||
_ ->
|
||||
true
|
||||
end
|
||||
end)
|
||||
|> Enum.map(& &1.name)
|
||||
|
||||
attribute_sort_values ++ aggregate_sort_values
|
||||
end
|
||||
|
||||
# sobelow_skip ["DOS.StringToAtom"]
|
||||
defp page_of(resource, schema) do
|
||||
type = Resource.type(resource)
|
||||
|
@ -960,7 +1102,7 @@ defmodule AshGraphql.Resource do
|
|||
end
|
||||
end
|
||||
|
||||
defp type_definition(resource, api, schema) do
|
||||
def type_definition(resource, api, schema) do
|
||||
type = Resource.type(resource)
|
||||
|
||||
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{
|
||||
|
@ -980,19 +1122,11 @@ defmodule AshGraphql.Resource do
|
|||
end
|
||||
|
||||
defp attributes(resource, schema) do
|
||||
resource
|
||||
|> Ash.Resource.public_attributes()
|
||||
|> Enum.map(fn
|
||||
%{name: :id} = attribute ->
|
||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||
description: attribute.description,
|
||||
identifier: :id,
|
||||
module: schema,
|
||||
name: "id",
|
||||
type: %Absinthe.Blueprint.TypeReference.NonNull{of_type: :id}
|
||||
}
|
||||
|
||||
attribute ->
|
||||
non_id_attributes =
|
||||
resource
|
||||
|> Ash.Resource.public_attributes()
|
||||
|> Enum.reject(& &1.primary_key?)
|
||||
|> Enum.map(fn attribute ->
|
||||
field_type = field_type(attribute.type, attribute, resource)
|
||||
|
||||
field_type =
|
||||
|
@ -1011,7 +1145,79 @@ defmodule AshGraphql.Resource do
|
|||
name: to_string(attribute.name),
|
||||
type: field_type
|
||||
}
|
||||
end)
|
||||
end)
|
||||
|
||||
pkey_fields =
|
||||
case Ash.Resource.primary_key(resource) do
|
||||
[field] ->
|
||||
attribute = Ash.Resource.attribute(resource, field)
|
||||
|
||||
if attribute.private? do
|
||||
non_id_attributes
|
||||
else
|
||||
field_type = field_type(attribute.type, attribute, resource)
|
||||
|
||||
field_type =
|
||||
if attribute.allow_nil? do
|
||||
field_type
|
||||
else
|
||||
%Absinthe.Blueprint.TypeReference.NonNull{
|
||||
of_type: field_type
|
||||
}
|
||||
end
|
||||
|
||||
[
|
||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||
description: attribute.description,
|
||||
identifier: attribute.name,
|
||||
module: schema,
|
||||
name: to_string(attribute.name),
|
||||
type: field_type
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
fields ->
|
||||
added_pkey_fields =
|
||||
if :id in fields do
|
||||
[]
|
||||
else
|
||||
for field <- fields do
|
||||
attribute = Ash.Resource.attribute(resource, field)
|
||||
|
||||
field_type = field_type(attribute.type, attribute, resource)
|
||||
|
||||
field_type =
|
||||
if attribute.allow_nil? do
|
||||
field_type
|
||||
else
|
||||
%Absinthe.Blueprint.TypeReference.NonNull{
|
||||
of_type: field_type
|
||||
}
|
||||
end
|
||||
|
||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||
description: attribute.description,
|
||||
identifier: attribute.name,
|
||||
module: schema,
|
||||
name: to_string(attribute.name),
|
||||
type: field_type
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
[
|
||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||
description: "The primary key of the resource",
|
||||
identifier: :id,
|
||||
module: schema,
|
||||
name: "id",
|
||||
type: :id
|
||||
}
|
||||
] ++ added_pkey_fields
|
||||
end
|
||||
|
||||
non_id_attributes ++ pkey_fields
|
||||
end
|
||||
|
||||
# sobelow_skip ["DOS.StringToAtom"]
|
||||
|
@ -1063,7 +1269,7 @@ defmodule AshGraphql.Resource do
|
|||
middleware: [
|
||||
{{AshGraphql.Graphql.Resolver, :resolve_assoc}, {api, relationship}}
|
||||
],
|
||||
arguments: args(:list, relationship.destination, read_action),
|
||||
arguments: args(:list, relationship.destination, read_action, schema),
|
||||
type: query_type
|
||||
}
|
||||
end)
|
||||
|
@ -1102,34 +1308,51 @@ defmodule AshGraphql.Resource do
|
|||
end)
|
||||
end
|
||||
|
||||
defp field_type({:array, type}, %Ash.Resource.Aggregate{} = aggregate, resource) do
|
||||
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)
|
||||
of_type: field_type(type, aggregate, resource, input?)
|
||||
}
|
||||
end
|
||||
|
||||
defp field_type({:array, type}, attribute, resource) do
|
||||
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)
|
||||
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)
|
||||
of_type: field_type(type, new_attribute, resource, input?)
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
defp field_type(type, attribute, resource) do
|
||||
# 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
|
||||
type.graphql_type(attribute, resource)
|
||||
if Ash.Type.embedded_type?(type) do
|
||||
case type(type) do
|
||||
nil ->
|
||||
:json
|
||||
|
||||
type ->
|
||||
if input? do
|
||||
:"#{type}_input"
|
||||
else
|
||||
type
|
||||
end
|
||||
end
|
||||
else
|
||||
type.graphql_type(attribute, resource)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -4,17 +4,27 @@ defmodule AshGraphql.Resource.Transformers.RequireIdPkey do
|
|||
|
||||
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)
|
||||
def transform(resource, dsl) do
|
||||
if Ash.Type.embedded_type?(resource) do
|
||||
{:ok, dsl}
|
||||
else
|
||||
primary_key =
|
||||
dsl
|
||||
|> Transformer.get_entities([:attributes])
|
||||
|> Enum.filter(& &1.primary_key?)
|
||||
|
||||
unless primary_key == [:id] do
|
||||
raise "AshGraphql currently requires the primary key to be a field called `id`"
|
||||
case primary_key do
|
||||
[_single] ->
|
||||
{:ok, dsl}
|
||||
|
||||
[_ | _] ->
|
||||
if AshGraphql.Resource.primary_key_delimiter(resource) do
|
||||
{:ok, dsl}
|
||||
else
|
||||
{:error,
|
||||
"AshGraphql requires a `primary_key_delimiter` to be set for composite primary keys."}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
{:ok, dsl}
|
||||
end
|
||||
end
|
||||
|
|
2
mix.exs
2
mix.exs
|
@ -78,7 +78,7 @@ defmodule AshGraphql.MixProject do
|
|||
# Run "mix help deps" to learn about dependencies.
|
||||
defp deps do
|
||||
[
|
||||
{:ash, ash_version("~> 1.26.11")},
|
||||
{:ash, ash_version("~> 1.28")},
|
||||
{:absinthe_plug, "~> 1.4"},
|
||||
{:absinthe, "~> 1.5.3"},
|
||||
{:dataloader, "~> 1.0"},
|
||||
|
|
6
mix.lock
6
mix.lock
|
@ -1,13 +1,13 @@
|
|||
%{
|
||||
"absinthe": {:hex, :absinthe, "1.5.3", "d255e6d825e63abd9ff22b6d2423540526c9d699f46b712aa76f4b9c06116ff9", [: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", "69a170f3a8630b2ca489367bc2aeeabd84e15cbd1e86fe8741b05885fda32a2e"},
|
||||
"absinthe": {:hex, :absinthe, "1.5.5", "22b26228f56dc6a1074c52cea9c64e869a0cb2427403bcf9056c422d36c66292", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "41e79ed4bffbab4986493ff4120c948d59871fd08ad5e31195129ce3c01aad58"},
|
||||
"absinthe_plug": {:hex, :absinthe_plug, "1.5.0", "018ef544cf577339018d1f482404b4bed762e1b530c78be9de4bbb88a6f3a805", [:mix], [{:absinthe, "~> 1.5.0", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.2 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "4c160f4ce9a1233a4219a42de946e4e05d0e8733537cd5d8d20e7d4ef8d4b7c7"},
|
||||
"ash": {:hex, :ash, "1.26.11", "c9cd5d01eca22a49e4d0fff0a638e1a86294c231db0b9183c149a9b8c9fd5cbb", [:mix], [{:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.3.5", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.1.5", [hex: :picosat_elixir, repo: "hexpm", optional: false]}], "hexpm", "b289c064bb83d4feff114d2229c70cab04b5e8f4c3bb0c7fb2d6cc2243abf4c3"},
|
||||
"ash": {:hex, :ash, "1.28.0", "4c1581b8f2c0325d39ed68be9c2d9e2ad8a126281884e75860265c250a648a5d", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.3.5", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.1.5", [hex: :picosat_elixir, repo: "hexpm", optional: false]}], "hexpm", "a33b14730cd0e1b17d2e868dccee7e624fa5ed1c1e34a6e35c7ebb77e669d14d"},
|
||||
"ashton": {:hex, :ashton, "0.4.1", "d0f7782ac44fa22da7ce544028ee3d2078592a834d8adf3e5b4b6aeb94413a55", [:mix], [], "hexpm", "24db667932517fdbc3f2dae777f28b8d87629271387d4490bc4ae8d9c46ff3d3"},
|
||||
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
|
||||
"certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"},
|
||||
"credo": {:hex, :credo, "1.4.0", "92339d4cbadd1e88b5ee43d427b639b68a11071b6f73854e33638e30a0ea11f5", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1fd3b70dce216574ce3c18bdf510b57e7c4c85c2ec9cad4bff854abaf7e58658"},
|
||||
"dataloader": {:hex, :dataloader, "1.0.8", "114294362db98a613f231589246aa5b0ce847412e8e75c4c94f31f204d272cbf", [:mix], [{:ecto, ">= 3.4.3 and < 4.0.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "eaf3c2aa2bc9dbd2f1e960561d616b7f593396c4754185b75904f6d66c82a667"},
|
||||
"decimal": {:hex, :decimal, "1.9.0", "83e8daf59631d632b171faabafb4a9f4242c514b0a06ba3df493951c08f64d07", [:mix], [], "hexpm", "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"},
|
||||
"decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
|
||||
"dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"},
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"},
|
||||
"ecto": {:hex, :ecto, "3.5.5", "48219a991bb86daba6e38a1e64f8cea540cded58950ff38fbc8163e062281a07", [: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", "98dd0e5e1de7f45beca6130d13116eae675db59adfa055fb79612406acf6f6f1"},
|
||||
|
|
Loading…
Reference in a new issue