"""Author RAG Chatbot SaaS — Book Model.""" from __future__ import annotations from typing import TYPE_CHECKING from sqlalchemy import Boolean, ForeignKey, Integer, String, Text from sqlalchemy.orm import Mapped, mapped_column, relationship from app.models.base import Base, TimestampMixin, generate_uuid if TYPE_CHECKING: from app.models.user import User from app.models.document import Document from app.models.link import Link from app.models.platform_listing import BookPlatformListing class Book(Base, TimestampMixin): """A book in an author's catalog.""" __tablename__ = "books" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) author_id: Mapped[str] = mapped_column( String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True ) # Metadata title: Mapped[str] = mapped_column(String(500), nullable=False) tagline: Mapped[str | None] = mapped_column(String(500), nullable=True) genre: Mapped[str | None] = mapped_column(String(300), nullable=True) # widened for long category strings description: Mapped[str | None] = mapped_column(Text, nullable=True) cover_path: Mapped[str | None] = mapped_column(String(500), nullable=True) # local file path cover_url: Mapped[str | None] = mapped_column(String(1000), nullable=True) # remote CDN URL (scraped) cover_thumbnail_path: Mapped[str | None] = mapped_column(String(500), nullable=True) cover_medium_path: Mapped[str | None] = mapped_column(String(500), nullable=True) ai_summary: Mapped[str | None] = mapped_column(Text, nullable=True) publish_year: Mapped[str | None] = mapped_column(String(4), nullable=True) # Processing status: Mapped[str] = mapped_column(String(50), default="created", nullable=False, index=True) error_message: Mapped[str | None] = mapped_column(Text, nullable=True) chroma_collection_id: Mapped[str | None] = mapped_column(String(100), nullable=True) chunk_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False) page_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False) # Display is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) display_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False) # Smart links buy_url: Mapped[str | None] = mapped_column(String(1000), nullable=True) preview_url: Mapped[str | None] = mapped_column(String(1000), nullable=True) # Import tracking source_url: Mapped[str | None] = mapped_column(String(1000), nullable=True) isbn: Mapped[str | None] = mapped_column(String(20), nullable=True) book_author: Mapped[str | None] = mapped_column(String(200), nullable=True) about_author: Mapped[str | None] = mapped_column(Text, nullable=True) language: Mapped[str | None] = mapped_column(String(50), nullable=True) rating: Mapped[str | None] = mapped_column(String(10), nullable=True) rating_count: Mapped[str | None] = mapped_column(String(20), nullable=True) # Relationships author: Mapped[User] = relationship("User", back_populates="books") documents: Mapped[list[Document]] = relationship("Document", back_populates="book", cascade="all, delete-orphan") links: Mapped[list[Link]] = relationship("Link", back_populates="book", cascade="all, delete-orphan") platform_listings: Mapped[list["BookPlatformListing"]] = relationship( "BookPlatformListing", back_populates="book", cascade="all, delete-orphan" ) def __repr__(self) -> str: return f""