2019-12-05 03:58:20 +13:00
|
|
|
defmodule AshPostgres do
|
2019-12-06 07:45:42 +13:00
|
|
|
@using_opts_schema Ashton.schema(
|
|
|
|
opts: [
|
|
|
|
repo: :atom
|
|
|
|
],
|
|
|
|
required: [:repo],
|
|
|
|
describe: [
|
|
|
|
repo:
|
|
|
|
"The repo that will be used to fetch your data. See the `Ecto.Repo` documentation for more"
|
|
|
|
],
|
|
|
|
constraints: [
|
|
|
|
repo:
|
|
|
|
{&AshPostgres.postgres_repo?/1, "must be using the postgres adapter"}
|
|
|
|
]
|
|
|
|
)
|
|
|
|
|
|
|
|
@moduledoc """
|
|
|
|
A postgres data layer that levereges Ecto's postgres tools.
|
|
|
|
|
|
|
|
To use it, add `use AshPostgres, repo: MyRepo` to your resource, after `use Ash.Resource`
|
|
|
|
|
|
|
|
#{Ashton.document(@using_opts_schema)}
|
|
|
|
"""
|
2019-11-27 11:33:52 +13:00
|
|
|
@behaviour Ash.DataLayer
|
|
|
|
|
2019-10-31 04:13:22 +13:00
|
|
|
defmacro __using__(opts) do
|
2019-12-06 07:45:42 +13:00
|
|
|
quote bind_quoted: [opts: opts] do
|
|
|
|
opts = AshPostgres.validate_using_opts(__MODULE__, opts)
|
2019-11-27 11:33:52 +13:00
|
|
|
|
2019-12-06 07:45:42 +13:00
|
|
|
@data_layer AshPostgres
|
|
|
|
@repo opts[:repo]
|
2019-11-27 11:33:52 +13:00
|
|
|
|
|
|
|
def repo() do
|
|
|
|
@repo
|
|
|
|
end
|
2019-10-31 04:13:22 +13:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-12-06 07:45:42 +13:00
|
|
|
def validate_using_opts(mod, opts) do
|
|
|
|
case Ashton.validate(opts, @using_opts_schema) do
|
|
|
|
{:ok, opts} ->
|
|
|
|
opts
|
|
|
|
|
|
|
|
{:error, [{key, message} | _]} ->
|
|
|
|
raise Ash.Error.ResourceDslError,
|
|
|
|
resource: mod,
|
|
|
|
using: __MODULE__,
|
|
|
|
option: key,
|
|
|
|
message: message
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def postgres_repo?(repo) do
|
|
|
|
repo.__adapter__() == Ecto.Adapters.Postgres
|
|
|
|
end
|
|
|
|
|
2019-11-27 11:33:52 +13:00
|
|
|
def repo(resource) do
|
|
|
|
resource.repo()
|
|
|
|
end
|
|
|
|
|
|
|
|
import Ecto.Query, only: [from: 2]
|
|
|
|
|
2019-12-09 08:02:30 +13:00
|
|
|
@impl true
|
2020-01-18 06:24:07 +13:00
|
|
|
def can?(:query_async), do: true
|
|
|
|
def can?(:transact), do: true
|
|
|
|
def can?(:composite_primary_key), 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_related, _}), do: true
|
|
|
|
def can?(_), do: false
|
2019-12-09 08:02:30 +13:00
|
|
|
|
2019-11-27 11:33:52 +13:00
|
|
|
@impl true
|
|
|
|
def limit(query, limit, _resource) do
|
|
|
|
{:ok, from(row in query, limit: ^limit)}
|
|
|
|
end
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
def offset(query, offset, _resource) do
|
|
|
|
{:ok, from(row in query, offset: ^offset)}
|
|
|
|
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)
|
|
|
|
|
2019-11-28 18:27:12 +13:00
|
|
|
@impl true
|
2019-12-09 08:02:30 +13:00
|
|
|
def create(resource, changeset) do
|
2020-01-18 06:24:07 +13:00
|
|
|
changeset = Map.update!(changeset, :action, fn
|
|
|
|
:create -> :insert
|
|
|
|
action -> action
|
|
|
|
end)
|
2019-12-03 19:48:04 +13:00
|
|
|
|
2020-01-18 06:24:07 +13:00
|
|
|
repo(resource).insert(changeset)
|
|
|
|
rescue
|
|
|
|
e ->
|
|
|
|
{:error, e}
|
|
|
|
end
|
2019-12-03 19:48:04 +13:00
|
|
|
|
2020-01-18 06:24:07 +13:00
|
|
|
@impl true
|
|
|
|
def update(resource, changeset) do
|
|
|
|
repo(resource).update(changeset)
|
|
|
|
rescue
|
|
|
|
e ->
|
|
|
|
{:error, e}
|
2019-11-28 18:27:12 +13:00
|
|
|
end
|
|
|
|
|
2019-11-30 05:38:14 +13:00
|
|
|
@impl true
|
|
|
|
def sort(query, sort, _resource) do
|
|
|
|
{:ok,
|
|
|
|
from(row in query,
|
|
|
|
order_by: ^sort
|
|
|
|
)}
|
|
|
|
end
|
|
|
|
|
2019-11-27 11:33:52 +13:00
|
|
|
@impl true
|
2020-01-18 06:24:07 +13:00
|
|
|
# TODO: I have learned from experience that no single approach here
|
|
|
|
# will be a one-size-fits-all. We need to either use complexity metrics,
|
|
|
|
# hints from the interface, or some other heuristic to do our best to
|
|
|
|
# make queries perform well. For now, I'm just choosing the most naive approach
|
|
|
|
# possible: left join to relationships that appear in `or` conditions, inner
|
|
|
|
# join to conditions in the mainline query.
|
2019-11-29 19:54:57 +13:00
|
|
|
def filter(query, filter, resource) do
|
2020-01-18 06:24:07 +13:00
|
|
|
IO.inspect(filter)
|
|
|
|
{:ok, query}
|
|
|
|
# filter
|
|
|
|
# |> join_relationships()
|
|
|
|
# |> Enum.flat_map(fn {key, filter} ->
|
|
|
|
# Enum.map(filter, fn {filter_type, value} ->
|
|
|
|
# {key, filter_type, value}
|
|
|
|
# end)
|
|
|
|
# end)
|
|
|
|
# |> Enum.reduce({:ok, query}, fn
|
|
|
|
# _, {:error, error} ->
|
|
|
|
# {:error, error}
|
2019-11-28 10:36:49 +13:00
|
|
|
|
2020-01-18 06:24:07 +13:00
|
|
|
# {key, filter_type, value}, {:ok, query} ->
|
|
|
|
# do_filter(query, key, filter_type, value)
|
|
|
|
# end)
|
2019-11-27 11:33:52 +13:00
|
|
|
end
|
|
|
|
|
2020-01-18 06:24:07 +13:00
|
|
|
defp do_filter(query, key, :equals, value) do
|
|
|
|
from(row in query,
|
|
|
|
where: field(row, ^key) == ^value
|
|
|
|
)
|
2019-11-27 11:33:52 +13:00
|
|
|
end
|
|
|
|
|
2020-01-18 06:24:07 +13:00
|
|
|
defp do_filter(query, key, :in, value) do
|
|
|
|
from(row in query,
|
|
|
|
where: field(row, ^key) == ^value
|
|
|
|
)
|
2019-11-28 10:36:49 +13:00
|
|
|
end
|
|
|
|
|
2020-01-18 06:24:07 +13:00
|
|
|
defp do_filter(_, key, type, value) do
|
|
|
|
{:error, "Invalid filter #{key} #{type} #{inspect(value)}"}
|
2019-11-27 11:33:52 +13:00
|
|
|
end
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
def can_query_async?(resource) do
|
|
|
|
repo(resource).in_transaction?()
|
|
|
|
end
|
2019-12-03 19:48:04 +13:00
|
|
|
|
2020-01-18 06:24:07 +13:00
|
|
|
@impl true
|
|
|
|
def transaction(resource, func) do
|
|
|
|
repo(resource).transaction(func)
|
|
|
|
end
|
2019-10-31 04:13:22 +13:00
|
|
|
end
|