From 7adb8c0f2257fc458af715a0a384ca465e43bfab Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Wed, 23 Sep 2020 20:54:57 -0400 Subject: [PATCH] feat: rewrite with dataloader --- documentation/introduction/getting_started.md | 32 ++- lib/api/api.ex | 12 +- lib/ash_graphql.ex | 123 +++++---- lib/graphql/dataloader.ex | 257 ++++++++++++++++++ lib/graphql/resolver.ex | 145 ++++------ lib/resource/resource.ex | 32 ++- mix.exs | 4 +- mix.lock | 13 +- 8 files changed, 432 insertions(+), 186 deletions(-) create mode 100644 lib/graphql/dataloader.ex diff --git a/documentation/introduction/getting_started.md b/documentation/introduction/getting_started.md index fd131e2..cf09296 100644 --- a/documentation/introduction/getting_started.md +++ b/documentation/introduction/getting_started.md @@ -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 ``` diff --git a/lib/api/api.ex b/lib/api/api.ex index 8733708..50823bf 100644 --- a/lib/api/api.ex +++ b/lib/api/api.ex @@ -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 diff --git a/lib/ash_graphql.ex b/lib/ash_graphql.ex index c6cf90c..0499544 100644 --- a/lib/ash_graphql.ex +++ b/lib/ash_graphql.ex @@ -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 - <> when is_digit(x) and is_digit(y) -> - Enum.random(1..String.to_integer(<>)) - - <> when is_digit(x) and is_digit(y) and is_digit(z) -> - Enum.random(1..String.to_integer(<>)) - - "adv" -> - {:max, roll(["d20", "d20"])} - - "dis" -> - {:min, roll(["d20", "d20"])} - - x -> - x - end) + Map.put(ctx, :ash_loader, dataloader) end end diff --git a/lib/graphql/dataloader.ex b/lib/graphql/dataloader.ex new file mode 100644 index 0000000..169ca7b --- /dev/null +++ b/lib/graphql/dataloader.ex @@ -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 diff --git a/lib/graphql/resolver.ex b/lib/graphql/resolver.ex index 5864d70..99b4ca4 100644 --- a/lib/graphql/resolver.ex +++ b/lib/graphql/resolver.ex @@ -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} -> diff --git a/lib/resource/resource.ex b/lib/resource/resource.ex index 4194ad2..c542f9d 100644 --- a/lib/resource/resource.ex +++ b/lib/resource/resource.ex @@ -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 diff --git a/mix.exs b/mix.exs index a3d1034..e32297d 100644 --- a/mix.exs +++ b/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}, diff --git a/mix.lock b/mix.lock index 4788b16..d18b6b2 100644 --- a/mix.lock +++ b/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"},