""" SQLAlchemy database models """ from datetime import datetime, timezone from typing import Optional from sqlalchemy import String, Integer, DateTime, Float, ForeignKey, JSON, Text, UniqueConstraint from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship class Base(DeclarativeBase): """Base class for all database models""" pass class User(Base): """User model for authentication and authorization""" __tablename__ = "users" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) username: Mapped[str] = mapped_column(String(100), unique=True, nullable=False, index=True) hashed_password: Mapped[str] = mapped_column(String(255), nullable=False) role: Mapped[str] = mapped_column(String(20), nullable=False) # "Admin" or "User" created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False) updated_at: Mapped[datetime] = mapped_column( DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False ) def __repr__(self) -> str: return f"" def is_admin(self) -> bool: """Check if user has admin role""" return self.role == "Admin" class SiteDeployment(Base): """Site deployment model for bunny.net infrastructure tracking""" __tablename__ = "site_deployments" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) site_name: Mapped[str] = mapped_column(String(255), nullable=False) custom_hostname: Mapped[Optional[str]] = mapped_column(String(255), unique=True, nullable=True, index=True) storage_zone_id: Mapped[int] = mapped_column(Integer, nullable=False) storage_zone_name: Mapped[str] = mapped_column(String(255), nullable=False) storage_zone_password: Mapped[str] = mapped_column(String(255), nullable=False) storage_zone_region: Mapped[str] = mapped_column(String(10), nullable=False) pull_zone_id: Mapped[int] = mapped_column(Integer, nullable=False) pull_zone_bcdn_hostname: Mapped[str] = mapped_column(String(255), unique=True, nullable=False) template_name: Mapped[str] = mapped_column(String(50), default="basic", nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False) updated_at: Mapped[datetime] = mapped_column( DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False ) def __repr__(self) -> str: hostname = self.custom_hostname or self.pull_zone_bcdn_hostname return f"" class Project(Base): """Project model for CORA-ingested SEO data""" __tablename__ = "projects" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) user_id: Mapped[int] = mapped_column(Integer, ForeignKey('users.id'), nullable=False, index=True) name: Mapped[str] = mapped_column(String(255), nullable=False) main_keyword: Mapped[str] = mapped_column(String(255), nullable=False, index=True) tier: Mapped[int] = mapped_column(Integer, nullable=False, default=1, index=True) money_site_url: Mapped[Optional[str]] = mapped_column(String(500), nullable=True, index=True) word_count: Mapped[int] = mapped_column(Integer, nullable=False, default=1250) term_frequency: Mapped[int] = mapped_column(Integer, nullable=False, default=3) related_search_density: Mapped[Optional[float]] = mapped_column(Float, nullable=True) entity_density: Mapped[Optional[float]] = mapped_column(Float, nullable=True) lsi_density: Mapped[Optional[float]] = mapped_column(Float, nullable=True) title_exact_match: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) title_related_search: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) meta_exact_match: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) meta_related_search: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) meta_entities: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) h1_exact: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) h1_related_search: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) h1_entities: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) h1_lsi: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) h2_total: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) h2_exact: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) h2_related_search: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) h2_entities: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) h2_lsi: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) h3_total: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) h3_exact: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) h3_related_search: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) h3_entities: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) h3_lsi: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) entities: Mapped[Optional[list]] = mapped_column(JSON, nullable=True) related_searches: Mapped[Optional[list]] = mapped_column(JSON, nullable=True) custom_anchor_text: Mapped[Optional[list]] = mapped_column(JSON, nullable=True) spintax_related_search_terms: Mapped[Optional[str]] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False) updated_at: Mapped[datetime] = mapped_column( DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False ) def __repr__(self) -> str: return f"" class GeneratedContent(Base): """Generated content model for AI-created articles""" __tablename__ = "generated_content" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) project_id: Mapped[int] = mapped_column(Integer, ForeignKey('projects.id'), nullable=False, index=True) tier: Mapped[str] = mapped_column(String(20), nullable=False, index=True) keyword: Mapped[str] = mapped_column(String(255), nullable=False, index=True) title: Mapped[str] = mapped_column(Text, nullable=False) outline: Mapped[dict] = mapped_column(JSON, nullable=False) content: Mapped[str] = mapped_column(Text, nullable=False) word_count: Mapped[int] = mapped_column(Integer, nullable=False) status: Mapped[str] = mapped_column(String(20), nullable=False) formatted_html: Mapped[Optional[str]] = mapped_column(Text, nullable=True) template_used: Mapped[Optional[str]] = mapped_column(String(50), nullable=True) site_deployment_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey('site_deployments.id'), nullable=True, index=True) deployed_url: Mapped[Optional[str]] = mapped_column(Text, nullable=True) deployed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True, index=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False) updated_at: Mapped[datetime] = mapped_column( DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False ) def __repr__(self) -> str: return f"" class ArticleLink(Base): """Article link tracking model for tiered linking, wheel links, etc.""" __tablename__ = "article_links" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) from_content_id: Mapped[int] = mapped_column( Integer, ForeignKey('generated_content.id', ondelete='CASCADE'), nullable=False, index=True ) to_content_id: Mapped[Optional[int]] = mapped_column( Integer, ForeignKey('generated_content.id', ondelete='CASCADE'), nullable=True, index=True ) to_url: Mapped[Optional[str]] = mapped_column(Text, nullable=True) anchor_text: Mapped[Optional[str]] = mapped_column(Text, nullable=True) link_type: Mapped[str] = mapped_column(String(20), nullable=False, index=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False) def __repr__(self) -> str: target = f"content_id={self.to_content_id}" if self.to_content_id else f"url={self.to_url}" return f"" class SitePage(Base): """Boilerplate pages for sites (about, contact, privacy)""" __tablename__ = "site_pages" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) site_deployment_id: Mapped[int] = mapped_column( Integer, ForeignKey('site_deployments.id', ondelete='CASCADE'), nullable=False, index=True ) page_type: Mapped[str] = mapped_column(String(20), nullable=False, index=True) content: Mapped[str] = mapped_column(Text, nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False) updated_at: Mapped[datetime] = mapped_column( DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False ) __table_args__ = ( UniqueConstraint('site_deployment_id', 'page_type', name='uq_site_page_type'), ) def __repr__(self) -> str: return f""