# database.py import os import uuid from sqlalchemy import ( create_engine, Column, Integer, String, Float, DateTime, Boolean, Text, ForeignKey, UniqueConstraint, Index ) from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker, relationship from sqlalchemy.sql import func from sqlalchemy.dialects.postgresql import UUID, JSONB # ----------------------------------------------------------------------------- # Database URL # ----------------------------------------------------------------------------- DATABASE_URL = os.getenv("DATABASE_URL") if not DATABASE_URL: POSTGRES_USER = os.getenv("POSTGRES_USER", "fastapi_user") POSTGRES_PASSWORD = os.getenv("POSTGRES_PASSWORD", "fastapi_password") POSTGRES_HOST = os.getenv("POSTGRES_HOST", "postgres") POSTGRES_PORT = os.getenv("POSTGRES_PORT", "5432") POSTGRES_DB = os.getenv("POSTGRES_DB", "fastapi_db") DATABASE_URL = ( f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}" f"@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DB}" ) # ----------------------------------------------------------------------------- # Engine / Session / Base # ----------------------------------------------------------------------------- engine = create_engine(DATABASE_URL) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() # ----------------------------------------------------------------------------- # Basic Item model (kept for backward compatibility / health checks) # ----------------------------------------------------------------------------- class Item(Base): __tablename__ = "items" id = Column(Integer, primary_key=True, index=True) name = Column(String, nullable=False) description = Column(String, nullable=True) price = Column(Float, nullable=False) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now()) # ----------------------------------------------------------------------------- # Cursor for BoxAPI pagination (persist next_max_id per username) # ----------------------------------------------------------------------------- class InstaCursor(Base): __tablename__ = "insta_cursors" id = Column(Integer, primary_key=True) username = Column(String(150), nullable=False, unique=True) next_max_id = Column(String(255), nullable=True) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) __table_args__ = ( UniqueConstraint("username", name="uq_insta_cursor_username"), Index("ix_insta_cursors_username", "username"), ) # ----------------------------------------------------------------------------- # Instagram Account # ----------------------------------------------------------------------------- class InstagramAccount(Base): __tablename__ = "instagram_accounts" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) seller_id = Column(UUID(as_uuid=True), nullable=False) username = Column(String(100), nullable=False, unique=True, index=True) is_active = Column(Boolean, default=True) last_synced = Column(DateTime(timezone=True), nullable=True) posts = relationship( "InstagramPost", back_populates="account", cascade="all, delete-orphan", passive_deletes=True, ) __table_args__ = ( UniqueConstraint("username", name="uq_instagram_accounts_username"), Index("ix_instagram_accounts_username", "username"), Index("ix_instagram_accounts_seller_id", "seller_id"), ) # ----------------------------------------------------------------------------- # Instagram Post # ----------------------------------------------------------------------------- class InstagramPost(Base): __tablename__ = "instagram_posts" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) account_id = Column( UUID(as_uuid=True), ForeignKey("instagram_accounts.id", ondelete="CASCADE"), nullable=False, index=True, ) ig_post_id = Column(String(100), nullable=False, unique=True, index=True) media_type = Column(String(10), nullable=False) # 'image', 'video', 'carousel' caption = Column(Text, nullable=True) thumbnail_url = Column(Text, nullable=True) # List[str] of public S3 URLs (stored as JSON array) local_media = Column(JSONB, nullable=True) # Original post time on Instagram created_at = Column(DateTime(timezone=True), nullable=False, index=True) # When we scraped it fetched_at = Column(DateTime(timezone=True), server_default=func.now()) account = relationship("InstagramAccount", back_populates="posts") __table_args__ = ( UniqueConstraint("ig_post_id", name="uq_instagram_posts_ig_post_id"), Index("ix_instagram_posts_created_at", "created_at"), Index("ix_instagram_posts_media_type", "media_type"), ) # ----------------------------------------------------------------------------- # Session dependency # ----------------------------------------------------------------------------- def get_db(): db = SessionLocal() try: yield db finally: db.close() # ----------------------------------------------------------------------------- # Table creation # ----------------------------------------------------------------------------- def create_tables(): Base.metadata.create_all(bind=engine)