Big-Link-Man/src/database/models.py

209 lines
9.9 KiB
Python

"""
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"<User(id={self.id}, username='{self.username}', role='{self.role}')>"
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"<SiteDeployment(id={self.id}, site_name='{self.site_name}', hostname='{hostname}')>"
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"<Project(id={self.id}, name='{self.name}', main_keyword='{self.main_keyword}', user_id={self.user_id})>"
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"<GeneratedContent(id={self.id}, project_id={self.project_id}, tier='{self.tier}', status='{self.status}')>"
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"<ArticleLink(id={self.id}, from={self.from_content_id}, to={target}, type='{self.link_type}')>"
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"<SitePage(id={self.id}, site_id={self.site_deployment_id}, page_type='{self.page_type}')>"