Skip to content

Commit 686fe5c

Browse files
authored
Add escape hatch for when one doesn't want prepare_query (#184)
- Add a way of configuring specific schemas to not get `deleted_at` clauses automatically. - Add readme example + explanation on `with_deleted` limitations
1 parent 4d14204 commit 686fe5c

File tree

5 files changed

+79
-5
lines changed

5 files changed

+79
-5
lines changed

README.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,26 @@ Import `Ecto.SoftDelete.Schema` into your Schema module, then add `soft_delete_s
4141
import Ecto.SoftDelete.Schema
4242

4343
schema "users" do
44-
field :email, :string
44+
field :email, :string
4545
soft_delete_schema()
4646
end
4747
end
4848
```
4949

50+
If you want to make sure auto-filtering is disabled for a schema, set the `auto_exclude_from_queries?` option to false
51+
52+
```elixir
53+
defmodule User do
54+
use Ecto.Schema
55+
import Ecto.SoftDelete.Schema
56+
57+
schema "users" do
58+
field :email, :string
59+
soft_delete_schema(auto_exclude_from_queries?: false)
60+
end
61+
end
62+
```
63+
5064
### Queries
5165

5266
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
@@ -72,6 +86,9 @@ query = from(u in User, select: u)
7286
results = Repo.all(query, with_deleted: true)
7387
```
7488

89+
> [!IMPORTANT]
90+
> 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.
91+
7592
## Repos
7693

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

120+
`Ecto.SoftDelete.Repo` will also intercept all queries made with the repo and automatically add a clause to filter out soft-deleted rows.
121+
103122
## Installation
104123

105124
Add to mix.exs:

lib/ecto/soft_delete_query.ex

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,17 @@ defmodule Ecto.SoftDelete.Query do
3939
Enum.member?(fields, :deleted_at)
4040
end
4141

42+
@doc"""
43+
Returns `true` if the schema is not flagged to skip auto-filtering
44+
"""
45+
@spec auto_include_deleted_at_clause?(Ecto.Queriable.t) :: boolean()
46+
def auto_include_deleted_at_clause?(query) do
47+
schema_module = get_schema_module(query)
48+
49+
!Kernel.function_exported?(schema_module, :skip_soft_delete_prepare_query?, 0) ||
50+
!schema_module.skip_soft_delete_prepare_query?()
51+
end
52+
4253
defp get_schema_module({_raw_schema, module}) when not is_nil(module), do: module
4354
defp get_schema_module(%Ecto.Query{from: %{source: source}}), do: get_schema_module(source)
4455
defp get_schema_module(%Ecto.SubQuery{query: query}), do: get_schema_module(query)

lib/ecto/soft_delete_repo.ex

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,13 @@ defmodule Ecto.SoftDelete.Repo do
7878
NOTE: will not exclude soft deleted records if :with_deleted option passed as true
7979
"""
8080
def prepare_query(_operation, query, opts) do
81-
if has_include_deleted_at_clause?(query) || opts[:with_deleted] || !soft_deletable?(query) do
81+
skip_deleted_at_clause? =
82+
has_include_deleted_at_clause?(query) ||
83+
opts[:with_deleted] ||
84+
!soft_deletable?(query) ||
85+
!auto_include_deleted_at_clause?(query)
86+
87+
if skip_deleted_at_clause? do
8288
{query, opts}
8389
else
8490
query = from(x in query, where: is_nil(x.deleted_at))

lib/ecto/soft_delete_schema.ex

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,28 @@ defmodule Ecto.SoftDelete.Schema do
1111
import Ecto.SoftDelete.Schema
1212
1313
schema "users" do
14-
field :email, :string
14+
field :email, :string
1515
soft_delete_schema()
1616
end
1717
end
1818
19+
Options:
20+
- `:auto_exclude_from_queries?` - If false, Ecto.SoftDelete.Repo won't
21+
automatically add the necessary clause to filter out soft-deleted rows. See
22+
`Ecto.SoftDelete.Repo.prepare_query` for more info. Defaults to `true`.
23+
1924
"""
20-
defmacro soft_delete_schema do
25+
defmacro soft_delete_schema(opts \\ []) do
26+
filter_tag_definition =
27+
unless Keyword.get(opts, :auto_exclude_from_queries?, true) do
28+
quote do
29+
def skip_soft_delete_prepare_query?, do: true
30+
end
31+
end
32+
2133
quote do
22-
field :deleted_at, :utc_datetime_usec
34+
field(:deleted_at, :utc_datetime_usec)
35+
unquote(filter_tag_definition)
2336
end
2437
end
2538
end

test/soft_delete_repo_test.exs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ defmodule Ecto.SoftDelete.Repo.Test do
1313
end
1414
end
1515

16+
defmodule UserWithSkipPrepareQuery do
17+
use Ecto.Schema
18+
import Ecto.SoftDelete.Schema
19+
20+
schema "users" do
21+
field(:email, :string)
22+
soft_delete_schema(auto_exclude_from_queries?: false)
23+
end
24+
end
25+
1626
defmodule Nondeletable do
1727
use Ecto.Schema
1828

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

143+
test "includes soft deleted records if `auto_exclude_from_queries?` is false" do
144+
user = Repo.insert!(%UserWithSkipPrepareQuery{email: "[email protected]"})
145+
146+
soft_deleted_user =
147+
Repo.insert!(%UserWithSkipPrepareQuery{
148+
149+
deleted_at: DateTime.utc_now()
150+
})
151+
152+
results = UserWithSkipPrepareQuery |> Repo.all()
153+
154+
assert Enum.member?(results, user)
155+
assert Enum.member?(results, soft_deleted_user)
156+
end
157+
133158
test "works with schemas that don't have deleted_at column" do
134159
Repo.insert!(%Nondeletable{value: "stuff"})
135160
results = Nondeletable |> Repo.all()

0 commit comments

Comments
 (0)