mirror of
https://github.com/ash-project/ash_postgres.git
synced 2024-09-20 05:23:18 +12:00
feat: use the new DSL builder for config
This commit is contained in:
parent
e5d0a459fa
commit
bab26479f9
6 changed files with 666 additions and 653 deletions
|
@ -1,649 +1,21 @@
|
||||||
defmodule AshPostgres do
|
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 """
|
@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
|
alias Ash.Dsl.Extension
|
||||||
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
|
|
||||||
|
|
||||||
|
@doc "Fetch the configured repo for a resource"
|
||||||
def repo(resource) do
|
def repo(resource) do
|
||||||
resource.repo()
|
Extension.get_opt(resource, [:postgres], :repo)
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@doc "Fetch the configured table for a resource"
|
||||||
def custom_filters(resource) do
|
def table(resource) do
|
||||||
config = repo(resource).config()
|
Extension.get_opt(resource, [:postgres], :table)
|
||||||
|
|
||||||
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
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
625
lib/data_layer.ex
Normal file
625
lib/data_layer.ex
Normal 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
|
|
@ -2,7 +2,7 @@ defmodule AshPostgres.Repo do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Resources that use the `AshPostgres` data layer use a `Repo` to access the database.
|
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()]
|
@callback installed_extensions() :: [String.t()]
|
||||||
|
|
||||||
|
|
BIN
logos/small-logo.png
Normal file
BIN
logos/small-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3 KiB |
22
mix.exs
22
mix.exs
|
@ -24,6 +24,7 @@ defmodule AshPostgres.MixProject do
|
||||||
dialyzer: [
|
dialyzer: [
|
||||||
plt_add_apps: [:ecto]
|
plt_add_apps: [:ecto]
|
||||||
],
|
],
|
||||||
|
docs: docs(),
|
||||||
aliases: aliases(),
|
aliases: aliases(),
|
||||||
package: package(),
|
package: package(),
|
||||||
source_url: "https://github.com/ash-project/ash_postgres",
|
source_url: "https://github.com/ash-project/ash_postgres",
|
||||||
|
@ -41,14 +42,29 @@ defmodule AshPostgres.MixProject do
|
||||||
]
|
]
|
||||||
end
|
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.
|
# Run "mix help deps" to learn about dependencies.
|
||||||
defp deps do
|
defp deps do
|
||||||
[
|
[
|
||||||
{:ecto_sql, "~> 3.0"},
|
{:ecto_sql, "~> 3.4"},
|
||||||
{:postgrex, ">= 0.0.0"},
|
{: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},
|
{: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},
|
{:ex_check, "~> 0.11.0", only: :dev},
|
||||||
{:credo, ">= 0.0.0", only: :dev, runtime: false},
|
{:credo, ">= 0.0.0", only: :dev, runtime: false},
|
||||||
{:dialyxir, ">= 0.0.0", only: :dev, runtime: false},
|
{:dialyxir, ">= 0.0.0", only: :dev, runtime: false},
|
||||||
|
|
24
mix.lock
24
mix.lock
|
@ -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"},
|
"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"},
|
||||||
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
|
"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"},
|
"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"},
|
"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"},
|
"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"},
|
"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"},
|
"earmark": {:hex, :earmark, "1.4.5", "62ffd3bd7722fb7a7b1ecd2419ea0b458c356e7168c1f5d65caf09b4fbdd13c8", [:mix], [], "hexpm", "b7d0e6263d83dc27141a523467799a685965bf8b13b6743413f19a7079843f4f"},
|
||||||
"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": {: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.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"},
|
"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"},
|
"elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"},
|
||||||
"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"},
|
||||||
"ex_check": {:hex, :ex_check, "0.11.0", "6d878d9ae30d19168157bcbf346b527825284e14e77a07ec0492b19cf0036479", [:mix], [], "hexpm", "d41894aa6193f089a05e3abb43ca457e289619fcfbbdd7b60d070b7a62b26832"},
|
"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"},
|
"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_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"},
|
"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"},
|
"idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"},
|
||||||
"jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"},
|
"jason": {:hex, :jason, "1.2.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"},
|
"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": {: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.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"},
|
"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"},
|
||||||
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"},
|
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"},
|
||||||
"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.2.1", "7eac99688c2544d4cc3ace36ee8f2bf4d738c14d031bd1e1193aab096309d488", [:mix], [], "hexpm", "ca48293609306791ce2634818d849b7defe09330adb7e4e1118a0bc59bed1cf4"},
|
"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"},
|
"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": {: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"},
|
"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"},
|
"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"},
|
"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"},
|
"unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"},
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue