feat: support embedded resources

This commit is contained in:
Zach Daniel 2021-01-12 15:14:35 -05:00
parent 9b12983ef8
commit a820beb474
8 changed files with 552 additions and 188 deletions

View file

@ -11,6 +11,7 @@ locals_without_parens = [
identity: 1,
list: 2,
list: 3,
primary_key_delimiter: 1,
type: 1,
update: 2,
update: 3

View file

@ -18,7 +18,7 @@ jobs:
matrix:
otp: ["23", "22"]
elixir: ["1.10.3"]
ash: ["master", "1.26.11"]
ash: ["master", "1.28.0"]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ASH_VERSION: ${{matrix.ash}}

View file

@ -54,8 +54,11 @@ defmodule AshGraphql do
type_definitions =
if unquote(first?) do
embedded_types = AshGraphql.get_embedded_types(unquote(apis))
AshGraphql.Api.global_type_definitions(__MODULE__) ++
AshGraphql.Api.type_definitions(api, __MODULE__)
AshGraphql.Api.type_definitions(api, __MODULE__) ++
embedded_types
else
AshGraphql.Api.type_definitions(api, __MODULE__)
end
@ -64,8 +67,7 @@ defmodule AshGraphql do
List.update_at(blueprint_with_mutations.schema_definitions, 0, fn schema_def ->
%{
schema_def
| imports: [{Absinthe.Type.Custom, []} | List.wrap(schema_def.imports)],
type_definitions: schema_def.type_definitions ++ type_definitions
| type_definitions: schema_def.type_definitions ++ type_definitions
}
end)
@ -73,11 +75,67 @@ defmodule AshGraphql do
end
end
if first? do
import_types(Absinthe.Type.Custom)
import_types(AshGraphql.Types.JSON)
end
@pipeline_modifier Module.concat(api, AshTypes)
end
end
end
def get_embedded_types(apis) do
apis
|> Enum.map(&elem(&1, 0))
|> Enum.flat_map(&Ash.Api.resources/1)
|> Enum.flat_map(&Ash.Resource.attributes/1)
|> Enum.map(& &1.type)
|> Enum.filter(&Ash.Type.embedded_type?/1)
|> Enum.map(fn
{:array, resource} ->
resource
resource ->
resource
end)
|> Enum.filter(&(AshGraphql.Resource in Ash.Resource.extensions(&1)))
|> Enum.flat_map(fn type ->
[type] ++ get_nested_embedded_types(type)
end)
|> Enum.flat_map(fn embedded_type ->
[
AshGraphql.Resource.type_definition(
embedded_type,
Module.concat(embedded_type, ShadowApi),
__MODULE__
),
AshGraphql.Resource.embedded_type_input(
embedded_type,
__MODULE__
)
]
end)
end
defp get_nested_embedded_types(embedded_type) do
embedded_type
|> Ash.Resource.attributes()
|> Enum.map(& &1.type)
|> Enum.filter(&Ash.Type.embedded_type?/1)
|> Enum.map(fn
{:array, resource} ->
resource
resource ->
resource
end)
|> Enum.filter(&(AshGraphql.Resource in Ash.Resource.extensions(&1)))
|> Enum.flat_map(fn type ->
[type] ++ get_nested_embedded_types(type)
end)
end
def add_context(ctx, apis) do
dataloader =
apis

View file

@ -15,24 +15,34 @@ defmodule AshGraphql.Graphql.Resolver do
filter =
if identity do
{:ok,
resource
|> Ash.Resource.identities()
|> Enum.find(&(&1.name == identity))
|> Map.get(:keys)
|> Enum.map(fn key ->
{key, Map.get(arguments, key)}
end)
end)}
else
[id: Map.get(arguments, :id)]
case AshGraphql.Resource.decode_primary_key(resource, Map.get(arguments, :id) || "") do
{:ok, value} -> {:ok, [id: value]}
{:error, error} -> {:error, error}
end
end
result =
case filter do
{:ok, filter} ->
resource
|> Ash.Query.new()
|> Ash.Query.set_tenant(Map.get(context, :tenant))
|> Ash.Query.filter(^filter)
|> api.read_one(opts)
{:error, error} ->
{:error, error}
end
Absinthe.Resolution.put_result(resolution, to_resolution(result))
end
@ -118,10 +128,7 @@ defmodule AshGraphql.Graphql.Resolver do
changeset = Ash.Changeset.new(resource, attributes)
changeset_with_relationships =
Enum.reduce(relationships, changeset, fn {relationship, replacement}, changeset ->
Ash.Changeset.replace_relationship(changeset, relationship, replacement)
end)
changeset_with_relationships = changeset_with_relationships(relationships, changeset)
opts = [
actor: Map.get(context, :actor),
@ -151,17 +158,23 @@ defmodule AshGraphql.Graphql.Resolver do
) do
filter =
if identity do
{:ok,
resource
|> Ash.Resource.identities()
|> Enum.find(&(&1.name == identity))
|> Map.get(:keys)
|> Enum.map(fn key ->
{key, Map.get(arguments, key)}
end)
end)}
else
[id: Map.get(arguments, :id)]
case AshGraphql.Resource.decode_primary_key(resource, Map.get(arguments, :id) || "") do
{:ok, value} -> {:ok, [id: value]}
{:error, error} -> {:error, error}
end
end
case filter do
{:ok, filter} ->
resource
|> Ash.Query.filter(^filter)
|> Ash.Query.set_tenant(Map.get(context, :tenant))
@ -174,10 +187,7 @@ defmodule AshGraphql.Graphql.Resolver do
{attributes, relationships, arguments} = split_attrs_rels_and_args(input, resource)
changeset = Ash.Changeset.new(initial, attributes)
changeset_with_relationships =
Enum.reduce(relationships, changeset, fn {relationship, replacement}, changeset ->
Ash.Changeset.replace_relationship(changeset, relationship, replacement)
end)
changeset_with_relationships = changeset_with_relationships(relationships, changeset)
opts = [
actor: Map.get(context, :actor),
@ -190,16 +200,14 @@ defmodule AshGraphql.Graphql.Resolver do
|> Ash.Changeset.set_tenant(Map.get(context, :tenant))
|> Ash.Changeset.set_arguments(arguments)
|> api.update(opts)
|> case do
{:ok, value} ->
{:ok, %{result: value, errors: []}}
{:error, error} ->
{:ok, %{result: nil, errors: List.wrap(error)}}
end
|> update_result()
Absinthe.Resolution.put_result(resolution, to_resolution(result))
end
{:error, error} ->
Absinthe.Resolution.put_result(resolution, to_resolution({:error, error}))
end
end
def mutate(
@ -208,17 +216,23 @@ defmodule AshGraphql.Graphql.Resolver do
) do
filter =
if identity do
{:ok,
resource
|> Ash.Resource.identities()
|> Enum.find(&(&1.name == identity))
|> Map.get(:keys)
|> Enum.map(fn key ->
{key, Map.get(arguments, key)}
end)
end)}
else
[id: Map.get(arguments, :id)]
case AshGraphql.Resource.decode_primary_key(resource, Map.get(arguments, :id) || "") do
{:ok, value} -> {:ok, [id: value]}
{:error, error} -> {:error, error}
end
end
case filter do
{:ok, filter} ->
resource
|> Ash.Query.filter(^filter)
|> Ash.Query.set_tenant(Map.get(context, :tenant))
@ -228,25 +242,83 @@ defmodule AshGraphql.Graphql.Resolver do
{:ok, %{result: nil, errors: [to_errors("not found")]}}
initial ->
opts =
if AshGraphql.Api.authorize?(api) do
[actor: Map.get(context, :actor), action: action]
else
[action: action]
end
opts = destroy_opts(api, context, action)
result =
initial
|> Ash.Changeset.new()
|> Ash.Changeset.set_tenant(Map.get(context, :tenant))
|> api.destroy(opts)
|> case do
:ok -> {:ok, %{result: initial, errors: []}}
{:error, error} -> {:ok, %{result: nil, errors: to_errors(error)}}
end
|> destroy_result(initial)
Absinthe.Resolution.put_result(resolution, to_resolution(result))
end
{:error, error} ->
Absinthe.Resolution.put_result(resolution, to_resolution({:error, error}))
end
end
defp destroy_opts(api, context, action) do
if AshGraphql.Api.authorize?(api) do
[actor: Map.get(context, :actor), action: action]
else
[action: action]
end
end
defp update_result(result) do
case result do
{:ok, value} ->
{:ok, %{result: value, errors: []}}
{:error, error} ->
{:ok, %{result: nil, errors: List.wrap(error)}}
end
end
defp destroy_result(result, initial) do
case result do
:ok -> {:ok, %{result: initial, errors: []}}
{:error, error} -> {:ok, %{result: nil, errors: to_errors(error)}}
end
end
defp changeset_with_relationships(relationships, changeset) do
Enum.reduce(relationships, changeset, fn {relationship, replacement}, changeset ->
case decode_related_pkeys(changeset, relationship, replacement) do
{:ok, replacement} ->
Ash.Changeset.replace_relationship(changeset, relationship, replacement)
{:error, _error} ->
Ash.Changeset.add_error(changeset, "Invalid relationship primary keys")
end
end)
end
defp decode_related_pkeys(changeset, relationship, primary_keys)
when is_list(primary_keys) do
primary_keys
|> Enum.reduce_while({:ok, []}, fn pkey, {:ok, list} ->
case AshGraphql.Resource.decode_primary_key(
Ash.Resource.related(changeset.resource, relationship),
pkey
) do
{:ok, value} -> {:cont, {:ok, [value | list]}}
{:error, error} -> {:halt, {:error, error}}
end
end)
|> case do
{:ok, values} -> {:ok, Enum.reverse(values)}
{:error, error} -> {:error, error}
end
end
defp decode_related_pkeys(changeset, relationship, primary_key) do
AshGraphql.Resource.decode_primary_key(
Ash.Resource.related(changeset.resource, relationship),
primary_key
)
end
defp split_attrs_rels_and_args(input, resource) do

