Create action with multiple nested resources

This is the action to register an account. There are 3 resources at play. An account has many memberships and every membership belongs to a user. The membership resource is simply as has_and_belongs_to_many with a role on it. When a new account is created, it should always create the account and membership for the owner/user. The user resource has an identity on email and is only created if the user doesn't already exist. What happens is the membership fails to insert because the user_id is nil.
actions do
create :register do
accept([:name, :slug])
allow_nil_input([:slug])

argument(:email, :string, allow_nil?: false)
...

change(fn changeset, _context ->
email = changeset |> Ash.Changeset.get_argument(:email)

changeset
|> Ash.Changeset.manage_relationship(
:memberships,
[%{role: "owner", user: %{email: email}}],
type: :create
)
end)
end
end
actions do
create :register do
accept([:name, :slug])
allow_nil_input([:slug])

argument(:email, :string, allow_nil?: false)
...

change(fn changeset, _context ->
email = changeset |> Ash.Changeset.get_argument(:email)

changeset
|> Ash.Changeset.manage_relationship(
:memberships,
[%{role: "owner", user: %{email: email}}],
type: :create
)
end)
end
end
8 Replies
ZachDaniel
ZachDaniel3y ago
Hey there! Sorry it took me so long to get back to you I'd have to see all of the actions in question, but I think especially given that these are create actions, you might be best served with a much simpler pattern of issuing creates/upserts instead of managing relationships.
actions do
create :register do
accept [:name, :slug]
allow_nil_input [:slug]

argument :email, :string, allow_nil?: false

change fn changeset, _context ->
Ash.Changeset.after_action(changeset, fn changeset, result ->
user = User.upsert!(email)
Membership.upsert!(result.id, user.id, "owner")
{:ok, result}
end)
end
end
end
actions do
create :register do
accept [:name, :slug]
allow_nil_input [:slug]

argument :email, :string, allow_nil?: false

change fn changeset, _context ->
Ash.Changeset.after_action(changeset, fn changeset, result ->
user = User.upsert!(email)
Membership.upsert!(result.id, user.id, "owner")
{:ok, result}
end)
end
end
end
Robert Graff
Robert GraffOP3y ago
That makes a lot of sense. I didn’t consider using after_action. What am I doing wrong? It's not attempting the upsert because the eager check fails but removing the eager check causes a DSL error.
identities do
identity(:unique_email, [:email]) do
eager_check_with(Iterup.Users)
end
end

actions do
create :upsert do
accept([:email, :name])
upsert?(true)
upsert_identity(:unique_email)
end
end
identities do
identity(:unique_email, [:email]) do
eager_check_with(Iterup.Users)
end
end

actions do
create :upsert do
accept([:email, :name])
upsert?(true)
upsert_identity(:unique_email)
end
end
here's my test output. The first two inserts are part of the setup. The query is the eager check, I assume.
16:33:45.414 [debug] QUERY OK db=6.4ms queue=2.3ms
INSERT INTO "accounts" ("id","inserted_at","name","slug","updated_at") VALUES ($1,$2,$3,$4,$5) ["5dc49df1-e95b-41f5-a2bd-60f5f89b2c1f", ~U[2023-05-02 16:33:45.370906Z], "Miller-McGlynn", "miller-mcglynn", ~U[2023-05-02 16:33:45.370906Z]]
16:33:45.427 [debug] QUERY OK db=0.8ms queue=0.4ms
INSERT INTO "users" ("email","hashed_password","id","inserted_at","name","updated_at") VALUES ($1,$2,$3,$4,$5,$6) ["[email protected]", "$2b$04$f0O3GRMLxDCQMqE147CvveRbjTY69yjbGJzYLYuBbtYxGW9v3oJ5y", "576e0d28-4705-4cf2-9a2c-28840c5773be", ~U[2023-05-02 16:33:45.422754Z], "Lisa Moen", ~U[2023-05-02 16:33:45.422754Z]]
16:33:45.552 [debug] QUERY OK source="users" db=0.5ms queue=0.5ms
SELECT u0."id", u0."email", u0."name", u0."avatar_path", u0."hashed_password", u0."confirmed_at", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."email"::citext = $1::citext) LIMIT $2 ["[email protected]", 1]


