feat: rewrite with dataloader

This commit is contained in:
Zach Daniel 2020-09-23 20:54:57 -04:00
parent c74f76f8a0
commit 7adb8c0f22
No known key found for this signature in database
GPG key ID: C377365383138D4B
8 changed files with 432 additions and 186 deletions

View file

@ -2,7 +2,7 @@
## Get familiar with Ash resources ## Get familiar with Ash resources
If you haven't already, read the getting started guide for Ash. This assumes that you already have resources set up, and only gives you the steps to *add* AshGraphql to your resources/apis. If you haven't already, read the getting started guide for Ash. This assumes that you already have resources set up, and only gives you the steps to _add_ AshGraphql to your resources/apis.
## Add the API Extension ## Add the API Extension
@ -11,7 +11,7 @@ defmodule MyApi do
use Ash.Api, extensions: [ use Ash.Api, extensions: [
AshGraphql.Api AshGraphql.Api
] ]
graphql do graphql do
authorize? false # Defaults to `true`, use this to disable authorization for the entire API (you probably only want this while prototyping) authorize? false # Defaults to `true`, use this to disable authorization for the entire API (you probably only want this while prototyping)
end end
@ -26,17 +26,17 @@ defmodule Post do
extensions: [ extensions: [
AshGraphql.Resource AshGraphql.Resource
] ]
graphql do graphql do
type :post type :post
fields [:name, :count_of_comments, :comments] # <- a list of all of the attributes/relationships/aggregates to include in the graphql API fields [:name, :count_of_comments, :comments] # <- a list of all of the attributes/relationships/aggregates to include in the graphql API
queries do queries do
get :get_post, :default # <- create a field called `get_post` that uses the `default` read action to fetch a single post get :get_post, :default # <- create a field called `get_post` that uses the `default` read action to fetch a single post
list :list_posts, :default # <- create a field called `list_posts` that uses the `default` read action to fetch a list of posts list :list_posts, :default # <- create a field called `list_posts` that uses the `default` read action to fetch a list of posts
end end
mutations do mutations do
# And so on # And so on
create :create_post, :default create :create_post, :default
@ -51,21 +51,35 @@ end
If you don't have an absinthe schema, you can create one just for ash If you don't have an absinthe schema, you can create one just for ash
If you don't have any queries or mutations in your schema, you may If you don't have any queries or mutations in your schema, you may
need to add empty query and mutation blocks. If you have no mutations, need to add empty query and mutation blocks. If you have no mutations,
don't add an empty mutations block, same for queries. don't add an empty mutations block, same for queries. Additionally,
define a `context/1` function, and call `AshGraphql.add_context/2` with
the current context and your apis. Additionally, add the `Absinthe.Middleware.Dataloader`
to your plugins, as shown below. If you're starting fresh, just copy the schema below and
adjust the module name and api name.
```elixir ```elixir
defmodule MyApp.Schema do defmodule MyApp.Schema do
use Absinthe.Schema use Absinthe.Schema
use AshGraphql, api: AshExample.Api @apis [MyApp.Api]
use AshGraphql, apis: @apis
query do query do
end end
mutation do mutation do
end end
def context(ctx) do
AshGraphql.add_context(ctx, @apis)
end
def plugins() do
[Absinthe.Middleware.Dataloader | Absinthe.Plugin.defaults()]
end
end end
``` ```

View file

@ -18,7 +18,7 @@ defmodule AshGraphql.Api do
use Ash.Dsl.Extension, sections: [@graphql] use Ash.Dsl.Extension, sections: [@graphql]
def authorize?(api) do def authorize?(api) do
Extension.get_opt(api, :api, :authorize?, true) Extension.get_opt(api, [:graphql], :authorize?, true)
end end
@doc false @doc false
@ -39,7 +39,7 @@ defmodule AshGraphql.Api do
end end
@doc false @doc false
def type_definitions(api, schema) do def type_definitions(api, schema, first?) do
resource_types = resource_types =
api api
|> Ash.Api.resources() |> Ash.Api.resources()
@ -47,11 +47,15 @@ defmodule AshGraphql.Api do
AshGraphql.Resource in Ash.Resource.extensions(resource) AshGraphql.Resource in Ash.Resource.extensions(resource)
end) end)
|> Enum.flat_map(fn resource -> |> Enum.flat_map(fn resource ->
AshGraphql.Resource.type_definitions(resource, schema) ++ AshGraphql.Resource.type_definitions(resource, api, schema) ++
AshGraphql.Resource.mutation_types(resource, schema) AshGraphql.Resource.mutation_types(resource, schema)
end) end)
[mutation_error(schema), relationship_change(schema)] ++ resource_types if first? do
[mutation_error(schema), relationship_change(schema)] ++ resource_types
else
resource_types
end
end end
defp relationship_change(schema) do defp relationship_change(schema) do

