diff --git a/.formatter.exs b/.formatter.exs index 2ff9403..9ba57ba 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -6,7 +6,6 @@ locals_without_parens = [ create: 3, destroy: 2, destroy: 3, - fields: 1, get: 2, get: 3, list: 2, diff --git a/documentation/introduction/getting_started.md b/documentation/introduction/getting_started.md index d77027e..007fcda 100644 --- a/documentation/introduction/getting_started.md +++ b/documentation/introduction/getting_started.md @@ -4,6 +4,20 @@ 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. +## Bring in the ash_graphql, and absinthe_plug dependencies + +```elixir +def deps() + [ + ... + {:ash_graphql, "~> x.x"} + {:absinthe_plug, "~> x.x"}, + ] +end +``` + +Use `mix hex.info ash_graphql` and `mix hex.info absinthe_plug` to quickly find the latest versions. + ## Add the API Extension ```elixir @@ -30,8 +44,6 @@ defmodule Post do 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 diff --git a/lib/api/api.ex b/lib/api/api.ex index 50823bf..8b78a05 100644 --- a/lib/api/api.ex +++ b/lib/api/api.ex @@ -52,12 +52,34 @@ defmodule AshGraphql.Api do end) if first? do - [mutation_error(schema), relationship_change(schema)] ++ resource_types + [mutation_error(schema), relationship_change(schema), sort_order(schema)] ++ resource_types else resource_types end end + defp sort_order(schema) do + %Absinthe.Blueprint.Schema.EnumTypeDefinition{ + module: schema, + name: "SortOrder", + values: [ + %Absinthe.Blueprint.Schema.EnumValueDefinition{ + module: schema, + identifier: :desc, + name: "DESC", + value: :desc + }, + %Absinthe.Blueprint.Schema.EnumValueDefinition{ + module: schema, + identifier: :asc, + name: "ASC", + value: :asc + } + ], + identifier: :sort_order + } + end + defp relationship_change(schema) do %Absinthe.Blueprint.Schema.InputObjectTypeDefinition{ description: "A set of changes to apply to a relationship", diff --git a/lib/ash_graphql.ex b/lib/ash_graphql.ex index f8c6762..a0952b8 100644 --- a/lib/ash_graphql.ex +++ b/lib/ash_graphql.ex @@ -51,7 +51,8 @@ defmodule AshGraphql do List.update_at(blueprint_with_mutations.schema_definitions, 0, fn schema_def -> %{ schema_def - | type_definitions: + | imports: [{Absinthe.Type.Custom, []} | List.wrap(schema_def.imports)], + type_definitions: schema_def.type_definitions ++ AshGraphql.Api.type_definitions(api, __MODULE__, unquote(first?)) } diff --git a/lib/graphql/dataloader.ex b/lib/graphql/dataloader.ex index ebf7c20..4a2f259 100644 --- a/lib/graphql/dataloader.ex +++ b/lib/graphql/dataloader.ex @@ -211,8 +211,7 @@ defmodule AshGraphql.Dataloader do case cardinality do :many -> Enum.map(loaded, fn record -> - related = List.wrap(Map.get(record, field)) - %{results: related, count: Enum.count(related)} + List.wrap(Map.get(record, field)) end) :one -> diff --git a/lib/graphql/resolver.ex b/lib/graphql/resolver.ex index 33b0883..fa4cb75 100644 --- a/lib/graphql/resolver.ex +++ b/lib/graphql/resolver.ex @@ -24,7 +24,7 @@ defmodule AshGraphql.Graphql.Resolver do end def resolve( - %{arguments: %{limit: limit, offset: offset} = args, context: context} = resolution, + %{arguments: args, context: context, definition: %{selections: selections}} = resolution, {api, resource, :list, action} ) do opts = [ @@ -33,24 +33,38 @@ defmodule AshGraphql.Graphql.Resolver do action: action ] - query = - resource - |> Ash.Query.limit(limit) - |> Ash.Query.offset(offset) + page_opts = + args + |> Map.take([:limit, :offset, :after, :before]) + |> Enum.reject(fn {_, val} -> is_nil(val) end) + + opts = + case page_opts do + [] -> + opts + + page_opts -> + if Enum.any?(selections, &(&1.name == :count)) do + page_opts = Keyword.put(page_opts, :count, true) + Keyword.put(opts, :page, page_opts) + else + Keyword.put(opts, :page, page_opts) + end + end query = case Map.fetch(args, :filter) do {:ok, filter} -> case Jason.decode(filter) do {:ok, decoded} -> - Ash.Query.filter(query, ^to_snake_case(decoded)) + Ash.Query.filter(resource, ^to_snake_case(decoded)) {:error, error} -> raise "Error parsing filter: #{inspect(error)}" end _ -> - query + Ash.Query.new(resource) end result = @@ -58,8 +72,11 @@ defmodule AshGraphql.Graphql.Resolver do |> Ash.Query.set_tenant(Map.get(context, :tenant)) |> api.read(opts) |> case do + {:ok, %{results: results, count: count}} -> + {:ok, %{results: results, count: count}} + {:ok, results} -> - {:ok, %{results: results, count: Enum.count(results)}} + {:ok, results} error -> error @@ -181,7 +198,7 @@ defmodule AshGraphql.Graphql.Resolver do defp split_attrs_and_rels(input, resource) do Enum.reduce(input, {%{}, %{}}, fn {key, value}, {attrs, rels} -> - if Ash.Resource.attribute(resource, key) do + if Ash.Resource.public_attribute(resource, key) do {Map.put(attrs, key, value), rels} else {attrs, Map.put(rels, key, value)} diff --git a/lib/resource/resource.ex b/lib/resource/resource.ex index c542f9d..b57ab1c 100644 --- a/lib/resource/resource.ex +++ b/lib/resource/resource.ex @@ -113,11 +113,6 @@ defmodule AshGraphql.Resource do type: :atom, required: true, doc: "The type to use for this entity in the graphql schema" - ], - fields: [ - type: {:custom, __MODULE__, :__fields, []}, - required: true, - doc: "The fields from this entity to include in the graphql" ] ], sections: [ @@ -126,17 +121,6 @@ defmodule AshGraphql.Resource do ] } - @doc false - def __fields(fields) do - fields = List.wrap(fields) - - if Enum.all?(fields, &is_atom/1) do - {:ok, fields} - else - {:error, "Expected `fields` to be a list of atoms"} - end - end - @transformers [ AshGraphql.Resource.Transformers.RequireIdPkey ] @@ -155,10 +139,6 @@ defmodule AshGraphql.Resource do Extension.get_opt(resource, [:graphql], :type, nil) end - def fields(resource) do - Extension.get_opt(resource, [:graphql], :fields, []) - end - @doc false def queries(api, resource, schema) do type = Resource.type(resource) @@ -166,15 +146,17 @@ defmodule AshGraphql.Resource do resource |> queries() |> Enum.map(fn query -> + query_action = Ash.Resource.action(resource, query.action, :read) + %Absinthe.Blueprint.Schema.FieldDefinition{ - arguments: args(query.type), + arguments: args(query.type, resource, query_action), identifier: query.name, middleware: [ {{AshGraphql.Graphql.Resolver, :resolve}, {api, resource, query.type, query.action}} ], module: schema, name: to_string(query.name), - type: query_type(query.type, type) + type: query_type(query.type, query_action, type) } end) end @@ -316,18 +298,15 @@ defmodule AshGraphql.Resource do end defp mutation_fields(resource, schema, mutation) do - fields = Resource.fields(resource) - attribute_fields = resource - |> Ash.Resource.attributes() + |> Ash.Resource.public_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) + type = field_type(attribute.type, attribute, resource) field_type = if attribute.allow_nil? || mutation.type == :update do @@ -349,8 +328,7 @@ defmodule AshGraphql.Resource do relationship_fields = resource - |> Ash.Resource.relationships() - |> Enum.filter(&(&1.name in fields)) + |> Ash.Resource.public_relationships() |> Enum.filter(fn relationship -> Resource in Ash.Resource.extensions(relationship.destination) end) @@ -388,11 +366,23 @@ defmodule AshGraphql.Resource do attribute_fields ++ relationship_fields end - defp query_type(:get, type), do: type + defp query_type(:get, _, type), do: type # sobelow_skip ["DOS.StringToAtom"] - defp query_type(:list, type), do: String.to_atom("page_of_#{type}") + defp query_type(:list, action, type) do + if action.pagination do + String.to_atom("page_of_#{type}") + else + %Absinthe.Blueprint.TypeReference.NonNull{ + of_type: %Absinthe.Blueprint.TypeReference.List{ + of_type: %Absinthe.Blueprint.TypeReference.NonNull{ + of_type: type + } + } + } + end + end - defp args(:get) do + defp args(:get, _resource, _action) do [ %Absinthe.Blueprint.Schema.InputValueDefinition{ name: "id", @@ -403,67 +393,197 @@ defmodule AshGraphql.Resource do ] end - defp args(:list) do + defp args(:list, resource, action) do [ - %Absinthe.Blueprint.Schema.InputValueDefinition{ - name: "limit", - identifier: :limit, - type: :integer, - description: "The limit of records to return", - default_value: 20 - }, - %Absinthe.Blueprint.Schema.InputValueDefinition{ - name: "offset", - identifier: :offset, - type: :integer, - description: "The count of records to skip", - default_value: 0 - }, %Absinthe.Blueprint.Schema.InputValueDefinition{ name: "filter", identifier: :filter, type: :string, description: "A json encoded filter to apply" + }, + %Absinthe.Blueprint.Schema.InputValueDefinition{ + name: "sort", + identifier: :sort, + type: resource_sort_type(resource), + description: "How to sort the records in the response" } - ] + ] ++ + pagination_args(action) + end + + defp pagination_args(action) do + if action.pagination do + max_message = + if action.pagination.max_page_size do + " Maximum #{action.pagination.max_page_size}" + else + "" + end + + limit_type = + if action.pagination.required? && is_nil(action.pagination.default_page_size) do + %Absinthe.Blueprint.TypeReference.NonNull{ + of_type: :integer + } + else + :integer + end + + [ + %Absinthe.Blueprint.Schema.InputValueDefinition{ + name: "limit", + identifier: :limit, + type: limit_type, + default_value: action.pagination.default_page_size, + description: "The number of records to return." <> max_message + } + ] ++ keyset_pagination_args(action) ++ offset_pagination_args(action) + else + [] + end + end + + # sobelow_skip ["DOS.StringToAtom"] + defp resource_sort_type(resource) do + String.to_atom(to_string(AshGraphql.Resource.type(resource)) <> "_sort_input") + end + + defp keyset_pagination_args(action) do + if action.pagination.keyset? do + [ + %Absinthe.Blueprint.Schema.InputValueDefinition{ + name: "before", + identifier: :before, + type: :string, + description: "Show records before the specified keyset." + }, + %Absinthe.Blueprint.Schema.InputValueDefinition{ + name: "after", + identifier: :after, + type: :string, + description: "Show records after the specified keyset." + } + ] + else + [] + end + end + + defp offset_pagination_args(action) do + if action.pagination.offset? do + [ + %Absinthe.Blueprint.Schema.InputValueDefinition{ + name: "offset", + identifier: :offset, + type: :integer, + description: "The number of records to skip." + } + ] + else + [] + end end @doc false def type_definitions(resource, api, schema) do [ type_definition(resource, api, schema), - page_of(resource, schema) - ] + sort_input(resource, schema) + ] ++ List.wrap(page_of(resource, schema)) ++ enum_definitions(resource, schema) + end + + defp sort_input(resource, schema) do + type = resource_sort_type(resource) + + %Absinthe.Blueprint.Schema.InputObjectTypeDefinition{ + fields: sort_fields(resource, schema), + identifier: type, + module: schema, + name: type |> to_string() |> Macro.camelize() + } + end + + defp sort_fields(resource, schema) do + resource + |> Ash.Resource.attributes() + |> Enum.map(fn attribute -> + %Absinthe.Blueprint.Schema.FieldDefinition{ + identifier: attribute.name, + module: schema, + name: to_string(attribute.name), + type: %Absinthe.Blueprint.TypeReference.NonNull{ + of_type: :sort_order + } + } + end) + end + + defp enum_definitions(resource, schema) do + resource + |> Ash.Resource.public_attributes() + |> Enum.filter(&(&1.type == Ash.Type.Atom)) + |> Enum.filter(&is_list(&1.constraints[:one_of])) + |> Enum.map(fn attribute -> + type_name = atom_enum_type(resource, attribute.name) + + %Absinthe.Blueprint.Schema.EnumTypeDefinition{ + module: schema, + name: type_name |> to_string() |> Macro.camelize(), + values: + Enum.map(attribute.constraints[:one_of], fn value -> + %Absinthe.Blueprint.Schema.EnumValueDefinition{ + module: schema, + identifier: value, + name: String.upcase(to_string(value)), + value: value + } + end), + identifier: type_name + } + end) end # sobelow_skip ["DOS.StringToAtom"] defp page_of(resource, schema) do type = Resource.type(resource) - %Absinthe.Blueprint.Schema.ObjectTypeDefinition{ - description: "A page of #{inspect(type)}", - fields: [ - %Absinthe.Blueprint.Schema.FieldDefinition{ - description: "The records contained in the page", - identifier: :results, - module: schema, - name: "results", - type: %Absinthe.Blueprint.TypeReference.List{ - of_type: type + paginatable? = + resource + |> Ash.Resource.actions() + |> Enum.any?(fn action -> + action.type == :read && action.pagination + end) + + if paginatable? do + %Absinthe.Blueprint.Schema.ObjectTypeDefinition{ + description: "A page of #{inspect(type)}", + fields: [ + %Absinthe.Blueprint.Schema.FieldDefinition{ + description: "The records contained in the page", + identifier: :results, + module: schema, + name: "results", + type: %Absinthe.Blueprint.TypeReference.List{ + of_type: %Absinthe.Blueprint.TypeReference.NonNull{ + of_type: type + } + } + }, + %Absinthe.Blueprint.Schema.FieldDefinition{ + description: "The count of records", + identifier: :count, + module: schema, + name: "count", + type: :integer } - }, - %Absinthe.Blueprint.Schema.FieldDefinition{ - description: "The count of records", - identifier: :count, - module: schema, - name: "count", - type: :integer - } - ], - identifier: String.to_atom("page_of_#{type}"), - module: schema, - name: Macro.camelize("page_of_#{type}") - } + ], + identifier: String.to_atom("page_of_#{type}"), + module: schema, + name: Macro.camelize("page_of_#{type}") + } + else + nil + end end defp type_definition(resource, api, schema) do @@ -479,17 +599,15 @@ defmodule AshGraphql.Resource do end defp fields(resource, api, schema) do - fields = Resource.fields(resource) - - attributes(resource, schema, fields) ++ - relationships(resource, api, schema, fields) ++ - aggregates(resource, schema, fields) + attributes(resource, schema) ++ + relationships(resource, api, schema) ++ + aggregates(resource, schema) ++ + calculations(resource, schema) end - defp attributes(resource, schema, fields) do + defp attributes(resource, schema) do resource - |> Ash.Resource.attributes() - |> Enum.filter(&(&1.name in fields)) + |> Ash.Resource.public_attributes() |> Enum.map(fn %{name: :id} = attribute -> %Absinthe.Blueprint.Schema.FieldDefinition{ @@ -506,16 +624,15 @@ defmodule AshGraphql.Resource do identifier: attribute.name, module: schema, name: to_string(attribute.name), - type: field_type(attribute.type) + type: field_type(attribute.type, attribute, resource) } end) end # sobelow_skip ["DOS.StringToAtom"] - defp relationships(resource, api, schema, fields) do + defp relationships(resource, api, schema) do resource - |> Ash.Resource.relationships() - |> Enum.filter(&(&1.name in fields)) + |> Ash.Resource.public_relationships() |> Enum.filter(fn relationship -> Resource in Ash.Resource.extensions(relationship.destination) end) @@ -535,8 +652,17 @@ defmodule AshGraphql.Resource do } %{cardinality: :many} = relationship -> + read_action = Ash.Resource.primary_action!(relationship.destination, :read) + type = Resource.type(relationship.destination) - query_type = String.to_atom("page_of_#{type}") + + query_type = %Absinthe.Blueprint.TypeReference.NonNull{ + of_type: %Absinthe.Blueprint.TypeReference.List{ + of_type: %Absinthe.Blueprint.TypeReference.NonNull{ + of_type: type + } + } + } %Absinthe.Blueprint.Schema.FieldDefinition{ identifier: relationship.name, @@ -545,16 +671,15 @@ defmodule AshGraphql.Resource do middleware: [ {{AshGraphql.Graphql.Resolver, :resolve_assoc}, {api, relationship}} ], - arguments: args(:list), + arguments: args(:list, relationship.destination, read_action), type: query_type } end) end - defp aggregates(resource, schema, fields) do + defp aggregates(resource, schema) do resource - |> Ash.Resource.aggregates() - |> Enum.filter(&(&1.name in fields)) + |> Ash.Resource.public_aggregates() |> Enum.map(fn aggregate -> {:ok, type} = Aggregate.kind_to_type(aggregate.kind) @@ -562,19 +687,76 @@ defmodule AshGraphql.Resource do identifier: aggregate.name, module: schema, name: to_string(aggregate.name), - type: field_type(type) + type: field_type(type, nil, resource) } end) end - defp field_type(Ash.Type.String), do: :string - defp field_type(Ash.Type.UUID), do: :string - defp field_type(Ash.Type.Integer), do: :integer - defp field_type(Ash.Type.Boolean), do: :boolean + defp calculations(resource, schema) do + resource + |> Ash.Resource.public_calculations() + |> Enum.map(fn calculation -> + %Absinthe.Blueprint.Schema.FieldDefinition{ + identifier: calculation.name, + module: schema, + name: to_string(calculation.name), + type: field_type(calculation.type, nil, resource) + } + end) + end - defp field_type({:array, type}) do - %Absinthe.Blueprint.TypeReference.List{ - of_type: field_type(type) - } + defp field_type({:array, type}, attribute, resource) do + new_attribute = + if attribute do + new_constraints = attribute.constraints[:items] || [] + %{attribute | constraints: new_constraints, type: type} + end + + if attribute.constraints[:nil_items?] do + %Absinthe.Blueprint.TypeReference.List{ + of_type: field_type(type, new_attribute, resource) + } + else + %Absinthe.Blueprint.TypeReference.List{ + of_type: %Absinthe.Blueprint.TypeReference.NonNull{ + of_type: field_type(type, new_attribute, resource) + } + } + end + end + + defp field_type(type, attribute, resource) do + if Ash.Type.builtin?(type) do + do_field_type(type, attribute, resource) + else + type.graphql_type(attribute, resource) + end + end + + defp do_field_type(Ash.Type.Atom, %{constraints: constraints, name: name}, resource) do + if is_list(constraints[:one_of]) do + atom_enum_type(resource, name) + else + :string + end + end + + defp do_field_type(Ash.Type.Map, _, _), do: :json + defp do_field_type(Ash.Type.Term, _, _), do: :string + defp do_field_type(Ash.Type.String, _, _), do: :string + defp do_field_type(Ash.Type.Integer, _, _), do: :integer + defp do_field_type(Ash.Type.Boolean, _, _), do: :boolean + defp do_field_type(Ash.Type.UUID, _, _), do: :string + defp do_field_type(Ash.Type.Date, _, _), do: :date + defp do_field_type(Ash.Type.UtcDatetime, _, _), do: :naive_datetime + + # sobelow_skip ["DOS.StringToAtom"] + defp atom_enum_type(resource, attribute_name) do + resource + |> AshGraphql.Resource.type() + |> to_string() + |> Kernel.<>("_") + |> Kernel.<>(to_string(attribute_name)) + |> String.to_atom() end end diff --git a/lib/types/json.ex b/lib/types/json.ex new file mode 100644 index 0000000..7eb9092 --- /dev/null +++ b/lib/types/json.ex @@ -0,0 +1,37 @@ +defmodule AshGraphql.Types.JSON do + @moduledoc """ + The Json scalar type allows arbitrary JSON values to be passed in and out. + """ + use Absinthe.Schema.Notation + + scalar :json, name: "Json" do + description(""" + The `Json` scalar type represents arbitrary json string data, represented as UTF-8 + character sequences. The Json type is most often used to represent a free-form + human-readable json string. + """) + + serialize(&encode/1) + parse(&decode/1) + end + + @spec decode(Absinthe.Blueprint.Input.String.t()) :: {:ok, term()} | :error + @spec decode(Absinthe.Blueprint.Input.Null.t()) :: {:ok, nil} + defp decode(%Absinthe.Blueprint.Input.String{value: value}) do + case Jason.decode(value) do + {:ok, result} -> {:ok, result} + _ -> :error + end + end + + defp decode(%Absinthe.Blueprint.Input.Null{}) do + {:ok, nil} + end + + defp decode(_) do + :error + end + + defp encode(nil), do: nil + defp encode(value), do: Jason.encode!(value) +end diff --git a/mix.exs b/mix.exs index ad3c0e9..e21dabb 100644 --- a/mix.exs +++ b/mix.exs @@ -69,7 +69,7 @@ defmodule AshGraphql.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:ash, ash_version("~> 1.22")}, + {:ash, ash_version("~> 1.23")}, {:absinthe, "~> 1.5.3"}, {:dataloader, "~> 1.0"}, {:jason, "~> 1.2"}, diff --git a/mix.lock b/mix.lock index c2c7034..f763a2f 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,6 @@ %{ "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.22.0", "1a017e3caf1d7a96039d4b12e9c13dd4f07a88391b68ce2d6510a914a535a812", [: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.5", [hex: :picosat_elixir, repo: "hexpm", optional: false]}], "hexpm", "059be8f2665e1ed355e281490b894e460e8a0b59984db4f2b0bc6cb7324348ad"}, + "ash": {:hex, :ash, "1.23.0", "48eebc2b028827784e3f00f61644276e8e5ccedce5f84a3b0818f4eeeeebec3d", [: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.5", [hex: :picosat_elixir, repo: "hexpm", optional: false]}], "hexpm", "5bcb53bc195193e4a8f60f21813fa7fb44c8a0b4259ca6c19f2aecd80b6fadde"}, "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"}, @@ -9,7 +9,7 @@ "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.5.3", "64aa70c6a64b8ee6a28ee186083b317b082beac8fed4d55bcc3f23199667a2f3", [: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", "c481b220bb080e94fd1ab528c3b62bdfdd29188c74aef44fc2b204efa8769532"}, + "ecto": {:hex, :ecto, "3.5.4", "73ee115deb10769c73fd2d27e19e36bc4af7c56711ad063616a86aec44f80f6f", [: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", "7f13f9c9c071bd2ca04652373ff3edd1d686364de573255096872a4abc471807"}, "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"}, @@ -26,7 +26,7 @@ "makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "nimble_options": {:hex, :nimble_options, "0.3.2", "6083b13b09944da6afb08b5cedc6ee98c6511c6fa2f37a23671f7944991032a2", [:mix], [], "hexpm", "3468123b4884f1b2e7e2251651b95d3497c01dec9d16b77a866058821ccd7dda"}, + "nimble_options": {:hex, :nimble_options, "0.3.3", "49f52786980c371435bab03246392dfa32f49738344f27662838566d5276b0de", [:mix], [], "hexpm", "79db909818900c5469616b4db25c5c194daeebf4b76ad324e434d9c3172ce0bd"}, "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.5", "23673bd3080a4489401e25b4896aff1f1138d47b2f650eab724aad1506188ebb", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "b30b3c3abd1f4281902d3b5bc9b67e716509092d6243b010c29d8be4a526e8c8"},