Skip to content

Commit b107cb0

Browse files
authored
improvement: Add prepend? opt to hooks and Ash.Subject transaction hooks (#2221)
- Adds `prepend?` opt for all hooks except for `Ash.Query.after_action` since it uses `@read_action_after_action_hooks_in_order?` * improvement(hooks): Add transaction hooks to Ash.Subject - Clean up type specs - Add before/after,around transaction hooks to Ash.Subject * improvement: Ash.Subject delegation and improvements - Update all functions to delegate to the subject type module function - Align type specs with subject type module types - Update `get_argument/3` to only return the default value when `:error` is returned from `fetch_argument/2` - Add `Ash.ActionInput.delete_argument/3` - Add `Ash.Changeset.fetch_attribute/2` - Add `Ash.Changeset.fetch_data/2` - Add `Ash.Changeset.fetch_argument_or_attribute/2` - Update `Ash.Query.fetch_argument/2` to first call `Ash.Query.new(query)` like other functions to ensure the subject is a query. - Remove `set_private_argument/3` tests because there is a bug where `new/1` doesn't add the `arguments`, but `set_argument/3` is using dot syntax to access the arguments. So, there's no way to use `set_private_argument/3` without getting the warning message. I'll submit another PR to fix this" -
1 parent c5e17c9 commit b107cb0

File tree

5 files changed

+807
-388
lines changed

5 files changed

+807
-388
lines changed

lib/ash/action_input.ex

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,7 @@ defmodule Ash.ActionInput do
507507
- `set_private_argument/3` for setting private arguments
508508
- `for_action/4` for providing initial arguments
509509
"""
510-
@spec set_argument(input :: t(), name :: atom, value :: term()) :: t()
510+
@spec set_argument(input :: t(), name :: atom | String.t(), value :: term()) :: t()
511511
def set_argument(input, argument, value) do
512512
if input.action do
513513
argument =
@@ -553,6 +553,26 @@ defmodule Ash.ActionInput do
553553
end
554554
end
555555

556+
@doc """
557+
Deletes one or more arguments from the subject.
558+
559+
## Parameters
560+
561+
* `subject` - The subject to delete arguments from
562+
* `arguments` - Single argument name or list of argument names to delete
563+
"""
564+
@spec delete_argument(
565+
input :: t(),
566+
argument_or_arguments :: atom | String.t() | list(atom | String.t())
567+
) :: t()
568+
def delete_argument(input, argument_or_arguments) do
569+
argument_or_arguments
570+
|> List.wrap()
571+
|> Enum.reduce(input, fn argument, input ->
572+
%{input | arguments: Map.delete(input.arguments, argument)}
573+
end)
574+
end
575+
556576
@doc """
557577
Sets a private argument value on the action input.
558578
@@ -998,10 +1018,15 @@ defmodule Ash.ActionInput do
9981018
"""
9991019
@spec after_action(
10001020
input :: t(),
1001-
fun :: after_action_fun()
1021+
fun :: after_action_fun(),
1022+
opts :: Keyword.t()
10021023
) :: t()
1003-
def after_action(input, func) do
1004-
%{input | after_action: input.after_action ++ [func]}
1024+
def after_action(input, func, opts \\ []) do
1025+
if opts[:prepend?] do
1026+
%{input | after_action: [func | input.after_action]}
1027+
else
1028+
%{input | after_action: input.after_action ++ [func]}
1029+
end
10051030
end
10061031

10071032
@doc """
@@ -1025,9 +1050,17 @@ defmodule Ash.ActionInput do
10251050
- `around_transaction/2` for hooks that wrap the entire transaction
10261051
- `before_action/3` for hooks that run before the action (inside transaction)
10271052
"""
1028-
@spec before_transaction(t, before_transaction_fun) :: t
1029-
def before_transaction(input, func) do
1030-
%{input | before_transaction: input.before_transaction ++ [func]}
1053+
@spec before_transaction(
1054+
input :: t(),
1055+
fun :: before_transaction_fun(),
1056+
opts :: Keyword.t()
1057+
) :: t()
1058+
def before_transaction(input, func, opts \\ []) do
1059+
if opts[:prepend?] do
1060+
%{input | before_transaction: [func | input.before_transaction]}
1061+
else
1062+
%{input | before_transaction: input.before_transaction ++ [func]}
1063+
end
10311064
end
10321065

10331066
@doc """
@@ -1051,9 +1084,17 @@ defmodule Ash.ActionInput do
10511084
- `around_transaction/2` for hooks that wrap the entire transaction
10521085
- `after_action/2` for hooks that run after the action (inside transaction)
10531086
"""
1054-
@spec after_transaction(t, after_transaction_fun) :: t
1055-
def after_transaction(input, func) do
1056-
%{input | after_transaction: input.after_transaction ++ [func]}
1087+
@spec after_transaction(
1088+
input :: t(),
1089+
fun :: after_transaction_fun(),
1090+
opts :: Keyword.t()
1091+
) :: t()
1092+
def after_transaction(input, func, opts \\ []) do
1093+
if opts[:prepend?] do
1094+
%{input | after_transaction: [func | input.after_transaction]}
1095+
else
1096+
%{input | after_transaction: input.after_transaction ++ [func]}
1097+
end
10571098
end
10581099

10591100
@doc """
@@ -1081,9 +1122,17 @@ defmodule Ash.ActionInput do
10811122
- `after_transaction/2` for hooks that run after the transaction
10821123
- `before_action/3` and `after_action/2` for hooks that run inside the transaction
10831124
"""
1084-
@spec around_transaction(t, around_transaction_fun) :: t
1085-
def around_transaction(input, func) do
1086-
%{input | around_transaction: input.around_transaction ++ [func]}
1125+
@spec around_transaction(
1126+
input :: t(),
1127+
fun :: around_transaction_fun(),
1128+
opts :: Keyword.t()
1129+
) :: t()
1130+
def around_transaction(input, func, opts \\ []) do
1131+
if opts[:prepend?] do
1132+
%{input | around_transaction: [func | input.around_transaction]}
1133+
else
1134+
%{input | around_transaction: input.around_transaction ++ [func]}
1135+
end
10871136
end
10881137

10891138
@doc false

lib/ash/changeset/changeset.ex

Lines changed: 90 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4828,6 +4828,28 @@ defmodule Ash.Changeset do
48284828
end
48294829
end
48304830

4831+
@doc """
4832+
Fetches the changing value or the original value of an attribute.
4833+
4834+
## Example
4835+
4836+
iex> changeset = Ash.Changeset.for_update(post, :update, %{title: "New Title"})
4837+
iex> Ash.Changeset.fetch_attribute(changeset, :title)
4838+
{:ok, "New Title"}
4839+
iex> Ash.Changeset.fetch_attribute(changeset, :content)
4840+
:error
4841+
"""
4842+
@spec fetch_attribute(t, atom) :: {:ok, term} | :error
4843+
def fetch_attribute(changeset, attribute) do
4844+
case fetch_change(changeset, attribute) do
4845+
{:ok, value} ->
4846+
{:ok, value}
4847+
4848+
:error ->
4849+
fetch_data(changeset, attribute)
4850+
end
4851+
end
4852+
48314853
@doc "Gets the value of an argument provided to the changeset, falling back to `Ash.Changeset.get_attribute/2` if nothing was provided."
48324854
@spec get_argument_or_attribute(t, atom) :: term
48334855
def get_argument_or_attribute(changeset, attribute) do
@@ -4837,6 +4859,25 @@ defmodule Ash.Changeset do
48374859
end
48384860
end
48394861

4862+
@doc """
4863+
Fetches the value of an argument provided to the changeset, falling back to `Ash.Changeset.fetch_attribute/2` if nothing was provided.
4864+
4865+
## Example
4866+
4867+
iex> changeset = Ash.Changeset.for_update(post, :update, %{title: "New Title"})
4868+
iex> Ash.Changeset.fetch_argument_or_attribute(changeset, :title)
4869+
{:ok, "New Title"}
4870+
iex> Ash.Changeset.fetch_argument_or_attribute(changeset, :content)
4871+
:error
4872+
"""
4873+
@spec fetch_argument_or_attribute(t, atom) :: {:ok, term} | :error
4874+
def fetch_argument_or_attribute(changeset, argument_or_attribute) do
4875+
case fetch_argument(changeset, argument_or_attribute) do
4876+
{:ok, value} -> {:ok, value}
4877+
:error -> fetch_attribute(changeset, argument_or_attribute)
4878+
end
4879+
end
4880+
48404881
@doc "Gets the new value for an attribute, or `:error` if it is not being changed."
48414882
@spec fetch_change(t, atom) :: {:ok, any} | :error
48424883
def fetch_change(changeset, attribute) do
@@ -4858,6 +4899,22 @@ defmodule Ash.Changeset do
48584899
Map.get(changeset.data, attribute)
48594900
end
48604901

4902+
@doc """
4903+
Gets the original value for an attribute, or `:error` if it is not available.
4904+
4905+
## Example
4906+
4907+
iex> changeset = Ash.Changeset.for_update(post, :update, %{title: "New Title"})
4908+
iex> Ash.Changeset.fetch_data(changeset, :title)
4909+
{:ok, "Original Title"}
4910+
iex> Ash.Changeset.fetch_data(changeset, :content)
4911+
:error
4912+
"""
4913+
@spec fetch_data(t, atom) :: {:ok, term} | :error
4914+
def fetch_data(changeset, attribute) do
4915+
Map.fetch(changeset.data, attribute)
4916+
end
4917+
48614918
@doc """
48624919
Puts a key/value in the changeset context that can be used later.
48634920
@@ -6546,9 +6603,9 @@ defmodule Ash.Changeset do
65466603
- `around_transaction/2` for hooks that wrap the entire transaction
65476604
"""
65486605
@spec before_transaction(
6549-
t(),
6550-
before_transaction_fun(),
6551-
Keyword.t()
6606+
changeset :: t(),
6607+
fun :: before_transaction_fun(),
6608+
opts :: Keyword.t()
65526609
) :: t()
65536610
def before_transaction(changeset, func, opts \\ []) do
65546611
changeset = maybe_dirty_hook(changeset, :before_transaction)
@@ -6622,9 +6679,9 @@ defmodule Ash.Changeset do
66226679
- `around_action/2` for hooks that wrap the data layer action
66236680
"""
66246681
@spec after_action(
6625-
t(),
6626-
after_action_fun(),
6627-
Keyword.t()
6682+
changeset :: t(),
6683+
fun :: after_action_fun(),
6684+
opts :: Keyword.t()
66286685
) :: t()
66296686
def after_action(changeset, func, opts \\ []) do
66306687
changeset = maybe_dirty_hook(changeset, :after_action)
@@ -6724,9 +6781,9 @@ defmodule Ash.Changeset do
67246781
- `around_transaction/2` for hooks that wrap the entire transaction
67256782
"""
67266783
@spec after_transaction(
6727-
t(),
6728-
after_transaction_fun(),
6729-
Keyword.t()
6784+
changeset :: t(),
6785+
fun :: after_transaction_fun(),
6786+
opts :: Keyword.t()
67306787
) :: t()
67316788
def after_transaction(changeset, func, opts \\ []) do
67326789
changeset = maybe_dirty_hook(changeset, :after_transaction)
@@ -6784,10 +6841,19 @@ defmodule Ash.Changeset do
67846841
- Multi-step actions guide for complex workflow patterns
67856842
"""
67866843

6787-
@spec around_action(t(), around_action_fun()) :: t()
6788-
def around_action(changeset, func) do
6844+
@spec around_action(
6845+
changeset :: t(),
6846+
fun :: around_action_fun(),
6847+
opts :: Keyword.t()
6848+
) :: t()
6849+
def around_action(changeset, func, opts \\ []) do
67896850
changeset = maybe_dirty_hook(changeset, :around_action)
6790-
%{changeset | around_action: changeset.around_action ++ [func]}
6851+
6852+
if opts[:prepend?] do
6853+
%{changeset | around_action: [func | changeset.around_action]}
6854+
else
6855+
%{changeset | around_action: changeset.around_action ++ [func]}
6856+
end
67916857
end
67926858

67936859
@doc """
@@ -6829,10 +6895,19 @@ defmodule Ash.Changeset do
68296895
- Multi-step actions guide for complex workflow patterns
68306896
"""
68316897

6832-
@spec around_transaction(t(), around_transaction_fun()) :: t()
6833-
def around_transaction(changeset, func) do
6898+
@spec around_transaction(
6899+
changeset :: t(),
6900+
fun :: around_transaction_fun(),
6901+
opts :: Keyword.t()
6902+
) :: t()
6903+
def around_transaction(changeset, func, opts \\ []) do
68346904
changeset = maybe_dirty_hook(changeset, :around_transaction)
6835-
%{changeset | around_transaction: changeset.around_transaction ++ [func]}
6905+
6906+
if opts[:prepend?] do
6907+
%{changeset | around_transaction: [func | changeset.around_transaction]}
6908+
else
6909+
%{changeset | around_transaction: changeset.around_transaction ++ [func]}
6910+
end
68366911
end
68376912

68386913
defp maybe_dirty_hook(changeset, type) do

lib/ash/query/query.ex

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,10 +1325,19 @@ defmodule Ash.Query do
13251325
- `around_transaction/2` for hooks that wrap the entire transaction
13261326
- `before_action/3` for hooks that run before the action (inside transaction)
13271327
"""
1328-
@spec before_transaction(t(), before_transaction_fun()) :: t()
1329-
def before_transaction(query, func) do
1328+
@spec before_transaction(
1329+
query :: t(),
1330+
fun :: before_transaction_fun(),
1331+
opts :: Keyword.t()
1332+
) :: t()
1333+
def before_transaction(query, func, opts \\ []) do
13301334
query = new(query)
1331-
%{query | before_transaction: query.before_transaction ++ [func]}
1335+
1336+
if opts[:prepend?] do
1337+
%{query | before_transaction: [func | query.before_transaction]}
1338+
else
1339+
%{query | before_transaction: query.before_transaction ++ [func]}
1340+
end
13321341
end
13331342

13341343
@doc """
@@ -1352,10 +1361,19 @@ defmodule Ash.Query do
13521361
- `around_transaction/2` for hooks that wrap the entire transaction
13531362
- `after_action/2` for hooks that run after the action (inside transaction)
13541363
"""
1355-
@spec after_transaction(t(), after_transaction_fun()) :: t()
1356-
def after_transaction(query, func) do
1364+
@spec after_transaction(
1365+
query :: t(),
1366+
fun :: after_transaction_fun(),
1367+
opts :: Keyword.t()
1368+
) :: t()
1369+
def after_transaction(query, func, opts \\ []) do
13571370
query = new(query)
1358-
%{query | after_transaction: query.after_transaction ++ [func]}
1371+
1372+
if opts[:prepend?] do
1373+
%{query | after_transaction: [func | query.after_transaction]}
1374+
else
1375+
%{query | after_transaction: query.after_transaction ++ [func]}
1376+
end
13591377
end
13601378

13611379
@doc """
@@ -1403,10 +1421,19 @@ defmodule Ash.Query do
14031421
- `Ash.read/2` for executing queries with hooks
14041422
"""
14051423

1406-
@spec around_transaction(t(), around_transaction_fun()) :: t()
1407-
def around_transaction(query, func) do
1424+
@spec around_transaction(
1425+
query :: t(),
1426+
fun :: around_transaction_fun(),
1427+
opts :: Keyword.t()
1428+
) :: t()
1429+
def around_transaction(query, func, opts \\ []) do
14081430
query = new(query)
1409-
%{query | around_transaction: query.around_transaction ++ [func]}
1431+
1432+
if opts[:prepend?] do
1433+
%{query | around_transaction: [func | query.around_transaction]}
1434+
else
1435+
%{query | around_transaction: query.around_transaction ++ [func]}
1436+
end
14101437
end
14111438

14121439
@doc """
@@ -1529,11 +1556,11 @@ defmodule Ash.Query do
15291556
- `Ash.read/2` for executing queries with hooks
15301557
"""
15311558
@spec after_action(
1532-
t(),
1533-
(t(), [Ash.Resource.record()] ->
1534-
{:ok, [Ash.Resource.record()]}
1535-
| {:ok, [Ash.Resource.record()], list(Ash.Notifier.Notification.t())}
1536-
| {:error, term})
1559+
query :: t(),
1560+
fun :: (t(), [Ash.Resource.record()] ->
1561+
{:ok, [Ash.Resource.record()]}
1562+
| {:ok, [Ash.Resource.record()], list(Ash.Notifier.Notification.t())}
1563+
| {:error, term})
15371564
) :: t()
15381565
# in 4.0, add an option to prepend hooks
15391566
def after_action(query, func) do
@@ -2769,9 +2796,12 @@ defmodule Ash.Query do
27692796
- `set_argument/3` for adding arguments to queries
27702797
- `for_read/4` for creating queries with arguments
27712798
"""
2772-
@spec get_argument(t, atom) :: term
2773-
def get_argument(query, argument) when is_atom(argument) do
2774-
Map.get(query.arguments, argument) || Map.get(query.arguments, to_string(argument))
2799+
@spec get_argument(t, atom | String.t()) :: term
2800+
def get_argument(query, argument) when is_atom(argument) or is_binary(argument) do
2801+
case fetch_argument(query, argument) do
2802+
{:ok, value} -> value
2803+
:error -> nil
2804+
end
27752805
end
27762806

27772807
@doc """
@@ -2804,8 +2834,10 @@ defmodule Ash.Query do
28042834
- `set_argument/3` for adding arguments to queries
28052835
- `for_read/4` for creating queries with arguments
28062836
"""
2807-
@spec fetch_argument(t, atom) :: {:ok, term} | :error
2808-
def fetch_argument(query, argument) when is_atom(argument) do
2837+
@spec fetch_argument(t, atom | String.t()) :: {:ok, term} | :error
2838+
def fetch_argument(query, argument) when is_atom(argument) or is_binary(argument) do
2839+
query = new(query)
2840+
28092841
case Map.fetch(query.arguments, argument) do
28102842
{:ok, value} ->
28112843
{:ok, value}

0 commit comments

Comments
 (0)