defmodule Spur do @moduledoc """ Simple activity tracking for Ecto-backed Elixir apps. """ alias Spur.{Activity, Trackable} alias Ecto.{Multi} @doc """ Inserts the given Struct and an `Spur.Activity` record. ## Examples iex> result = Spur.insert(%SpurTest.TrackableStruct{user: %SpurTest.AppUser{}}) ...> with {:ok, %SpurTest.TrackableStruct{}} <- result, do: :ok :ok iex> result = Spur.insert(%SpurTest.TrackableStruct{}) ...> with {:error, %Ecto.Changeset{}} <- result, do: :error :error iex> Spur.insert(%SpurTest.TrackableStruct{user: %SpurTest.AppUser{}}, %{object: "special-user"}) ...> Spur.Activity |> where(object: "special-user") |> Repo.exists? true iex> Spur.insert(%SpurTest.TrackableStruct{user: %SpurTest.AppUser{name: "Buddy"}}, fn trackable, params -> %{actor: trackable.user.name} end) ...> Spur.Activity |> where(actor: "Buddy") |> Repo.exists? true """ def insert(trackable, func_or_params \\ %{}), do: track_with_callback(:insert, trackable, func_or_params) @doc """ Updates the given Struct and an `Spur.Activity` record. ## Examples iex> trackable = %SpurTest.TrackableStruct{user: %SpurTest.AppUser{}} |> SpurTest.Repo.insert! |> Ecto.Changeset.change ...> result = Spur.update(trackable) ...> with {:ok, %SpurTest.TrackableStruct{}} <- result, do: :ok :ok """ def update(trackable, func_or_params \\ %{}), do: track_with_callback(:update, trackable, func_or_params) @doc """ Deletes the given Struct and an `Spur.Activity` record. ## Examples iex> trackable = %SpurTest.TrackableStruct{user: %SpurTest.AppUser{}} |> SpurTest.Repo.insert! ...> result = Spur.delete(trackable) ...> with {:ok, %SpurTest.TrackableStruct{}} <- result, do: :ok :ok """ def delete(trackable, func_or_params \\ %{}), do: track_with_callback(:delete, trackable, func_or_params) defp track_with_callback(action, trackable, func_or_params) do Kernel.apply(Multi, action, [Multi.new, :trackable, trackable]) |> Multi.insert(:activity, fn %{trackable: trackable} -> changeset_for_action(trackable, action, func_or_params) end) |> Multi.run(:audience, fn _repo, %{trackable: trackable, activity: activity} -> associate_with_audience(audience_module(), activity, trackable) end) |> repo().transaction |> transaction_or_result(Application.fetch_env!(:spur, :expose_transactions)) end defp associate_with_audience(nil, _, _), do: {:ok, []} defp associate_with_audience(module, activity, trackable) do audience_assoc_reflection = module.__schema__(:association, audience_assoc_name()) [{audience_fkey_name, audience_pkey_name}, {activity_fkey_name, activity_pkey_name}] = audience_assoc_reflection.join_keys audience_assocs = Trackable.audience(trackable) |> query_to_list |> Enum.map(fn audience -> %{audience_fkey_name => extract_audience_pkey(audience, audience_pkey_name), activity_fkey_name => activity |> Map.get(activity_pkey_name)} end) {n, nil} = repo().insert_all(audience_assoc_reflection.join_through, audience_assocs) {:ok, n} end defp extract_audience_pkey(%{} = attrs, pkey_name), do: attrs |> Map.get(pkey_name) defp extract_audience_pkey(id, _pkey_name), do: id defp changeset_for_action(trackable, action, func_or_params) do %Activity{ action: Atom.to_string(action), actor: Trackable.actor(trackable), object: Trackable.object(trackable), target: Trackable.target(trackable) } |> Activity.changeset(func_or_params |> extract_params(trackable)) end defp extract_params(%{} = params, _obj), do: params defp extract_params(func, trackable) when is_function(func), do: func.(trackable, %{}) defp repo do Application.fetch_env!(:spur, :repo) end defp audience_module do case Application.fetch_env(:spur, :audience_module) do :error -> nil {:ok, module} -> module end end defp audience_assoc_name do Application.fetch_env!(:spur, :audience_assoc_name) end defp query_to_list(%Ecto.Query{} = query), do: query |> repo().all defp query_to_list([] = list), do: list defp transaction_or_result(transaction, _expose_transactions = true), do: transaction defp transaction_or_result(transaction, _expose_transactions = false) do case transaction do {:ok, %{trackable: trackable}} -> {:ok, trackable} {:error, _change, changeset, _multi} -> {:error, changeset} end end end