Skip to content

Commit

Permalink
feat: Full diff change tracking mode (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
rgraff authored Dec 27, 2023
1 parent 97510a8 commit 449cd2a
Show file tree
Hide file tree
Showing 24 changed files with 2,086 additions and 89 deletions.
88 changes: 57 additions & 31 deletions documentation/dsls/DSL:-AshPaperTrail.Resource.cheatmd
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,28 @@ A section for configuring how versioning is derived for the resource.

</td>
<td style="text-align: left">
<code class="inline">:snapshot | :changes_only</code>
<code class="inline">:snapshot | :changes_only | :full_diff</code>
</td>
<td style="text-align: left">
<code class="inline">:snapshot</code>
</td>
<td style="text-align: left" colspan=2>
The mode to use for change tracking. Valid options are `:snapshot` and `:changes_only`.
`:snapshot` will store the entire resource in the `changes` attribute, while `:changes_only`
will only store the attributes that have changed.
Changes are stored in a map attribute called `changes`. The `change_tracking_mode`
determines what's stored. Valid options are `:snapshot` and `:changes_only` and `:full_diff`.

:snapshot will json dump the contents of every attribute whether they changed or not.

`{ subject: "new subject", body: "unchanged body", author: { name: "bob"}}`

:changes_only will json dump the contents of only the attributes that have changed.
Note if any part of an embedded attribute and array of embedded attributes, changes then
the entire top level attribute is dumped.

`{ subject: "new subject" }`

:full_diff will json dump the contents of each attribute.
`{ subject: { from: "subject", to: "new subject" }, body: { unchanged: "unchanged_body" }}, author: { changes: { unchanged: "bob" }}`


</td>
</tr>
Expand Down Expand Up @@ -236,8 +249,7 @@ belongs_to_actor :user, MyApp.Users.User, api: MyApp.Users




### Options
### Arguments

<table>
<thead>
Expand All @@ -251,7 +263,7 @@ belongs_to_actor :user, MyApp.Users.User, api: MyApp.Users
<tbody>
<tr>
<td style="text-align: left">
<a id="name-destination-name" href="#name-destination-name">
<a id="paper_trail-belongs_to_actor-name" href="#paper_trail-belongs_to_actor-name">
<span style="font-family: Inconsolata, Menlo, Courier, monospace;">
name
</span>
Expand All @@ -272,7 +284,41 @@ belongs_to_actor :user, MyApp.Users.User, api: MyApp.Users

<tr>
<td style="text-align: left">
<a id="name-destination-allow_nil?" href="#name-destination-allow_nil?">
<a id="paper_trail-belongs_to_actor-destination" href="#paper_trail-belongs_to_actor-destination">
<span style="font-family: Inconsolata, Menlo, Courier, monospace;">
destination
</span>
</a>

</td>
<td style="text-align: left">
<code class="inline">module</code>
</td>
<td style="text-align: left">

</td>
<td style="text-align: left" colspan=2>
The resource of the actor (e.g. MyApp.Users.User)
</td>
</tr>

</tbody>
</table>
### Options

<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Default</th>
<th colspan=2>Docs</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left">
<a id="paper_trail-belongs_to_actor-allow_nil?" href="#paper_trail-belongs_to_actor-allow_nil?">
<span style="font-family: Inconsolata, Menlo, Courier, monospace;">
allow_nil?
</span>
Expand All @@ -292,7 +338,7 @@ belongs_to_actor :user, MyApp.Users.User, api: MyApp.Users

<tr>
<td style="text-align: left">
<a id="name-destination-api" href="#name-destination-api">
<a id="paper_trail-belongs_to_actor-api" href="#paper_trail-belongs_to_actor-api">
<span style="font-family: Inconsolata, Menlo, Courier, monospace;">
api
</span>
Expand All @@ -313,7 +359,7 @@ belongs_to_actor :user, MyApp.Users.User, api: MyApp.Users

<tr>
<td style="text-align: left">
<a id="name-destination-attribute_type" href="#name-destination-attribute_type">
<a id="paper_trail-belongs_to_actor-attribute_type" href="#paper_trail-belongs_to_actor-attribute_type">
<span style="font-family: Inconsolata, Menlo, Courier, monospace;">
attribute_type
</span>
Expand All @@ -333,7 +379,7 @@ belongs_to_actor :user, MyApp.Users.User, api: MyApp.Users

<tr>
<td style="text-align: left">
<a id="name-destination-define_attribute?" href="#name-destination-define_attribute?">
<a id="paper_trail-belongs_to_actor-define_attribute?" href="#paper_trail-belongs_to_actor-define_attribute?">
<span style="font-family: Inconsolata, Menlo, Courier, monospace;">
define_attribute?
</span>
Expand All @@ -351,26 +397,6 @@ belongs_to_actor :user, MyApp.Users.User, api: MyApp.Users
</td>
</tr>

<tr>
<td style="text-align: left">
<a id="name-destination-destination" href="#name-destination-destination">
<span style="font-family: Inconsolata, Menlo, Courier, monospace;">
destination
</span>
</a>

</td>
<td style="text-align: left">
<code class="inline">module</code>
</td>
<td style="text-align: left">

</td>
<td style="text-align: left" colspan=2>
The resource of the actor (e.g. MyApp.Users.User)
</td>
</tr>

</tbody>
</table>

Expand Down
18 changes: 18 additions & 0 deletions lib/change_builders/changes_only.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
defmodule AshPaperTrail.ChangeBuilders.ChangesOnly do
def build_changes(attributes, changeset) do
Enum.reduce(attributes, %{}, &build_attribute_change(&1, changeset, &2))
end

def build_attribute_change(attribute, changeset, changes) do
if Ash.Changeset.changing_attribute?(changeset, attribute.name) do
value = Ash.Changeset.get_attribute(changeset, attribute.name)

{:ok, dumped_value} =
Ash.Type.dump_to_embedded(attribute.type, value, attribute.constraints)

Map.put(changes, attribute.name, dumped_value)
else
changes
end
end
end
51 changes: 51 additions & 0 deletions lib/change_builders/full_diff/embedded_change.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
defmodule AshPaperTrail.ChangeBuilders.FullDiff.EmbeddedChange do
import AshPaperTrail.ChangeBuilders.FullDiff.Helpers

@moduledoc """
A simple attribute change will be represented as a map:
%{ created: %{ subject: %{to: "subject"} } }
%{ updated: %{ subject: %{from: "subject", to: "new subject"} } }
%{ unchanged: %{ subject: %{unchanged: "subject"} } }
%{ destroyed: %{ subject: %{unchanged: "subject"} } }
"""

def build(attribute, changeset) do
dump_data_value(changeset, attribute)
|> embedded_change_map()
end

def dump_data_value(changeset, attribute) do
data_tuple =
if changeset.action_type == :create do
:not_present
else
case Ash.Changeset.get_data(changeset, attribute.name) do
nil ->
nil

data ->
dumped_data = dump_value(data, attribute)
uid = unique_id(data, dumped_data)
{uid, dumped_data}
end
end

value_tuple =
case Ash.Changeset.fetch_change(changeset, attribute.name) do
{:ok, nil} ->
nil

{:ok, value} ->
dumped_value = dump_value(value, attribute)
uid = unique_id(value, dumped_value)
{uid, dumped_value}

:error ->
:not_present
end

{data_tuple, value_tuple}
end
end
53 changes: 53 additions & 0 deletions lib/change_builders/full_diff/full_diff.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
defmodule AshPaperTrail.ChangeBuilders.FullDiff do
@moduledoc """
Builds a diff of the changeset that is both fairly easy read and includes a complete
representation of the changes mades.
"""

import AshPaperTrail.ChangeBuilders.FullDiff.Helpers

alias AshPaperTrail.ChangeBuilders.FullDiff.{
SimpleChange,
EmbeddedChange,
UnionChange,
ListChange
}

@doc """
Return a map of the changes made with a key for each attribute and a value
that is a map representing each change. The structure of map representing the
each change comes in multiple: simple/native, embedded, union, and array of embedded and array of unions.
%{
subject: %{ from: "subject", to: "new subject" },
body: { unchanged: "body" }
}
"""
def build_changes(attributes, changeset) do
Enum.reduce(attributes, %{}, fn attribute, changes ->
Map.put(
changes,
attribute.name,
build_attribute_change(attribute, changeset)
)
end)
end

defp build_attribute_change(%{type: {:array, _}} = attribute, changeset) do
ListChange.build(attribute, changeset)
end

defp build_attribute_change(attribute, changeset) do
cond do
is_union?(attribute.type) ->
UnionChange.build(attribute, changeset)

is_embedded?(attribute.type) ->
EmbeddedChange.build(attribute, changeset)

true ->
SimpleChange.build(attribute, changeset)
end
end
end
Loading

0 comments on commit 449cd2a

Please sign in to comment.