fix: change users.id and FK columns from uuid to text for Better-Auth compatibility
Better-Auth generates nanoid-style text IDs (e.g. pGud2ln2WAFHC0KYjBVKR4Rc7mM8OcTI), but the users table used PostgreSQL uuid type, causing registration failures: ERROR: invalid input syntax for type uuid: "pGud2ln2WAFHC0KYjBVKR4Rc7mM8OcTI" Changes: - User.id: removed UUIDPrimaryKeyMixin, use explicit text PK - UserStoreAccount.user_id: Mapped[uuid.UUID] -> Mapped[str] - Purchase.user_id: Mapped[uuid.UUID] -> Mapped[str] - UserResponse schema: id field from UUID -> str - New Alembic migration 004_fix_user_id_text: drops FKs, alters column types, re-adds FKs (using id::text cast) Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,122 @@
|
|||||||
|
"""Fix users.id UUID->text type mismatch for Better-Auth compatibility.
|
||||||
|
|
||||||
|
Better-Auth generates nanoid-style text IDs (e.g. pGud2ln2WAFHC0KYjBVKR4Rc7mM8OcTI),
|
||||||
|
but the users table was using PostgreSQL uuid type. When Better-Auth tries to INSERT
|
||||||
|
a new user, Postgres throws:
|
||||||
|
ERROR: invalid input syntax for type uuid: "pGud2ln2WAFHC0KYjBVKR4Rc7mM8OcTI"
|
||||||
|
|
||||||
|
The sessions, accounts, and verifications tables already use text IDs — only users,
|
||||||
|
user_store_accounts.user_id, and purchases.user_id needed fixing.
|
||||||
|
|
||||||
|
Revision ID: 004_fix_user_id_text
|
||||||
|
Revises: 003_make_users_hashed_password_nullable
|
||||||
|
Create Date: 2026-03-31
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision = "004_fix_user_id_text"
|
||||||
|
down_revision = "003_make_users_hashed_password_nullable"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Step 1: Drop existing FK constraints
|
||||||
|
op.execute(text("ALTER TABLE user_store_accounts DROP CONSTRAINT IF EXISTS user_store_accounts_user_id_fkey"))
|
||||||
|
op.execute(text("ALTER TABLE purchases DROP CONSTRAINT IF EXISTS purchases_user_id_fkey"))
|
||||||
|
|
||||||
|
# Step 2: Alter users.id from uuid to text
|
||||||
|
op.alter_column(
|
||||||
|
"users",
|
||||||
|
"id",
|
||||||
|
type_=sa.Text(),
|
||||||
|
existing_type=sa.UUID(),
|
||||||
|
postgresql_using="id::text",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 3: Alter user_store_accounts.user_id from uuid to text
|
||||||
|
op.alter_column(
|
||||||
|
"user_store_accounts",
|
||||||
|
"user_id",
|
||||||
|
type_=sa.Text(),
|
||||||
|
existing_type=sa.UUID(),
|
||||||
|
postgresql_using="user_id::text",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 4: Alter purchases.user_id from uuid to text
|
||||||
|
op.alter_column(
|
||||||
|
"purchases",
|
||||||
|
"user_id",
|
||||||
|
type_=sa.Text(),
|
||||||
|
existing_type=sa.UUID(),
|
||||||
|
postgresql_using="user_id::text",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 5: Re-add FK constraints
|
||||||
|
op.execute(
|
||||||
|
text(
|
||||||
|
"ALTER TABLE user_store_accounts "
|
||||||
|
"ADD CONSTRAINT user_store_accounts_user_id_fkey "
|
||||||
|
"FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
text(
|
||||||
|
"ALTER TABLE purchases "
|
||||||
|
"ADD CONSTRAINT purchases_user_id_fkey "
|
||||||
|
"FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Drop FK constraints
|
||||||
|
op.execute(text("ALTER TABLE user_store_accounts DROP CONSTRAINT IF EXISTS user_store_accounts_user_id_fkey"))
|
||||||
|
op.execute(text("ALTER TABLE purchases DROP CONSTRAINT IF EXISTS purchases_user_id_fkey"))
|
||||||
|
|
||||||
|
# Revert users.id from text to uuid
|
||||||
|
op.alter_column(
|
||||||
|
"users",
|
||||||
|
"id",
|
||||||
|
type_=sa.UUID(),
|
||||||
|
existing_type=sa.Text(),
|
||||||
|
postgresql_using="id::uuid",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Revert user_store_accounts.user_id from text to uuid
|
||||||
|
op.alter_column(
|
||||||
|
"user_store_accounts",
|
||||||
|
"user_id",
|
||||||
|
type_=sa.UUID(),
|
||||||
|
existing_type=sa.Text(),
|
||||||
|
postgresql_using="user_id::uuid",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Revert purchases.user_id from text to uuid
|
||||||
|
op.alter_column(
|
||||||
|
"purchases",
|
||||||
|
"user_id",
|
||||||
|
type_=sa.UUID(),
|
||||||
|
existing_type=sa.Text(),
|
||||||
|
postgresql_using="user_id::uuid",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Re-add FK constraints (PostgreSQL will auto-name them)
|
||||||
|
op.execute(
|
||||||
|
text(
|
||||||
|
"ALTER TABLE user_store_accounts "
|
||||||
|
"ADD CONSTRAINT user_store_accounts_user_id_fkey "
|
||||||
|
"FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
text(
|
||||||
|
"ALTER TABLE purchases "
|
||||||
|
"ADD CONSTRAINT purchases_user_id_fkey "
|
||||||
|
"FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE"
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -32,7 +32,7 @@ class Purchase(UUIDPrimaryKeyMixin, TimestampMixin, Base):
|
|||||||
|
|
||||||
__tablename__ = "purchases"
|
__tablename__ = "purchases"
|
||||||
|
|
||||||
user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), nullable=False)
|
user_id: Mapped[str] = mapped_column(ForeignKey("users.id"), nullable=False)
|
||||||
store_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("stores.id"), nullable=False)
|
store_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("stores.id"), nullable=False)
|
||||||
store_location_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("store_locations.id"))
|
store_location_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("store_locations.id"))
|
||||||
receipt_id: Mapped[str] = mapped_column(String(200), nullable=False)
|
receipt_id: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import uuid
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import DateTime, ForeignKey, String, UniqueConstraint
|
from sqlalchemy import DateTime, ForeignKey, String, Text, UniqueConstraint
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from cartsnitch_api.constants import AccountStatus
|
from cartsnitch_api.constants import AccountStatus
|
||||||
@@ -16,11 +16,12 @@ if TYPE_CHECKING:
|
|||||||
from cartsnitch_api.models.store import Store
|
from cartsnitch_api.models.store import Store
|
||||||
|
|
||||||
|
|
||||||
class User(UUIDPrimaryKeyMixin, TimestampMixin, Base):
|
class User(TimestampMixin, Base):
|
||||||
"""Application user."""
|
"""Application user."""
|
||||||
|
|
||||||
__tablename__ = "users"
|
__tablename__ = "users"
|
||||||
|
|
||||||
|
id: Mapped[str] = mapped_column(Text, primary_key=True)
|
||||||
email: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
|
email: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
|
||||||
hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)
|
hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
display_name: Mapped[str | None] = mapped_column(String(100))
|
display_name: Mapped[str | None] = mapped_column(String(100))
|
||||||
@@ -36,7 +37,7 @@ class UserStoreAccount(UUIDPrimaryKeyMixin, TimestampMixin, Base):
|
|||||||
__tablename__ = "user_store_accounts"
|
__tablename__ = "user_store_accounts"
|
||||||
__table_args__ = (UniqueConstraint("user_id", "store_id", name="uq_user_store_account"),)
|
__table_args__ = (UniqueConstraint("user_id", "store_id", name="uq_user_store_account"),)
|
||||||
|
|
||||||
user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), nullable=False)
|
user_id: Mapped[str] = mapped_column(ForeignKey("users.id"), nullable=False)
|
||||||
store_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("stores.id"), nullable=False)
|
store_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("stores.id"), nullable=False)
|
||||||
session_data: Mapped[dict | None] = mapped_column(EncryptedJSON)
|
session_data: Mapped[dict | None] = mapped_column(EncryptedJSON)
|
||||||
session_expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
session_expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class UpdateUserRequest(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class UserResponse(BaseModel):
|
class UserResponse(BaseModel):
|
||||||
id: UUID
|
id: str
|
||||||
email: str
|
email: str
|
||||||
display_name: str
|
display_name: str
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|||||||
Reference in New Issue
Block a user