feat: overhaul, better type support, pagination

This commit is contained in:
Zach Daniel 2020-11-05 20:59:06 -05:00
parent 51dc4b8a0c
commit d5cf6ee3a0
No known key found for this signature in database
GPG key ID: C377365383138D4B
10 changed files with 388 additions and 119 deletions

View file

@ -6,7 +6,6 @@ locals_without_parens = [
create: 3, create: 3,
destroy: 2, destroy: 2,
destroy: 3, destroy: 3,
fields: 1,
get: 2, get: 2,
get: 3, get: 3,
list: 2, list: 2,

View file

@ -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. 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 ## Add the API Extension
```elixir ```elixir
@ -30,8 +44,6 @@ defmodule Post do
graphql do graphql do
type :post type :post
fields [:name, :count_of_comments, :comments] # <- a list of all of the attributes/relationships/aggregates to include in the graphql API
queries do queries do
get :get_post, :default # <- create a field called `get_post` that uses the `default` read action to fetch a single post get :get_post, :default # <- create a field called `get_post` that uses the `default` read action to fetch a single post
list :list_posts, :default # <- create a field called `list_posts` that uses the `default` read action to fetch a list of posts list :list_posts, :default # <- create a field called `list_posts` that uses the `default` read action to fetch a list of posts

View file

@ -52,12 +52,34 @@ defmodule AshGraphql.Api do
end) end)
if first? do if first? do
[mutation_error(schema), relationship_change(schema)] ++ resource_types [mutation_error(schema), relationship_change(schema), sort_order(schema)] ++ resource_types
else else
resource_types resource_types
end end
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 defp relationship_change(schema) do
%Absinthe.Blueprint.Schema.InputObjectTypeDefinition{ %Absinthe.Blueprint.Schema.InputObjectTypeDefinition{
description: "A set of changes to apply to a relationship", description: "A set of changes to apply to a relationship",

View file

@ -51,7 +51,8 @@ defmodule AshGraphql do
List.update_at(blueprint_with_mutations.schema_definitions, 0, fn schema_def -> List.update_at(blueprint_with_mutations.schema_definitions, 0, fn schema_def ->
%{ %{
schema_def schema_def
| type_definitions: | imports: [{Absinthe.Type.Custom, []} | List.wrap(schema_def.imports)],
type_definitions:
schema_def.type_definitions ++ schema_def.type_definitions ++
AshGraphql.Api.type_definitions(api, __MODULE__, unquote(first?)) AshGraphql.Api.type_definitions(api, __MODULE__, unquote(first?))
} }

View file

@ -211,8 +211,7 @@ defmodule AshGraphql.Dataloader do
case cardinality do case cardinality do
:many -> :many ->
Enum.map(loaded, fn record -> Enum.map(loaded, fn record ->
related = List.wrap(Map.get(record, field)) List.wrap(Map.get(record, field))
%{results: related, count: Enum.count(related)}
end) end)
:one -> :one ->

View file

@ -24,7 +24,7 @@ defmodule AshGraphql.Graphql.Resolver do
end end
def resolve( def resolve(
%{arguments: %{limit: limit, offset: offset} = args, context: context} = resolution, %{arguments: args, context: context, definition: %{selections: selections}} = resolution,
{api, resource, :list, action} {api, resource, :list, action}
) do ) do
opts = [ opts = [
@ -33,24 +33,38 @@ defmodule AshGraphql.Graphql.Resolver do
action: action action: action
] ]
query = page_opts =
resource args
|> Ash.Query.limit(limit) |> Map.take([:limit, :offset, :after, :before])
|> Ash.Query.offset(offset) |> 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 = query =
case Map.fetch(args, :filter) do case Map.fetch(args, :filter) do
{:ok, filter} -> {:ok, filter} ->
case Jason.decode(filter) do case Jason.decode(filter) do
{:ok, decoded} -> {:ok, decoded} ->
Ash.Query.filter(query, ^to_snake_case(decoded)) Ash.Query.filter(resource, ^to_snake_case(decoded))
{:error, error} -> {:error, error} ->
raise "Error parsing filter: #{inspect(error)}" raise "Error parsing filter: #{inspect(error)}"
end end
_ -> _ ->
query Ash.Query.new(resource)
end end
result = result =
@ -58,8 +72,11 @@ defmodule AshGraphql.Graphql.Resolver do
|> Ash.Query.set_tenant(Map.get(context, :tenant)) |> Ash.Query.set_tenant(Map.get(context, :tenant))
|> api.read(opts) |> api.read(opts)
|> case do |> case do
{:ok, %{results: results, count: count}} ->
{:ok, %{results: results, count: count}}
{:ok, results} -> {:ok, results} ->
{:ok, %{results: results, count: Enum.count(results)}} {:ok, results}
error -> error ->
error error
@ -181,7 +198,7 @@ defmodule AshGraphql.Graphql.Resolver do
defp split_attrs_and_rels(input, resource) do defp split_attrs_and_rels(input, resource) do
Enum.reduce(input, {%{}, %{}}, fn {key, value}, {attrs, rels} -> 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} {Map.put(attrs, key, value), rels}
else else
{attrs, Map.put(rels, key, value)} {attrs, Map.put(rels, key, value)}

View file

@ -113,11 +113,6 @@ defmodule AshGraphql.Resource do
type: :atom, type: :atom,
required: true, required: true,
doc: "The type to use for this entity in the graphql schema" 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: [ 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 [ @transformers [
AshGraphql.Resource.Transformers.RequireIdPkey AshGraphql.Resource.Transformers.RequireIdPkey
] ]
@ -155,10 +139,6 @@ defmodule AshGraphql.Resource do
Extension.get_opt(resource, [:graphql], :type, nil) Extension.get_opt(resource, [:graphql], :type, nil)
end end
def fields(resource) do
Extension.get_opt(resource, [:graphql], :fields, [])
end
@doc false @doc false
def queries(api, resource, schema) do def queries(api, resource, schema) do
type = Resource.type(resource) type = Resource.type(resource)
@ -166,15 +146,17 @@ defmodule AshGraphql.Resource do
resource resource
|> queries() |> queries()
|> Enum.map(fn query -> |> Enum.map(fn query ->
query_action = Ash.Resource.action(resource, query.action, :read)
%Absinthe.Blueprint.Schema.FieldDefinition{ %Absinthe.Blueprint.Schema.FieldDefinition{
arguments: args(query.type), arguments: args(query.type, resource, query_action),
identifier: query.name, identifier: query.name,
middleware: [ middleware: [
{{AshGraphql.Graphql.Resolver, :resolve}, {api, resource, query.type, query.action}} {{AshGraphql.Graphql.Resolver, :resolve}, {api, resource, query.type, query.action}}
], ],
module: schema, module: schema,
name: to_string(query.name), name: to_string(query.name),
type: query_type(query.type, type) type: query_type(query.type, query_action, type)
} }
end) end)
end end
@ -316,18 +298,15 @@ defmodule AshGraphql.Resource do
end end
defp mutation_fields(resource, schema, mutation) do defp mutation_fields(resource, schema, mutation) do
fields = Resource.fields(resource)
attribute_fields = attribute_fields =
resource resource
|> Ash.Resource.attributes() |> Ash.Resource.public_attributes()
|> Enum.filter(fn attribute -> |> Enum.filter(fn attribute ->
is_nil(mutation.action.accept) || attribute.name in mutation.action.accept is_nil(mutation.action.accept) || attribute.name in mutation.action.accept
end) end)
|> 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, attribute, resource)
field_type = field_type =
if attribute.allow_nil? || mutation.type == :update do if attribute.allow_nil? || mutation.type == :update do
@ -349,8 +328,7 @@ defmodule AshGraphql.Resource do
relationship_fields = relationship_fields =
resource resource
|> Ash.Resource.relationships() |> Ash.Resource.public_relationships()
|> Enum.filter(&(&1.name in fields))
|> Enum.filter(fn relationship -> |> Enum.filter(fn relationship ->
Resource in Ash.Resource.extensions(relationship.destination) Resource in Ash.Resource.extensions(relationship.destination)
end) end)
@ -388,11 +366,23 @@ defmodule AshGraphql.Resource do
attribute_fields ++ relationship_fields attribute_fields ++ relationship_fields
end end
defp query_type(:get, type), do: type defp query_type(:get, _, type), do: type
# sobelow_skip ["DOS.StringToAtom"] # 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{ %Absinthe.Blueprint.Schema.InputValueDefinition{
name: "id", name: "id",
@ -403,67 +393,197 @@ defmodule AshGraphql.Resource do
] ]
end 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{ %Absinthe.Blueprint.Schema.InputValueDefinition{
name: "filter", name: "filter",
identifier: :filter, identifier: :filter,
type: :string, type: :string,
description: "A json encoded filter to apply" 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 end
@doc false @doc false
def type_definitions(resource, api, schema) do def type_definitions(resource, api, schema) do
[ [
type_definition(resource, api, schema), 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 end
# sobelow_skip ["DOS.StringToAtom"] # sobelow_skip ["DOS.StringToAtom"]
defp page_of(resource, schema) do defp page_of(resource, schema) do
type = Resource.type(resource) type = Resource.type(resource)
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{ paginatable? =
description: "A page of #{inspect(type)}", resource
fields: [ |> Ash.Resource.actions()
%Absinthe.Blueprint.Schema.FieldDefinition{ |> Enum.any?(fn action ->
description: "The records contained in the page", action.type == :read && action.pagination
identifier: :results, end)
module: schema,
name: "results", if paginatable? do
type: %Absinthe.Blueprint.TypeReference.List{ %Absinthe.Blueprint.Schema.ObjectTypeDefinition{
of_type: type 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{ identifier: String.to_atom("page_of_#{type}"),
description: "The count of records", module: schema,
identifier: :count, name: Macro.camelize("page_of_#{type}")
module: schema, }
name: "count", else
type: :integer nil
} end
],
identifier: String.to_atom("page_of_#{type}"),
module: schema,
name: Macro.camelize("page_of_#{type}")
}
end end
defp type_definition(resource, api, schema) do defp type_definition(resource, api, schema) do
@ -479,17 +599,15 @@ defmodule AshGraphql.Resource do
end end
defp fields(resource, api, schema) do defp fields(resource, api, schema) do
fields = Resource.fields(resource) attributes(resource, schema) ++
relationships(resource, api, schema) ++
attributes(resource, schema, fields) ++ aggregates(resource, schema) ++
relationships(resource, api, schema, fields) ++ calculations(resource, schema)
aggregates(resource, schema, fields)
end end
defp attributes(resource, schema, fields) do defp attributes(resource, schema) do
resource resource
|> Ash.Resource.attributes() |> Ash.Resource.public_attributes()
|> Enum.filter(&(&1.name in fields))
|> Enum.map(fn |> Enum.map(fn
%{name: :id} = attribute -> %{name: :id} = attribute ->
%Absinthe.Blueprint.Schema.FieldDefinition{ %Absinthe.Blueprint.Schema.FieldDefinition{
@ -506,16 +624,15 @@ defmodule AshGraphql.Resource do
identifier: attribute.name, identifier: attribute.name,
module: schema, module: schema,
name: to_string(attribute.name), name: to_string(attribute.name),
type: field_type(attribute.type) type: field_type(attribute.type, attribute, resource)
} }
end) end)
end end
# sobelow_skip ["DOS.StringToAtom"] # sobelow_skip ["DOS.StringToAtom"]
defp relationships(resource, api, schema, fields) do defp relationships(resource, api, schema) do
resource resource
|> Ash.Resource.relationships() |> Ash.Resource.public_relationships()
|> Enum.filter(&(&1.name in fields))
|> Enum.filter(fn relationship -> |> Enum.filter(fn relationship ->
Resource in Ash.Resource.extensions(relationship.destination) Resource in Ash.Resource.extensions(relationship.destination)
end) end)
@ -535,8 +652,17 @@ defmodule AshGraphql.Resource do
} }
%{cardinality: :many} = relationship -> %{cardinality: :many} = relationship ->
read_action = Ash.Resource.primary_action!(relationship.destination, :read)
type = Resource.type(relationship.destination) 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{ %Absinthe.Blueprint.Schema.FieldDefinition{
identifier: relationship.name, identifier: relationship.name,
@ -545,16 +671,15 @@ defmodule AshGraphql.Resource do
middleware: [ middleware: [
{{AshGraphql.Graphql.Resolver, :resolve_assoc}, {api, relationship}} {{AshGraphql.Graphql.Resolver, :resolve_assoc}, {api, relationship}}
], ],
arguments: args(:list), arguments: args(:list, relationship.destination, read_action),
type: query_type type: query_type
} }
end) end)
end end
defp aggregates(resource, schema, fields) do defp aggregates(resource, schema) do
resource resource
|> Ash.Resource.aggregates() |> Ash.Resource.public_aggregates()
|> Enum.filter(&(&1.name in fields))
|> Enum.map(fn aggregate -> |> Enum.map(fn aggregate ->
{:ok, type} = Aggregate.kind_to_type(aggregate.kind) {:ok, type} = Aggregate.kind_to_type(aggregate.kind)
@ -562,19 +687,76 @@ defmodule AshGraphql.Resource do
identifier: aggregate.name, identifier: aggregate.name,
module: schema, module: schema,
name: to_string(aggregate.name), name: to_string(aggregate.name),
type: field_type(type) type: field_type(type, nil, resource)
} }
end) end)
end end
defp field_type(Ash.Type.String), do: :string defp calculations(resource, schema) do
defp field_type(Ash.Type.UUID), do: :string resource
defp field_type(Ash.Type.Integer), do: :integer |> Ash.Resource.public_calculations()
defp field_type(Ash.Type.Boolean), do: :boolean |> 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 defp field_type({:array, type}, attribute, resource) do
%Absinthe.Blueprint.TypeReference.List{ new_attribute =
of_type: field_type(type) 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
end end

37
lib/types/json.ex Normal file
View file

@ -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

View file

@ -69,7 +69,7 @@ defmodule AshGraphql.MixProject do
# Run "mix help deps" to learn about dependencies. # Run "mix help deps" to learn about dependencies.
defp deps do defp deps do
[ [
{:ash, ash_version("~> 1.22")}, {:ash, ash_version("~> 1.23")},
{:absinthe, "~> 1.5.3"}, {:absinthe, "~> 1.5.3"},
{:dataloader, "~> 1.0"}, {:dataloader, "~> 1.0"},
{:jason, "~> 1.2"}, {:jason, "~> 1.2"},

View file

@ -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"}, "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"}, "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"},
@ -9,7 +9,7 @@
"decimal": {:hex, :decimal, "1.9.0", "83e8daf59631d632b171faabafb4a9f4242c514b0a06ba3df493951c08f64d07", [:mix], [], "hexpm", "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"}, "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.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"}, "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"},
@ -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"}, "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"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, "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"}, "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.5", "23673bd3080a4489401e25b4896aff1f1138d47b2f650eab724aad1506188ebb", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "b30b3c3abd1f4281902d3b5bc9b67e716509092d6243b010c29d8be4a526e8c8"}, "picosat_elixir": {:hex, :picosat_elixir, "0.1.5", "23673bd3080a4489401e25b4896aff1f1138d47b2f650eab724aad1506188ebb", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "b30b3c3abd1f4281902d3b5bc9b67e716509092d6243b010c29d8be4a526e8c8"},