Skip to content

Commit

Permalink
can track a lifecycle of an entity without associations
Browse files Browse the repository at this point in the history
repo functions that can track now:

 * update/2
 * create/2
 * delete/2
 * !update/2
 * !create/2
 * !delete/2
  • Loading branch information
narrowtux committed Oct 19, 2017
1 parent 6b0a3d7 commit 75d64a0
Show file tree
Hide file tree
Showing 11 changed files with 203 additions and 14 deletions.
14 changes: 12 additions & 2 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
use Mix.Config

config :ex_audit, ecto_repos: [ExAudit.Test.Repo]
config :ex_audit,
ecto_repos: [ExAudit.Test.Repo],
version_schema: ExAudit.Test.Version,
tracked_schemas: [
ExAudit.Test.User,
ExAudit.Test.BlogPost,
ExAudit.Test.BlogPost.Section,
ExAudit.Test.Comment
]

config :ex_audit, ExAudit.Test.Repo,
adapter: Ecto.Adapters.Postgres,
username: "postgres",
password: "postgres",
database: "ex_audit_test",
hostname: "localhost",
pool_size: 10
pool_size: 10

import_config "#{Mix.env}.exs"
10 changes: 10 additions & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use Mix.Config

config :ex_audit, ExAudit.Test.Repo,
adapter: Ecto.Adapters.Postgres,
pool: Ecto.Adapters.SQL.Sandbox,
username: "postgres",
password: "postgres",
database: "ex_audit_test",
hostname: "localhost",
pool_size: 10
6 changes: 3 additions & 3 deletions example/blog_post.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ defmodule ExAudit.Test.BlogPost do
schema "blog_post" do
field :title, :string

has_one :author, Test.User
embeds_many :sections, Test.BlogPost.Section
belongs_to :author, ExAudit.Test.User
embeds_many :sections, ExAudit.Test.BlogPost.Section

has_many :comments, Test.Comment
has_many :comments, ExAudit.Test.Comment

timestamps(type: :utc_datetime)
end
Expand Down
4 changes: 3 additions & 1 deletion example/comment.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ defmodule ExAudit.Test.Comment do
import Ecto.Changeset

schema "blog_post" do
has_one :author, Test.User
belongs_to :author, ExAudit.Test.User
field :body, :string

belongs_to :blog_post, ExAudit.Test.BlogPost

timestamps(type: :utc_datetime)
end

Expand Down
2 changes: 1 addition & 1 deletion example/version.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule ExAudit.Test.Version do
schema "versions" do
field :patch, ExAudit.Type.Patch
field :entity_id, :integer
field :entity_schema, :string
field :entity_schema, ExAudit.Type.Schema
field :action, ExAudit.Type.Action
field :recorded_at, :utc_datetime

Expand Down
48 changes: 42 additions & 6 deletions lib/repo/schema.ex
Original file line number Diff line number Diff line change
@@ -1,37 +1,73 @@
defmodule ExAudit.Schema do
def insert_all(module, adapter, schema_or_source, entries, opts) do
# TODO!
Ecto.Repo.Schema.insert_all(module, adapter, schema_or_source, entries, opts)
end

def insert(module, adapter, struct, opts) do
Ecto.Repo.Schema.insert(module, adapter, struct, opts)
result = Ecto.Repo.Schema.insert(module, adapter, struct, opts)

case result do
{:ok, resulting_struct} ->
ExAudit.Tracking.track_change(module, adapter, :created, struct, resulting_struct, opts)
_ ->
:ok
end

result
end

def update(module, adapter, struct, opts) do
Ecto.Repo.Schema.update(module, adapter, struct, opts)
result = Ecto.Repo.Schema.update(module, adapter, struct, opts)

case result do
{:ok, resulting_struct} ->
ExAudit.Tracking.track_change(module, adapter, :updated, struct, resulting_struct, opts)
_ ->
:ok
end

result
end

def insert_or_update(module, adapter, changeset, opts) do
# TODO!
Ecto.Repo.Schema.insert_or_update(module, adapter, changeset, opts)
end

def delete(module, adapter, struct, opts) do
Ecto.Repo.Schema.delete(module, adapter, struct, opts)
result = Ecto.Repo.Schema.delete(module, adapter, struct, opts)

case result do
{:ok, resulting_struct} ->
ExAudit.Tracking.track_change(module, adapter, :deleted, struct, resulting_struct, opts)
_ ->
:ok
end

result
end

def insert!(module, adapter, struct, opts) do
Ecto.Repo.Schema.insert!(module, adapter, struct, opts)
result = Ecto.Repo.Schema.insert!(module, adapter, struct, opts)
ExAudit.Tracking.track_change(module, adapter, :created, struct, result, opts)
result
end

def update!(module, adapter, struct, opts) do
Ecto.Repo.Schema.update!(module, adapter, struct, opts)
result = Ecto.Repo.Schema.update!(module, adapter, struct, opts)
ExAudit.Tracking.track_change(module, adapter, :updated, struct, result, opts)
result
end

def insert_or_update!(module, adapter, changeset, opts) do
# TODO
Ecto.Repo.Schema.insert_or_update!(module, adapter, changeset, opts)
end

