defmodule Ecto.SoftDelete.Repo do @moduledoc """ Adds soft delete functions to an repository. defmodule Repo do use Ecto.Repo, otp_app: :my_app, adapter: Ecto.Adapters.Postgres use Ecto.SoftDelete.Repo end """ @doc """ Soft deletes 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. ## Options All options supported by `c:Ecto.Repo.update_all/3` can be used. ## Examples MyRepo.soft_delete_all(Post) from(p in Post, where: p.id < 10) |> MyRepo.soft_delete_all() # With schema prefix for multi-tenant databases MyRepo.soft_delete_all(Post, prefix: "tenant_abc") """ @callback soft_delete_all(queryable :: Ecto.Queryable.t(), opts :: Keyword.t()) :: {integer, nil | [term()]} @doc """ Soft deletes a struct. Updates the `deleted_at` field with the current datetime in UTC. 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. ## Options All options supported by `c:Ecto.Repo.update/2` can be used. ## Examples post = MyRepo.get!(Post, 42) case MyRepo.soft_delete post do {:ok, struct} -> # Soft deleted with success {:error, changeset} -> # Something went wrong end # With schema prefix for multi-tenant databases MyRepo.soft_delete(post, prefix: "tenant_abc") """ @callback soft_delete( struct_or_changeset :: Ecto.Schema.t() | Ecto.Changeset.t(), opts :: Keyword.t() ) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} @doc """ Same as `c:soft_delete/2` but returns the struct or raises if the changeset is invalid. """ @callback soft_delete!( struct_or_changeset :: Ecto.Schema.t() | Ecto.Changeset.t(), opts :: Keyword.t() ) :: Ecto.Schema.t() defmacro __using__(_opts) do quote do import Ecto.Query import Ecto.SoftDelete.Query def soft_delete_all(queryable, opts \\ []) do set_expr = [set: [deleted_at: DateTime.utc_now()]] update_all(queryable, set_expr, opts) end # Define soft_delete and soft_delete! with options def soft_delete(struct_or_changeset, opts \\ []) do struct_or_changeset |> Ecto.Changeset.change(deleted_at: DateTime.utc_now()) |> __MODULE__.update(opts) end def soft_delete!(struct_or_changeset, opts \\ []) do struct_or_changeset |> Ecto.Changeset.change(deleted_at: DateTime.utc_now()) |> __MODULE__.update!(opts) end @doc """ Overrides all query operations to exclude soft deleted records if the schema in the from clause has a deleted_at column NOTE: will not exclude soft deleted records if :with_deleted option passed as true """ def prepare_query(_operation, query, opts) do skip_deleted_at_clause? = has_include_deleted_at_clause?(query) || opts[:with_deleted] || !soft_deletable?(query) || !auto_include_deleted_at_clause?(query) if skip_deleted_at_clause? do {query, opts} else query = from(x in query, where: is_nil(x.deleted_at)) {query, opts} end end # Checks the query to see if it contains a where not is_nil(deleted_at) # if it does, we want to be sure that we don't exclude soft deleted records defp has_include_deleted_at_clause?(%Ecto.Query{wheres: wheres}) do Enum.any?(wheres, fn %{expr: expr} -> expr |> Inspect.Algebra.to_doc(%Inspect.Opts{ inspect_fun: fn expr, _ -> inspect(expr, limit: :infinity) end }) |> String.contains?( "{:not, [], [{:is_nil, [], [{{:., [], [{:&, [], [0]}, :deleted_at]}, [], []}]}]}" ) end) end end end end