Skip to content

Commit 12b61b4

Browse files
committed
Allow to disable "tagged" not found errors in Repo.fetch/2
1 parent 2a6e94b commit 12b61b4

File tree

5 files changed

+91
-21
lines changed

5 files changed

+91
-21
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Added
66

77
* Ensure field/assoc/embed exists when listing errors in `flat_errors_on/3`. This prevents accidental test passes on typos in assertions like `refute_errors_on(cs, :sommtypo)`.
8+
* Add ability to disable "tagged" not found errors in `Repo.fetch/2` and friends (local to calls or global option).
89

910
## [1.0.0] - 2023-12-21
1011

config/config.exs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
import Config
22

3+
config = [
4+
migration_timestamps: [type: :utc_datetime_usec],
5+
migration_primary_key: [name: :id, type: :binary_id],
6+
database: "bitcrowd_ecto_#{config_env()}",
7+
username: "postgres",
8+
password: "postgres",
9+
hostname: "localhost",
10+
priv: "test/support/test_repo"
11+
]
12+
313
if config_env() in [:dev, :test] do
4-
config :bitcrowd_ecto, BitcrowdEcto.TestRepo,
5-
migration_timestamps: [type: :utc_datetime_usec],
6-
migration_primary_key: [name: :id, type: :binary_id],
7-
database: "bitcrowd_ecto_#{config_env()}",
8-
username: "postgres",
9-
password: "postgres",
10-
hostname: "localhost",
11-
priv: "test/support/test_repo"
14+
config :bitcrowd_ecto, BitcrowdEcto.TestRepo, config
15+
config :bitcrowd_ecto, BitcrowdEcto.TestRepoWithUntaggedNotFoundErrors, config
1216

13-
config :bitcrowd_ecto, ecto_repos: [BitcrowdEcto.TestRepo]
17+
config :bitcrowd_ecto,
18+
ecto_repos: [BitcrowdEcto.TestRepo, BitcrowdEcto.TestRepoWithUntaggedNotFoundErrors]
1419

1520
config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase
1621
end
@@ -21,6 +26,9 @@ if config_env() == :test do
2126

2227
config :bitcrowd_ecto, BitcrowdEcto.TestRepo, pool: Ecto.Adapters.SQL.Sandbox
2328

29+
config :bitcrowd_ecto, BitcrowdEcto.TestRepoWithUntaggedNotFoundErrors,
30+
pool: Ecto.Adapters.SQL.Sandbox
31+
2432
config :ex_cldr,
2533
default_backend: BitcrowdEcto.TestCldr,
2634
default_locale: "en"

lib/bitcrowd_ecto/repo.ex

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,14 @@ defmodule BitcrowdEcto.Repo do
2323
@type fetch_option ::
2424
{:lock, lock_mode | false}
2525
| {:preload, atom | list}
26-
| {:error_tag, any}
26+
| {:error_tag, false | any}
2727
| {:raise_cast_error, boolean()}
2828
| ecto_option
2929

30-
@type fetch_result :: {:ok, Ecto.Schema.t()} | {:error, {:not_found, Ecto.Queryable.t() | any}}
30+
@type fetch_result ::
31+
{:ok, Ecto.Schema.t()}
32+
| {:error, {:not_found, Ecto.Queryable.t() | any}}
33+
| {:error, :not_found}
3134

3235
@ecto_options [:prefix, :timeout, :log, :telemetry_event, :telemetry_options]
3336

@@ -39,46 +42,62 @@ defmodule BitcrowdEcto.Repo do
3942
| {:telemetry_options, any}
4043

4144
@doc """
42-
Fetches a record by primary key or returns a "tagged" error tuple.
45+
Fetches a record by primary key or returns an error tuple.
4346
4447
See `c:fetch_by/3`.
4548
"""
4649
@doc since: "0.1.0"
4750
@callback fetch(schema :: module, id :: any) :: fetch_result()
4851

4952
@doc """
50-
Fetches a record by given clauses or returns a "tagged" error tuple.
53+
Fetches a record by given clauses or returns an error tuple.
5154
5255
See `c:fetch_by/3` for options.
5356
"""
5457
@doc since: "0.1.0"
5558
@callback fetch(schema :: module, id :: any, [fetch_option()]) :: fetch_result()
5659

5760
@doc """
58-
Fetches a record by given clauses or returns a "tagged" error tuple.
61+
Fetches a record by given clauses or returns an error tuple.
5962
6063
See `c:fetch_by/3` for options.
6164
"""
6265
@doc since: "0.1.0"
6366
@callback fetch_by(queryable :: Ecto.Queryable.t(), clauses :: map | keyword) :: fetch_result()
6467

