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
@ -53,19 +53,33 @@ 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)
if first? do
[mutation_error(schema), relationship_change(schema)] ++ resource_types [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,7 +7,15 @@ 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
apis =
api
|> List.wrap()
|> Kernel.++(List.wrap(apis))
|> Enum.map(&{&1, false})
|> List.update_at(0, fn {api, _} -> {api, true} end)
for {api, first?} <- apis do
defmodule Module.concat(api, AshTypes) do defmodule Module.concat(api, AshTypes) do
@moduledoc false @moduledoc false
alias Absinthe.{Blueprint, Phase, Pipeline} alias Absinthe.{Blueprint, Phase, Pipeline}
@ -45,7 +53,7 @@ defmodule AshGraphql do
schema_def schema_def
| type_definitions: | type_definitions:
schema_def.type_definitions ++ schema_def.type_definitions ++
AshGraphql.Api.type_definitions(api, __MODULE__) AshGraphql.Api.type_definitions(api, __MODULE__, unquote(first?))
} }
end) end)
@ -61,25 +69,20 @@ defmodule AshGraphql do
@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 =
def roll(schema) do apis
Enum.map(schema, fn |> List.wrap()
<<?d, x, y>> when is_digit(x) and is_digit(y) -> |> Enum.reduce(Dataloader.new(), fn api, dataloader ->
Enum.random(1..String.to_integer(<<x, y>>)) Dataloader.add_source(
dataloader,
<<?d, x, y, z>> when is_digit(x) and is_digit(y) and is_digit(z) -> api,
Enum.random(1..String.to_integer(<<x, y, z>>)) AshGraphql.Dataloader.new(api)
)
"adv" ->
{:max, roll(["d20", "d20"])}
"dis" ->
{:min, roll(["d20", "d20"])}
x ->
x
end) end)
Map.put(ctx, :ash_loader, dataloader)
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,65 +182,59 @@ 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 end
defp load_nested(resource, fields) do Absinthe.Resolution.put_result(
Enum.map(fields, fn field -> resolution,
relationship = Ash.Resource.relationship(resource, field.schema_node.identifier) {:middleware, Absinthe.Middleware.Dataloader, {loader, fun}}
)
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)
end end
defp apply_load_arguments(field, query) do defp default_callback(%{options: loader_options}) do
Enum.reduce(field.arguments, query, fn if loader_options[:get_policy] == :tuples do
%{name: "limit", value: value}, query -> fn result, _parent, _args -> result end
Ash.Query.limit(query, value)
%{name: "offset", value: value}, query ->
Ash.Query.offset(query, value)
%{name: "filter", value: 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 else
[] fn result, _parent, _args -> {:ok, result} end
end end
end
defp apply_load_arguments(arguments, query) do
Enum.reduce(arguments, query, fn
{:limit, limit}, query ->
Ash.Query.limit(query, limit)
{:offset, offset}, query ->
Ash.Query.offset(query, offset)
{:filter, value}, query ->
decode_and_filter(query, value)
end) end)
end end

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"},