diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d557ad3 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,5 @@ +# Used by "mix format" +[ + line_length: 110, + inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] +] \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6f7cd34..18ba9c9 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ GitHub.sublime-settings .tags_sorted_by_file .vagrant .DS_Store +.elixir_ls # Ignore released binaries .deliver diff --git a/.travis.yml b/.travis.yml index a8191ff..317c3c5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,5 +29,3 @@ script: - mix coveralls.travis --exclude pending # Run static code analysis - mix credo --strict - # Check code style - - mix dogma diff --git a/README.md b/README.md index 89905df..9d1a718 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # EctoTrail + [![Hex.pm Downloads](https://img.shields.io/hexpm/dw/ecto_trail.svg?maxAge=3600)](https://hex.pm/packages/ecto_trail) [![Latest Version](https://img.shields.io/hexpm/v/ecto_trail.svg?maxAge=3600)](https://hex.pm/packages/ecto_trail) [![License](https://img.shields.io/hexpm/l/ecto_trail.svg?maxAge=3600)](https://hex.pm/packages/ecto_trail) [![Build Status](https://travis-ci.org/Nebo15/ecto_trail.svg?branch=master)](https://travis-ci.org/Nebo15/ecto_trail) [![Coverage Status](https://coveralls.io/repos/github/Nebo15/ecto_trail/badge.svg?branch=master)](https://coveralls.io/github/Nebo15/ecto_trail?branch=master) [![Ebert](https://ebertapp.io/github/Nebo15/ecto_trail.svg)](https://ebertapp.io/github/Nebo15/ecto_trail) EctoTrail allows to store changeset changes into a separate `audit_log` table. @@ -7,59 +8,61 @@ EctoTrail allows to store changeset changes into a separate `audit_log` table. 1. Add `ecto_trail` to your list of dependencies in `mix.exs`: - ```elixir - def deps do - [{:ecto_trail, "~> 0.2.0"}] - end - ``` +```elixir +def deps do + [{:ecto_trail, "~> 0.2.0"}] +end +``` 2. Ensure `ecto_trail` is started before your application: - ```elixir - def application do - [extra_applications: [:ecto_trail]] - end - ``` +```elixir +def application do + [extra_applications: [:ecto_trail]] +end +``` 3. Add a migration that creates `audit_log` table to `priv/repo/migrations` folder: - ```elixir - defmodule EctoTrail.TestRepo.Migrations.CreateAuditLogTable do - @moduledoc false - use Ecto.Migration - - def change do - create table(:audit_log, primary_key: false) do - add :id, :uuid, primary_key: true - add :actor_id, :string, null: false - add :resource, :string, null: false - add :resource_id, :string, null: false - add :changeset, :map, null: false - - timestamps([type: :utc_datetime, updated_at: false]) - end +```elixir +defmodule EctoTrail.TestRepo.Migrations.CreateAuditLogTable do + @moduledoc false + use Ecto.Migration + + @table_name String.to_atom(Application.fetch_env!(:ecto_trail, :table_name)) + + def change(table_name \\ @table_name) do + EctoTrailChangeEnum.create_type + create table(table_name) do + add :actor_id, :string, null: false + add :resource, :string, null: false + add :resource_id, :string, null: false + add :changeset, :map, null: false + add(:change_type, :change) + + timestamps([type: :utc_datetime, updated_at: false]) end end - ``` +end +``` 4. Use `EctoTrail` in your repo: - ```elixir - defmodule MyApp.Repo do - use Ecto.Repo, otp_app: :my_app - use EctoTrail - end - ``` - +```elixir +defmodule MyApp.Repo do + use Ecto.Repo, otp_app: :my_app + use EctoTrail +end +``` + 5. Configure table name which is used to store audit log (in `config.ex`): - ```elixir - config :ecto_trail, table_name: "audit_log" - ``` +```elixir +config :ecto_trail, table_name: "audit_log", redacted_fields: [:password, :token] +``` 6. Use logging functions instead of defaults. See `EctoTrail` module docs. ## Docs The docs can be found at [https://hexdocs.pm/ecto_trail](https://hexdocs.pm/ecto_trail). - diff --git a/config/.credo.exs b/config/.credo.exs index 4b885f5..9f22aeb 100644 --- a/config/.credo.exs +++ b/config/.credo.exs @@ -8,7 +8,7 @@ checks: [ {Credo.Check.Design.TagTODO, exit_status: 0}, {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 120}, - {Credo.Check.Readability.Specs, exit_status: 0}, + {Credo.Check.Readability.Specs, exit_status: 0} ] } ] diff --git a/config/config.exs b/config/config.exs index e0e986c..8550b3a 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,4 +1,3 @@ use Mix.Config -config :ecto_trail, - table_name: "audit_log" +config :ecto_trail, table_name: "audit_log", redacted_fields: [:password] diff --git a/config/dogma.exs b/config/dogma.exs deleted file mode 100644 index 0bb5e77..0000000 --- a/config/dogma.exs +++ /dev/null @@ -1,13 +0,0 @@ -use Mix.Config -alias Dogma.Rule - -config :dogma, - rule_set: Dogma.RuleSet.All, - exclude: [ - ~r(\Adeps/), - ], - override: [ - %Rule.LineLength{ max_length: 120 }, - %Rule.InfixOperatorPadding{ enabled: false }, - %Rule.FunctionArity{ max: 5 }, - ] diff --git a/lib/ecto_trail/changelog.ex b/lib/ecto_trail/changelog.ex index b32970a..0f5ec67 100644 --- a/lib/ecto_trail/changelog.ex +++ b/lib/ecto_trail/changelog.ex @@ -4,13 +4,13 @@ defmodule EctoTrail.Changelog do """ use Ecto.Schema - @primary_key {:id, :binary_id, autogenerate: true} schema Application.fetch_env!(:ecto_trail, :table_name) do - field :actor_id, :string - field :resource, :string - field :resource_id, :string - field :changeset, :map + field(:actor_id, :string) + field(:resource, :string) + field(:resource_id, :string) + field(:changeset, :map) + field(:change_type, EctoTrailChangeEnum) - timestamps([type: :utc_datetime, updated_at: false]) + timestamps(type: :utc_datetime, updated_at: false) end end diff --git a/lib/ecto_trail/ecto_trail.ex b/lib/ecto_trail/ecto_trail.ex index f2bd5c3..961c775 100644 --- a/lib/ecto_trail/ecto_trail.ex +++ b/lib/ecto_trail/ecto_trail.ex @@ -51,15 +51,38 @@ defmodule EctoTrail do defmacro __using__(_) do quote do + @doc """ + Store changes in a `change_log` table. + """ + @spec log( + struct_or_changeset :: Ecto.Schema.t() | Ecto.Changeset.t(), + changes :: Map.t(), + actor_id :: String.T + ) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} + def log(struct_or_changeset, changes, actor_id), + do: EctoTrail.log(__MODULE__, struct_or_changeset, changes, actor_id) + + @doc """ + Store bulk changes in a `change_log` table. + """ + @spec log_bulk( + structs :: list(Ecto.Schema.t()), + changes :: list(Map.t()), + actor_id :: String.T + ) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} + def log_bulk(structs, changes, actor_id), + do: EctoTrail.log_bulk(__MODULE__, structs, changes, actor_id) + @doc """ Call `c:Ecto.Repo.insert/2` operation and store changes in a `change_log` table. Insert arguments, return and options same as `c:Ecto.Repo.insert/2` has. """ - @spec insert_and_log(struct_or_changeset :: Ecto.Schema.t | Ecto.Changeset.t, - actor_id :: String.T, - opts :: Keyword.t) :: - {:ok, Ecto.Schema.t} | {:error, Ecto.Changeset.t} + @spec insert_and_log( + struct_or_changeset :: Ecto.Schema.t() | Ecto.Changeset.t(), + actor_id :: String.T, + opts :: Keyword.t() + ) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} def insert_and_log(struct_or_changeset, actor_id, opts \\ []), do: EctoTrail.insert_and_log(__MODULE__, struct_or_changeset, actor_id, opts) @@ -68,30 +91,94 @@ defmodule EctoTrail do Insert arguments, return and options same as `c:Ecto.Repo.update/2` has. """ - @spec update_and_log(changeset :: Ecto.Changeset.t, - actor_id :: String.T, - opts :: Keyword.t) :: - {:ok, Ecto.Schema.t} | - {:error, Ecto.Changeset.t} + @spec update_and_log( + changeset :: Ecto.Changeset.t(), + actor_id :: String.T, + opts :: Keyword.t() + ) :: + {:ok, Ecto.Schema.t()} + | {:error, Ecto.Changeset.t()} def update_and_log(changeset, actor_id, opts \\ []), do: EctoTrail.update_and_log(__MODULE__, changeset, actor_id, opts) + + @doc """ + Call `c:Ecto.Repo.upsert/2` operation and store changes in a `change_log` table. + + Insert arguments, return and options same as `c:Ecto.Repo.upsert/2` has. + """ + @spec upsert_and_log( + struct_or_changeset :: Ecto.Schema.t() | Ecto.Changeset.t(), + actor_id :: String.T, + opts :: Keyword.t() + ) :: + {:ok, Ecto.Schema.t()} + | {:error, Ecto.Changeset.t()} + def upsert_and_log(struct_or_changeset, actor_id, opts \\ []), + do: EctoTrail.upsert_and_log(__MODULE__, struct_or_changeset, actor_id, opts) + + @doc """ + Call `c:Ecto.Repo.delete/2` operation and store deleted objext in a `change_log` table. + """ + @spec delete_and_log( + struct_or_changeset :: Ecto.Schema.t() | Ecto.Changeset.t(), + actor_id :: String.T, + opts :: Keyword.t() + ) :: + {:ok, Ecto.Schema.t()} + | {:error, Ecto.Changeset.t()} + def delete_and_log(struct_or_changeset, actor_id, opts \\ []), + do: EctoTrail.delete_and_log(__MODULE__, struct_or_changeset, actor_id, opts) end end + @doc """ + Store changes in a `change_log` table. + """ + @spec log( + repo :: Ecto.Repo.t(), + struct_or_changeset :: Ecto.Schema.t() | Ecto.Changeset.t(), + changes :: Map.t(), + actor_id :: String.T + ) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} + def log(repo, struct_or_changeset, changes, actor_id) do + Multi.new() + |> Ecto.Multi.run(:operation, fn _, _ -> {:ok, struct_or_changeset} end) + |> run_logging_transaction_alone(repo, struct_or_changeset, changes, actor_id, :insert) + end + + @doc """ + Store bulk changes in a `change_log` table. + """ + @spec log_bulk( + repo :: Ecto.Repo.t(), + structs :: list(Ecto.Schema.t()), + changes :: list(Map.t()), + actor_id :: String.T + ) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} + def log_bulk(repo, structs, changes, actor_id) do + Enum.zip(structs, changes) + |> Enum.each(fn {struct, change} -> + Multi.new() + |> Ecto.Multi.run(:operation, fn _, _ -> {:ok, struct} end) + |> run_logging_transaction_alone(repo, struct, change, actor_id, :insert) + end) + end + @doc """ Call `c:Ecto.Repo.insert/2` operation and store changes in a `change_log` table. Insert arguments, return and options same as `c:Ecto.Repo.insert/2` has. """ - @spec insert_and_log(repo :: Ecto.Repo.t, - struct_or_changeset :: Ecto.Schema.t | Ecto.Changeset.t, - actor_id :: String.T, - opts :: Keyword.t) :: - {:ok, Ecto.Schema.t} | {:error, Ecto.Changeset.t} + @spec insert_and_log( + repo :: Ecto.Repo.t(), + struct_or_changeset :: Ecto.Schema.t() | Ecto.Changeset.t(), + actor_id :: String.T, + opts :: Keyword.t() + ) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} def insert_and_log(repo, struct_or_changeset, actor_id, opts \\ []) do - Multi.new + Multi.new() |> Multi.insert(:operation, struct_or_changeset, opts) - |> run_logging_transaction(repo, struct_or_changeset, actor_id) + |> run_logging_transaction(repo, struct_or_changeset, actor_id, :insert) end @doc """ @@ -99,48 +186,134 @@ defmodule EctoTrail do Insert arguments, return and options same as `c:Ecto.Repo.update/2` has. """ - @spec update_and_log(repo :: Ecto.Repo.t, - changeset :: Ecto.Changeset.t, - actor_id :: String.T, - opts :: Keyword.t) :: - {:ok, Ecto.Schema.t} | - {:error, Ecto.Changeset.t} + @spec update_and_log( + repo :: Ecto.Repo.t(), + changeset :: Ecto.Changeset.t(), + actor_id :: String.T, + opts :: Keyword.t() + ) :: + {:ok, Ecto.Schema.t()} + | {:error, Ecto.Changeset.t()} def update_and_log(repo, changeset, actor_id, opts \\ []) do - Multi.new + Multi.new() |> Multi.update(:operation, changeset, opts) - |> run_logging_transaction(repo, changeset, actor_id) + |> run_logging_transaction(repo, changeset, actor_id, :update) + end + + @doc """ + Call `c:Ecto.Repo.upsert/2` operation and store changes in a `change_log` table. + + Insert arguments, return and options same as `c:Ecto.Repo.upsert/2` has. + """ + @spec upsert_and_log( + repo :: Ecto.Repo.t(), + struct_or_changeset :: Ecto.Schema.t() | Ecto.Changeset.t(), + actor_id :: String.T, + opts :: Keyword.t() + ) :: + {:ok, Ecto.Schema.t()} + | {:error, Ecto.Changeset.t()} + def upsert_and_log(repo, struct_or_changeset, actor_id, opts \\ []) do + Multi.new() + |> Multi.insert_or_update(:operation, struct_or_changeset, opts) + |> run_logging_transaction(repo, struct_or_changeset, actor_id, :upsert) + end + + @doc """ + Call `c:Ecto.Repo.delete/2` operation and store deleted objext in a `change_log` table. + """ + @spec delete_and_log( + repo :: Ecto.Repo.t(), + struct_or_changeset :: Ecto.Schema.t() | Ecto.Changeset.t(), + actor_id :: String.T, + opts :: Keyword.t() + ) :: + {:ok, Ecto.Schema.t()} + | {:error, Ecto.Changeset.t()} + def delete_and_log(repo, struct_or_changeset, actor_id, opts \\ []) do + Multi.new() + |> Multi.delete(:operation, struct_or_changeset, opts) + |> run_logging_transaction(repo, struct_or_changeset, actor_id, :delete) + end + + defp run_logging_transaction(multi, repo, struct_or_changeset, actor_id, operation_type) do + multi + |> Multi.run(:changelog, &log_changes(&1, &2, struct_or_changeset, actor_id, operation_type)) + |> repo.transaction() + |> build_result() end - defp run_logging_transaction(multi, repo, struct_or_changeset, actor_id) do + defp run_logging_transaction_alone(multi, repo, struct, changes, actor_id, operation_type) do multi - |> Multi.run(:changelog, &log_changes(repo, &1, struct_or_changeset, actor_id)) + |> Multi.run( + :changelog, + &log_changes_alone(&1, &2, struct, changes, actor_id, operation_type) + ) |> repo.transaction() |> build_result() end - defp build_result({:ok, %{operation: operation}}), - do: {:ok, operation} - defp build_result({:error, :operation, reason, _changes_so_far}), - do: {:error, reason} + defp build_result({:ok, %{operation: operation}}), do: {:ok, operation} + + defp build_result({:error, :operation, reason, _changes_so_far}), do: {:error, reason} + + defp log_changes_alone(repo, multi_acc, struct_or_changeset, changes, actor_id, operation_type) do + %{operation: operation} = multi_acc + resource = operation.__struct__.__schema__(:source) + + result = + %{ + actor_id: to_string(actor_id), + resource: resource, + resource_id: to_string(operation.id), + changeset: changes, + change_type: operation_type + } + |> changelog_changeset() + |> repo.insert() + + case result do + {:ok, changelog} -> + {:ok, changelog} + + {:error, reason} -> + Logger.error( + "Failed to store changes in audit log: #{inspect(struct_or_changeset)} " <> + "by actor #{inspect(actor_id)}. Reason: #{inspect(reason)}" + ) - defp log_changes(repo, multi_acc, struct_or_changeset, actor_id) do + {:ok, reason} + end + end + + defp log_changes(repo, multi_acc, struct_or_changeset, actor_id, operation_type) do %{operation: operation} = multi_acc associations = operation.__struct__.__schema__(:associations) resource = operation.__struct__.__schema__(:source) embeds = operation.__struct__.__schema__(:embeds) + struct_or_changeset = + if operation_type == :delete and struct_or_changeset.__struct__ == Ecto.Changeset do + struct_or_changeset.data + else + struct_or_changeset + end + changes = struct_or_changeset |> get_changes() |> get_embed_changes(embeds) |> get_assoc_changes(associations) + |> redact_custom_fields() + |> validate_changes(struct_or_changeset, operation_type) result = %{ actor_id: to_string(actor_id), resource: resource, resource_id: to_string(operation.id), - changeset: changes + changeset: changes, + change_type: operation_type } |> changelog_changeset() |> repo.insert() @@ -148,25 +321,91 @@ defmodule EctoTrail do case result do {:ok, changelog} -> {:ok, changelog} + {:error, reason} -> - Logger.error("Failed to store changes in audit log: #{inspect struct_or_changeset} " <> - "by actor #{inspect actor_id}. Reason: #{inspect reason}") + Logger.error( + "Failed to store changes in audit log: #{inspect(struct_or_changeset)} " <> + "by actor #{inspect(actor_id)}. Reason: #{inspect(reason)}" + ) + {:ok, reason} end end + defp validate_changes(changes, schema, operation_type) do + case operation_type do + :insert -> + # This case is true when the operation type is an insert operation. + changes + + :update -> + # This case is true when the operation type is an update operation. + changes + + :delete -> + # This case is true when the operation type is an delete operation. + {_, return} = + Map.from_struct(schema) + |> Map.pop(:__meta__) + + remove_empty_assosiations(return) + + :upsert -> + # This case is true when the operation type is an upsert operation. + changes + end + end + + defp redact_custom_fields(changeset) do + redacted_fields = Application.fetch_env(:ecto_trail, :redacted_fields) + + if redacted_fields == :error do + changeset + else + {:ok, redacted_fields} = redacted_fields + + Enum.map(changeset, fn {key, value} -> + {key, + if Enum.member?(redacted_fields, key) do + "[REDACTED]" + else + value + end} + end) + |> Map.new() + end + end + + defp remove_empty_assosiations(struct) do + Enum.map(struct, fn {key, value} -> + {key, + if String.contains?(Kernel.inspect(value), "Ecto.Association.NotLoaded") do + nil + else + value + end} + end) + |> Map.new() + end + defp get_changes(%Changeset{changes: changes}), do: map_custom_ecto_types(changes) + defp get_changes(changes) when is_map(changes), do: changes |> Changeset.change(%{}) |> get_changes() + defp get_changes(changes) when is_list(changes), - do: changes |> Enum.map_reduce([], fn(ch, acc) -> {nil, List.insert_at(acc, -1, get_changes(ch))} end) |> elem(1) + do: + changes + |> Enum.map_reduce([], fn ch, acc -> {nil, List.insert_at(acc, -1, get_changes(ch))} end) + |> elem(1) defp get_embed_changes(changeset, embeds) do Enum.reduce(embeds, changeset, fn embed, changeset -> case Map.get(changeset, embed) do nil -> changeset + embed_changes -> Map.put(changeset, embed, get_changes(embed_changes)) end @@ -178,6 +417,7 @@ defmodule EctoTrail do case Map.get(changeset, assoc) do nil -> changeset + assoc_changes -> Map.put(changeset, assoc, get_changes(assoc_changes)) end @@ -205,6 +445,7 @@ defmodule EctoTrail do :resource, :resource_id, :changeset, + :change_type ]) end end diff --git a/lib/ecto_trail/enums.ex b/lib/ecto_trail/enums.ex new file mode 100644 index 0000000..b06eb56 --- /dev/null +++ b/lib/ecto_trail/enums.ex @@ -0,0 +1,2 @@ +import EctoEnum +defenum(EctoTrailChangeEnum, :change, [:insert, :delete, :update, :upsert]) diff --git a/mix.exs b/mix.exs index f0e1d3e..b0b5709 100644 --- a/mix.exs +++ b/mix.exs @@ -1,22 +1,22 @@ defmodule EctoTrail.Mixfile do use Mix.Project - @version "0.2.4" + @version "1.0.0" def project do - [app: :ecto_trail, - description: description(), - package: package(), - version: @version, - elixir: "~> 1.4", - elixirc_paths: elixirc_paths(Mix.env), - compilers: [] ++ Mix.compilers, - build_embedded: Mix.env == :prod, - start_permanent: Mix.env == :prod, - deps: deps(), - test_coverage: [tool: ExCoveralls], - preferred_cli_env: [coveralls: :test], - docs: [source_ref: "v#\{@version\}", main: "readme", extras: ["README.md"]]] + [ + app: :ecto_trail, + description: description(), + package: package(), + version: @version, + elixir: "~> 1.6", + elixirc_paths: elixirc_paths(Mix.env()), + compilers: [] ++ Mix.compilers(), + build_embedded: Mix.env() == :prod, + start_permanent: Mix.env() == :prod, + deps: deps(), + docs: [source_ref: "v#\{@version\}", main: "readme", extras: ["README.md"]] + ] end def description do @@ -28,24 +28,27 @@ defmodule EctoTrail.Mixfile do end defp elixirc_paths(:test), do: ["lib", "test/support"] - defp elixirc_paths(_), do: ["lib"] + defp elixirc_paths(_), do: ["lib"] defp deps do - [{:ecto, "~> 2.1"}, - {:postgrex, "~> 0.13.2", optional: true}, - {:dialyxir, "~> 0.5", only: [:dev, :test], runtime: false}, - {:geo, "~> 1.4", only: [:dev, :test]}, - {:ex_doc, ">= 0.15.0", only: [:dev, :test]}, - {:excoveralls, ">= 0.5.0", only: [:dev, :test]}, - {:dogma, ">= 0.1.12", only: [:dev, :test]}, - {:credo, ">= 0.5.1", only: [:dev, :test]}] + [ + {:ecto_sql, "~> 3.0"}, + {:postgrex, ">= 0.14.0"}, + {:dialyxir, "~> 1.1", only: [:dev, :test], runtime: false}, + {:ex_doc, ">= 0.15.0", only: [:dev, :test]}, + {:excoveralls, ">= 0.5.0", only: [:dev, :test]}, + {:credo, ">= 0.5.1", only: [:dev, :test]}, + {:ecto_enum, "~> 1.0"} + ] end defp package do - [contributors: ["Nebo #15"], - maintainers: ["Nebo #15"], - licenses: ["LISENSE.md"], - links: %{github: "https://github.com/Nebo15/ecto_trail"}, - files: ~w(lib LICENSE.md mix.exs README.md)] + [ + contributors: ["Valiot, Nebo #15"], + maintainers: ["Valiot"], + licenses: ["LICENSE.md"], + links: %{github: "https://github.com/Valiot/ecto_trail"}, + files: ~w(lib LICENSE.md mix.exs README.md) + ] end end diff --git a/mix.lock b/mix.lock index c06c483..de8a738 100644 --- a/mix.lock +++ b/mix.lock @@ -1,24 +1,35 @@ -%{"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, - "certifi": {:hex, :certifi, "1.2.1", "c3904f192bd5284e5b13f20db3ceac9626e14eeacfbb492e19583cf0e37b22be", [:rebar3], [], "hexpm"}, - "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, - "credo": {:hex, :credo, "0.8.1", "137efcc99b4bc507c958ba9b5dff70149e971250813cbe7d4537ec7e36997402", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"}, - "db_connection": {:hex, :db_connection, "1.1.2", "2865c2a4bae0714e2213a0ce60a1b12d76a6efba0c51fbda59c9ab8d1accc7a8", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, - "decimal": {:hex, :decimal, "1.3.1", "157b3cedb2bfcb5359372a7766dd7a41091ad34578296e951f58a946fcab49c6", [:mix], [], "hexpm"}, - "dialyxir": {:hex, :dialyxir, "0.5.0", "5bc543f9c28ecd51b99cc1a685a3c2a1a93216990347f259406a910cf048d1d7", [:mix], [], "hexpm"}, - "dogma": {:hex, :dogma, "0.1.15", "5bceba9054b2b97a4adcb2ab4948ca9245e5258b883946e82d32f785340fd411", [:mix], [{:poison, ">= 2.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, - "earmark": {:hex, :earmark, "1.2.2", "f718159d6b65068e8daeef709ccddae5f7fdc770707d82e7d126f584cd925b74", [:mix], [], "hexpm"}, - "ecto": {:hex, :ecto, "2.1.4", "d1ba932813ec0e0d9db481ef2c17777f1cefb11fc90fa7c142ff354972dfba7e", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, - "ex_doc": {:hex, :ex_doc, "0.16.1", "b4b8a23602b4ce0e9a5a960a81260d1f7b29635b9652c67e95b0c2f7ccee5e81", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, - "excoveralls": {:hex, :excoveralls, "0.7.0", "05cb3332c2b0f799df3ab90eb7df1ae5a147c86776e91792848a12b7ed87242f", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, - "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, - "geo": {:hex, :geo, "1.5.0", "14301aae05bc9124f36b3726e028830477d2ee6b397b7a00b98eca3da06de193", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm"}, - "hackney": {:hex, :hackney, "1.8.6", "21a725db3569b3fb11a6af17d5c5f654052ce9624219f1317e8639183de4a423", [:rebar3], [{:certifi, "1.2.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.0.2", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, - "idna": {:hex, :idna, "5.0.2", "ac203208ada855d95dc591a764b6e87259cb0e2a364218f215ad662daa8cd6b4", [:rebar3], [{:unicode_util_compat, "0.2.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, - "jsx": {:hex, :jsx, "2.8.2", "7acc7d785b5abe8a6e9adbde926a24e481f29956dd8b4df49e3e4e7bcc92a018", [:mix, :rebar3], [], "hexpm"}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, - "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, +%{ + "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, + "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, + "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, + "credo": {:hex, :credo, "1.7.1", "6e26bbcc9e22eefbff7e43188e69924e78818e2fe6282487d0703652bc20fd62", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e9871c6095a4c0381c89b6aa98bc6260a8ba6addccf7f6a53da8849c748a58a2"}, + "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, + "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, + "dialyxir": {:hex, :dialyxir, "1.4.2", "764a6e8e7a354f0ba95d58418178d486065ead1f69ad89782817c296d0d746a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "516603d8067b2fd585319e4b13d3674ad4f314a5902ba8130cd97dc902ce6bbd"}, + "earmark": {:hex, :earmark, "1.3.1", "73812f447f7a42358d3ba79283cfa3075a7580a3a2ed457616d6517ac3738cb9", [:mix], [], "hexpm", "000aaeff08919e95e7aea13e4af7b2b9734577b3e6a7c50ee31ee88cab6ec4fb"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.37", "2ad73550e27c8946648b06905a57e4d454e4d7229c2dafa72a0348c99d8be5f7", [:mix], [], "hexpm", "6b19783f2802f039806f375610faa22da130b8edc21209d0bff47918bb48360e"}, + "ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"}, + "ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"}, + "ecto_sql": {:hex, :ecto_sql, "3.10.2", "6b98b46534b5c2f8b8b5f03f126e75e2a73c64f3c071149d32987a5378b0fdbd", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "68c018debca57cb9235e3889affdaec7a10616a4e3a80c99fa1d01fdafaa9007"}, + "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "ex_doc": {:hex, :ex_doc, "0.30.9", "d691453495c47434c0f2052b08dd91cc32bc4e1a218f86884563448ee2502dd2", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "d7aaaf21e95dc5cddabf89063327e96867d00013963eadf2c6ad135506a8bc10"}, + "excoveralls": {:hex, :excoveralls, "0.18.0", "b92497e69465dc51bc37a6422226ee690ab437e4c06877e836f1c18daeb35da9", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1109bb911f3cb583401760be49c02cbbd16aed66ea9509fc5479335d284da60b"}, + "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "geo": {:hex, :geo, "3.0.0", "bb1e9baac6031c5bbddcde4937af1c1ab1cbfbbe2f7870038fdfc93a9cad4359", [:mix], [], "hexpm", "40a1acc0c8c437d548b5c58505de2800ab6c0fb40950b1e39b6f21dd08e5ba0d"}, + "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, + "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"}, - "postgrex": {:hex, :postgrex, "0.13.3", "c277cfb2a9c5034d445a722494c13359e361d344ef6f25d604c2353185682bfc", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.2.0", "dbbccf6781821b1c0701845eaf966c9b6d83d7c3bfc65ca2b78b88b8678bfa35", [:rebar3], [], "hexpm"}} + "postgrex": {:hex, :postgrex, "0.17.3", "c92cda8de2033a7585dae8c61b1d420a1a1322421df84da9a82a6764580c503d", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "946cf46935a4fdca7a81448be76ba3503cff082df42c6ec1ff16a4bdfbfb098d"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, + "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, +} diff --git a/priv/repo/migrations/20170419082821_create_log_changes_table.exs b/priv/repo/migrations/20170419082821_create_log_changes_table.exs index 3848f1d..6cbc5b9 100644 --- a/priv/repo/migrations/20170419082821_create_log_changes_table.exs +++ b/priv/repo/migrations/20170419082821_create_log_changes_table.exs @@ -5,12 +5,13 @@ defmodule EctoTrail.TestRepo.Migrations.CreateAuditLogTable do @table_name String.to_atom(Application.fetch_env!(:ecto_trail, :table_name)) def change(table_name \\ @table_name) do - create table(table_name, primary_key: false) do - add :id, :uuid, primary_key: true + EctoTrailChangeEnum.create_type + create table(table_name) do add :actor_id, :string, null: false add :resource, :string, null: false add :resource_id, :string, null: false add :changeset, :map, null: false + add(:change_type, :change) timestamps([type: :utc_datetime, updated_at: false]) end diff --git a/priv/repo/migrations/20170419082822_create_resources_table.exs b/priv/repo/migrations/20170419082822_create_resources_table.exs index 5d4d784..f172a3a 100644 --- a/priv/repo/migrations/20170419082822_create_resources_table.exs +++ b/priv/repo/migrations/20170419082822_create_resources_table.exs @@ -5,6 +5,7 @@ defmodule EctoTrail.TestRepo.Migrations.CreateResourcesTable do def change do create table(:resources) do add :name, :string + add :password, :string add :data, :map timestamps() diff --git a/priv/repo/migrations/20170621171954_add_location_to_resource_table.exs b/priv/repo/migrations/20170621171954_add_location_to_resource_table.exs deleted file mode 100644 index 1e22a2e..0000000 --- a/priv/repo/migrations/20170621171954_add_location_to_resource_table.exs +++ /dev/null @@ -1,10 +0,0 @@ -defmodule EctoTrail.TestRepo.Migrations.AddLocationToResourceTable do - use Ecto.Migration - - def change do - execute "CREATE EXTENSION IF NOT EXISTS postgis" - alter table(:resources) do - add :location, :geometry - end - end -end diff --git a/pull_request_template.md b/pull_request_template.md new file mode 100644 index 0000000..0d5dc83 --- /dev/null +++ b/pull_request_template.md @@ -0,0 +1,38 @@ +# Description + +Please include a summary of the change and/or which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. + +Fixes # (issue) + +## Type of change + +Please delete options that are not relevant. + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] This change requires a documentation update + +# How Has This Been Tested? + +Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration + +- [ ] Test A +- [ ] Test B + +**Test Configuration**: +* Firmware version: +* Hardware: +* Toolchain: +* SDK: + +# Checklist: + +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes +- [ ] Any dependent changes have been merged and published in downstream modules diff --git a/test/test_helper.exs b/test/test_helper.exs index 206e981..b16d2ff 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,23 +1,17 @@ -# Enable PostGIS for Ecto -Postgrex.Types.define( - EHealth.PostgresTypes, - [Geo.PostGIS.Extension] ++ Ecto.Adapters.Postgres.extensions(), - json: Poison -) Application.put_env(:ex_unit, :capture_log, true) + Application.put_env(:ecto_trail, TestRepo, pool: Ecto.Adapters.SQL.Sandbox, database: "ecto_trail_test", - username: "postgres", - password: "postgres", hostname: "localhost", - pool_size: 10, - types: EHealth.PostgresTypes) + pool_size: 10 +) defmodule TestRepo do use Ecto.Repo, otp_app: :ecto_trail, adapter: Ecto.Adapters.Postgres + use EctoTrail end @@ -25,8 +19,8 @@ defmodule Comment do use Ecto.Schema schema "comments" do - field :title, :string - belongs_to :resource, ResourcesSchema + field(:title, :string) + belongs_to(:resource, Resource) end def changeset(%Comment{} = schema, attrs) do @@ -38,8 +32,8 @@ defmodule Category do use Ecto.Schema schema "categories" do - field :title, :string - belongs_to :resource, ResourcesSchema + field(:title, :string) + belongs_to(:resource, Resource) end def changeset(%Category{} = schema, attrs) do @@ -47,27 +41,27 @@ defmodule Category do end end -defmodule ResourcesSchema do +defmodule Resource do @moduledoc false use Ecto.Schema schema "resources" do - field :name, :string - field :array, {:array, :string} - field :map, :map - field :location, Geo.Geometry + field(:name, :string) + field(:password, :string) + field(:array, {:array, :string}) + field(:map, :map) embeds_one :data, Data, primary_key: false do - field :key1, :string - field :key2, :string + field(:key1, :string) + field(:key2, :string) end embeds_many :items, Item, primary_key: false do - field :name, :string + field(:name, :string) end - has_many :comments, Comment - has_one :category, {"categories", Category}, on_replace: :delete + has_many(:comments, Comment) + has_one(:category, {"categories", Category}, on_replace: :delete) timestamps() end @@ -85,7 +79,7 @@ end {:ok, _pids} = Application.ensure_all_started(:postgrex) # Create DB -_ = TestRepo.__adapter__.storage_up(TestRepo.config) +_ = TestRepo.__adapter__().storage_up(TestRepo.config()) # Start Repo {:ok, _pid} = TestRepo.start_link() diff --git a/test/unit/ecto_trail_log_only_test.exs b/test/unit/ecto_trail_log_only_test.exs new file mode 100644 index 0000000..109b605 --- /dev/null +++ b/test/unit/ecto_trail_log_only_test.exs @@ -0,0 +1,38 @@ +defmodule EctoTrailLogOnlyTest do + use EctoTrail.DataCase + alias EctoTrail.Changelog + alias Ecto.Changeset + alias Ecto.Multi + doctest EctoTrail + + describe "log_bulk" do + test "logs inserted structs with associated changes" do + changes_list = [%{name: "My name"}, %{name: "Your name"}] + + dt_now = DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_naive() + + ready_changes = + Enum.map(changes_list, fn change -> + change + |> Map.update(:inserted_at, dt_now, fn dt -> dt end) + |> Map.update(:updated_at, dt_now, fn dt -> dt end) + end) + + {_n, structs_list} = TestRepo.insert_all(Resource, ready_changes, returning: true) + + result = TestRepo.log_bulk(structs_list, changes_list, "cowboy") + + ids = Enum.map(structs_list, fn inserted_struct -> inserted_struct.id end) + + assert :ok = result + + Enum.each(Enum.zip([ids, changes_list]), fn {an_id, a_change} -> + assert %{ + changeset: a_change, + actor_id: "cowboy", + change_type: :insert + } = TestRepo.get_by(Changelog, %{resource_id: an_id |> to_string()}) + end) + end + end +end diff --git a/test/unit/ecto_trail_test.exs b/test/unit/ecto_trail_test.exs index a73482b..1da6fb5 100644 --- a/test/unit/ecto_trail_test.exs +++ b/test/unit/ecto_trail_test.exs @@ -6,56 +6,78 @@ defmodule EctoTrailTest do describe "insert_and_log/3" do test "logs changes when schema is inserted" do - result = TestRepo.insert_and_log(%ResourcesSchema{name: "name"}, "cowboy") - assert {:ok, %ResourcesSchema{name: "name"}} = result + result = TestRepo.insert_and_log(%Resource{name: "name"}, "cowboy") + assert {:ok, %Resource{name: "name"}} = result - resource = TestRepo.one(ResourcesSchema) + resource = TestRepo.one(Resource) resource_id = to_string(resource.id) assert %{ - changeset: %{}, - actor_id: "cowboy", - resource_id: ^resource_id, - resource: "resources" - } = TestRepo.one(Changelog) + changeset: %{}, + actor_id: "cowboy", + resource_id: ^resource_id, + resource: "resources", + change_type: :insert + } = TestRepo.one(Changelog) end test "logs changes when changeset is inserted" do result = - %ResourcesSchema{} + %Resource{} |> Changeset.change(%{name: "My name"}) |> TestRepo.insert_and_log("cowboy") - assert {:ok, %ResourcesSchema{name: "My name"}} = result + assert {:ok, %Resource{name: "My name"}} = result - resource = TestRepo.one(ResourcesSchema) + resource = TestRepo.one(Resource) resource_id = to_string(resource.id) assert %{ - changeset: %{"name" => "My name"}, - actor_id: "cowboy", - resource_id: ^resource_id, - resource: "resources" - } = TestRepo.one(Changelog) + changeset: %{"name" => "My name"}, + actor_id: "cowboy", + resource_id: ^resource_id, + resource: "resources", + change_type: :insert + } = TestRepo.one(Changelog) + end + + test "logs changes with redacted field when changeset is inserted" do + result = + %Resource{} + |> Changeset.change(%{name: "My password Redacted", password: "secret"}) + |> TestRepo.insert_and_log("cowboy") + + assert {:ok, %Resource{name: "My password Redacted", password: "secret"}} = result + resource = TestRepo.one(Resource) + resource_id = to_string(resource.id) + + assert %{ + changeset: %{"name" => "My password Redacted", "password" => "[REDACTED]"}, + actor_id: "cowboy", + resource_id: ^resource_id, + resource: "resources", + change_type: :insert + } = TestRepo.one(Changelog) end test "logs changes when changeset is empty" do result = - %ResourcesSchema{} + %Resource{} |> Changeset.change(%{}) |> TestRepo.insert_and_log("cowboy") - assert {:ok, %ResourcesSchema{name: nil}} = result + assert {:ok, %Resource{name: nil}} = result - resource = TestRepo.one(ResourcesSchema) + resource = TestRepo.one(Resource) resource_id = to_string(resource.id) assert %{ - changeset: changes, - actor_id: "cowboy", - resource_id: ^resource_id, - resource: "resources" - } = TestRepo.one(Changelog) + changeset: changes, + actor_id: "cowboy", + resource_id: ^resource_id, + resource: "resources", + change_type: :insert + } = TestRepo.one(Changelog) assert %{} == changes end @@ -65,76 +87,73 @@ defmodule EctoTrailTest do name: "My name", array: ["apple", "banana"], map: %{longitude: 50.45000, latitude: 30.52333}, - location: %Geo.Point{coordinates: {49.44, 17.87}}, data: %{key2: "key2"}, category: %{"title" => "test"}, comments: [ %{"title" => "wow"}, - %{"title" => "very impressive"}, + %{"title" => "very impressive"} ], items: [ %{name: "Morgan"}, %{name: "Freeman"} - ]} + ] + } result = - %ResourcesSchema{} - |> Changeset.cast(attrs, [:name, :array, :map, :location]) - |> Changeset.cast_embed(:data, with: &ResourcesSchema.embed_changeset/2) - |> Changeset.cast_embed(:items, with: &ResourcesSchema.embeds_many_changeset/2) + %Resource{} + |> Changeset.cast(attrs, [:name, :array, :map]) + |> Changeset.cast_embed(:data, with: &Resource.embed_changeset/2) + |> Changeset.cast_embed(:items, with: &Resource.embeds_many_changeset/2) |> Changeset.cast_assoc(:category) |> Changeset.cast_assoc(:comments) |> TestRepo.insert_and_log("cowboy") - assert {:ok, %ResourcesSchema{name: "My name"}} = result + assert {:ok, %Resource{name: "My name"}} = result - resource = TestRepo.one(ResourcesSchema) + resource = TestRepo.one(Resource) resource_id = to_string(resource.id) assert %{ - changeset: changes, - actor_id: "cowboy", - resource_id: ^resource_id, - resource: "resources" - } = TestRepo.one(Changelog) + changeset: changes, + actor_id: "cowboy", + resource_id: ^resource_id, + resource: "resources" + } = TestRepo.one(Changelog) assert %{ - "name" => "My name", - "data" => %{"key2" => "key2"}, - "category" => %{"title" => "test"}, - "comments" => [ - %{"title" => "wow"}, - %{"title" => "very impressive"}, - ], - "items" => [ - %{"name" => "Morgan"}, - %{"name" => "Freeman"} - ], - "location" => "%Geo.Point{coordinates: {49.44, 17.87}, srid: nil}", - "array" => ["apple", "banana"], - "map" => %{ - "latitude" => 30.52333, - "longitude" => 50.45} - } == changes + "name" => "My name", + "data" => %{"key2" => "key2"}, + "category" => %{"title" => "test"}, + "comments" => [ + %{"title" => "wow"}, + %{"title" => "very impressive"} + ], + "items" => [ + %{"name" => "Morgan"}, + %{"name" => "Freeman"} + ], + "array" => ["apple", "banana"], + "map" => %{"latitude" => 30.52333, "longitude" => 50.45} + } == changes end test "returns error when changeset is invalid" do changeset = - %ResourcesSchema{} + %Resource{} |> Changeset.change(%{name: "My name"}) |> Changeset.add_error(:name, "invalid") result = TestRepo.insert_and_log(changeset, "cowboy") assert {:error, %Changeset{valid?: false}} = result - assert [] == TestRepo.all(ResourcesSchema) + assert [] == TestRepo.all(Resource) assert [] == TestRepo.all(Changelog) end end describe "update_and_log/3" do setup do - {:ok, schema} = TestRepo.insert(%ResourcesSchema{name: "name"}) + {:ok, schema} = TestRepo.insert(%Resource{name: "name"}) {:ok, %{schema: schema}} end @@ -144,17 +163,18 @@ defmodule EctoTrailTest do |> Changeset.change(%{name: "My new name"}) |> TestRepo.update_and_log("cowboy") - assert {:ok, %ResourcesSchema{name: "My new name"}} = result + assert {:ok, %Resource{name: "My new name"}} = result - resource = TestRepo.one(ResourcesSchema) + resource = TestRepo.one(Resource) resource_id = to_string(resource.id) assert %{ - changeset: %{"name" => "My new name"}, - actor_id: "cowboy", - resource_id: ^resource_id, - resource: "resources" - } = TestRepo.one(Changelog) + changeset: %{"name" => "My new name"}, + actor_id: "cowboy", + resource_id: ^resource_id, + resource: "resources", + change_type: :update + } = TestRepo.one(Changelog) end test "returns error when changeset is invalid", %{schema: schema} do @@ -166,8 +186,35 @@ defmodule EctoTrailTest do result = TestRepo.update_and_log(changeset, "cowboy") assert {:error, %Changeset{valid?: false}} = result - assert [%{name: "name"}] = TestRepo.all(ResourcesSchema) + assert [%{name: "name"}] = TestRepo.all(Resource) assert [] == TestRepo.all(Changelog) end end + + describe "upsert_and_log/3" do + setup do + {:ok, schema} = TestRepo.insert(%Resource{name: "name"}) + {:ok, %{schema: schema}} + end + + test "logs changes when changeset is inserted", %{schema: schema} do + result = + schema + |> Changeset.change(%{name: "My new name"}) + |> TestRepo.upsert_and_log("cowboy") + + assert {:ok, %Resource{name: "My new name"}} = result + + resource = TestRepo.one(Resource) + resource_id = to_string(resource.id) + + assert %{ + changeset: %{"name" => "My new name"}, + actor_id: "cowboy", + resource_id: ^resource_id, + resource: "resources", + change_type: :upsert + } = TestRepo.one(Changelog) + end + end end