def delete!(module, adapter, struct, opts) do
Ecto.Repo.Schema.delete!(module, adapter, struct, opts)
result = Ecto.Repo.Schema.delete!(module, adapter, struct, opts)
ExAudit.Tracking.track_change(module, adapter, :deleted, struct, result, opts)
result
end
end
20 changes: 20 additions & 0 deletions lib/repo/schema_type.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
defmodule ExAudit.Type.Schema do
@behaviour Ecto.Type

@schemas Application.get_env(:ex_audit, :tracked_schemas)

for schema <- @schemas do
def cast(unquote(schema)), do: {:ok, unquote(schema)}
def cast(unquote(schema.__schema__(:source))), do: {:ok, unquote(schema)}

def load(unquote(schema.__schema__(:source))), do: {:ok, unquote(schema)}

def dump(unquote(schema)), do: {:ok, unquote(schema.__schema__(:source))}
end

def cast(_), do: :error
def load(_), do: :error
def dump(_), do: :error

def type, do: :string
end
64 changes: 64 additions & 0 deletions lib/tracking/tracking.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
defmodule ExAudit.Tracking do
@version_schema Application.get_env(:ex_audit, :version_schema)
@ignored_fields [:__meta__, :__struct__]

def find_changes(action, struct_or_changeset, resulting_struct) do
old = case {action, struct_or_changeset} do
{:created, _} -> %{}
{_, %Ecto.Changeset{data: struct}} -> struct
{_, %{} = struct} -> struct
{_, nil} -> %{}
end

new = case action do
x when x in [:updated, :created] ->
resulting_struct
:deleted -> %{}
end

compare_versions(action, old, new)
end

def compare_versions(guessed_action, old, new) do
schema = Map.get(old, :__struct__, Map.get(new, :__struct__))

assocs = schema.__schema__(:associations)

ignored_fields = @ignored_fields ++ assocs

patch = ExAudit.Diff.diff(Map.drop(old, ignored_fields), Map.drop(new, ignored_fields))

guessed_action = guessed_action || guess_action(old, new)

params = %{
entity_id: Map.get(old, :id) || Map.get(new, :id),
entity_schema: schema,
patch: patch,
action: guessed_action
}

[params]
end

def guess_action(%{id: id}, %{id: id}) when not is_nil(id), do: :updated
def guess_action(%{}, %{id: id}) when not is_nil(id), do: :created
def guess_action(%{id: id}, nil) when not is_nil(id), do: :deleted

def track_change(module, adapter, action, changeset, resulting_struct, opts) do
changes = find_changes(action, changeset, resulting_struct)

now = DateTime.utc_now
custom_fields = Keyword.get(opts, :ex_audit_custom, []) |> Enum.into(%{})

changes = Enum.map(changes, fn change ->
change = Map.put(change, :recorded_at, now)
Map.merge(change, custom_fields)
end)

case changes do
[] -> :ok
_ ->
Ecto.Repo.Schema.insert_all(module, adapter, @version_schema, changes, opts)
end
end
end
3 changes: 2 additions & 1 deletion priv/repo/migrations/20171018124842_initial_tables.exs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ defmodule ExAudit.Test.Repo.Migrations.InitialTables do
create table(:comments) do
add :author_id, references(:users, on_update: :update_all, on_delete: :delete_all)
add :body, :text
add :blog_post_id, references(:blog_post, on_update: :update_all, on_delete: :delete_all)

timestamps(type: :utc_datetime)
end
Expand All @@ -38,7 +39,7 @@ defmodule ExAudit.Test.Repo.Migrations.InitialTables do
add :action, :string

# when has this happened
add :recorded_at, :datetime
add :recorded_at, :utc_datetime

# optional fields that you can define yourself
# for example, it's a good idea to track who did the change
Expand Down
43 changes: 43 additions & 0 deletions test/ex_audit_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,48 @@ defmodule ExAuditTest do
use ExUnit.Case
doctest ExAudit

import Ecto.Query

alias ExAudit.Test.{Repo, User, Version}

test "should document lifecycle of an entity" do
params = %{
name: "Moritz Schmale",
email: "foo@bar.com"
}

changeset = User.changeset(%User{}, params)

{:ok, user} = Repo.insert(changeset)

assert params.name == user.name
assert params.email == user.email

version = Repo.one(from v in Version,
where: v.entity_id == ^user.id,
where: v.action == ^:created)

assert version.action == :created
assert version.patch.name == {:added, params.name}
assert version.patch.email == {:added, params.email}

params = %{
email: "real@email.com"
}
changeset = User.changeset(user, params)

{:ok, user} = Repo.update(changeset)
version = Repo.one(from v in Version,
where: v.entity_id == ^user.id,
where: v.action == ^:updated)

assert version.patch.email == {:changed, {:primitive_change, changeset.data.email, params.email}}

{:ok, user} = Repo.delete(user)
version = Repo.one(from v in Version,
where: v.entity_id == ^user.id,
where: v.action == ^:deleted)

assert not is_nil(version)
end
end
3 changes: 3 additions & 0 deletions test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
ExAudit.Test.Repo.start_link()
Ecto.Adapters.SQL.Sandbox.mode(ExAudit.Test.Repo, :auto)

ExUnit.start()

0 comments on commit 75d64a0

Please sign in to comment.