from datetime import UTC, date, datetime from uuid import UUID from sqlalchemy import ( JSON, Boolean, Date, DateTime, Float, ForeignKey, Index, Integer, String, Text, UniqueConstraint, ) from sqlalchemy.orm import Mapped, mapped_column from uuid_utils import uuid7 from .db import Base def new_id() -> UUID: return UUID(str(uuid7())) def utcnow() -> datetime: return datetime.now(UTC) class AppState(Base): __tablename__ = "app_state" key: Mapped[str] = mapped_column(String(64), primary_key=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) class User(Base): __tablename__ = "users" id: Mapped[UUID] = mapped_column(primary_key=True, default=new_id) username: Mapped[str] = mapped_column(String(64), unique=True, index=True) password_hash: Mapped[str] = mapped_column(Text) timezone: Mapped[str] = mapped_column(String(64), default="Asia/Shanghai") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) class Session(Base): __tablename__ = "sessions" id: Mapped[UUID] = mapped_column(primary_key=True, default=new_id) token_hash: Mapped[str] = mapped_column(String(64), unique=True, index=True) user_id: Mapped[UUID] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True) expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) last_seen_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) ip_address: Mapped[str | None] = mapped_column(String(64), nullable=True) user_agent: Mapped[str | None] = mapped_column(String(255), nullable=True) class Folder(Base): __tablename__ = "folders" id: Mapped[UUID] = mapped_column(primary_key=True, default=new_id) user_id: Mapped[UUID] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True) name: Mapped[str] = mapped_column(String(120)) position: Mapped[int] = mapped_column(Integer, default=0) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) class TaskList(Base): __tablename__ = "task_lists" id: Mapped[UUID] = mapped_column(primary_key=True, default=new_id) user_id: Mapped[UUID] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True) folder_id: Mapped[UUID | None] = mapped_column(ForeignKey("folders.id", ondelete="SET NULL"), nullable=True) name: Mapped[str] = mapped_column(String(120)) is_inbox: Mapped[bool] = mapped_column(Boolean, default=False) position: Mapped[int] = mapped_column(Integer, default=0) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) class Task(Base): __tablename__ = "tasks" __table_args__ = ( UniqueConstraint("user_id", "external_id", name="uq_tasks_external_id"), Index("ix_tasks_user_active_created", "user_id", "deleted_at", "parent_id", "created_at", "id"), Index("ix_tasks_user_due", "user_id", "deleted_at", "due_at"), Index("ix_tasks_user_list_active", "user_id", "list_id", "deleted_at", "parent_id", "created_at", "id"), ) id: Mapped[UUID] = mapped_column(primary_key=True, default=new_id) user_id: Mapped[UUID] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True) list_id: Mapped[UUID] = mapped_column(ForeignKey("task_lists.id", ondelete="CASCADE"), index=True) parent_id: Mapped[UUID | None] = mapped_column(ForeignKey("tasks.id", ondelete="CASCADE"), nullable=True) title: Mapped[str] = mapped_column(String(500)) description: Mapped[str] = mapped_column(Text, default="") priority: Mapped[int] = mapped_column(Integer, default=0) completed: Mapped[bool] = mapped_column(Boolean, default=False, index=True) due_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) version: Mapped[int] = mapped_column(Integer, default=1) position: Mapped[int] = mapped_column(Integer, default=0) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, onupdate=utcnow) deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) external_id: Mapped[str | None] = mapped_column(String(255), nullable=True) class RecurrenceTemplate(Base): __tablename__ = "recurrence_templates" id: Mapped[UUID] = mapped_column(primary_key=True, default=new_id) user_id: Mapped[UUID] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True) task_id: Mapped[UUID] = mapped_column(ForeignKey("tasks.id", ondelete="CASCADE"), unique=True) rrule: Mapped[str] = mapped_column(Text) starts_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) ends_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) class RecurrenceException(Base): __tablename__ = "recurrence_exceptions" __table_args__ = (UniqueConstraint("template_id", "occurrence_at", name="uq_recurrence_exception"),) id: Mapped[UUID] = mapped_column(primary_key=True, default=new_id) template_id: Mapped[UUID] = mapped_column(ForeignKey("recurrence_templates.id", ondelete="CASCADE"), index=True) occurrence_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) title: Mapped[str | None] = mapped_column(String(500), nullable=True) due_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) completed: Mapped[bool] = mapped_column(Boolean, default=False) deleted: Mapped[bool] = mapped_column(Boolean, default=False) class Countdown(Base): __tablename__ = "countdowns" __table_args__ = ( Index("ix_countdowns_user_archive_date", "user_id", "archived_at", "event_date"), ) id: Mapped[UUID] = mapped_column(primary_key=True, default=new_id) user_id: Mapped[UUID] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True) title: Mapped[str] = mapped_column(String(200)) event_date: Mapped[date] = mapped_column(Date) calendar_mode: Mapped[str] = mapped_column(String(8), default="solar") lunar_month: Mapped[int | None] = mapped_column(Integer, nullable=True) lunar_day: Mapped[int | None] = mapped_column(Integer, nullable=True) ignore_year: Mapped[bool] = mapped_column(Boolean, default=False) kind: Mapped[str] = mapped_column(String(16), default="countdown") repeat_rule: Mapped[str] = mapped_column(String(16), default="none") icon: Mapped[str] = mapped_column(String(32), default="📅") pinned: Mapped[bool] = mapped_column(Boolean, default=False) archived_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, onupdate=utcnow) class Habit(Base): __tablename__ = "habits" id: Mapped[UUID] = mapped_column(primary_key=True, default=new_id) user_id: Mapped[UUID] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True) name: Mapped[str] = mapped_column(String(200)) kind: Mapped[str] = mapped_column(String(16), default="boolean") target: Mapped[float] = mapped_column(Float, default=1) max_value: Mapped[float | None] = mapped_column(Float, nullable=True) schedule_type: Mapped[str] = mapped_column(String(16), default="daily") weekdays: Mapped[str | None] = mapped_column(String(32), nullable=True) month_days: Mapped[str | None] = mapped_column(String(100), nullable=True) interval_days: Mapped[int | None] = mapped_column(Integer, nullable=True) start_date: Mapped[date] = mapped_column(Date) archived_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) class HabitLog(Base): __tablename__ = "habit_logs" __table_args__ = (UniqueConstraint("habit_id", "day", name="uq_habit_log_day"),) id: Mapped[UUID] = mapped_column(primary_key=True, default=new_id) habit_id: Mapped[UUID] = mapped_column(ForeignKey("habits.id", ondelete="CASCADE"), index=True) day: Mapped[date] = mapped_column(Date) value: Mapped[float] = mapped_column(Float) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) class HabitPause(Base): __tablename__ = "habit_pauses" id: Mapped[UUID] = mapped_column(primary_key=True, default=new_id) habit_id: Mapped[UUID] = mapped_column(ForeignKey("habits.id", ondelete="CASCADE"), index=True) start_date: Mapped[date] = mapped_column(Date) end_date: Mapped[date] = mapped_column(Date) class Attachment(Base): __tablename__ = "attachments" id: Mapped[UUID] = mapped_column(primary_key=True, default=new_id) user_id: Mapped[UUID] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True) task_id: Mapped[UUID] = mapped_column(ForeignKey("tasks.id", ondelete="CASCADE"), index=True) filename: Mapped[str] = mapped_column(String(255)) storage_name: Mapped[str] = mapped_column(String(255), unique=True) mime_type: Mapped[str] = mapped_column(String(127)) size: Mapped[int] = mapped_column(Integer) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) class AuditLog(Base): __tablename__ = "audit_logs" id: Mapped[UUID] = mapped_column(primary_key=True, default=new_id) user_id: Mapped[UUID] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True) action: Mapped[str] = mapped_column(String(32)) entity_type: Mapped[str] = mapped_column(String(32)) entity_id: Mapped[UUID | None] = mapped_column(nullable=True) details: Mapped[dict] = mapped_column(JSON, default=dict) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)