View file

@ -142,6 +142,11 @@ defmodule AshGraphql.Resource do
type: :atom,
required: true,
doc: "The type to use for this entity in the graphql schema"
],
primary_key_delimiter: [
type: :string,
doc:
"If a composite primary key exists, this must be set to determine the `id` field value"
]
],
sections: [
@ -179,6 +184,44 @@ defmodule AshGraphql.Resource do
Extension.get_opt(resource, [:graphql], :type, nil)
end
def primary_key_delimiter(resource) do
Extension.get_opt(resource, [:graphql], :primary_key_delimiter, [], false)
end
def encode_primary_key(%resource{} = record) do
case Ash.Resource.primary_key(resource) do
[field] ->
Map.get(record, field)
keys ->
delimiter = primary_key_delimiter(resource)
[_ | concatenated_keys] =
keys
|> Enum.reverse()
|> Enum.reduce([], fn key, acc -> [delimiter, to_string(Map.get(record, key)), acc] end)
IO.iodata_to_binary(concatenated_keys)
end
end
def decode_primary_key(resource, value) do
case Ash.Resource.primary_key(resource) do
[_field] ->
{:ok, value}
fields ->
delimiter = primary_key_delimiter(resource)
parts = String.split(value, delimiter)
if Enum.count(parts) == Enum.count(fields) do
{:ok, Enum.zip(fields, parts)}
else
{:error, "Invalid primary key"}
end
end
end
@doc false
def queries(api, resource, schema) do
type = Resource.type(resource)
@ -190,7 +233,7 @@ defmodule AshGraphql.Resource do
query_action = Ash.Resource.action(resource, query.action, :read)
%Absinthe.Blueprint.Schema.FieldDefinition{
arguments: args(query.type, resource, query_action, query.identity),
arguments: args(query.type, resource, query_action, schema, query.identity),
identifier: query.name,
middleware: [
{{AshGraphql.Graphql.Resolver, :resolve}, {api, resource, query}}
@ -356,6 +399,35 @@ defmodule AshGraphql.Resource do
end)
end
@doc false
# sobelow_skip ["DOS.StringToAtom"]
def embedded_type_input(resource, schema) do
attribute_fields =
resource
|> Ash.Resource.public_attributes()
|> Enum.filter(& &1.writable?)
|> Enum.map(fn attribute ->
type = field_type(attribute.type, attribute, resource)
%Absinthe.Blueprint.Schema.FieldDefinition{
description: attribute.description,
identifier: attribute.name,
module: schema,
name: to_string(attribute.name),
type: type
}
end)
name = AshGraphql.Resource.type(resource)
%Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
fields: attribute_fields,
identifier: String.to_atom("#{name}_input"),
module: schema,
name: Macro.camelize("#{name}_input")
}
end
defp mutation_fields(resource, schema, mutation) do
attribute_fields =
resource
@ -365,7 +437,7 @@ defmodule AshGraphql.Resource do
end)
|> Enum.filter(& &1.writable?)
|> Enum.map(fn attribute ->
type = field_type(attribute.type, attribute, resource)
type = field_type(attribute.type, attribute, resource, true)
field_type =
if attribute.allow_nil? || attribute.default || mutation.type == :update do
@ -436,10 +508,10 @@ defmodule AshGraphql.Resource do
type =
if argument.allow_nil? do
%Absinthe.Blueprint.TypeReference.NonNull{
of_type: field_type(argument.type, argument, resource)
of_type: field_type(argument.type, argument, resource, true)
}
else
field_type(argument.type, argument, resource)
field_type(argument.type, argument, resource, true)
end
%Absinthe.Blueprint.Schema.FieldDefinition{
@ -469,9 +541,9 @@ defmodule AshGraphql.Resource do
end
end
defp args(action_type, resource, action, identity \\ nil)
defp args(action_type, resource, action, schema, identity \\ nil)
defp args(:get, _resource, _action, nil) do
defp args(:get, _resource, _action, _schema, nil) do
[
%Absinthe.Blueprint.Schema.InputValueDefinition{
name: "id",
@ -482,7 +554,7 @@ defmodule AshGraphql.Resource do
]
end
defp args(:get, resource, _action, identity) do
defp args(:get, resource, _action, _schema, identity) do
resource
|> Ash.Resource.identities()
|> Enum.find(&(&1.name == identity))
@ -494,21 +566,37 @@ defmodule AshGraphql.Resource do
name: to_string(key),
identifier: key,
type: %Absinthe.Blueprint.TypeReference.NonNull{
of_type: field_type(attribute.type, attribute, resource)
of_type: field_type(attribute.type, attribute, resource, true)
},
description: attribute.description || ""
}
end)
end
defp args(:list, resource, action, _) do
defp args(:list, resource, action, schema, _) do
args =
case resource_filter_fields(resource, schema) do
[] ->
[]
_ ->
[
%Absinthe.Blueprint.Schema.InputValueDefinition{
name: "filter",
identifier: :filter,
type: resource_filter_type(resource),
description: "A filter to limit the results"
},
}
]
end
args =
case sort_values(resource) do
[] ->
args
_ ->
[
%Absinthe.Blueprint.Schema.InputValueDefinition{
name: "sort",
identifier: :sort,
@ -517,8 +605,11 @@ defmodule AshGraphql.Resource do
},
description: "How to sort the records in the response"
}
] ++
pagination_args(action)
| args
]
end
args ++ pagination_args(action)
end
defp pagination_args(action) do
@ -609,10 +700,10 @@ defmodule AshGraphql.Resource do
@doc false
def type_definitions(resource, api, schema) do
[
type_definition(resource, api, schema),
sort_input(resource, schema),
filter_input(resource, schema)
type_definition(resource, api, schema)
] ++
List.wrap(sort_input(resource, schema)) ++
List.wrap(filter_input(resource, schema)) ++
filter_field_types(resource, schema) ++
List.wrap(page_of(resource, schema)) ++ enum_definitions(resource, schema)
end
@ -671,7 +762,7 @@ defmodule AshGraphql.Resource do
identifier: operator.name(),
module: schema,
name: to_string(operator.name()),
type: field_type(type, attribute_or_aggregate, resource)
type: field_type(type, attribute_or_aggregate, resource, true)
}
]
else
@ -710,7 +801,7 @@ defmodule AshGraphql.Resource do
identifier: operator.name(),
module: schema,
name: to_string(operator.name()),
type: field_type(type, attribute_or_aggregate, resource)
type: field_type(type, attribute_or_aggregate, resource, true)
}
]
else
@ -751,6 +842,11 @@ defmodule AshGraphql.Resource do
defp constraints_to_item_constraints(_, attribute_or_aggregate), do: attribute_or_aggregate
defp sort_input(resource, schema) do
case sort_values(resource) do
[] ->
nil
_ ->
%Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
fields: [
%Absinthe.Blueprint.Schema.FieldDefinition{
@ -772,15 +868,22 @@ defmodule AshGraphql.Resource do
name: resource |> resource_sort_type() |> to_string() |> Macro.camelize()
}
end
end
defp filter_input(resource, schema) 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: resource_filter_fields(resource, schema)
fields: fields
}
end
end
defp resource_filter_fields(resource, schema) do
boolean_filter_fields(resource, schema) ++
@ -791,6 +894,14 @@ defmodule AshGraphql.Resource do
defp attribute_filter_fields(resource, schema) do
resource
|> Ash.Resource.public_attributes()
|> Enum.reject(fn
{:array, _} ->
true
_ ->
false
end)
|> Enum.reject(&Ash.Type.embedded_type?/1)
|> Enum.flat_map(fn attribute ->
[
%Absinthe.Blueprint.Schema.FieldDefinition{
@ -894,10 +1005,7 @@ defmodule AshGraphql.Resource do
}
end)
attribute_sort_values = Enum.map(Ash.Resource.attributes(resource), & &1.name)
aggregate_sort_values = Enum.map(Ash.Resource.aggregates(resource), & &1.name)
sort_values = attribute_sort_values ++ aggregate_sort_values
sort_values = sort_values(resource)
sort_order = %Absinthe.Blueprint.Schema.EnumTypeDefinition{
module: schema,
@ -917,6 +1025,40 @@ defmodule AshGraphql.Resource do
[sort_order | atom_enums]
end
defp sort_values(resource) do
attribute_sort_values =
resource
|> Ash.Resource.attributes()
|> Enum.reject(fn
%{type: {:array, _}} ->
false
_ ->
true
end)
|> Enum.reject(&Ash.Type.embedded_type?(&1.type))
|> Enum.map(& &1.name)
aggregate_sort_values =
resource
|> Ash.Resource.aggregates()
|> Enum.reject(fn aggregate ->
case Ash.Query.Aggregate.kind_to_type(aggregate.kind, nil) do
{:ok, {:array, _}} ->
true
{:ok, type} ->
Ash.Type.embedded_type?(type)
_ ->
true
end
end)
|> Enum.map(& &1.name)
attribute_sort_values ++ aggregate_sort_values
end
# sobelow_skip ["DOS.StringToAtom"]
defp page_of(resource, schema) do
type = Resource.type(resource)
@ -960,7 +1102,7 @@ defmodule AshGraphql.Resource do
end
end
defp type_definition(resource, api, schema) do
def type_definition(resource, api, schema) do
type = Resource.type(resource)
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{
@ -980,19 +1122,11 @@ defmodule AshGraphql.Resource do
end
defp attributes(resource, schema) do
non_id_attributes =
resource
|> Ash.Resource.public_attributes()
|> Enum.map(fn
%{name: :id} = attribute ->
%Absinthe.Blueprint.Schema.FieldDefinition{
description: attribute.description,
identifier: :id,
module: schema,
name: "id",
type: %Absinthe.Blueprint.TypeReference.NonNull{of_type: :id}
}
attribute ->
|> Enum.reject(& &1.primary_key?)
|> Enum.map(fn attribute ->
field_type = field_type(attribute.type, attribute, resource)
field_type =
@ -1012,6 +1146,78 @@ defmodule AshGraphql.Resource do
type: field_type
}
end)
pkey_fields =
case Ash.Resource.primary_key(resource) do
[field] ->
attribute = Ash.Resource.attribute(resource, field)
if attribute.private? do
non_id_attributes
else
field_type = field_type(attribute.type, attribute, resource)
field_type =
if attribute.allow_nil? do
field_type
else
%Absinthe.Blueprint.TypeReference.NonNull{
of_type: field_type
}
end
[
%Absinthe.Blueprint.Schema.FieldDefinition{
description: attribute.description,
identifier: attribute.name,
module: schema,
name: to_string(attribute.name),
type: field_type
}
]
end
fields ->
added_pkey_fields =
if :id in fields do
[]
else
for field <- fields do
attribute = Ash.Resource.attribute(resource, field)
field_type = field_type(attribute.type, attribute, resource)
field_type =
if attribute.allow_nil? do
field_type
else
%Absinthe.Blueprint.TypeReference.NonNull{
of_type: field_type
}
end
%Absinthe.Blueprint.Schema.FieldDefinition{
description: attribute.description,
identifier: attribute.name,
module: schema,
name: to_string(attribute.name),
type: field_type
}
end
end
[
%Absinthe.Blueprint.Schema.FieldDefinition{
description: "The primary key of the resource",
identifier: :id,
module: schema,
name: "id",
type: :id
}
] ++ added_pkey_fields
end
non_id_attributes ++ pkey_fields
end
# sobelow_skip ["DOS.StringToAtom"]
@ -1063,7 +1269,7 @@ defmodule AshGraphql.Resource do
middleware: [
{{AshGraphql.Graphql.Resolver, :resolve_assoc}, {api, relationship}}
],
arguments: args(:list, relationship.destination, read_action),
arguments: args(:list, relationship.destination, read_action, schema),
type: query_type
}
end)
@ -1102,36 +1308,53 @@ defmodule AshGraphql.Resource do
end)
end
defp field_type({:array, type}, %Ash.Resource.Aggregate{} = aggregate, resource) do
defp field_type(type, field, resource, input? \\ false)
defp field_type({:array, type}, %Ash.Resource.Aggregate{} = aggregate, resource, input?) do
%Absinthe.Blueprint.TypeReference.List{
of_type: field_type(type, aggregate, resource)
of_type: field_type(type, aggregate, resource, input?)
}
end
defp field_type({:array, type}, attribute, resource) do
defp field_type({:array, type}, attribute, resource, input?) do
new_constraints = attribute.constraints[:items] || []
new_attribute = %{attribute | constraints: new_constraints, type: type}
if attribute.constraints[:nil_items?] do
%Absinthe.Blueprint.TypeReference.List{
of_type: field_type(type, new_attribute, resource)
of_type: field_type(type, new_attribute, resource, input?)
}
else
%Absinthe.Blueprint.TypeReference.List{
of_type: %Absinthe.Blueprint.TypeReference.NonNull{
of_type: field_type(type, new_attribute, resource)
of_type: field_type(type, new_attribute, resource, input?)
}
}
end
end
defp field_type(type, attribute, resource) do
# sobelow_skip ["DOS.BinToAtom"]
defp field_type(type, attribute, resource, input?) do
if Ash.Type.builtin?(type) do
do_field_type(type, attribute, resource)
else
if Ash.Type.embedded_type?(type) do
case type(type) do
nil ->
:json
type ->
if input? do
:"#{type}_input"
else
type
end
end
else
type.graphql_type(attribute, resource)
end
end
end
defp do_field_type(Ash.Type.Atom, %{constraints: constraints, name: name}, resource) do
if is_list(constraints[:one_of]) do

