mirror of
https://github.com/ash-project/ash_graphql.git
synced 2024-09-20 05:13:33 +12:00
feat: overhaul, better type support, pagination
This commit is contained in:
parent
51dc4b8a0c
commit
d5cf6ee3a0
10 changed files with 388 additions and 119 deletions
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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?))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
|
@ -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)}
|
||||||
|
|
|
@ -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
37
lib/types/json.ex
Normal 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
|
2
mix.exs
2
mix.exs
|
@ -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"},
|
||||||
|
|
6
mix.lock
6
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"},
|
"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"},
|
||||||
|
|
Loading…
Reference in a new issue