ash_graphql/lib/resource/resource.ex

3479 lines
104 KiB
Elixir
Raw Normal View History

2020-08-14 09:39:59 +12:00
defmodule AshGraphql.Resource do
alias Ash.Changeset.ManagedRelationshipHelpers
2020-08-15 02:20:47 +12:00
alias Ash.Query.Aggregate
alias AshGraphql.Resource
alias AshGraphql.Resource.{ManagedRelationship, Mutation, Query}
2020-08-15 02:20:47 +12:00
2022-08-31 13:08:16 +12:00
@get %Spark.Dsl.Entity{
2020-08-14 09:39:59 +12:00
name: :get,
args: [:name, :action],
describe: "A query to fetch a record by primary key",
examples: [
"get :get_post, :read"
2020-08-14 09:39:59 +12:00
],
2020-08-15 02:20:47 +12:00
schema: Query.get_schema(),
target: Query,
2020-08-14 09:39:59 +12:00
auto_set_fields: [
type: :get
]
}
2022-08-31 13:08:16 +12:00
@read_one %Spark.Dsl.Entity{
2021-04-04 19:10:50 +12:00
name: :read_one,
args: [:name, :action],
describe: "A query to fetch a record",
examples: [
"read_one :current_user, :current_user"
],
schema: Query.read_one_schema(),
target: Query,
auto_set_fields: [
type: :read_one
]
}
2022-08-31 13:08:16 +12:00
@list %Spark.Dsl.Entity{
2020-08-14 09:39:59 +12:00
name: :list,
2020-08-15 02:20:47 +12:00
schema: Query.list_schema(),
2020-08-14 09:39:59 +12:00
args: [:name, :action],
describe: "A query to fetch a list of records",
examples: [
"list :list_posts, :read",
"list :list_posts_paginated, :read, relay?: true"
2020-08-14 09:39:59 +12:00
],
2020-08-15 02:20:47 +12:00
target: Query,
2020-08-14 09:39:59 +12:00
auto_set_fields: [
type: :list
]
}
2022-08-31 13:08:16 +12:00
@create %Spark.Dsl.Entity{
2020-08-14 09:39:59 +12:00
name: :create,
2020-08-15 02:20:47 +12:00
schema: Mutation.create_schema(),
2020-08-14 09:39:59 +12:00
args: [:name, :action],
describe: "A mutation to create a record",
examples: [
"create :create_post, :create"
2020-08-14 09:39:59 +12:00
],
2020-08-15 02:20:47 +12:00
target: Mutation,
2020-08-14 09:39:59 +12:00
auto_set_fields: [
type: :create
]
}
2022-08-31 13:08:16 +12:00
@update %Spark.Dsl.Entity{
2020-08-14 09:39:59 +12:00
name: :update,
2020-08-15 02:20:47 +12:00
schema: Mutation.update_schema(),
2020-08-14 09:39:59 +12:00
args: [:name, :action],
describe: "A mutation to update a record",
examples: [
"update :update_post, :update"
2020-08-14 09:39:59 +12:00
],
2020-08-15 02:20:47 +12:00
target: Mutation,
2020-08-14 09:39:59 +12:00
auto_set_fields: [
type: :update
]
}
2022-08-31 13:08:16 +12:00
@destroy %Spark.Dsl.Entity{
2020-08-14 09:39:59 +12:00
name: :destroy,
2020-08-15 02:20:47 +12:00
schema: Mutation.destroy_schema(),
2020-08-14 09:39:59 +12:00
args: [:name, :action],
describe: "A mutation to destroy a record",
examples: [
"destroy :destroy_post, :destroy"
2020-08-14 09:39:59 +12:00
],
2020-08-15 02:20:47 +12:00
target: Mutation,
2020-08-14 09:39:59 +12:00
auto_set_fields: [
type: :destroy
]
}
2022-08-31 13:08:16 +12:00
@queries %Spark.Dsl.Section{
2020-08-14 09:39:59 +12:00
name: :queries,
describe: """
Queries (read actions) to expose for the resource.
""",
2020-12-27 19:49:35 +13:00
examples: [
"""
queries do
get :get_post, :read
2021-04-04 19:10:50 +12:00
read_one :current_user, :current_user
list :list_posts, :read
2020-12-27 19:49:35 +13:00
end
"""
],
2020-08-14 09:39:59 +12:00
entities: [
@get,
2021-04-04 19:10:50 +12:00
@read_one,
2020-08-14 09:39:59 +12:00
@list
]
}
2022-08-31 13:08:16 +12:00
@managed_relationship %Spark.Dsl.Entity{
name: :managed_relationship,
schema: ManagedRelationship.schema(),
args: [:action, :argument],
target: ManagedRelationship,
describe: """
Instructs ash_graphql that a given argument with a `manage_relationship` change should have its input objects derived automatically from the potential actions to be called.
For example, given an action like:
```elixir
actions do
create :create do
argument :comments, {:array, :map}
change manage_relationship(:comments, type: :direct_control) # <- we look for this change with a matching argument name
end
end
```
You could add the following managed_relationship
```elixir
graphql do
...
managed_relationships do
2022-12-30 17:38:42 +13:00
managed_relationship :create, :comments
end
end
```
By default, the `{:array, :map}` would simply be a `json[]` type. If the argument name
is placed in this list, all of the potential actions that could be called will be combined
into a single input object. If there are type conflicts (for example, if the input could create
or update a record, and the create and update actions have an argument of the same name but with a different type),
a warning is emitted at compile time and the first one is used. If that is insufficient, you will need to do one of the following:
1.) provide the `:types` option to the `managed_relationship` constructor (see that option for more)
2.) define a custom type, with a custom input object (see the custom types guide), and use that custom type instead of `:map`
3.) change your actions to not have overlapping inputs with different types
"""
}
2022-08-31 13:08:16 +12:00
@managed_relationships %Spark.Dsl.Section{
name: :managed_relationships,
describe: """
2022-08-11 09:34:13 +12:00
Generates input objects for `manage_relationship` arguments on resource actions.
""",
examples: [
"""
managed_relationships do
manage_relationship :create_post, :comments
end
"""
],
entities: [
@managed_relationship
]
}
2022-08-31 13:08:16 +12:00
@mutations %Spark.Dsl.Section{
2020-08-14 09:39:59 +12:00
name: :mutations,
describe: """
Mutations (create/update/destroy actions) to expose for the resource.
""",
2020-12-27 19:49:35 +13:00
examples: [
"""
mutations do
create :create_post, :create
update :update_post, :update
destroy :destroy_post, :destroy
2020-12-27 19:49:35 +13:00
end
"""
],
2020-08-14 09:39:59 +12:00
entities: [
@create,
@update,
@destroy
]
}
2022-08-31 13:08:16 +12:00
@graphql %Spark.Dsl.Section{
2020-08-14 09:39:59 +12:00
name: :graphql,
imports: [AshGraphql.Resource.Helpers],
2020-08-14 09:39:59 +12:00
describe: """
Configuration for a given resource in graphql
""",
2020-12-27 19:49:35 +13:00
examples: [
"""
graphql do
type :post
queries do
get :get_post, :read
list :list_posts, :read
2020-12-27 19:49:35 +13:00
end
mutations do
create :create_post, :create
update :update_post, :update
destroy :destroy_post, :destroy
2020-12-27 19:49:35 +13:00
end
end
"""
],
2020-08-14 09:39:59 +12:00
schema: [
type: [
type: :atom,
required: true,
doc: "The type to use for this entity in the graphql schema"
2021-01-13 09:14:35 +13:00
],
derive_filter?: [
type: :boolean,
default: true,
doc: """
Set to false to disable the automatic generation of a filter input for read actions.
"""
],
derive_sort?: [
type: :boolean,
default: true,
doc: """
Set to false to disable the automatic generation of a sort input for read actions.
"""
],
encode_primary_key?: [
type: :boolean,
default: true,
doc:
"For resources with composite primary keys, or primary keys not called `:id`, this will cause the id to be encoded as a single `id` attribute, both in the representation of the resource and in get requests"
],
relationships: [
type: {:list, :atom},
required: false,
doc:
"A list of relationships to include on the created type. Defaults to all public relationships where the destination defines a graphql type."
],
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]`"
],
keyset_field: [
type: :atom,
doc: """
If set, the keyset will be displayed on all read actions in this field.
It will always be `nil` unless at least one of the read actions on a resource uses keyset pagination.
It will also be nil on any mutation results.
"""
],
attribute_types: [
type: :keyword_list,
doc:
"A keyword list of type overrides for attributes. The type overrides should refer to types available in the graphql (absinthe) schema. `list_of/1` and `non_null/1` helpers can be used."
],
attribute_input_types: [
type: :keyword_list,
doc:
"A keyword list of input type overrides for attributes. The type overrides should refer to types available in the graphql (absinthe) schema. `list_of/1` and `non_null/1` helpers can be used."
],
2021-01-13 09:14:35 +13:00
primary_key_delimiter: [
type: :string,
doc:
"If a composite primary key exists, this must be set to determine the `id` field value"
],
depth_limit: [
type: :integer,
doc: """
A simple way to prevent massive queries.
"""
],
generate_object?: [
type: :boolean,
doc:
"Whether or not to create the GraphQL object, this allows you to manually create the GraphQL object.",
default: true
2020-08-14 09:39:59 +12:00
]
],
sections: [
@queries,
@mutations,
@managed_relationships
2020-08-14 09:39:59 +12:00
]
}
@transformers [
AshGraphql.Resource.Transformers.RequirePkeyDelimiter,
AshGraphql.Resource.Transformers.RequireKeysetForRelayQueries,
AshGraphql.Resource.Transformers.ValidateActions,
AshGraphql.Resource.Transformers.ValidateCompatibleNames,
AshGraphql.Resource.Transformers.AddUnionTypeResolvers
2020-08-14 09:39:59 +12:00
]
@verifiers [
AshGraphql.Resource.Verifiers.VerifyQueryMetadata
]
2020-12-27 19:49:35 +13:00
@sections [@graphql]
@moduledoc """
This Ash resource extension adds configuration for exposing a resource in a graphql.
2022-08-31 13:08:16 +12:00
<!--- ash-hq-hide-start --> <!--- -->
2020-12-27 19:49:35 +13:00
2022-08-31 13:08:16 +12:00
## DSL Documentation
### Index
#{Spark.Dsl.Extension.doc_index(@sections)}
### Docs
#{Spark.Dsl.Extension.doc(@sections)}
<!--- ash-hq-hide-stop --> <!--- -->
2020-12-27 19:49:35 +13:00
"""
use Spark.Dsl.Extension, sections: @sections, transformers: @transformers, verifiers: @verifiers
2020-08-14 09:39:59 +12:00
2022-08-31 13:08:16 +12:00
@deprecated "See `AshGraphql.Resource.Info.queries/1`"
defdelegate queries(resource), to: AshGraphql.Resource.Info
2020-08-14 09:39:59 +12:00
2022-08-31 13:08:16 +12:00
@deprecated "See `AshGraphql.Resource.Info.mutations/1`"
defdelegate mutations(resource), to: AshGraphql.Resource.Info
2020-08-14 09:39:59 +12:00
2022-08-31 13:08:16 +12:00
@deprecated "See `AshGraphql.Resource.Info.managed_relationships/1`"
defdelegate managed_relationships(resource), to: AshGraphql.Resource.Info
2022-08-31 13:08:16 +12:00
@deprecated "See `AshGraphql.Resource.Info.type/1`"
defdelegate type(resource), to: AshGraphql.Resource.Info
2020-08-14 09:39:59 +12:00
2022-08-31 13:08:16 +12:00
@deprecated "See `AshGraphql.Resource.Info.primary_key_delimiter/1`"
defdelegate primary_key_delimiter(resource), to: AshGraphql.Resource.Info
2021-01-13 09:14:35 +13:00
2022-08-31 13:08:16 +12:00
@deprecated "See `AshGraphql.Resource.Info.generate_object?/1`"
defdelegate generate_object?(resource), to: AshGraphql.Resource.Info
def ref(env) do
%{module: __MODULE__, location: %{file: env.file, line: env.line}}
end
2021-01-13 09:14:35 +13:00
def encode_primary_key(%resource{} = record) do
2021-02-19 04:16:00 +13:00
case Ash.Resource.Info.primary_key(resource) do
2021-01-13 09:14:35 +13:00
[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
2021-02-19 04:16:00 +13:00
case Ash.Resource.Info.primary_key(resource) do
[field] ->
{:ok, [{field, value}]}
2021-01-13 09:14:35 +13:00
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
2020-08-14 09:39:59 +12:00
@doc false
def queries(api, resource, action_middleware, schema, as_mutations? \\ false) do
2022-08-31 13:08:16 +12:00
type = AshGraphql.Resource.Info.type(resource)
2020-08-14 09:39:59 +12:00
2020-12-30 09:11:16 +13:00
if type do
resource
|> queries()
|> Enum.filter(&(&1.as_mutation? == as_mutations?))
2020-12-30 09:11:16 +13:00
|> Enum.map(fn query ->
query_action =
Ash.Resource.Info.action(resource, query.action) ||
raise "No such action #{query.action} on #{resource}"
2020-12-30 09:11:16 +13:00
%Absinthe.Blueprint.Schema.FieldDefinition{
2021-01-13 09:14:35 +13:00
arguments: args(query.type, resource, query_action, schema, query.identity),
2020-12-30 09:11:16 +13:00
identifier: query.name,
middleware:
action_middleware ++
[
{{AshGraphql.Graphql.Resolver, :resolve}, {api, resource, query}}
],
complexity: {AshGraphql.Graphql.Resolver, :query_complexity},
2020-12-30 09:11:16 +13:00
module: schema,
name: to_string(query.name),
type: query_type(query, resource, query_action, type),
__reference__: ref(__ENV__)
2020-12-30 09:11:16 +13:00
}
end)
else
[]
end
2020-08-14 09:39:59 +12:00
end
2020-08-15 02:20:47 +12:00
# sobelow_skip ["DOS.StringToAtom"]
2020-08-14 09:39:59 +12:00
@doc false
def mutations(api, resource, action_middleware, schema) do
2020-08-14 09:39:59 +12:00
resource
|> mutations()
|> Enum.map(fn
%{type: :destroy} = mutation ->
2022-09-30 08:38:38 +13:00
action =
Ash.Resource.Info.action(resource, mutation.action) ||
raise "No such action #{mutation.action} for #{inspect(resource)}"
if action.soft? do
update_mutation(resource, schema, mutation, schema, action_middleware, api)
else
%Absinthe.Blueprint.Schema.FieldDefinition{
arguments: mutation_args(mutation, resource, schema),
identifier: mutation.name,
middleware:
action_middleware ++
[
{{AshGraphql.Graphql.Resolver, :mutate}, {api, resource, mutation}}
],
module: schema,
name: to_string(mutation.name),
type: String.to_atom("#{mutation.name}_result"),
__reference__: ref(__ENV__)
}
end
2020-08-14 09:39:59 +12:00
%{type: :create} = mutation ->
2022-09-30 08:38:38 +13:00
action =
Ash.Resource.Info.action(resource, mutation.action) ||
raise "No such action #{mutation.action} for #{inspect(resource)}"
2020-08-14 09:39:59 +12:00
args =
case mutation_fields(
resource,
schema,
action,
mutation.type
) do
[] ->
[]
_ ->
[
%Absinthe.Blueprint.Schema.InputValueDefinition{
identifier: :input,
module: schema,
name: "input",
placement: :argument_definition,
type: String.to_atom("#{mutation.name}_input")
}
]
end
%Absinthe.Blueprint.Schema.FieldDefinition{
arguments: args,
2020-08-14 09:39:59 +12:00
identifier: mutation.name,
middleware:
action_middleware ++
[
{{AshGraphql.Graphql.Resolver, :mutate}, {api, resource, mutation}}
],
2020-08-14 09:39:59 +12:00
module: schema,
name: to_string(mutation.name),
type: String.to_atom("#{mutation.name}_result"),
__reference__: ref(__ENV__)
2020-08-14 09:39:59 +12:00
}
mutation ->
update_mutation(resource, schema, mutation, schema, action_middleware, api)
2020-08-14 09:39:59 +12:00
end)
|> Enum.concat(queries(api, resource, action_middleware, schema, true))
2020-08-14 09:39:59 +12:00
end
# sobelow_skip ["DOS.StringToAtom"]
defp update_mutation(resource, schema, mutation, schema, action_middleware, api) do
2022-09-30 08:38:38 +13:00
action =
Ash.Resource.Info.action(resource, mutation.action) ||
raise "No such action #{mutation.action} for #{inspect(resource)}"
args =
case mutation_fields(
resource,
schema,
action,
mutation.type
) do
[] ->
mutation_args(mutation, resource, schema)
_ ->
mutation_args(mutation, resource, schema) ++
[
%Absinthe.Blueprint.Schema.InputValueDefinition{
identifier: :input,
module: schema,
name: "input",
placement: :argument_definition,
type: String.to_atom("#{mutation.name}_input"),
__reference__: ref(__ENV__)
}
]
end
%Absinthe.Blueprint.Schema.FieldDefinition{
arguments: args,
identifier: mutation.name,
middleware:
action_middleware ++
[
{{AshGraphql.Graphql.Resolver, :mutate}, {api, resource, mutation}}
],
module: schema,
name: to_string(mutation.name),
type: String.to_atom("#{mutation.name}_result"),
__reference__: ref(__ENV__)
}
end
defp mutation_args(%{identity: false} = mutation, resource, schema) do
mutation_read_args(mutation, resource, schema)
end
defp mutation_args(%{identity: identity} = mutation, resource, schema)
when not is_nil(identity) do
resource
2021-02-19 04:16:00 +13:00
|> Ash.Resource.Info.identities()
|> Enum.find(&(&1.name == identity))
|> Map.get(:keys)
|> Enum.map(fn key ->
2021-02-19 04:16:00 +13:00
attribute = Ash.Resource.Info.attribute(resource, key)
%Absinthe.Blueprint.Schema.InputValueDefinition{
name: to_string(key),
identifier: key,
type: %Absinthe.Blueprint.TypeReference.NonNull{
of_type: field_type(attribute.type, attribute, resource)
},
description: attribute.description || "",
__reference__: ref(__ENV__)
}
end)
|> Enum.concat(mutation_read_args(mutation, resource, schema))
end
defp mutation_args(mutation, resource, schema) do
[
%Absinthe.Blueprint.Schema.InputValueDefinition{
identifier: :id,
module: schema,
name: "id",
placement: :argument_definition,
type: :id,
__reference__: ref(__ENV__)
}
| mutation_read_args(mutation, resource, schema)
]
end
defp mutation_read_args(%{read_action: read_action}, resource, schema) do
read_action =
cond do
is_nil(read_action) ->
Ash.Resource.Info.primary_action!(resource, :read)
is_atom(read_action) ->
Ash.Resource.Info.action(resource, read_action)
true ->
read_action
end
read_action.arguments
|> Enum.reject(& &1.private?)
|> Enum.map(fn argument ->
type =
argument.type
|> field_type(argument, resource, true)
|> maybe_wrap_non_null(argument_required?(argument))
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: argument.name,
module: schema,
name: to_string(argument.name),
type: type,
__reference__: ref(__ENV__)
}
end)
end
2020-08-14 09:39:59 +12:00
@doc false
2020-08-15 02:20:47 +12:00
# sobelow_skip ["DOS.StringToAtom"]
2020-08-14 09:39:59 +12:00
def mutation_types(resource, schema) do
resource
|> mutations()
|> Enum.flat_map(fn mutation ->
2020-09-24 12:54:57 +12:00
mutation = %{
mutation
2022-09-30 08:38:38 +13:00
| action:
Ash.Resource.Info.action(resource, mutation.action) ||
raise("No such action #{mutation.action} for #{inspect(resource)}")
2020-09-24 12:54:57 +12:00
}
2020-08-14 09:39:59 +12:00
description =
if mutation.type == :destroy do
"The record that was successfully deleted"
else
"The successful result of the mutation"
end
fields = [
%Absinthe.Blueprint.Schema.FieldDefinition{
description: description,
identifier: :result,
module: schema,
name: "result",
2022-08-31 13:08:16 +12:00
type: AshGraphql.Resource.Info.type(resource),
__reference__: ref(__ENV__)
},
%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
},
__reference__: ref(__ENV__)
}
]
metadata_object_type = metadata_field(resource, mutation, schema)
fields =
if metadata_object_type do
fields ++
[
%Absinthe.Blueprint.Schema.FieldDefinition{
description: "Metadata produced by the mutation",
identifier: :metadata,
module: schema,
name: "metadata",
type: metadata_object_type.identifier,
__reference__: ref(__ENV__)
}
]
else
fields
end
2020-08-14 09:39:59 +12:00
result = %Absinthe.Blueprint.Schema.ObjectTypeDefinition{
description: "The result of the #{inspect(mutation.name)} mutation",
fields: fields,
2020-08-14 09:39:59 +12:00
identifier: String.to_atom("#{mutation.name}_result"),
module: schema,
name: Macro.camelize("#{mutation.name}_result"),
__reference__: ref(__ENV__)
2020-08-14 09:39:59 +12:00
}
case mutation_fields(
resource,
schema,
mutation.action,
mutation.type
) do
[] ->
[result] ++ List.wrap(metadata_object_type)
2020-08-14 09:39:59 +12:00
fields ->
input = %Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
fields: fields,
identifier: String.to_atom("#{mutation.name}_input"),
module: schema,
name: Macro.camelize("#{mutation.name}_input"),
__reference__: ref(__ENV__)
}
[input, result] ++ List.wrap(metadata_object_type)
2020-08-14 09:39:59 +12:00
end
end)
end
# sobelow_skip ["DOS.StringToAtom"]
defp metadata_field(resource, mutation, schema) do
metadata_fields =
Map.get(mutation.action, :metadata, [])
|> Enum.map(fn metadata ->
field_type =
metadata.type
|> field_type(metadata, resource)
|> maybe_wrap_non_null(not metadata.allow_nil?)
%Absinthe.Blueprint.Schema.FieldDefinition{
description: metadata.description,
identifier: metadata.name,
module: schema,
name: to_string(metadata.name),
type: field_type,
__reference__: ref(__ENV__)
}
end)
if !Enum.empty?(metadata_fields) do
name = "#{mutation.name}_metadata"
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{
fields: metadata_fields,
identifier: String.to_atom(name),
module: schema,
name: Macro.camelize(name),
__reference__: ref(__ENV__)
}
end
end
2021-01-13 09:14:35 +13:00
@doc false
# sobelow_skip ["DOS.StringToAtom"]
def embedded_type_input(source_resource, attribute, resource, schema) do
attribute = %{
attribute
| constraints: Ash.Type.NewType.constraints(resource, attribute.constraints)
}
resource = Ash.Type.NewType.subtype_of(resource)
create_action =
case attribute.constraints[:create_action] do
nil ->
Ash.Resource.Info.primary_action!(resource, :create)
name ->
Ash.Resource.Info.action(resource, name)
end
2021-01-13 09:14:35 +13:00
update_action =
case attribute.constraints[:update_action] do
nil ->
Ash.Resource.Info.primary_action!(resource, :update)
name ->
Ash.Resource.Info.action(resource, name)
end
fields =
mutation_fields(resource, schema, create_action, :create) ++
mutation_fields(resource, schema, update_action, :update)
fields =
fields
|> Enum.group_by(& &1.identifier)
# We only want one field per id. Right now we just take the first one
# If there are overlaps, and the field isn't `NonNull` in *all* cases, then
# we pick one and mark it explicitly as nullable (we unwrap the `NonNull`)
|> Enum.map(fn {_id, fields} ->
if Enum.all?(
fields,
&match?(%Absinthe.Blueprint.TypeReference.NonNull{}, &1.type)
) do
Enum.at(fields, 0)
else
fields
|> Enum.at(0)
|> case do
%{type: %Absinthe.Blueprint.TypeReference.NonNull{of_type: type}} = field ->
%{field | type: type}
field ->
field
end
end
2021-01-13 09:14:35 +13:00
end)
2022-08-31 13:08:16 +12:00
name = "#{AshGraphql.Resource.Info.type(source_resource)}_#{attribute.name}_input"
2021-01-13 09:14:35 +13:00
%Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
fields: fields,
identifier: String.to_atom(name),
2021-01-13 09:14:35 +13:00
module: schema,
name: Macro.camelize(name),
__reference__: ref(__ENV__)
2021-01-13 09:14:35 +13:00
}
end
defp mutation_fields(resource, schema, action, type) do
managed_relationships =
Enum.filter(
2022-08-31 13:08:16 +12:00
AshGraphql.Resource.Info.managed_relationships(resource),
&(&1.action == action.name)
)
field_names = AshGraphql.Resource.Info.field_names(resource)
argument_names = AshGraphql.Resource.Info.argument_names(resource)
2020-08-14 09:39:59 +12:00
attribute_fields =
if action.type == :destroy && !action.soft? do
[]
else
resource
|> Ash.Resource.Info.public_attributes()
|> Enum.filter(fn attribute ->
2022-04-27 03:35:09 +12:00
(is_nil(action.accept) || attribute.name in action.accept) && attribute.writable?
end)
|> Enum.map(fn attribute ->
allow_nil? =
attribute.allow_nil? || attribute.default != nil || type == :update ||
attribute.generated? ||
(type == :create && attribute.name in action.allow_nil_input)
explicitly_required = attribute.name in action.require_attributes
field_type =
attribute.type
|> field_type(attribute, resource, true)
|> maybe_wrap_non_null(explicitly_required || not allow_nil?)
2020-08-14 09:39:59 +12:00
name = field_names[attribute.name] || attribute.name
%Absinthe.Blueprint.Schema.FieldDefinition{
description: attribute.description,
identifier: attribute.name,
module: schema,
name: to_string(name),
type: field_type,
__reference__: ref(__ENV__)
}
end)
end
2020-08-14 09:39:59 +12:00
2020-12-02 18:07:15 +13:00
argument_fields =
action.arguments
|> Enum.reject(& &1.private?)
|> Enum.map(fn argument ->
name = argument_names[action.name][argument.name] || argument.name
case find_manage_change(argument, action, managed_relationships) do
nil ->
type =
argument.type
|> field_type(argument, resource, true)
|> maybe_wrap_non_null(argument_required?(argument))
2020-12-02 18:07:15 +13:00
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: name,
module: schema,
name: to_string(name),
type: type,
__reference__: ref(__ENV__)
}
_manage_opts ->
managed = Enum.find(managed_relationships, &(&1.argument == argument.name))
type =
if managed.type_name do
managed.type_name
else
default_managed_type_name(resource, action, argument)
end
type = wrap_arrays(argument.type, type, argument.constraints)
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: argument.name,
module: schema,
name: to_string(name),
type: maybe_wrap_non_null(type, argument_required?(argument)),
__reference__: ref(__ENV__)
}
end
2020-12-02 18:07:15 +13:00
end)
2021-04-06 05:08:39 +12:00
attribute_fields ++ argument_fields
2020-08-14 09:39:59 +12:00
end
defp wrap_arrays({:array, arg_type}, type, constraints) do
%Absinthe.Blueprint.TypeReference.List{
of_type:
maybe_wrap_non_null(
wrap_arrays(arg_type, type, constraints[:items] || []),
!constraints[:nil_items?] || Ash.Type.embedded_type?(type)
)
}
end
defp wrap_arrays(_, type, _), do: type
type_name_template =
Application.compile_env(
:ash_graphql,
:default_managed_relationship_type_name_template,
:action_type
)
case type_name_template do
:action_type ->
2023-03-22 14:27:07 +13:00
# sobelow_skip ["DOS.StringToAtom"]
defp default_managed_type_name(resource, action, argument) do
type_name =
String.to_atom(
to_string(action.type) <>
"_" <>
to_string(AshGraphql.Resource.Info.type(resource)) <>
"_" <> to_string(argument.name) <> "_input"
)
IO.warn("""
#{inspect(resource)}:
Type Name Error in `managed_relationship :#{action.name}, #{argument.name}`.
Type names for managed_relationships have been updated, but for backwards compatibility must
be explicitly opted into. These type names are better because the old ones are based off of the
action type, not the action name, and therefore could produce clashes in their type names.
To resolve this warning, do the following things:
1) If you want to keep the current type name, set an explicit type name for this and any other
affected `managed_relationship`. Here is an example of the specific `managed_relationship` with the fix
applied:
managed_relationship :#{action.name}, #{argument.name} do
type_name :#{type_name} # <- add this line
end
2) Once you have done the above (or skipped it because you don't care about the type names),
you can set the following configuration:
config :ash_graphql, :default_managed_relationship_type_name_template, :action_name
""")
type_name
end
:action_name ->
2023-03-22 14:27:07 +13:00
# sobelow_skip ["DOS.StringToAtom"]
defp default_managed_type_name(_resource, action, argument) do
String.to_atom(
to_string(action.name) <>
"_" <> to_string(argument.name) <> "_input"
)
end
end
defp find_manage_change(argument, action, managed_relationships) do
if argument.name in Enum.map(managed_relationships, & &1.argument) do
Enum.find_value(action.changes, fn
%{change: {Ash.Resource.Change.ManageRelationship, opts}} ->
opts[:argument] == argument.name && opts
_ ->
nil
end)
end
end
2020-08-15 02:20:47 +12:00
# sobelow_skip ["DOS.StringToAtom"]
defp query_type(%{type: :list, relay?: relay?} = query, _resource, action, type) do
type = query.type_name || type
if action.pagination do
cond do
relay? ->
String.to_atom("#{type}_connection")
action.pagination.keyset? ->
String.to_atom("keyset_page_of_#{type}")
true ->
String.to_atom("page_of_#{type}")
end
else
%Absinthe.Blueprint.TypeReference.NonNull{
of_type: %Absinthe.Blueprint.TypeReference.List{
of_type: %Absinthe.Blueprint.TypeReference.NonNull{
of_type: type
}
}
}
end
end
2020-08-14 09:39:59 +12:00
defp query_type(query, _resource, _action, type) do
type = query.type_name || type
maybe_wrap_non_null(type, not query.allow_nil?)
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
%Absinthe.Blueprint.TypeReference.NonNull{
of_type: type
}
end
defp maybe_wrap_non_null(type, _), do: type
defp get_fields(resource) do
if AshGraphql.Resource.Info.encode_primary_key?(resource) do
[
%Absinthe.Blueprint.Schema.InputValueDefinition{
name: "id",
identifier: :id,
type: %Absinthe.Blueprint.TypeReference.NonNull{of_type: :id},
description: "The id of the record",
__reference__: ref(__ENV__)
}
]
else
resource
|> Ash.Resource.Info.primary_key()
|> Enum.map(fn key ->
attribute = Ash.Resource.Info.attribute(resource, key)
%Absinthe.Blueprint.Schema.InputValueDefinition{
name: to_string(key),
identifier: key,
type: %Absinthe.Blueprint.TypeReference.NonNull{
of_type: field_type(attribute.type, attribute, resource, true)
},
description: attribute.description || "",
__reference__: ref(__ENV__)
}
end)
end
end
2021-01-13 09:14:35 +13:00
defp args(action_type, resource, action, schema, identity \\ nil)
defp args(:get, resource, action, schema, nil) do
get_fields(resource) ++
read_args(resource, action, schema)
2020-08-14 09:39:59 +12:00
end
defp args(:get, resource, action, schema, identity) do
if identity do
resource
|> Ash.Resource.Info.identities()
|> Enum.find(&(&1.name == identity))
|> Map.get(:keys)
|> Enum.map(fn key ->
attribute = Ash.Resource.Info.attribute(resource, key)
%Absinthe.Blueprint.Schema.InputValueDefinition{
name: to_string(key),
identifier: key,
type: %Absinthe.Blueprint.TypeReference.NonNull{
of_type: field_type(attribute.type, attribute, resource, true)
},
description: attribute.description || "",
__reference__: ref(__ENV__)
}
end)
else
[]
end
|> Enum.concat(read_args(resource, action, schema))
end
2021-04-04 19:10:50 +12:00
defp args(:read_one, resource, action, schema, _) do
args =
if AshGraphql.Resource.Info.derive_filter?(resource) do
case resource_filter_fields(resource, schema) do
[] ->
[]
2021-04-04 19:10:50 +12:00
_ ->
[
%Absinthe.Blueprint.Schema.InputValueDefinition{
name: "filter",
identifier: :filter,
type: resource_filter_type(resource),
description: "A filter to limit the results",
__reference__: ref(__ENV__)
}
]
end
else
[]
2021-04-04 19:10:50 +12:00
end
args ++ read_args(resource, action, schema)
end
2021-01-13 09:14:35 +13:00
defp args(:list, resource, action, schema, _) do
args =
if AshGraphql.Resource.Info.derive_filter?(resource) do
case resource_filter_fields(resource, schema) do
[] ->
[]
2021-01-13 09:14:35 +13:00
_ ->
[
%Absinthe.Blueprint.Schema.InputValueDefinition{
name: "filter",
identifier: :filter,
type: resource_filter_type(resource),
description: "A filter to limit the results",
__reference__: ref(__ENV__)
}
]
end
else
[]
2021-01-13 09:14:35 +13:00
end
args =
if AshGraphql.Resource.Info.derive_sort?(resource) do
case sort_values(resource) do
[] ->
args
2021-01-13 09:14:35 +13:00
_ ->
[
%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",
__reference__: ref(__ENV__)
}
| args
]
end
else
args
2021-01-13 09:14:35 +13:00
end
args ++ pagination_args(action) ++ read_args(resource, action, schema)
end
defp args(:list_related, resource, action, schema, identity) do
args(:list, resource, action, schema, identity) ++
[
%Absinthe.Blueprint.Schema.InputValueDefinition{
name: "limit",
identifier: :limit,
type: :integer,
description: "The number of records to return.",
__reference__: ref(__ENV__)
},
%Absinthe.Blueprint.Schema.InputValueDefinition{
name: "offset",
identifier: :offset,
type: :integer,
description: "The number of records to skip.",
__reference__: ref(__ENV__)
}
]
end
defp args(:one_related, resource, action, schema, _identity) do
read_args(resource, action, schema)
end
defp read_args(resource, action, schema) do
action.arguments
|> Enum.reject(& &1.private?)
|> Enum.map(fn argument ->
type =
argument.type
|> field_type(argument, resource, true)
|> maybe_wrap_non_null(argument_required?(argument))
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: argument.name,
module: schema,
name: to_string(argument.name),
type: type,
__reference__: ref(__ENV__)
}
end)
end
defp pagination_args(action) do
if action.pagination do
if action.pagination.keyset? do
keyset_pagination_args(action)
else
offset_pagination_args(action)
end
else
[]
end
end
# sobelow_skip ["DOS.StringToAtom"]
defp resource_sort_type(resource) do
2022-08-31 13:08:16 +12:00
String.to_atom(to_string(AshGraphql.Resource.Info.type(resource)) <> "_sort_input")
end
2020-12-30 09:11:16 +13:00
# sobelow_skip ["DOS.StringToAtom"]
defp resource_filter_type(resource) do
2022-08-31 13:08:16 +12:00
String.to_atom(to_string(AshGraphql.Resource.Info.type(resource)) <> "_filter_input")
2020-12-30 09:11:16 +13:00
end
# sobelow_skip ["DOS.StringToAtom"]
defp attribute_filter_field_type(resource, attribute) do
field_names = AshGraphql.Resource.Info.field_names(resource)
2020-12-30 09:11:16 +13:00
String.to_atom(
2022-08-31 13:08:16 +12:00
to_string(AshGraphql.Resource.Info.type(resource)) <>
"_filter_" <> to_string(field_names[attribute.name] || attribute.name)
2020-12-30 09:11:16 +13:00
)
end
# sobelow_skip ["DOS.StringToAtom"]
defp calculation_filter_field_type(resource, calculation) do
field_names = AshGraphql.Resource.Info.field_names(resource)
String.to_atom(
2022-08-31 13:08:16 +12:00
to_string(AshGraphql.Resource.Info.type(resource)) <>
"_filter_" <> to_string(field_names[calculation.name] || calculation.name)
)
end
defp keyset_pagination_args(action) do
if action.pagination.keyset? do
max_message =
if action.pagination.max_page_size do
" Maximum #{action.pagination.max_page_size}"
else
""
end
[
%Absinthe.Blueprint.Schema.InputValueDefinition{
name: "first",
identifier: :first,
type: :integer,
description: "The number of records to return from the beginning." <> max_message,
__reference__: ref(__ENV__)
},
%Absinthe.Blueprint.Schema.InputValueDefinition{
name: "before",
identifier: :before,
type: :string,
description: "Show records before the specified keyset.",
__reference__: ref(__ENV__)
},
%Absinthe.Blueprint.Schema.InputValueDefinition{
name: "after",
identifier: :after,
type: :string,
description: "Show records after the specified keyset.",
__reference__: ref(__ENV__)
},
%Absinthe.Blueprint.Schema.InputValueDefinition{
name: "last",
identifier: :last,
type: :integer,
description: "The number of records to return to the end." <> max_message,
__reference__: ref(__ENV__)
}
]
else
[]
end
end
defp offset_pagination_args(action) do
if action.pagination.offset? do
max_message =
if action.pagination.max_page_size do
" Maximum #{action.pagination.max_page_size}"
else
""
end
limit_type =
maybe_wrap_non_null(
:integer,
action.pagination.required? && is_nil(action.pagination.default_limit)
)
[
%Absinthe.Blueprint.Schema.InputValueDefinition{
name: "limit",
identifier: :limit,
type: limit_type,
default_value: action.pagination.default_limit,
description: "The number of records to return." <> max_message,
__reference__: ref(__ENV__)
},
%Absinthe.Blueprint.Schema.InputValueDefinition{
name: "offset",
identifier: :offset,
type: :integer,
description: "The number of records to skip.",
__reference__: ref(__ENV__)
}
]
else
[]
end
2020-08-14 09:39:59 +12:00
end
@doc false
2020-09-24 12:54:57 +12:00
def type_definitions(resource, api, schema) do
List.wrap(calculation_input(resource, schema)) ++
List.wrap(type_definition(resource, api, schema)) ++
List.wrap(query_type_definitions(resource, api, schema)) ++
2021-01-13 09:14:35 +13:00
List.wrap(sort_input(resource, schema)) ++
List.wrap(filter_input(resource, schema)) ++
2020-12-30 09:11:16 +13:00
filter_field_types(resource, schema) ++
List.wrap(page_of(resource, schema)) ++
List.wrap(relay_page(resource, schema)) ++
List.wrap(keyset_page_of(resource, schema)) ++
enum_definitions(resource, schema, __ENV__) ++
union_definitions(resource, schema, __ENV__) ++
managed_relationship_definitions(resource, schema)
end
def no_graphql_types(resource, schema) do
2022-04-27 04:15:48 +12:00
enum_definitions(resource, schema, __ENV__, true) ++
union_definitions(resource, schema, __ENV__) ++
managed_relationship_definitions(resource, schema)
end
defp managed_relationship_definitions(resource, schema) do
resource
|> Ash.Resource.Info.actions()
|> Enum.flat_map(fn action ->
resource
2022-08-31 13:08:16 +12:00
|> AshGraphql.Resource.Info.managed_relationships()
|> Enum.filter(&(&1.action == action.name))
|> Enum.map(fn managed_relationship ->
argument =
Enum.find(action.arguments, &(&1.name == managed_relationship.argument)) ||
raise """
No such argument #{managed_relationship.argument}, in `managed_relationship`
"""
opts =
find_manage_change(argument, action, [managed_relationship]) ||
raise """
There is no corresponding `change manage_change(...)` for the given argument and action
combination.
"""
managed_relationship_input(
resource,
action,
opts,
argument,
managed_relationship,
schema
)
end)
end)
end
defp managed_relationship_input(resource, action, opts, argument, managed_relationship, schema) do
relationship =
Ash.Resource.Info.relationship(resource, opts[:relationship]) ||
raise """
No relationship found when building managed relationship input: #{opts[:relationship]}
"""
manage_opts_schema =
if opts[:opts][:type] do
defaults = Ash.Changeset.manage_relationship_opts(opts[:opts][:type])
Enum.reduce(defaults, Ash.Changeset.manage_relationship_schema(), fn {key, value},
manage_opts ->
2022-08-31 13:08:16 +12:00
Spark.OptionsHelpers.set_default!(manage_opts, key, value)
end)
else
Ash.Changeset.manage_relationship_schema()
end
2022-08-31 13:08:16 +12:00
manage_opts = Spark.OptionsHelpers.validate!(opts[:opts], manage_opts_schema)
fields =
on_match_fields(manage_opts, relationship, schema) ++
on_no_match_fields(manage_opts, relationship, schema) ++
on_lookup_fields(manage_opts, relationship, schema) ++
2021-05-15 04:25:43 +12:00
manage_pkey_fields(manage_opts, managed_relationship, relationship, schema)
type = managed_relationship.type_name || default_managed_type_name(resource, action, argument)
fields = check_for_conflicts!(fields, managed_relationship, resource)
if Enum.empty?(fields) do
raise """
Input object for managed relationship #{relationship.name} on #{inspect(relationship.source)}#{action.name} would have no fields.
This typically means that you are missing the `lookup_with_primary_key?` option or the `lookup_identities` option on the configured
managed_relationship DSL. For example, calls to `manage_relationship` that only look things up and accept no modifications
(like `type: :accept`), they will have no fields because we don't assume the primary key or specific identities should be included in the
input object.
"""
end
%Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
identifier: type,
fields: fields,
module: schema,
name: type |> to_string() |> Macro.camelize(),
__reference__: ref(__ENV__)
}
end
defp check_for_conflicts!(fields, managed_relationship, resource) do
{ok, errors} =
fields
|> Enum.map(fn {resource, action, field} ->
%{field: field, source: %{resource: resource, action: action}}
end)
|> Enum.group_by(& &1.field.identifier)
|> Enum.map(fn {identifier, data} ->
case Keyword.fetch(managed_relationship.types || [], identifier) do
{:ok, nil} ->
nil
{:ok, type} ->
type = unwrap_literal_type(type)
{:ok, %{Enum.at(data, 0).field | type: type}}
:error ->
get_conflicts(data)
end
end)
|> Enum.reject(&is_nil/1)
|> Enum.split_with(&match?({:ok, _}, &1))
unless Enum.empty?(errors) do
raise_conflicts!(Enum.map(errors, &elem(&1, 1)), managed_relationship, resource)
end
Enum.map(ok, &elem(&1, 1))
end
defp raise_conflicts!(conflicts, managed_relationship, resource) do
raise """
#{inspect(resource)}: #{managed_relationship.action}.#{managed_relationship.argument}
Error while deriving managed relationship input object type: type conflict.
Because multiple actions could be called, and those actions may have different
derived types, you will need to override the graphql schema to specify the type
for the following fields. This can be done by specifying the `types` option on your
`managed_relationship` inside of the `managed_relationships` in your resource's
`graphql` configuration.
#{Enum.map_join(conflicts, "\n\n", &conflict_message(&1, managed_relationship))}
"""
end
defp conflict_message(
{_reducing_type, _type, [%{field: %{name: name}} | _] = fields},
managed_relationship
) do
formatted_types =
fields
|> Enum.map(fn
%{source: %{action: :__primary_key}} = field ->
"#{inspect(format_type(field.field.type))} - from #{inspect(field.source.resource)}'s lookup by primary key"
%{source: %{action: {:identity, identity}}} = field ->
"#{inspect(format_type(field.field.type))} - from #{inspect(field.source.resource)}'s identity: #{identity}"
field ->
"#{inspect(format_type(field.field.type))} - from #{inspect(field.source.resource)}.#{field.source.action}"
end)
|> Enum.uniq()
"""
Possible types for #{managed_relationship.action}.#{managed_relationship.argument}.#{name}:
#{Enum.map(formatted_types, &" * #{&1}\n")}
"""
end
defp unwrap_literal_type({:non_null, {:non_null, type}}) do
unwrap_literal_type({:non_null, type})
end
defp unwrap_literal_type({:array, {:array, type}}) do
unwrap_literal_type({:array, type})
end
defp unwrap_literal_type({:non_null, type}) do
%Absinthe.Blueprint.TypeReference.NonNull{of_type: unwrap_literal_type(type)}
end
defp unwrap_literal_type({:array, type}) do
%Absinthe.Blueprint.TypeReference.List{of_type: unwrap_literal_type(type)}
end
defp unwrap_literal_type(type) do
type
end
defp format_type(%Absinthe.Blueprint.TypeReference.NonNull{of_type: type}) do
{:non_null, format_type(type)}
end
defp format_type(%Absinthe.Blueprint.TypeReference.List{of_type: type}) do
{:array, format_type(type)}
end
defp format_type(type) do
type
end
defp get_conflicts([field]) do
{:ok, field.field}
end
defp get_conflicts([field | _] = fields) do
case reduce_types(fields) do
{:ok, res} ->
{:ok, %{field.field | type: res}}
{:error, {reducing_type, type}} ->
{:error, {reducing_type, type, fields}}
end
end
defp reduce_types(fields) do
Enum.reduce_while(fields, {:ok, nil}, fn field, {:ok, type} ->
if type do
case match_types(type, field.field.type) do
{:ok, value} ->
{:cont, {:ok, value}}
:error ->
{:halt, {:error, {type, field.field.type}}}
end
else
{:cont, {:ok, field.field.type}}
end
end)
end
defp match_types(
%Absinthe.Blueprint.TypeReference.NonNull{
of_type: type
},
type
) do
{:ok, type}
end
defp match_types(
type,
%Absinthe.Blueprint.TypeReference.NonNull{
of_type: type
}
) do
{:ok, type}
end
defp match_types(
type,
type
) do
{:ok, type}
end
defp match_types(_, _) do
:error
end
defp on_lookup_fields(opts, relationship, schema) do
case ManagedRelationshipHelpers.on_lookup_update_action(opts, relationship) do
{:destination, nil} ->
[]
{:destination, action} ->
action = Ash.Resource.Info.action(relationship.destination, action)
relationship.destination
|> mutation_fields(schema, action, action.type)
|> Enum.map(fn field ->
{relationship.destination, action.name, field}
end)
{:source, nil} ->
[]
{:source, action} ->
action = Ash.Resource.Info.action(relationship.source, action)
relationship.source
|> mutation_fields(schema, action, action.type)
|> Enum.map(fn field ->
{relationship.source, action.name, field}
end)
{:join, nil, _} ->
[]
{:join, action, fields} ->
action = Ash.Resource.Info.action(relationship.through, action)
if fields == :all do
mutation_fields(relationship.through, schema, action, action.type)
else
relationship.through
|> mutation_fields(schema, action, action.type)
|> Enum.filter(&(&1.identifier in fields))
end
|> Enum.map(fn field ->
{relationship.through, action.name, field}
end)
nil ->
[]
end
end
defp on_match_fields(opts, relationship, schema) do
opts
|> ManagedRelationshipHelpers.on_match_destination_actions(relationship)
|> List.wrap()
|> Enum.flat_map(fn
{:destination, nil} ->
[]
{:destination, action_name} ->
action = Ash.Resource.Info.action(relationship.destination, action_name)
relationship.destination
|> mutation_fields(schema, action, action.type)
|> Enum.map(fn field ->
{relationship.destination, action.name, field}
end)
{:join, nil, _} ->
[]
{:join, action_name, fields} ->
action = Ash.Resource.Info.action(relationship.through, action_name)
if fields == :all do
mutation_fields(relationship.through, schema, action, action.type)
else
relationship.through
|> mutation_fields(schema, action, action.type)
|> Enum.filter(&(&1.identifier in fields))
end
|> Enum.map(fn field ->
{relationship.through, action.name, field}
end)
end)
end
defp on_no_match_fields(opts, relationship, schema) do
opts
|> ManagedRelationshipHelpers.on_no_match_destination_actions(relationship)
|> List.wrap()
|> Enum.flat_map(fn
{:destination, nil} ->
[]
{:destination, action_name} ->
action = Ash.Resource.Info.action(relationship.destination, action_name)
relationship.destination
|> mutation_fields(schema, action, action.type)
|> Enum.map(fn field ->
{relationship.destination, action.name, field}
end)
{:join, nil, _} ->
[]
{:join, action_name, fields} ->
action = Ash.Resource.Info.action(relationship.through, action_name)
if fields == :all do
mutation_fields(relationship.through, schema, action, action.type)
else
relationship.through
|> mutation_fields(schema, action, action.type)
|> Enum.filter(&(&1.identifier in fields))
end
|> Enum.map(fn field ->
{relationship.through, action.name, field}
end)
end)
end
2021-05-15 04:25:43 +12:00
defp manage_pkey_fields(opts, managed_relationship, relationship, schema) do
resource = relationship.destination
could_lookup? = ManagedRelationshipHelpers.could_lookup?(opts)
could_match? = ManagedRelationshipHelpers.could_update?(opts)
if could_lookup? || could_match? do
pkey_fields =
if managed_relationship.lookup_with_primary_key? || could_match? do
resource
|> pkey_fields(schema, false)
|> Enum.map(fn field ->
{resource, :__primary_key, field}
end)
else
[]
end
resource
|> Ash.Resource.Info.identities()
|> then(fn identities ->
if could_lookup? do
identities
else
[]
end
end)
|> Enum.filter(fn identity ->
is_nil(managed_relationship.lookup_identities) ||
2021-05-12 09:36:09 +12:00
identity.name in managed_relationship.lookup_identities
end)
|> Enum.flat_map(fn identity ->
identity
|> Map.get(:keys)
|> Enum.map(fn key ->
{identity.name, key}
end)
end)
|> Enum.uniq_by(&elem(&1, 1))
|> Enum.map(fn {identity_name, key} ->
attribute = Ash.Resource.Info.attribute(resource, key)
field = %Absinthe.Blueprint.Schema.InputValueDefinition{
name: to_string(key),
identifier: key,
type: field_type(attribute.type, attribute, resource),
description: attribute.description || "",
__reference__: ref(__ENV__)
}
{resource, {:identity, identity_name}, field}
end)
|> Enum.concat(pkey_fields)
else
[]
end
2020-12-30 09:11:16 +13:00
end
defp filter_field_types(resource, schema) do
filter_attribute_types(resource, schema) ++ filter_aggregate_types(resource, schema)
end
defp filter_attribute_types(resource, schema) do
resource
2021-02-19 04:16:00 +13:00
|> Ash.Resource.Info.public_attributes()
|> Enum.filter(&filterable?(&1, resource))
2020-12-30 09:11:16 +13:00
|> Enum.flat_map(&filter_type(&1, resource, schema))
end
defp filter_aggregate_types(resource, schema) do
resource
2021-02-19 04:16:00 +13:00
|> Ash.Resource.Info.public_aggregates()
|> Enum.filter(&filterable?(&1, resource))
2020-12-30 09:11:16 +13:00
|> Enum.flat_map(&filter_type(&1, resource, schema))
end
2021-02-19 04:16:00 +13:00
defp attribute_or_aggregate_type(%Ash.Resource.Attribute{type: type}, _resource),
do: type
2020-12-30 09:11:16 +13:00
2021-04-27 09:05:29 +12:00
defp attribute_or_aggregate_type(
%Ash.Resource.Aggregate{kind: kind, field: field, relationship_path: relationship_path},
resource
) do
2020-12-30 09:11:16 +13:00
field_type =
2021-04-27 09:05:29 +12:00
with field when not is_nil(field) <- field,
related when not is_nil(related) <-
Ash.Resource.Info.related(resource, relationship_path),
attr when not is_nil(attr) <- Ash.Resource.Info.attribute(related, field) do
attr.type
2020-12-30 09:11:16 +13:00
end
{:ok, aggregate_type} = Ash.Query.Aggregate.kind_to_type(kind, field_type)
aggregate_type
2020-12-30 09:11:16 +13:00
end
defp filter_type(attribute_or_aggregate, resource, schema) do
type = attribute_or_aggregate_type(attribute_or_aggregate, resource)
2021-04-28 05:17:16 +12:00
array_type? = match?({:array, _}, type)
2020-12-30 09:11:16 +13:00
fields =
Ash.Filter.builtin_operators()
|> Enum.filter(& &1.predicate?)
|> restrict_for_lists(type)
|> Enum.flat_map(fn operator ->
filter_fields(operator, type, array_type?, schema, attribute_or_aggregate, resource)
end)
2020-12-30 09:11:16 +13:00
if fields == [] do
[]
else
identifier = attribute_filter_field_type(resource, attribute_or_aggregate)
2021-04-28 05:17:16 +12:00
[
%Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
identifier: identifier,
fields: fields,
module: schema,
name: identifier |> to_string() |> Macro.camelize(),
__reference__: ref(__ENV__)
}
]
end
end
2020-12-30 09:11:16 +13:00
defp filter_fields(operator, type, array_type?, schema, attribute_or_aggregate, resource) do
expressable_types =
Enum.filter(operator.types(), fn
[:any, {:array, type}] when is_atom(type) ->
true
2020-12-30 09:11:16 +13:00
[{:array, inner_type}, :same] when is_atom(inner_type) and array_type? ->
true
:same ->
true
2020-12-30 09:11:16 +13:00
:any ->
true
2021-04-28 05:17:16 +12:00
[:any, type] when is_atom(type) ->
true
2020-12-30 09:11:16 +13:00
_ ->
false
end)
2020-12-30 09:11:16 +13:00
if Enum.any?(expressable_types, &(&1 == :same)) do
[
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: operator.name(),
module: schema,
name: to_string(operator.name()),
type: field_type(type, attribute_or_aggregate, resource, true),
__reference__: ref(__ENV__)
}
]
else
type =
case Enum.at(expressable_types, 0) do
[{:array, :any}, :same] ->
{:unwrap, type}
2020-12-30 09:11:16 +13:00
[_, {:array, :same}] ->
{:array, type}
2020-12-30 09:11:16 +13:00
[_, :same] ->
type
2020-12-30 09:11:16 +13:00
[_, :any] ->
Ash.Type.String
2021-04-28 05:17:16 +12:00
[_, type] when is_atom(type) ->
Ash.Type.get_type(type)
2021-04-28 05:17:16 +12:00
_ ->
nil
end
2021-04-28 05:17:16 +12:00
if type do
{type, attribute_or_aggregate} =
case type do
{:unwrap, type} ->
{:array, type} = type
constraints = Map.get(attribute_or_aggregate, :constraints) || []
2021-04-28 05:17:16 +12:00
{type,
%{attribute_or_aggregate | type: type, constraints: constraints[:items] || []}}
type ->
{type, attribute_or_aggregate}
2020-12-30 09:11:16 +13:00
end
if Ash.Type.embedded_type?(type) do
[]
else
attribute_or_aggregate = constraints_to_item_constraints(type, attribute_or_aggregate)
2020-12-30 09:11:16 +13:00
[
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: operator.name(),
module: schema,
name: to_string(operator.name()),
type: field_type(type, attribute_or_aggregate, resource, true),
__reference__: ref(__ENV__)
}
]
end
else
[]
end
2020-12-30 09:11:16 +13:00
end
rescue
_ ->
[]
end
defp restrict_for_lists(operators, {:array, _}) do
2021-04-28 05:17:16 +12:00
list_predicates = [Ash.Query.Operator.IsNil, Ash.Query.Operator.Has]
Enum.filter(operators, &(&1 in list_predicates))
end
defp restrict_for_lists(operators, _), do: operators
2020-12-31 12:00:36 +13:00
defp constraints_to_item_constraints(
{:array, _},
%Ash.Resource.Attribute{
constraints: constraints,
allow_nil?: allow_nil?
} = attribute
) do
%{
attribute
| constraints: [
items: constraints,
nil_items?: allow_nil? || Ash.Type.embedded_type?(attribute.type)
]
2020-12-31 12:00:36 +13:00
}
end
defp constraints_to_item_constraints(_, attribute_or_aggregate), do: attribute_or_aggregate
defp sort_input(resource, schema) do
if AshGraphql.Resource.Info.type(resource) && AshGraphql.Resource.Info.derive_sort?(resource) do
case sort_values(resource) do
[] ->
nil
2021-01-13 09:14:35 +13:00
_ ->
%Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
fields:
[
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: :order,
module: schema,
name: "order",
default_value: :asc,
type: :sort_order,
__reference__: ref(__ENV__)
},
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: :field,
module: schema,
name: "field",
type: %Absinthe.Blueprint.TypeReference.NonNull{
of_type: resource_sort_field_type(resource)
},
__reference__: ref(__ENV__)
}
] ++ calc_input_fields(resource, schema),
identifier: resource_sort_type(resource),
module: schema,
name: resource |> resource_sort_type() |> to_string() |> Macro.camelize(),
__reference__: ref(__ENV__)
}
end
else
nil
2021-01-13 09:14:35 +13:00
end
end
# sobelow_skip ["DOS.StringToAtom"]
defp calc_input_fields(resource, schema) do
calcs =
resource
|> Ash.Resource.Info.public_calculations()
|> Enum.reject(fn
%{type: {:array, _}} ->
true
calc ->
Ash.Type.embedded_type?(calc.type) || Enum.empty?(calc.arguments)
end)
field_names = AshGraphql.Resource.Info.field_names(resource)
Enum.map(calcs, fn calc ->
input_name = "#{field_names[calc.name] || calc.name}_input"
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: String.to_atom("#{calc.name}_input"),
module: schema,
name: input_name,
type: calc_input_type(calc.name, resource),
__reference__: ref(__ENV__)
}
end)
end
# sobelow_skip ["DOS.StringToAtom"]
defp calc_input_type(calc, resource) do
field_names = AshGraphql.Resource.Info.field_names(resource)
String.to_atom(
"#{AshGraphql.Resource.Info.type(resource)}_#{field_names[calc] || calc}_field_input"
)
end
2020-12-30 09:11:16 +13:00
defp filter_input(resource, schema) do
if AshGraphql.Resource.Info.derive_filter?(resource) do
case resource_filter_fields(resource, schema) do
[] ->
nil
2021-01-13 09:14:35 +13:00
fields ->
%Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
identifier: resource_filter_type(resource),
module: schema,
name: resource |> resource_filter_type() |> to_string() |> Macro.camelize(),
fields: fields,
__reference__: ref(__ENV__)
}
end
else
nil
2021-01-13 09:14:35 +13:00
end
2020-12-30 09:11:16 +13:00
end
# sobelow_skip ["DOS.StringToAtom"]
defp calculation_input(resource, schema) do
resource
|> Ash.Resource.Info.public_calculations()
|> Enum.flat_map(fn %{calculation: {module, _}} = calculation ->
Code.ensure_compiled(module)
filterable? = filterable?(calculation, resource)
field_type = calculation_type(calculation, resource)
arguments = calculation_args(calculation, resource, schema)
array_type? = match?({:array, _}, field_type)
filter_fields =
Ash.Filter.builtin_operators()
|> Enum.filter(& &1.predicate?)
|> restrict_for_lists(field_type)
|> Enum.flat_map(
&filter_fields(
&1,
calculation.type,
array_type?,
schema,
calculation,
resource
)
)
input = %Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
fields: arguments,
identifier: String.to_atom(to_string(calc_input_type(calculation.name, resource))),
module: schema,
name: Macro.camelize(to_string(calc_input_type(calculation.name, resource))),
__reference__: ref(__ENV__)
}
types =
if Enum.empty?(arguments) do
[]
else
[input]
end
if filterable? do
type_def =
if Enum.empty?(arguments) do
%Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
fields: filter_fields,
identifier: calculation_filter_field_type(resource, calculation),
module: schema,
name:
Macro.camelize(to_string(calculation_filter_field_type(resource, calculation))),
__reference__: ref(__ENV__)
}
else
filter_input_field = %Absinthe.Blueprint.Schema.FieldDefinition{
identifier: :input,
module: schema,
name: "input",
type: String.to_atom(to_string(calc_input_type(calculation.name, resource))),
__reference__: ref(__ENV__)
}
2021-07-02 14:59:16 +12:00
%Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
fields: [filter_input_field | filter_fields],
identifier: calculation_filter_field_type(resource, calculation),
module: schema,
name:
Macro.camelize(to_string(calculation_filter_field_type(resource, calculation))),
__reference__: ref(__ENV__)
}
end
2021-07-02 14:59:16 +12:00
[type_def | types]
else
types
2021-07-02 14:59:16 +12:00
end
end)
end
2020-12-30 09:11:16 +13:00
defp resource_filter_fields(resource, schema) do
boolean_filter_fields(resource, schema) ++
attribute_filter_fields(resource, schema) ++
relationship_filter_fields(resource, schema) ++
aggregate_filter_fields(resource, schema) ++ calculation_filter_fields(resource, schema)
2020-12-30 09:11:16 +13:00
end
defp attribute_filter_fields(resource, schema) do
field_names = AshGraphql.Resource.Info.field_names(resource)
2020-12-30 09:11:16 +13:00
resource
2021-02-19 04:16:00 +13:00
|> Ash.Resource.Info.public_attributes()
|> Enum.filter(&filterable?(&1, resource))
2020-12-30 09:11:16 +13:00
|> Enum.flat_map(fn attribute ->
[
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: attribute.name,
module: schema,
name: to_string(field_names[attribute.name] || attribute.name),
type: attribute_filter_field_type(resource, attribute),
__reference__: ref(__ENV__)
2020-12-30 09:11:16 +13:00
}
]
end)
end
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
resource
|> Ash.Resource.Info.public_aggregates()
|> Enum.filter(&filterable?(&1, resource))
|> Enum.flat_map(fn aggregate ->
[
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: aggregate.name,
module: schema,
name: to_string(field_names[aggregate.name] || aggregate.name),
type: attribute_filter_field_type(resource, aggregate),
__reference__: ref(__ENV__)
}
]
end)
else
[]
end
2020-12-30 09:11:16 +13:00
end
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
resource
|> Ash.Resource.Info.public_calculations()
|> Enum.filter(&filterable?(&1, resource))
|> Enum.map(fn calculation ->
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: calculation.name,
module: schema,
name: to_string(field_names[calculation.name] || calculation.name),
type: calculation_filter_field_type(resource, calculation),
__reference__: ref(__ENV__)
}
end)
else
[]
end
end
defp filterable?(%Ash.Resource.Aggregate{} = aggregate, resource) do
field_type =
with field when not is_nil(field) <- aggregate.field,
related when not is_nil(related) <-
Ash.Resource.Info.related(resource, aggregate.relationship_path),
attr when not is_nil(attr) <- Ash.Resource.Info.attribute(related, aggregate.field) do
attr.type
end
{:ok, type} = Aggregate.kind_to_type(aggregate.kind, field_type)
filterable?(%Ash.Resource.Attribute{name: aggregate.name, type: type}, resource)
end
defp filterable?(%{type: {:array, _}}, _), do: false
defp filterable?(%{filterable?: false}, _), do: false
defp filterable?(%{type: Ash.Type.Union}, _), do: false
defp filterable?(%Ash.Resource.Calculation{type: type, calculation: {module, _opts}}, _) do
!Ash.Type.embedded_type?(type) && function_exported?(module, :expression, 2)
end
defp filterable?(%{type: type}, _) do
!Ash.Type.embedded_type?(type)
end
defp filterable?(_, _), do: false
2020-12-30 09:11:16 +13:00
defp relationship_filter_fields(resource, schema) do
field_names = AshGraphql.Resource.Info.field_names(resource)
relationships = AshGraphql.Resource.Info.relationships(resource)
2020-12-30 09:11:16 +13:00
resource
2021-02-19 04:16:00 +13:00
|> Ash.Resource.Info.public_relationships()
2020-12-30 09:11:16 +13:00
|> Enum.filter(fn relationship ->
Resource in Spark.extensions(relationship.destination) && relationship.name in relationships
2020-12-30 09:11:16 +13:00
end)
|> Enum.map(fn relationship ->
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: relationship.name,
module: schema,
name: to_string(field_names[relationship.name] || relationship.name),
type: resource_filter_type(relationship.destination),
__reference__: ref(__ENV__)
2020-12-30 09:11:16 +13:00
}
end)
end
defp boolean_filter_fields(resource, schema) do
if Ash.DataLayer.can?(:boolean_filter, resource) do
[
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: :and,
module: schema,
name: "and",
type: %Absinthe.Blueprint.TypeReference.List{
of_type: %Absinthe.Blueprint.TypeReference.NonNull{
of_type: resource_filter_type(resource)
}
},
__reference__: ref(__ENV__)
2020-12-30 09:11:16 +13:00
},
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: :or,
module: schema,
name: "or",
__reference__: ref(__ENV__),
2020-12-30 09:11:16 +13:00
type: %Absinthe.Blueprint.TypeReference.List{
of_type: %Absinthe.Blueprint.TypeReference.NonNull{
of_type: resource_filter_type(resource)
}
}
}
]
else
[]
end
end
2020-11-12 17:01:48 +13:00
# sobelow_skip ["DOS.StringToAtom"]
2020-11-12 16:41:54 +13:00
defp resource_sort_field_type(resource) do
2022-08-31 13:08:16 +12:00
type = AshGraphql.Resource.Info.type(resource)
2020-11-12 16:41:54 +13:00
String.to_atom(to_string(type) <> "_sort_field")
end
def enum_definitions(resource, schema, env, only_auto? \\ false) do
resource = Ash.Type.NewType.subtype_of(resource)
2022-09-15 08:50:35 +12:00
if AshGraphql.Resource.Info.type(resource) do
atom_enums =
resource
|> get_auto_enums()
|> Enum.flat_map(fn attribute ->
constraints = Ash.Type.NewType.constraints(attribute.type, attribute.constraints)
2022-09-15 08:50:35 +12:00
type_name =
2023-03-14 12:13:35 +13:00
if constraints[:one_of] do
if Ash.Type.NewType.new_type?(attribute.type) do
cond do
function_exported?(attribute.type, :graphql_type, 0) ->
attribute.type.graphql_type()
function_exported?(attribute.type, :graphql_type, 1) ->
attribute.type.graphql_type(attribute.constraints)
true ->
atom_enum_type(resource, attribute.name)
end
else
atom_enum_type(resource, attribute.name)
end
end
additional_type_name =
2023-03-14 12:13:35 +13:00
if constraints[:one_of] && Ash.Type.NewType.new_type?(attribute.type) do
cond do
function_exported?(attribute.type, :graphql_input_type, 0) ->
attribute.type.graphql_input_type()
function_exported?(attribute.type, :graphql_input_type, 1) ->
attribute.type.graphql_input_type(attribute.constraints)
true ->
atom_enum_type(resource, attribute.name)
end
else
nil
end
[
type_name,
additional_type_name
]
|> Enum.filter(& &1)
|> Enum.map(fn type_name ->
%Absinthe.Blueprint.Schema.EnumTypeDefinition{
module: schema,
name: type_name |> to_string() |> Macro.camelize(),
values:
Enum.map(constraints[:one_of], fn value ->
%Absinthe.Blueprint.Schema.EnumValueDefinition{
module: schema,
identifier: value,
__reference__: AshGraphql.Resource.ref(env),
name: String.upcase(to_string(value)),
value: value
}
end),
identifier: type_name,
__reference__: ref(__ENV__)
}
end)
2022-09-15 08:50:35 +12:00
end)
if only_auto? || !AshGraphql.Resource.Info.derive_sort?(resource) do
2022-09-15 08:50:35 +12:00
atom_enums
else
sort_values = sort_values(resource)
2022-09-15 08:50:35 +12:00
sort_order = %Absinthe.Blueprint.Schema.EnumTypeDefinition{
2020-11-12 16:41:54 +13:00
module: schema,
2022-09-15 08:50:35 +12:00
name: resource |> resource_sort_field_type() |> to_string() |> Macro.camelize(),
identifier: resource_sort_field_type(resource),
__reference__: ref(__ENV__),
2020-11-12 16:41:54 +13:00
values:
Enum.map(sort_values, fn {sort_value_alias, sort_value} ->
2020-11-12 16:41:54 +13:00
%Absinthe.Blueprint.Schema.EnumValueDefinition{
module: schema,
identifier: sort_value_alias,
__reference__: AshGraphql.Resource.ref(env),
name: String.upcase(to_string(sort_value_alias)),
2022-09-15 08:50:35 +12:00
value: sort_value
2020-11-12 16:41:54 +13:00
}
2022-09-15 08:50:35 +12:00
end)
2020-11-12 16:41:54 +13:00
}
2022-09-15 08:50:35 +12:00
[sort_order | atom_enums]
end
else
2022-09-15 08:50:35 +12:00
[]
end
2020-08-14 09:39:59 +12:00
end
# sobelow_skip ["RCE.CodeModule", "DOS.BinToAtom", "DOS.StringToAtom"]
def union_definitions(resource, schema, env) do
if AshGraphql.Resource.Info.type(resource) do
resource
|> get_auto_unions()
|> Enum.flat_map(fn attribute ->
type_name =
if Ash.Type.NewType.new_type?(attribute.type) do
cond do
function_exported?(attribute.type, :graphql_type, 0) ->
attribute.type.graphql_type()
function_exported?(attribute.type, :graphql_type, 1) ->
attribute.type.graphql_type(attribute.constraints)
true ->
atom_enum_type(resource, attribute.name)
end
else
atom_enum_type(resource, attribute.name)
end
input_type_name =
if Ash.Type.NewType.new_type?(attribute.type) do
cond do
function_exported?(attribute.name, :graphql_input_type, 0) ->
attribute.type.graphql_input_type()
function_exported?(attribute.name, :graphql_input_type, 1) ->
attribute.type.graphql_input_type(attribute.constraints)
true ->
"#{atom_enum_type(resource, attribute.name)}_input"
end
else
"#{atom_enum_type(resource, attribute.name)}_input"
end
constraints = Ash.Type.NewType.constraints(attribute.type, attribute.constraints)
names_to_field_types =
Map.new(constraints[:types] || %{}, fn {name, config} ->
{name,
field_type(
config[:type],
%{attribute | name: String.to_atom("#{attribute.name}_#{name}")},
resource
)}
end)
func_name = :"resolve_gql_union_#{type_name}"
{func, _} =
Code.eval_quoted(
{:&, [],
[
{:/, [],
[
{{:., [], [resource, func_name]}, [], []},
2
]}
]},
[]
)
constraints = Ash.Type.NewType.constraints(attribute.type, attribute.constraints)
[
%Absinthe.Blueprint.Schema.UnionTypeDefinition{
module: schema,
name: type_name |> to_string() |> Macro.camelize(),
resolve_type: func,
types:
Enum.map(constraints[:types], fn {name, _} ->
%Absinthe.Blueprint.TypeReference.Name{
name: "#{type_name}_#{name}" |> Macro.camelize()
}
end),
identifier: type_name,
__reference__: ref(env)
},
%Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
module: schema,
name: input_type_name |> to_string() |> Macro.camelize(),
identifier: String.to_atom(input_type_name),
__reference__: ref(env),
fields:
Enum.map(constraints[:types], fn {name, config} ->
%Absinthe.Blueprint.Schema.InputValueDefinition{
name: name |> to_string(),
identifier: name,
__reference__: ref(env),
type:
field_type(
config[:type],
%{attribute | name: String.to_atom("#{attribute.name}_#{name}")},
resource,
true
)
}
end)
}
] ++
Enum.map(constraints[:types], fn {name, _} ->
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{
module: schema,
name: "#{type_name}_#{name}" |> Macro.camelize(),
identifier: :"#{type_name}_#{name}",
__reference__: ref(env),
fields: [
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: :value,
module: schema,
name: "value",
__reference__: ref(env),
type: %Absinthe.Blueprint.TypeReference.NonNull{
of_type: names_to_field_types[name]
}
}
]
}
end)
end)
else
[]
end
end
@doc false
def get_auto_enums(resource) do
resource
|> AshGraphql.all_attributes_and_arguments([], false)
|> Enum.map(&unnest/1)
|> Enum.filter(&(Ash.Type.NewType.subtype_of(&1.type) == Ash.Type.Atom))
2023-04-04 03:20:14 +12:00
|> Enum.uniq_by(& &1.name)
end
defp unnest(%{type: {:array, type}, constraints: constraints} = attribute) do
%{attribute | type: type, constraints: constraints[:items] || []}
end
defp unnest(other), do: other
@doc false
def get_auto_unions(resource) do
resource
|> AshGraphql.all_attributes_and_arguments()
|> Enum.map(fn attribute ->
unnest(attribute)
end)
|> Enum.filter(&(Ash.Type.NewType.subtype_of(&1.type) == Ash.Type.Union))
end
2021-01-13 09:14:35 +13:00
defp sort_values(resource) do
field_names = AshGraphql.Resource.Info.field_names(resource)
resource
|> Ash.Resource.Info.public_attributes()
|> Enum.concat(Ash.Resource.Info.public_calculations(resource))
|> Enum.concat(Ash.Resource.Info.public_aggregates(resource))
|> Enum.filter(&filterable?(&1, resource))
|> Enum.map(& &1.name)
|> Enum.uniq()
|> Enum.map(fn name ->
{field_names[name] || name, name}
end)
2021-01-13 09:14:35 +13:00
end
2022-10-21 10:57:37 +13:00
# sobelow_skip ["DOS.StringToAtom"]
defp relay_page(resource, schema) do
2022-08-31 13:08:16 +12:00
type = AshGraphql.Resource.Info.type(resource)
2020-08-14 09:39:59 +12:00
paginatable? =
resource
2021-02-19 04:16:00 +13:00
|> Ash.Resource.Info.actions()
|> Enum.any?(fn action ->
action.type == :read && action.pagination
end)
if paginatable? do
relay? =
resource
|> queries()
|> Enum.any?(& &1.relay?)
countable? =
resource
|> queries()
|> Enum.any?(fn query ->
action = Ash.Resource.Info.action(resource, query.action)
query.relay? && action.pagination && action.pagination.countable
end)
if relay? do
[
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{
description: "#{inspect(type)} edge",
fields: [
%Absinthe.Blueprint.Schema.FieldDefinition{
description: "Cursor",
identifier: :cursor,
module: schema,
name: "cursor",
__reference__: ref(__ENV__),
type: %Absinthe.Blueprint.TypeReference.NonNull{
of_type: :string
}
},
%Absinthe.Blueprint.Schema.FieldDefinition{
description: "#{inspect(type)} node",
identifier: :node,
module: schema,
name: "node",
__reference__: ref(__ENV__),
type: type
}
],
identifier: String.to_atom("#{type}_edge"),
module: schema,
name: Macro.camelize("#{type}_edge"),
__reference__: ref(__ENV__)
},
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{
description: "#{inspect(type)} connection",
fields:
[
%Absinthe.Blueprint.Schema.FieldDefinition{
description: "Page information",
identifier: :page_info,
module: schema,
name: "page_info",
__reference__: ref(__ENV__),
type: %Absinthe.Blueprint.TypeReference.NonNull{
of_type: :page_info
}
},
%Absinthe.Blueprint.Schema.FieldDefinition{
description: "#{inspect(type)} edges",
identifier: :edges,
module: schema,
name: "edges",
__reference__: ref(__ENV__),
type: %Absinthe.Blueprint.TypeReference.List{
of_type: String.to_atom("#{type}_edge")
}
}
]
|> add_count_to_page(schema, countable?),
identifier: String.to_atom("#{type}_connection"),
module: schema,
name: Macro.camelize("#{type}_connection"),
__reference__: ref(__ENV__)
}
]
end
end
end
defp add_count_to_page(fields, schema, true) do
[
%Absinthe.Blueprint.Schema.FieldDefinition{
description: "Total count on all pages",
identifier: :count,
module: schema,
name: "count",
__reference__: ref(__ENV__),
type: :integer
}
| fields
]
end
defp add_count_to_page(fields, _, _), do: fields
# sobelow_skip ["DOS.StringToAtom"]
defp page_of(resource, schema) do
type = AshGraphql.Resource.Info.type(resource)
paginatable? =
resource
|> queries()
|> Enum.any?(fn query ->
action = Ash.Resource.Info.action(resource, query.action)
action.type == :read && action.pagination
end)
countable? =
resource
|> queries()
|> Enum.any?(fn query ->
action = Ash.Resource.Info.action(resource, query.action)
action.pagination && action.pagination.offset? && action.pagination.countable
end)
if paginatable? do
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{
description: "A page of #{inspect(type)}",
fields:
[
%Absinthe.Blueprint.Schema.FieldDefinition{
description: "The records contained in the page",
identifier: :results,
module: schema,
name: "results",
__reference__: ref(__ENV__),
type: %Absinthe.Blueprint.TypeReference.List{
of_type: %Absinthe.Blueprint.TypeReference.NonNull{
of_type: type
}
}
},
%Absinthe.Blueprint.Schema.FieldDefinition{
description: "Wether or not there is a next page",
identifier: :more?,
module: schema,
name: "has_next_page",
__reference__: ref(__ENV__),
type: %Absinthe.Blueprint.TypeReference.NonNull{
of_type: :boolean
}
}
]
|> add_count_to_page(schema, countable?),
identifier: String.to_atom("page_of_#{type}"),
module: schema,
name: Macro.camelize("page_of_#{type}"),
__reference__: ref(__ENV__)
}
end
end
# sobelow_skip ["DOS.StringToAtom"]
defp keyset_page_of(resource, schema) do
type = AshGraphql.Resource.Info.type(resource)
paginatable? =
resource
|> Ash.Resource.Info.actions()
|> Enum.any?(fn action ->
action.type == :read && action.pagination
end)
countable? =
resource
|> queries()
|> Enum.any?(fn query ->
action = Ash.Resource.Info.action(resource, query.action)
action.pagination && action.pagination.keyset? && action.pagination.countable
end)
if paginatable? do
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{
description: "A keyset page of #{inspect(type)}",
fields:
[
%Absinthe.Blueprint.Schema.FieldDefinition{
description: "The records contained in the page",
identifier: :results,
module: schema,
name: "results",
__reference__: ref(__ENV__),
type: %Absinthe.Blueprint.TypeReference.List{
of_type: %Absinthe.Blueprint.TypeReference.NonNull{
of_type: type
}
}
},
%Absinthe.Blueprint.Schema.FieldDefinition{
description: "The first keyset in the results",
identifier: :start_keyset,
module: schema,
name: "start_keyset",
type: :string,
__reference__: ref(__ENV__)
},
%Absinthe.Blueprint.Schema.FieldDefinition{
description: "The last keyset in the results",
identifier: :end_keyset,
module: schema,
name: "end_keyset",
type: :string,
__reference__: ref(__ENV__)
}
]
|> add_count_to_page(schema, countable?),
identifier: String.to_atom("keyset_page_of_#{type}"),
module: schema,
name: Macro.camelize("keyset_page_of_#{type}"),
__reference__: ref(__ENV__)
}
end
2020-08-14 09:39:59 +12:00
end
def is_node_type(type) do
type.identifier == :node
end
def query_type_definitions(resource, api, schema) do
resource_type = AshGraphql.Resource.Info.type(resource)
resource
|> AshGraphql.Resource.Info.queries()
|> Enum.filter(&(&1.type_name && &1.type_name != resource_type))
|> Enum.map(fn query ->
relay? = Map.get(query, :relay?)
interfaces =
if relay? do
[:node]
else
[]
end
is_type_of =
if relay? do
&AshGraphql.Resource.is_node_type/1
else
nil
end
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{
description: Ash.Resource.Info.description(resource),
interfaces: interfaces,
fields: fields(resource, api, schema, query),
identifier: query.type_name,
module: schema,
name: Macro.camelize(to_string(query.type_name)),
__reference__: ref(__ENV__),
is_type_of: is_type_of
}
end)
end
2021-01-13 09:14:35 +13:00
def type_definition(resource, api, schema) do
actual_resource = Ash.Type.NewType.subtype_of(resource)
if generate_object?(resource) do
2022-08-31 13:08:16 +12:00
type = AshGraphql.Resource.Info.type(resource)
2020-08-14 09:39:59 +12:00
resource = actual_resource
relay? =
resource
|> queries()
|> Enum.any?(& &1.relay?)
interfaces =
if relay? do
[:node]
else
[]
end
is_type_of =
if relay? do
&AshGraphql.Resource.is_node_type/1
else
nil
end
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{
description: Ash.Resource.Info.description(resource),
interfaces: interfaces,
fields: fields(resource, api, schema),
identifier: type,
module: schema,
name: Macro.camelize(to_string(type)),
__reference__: ref(__ENV__),
is_type_of: is_type_of
}
end
2020-08-14 09:39:59 +12:00
end
defp fields(resource, api, schema, query \\ nil) do
attributes(resource, schema) ++
metadata(query, resource, schema) ++
relationships(resource, api, schema) ++
aggregates(resource, schema) ++
calculations(resource, api, schema) ++
keyset(resource, schema)
end
defp metadata(nil, _resource, _schema) do
[]
end
defp metadata(query, resource, schema) do
action = Ash.Resource.Info.action(resource, query.action)
show_metadata = query.show_metadata || Enum.map(Map.get(action, :metadata, []), & &1.name)
action.metadata
|> Enum.filter(&(&1.name in show_metadata))
|> Enum.map(fn metadata ->
field_type =
case query.metadata_types[metadata.name] do
nil ->
metadata.type
|> field_type(metadata, resource)
|> maybe_wrap_non_null(not metadata.allow_nil?)
type ->
unwrap_literal_type(type)
end
%Absinthe.Blueprint.Schema.FieldDefinition{
description: metadata.description,
identifier: metadata.name,
module: schema,
name: to_string(query.metadata_names[metadata.name] || metadata.name),
type: field_type,
__reference__: ref(__ENV__)
}
end)
end
defp keyset(resource, schema) do
case AshGraphql.Resource.Info.keyset_field(resource) do
nil ->
[]
field ->
[
%Absinthe.Blueprint.Schema.FieldDefinition{
description: "The pagination #{field}.",
identifier: field,
module: schema,
middleware: [
{{AshGraphql.Graphql.Resolver, :resolve_keyset}, field}
],
name: to_string(field),
type: :string,
__reference__: ref(__ENV__)
}
]
end
2020-08-14 09:39:59 +12:00
end
defp attributes(resource, schema) do
attribute_names = AshGraphql.Resource.Info.field_names(resource)
attributes =
if AshGraphql.Resource.Info.encode_primary_key?(resource) do
resource
|> Ash.Resource.Info.public_attributes()
|> Enum.reject(& &1.primary_key?)
else
Ash.Resource.Info.public_attributes(resource)
end
attributes =
attributes
2021-01-13 09:14:35 +13:00
|> Enum.map(fn attribute ->
2020-11-12 16:41:54 +13:00
field_type =
attribute.type
|> field_type(attribute, resource)
|> maybe_wrap_non_null(not attribute.allow_nil?)
2020-11-12 16:41:54 +13:00
name = attribute_names[attribute.name] || attribute.name
2020-08-14 09:39:59 +12:00
%Absinthe.Blueprint.Schema.FieldDefinition{
description: attribute.description,
identifier: attribute.name,
module: schema,
name: to_string(name),
type: field_type,
__reference__: ref(__ENV__)
2020-08-14 09:39:59 +12:00
}
2021-01-13 09:14:35 +13:00
end)
if AshGraphql.Resource.Info.encode_primary_key?(resource) do
encoded_primary_key_attributes(resource, schema) ++
attributes
else
attributes
end
end
defp encoded_primary_key_attributes(resource, schema) do
case Ash.Resource.Info.primary_key(resource) do
[field] ->
attribute = Ash.Resource.Info.attribute(resource, field)
if attribute.private? do
[]
else
[
%Absinthe.Blueprint.Schema.FieldDefinition{
description: attribute.description,
identifier: :id,
module: schema,
name: "id",
type: %Absinthe.Blueprint.TypeReference.NonNull{of_type: :id},
middleware: [
{{AshGraphql.Graphql.Resolver, :resolve_id}, {resource, field}}
],
__reference__: ref(__ENV__)
}
]
end
fields ->
[
%Absinthe.Blueprint.Schema.FieldDefinition{
description: "A unique identifier",
identifier: :id,
module: schema,
name: "id",
type: %Absinthe.Blueprint.TypeReference.NonNull{of_type: :id},
middleware: [
{{AshGraphql.Graphql.Resolver, :resolve_composite_id}, {resource, fields}}
],
__reference__: ref(__ENV__)
}
]
end
end
2021-01-13 09:14:35 +13:00
defp pkey_fields(resource, schema, require?) do
encode? = AshGraphql.Resource.Info.encode_primary_key?(resource)
case Ash.Resource.Info.primary_key(resource) do
[field] when encode? ->
attribute = Ash.Resource.Info.attribute(resource, field)
field_type = maybe_wrap_non_null(:id, require?)
2023-02-25 04:00:39 +13:00
[
%Absinthe.Blueprint.Schema.FieldDefinition{
description: attribute.description,
identifier: field,
module: schema,
name: to_string(attribute.name),
type: field_type,
__reference__: ref(__ENV__)
}
]
fields ->
for field <- fields do
attribute = Ash.Resource.Info.attribute(resource, field)
field_type =
attribute.type
|> field_type(attribute, resource)
|> maybe_wrap_non_null(require?)
%Absinthe.Blueprint.Schema.FieldDefinition{
description: attribute.description,
identifier: attribute.name,
module: schema,
name: to_string(attribute.name),
type: field_type,
__reference__: ref(__ENV__)
}
end
end
2020-08-14 09:39:59 +12:00
end
defp argument_required?(%{allow_nil?: true}), do: false
defp argument_required?(%{default: default}) when not is_nil(default), do: false
defp argument_required?(_), do: true
2020-08-15 02:20:47 +12:00
# sobelow_skip ["DOS.StringToAtom"]
defp relationships(resource, api, schema) do
field_names = AshGraphql.Resource.Info.field_names(resource)
relationships = AshGraphql.Resource.Info.relationships(resource)
2020-08-14 09:39:59 +12:00
resource
2021-02-19 04:16:00 +13:00
|> Ash.Resource.Info.public_relationships()
2020-08-14 09:39:59 +12:00
|> Enum.filter(fn relationship ->
Resource in Spark.extensions(relationship.destination) && relationship.name in relationships &&
AshGraphql.Resource.Info.type(relationship.destination)
2020-08-14 09:39:59 +12:00
end)
|> Enum.map(fn
%{cardinality: :one} = relationship ->
name = field_names[relationship.name] || relationship.name
2020-11-12 16:53:53 +13:00
type =
relationship.destination
2022-08-31 13:08:16 +12:00
|> AshGraphql.Resource.Info.type()
2022-09-15 04:46:12 +12:00
|> maybe_wrap_non_null(!relationship.allow_nil?)
2020-08-14 09:39:59 +12:00
read_action =
if relationship.read_action do
Ash.Resource.Info.action(relationship.destination, relationship.read_action)
else
Ash.Resource.Info.primary_action!(relationship.destination, :read)
end
2020-08-14 09:39:59 +12:00
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: relationship.name,
module: schema,
name: to_string(name),
arguments: args(:one_related, relationship.destination, read_action, schema),
2020-08-14 09:39:59 +12:00
middleware: [
2020-09-24 12:54:57 +12:00
{{AshGraphql.Graphql.Resolver, :resolve_assoc}, {api, relationship}}
2020-08-14 09:39:59 +12:00
],
type: type,
__reference__: ref(__ENV__)
2020-08-14 09:39:59 +12:00
}
%{cardinality: :many} = relationship ->
name = field_names[relationship.name] || relationship.name
read_action =
if relationship.read_action do
Ash.Resource.Info.action(relationship.destination, relationship.read_action)
else
Ash.Resource.Info.primary_action!(relationship.destination, :read)
end
2022-08-31 13:08:16 +12:00
type = AshGraphql.Resource.Info.type(relationship.destination)
query_type = %Absinthe.Blueprint.TypeReference.NonNull{
of_type: %Absinthe.Blueprint.TypeReference.List{
of_type: %Absinthe.Blueprint.TypeReference.NonNull{
of_type: type
}
}
}
2020-08-14 09:39:59 +12:00
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: relationship.name,
module: schema,
name: to_string(name),
complexity: {AshGraphql.Graphql.Resolver, :query_complexity},
2020-08-14 09:39:59 +12:00
middleware: [
2020-09-24 12:54:57 +12:00
{{AshGraphql.Graphql.Resolver, :resolve_assoc}, {api, relationship}}
2020-08-14 09:39:59 +12:00
],
arguments: args(:list_related, relationship.destination, read_action, schema),
type: query_type,
__reference__: ref(__ENV__)
2020-08-14 09:39:59 +12:00
}
end)
end
defp aggregates(resource, schema) do
field_names = AshGraphql.Resource.Info.field_names(resource)
2020-08-14 09:39:59 +12:00
resource
2021-02-19 04:16:00 +13:00
|> Ash.Resource.Info.public_aggregates()
2020-08-14 09:39:59 +12:00
|> Enum.map(fn aggregate ->
name = field_names[aggregate.name] || aggregate.name
2020-12-30 09:11:16 +13:00
field_type =
2021-04-27 08:48:26 +12:00
with field when not is_nil(field) <- aggregate.field,
related when not is_nil(related) <-
Ash.Resource.Info.related(resource, aggregate.relationship_path),
attr when not is_nil(attr) <- Ash.Resource.Info.attribute(related, aggregate.field) do
attr.type
2020-12-30 09:11:16 +13:00
end
{:ok, type} = Aggregate.kind_to_type(aggregate.kind, field_type)
2020-08-14 09:39:59 +12:00
type =
if is_nil(Ash.Query.Aggregate.default_value(aggregate.kind)) do
field_type(type, aggregate, resource)
else
%Absinthe.Blueprint.TypeReference.NonNull{
of_type: field_type(type, aggregate, resource)
}
end
2020-08-14 09:39:59 +12:00
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: aggregate.name,
module: schema,
name: to_string(name),
type: type,
__reference__: ref(__ENV__)
}
end)
end
defp calculations(resource, api, schema) do
field_names = AshGraphql.Resource.Info.field_names(resource)
resource
2021-02-19 04:16:00 +13:00
|> Ash.Resource.Info.public_calculations()
|> Enum.map(fn calculation ->
name = field_names[calculation.name] || calculation.name
field_type = calculation_type(calculation, resource)
arguments = calculation_args(calculation, resource, schema)
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: calculation.name,
module: schema,
arguments: arguments,
complexity: 2,
middleware: [
{{AshGraphql.Graphql.Resolver, :resolve_calculation}, {api, resource, calculation}}
],
name: to_string(name),
type: field_type,
__reference__: ref(__ENV__)
2020-08-14 09:39:59 +12:00
}
end)
end
defp calculation_type(calculation, resource) do
calculation.type
|> Ash.Type.get_type()
|> field_type(calculation, resource)
|> maybe_wrap_non_null(not calculation.allow_nil?)
end
defp calculation_args(calculation, resource, schema) do
Enum.map(calculation.arguments, fn argument ->
type =
argument.type
|> field_type(argument, resource, true)
|> maybe_wrap_non_null(argument_required?(argument))
%Absinthe.Blueprint.Schema.FieldDefinition{
identifier: argument.name,
module: schema,
name: to_string(argument.name),
type: type,
__reference__: ref(__ENV__)
}
end)
end
def field_type(type, field, resource, input? \\ false) do
case field do
%Ash.Resource.Attribute{name: name} ->
override =
if input? do
AshGraphql.Resource.Info.attribute_input_types(resource)[name]
else
AshGraphql.Resource.Info.attribute_types(resource)[name]
end
if override do
unwrap_literal_type(override)
else
do_field_type(type, field, resource, input?)
end
_ ->
do_field_type(type, field, resource, input?)
end
end
defp do_field_type(type, field, resource, input?)
2021-01-13 09:14:35 +13:00
defp do_field_type(
{:array, type},
%Ash.Resource.Aggregate{kind: :list} = aggregate,
resource,
input?
) do
with related when not is_nil(related) <-
Ash.Resource.Info.related(resource, aggregate.relationship_path),
attr when not is_nil(related) <- Ash.Resource.Info.attribute(related, aggregate.field) do
if attr.allow_nil? do
%Absinthe.Blueprint.TypeReference.List{
of_type: do_field_type(type, aggregate, resource, input?)
}
else
%Absinthe.Blueprint.TypeReference.List{
of_type: %Absinthe.Blueprint.TypeReference.NonNull{
of_type: do_field_type(type, aggregate, resource, input?)
}
}
end
end
end
defp do_field_type({:array, type}, %Ash.Resource.Aggregate{} = aggregate, resource, input?) do
2020-12-30 09:11:16 +13:00
%Absinthe.Blueprint.TypeReference.List{
of_type: do_field_type(type, aggregate, resource, input?)
2020-12-30 09:11:16 +13:00
}
end
defp do_field_type({:array, type}, nil, resource, input?) do
field_type = do_field_type(type, nil, resource, input?)
%Absinthe.Blueprint.TypeReference.List{
of_type: field_type
}
end
defp do_field_type({:array, type}, attribute, resource, input?) do
2020-12-30 09:11:16 +13:00
new_constraints = attribute.constraints[:items] || []
new_attribute = %{attribute | constraints: new_constraints, type: type}
2020-08-14 09:39:59 +12:00
field_type =
type
|> do_field_type(new_attribute, resource, input?)
|> maybe_wrap_non_null(
!attribute.constraints[:nil_items?] || Ash.Type.embedded_type?(attribute.type)
)
%Absinthe.Blueprint.TypeReference.List{
of_type: field_type
}
end
2021-01-13 09:14:35 +13:00
# sobelow_skip ["DOS.BinToAtom"]
defp do_field_type(type, attribute, resource, input?) do
type = Ash.Type.get_type(type)
if Ash.Type.builtin?(type) do
get_specific_field_type(type, attribute, resource, input?)
else
if Spark.Dsl.is?(type, Ash.Resource) && !Ash.Type.embedded_type?(type) do
if input? do
raise "Cannot construct an input type for #{inspect(type)}"
2021-01-13 09:14:35 +13:00
end
AshGraphql.Resource.Info.type(type)
else
if Ash.Type.embedded_type?(type) do
if input? && type(type) do
:"#{AshGraphql.Resource.Info.type(resource)}_#{attribute.name}_input"
else
case type(type) do
nil ->
Application.get_env(:ash_graphql, :json_type) || :json_string
type ->
type
end
end
else
if Spark.implements_behaviour?(type, Ash.Type.Enum) do
cond do
function_exported?(type, :graphql_type, 0) ->
type.graphql_type()
function_exported?(type, :graphql_type, 1) ->
type.graphql_type(attribute.constraints)
true ->
:string
end
else
function =
if input? do
:graphql_input_type
else
:graphql_type
end
cond do
function_exported?(type, function, 1) ->
apply(type, function, [Map.get(attribute, :constraints)])
function_exported?(type, function, 0) ->
apply(type, function, [])
true ->
if Ash.Type.NewType.new_type?(type) do
do_field_type(
type.subtype_of(),
%{
attribute
| type: type.subtype_of(),
constraints:
type.type_constraints(
attribute.constraints,
type.subtype_constraints()
)
},
resource,
input?
)
else
raise """
Could not determine graphql type for #{inspect(type)}, please define: #{function}/1!
"""
end
end
end
end
2021-01-13 09:14:35 +13:00
end
end
end
defp get_specific_field_type(
Ash.Type.Atom,
%Ash.Resource.Attribute{constraints: constraints, name: name},
resource,
_input?
)
when not is_nil(resource) do
if is_list(constraints[:one_of]) do
atom_enum_type(resource, name)
else
:string
end
end
# sobelow_skip ["DOS.BinToAtom"]
defp get_specific_field_type(
Ash.Type.Union,
%Ash.Resource.Attribute{name: name},
resource,
input?
)
when not is_nil(resource) do
# same logic for naming a union currently
base_type_name = atom_enum_type(resource, name)
if input? do
:"#{base_type_name}_input"
else
base_type_name
end
end
defp get_specific_field_type(Ash.Type.Boolean, _, _, _), do: :boolean
defp get_specific_field_type(Ash.Type.Atom, _, _, _) do
:string
end
defp get_specific_field_type(Ash.Type.CiString, _, _, _), do: :string
defp get_specific_field_type(Ash.Type.Date, _, _, _), do: :date
defp get_specific_field_type(Ash.Type.Decimal, _, _, _), do: :decimal
defp get_specific_field_type(Ash.Type.Integer, _, _, _), do: :integer
defp get_specific_field_type(Ash.Type.DurationName, _, _, _), do: :duration_name
defp get_specific_field_type(Ash.Type.Map, _, _, _),
do: Application.get_env(:ash_graphql, :json_type) || :json_string
defp get_specific_field_type(Ash.Type.String, _, _, _), do: :string
defp get_specific_field_type(Ash.Type.Term, _, _, _), do: :string
defp get_specific_field_type(Ash.Type.UtcDatetime, _, _, _),
do: Application.get_env(:ash, :utc_datetime_type) || raise_datetime_error()
defp get_specific_field_type(Ash.Type.UtcDatetimeUsec, _, _, _),
do: Application.get_env(:ash, :utc_datetime_type) || raise_datetime_error()
2023-03-04 11:54:45 +13:00
defp get_specific_field_type(Ash.Type.NaiveDatetime, _, _, _), do: :naive_datetime
defp get_specific_field_type(Ash.Type.Time, _, _, _), do: :time
defp get_specific_field_type(Ash.Type.UUID, _, _, _), do: :id
defp get_specific_field_type(Ash.Type.Float, _, _, _), do: :float
defp get_specific_field_type(type, attribute, resource, _) do
raise """
Could not determine graphql field type for #{inspect(type)} on #{inspect(resource)}.#{attribute.name}
If this is a custom type, you can add `def graphql_type/1` to your type to define the graphql type.
If this is not your type, you will need to use `types` or `attribute_types` or `attribute_input_types`
to configure the type for any field using this type. If this is an `Ash.Type.NewType`, you may need to define
`graphql_type` and `graphql_input_type`s for it.
"""
end
defp raise_datetime_error do
raise """
No type configured for utc_datetimes!
The existing default of using `:naive_datetime` for `:utc_datetime` and `:utc_datetime_usec` is being deprecated.
To prevent accidental API breakages, we are requiring that you configure your selected type for these, via
# This was the previous default, so use this if you want to ensure no unintended
# change in your API, although switching to `:datetime` eventually is suggested.
config :ash, :utc_datetime_type, :naive_datetime
or
config :ash, :utc_datetime_type, :datetime
When the 1.0 version of ash_graphql is released, the default will be changed to `:datetime`, and this error message will
no longer be shown (but any configuration set will be retained indefinitely).
"""
end
# sobelow_skip ["DOS.StringToAtom"]
@doc false
def atom_enum_type(resource, attribute_name) do
field_names = AshGraphql.Resource.Info.field_names(resource)
resource
2022-08-31 13:08:16 +12:00
|> AshGraphql.Resource.Info.type()
|> to_string()
|> Kernel.<>("_")
|> Kernel.<>(to_string(field_names[attribute_name] || attribute_name))
|> String.to_atom()
2020-08-14 09:39:59 +12:00
end
end