From dc94994ea3aa84166ef39e3ba6b064c3f879c777 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Wed, 4 Dec 2019 09:58:20 -0500 Subject: [PATCH] WIP --- .gitignore | 2 +- README.md | 2 +- lib/ash_ecto.ex | 343 +++++++++++++++++++++++++++++++++++- lib/ash_ecto/data_layer.ex | 351 ------------------------------------- mix.exs | 4 +- test/ash_ecto_test.exs | 6 +- 6 files changed, 346 insertions(+), 362 deletions(-) delete mode 100644 lib/ash_ecto/data_layer.ex diff --git a/.gitignore b/.gitignore index 94d126c..40b903f 100644 --- a/.gitignore +++ b/.gitignore @@ -20,5 +20,5 @@ erl_crash.dump *.ez # Ignore package tarball (built via "mix hex.build"). -ash_ecto-*.tar +ash_postgres-*.tar diff --git a/README.md b/README.md index f090ae5..ab29e61 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# AshEcto +# AshPostgres **TODO: Add description** diff --git a/lib/ash_ecto.ex b/lib/ash_ecto.ex index 0454bd2..1fd24bb 100644 --- a/lib/ash_ecto.ex +++ b/lib/ash_ecto.ex @@ -1,17 +1,17 @@ -defmodule AshEcto do +defmodule AshPostgres do @behaviour Ash.DataLayer defmacro __using__(opts) do quote bind_quoted: [repo: opts[:repo]] do - @data_layer AshEcto + @data_layer AshPostgres @repo repo unless repo do - raise "You must pass the `repo` option to `use AshEcto` for #{__MODULE__}" + raise "You must pass the `repo` option to `use AshPostgres` for #{__MODULE__}" end unless repo.__adapter__() == Ecto.Adapters.Postgres do - raise "Only Ecto.Adapters.Postgres is supported with AshEcto for now" + raise "Only Ecto.Adapters.Postgres is supported with AshPostgres for now" end def repo() do @@ -359,4 +359,339 @@ defmodule AshEcto do |> 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 + end + 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 diff --git a/lib/ash_ecto/data_layer.ex b/lib/ash_ecto/data_layer.ex deleted file mode 100644 index 262446c..0000000 --- a/lib/ash_ecto/data_layer.ex +++ /dev/null @@ -1,351 +0,0 @@ -defmodule AshEcto.DataLayer do - defmacro __using__(opts) do - quote bind_quoted: [opts: opts], location: :keep do - @behaviour Ash.DataLayer - # TODOs: It might be weird that they have to provide their own repo? - - require AshEcto.Schema - - unless opts[:repo] do - raise "You must configure your own repo" - end - - unless opts[:repo].__adapter__() == Ecto.Adapters.Postgres do - raise "Only Ecto.Adapters.Postgres is supported with AshEcto for now" - end - - @repo opts[:repo] - - # @impl true - # def update(%resource{} = record, _action, attributes, relationships, _params) do - # @repo.transaction(fn -> - # changeset = - # record - # |> Ecto.Changeset.cast(attributes, Map.keys(attributes)) - # |> AshEcto.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 -> - # AshEcto.DataLayer.append_related(@repo, record, relationship, resource_identifiers) - # end) - # end - - # @impl true - # def delete_related(record, relationship, resource_identifiers) do - # @repo.transaction(fn -> - # AshEcto.DataLayer.delete_related(@repo, record, relationship, resource_identifiers) - # end) - # end - - # @impl true - # def replace_related(record, relationship, resource_identifiers) do - # @repo.transaction(fn -> - # AshEcto.DataLayer.replace_related(@repo, record, relationship, resource_identifiers) - # end) - # end - - # @impl true - # def delete(record, _action, _params) do - # @repo.delete(record) - # end - end - 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 diff --git a/mix.exs b/mix.exs index d7e0002..ad23990 100644 --- a/mix.exs +++ b/mix.exs @@ -1,9 +1,9 @@ -defmodule AshEcto.MixProject do +defmodule AshPostgres.MixProject do use Mix.Project def project do [ - app: :ash_ecto, + app: :ash_postgres, version: "0.1.0", elixir: "~> 1.9", start_permanent: Mix.env() == :prod, diff --git a/test/ash_ecto_test.exs b/test/ash_ecto_test.exs index 7b999e7..9867571 100644 --- a/test/ash_ecto_test.exs +++ b/test/ash_ecto_test.exs @@ -1,8 +1,8 @@ -defmodule AshEctoTest do +defmodule AshPostgresTest do use ExUnit.Case - doctest AshEcto + doctest AshPostgres test "greets the world" do - assert AshEcto.hello() == :world + assert AshPostgres.hello() == :world end end