View file

@ -7,79 +7,82 @@ defmodule AshGraphql do
""" """
defmacro __using__(opts) do defmacro __using__(opts) do
quote bind_quoted: [api: opts[:api]] do quote bind_quoted: [apis: opts[:apis], api: opts[:api]] do
defmodule Module.concat(api, AshTypes) do apis =
@moduledoc false api
alias Absinthe.{Blueprint, Phase, Pipeline} |> List.wrap()
|> Kernel.++(List.wrap(apis))
|> Enum.map(&{&1, false})
|> List.update_at(0, fn {api, _} -> {api, true} end)
def pipeline(pipeline) do for {api, first?} <- apis do
Pipeline.insert_before( defmodule Module.concat(api, AshTypes) do
pipeline, @moduledoc false
Phase.Schema.Validation.QueryTypeMustBeObject, alias Absinthe.{Blueprint, Phase, Pipeline}
__MODULE__
)
end
def run(blueprint, _opts) do def pipeline(pipeline) do
api = unquote(api) Pipeline.insert_before(
pipeline,
Phase.Schema.Validation.QueryTypeMustBeObject,
__MODULE__
)
end
case Code.ensure_compiled(api) do def run(blueprint, _opts) do
{:module, _} -> api = unquote(api)
blueprint_with_queries =
api
|> AshGraphql.Api.queries(__MODULE__)
|> Enum.reduce(blueprint, fn query, blueprint ->
Absinthe.Blueprint.add_field(blueprint, "RootQueryType", query)
end)
blueprint_with_mutations = case Code.ensure_compiled(api) do
api {:module, _} ->
|> AshGraphql.Api.mutations(__MODULE__) blueprint_with_queries =
|> Enum.reduce(blueprint_with_queries, fn mutation, blueprint -> api
Absinthe.Blueprint.add_field(blueprint, "RootMutationType", mutation) |> AshGraphql.Api.queries(__MODULE__)
end) |> Enum.reduce(blueprint, fn query, blueprint ->
Absinthe.Blueprint.add_field(blueprint, "RootQueryType", query)
end)
new_defs = blueprint_with_mutations =
List.update_at(blueprint_with_mutations.schema_definitions, 0, fn schema_def -> api
%{ |> AshGraphql.Api.mutations(__MODULE__)
schema_def |> Enum.reduce(blueprint_with_queries, fn mutation, blueprint ->
| type_definitions: Absinthe.Blueprint.add_field(blueprint, "RootMutationType", mutation)
schema_def.type_definitions ++ end)
AshGraphql.Api.type_definitions(api, __MODULE__)
}
end)
{:ok, %{blueprint_with_mutations | schema_definitions: new_defs}} new_defs =
List.update_at(blueprint_with_mutations.schema_definitions, 0, fn schema_def ->
%{
schema_def
| type_definitions:
schema_def.type_definitions ++
AshGraphql.Api.type_definitions(api, __MODULE__, unquote(first?))
}
end)
{:error, _} -> {:ok, %{blueprint_with_mutations | schema_definitions: new_defs}}
# Something else will fail here, so we don't need to
{:ok, blueprint} {:error, _} ->
# Something else will fail here, so we don't need to
{:ok, blueprint}
end
end end
end end
end
@pipeline_modifier Module.concat(api, AshTypes) @pipeline_modifier Module.concat(api, AshTypes)
end
end end
end end
defguard is_digit(x) when x in ?0..?0 def add_context(ctx, apis) do
dataloader =
apis
|> List.wrap()
|> Enum.reduce(Dataloader.new(), fn api, dataloader ->
Dataloader.add_source(
dataloader,
api,
AshGraphql.Dataloader.new(api)
)
end)
def roll(schema) do Map.put(ctx, :ash_loader, dataloader)
Enum.map(schema, fn
<<?d, x, y>> when is_digit(x) and is_digit(y) ->
Enum.random(1..String.to_integer(<<x, y>>))
<<?d, x, y, z>> when is_digit(x) and is_digit(y) and is_digit(z) ->
Enum.random(1..String.to_integer(<<x, y, z>>))
"adv" ->
{:max, roll(["d20", "d20"])}
"dis" ->
{:min, roll(["d20", "d20"])}
x ->
x
end)
end end
end end

