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 👍