Ash FrameworkAF
Ash Frameworkβ€’6mo agoβ€’
6 replies
EAJ

Return resource A from an action on resource B

I am working on an invite-only application. I have a User resource and an Invitation resource. I would like to create an :accept_invitation action which validates an invitation record, creates a user, and returns the user. This is what I have attempted so far:

# on the invitation resource
    update :create_user_with_invitation do
      validate absent(:used_at)
      validate compare(:expires_at, greater_than: &DateTime.utc_now/0)
      change set_attribute(:used_at, &DateTime.utc_now/0)
      change Dreng.Changes.CreateUserFromInvitation
    end


The CreateUserFromInvitation change looks like this:
defmodule Dreng.Changes.CreateUserFromInvitation do
  use Ash.Resource.Change

  @impl true
  def change(changeset, _opts, _context) do
    Ash.Changeset.after_action(changeset, fn _changeset, invitation ->
      {:ok, %Dreng.Accounts.User{}} =
        Dreng.Accounts.manually_create_user(%{role: invitation.role, email: invitation.email},
          authorize?: false
        )
    end)
  end

  @impl true
  def atomic(changeset, _opts, _context) do
    {:ok, changeset}
  end
end

My understanding was that the return value of the after_action beccomes the return value of the whole action, but this test fails:
    test "if invitation is valid, a user with matching attributes is created" do
      invitation = generate(invitation())
      assert {:ok, %Dreng.Accounts.User{} = user} = Dreng.Accounts.accept_invitation(invitation)
    end

And in the stack trace it is because the returned struct is an Invitation.

Is what I'm trying to do (a) possible, and (b) reasonable, or should I go about it in some other way?
Solution
This seems to have done the trick:
defmodule Dreng.Changes.ValidateInvitation do
  use Ash.Resource.Change

  @impl true
  def change(changeset, _opts, _context) do
    Ash.Changeset.before_action(changeset, fn changeset ->
      Ash.Changeset.get_argument(changeset, :token)
      |> Dreng.Accounts.get_invitation_by_token()
      |> case do
        {:ok, invitation} -> validate_invitation(changeset, invitation)
        {:error, _} -> Ash.Changeset.add_error(changeset, "missing or invalid invitation")
      end
    end)
  end

  defp validate_invitation(changeset, %Dreng.Accounts.Invitation{} = invitation) do
    cond do
      invitation.used_at != nil ->
        Ash.Changeset.add_error(changeset, "invitation has already been used")

      DateTime.compare(invitation.expires_at, DateTime.utc_now()) in [:lt, :eq] ->
        Ash.Changeset.add_error(changeset, "invitation is expired")

      true ->
        changeset
        |> Ash.Changeset.force_change_attribute(:email, invitation.email)
        |> Ash.Changeset.force_change_attribute(:role, invitation.role)
    end
  end
end

Was a little surprised to see the warning when using change_attribute instead of force_change_attribute, so opened a PR with a suggestion of updating the docs as well: https://github.com/ash-project/ash/pull/2258
GitHub
Using change_attribute/2 in a before_action hook triggers a warning that the changeset has already been validated, suggests a new pattern that isn't compatible with the before_action hook. ...
Update before_action docs to use force_change_attribute/2 by jakobs...
Was this page helpful?