View file

@ -4,17 +4,27 @@ defmodule AshGraphql.Resource.Transformers.RequireIdPkey do
alias Ash.Dsl.Transformer
def transform(_resource, dsl) do
def transform(resource, dsl) do
if Ash.Type.embedded_type?(resource) do
{:ok, dsl}
else
primary_key =
dsl
|> Transformer.get_entities([:attributes])
|> Enum.filter(& &1.primary_key?)
|> Enum.map(& &1.name)
unless primary_key == [:id] do
raise "AshGraphql currently requires the primary key to be a field called `id`"
end
case primary_key do
[_single] ->
{:ok, dsl}
[_ | _] ->
if AshGraphql.Resource.primary_key_delimiter(resource) do
{:ok, dsl}
else
{:error,
"AshGraphql requires a `primary_key_delimiter` to be set for composite primary keys."}
end
end
end
end
end

View file

@ -78,7 +78,7 @@ defmodule AshGraphql.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:ash, ash_version("~> 1.26.11")},
{:ash, ash_version("~> 1.28")},
{:absinthe_plug, "~> 1.4"},
{:absinthe, "~> 1.5.3"},
{:dataloader, "~> 1.0"},

View file

@ -1,13 +1,13 @@
%{
"absinthe": {:hex, :absinthe, "1.5.3", "d255e6d825e63abd9ff22b6d2423540526c9d699f46b712aa76f4b9c06116ff9", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69a170f3a8630b2ca489367bc2aeeabd84e15cbd1e86fe8741b05885fda32a2e"},
"absinthe": {:hex, :absinthe, "1.5.5", "22b26228f56dc6a1074c52cea9c64e869a0cb2427403bcf9056c422d36c66292", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "41e79ed4bffbab4986493ff4120c948d59871fd08ad5e31195129ce3c01aad58"},
"absinthe_plug": {:hex, :absinthe_plug, "1.5.0", "018ef544cf577339018d1f482404b4bed762e1b530c78be9de4bbb88a6f3a805", [:mix], [{:absinthe, "~> 1.5.0", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.2 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "4c160f4ce9a1233a4219a42de946e4e05d0e8733537cd5d8d20e7d4ef8d4b7c7"},
"ash": {:hex, :ash, "1.26.11", "c9cd5d01eca22a49e4d0fff0a638e1a86294c231db0b9183c149a9b8c9fd5cbb", [:mix], [{:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.3.5", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.1.5", [hex: :picosat_elixir, repo: "hexpm", optional: false]}], "hexpm", "b289c064bb83d4feff114d2229c70cab04b5e8f4c3bb0c7fb2d6cc2243abf4c3"},
"ash": {:hex, :ash, "1.28.0", "4c1581b8f2c0325d39ed68be9c2d9e2ad8a126281884e75860265c250a648a5d", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.3.5", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.1.5", [hex: :picosat_elixir, repo: "hexpm", optional: false]}], "hexpm", "a33b14730cd0e1b17d2e868dccee7e624fa5ed1c1e34a6e35c7ebb77e669d14d"},
"ashton": {:hex, :ashton, "0.4.1", "d0f7782ac44fa22da7ce544028ee3d2078592a834d8adf3e5b4b6aeb94413a55", [:mix], [], "hexpm", "24db667932517fdbc3f2dae777f28b8d87629271387d4490bc4ae8d9c46ff3d3"},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
"certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"},
"credo": {:hex, :credo, "1.4.0", "92339d4cbadd1e88b5ee43d427b639b68a11071b6f73854e33638e30a0ea11f5", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1fd3b70dce216574ce3c18bdf510b57e7c4c85c2ec9cad4bff854abaf7e58658"},
"dataloader": {:hex, :dataloader, "1.0.8", "114294362db98a613f231589246aa5b0ce847412e8e75c4c94f31f204d272cbf", [:mix], [{:ecto, ">= 3.4.3 and < 4.0.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "eaf3c2aa2bc9dbd2f1e960561d616b7f593396c4754185b75904f6d66c82a667"},
"decimal": {:hex, :decimal, "1.9.0", "83e8daf59631d632b171faabafb4a9f4242c514b0a06ba3df493951c08f64d07", [:mix], [], "hexpm", "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"},
"decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
"dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"},
"earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"},
"ecto": {:hex, :ecto, "3.5.5", "48219a991bb86daba6e38a1e64f8cea540cded58950ff38fbc8163e062281a07", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "98dd0e5e1de7f45beca6130d13116eae675db59adfa055fb79612406acf6f6f1"},