mirror of
https://github.com/ash-project/ash_graphql.git
synced 2024-09-20 13:24:14 +12:00
4549 lines
137 KiB
Elixir
4549 lines
137 KiB
Elixir
defmodule AshGraphql.Resource do
|
|
alias Ash.Changeset.ManagedRelationshipHelpers
|
|
alias Ash.Query.Aggregate
|
|
alias AshGraphql.Resource
|
|
alias AshGraphql.Resource.{ManagedRelationship, Mutation, Query, Subscription}
|
|
|
|
@get %Spark.Dsl.Entity{
|
|
name: :get,
|
|
args: [:name, :action],
|
|
describe: "A query to fetch a record by primary key",
|
|
examples: [
|
|
"get :get_post, :read"
|
|
],
|
|
schema: Query.get_schema(),
|
|
target: Query,
|
|
auto_set_fields: [
|
|
type: :get
|
|
]
|
|
}
|
|
|
|
@read_one %Spark.Dsl.Entity{
|
|
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
|
|
]
|
|
}
|
|
|
|
@list %Spark.Dsl.Entity{
|
|
name: :list,
|
|
schema: Query.list_schema(),
|
|
args: [:name, :action],
|
|
describe: "A query to fetch a list of records",
|
|
examples: [
|
|
"list :list_posts, :read",
|
|
"list :list_posts_paginated, :read, relay?: true"
|
|
],
|
|
target: Query,
|
|
auto_set_fields: [
|
|
type: :list
|
|
]
|
|
}
|
|
|
|
@action_schema [
|
|
name: [
|
|
type: :atom,
|
|
doc: "The name to use for the query.",
|
|
default: :get
|
|
],
|
|
action: [
|
|
type: :atom,
|
|
doc: "The action to use for the query.",
|
|
required: true
|
|
],
|
|
description: [
|
|
type: :string,
|
|
doc:
|
|
"The description that gets shown in the Graphql schema. If not provided, the action description will be used."
|
|
],
|
|
hide_inputs: [
|
|
type: {:list, :atom},
|
|
doc: "Inputs to hide in the mutation/query",
|
|
default: []
|
|
],
|
|
relay_id_translations: [
|
|
type: :keyword_list,
|
|
doc: """
|
|
A keyword list indicating arguments or attributes that have to be translated from global Relay IDs to internal IDs. See the [Relay guide](/documentation/topics/relay.md#translating-relay-global-ids-passed-as-arguments) for more.
|
|
""",
|
|
default: []
|
|
]
|
|
]
|
|
|
|
defmodule Action do
|
|
@moduledoc "Represents a configured generic action"
|
|
defstruct [
|
|
:type,
|
|
:name,
|
|
:action,
|
|
:resource,
|
|
:description,
|
|
:relay_id_translations,
|
|
hide_inputs: []
|
|
]
|
|
end
|
|
|
|
@action %Spark.Dsl.Entity{
|
|
name: :action,
|
|
schema: @action_schema,
|
|
args: [:name, :action],
|
|
describe: "Runs a generic action",
|
|
examples: [
|
|
"action :check_status, :check_status"
|
|
],
|
|
target: Action,
|
|
auto_set_fields: [
|
|
type: :action
|
|
]
|
|
}
|
|
|
|
@create %Spark.Dsl.Entity{
|
|
name: :create,
|
|
schema: Mutation.create_schema(),
|
|
args: [:name, :action],
|
|
describe: "A mutation to create a record",
|
|
examples: [
|
|
"create :create_post, :create"
|
|
],
|
|
target: Mutation,
|
|
auto_set_fields: [
|
|
type: :create
|
|
]
|
|
}
|
|
|
|
@update %Spark.Dsl.Entity{
|
|
name: :update,
|
|
schema: Mutation.update_schema(),
|
|
args: [:name, :action],
|
|
describe: "A mutation to update a record",
|
|
examples: [
|
|
"update :update_post, :update"
|
|
],
|
|
target: Mutation,
|
|
auto_set_fields: [
|
|
type: :update
|
|
]
|
|
}
|
|
|
|
@destroy %Spark.Dsl.Entity{
|
|
name: :destroy,
|
|
schema: Mutation.destroy_schema(),
|
|
args: [:name, :action],
|
|
describe: "A mutation to destroy a record",
|
|
examples: [
|
|
"destroy :destroy_post, :destroy"
|
|
],
|
|
target: Mutation,
|
|
auto_set_fields: [
|
|
type: :destroy
|
|
]
|
|
}
|
|
|
|
@queries %Spark.Dsl.Section{
|
|
name: :queries,
|
|
describe: """
|
|
Queries (read actions) to expose for the resource.
|
|
""",
|
|
examples: [
|
|
"""
|
|
queries do
|
|
get :get_post, :read
|
|
read_one :current_user, :current_user
|
|
list :list_posts, :read
|
|
end
|
|
"""
|
|
],
|
|
entities: [
|
|
@get,
|
|
@read_one,
|
|
@list,
|
|
@action
|
|
]
|
|
}
|
|
|
|
def queries, do: [@get, @read_one, @list, @action]
|
|
|
|
@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
|
|
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
|
|
|
|
Since managed relationships can ultimately call multiple actions, there is the possibility
|
|
of field type conflicts. Use the `types` option to determine the type of fields and remove the conflict warnings.
|
|
|
|
For `non_null` use `{:non_null, type}`, and for a list, use `{:array, type}`, for example:
|
|
|
|
`{:non_null, {:array, {:non_null, :string}}}` for a non null list of non null strings.
|
|
|
|
To *remove* a key from the input object, simply pass `nil` as the type.
|
|
"""
|
|
}
|
|
|
|
@managed_relationships %Spark.Dsl.Section{
|
|
name: :managed_relationships,
|
|
describe: """
|
|
Generates input objects for `manage_relationship` arguments on resource actions.
|
|
""",
|
|
examples: [
|
|
"""
|
|
managed_relationships do
|
|
manage_relationship :create_post, :comments
|
|
end
|
|
"""
|
|
],
|
|
schema: [
|
|
auto?: [
|
|
type: :boolean,
|
|
doc:
|
|
"Automatically derive types for all arguments that have a `manage_relationship` call change.",
|
|
default: true
|
|
]
|
|
],
|
|
entities: [
|
|
@managed_relationship
|
|
]
|
|
}
|
|
|
|
@mutations %Spark.Dsl.Section{
|
|
name: :mutations,
|
|
describe: """
|
|
Mutations (create/update/destroy actions) to expose for the resource.
|
|
""",
|
|
examples: [
|
|
"""
|
|
mutations do
|
|
create :create_post, :create
|
|
update :update_post, :update
|
|
destroy :destroy_post, :destroy
|
|
end
|
|
"""
|
|
],
|
|
entities: [
|
|
@create,
|
|
@update,
|
|
@destroy,
|
|
@action
|
|
]
|
|
}
|
|
|
|
def mutations, do: [@create, @update, @destroy, @action]
|
|
|
|
@subscribe %Spark.Dsl.Entity{
|
|
name: :subscribe,
|
|
args: [:name],
|
|
describe: "A query to fetch a record by primary key",
|
|
examples: [
|
|
"get :get_post, :read"
|
|
],
|
|
schema: Subscription.schema(),
|
|
target: Subscription
|
|
}
|
|
|
|
@subscriptions %Spark.Dsl.Section{
|
|
name: :subscriptions,
|
|
schema: [
|
|
pubsub: [
|
|
type: :module,
|
|
required: true,
|
|
doc: "The pubsub module to use for the subscription"
|
|
]
|
|
],
|
|
describe: """
|
|
Subscriptions (notifications) to expose for the resource.
|
|
""",
|
|
examples: [
|
|
"""
|
|
subscriptions do
|
|
subscribe :subscription_name, fn notifications -> ... end
|
|
end
|
|
"""
|
|
],
|
|
entities: [
|
|
@subscribe
|
|
]
|
|
}
|
|
|
|
@graphql %Spark.Dsl.Section{
|
|
name: :graphql,
|
|
imports: [AshGraphql.Resource.Helpers],
|
|
describe: """
|
|
Configuration for a given resource in graphql
|
|
""",
|
|
examples: [
|
|
"""
|
|
graphql do
|
|
type :post
|
|
|
|
queries do
|
|
get :get_post, :read
|
|
list :list_posts, :read
|
|
end
|
|
|
|
mutations do
|
|
create :create_post, :create
|
|
update :update_post, :update
|
|
destroy :destroy_post, :destroy
|
|
end
|
|
end
|
|
"""
|
|
],
|
|
schema: [
|
|
type: [
|
|
type: :atom,
|
|
doc:
|
|
"The type to use for this entity in the graphql schema. If the resource doesn't have a type, it also needs to have `generate_object? false` and can only expose generic action queries."
|
|
],
|
|
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."
|
|
],
|
|
paginate_relationship_with: [
|
|
type: :keyword_list,
|
|
default: [],
|
|
doc:
|
|
"A keyword list indicating which kind of pagination should be used for each `has_many` and `many_to_many` relationships, e.g. `related_things: :keyset, other_related_things: :offset`. Valid pagination values are `nil`, `:offset`, `:keyset` and `:relay`."
|
|
],
|
|
field_names: [
|
|
type: :keyword_list,
|
|
doc: "A keyword list of name overrides for attributes."
|
|
],
|
|
hide_fields: [
|
|
type: {:list, :atom},
|
|
doc: "A list of attributes to hide from the domain"
|
|
],
|
|
show_fields: [
|
|
type: {:list, :atom},
|
|
doc:
|
|
"A list of attributes to show in the domain. If not specified includes all (excluding `hide_fiels`)."
|
|
],
|
|
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 be `nil` unless at least one of the read actions on a resource uses keyset pagination or it is the result of a mutation
|
|
"""
|
|
],
|
|
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."
|
|
],
|
|
argument_input_types: [
|
|
type: :keyword_list,
|
|
doc:
|
|
"A keyword list of actions and their input type overrides for arguments. The type overrides should refer to types available in the graphql (absinthe) schema. `list_of/1` and `non_null/1` helpers can be used."
|
|
],
|
|
primary_key_delimiter: [
|
|
type: :string,
|
|
default: "~",
|
|
doc:
|
|
"If a composite primary key exists, this can be set to determine delimiter used in 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
|
|
],
|
|
filterable_fields: [
|
|
type: {:list, :atom},
|
|
required: false,
|
|
doc:
|
|
"A list of fields that are allowed to be filtered on. Defaults to all filterable fields for which a GraphQL type can be created."
|
|
],
|
|
nullable_fields: [
|
|
type: {:wrap_list, :atom},
|
|
doc:
|
|
"Mark fields as nullable even if they are required. This is useful when using field policies. See the authorization guide for more."
|
|
]
|
|
],
|
|
sections: [
|
|
@queries,
|
|
@mutations,
|
|
@subscriptions,
|
|
@managed_relationships
|
|
]
|
|
}
|
|
|
|
@transformers [
|
|
AshGraphql.Resource.Transformers.RequireKeysetForRelayQueries,
|
|
AshGraphql.Resource.Transformers.ValidateActions,
|
|
AshGraphql.Resource.Transformers.ValidateCompatibleNames,
|
|
AshGraphql.Resource.Transformers.Subscription
|
|
]
|
|
|
|
@verifiers [
|
|
AshGraphql.Resource.Verifiers.VerifyQueryMetadata,
|
|
AshGraphql.Resource.Verifiers.RequirePkeyDelimiter,
|
|
AshGraphql.Resource.Verifiers.VerifyPaginateRelationshipWith
|
|
]
|
|
|
|
@sections [@graphql]
|
|
|
|
@moduledoc """
|
|
This Ash resource extension adds configuration for exposing a resource in a graphql.
|
|
"""
|
|
|
|
use Spark.Dsl.Extension, sections: @sections, transformers: @transformers, verifiers: @verifiers
|
|
|
|
@deprecated "See `AshGraphql.Resource.Info.queries/1`"
|
|
defdelegate queries(resource, domain \\ []), to: AshGraphql.Resource.Info
|
|
|
|
@deprecated "See `AshGraphql.Resource.Info.mutations/1`"
|
|
defdelegate mutations(resource, domain \\ []), to: AshGraphql.Resource.Info
|
|
|
|
@deprecated "See `AshGraphql.Resource.Info.mutations/1`"
|
|
defdelegate subscriptions(resource), to: AshGraphql.Resource.Info
|
|
|
|
@deprecated "See `AshGraphql.Resource.Info.managed_relationships/1`"
|
|
defdelegate managed_relationships(resource), to: AshGraphql.Resource.Info
|
|
|
|
@deprecated "See `AshGraphql.Resource.Info.type/1`"
|
|
defdelegate type(resource), to: AshGraphql.Resource.Info
|
|
|
|
@deprecated "See `AshGraphql.Resource.Info.primary_key_delimiter/1`"
|
|
defdelegate primary_key_delimiter(resource), to: AshGraphql.Resource.Info
|
|
|
|
@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
|
|
|
|
def codegen(argv) do
|
|
schemas = AshGraphql.Codegen.schemas()
|
|
|
|
check? = "--check" in argv
|
|
|
|
for schema <- schemas, schema.generate_sdl_file() do
|
|
AshGraphql.Codegen.generate_sdl_file(schema, check?: check?)
|
|
end
|
|
end
|
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
|
def install(igniter, module, Ash.Resource, _path, _argv) do
|
|
type =
|
|
module
|
|
|> Module.split()
|
|
|> List.last()
|
|
|> Macro.underscore()
|
|
|> String.to_atom()
|
|
|
|
igniter =
|
|
case Ash.Resource.Igniter.domain(igniter, module) do
|
|
{:ok, igniter, domain} ->
|
|
AshGraphql.Domain.install(igniter, domain, Ash.Domain, nil, nil)
|
|
|
|
{:error, igniter} ->
|
|
igniter
|
|
end
|
|
|
|
igniter
|
|
|> Spark.Igniter.add_extension(
|
|
module,
|
|
Ash.Resource,
|
|
:extensions,
|
|
AshGraphql.Resource
|
|
)
|
|
|> Spark.Igniter.set_option(module, [:graphql, :type], type)
|
|
end
|
|
|
|
def encode_id(record, relay_ids?) do
|
|
if relay_ids? do
|
|
encode_relay_id(record)
|
|
else
|
|
encode_primary_key(record)
|
|
end
|
|
end
|
|
|
|
def encode_primary_key(%resource{} = record) do
|
|
case Ash.Resource.Info.primary_key(resource) do
|
|
[field] ->
|
|
Map.get(record, field)
|
|
|
|
keys ->
|
|
delimiter = primary_key_delimiter(resource)
|
|
|
|
[_ | concatenated_keys] =
|
|
keys
|
|
|> Enum.reverse()
|
|
|> Enum.reduce([], fn key, acc -> [delimiter, to_string(Map.get(record, key)), acc] end)
|
|
|
|
IO.iodata_to_binary(concatenated_keys)
|
|
end
|
|
end
|
|
|
|
def encode_relay_id(%resource{} = record) do
|
|
type = type(resource)
|
|
primary_key = encode_primary_key(record)
|
|
|
|
"#{type}:#{primary_key}"
|
|
|> Base.encode64()
|
|
end
|
|
|
|
def decode_id(resource, id, relay_ids?) do
|
|
type = type(resource)
|
|
|
|
if relay_ids? do
|
|
case decode_relay_id(id) do
|
|
{:ok, %{type: ^type, id: primary_key}} ->
|
|
decode_primary_key(resource, primary_key)
|
|
|
|
_ ->
|
|
{:error, Ash.Error.Invalid.InvalidPrimaryKey.exception(resource: resource, value: id)}
|
|
end
|
|
else
|
|
decode_primary_key(resource, id)
|
|
end
|
|
end
|
|
|
|
def decode_primary_key(resource, value) do
|
|
case Ash.Resource.Info.primary_key(resource) do
|
|
[field] ->
|
|
{:ok, [{field, value}]}
|
|
|
|
fields ->
|
|
delimiter = primary_key_delimiter(resource)
|
|
parts = String.split(value, delimiter)
|
|
|
|
if Enum.count(parts) == Enum.count(fields) do
|
|
{:ok, Enum.zip(fields, parts)}
|
|
else
|
|
{:error, "Invalid primary key"}
|
|
end
|
|
end
|
|
end
|
|
|
|
def decode_relay_id(id) do
|
|
[type_string, primary_key] =
|
|
id
|
|
|> Base.decode64!()
|
|
|> String.split(":", parts: 2)
|
|
|
|
type = String.to_existing_atom(type_string)
|
|
|
|
{:ok, %{type: type, id: primary_key}}
|
|
rescue
|
|
_ ->
|
|
{:error, Ash.Error.Invalid.InvalidPrimaryKey.exception(resource: nil, value: id)}
|
|
end
|
|
|
|
@doc false
|
|
def queries(
|
|
domain,
|
|
all_domains,
|
|
resource,
|
|
action_middleware,
|
|
schema,
|
|
relay_ids?,
|
|
as_mutations? \\ false
|
|
) do
|
|
resource
|
|
|> queries(all_domains)
|
|
|> Enum.filter(&(Map.get(&1, :as_mutation?, false) == as_mutations?))
|
|
|> Enum.map(fn
|
|
%{type: :action, name: name, action: action} = query ->
|
|
query_action =
|
|
Ash.Resource.Info.action(resource, action) ||
|
|
raise "No such action #{action} on #{resource}"
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
arguments: generic_action_args(query_action, resource, schema),
|
|
identifier: name,
|
|
middleware:
|
|
action_middleware ++
|
|
domain_middleware(domain) ++
|
|
id_translation_middleware(query.relay_id_translations, relay_ids?) ++
|
|
[
|
|
{{AshGraphql.Graphql.Resolver, :resolve}, {domain, resource, query, false}}
|
|
],
|
|
complexity: {AshGraphql.Graphql.Resolver, :query_complexity},
|
|
module: schema,
|
|
name: to_string(name),
|
|
description: query.description || query_action.description,
|
|
type: generic_action_type(query_action, resource),
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
|
|
query ->
|
|
query_action =
|
|
Ash.Resource.Info.action(resource, query.action) ||
|
|
raise "No such action #{query.action} on #{resource}"
|
|
|
|
type =
|
|
AshGraphql.Resource.Info.type(resource) ||
|
|
raise """
|
|
Resource #{inspect(resource)} is trying to define the query #{inspect(query.name)}
|
|
which requires a GraphQL type to be defined.
|
|
|
|
You should define the type of your resource with `type :my_resource_type`.
|
|
"""
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
arguments:
|
|
args(
|
|
query.type,
|
|
resource,
|
|
query_action,
|
|
schema,
|
|
query.identity,
|
|
query.hide_inputs,
|
|
query
|
|
),
|
|
identifier: query.name,
|
|
middleware:
|
|
action_middleware ++
|
|
domain_middleware(domain) ++
|
|
id_translation_middleware(query.relay_id_translations, relay_ids?) ++
|
|
[
|
|
{{AshGraphql.Graphql.Resolver, :resolve}, {domain, resource, query, relay_ids?}}
|
|
],
|
|
complexity: {AshGraphql.Graphql.Resolver, :query_complexity},
|
|
module: schema,
|
|
name: to_string(query.name),
|
|
description: query.description || query_action.description,
|
|
type: query_type(query, resource, query_action, type),
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
end)
|
|
end
|
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
|
@doc false
|
|
def mutations(domain, all_domains, resource, action_middleware, schema, relay_ids?) do
|
|
resource
|
|
|> mutations(all_domains)
|
|
|> Enum.map(fn
|
|
%{type: :action, name: name, action: action} = query ->
|
|
query_action =
|
|
Ash.Resource.Info.action(resource, action) ||
|
|
raise "No such action #{action} on #{resource}"
|
|
|
|
args =
|
|
case query_action.arguments do
|
|
[] ->
|
|
[]
|
|
|
|
fields ->
|
|
[
|
|
%Absinthe.Blueprint.Schema.InputValueDefinition{
|
|
identifier: :input,
|
|
module: schema,
|
|
name: "input",
|
|
placement: :argument_definition,
|
|
type: mutation_input_type(name, fields)
|
|
}
|
|
]
|
|
end
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
arguments: args,
|
|
identifier: name,
|
|
middleware:
|
|
action_middleware ++
|
|
domain_middleware(domain) ++
|
|
id_translation_middleware(query.relay_id_translations, relay_ids?) ++
|
|
[
|
|
{{AshGraphql.Graphql.Resolver, :resolve}, {domain, resource, query, true}}
|
|
],
|
|
complexity: {AshGraphql.Graphql.Resolver, :query_complexity},
|
|
module: schema,
|
|
name: to_string(name),
|
|
description: query.description || query_action.description,
|
|
type: generic_action_type(query_action, resource),
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
|
|
%{type: :destroy} = mutation ->
|
|
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,
|
|
domain,
|
|
relay_ids?
|
|
)
|
|
else
|
|
args =
|
|
case mutation_fields(
|
|
resource,
|
|
schema,
|
|
action,
|
|
mutation.type,
|
|
mutation.hide_inputs
|
|
) do
|
|
[] ->
|
|
mutation_args(mutation, resource, schema)
|
|
|
|
fields ->
|
|
mutation_args(mutation, resource, schema) ++
|
|
[
|
|
%Absinthe.Blueprint.Schema.InputValueDefinition{
|
|
identifier: :input,
|
|
module: schema,
|
|
name: "input",
|
|
placement: :argument_definition,
|
|
type: mutation_input_type(mutation.name, fields),
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
]
|
|
end
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
arguments: args,
|
|
identifier: mutation.name,
|
|
middleware:
|
|
action_middleware ++
|
|
domain_middleware(domain) ++
|
|
id_translation_middleware(mutation.relay_id_translations, relay_ids?) ++
|
|
[
|
|
{{AshGraphql.Graphql.Resolver, :mutate},
|
|
{domain, resource, mutation, relay_ids?}}
|
|
],
|
|
module: schema,
|
|
name: to_string(mutation.name),
|
|
description:
|
|
mutation.description ||
|
|
Ash.Resource.Info.action(resource, mutation.action).description,
|
|
type: mutation_result_type(mutation.name, domain),
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
end
|
|
|
|
%{type: :create} = mutation ->
|
|
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,
|
|
mutation.hide_inputs
|
|
) do
|
|
[] ->
|
|
[]
|
|
|
|
fields ->
|
|
[
|
|
%Absinthe.Blueprint.Schema.InputValueDefinition{
|
|
identifier: :input,
|
|
module: schema,
|
|
name: "input",
|
|
placement: :argument_definition,
|
|
type: mutation_input_type(mutation.name, fields)
|
|
}
|
|
]
|
|
end
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
arguments: args,
|
|
identifier: mutation.name,
|
|
middleware:
|
|
action_middleware ++
|
|
domain_middleware(domain) ++
|
|
id_translation_middleware(mutation.relay_id_translations, relay_ids?) ++
|
|
[
|
|
{{AshGraphql.Graphql.Resolver, :mutate}, {domain, resource, mutation, relay_ids?}}
|
|
],
|
|
module: schema,
|
|
name: to_string(mutation.name),
|
|
description:
|
|
mutation.description ||
|
|
Ash.Resource.Info.action(resource, mutation.action).description,
|
|
type: mutation_result_type(mutation.name, domain),
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
|
|
mutation ->
|
|
update_mutation(resource, schema, mutation, schema, action_middleware, domain, relay_ids?)
|
|
end)
|
|
|> Enum.concat(
|
|
queries(domain, all_domains, resource, action_middleware, schema, relay_ids?, true)
|
|
)
|
|
end
|
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
|
defp update_mutation(resource, schema, mutation, schema, action_middleware, domain, relay_ids?) do
|
|
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,
|
|
mutation.hide_inputs
|
|
) do
|
|
[] ->
|
|
mutation_args(mutation, resource, schema)
|
|
|
|
fields ->
|
|
mutation_args(mutation, resource, schema) ++
|
|
[
|
|
%Absinthe.Blueprint.Schema.InputValueDefinition{
|
|
identifier: :input,
|
|
module: schema,
|
|
name: "input",
|
|
placement: :argument_definition,
|
|
type: mutation_input_type(mutation.name, fields),
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
]
|
|
end
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
arguments: args,
|
|
identifier: mutation.name,
|
|
middleware:
|
|
action_middleware ++
|
|
domain_middleware(domain) ++
|
|
id_translation_middleware(mutation.relay_id_translations, relay_ids?) ++
|
|
[
|
|
{{AshGraphql.Graphql.Resolver, :mutate}, {domain, resource, mutation, relay_ids?}}
|
|
],
|
|
module: schema,
|
|
name: to_string(mutation.name),
|
|
description:
|
|
mutation.description ||
|
|
Ash.Resource.Info.action(resource, mutation.action).description,
|
|
type: mutation_result_type(mutation.name, domain),
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
end
|
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
|
defp mutation_result_type(mutation_name, domain) do
|
|
type = String.to_atom("#{mutation_name}_result")
|
|
root_level_errors? = AshGraphql.Domain.Info.root_level_errors?(domain)
|
|
|
|
maybe_wrap_non_null(type, not root_level_errors?)
|
|
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
|
|
|> 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)
|
|
},
|
|
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: maybe_wrap_non_null(:id, true),
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
| mutation_read_args(mutation, resource, schema)
|
|
]
|
|
end
|
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
|
defp mutation_input_type(mutation_name, mutation_fields) do
|
|
any_non_null_field? =
|
|
mutation_fields
|
|
|> Enum.any?(fn
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
type: %Absinthe.Blueprint.TypeReference.NonNull{}
|
|
} ->
|
|
true
|
|
|
|
_ ->
|
|
false
|
|
end)
|
|
|
|
String.to_atom("#{mutation_name}_input")
|
|
|> maybe_wrap_non_null(any_non_null_field?)
|
|
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.filter(& &1.public?)
|
|
|> 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),
|
|
description: argument.description,
|
|
type: type,
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
end)
|
|
end
|
|
|
|
@doc false
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
|
def mutation_types(resource, all_domains, schema) do
|
|
resource_type = AshGraphql.Resource.Info.type(resource)
|
|
|
|
resource
|
|
|> mutations(all_domains)
|
|
|> Enum.flat_map(fn mutation ->
|
|
unless resource_type do
|
|
raise """
|
|
Resource #{inspect(resource)} is trying to define the mutation #{inspect(mutation.name)}
|
|
which requires a GraphQL type to be defined.
|
|
|
|
You should define the type of your resource with `type :my_resource_type`.
|
|
"""
|
|
end
|
|
|
|
mutation = %{
|
|
mutation
|
|
| action:
|
|
Ash.Resource.Info.action(resource, mutation.action) ||
|
|
raise("No such action #{mutation.action} for #{inspect(resource)}")
|
|
}
|
|
|
|
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",
|
|
type: resource_type,
|
|
__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.NonNull{
|
|
of_type: %Absinthe.Blueprint.TypeReference.List{
|
|
of_type: %Absinthe.Blueprint.TypeReference.NonNull{
|
|
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
|
|
|
|
result = %Absinthe.Blueprint.Schema.ObjectTypeDefinition{
|
|
description: "The result of the #{inspect(mutation.name)} mutation",
|
|
fields: fields,
|
|
identifier: String.to_atom("#{mutation.name}_result"),
|
|
module: schema,
|
|
name: Macro.camelize("#{mutation.name}_result"),
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
|
|
case mutation_fields(
|
|
resource,
|
|
schema,
|
|
mutation.action,
|
|
mutation.type,
|
|
mutation.hide_inputs
|
|
) do
|
|
[] ->
|
|
[result] ++ List.wrap(metadata_object_type)
|
|
|
|
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)
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp id_translation_middleware(relay_id_translations, true) do
|
|
[{{AshGraphql.Graphql.IdTranslator, :translate_relay_ids}, relay_id_translations}]
|
|
end
|
|
|
|
defp id_translation_middleware(_relay_id_translations, _relay_ids?) do
|
|
[]
|
|
end
|
|
|
|
defp domain_middleware(domain) do
|
|
[{{AshGraphql.Graphql.DomainMiddleware, :set_domain}, domain}]
|
|
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
|
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
|
@doc false
|
|
def subscriptions(api, resource, action_middleware, schema) do
|
|
resource
|
|
|> subscriptions()
|
|
|> Enum.map(fn %Subscription{name: name} = subscription ->
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
arguments:
|
|
args(:subscription, resource, nil, schema, nil)
|
|
|> IO.inspect(label: "args"),
|
|
identifier: name,
|
|
name: to_string(name),
|
|
config:
|
|
AshGraphql.Resource.Subscription.DefaultConfig.create_config(
|
|
subscription,
|
|
api,
|
|
resource
|
|
),
|
|
module: schema,
|
|
middleware:
|
|
action_middleware ++
|
|
[
|
|
{{AshGraphql.Graphql.Resolver, :resolve}, {api, resource, subscription, true}}
|
|
],
|
|
type: AshGraphql.Resource.Info.type(resource),
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
end)
|
|
end
|
|
|
|
@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
|
|
|
|
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
|
|
end)
|
|
|
|
unless Enum.empty?(fields) do
|
|
name = "#{AshGraphql.Resource.Info.type(source_resource)}_#{attribute.name}_input"
|
|
|
|
%Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
|
|
fields: fields,
|
|
identifier: String.to_atom(name),
|
|
module: schema,
|
|
name: Macro.camelize(name),
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
end
|
|
end
|
|
|
|
defp mutation_fields(resource, schema, action, type, hide_inputs \\ []) do
|
|
field_names = AshGraphql.Resource.Info.field_names(resource)
|
|
argument_names = AshGraphql.Resource.Info.argument_names(resource)
|
|
|
|
attribute_fields =
|
|
cond do
|
|
action.type == :action ->
|
|
[]
|
|
|
|
action.type == :destroy && !action.soft? ->
|
|
[]
|
|
|
|
true ->
|
|
resource
|
|
|> Ash.Resource.Info.attributes()
|
|
|> Enum.filter(fn attribute ->
|
|
AshGraphql.Resource.Info.show_field?(resource, attribute.name) &&
|
|
attribute.name in action.accept && attribute.writable? &&
|
|
attribute.name not in hide_inputs
|
|
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?)
|
|
|
|
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
|
|
|
|
argument_fields =
|
|
action.arguments
|
|
|> Enum.filter(& &1.public?)
|
|
|> Enum.map(fn argument ->
|
|
name = argument_names[action.name][argument.name] || argument.name
|
|
|
|
case find_manage_change(argument, action, resource) do
|
|
nil ->
|
|
type =
|
|
case AshGraphql.Resource.Info.argument_input_types(resource)[action.name][name] do
|
|
nil ->
|
|
argument.type
|
|
|> field_type(argument, resource, true)
|
|
|> maybe_wrap_non_null(argument_required?(argument))
|
|
|
|
override ->
|
|
unwrap_literal_type(override)
|
|
end
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
identifier: name,
|
|
module: schema,
|
|
name: to_string(name),
|
|
description: argument.description,
|
|
type: type,
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
|
|
_manage_opts ->
|
|
managed = AshGraphql.Resource.Info.managed_relationship(resource, action, argument)
|
|
|
|
if managed do
|
|
current = Process.get(:managed_relationship_requirements, [])
|
|
|
|
Process.put(
|
|
:managed_relationship_requirements,
|
|
[
|
|
{resource, managed, action, argument} | current
|
|
]
|
|
)
|
|
|
|
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),
|
|
description: argument.description,
|
|
type: maybe_wrap_non_null(type, argument_required?(argument)),
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
else
|
|
type =
|
|
argument.type
|
|
|> field_type(argument, resource, true)
|
|
|> maybe_wrap_non_null(argument_required?(argument))
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
identifier: name,
|
|
module: schema,
|
|
name: to_string(name),
|
|
description: Map.get(argument, :description, ""),
|
|
type: type,
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
end
|
|
end
|
|
end)
|
|
|
|
attribute_fields ++ argument_fields
|
|
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?] || embedded?(type)
|
|
)
|
|
}
|
|
end
|
|
|
|
defp wrap_arrays(_, type, _), do: type
|
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
|
defp default_managed_type_name(resource, action, argument) do
|
|
String.to_atom(
|
|
to_string(AshGraphql.Resource.Info.type(resource)) <>
|
|
"_" <>
|
|
to_string(action.name) <>
|
|
"_" <> to_string(argument.name) <> "_input"
|
|
)
|
|
end
|
|
|
|
@doc false
|
|
def find_manage_change(argument, action, resource) do
|
|
if AshGraphql.Resource.Info.managed_relationship(resource, action, argument) do
|
|
Enum.find_value(action.changes, fn
|
|
%{change: {Ash.Resource.Change.ManageRelationship, opts}} ->
|
|
opts[:argument] == argument.name && opts
|
|
|
|
_ ->
|
|
nil
|
|
end)
|
|
end
|
|
end
|
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
|
defp query_type(%{type: :list, relay?: relay?} = query, _resource, action, type) do
|
|
type = query.type_name || type
|
|
|
|
if relay? do
|
|
String.to_atom("#{type}_connection")
|
|
else
|
|
case query_pagination_strategy(query, action) do
|
|
:keyset ->
|
|
String.to_atom("keyset_page_of_#{type}")
|
|
|
|
:offset ->
|
|
String.to_atom("page_of_#{type}")
|
|
|
|
nil ->
|
|
%Absinthe.Blueprint.TypeReference.NonNull{
|
|
of_type: %Absinthe.Blueprint.TypeReference.List{
|
|
of_type: %Absinthe.Blueprint.TypeReference.NonNull{
|
|
of_type: type
|
|
}
|
|
}
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
defp query_type(query, _resource, _action, type) do
|
|
type = query.type_name || type
|
|
|
|
maybe_wrap_non_null(type, not query.allow_nil?)
|
|
end
|
|
|
|
@doc false
|
|
def query_pagination_strategy(nil, _) do
|
|
nil
|
|
end
|
|
|
|
def query_pagination_strategy(%{paginate_with: strategy}, action) do
|
|
pagination_strategy(strategy, action)
|
|
end
|
|
|
|
@doc false
|
|
def relationship_pagination_strategy(resource, relationship_name, action) do
|
|
resource
|
|
|> AshGraphql.Resource.Info.paginate_relationship_with()
|
|
|> Keyword.get(relationship_name)
|
|
|> pagination_strategy(action, true)
|
|
end
|
|
|
|
defp pagination_strategy(strategy, action, allow_relay? \\ false)
|
|
|
|
defp pagination_strategy(_strategy, %{pagination: pagination}, _allow_relay?)
|
|
when pagination in [nil, false] do
|
|
nil
|
|
end
|
|
|
|
defp pagination_strategy(strategy, action, allow_relay?) do
|
|
strategies =
|
|
if action.pagination.required? do
|
|
[]
|
|
else
|
|
[nil]
|
|
end
|
|
|
|
strategies =
|
|
if action.pagination.keyset? do
|
|
[:keyset | strategies]
|
|
else
|
|
strategies
|
|
end
|
|
|
|
strategies =
|
|
if action.pagination.offset? do
|
|
[:offset | strategies]
|
|
else
|
|
strategies
|
|
end
|
|
|
|
strategies =
|
|
if allow_relay? and action.pagination.keyset? do
|
|
[:relay | strategies]
|
|
else
|
|
strategies
|
|
end
|
|
|
|
if strategy in strategies do
|
|
strategy
|
|
else
|
|
Enum.at(strategies, 0)
|
|
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
|
|
%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
|
|
|
|
defp generic_action_type(action, resource) do
|
|
if !action.returns do
|
|
raise "Cannot use #{inspect(resource)}.#{action.name} with AshGraphql, because it does not have a return type."
|
|
end
|
|
|
|
fake_attribute = %{
|
|
type: action.returns,
|
|
constraints: action.constraints,
|
|
allow_nil?: Map.get(action, :allow_nil?, false),
|
|
name: action.name
|
|
}
|
|
|
|
fake_attribute.type
|
|
|> field_type(fake_attribute, resource, false)
|
|
|> maybe_wrap_non_null(argument_required?(fake_attribute))
|
|
end
|
|
|
|
defp generic_action_args(action, resource, schema) do
|
|
action.arguments
|
|
|> Enum.filter(& &1.public?)
|
|
|> 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),
|
|
description: argument.description,
|
|
type: type,
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
end)
|
|
end
|
|
|
|
defp args(
|
|
action_type,
|
|
resource,
|
|
action,
|
|
schema,
|
|
identity \\ nil,
|
|
hide_inputs \\ [],
|
|
query \\ nil
|
|
)
|
|
|
|
defp args(:get, resource, action, schema, nil, hide_inputs, _query) do
|
|
get_fields(resource) ++
|
|
read_args(resource, action, schema, hide_inputs)
|
|
end
|
|
|
|
defp args(:get, resource, action, schema, identity, hide_inputs, _query) 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, hide_inputs))
|
|
end
|
|
|
|
defp args(:read_one, resource, action, schema, _, hide_inputs, _query) do
|
|
args =
|
|
if AshGraphql.Resource.Info.derive_filter?(resource) do
|
|
case resource_filter_fields(resource, schema) do
|
|
[] ->
|
|
[]
|
|
|
|
_ ->
|
|
[
|
|
%Absinthe.Blueprint.Schema.InputValueDefinition{
|
|
name: "filter",
|
|
identifier: :filter,
|
|
type: resource_filter_type(resource),
|
|
description: "A filter to limit the results",
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
]
|
|
end
|
|
else
|
|
[]
|
|
end
|
|
|
|
args ++ read_args(resource, action, schema, hide_inputs)
|
|
end
|
|
|
|
defp args(:list, resource, action, schema, _, hide_inputs, query) do
|
|
args =
|
|
if AshGraphql.Resource.Info.derive_filter?(resource) do
|
|
case resource_filter_fields(resource, schema) do
|
|
[] ->
|
|
[]
|
|
|
|
_ ->
|
|
[
|
|
%Absinthe.Blueprint.Schema.InputValueDefinition{
|
|
name: "filter",
|
|
identifier: :filter,
|
|
type: resource_filter_type(resource),
|
|
description: "A filter to limit the results",
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
]
|
|
end
|
|
else
|
|
[]
|
|
end
|
|
|
|
args =
|
|
if AshGraphql.Resource.Info.derive_sort?(resource) do
|
|
case sort_values(resource) do
|
|
[] ->
|
|
args
|
|
|
|
_ ->
|
|
[
|
|
%Absinthe.Blueprint.Schema.InputValueDefinition{
|
|
name: "sort",
|
|
identifier: :sort,
|
|
type: %Absinthe.Blueprint.TypeReference.List{
|
|
of_type: resource_sort_type(resource)
|
|
},
|
|
description: "How to sort the records in the response",
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
| args
|
|
]
|
|
end
|
|
else
|
|
args
|
|
end
|
|
|
|
args ++ pagination_args(query, action) ++ read_args(resource, action, schema, hide_inputs)
|
|
end
|
|
|
|
defp args(:one_related, resource, action, schema, _identity, hide_inputs, _) do
|
|
read_args(resource, action, schema, hide_inputs)
|
|
end
|
|
|
|
defp args(:subscription, resource, _action, schema, _identity, _hide_inputs, _query) do
|
|
if AshGraphql.Resource.Info.derive_filter?(resource) do
|
|
case resource_filter_fields(resource, schema) do
|
|
[] ->
|
|
[]
|
|
|
|
_ ->
|
|
[
|
|
%Absinthe.Blueprint.Schema.InputValueDefinition{
|
|
name: "filter",
|
|
identifier: :filter,
|
|
type: resource_filter_type(resource),
|
|
description: "A filter to limit the results",
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
]
|
|
end
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
defp related_list_args(resource, related_resource, relationship_name, action, schema) do
|
|
args(:list, related_resource, action, schema) ++
|
|
relationship_pagination_args(resource, relationship_name, action)
|
|
end
|
|
|
|
defp read_args(resource, action, schema, hide_inputs) do
|
|
action.arguments
|
|
|> Enum.filter(&(&1.public? && &1.name not in hide_inputs))
|
|
|> 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),
|
|
description: argument.description,
|
|
type: type,
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
end)
|
|
end
|
|
|
|
defp relationship_pagination_args(resource, relationship_name, action) do
|
|
case relationship_pagination_strategy(resource, relationship_name, action) do
|
|
nil ->
|
|
default_relationship_pagination_args()
|
|
|
|
:keyset ->
|
|
keyset_pagination_args(action)
|
|
|
|
:relay ->
|
|
# Relay has the same args as keyset
|
|
keyset_pagination_args(action)
|
|
|
|
:offset ->
|
|
offset_pagination_args(action)
|
|
end
|
|
end
|
|
|
|
defp default_relationship_pagination_args do
|
|
[
|
|
%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 pagination_args(query, action) do
|
|
case query_pagination_strategy(query, action) do
|
|
nil ->
|
|
[]
|
|
|
|
:keyset ->
|
|
keyset_pagination_args(action)
|
|
|
|
:offset ->
|
|
offset_pagination_args(action)
|
|
end
|
|
end
|
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
|
defp resource_sort_type(resource) do
|
|
String.to_atom(to_string(AshGraphql.Resource.Info.type(resource)) <> "_sort_input")
|
|
end
|
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
|
defp resource_filter_type(resource) do
|
|
String.to_atom(to_string(AshGraphql.Resource.Info.type(resource)) <> "_filter_input")
|
|
end
|
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
|
defp attribute_filter_field_type(resource, attribute) do
|
|
field_names = AshGraphql.Resource.Info.field_names(resource)
|
|
|
|
String.to_atom(
|
|
to_string(AshGraphql.Resource.Info.type(resource)) <>
|
|
"_filter_" <> to_string(field_names[attribute.name] || attribute.name)
|
|
)
|
|
end
|
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
|
defp calculation_filter_field_type(resource, calculation) do
|
|
field_names = AshGraphql.Resource.Info.field_names(resource)
|
|
|
|
String.to_atom(
|
|
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
|
|
end
|
|
|
|
@doc false
|
|
def type_definitions(resource, domain, all_domains, schema, relay_ids?) do
|
|
List.wrap(type_definition(resource, domain, all_domains, schema, relay_ids?)) ++
|
|
List.wrap(query_type_definitions(resource, domain, all_domains, schema, relay_ids?)) ++
|
|
List.wrap(sort_input(resource, schema)) ++
|
|
List.wrap(filter_input(resource, schema)) ++
|
|
filter_field_types(resource, schema) ++
|
|
List.wrap(page_type_definitions(resource, schema)) ++
|
|
enum_definitions(resource, schema, __ENV__)
|
|
end
|
|
|
|
def no_graphql_types(resource, schema) do
|
|
enum_definitions(resource, schema, __ENV__)
|
|
end
|
|
|
|
def managed_relationship_definitions(used, schema) do
|
|
Enum.map(used, fn {resource, managed_relationship, action, argument} ->
|
|
opts =
|
|
find_manage_change(argument, action, resource) ||
|
|
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
|
|
|
|
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 ->
|
|
Spark.Options.Helpers.set_default!(manage_opts, key, value)
|
|
end)
|
|
else
|
|
Ash.Changeset.manage_relationship_schema()
|
|
end
|
|
|
|
manage_opts = Spark.Options.validate!(opts[:opts], manage_opts_schema)
|
|
|
|
fields = manage_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
|
|
|
|
@doc false
|
|
def manage_fields(manage_opts, managed_relationship, relationship, schema) do
|
|
on_match_fields(manage_opts, relationship, schema) ++
|
|
on_no_match_fields(manage_opts, relationship, schema) ++
|
|
on_lookup_fields(manage_opts, relationship, schema) ++
|
|
manage_pkey_fields(manage_opts, managed_relationship, relationship, schema)
|
|
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
|
|
action_name =
|
|
case managed_relationship.action do
|
|
name when is_atom(name) -> name
|
|
%{name: name} -> name
|
|
end
|
|
|
|
raise """
|
|
#{inspect(resource)}: #{action_name}.#{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 ->
|
|
action =
|
|
case field.source.action do
|
|
%{name: name} -> name
|
|
name -> name
|
|
end
|
|
|> then(fn action ->
|
|
try do
|
|
to_string(action)
|
|
rescue
|
|
_ ->
|
|
inspect(action)
|
|
end
|
|
end)
|
|
|
|
"#{inspect(format_type(field.field.type))} - from #{inspect(field.source.resource)}.#{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)
|
|
|> Enum.reject(fn {_, _, field} ->
|
|
field.identifier == relationship.destination_attribute
|
|
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)
|
|
|> Enum.reject(fn {_, _, field} ->
|
|
field.identifier in [
|
|
relationship.destination_attribute_on_join_resource,
|
|
relationship.source_attribute_on_join_resource
|
|
]
|
|
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)
|
|
|> Enum.reject(fn {_, _, field} ->
|
|
field.identifier == relationship.destination_attribute
|
|
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)
|
|
|> Enum.reject(fn {_, _, field} ->
|
|
field.identifier in [
|
|
relationship.destination_attribute_on_join_resource,
|
|
relationship.source_attribute_on_join_resource
|
|
]
|
|
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)
|
|
|> Enum.reject(fn {_, _, field} ->
|
|
field.identifier == relationship.destination_attribute
|
|
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)
|
|
|> Enum.reject(fn {_, _, field} ->
|
|
field.identifier in [
|
|
relationship.destination_attribute_on_join_resource,
|
|
relationship.source_attribute_on_join_resource
|
|
]
|
|
end)
|
|
end)
|
|
end
|
|
|
|
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 ->
|
|
if is_nil(managed_relationship.lookup_identities) do
|
|
identity.name in List.wrap(opts[:use_identities])
|
|
else
|
|
identity.name in managed_relationship.lookup_identities
|
|
end
|
|
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
|
|
end
|
|
|
|
defp filter_field_types(resource, schema) do
|
|
filter_attribute_types(resource, schema) ++
|
|
filter_aggregate_types(resource, schema) ++
|
|
List.wrap(calculation_filter_inputs(resource, schema))
|
|
end
|
|
|
|
defp filter_attribute_types(resource, schema) do
|
|
resource
|
|
|> Ash.Resource.Info.public_attributes()
|
|
|> Enum.filter(
|
|
&(AshGraphql.Resource.Info.show_field?(resource, &1.name) && filterable?(&1, resource))
|
|
)
|
|
|> Enum.flat_map(&filter_type(&1, resource, schema))
|
|
end
|
|
|
|
defp filter_aggregate_types(resource, schema) do
|
|
resource
|
|
|> Ash.Resource.Info.public_aggregates()
|
|
|> Enum.filter(
|
|
&(AshGraphql.Resource.Info.show_field?(resource, &1.name) && filterable?(&1, resource))
|
|
)
|
|
|> Enum.flat_map(&filter_type(&1, resource, schema))
|
|
end
|
|
|
|
defp attribute_or_aggregate_type(%Ash.Resource.Attribute{type: type}, _resource),
|
|
do: type
|
|
|
|
defp attribute_or_aggregate_type(
|
|
%Ash.Resource.Aggregate{kind: kind, field: field, relationship_path: relationship_path},
|
|
resource
|
|
) do
|
|
field_type =
|
|
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.field(related, field) do
|
|
attr.type
|
|
end
|
|
|
|
{:ok, aggregate_type, _} = Ash.Query.Aggregate.kind_to_type(kind, field_type, [])
|
|
|
|
aggregate_type
|
|
end
|
|
|
|
@doc false
|
|
def filter_type(attribute_or_aggregate, resource, schema) do
|
|
type = attribute_or_aggregate_type(attribute_or_aggregate, resource)
|
|
|
|
array_type? = match?({:array, _}, type)
|
|
|
|
fields =
|
|
Ash.Filter.builtin_operators()
|
|
|> Enum.concat(Ash.DataLayer.functions(resource))
|
|
|> 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)
|
|
|
|
if fields == [] do
|
|
[]
|
|
else
|
|
identifier = attribute_filter_field_type(resource, attribute_or_aggregate)
|
|
|
|
[
|
|
%Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
|
|
identifier: identifier,
|
|
fields: fields,
|
|
module: schema,
|
|
name: identifier |> to_string() |> Macro.camelize(),
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
]
|
|
end
|
|
end
|
|
|
|
defp filter_fields(
|
|
operator,
|
|
type,
|
|
array_type?,
|
|
schema,
|
|
attribute_or_aggregate,
|
|
resource
|
|
) do
|
|
expressable_types = get_expressable_types(operator, type, array_type?)
|
|
|
|
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}
|
|
|
|
[_, {:array, :same}] ->
|
|
{:array, type}
|
|
|
|
[_, :same] ->
|
|
type
|
|
|
|
[_, :any] ->
|
|
Ash.Type.String
|
|
|
|
[_, type] when is_atom(type) ->
|
|
Ash.Type.get_type(type)
|
|
|
|
_ ->
|
|
nil
|
|
end
|
|
|
|
if type do
|
|
{type, attribute_or_aggregate} =
|
|
case type do
|
|
{:unwrap, type} ->
|
|
{:array, type} = type
|
|
constraints = Map.get(attribute_or_aggregate, :constraints) || []
|
|
|
|
{type,
|
|
%{attribute_or_aggregate | type: type, constraints: constraints[:items] || []}}
|
|
|
|
type ->
|
|
{type, attribute_or_aggregate}
|
|
end
|
|
|
|
if embedded?(type) do
|
|
[]
|
|
else
|
|
attribute_or_aggregate = constraints_to_item_constraints(type, attribute_or_aggregate)
|
|
|
|
[
|
|
%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
|
|
end
|
|
rescue
|
|
_e ->
|
|
[]
|
|
end
|
|
|
|
defp get_expressable_types(operator_or_function, field_type, array_type?) do
|
|
if :attributes
|
|
|> operator_or_function.__info__()
|
|
|> Keyword.get_values(:behaviour)
|
|
|> List.flatten()
|
|
|> Enum.any?(&(&1 == Ash.Query.Operator)) do
|
|
do_get_expressable_types(operator_or_function.types(), field_type, array_type?)
|
|
else
|
|
do_get_expressable_types(operator_or_function.args(), field_type, array_type?)
|
|
end
|
|
end
|
|
|
|
defp do_get_expressable_types(operator_types, field_type, array_type?) do
|
|
field_type_short_name =
|
|
case Ash.Type.short_names()
|
|
|> Enum.find(fn {_, type} -> type == field_type end) do
|
|
nil -> nil
|
|
{short_name, _} -> short_name
|
|
end
|
|
|
|
operator_types
|
|
|> Enum.filter(fn
|
|
[:any, {:array, type}] when is_atom(type) ->
|
|
true
|
|
|
|
[{:array, inner_type}, :same] when is_atom(inner_type) and array_type? ->
|
|
true
|
|
|
|
:same ->
|
|
true
|
|
|
|
:any ->
|
|
true
|
|
|
|
[:any, type] when is_atom(type) ->
|
|
true
|
|
|
|
[^field_type_short_name, type] when is_atom(type) and not is_nil(field_type_short_name) ->
|
|
true
|
|
|
|
_ ->
|
|
false
|
|
end)
|
|
end
|
|
|
|
defp restrict_for_lists(operators, {:array, _}) do
|
|
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
|
|
|
|
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? || embedded?(attribute.type)
|
|
]
|
|
}
|
|
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
|
|
|
|
_ ->
|
|
%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
|
|
end
|
|
end
|
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
|
defp calc_input_fields(resource, schema) do
|
|
calcs =
|
|
resource
|
|
|> Ash.Resource.Info.public_calculations()
|
|
|> Enum.filter(&AshGraphql.Resource.Info.show_field?(resource, &1.name))
|
|
|> Enum.reject(fn
|
|
%{type: {:array, _}} ->
|
|
true
|
|
|
|
calc ->
|
|
embedded?(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
|
|
|
|
defp filter_input(resource, schema) do
|
|
if AshGraphql.Resource.Info.derive_filter?(resource) do
|
|
case resource_filter_fields(resource, schema) do
|
|
[] ->
|
|
nil
|
|
|
|
fields ->
|
|
%Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
|
|
identifier: resource_filter_type(resource),
|
|
module: schema,
|
|
name: resource |> resource_filter_type() |> to_string() |> Macro.camelize(),
|
|
fields: fields,
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
end
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
|
defp calculation_filter_inputs(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__)
|
|
}
|
|
|
|
%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
|
|
|
|
[type_def | types]
|
|
else
|
|
types
|
|
end
|
|
end)
|
|
end
|
|
|
|
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)
|
|
end
|
|
|
|
defp filterable_and_shown_field?(resource, field) do
|
|
AshGraphql.Resource.Info.show_field?(resource, field.name) &&
|
|
AshGraphql.Resource.Info.filterable_field?(resource, field.name)
|
|
end
|
|
|
|
defp attribute_filter_fields(resource, schema) do
|
|
field_names = AshGraphql.Resource.Info.field_names(resource)
|
|
|
|
resource
|
|
|> Ash.Resource.Info.public_attributes()
|
|
|> Enum.filter(&(filterable_and_shown_field?(resource, &1) && filterable?(&1, resource)))
|
|
|> Enum.flat_map(fn attribute ->
|
|
[
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
identifier: attribute.name,
|
|
module: schema,
|
|
name: to_string(field_names[attribute.name] || attribute.name),
|
|
description: attribute.description,
|
|
type: attribute_filter_field_type(resource, attribute),
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
]
|
|
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_and_shown_field?(resource, &1) && 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),
|
|
description: aggregate.description,
|
|
type: attribute_filter_field_type(resource, aggregate),
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
]
|
|
end)
|
|
else
|
|
[]
|
|
end
|
|
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_and_shown_field?(resource, &1) && 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),
|
|
description: calculation.description,
|
|
type: calculation_filter_field_type(resource, calculation),
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
end)
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
defp filterable?(%Ash.Resource.Aggregate{} = aggregate, resource) do
|
|
attribute =
|
|
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.field(related, aggregate.field) do
|
|
attr
|
|
end
|
|
|
|
field_type =
|
|
if attribute do
|
|
attribute.type
|
|
end
|
|
|
|
field_constraints =
|
|
if attribute do
|
|
attribute.constraints
|
|
end
|
|
|
|
{:ok, type, constraints} =
|
|
Aggregate.kind_to_type(aggregate.kind, field_type, field_constraints)
|
|
|
|
filterable?(
|
|
%Ash.Resource.Attribute{name: aggregate.name, type: type, constraints: constraints},
|
|
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
|
|
!embedded?(type) && module.has_expression?()
|
|
end
|
|
|
|
defp filterable?(%{type: type} = attribute, resource) do
|
|
if Ash.Type.NewType.new_type?(type) do
|
|
filterable?(
|
|
%{
|
|
attribute
|
|
| constraints: Ash.Type.NewType.constraints(type, attribute.constraints),
|
|
type: Ash.Type.NewType.subtype_of(type)
|
|
},
|
|
resource
|
|
)
|
|
else
|
|
!embedded?(type)
|
|
end
|
|
end
|
|
|
|
defp filterable?(_, _), do: false
|
|
|
|
defp sortable?(%Ash.Resource.Aggregate{} = aggregate, resource) do
|
|
attribute =
|
|
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.field(related, aggregate.field) do
|
|
attr
|
|
end
|
|
|
|
field_type =
|
|
if attribute do
|
|
attribute.type
|
|
end
|
|
|
|
field_constraints =
|
|
if attribute do
|
|
attribute.constraints
|
|
end
|
|
|
|
{:ok, type, constraints} =
|
|
Aggregate.kind_to_type(aggregate.kind, field_type, field_constraints)
|
|
|
|
sortable?(
|
|
%Ash.Resource.Attribute{name: aggregate.name, type: type, constraints: constraints},
|
|
resource
|
|
)
|
|
end
|
|
|
|
defp sortable?(%{type: {:array, _}}, _), do: false
|
|
defp sortable?(%{sortable?: false}, _), do: false
|
|
defp sortable?(%{type: Ash.Type.Union}, _), do: false
|
|
|
|
defp sortable?(%Ash.Resource.Calculation{type: type, calculation: {module, _opts}}, _) do
|
|
!embedded?(type) && module.has_expression?()
|
|
end
|
|
|
|
defp sortable?(%{type: type} = attribute, resource) do
|
|
if Ash.Type.NewType.new_type?(type) do
|
|
sortable?(
|
|
%{
|
|
attribute
|
|
| constraints: Ash.Type.NewType.constraints(type, attribute.constraints),
|
|
type: Ash.Type.NewType.subtype_of(type)
|
|
},
|
|
resource
|
|
)
|
|
else
|
|
!embedded?(type)
|
|
end
|
|
end
|
|
|
|
defp sortable?(_, _), do: false
|
|
|
|
defp relationship_filter_fields(resource, schema) do
|
|
field_names = AshGraphql.Resource.Info.field_names(resource)
|
|
|
|
relationships = AshGraphql.Resource.Info.relationships(resource)
|
|
|
|
resource
|
|
|> Ash.Resource.Info.public_relationships()
|
|
|> Enum.filter(
|
|
&(filterable_and_shown_field?(resource, &1) &&
|
|
AshGraphql.Resource.Info.derive_filter?(&1.destination) &&
|
|
Resource in Spark.extensions(&1.destination) && &1.name in relationships)
|
|
)
|
|
|> Enum.map(fn relationship ->
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
identifier: relationship.name,
|
|
module: schema,
|
|
name: to_string(field_names[relationship.name] || relationship.name),
|
|
description: relationship.description,
|
|
type: resource_filter_type(relationship.destination),
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
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__)
|
|
},
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
identifier: :or,
|
|
module: schema,
|
|
name: "or",
|
|
__reference__: ref(__ENV__),
|
|
type: %Absinthe.Blueprint.TypeReference.List{
|
|
of_type: %Absinthe.Blueprint.TypeReference.NonNull{
|
|
of_type: resource_filter_type(resource)
|
|
}
|
|
}
|
|
},
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
identifier: :not,
|
|
module: schema,
|
|
name: "not",
|
|
type: %Absinthe.Blueprint.TypeReference.List{
|
|
of_type: %Absinthe.Blueprint.TypeReference.NonNull{
|
|
of_type: resource_filter_type(resource)
|
|
}
|
|
},
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
]
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
|
defp resource_sort_field_type(resource) do
|
|
type = AshGraphql.Resource.Info.type(resource)
|
|
String.to_atom(to_string(type) <> "_sort_field")
|
|
end
|
|
|
|
def map_definitions(resource, all_domains, schema, env) do
|
|
if AshGraphql.Resource.Info.type(resource) do
|
|
resource
|
|
|> global_maps(all_domains)
|
|
|> Enum.flat_map(fn attribute ->
|
|
constraints = Ash.Type.NewType.constraints(attribute.type, attribute.constraints)
|
|
|
|
type_name =
|
|
if function_exported?(attribute.type, :graphql_type, 1) do
|
|
attribute.type.graphql_type(attribute.constraints)
|
|
end
|
|
|
|
input_type_name =
|
|
if function_exported?(attribute.type, :graphql_input_type, 1) do
|
|
attribute.type.graphql_input_type(attribute.constraints)
|
|
end
|
|
|
|
[
|
|
type_name
|
|
]
|
|
|> define_map_types(constraints, schema, resource, env)
|
|
|> Enum.concat(
|
|
[
|
|
input_type_name
|
|
]
|
|
|> define_input_map_types(constraints, schema, env)
|
|
)
|
|
end)
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
|
defp define_map_types(type_names, constraints, schema, resource, env) do
|
|
type_names
|
|
|> Enum.filter(& &1)
|
|
|> Enum.flat_map(fn type_name ->
|
|
{types, fields} =
|
|
Enum.reduce(constraints[:fields] || [], {[], []}, fn {name, attribute}, {types, fields} ->
|
|
map_type? =
|
|
attribute[:type] in [:map, Ash.Type.Map, :struct, Ash.Type.Struct] ||
|
|
(Ash.Type.NewType.new_type?(attribute[:type]) &&
|
|
Ash.Type.NewType.subtype_of(attribute[:type]) in [
|
|
:map,
|
|
Ash.Type.Map,
|
|
:struct,
|
|
Ash.Type.Struct
|
|
])
|
|
|
|
if map_type? && attribute[:constraints] not in [nil, []] do
|
|
nested_type_name =
|
|
String.to_atom("#{Atom.to_string(type_name)}_#{Atom.to_string(name)}")
|
|
|
|
{
|
|
define_map_types(
|
|
[nested_type_name],
|
|
attribute[:constraints],
|
|
schema,
|
|
resource,
|
|
env
|
|
) ++ types,
|
|
[
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
module: schema,
|
|
identifier: name,
|
|
__reference__: AshGraphql.Resource.ref(env),
|
|
name: to_string(name),
|
|
middleware:
|
|
middleware_for_field(
|
|
resource,
|
|
%{
|
|
name: name,
|
|
type: attribute[:type],
|
|
constraints: attribute[:constraints] || []
|
|
},
|
|
name,
|
|
attribute[:type],
|
|
attribute[:constraints] || [],
|
|
nil
|
|
),
|
|
type:
|
|
if Keyword.get(
|
|
attribute,
|
|
:allow_nil?,
|
|
true
|
|
) do
|
|
nested_type_name
|
|
else
|
|
%Absinthe.Blueprint.TypeReference.NonNull{
|
|
of_type: nested_type_name
|
|
}
|
|
end
|
|
}
|
|
| fields
|
|
]
|
|
}
|
|
else
|
|
{types,
|
|
[
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
module: schema,
|
|
identifier: name,
|
|
__reference__: AshGraphql.Resource.ref(env),
|
|
name: to_string(name),
|
|
middleware:
|
|
middleware_for_field(
|
|
resource,
|
|
%{
|
|
name: name,
|
|
type: attribute[:type],
|
|
constraints: attribute[:constraints] || []
|
|
},
|
|
name,
|
|
attribute[:type],
|
|
attribute[:constraints] || [],
|
|
nil
|
|
),
|
|
type:
|
|
if Keyword.get(
|
|
attribute,
|
|
:allow_nil?,
|
|
true
|
|
) do
|
|
do_field_type(
|
|
attribute[:type],
|
|
nil,
|
|
nil,
|
|
false,
|
|
Keyword.get(constraints, :constraints) || []
|
|
)
|
|
else
|
|
%Absinthe.Blueprint.TypeReference.NonNull{
|
|
of_type:
|
|
do_field_type(
|
|
attribute[:type],
|
|
nil,
|
|
nil,
|
|
false,
|
|
Keyword.get(constraints, :constraints) || []
|
|
)
|
|
}
|
|
end
|
|
}
|
|
| fields
|
|
]}
|
|
end
|
|
end)
|
|
|
|
[
|
|
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{
|
|
module: schema,
|
|
name: type_name |> to_string() |> Macro.camelize(),
|
|
fields: fields,
|
|
identifier: type_name,
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
| types
|
|
]
|
|
end)
|
|
end
|
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
|
defp define_input_map_types(input_type_names, constraints, schema, env) do
|
|
input_type_names
|
|
|> Enum.filter(& &1)
|
|
|> Enum.flat_map(fn type_name ->
|
|
{types, fields} =
|
|
Enum.reduce(constraints[:fields], {[], []}, fn {name, attribute}, {types, fields} ->
|
|
map_type? =
|
|
attribute[:type] in [:map, Ash.Type.Map, :struct, Ash.Type.Struct] ||
|
|
(Ash.Type.NewType.new_type?(attribute[:type]) &&
|
|
Ash.Type.NewType.subtype_of(attribute[:type]) in [
|
|
:map,
|
|
Ash.Type.Map,
|
|
:struct,
|
|
Ash.Type.Struct
|
|
])
|
|
|
|
if map_type? && attribute[:constraints] not in [nil, []] do
|
|
nested_type_name =
|
|
String.to_atom(
|
|
"#{Atom.to_string(type_name) |> String.replace("_input", "")}_#{Atom.to_string(name)}_input"
|
|
)
|
|
|
|
{
|
|
define_input_map_types(
|
|
[nested_type_name],
|
|
attribute[:constraints] || [],
|
|
schema,
|
|
env
|
|
) ++ types,
|
|
[
|
|
%Absinthe.Blueprint.Schema.InputValueDefinition{
|
|
module: schema,
|
|
identifier: name,
|
|
__reference__: AshGraphql.Resource.ref(env),
|
|
name: to_string(name),
|
|
type:
|
|
if Keyword.get(attribute, :allow_nil?, true) do
|
|
nested_type_name
|
|
else
|
|
%Absinthe.Blueprint.TypeReference.NonNull{
|
|
of_type: nested_type_name
|
|
}
|
|
end
|
|
}
|
|
| fields
|
|
]
|
|
}
|
|
else
|
|
{types,
|
|
[
|
|
%Absinthe.Blueprint.Schema.InputValueDefinition{
|
|
module: schema,
|
|
identifier: name,
|
|
__reference__: AshGraphql.Resource.ref(env),
|
|
name: to_string(name),
|
|
type:
|
|
if Keyword.get(attribute, :allow_nil?, true) do
|
|
do_field_type(
|
|
attribute[:type],
|
|
nil,
|
|
nil,
|
|
true,
|
|
attribute[:constraints] || []
|
|
)
|
|
else
|
|
%Absinthe.Blueprint.TypeReference.NonNull{
|
|
of_type:
|
|
do_field_type(
|
|
attribute[:type],
|
|
nil,
|
|
nil,
|
|
true,
|
|
attribute[:constraints] || []
|
|
)
|
|
}
|
|
end
|
|
}
|
|
| fields
|
|
]}
|
|
end
|
|
end)
|
|
|
|
[
|
|
%Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
|
|
module: schema,
|
|
name: type_name |> to_string() |> Macro.camelize(),
|
|
fields: fields,
|
|
identifier: type_name,
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
| types
|
|
]
|
|
end)
|
|
end
|
|
|
|
def enum_definitions(resource, schema, env) do
|
|
resource = Ash.Type.NewType.subtype_of(resource)
|
|
|
|
if AshGraphql.Resource.Info.type(resource) && AshGraphql.Resource.Info.derive_sort?(resource) do
|
|
sort_values = sort_values(resource)
|
|
|
|
sort_order = %Absinthe.Blueprint.Schema.EnumTypeDefinition{
|
|
module: schema,
|
|
name: resource |> resource_sort_field_type() |> to_string() |> Macro.camelize(),
|
|
identifier: resource_sort_field_type(resource),
|
|
__reference__: ref(__ENV__),
|
|
values:
|
|
Enum.map(sort_values, fn {sort_value_alias, sort_value} ->
|
|
%Absinthe.Blueprint.Schema.EnumValueDefinition{
|
|
module: schema,
|
|
identifier: sort_value_alias,
|
|
__reference__: AshGraphql.Resource.ref(env),
|
|
name: String.upcase(to_string(sort_value_alias)),
|
|
value: sort_value
|
|
}
|
|
end)
|
|
}
|
|
|
|
[sort_order]
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
@doc false
|
|
# sobelow_skip ["RCE.CodeModule", "DOS.BinToAtom", "DOS.StringToAtom"]
|
|
def union_type_definitions(resource, attribute, type_name, schema, env, input_type_name) do
|
|
grapqhl_unnested_unions =
|
|
if function_exported?(attribute.type, :graphql_unnested_unions, 1) do
|
|
attribute.type.graphql_unnested_unions(attribute.constraints)
|
|
else
|
|
[]
|
|
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: nested_union_type_name(attribute, name),
|
|
constraints: config[:constraints]
|
|
},
|
|
resource,
|
|
false
|
|
)}
|
|
end)
|
|
|
|
func_name = :"resolve_gql_union_#{type_name}"
|
|
|
|
{func, _} =
|
|
Code.eval_quoted(
|
|
{:&, [],
|
|
[
|
|
{:/, [],
|
|
[
|
|
{{:., [], [schema, func_name]}, [], []},
|
|
2
|
|
]}
|
|
]},
|
|
[]
|
|
)
|
|
|
|
input_definitions = [
|
|
%Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
|
|
module: schema,
|
|
name: input_type_name |> to_string() |> Macro.camelize(),
|
|
identifier: String.to_atom(to_string(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)
|
|
}
|
|
]
|
|
|
|
object_type_definitions =
|
|
constraints[:types]
|
|
|> Enum.reject(fn {name, _} ->
|
|
name in grapqhl_unnested_unions
|
|
end)
|
|
|> Enum.map(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)
|
|
|
|
[
|
|
%Absinthe.Blueprint.Schema.UnionTypeDefinition{
|
|
module: schema,
|
|
name: type_name |> to_string() |> Macro.camelize(),
|
|
resolve_type: func,
|
|
types:
|
|
Enum.map(constraints[:types], fn {name, _config} ->
|
|
if name in grapqhl_unnested_unions do
|
|
%Absinthe.Blueprint.TypeReference.Name{
|
|
name: to_string(names_to_field_types[name]) |> Macro.camelize()
|
|
}
|
|
else
|
|
%Absinthe.Blueprint.TypeReference.Name{
|
|
name: "#{type_name}_#{name}" |> Macro.camelize()
|
|
}
|
|
end
|
|
end),
|
|
identifier: type_name,
|
|
__reference__: ref(env)
|
|
}
|
|
] ++
|
|
input_definitions ++
|
|
object_type_definitions
|
|
end
|
|
|
|
@doc false
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
|
def nested_union_type_name(attribute, name, existing_only? \\ false) do
|
|
str = "#{attribute.name}_#{name}"
|
|
|
|
if existing_only? do
|
|
String.to_existing_atom(str)
|
|
else
|
|
String.to_atom(str)
|
|
end
|
|
end
|
|
|
|
@doc false
|
|
def global_maps(resource, all_domains) do
|
|
resource
|
|
|> AshGraphql.all_attributes_and_arguments(all_domains, [], false)
|
|
|> Enum.map(&unnest/1)
|
|
|> Enum.filter(
|
|
&(Ash.Type.NewType.subtype_of(&1.type) in [Ash.Type.Map, Ash.Type.Struct] &&
|
|
!Enum.empty?(Ash.Type.NewType.constraints(&1.type, &1.constraints)[:fields] || []) &&
|
|
define_type?(&1.type, &1.constraints))
|
|
)
|
|
end
|
|
|
|
@spec define_type?(Ash.Type.t(), Ash.Type.constraints()) :: boolean()
|
|
def define_type?({:array, type}, constraints), do: define_type?(type, constraints)
|
|
|
|
def define_type?(type, constraints) do
|
|
type = Ash.Type.get_type(type)
|
|
|
|
if function_exported?(type, :graphql_define_type?, 1) do
|
|
type.graphql_define_type?(constraints)
|
|
else
|
|
true
|
|
end
|
|
end
|
|
|
|
defp unnest(%{type: {:array, type}, constraints: constraints} = attribute) do
|
|
unnest(%{attribute | type: type, constraints: constraints[:items] || []})
|
|
end
|
|
|
|
defp unnest(other), do: other
|
|
|
|
@doc false
|
|
def global_unions(resource, all_domains) do
|
|
resource
|
|
|> AshGraphql.all_attributes_and_arguments(all_domains)
|
|
|> Enum.filter(&define_type?(&1.type, &1.constraints))
|
|
|> AshGraphql.only_union_types()
|
|
|> Enum.uniq_by(&elem(&1, 0))
|
|
end
|
|
|
|
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(
|
|
&(AshGraphql.Resource.Info.show_field?(resource, &1.name) && sortable?(&1, resource))
|
|
)
|
|
|> Enum.map(& &1.name)
|
|
|> Enum.uniq()
|
|
|> Enum.map(fn name ->
|
|
{field_names[name] || name, name}
|
|
end)
|
|
end
|
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
|
defp relay_page(type, schema, countable?) 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: %Absinthe.Blueprint.TypeReference.NonNull{
|
|
of_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: %Absinthe.Blueprint.TypeReference.NonNull{
|
|
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
|
|
|
|
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(type, schema, countable?) 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: "Whether 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
|
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
|
defp keyset_page_of(type, schema, countable?) 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
|
|
|
|
defp page_type_definitions(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
|
|
|> Ash.Resource.Info.actions()
|
|
|> Enum.any?(fn action ->
|
|
action.type == :read && action.pagination && action.pagination.countable
|
|
end)
|
|
|
|
if type && paginatable? do
|
|
List.wrap(page_of(type, schema, countable?)) ++
|
|
List.wrap(keyset_page_of(type, schema, countable?)) ++
|
|
List.wrap(relay_page(type, schema, countable?))
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
|
|
def node_type?(type) do
|
|
type.identifier == :node
|
|
end
|
|
|
|
def query_type_definitions(resource, domain, all_domains, schema, relay_ids?) do
|
|
resource_type = AshGraphql.Resource.Info.type(resource)
|
|
|
|
resource
|
|
|> AshGraphql.Resource.Info.queries(domain)
|
|
|> Enum.filter(&(Map.get(&1, :type_name) && &1.type_name != resource_type))
|
|
|> Enum.map(fn query ->
|
|
relay? = Map.get(query, :relay?)
|
|
|
|
# We can implement the Relay node interface only if the resource has a get
|
|
# query using the primary key as identity
|
|
interfaces =
|
|
if relay? and primary_key_get_query(resource, all_domains) != nil do
|
|
[:node]
|
|
else
|
|
[]
|
|
end
|
|
|
|
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{
|
|
description: Ash.Resource.Info.description(resource),
|
|
interfaces: interfaces,
|
|
fields: fields(resource, domain, schema, relay_ids?, query),
|
|
identifier: query.type_name,
|
|
module: schema,
|
|
name: Macro.camelize(to_string(query.type_name)),
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
end)
|
|
end
|
|
|
|
def type_definition(resource, domain, all_domains, schema, relay_ids?) do
|
|
actual_resource = Ash.Type.NewType.subtype_of(resource)
|
|
|
|
if generate_object?(resource) do
|
|
type =
|
|
AshGraphql.Resource.Info.type(resource) ||
|
|
raise """
|
|
Resource #{inspect(resource)} needs to generate its GraphQL type but doesn't have a type
|
|
configured in its `graphql` section.
|
|
|
|
To fix this do one of the following:
|
|
|
|
1) Define the type of your resource with `type :your_resource_type` to let Ash generate it.
|
|
|
|
2) Pass both `generate_object? false` and `type :your_resource_type` to manually define
|
|
your resource type using Absinthe.
|
|
|
|
3) Pass only `generate_object? false` to skip the resource type entirely. This means that
|
|
you can only use actions which don't require the type (e.g. `action` queries).
|
|
"""
|
|
|
|
resource = actual_resource
|
|
|
|
relay? =
|
|
resource
|
|
|> queries(all_domains)
|
|
|> Enum.any?(&Map.get(&1, :relay?))
|
|
|> Kernel.or(relay_ids?)
|
|
|
|
# We can implement the Relay node interface only if the resource has a get
|
|
# query using the primary key as identity
|
|
interfaces =
|
|
if relay? and primary_key_get_query(resource, all_domains) != nil do
|
|
[:node]
|
|
else
|
|
[]
|
|
end
|
|
|
|
case fields(resource, domain, schema, relay_ids?) do
|
|
[] ->
|
|
raise """
|
|
The resource #{inspect(resource)} does not have any visible fields in GraphQL.
|
|
|
|
This typically means that there are no public fields on the resource in question.
|
|
"""
|
|
|
|
fields ->
|
|
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{
|
|
description: Ash.Resource.Info.description(resource),
|
|
interfaces: interfaces,
|
|
fields: fields,
|
|
identifier: type,
|
|
module: schema,
|
|
name: Macro.camelize(to_string(type)),
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
defp fields(resource, domain, schema, relay_ids?, query \\ nil) do
|
|
attributes(resource, domain, schema, relay_ids?) ++
|
|
metadata(query, resource, schema) ++
|
|
relationships(resource, domain, schema) ++
|
|
aggregates(resource, domain, schema) ++
|
|
calculations(resource, domain, 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
|
|
end
|
|
|
|
defp attributes(resource, domain, schema, relay_ids?) 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.name == :id))
|
|
else
|
|
Ash.Resource.Info.public_attributes(resource)
|
|
end
|
|
|
|
attributes =
|
|
attributes
|
|
|> Enum.filter(&AshGraphql.Resource.Info.show_field?(resource, &1.name))
|
|
|> Enum.map(fn attribute ->
|
|
field_type =
|
|
attribute.type
|
|
|> field_type(attribute, resource)
|
|
|> maybe_wrap_non_null(
|
|
not (nullable_field?(resource, attribute.name) or attribute.allow_nil?)
|
|
)
|
|
|
|
name = attribute_names[attribute.name] || attribute.name
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
description: attribute.description,
|
|
identifier: attribute.name,
|
|
module: schema,
|
|
middleware:
|
|
middleware_for_field(
|
|
resource,
|
|
attribute,
|
|
attribute.name,
|
|
attribute.type,
|
|
attribute.constraints,
|
|
domain
|
|
),
|
|
name: to_string(name),
|
|
type: field_type,
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
end)
|
|
|
|
if relay_ids? or AshGraphql.Resource.Info.encode_primary_key?(resource) do
|
|
encoded_id(resource, schema, relay_ids?) ++
|
|
attributes
|
|
else
|
|
attributes
|
|
end
|
|
end
|
|
|
|
defp encoded_id(resource, schema, relay_ids?) do
|
|
case Ash.Resource.Info.primary_key(resource) do
|
|
[] ->
|
|
[]
|
|
|
|
[field] ->
|
|
attribute = Ash.Resource.Info.attribute(resource, field)
|
|
|
|
if attribute.public? do
|
|
[
|
|
%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, relay_ids?}}
|
|
],
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
]
|
|
else
|
|
[]
|
|
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, relay_ids?}}
|
|
],
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
]
|
|
end
|
|
end
|
|
|
|
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?)
|
|
|
|
[
|
|
%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
|
|
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
|
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
|
defp relationships(resource, domain, schema) do
|
|
field_names = AshGraphql.Resource.Info.field_names(resource)
|
|
|
|
relationships = AshGraphql.Resource.Info.relationships(resource)
|
|
|
|
resource
|
|
|> Ash.Resource.Info.public_relationships()
|
|
|> Enum.filter(fn relationship ->
|
|
AshGraphql.Resource.Info.show_field?(resource, relationship.name) &&
|
|
Resource in Spark.extensions(relationship.destination) &&
|
|
relationship.name in relationships &&
|
|
AshGraphql.Resource.Info.type(relationship.destination)
|
|
end)
|
|
|> Enum.map(fn
|
|
%{cardinality: :one} = relationship ->
|
|
name = field_names[relationship.name] || relationship.name
|
|
|
|
type =
|
|
relationship.destination
|
|
|> AshGraphql.Resource.Info.type()
|
|
|> maybe_wrap_non_null(
|
|
not (nullable_field?(resource, relationship.name) or relationship.allow_nil?)
|
|
)
|
|
|
|
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
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
identifier: relationship.name,
|
|
module: schema,
|
|
name: to_string(name),
|
|
description: relationship.description,
|
|
arguments: args(:one_related, relationship.destination, read_action, schema),
|
|
middleware: [
|
|
{{AshGraphql.Graphql.Resolver, :resolve_assoc_one}, {domain, relationship}}
|
|
],
|
|
type: type,
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
|
|
%{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
|
|
|
|
type = AshGraphql.Resource.Info.type(relationship.destination)
|
|
|
|
pagination_strategy =
|
|
relationship_pagination_strategy(resource, relationship.name, read_action)
|
|
|
|
query_type = related_list_type(pagination_strategy, type, resource, relationship)
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
identifier: relationship.name,
|
|
module: schema,
|
|
name: to_string(name),
|
|
description: relationship.description,
|
|
complexity: {AshGraphql.Graphql.Resolver, :query_complexity},
|
|
middleware: [
|
|
{{AshGraphql.Graphql.Resolver, :resolve_assoc_many},
|
|
{domain, relationship, pagination_strategy}}
|
|
],
|
|
arguments:
|
|
related_list_args(
|
|
resource,
|
|
relationship.destination,
|
|
relationship.name,
|
|
read_action,
|
|
schema
|
|
),
|
|
type: query_type,
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
end)
|
|
end
|
|
|
|
defp related_list_type(nil, type, resource, relationship) do
|
|
inner_type = %Absinthe.Blueprint.TypeReference.List{
|
|
of_type: %Absinthe.Blueprint.TypeReference.NonNull{
|
|
of_type: type
|
|
}
|
|
}
|
|
|
|
if nullable_field?(resource, relationship.name) do
|
|
inner_type
|
|
else
|
|
%Absinthe.Blueprint.TypeReference.NonNull{
|
|
of_type: inner_type
|
|
}
|
|
end
|
|
end
|
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
|
defp related_list_type(:relay, type, resource, relationship) do
|
|
inner_type = String.to_atom("#{type}_connection")
|
|
|
|
if nullable_field?(resource, relationship.name) do
|
|
inner_type
|
|
else
|
|
%Absinthe.Blueprint.TypeReference.NonNull{
|
|
of_type: inner_type
|
|
}
|
|
end
|
|
end
|
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
|
defp related_list_type(:keyset, type, resource, relationship) do
|
|
inner_type = String.to_atom("keyset_page_of_#{type}")
|
|
|
|
if nullable_field?(resource, relationship.name) do
|
|
inner_type
|
|
else
|
|
%Absinthe.Blueprint.TypeReference.NonNull{
|
|
of_type: inner_type
|
|
}
|
|
end
|
|
end
|
|
|
|
# sobelow_skip ["DOS.StringToAtom"]
|
|
defp related_list_type(:offset, type, resource, relationship) do
|
|
inner_type = String.to_atom("page_of_#{type}")
|
|
|
|
if nullable_field?(resource, relationship.name) do
|
|
inner_type
|
|
else
|
|
%Absinthe.Blueprint.TypeReference.NonNull{
|
|
of_type: inner_type
|
|
}
|
|
end
|
|
end
|
|
|
|
defp nullable_field?(resource, field) do
|
|
field in AshGraphql.Resource.Info.nullable_fields(resource)
|
|
end
|
|
|
|
defp aggregates(resource, domain, schema) do
|
|
field_names = AshGraphql.Resource.Info.field_names(resource)
|
|
|
|
resource
|
|
|> Ash.Resource.Info.public_aggregates()
|
|
|> Enum.filter(&AshGraphql.Resource.Info.show_field?(resource, &1.name))
|
|
|> Enum.map(fn aggregate ->
|
|
name = field_names[aggregate.name] || aggregate.name
|
|
|
|
{field, field_type, constraints} =
|
|
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.field(related, aggregate.field) do
|
|
{attr, attr.type, attr.constraints}
|
|
else
|
|
_ ->
|
|
{nil, nil, []}
|
|
end
|
|
|
|
{:ok, agg_type, constraints} =
|
|
Aggregate.kind_to_type(aggregate.kind, field_type, constraints)
|
|
|
|
attribute = field || Map.put(aggregate, :constraints, constraints)
|
|
|
|
type =
|
|
if is_nil(Ash.Query.Aggregate.default_value(aggregate.kind)) ||
|
|
nullable_field?(resource, attribute.name) do
|
|
resource =
|
|
if field do
|
|
Ash.Resource.Info.related(resource, aggregate.relationship_path)
|
|
else
|
|
resource
|
|
end
|
|
|
|
field_type(agg_type, attribute, resource)
|
|
else
|
|
resource =
|
|
if field && aggregate.type in [:first, :list] do
|
|
Ash.Resource.Info.related(resource, aggregate.relationship_path)
|
|
else
|
|
resource
|
|
end
|
|
|
|
%Absinthe.Blueprint.TypeReference.NonNull{
|
|
of_type: field_type(agg_type, attribute, resource)
|
|
}
|
|
end
|
|
|
|
%Absinthe.Blueprint.Schema.FieldDefinition{
|
|
identifier: aggregate.name,
|
|
module: schema,
|
|
middleware:
|
|
middleware_for_field(resource, aggregate, aggregate.name, agg_type, constraints, domain),
|
|
name: to_string(name),
|
|
description: aggregate.description,
|
|
type: type,
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
end)
|
|
end
|
|
|
|
defp middleware_for_field(resource, field, name, {:array, type}, constraints, domain) do
|
|
middleware_for_field(resource, field, name, type, constraints, domain)
|
|
end
|
|
|
|
defp middleware_for_field(resource, field, name, type, constraints, domain) do
|
|
if Ash.Type.NewType.new_type?(type) &&
|
|
Ash.Type.NewType.subtype_of(type) == Ash.Type.Union &&
|
|
function_exported?(type, :graphql_unnested_unions, 1) do
|
|
unnested_types = type.graphql_unnested_unions(constraints)
|
|
|
|
[
|
|
{{AshGraphql.Graphql.Resolver, :resolve_union},
|
|
{name, type, field, resource, unnested_types, domain}}
|
|
]
|
|
else
|
|
[
|
|
{{AshGraphql.Graphql.Resolver, :resolve_attribute}, {name, type, constraints, domain}}
|
|
]
|
|
end
|
|
end
|
|
|
|
defp calculations(resource, domain, schema) do
|
|
field_names = AshGraphql.Resource.Info.field_names(resource)
|
|
|
|
resource
|
|
|> Ash.Resource.Info.public_calculations()
|
|
|> Enum.filter(&AshGraphql.Resource.Info.show_field?(resource, &1.name))
|
|
|> 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}, {domain, resource, calculation}}
|
|
],
|
|
name: to_string(name),
|
|
description: calculation.description,
|
|
type: field_type,
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
end)
|
|
end
|
|
|
|
defp calculation_type(calculation, resource) do
|
|
calculation.type
|
|
|> Ash.Type.get_type()
|
|
|> field_type(calculation, resource)
|
|
|> maybe_wrap_non_null(
|
|
not (nullable_field?(resource, calculation.name) or 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),
|
|
# Will be replaced with `argument.description`.
|
|
description: Map.get(argument, :description),
|
|
type: type,
|
|
__reference__: ref(__ENV__)
|
|
}
|
|
end)
|
|
end
|
|
|
|
@doc false
|
|
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?, constraints \\ nil)
|
|
|
|
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
|
|
%Absinthe.Blueprint.TypeReference.List{
|
|
of_type: do_field_type(type, aggregate, resource, input?)
|
|
}
|
|
end
|
|
|
|
defp do_field_type({:array, type}, nil, resource, input?, constraints) do
|
|
field_type = do_field_type(type, nil, resource, input?, constraints[:items] || [])
|
|
|
|
%Absinthe.Blueprint.TypeReference.List{
|
|
of_type: field_type
|
|
}
|
|
end
|
|
|
|
defp do_field_type({:array, type}, attribute, resource, input?, _) do
|
|
new_constraints = attribute.constraints[:items] || []
|
|
new_attribute = %{attribute | constraints: new_constraints, type: type}
|
|
|
|
field_type =
|
|
type
|
|
|> do_field_type(new_attribute, resource, input?)
|
|
|> maybe_wrap_non_null(!attribute.constraints[:nil_items?] || embedded?(attribute.type))
|
|
|
|
%Absinthe.Blueprint.TypeReference.List{
|
|
of_type: field_type
|
|
}
|
|
end
|
|
|
|
# sobelow_skip ["DOS.BinToAtom"]
|
|
defp do_field_type(type, attribute, resource, input?, constraints) do
|
|
type = Ash.Type.get_type(type)
|
|
|
|
constraints =
|
|
case attribute do
|
|
%{constraints: constraints} -> constraints
|
|
_ -> constraints
|
|
end || []
|
|
|
|
if Ash.Type.builtin?(type) do
|
|
get_specific_field_type(type, attribute, resource, input?)
|
|
else
|
|
if Ash.Resource.Info.resource?(type) && !Ash.Resource.Info.embedded?(type) do
|
|
if input? do
|
|
Application.get_env(:ash_graphql, :json_type) || :json_string
|
|
else
|
|
AshGraphql.Resource.Info.type(type) || Application.get_env(:ash_graphql, :json_type) ||
|
|
:json_string
|
|
end
|
|
else
|
|
if Ash.Type.embedded_type?(type) do
|
|
if input? && type(type) do
|
|
case embedded_type_input(resource, attribute, type, nil) do
|
|
nil ->
|
|
IO.warn(
|
|
"Embedded type #{inspect(type)} does not have an input type defined, but is accepted as input in at least one location."
|
|
)
|
|
|
|
Application.get_env(:ash_graphql, :json_type) || :json_string
|
|
|
|
type ->
|
|
type.identifier
|
|
end
|
|
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
|
|
if function_exported?(type, :graphql_type, 1) do
|
|
type.graphql_type(constraints)
|
|
else
|
|
:string
|
|
end
|
|
else
|
|
function =
|
|
if input? do
|
|
:graphql_input_type
|
|
else
|
|
:graphql_type
|
|
end
|
|
|
|
cond do
|
|
function_exported?(type, function, 1) ->
|
|
apply(type, function, [constraints])
|
|
|
|
input? && Ash.Type.NewType.new_type?(type) &&
|
|
Ash.Type.NewType.subtype_of(type) == Ash.Type.Union &&
|
|
function_exported?(type, :graphql_type, 1) ->
|
|
:"#{type.graphql_type(constraints)}_input"
|
|
|
|
true ->
|
|
if Ash.Type.NewType.new_type?(type) do
|
|
do_field_type(
|
|
type.subtype_of(),
|
|
%{
|
|
attribute
|
|
| type: type.subtype_of(),
|
|
constraints:
|
|
type.type_constraints(
|
|
constraints,
|
|
type.subtype_constraints()
|
|
)
|
|
},
|
|
resource,
|
|
input?
|
|
)
|
|
else
|
|
raise """
|
|
Could not determine graphql type for #{inspect(type)}, please define: #{function}/1!
|
|
"""
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
defp get_specific_field_type(
|
|
Ash.Type.Atom,
|
|
_,
|
|
_resource,
|
|
_input?
|
|
) do
|
|
:string
|
|
end
|
|
|
|
defp get_specific_field_type(
|
|
Ash.Type.Map,
|
|
_attribute,
|
|
_resource,
|
|
_input?
|
|
) do
|
|
Application.get_env(:ash_graphql, :json_type) || :json_string
|
|
end
|
|
|
|
defp get_specific_field_type(Ash.Type.Boolean, _, _, _), do: :boolean
|
|
|
|
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.String, _, _, _), do: :string
|
|
defp get_specific_field_type(Ash.Type.Term, _, _, _), do: :string
|
|
|
|
defp get_specific_field_type(Ash.Type.DateTime, _, _, _),
|
|
do: Application.get_env(:ash, :utc_datetime_type) || :datetime
|
|
|
|
defp get_specific_field_type(Ash.Type.UtcDatetime, _, _, _),
|
|
do: Application.get_env(:ash, :utc_datetime_type) || :datetime
|
|
|
|
defp get_specific_field_type(Ash.Type.UtcDatetimeUsec, _, _, _),
|
|
do: Application.get_env(:ash, :utc_datetime_type) || :datetime
|
|
|
|
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(Ash.Type.Struct, %{constraints: constraints}, resource, input?) do
|
|
type =
|
|
if !input? && constraints[:instance_of] &&
|
|
Ash.Resource.Info.resource?(constraints[:instance_of]) do
|
|
AshGraphql.Resource.Info.type(constraints[:instance_of])
|
|
end
|
|
|
|
type || get_specific_field_type(Ash.Type.Map, %{constraints: constraints}, resource, input?)
|
|
end
|
|
|
|
defp get_specific_field_type(Ash.Type.File, _, _, _), do: :upload
|
|
|
|
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 an `Ash.Type.Enum` or a custom type, you can add `def graphql_type/1` or `def graphql_input_type/1`
|
|
to your type. This does not *define* the type, but tells us what type to use for it, so you may need
|
|
to add a type to your absinthe schema if it does not map to a built in absinthe data type.
|
|
|
|
## Ash.Type.NewType
|
|
|
|
The exception to the above are special instances of `Ash.Type.NewType`. If you have an `Ash.Type.NewType`,
|
|
that is a subset of `:union` or `:map`(with the `:fields` constraint), AshGraphql will define that type for you.
|
|
If you are seeing this message, it likely means that you are missing `def graphql_type/1` or `def graphql_input_type/1`
|
|
on your type definition.
|
|
"""
|
|
end
|
|
|
|
def primary_key_get_query(resource, all_domains) do
|
|
# Find the get query with no identities, i.e. the one that uses the primary key
|
|
resource
|
|
|> AshGraphql.Resource.Info.queries(all_domains)
|
|
|> Enum.find(&(&1.type == :get and (&1.identity == nil or &1.identity == false)))
|
|
end
|
|
|
|
def embedded?({:array, resource_or_type}) do
|
|
embedded?(resource_or_type)
|
|
end
|
|
|
|
def embedded?(resource_or_type) do
|
|
if Ash.Resource.Info.resource?(resource_or_type) do
|
|
Ash.Resource.Info.embedded?(resource_or_type)
|
|
else
|
|
Ash.Type.embedded_type?(resource_or_type)
|
|
end
|
|
end
|
|
end
|