diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..90c80fe --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,5 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], + import_deps: [:ecto] +] diff --git a/config/config.exs b/config/config.exs index a21eadd..61fc384 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,6 +1,6 @@ # This file is responsible for configuring your application # and its dependencies with the aid of the Mix.Config module. -use Mix.Config +import Config # This configuration is loaded before any dependency and is restricted # to this project. If another project depends on this project, this @@ -27,4 +27,4 @@ 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/dev.exs b/config/dev.exs index d2d855e..becde76 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -1 +1 @@ -use Mix.Config +import Config diff --git a/config/prod.exs b/config/prod.exs index d2d855e..becde76 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -1 +1 @@ -use Mix.Config +import Config diff --git a/config/test.exs b/config/test.exs index f674c6b..f58e5f7 100644 --- a/config/test.exs +++ b/config/test.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config config :ecto_soft_delete, ecto_repos: [Ecto.SoftDelete.Test.Repo] diff --git a/config/test.exs.travis b/config/test.exs.travis index c5464c6..ff5b4bf 100644 --- a/config/test.exs.travis +++ b/config/test.exs.travis @@ -1,4 +1,4 @@ -use Mix.Config +import Config config :ecto_soft_delete, ecto_repos: [Ecto.SoftDelete.Test.Repo] diff --git a/lib/ecto/soft_delete_query.ex b/lib/ecto/soft_delete_query.ex index d5bd2f7..568dc3f 100644 --- a/lib/ecto/soft_delete_query.ex +++ b/lib/ecto/soft_delete_query.ex @@ -14,7 +14,7 @@ defmodule Ecto.SoftDelete.Query do results = Repo.all(query) """ - @spec with_undeleted(Ecto.Queryable.t) :: Ecto.Queryable.t + @spec with_undeleted(Ecto.Queryable.t()) :: Ecto.Queryable.t() def with_undeleted(query) do query |> where([t], is_nil(t.deleted_at)) diff --git a/lib/ecto/soft_delete_repo.ex b/lib/ecto/soft_delete_repo.ex index 0643fe1..499d765 100644 --- a/lib/ecto/soft_delete_repo.ex +++ b/lib/ecto/soft_delete_repo.ex @@ -26,6 +26,21 @@ defmodule Ecto.SoftDelete.Repo do """ @callback soft_delete_all(queryable :: Ecto.Queryable.t()) :: {integer, nil | [term]} + @doc """ + Soft restores all entries matching the given query. + + It returns a tuple containing the number of entries and any returned + result as second element. The second element is `nil` by default + unless a `select` is supplied in the update query. + + ## Examples + + MyRepo.soft_restore_all(%Post{}, MyRepo) + + + """ + @callback soft_restore_all(struct :: Ecto.Queryable.t()) :: {integer, nil | [term]} + @doc """ Soft deletes a struct. Updates the `deleted_at` field with the current datetime in UTC. @@ -37,8 +52,8 @@ defmodule Ecto.SoftDelete.Repo do post = MyRepo.get!(Post, 42) case MyRepo.soft_delete post do - {:ok, struct} -> # Soft deleted with success - {:error, changeset} -> # Something went wrong + {:ok, struct} -> "Soft deleted with success" + {:error, changeset} -> "Something went wrong" end """ @@ -51,6 +66,31 @@ defmodule Ecto.SoftDelete.Repo do @callback soft_delete!(struct_or_changeset :: Ecto.Schema.t() | Ecto.Changeset.t()) :: Ecto.Schema.t() + @doc """ + Soft restores a struct. + Updates the `deleted_at` to null. + It returns `{:ok, struct}` if the struct has been successfully + soft deleted or `{:error, changeset}` if there was a validation + or a known constraint error. + + ## Examples + + post = MyRepo.get!(Post, 42) + case MyRepo.soft_restore post do + {:ok, struct} -> "Soft restore with success" + {:error, changeset} -> "Something went wrong" + end + + """ + @callback soft_restore(struct :: Ecto.Schema.t()) :: + {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} + + @doc """ + Same as `c:soft_restore/1` but returns the struct or raises if the changeset is invalid. + """ + @callback soft_restore!(struct :: Ecto.Schema.t()) :: + Ecto.Schema.t() + defmacro __using__(_opts) do quote do import Ecto.Query @@ -71,6 +111,22 @@ defmodule Ecto.SoftDelete.Repo do |> update!() end + def soft_restore_all(queryable) do + update_all(queryable, set: [deleted_at: nil]) + end + + def soft_restore(struct_or_changeset) do + struct_or_changeset + |> Ecto.Changeset.change(deleted_at: nil) + |> update() + end + + def soft_restore!(struct_or_changeset) do + struct_or_changeset + |> Ecto.Changeset.change(deleted_at: nil) + |> update!() + end + @doc """ Overrides all query operations to exclude soft deleted records if the schema in the from clause has a deleted_at column diff --git a/lib/ecto/soft_delete_schema.ex b/lib/ecto/soft_delete_schema.ex index c1a73b0..dff8816 100644 --- a/lib/ecto/soft_delete_schema.ex +++ b/lib/ecto/soft_delete_schema.ex @@ -19,7 +19,7 @@ defmodule Ecto.SoftDelete.Schema do """ defmacro soft_delete_schema do quote do - field :deleted_at, :utc_datetime_usec + field(:deleted_at, :utc_datetime_usec) end end end diff --git a/mix.exs b/mix.exs index a9657a0..55891e3 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule EctoSoftDelete.Mixfile do def project do [ app: :ecto_soft_delete, - version: "2.0.2", + version: "2.0.3", elixir: "~> 1.9", elixirc_paths: elixirc_paths(Mix.env()), build_embedded: Mix.env() == :prod, diff --git a/test/soft_delete_migration_test.exs b/test/soft_delete_migration_test.exs index 733b6aa..ca1f720 100644 --- a/test/soft_delete_migration_test.exs +++ b/test/soft_delete_migration_test.exs @@ -7,7 +7,12 @@ defmodule Ecto.SoftDelete.Migration.Test do setup meta do :ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo) - {:ok, runner} = Runner.start_link({self(), Repo, __MODULE__, meta[:direction] || :forward, :up, %{level: false, sql: false}}) + + {:ok, runner} = + Runner.start_link( + {self(), Repo, __MODULE__, meta[:direction] || :forward, :up, %{level: false, sql: false}} + ) + Runner.metadata(runner, meta) {:ok, runner: runner} end @@ -21,7 +26,6 @@ defmodule Ecto.SoftDelete.Migration.Test do flush() - assert {:create, _, - [{:add, :deleted_at, :utc_datetime_usec, []}]} = create_command + assert {:create, _, [{:add, :deleted_at, :utc_datetime_usec, []}]} = create_command end end diff --git a/test/soft_delete_repo_test.exs b/test/soft_delete_repo_test.exs index 6d4d9e3..0a1e403 100644 --- a/test/soft_delete_repo_test.exs +++ b/test/soft_delete_repo_test.exs @@ -90,6 +90,33 @@ defmodule Ecto.SoftDelete.Repo.Test do end end + describe "soft_restore/1" do + test "should soft restore the queryable" do + user = Repo.insert!(%User{email: "test0@example.com"}) + user = Repo.soft_delete!(user) + Repo.soft_restore(user, Repo) + + assert Repo.get_by!(User, [email: "test0@example.com"], with_deleted: true).deleted_at == + nil + end + end + + describe "soft_restore_all/1" do + test "soft deleted the query" do + Repo.insert!(%User{email: "test0@example.com"}) + Repo.insert!(%User{email: "test1@example.com"}) + Repo.insert!(%User{email: "test2@example.com"}) + + assert Repo.soft_delete_all(User) == {3, nil} + + assert Repo.soft_restore_all(%User{}, Repo) == {3, nil} + end + + test "when no results are found" do + assert Repo.soft_delete_all(User) == {0, nil} + end + end + describe "prepare_query/3" do test "excludes soft deleted records by default" do user = Repo.insert!(%User{email: "test0@example.com"}) diff --git a/test/test_helper.exs b/test/test_helper.exs index b1e8af0..9bc7977 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,19 +1,20 @@ {:ok, _} = Application.ensure_all_started(:postgrex) -{:ok, _pid} = Ecto.SoftDelete.Test.Repo.start_link +{:ok, _pid} = Ecto.SoftDelete.Test.Repo.start_link() defmodule Ecto.SoftDelete.Test.Migrations do use Ecto.Migration import Ecto.SoftDelete.Migration def change do - drop_if_exists table(:users) + drop_if_exists(table(:users)) + create table(:users) do - add :email, :string + add(:email, :string) soft_delete_columns() end create table(:nondeletable) do - add :value, :string + add(:value, :string) end end end