1) test upsert/1 will return existing user if exists (Iterup.Users.UserTest)
test/iterup/users/user_test.exs:35
** (Ash.Error.Invalid) Input Invalid

* email: has already been taken.
code: existing_user = User.upsert!(%{email: user.email})
stacktrace:
(ash 2.9.1) lib/ash/api/api.ex:1749: Ash.Api.unwrap_or_raise!/3
test/iterup/users/user_test.exs:36: (test)


Finished in 0.4 seconds (0.4s async, 0.00s sync)
12 tests, 1 failure, 11 excluded
16:33:45.414 [debug] QUERY OK db=6.4ms queue=2.3ms
INSERT INTO "accounts" ("id","inserted_at","name","slug","updated_at") VALUES ($1,$2,$3,$4,$5) ["5dc49df1-e95b-41f5-a2bd-60f5f89b2c1f", ~U[2023-05-02 16:33:45.370906Z], "Miller-McGlynn", "miller-mcglynn", ~U[2023-05-02 16:33:45.370906Z]]
16:33:45.427 [debug] QUERY OK db=0.8ms queue=0.4ms
INSERT INTO "users" ("email","hashed_password","id","inserted_at","name","updated_at") VALUES ($1,$2,$3,$4,$5,$6) ["[email protected]", "$2b$04$f0O3GRMLxDCQMqE147CvveRbjTY69yjbGJzYLYuBbtYxGW9v3oJ5y", "576e0d28-4705-4cf2-9a2c-28840c5773be", ~U[2023-05-02 16:33:45.422754Z], "Lisa Moen", ~U[2023-05-02 16:33:45.422754Z]]
16:33:45.552 [debug] QUERY OK source="users" db=0.5ms queue=0.5ms
SELECT u0."id", u0."email", u0."name", u0."avatar_path", u0."hashed_password", u0."confirmed_at", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."email"::citext = $1::citext) LIMIT $2 ["[email protected]", 1]


1) test upsert/1 will return existing user if exists (Iterup.Users.UserTest)
test/iterup/users/user_test.exs:35
** (Ash.Error.Invalid) Input Invalid

* email: has already been taken.
code: existing_user = User.upsert!(%{email: user.email})
stacktrace:
(ash 2.9.1) lib/ash/api/api.ex:1749: Ash.Api.unwrap_or_raise!/3
test/iterup/users/user_test.exs:36: (test)


Finished in 0.4 seconds (0.4s async, 0.00s sync)
12 tests, 1 failure, 11 excluded
ZachDaniel
ZachDaniel3y ago
What DSL error do you get without the eager_check_with?
Robert Graff
Robert GraffOP3y ago
== Compilation error in file lib/iterup/users/resources/user.ex ==
** (Spark.Error.DslError) [Iterup.Users.User]
identities -> identity:
The unique_email identity on the resource `Iterup.Users.User` needs the `eager_check_with` property set so that inhibited changes are still validated.
(spark 1.0.8) lib/spark/dsl/extension.ex:613: Spark.Dsl.Extension.raise_transformer_error/2
== Compilation error in file lib/iterup/users/resources/user.ex ==
** (Spark.Error.DslError) [Iterup.Users.User]
identities -> identity:
The unique_email identity on the resource `Iterup.Users.User` needs the `eager_check_with` property set so that inhibited changes are still validated.
(spark 1.0.8) lib/spark/dsl/extension.ex:613: Spark.Dsl.Extension.raise_transformer_error/2
ZachDaniel
ZachDaniel3y ago
🤔 I see I've just pushed something up to main that might help Sorry, that actually probably broke stuff, lemme fix
Robert Graff
Robert GraffOP3y ago
Was just looking at the Ash CI errors
ZachDaniel
ZachDaniel3y ago
okay, should be good now 🙂
Robert Graff
Robert GraffOP3y ago
Checking it out works! 🎉

Did you find this page helpful?