Intercept Identities Behaviour

I have this code elixir
identities do
identity :email, [:email]
identity :confirmation_token, [:confirmation_token]
eager_check_with: Ash.API
end
...

change fn changeset, _struct ->
token = Utils.generate_token()

changeset
|> Ash.Changeset.change_attribute(
:confirmation_token,
Utils.hash_token(token) |> Base.encode64()
)
|> Ash.Changeset.change_attribute(
:expires_at,
NaiveDateTime.add(Dates.now(), @token_expiry_in_seconds)
)
|> Ash.Changeset.after_action(fn changeset, customer ->
#send email here

{:ok, customer}
end)
end
identities do
identity :email, [:email]
identity :confirmation_token, [:confirmation_token]
eager_check_with: Ash.API
end
...

change fn changeset, _struct ->
token = Utils.generate_token()

changeset
|> Ash.Changeset.change_attribute(
:confirmation_token,
Utils.hash_token(token) |> Base.encode64()
)
|> Ash.Changeset.change_attribute(
:expires_at,
NaiveDateTime.add(Dates.now(), @token_expiry_in_seconds)
)
|> Ash.Changeset.after_action(fn changeset, customer ->
#send email here

{:ok, customer}
end)
end
Incase of a unique error by email found using eager_check_with. rerun change again and upsert the record raising the error ? so that the API never errors out.
7 Replies
ZachDaniel
ZachDaniel•2y ago
🤔 I think eager_check_with wouldn't be interceptable like that but I think if you did pre_check_with then you could do it You might need to do some manual work on this one, but what you could try is using pre_check_with instead of eager_check_with and then in your change adding a after_transaction that looks for your specific kind of error
change fn changeset, _struct ->
Ash.Changeset.after_transaction(changeset, fn
changeset, {:ok, result} ->
{:ok, result}

changeset, {:error, error} ->
# if its an invalid error about `confirmation_token` being taken
# then call the action again. If you return `{:ok, result}` from this function then the action won't fail.
end)
end
change fn changeset, _struct ->
Ash.Changeset.after_transaction(changeset, fn
changeset, {:ok, result} ->
{:ok, result}

changeset, {:error, error} ->
# if its an invalid error about `confirmation_token` being taken
# then call the action again. If you return `{:ok, result}` from this function then the action won't fail.
end)
end
edwinofdawn
edwinofdawnOP•2y ago
Nice let me try this Update my code to this elixir
change fn changeset, _struct ->
token = Utils.generate_token()

changeset
|> Ash.Changeset.change_attribute(
:confirmation_token,
Utils.hash_token(token) |> Base.encode64()
)
|> Ash.Changeset.before_action(fn changeset ->
IO.inspect(changeset)
changeset
end)
|> Ash.Changeset.after_action(fn
_changeset, {:ok, result} ->
{:ok, _} =
EmailWorker.enqueue(%{
customer: result,
token: Base.encode64(token)
})

{:ok, result}

_changeset, {:error, error} ->
IO.inspect(error, label: "error")
# generate token
token = Utils.generate_token()
{:ok, customer} =
GF.Customers.Customer
|> Ash.Changeset.for_update(:update, %{confirmation_token: token})
|> GF.Ash.update()

# send email
{:ok, customer}
end)
change fn changeset, _struct ->
token = Utils.generate_token()

changeset
|> Ash.Changeset.change_attribute(
:confirmation_token,
Utils.hash_token(token) |> Base.encode64()
)
|> Ash.Changeset.before_action(fn changeset ->
IO.inspect(changeset)
changeset
end)
|> Ash.Changeset.after_action(fn
_changeset, {:ok, result} ->
{:ok, _} =
EmailWorker.enqueue(%{
customer: result,
token: Base.encode64(token)
})

{:ok, result}

_changeset, {:error, error} ->
IO.inspect(error, label: "error")
# generate token
token = Utils.generate_token()
{:ok, customer} =
GF.Customers.Customer
|> Ash.Changeset.for_update(:update, %{confirmation_token: token})
|> GF.Ash.update()

# send email
{:ok, customer}
end)
The problem ispre_check_with clearly states it can only be used in a before_action hook. The above solution does not work . I can only see the error being thrown by the database("Postgres ") itself.
ZachDaniel
ZachDaniel•2y ago
pre_check_with should check once when the action is submitted. You’re not seeing that?
moxley
moxley•2y ago
@Zach Daniel Are we taking the correct approach? We're trying to build an upsert-style action that either creates a customer or updates the customer, depending on whether the email matches a record in the database. I see this section on "upserts" in the Identities documentation: https://ash-hq.org/docs/guides/ash/latest/identities#using-upserts And that suggests we should be using it. Would that make a big difference in the approach? I'm still trying to understand pre_check_with, and what it does, and how to use it correctly. Or does using pre_check_with become moot if we make this an actual upsert?
ZachDaniel
ZachDaniel•2y ago
yes 🙂 So pre_check_with and eager_check_with allow you to basically show a validation error early on unique constraint conflicts like if you wanted a sign up form to show an error on email being taken early on you would do eager_check_with to have it validated on every validate. If you did pre_check_with we'd check before submitting the action. So if you want to upsert, you'd likely do something like this:
update :create_or_confirm do
accept [:email, :confirmation_token]
upsert? true
upsert_identity :unique_email
end
update :create_or_confirm do
accept [:email, :confirmation_token]
upsert? true
upsert_identity :unique_email
end
And if you want to accept additional fields to be used only in creation, you'd do:
update :create_or_confirm do
accept [:email, :confirmation_token, :foo, :bar]
upsert? true
upsert_identity :unique_email
upsert_fields [:confirmation_token]
end
update :create_or_confirm do
accept [:email, :confirmation_token, :foo, :bar]
upsert? true
upsert_identity :unique_email
upsert_fields [:confirmation_token]
end
That would create a user w/ email, confirmation_token, foo and bar, and if the user already exists with that email, it would update their confirmation_token only
moxley
moxley•2y ago
It's working! I had your solution in place by the time you sent it to me
create :create do
upsert? true
upsert_identity :email
accept [:contact_first_name, :contact_last_name, :email]

# generate a token and hash it
change fn changeset, _struct ->
token = Utils.generate_token()

changeset
|> Ash.Changeset.change_attribute(
:confirmation_token,
Utils.hash_token(token) |> Base.encode64()
)
|> Ash.Changeset.after_action(fn
_changeset, customer ->
{:ok, _} = EmailWorker.enqueue(%{customer: customer, token: Base.encode64(token)})
{:ok, customer}
end)
end
end
create :create do
upsert? true
upsert_identity :email
accept [:contact_first_name, :contact_last_name, :email]

# generate a token and hash it
change fn changeset, _struct ->
token = Utils.generate_token()

changeset
|> Ash.Changeset.change_attribute(
:confirmation_token,
Utils.hash_token(token) |> Base.encode64()
)
|> Ash.Changeset.after_action(fn
_changeset, customer ->
{:ok, _} = EmailWorker.enqueue(%{customer: customer, token: Base.encode64(token)})
{:ok, customer}
end)
end
end
Thanks @Zach Daniel ! Part of the problem was that we were using after_action() incorrectly. The anon fn was matching the wrong pattern.
ZachDaniel
ZachDaniel•2y ago
ah, yeah that works for after_transaction after_action only runs on successes and just gets changeset, result

Did you find this page helpful?