Ecto.Multi Usage

edwin:

I saw only one post about how to use Ecto.Multi it directed me to Ash.Flow what I didn’t find an example usage. For context I have a record I want fetch using token provided its valid, after I get the record successfully I want to update a field on it and finally on success issue a JWT or on error just return the error. How will this look like in the update function ?. Is this a candidate for to consider using ManualActions ?

zachdaniel:

It depends 😄 There are a few options. Are you looking to hook this up to graphql/json_api?

edwin:

Yes into graphql

zachdaniel:

I think you can likely get a way with some regular-old-actions

zachdaniel:

read :get_by_token do
  get? true
  argument :token, :string, allow_nil?: false

  prepare fn query, _ -> 
    if is_valid?(query.arguments.token) do
      Ash.Query.filter(query, token == ^query.arguments.token)
    else
      Ash.Query.add_error(query, field: :token, message: "is invalid")
    end
  end
end

update :update do
  # your update action
end

graphql do
  ...
  mutations do
    update :update_thing, :update do
      read_action :get_by_token
      identity false
    end
  end
end

zachdaniel:

That should give you something like this:

updateThing(token: "token", input: {...update input}) {
  result {}
}

edwin:

Thanks let me try this out 👍

edwin:

elixir prepare fn query, _ -> token = query.arguments.token |> Utils.hash_token() |> Base.encode64() Ash.Query.filter(query, token == ^token) end
I get two errors from this token is flagged as undefined but it exists in my resource attributes and ^token cannot be used outside of match clauses.

zachdaniel:

You need to require Ash.Query at the top of your resource

edwin:

thanks that got it the first part working .

edwin:

lastly, what is the acceptance criteria for the fetched resource in read_action . for update. I tried elixir argument :user , :struct which was a bad idea . elixir argument :email, :string but the above should have worked but I get The field "email" is not unique in type "UpdateUserInput I have identities setup elixir identities do identity :email, [:email] identity :token, [:token] end for uniqueness and the unique_index is migrated in my migrations file. whats missing and how do I receive the user fetched from read_action ?

zachdaniel:

By default, update actions accept all public writable attributes

zachdaniel:

Adding an argument for :email is unnecessary

zachdaniel:

You’d say accept [:email]

zachdaniel:

I’ll fix the error though in future versions.

edwin:

okay and how is the token param passed from the update to read_action read_action because that is failing elixir key :token not found in: %{}

zachdaniel:

does the read action have an argument?

zachdaniel:

you’d need to have the token argument on the read action

moxley:

I’m picking this up from where <@653498934274293780> left. I added an :authenticate_by_token action and GQL query, because that seemed more appropriate:

graphql do
  update :authenticate_by_token, :authenticate_by_token do
    read_action :get_by_token
    identity false
  end
end

actions do
  read :get_by_token do
    get? true
    argument :confirmation_token, :string, allow_nil?: false

    prepare fn query, _ ->
      # query.errors has an %Ash.Error.Query.Required{} error,
      # that says :confirmation_token is required
      
      # TBD
      query
    end
  end

  update :authenticate_by_token do
    accept [:confirmation_token]

    # This is not called, because of the error in :get_by_token
    change fn changeset, _struct ->
      # TBD
      {:ok, changeset}
    end
  end
end

attributes do
  attribute :confirmation_token, :string do
    allow_nil? false
  end
end

moxley:

The :authenticate_by_token uses :get_by_token to get the record (Customer). However, :get_by_token fails because of a missing :confirmation_token , even though I am passing that.

moxley:

When I call :get_by_token directly (there’s a GQL query for that too), it works fine.

zachdaniel:

🤔

zachdaniel:

can I see how you’re calling it?

moxley:

Like this:

  describe "authenticate_by_token" do
    @authenticate_by_token """
      mutation ($confirmationToken: String!){
        authenticateByToken(confirmationToken: $confirmationToken) {
          result {
            email
            expires_at
          }
          errors {
            fields
            message
          }
        }
      }
    """

    test "authenticates customer if token is valid", %{conn: conn} do
      token = generate_token()

      customer =
        insert(:customer,
          confirmation_token: token,
          confirmed_at: nil,
          expires_at: GF.Util.Dates.seconds_ahead(1)
        )

      variables = %{confirmationToken: Base.encode64(token)}

      conn = post(conn, "/api/gql", query: @authenticate_by_token, variables: variables)

      json_response = json_response(conn, 200)
      result = json_response["data"]
      dbg(result)

      assert result["email"] == customer.email
    end
  end

moxley:

Here’s the get_by_token action:

  actions do
    read :get_by_token do
      get? true
      argument :confirmation_token, :string, allow_nil?: false
      prepare fn query, _ ->
        ...
      end
    end
  end

moxley:

And here’s get_by_token being called:

  @get_by_token """
  query ($confirmationToken: String!) {
    getByToken(confirmationToken: $confirmationToken) {
        email
        contact_first_name
        contact_last_name
        expires_at
    }
  }
  """

  describe "get_by_token" do
    test "returns customer if token is valid", %{conn: conn} do
      token = generate_token()

      customer =
        insert(:customer,
          confirmation_token: token,
          confirmed_at: nil,
          expires_at: GF.Util.Dates.seconds_ahead(1)
        )

      variables = %{confirmationToken: Base.encode64(token)}
      conn = post(conn, "/api/gql", query: @get_by_token, variables: variables)
      json_response = json_response(conn, 200)
      %{"data" => %{"getByToken" => values}} = json_response
      assert values["email"] == customer.email
    end

moxley:

I think I found out what’s causing the issue: confirmation_token is an attribute of the resource. If I switch to different name, like :token that isn’t an attribute, I don’t see errors

moxley:

Okay, yeah, that’s the issue. It looks like the underlying Ash logic isn’t passing :confirmation_token to the read_action when that field is an attribute of the resource. It only works when the field isn’t the same as an attribute of the resource.

zachdaniel:

Very interesting

zachdaniel:

Trying to figure out how that would be happening

zachdaniel:

have it reproduced

zachdaniel:

question

zachdaniel:

nvm

zachdaniel:

<@643532378756743237> fixed in 0.25.3

zachdaniel:

Sorry it took so long to figure out 😢

moxley:

Yay!!! Thank you <@197905764424089601> !

edwin:

🎉 thank you <@197905764424089601>

edwin:

tested it and it works

moxley:

Hey <@197905764424089601>, after integrating 0.25.3 and then merging some newer changes into our main branch, we’re seeing this new error:

     13:35:07.392 request_id=F2GRfkYnaU7-Lr0AAAQB [error] 87889857-f761-4022-8253-104f8fc26a67: Exception raised while resolving query.
     
     ** (KeyError) key :arguments not found in: nil. If you are using the dot syntax, such as map.field, make sure the left-hand side of the dot is a map
         (ash_graphql 0.25.3) lib/graphql/resolver.ex:1439: AshGraphql.Graphql.Resolver.set_query_arguments/3
         (ash_graphql 0.25.3) lib/graphql/resolver.ex:960: AshGraphql.Graphql.Resolver.mutate/2
         (absinthe 1.7.1) lib/absinthe/phase/document/execution/resolution.ex:232: Absinthe.Phase.Document.Execution.Resolution.reduce_resolution/1
         (absinthe 1.7.1) lib/absinthe/phase/document/execution/resolution.ex:187: Absinthe.Phase.Document.Execution.Resolution.do_resolve_field/3
         (absinthe 1.7.1) lib/absinthe/phase/document/execution/resolution.ex:172: Absinthe.Phase.Document.Execution.Resolution.do_resolve_fields/6
         (absinthe 1.7.1) lib/absinthe/phase/document/execution/resolution.ex:143: Absinthe.Phase.Document.Execution.Resolution.resolve_fields/4

zachdaniel:

fixed in 0.25.4

moxley:

Got it 👍