Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds store_inputs? true/false option #29

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ spark_locals_without_parens = [
on_actions: 1,
reference_source?: 1,
store_action_name?: 1,
version_extensions: 1
version_extensions: 1,
store_inputs?: 1
]

[
Expand Down
50 changes: 43 additions & 7 deletions lib/resource/changes/create_new_version.ex
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,15 @@ defmodule AshPaperTrail.Resource.Changes.CreateNewVersion do

change_tracking_mode = AshPaperTrail.Resource.Info.change_tracking_mode(changeset.resource)

belongs_to_actors =
AshPaperTrail.Resource.Info.belongs_to_actor(changeset.resource)
belongs_to_actors = AshPaperTrail.Resource.Info.belongs_to_actor(changeset.resource)

actor = changeset.context[:private][:actor]

resource_attributes =
changeset.resource
|> Ash.Resource.Info.attributes()

{input, private} =
{public_params, private_params} =
resource_attributes
|> Enum.filter(&(&1.name in attributes_as_attributes))
|> Enum.reduce({%{}, %{}}, &build_inputs(changeset, &1, &2))
Expand All @@ -66,8 +65,8 @@ defmodule AshPaperTrail.Resource.Changes.CreateNewVersion do
)
|> Enum.reduce(%{}, &build_changes(changeset, &1, &2))

input =
Enum.reduce(belongs_to_actors, input, fn belongs_to_actor, input ->
public_params =
Enum.reduce(belongs_to_actors, public_params, fn belongs_to_actor, input ->
with true <- is_struct(actor) && actor.__struct__ == belongs_to_actor.destination,
relationship when not is_nil(relationship) <-
Ash.Resource.Info.relationship(version_resource, belongs_to_actor.name) do
Expand All @@ -85,15 +84,18 @@ defmodule AshPaperTrail.Resource.Changes.CreateNewVersion do
version_action_name: changeset.action.name,
changes: changes
})
|> maybe_add_original_inputs(changeset)

{_, notifications} =
version_changeset
|> Ash.Changeset.for_create(:create, input,
|> Ash.Changeset.for_create(:create, public_params,
tenant: changeset.tenant,
authorize?: false,
actor: actor
)
|> Ash.Changeset.force_change_attributes(Map.take(private, version_resource_attributes))
|> Ash.Changeset.force_change_attributes(
Map.take(private_params, version_resource_attributes)
)
|> changeset.api.create!(return_notifications?: true)

notifications
Expand Down Expand Up @@ -128,4 +130,38 @@ defmodule AshPaperTrail.Resource.Changes.CreateNewVersion do
{:ok, dumped_value} = Ash.Type.dump_to_embedded(attribute.type, value, [])
Map.put(changes, attribute.name, dumped_value)
end

defp maybe_add_original_inputs(params, changeset) do
if AshPaperTrail.Resource.Info.store_inputs?(changeset.resource) do
inputs =
changeset.attributes
|> Map.merge(changeset.arguments)
|> normalise_inputs()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't do this by looking for structs, we should get the corresponding argument or attribute and use dump_to_embedded on it regardless of if the value is a struct or not.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That will take care of the recursive aspect as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just this one open issue. Otherwise, looks good.


Map.put(params, :inputs, inputs)
else
params
end
end

defp normalise_inputs(input = %mod{}) when is_struct(input) do
case Ash.Type.dump_to_embedded(mod, input, []) do
{:ok, embedded_map} -> embedded_map
other -> raise "Unexpected result from dump_to_embedded: #{inspect(other)}"
end
end

defp normalise_inputs(inputs) when is_map(inputs) do
Map.new(inputs, fn {k, v} ->
{k, normalise_inputs(v)}
end)
end

defp normalise_inputs(inputs = [input | rest]) when is_list(inputs) do
[normalise_inputs(input) | normalise_inputs(rest)]
end

defp normalise_inputs(input) do
input
end
end
5 changes: 5 additions & 0 deletions lib/resource/info.ex
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,9 @@ defmodule AshPaperTrail.Resource.Info do
Module.concat([Spark.Dsl.Extension.get_persisted(resource, :module), Version])
end
end

@spec store_inputs?(Spark.Dsl.t() | Ash.Resource.t()) :: boolean
def store_inputs?(resource) do
Spark.Dsl.Extension.get_opt(resource, [:paper_trail], :store_inputs?, false)
end
end
13 changes: 10 additions & 3 deletions lib/resource/resource.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ defmodule AshPaperTrail.Resource do
@belongs_to_actor %Spark.Dsl.Entity{
name: :belongs_to_actor,
describe: """
Creates a belongs_to relationship for the actor resource. When creating a new version, if the actor on the action is set and
matches the resource type, the version will be related to the actor. If your actors are polymorphic or varying types, declare a
Creates a belongs_to relationship for the actor resource. When creating a new version, if the actor on the action is set and
matches the resource type, the version will be related to the actor. If your actors are polymorphic or varying types, declare a
belongs_to_actor for each type.

A reference is also created with `on_delete: :nilify` and `on_update: :update`

If you need more complex relationships, set `define_attribute? false` and add the relationship via a mixin.

If your actor is not a resource, add a mixin and with a change for all creates that sets the actor's to one your attributes.
If your actor is not a resource, add a mixin and with a change for all creates that sets the actor's to one your attributes.
The actor on the version changeset is set.
""",
examples: [
Expand Down Expand Up @@ -94,6 +94,13 @@ defmodule AshPaperTrail.Resource do
doc: """
Extensions that should be used by the version resource. For example: `extensions: [AshGraphql.Resource], notifier: [Ash.Notifiers.PubSub]`
"""
],
store_inputs?: [
chrishop marked this conversation as resolved.
Show resolved Hide resolved
type: :boolean,
default: false,
doc: """
Whether or not to store the original inputs to the action as well as the changes within the version resource. Note: There's no filtering of the user input, so if your resource has password attributes or arguments, beware the will be stored in plain text.
"""
]
]
}
Expand Down
10 changes: 9 additions & 1 deletion lib/resource/transformers/create_version_resource.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ defmodule AshPaperTrail.Resource.Transformers.CreateVersionResource do
reference_source? = AshPaperTrail.Resource.Info.reference_source?(dsl_state)
store_action_name? = AshPaperTrail.Resource.Info.store_action_name?(dsl_state)
version_extensions = AshPaperTrail.Resource.Info.version_extensions(dsl_state)
store_inputs? = AshPaperTrail.Resource.Info.store_inputs?(dsl_state)

