Packages
ash_postgres
0.1.1
2.10.0
2.9.1
2.9.0
2.8.0
2.7.0
2.6.32
2.6.31
2.6.30
2.6.29
2.6.28
2.6.27
2.6.26
2.6.25
2.6.24
2.6.23
2.6.22
2.6.21
2.6.20
2.6.19
2.6.18
2.6.17
2.6.16
2.6.15
2.6.14
2.6.13
2.6.12
2.6.11
2.6.10
2.6.9
2.6.8
2.6.7
2.6.6
2.6.5
2.6.4
2.6.3
2.6.2
2.6.1
2.6.0
2.5.22
2.5.21
2.5.20
2.5.19
2.5.18
2.5.17
2.5.16
2.5.15
2.5.14
2.5.13
2.5.12
2.5.11
2.5.10
2.5.9
2.5.8
2.5.7
2.5.6
2.5.5
2.5.4
2.5.3
2.5.2
2.5.1
2.5.0
2.4.21
2.4.20
2.4.19
2.4.18
2.4.17
2.4.16
2.4.15
2.4.14
2.4.13
2.4.12
2.4.11
2.4.10
2.4.9
2.4.8
2.4.7
2.4.6
2.4.5
2.4.4
2.4.3
2.4.2
2.4.1
2.4.0
2.3.1
2.3.0
2.2.5
2.2.4
2.2.3
2.2.2
2.2.1
2.2.0
2.1.19
2.1.18
2.1.17
2.1.15
2.1.14
2.1.13
2.1.12
2.1.11
2.1.10
2.1.9
2.1.8
2.1.6
2.1.5
2.1.4
2.1.3
2.1.2
2.1.1
2.1.0
2.0.12
2.0.11
2.0.10
2.0.9
2.0.8
2.0.7
2.0.6
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
2.0.0-rc.15
2.0.0-rc.14
2.0.0-rc.13
2.0.0-rc.12
2.0.0-rc.11
2.0.0-rc.10
2.0.0-rc.9
2.0.0-rc.8
2.0.0-rc.7
2.0.0-rc.6
2.0.0-rc.5
2.0.0-rc.4
2.0.0-rc.3
2.0.0-rc.2
2.0.0-rc.1
2.0.0-rc.0
1.5.30
1.5.29
1.5.28
1.5.27
1.5.26
1.5.25
1.5.24
1.5.23
1.5.22
1.5.21
1.5.20
1.5.19
1.5.18
1.5.17
1.5.16
1.5.15
1.5.14
1.5.13
1.5.12
1.5.11
1.5.10
1.5.9
1.5.8
1.5.7
1.5.6
1.5.5
1.5.4
1.5.3
1.5.2
1.5.1
1.5.0
1.4.0
1.3.68
1.3.67
1.3.66
1.3.65
1.3.64
1.3.63
1.3.62
1.3.61
1.3.60
1.3.59
1.3.58
1.3.56
1.3.55
1.3.54
1.3.53
1.3.52
1.3.51
1.3.50
1.3.49
1.3.48
1.3.47
1.3.46
1.3.45
1.3.44
1.3.43
1.3.42
1.3.41
1.3.40
1.3.39
1.3.38
1.3.37
1.3.36
1.3.35
1.3.34
1.3.33
1.3.32
1.3.31
1.3.30
1.3.29
1.3.28
1.3.26
1.3.25
1.3.24
1.3.23
1.3.22
1.3.21
1.3.20
1.3.19
1.3.18
1.3.17
1.3.16
1.3.15
1.3.14
1.3.12
1.3.11
1.3.10
1.3.9
1.3.8
1.3.6
1.3.5
1.3.4
1.3.3
1.3.2
1.3.1
1.3.0
1.3.0-rc.4
1.3.0-rc.3
1.3.0-rc.2
1.3.0-rc.0
1.2.6
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.2.0-rc.1
1.2.0-rc.0
1.1.3
1.1.2
1.1.1
1.1.0
1.0.0
1.0.0-rc.9
1.0.0-rc.8
1.0.0-rc.7
1.0.0-rc.6
1.0.0-rc.5
1.0.0-rc.4
1.0.0-rc.3
1.0.0-rc.2
1.0.0-rc.1
1.0.0-rc.0
1.0.0-pre.3
1.0.0-pre.2
1.0.0-pre.1
1.0.0-pre.0
0.43.0
0.42.0-rc.7
0.42.0-rc.6
0.42.0-rc.5
0.42.0-rc.4
0.42.0-rc.3
0.42.0-rc.1
0.42.0-rc.0
0.41.7
0.41.6
0.41.5
0.41.4
0.41.3
0.41.2
0.41.1
0.41.0-rc0
retired
0.41.0-rc.9
0.41.0-rc.8
0.41.0-rc.7
0.41.0-rc.6
0.41.0-rc.5
0.41.0-rc.4
0.41.0-rc.3
0.41.0-rc.2
0.41.0-rc.1
0.41.0-rc.0
0.40.11
0.40.10
0.40.9
0.40.8
0.40.7
0.40.6
0.40.5
0.40.4
0.40.3
0.40.2
0.40.1
0.40.0-rc5
0.40.0-rc4
0.40.0-rc3
0.40.0-rc2
0.40.0-rc1
0.39.0-rc0
0.38.11
0.38.10
0.38.9
0.38.8
0.38.7
0.38.6
0.38.5
0.38.4
0.38.3
0.38.2
0.38.1
0.38.0
0.37.8
0.37.7
0.37.6
0.37.5
0.37.4
0.37.3
0.37.2
0.37.1
0.37.0
0.36.5
0.36.4
0.36.3
0.36.2
0.36.1
0.36.0
0.35.5
0.35.4
0.35.3
0.35.1
0.34.5
0.34.4
0.34.3
0.34.2
0.34.1
0.34.0
0.33.1
0.33.0
0.32.2
0.32.1
0.32.0
0.31.1
0.30.1
0.29.6
0.29.5
0.29.4
0.29.3
0.29.2
0.29.1
0.28.1
0.28.0
0.27.0
0.26.2
0.26.1
0.26.0
0.25.5
0.25.4
0.25.3
0.25.2
0.25.1
0.25.0
0.24.0
0.23.2
0.23.1
0.23.0
0.22.1
0.22.0
0.21.0
0.20.1
0.20.0
0.19.0
0.18.0
0.17.0
0.16.1
0.15.0
0.14.0
0.13.0
0.12.1
0.12.0
0.11.2
0.10.0
0.9.0
0.8.0
0.7.0
0.6.0
0.5.0
0.4.0
0.3.0
0.2.1
0.2.0
0.1.4
0.1.3
0.1.2
0.1.1
0.1.1-rc.2
0.1.1-rc.1
0.1.1-rc.0
0.1.0
The PostgreSQL data layer for Ash Framework
Current section
Files
Jump to
Current section
Files
lib/ash_postgres.ex
defmodule AshPostgres do
@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)}
"""
@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]
def repo() do
@repo
end
end
end
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
def repo(resource) do
resource.repo()
end
import Ecto.Query, only: [from: 2]
@impl true
def can?(:query_async), do: false
def can?(:transact), do: false
@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)
@impl true
def create(resource, changeset) do
repo = repo(resource)
repo.transaction(fn ->
# changeset = cast_assocs(changeset, repo, resource, )
result =
try do
changeset
|> repo.insert()
|> case do
{:ok, result} -> result
{:error, changeset} -> repo.rollback(changeset)
end
rescue
e ->
repo.rollback(e)
end
case changeset do
%{__after_action__: [_ | _] = after_action_hooks} ->
Enum.each(after_action_hooks, fn hook ->
case hook.(changeset, result, repo) do
:ok -> :ok
{:error, error} -> repo.rollback(error)
:error -> repo.rollback(:error)
end
end)
result
_ ->
result
end
end)
end
@impl true
def sort(query, sort, _resource) do
{:ok,
from(row in query,
order_by: ^sort
)}
end
@impl true
def filter(query, filter, resource) do
Enum.reduce(filter, {:ok, query}, fn
_, {:error, error} ->
{:error, error}
{key, value}, {:ok, query} ->
do_filter(query, key, value, resource)
end)
end
defp do_filter(
query,
:from_related,
{records, %{cardinality: :many_to_many} = relationship},
_resource
) do
ids = Enum.map(records, &Map.get(&1, relationship.source_field))
{:ok,
from(row in query,
join: join_row in ^relationship.through,
on:
field(join_row, ^relationship.destination_field_on_join_table) ==
field(row, ^relationship.destination_field),
where: field(join_row, ^relationship.source_field_on_join_table) in ^ids,
select_merge: %{__related_id__: field(join_row, ^relationship.source_field_on_join_table)}
)}
end
defp do_filter(query, :from_related, {records, relationship}, _resource) do
ids = Enum.map(records, &Map.get(&1, relationship.source_field))
{:ok,
from(row in query,
where: field(row, ^relationship.destination_field) in ^ids
)}
end
defp do_filter(query, key, value, resource) do
cond do
attr = Ash.attribute(resource, key) ->
filter_attribute(query, attr, value, resource)
rel = Ash.relationship(resource, key) ->
filter_relationship(query, rel, value, resource)
true ->
{:error, "No such filter"}
end
end
defp filter_attribute(query, attribute, value, _resource) do
query =
from(row in query,
where: field(row, ^attribute.name) == ^value
)
{:ok, query}
end
# Only supports a single id for now
defp filter_relationship(query, %{name: name}, id, _resource) do
query =
from(row in query,
join: related in assoc(row, ^name),
where: related.id == ^id
)
{:ok, query}
end
@impl true
def can_query_async?(resource) do
repo(resource).in_transaction?()
end
# def cast_assocs(changeset, repo, resource, relationships) do
# Enum.reduce(relationships, changeset, fn {relationship, value}, changeset ->
# case Ash.relationship(resource, relationship) do
# %{type: :belongs_to, source_field: source_field} ->
# belongs_to_assoc_update(changeset, source_field, value)
# %{type: :has_one} = rel ->
# has_one_assoc_update(changeset, repo, rel, value)
# %{type: :has_many} = rel ->
# has_many_assoc_update(changeset, rel, value)
# %{type: :many_to_many} = rel ->
# many_to_many_assoc_update(changeset, rel, value, repo)
# _ ->
# changeset
# end
# end)
# end
# defp has_one_assoc_update(
# changeset,
# repo,
# %{
# destination: destination,
# destination_field: destination_field,
# source_field: source_field
# },
# identifier
# ) do
# Ecto.Changeset.prepare_changes(changeset, fn changeset ->
# value =
# case identifier do
# %{id: id} -> id
# nil -> nil
# _ -> raise "what"
# end
# query =
# from(row in destination,
# where:
# field(row, ^destination_field) == ^Ecto.Changeset.get_field(changeset, source_field)
# )
# repo.update_all(query, set: [{destination_field, value}])
# changeset
# end)
# end
# defp belongs_to_assoc_update(changeset, source_field, %{id: id}) do
# Ecto.Changeset.cast(changeset, %{source_field => id}, [source_field])
# end
# defp belongs_to_assoc_update(changeset, source_field, nil) do
# Ecto.Changeset.cast(changeset, %{source_field => nil}, [source_field])
# end
# defp has_many_assoc_update(
# changeset,
# %{
# destination: destination,
# destination_field: destination_field,
# source_field: source_field
# },
# values
# ) do
# ids = values |> Enum.map(&Map.get(&1, :id)) |> Enum.reject(&is_nil/1)
# add_after_action_hook(changeset, fn _changeset, result, repo ->
# field_value = Map.get(result, source_field)
# query =
# from(row in destination,
# where: row.id in ^ids
# )
# repo.update_all(query, set: [{destination_field, field_value}])
# :ok
# end)
# end
# defp many_to_many_assoc_update(
# changeset,
# %{
# through: through,
# source_field: source_field,
# source_field_on_join_table: source_field_on_join_table,
# destination_field_on_join_table: destination_field_on_join_table,
# join_table_fields: join_table_fields
# },
# values,
# repo
# ) do
# ids = values |> Enum.map(&Map.get(&1, :id)) |> Enum.reject(&is_nil/1)
# source_id =
# Ecto.Changeset.get_field(
# changeset,
# source_field
# )
# changeset
# |> Ecto.Changeset.prepare_changes(fn changeset ->
# delete_now_unrelated_ids(
# repo,
# source_id,
# through,
# source_field_on_join_table,
# destination_field_on_join_table,
# ids
# )
# changeset
# end)
# |> add_after_action_hook(fn _changeset, _result, repo ->
# case upsert_join_table_rows(
# repo,
# source_id,
# through,
# values,
# source_field_on_join_table,
# destination_field_on_join_table,
# join_table_fields
# ) do
# :ok ->
# :ok
# end
# end)
# end
# defp delete_now_unrelated_ids(
# repo,
# source_id,
# through,
# source_field_on_join_table,
# destination_field_on_join_table,
# ids
# ) do
# query =
# from(join_row in through,
# where: field(join_row, ^destination_field_on_join_table) not in ^ids,
# where: field(join_row, ^source_field_on_join_table) == ^source_id
# )
# repo.delete_all(query)
# end
# defp upsert_join_table_rows(
# repo,
# source_id,
# through,
# values,
# source_field_on_join_table,
# destination_field_on_join_table,
# join_table_fields
# ) do
# values
# |> Enum.map(fn fields ->
# update_fields = Map.delete(fields, :id)
# cond do
# is_nil(Map.get(fields, :id)) ->
# {:error, "No destination id specified"}
# is_bitstring(through) ->
# {:error, "Cannot update the fields of a join table only specified as a table name"}
# Map.keys(update_fields) -- join_table_fields != [] ->
# {:error, "no such fields to update"}
# true ->
# attributes =
# update_fields
# |> Map.put(source_field_on_join_table, source_id)
# |> Map.put(destination_field_on_join_table, Map.get(fields, :id))
# changeset =
# through
# |> struct()
# |> Ecto.Changeset.cast(
# attributes,
# join_table_fields ++ [source_field_on_join_table, destination_field_on_join_table]
# )
# repo.insert(changeset,
# on_conflict: :replace_all_except_primary_key,
# conflict_target: [source_field_on_join_table, destination_field_on_join_table]
# )
# end
# end)
# |> Enum.filter(fn result -> match?({:error, _}, result) end)
# |> case do
# [] -> :ok
# errors -> {:error, errors}
# end
# end
# defp add_after_action_hook(changeset, hook) do
# changeset
# |> Map.put_new(:__after_action__, [])
# |> Map.update!(:__after_action__, fn list -> [hook | list] end)
# end
# Copied from an older file, to be added as more functionality is added back in
# @impl true
# def update(%resource{} = record, _action, attributes, relationships, _params) do
# @repo.transaction(fn ->
# changeset =
# record
# |> Ecto.Changeset.cast(attributes, Map.keys(attributes))
# |> AshPostgres.DataLayer.cast_assocs(@repo, resource, relationships)
# result =
# case @repo.update(changeset) do
# {:ok, result} -> result
# {:error, changeset} -> @repo.rollback(changeset)
# end
# case changeset do
# %{__after_action__: [_ | _] = after_action_hooks} ->
# Enum.each(after_action_hooks, fn hook ->
# case hook.(changeset, result, @repo) do
# :ok -> :ok
# {:error, error} -> @repo.rollback(error)
# :error -> @repo.rollback(:error)
# end
# end)
# result
# _other ->
# result
# end
# end)
# end
# @impl true
# def append_related(record, relationship, resource_identifiers) do
# @repo.transaction(fn ->
# AshPostgres.DataLayer.append_related(@repo, record, relationship, resource_identifiers)
# end)
# end
# @impl true
# def delete_related(record, relationship, resource_identifiers) do
# @repo.transaction(fn ->
# AshPostgres.DataLayer.delete_related(@repo, record, relationship, resource_identifiers)
# end)
# end
# @impl true
# def replace_related(record, relationship, resource_identifiers) do
# @repo.transaction(fn ->
# AshPostgres.DataLayer.replace_related(@repo, record, relationship, resource_identifiers)
# end)
# end
# @impl true
# def delete(record, _action, _params) do
# @repo.delete(record)
# end
# @doc false
# def replace_related(
# repo,
# record,
# %{
# type: :many_to_many,
# through: through,
# source_field: source_field,
# source_field_on_join_table: source_field_on_join_table,
# destination_field_on_join_table: destination_field_on_join_table
# },
# identifiers
# ) do
# ids = identifiers |> Enum.map(&Map.get(&1, :id)) |> Enum.reject(&is_nil/1)
# source_id = Map.get(record, source_field)
# delete_now_unrelated_ids(
# repo,
# source_id,
# through,
# source_field_on_join_table,
# destination_field_on_join_table,
# ids
# )
# upsert_join_table_rows(
# repo,
# source_id,
# through,
# identifiers,
# source_field_on_join_table,
# destination_field_on_join_table
# )
# record
# end
# def replace_related(
# repo,
# record,
# %{
# type: :has_many,
# source_field: source_field,
# destination: destination,
# destination_field: destination_field
# },
# identifiers
# ) do
# ids = identifiers |> Enum.map(&Map.get(&1, :id)) |> Enum.reject(&is_nil/1)
# field_value = Map.get(record, source_field)
# query =
# from(row in destination,
# where: row.id in ^ids
# )
# repo.update_all(query, set: [{destination_field, field_value}])
# record
# end
# def replace_related(
# repo,
# record,
# %{
# type: :belongs_to,
# source_field: source_field
# },
# identifier
# ) do
# value =
# case identifier do
# %{id: id} -> id
# nil -> nil
# _ -> raise "what do"
# end
# record
# |> Ecto.Changeset.cast(%{source_field => value}, [source_field])
# |> repo.update()
# |> case do
# {:ok, record} -> record
# {:error, error} -> repo.rollback(error)
# end
# end
# def replace_related(
# repo,
# record,
# %{
# type: :has_one,
# source_field: source_field,
# destination_field: destination_field,
# destination: destination
# },
# identifier
# ) do
# value =
# case identifier do
# %{id: id} -> id
# nil -> nil
# _ -> raise "what"
# end
# query =
# from(row in destination,
# where: field(row, ^destination_field) == ^Map.get(record, source_field)
# )
# repo.update_all(query, set: [{destination_field, value}])
# record
# end
# @doc false
# def append_related(repo, record, %{type: :many_to_many} = relationship, identifiers) do
# source_id = Map.get(record, relationship.source_field)
# upsert_join_table_rows(
# repo,
# source_id,
# relationship.through,
# identifiers,
# relationship.source_field_on_join_table,
# relationship.destination_field_on_join_table
# )
# record
# end
# def append_related(
# repo,
# record,
# %{type: :has_many, destination: destination, destination_field: destination_field},
# identifiers
# ) do
# ids =
# identifiers
# |> Enum.map(&Map.get(&1, :id))
# |> Enum.reject(&is_nil/1)
# query =
# from(related in destination,
# where: related.id in ^ids,
# where: field(related, ^destination_field) != ^record.id
# )
# repo.update_all(query, set: [{destination_field, record.id}])
# end
# @doc false
# def delete_related(repo, record, %{type: :many_to_many} = relationship, identifiers) do
# source_id = Map.get(record, relationship.source_field)
# ids =
# identifiers
# |> Enum.map(&Map.get(&1, :id))
# |> Enum.reject(&is_nil/1)
# delete_related_ids(
# repo,
# source_id,
# relationship.through,
# relationship.source_field_on_join_table,
# relationship.destination_field_on_join_table,
# ids
# )
# record
# end
# def delete_related(
# repo,
# record,
# %{type: :has_many, destination: destination, destination_field: destination_field},
# identifiers
# ) do
# ids =
# identifiers
# |> Enum.map(&Map.get(&1, :id))
# |> Enum.reject(&is_nil/1)
# query =
# from(related in destination,
# where: related.id in ^ids,
# where: field(related, ^destination_field) != ^record.id
# )
# # TODO: Validate the a delete_related action doesn't exist for has_many relationships
# # where the destination field is not nullable. That will only ever error.
# repo.update_all(query, set: [{destination_field, record.id}])
# end
# defp delete_related_ids(
# repo,
# source_id,
# through,
# source_field_on_join_table,
# destination_field_on_join_table,
# ids
# ) do
# query =
# from(join_row in through,
# where: field(join_row, ^destination_field_on_join_table) in ^ids,
# where: field(join_row, ^source_field_on_join_table) == ^source_id
# )
# repo.delete_all(query)
# end
# def add_through_schema_fields(repo, resource, includes) when is_list(includes) do
# Enum.flat_map(includes, fn {rel, further} ->
# case Ash.relationship(resource, rel) do
# %{type: :many_to_many, destination: destination} = relationship ->
# [
# {rel, &fetch_and_add_through_row(&1, repo, relationship)},
# {rel, add_through_schema_fields(repo, destination, further)}
# ]
# %{destination: destination} ->
# [
# {rel, add_through_schema_fields(repo, destination, further)}
# ]
# end
# end)
# end
# def add_through_schema_fields(_repo, _resource, includes), do: includes
# defp fetch_and_add_through_row(source_ids, repo, relationship) do
# query =
# from(join_row in relationship.through,
# where:
# type(field(join_row, ^relationship.source_field_on_join_table), :binary_id) in ^source_ids,
# join: destination_row in ^relationship.destination,
# on:
# type(field(join_row, ^relationship.destination_field_on_join_table), :binary_id) ==
# field(destination_row, ^relationship.destination_field)
# )
# query
# |> add_select(relationship)
# |> repo.all()
# |> Enum.map(fn {destination_row, join_row} ->
# {Map.get(join_row, relationship.source_field_on_join_table),
# Map.put(destination_row, :__join_row__, join_row)}
# end)
# end
# defp add_select(query, relationship) do
# case relationship.through do
# string when is_bitstring(string) ->
# from([join_row, destination_row] in query,
# select:
# {destination_row,
# %{
# ^relationship.source_field_on_join_table =>
# type(field(join_row, ^relationship.source_field_on_join_table), :binary_id),
# ^relationship.destination_field_on_join_table =>
# type(field(join_row, ^relationship.destination_field_on_join_table), :binary_id)
# }}
# )
# module when is_atom(module) ->
# from([join_row, destination_row] in query,
# select: {destination_row, join_row}
# )
# end
# end
end