Identity on two attributes causing errors
I have an identity on two attributes (context, and token). I would expect this to require a unique context <> token. But it's not allowing duplicate contexts.
Here's my resource:
Here's the error:
What am I missing?
defmodule Iterup.Account.UserToken do
use Ash.Resource,
data_layer: AshPostgres.DataLayer
@rand_size 32
postgres do
table("user_tokens")
repo(Iterup.Repo)
end
attributes do
uuid_primary_key(:id)
attribute :sent_to, :ci_string, allow_nil?: false
attribute :token, :binary, allow_nil?: false
attribute :context, :string, allow_nil?: false
attribute :confirmed_at, :utc_datetime
create_timestamp :inserted_at
end
relationships do
belongs_to :user, Iterup.Account.User
end
identities do
identity :context_token, [:context, :token]
end
code_interface do
define_for(Iterup.Account)
define(:create_session_token, action: :session_token)
end
actions do
create :session_token do
accept [:sent_to]
argument :user_id, :uuid, allow_nil?: false
change manage_relationship(:user_id, :user, type: :append)
change set_attribute(:token, :crypto.strong_rand_bytes(@rand_size))
change set_attribute(:context, "session")
end
end
end
defmodule Iterup.Account.UserToken do
use Ash.Resource,
data_layer: AshPostgres.DataLayer
@rand_size 32
postgres do
table("user_tokens")
repo(Iterup.Repo)
end
attributes do
uuid_primary_key(:id)
attribute :sent_to, :ci_string, allow_nil?: false
attribute :token, :binary, allow_nil?: false
attribute :context, :string, allow_nil?: false
attribute :confirmed_at, :utc_datetime
create_timestamp :inserted_at
end
relationships do
belongs_to :user, Iterup.Account.User
end
identities do
identity :context_token, [:context, :token]
end
code_interface do
define_for(Iterup.Account)
define(:create_session_token, action: :session_token)
end
actions do
create :session_token do
accept [:sent_to]
argument :user_id, :uuid, allow_nil?: false
change manage_relationship(:user_id, :user, type: :append)
change set_attribute(:token, :crypto.strong_rand_bytes(@rand_size))
change set_attribute(:context, "session")
end
end
end
# First call is successful
session_token_1 = UserToken.create_session_token!(%{user_id: user.id, sent_to: user.email})
...
# Second call is unsuccessful
session_token_2 = UserToken.create_session_token!(%{user_id: user.id, sent_to: user.email})
...
↳ anonymous fn/3 in Ash.Changeset.with_hooks/3, at: lib/ash/changeset/changeset.ex:1573
** (Ash.Error.Invalid) Input Invalid
* Invalid value provided for context: has already been taken.
# First call is successful
session_token_1 = UserToken.create_session_token!(%{user_id: user.id, sent_to: user.email})
...
# Second call is unsuccessful
session_token_2 = UserToken.create_session_token!(%{user_id: user.id, sent_to: user.email})
...
↳ anonymous fn/3 in Ash.Changeset.with_hooks/3, at: lib/ash/changeset/changeset.ex:1573
** (Ash.Error.Invalid) Input Invalid
* Invalid value provided for context: has already been taken.
4 Replies
My migration looks good to me:
create table(:user_tokens, primary_key: false) do
add :id, :uuid, null: false, default: fragment("uuid_generate_v4()"), primary_key: true
add :sent_to, :citext, null: false
add :token, :binary, null: false
add :context, :text, null: false
add :inserted_at, :utc_datetime_usec, null: false, default: fragment("now()")
add :user_id,
references(:users,
column: :id,
name: "user_tokens_user_id_fkey",
type: :uuid,
prefix: "public"
)
end
create table(:user_tokens, primary_key: false) do
add :id, :uuid, null: false, default: fragment("uuid_generate_v4()"), primary_key: true
add :sent_to, :citext, null: false
add :token, :binary, null: false
add :context, :text, null: false
add :inserted_at, :utc_datetime_usec, null: false, default: fragment("now()")
add :user_id,
references(:users,
column: :id,
name: "user_tokens_user_id_fkey",
type: :uuid,
prefix: "public"
)
end
🤔 did it originally have just
:context
in the list?
What happens if you try to insert into the db directly?If I insert directly into the database, it works as expected.
When I do it from iex, here's the SQL error generated:
Argh! I see the problem. The random is token isn't being regenerated each time.
Here's my solution to generate a random token. Welcome any feedback as I'm still learning best practices.
iterup_dev=# \d+ user_tokens
....
Indexes:
"user_tokens_pkey" PRIMARY KEY, btree (id)
"user_tokens_context_token_index" UNIQUE, btree (context, token)
Foreign-key constraints:
"user_tokens_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id)
Access method: heap
iterup_dev=# INSERT INTO user_tokens (sent_to, token, context, user_id) VALUES ('[email protected]', 'token1', 'session', '387ed47c-7799-4571-b8e2-5e53b0a54c87');
INSERT 0 1
iterup_dev=# INSERT INTO user_tokens (sent_to, token, context, user_id) VALUES ('[email protected]', 'token1', 'session', '387ed47c-7799-4571-b8e2-5e53b0a54c87');
ERROR: duplicate key value violates unique constraint "user_tokens_context_token_index"
DETAIL: Key (context, token)=(session, \x746f6b656e31) already exists.
iterup_dev=# INSERT INTO user_tokens (sent_to, token, context, user_id) VALUES ('[email protected]', 'token2', 'session', '387ed47c-7799-4571-b8e2-5e53b0a54c87');
iterup_dev=# \d+ user_tokens
....
Indexes:
"user_tokens_pkey" PRIMARY KEY, btree (id)
"user_tokens_context_token_index" UNIQUE, btree (context, token)
Foreign-key constraints:
"user_tokens_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id)
Access method: heap
iterup_dev=# INSERT INTO user_tokens (sent_to, token, context, user_id) VALUES ('[email protected]', 'token1', 'session', '387ed47c-7799-4571-b8e2-5e53b0a54c87');
INSERT 0 1
iterup_dev=# INSERT INTO user_tokens (sent_to, token, context, user_id) VALUES ('[email protected]', 'token1', 'session', '387ed47c-7799-4571-b8e2-5e53b0a54c87');
ERROR: duplicate key value violates unique constraint "user_tokens_context_token_index"
DETAIL: Key (context, token)=(session, \x746f6b656e31) already exists.
iterup_dev=# INSERT INTO user_tokens (sent_to, token, context, user_id) VALUES ('[email protected]', 'token2', 'session', '387ed47c-7799-4571-b8e2-5e53b0a54c87');
iex(7)> UserToken.create_session_token!(%{user_id: user.id, sent_to: user.email})
[debug] QUERY OK db=0.2ms idle=488.4ms
begin []
↳ anonymous fn/3 in Ash.Changeset.with_hooks/3, at: lib/ash/changeset/changeset.ex:1573
[debug] QUERY OK source="users" db=1.4ms
SELECT u0."id", u0."email", u0."name", u0."avatar_path", u0."hashed_password", u0."confirmed_at", u0."inserted_at", u0."updated_at", u0."account_id" FROM "users" AS u0 WHERE (u0."id"::uuid = $1::uuid) ["cfc81eb1-86e4-4352-94e4-904c6fe3f437"]
↳ AshPostgres.DataLayer.run_query/2, at: lib/data_layer.ex:613
[debug] QUERY ERROR db=6.3ms
INSERT INTO "user_tokens" ("context","id","inserted_at","sent_to","token","user_id") VALUES ($1,$2,$3,$4,$5,$6) ["session", "67cb86dd-1f5e-4a4e-9372-d941a85ea26e", ~U[2023-04-19 15:15:53.873539Z], #Ash.CiString<"[email protected]">, <<45, 255, 213, 65, 128, 102, 51, 110, 251, 18, 28, 121, 107, 85, 235, 120, 248, 59, 7, 109, 93, 10, 74, 218, 108, 226, 122, 3, 67, 71, 42, 231>>, "cfc81eb1-86e4-4352-94e4-904c6fe3f437"]
↳ AshPostgres.DataLayer.create/2, at: lib/data_layer.ex:1037
[debug] QUERY OK db=0.1ms
rollback []
↳ anonymous fn/3 in Ash.Changeset.with_hooks/3, at: lib/ash/changeset/changeset.ex:1573
** (Ash.Error.Invalid) Input Invalid
* Invalid value provided for context: has already been taken.
nil
iex(7)> UserToken.create_session_token!(%{user_id: user.id, sent_to: user.email})
[debug] QUERY OK db=0.2ms idle=488.4ms
begin []
↳ anonymous fn/3 in Ash.Changeset.with_hooks/3, at: lib/ash/changeset/changeset.ex:1573
[debug] QUERY OK source="users" db=1.4ms
SELECT u0."id", u0."email", u0."name", u0."avatar_path", u0."hashed_password", u0."confirmed_at", u0."inserted_at", u0."updated_at", u0."account_id" FROM "users" AS u0 WHERE (u0."id"::uuid = $1::uuid) ["cfc81eb1-86e4-4352-94e4-904c6fe3f437"]
↳ AshPostgres.DataLayer.run_query/2, at: lib/data_layer.ex:613
[debug] QUERY ERROR db=6.3ms
INSERT INTO "user_tokens" ("context","id","inserted_at","sent_to","token","user_id") VALUES ($1,$2,$3,$4,$5,$6) ["session", "67cb86dd-1f5e-4a4e-9372-d941a85ea26e", ~U[2023-04-19 15:15:53.873539Z], #Ash.CiString<"[email protected]">, <<45, 255, 213, 65, 128, 102, 51, 110, 251, 18, 28, 121, 107, 85, 235, 120, 248, 59, 7, 109, 93, 10, 74, 218, 108, 226, 122, 3, 67, 71, 42, 231>>, "cfc81eb1-86e4-4352-94e4-904c6fe3f437"]
↳ AshPostgres.DataLayer.create/2, at: lib/data_layer.ex:1037
[debug] QUERY OK db=0.1ms
rollback []
↳ anonymous fn/3 in Ash.Changeset.with_hooks/3, at: lib/ash/changeset/changeset.ex:1573
** (Ash.Error.Invalid) Input Invalid
* Invalid value provided for context: has already been taken.
nil
actions do
create :session_token do
accept [:sent_to]
argument :user_id, :uuid, allow_nil?: false
change manage_relationship(:user_id, :user, type: :append)
change set_attribute(:token, &__MODULE__.generate_token/0)
change set_attribute(:context, "session")
end
end
@rand_size 32
def generate_token() do
:crypto.strong_rand_bytes(@rand_size)
end
actions do
create :session_token do
accept [:sent_to]
argument :user_id, :uuid, allow_nil?: false
change manage_relationship(:user_id, :user, type: :append)
change set_attribute(:token, &__MODULE__.generate_token/0)
change set_attribute(:context, "session")
end
end
@rand_size 32
def generate_token() do
:crypto.strong_rand_bytes(@rand_size)
end
That seems reasonable 🙂