6568
@doc """
66-
Fetches a record by given clauses or returns a "tagged" error tuple.
69+
Fetches a record by given clauses or returns an error tuple.
6770
6871
- On success, the record is wrapped in a `:ok` tuple.
69-
- On error, a "tagged" error tuple is returned that contains the *original* queryable or module
70-
as the tag, e.g. `{:error, {:not_found, Account}}` for a `fetch_by(Account, id: 1)` call.
72+
- On error, an error tuple is returned
73+
74+
## Tagged error tuples
75+
76+
By default, the error tuple will be a "tagged" `:not_found` tuple, e.g.
77+
`{:error, {:not_found, Account}}` for a `fetch_by(Account, id: 1)` call, where the "tag" is
78+
the unmodified `queryable` parameter. The idea behind this is that it avoid mix-ups of
79+
naked `:not_found` errors, particularly in `with` clauses.
80+
81+
Tagging behaviour may be disabled by passing the `error_tag: false` option to return
82+
naked `{:error, :not_found}` tuples instead. For existing applications where untagged errors
83+
are the norm, one may set the `tagged_not_found_errors: false` option when using this module.
84+
85+
use BitcrowdEcto.Repo, tagged_not_found_errors: false
86+
87+
## Automatic conversion of `CastError`
7188
7289
Passing invalid values that would normally result in an `Ecto.Query.CastError` will result in
73-
a `:not_found` error tuple as well.
90+
a `:not_found` error tuple. This is useful for low-level handling of invalid UUIDs passed
91+
from a hand-edited URL to the domain layer.
7492
75-
This function can also apply row locks.
93+
This behaviour can be disabled by passing `raise_cast_error: false`.
7694
7795
## Options
7896
7997
* `lock` any of `[:no_key_update, :update]` (defaults to `false`)
8098
* `preload` allows to preload associations
8199
* `error_tag` allows to specify a custom "tag" value (instead of the queryable)
100+
or `false` to disabled tagged error tuples
82101
* `raise_cast_error` raise `CastError` instead of converting to `:not_found` (defaults to `false`)
83102
84103
## Ecto options
@@ -149,12 +168,18 @@ defmodule BitcrowdEcto.Repo do
149168
@doc since: "0.1.0"
150169
@callback advisory_xact_lock(atom | binary) :: :ok
151170

152-
defmacro __using__(_) do
171+
defmacro __using__(opts) do
172+
tagged_not_found_errors = Keyword.get(opts, :tagged_not_found_errors, true)
173+
153174
quote do
154175
alias BitcrowdEcto.Repo, as: BER
155176

156177
@behaviour BER
157178

179+
@doc false
180+
@spec __tagged_not_found_errors__() :: boolean
181+
def __tagged_not_found_errors__, do: unquote(tagged_not_found_errors)
182+
158183
@impl BER
159184
def fetch(module, id, opts \\ []) when is_atom(module) do
160185
BER.fetch(__MODULE__, module, id, opts)
@@ -206,7 +231,7 @@ defmodule BitcrowdEcto.Repo do
206231
end)
207232

208233
case result do
209-
nil -> {:error, {:not_found, Keyword.get(opts, :error_tag, queryable)}}
234+
nil -> handle_not_found_error(repo, queryable, opts)
210235
value -> {:ok, value}
211236
end
212237
end
@@ -247,6 +272,16 @@ defmodule BitcrowdEcto.Repo do
247272
end
248273
end
249274

275+
defp handle_not_found_error(repo, queryable, opts) do
276+
tag = Keyword.get(opts, :error_tag, queryable)
277+
278+
if repo.__tagged_not_found_errors__() == false or tag == false do
279+
{:error, :not_found}
280+
else
281+
{:error, {:not_found, tag}}
282+
end
283+
end
284+
250285
@doc false
251286
@spec count(module, Ecto.Queryable.t(), keyword) :: non_neg_integer
252287
def count(repo, queryable, opts) do

test/bitcrowd_ecto/repo_test.exs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
defmodule BitcrowdEcto.RepoTest do
44
use BitcrowdEcto.TestCase, async: true
55
require Ecto.Query
6+
alias BitcrowdEcto.TestRepoWithUntaggedNotFoundErrors
67

78
defp insert_test_schema(_) do
89
%{resource: insert(:test_schema)}
@@ -161,4 +162,18 @@ defmodule BitcrowdEcto.RepoTest do
161162
assert TestRepo.fetch_by(TestSchema, [id: resource.id], prefix: prefix) == {:ok, resource}
162163
end
163164
end
165+
166+
describe "error tagging can be disabled" do
167+
test "error tagging can be disabled on fetch/2, fetch/3, fetch_by/3 calls" do
168+
assert TestRepo.fetch(TestSchema, Ecto.UUID.generate(), error_tag: false) ==
169+
{:error, :not_found}
170+
end
171+
172+
test "error tagging can be disabled globally" do
173+
start_supervised!(TestRepoWithUntaggedNotFoundErrors)
174+
175+
assert TestRepoWithUntaggedNotFoundErrors.fetch(TestSchema, Ecto.UUID.generate()) ==
176+
{:error, :not_found}
177+
end
178+
end
164179
end

test/support/test_repo.ex

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,14 @@ defmodule BitcrowdEcto.TestRepo do
1010

1111
use BitcrowdEcto.Repo
1212
end
13+
14+
defmodule BitcrowdEcto.TestRepoWithUntaggedNotFoundErrors do
15+
@moduledoc false
16+
17+
use Ecto.Repo,
18+
otp_app: :bitcrowd_ecto,
19+
adapter: Ecto.Adapters.Postgres,
20+
priv: "test/support/test_repo"
21+
22+
use BitcrowdEcto.Repo, tagged_not_found_errors: false
23+
end

0 commit comments

Comments
 (0)