diff --git a/config/config.exs b/config/config.exs index d26f12bd..3024fce1 100644 --- a/config/config.exs +++ b/config/config.exs @@ -28,4 +28,5 @@ use Mix.Config # Configuration from the imported file will override the ones defined # here (which is why it is important to import them last). # -# import_config "#{Mix.env}.exs" + +import_config "#{Mix.env}.exs" diff --git a/config/test.exs b/config/test.exs new file mode 100644 index 00000000..86a56ec4 --- /dev/null +++ b/config/test.exs @@ -0,0 +1,11 @@ +use Mix.Config + +config :paper_trail, ecto_repos: [Repo] + +config :paper_trail, Repo, + adapter: Ecto.Adapters.Postgres, + username: "postgres", + password: "postgres", + database: "paper_trail_test", + hostname: "localhost", + poolsize: 10 diff --git a/example/lib/example.ex b/example/lib/example.ex index a25e75fa..0d0c8e8f 100644 --- a/example/lib/example.ex +++ b/example/lib/example.ex @@ -5,11 +5,7 @@ defmodule Example do import Supervisor.Spec, warn: false children = [ - # Start the endpoint when the application starts supervisor(Repo, []), - # Start the Ecto repository - # Here you could define other workers and supervisors as children - # worker(Caplair.Worker, [arg1, arg2, arg3]), ] # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html diff --git a/example/mix.lock b/example/mix.lock index 640b1e49..6cce6261 100644 --- a/example/mix.lock +++ b/example/mix.lock @@ -2,7 +2,7 @@ "db_connection": {:hex, :db_connection, "1.0.0-rc.3", "d9ceb670fe300271140af46d357b669983cd16bc0d01206d7d3222dde56cf038", [:mix], [{:sbroker, "~> 1.0.0-beta.3", [hex: :sbroker, optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: true]}, {:connection, "~> 1.0.2", [hex: :connection, optional: false]}]}, "decimal": {:hex, :decimal, "1.1.2", "79a769d4657b2d537b51ef3c02d29ab7141d2b486b516c109642d453ee08e00c", [:mix], []}, "ecto": {:hex, :ecto, "2.0.2", "b02331c1f20bbe944dbd33c8ecd8f1ccffecc02e344c4471a891baf3a25f5406", [:mix], [{:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: true]}, {:sbroker, "~> 1.0-beta", [hex: :sbroker, optional: true]}, {:mariaex, "~> 0.7.7", [hex: :mariaex, optional: true]}, {:postgrex, "~> 0.11.2", [hex: :postgrex, optional: true]}, {:db_connection, "~> 1.0-rc.2", [hex: :db_connection, optional: true]}, {:decimal, "~> 1.0", [hex: :decimal, optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: false]}]}, - "paper_trail": {:hex, :paper_trail, "0.0.9", "4e9342ee2bae6df9c80a2a3b5619e8df2a0113a545e5671d964ffa5dc9c37b69", [:mix], [{:poison, "2.1.0", [hex: :poison, optional: false]}, {:ecto, "~> 2.0.2", [hex: :ecto, optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, optional: false]}]}, + "paper_trail": {:hex, :paper_trail, "0.1.0", "0ebc0159795bb8d99b1633a552312a947b80f88f48d338ff02f1ba445fd3605e", [:mix], [{:poison, "2.1.0", [hex: :poison, optional: false]}, {:ecto, "~> 2.0.2", [hex: :ecto, optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, optional: false]}]}, "poison": {:hex, :poison, "2.1.0", "f583218ced822675e484648fa26c933d621373f01c6c76bd00005d7bd4b82e27", [:mix], []}, "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], []}, "postgrex": {:hex, :postgrex, "0.11.2", "139755c1359d3c5c6d6e8b1ea72556d39e2746f61c6ddfb442813c91f53487e8", [:mix], [{:connection, "~> 1.0", [hex: :connection, optional: false]}, {:db_connection, "~> 1.0-rc", [hex: :db_connection, optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, optional: false]}]}} diff --git a/example/test/company_test.exs b/example/test/company_test.exs index 49eb77c3..3da9490a 100644 --- a/example/test/company_test.exs +++ b/example/test/company_test.exs @@ -4,8 +4,6 @@ defmodule CompanyTest do doctest Company - # maybe test meta tag insertion and relationships - setup_all do Repo.delete_all(Person) Repo.delete_all(Company) diff --git a/lib/paper_trail.ex b/lib/paper_trail.ex index 5dd44537..f33c79e8 100644 --- a/lib/paper_trail.ex +++ b/lib/paper_trail.ex @@ -1,47 +1,10 @@ defmodule PaperTrail do - alias Ecto.Multi import Ecto.Query + import PaperTrail.VersionQueries + + alias Ecto.Multi alias PaperTrail.Version - @doc """ - Gets all the versions of a record given a module and its id - """ - def get_versions(model, id) do - item_type = model |> Module.split |> List.last - version_query(item_type, id) |> Repo.all - end - - @doc """ - Gets all the versions of a record - """ - def get_versions(record) do - item_type = record.__struct__ |> Module.split |> List.last - version_query(item_type, record.id) |> Repo.all - end - - @doc """ - Gets the last version of a record given its module reference and its id - """ - def get_version(model, id) do - item_type = Module.split(model) |> List.last - last(version_query(item_type, id)) |> Repo.one - end - - @doc """ - Gets the last version of a record - """ - def get_version(record) do - item_type = record.__struct__ |> Module.split |> List.last - last(version_query(item_type, record.id)) |> Repo.one - end - - defp version_query(item_type, id) do - from v in Version, - where: v.item_type == ^item_type and v.item_id == ^id - end - - # changeset = Model.changeset(Ecto.Repo.get(Model, id), params) - @doc """ Inserts a record to the database with a related version insertion in one transaction """ @@ -55,24 +18,6 @@ defmodule PaperTrail do |> Repo.transaction end - defp make_version_struct(%{event: "create"}, model, meta) do - %Version{ - event: "create", - item_type: model.__struct__ |> Module.split |> List.last, - item_id: model.id, - item_changes: filter_item_changes(model), - meta: meta - } - end - - defp filter_item_changes(model) do - relationships = model.__struct__.__schema__(:associations) - - Map.drop(model, [:__struct__, :__meta__] ++ relationships) - end - - # might make the changeset version - @doc """ Updates a record from the database with a related version insertion in one transaction """ @@ -86,16 +31,6 @@ defmodule PaperTrail do |> Repo.transaction end - defp make_version_struct(%{event: "update"}, changeset, meta) do - %Version{ - event: "update", - item_type: changeset.data.__struct__ |> Module.split |> List.last, - item_id: changeset.data.id, - item_changes: changeset.changes, - meta: meta - } - end - @doc """ Deletes a record from the database with a related version insertion in one transaction """ @@ -109,6 +44,26 @@ defmodule PaperTrail do |> Repo.transaction end + defp make_version_struct(%{event: "create"}, model, meta) do + %Version{ + event: "create", + item_type: model.__struct__ |> Module.split |> List.last, + item_id: model.id, + item_changes: filter_item_changes(model), + meta: meta + } + end + + defp make_version_struct(%{event: "update"}, changeset, meta) do + %Version{ + event: "update", + item_type: changeset.data.__struct__ |> Module.split |> List.last, + item_id: changeset.data.id, + item_changes: changeset.changes, + meta: meta + } + end + defp make_version_struct(%{event: "destroy"}, model, meta) do %Version{ event: "destroy", @@ -118,4 +73,10 @@ defmodule PaperTrail do meta: meta } end + + defp filter_item_changes(model) do + relationships = model.__struct__.__schema__(:associations) + + Map.drop(model, [:__struct__, :__meta__] ++ relationships) + end end diff --git a/lib/paper_trail/migration.ex b/lib/paper_trail/migration.ex new file mode 100644 index 00000000..d90ecabe --- /dev/null +++ b/lib/paper_trail/migration.ex @@ -0,0 +1,4 @@ +defmodule PaperTrail.Migration do + # TODO: this module will handle column changes in database schemas. It will query legacy column names + # and change the legacy column names to new column names in every :item_changes field of a PaperTrail.Version +end diff --git a/lib/paper_trail/version_queries.ex b/lib/paper_trail/version_queries.ex new file mode 100644 index 00000000..0aa75de2 --- /dev/null +++ b/lib/paper_trail/version_queries.ex @@ -0,0 +1,42 @@ +defmodule PaperTrail.VersionQueries do + import Ecto.Query + alias PaperTrail.Version + + + @doc """ + Gets all the versions of a record given a module and its id + """ + def get_versions(model, id) do + item_type = model |> Module.split |> List.last + version_query(item_type, id) |> Repo.all + end + + @doc """ + Gets all the versions of a record + """ + def get_versions(record) do + item_type = record.__struct__ |> Module.split |> List.last + version_query(item_type, record.id) |> Repo.all + end + + @doc """ + Gets the last version of a record given its module reference and its id + """ + def get_version(model, id) do + item_type = Module.split(model) |> List.last + last(version_query(item_type, id)) |> Repo.one + end + + @doc """ + Gets the last version of a record + """ + def get_version(record) do + item_type = record.__struct__ |> Module.split |> List.last + last(version_query(item_type, record.id)) |> Repo.one + end + + defp version_query(item_type, id) do + from v in Version, + where: v.item_type == ^item_type and v.item_id == ^id + end +end diff --git a/lib/version.ex b/lib/version.ex index 093eb8fc..1860407b 100644 --- a/lib/version.ex +++ b/lib/version.ex @@ -5,8 +5,6 @@ defmodule PaperTrail.Version do import Ecto.Changeset import Ecto.Query - # how to record column changes in migration ? - schema "versions" do field :event, :string field :item_type, :string diff --git a/mix.exs b/mix.exs index f24d14b0..0980ff1e 100644 --- a/mix.exs +++ b/mix.exs @@ -16,7 +16,9 @@ defmodule PaperTrail.Mixfile do # # Type "mix help compile.app" for more information def application do - [] + [ + applications: [:logger, :postgrex, :ecto] + ] end defp deps do diff --git a/priv/repo/migrations/20160619190937_add_companies.exs b/priv/repo/migrations/20160619190937_add_companies.exs index e2f0549f..76112671 100644 --- a/priv/repo/migrations/20160619190937_add_companies.exs +++ b/priv/repo/migrations/20160619190937_add_companies.exs @@ -1,10 +1,10 @@ -defmodule Repo.Migrations.AddCompanies do +defmodule Repo.Migrations.CreateCompanies do use Ecto.Migration def change do create table(:companies) do add :name, :string - add :is_active, :string + add :is_active, :boolean add :website, :string add :city, :string add :address, :string diff --git a/priv/repo/migrations/20160619190938_add_people.exs b/priv/repo/migrations/20160619190938_add_people.exs index df486fb1..b4366260 100644 --- a/priv/repo/migrations/20160619190938_add_people.exs +++ b/priv/repo/migrations/20160619190938_add_people.exs @@ -1,4 +1,4 @@ -defmodule Repo.Migrations.AddPeople do +defmodule Repo.Migrations.CreatePeople do use Ecto.Migration def change do @@ -9,7 +9,11 @@ defmodule Repo.Migrations.AddPeople do add :gender, :boolean add :birthdate, :date + add :company_id, references(:companies), null: false + timestamps end + + create index(:people, [:company_id]) end end diff --git a/test/paper_trail_test.exs b/test/paper_trail_test.exs index 565eb4c7..177b7b0d 100644 --- a/test/paper_trail_test.exs +++ b/test/paper_trail_test.exs @@ -1,63 +1,162 @@ defmodule PaperTrailTest do use ExUnit.Case + import Ecto.Query + doctest PaperTrail - defmodule Company do - use Ecto.Schema - - import Ecto - import Ecto.Changeset - import Ecto.Query - - schema "companies" do - field :name, :string - field :is_active, :boolean - field :website, :string - field :city, :string - field :address, :string - field :facebook, :string - field :twitter, :string - field :founded_in, :string - - timestamps - end - - @required_fields ~w() - @optional_fields ~w() - - def changeset(model, params \\ :empty) do - model - |> cast(params, @required_fields, @optional_fields) - end + setup_all do + Repo.delete_all(Person) + Repo.delete_all(Company) + Repo.delete_all(PaperTrail.Version) + :ok end - defmodule Person do - use Ecto.Schema + test "creating a company creates a company version with correct attributes" do + new_company = Company.changeset(%Company{}, %{ + name: "Acme LLC", is_active: true, city: "Greenwich", people: [] + }) + + {:ok, result} = PaperTrail.insert(new_company) + + company_count = Repo.all( + from company in Company, + select: count(company.id) + ) - import Ecto - import Ecto.Changeset - import Ecto.Query + company = result[:model] |> Map.drop([:__meta__, :__struct__, :inserted_at, :updated_at, :id]) - schema "people" do - field :first_name, :string - field :last_name, :string - field :visit_count, :integer - field :gender, :boolean - field :birthdate, Ecto.Date + version_count = Repo.all( + from version in PaperTrail.Version, + select: count(version.id) + ) - timestamps - end + version = result[:version] |> Map.drop([:__meta__, :__struct__, :inserted_at]) - @required_fields ~w() - @optional_fields ~w() + assert company_count == [1] + assert version_count == [1] - def changeset(model, params \\ :empty) do - model - |> cast(params, @required_fields, @optional_fields) - end + assert company == %{ + name: "Acme LLC", + is_active: true, + city: "Greenwich", + website: nil, + address: nil, + facebook: nil, + twitter: nil, + founded_in: nil, + people: [] + } + + assert Map.drop(version, [:id]) == %{ + event: "create", + item_type: "Company", + item_id: Repo.one(first(Company, :id)).id, + item_changes: Map.drop(result[:model], [:__meta__, :__struct__, :people]), + meta: nil + } end - test "" do - assert 1 + 1 == 2 + test "updating a company creates a company version with correct item_changes" do + old_company = first(Company, :id) |> preload(:people) |> Repo.one + new_company = Company.changeset(old_company, %{ + city: "Hong Kong", + website: "http://www.acme.com", + facebook: "acme.llc" + }) + + {:ok, result} = PaperTrail.update(new_company) + + company_count = Repo.all( + from company in Company, + select: count(company.id) + ) + + company = result[:model] |> Map.drop([:__meta__, :__struct__, :inserted_at, :updated_at, :id]) + + version_count = Repo.all( + from version in PaperTrail.Version, + select: count(version.id) + ) + + version = result[:version] |> Map.drop([:__meta__, :__struct__, :inserted_at]) + + assert company_count == [1] + assert version_count == [2] + + assert company == %{ + name: "Acme LLC", + is_active: true, + city: "Hong Kong", + website: "http://www.acme.com", + address: nil, + facebook: "acme.llc", + twitter: nil, + founded_in: nil, + people: [] + } + + assert Map.drop(version, [:id]) == %{ + event: "update", + item_type: "Company", + item_id: Repo.one(first(Company, :id)).id, + item_changes: %{city: "Hong Kong", website: "http://www.acme.com", facebook: "acme.llc"}, + meta: nil + } end + + test "deleting a company creates a company version with correct attributes" do + company = first(Company, :id) |> preload(:people) |> Repo.one + + {:ok, result} = PaperTrail.delete(company) + + company_count = Repo.all( + from company in Company, + select: count(company.id) + ) + + company_ref = result[:model] |> Map.drop([:__meta__, :__struct__, :inserted_at, :updated_at, :id]) + + version_count = Repo.all( + from version in PaperTrail.Version, + select: count(version.id) + ) + + version = result[:version] |> Map.drop([:__meta__, :__struct__, :inserted_at]) + + assert company_count == [0] + assert version_count == [3] + + assert company_ref == %{ + name: "Acme LLC", + is_active: true, + city: "Hong Kong", + website: "http://www.acme.com", + address: nil, + facebook: "acme.llc", + twitter: nil, + founded_in: nil, + people: [] + } + + assert Map.drop(version, [:id]) == %{ + event: "destroy", + item_type: "Company", + item_id: company.id, + item_changes: %{ + id: company.id, + inserted_at: company.inserted_at, + updated_at: company.updated_at, + name: "Acme LLC", + is_active: true, + website: "http://www.acme.com", + city: "Hong Kong", + address: nil, + facebook: "acme.llc", + twitter: nil, + founded_in: nil + }, + meta: nil + } + end + end diff --git a/test/test_helper.exs b/test/test_helper.exs index 869559e7..29292199 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1 +1,70 @@ +defmodule Repo do + use Ecto.Repo, otp_app: :paper_trail +end + +Mix.Task.run "ecto.create", ~w(-r Repo) +Mix.Task.run "ecto.migrate", ~w(-r Repo) + +defmodule Company do + use Ecto.Schema + + import Ecto + import Ecto.Changeset + import Ecto.Query + + schema "companies" do + field :name, :string + field :is_active, :boolean + field :website, :string + field :city, :string + field :address, :string + field :facebook, :string + field :twitter, :string + field :founded_in, :string + + has_many :people, Person + + timestamps + end + + @optional_fields ~w(name is_active website city address facebook twitter founded_in) + + def changeset(model, params \\ %{}) do + model + |> cast(params, @optional_fields) + |> cast_assoc(:people, required: false) + end +end + +defmodule Person do + use Ecto.Schema + + import Ecto + import Ecto.Changeset + import Ecto.Query + + schema "people" do + field :first_name, :string + field :last_name, :string + field :visit_count, :integer + field :gender, :boolean + field :birthdate, Ecto.Date + + belongs_to :company, Company + + timestamps + end + + @optional_fields ~w(first_name last_name visit_count gender birthdate company_id) + + def changeset(model, params \\ %{}) do + model + |> cast(params, @optional_fields) + |> foreign_key_constraint(:company_id) + end +end + +Repo.start_link + +ExUnit.configure seed: 0 ExUnit.start()