import uuid from datetime import datetime from decimal import Decimal from sqlalchemy import ( Boolean, CheckConstraint, DateTime, ForeignKey, Index, Numeric, SmallInteger, Text, UniqueConstraint, Uuid, func, text, ) from sqlalchemy.orm import Mapped, mapped_column, relationship from app.database import Base class TimestampMixin: created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False ) class User(Base, TimestampMixin): __tablename__ = "users" id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) email: Mapped[str] = mapped_column(Text, unique=True, nullable=False) password_hash: Mapped[str] = mapped_column(Text, nullable=False) email_verified_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) profile: Mapped["Profile"] = relationship( back_populates="user", cascade="all, delete-orphan", uselist=False, lazy="raise" ) clouds: Mapped[list["Cloud"]] = relationship(back_populates="user", lazy="raise") class Profile(Base): __tablename__ = "profiles" __table_args__ = ( CheckConstraint("length(trim(username)) >= 2", name="ck_profiles_username_length"), CheckConstraint("role IN ('user', 'admin')", name="ck_profiles_role"), ) id: Mapped[uuid.UUID] = mapped_column( Uuid, ForeignKey("users.id", ondelete="CASCADE"), primary_key=True ) username: Mapped[str] = mapped_column(Text, unique=True, nullable=False) avatar_path: Mapped[str | None] = mapped_column(Text) role: Mapped[str] = mapped_column(Text, nullable=False, default="user", server_default="user") is_disabled: Mapped[bool] = mapped_column( Boolean, nullable=False, default=False, server_default=text("false") ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False ) user: Mapped[User] = relationship(back_populates="profile", lazy="raise") class RefreshSession(Base): __tablename__ = "refresh_sessions" __table_args__ = ( Index("ix_refresh_sessions_user_id", "user_id"), Index("ix_refresh_sessions_replaced_by_id", "replaced_by_id"), Index( "ix_refresh_sessions_active_user", "user_id", "expires_at", postgresql_where=text("revoked_at IS NULL"), ), ) id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column( Uuid, ForeignKey("users.id", ondelete="CASCADE"), nullable=False ) token_hash: Mapped[str] = mapped_column(Text, unique=True, nullable=False) expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) replaced_by_id: Mapped[uuid.UUID | None] = mapped_column( Uuid, ForeignKey("refresh_sessions.id", ondelete="SET NULL") ) user_agent: Mapped[str | None] = mapped_column(Text) ip_address: Mapped[str | None] = mapped_column(Text) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False ) user: Mapped[User] = relationship(lazy="raise") class EmailToken(Base): __tablename__ = "email_tokens" __table_args__ = ( CheckConstraint( "purpose IN ('confirm_email', 'reset_password')", name="ck_email_tokens_purpose" ), Index("ix_email_tokens_user_purpose", "user_id", "purpose"), ) id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column( Uuid, ForeignKey("users.id", ondelete="CASCADE"), nullable=False ) purpose: Mapped[str] = mapped_column(Text, nullable=False) token_hash: Mapped[str] = mapped_column(Text, unique=True, nullable=False) expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False ) class CloudType(Base): __tablename__ = "cloud_types" __table_args__ = ( CheckConstraint("rarity IN ('common', 'uncommon', 'rare')", name="ck_cloud_types_rarity"), ) id: Mapped[int] = mapped_column(SmallInteger, primary_key=True, autoincrement=False) name: Mapped[str] = mapped_column(Text, unique=True, nullable=False) name_en: Mapped[str] = mapped_column(Text, unique=True, nullable=False) genus: Mapped[str] = mapped_column(Text, nullable=False) rarity: Mapped[str] = mapped_column(Text, nullable=False, default="common") description: Mapped[str | None] = mapped_column(Text) icon_url: Mapped[str | None] = mapped_column(Text) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False ) class Cloud(Base, TimestampMixin): __tablename__ = "clouds" __table_args__ = ( CheckConstraint("status IN ('pending', 'approved', 'rejected')", name="ck_clouds_status"), CheckConstraint( "((latitude IS NULL AND longitude IS NULL) OR " "(latitude IS NOT NULL AND longitude IS NOT NULL))", name="ck_clouds_coordinate_pair", ), CheckConstraint("latitude IS NULL OR latitude BETWEEN -90 AND 90", name="ck_clouds_latitude"), CheckConstraint( "longitude IS NULL OR longitude BETWEEN -180 AND 180", name="ck_clouds_longitude" ), CheckConstraint( "((cloud_type_id IS NOT NULL AND custom_cloud_type IS NULL) OR " "(cloud_type_id IS NULL AND custom_cloud_type IS NOT NULL " "AND length(trim(custom_cloud_type)) > 0))", name="ck_clouds_type_choice", ), Index("ix_clouds_user_id", "user_id"), Index("ix_clouds_cloud_type_id", "cloud_type_id"), Index("ix_clouds_user_captured", "user_id", text("captured_at DESC"), text("created_at DESC")), Index("ix_clouds_status_created", "status", text("created_at DESC")), Index( "ix_clouds_public_gallery", text("created_at DESC"), text("id DESC"), postgresql_where=text("status = 'approved' AND is_hidden = false"), ), Index( "ix_clouds_public_map", "captured_at", postgresql_where=text( "status = 'approved' AND is_hidden = false " "AND latitude IS NOT NULL AND longitude IS NOT NULL" ), ), Index( "ix_clouds_public_type", "cloud_type_id", text("captured_at DESC"), postgresql_where=text("status = 'approved' AND is_hidden = false"), ), ) id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column( Uuid, ForeignKey("users.id", ondelete="CASCADE"), nullable=False ) cloud_type_id: Mapped[int | None] = mapped_column( SmallInteger, ForeignKey("cloud_types.id", ondelete="SET NULL") ) custom_cloud_type: Mapped[str | None] = mapped_column(Text) image_path: Mapped[str] = mapped_column(Text, unique=True, nullable=False) thumbnail_path: Mapped[str] = mapped_column(Text, unique=True, nullable=False) latitude: Mapped[Decimal | None] = mapped_column(Numeric(4, 2)) longitude: Mapped[Decimal | None] = mapped_column(Numeric(5, 2)) location_name: Mapped[str | None] = mapped_column(Text) description: Mapped[str | None] = mapped_column(Text) captured_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) status: Mapped[str] = mapped_column(Text, nullable=False, default="pending", server_default="pending") is_hidden: Mapped[bool] = mapped_column( Boolean, nullable=False, default=False, server_default=text("false") ) user: Mapped[User] = relationship(back_populates="clouds", lazy="raise") cloud_type: Mapped[CloudType | None] = relationship(lazy="raise") class UserCollection(Base): __tablename__ = "user_collections" __table_args__ = ( UniqueConstraint("user_id", "cloud_type_id", name="uq_user_collections_user_type"), Index("ix_user_collections_user_id", "user_id"), Index("ix_user_collections_cloud_type_id", "cloud_type_id"), Index("ix_user_collections_first_cloud_id", "first_cloud_id"), ) id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column( Uuid, ForeignKey("users.id", ondelete="CASCADE"), nullable=False ) cloud_type_id: Mapped[int] = mapped_column( SmallInteger, ForeignKey("cloud_types.id", ondelete="CASCADE"), nullable=False ) first_cloud_id: Mapped[uuid.UUID | None] = mapped_column( Uuid, ForeignKey("clouds.id", ondelete="SET NULL") ) unlocked_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False ) cloud_type: Mapped[CloudType] = relationship(lazy="raise") first_cloud: Mapped[Cloud | None] = relationship(lazy="raise")