mirror of
https://github.com/ash-project/ash_graphql.git
synced 2024-09-19 12:53:40 +12:00
feat: rewrite with dataloader
This commit is contained in:
commit
d0e3a2c02e
9 changed files with 432 additions and 191 deletions
5
.github/PULL_REQUEST_TEMPLATE.md
vendored
5
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -1,9 +1,4 @@
|
|||
### Contributor checklist
|
||||
|
||||
- [ ] My commit messages follow the [Conventional Commit Message Format](https://gist.github.com/stephenparish/9941e89d80e2bc58a153#format-of-the-commit-message)
|
||||
For example: `fix: Multiply by appropriate coefficient`, or
|
||||
`feat(Calculator): Correctly preserve history`
|
||||
Any explanation or long form information in your commit message should be
|
||||
in a separate paragraph, separated by a blank line from the primary message
|
||||
- [ ] Bug fixes include regression tests
|
||||
- [ ] Features include unit/acceptance tests
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## 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
|
||||
|
||||
|
@ -11,7 +11,7 @@ defmodule MyApi do
|
|||
use Ash.Api, extensions: [
|
||||
AshGraphql.Api
|
||||
]
|
||||
|
||||
|
||||
graphql do
|
||||
authorize? false # Defaults to `true`, use this to disable authorization for the entire API (you probably only want this while prototyping)
|
||||
end
|
||||
|
@ -26,17 +26,17 @@ defmodule Post do
|
|||
extensions: [
|
||||
AshGraphql.Resource
|
||||
]
|
||||
|
||||
|
||||
graphql do
|
||||
type :post
|
||||
|
||||
|
||||
fields [:name, :count_of_comments, :comments] # <- a list of all of the attributes/relationships/aggregates to include in the graphql API
|
||||
|
||||
|
||||
queries do
|
||||
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
|
||||
end
|
||||
|
||||
|
||||
mutations do
|
||||
# And so on
|
||||
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 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,
|
||||
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
|
||||
defmodule MyApp.Schema do
|
||||
use Absinthe.Schema
|
||||
|
||||
use AshGraphql, api: AshExample.Api
|
||||
@apis [MyApp.Api]
|
||||
|
||||
use AshGraphql, apis: @apis
|
||||
|
||||
query do
|
||||
end
|
||||
|
||||
mutation do
|
||||
end
|
||||
|
||||
def context(ctx) do
|
||||
AshGraphql.add_context(ctx, @apis)
|
||||
end
|
||||
|
||||
def plugins() do
|
||||
[Absinthe.Middleware.Dataloader | Absinthe.Plugin.defaults()]
|
||||
end
|
||||
end
|
||||
|
||||
```
|
||||
|
|
|
@ -18,7 +18,7 @@ defmodule AshGraphql.Api do
|
|||
use Ash.Dsl.Extension, sections: [@graphql]
|
||||
|
||||
def authorize?(api) do
|
||||
Extension.get_opt(api, :api, :authorize?, true)
|
||||
Extension.get_opt(api, [:graphql], :authorize?, true)
|
||||
end
|
||||
|
||||
@doc false
|
||||
|
@ -39,7 +39,7 @@ defmodule AshGraphql.Api do
|
|||
end
|
||||
|
||||
@doc false
|
||||
def type_definitions(api, schema) do
|
||||
def type_definitions(api, schema, first?) do
|
||||
resource_types =
|
||||
api
|
||||
|> Ash.Api.resources()
|
||||
|
@ -47,11 +47,15 @@ defmodule AshGraphql.Api do
|
|||
AshGraphql.Resource in Ash.Resource.extensions(resource)
|
||||
end)
|
||||
|> Enum.flat_map(fn resource ->
|
||||
AshGraphql.Resource.type_definitions(resource, schema) ++
|
||||
AshGraphql.Resource.type_definitions(resource, api, schema) ++
|
||||
AshGraphql.Resource.mutation_types(resource, schema)
|
||||
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
|
||||
|
||||
defp relationship_change(schema) do
|
||||
|
|
|
@ -7,79 +7,82 @@ defmodule AshGraphql do
|
|||
"""
|
||||
|
||||
defmacro __using__(opts) do
|
||||
quote bind_quoted: [api: opts[:api]] do
|
||||
defmodule Module.concat(api, AshTypes) do
|
||||
@moduledoc false
|
||||
alias Absinthe.{Blueprint, Phase, Pipeline}
|
||||
quote bind_quoted: [apis: opts[:apis], api: opts[:api]] do
|
||||
apis =
|
||||
api
|
||||
|> List.wrap()
|
||||
|> Kernel.++(List.wrap(apis))
|
||||
|> Enum.map(&{&1, false})
|
||||
|> List.update_at(0, fn {api, _} -> {api, true} end)
|
||||
|
||||
def pipeline(pipeline) do
|
||||
Pipeline.insert_before(
|
||||
pipeline,
|
||||
Phase.Schema.Validation.QueryTypeMustBeObject,
|
||||
__MODULE__
|
||||
)
|
||||
end
|
||||
for {api, first?} <- apis do
|
||||
defmodule Module.concat(api, AshTypes) do
|
||||
@moduledoc false
|
||||
alias Absinthe.{Blueprint, Phase, Pipeline}
|
||||
|
||||
def run(blueprint, _opts) do
|
||||
api = unquote(api)
|
||||
def pipeline(pipeline) do
|
||||
Pipeline.insert_before(
|
||||
pipeline,
|
||||
Phase.Schema.Validation.QueryTypeMustBeObject,
|
||||
__MODULE__
|
||||
)
|
||||
end
|
||||
|
||||
case Code.ensure_compiled(api) do
|
||||
{:module, _} ->
|
||||
blueprint_with_queries =
|
||||
api
|
||||
|> AshGraphql.Api.queries(__MODULE__)
|
||||
|> Enum.reduce(blueprint, fn query, blueprint ->
|
||||
Absinthe.Blueprint.add_field(blueprint, "RootQueryType", query)
|
||||
end)
|
||||
def run(blueprint, _opts) do
|
||||
api = unquote(api)
|
||||
|
||||
blueprint_with_mutations =
|
||||
api
|
||||
|> AshGraphql.Api.mutations(__MODULE__)
|
||||
|> Enum.reduce(blueprint_with_queries, fn mutation, blueprint ->
|
||||
Absinthe.Blueprint.add_field(blueprint, "RootMutationType", mutation)
|
||||
end)
|
||||
case Code.ensure_compiled(api) do
|
||||
{:module, _} ->
|
||||
blueprint_with_queries =
|
||||
api
|
||||
|> AshGraphql.Api.queries(__MODULE__)
|
||||
|> Enum.reduce(blueprint, fn query, blueprint ->
|
||||
Absinthe.Blueprint.add_field(blueprint, "RootQueryType", query)
|
||||
end)
|
||||
|
||||
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__)
|
||||
}
|
||||
end)
|
||||
blueprint_with_mutations =
|
||||
api
|
||||
|> AshGraphql.Api.mutations(__MODULE__)
|
||||
|> Enum.reduce(blueprint_with_queries, fn mutation, blueprint ->
|
||||
Absinthe.Blueprint.add_field(blueprint, "RootMutationType", mutation)
|
||||
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, _} ->
|
||||
# Something else will fail here, so we don't need to
|
||||
{:ok, blueprint}
|
||||
{:ok, %{blueprint_with_mutations | schema_definitions: new_defs}}
|
||||
|
||||
{:error, _} ->
|
||||
# Something else will fail here, so we don't need to
|
||||
{:ok, blueprint}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@pipeline_modifier Module.concat(api, AshTypes)
|
||||
@pipeline_modifier Module.concat(api, AshTypes)
|
||||
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
|
||||
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)
|
||||
Map.put(ctx, :ash_loader, dataloader)
|
||||
end
|
||||
end
|
||||
|
|
257
lib/graphql/dataloader.ex
Normal file
257
lib/graphql/dataloader.ex
Normal 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
|
|
@ -11,8 +11,6 @@ defmodule AshGraphql.Graphql.Resolver do
|
|||
[action: action]
|
||||
end
|
||||
|
||||
opts = Keyword.put(opts, :load, load_nested(resource, resolution.definition.selections))
|
||||
|
||||
result = api.get(resource, id, opts)
|
||||
|
||||
Absinthe.Resolution.put_result(resolution, to_resolution(result))
|
||||
|
@ -29,20 +27,10 @@ defmodule AshGraphql.Graphql.Resolver do
|
|||
[action: action]
|
||||
end
|
||||
|
||||
selections =
|
||||
case Enum.find(resolution.definition.selections, &(&1.schema_node.identifier == :results)) do
|
||||
nil ->
|
||||
[]
|
||||
|
||||
field ->
|
||||
field.selections
|
||||
end
|
||||
|
||||
query =
|
||||
resource
|
||||
|> Ash.Query.limit(limit)
|
||||
|> Ash.Query.offset(offset)
|
||||
|> Ash.Query.load(load_nested(resource, selections))
|
||||
|
||||
query =
|
||||
case Map.fetch(args, :filter) do
|
||||
|
@ -79,17 +67,6 @@ defmodule AshGraphql.Graphql.Resolver do
|
|||
) do
|
||||
{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_with_relationships =
|
||||
|
@ -105,10 +82,10 @@ defmodule AshGraphql.Graphql.Resolver do
|
|||
end
|
||||
|
||||
result =
|
||||
with {:ok, value} <- api.create(changeset_with_relationships, opts),
|
||||
{:ok, value} <- api.load(value, load) do
|
||||
{:ok, %{result: value, errors: []}}
|
||||
else
|
||||
case api.create(changeset_with_relationships, opts) do
|
||||
{:ok, value} ->
|
||||
{:ok, %{result: value, errors: []}}
|
||||
|
||||
{:error, error} ->
|
||||
{:ok, %{result: nil, errors: to_errors(error)}}
|
||||
end
|
||||
|
@ -140,25 +117,11 @@ defmodule AshGraphql.Graphql.Resolver do
|
|||
[action: action]
|
||||
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 =
|
||||
with {:ok, value} <- api.update(changeset_with_relationships, opts),
|
||||
{:ok, value} <- api.load(value, load) do
|
||||
{:ok, %{result: value, errors: []}}
|
||||
else
|
||||
case api.update(changeset_with_relationships, opts) do
|
||||
{:ok, value} ->
|
||||
{:ok, %{result: value, errors: []}}
|
||||
|
||||
{:error, error} ->
|
||||
{:ok, %{result: nil, errors: List.wrap(error)}}
|
||||
end
|
||||
|
@ -219,68 +182,62 @@ defmodule AshGraphql.Graphql.Resolver do
|
|||
end)
|
||||
end
|
||||
|
||||
def resolve_assoc(%{source: parent} = resolution, {:one, name}) do
|
||||
Absinthe.Resolution.put_result(resolution, {:ok, Map.get(parent, name)})
|
||||
def resolve_assoc(
|
||||
%{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
|
||||
|
||||
def resolve_assoc(%{source: parent} = resolution, {:many, name}) do
|
||||
values = Map.get(parent, name)
|
||||
paginator = %{results: values, count: Enum.count(values)}
|
||||
defp do_dataloader(
|
||||
resolution,
|
||||
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
|
||||
|
||||
defp load_nested(resource, fields) do
|
||||
Enum.map(fields, fn field ->
|
||||
relationship = Ash.Resource.relationship(resource, field.schema_node.identifier)
|
||||
|
||||
cond do
|
||||
!relationship ->
|
||||
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)
|
||||
defp default_callback(%{options: loader_options}) do
|
||||
if loader_options[:get_policy] == :tuples do
|
||||
fn result, _parent, _args -> result end
|
||||
else
|
||||
fn result, _parent, _args -> {:ok, result} end
|
||||
end
|
||||
end
|
||||
|
||||
defp apply_load_arguments(field, query) do
|
||||
Enum.reduce(field.arguments, query, fn
|
||||
%{name: "limit", value: value}, query ->
|
||||
Ash.Query.limit(query, value)
|
||||
defp apply_load_arguments(arguments, query) do
|
||||
Enum.reduce(arguments, query, fn
|
||||
{:limit, limit}, query ->
|
||||
Ash.Query.limit(query, limit)
|
||||
|
||||
%{name: "offset", value: value}, query ->
|
||||
Ash.Query.offset(query, value)
|
||||
{:offset, offset}, query ->
|
||||
Ash.Query.offset(query, offset)
|
||||
|
||||
%{name: "filter", value: value}, query ->
|
||||
{:filter, value}, query ->
|
||||
decode_and_filter(query, value)
|
||||
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
|
||||
case Jason.decode(value) do
|
||||
{:ok, decoded} ->
|
||||
|
|
|
@ -263,6 +263,11 @@ defmodule AshGraphql.Resource do
|
|||
resource
|
||||
|> mutations()
|
||||
|> Enum.flat_map(fn mutation ->
|
||||
mutation = %{
|
||||
mutation
|
||||
| action: Ash.Resource.action(resource, mutation.action, mutation.type)
|
||||
}
|
||||
|
||||
description =
|
||||
if mutation.type == :destroy do
|
||||
"The record that was successfully deleted"
|
||||
|
@ -310,19 +315,22 @@ defmodule AshGraphql.Resource do
|
|||
end)
|
||||
end
|
||||
|
||||
defp mutation_fields(resource, schema, query) do
|
||||
defp mutation_fields(resource, schema, mutation) do
|
||||
fields = Resource.fields(resource)
|
||||
|
||||
attribute_fields =
|
||||
resource
|
||||
|> 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.writable?)
|
||||
|> Enum.map(fn attribute ->
|
||||
type = field_type(attribute.type)
|
||||
|
||||
field_type =
|
||||
if attribute.allow_nil? || query.type == :update do
|
||||
if attribute.allow_nil? || mutation.type == :update do
|
||||
type
|
||||
else
|
||||
%Absinthe.Blueprint.TypeReference.NonNull{
|
||||
|
@ -356,7 +364,7 @@ defmodule AshGraphql.Resource do
|
|||
}
|
||||
|
||||
%{cardinality: :many} = relationship ->
|
||||
case query.type do
|
||||
case mutation.type do
|
||||
:update ->
|
||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||
identifier: relationship.name,
|
||||
|
@ -421,9 +429,9 @@ defmodule AshGraphql.Resource do
|
|||
end
|
||||
|
||||
@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)
|
||||
]
|
||||
end
|
||||
|
@ -458,23 +466,23 @@ defmodule AshGraphql.Resource do
|
|||
}
|
||||
end
|
||||
|
||||
defp type_definition(resource, schema) do
|
||||
defp type_definition(resource, api, schema) do
|
||||
type = Resource.type(resource)
|
||||
|
||||
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{
|
||||
description: Ash.Resource.description(resource),
|
||||
fields: fields(resource, schema),
|
||||
fields: fields(resource, api, schema),
|
||||
identifier: type,
|
||||
module: schema,
|
||||
name: Macro.camelize(to_string(type))
|
||||
}
|
||||
end
|
||||
|
||||
defp fields(resource, schema) do
|
||||
defp fields(resource, api, schema) do
|
||||
fields = Resource.fields(resource)
|
||||
|
||||
attributes(resource, schema, fields) ++
|
||||
relationships(resource, schema, fields) ++
|
||||
relationships(resource, api, schema, fields) ++
|
||||
aggregates(resource, schema, fields)
|
||||
end
|
||||
|
||||
|
@ -504,7 +512,7 @@ defmodule AshGraphql.Resource do
|
|||
end
|
||||
|
||||
# sobelow_skip ["DOS.StringToAtom"]
|
||||
defp relationships(resource, schema, fields) do
|
||||
defp relationships(resource, api, schema, fields) do
|
||||
resource
|
||||
|> Ash.Resource.relationships()
|
||||
|> Enum.filter(&(&1.name in fields))
|
||||
|
@ -520,7 +528,7 @@ defmodule AshGraphql.Resource do
|
|||
module: schema,
|
||||
name: to_string(relationship.name),
|
||||
middleware: [
|
||||
{{AshGraphql.Graphql.Resolver, :resolve_assoc}, {:one, relationship.name}}
|
||||
{{AshGraphql.Graphql.Resolver, :resolve_assoc}, {api, relationship}}
|
||||
],
|
||||
arguments: [],
|
||||
type: type
|
||||
|
@ -535,7 +543,7 @@ defmodule AshGraphql.Resource do
|
|||
module: schema,
|
||||
name: to_string(relationship.name),
|
||||
middleware: [
|
||||
{{AshGraphql.Graphql.Resolver, :resolve_assoc}, {:many, relationship.name}}
|
||||
{{AshGraphql.Graphql.Resolver, :resolve_assoc}, {api, relationship}}
|
||||
],
|
||||
arguments: args(:list),
|
||||
type: query_type
|
||||
|
|
4
mix.exs
4
mix.exs
|
@ -16,6 +16,7 @@ defmodule AshGraphql.MixProject do
|
|||
package: package(),
|
||||
aliases: aliases(),
|
||||
deps: deps(),
|
||||
dialyzer: [plt_add_apps: [:ash]],
|
||||
test_coverage: [tool: ExCoveralls],
|
||||
preferred_cli_env: [
|
||||
coveralls: :test,
|
||||
|
@ -68,7 +69,8 @@ defmodule AshGraphql.MixProject do
|
|||
defp deps do
|
||||
[
|
||||
{:ash, ash_version("~> 1.11.0")},
|
||||
{:absinthe, "~> 1.5.2"},
|
||||
{:absinthe, "~> 1.5.3"},
|
||||
{:dataloader, "~> 1.0"},
|
||||
{:jason, "~> 1.2"},
|
||||
{:ex_doc, "~> 0.22", only: :dev, runtime: false},
|
||||
{:ex_check, "~> 0.12.0", only: :dev},
|
||||
|
|
13
mix.lock
13
mix.lock
|
@ -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"},
|
||||
"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"},
|
||||
"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.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"},
|
||||
"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"},
|
||||
"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"},
|
||||
"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"},
|
||||
"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"},
|
||||
"ets": {:hex, :ets, "0.8.1", "8ff9bcda5682b98493f8878fc9dbd990e48d566cba8cce59f7c2a78130da29ea", [:mix], [], "hexpm", "6be41b50adb5bc5c43626f25ea2d0af1f4a242fb3fad8d53f0c67c20b78915cc"},
|
||||
"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"},
|
||||
"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"},
|
||||
"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"},
|
||||
"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"},
|
||||
|
@ -28,7 +29,7 @@
|
|||
"nimble_options": {:hex, :nimble_options, "0.3.0", "1872911bf50a048f04da26e02704e6aeafc362c2daa7636b6dbfda9492ccfcfa", [:mix], [], "hexpm", "180790a8644fea402452bc15bb54b9bf2c8e5c1fdeb6b39d8072e59c324edf7f"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"},
|
||||
"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"},
|
||||
"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"},
|
||||
|
|
Loading…
Reference in a new issue