mirror of
https://github.com/ash-project/ash_graphql.git
synced 2024-09-20 05:13:33 +12:00
improvement: add options for remapping field/argument names
fix: use the dataloader for loading calculations, to allow for aliases
This commit is contained in:
parent
9095a5ae45
commit
92631f91b6
11 changed files with 359 additions and 34 deletions
|
@ -1,5 +1,6 @@
|
||||||
spark_locals_without_parens = [
|
spark_locals_without_parens = [
|
||||||
allow_nil?: 1,
|
allow_nil?: 1,
|
||||||
|
argument_names: 1,
|
||||||
as_mutation?: 1,
|
as_mutation?: 1,
|
||||||
attribute_input_types: 1,
|
attribute_input_types: 1,
|
||||||
attribute_types: 1,
|
attribute_types: 1,
|
||||||
|
@ -10,6 +11,7 @@ spark_locals_without_parens = [
|
||||||
depth_limit: 1,
|
depth_limit: 1,
|
||||||
destroy: 2,
|
destroy: 2,
|
||||||
destroy: 3,
|
destroy: 3,
|
||||||
|
field_names: 1,
|
||||||
generate_object?: 1,
|
generate_object?: 1,
|
||||||
get: 2,
|
get: 2,
|
||||||
get: 3,
|
get: 3,
|
||||||
|
|
|
@ -122,7 +122,8 @@ defmodule AshGraphql.Dataloader do
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_keys({assoc_field, opts}, %resource{} = record) when is_atom(assoc_field) do
|
defp get_keys({assoc_field, %{type: :relationship} = opts}, %resource{} = record)
|
||||||
|
when is_atom(assoc_field) do
|
||||||
validate_resource(resource)
|
validate_resource(resource)
|
||||||
pkey = Ash.Resource.Info.primary_key(resource)
|
pkey = Ash.Resource.Info.primary_key(resource)
|
||||||
id = Enum.map(pkey, &Map.get(record, &1))
|
id = Enum.map(pkey, &Map.get(record, &1))
|
||||||
|
@ -132,11 +133,18 @@ defmodule AshGraphql.Dataloader do
|
||||||
{{:assoc, resource, self(), assoc_field, queryable, opts}, id, record}
|
{{:assoc, resource, self(), assoc_field, queryable, opts}, id, record}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp get_keys({calc, %{type: :calculation} = opts}, %resource{} = record) do
|
||||||
|
validate_resource(resource)
|
||||||
|
pkey = Ash.Resource.Info.primary_key(resource)
|
||||||
|
id = Enum.map(pkey, &Map.get(record, &1))
|
||||||
|
|
||||||
|
{{:calc, resource, self(), calc, opts}, id, record}
|
||||||
|
end
|
||||||
|
|
||||||
defp get_keys(key, item) do
|
defp get_keys(key, item) do
|
||||||
raise """
|
raise """
|
||||||
Invalid: #{inspect(key)}
|
Invalid batch key: #{inspect(key)}
|
||||||
#{inspect(item)}
|
#{inspect(item)}
|
||||||
The batch key must either be a schema module, or an association name.
|
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -232,6 +240,20 @@ defmodule AshGraphql.Dataloader do
|
||||||
{key, Map.new(Enum.zip(ids, results))}
|
{key, Map.new(Enum.zip(ids, results))}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp run_batch(
|
||||||
|
{{:calc, _, _pid, calc, %{args: args, api_opts: api_opts}} = key, records},
|
||||||
|
source
|
||||||
|
) do
|
||||||
|
{ids, records} = Enum.unzip(records)
|
||||||
|
|
||||||
|
results =
|
||||||
|
records
|
||||||
|
|> source.api.load!([{calc, args}], api_opts)
|
||||||
|
|> Enum.map(&Map.get(&1, calc))
|
||||||
|
|
||||||
|
{key, Map.new(Enum.zip(ids, results))}
|
||||||
|
end
|
||||||
|
|
||||||
defp tenant_from_records([%{__metadata__: %{tenant: tenant}}]) when not is_nil(tenant) do
|
defp tenant_from_records([%{__metadata__: %{tenant: tenant}}]) when not is_nil(tenant) do
|
||||||
tenant
|
tenant
|
||||||
end
|
end
|
||||||
|
|
|
@ -774,8 +774,7 @@ defmodule AshGraphql.Graphql.Resolver do
|
||||||
|> fields(["result"])
|
|> fields(["result"])
|
||||||
|> names_only()
|
|> names_only()
|
||||||
|> Enum.map(fn identifier ->
|
|> Enum.map(fn identifier ->
|
||||||
Ash.Resource.Info.aggregate(resource, identifier) ||
|
Ash.Resource.Info.aggregate(resource, identifier)
|
||||||
Ash.Resource.Info.calculation(resource, identifier)
|
|
||||||
end)
|
end)
|
||||||
|> Enum.filter(& &1)
|
|> Enum.filter(& &1)
|
||||||
|> Enum.map(& &1.name)
|
|> Enum.map(& &1.name)
|
||||||
|
@ -793,12 +792,6 @@ defmodule AshGraphql.Graphql.Resolver do
|
||||||
|
|
||||||
if aggregate do
|
if aggregate do
|
||||||
aggregate.name
|
aggregate.name
|
||||||
else
|
|
||||||
calculation = Ash.Resource.Info.calculation(resource, selection.schema_node.identifier)
|
|
||||||
|
|
||||||
if calculation do
|
|
||||||
{calculation.name, selection.argument_data || %{}}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|> Enum.filter(& &1)
|
|> Enum.filter(& &1)
|
||||||
|
@ -1035,6 +1028,29 @@ defmodule AshGraphql.Graphql.Resolver do
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def resolve_calculation(
|
||||||
|
%{source: parent, arguments: args, context: %{loader: loader} = context} = resolution,
|
||||||
|
{api, _resource, calculation}
|
||||||
|
) do
|
||||||
|
api_opts = [
|
||||||
|
actor: Map.get(context, :actor),
|
||||||
|
authorize?: AshGraphql.Api.Info.authorize?(api),
|
||||||
|
verbose?: AshGraphql.Api.Info.debug?(api),
|
||||||
|
stacktraces?: AshGraphql.Api.Info.debug?(api) || AshGraphql.Api.Info.stacktraces?(api)
|
||||||
|
]
|
||||||
|
|
||||||
|
opts = [
|
||||||
|
api_opts: api_opts,
|
||||||
|
type: :calculation,
|
||||||
|
args: args,
|
||||||
|
tenant: Map.get(context, :tenant)
|
||||||
|
]
|
||||||
|
|
||||||
|
batch_key = {calculation.name, opts}
|
||||||
|
|
||||||
|
do_dataloader(resolution, loader, api, batch_key, opts, parent)
|
||||||
|
end
|
||||||
|
|
||||||
def resolve_assoc(
|
def resolve_assoc(
|
||||||
%{source: parent, arguments: args, context: %{loader: loader} = context} = resolution,
|
%{source: parent, arguments: args, context: %{loader: loader} = context} = resolution,
|
||||||
{api, relationship}
|
{api, relationship}
|
||||||
|
@ -1057,11 +1073,13 @@ defmodule AshGraphql.Graphql.Resolver do
|
||||||
opts = [
|
opts = [
|
||||||
query: related_query,
|
query: related_query,
|
||||||
api_opts: api_opts,
|
api_opts: api_opts,
|
||||||
|
type: :relationship,
|
||||||
args: args,
|
args: args,
|
||||||
|
resource: relationship.source,
|
||||||
tenant: Map.get(context, :tenant)
|
tenant: Map.get(context, :tenant)
|
||||||
]
|
]
|
||||||
|
|
||||||
{batch_key, parent} = {{relationship.name, opts}, parent}
|
batch_key = {relationship.name, opts}
|
||||||
do_dataloader(resolution, loader, api, batch_key, args, parent)
|
do_dataloader(resolution, loader, api, batch_key, args, parent)
|
||||||
|
|
||||||
{:error, error} ->
|
{:error, error} ->
|
||||||
|
|
|
@ -28,6 +28,16 @@ defmodule AshGraphql.Resource.Info do
|
||||||
Extension.get_opt(resource, [:graphql], :attribute_types, [])
|
Extension.get_opt(resource, [:graphql], :attribute_types, [])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc "Graphql field name (attribute/relationship/calculation/arguments) overrides for the resource"
|
||||||
|
def field_names(resource) do
|
||||||
|
Extension.get_opt(resource, [:graphql], :field_names, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "Graphql argument name overrides for the resource"
|
||||||
|
def argument_names(resource) do
|
||||||
|
Extension.get_opt(resource, [:graphql], :argument_names, [])
|
||||||
|
end
|
||||||
|
|
||||||
@doc "Graphql type overrides for the resource"
|
@doc "Graphql type overrides for the resource"
|
||||||
def attribute_input_types(resource) do
|
def attribute_input_types(resource) do
|
||||||
Extension.get_opt(resource, [:graphql], :attribute_input_types, [])
|
Extension.get_opt(resource, [:graphql], :attribute_input_types, [])
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
defmodule AshGraphql.Resource.Mutation do
|
defmodule AshGraphql.Resource.Mutation do
|
||||||
@moduledoc "Represents a configured mutation on a resource"
|
@moduledoc "Represents a configured mutation on a resource"
|
||||||
defstruct [:name, :action, :type, :identity, :read_action, :upsert?, :modify_resolution]
|
defstruct [
|
||||||
|
:name,
|
||||||
|
:action,
|
||||||
|
:type,
|
||||||
|
:identity,
|
||||||
|
:read_action,
|
||||||
|
:upsert?,
|
||||||
|
:modify_resolution
|
||||||
|
]
|
||||||
|
|
||||||
@create_schema [
|
@create_schema [
|
||||||
name: [
|
name: [
|
||||||
|
|
|
@ -222,6 +222,15 @@ defmodule AshGraphql.Resource do
|
||||||
required: true,
|
required: true,
|
||||||
doc: "The type to use for this entity in the graphql schema"
|
doc: "The type to use for this entity in the graphql schema"
|
||||||
],
|
],
|
||||||
|
field_names: [
|
||||||
|
type: :keyword_list,
|
||||||
|
doc: "A keyword list of name overrides for attributes."
|
||||||
|
],
|
||||||
|
argument_names: [
|
||||||
|
type: :keyword_list,
|
||||||
|
doc:
|
||||||
|
"A nested keyword list of action names, to argument name remappings. i.e `create: [arg_name: :new_name]`"
|
||||||
|
],
|
||||||
attribute_types: [
|
attribute_types: [
|
||||||
type: :keyword_list,
|
type: :keyword_list,
|
||||||
doc:
|
doc:
|
||||||
|
@ -259,7 +268,8 @@ defmodule AshGraphql.Resource do
|
||||||
|
|
||||||
@transformers [
|
@transformers [
|
||||||
AshGraphql.Resource.Transformers.RequireIdPkey,
|
AshGraphql.Resource.Transformers.RequireIdPkey,
|
||||||
AshGraphql.Resource.Transformers.ValidateActions
|
AshGraphql.Resource.Transformers.ValidateActions,
|
||||||
|
AshGraphql.Resource.Transformers.ValidateCompatibleNames
|
||||||
]
|
]
|
||||||
|
|
||||||
@sections [@graphql]
|
@sections [@graphql]
|
||||||
|
@ -705,6 +715,9 @@ defmodule AshGraphql.Resource do
|
||||||
&(&1.action == action.name)
|
&(&1.action == action.name)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
field_names = AshGraphql.Resource.Info.field_names(resource)
|
||||||
|
argument_names = AshGraphql.Resource.Info.argument_names(resource)
|
||||||
|
|
||||||
attribute_fields =
|
attribute_fields =
|
||||||
if action.type == :destroy && !action.soft? do
|
if action.type == :destroy && !action.soft? do
|
||||||
[]
|
[]
|
||||||
|
@ -727,11 +740,13 @@ defmodule AshGraphql.Resource do
|
||||||
|> field_type(attribute, resource, true)
|
|> field_type(attribute, resource, true)
|
||||||
|> maybe_wrap_non_null(explicitly_required || not allow_nil?)
|
|> maybe_wrap_non_null(explicitly_required || not allow_nil?)
|
||||||
|
|
||||||
|
name = field_names[attribute.name] || attribute.name
|
||||||
|
|
||||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||||
description: attribute.description,
|
description: attribute.description,
|
||||||
identifier: attribute.name,
|
identifier: attribute.name,
|
||||||
module: schema,
|
module: schema,
|
||||||
name: to_string(attribute.name),
|
name: to_string(name),
|
||||||
type: field_type,
|
type: field_type,
|
||||||
__reference__: ref(__ENV__)
|
__reference__: ref(__ENV__)
|
||||||
}
|
}
|
||||||
|
@ -742,6 +757,8 @@ defmodule AshGraphql.Resource do
|
||||||
action.arguments
|
action.arguments
|
||||||
|> Enum.reject(& &1.private?)
|
|> Enum.reject(& &1.private?)
|
||||||
|> Enum.map(fn argument ->
|
|> Enum.map(fn argument ->
|
||||||
|
name = argument_names[action.name][argument.name] || argument.name
|
||||||
|
|
||||||
case find_manage_change(argument, action, managed_relationships) do
|
case find_manage_change(argument, action, managed_relationships) do
|
||||||
nil ->
|
nil ->
|
||||||
type =
|
type =
|
||||||
|
@ -752,7 +769,7 @@ defmodule AshGraphql.Resource do
|
||||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||||
identifier: argument.name,
|
identifier: argument.name,
|
||||||
module: schema,
|
module: schema,
|
||||||
name: to_string(argument.name),
|
name: to_string(name),
|
||||||
type: type,
|
type: type,
|
||||||
__reference__: ref(__ENV__)
|
__reference__: ref(__ENV__)
|
||||||
}
|
}
|
||||||
|
@ -772,7 +789,7 @@ defmodule AshGraphql.Resource do
|
||||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||||
identifier: argument.name,
|
identifier: argument.name,
|
||||||
module: schema,
|
module: schema,
|
||||||
name: to_string(argument.name),
|
name: to_string(name),
|
||||||
type: maybe_wrap_non_null(type, argument_required?(argument)),
|
type: maybe_wrap_non_null(type, argument_required?(argument)),
|
||||||
__reference__: ref(__ENV__)
|
__reference__: ref(__ENV__)
|
||||||
}
|
}
|
||||||
|
@ -839,6 +856,12 @@ defmodule AshGraphql.Resource do
|
||||||
maybe_wrap_non_null(type, not query.allow_nil?)
|
maybe_wrap_non_null(type, not query.allow_nil?)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp maybe_wrap_non_null({:non_null, type}, true) do
|
||||||
|
%Absinthe.Blueprint.TypeReference.NonNull{
|
||||||
|
of_type: type
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
defp maybe_wrap_non_null(type, true) do
|
defp maybe_wrap_non_null(type, true) do
|
||||||
%Absinthe.Blueprint.TypeReference.NonNull{
|
%Absinthe.Blueprint.TypeReference.NonNull{
|
||||||
of_type: type
|
of_type: type
|
||||||
|
@ -1007,17 +1030,21 @@ defmodule AshGraphql.Resource do
|
||||||
|
|
||||||
# sobelow_skip ["DOS.StringToAtom"]
|
# sobelow_skip ["DOS.StringToAtom"]
|
||||||
defp attribute_filter_field_type(resource, attribute) do
|
defp attribute_filter_field_type(resource, attribute) do
|
||||||
|
field_names = AshGraphql.Resource.Info.field_names(resource)
|
||||||
|
|
||||||
String.to_atom(
|
String.to_atom(
|
||||||
to_string(AshGraphql.Resource.Info.type(resource)) <>
|
to_string(AshGraphql.Resource.Info.type(resource)) <>
|
||||||
"_filter_" <> to_string(attribute.name)
|
"_filter_" <> to_string(field_names[attribute.name] || attribute.name)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
# sobelow_skip ["DOS.StringToAtom"]
|
# sobelow_skip ["DOS.StringToAtom"]
|
||||||
defp calculation_filter_field_type(resource, calculation) do
|
defp calculation_filter_field_type(resource, calculation) do
|
||||||
|
field_names = AshGraphql.Resource.Info.field_names(resource)
|
||||||
|
|
||||||
String.to_atom(
|
String.to_atom(
|
||||||
to_string(AshGraphql.Resource.Info.type(resource)) <>
|
to_string(AshGraphql.Resource.Info.type(resource)) <>
|
||||||
"_filter_" <> to_string(calculation.name)
|
"_filter_" <> to_string(field_names[calculation.name] || calculation.name)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1856,6 +1883,8 @@ defmodule AshGraphql.Resource do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp attribute_filter_fields(resource, schema) do
|
defp attribute_filter_fields(resource, schema) do
|
||||||
|
field_names = AshGraphql.Resource.Info.field_names(resource)
|
||||||
|
|
||||||
resource
|
resource
|
||||||
|> Ash.Resource.Info.public_attributes()
|
|> Ash.Resource.Info.public_attributes()
|
||||||
|> Enum.reject(fn
|
|> Enum.reject(fn
|
||||||
|
@ -1870,7 +1899,7 @@ defmodule AshGraphql.Resource do
|
||||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||||
identifier: attribute.name,
|
identifier: attribute.name,
|
||||||
module: schema,
|
module: schema,
|
||||||
name: to_string(attribute.name),
|
name: to_string(field_names[attribute.name] || attribute.name),
|
||||||
type: attribute_filter_field_type(resource, attribute),
|
type: attribute_filter_field_type(resource, attribute),
|
||||||
__reference__: ref(__ENV__)
|
__reference__: ref(__ENV__)
|
||||||
}
|
}
|
||||||
|
@ -1879,6 +1908,8 @@ defmodule AshGraphql.Resource do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp aggregate_filter_fields(resource, schema) do
|
defp aggregate_filter_fields(resource, schema) do
|
||||||
|
field_names = AshGraphql.Resource.Info.field_names(resource)
|
||||||
|
|
||||||
if Ash.DataLayer.data_layer_can?(resource, :aggregate_filter) do
|
if Ash.DataLayer.data_layer_can?(resource, :aggregate_filter) do
|
||||||
resource
|
resource
|
||||||
|> Ash.Resource.Info.public_aggregates()
|
|> Ash.Resource.Info.public_aggregates()
|
||||||
|
@ -1887,7 +1918,7 @@ defmodule AshGraphql.Resource do
|
||||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||||
identifier: aggregate.name,
|
identifier: aggregate.name,
|
||||||
module: schema,
|
module: schema,
|
||||||
name: to_string(aggregate.name),
|
name: to_string(field_names[aggregate.name] || aggregate.name),
|
||||||
type: attribute_filter_field_type(resource, aggregate),
|
type: attribute_filter_field_type(resource, aggregate),
|
||||||
__reference__: ref(__ENV__)
|
__reference__: ref(__ENV__)
|
||||||
}
|
}
|
||||||
|
@ -1899,6 +1930,8 @@ defmodule AshGraphql.Resource do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp calculation_filter_fields(resource, schema) do
|
defp calculation_filter_fields(resource, schema) do
|
||||||
|
field_names = AshGraphql.Resource.Info.field_names(resource)
|
||||||
|
|
||||||
if Ash.DataLayer.data_layer_can?(resource, :expression_calculation) do
|
if Ash.DataLayer.data_layer_can?(resource, :expression_calculation) do
|
||||||
resource
|
resource
|
||||||
|> Ash.Resource.Info.public_calculations()
|
|> Ash.Resource.Info.public_calculations()
|
||||||
|
@ -1909,7 +1942,7 @@ defmodule AshGraphql.Resource do
|
||||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||||
identifier: calculation.name,
|
identifier: calculation.name,
|
||||||
module: schema,
|
module: schema,
|
||||||
name: to_string(calculation.name),
|
name: to_string(field_names[calculation.name] || calculation.name),
|
||||||
type: calculation_filter_field_type(resource, calculation),
|
type: calculation_filter_field_type(resource, calculation),
|
||||||
__reference__: ref(__ENV__)
|
__reference__: ref(__ENV__)
|
||||||
}
|
}
|
||||||
|
@ -1920,6 +1953,8 @@ defmodule AshGraphql.Resource do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp relationship_filter_fields(resource, schema) do
|
defp relationship_filter_fields(resource, schema) do
|
||||||
|
field_names = AshGraphql.Resource.Info.field_names(resource)
|
||||||
|
|
||||||
resource
|
resource
|
||||||
|> Ash.Resource.Info.public_relationships()
|
|> Ash.Resource.Info.public_relationships()
|
||||||
|> Enum.filter(fn relationship ->
|
|> Enum.filter(fn relationship ->
|
||||||
|
@ -1929,7 +1964,7 @@ defmodule AshGraphql.Resource do
|
||||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||||
identifier: relationship.name,
|
identifier: relationship.name,
|
||||||
module: schema,
|
module: schema,
|
||||||
name: to_string(relationship.name),
|
name: to_string(field_names[relationship.name] || relationship.name),
|
||||||
type: resource_filter_type(relationship.destination),
|
type: resource_filter_type(relationship.destination),
|
||||||
__reference__: ref(__ENV__)
|
__reference__: ref(__ENV__)
|
||||||
}
|
}
|
||||||
|
@ -2045,6 +2080,8 @@ defmodule AshGraphql.Resource do
|
||||||
defp unnest(other), do: other
|
defp unnest(other), do: other
|
||||||
|
|
||||||
defp sort_values(resource) do
|
defp sort_values(resource) do
|
||||||
|
field_names = AshGraphql.Resource.Info.field_names(resource)
|
||||||
|
|
||||||
attribute_sort_values =
|
attribute_sort_values =
|
||||||
resource
|
resource
|
||||||
|> Ash.Resource.Info.public_attributes()
|
|> Ash.Resource.Info.public_attributes()
|
||||||
|
@ -2082,7 +2119,12 @@ defmodule AshGraphql.Resource do
|
||||||
end)
|
end)
|
||||||
|> Enum.map(& &1.name)
|
|> Enum.map(& &1.name)
|
||||||
|
|
||||||
attribute_sort_values ++ aggregate_sort_values
|
attribute_sort_values
|
||||||
|
|> Enum.concat(aggregate_sort_values)
|
||||||
|
|> Enum.map(fn name ->
|
||||||
|
field_names[name] || name
|
||||||
|
end)
|
||||||
|
|> Enum.uniq()
|
||||||
end
|
end
|
||||||
|
|
||||||
# sobelow_skip ["DOS.StringToAtom"]
|
# sobelow_skip ["DOS.StringToAtom"]
|
||||||
|
@ -2241,10 +2283,12 @@ defmodule AshGraphql.Resource do
|
||||||
attributes(resource, schema) ++
|
attributes(resource, schema) ++
|
||||||
relationships(resource, api, schema) ++
|
relationships(resource, api, schema) ++
|
||||||
aggregates(resource, schema) ++
|
aggregates(resource, schema) ++
|
||||||
calculations(resource, schema)
|
calculations(resource, api, schema)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp attributes(resource, schema) do
|
defp attributes(resource, schema) do
|
||||||
|
attribute_names = AshGraphql.Resource.Info.field_names(resource)
|
||||||
|
|
||||||
non_id_attributes =
|
non_id_attributes =
|
||||||
resource
|
resource
|
||||||
|> Ash.Resource.Info.public_attributes()
|
|> Ash.Resource.Info.public_attributes()
|
||||||
|
@ -2255,11 +2299,13 @@ defmodule AshGraphql.Resource do
|
||||||
|> field_type(attribute, resource)
|
|> field_type(attribute, resource)
|
||||||
|> maybe_wrap_non_null(not attribute.allow_nil?)
|
|> maybe_wrap_non_null(not attribute.allow_nil?)
|
||||||
|
|
||||||
|
name = attribute_names[attribute.name] || attribute.name
|
||||||
|
|
||||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||||
description: attribute.description,
|
description: attribute.description,
|
||||||
identifier: attribute.name,
|
identifier: attribute.name,
|
||||||
module: schema,
|
module: schema,
|
||||||
name: to_string(attribute.name),
|
name: to_string(name),
|
||||||
type: field_type,
|
type: field_type,
|
||||||
__reference__: ref(__ENV__)
|
__reference__: ref(__ENV__)
|
||||||
}
|
}
|
||||||
|
@ -2376,6 +2422,8 @@ defmodule AshGraphql.Resource do
|
||||||
|
|
||||||
# sobelow_skip ["DOS.StringToAtom"]
|
# sobelow_skip ["DOS.StringToAtom"]
|
||||||
defp relationships(resource, api, schema) do
|
defp relationships(resource, api, schema) do
|
||||||
|
field_names = AshGraphql.Resource.Info.field_names(resource)
|
||||||
|
|
||||||
resource
|
resource
|
||||||
|> Ash.Resource.Info.public_relationships()
|
|> Ash.Resource.Info.public_relationships()
|
||||||
|> Enum.filter(fn relationship ->
|
|> Enum.filter(fn relationship ->
|
||||||
|
@ -2383,6 +2431,8 @@ defmodule AshGraphql.Resource do
|
||||||
end)
|
end)
|
||||||
|> Enum.map(fn
|
|> Enum.map(fn
|
||||||
%{cardinality: :one} = relationship ->
|
%{cardinality: :one} = relationship ->
|
||||||
|
name = field_names[relationship.name] || relationship.name
|
||||||
|
|
||||||
type =
|
type =
|
||||||
relationship.destination
|
relationship.destination
|
||||||
|> AshGraphql.Resource.Info.type()
|
|> AshGraphql.Resource.Info.type()
|
||||||
|
@ -2391,7 +2441,7 @@ defmodule AshGraphql.Resource do
|
||||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||||
identifier: relationship.name,
|
identifier: relationship.name,
|
||||||
module: schema,
|
module: schema,
|
||||||
name: to_string(relationship.name),
|
name: to_string(name),
|
||||||
middleware: [
|
middleware: [
|
||||||
{{AshGraphql.Graphql.Resolver, :resolve_assoc}, {api, relationship}}
|
{{AshGraphql.Graphql.Resolver, :resolve_assoc}, {api, relationship}}
|
||||||
],
|
],
|
||||||
|
@ -2401,6 +2451,7 @@ defmodule AshGraphql.Resource do
|
||||||
}
|
}
|
||||||
|
|
||||||
%{cardinality: :many} = relationship ->
|
%{cardinality: :many} = relationship ->
|
||||||
|
name = field_names[relationship.name] || relationship.name
|
||||||
read_action = Ash.Resource.Info.primary_action!(relationship.destination, :read)
|
read_action = Ash.Resource.Info.primary_action!(relationship.destination, :read)
|
||||||
|
|
||||||
type = AshGraphql.Resource.Info.type(relationship.destination)
|
type = AshGraphql.Resource.Info.type(relationship.destination)
|
||||||
|
@ -2416,7 +2467,7 @@ defmodule AshGraphql.Resource do
|
||||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||||
identifier: relationship.name,
|
identifier: relationship.name,
|
||||||
module: schema,
|
module: schema,
|
||||||
name: to_string(relationship.name),
|
name: to_string(name),
|
||||||
complexity: {AshGraphql.Graphql.Resolver, :query_complexity},
|
complexity: {AshGraphql.Graphql.Resolver, :query_complexity},
|
||||||
middleware: [
|
middleware: [
|
||||||
{{AshGraphql.Graphql.Resolver, :resolve_assoc}, {api, relationship}}
|
{{AshGraphql.Graphql.Resolver, :resolve_assoc}, {api, relationship}}
|
||||||
|
@ -2429,9 +2480,13 @@ defmodule AshGraphql.Resource do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp aggregates(resource, schema) do
|
defp aggregates(resource, schema) do
|
||||||
|
field_names = AshGraphql.Resource.Info.field_names(resource)
|
||||||
|
|
||||||
resource
|
resource
|
||||||
|> Ash.Resource.Info.public_aggregates()
|
|> Ash.Resource.Info.public_aggregates()
|
||||||
|> Enum.map(fn aggregate ->
|
|> Enum.map(fn aggregate ->
|
||||||
|
name = field_names[aggregate.name] || aggregate.name
|
||||||
|
|
||||||
field_type =
|
field_type =
|
||||||
with field when not is_nil(field) <- aggregate.field,
|
with field when not is_nil(field) <- aggregate.field,
|
||||||
related when not is_nil(related) <-
|
related when not is_nil(related) <-
|
||||||
|
@ -2454,17 +2509,20 @@ defmodule AshGraphql.Resource do
|
||||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||||
identifier: aggregate.name,
|
identifier: aggregate.name,
|
||||||
module: schema,
|
module: schema,
|
||||||
name: to_string(aggregate.name),
|
name: to_string(name),
|
||||||
type: type,
|
type: type,
|
||||||
__reference__: ref(__ENV__)
|
__reference__: ref(__ENV__)
|
||||||
}
|
}
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp calculations(resource, schema) do
|
defp calculations(resource, api, schema) do
|
||||||
|
field_names = AshGraphql.Resource.Info.field_names(resource)
|
||||||
|
|
||||||
resource
|
resource
|
||||||
|> Ash.Resource.Info.public_calculations()
|
|> Ash.Resource.Info.public_calculations()
|
||||||
|> Enum.map(fn calculation ->
|
|> Enum.map(fn calculation ->
|
||||||
|
name = field_names[calculation.name] || calculation.name
|
||||||
field_type = calculation_type(calculation, resource)
|
field_type = calculation_type(calculation, resource)
|
||||||
|
|
||||||
arguments = calculation_args(calculation, resource, schema)
|
arguments = calculation_args(calculation, resource, schema)
|
||||||
|
@ -2474,7 +2532,10 @@ defmodule AshGraphql.Resource do
|
||||||
module: schema,
|
module: schema,
|
||||||
arguments: arguments,
|
arguments: arguments,
|
||||||
complexity: 2,
|
complexity: 2,
|
||||||
name: to_string(calculation.name),
|
middleware: [
|
||||||
|
{{AshGraphql.Graphql.Resolver, :resolve_calculation}, {api, resource, calculation}}
|
||||||
|
],
|
||||||
|
name: to_string(name),
|
||||||
type: field_type,
|
type: field_type,
|
||||||
__reference__: ref(__ENV__)
|
__reference__: ref(__ENV__)
|
||||||
}
|
}
|
||||||
|
@ -2683,11 +2744,13 @@ defmodule AshGraphql.Resource do
|
||||||
|
|
||||||
# sobelow_skip ["DOS.StringToAtom"]
|
# sobelow_skip ["DOS.StringToAtom"]
|
||||||
defp atom_enum_type(resource, attribute_name) do
|
defp atom_enum_type(resource, attribute_name) do
|
||||||
|
field_names = AshGraphql.Resource.Info.field_names(resource)
|
||||||
|
|
||||||
resource
|
resource
|
||||||
|> AshGraphql.Resource.Info.type()
|
|> AshGraphql.Resource.Info.type()
|
||||||
|> to_string()
|
|> to_string()
|
||||||
|> Kernel.<>("_")
|
|> Kernel.<>("_")
|
||||||
|> Kernel.<>(to_string(attribute_name))
|
|> Kernel.<>(to_string(field_names[attribute_name] || attribute_name))
|
||||||
|> String.to_atom()
|
|> String.to_atom()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -31,7 +31,7 @@ defmodule AshGraphql.Resource.Transformers.ValidateActions do
|
||||||
resource = Transformer.get_persisted(dsl, :module)
|
resource = Transformer.get_persisted(dsl, :module)
|
||||||
|
|
||||||
raise Spark.Error.DslError,
|
raise Spark.Error.DslError,
|
||||||
module: __MODULE__,
|
module: resource,
|
||||||
message: """
|
message: """
|
||||||
No such action #{query_or_mutation.action} of type #{type} on #{inspect(resource)}
|
No such action #{query_or_mutation.action} of type #{type} on #{inspect(resource)}
|
||||||
|
|
||||||
|
|
119
lib/resource/transformers/validate_compatible_names.ex
Normal file
119
lib/resource/transformers/validate_compatible_names.ex
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
defmodule AshGraphql.Resource.Transformers.ValidateCompatibleNames do
|
||||||
|
@moduledoc "Ensures that all field names are valid or remapped to something valid exist"
|
||||||
|
use Spark.Dsl.Transformer
|
||||||
|
|
||||||
|
alias Spark.Dsl.Transformer
|
||||||
|
|
||||||
|
def after_compile?, do: true
|
||||||
|
|
||||||
|
def transform(dsl) do
|
||||||
|
field_names = AshGraphql.Resource.Info.field_names(dsl)
|
||||||
|
argument_names = AshGraphql.Resource.Info.argument_names(dsl)
|
||||||
|
resource = Transformer.get_persisted(dsl, :module)
|
||||||
|
|
||||||
|
dsl
|
||||||
|
|> Ash.Resource.Info.public_attributes()
|
||||||
|
|> Enum.concat(Ash.Resource.Info.public_aggregates(dsl))
|
||||||
|
|> Enum.concat(Ash.Resource.Info.public_calculations(dsl))
|
||||||
|
|> Enum.concat(Ash.Resource.Info.public_relationships(dsl))
|
||||||
|
|> Enum.each(fn field ->
|
||||||
|
name = field_names[field.name] || field.name
|
||||||
|
|
||||||
|
if invalid_name?(name) do
|
||||||
|
raise_invalid_name_error(resource, field, name)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
dsl
|
||||||
|
|> Transformer.get_entities([:graphql, :queries])
|
||||||
|
|> Enum.concat(Transformer.get_entities(dsl, [:graphql, :mutations]))
|
||||||
|
|> Enum.map(& &1.action)
|
||||||
|
|> Enum.uniq()
|
||||||
|
|> Enum.each(fn action ->
|
||||||
|
action = Ash.Resource.Info.action(dsl, action)
|
||||||
|
|
||||||
|
Enum.each(action.arguments, fn argument ->
|
||||||
|
name = argument_names[action.name][argument.name] || argument.name
|
||||||
|
|
||||||
|
if invalid_name?(name) do
|
||||||
|
raise_invalid_argument_name_error(resource, action, argument.name, name)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
{:ok, dsl}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp invalid_name?(name) do
|
||||||
|
Regex.match?(~r/_+\d/, to_string(name))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp raise_invalid_name_error(resource, field, name) do
|
||||||
|
path =
|
||||||
|
case field do
|
||||||
|
%Ash.Resource.Relationships.BelongsTo{} -> [:relationships, :belongs_to, field.name]
|
||||||
|
%Ash.Resource.Relationships.HasMany{} -> [:relationships, :has_many, field.name]
|
||||||
|
%Ash.Resource.Relationships.HasOne{} -> [:relationships, :has_one, field.name]
|
||||||
|
%Ash.Resource.Relationships.ManyToMany{} -> [:relationships, :many_to_many, field.name]
|
||||||
|
%Ash.Resource.Calculation{} -> [:calculations, field.name]
|
||||||
|
%Ash.Resource.Aggregate{} -> [:aggregates, field.name]
|
||||||
|
%Ash.Resource.Attribute{} -> [:attributes, field.name]
|
||||||
|
end
|
||||||
|
|
||||||
|
raise Spark.Error.DslError,
|
||||||
|
module: resource,
|
||||||
|
path: path,
|
||||||
|
message: """
|
||||||
|
Name #{name} is invalid.
|
||||||
|
|
||||||
|
Due to issues in the underlying tooling with camel/snake case conversion of names that
|
||||||
|
include underscores immediately preceding integers, a different name must be provided to
|
||||||
|
use in the graphql. To do so, add a mapping in your configured field_names, i.e
|
||||||
|
|
||||||
|
graphql do
|
||||||
|
...
|
||||||
|
|
||||||
|
field_names #{name}: :#{make_name_better(name)}
|
||||||
|
|
||||||
|
...
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
For more information on the underlying issue, see: https://github.com/absinthe-graphql/absinthe/issues/601
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp raise_invalid_argument_name_error(resource, action, argument_name, name) do
|
||||||
|
path = [:actions, action.type, action.name, :argument, argument_name]
|
||||||
|
|
||||||
|
raise Spark.Error.DslError,
|
||||||
|
module: resource,
|
||||||
|
path: path,
|
||||||
|
message: """
|
||||||
|
Name #{name} is invalid.
|
||||||
|
|
||||||
|
Due to issues in the underlying tooling with camel/snake case conversion of names that
|
||||||
|
include underscores immediately preceding integers, a different name must be provided to
|
||||||
|
use in the graphql. To do so, add a mapping in your configured argument_names, i.e
|
||||||
|
|
||||||
|
graphql do
|
||||||
|
...
|
||||||
|
|
||||||
|
argument_names #{action.name}: [#{argument_name}: :#{make_name_better(name)}]
|
||||||
|
|
||||||
|
...
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
For more information on the underlying issue, see: https://github.com/absinthe-graphql/absinthe/issues/601
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp make_name_better(name) do
|
||||||
|
name
|
||||||
|
|> to_string()
|
||||||
|
|> String.replace(~r/_+\d/, fn v ->
|
||||||
|
String.trim_leading(v, "_")
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
3
mix.exs
3
mix.exs
|
@ -115,7 +115,8 @@ defmodule AshGraphql.MixProject do
|
||||||
# Run "mix help deps" to learn about dependencies.
|
# Run "mix help deps" to learn about dependencies.
|
||||||
defp deps do
|
defp deps do
|
||||||
[
|
[
|
||||||
{:ash, ash_version("~> 2.0.0-rc.9")},
|
{:ash,
|
||||||
|
ash_version(github: "ash-project/ash", ref: "fe596db0b86ea58701e00523a0be408487bb8f27")},
|
||||||
{:absinthe_plug, "~> 1.4"},
|
{:absinthe_plug, "~> 1.4"},
|
||||||
{:absinthe, "~> 1.7"},
|
{:absinthe, "~> 1.7"},
|
||||||
{:dataloader, "~> 1.0"},
|
{:dataloader, "~> 1.0"},
|
||||||
|
|
|
@ -6,6 +6,7 @@ defmodule AshGraphql.ReadTest do
|
||||||
setup do
|
setup do
|
||||||
on_exit(fn ->
|
on_exit(fn ->
|
||||||
Ash.DataLayer.Ets.stop(AshGraphql.Test.Post)
|
Ash.DataLayer.Ets.stop(AshGraphql.Test.Post)
|
||||||
|
Ash.DataLayer.Ets.stop(AshGraphql.Test.Comment)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -125,6 +126,56 @@ defmodule AshGraphql.ReadTest do
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "the same relationship can be fetched with different parameters" do
|
||||||
|
post =
|
||||||
|
AshGraphql.Test.Post
|
||||||
|
|> Ash.Changeset.for_create(:create, text: "foo", published: true)
|
||||||
|
|> AshGraphql.Test.Api.create!()
|
||||||
|
|
||||||
|
for _ <- 0..1 do
|
||||||
|
AshGraphql.Test.Comment
|
||||||
|
|> Ash.Changeset.for_create(:create, %{text: "stuff"})
|
||||||
|
|> Ash.Changeset.force_change_attribute(:post_id, post.id)
|
||||||
|
|> AshGraphql.Test.Api.create!()
|
||||||
|
end
|
||||||
|
|
||||||
|
resp =
|
||||||
|
"""
|
||||||
|
query PostLibrary($published: Boolean) {
|
||||||
|
postLibrary(published: $published) {
|
||||||
|
text
|
||||||
|
foo: comments(limit: 1){
|
||||||
|
text
|
||||||
|
}
|
||||||
|
bar: comments(limit: 2){
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|> Absinthe.run(AshGraphql.Test.Schema,
|
||||||
|
variables: %{
|
||||||
|
"published" => true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert {:ok, result} = resp
|
||||||
|
|
||||||
|
refute Map.has_key?(result, :errors)
|
||||||
|
|
||||||
|
assert %{
|
||||||
|
data: %{
|
||||||
|
"postLibrary" => [
|
||||||
|
%{
|
||||||
|
"text" => "foo",
|
||||||
|
"foo" => [%{"text" => "stuff"}],
|
||||||
|
"bar" => [%{"text" => "stuff"}, %{"text" => "stuff"}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} = result
|
||||||
|
end
|
||||||
|
|
||||||
test "complexity is calculated for relationships" do
|
test "complexity is calculated for relationships" do
|
||||||
query = """
|
query = """
|
||||||
query PostLibrary {
|
query PostLibrary {
|
||||||
|
@ -198,6 +249,29 @@ defmodule AshGraphql.ReadTest do
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "the same calculation can be loaded twice with different arguments via aliases" do
|
||||||
|
AshGraphql.Test.Post
|
||||||
|
|> Ash.Changeset.for_create(:create, text: "bar", text1: "1", text2: "2", published: true)
|
||||||
|
|> AshGraphql.Test.Api.create!()
|
||||||
|
|
||||||
|
resp =
|
||||||
|
"""
|
||||||
|
query PostLibrary($published: Boolean) {
|
||||||
|
postLibrary(published: $published) {
|
||||||
|
foo: text1And2(separator: "foo")
|
||||||
|
bar: text1And2(separator: "bar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|> Absinthe.run(AshGraphql.Test.Schema)
|
||||||
|
|
||||||
|
assert {:ok, result} = resp
|
||||||
|
|
||||||
|
refute Map.has_key?(result, :errors)
|
||||||
|
|
||||||
|
assert %{data: %{"postLibrary" => [%{"foo" => "1foo2", "bar" => "1bar2"}]}} = result
|
||||||
|
end
|
||||||
|
|
||||||
test "a read with a non-id primary key fills in the id field" do
|
test "a read with a non-id primary key fills in the id field" do
|
||||||
record =
|
record =
|
||||||
AshGraphql.Test.NonIdPrimaryKey
|
AshGraphql.Test.NonIdPrimaryKey
|
||||||
|
|
|
@ -54,6 +54,7 @@ defmodule AshGraphql.Test.Post do
|
||||||
|
|
||||||
attribute_types integer_as_string_in_api: :string
|
attribute_types integer_as_string_in_api: :string
|
||||||
attribute_input_types integer_as_string_in_api: :string
|
attribute_input_types integer_as_string_in_api: :string
|
||||||
|
field_names(text_1_and_2: :text1_and2)
|
||||||
|
|
||||||
queries do
|
queries do
|
||||||
get :get_post, :read
|
get :get_post, :read
|
||||||
|
@ -208,6 +209,13 @@ defmodule AshGraphql.Test.Post do
|
||||||
calculations do
|
calculations do
|
||||||
calculate(:static_calculation, :string, AshGraphql.Test.StaticCalculation)
|
calculate(:static_calculation, :string, AshGraphql.Test.StaticCalculation)
|
||||||
calculate(:full_text, :string, FullTextCalculation)
|
calculate(:full_text, :string, FullTextCalculation)
|
||||||
|
|
||||||
|
calculate(:text_1_and_2, :string, expr(text1 <> ^arg(:separator) <> text2)) do
|
||||||
|
argument :separator, :string do
|
||||||
|
allow_nil? false
|
||||||
|
default(" ")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
relationships do
|
relationships do
|
||||||
|
|
Loading…
Reference in a new issue