257
lib/graphql/dataloader.ex Normal file
View file

@ -0,0 +1,257 @@
defmodule AshGraphql.Dataloader do
@moduledoc "The dataloader in charge of resolving "
defstruct [
:api,
batches: %{},
results: %{},
default_params: %{}
]
@type t :: %__MODULE__{
api: Ash.api(),
batches: map,
results: map,
default_params: map
}
@type api_opts :: Keyword.t()
@type batch_fun :: (Ash.resource(), Ash.query(), any, [any], api_opts -> [any])
@doc """
Create an Ash Dataloader source.
This module handles retrieving data from Ash for dataloader. It requires a
valid Ash API.
"""
@spec new(Ash.api()) :: t
def new(api) do
%__MODULE__{api: api}
end
defimpl Dataloader.Source do
def run(source) do
results = Dataloader.async_safely(__MODULE__, :run_batches, [source])
results =
Map.merge(source.results, results, fn _, {:ok, v1}, {:ok, v2} ->
{:ok, Map.merge(v1, v2)}
end)
%{source | results: results, batches: %{}}
end
def fetch(source, batch_key, item) do
{batch_key, item_key, _item} =
batch_key
|> normalize_key(source.default_params)
|> get_keys(item)
case Map.fetch(source.results, batch_key) do
{:ok, batch} ->
fetch_item_from_batch(batch, item_key)
:error ->
{:error, "Unable to find batch #{inspect(batch_key)}"}
end
end
defp fetch_item_from_batch({:error, _reason} = tried_and_failed, _item_key),
do: tried_and_failed
defp fetch_item_from_batch({:ok, batch}, item_key) do
case Map.fetch(batch, item_key) do
:error -> {:error, "Unable to find item #{inspect(item_key)} in batch"}
result -> result
end
end
def put(source, _batch, _item, %Ash.NotLoaded{type: :relationship}) do
source
end
def put(source, batch, item, result) do
batch = normalize_key(batch, source.default_params)
{batch_key, item_key, _item} = get_keys(batch, item)
results =
Map.update(
source.results,
batch_key,
{:ok, %{item_key => result}},
fn {:ok, map} -> {:ok, Map.put(map, item_key, result)} end
)
%{source | results: results}
end
def load(source, batch, item) do
{batch_key, item_key, item} =
batch
|> normalize_key(source.default_params)
|> get_keys(item)
if fetched?(source.results, batch_key, item_key) do
source
else
entry = {item_key, item}
update_in(source.batches, fn batches ->
Map.update(batches, batch_key, MapSet.new([entry]), &MapSet.put(&1, entry))
end)
end
end
defp fetched?(results, batch_key, item_key) do
case results do
%{^batch_key => {:ok, %{^item_key => _}}} -> true
_ -> false
end
end
def pending_batches?(%{batches: batches}) do
batches != %{}
end
def timeout(%{options: options}) do
options[:timeout]
end
defp related(path, resource) do
Ash.Resource.related(resource, path) ||
raise """
Valid relationship for path #{inspect(path)} not found on resource #{inspect(resource)}
"""
end
defp get_keys({assoc_field, opts}, %resource{} = record) when is_atom(assoc_field) do
validate_resource(resource)
primary_keys = Ash.Resource.primary_key(resource)
id = Enum.map(primary_keys, &Map.get(record, &1))
queryable = related([assoc_field], resource)
{{:assoc, resource, self(), assoc_field, queryable, opts}, id, record}
end
# defp get_keys({{cardinality, resource}, opts}, value) when is_atom(resource) do
# validate_resource(resource)
# {_, col, value} = normalize_value(resource, value)
# {{:resource, self(), resource, cardinality, col, opts}, value, value}
# end
# defp get_keys({resource, opts}, value) when is_atom(resource) do
# validate_resource(resource)
# case normalize_value(resource, value) do
# {:primary, col, value} ->
# {{:resource, self(), resource, :one, col, opts}, value, value}
# {:not_primary, col, _value} ->
# raise """
# Cardinality required unless using primary key
# The non-primary key column specified was: #{inspect(col)}
# """
# end
# end
defp get_keys(key, item) do
raise """
Invalid: #{inspect(key)}
#{inspect(item)}
The batch key must either be a schema module, or an association name.
"""
end
defp validate_resource(resource) do
unless Ash.Resource.resource?(resource) do
raise "The given module - #{resource} - is not an Ash resouce."
end
end
defp normalize_key({key, params}, default_params) do
{key, Enum.into(params, default_params)}
end
defp normalize_key(key, default_params) do
{key, default_params}
end
def run_batches(source) do
options = [
timeout: Dataloader.default_timeout(),
on_timeout: :kill_task
]
results =
source.batches
|> Task.async_stream(
fn batch ->
id = :erlang.unique_integer()
system_time = System.system_time()
start_time_mono = System.monotonic_time()
emit_start_event(id, system_time, batch)
batch_result = run_batch(batch, source)
emit_stop_event(id, start_time_mono, batch)
batch_result
end,
options
)
|> Enum.map(fn
{:ok, {_key, result}} -> {:ok, result}
{:exit, reason} -> {:error, reason}
end)
source.batches
|> Enum.map(fn {key, _set} -> key end)
|> Enum.zip(results)
|> Map.new()
end
defp run_batch(
{{:assoc, source_resource, _pid, field, _resource, opts} = key, records},
source
) do
{ids, records} = Enum.unzip(records)
query = opts[:query]
empty = source_resource |> struct |> Map.fetch!(field)
records = records |> Enum.map(&Map.put(&1, field, empty))
cardinality = Ash.Resource.relationship(source_resource, field).cardinality
loaded = source.api.load!(records, [{field, Ash.Query.new(query)}], source.api_opts)
results =
case cardinality do
:many ->
Enum.map(loaded, fn record ->
related = List.wrap(Map.get(record, field))
%{results: related, count: Enum.count(related)}
end)
:one ->
Enum.map(loaded, fn record ->
Map.get(record, field)
end)
end
{key, Map.new(Enum.zip(ids, results))}
end
defp emit_start_event(id, system_time, batch) do
:telemetry.execute(
[:dataloader, :source, :batch, :run, :start],
%{system_time: system_time},
%{id: id, batch: batch}
)
end
defp emit_stop_event(id, start_time_mono, batch) do
:telemetry.execute(
[:dataloader, :source, :batch, :run, :stop],
%{duration: System.monotonic_time() - start_time_mono},
%{id: id, batch: batch}
)
end
end
end

