mirror of
https://github.com/ash-project/ash_postgres.git
synced 2024-09-19 13:03:14 +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
|
||||
@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
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 """
|
||||
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
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: [
|
||||
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},
|
||||
|
|
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"},
|
||||
"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"},
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue