Skip to content

Commit 45a5427

Browse files
committed
improvement: add create_version_on_destroy? option
1 parent fab40a5 commit 45a5427

File tree

8 files changed

+195
-10
lines changed

8 files changed

+195
-10
lines changed

documentation/dsls/DSL-AshPaperTrail.Resource.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ A section for configuring how versioning is derived for the resource.
3434
| [`relationship_opts`](#paper_trail-relationship_opts){: #paper_trail-relationship_opts } | `keyword` | | Options to pass to the has_many :paper_trail_versions relationship that is created on this resource. For example, `public?: true` to expose the relationship over graphql. See `d:Ash.Resource.Dsl.relationships.has_many`. |
3535
| [`store_action_name?`](#paper_trail-store_action_name?){: #paper_trail-store_action_name? } | `boolean` | `false` | Whether or not to add the `version_action_name` attribute to the version resource. This is useful for auditing purposes. The `version_action_type` attribute is always stored. |
3636
| [`store_action_inputs?`](#paper_trail-store_action_inputs?){: #paper_trail-store_action_inputs? } | `boolean` | `false` | Whether or not to add the `version_action_inputs` attribute to the version resource, which will store all attributes and arguments for the called action, redacting any sensitive values. This is useful for auditing purposes. The `version_action_inputs` attribute is always stored. |
37+
| [`create_version_on_destroy?`](#paper_trail-create_version_on_destroy?){: #paper_trail-create_version_on_destroy? } | `boolean` | `true` | Whether or not to create a version on destroy. You will need to set this to `false` unless you are doing soft destroys (like with `AshArchival`) |
3738
| [`store_resource_identifier?`](#paper_trail-store_resource_identifier?){: #paper_trail-store_resource_identifier? } | `boolean` | `false` | Whether or not to add the `version_resource_identifier` attribute to the version resource. This is useful for auditing purposes. |
3839
| [`resource_identifier`](#paper_trail-resource_identifier){: #paper_trail-resource_identifier } | `atom` | | A name to use for this resource in the `version_resource_identifier`. Defaults to `Ash.Resource.Info.short_name/1`. |
3940
| [`version_extensions`](#paper_trail-version_extensions){: #paper_trail-version_extensions } | `keyword` | `[]` | Extensions that should be used by the version resource. For example: `extensions: [AshGraphql.Resource], notifier: [Ash.Notifiers.PubSub]` |

documentation/tutorials/getting-started-with-ash-paper-trail.md

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,25 @@ end
6969

7070
## Destroy Actions
7171

72-
If you are using `AshPostgres`, and you want to support destroy actions, you need to do one of two things:
72+
If you are using `AshPostgres`, and you want to support destroy actions, you will need to do one of the following:
7373

74-
1. (preferred) use the version mixin to set `on_delete: :delete` on the ference.
74+
<!-- tabs-open -->
75+
76+
### Don't version destroys
77+
78+
1. configure versions not to be created on destroy actions
7579

7680
```elixir
7781
paper_trail do
78-
mixin {MyApp.MyResource.PaperTrailMixin, :mixin, []}
82+
create_version_on_destroy? false
7983
end
84+
```
85+
86+
2. use the version mixin to set `on_delete: :delete` on the underlying reference to the parent resource.
8087

81-
...
88+
First, create a paper trail mixin if you haven't already
8289

90+
```elixir
8391
defmodule MyApp.MyResource.PaperTrailMixin do
8492
def mixin do
8593
# quote here is because we are returning code to be evaluated inside of the
@@ -96,17 +104,33 @@ end
96104
```
97105

98106

99-
2. use something like `AshArchival` in conjunction with this resource to ensure that destroy actions are `soft?` and do not actually result in row deletion
107+
Second, configure it in the resource
108+
109+
```elixir
110+
paper_trail do
111+
mixin {MyApp.MyResource.PaperTrailMixin, :mixin, []}
112+
end
113+
```
114+
115+
116+
### Soft Destroys
100117

101-
3. configure `AshPaperTrail` to ignore the reference, via:
118+
Manually implement soft deletion, or use something like [`AshArchival`](https://hexdocs.pm/ash_archival) to ensure that destroy actions are `soft?` and do not actually result in row deletion
119+
120+
### Disable Foreign Keys (not recommended)
121+
122+
configure `AshPaperTrail` to ignore the reference, like so:
102123

103124
```elixir
104125
paper_trail do
105126
reference_source? false
106127
end
107128
```
108129

109-
and you could then use the `mixin` described below to add `on_delete` options to the reference.
130+
This will make it skip creating a foreign key for the version source attribute
131+
132+
133+
<!-- tabs-close -->
110134

111135
## Attributes
112136

lib/resource/changes/create_new_version.ex

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ defmodule AshPaperTrail.Resource.Changes.CreateNewVersion do
5353

5454
defp valid_for_tracking?(%Ash.Changeset{} = changeset) do
5555
changeset.action.name not in AshPaperTrail.Resource.Info.ignore_actions(changeset.resource) &&
56-
(changeset.action_type in [:create, :destroy] ||
56+
(changeset.action_type == :create ||
57+
(changeset.action_type == :destroy &&
58+
AshPaperTrail.Resource.Info.create_version_on_destroy?(changeset.resource)) ||
5759
(changeset.action_type == :update &&
5860
changeset.action.name in AshPaperTrail.Resource.Info.on_actions(changeset.resource)))
5961
end
@@ -62,7 +64,9 @@ defmodule AshPaperTrail.Resource.Changes.CreateNewVersion do
6264
Ash.Changeset.after_action(changeset, fn changeset, result ->
6365
changed? = changed?(changeset)
6466

65-
if changeset.action_type in [:create, :destroy] ||
67+
if changeset.action_type == :create ||
68+
(changeset.action_type == :destroy &&
69+
AshPaperTrail.Resource.Info.create_version_on_destroy?(changeset.resource)) ||
6670
(changeset.action_type == :update && changed?) do
6771
{version_changeset, input, actor} = build_notifications(changeset, result)
6872
create!(changeset, version_changeset, input, actor)
@@ -78,7 +82,9 @@ defmodule AshPaperTrail.Resource.Changes.CreateNewVersion do
7882
|> Enum.filter(fn {changeset, _result} ->
7983
changed? = changed?(changeset)
8084

81-
changeset.action_type in [:create, :destroy] ||
85+
changeset.action_type == :create ||
86+
(changeset.action_type == :destroy &&
87+
AshPaperTrail.Resource.Info.create_version_on_destroy?(changeset.resource)) ||
8288
(changeset.action_type == :update && changed?)
8389
end)
8490
|> Enum.map(fn {changeset, result} -> build_notifications(changeset, result, bulk?: true) end)

lib/resource/info.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,9 @@ defmodule AshPaperTrail.Resource.Info do
107107
def public_timestamps?(resource) do
108108
Spark.Dsl.Extension.get_opt(resource, [:paper_trail], :public_timestamps?, false)
109109
end
110+
111+
@spec create_version_on_destroy?(Spark.Dsl.t() | Ash.Resource.t()) :: boolean
112+
def create_version_on_destroy?(resource) do
113+
Spark.Dsl.Extension.get_opt(resource, [:paper_trail], :create_version_on_destroy?, true)
114+
end
110115
end

lib/resource/resource.ex

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,12 @@ defmodule AshPaperTrail.Resource do
115115
doc:
116116
"Whether or not to add the `version_action_inputs` attribute to the version resource, which will store all attributes and arguments for the called action, redacting any sensitive values. This is useful for auditing purposes. The `version_action_inputs` attribute is always stored."
117117
],
118+
create_version_on_destroy?: [
119+
type: :boolean,
120+
default: true,
121+
doc:
122+
"Whether or not to create a version on destroy. You will need to set this to `false` unless you are doing soft destroys (like with `AshArchival`)"
123+
],
118124
store_resource_identifier?: [
119125
type: :boolean,
120126
default: false,

test/ash_paper_trail_test.exs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,98 @@ defmodule AshPaperTrailTest do
734734
end
735735
end
736736

737+
describe "create_version_on_destroy?" do
738+
test "when create_version_on_destroy? is false, no version is created on destroy" do
739+
assert %{subject: "subject", body: "body", id: post_id} =
740+
post = Posts.NoDestroyVersionPost.create!(%{subject: "subject", body: "body"})
741+
742+
assert :ok = Posts.NoDestroyVersionPost.destroy!(post)
743+
744+
assert [
745+
%{
746+
subject: "subject",
747+
body: "body",
748+
version_action_type: :create,
749+
version_source_id: ^post_id
750+
}
751+
] = Ash.read!(Posts.NoDestroyVersionPost.Version)
752+
753+
refute Enum.any?(Ash.read!(Posts.NoDestroyVersionPost.Version), &(&1.version_action_type == :destroy))
754+
end
755+
756+
test "when create_version_on_destroy? is false, no version is created on bulk destroy with enumerable" do
757+
%{subject: "subject", body: "body", id: post_id} =
758+
post = Posts.NoDestroyVersionPost.create!(%{subject: "subject", body: "body"})
759+
760+
%Ash.BulkResult{
761+
status: :success
762+
} =
763+
Ash.bulk_destroy!([post], :destroy, %{},
764+
strategy: :stream,
765+
return_errors?: true
766+
)
767+
768+
assert [
769+
%{
770+
subject: "subject",
771+
body: "body",
772+
version_action_type: :create,
773+
version_source_id: ^post_id
774+
}
775+
] = Ash.read!(Posts.NoDestroyVersionPost.Version)
776+
777+
refute Enum.any?(Ash.read!(Posts.NoDestroyVersionPost.Version), &(&1.version_action_type == :destroy))
778+
end
779+
780+
test "when create_version_on_destroy? is false, no version is created on bulk destroy with query" do
781+
%{subject: "subject", body: "body", id: post_id} =
782+
Posts.NoDestroyVersionPost.create!(%{subject: "subject", body: "body"})
783+
784+
%Ash.BulkResult{
785+
status: :success
786+
} =
787+
Posts.NoDestroyVersionPost
788+
|> Ash.Query.filter(id: post_id)
789+
|> Ash.bulk_destroy!(:destroy, %{},
790+
strategy: :stream,
791+
return_errors?: true
792+
)
793+
794+
assert [
795+
%{
796+
subject: "subject",
797+
body: "body",
798+
version_action_type: :create,
799+
version_source_id: ^post_id
800+
}
801+
] = Ash.read!(Posts.NoDestroyVersionPost.Version)
802+
803+
refute Enum.any?(Ash.read!(Posts.NoDestroyVersionPost.Version), &(&1.version_action_type == :destroy))
804+
end
805+
806+
test "when create_version_on_destroy? is true (default), a version is created on destroy" do
807+
assert %{subject: "subject", body: "body", id: post_id} =
808+
post = Posts.Post.create!(@valid_attrs, tenant: "acme")
809+
810+
assert :ok = Posts.Post.destroy!(post, tenant: "acme")
811+
812+
versions =
813+
Ash.read!(Posts.Post.Version, tenant: "acme")
814+
|> Enum.sort_by(& &1.version_inserted_at)
815+
816+
assert [
817+
%{
818+
version_action_type: :create,
819+
version_source_id: ^post_id
820+
},
821+
%{
822+
version_action_type: :destroy,
823+
version_source_id: ^post_id
824+
}
825+
] = versions
826+
end
827+
end
828+
737829
describe "public_timestamps?" do
738830
test "when public_timestamps? is false, timestamps are not public on the version resource" do
739831
assert AshPaperTrail.Resource.Info.public_timestamps?(AshPaperTrail.Test.Articles.Article) ==

test/support/posts/domain.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,7 @@ defmodule AshPaperTrail.Test.Posts.Domain do
1111
resource AshPaperTrail.Test.Posts.FullDiffPost.Version
1212
resource AshPaperTrail.Test.Posts.StoreInputsPost
1313
resource AshPaperTrail.Test.Posts.StoreInputsPost.Version
14+
resource AshPaperTrail.Test.Posts.NoDestroyVersionPost
15+
resource AshPaperTrail.Test.Posts.NoDestroyVersionPost.Version
1416
end
1517
end
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
defmodule AshPaperTrail.Test.Posts.NoDestroyVersionPost do
2+
@moduledoc """
3+
A post resource that doesn't create versions on destroy
4+
"""
5+
6+
use Ash.Resource,
7+
domain: AshPaperTrail.Test.Posts.Domain,
8+
data_layer: Ash.DataLayer.Ets,
9+
extensions: [AshPaperTrail.Resource],
10+
validate_domain_inclusion?: false
11+
12+
ets do
13+
private? true
14+
end
15+
16+
paper_trail do
17+
attributes_as_attributes [:subject, :body]
18+
create_version_on_destroy? false
19+
end
20+
21+
code_interface do
22+
define :create
23+
define :read
24+
define :update
25+
define :destroy
26+
end
27+
28+
actions do
29+
default_accept :*
30+
defaults [:create, :read, :update, :destroy]
31+
end
32+
33+
attributes do
34+
uuid_primary_key :id
35+
36+
attribute :subject, :string do
37+
public? true
38+
allow_nil? false
39+
end
40+
41+
attribute :body, :string do
42+
public? true
43+
allow_nil? false
44+
end
45+
46+
create_timestamp :inserted_at
47+
update_timestamp :updated_at
48+
end
49+
end

0 commit comments

Comments
 (0)