View file

@ -11,8 +11,6 @@ defmodule AshGraphql.Graphql.Resolver do
[action: action] [action: action]
end end
opts = Keyword.put(opts, :load, load_nested(resource, resolution.definition.selections))
result = api.get(resource, id, opts) result = api.get(resource, id, opts)
Absinthe.Resolution.put_result(resolution, to_resolution(result)) Absinthe.Resolution.put_result(resolution, to_resolution(result))
@ -29,20 +27,10 @@ defmodule AshGraphql.Graphql.Resolver do
[action: action] [action: action]
end end
selections =
case Enum.find(resolution.definition.selections, &(&1.schema_node.identifier == :results)) do
nil ->
[]
field ->
field.selections
end
query = query =
resource resource
|> Ash.Query.limit(limit) |> Ash.Query.limit(limit)
|> Ash.Query.offset(offset) |> Ash.Query.offset(offset)
|> Ash.Query.load(load_nested(resource, selections))
query = query =
case Map.fetch(args, :filter) do case Map.fetch(args, :filter) do
@ -79,17 +67,6 @@ defmodule AshGraphql.Graphql.Resolver do
) do ) do
{attributes, relationships} = split_attrs_and_rels(input, resource) {attributes, relationships} = split_attrs_and_rels(input, resource)
selections =
case Enum.find(resolution.definition.selections, &(&1.schema_node.identifier == :result)) do
nil ->
[]
field ->
field.selections
end
load = load_nested(resource, selections)
changeset = Ash.Changeset.new(resource, attributes) changeset = Ash.Changeset.new(resource, attributes)
changeset_with_relationships = changeset_with_relationships =
@ -105,10 +82,10 @@ defmodule AshGraphql.Graphql.Resolver do
end end
result = result =
with {:ok, value} <- api.create(changeset_with_relationships, opts), case api.create(changeset_with_relationships, opts) do
{:ok, value} <- api.load(value, load) do {:ok, value} ->
{:ok, %{result: value, errors: []}} {:ok, %{result: value, errors: []}}
else
{:error, error} -> {:error, error} ->
{:ok, %{result: nil, errors: to_errors(error)}} {:ok, %{result: nil, errors: to_errors(error)}}
end end
@ -140,25 +117,11 @@ defmodule AshGraphql.Graphql.Resolver do
[action: action] [action: action]
end end
selections =
case Enum.find(
resolution.definition.selections,
&(&1.schema_node.identifier == :result)
) do
nil ->
[]
field ->
field.selections
end
load = load_nested(resource, selections)
result = result =
with {:ok, value} <- api.update(changeset_with_relationships, opts), case api.update(changeset_with_relationships, opts) do
{:ok, value} <- api.load(value, load) do {:ok, value} ->
{:ok, %{result: value, errors: []}} {:ok, %{result: value, errors: []}}
else
{:error, error} -> {:error, error} ->
{:ok, %{result: nil, errors: List.wrap(error)}} {:ok, %{result: nil, errors: List.wrap(error)}}
end end
@ -219,68 +182,62 @@ defmodule AshGraphql.Graphql.Resolver do
end) end)
end end
def resolve_assoc(%{source: parent} = resolution, {:one, name}) do def resolve_assoc(
Absinthe.Resolution.put_result(resolution, {:ok, Map.get(parent, name)}) %{source: parent, arguments: args, context: %{ash_loader: loader}} = resolution,
{api, relationship}
) do
opts = [query: apply_load_arguments(args, Ash.Query.new(relationship.destination))]
{batch_key, parent} = {{relationship.name, opts}, parent}
do_dataloader(resolution, loader, api, batch_key, args, parent, opts)
end end
def resolve_assoc(%{source: parent} = resolution, {:many, name}) do defp do_dataloader(
values = Map.get(parent, name) resolution,
paginator = %{results: values, count: Enum.count(values)} loader,
api,
batch_key,
args,
parent,
opts
) do
loader = Dataloader.load(loader, api, batch_key, parent)
Absinthe.Resolution.put_result(resolution, {:ok, paginator}) fun = fn loader ->
callback = Keyword.get(opts, :callback, default_callback(loader))
loader
|> Dataloader.get(api, batch_key, parent)
|> callback.(parent, args)
end
Absinthe.Resolution.put_result(
resolution,
{:middleware, Absinthe.Middleware.Dataloader, {loader, fun}}
)
end end
defp load_nested(resource, fields) do defp default_callback(%{options: loader_options}) do
Enum.map(fields, fn field -> if loader_options[:get_policy] == :tuples do
relationship = Ash.Resource.relationship(resource, field.schema_node.identifier) fn result, _parent, _args -> result end
else
cond do fn result, _parent, _args -> {:ok, result} end
!relationship -> end
field.schema_node.identifier
relationship.cardinality == :many ->
trimmed_nested = nested_selections_with_pagination(field)
nested_loads = load_nested(relationship.destination, trimmed_nested)
query = Ash.Query.load(relationship.destination, nested_loads)
query = apply_load_arguments(field, query)
{field.schema_node.identifier, query}
true ->
nested_loads = load_nested(relationship.destination, field.selections)
query = Ash.Query.load(relationship.destination, nested_loads)
{field.schema_node.identifier, query}
end
end)
end end
defp apply_load_arguments(field, query) do defp apply_load_arguments(arguments, query) do
Enum.reduce(field.arguments, query, fn Enum.reduce(arguments, query, fn
%{name: "limit", value: value}, query -> {:limit, limit}, query ->
Ash.Query.limit(query, value) Ash.Query.limit(query, limit)
%{name: "offset", value: value}, query -> {:offset, offset}, query ->
Ash.Query.offset(query, value) Ash.Query.offset(query, offset)
%{name: "filter", value: value}, query -> {:filter, value}, query ->
decode_and_filter(query, value) decode_and_filter(query, value)
end) end)
end end
defp nested_selections_with_pagination(field) do
Enum.flat_map(field.selections, fn nested ->
if nested.schema_node.identifier == :results do
nested.selections
else
[]
end
end)
end
defp decode_and_filter(query, value) do defp decode_and_filter(query, value) do
case Jason.decode(value) do case Jason.decode(value) do
{:ok, decoded} -> {:ok, decoded} ->

