defmodule AshPaperTrail.Resource.Changes.CreateNewVersion do @moduledoc "Creates a new version whenever a resource is created, deleted, or updated" use Ash.Resource.Change require Ash.Query require Logger def change(changeset, _, _) do if changeset.action_type in [:create, :destroy] || (changeset.action_type == :update && changeset.action.name in AshPaperTrail.Resource.Info.on_actions(changeset.resource)) do create_new_version(changeset) else changeset end end defp create_new_version(changeset) do Ash.Changeset.after_action(changeset, fn changeset, result -> if changeset.action_type in [:create, :destroy] || (changeset.action_type == :update && changeset.context.changed?) do {:ok, result, build_notifications(changeset, result)} else {:ok, result} end end) end defp build_notifications(changeset, result) do version_resource = AshPaperTrail.Resource.Info.version_resource(changeset.resource) version_resource_attributes = version_resource |> Ash.Resource.Info.attributes() |> Enum.map(& &1.name) version_changeset = Ash.Changeset.new(version_resource) to_skip = Ash.Resource.Info.primary_key(changeset.resource) ++ AshPaperTrail.Resource.Info.ignore_attributes(changeset.resource) attributes_as_attributes = AshPaperTrail.Resource.Info.attributes_as_attributes(changeset.resource) change_tracking_mode = AshPaperTrail.Resource.Info.change_tracking_mode(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} = resource_attributes |> Enum.filter(&(&1.name in attributes_as_attributes)) |> Enum.reduce({%{}, %{}}, &build_inputs(changeset, &1, &2)) changes = resource_attributes |> Enum.reject(&(&1.name in to_skip)) |> build_changes(change_tracking_mode, changeset) input = Enum.reduce(belongs_to_actors, input, 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 primary_key = Map.get(actor, hd(Ash.Resource.Info.primary_key(actor.__struct__))) source_attribute = Map.get(relationship, :source_attribute) Map.put(input, source_attribute, primary_key) else _ -> input end end) |> Map.merge(%{ version_source_id: Map.get(result, hd(Ash.Resource.Info.primary_key(changeset.resource))), version_action_type: changeset.action.type, version_action_name: changeset.action.name, changes: changes }) {_, notifications} = version_changeset |> Ash.Changeset.for_create(:create, input, tenant: changeset.tenant, authorize?: false, actor: actor, domain: changeset.domain, skip_unknown_inputs: Map.keys(input) ) |> Ash.Changeset.force_change_attributes(Map.take(private, version_resource_attributes)) |> Ash.create!(return_notifications?: true) notifications end defp build_inputs(changeset, %{public?: true} = attribute, {input, private}) do { Map.put( input, attribute.name, Ash.Changeset.get_attribute(changeset, attribute.name) ), private } end defp build_inputs(changeset, attribute, {input, private}) do {input, Map.put( private, attribute.name, Ash.Changeset.get_attribute(changeset, attribute.name) )} end defp build_changes(attributes, :changes_only, changeset) do AshPaperTrail.ChangeBuilders.ChangesOnly.build_changes(attributes, changeset) end defp build_changes(attributes, :snapshot, changeset) do AshPaperTrail.ChangeBuilders.Snapshot.build_changes(attributes, changeset) end defp build_changes(attributes, :full_diff, changeset) do AshPaperTrail.ChangeBuilders.FullDiff.build_changes(attributes, changeset) end end