attributes =
dsl_state
|> Ash.Resource.Info.attributes()
|> Enum.filter(&(&1.name in attributes_as_attributes))

sensitive_changes? = dsl_state
sensitive_changes? =
dsl_state
|> Ash.Resource.Info.attributes()
|> Enum.filter(&(&1.name in ignore_attributes))
|> Enum.any?(& &1.sensitive?)
Expand Down Expand Up @@ -162,6 +164,12 @@ defmodule AshPaperTrail.Resource.Transformers.CreateVersionResource do
end
end

if unquote(store_inputs?) do
attribute :inputs, :map do
sensitive? true
end
end

attribute :changes, :map do
sensitive? unquote(sensitive_changes?)
end
Expand Down
35 changes: 29 additions & 6 deletions test/ash_paper_trail_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ defmodule AshPaperTrailTest do
assert %{subject: "new subject", body: "new body"} =
Posts.Post.update!(post, %{subject: "new subject", body: "new body"})

assert [%{subject: "new subject", body: "new body"}] =
Posts.Post.read!(tenant: "acme")
assert [%{subject: "new subject", body: "new body"}] = Posts.Post.read!(tenant: "acme")
end

test "destroys work as normal" do
Expand Down Expand Up @@ -66,8 +65,7 @@ defmodule AshPaperTrailTest do
version_action_name: :create,
version_source_id: ^post_id
}
] =
Articles.Api.read!(Posts.Post.Version, tenant: "acme")
] = Articles.Api.read!(Posts.Post.Version, tenant: "acme")
end

test "a new version is created on update" do
Expand Down Expand Up @@ -133,8 +131,7 @@ defmodule AshPaperTrailTest do
Articles.Api.read!(Articles.Article.Version)
|> Enum.filter(&(&1.version_action_type == :update))

assert [:body, :subject] =
Map.keys(updated_version.changes) |> Enum.sort()
assert [:body, :subject] = Map.keys(updated_version.changes) |> Enum.sort()
end

test "a new version is created on destroy" do
Expand Down Expand Up @@ -260,4 +257,30 @@ defmodule AshPaperTrailTest do
assert [] = Articles.Article.read!()
end
end

describe "store_inputs? options" do
test "when true, on create will create a version with the original inputs" do
assert AshPaperTrail.Resource.Info.store_inputs?(Posts.Post) == true

Posts.Post.create!(@valid_attrs, tenant: "acme")

post_version = Posts.Api.read!(Posts.Post.Version, tenant: "acme")

assert [
%{
inputs: %{
author: %{autogenerated_id: _author_id, first_name: "John", last_name: "Doe"},
body: "body",
id: _id,
published: false,
subject: "subject",
tags: [
%{autogenerated_id: _ash_tag_id, tag: "ash"},
%{autogenerated_id: _phoenix_tag_id, tag: "phoenix"}
]
}
}
] = post_version
end
end
end
1 change: 1 addition & 0 deletions test/support/posts/post.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ defmodule AshPaperTrail.Test.Posts.Post do
attributes_as_attributes [:subject, :body, :tenant]
change_tracking_mode :changes_only
store_action_name? true
store_inputs? true
belongs_to_actor :user, AshPaperTrail.Test.Accounts.User, api: AshPaperTrail.Test.Accounts.Api

belongs_to_actor :news_feed, AshPaperTrail.Test.Accounts.NewsFeed,
Expand Down