View file

@ -263,6 +263,11 @@ defmodule AshGraphql.Resource do
resource resource
|> mutations() |> mutations()
|> Enum.flat_map(fn mutation -> |> Enum.flat_map(fn mutation ->
mutation = %{
mutation
| action: Ash.Resource.action(resource, mutation.action, mutation.type)
}
description = description =
if mutation.type == :destroy do if mutation.type == :destroy do
"The record that was successfully deleted" "The record that was successfully deleted"
@ -310,19 +315,22 @@ defmodule AshGraphql.Resource do
end) end)
end end
defp mutation_fields(resource, schema, query) do defp mutation_fields(resource, schema, mutation) do
fields = Resource.fields(resource) fields = Resource.fields(resource)
attribute_fields = attribute_fields =
resource resource
|> Ash.Resource.attributes() |> Ash.Resource.attributes()
|> Enum.filter(fn attribute ->
is_nil(mutation.action.accept) || attribute.name in mutation.action.accept
end)
|> Enum.filter(&(&1.name in fields)) |> Enum.filter(&(&1.name in fields))
|> Enum.filter(& &1.writable?) |> Enum.filter(& &1.writable?)
|> Enum.map(fn attribute -> |> Enum.map(fn attribute ->
type = field_type(attribute.type) type = field_type(attribute.type)
field_type = field_type =
if attribute.allow_nil? || query.type == :update do if attribute.allow_nil? || mutation.type == :update do
type type
else else
%Absinthe.Blueprint.TypeReference.NonNull{ %Absinthe.Blueprint.TypeReference.NonNull{
@ -356,7 +364,7 @@ defmodule AshGraphql.Resource do
} }
%{cardinality: :many} = relationship -> %{cardinality: :many} = relationship ->
case query.type do case mutation.type do
:update -> :update ->
%Absinthe.Blueprint.Schema.FieldDefinition{ %Absinthe.Blueprint.Schema.FieldDefinition{
identifier: relationship.name, identifier: relationship.name,
@ -421,9 +429,9 @@ defmodule AshGraphql.Resource do
end end
@doc false @doc false
def type_definitions(resource, schema) do def type_definitions(resource, api, schema) do
[ [
type_definition(resource, schema), type_definition(resource, api, schema),
page_of(resource, schema) page_of(resource, schema)
] ]
end end
@ -458,23 +466,23 @@ defmodule AshGraphql.Resource do
} }
end end
defp type_definition(resource, schema) do defp type_definition(resource, api, schema) do
type = Resource.type(resource) type = Resource.type(resource)
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{ %Absinthe.Blueprint.Schema.ObjectTypeDefinition{
description: Ash.Resource.description(resource), description: Ash.Resource.description(resource),
fields: fields(resource, schema), fields: fields(resource, api, schema),
identifier: type, identifier: type,
module: schema, module: schema,
name: Macro.camelize(to_string(type)) name: Macro.camelize(to_string(type))
} }
end end
defp fields(resource, schema) do defp fields(resource, api, schema) do
fields = Resource.fields(resource) fields = Resource.fields(resource)
attributes(resource, schema, fields) ++ attributes(resource, schema, fields) ++
relationships(resource, schema, fields) ++ relationships(resource, api, schema, fields) ++
aggregates(resource, schema, fields) aggregates(resource, schema, fields)
end end
@ -504,7 +512,7 @@ defmodule AshGraphql.Resource do
end end
# sobelow_skip ["DOS.StringToAtom"] # sobelow_skip ["DOS.StringToAtom"]
defp relationships(resource, schema, fields) do defp relationships(resource, api, schema, fields) do
resource resource
|> Ash.Resource.relationships() |> Ash.Resource.relationships()
|> Enum.filter(&(&1.name in fields)) |> Enum.filter(&(&1.name in fields))
@ -520,7 +528,7 @@ defmodule AshGraphql.Resource do
module: schema, module: schema,
name: to_string(relationship.name), name: to_string(relationship.name),
middleware: [ middleware: [
{{AshGraphql.Graphql.Resolver, :resolve_assoc}, {:one, relationship.name}} {{AshGraphql.Graphql.Resolver, :resolve_assoc}, {api, relationship}}
], ],
arguments: [], arguments: [],
type: type type: type
@ -535,7 +543,7 @@ defmodule AshGraphql.Resource do
module: schema, module: schema,
name: to_string(relationship.name), name: to_string(relationship.name),
middleware: [ middleware: [
{{AshGraphql.Graphql.Resolver, :resolve_assoc}, {:many, relationship.name}} {{AshGraphql.Graphql.Resolver, :resolve_assoc}, {api, relationship}}
], ],
arguments: args(:list), arguments: args(:list),
type: query_type type: query_type

View file

@ -16,6 +16,7 @@ defmodule AshGraphql.MixProject do
package: package(), package: package(),
aliases: aliases(), aliases: aliases(),
deps: deps(), deps: deps(),
dialyzer: [plt_add_apps: [:ash]],
test_coverage: [tool: ExCoveralls], test_coverage: [tool: ExCoveralls],
preferred_cli_env: [ preferred_cli_env: [
coveralls: :test, coveralls: :test,
@ -68,7 +69,8 @@ defmodule AshGraphql.MixProject do
defp deps do defp deps do
[ [
{:ash, ash_version("~> 1.11.0")}, {:ash, ash_version("~> 1.11.0")},
{:absinthe, "~> 1.5.2"}, {:absinthe, "~> 1.5.3"},
{:dataloader, "~> 1.0"},
{:jason, "~> 1.2"}, {:jason, "~> 1.2"},
{:ex_doc, "~> 0.22", only: :dev, runtime: false}, {:ex_doc, "~> 0.22", only: :dev, runtime: false},
{:ex_check, "~> 0.12.0", only: :dev}, {:ex_check, "~> 0.12.0", only: :dev},

View file

@ -1,15 +1,16 @@
%{ %{
"absinthe": {:hex, :absinthe, "1.5.2", "2f9449b0c135ea61c09c11968d3d4fe6abd5bed38cf9be1c6d6b7c5ec858cfa0", [: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", "669c84879629b7fffdc6cda9361ab9c81c9c7691e65418ba089b912a227963ac"}, "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"},
"ash": {:hex, :ash, "1.10.0", "c3b3eb98ac41da14bb70daeec15d498ea48a91105db9386cb2814eaf38ff0cc6", [:mix], [{:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.3.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.1.4", [hex: :picosat_elixir, repo: "hexpm", optional: false]}], "hexpm", "2a8babe6cfa9fa0274037074ba4a3c23bda9f45a77006c4e745bc6336629a5d1"}, "ash": {:hex, :ash, "1.11.1", "662b2d65731c4e61622a49293c6bcd41f567c6c3681818b9be7f0da868b4d78e", [:mix], [{:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.3.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.1.4", [hex: :picosat_elixir, repo: "hexpm", optional: false]}], "hexpm", "77e92ca815aabedbebfe53aee87d96de26b75338351de6a8d5e831243d4241f4"},
"ashton": {:hex, :ashton, "0.4.1", "d0f7782ac44fa22da7ce544028ee3d2078592a834d8adf3e5b4b6aeb94413a55", [:mix], [], "hexpm", "24db667932517fdbc3f2dae777f28b8d87629271387d4490bc4ae8d9c46ff3d3"}, "ashton": {:hex, :ashton, "0.4.1", "d0f7782ac44fa22da7ce544028ee3d2078592a834d8adf3e5b4b6aeb94413a55", [:mix], [], "hexpm", "24db667932517fdbc3f2dae777f28b8d87629271387d4490bc4ae8d9c46ff3d3"},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, "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"}, "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"}, "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"},
"decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"}, "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"},
"dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"}, "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"}, "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"},
"ecto": {:hex, :ecto, "3.4.6", "08f7afad3257d6eb8613309af31037e16c36808dfda5a3cd0cb4e9738db030e4", [: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", "6f13a9e2a62e75c2dcfc7207bfc65645ab387af8360db4c89fee8b5a4bf3f70b"}, "ecto": {:hex, :ecto, "3.4.6", "08f7afad3257d6eb8613309af31037e16c36808dfda5a3cd0cb4e9738db030e4", [: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", "6f13a9e2a62e75c2dcfc7207bfc65645ab387af8360db4c89fee8b5a4bf3f70b"},
"elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"}, "elixir_make": {:hex, :elixir_make, "0.6.1", "8faa29a5597faba999aeeb72bbb9c91694ef8068f0131192fb199f98d32994ef", [:mix], [], "hexpm", "35d33270680f8d839a4003c3e9f43afb595310a592405a00afc12de4c7f55a18"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"ets": {:hex, :ets, "0.8.1", "8ff9bcda5682b98493f8878fc9dbd990e48d566cba8cce59f7c2a78130da29ea", [:mix], [], "hexpm", "6be41b50adb5bc5c43626f25ea2d0af1f4a242fb3fad8d53f0c67c20b78915cc"}, "ets": {:hex, :ets, "0.8.1", "8ff9bcda5682b98493f8878fc9dbd990e48d566cba8cce59f7c2a78130da29ea", [:mix], [], "hexpm", "6be41b50adb5bc5c43626f25ea2d0af1f4a242fb3fad8d53f0c67c20b78915cc"},
"ex_check": {:hex, :ex_check, "0.12.0", "c0e2919ecc06afeaf62c52d64f3d91bd4bc7dd8deaac5f84becb6278888c967a", [:mix], [], "hexpm", "cfafa8ef97c2596d45a1f19b5794cb5c7f700f25d164d3c9f8d7ec17ee67cf42"}, "ex_check": {:hex, :ex_check, "0.12.0", "c0e2919ecc06afeaf62c52d64f3d91bd4bc7dd8deaac5f84becb6278888c967a", [:mix], [], "hexpm", "cfafa8ef97c2596d45a1f19b5794cb5c7f700f25d164d3c9f8d7ec17ee67cf42"},
@ -19,7 +20,7 @@
"git_ops": {:hex, :git_ops, "2.0.1", "9d3df6c710a80a8779dbb144c79fb24c777660ae862cc454ab3193afd0c02a37", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cd499a72523ba338c20973eadb707d25a42e4a77c46d2ff5c45e61e7adae6190"}, "git_ops": {:hex, :git_ops, "2.0.1", "9d3df6c710a80a8779dbb144c79fb24c777660ae862cc454ab3193afd0c02a37", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cd499a72523ba338c20973eadb707d25a42e4a77c46d2ff5c45e61e7adae6190"},
"hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"}, "hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"},
"idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"}, "idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"},
"jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"}, "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
"machinery": {:hex, :machinery, "1.0.0", "df6968d84c651b9971a33871c78c10157b6e13e4f3390b0bee5b0e8bdea8c781", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "4f6eb4185a48e7245360bedf653af4acc6fa6ae8ff4690619395543fa1a8395f"}, "machinery": {:hex, :machinery, "1.0.0", "df6968d84c651b9971a33871c78c10157b6e13e4f3390b0bee5b0e8bdea8c781", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "4f6eb4185a48e7245360bedf653af4acc6fa6ae8ff4690619395543fa1a8395f"},
"makeup": {:hex, :makeup, "1.0.3", "e339e2f766d12e7260e6672dd4047405963c5ec99661abdc432e6ec67d29ef95", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "2e9b4996d11832947731f7608fed7ad2f9443011b3b479ae288011265cdd3dad"}, "makeup": {:hex, :makeup, "1.0.3", "e339e2f766d12e7260e6672dd4047405963c5ec99661abdc432e6ec67d29ef95", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "2e9b4996d11832947731f7608fed7ad2f9443011b3b479ae288011265cdd3dad"},
"makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"}, "makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"},
@ -28,7 +29,7 @@
"nimble_options": {:hex, :nimble_options, "0.3.0", "1872911bf50a048f04da26e02704e6aeafc362c2daa7636b6dbfda9492ccfcfa", [:mix], [], "hexpm", "180790a8644fea402452bc15bb54b9bf2c8e5c1fdeb6b39d8072e59c324edf7f"}, "nimble_options": {:hex, :nimble_options, "0.3.0", "1872911bf50a048f04da26e02704e6aeafc362c2daa7636b6dbfda9492ccfcfa", [:mix], [], "hexpm", "180790a8644fea402452bc15bb54b9bf2c8e5c1fdeb6b39d8072e59c324edf7f"},
"nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"}, "nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
"picosat_elixir": {:hex, :picosat_elixir, "0.1.4", "d259219ae27148c07c4aa3fdee61b1a14f4bc7f83b0ebdf2752558d06b302c62", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "eb41cb16053a45c8556de32f065084af98ea0b13a523fb46dfb4f9cff4152474"}, "picosat_elixir": {:hex, :picosat_elixir, "0.1.5", "23673bd3080a4489401e25b4896aff1f1138d47b2f650eab724aad1506188ebb", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "b30b3c3abd1f4281902d3b5bc9b67e716509092d6243b010c29d8be4a526e8c8"},
"sobelow": {:hex, :sobelow, "0.10.4", "44ba642da120d84fedb9e85473375084034330c8f15a992351dd164a82963103", [:mix], [], "hexpm", "fea62a94a4112de45ee9c9d076fd636fbbc10b7c7c2ea99a928e7c289b8498d1"}, "sobelow": {:hex, :sobelow, "0.10.4", "44ba642da120d84fedb9e85473375084034330c8f15a992351dd164a82963103", [:mix], [], "hexpm", "fea62a94a4112de45ee9c9d076fd636fbbc10b7c7c2ea99a928e7c289b8498d1"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},