From 09e77eb445bee16ac06c6d2901c4631095f3174b Mon Sep 17 00:00:00 2001 From: Barnabas Jovanovics Date: Tue, 21 Jan 2025 17:32:20 +0100 Subject: [PATCH] feat: allow passing of arguments to related resources (#93) --- .formatter.exs | 1 + config/config.exs | 1 + ...esource.md => DSL-AshArchival.Resource.md} | 1 + .../resource/archive_related_arguments.ex | 10 ++ .../archive_related_arguments/function.ex | 15 +++ .../resource/changes/archive_related.ex | 31 ++++-- lib/ash_archival/resource/resource.ex | 8 ++ mix.exs | 5 +- .../with_args_children/20250121130028.json | 78 ++++++++++++++ .../with_args_parents/20250121130028.json | 49 +++++++++ .../20250121130028_add_arg_test_resources.exs | 42 ++++++++ test/argument_test.exs | 35 ++++++ test/support/domain.ex | 2 + test/support/with_args.ex | 100 ++++++++++++++++++ 14 files changed, 370 insertions(+), 8 deletions(-) rename documentation/dsls/{DSL:-AshArchival.Resource.md => DSL-AshArchival.Resource.md} (89%) create mode 100644 lib/ash_archival/resource/archive_related_arguments.ex create mode 100644 lib/ash_archival/resource/archive_related_arguments/function.ex create mode 100644 priv/resource_snapshots/test_repo/with_args_children/20250121130028.json create mode 100644 priv/resource_snapshots/test_repo/with_args_parents/20250121130028.json create mode 100644 priv/test_repo/migrations/20250121130028_add_arg_test_resources.exs create mode 100644 test/argument_test.exs create mode 100644 test/support/with_args.ex diff --git a/.formatter.exs b/.formatter.exs index f5ffad3..0191194 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,5 +1,6 @@ spark_locals_without_parens = [ archive_related: 1, + archive_related_arguments: 1, attribute: 1, attribute_type: 1, base_filter?: 1, diff --git a/config/config.exs b/config/config.exs index a9afd5b..d1eabbf 100644 --- a/config/config.exs +++ b/config/config.exs @@ -12,6 +12,7 @@ if Mix.env() == :test do config :ash_archival, AshArchival.TestRepo, username: "postgres", + password: "postgres", database: "ash_archival_test", hostname: "localhost", pool: Ecto.Adapters.SQL.Sandbox diff --git a/documentation/dsls/DSL:-AshArchival.Resource.md b/documentation/dsls/DSL-AshArchival.Resource.md similarity index 89% rename from documentation/dsls/DSL:-AshArchival.Resource.md rename to documentation/dsls/DSL-AshArchival.Resource.md index 8faf542..873fe0c 100644 --- a/documentation/dsls/DSL:-AshArchival.Resource.md +++ b/documentation/dsls/DSL-AshArchival.Resource.md @@ -27,6 +27,7 @@ A section for configuring how archival is configured for a resource. | [`exclude_upsert_actions`](#archive-exclude_upsert_actions){: #archive-exclude_upsert_actions } | `atom \| list(atom)` | `[]` | This option is deprecated as it no longer has any effect. Upserts are handled according to the upsert identity. See the upserts guide for more. | | [`exclude_destroy_actions`](#archive-exclude_destroy_actions){: #archive-exclude_destroy_actions } | `atom \| list(atom)` | `[]` | A destroy action or actions that should *not* archive, but instead be left alone. This allows for having a destroy *or* archive pattern. | | [`archive_related`](#archive-archive_related){: #archive-archive_related } | `list(atom)` | `[]` | A list of relationships that should have all related items archived when this is archived. Notifications are not sent for this operation. | +| [`archive_related_arguments`](#archive-archive_related_arguments){: #archive-archive_related_arguments } | `(any, any -> any) \| module` | | A function to allow passing along some of the arguments to related resources when archiving them. | diff --git a/lib/ash_archival/resource/archive_related_arguments.ex b/lib/ash_archival/resource/archive_related_arguments.ex new file mode 100644 index 0000000..44a3ae8 --- /dev/null +++ b/lib/ash_archival/resource/archive_related_arguments.ex @@ -0,0 +1,10 @@ +defmodule AshArchival.ArchiveRelatedArguments do + @moduledoc """ + The behaviour for specifiying arguments for related resources + """ + @callback arguments( + original_arguments :: map(), + relationship :: atom(), + opts :: Keyword.t() + ) :: map() +end diff --git a/lib/ash_archival/resource/archive_related_arguments/function.ex b/lib/ash_archival/resource/archive_related_arguments/function.ex new file mode 100644 index 0000000..c563529 --- /dev/null +++ b/lib/ash_archival/resource/archive_related_arguments/function.ex @@ -0,0 +1,15 @@ +defmodule AshArchival.ArchiveRelatedArguments.Function do + @moduledoc false + + @behaviour AshArchival.ArchiveRelatedArguments + + @impl true + def arguments(arguments, relationship, [{:fun, {m, f, a}}]) do + apply(m, f, [arguments, relationship, a]) + end + + @impl true + def arguments(arguments, relationship, [{:fun, fun}]) do + fun.(arguments, relationship) + end +end diff --git a/lib/ash_archival/resource/changes/archive_related.ex b/lib/ash_archival/resource/changes/archive_related.ex index a4d4093..18bb6cc 100644 --- a/lib/ash_archival/resource/changes/archive_related.ex +++ b/lib/ash_archival/resource/changes/archive_related.ex @@ -5,7 +5,14 @@ defmodule AshArchival.Resource.Changes.ArchiveRelated do def change(changeset, _, context) do Ash.Changeset.after_action(changeset, fn changeset, result -> - archive_related([result], changeset.resource, changeset.domain, context) + archive_related( + [result], + changeset.resource, + changeset.domain, + changeset.arguments, + context + ) + {:ok, result} end) end @@ -15,7 +22,7 @@ defmodule AshArchival.Resource.Changes.ArchiveRelated do end def after_atomic(changeset, _, record, context) do - archive_related([record], changeset.resource, changeset.domain, context) + archive_related([record], changeset.resource, changeset.domain, changeset.arguments, context) :ok end @@ -24,7 +31,13 @@ defmodule AshArchival.Resource.Changes.ArchiveRelated do records = Enum.map(changesets_and_results, &elem(&1, 1)) - archive_related(records, first_changeset.resource, first_changeset.domain, context) + archive_related( + records, + first_changeset.resource, + first_changeset.domain, + first_changeset.arguments, + context + ) Enum.map(records, fn result -> {:ok, result} @@ -45,11 +58,11 @@ defmodule AshArchival.Resource.Changes.ArchiveRelated do |> Enum.any?() end - defp archive_related([], _, _, _) do + defp archive_related([], _, _, _, _) do :ok end - defp archive_related(data, resource, domain, context) do + defp archive_related(data, resource, domain, arguments, context) do opts = context |> Ash.Context.to_opts( @@ -67,12 +80,18 @@ defmodule AshArchival.Resource.Changes.ArchiveRelated do destroy_action = Ash.Resource.Info.primary_action!(relationship.destination, :destroy).name + arguments = + case AshArchival.Resource.Info.archive_archive_related_arguments(resource) do + {:ok, {module, options}} -> module.arguments(arguments, relationship, options) + _ -> %{} + end + case related_query(data, relationship) do {:ok, query} -> Ash.bulk_destroy!( query, destroy_action, - %{}, + arguments, Keyword.update( opts, :context, diff --git a/lib/ash_archival/resource/resource.ex b/lib/ash_archival/resource/resource.ex index 96e98a2..4a9543f 100644 --- a/lib/ash_archival/resource/resource.ex +++ b/lib/ash_archival/resource/resource.ex @@ -47,6 +47,14 @@ defmodule AshArchival.Resource do doc: """ A list of relationships that should have all related items archived when this is archived. Notifications are not sent for this operation. """ + ], + archive_related_arguments: [ + type: + {:spark_function_behaviour, AshArchival.ArchiveRelatedArguments, + {AshArchival.ArchiveRelatedArguments.Function, 2}}, + doc: """ + A function to allow passing along some of the arguments to related resources when archiving them. + """ ] ] } diff --git a/mix.exs b/mix.exs index 6d0cb25..521b345 100644 --- a/mix.exs +++ b/mix.exs @@ -49,7 +49,7 @@ defmodule AshArchival.MixProject do "documentation/topics/unarchiving.md", "documentation/topics/how-does-ash-archival-work.md", "documentation/topics/upserts-and-identities.md", - "documentation/dsls/DSL:-AshArchival.Resource.md", + "documentation/dsls/DSL-AshArchival.Resource.md", "CHANGELOG.md" ], groups_for_extras: [ @@ -105,7 +105,8 @@ defmodule AshArchival.MixProject do {:credo, ">= 0.0.0", only: [:dev, :test], runtime: false}, {:dialyxir, ">= 0.0.0", only: [:dev, :test], runtime: false}, {:sobelow, ">= 0.0.0", only: [:dev, :test], runtime: false}, - {:mix_audit, ">= 0.0.0", only: [:dev, :test], runtime: false} + {:mix_audit, ">= 0.0.0", only: [:dev, :test], runtime: false}, + {:igniter, "~> 0.5", only: [:dev, :test]} ] end diff --git a/priv/resource_snapshots/test_repo/with_args_children/20250121130028.json b/priv/resource_snapshots/test_repo/with_args_children/20250121130028.json new file mode 100644 index 0000000..6d88cb4 --- /dev/null +++ b/priv/resource_snapshots/test_repo/with_args_children/20250121130028.json @@ -0,0 +1,78 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "arg1", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "with_args_children_parent_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "with_args_parents" + }, + "size": null, + "source": "parent_id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "archived_at", + "type": "utc_datetime_usec" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "00ABDD1E513897CE70121C43F7DD565B216E05F18C3C8325864604E5A71EBC94", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.AshArchival.TestRepo", + "schema": null, + "table": "with_args_children" +} \ No newline at end of file diff --git a/priv/resource_snapshots/test_repo/with_args_parents/20250121130028.json b/priv/resource_snapshots/test_repo/with_args_parents/20250121130028.json new file mode 100644 index 0000000..88fe192 --- /dev/null +++ b/priv/resource_snapshots/test_repo/with_args_parents/20250121130028.json @@ -0,0 +1,49 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "arg1", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "archived_at", + "type": "utc_datetime_usec" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "0BBBBD5986B09FEED6E467320D9B2A32E2F63FE1297E1E47F97155400F7B113D", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.AshArchival.TestRepo", + "schema": null, + "table": "with_args_parents" +} \ No newline at end of file diff --git a/priv/test_repo/migrations/20250121130028_add_arg_test_resources.exs b/priv/test_repo/migrations/20250121130028_add_arg_test_resources.exs new file mode 100644 index 0000000..05bd085 --- /dev/null +++ b/priv/test_repo/migrations/20250121130028_add_arg_test_resources.exs @@ -0,0 +1,42 @@ +defmodule AshArchival.TestRepo.Migrations.AddArgTestResources do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + create table(:with_args_parents, primary_key: false) do + add(:id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true) + add(:arg1, :text) + add(:archived_at, :utc_datetime_usec) + end + + create table(:with_args_children, primary_key: false) do + add(:id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true) + add(:arg1, :text) + + add( + :parent_id, + references(:with_args_parents, + column: :id, + name: "with_args_children_parent_id_fkey", + type: :uuid, + prefix: "public" + ) + ) + + add(:archived_at, :utc_datetime_usec) + end + end + + def down do + drop(constraint(:with_args_children, "with_args_children_parent_id_fkey")) + + drop(table(:with_args_children)) + + drop(table(:with_args_parents)) + end +end diff --git a/test/argument_test.exs b/test/argument_test.exs new file mode 100644 index 0000000..bb44fab --- /dev/null +++ b/test/argument_test.exs @@ -0,0 +1,35 @@ +defmodule AshArchival.Test.ArgumentTest do + use AshArchival.RepoCase + + alias AshArchival.Test.WithArgsParent + alias AshArchival.Test.WithArgsChild + + test "can pass arguments when archiving related resources" do + parent = + WithArgsParent + |> Ash.Changeset.for_create(:create) + |> Ash.create!() + + WithArgsChild + |> Ash.Changeset.for_create(:create, %{parent_id: parent.id}) + |> Ash.create!() + + parent + |> Ash.Changeset.for_destroy(:archive, %{arg: "test"}) + |> Ash.destroy!() + + parent = + WithArgsParent + |> Ash.Query.for_read(:read) + |> Ash.read_one!() + + assert parent.arg1 == "test" + + child = + WithArgsChild + |> Ash.Query.for_read(:read) + |> Ash.read_one!() + + assert child.arg1 == "test" + end +end diff --git a/test/support/domain.ex b/test/support/domain.ex index ce091f6..bd98d9c 100644 --- a/test/support/domain.ex +++ b/test/support/domain.ex @@ -4,5 +4,7 @@ defmodule AshArchival.Test.Domain do resources do resource(AshArchival.Test.Post) + resource(AshArchival.Test.WithArgsParent) + resource(AshArchival.Test.WithArgsChild) end end diff --git a/test/support/with_args.ex b/test/support/with_args.ex new file mode 100644 index 0000000..fe959f6 --- /dev/null +++ b/test/support/with_args.ex @@ -0,0 +1,100 @@ +defmodule CreateArgs do + @behaviour AshArchival.ArchiveRelatedArguments + + @impl true + def arguments(arguments, _rel, _opts) do + %{arg: arguments[:arg]} + end +end + +defmodule AshArchival.Test.WithArgsParent do + @moduledoc false + use Ash.Resource, + domain: AshArchival.Test.Domain, + data_layer: AshPostgres.DataLayer, + extensions: [AshArchival.Resource] + + archive do + exclude_read_actions :read + archive_related [:children] + + archive_related_arguments CreateArgs + end + + postgres do + table("with_args_parents") + repo(AshArchival.TestRepo) + end + + attributes do + uuid_primary_key(:id) + attribute(:arg1, :string) + end + + actions do + default_accept(:*) + defaults([:create, :read, :update]) + + destroy :archive do + primary?(true) + accept([]) + + argument :arg, :string do + allow_nil?(false) + end + + change(set_attribute(:arg1, arg(:arg))) + end + end + + relationships do + has_many(:children, AshArchival.Test.WithArgsChild) do + destination_attribute(:parent_id) + source_attribute(:id) + end + end +end + +defmodule AshArchival.Test.WithArgsChild do + @moduledoc false + use Ash.Resource, + domain: AshArchival.Test.Domain, + data_layer: AshPostgres.DataLayer, + extensions: [AshArchival.Resource] + + archive do + exclude_read_actions :read + end + + postgres do + table("with_args_children") + repo(AshArchival.TestRepo) + end + + attributes do + uuid_primary_key(:id) + attribute(:arg1, :string) + end + + actions do + default_accept(:*) + defaults([:create, :read, :update]) + + destroy :archive do + primary?(true) + accept([]) + + argument :arg, :string do + allow_nil?(false) + end + + change(set_attribute(:arg1, arg(:arg))) + end + end + + relationships do + belongs_to(:parent, AshArchival.Test.WithArgsParent) do + public?(true) + end + end +end