Ecto.Multi Usage

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 ?
18 Replies
ZachDaniel
ZachDaniel2y ago
It depends 😄 There are a few options. Are you looking to hook this up to graphql/json_api?
edwinofdawn
edwinofdawnOP2y ago
Yes into graphql
ZachDaniel
ZachDaniel2y ago
I think you can likely get a way with some regular-old-actions
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
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
That should give you something like this:
updateThing(token: "token", input: {...update input}) {
result {}
}
updateThing(token: "token", input: {...update input}) {
result {}
}
edwinofdawn
edwinofdawnOP2y ago
Thanks let me try this out 👍 elixir

prepare fn query, _ ->
token = query.arguments.token |> Utils.hash_token() |> Base.encode64()
Ash.Query.filter(query, token == ^token)
end

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
ZachDaniel2y ago
You need to require Ash.Query at the top of your resource
edwinofdawn
edwinofdawnOP2y ago
thanks that got it the first part working . lastly, what is the acceptance criteria for the fetched resource in read_action. for update. I tried elixir

argument :user , :struct

argument :user , :struct
which was a bad idea . elixir

argument :email, :string

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

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
ZachDaniel2y ago
By default, update actions accept all public writable attributes Adding an argument for :email is unnecessary You’d say accept [:email] I’ll fix the error though in future versions.
edwinofdawn
edwinofdawnOP2y ago
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: %{}

key :token not found in: %{}
ZachDaniel
ZachDaniel2y ago
does the read action have an argument? you'd need to have the token argument on the read action
moxley
moxley2y ago
I'm picking this up from where @edwinofdawn 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
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
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. When I call :get_by_token directly (there's a GQL query for that too), it works fine.
ZachDaniel
ZachDaniel2y ago
🤔 can I see how you're calling it?
moxley
moxley2y ago
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
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
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
actions do
read :get_by_token do
get? true
argument :confirmation_token, :string, allow_nil?: false
prepare fn query, _ ->
...
end
end
end
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
@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
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 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
ZachDaniel2y ago
Very interesting Trying to figure out how that would be happening have it reproduced question nvm @moxley fixed in 0.25.3 Sorry it took so long to figure out 😢
moxley
moxley2y ago
Yay!!! Thank you @Zach Daniel !
edwinofdawn
edwinofdawnOP2y ago
🎉 thank you @Zach Daniel tested it and it works
moxley
moxley2y ago
Hey @Zach Daniel, 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
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
ZachDaniel2y ago
fixed in 0.25.4
moxley
moxley2y ago
Got it 👍

Did you find this page helpful?