Skip to content

Commit

Permalink
Add escape hatch for when one doesn't want prepare_query (#184)
Browse files Browse the repository at this point in the history
- Add a way of configuring specific schemas to not get `deleted_at`
clauses automatically.
- Add readme example + explanation on `with_deleted` limitations
  • Loading branch information
caioaao authored Oct 28, 2024
1 parent 4d14204 commit 686fe5c
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 5 deletions.
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,26 @@ Import `Ecto.SoftDelete.Schema` into your Schema module, then add `soft_delete_s
import Ecto.SoftDelete.Schema

schema "users" do
field :email, :string
field :email, :string
soft_delete_schema()
end
end
```

If you want to make sure auto-filtering is disabled for a schema, set the `auto_exclude_from_queries?` option to false

```elixir
defmodule User do
use Ecto.Schema
import Ecto.SoftDelete.Schema

schema "users" do
field :email, :string
soft_delete_schema(auto_exclude_from_queries?: false)
end
end
```

### Queries

To query for items that have not been deleted, use `with_undeleted(query)` which will filter out deleted items using the `deleted_at` column produced by the previous 2 steps
Expand All @@ -72,6 +86,9 @@ query = from(u in User, select: u)
results = Repo.all(query, with_deleted: true)
```

> [!IMPORTANT]
> This only works for the topmost schema. If using `Ecto.SoftDelete.Repo`, rows fetched through associations (such as when using `Repo.preload/2`) will still be filtered.
## Repos

To support deletion in repos, just add `use Ecto.SoftDelete.Repo` to your repo.
Expand Down Expand Up @@ -100,6 +117,8 @@ post = Repo.get!(Post, 42)
struct = Repo.soft_delete!(post)
```

`Ecto.SoftDelete.Repo` will also intercept all queries made with the repo and automatically add a clause to filter out soft-deleted rows.

## Installation

Add to mix.exs:
Expand Down
11 changes: 11 additions & 0 deletions lib/ecto/soft_delete_query.ex
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ defmodule Ecto.SoftDelete.Query do
Enum.member?(fields, :deleted_at)
end

@doc"""
Returns `true` if the schema is not flagged to skip auto-filtering
"""
@spec auto_include_deleted_at_clause?(Ecto.Queriable.t) :: boolean()
def auto_include_deleted_at_clause?(query) do
schema_module = get_schema_module(query)

!Kernel.function_exported?(schema_module, :skip_soft_delete_prepare_query?, 0) ||
!schema_module.skip_soft_delete_prepare_query?()
end

defp get_schema_module({_raw_schema, module}) when not is_nil(module), do: module
defp get_schema_module(%Ecto.Query{from: %{source: source}}), do: get_schema_module(source)
defp get_schema_module(%Ecto.SubQuery{query: query}), do: get_schema_module(query)
Expand Down
8 changes: 7 additions & 1 deletion lib/ecto/soft_delete_repo.ex
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,13 @@ defmodule Ecto.SoftDelete.Repo do
NOTE: will not exclude soft deleted records if :with_deleted option passed as true
"""
def prepare_query(_operation, query, opts) do
if has_include_deleted_at_clause?(query) || opts[:with_deleted] || !soft_deletable?(query) 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))
Expand Down
19 changes: 16 additions & 3 deletions lib/ecto/soft_delete_schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,28 @@ defmodule Ecto.SoftDelete.Schema do
import Ecto.SoftDelete.Schema
schema "users" do
field :email, :string
field :email, :string
soft_delete_schema()
end
end
Options:
- `:auto_exclude_from_queries?` - If false, Ecto.SoftDelete.Repo won't
automatically add the necessary clause to filter out soft-deleted rows. See
`Ecto.SoftDelete.Repo.prepare_query` for more info. Defaults to `true`.
"""
defmacro soft_delete_schema do
defmacro soft_delete_schema(opts \\ []) do
filter_tag_definition =
unless Keyword.get(opts, :auto_exclude_from_queries?, true) do
quote do
def skip_soft_delete_prepare_query?, do: true
end
end

quote do
field :deleted_at, :utc_datetime_usec
field(:deleted_at, :utc_datetime_usec)
unquote(filter_tag_definition)
end
end
end
25 changes: 25 additions & 0 deletions test/soft_delete_repo_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ defmodule Ecto.SoftDelete.Repo.Test do
end
end

defmodule UserWithSkipPrepareQuery do
use Ecto.Schema
import Ecto.SoftDelete.Schema

schema "users" do
field(:email, :string)
soft_delete_schema(auto_exclude_from_queries?: false)
end
end

defmodule Nondeletable do
use Ecto.Schema

Expand Down Expand Up @@ -130,6 +140,21 @@ defmodule Ecto.SoftDelete.Repo.Test do
assert Enum.member?(results, soft_deleted_user)
end

test "includes soft deleted records if `auto_exclude_from_queries?` is false" do
user = Repo.insert!(%UserWithSkipPrepareQuery{email: "[email protected]"})

soft_deleted_user =
Repo.insert!(%UserWithSkipPrepareQuery{
email: "[email protected]",
deleted_at: DateTime.utc_now()
})

results = UserWithSkipPrepareQuery |> Repo.all()

assert Enum.member?(results, user)
assert Enum.member?(results, soft_deleted_user)
end

test "works with schemas that don't have deleted_at column" do
Repo.insert!(%Nondeletable{value: "stuff"})
results = Nondeletable |> Repo.all()
Expand Down

0 comments on commit 686fe5c

Please sign in to comment.