feat: use the new DSL builder for config (#7)

This commit is contained in:
Zach Daniel 2020-06-14 03:04:18 -04:00 committed by GitHub
parent e5d0a459fa
commit 6c5265b4a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 666 additions and 653 deletions

View file

@ -1,649 +1,21 @@
defmodule AshPostgres do
@using_opts_schema [
repo: [
type: :atom,
required: true,
doc:
"The repo that will be used to fetch your data. See the `Ecto.Repo` documentation for more"
],
table: [
type: :string,
doc: "The name of the database table backing the resource",
required: true
]
]
alias Ash.Filter.{And, Eq, In, NotEq, NotIn, Or}
alias AshPostgres.Predicates.Trigram
@moduledoc """
A postgres data layer that levereges Ecto's postgres tools.
A postgres extension library for `Ash`.
To use it, add `use AshPostgres, repo: MyRepo` to your resource, after `use Ash.Resource`
`AshPostgres.DataLayer` provides a DataLayer, and a DSL extension to configure that data layer.
#{NimbleOptions.docs(@using_opts_schema)}
The dsl extension exposes the `postgres` section. See: `AshPostgres.DataLayer.postgres/1` for more.
"""
@behaviour Ash.DataLayer
defmacro __using__(opts) do
quote bind_quoted: [opts: opts] do
opts = AshPostgres.validate_using_opts(__MODULE__, opts)
@data_layer AshPostgres
@repo opts[:repo]
@table opts[:table]
def repo do
@repo
end
def postgres_table do
@table
end
end
end
def validate_using_opts(mod, opts) do
case NimbleOptions.validate(opts, @using_opts_schema) do
{:ok, opts} ->
opts
{:error, message} ->
raise Ash.Error.ResourceDslError,
resource: mod,
using: __MODULE__,
message: message
end
end
def postgres_repo?(repo) do
repo.__adapter__() == Ecto.Adapters.Postgres
end
alias Ash.Dsl.Extension
@doc "Fetch the configured repo for a resource"
def repo(resource) do
resource.repo()
Extension.get_opt(resource, [:postgres], :repo)
end
@impl true
def custom_filters(resource) do
config = repo(resource).config()
add_pg_trgm_search(%{}, config)
end
defp add_pg_trgm_search(filters, config) do
if "pg_trgm" in config[:installed_extensions] do
Map.update(filters, :string, [{:trigram, AshPostgres.Predicates.Trigram}], fn filters ->
[{:trigram, AshPostgres.Predicates.Trigram} | filters]
end)
else
filters
end
end
import Ecto.Query, only: [from: 2]
@impl true
def can?(_, capability), do: can?(capability)
def can?(:query_async), do: true
def can?(:transact), do: true
def can?(:composite_primary_key), do: true
def can?(:upsert), do: true
def can?({:filter, :in}), do: true
def can?({:filter, :not_in}), do: true
def can?({:filter, :not_eq}), do: true
def can?({:filter, :eq}), do: true
def can?({:filter, :and}), do: true
def can?({:filter, :or}), do: true
def can?({:filter, :not}), do: true
def can?({:filter, :trigram}), do: true
def can?({:filter_related, _}), do: true
def can?(_), do: false
@impl true
def limit(query, nil, _), do: {:ok, query}
def limit(query, limit, _resource) do
{:ok, from(row in query, limit: ^limit)}
end
@impl true
def offset(query, nil, _), do: query
def offset(query, offset, _resource) do
{:ok, from(row in query, offset: ^offset)}
end
@impl true
def run_query(%{__impossible__: true}, _) do
{:ok, []}
end
@impl true
def run_query(query, resource) do
{:ok, repo(resource).all(query)}
end
@impl true
def resource_to_query(resource),
do: Ecto.Queryable.to_query({resource.postgres_table(), resource})
@impl true
def create(resource, changeset) do
changeset =
Map.update!(changeset, :action, fn
:create -> :insert
action -> action
end)
changeset =
Map.update!(changeset, :data, fn data ->
Map.update!(data, :__meta__, &Map.put(&1, :source, resource.postgres_table()))
end)
repo(resource).insert(changeset)
rescue
e ->
{:error, e}
end
@impl true
def upsert(resource, changeset) do
changeset =
Map.update!(changeset, :action, fn
:create -> :insert
action -> action
end)
changeset =
Map.update!(changeset, :data, fn data ->
Map.update!(data, :__meta__, &Map.put(&1, :source, resource.postgres_table()))
end)
repo(resource).insert(changeset,
on_conflict: :replace_all,
conflict_target: Ash.primary_key(resource)
)
rescue
e ->
{:error, e}
end
@impl true
def update(resource, changeset) do
repo(resource).update(changeset)
rescue
e ->
{:error, e}
end
@impl true
def destroy(%resource{} = record) do
case repo(resource).delete(record) do
{:ok, _record} -> :ok
{:error, error} -> {:error, error}
end
rescue
e ->
{:error, e}
end
@impl true
def sort(query, sort, _resource) do
{:ok,
from(row in query,
order_by: ^sanitize_sort(sort)
)}
end
defp sanitize_sort(sort) do
sort
|> List.wrap()
|> Enum.map(fn
{sort, order} -> {order, sort}
sort -> sort
end)
end
@impl true
def filter(query, filter, _resource) do
new_query =
query
|> Map.put(:bindings, %{})
|> join_all_relationships(filter)
|> add_filter_expression(filter)
impossible_query =
if filter.impossible? do
Map.put(new_query, :__impossible__, true)
else
new_query
end
{:ok, impossible_query}
end
defp join_all_relationships(query, filter, path \\ []) do
query =
Map.put_new(query, :__ash_bindings__, %{current: Enum.count(query.joins) + 1, bindings: %{}})
Enum.reduce(filter.relationships, query, fn {name, relationship_filter}, query ->
join_type = :left
case {join_type, relationship_filter} do
{join_type, relationship_filter} ->
relationship = Ash.relationship(filter.resource, name)
current_path = [relationship | path]
joined_query = join_relationship(query, current_path, join_type)
joined_query_with_distinct =
join_and_add_distinct(relationship, join_type, joined_query, filter)
join_all_relationships(joined_query_with_distinct, relationship_filter, current_path)
end
end)
end
defp join_and_add_distinct(relationship, join_type, joined_query, filter) do
if relationship.cardinality == :many and join_type == :left && !joined_query.distinct do
from(row in joined_query,
distinct: ^Ash.primary_key(filter.resource)
)
else
joined_query
end
end
defp join_relationship(query, path, join_type) do
path_names = Enum.map(path, & &1.name)
case Map.get(query.__ash_bindings__.bindings, path_names) do
%{type: existing_join_type} when join_type != existing_join_type ->
raise "unreachable?"
nil ->
do_join_relationship(query, path, join_type)
_ ->
query
end
end
defp do_join_relationship(query, relationships, kind, path \\ [])
defp do_join_relationship(_, [], _, _), do: nil
defp do_join_relationship(query, [%{type: :many_to_many} = relationship | rest], :inner, path) do
relationship_through = maybe_get_resource_query(relationship.through)
relationship_destination =
do_join_relationship(query, rest, :inner, [relationship.name | path]) ||
Ecto.Queryable.to_query(maybe_get_resource_query(relationship.destination))
new_query =
from(row in query,
join: through in ^relationship_through,
on:
field(row, ^relationship.source_field) ==
field(through, ^relationship.source_field_on_join_table),
join: destination in ^relationship_destination,
on:
field(destination, ^relationship.destination_field) ==
field(through, ^relationship.destination_field_on_join_table)
)
join_path =
Enum.reverse([String.to_existing_atom(to_string(relationship.name) <> "_join_assoc") | path])
full_path = Enum.reverse([relationship.name | path])
new_query
|> add_binding(join_path, :inner)
|> add_binding(full_path, :inner)
|> merge_bindings(relationship_destination)
end
defp do_join_relationship(query, [relationship | rest], :inner, path) do
relationship_destination =
do_join_relationship(query, rest, :inner, [relationship.name | path]) ||
Ecto.Queryable.to_query(maybe_get_resource_query(relationship.destination))
new_query =
from(row in query,
join: destination in ^relationship_destination,
on:
field(row, ^relationship.source_field) ==
field(destination, ^relationship.destination_field)
)
new_query
|> add_binding(Enum.reverse([relationship.name | path]), :inner)
|> merge_bindings(relationship_destination)
end
defp do_join_relationship(query, [%{type: :many_to_many} = relationship | rest], :left, path) do
relationship_through = maybe_get_resource_query(relationship.through)
relationship_destination =
do_join_relationship(query, rest, :inner, [relationship.name | path]) ||
Ecto.Queryable.to_query(maybe_get_resource_query(relationship.destination))
new_query =
from(row in query,
left_join: through in ^relationship_through,
on:
field(row, ^relationship.source_field) ==
field(through, ^relationship.source_field_on_join_table),
left_join: destination in ^relationship_destination,
on:
field(destination, ^relationship.destination_field) ==
field(through, ^relationship.destination_field_on_join_table)
)
join_path =
Enum.reverse([String.to_existing_atom(to_string(relationship.name) <> "_join_assoc") | path])
full_path = Enum.reverse([relationship.name | path])
new_query
|> add_binding(join_path, :left)
|> add_binding(full_path, :left)
|> merge_bindings(relationship_destination)
end
defp do_join_relationship(query, [relationship | rest], :left, path) do
relationship_destination =
do_join_relationship(query, rest, :inner, [relationship.name | path]) ||
Ecto.Queryable.to_query(maybe_get_resource_query(relationship.destination))
new_query =
from(row in query,
left_join: destination in ^relationship_destination,
on:
field(row, ^relationship.source_field) ==
field(destination, ^relationship.destination_field)
)
new_query
|> add_binding(Enum.reverse([relationship.name | path]), :left)
|> merge_bindings(relationship_destination)
end
defp add_filter_expression(query, filter) do
{params, expr} = filter_to_expr(filter, query.__ash_bindings__.bindings, [])
if expr do
boolean_expr = %Ecto.Query.BooleanExpr{
expr: expr,
op: :and,
params: params
}
%{query | wheres: [boolean_expr | query.wheres]}
else
query
end
end
defp join_exprs(nil, nil, _op), do: nil
defp join_exprs(expr, nil, _op), do: expr
defp join_exprs(nil, expr, _op), do: expr
defp join_exprs(left_expr, right_expr, op), do: {op, [], [left_expr, right_expr]}
defp filter_to_expr(filter, bindings, params, current_binding \\ 0, path \\ [])
# A completely empty filter means "everything"
defp filter_to_expr(%{impossible?: true}, _, _, _, _), do: {[], false}
defp filter_to_expr(
%{ands: [], ors: [], not: nil, attributes: attrs, relationships: rels},
_,
_,
_,
_
)
when attrs == %{} and rels == %{} do
{[], true}
end
defp filter_to_expr(filter, bindings, params, current_binding, path) do
{params, expr} =
Enum.reduce(filter.attributes, {params, nil}, fn {attribute, filter},
{params, existing_expr} ->
{params, new_expr} = filter_value_to_expr(attribute, filter, current_binding, params)
{params, join_exprs(existing_expr, new_expr, :and)}
end)
{params, expr} =
Enum.reduce(filter.relationships, {params, expr}, fn {relationship, relationship_filter},
{params, existing_expr} ->
full_path = path ++ [relationship]
binding =
Map.get(bindings, full_path) ||
raise "unbound relationship #{inspect(full_path)} referenced! #{inspect(bindings)}"
{params, new_expr} =
filter_to_expr(relationship_filter, bindings, params, binding.binding, full_path)
{params, join_exprs(new_expr, existing_expr, :and)}
end)
{params, expr} =
Enum.reduce(filter.ors, {params, expr}, fn or_filter, {params, existing_expr} ->
{params, new_expr} = filter_to_expr(or_filter, bindings, params, current_binding, path)
{params, join_exprs(existing_expr, new_expr, :or)}
end)
{params, expr} =
case filter.not do
nil ->
{params, expr}
not_filter ->
{params, new_expr} = filter_to_expr(not_filter, bindings, params, current_binding, path)
{params, join_exprs(expr, {:not, [], [new_expr]}, :and)}
end
{params, expr} =
Enum.reduce(filter.ands, {params, expr}, fn and_filter, {params, existing_expr} ->
{params, new_expr} = filter_to_expr(and_filter, bindings, params, current_binding, path)
{params, join_exprs(existing_expr, new_expr, :and)}
end)
if expr do
{params, expr}
else
# A filter that was not empty, but didn't generate an expr for some reason, should default to `false`
# AFAIK this shouldn't actually be possible
{params, false}
end
end
# THe fact that we keep counting params here is very silly.
defp filter_value_to_expr(attribute, %Eq{value: value}, current_binding, params) do
{params ++ [{value, {current_binding, attribute}}],
{:==, [],
[
{{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
{:^, [], [Enum.count(params)]}
]}}
end
defp filter_value_to_expr(attribute, %NotEq{value: value}, current_binding, params) do
{params ++ [{value, {current_binding, attribute}}],
{:==, [],
[
{{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
{:^, [], [Enum.count(params)]}
]}}
end
defp filter_value_to_expr(attribute, %In{values: values}, current_binding, params) do
{params ++ [{values, {:in, {current_binding, attribute}}}],
{:in, [],
[
{{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
{:^, [], [Enum.count(params)]}
]}}
end
defp filter_value_to_expr(
attribute,
%NotIn{values: values},
current_binding,
params
) do
{params ++ [{values, {:in, {current_binding, attribute}}}],
{:not,
{:in, [],
[
{{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
{:^, [], [Enum.count(params)]}
]}}}
end
defp filter_value_to_expr(
attribute,
%Trigram{} = trigram,
current_binding,
params
) do
param_count = Enum.count(params)
case trigram do
%{equals: equals, greater_than: nil, less_than: nil, text: text} ->
{params ++ [{text, {current_binding, attribute}}, {equals, :float}],
{:fragment, [],
[
raw: "similarity(",
expr: {{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
raw: ", ",
expr: {:^, [], [param_count]},
raw: ") = ",
expr: {:^, [], [param_count + 1]},
raw: ""
]}}
%{equals: nil, greater_than: greater_than, less_than: nil, text: text} ->
{params ++ [{text, {current_binding, attribute}}, {greater_than, :float}],
{:fragment, [],
[
raw: "similarity(",
expr: {{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
raw: ", ",
expr: {:^, [], [param_count]},
raw: ") > ",
expr: {:^, [], [param_count + 1]},
raw: ""
]}}
%{equals: nil, greater_than: nil, less_than: less_than, text: text} ->
{params ++ [{text, {current_binding, attribute}}, {less_than, :float}],
{:fragment, [],
[
raw: "similarity(",
expr: {{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
raw: ", ",
expr: {:^, [], [param_count]},
raw: ") < ",
expr: {:^, [], [param_count + 1]},
raw: ""
]}}
%{equals: nil, greater_than: greater_than, less_than: less_than, text: text} ->
{params ++
[{text, {current_binding, attribute}}, {less_than, :float}, {greater_than, :float}],
{:fragment, [],
[
raw: "similarity(",
expr: {{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
raw: ", ",
expr: {:^, [], [param_count]},
raw: ") BETWEEN ",
expr: {:^, [], [param_count + 1]},
raw: " AND ",
expr: {:^, [], [param_count + 2]},
raw: ""
]}}
end
end
defp filter_value_to_expr(
attribute,
%And{left: left, right: right},
current_binding,
params
) do
{params, left_expr} = filter_value_to_expr(attribute, left, current_binding, params)
{params, right_expr} = filter_value_to_expr(attribute, right, current_binding, params)
{params, join_exprs(left_expr, right_expr, :and)}
end
defp filter_value_to_expr(
attribute,
%Or{left: left, right: right},
current_binding,
params
) do
{params, left_expr} = filter_value_to_expr(attribute, left, current_binding, params)
{params, right_expr} = filter_value_to_expr(attribute, right, current_binding, params)
{params, join_exprs(left_expr, right_expr, :or)}
end
defp merge_bindings(query, %{__ash_bindings__: ash_bindings}) do
ash_bindings
|> Map.get(:bindings)
|> Enum.reduce(query, fn {path, data}, query ->
add_binding(query, path, data)
end)
end
defp merge_bindings(query, _) do
query
end
defp add_binding(query, path, type) do
current = query.__ash_bindings__.current
bindings = query.__ash_bindings__.bindings
new_ash_bindings = %{
query.__ash_bindings__
| bindings: do_add_binding(bindings, path, current, type),
current: current + 1
}
%{query | __ash_bindings__: new_ash_bindings}
end
defp do_add_binding(bindings, path, current, type) do
Map.put(bindings, path, %{binding: current, type: type})
end
@impl true
def can_query_async?(resource) do
repo(resource).in_transaction?()
end
@impl true
def transaction(resource, func) do
repo(resource).transaction(func)
end
defp maybe_get_resource_query(resource) do
if Ash.resource_module?(resource) do
{resource.postgres_table(), resource}
else
resource
end
@doc "Fetch the configured table for a resource"
def table(resource) do
Extension.get_opt(resource, [:postgres], :table)
end
end

625
lib/data_layer.ex Normal file
View file

@ -0,0 +1,625 @@
defmodule AshPostgres.DataLayer do
@moduledoc """
A postgres data layer that levereges Ecto's postgres capabilities.
"""
@postgres %Ash.Dsl.Section{
name: :postgres,
describe: """
Postgres data layer configuration
""",
schema: [
repo: [
type: {:custom, AshPostgres.DataLayer, :validate_repo, []},
required: true,
doc:
"The repo that will be used to fetch your data. See the `Ecto.Repo` documentation for more"
],
table: [
type: :string,
required: true,
doc: "The table to store and read the resource from"
]
]
}
alias Ash.Filter.{And, Eq, In, NotEq, NotIn, Or}
alias AshPostgres.Predicates.Trigram
import AshPostgres, only: [table: 1, repo: 1]
@behaviour Ash.DataLayer
use Ash.Dsl.Extension, sections: [@postgres]
@doc false
def validate_repo(repo) do
if repo.__adapter__() == Ecto.Adapters.Postgres do
{:ok, repo}
else
{:error, "Expected a repo using the postgres adapter `Ecto.Adapters.Postgres`"}
end
end
@impl true
def custom_filters(resource) do
config = repo(resource).config()
add_pg_trgm_search(%{}, config)
end
defp add_pg_trgm_search(filters, config) do
if "pg_trgm" in config[:installed_extensions] do
Map.update(filters, :string, [{:trigram, AshPostgres.Predicates.Trigram}], fn filters ->
[{:trigram, AshPostgres.Predicates.Trigram} | filters]
end)
else
filters
end
end
import Ecto.Query, only: [from: 2]
@impl true
def can?(_, capability), do: can?(capability)
defp can?(:query_async), do: true
defp can?(:transact), do: true
defp can?(:composite_primary_key), do: true
defp can?(:upsert), do: true
defp can?({:filter, :in}), do: true
defp can?({:filter, :not_in}), do: true
defp can?({:filter, :not_eq}), do: true
defp can?({:filter, :eq}), do: true
defp can?({:filter, :and}), do: true
defp can?({:filter, :or}), do: true
defp can?({:filter, :not}), do: true
defp can?({:filter, :trigram}), do: true
defp can?({:filter_related, _}), do: true
defp can?(_), do: false
@impl true
def limit(query, nil, _), do: {:ok, query}
def limit(query, limit, _resource) do
{:ok, from(row in query, limit: ^limit)}
end
@impl true
def offset(query, nil, _), do: query
def offset(query, offset, _resource) do
{:ok, from(row in query, offset: ^offset)}
end
@impl true
def run_query(%{__impossible__: true}, _) do
{:ok, []}
end
@impl true
def run_query(query, resource) do
{:ok, repo(resource).all(query)}
end
@impl true
def resource_to_query(resource),
do: Ecto.Queryable.to_query({table(resource), resource})
@impl true
def create(resource, changeset) do
changeset =
Map.update!(changeset, :action, fn
:create -> :insert
action -> action
end)
changeset =
Map.update!(changeset, :data, fn data ->
Map.update!(data, :__meta__, &Map.put(&1, :source, table(resource)))
end)
repo(resource).insert(changeset)
rescue
e ->
{:error, e}
end
@impl true
def upsert(resource, changeset) do
changeset =
Map.update!(changeset, :action, fn
:create -> :insert
action -> action
end)
changeset =
Map.update!(changeset, :data, fn data ->
Map.update!(data, :__meta__, &Map.put(&1, :source, table(resource)))
end)
repo(resource).insert(changeset,
on_conflict: :replace_all,
conflict_target: Ash.primary_key(resource)
)
rescue
e ->
{:error, e}
end
@impl true
def update(resource, changeset) do
repo(resource).update(changeset)
rescue
e ->
{:error, e}
end
@impl true
def destroy(%resource{} = record) do
case repo(resource).delete(record) do
{:ok, _record} -> :ok
{:error, error} -> {:error, error}
end
rescue
e ->
{:error, e}
end
@impl true
def sort(query, sort, _resource) do
{:ok,
from(row in query,
order_by: ^sanitize_sort(sort)
)}
end
defp sanitize_sort(sort) do
sort
|> List.wrap()
|> Enum.map(fn
{sort, order} -> {order, sort}
sort -> sort
end)
end
@impl true
def filter(query, filter, _resource) do
new_query =
query
|> Map.put(:bindings, %{})
|> join_all_relationships(filter)
|> add_filter_expression(filter)
impossible_query =
if filter.impossible? do
Map.put(new_query, :__impossible__, true)
else
new_query
end
{:ok, impossible_query}
end
defp join_all_relationships(query, filter, path \\ []) do
query =
Map.put_new(query, :__ash_bindings__, %{current: Enum.count(query.joins) + 1, bindings: %{}})
Enum.reduce(filter.relationships, query, fn {name, relationship_filter}, query ->
join_type = :left
case {join_type, relationship_filter} do
{join_type, relationship_filter} ->
relationship = Ash.relationship(filter.resource, name)
current_path = [relationship | path]
joined_query = join_relationship(query, current_path, join_type)
joined_query_with_distinct =
join_and_add_distinct(relationship, join_type, joined_query, filter)
join_all_relationships(joined_query_with_distinct, relationship_filter, current_path)
end
end)
end
defp join_and_add_distinct(relationship, join_type, joined_query, filter) do
if relationship.cardinality == :many and join_type == :left && !joined_query.distinct do
from(row in joined_query,
distinct: ^Ash.primary_key(filter.resource)
)
else
joined_query
end
end
defp join_relationship(query, path, join_type) do
path_names = Enum.map(path, & &1.name)
case Map.get(query.__ash_bindings__.bindings, path_names) do
%{type: existing_join_type} when join_type != existing_join_type ->
raise "unreachable?"
nil ->
do_join_relationship(query, path, join_type)
_ ->
query
end
end
defp do_join_relationship(query, relationships, kind, path \\ [])
defp do_join_relationship(_, [], _, _), do: nil
defp do_join_relationship(query, [%{type: :many_to_many} = relationship | rest], :inner, path) do
relationship_through = maybe_get_resource_query(relationship.through)
relationship_destination =
do_join_relationship(query, rest, :inner, [relationship.name | path]) ||
Ecto.Queryable.to_query(maybe_get_resource_query(relationship.destination))
new_query =
from(row in query,
join: through in ^relationship_through,
on:
field(row, ^relationship.source_field) ==
field(through, ^relationship.source_field_on_join_table),
join: destination in ^relationship_destination,
on:
field(destination, ^relationship.destination_field) ==
field(through, ^relationship.destination_field_on_join_table)
)
join_path =
Enum.reverse([String.to_existing_atom(to_string(relationship.name) <> "_join_assoc") | path])
full_path = Enum.reverse([relationship.name | path])
new_query
|> add_binding(join_path, :inner)
|> add_binding(full_path, :inner)
|> merge_bindings(relationship_destination)
end
defp do_join_relationship(query, [relationship | rest], :inner, path) do
relationship_destination =
do_join_relationship(query, rest, :inner, [relationship.name | path]) ||
Ecto.Queryable.to_query(maybe_get_resource_query(relationship.destination))
new_query =
from(row in query,
join: destination in ^relationship_destination,
on:
field(row, ^relationship.source_field) ==
field(destination, ^relationship.destination_field)
)
new_query
|> add_binding(Enum.reverse([relationship.name | path]), :inner)
|> merge_bindings(relationship_destination)
end
defp do_join_relationship(query, [%{type: :many_to_many} = relationship | rest], :left, path) do
relationship_through = maybe_get_resource_query(relationship.through)
relationship_destination =
do_join_relationship(query, rest, :inner, [relationship.name | path]) ||
Ecto.Queryable.to_query(maybe_get_resource_query(relationship.destination))
new_query =
from(row in query,
left_join: through in ^relationship_through,
on:
field(row, ^relationship.source_field) ==
field(through, ^relationship.source_field_on_join_table),
left_join: destination in ^relationship_destination,
on:
field(destination, ^relationship.destination_field) ==
field(through, ^relationship.destination_field_on_join_table)
)
join_path =
Enum.reverse([String.to_existing_atom(to_string(relationship.name) <> "_join_assoc") | path])
full_path = Enum.reverse([relationship.name | path])
new_query
|> add_binding(join_path, :left)
|> add_binding(full_path, :left)
|> merge_bindings(relationship_destination)
end
defp do_join_relationship(query, [relationship | rest], :left, path) do
relationship_destination =
do_join_relationship(query, rest, :inner, [relationship.name | path]) ||
Ecto.Queryable.to_query(maybe_get_resource_query(relationship.destination))
new_query =
from(row in query,
left_join: destination in ^relationship_destination,
on:
field(row, ^relationship.source_field) ==
field(destination, ^relationship.destination_field)
)
new_query
|> add_binding(Enum.reverse([relationship.name | path]), :left)
|> merge_bindings(relationship_destination)
end
defp add_filter_expression(query, filter) do
{params, expr} = filter_to_expr(filter, query.__ash_bindings__.bindings, [])
if expr do
boolean_expr = %Ecto.Query.BooleanExpr{
expr: expr,
op: :and,
params: params
}
%{query | wheres: [boolean_expr | query.wheres]}
else
query
end
end
defp join_exprs(nil, nil, _op), do: nil
defp join_exprs(expr, nil, _op), do: expr
defp join_exprs(nil, expr, _op), do: expr
defp join_exprs(left_expr, right_expr, op), do: {op, [], [left_expr, right_expr]}
defp filter_to_expr(filter, bindings, params, current_binding \\ 0, path \\ [])
# A completely empty filter means "everything"
defp filter_to_expr(%{impossible?: true}, _, _, _, _), do: {[], false}
defp filter_to_expr(
%{ands: [], ors: [], not: nil, attributes: attrs, relationships: rels},
_,
_,
_,
_
)
when attrs == %{} and rels == %{} do
{[], true}
end
defp filter_to_expr(filter, bindings, params, current_binding, path) do
{params, expr} =
Enum.reduce(filter.attributes, {params, nil}, fn {attribute, filter},
{params, existing_expr} ->
{params, new_expr} = filter_value_to_expr(attribute, filter, current_binding, params)
{params, join_exprs(existing_expr, new_expr, :and)}
end)
{params, expr} =
Enum.reduce(filter.relationships, {params, expr}, fn {relationship, relationship_filter},
{params, existing_expr} ->
full_path = path ++ [relationship]
binding =
Map.get(bindings, full_path) ||
raise "unbound relationship #{inspect(full_path)} referenced! #{inspect(bindings)}"
{params, new_expr} =
filter_to_expr(relationship_filter, bindings, params, binding.binding, full_path)
{params, join_exprs(new_expr, existing_expr, :and)}
end)
{params, expr} =
Enum.reduce(filter.ors, {params, expr}, fn or_filter, {params, existing_expr} ->
{params, new_expr} = filter_to_expr(or_filter, bindings, params, current_binding, path)
{params, join_exprs(existing_expr, new_expr, :or)}
end)
{params, expr} =
case filter.not do
nil ->
{params, expr}
not_filter ->
{params, new_expr} = filter_to_expr(not_filter, bindings, params, current_binding, path)
{params, join_exprs(expr, {:not, [], [new_expr]}, :and)}
end
{params, expr} =
Enum.reduce(filter.ands, {params, expr}, fn and_filter, {params, existing_expr} ->
{params, new_expr} = filter_to_expr(and_filter, bindings, params, current_binding, path)
{params, join_exprs(existing_expr, new_expr, :and)}
end)
if expr do
{params, expr}
else
# A filter that was not empty, but didn't generate an expr for some reason, should default to `false`
# AFAIK this shouldn't actually be possible
{params, false}
end
end
# THe fact that we keep counting params here is very silly.
defp filter_value_to_expr(attribute, %Eq{value: value}, current_binding, params) do
{params ++ [{value, {current_binding, attribute}}],
{:==, [],
[
{{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
{:^, [], [Enum.count(params)]}
]}}
end
defp filter_value_to_expr(attribute, %NotEq{value: value}, current_binding, params) do
{params ++ [{value, {current_binding, attribute}}],
{:==, [],
[
{{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
{:^, [], [Enum.count(params)]}
]}}
end
defp filter_value_to_expr(attribute, %In{values: values}, current_binding, params) do
{params ++ [{values, {:in, {current_binding, attribute}}}],
{:in, [],
[
{{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
{:^, [], [Enum.count(params)]}
]}}
end
defp filter_value_to_expr(
attribute,
%NotIn{values: values},
current_binding,
params
) do
{params ++ [{values, {:in, {current_binding, attribute}}}],
{:not,
{:in, [],
[
{{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
{:^, [], [Enum.count(params)]}
]}}}
end
defp filter_value_to_expr(
attribute,
%Trigram{} = trigram,
current_binding,
params
) do
param_count = Enum.count(params)
case trigram do
%{equals: equals, greater_than: nil, less_than: nil, text: text} ->
{params ++ [{text, {current_binding, attribute}}, {equals, :float}],
{:fragment, [],
[
raw: "similarity(",
expr: {{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
raw: ", ",
expr: {:^, [], [param_count]},
raw: ") = ",
expr: {:^, [], [param_count + 1]},
raw: ""
]}}
%{equals: nil, greater_than: greater_than, less_than: nil, text: text} ->
{params ++ [{text, {current_binding, attribute}}, {greater_than, :float}],
{:fragment, [],
[
raw: "similarity(",
expr: {{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
raw: ", ",
expr: {:^, [], [param_count]},
raw: ") > ",
expr: {:^, [], [param_count + 1]},
raw: ""
]}}
%{equals: nil, greater_than: nil, less_than: less_than, text: text} ->
{params ++ [{text, {current_binding, attribute}}, {less_than, :float}],
{:fragment, [],
[
raw: "similarity(",
expr: {{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
raw: ", ",
expr: {:^, [], [param_count]},
raw: ") < ",
expr: {:^, [], [param_count + 1]},
raw: ""
]}}
%{equals: nil, greater_than: greater_than, less_than: less_than, text: text} ->
{params ++
[{text, {current_binding, attribute}}, {less_than, :float}, {greater_than, :float}],
{:fragment, [],
[
raw: "similarity(",
expr: {{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
raw: ", ",
expr: {:^, [], [param_count]},
raw: ") BETWEEN ",
expr: {:^, [], [param_count + 1]},
raw: " AND ",
expr: {:^, [], [param_count + 2]},
raw: ""
]}}
end
end
defp filter_value_to_expr(
attribute,
%And{left: left, right: right},
current_binding,
params
) do
{params, left_expr} = filter_value_to_expr(attribute, left, current_binding, params)
{params, right_expr} = filter_value_to_expr(attribute, right, current_binding, params)
{params, join_exprs(left_expr, right_expr, :and)}
end
defp filter_value_to_expr(
attribute,
%Or{left: left, right: right},
current_binding,
params
) do
{params, left_expr} = filter_value_to_expr(attribute, left, current_binding, params)
{params, right_expr} = filter_value_to_expr(attribute, right, current_binding, params)
{params, join_exprs(left_expr, right_expr, :or)}
end
defp merge_bindings(query, %{__ash_bindings__: ash_bindings}) do
ash_bindings
|> Map.get(:bindings)
|> Enum.reduce(query, fn {path, data}, query ->
add_binding(query, path, data)
end)
end
defp merge_bindings(query, _) do
query
end
defp add_binding(query, path, type) do
current = query.__ash_bindings__.current
bindings = query.__ash_bindings__.bindings
new_ash_bindings = %{
query.__ash_bindings__
| bindings: do_add_binding(bindings, path, current, type),
current: current + 1
}
%{query | __ash_bindings__: new_ash_bindings}
end
defp do_add_binding(bindings, path, current, type) do
Map.put(bindings, path, %{binding: current, type: type})
end
@impl true
def can_query_async?(resource) do
repo(resource).in_transaction?()
end
@impl true
def transaction(resource, func) do
repo(resource).transaction(func)
end
defp maybe_get_resource_query(resource) do
if Ash.resource_module?(resource) do
{table(resource), resource}
else
resource
end
end
end

View file

@ -2,7 +2,7 @@ defmodule AshPostgres.Repo do
@moduledoc """
Resources that use the `AshPostgres` data layer use a `Repo` to access the database.
This module is where database connection and configuration options go.
This repo is a slightly modified version of an ecto repo.
"""
@callback installed_extensions() :: [String.t()]

BIN
logos/small-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

22
mix.exs
View file

@ -24,6 +24,7 @@ defmodule AshPostgres.MixProject do
dialyzer: [
plt_add_apps: [:ecto]
],
docs: docs(),
aliases: aliases(),
package: package(),
source_url: "https://github.com/ash-project/ash_postgres",
@ -41,14 +42,29 @@ defmodule AshPostgres.MixProject do
]
end
defp docs do
[
main: "AshPostgres",
source_ref: "v#{@version}",
logo: "logos/small-logo.png",
groups_for_modules: [
"entry point": [AshPostgres],
"data layer": [AshPostgres.DataLayer],
repo: [AshPostgres.Repo],
"filter predicates": ~r/AshPostgres.Predicates/
]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:ecto_sql, "~> 3.0"},
{:ecto_sql, "~> 3.4"},
{:postgrex, ">= 0.0.0"},
{:ash, "~> 0.3.0"},
{:ash, "~> 0.4.0"},
{:db_connection, "~> 2.2", override: true},
{:git_ops, "~> 2.0.0", only: :dev},
{:ex_doc, "~> 0.21", only: :dev, runtime: false},
{:ex_doc, "~> 0.22", only: :dev, runtime: false},
{:ex_check, "~> 0.11.0", only: :dev},
{:credo, ">= 0.0.0", only: :dev, runtime: false},
{:dialyxir, ">= 0.0.0", only: :dev, runtime: false},

View file

@ -1,21 +1,21 @@
%{
"ash": {:hex, :ash, "0.3.0", "932799b55435893354b9ebb4c219509b9cd5efea3575cea9185052672b493b2e", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.2.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.1.3", [hex: :picosat_elixir, repo: "hexpm", optional: false]}], "hexpm", "9106ba425611020f33e232db123da63ba3b0323fa2b67bc3b8f2bea11b020d02"},
"ash": {:hex, :ash, "0.4.0", "23cff17ca4e13e12ea82f060cd3bfa409e702d5b5867aee0a9090ad8bd816178", [:mix], [{:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.2.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.1.4", [hex: :picosat_elixir, repo: "hexpm", optional: false]}], "hexpm", "81579015955c3028a36d95fe3a6a69ca69c0d2a25ede014a1cc9f5ccc8eae5a8"},
"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"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
"credo": {:hex, :credo, "1.4.0", "92339d4cbadd1e88b5ee43d427b639b68a11071b6f73854e33638e30a0ea11f5", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1fd3b70dce216574ce3c18bdf510b57e7c4c85c2ec9cad4bff854abaf7e58658"},
"dataloader": {:hex, :dataloader, "1.0.6", "fb724d6d3fb6acb87d27e3b32dea3a307936ad2d245faf9cf5221d1323d6a4ba", [:mix], [{:ecto, ">= 0.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
"db_connection": {:hex, :db_connection, "2.1.1", "a51e8a2ee54ef2ae6ec41a668c85787ed40cb8944928c191280fe34c15b76ae5", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "5a0e8c1c722dbcd31c0cbd1906b1d1074c863d335c295e4b994849b65a1fbe47"},
"db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"},
"decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"},
"dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"},
"earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"},
"ecto": {:hex, :ecto, "3.2.5", "76c864b77948a479e18e69cc1d0f0f4ee7cced1148ffe6a093ff91eba644f0b5", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "01251d9b28081b7e0af02a1875f9b809b057f064754ca3b274949d5216ea6f5f"},
"ecto_sql": {:hex, :ecto_sql, "3.2.0", "751cea597e8deb616084894dd75cbabfdbe7255ff01e8c058ca13f0353a3921b", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.2.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.2.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a2e23cf761668126252418cae07eff7967ad0152fbc5e2d0dc3de487a5ec774c"},
"earmark": {:hex, :earmark, "1.4.5", "62ffd3bd7722fb7a7b1ecd2419ea0b458c356e7168c1f5d65caf09b4fbdd13c8", [:mix], [], "hexpm", "b7d0e6263d83dc27141a523467799a685965bf8b13b6743413f19a7079843f4f"},
"ecto": {:hex, :ecto, "3.4.4", "a2c881e80dc756d648197ae0d936216c0308370332c5e77a2325a10293eef845", [: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", "cc4bd3ad62abc3b21fb629f0f7a3dab23a192fca837d257dd08449fba7373561"},
"ecto_sql": {:hex, :ecto_sql, "3.4.4", "d28bac2d420f708993baed522054870086fd45016a9d09bb2cd521b9c48d32ea", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "edb49af715dd72f213b66adfd0f668a43c17ed510b5d9ac7528569b23af57fe8"},
"elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"ets": {:hex, :ets, "0.8.1", "8ff9bcda5682b98493f8878fc9dbd990e48d566cba8cce59f7c2a78130da29ea", [:mix], [], "hexpm", "6be41b50adb5bc5c43626f25ea2d0af1f4a242fb3fad8d53f0c67c20b78915cc"},
"ex_check": {:hex, :ex_check, "0.11.0", "6d878d9ae30d19168157bcbf346b527825284e14e77a07ec0492b19cf0036479", [:mix], [], "hexpm", "d41894aa6193f089a05e3abb43ca457e289619fcfbbdd7b60d070b7a62b26832"},
"ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f1155337ae17ff7a1255217b4c1ceefcd1860b7ceb1a1874031e7a861b052e39"},
"ex_doc": {:hex, :ex_doc, "0.22.1", "9bb6d51508778193a4ea90fa16eac47f8b67934f33f8271d5e1edec2dc0eee4c", [:mix], [{:earmark, "~> 1.4.0", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "d957de1b75cb9f78d3ee17820733dc4460114d8b1e11f7ee4fd6546e69b1db60"},
"excoveralls": {:hex, :excoveralls, "0.13.0", "4e1b7cc4e0351d8d16e9be21b0345a7e165798ee5319c7800b9138ce17e0b38e", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "fe2a56c8909564e2e6764765878d7d5e141f2af3bc8ff3b018a68ee2a218fced"},
"git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"},
"git_ops": {:hex, :git_ops, "2.0.0", "d720b54de2ce9ca242164c57c982e4f05c1b6c020db2785e338f93b6190980aa", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "9aa270ea1cd4500eac4f38cac9b24019eee0aa524b96c95ad2f90cb0010840db"},
@ -23,20 +23,20 @@
"idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"},
"jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"},
"machinery": {:hex, :machinery, "1.0.0", "df6968d84c651b9971a33871c78c10157b6e13e4f3390b0bee5b0e8bdea8c781", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "4f6eb4185a48e7245360bedf653af4acc6fa6ae8ff4690619395543fa1a8395f"},
"makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "a10c6eb62cca416019663129699769f0c2ccf39428b3bb3c0cb38c718a0c186d"},
"makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"},
"makeup": {:hex, :makeup, "1.0.3", "e339e2f766d12e7260e6672dd4047405963c5ec99661abdc432e6ec67d29ef95", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "2e9b4996d11832947731f7608fed7ad2f9443011b3b479ae288011265cdd3dad"},
"makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"nimble_options": {:hex, :nimble_options, "0.2.1", "7eac99688c2544d4cc3ace36ee8f2bf4d738c14d031bd1e1193aab096309d488", [:mix], [], "hexpm", "ca48293609306791ce2634818d849b7defe09330adb7e4e1118a0bc59bed1cf4"},
"nimble_parsec": {:hex, :nimble_parsec, "0.5.2", "1d71150d5293d703a9c38d4329da57d3935faed2031d64bc19e77b654ef2d177", [:mix], [], "hexpm", "51aa192e0941313c394956718bdb1e59325874f88f45871cff90345b97f60bba"},
"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.3", "1e4eab27786b7dc7764c307555d8943cbba82912ed943737372760377be05ec8", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3add4d5ea1afa49f51bb7576bae000fe88091969804cc25baf47ffec48a9c626"},
"picosat_elixir": {:hex, :picosat_elixir, "0.1.4", "d259219ae27148c07c4aa3fdee61b1a14f4bc7f83b0ebdf2752558d06b302c62", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "eb41cb16053a45c8556de32f065084af98ea0b13a523fb46dfb4f9cff4152474"},
"plug": {:hex, :plug, "1.8.3", "12d5f9796dc72e8ac9614e94bda5e51c4c028d0d428e9297650d09e15a684478", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"},
"plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"},
"postgrex": {:hex, :postgrex, "0.15.1", "23ce3417de70f4c0e9e7419ad85bdabcc6860a6925fe2c6f3b1b5b1e8e47bf2f", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "12cd418e207b8ed787dfe0f520fccd6c001f58d9108233feae7df36462593d1f"},
"postgrex": {:hex, :postgrex, "0.15.5", "aec40306a622d459b01bff890fa42f1430dac61593b122754144ad9033a2152f", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ed90c81e1525f65a2ba2279dbcebf030d6d13328daa2f8088b9661eb9143af7f"},
"sobelow": {:hex, :sobelow, "0.10.2", "00e91208046d3b434f9f08779fe0ca7c6d6595b7fa33b289e792dffa6dde8081", [:mix], [], "hexpm", "e30fc994330cf6f485c1c4f2fb7c4b2d403557d0e101c6e5329fd17a58e55a7e"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm", "e9e3cacfd37c1531c0ca70ca7c0c30ce2dbb02998a4f7719de180fe63f8d41e4"},
"telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"},
}