from datetime import datetime from typing import List, Optional from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy import ( Integer, String, Text, Boolean, DateTime, ForeignKey, Column, func, ) from shared.db.base import Base # whatever your Base is # from .author import Author # make sure imports resolve # from ..app.blog.calendars.model import Calendar class Tag(Base): __tablename__ = "tags" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) ghost_id: Mapped[Optional[str]] = mapped_column(String(64), index=True, unique=True, nullable=True) slug: Mapped[str] = mapped_column(String(191), index=True, nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) description: Mapped[Optional[str]] = mapped_column(Text()) visibility: Mapped[str] = mapped_column(String(32), default="public", nullable=False) feature_image: Mapped[Optional[str]] = mapped_column(Text()) meta_title: Mapped[Optional[str]] = mapped_column(String(300)) meta_description: Mapped[Optional[str]] = mapped_column(Text()) created_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) # NEW: posts relationship is now direct Post objects via PostTag posts: Mapped[List["Post"]] = relationship( "Post", secondary="post_tags", primaryjoin="Tag.id==post_tags.c.tag_id", secondaryjoin="Post.id==post_tags.c.post_id", back_populates="tags", order_by="PostTag.sort_order", ) class Post(Base): __tablename__ = "posts" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) ghost_id: Mapped[Optional[str]] = mapped_column(String(64), index=True, unique=True, nullable=True) uuid: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, server_default=func.gen_random_uuid()) slug: Mapped[str] = mapped_column(String(191), index=True, nullable=False) title: Mapped[str] = mapped_column(String(500), nullable=False) html: Mapped[Optional[str]] = mapped_column(Text()) plaintext: Mapped[Optional[str]] = mapped_column(Text()) mobiledoc: Mapped[Optional[str]] = mapped_column(Text()) lexical: Mapped[Optional[str]] = mapped_column(Text()) sx_content: Mapped[Optional[str]] = mapped_column(Text()) feature_image: Mapped[Optional[str]] = mapped_column(Text()) feature_image_alt: Mapped[Optional[str]] = mapped_column(Text()) feature_image_caption: Mapped[Optional[str]] = mapped_column(Text()) excerpt: Mapped[Optional[str]] = mapped_column(Text()) custom_excerpt: Mapped[Optional[str]] = mapped_column(Text()) visibility: Mapped[str] = mapped_column(String(32), default="public", nullable=False) status: Mapped[str] = mapped_column(String(32), default="draft", nullable=False) featured: Mapped[bool] = mapped_column(Boolean(), default=False, nullable=False) is_page: Mapped[bool] = mapped_column(Boolean(), default=False, nullable=False) email_only: Mapped[bool] = mapped_column(Boolean(), default=False, nullable=False) canonical_url: Mapped[Optional[str]] = mapped_column(Text()) meta_title: Mapped[Optional[str]] = mapped_column(String(500)) meta_description: Mapped[Optional[str]] = mapped_column(Text()) og_image: Mapped[Optional[str]] = mapped_column(Text()) og_title: Mapped[Optional[str]] = mapped_column(String(500)) og_description: Mapped[Optional[str]] = mapped_column(Text()) twitter_image: Mapped[Optional[str]] = mapped_column(Text()) twitter_title: Mapped[Optional[str]] = mapped_column(String(500)) twitter_description: Mapped[Optional[str]] = mapped_column(Text()) custom_template: Mapped[Optional[str]] = mapped_column(String(191)) reading_time: Mapped[Optional[int]] = mapped_column(Integer()) comment_id: Mapped[Optional[str]] = mapped_column(String(191)) published_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), server_default=func.now()) created_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), server_default=func.now()) deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) user_id: Mapped[Optional[int]] = mapped_column( Integer, index=True ) publish_requested: Mapped[bool] = mapped_column(Boolean(), default=False, server_default="false", nullable=False) primary_author_id: Mapped[Optional[int]] = mapped_column( Integer, ForeignKey("authors.id", ondelete="SET NULL") ) primary_tag_id: Mapped[Optional[int]] = mapped_column( Integer, ForeignKey("tags.id", ondelete="SET NULL") ) primary_author: Mapped[Optional["Author"]] = relationship( "Author", foreign_keys=[primary_author_id] ) primary_tag: Mapped[Optional[Tag]] = relationship( "Tag", foreign_keys=[primary_tag_id] ) # AUTHORS RELATIONSHIP (many-to-many via post_authors) authors: Mapped[List["Author"]] = relationship( "Author", secondary="post_authors", primaryjoin="Post.id==post_authors.c.post_id", secondaryjoin="Author.id==post_authors.c.author_id", back_populates="posts", order_by="PostAuthor.sort_order", ) # TAGS RELATIONSHIP (many-to-many via post_tags) tags: Mapped[List[Tag]] = relationship( "Tag", secondary="post_tags", primaryjoin="Post.id==post_tags.c.post_id", secondaryjoin="Tag.id==post_tags.c.tag_id", back_populates="posts", order_by="PostTag.sort_order", ) class Author(Base): __tablename__ = "authors" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) ghost_id: Mapped[Optional[str]] = mapped_column(String(64), index=True, unique=True, nullable=True) slug: Mapped[str] = mapped_column(String(191), index=True, nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) email: Mapped[Optional[str]] = mapped_column(String(255)) profile_image: Mapped[Optional[str]] = mapped_column(Text()) cover_image: Mapped[Optional[str]] = mapped_column(Text()) bio: Mapped[Optional[str]] = mapped_column(Text()) website: Mapped[Optional[str]] = mapped_column(Text()) location: Mapped[Optional[str]] = mapped_column(Text()) facebook: Mapped[Optional[str]] = mapped_column(Text()) twitter: Mapped[Optional[str]] = mapped_column(Text()) created_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) # backref to posts via post_authors posts: Mapped[List[Post]] = relationship( "Post", secondary="post_authors", primaryjoin="Author.id==post_authors.c.author_id", secondaryjoin="Post.id==post_authors.c.post_id", back_populates="authors", order_by="PostAuthor.sort_order", ) class PostAuthor(Base): __tablename__ = "post_authors" post_id: Mapped[int] = mapped_column( ForeignKey("posts.id", ondelete="CASCADE"), primary_key=True, ) author_id: Mapped[int] = mapped_column( ForeignKey("authors.id", ondelete="CASCADE"), primary_key=True, ) sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False) class PostTag(Base): __tablename__ = "post_tags" post_id: Mapped[int] = mapped_column( ForeignKey("posts.id", ondelete="CASCADE"), primary_key=True, ) tag_id: Mapped[int] = mapped_column( ForeignKey("tags.id", ondelete="CASCADE"), primary_key=True, ) sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False) class PostUser(Base): """Multi-author M2M: links posts to users (cross-DB, no FK on user_id).""" __tablename__ = "post_users" post_id: Mapped[int] = mapped_column( ForeignKey("posts.id", ondelete="CASCADE"), primary_key=True, ) user_id: Mapped[int] = mapped_column( Integer, primary_key=True, index=True, ) sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False)