""" LinkSyncServer - Database Base Models """ from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, Boolean, ForeignKey, Float, JSON, text from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship from sqlalchemy.sql import func import uuid from datetime import datetime Base = declarative_base() def get_engine(): """Get database engine from environment variable.""" import os database_url = os.environ.get('DATABASE_URL', 'sqlite:///linksync.db') return create_engine(database_url, echo=False, future=True) def init_db(): """Initialize database tables.""" Base.metadata.create_all() class TimestampMixin: """Mixin for timestamps.""" created_at = Column(DateTime, server_default=func.now(), nullable=False) updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False) class User(Base, TimestampMixin): """User model for authentication.""" __tablename__ = 'users' id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) username = Column(String(100), unique=True, nullable=False) email = Column(String(255), unique=True, nullable=False) password_hash = Column(String(255), nullable=False) role = Column(String(20), nullable=False, default='user') is_active = Column(Boolean, default=True) # Relationships bookmarks = relationship('Bookmark', back_populates='user', foreign_keys='Bookmark.user_id') collections = relationship('Collection', back_populates='user', foreign_keys='Collection.created_by') api_keys = relationship('ApiKey', back_populates='user', foreign_keys='ApiKey.user_id') audit_logs = relationship('AuditLog', back_populates='user', foreign_keys='AuditLog.user_id') class ApiKey(Base, TimestampMixin): """API Key for authentication.""" __tablename__ = 'api_keys' id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) user_id = Column(String(36), ForeignKey('users.id'), nullable=False, index=True) key_hash = Column(String(255), nullable=False, unique=True) name = Column(String(100)) expires_at = Column(DateTime) is_active = Column(Boolean, default=True) # Relationships user = relationship('User', back_populates='api_keys') class Tag(Base, TimestampMixin): """Tag model for bookmarks.""" __tablename__ = 'tags' id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) name = Column(String(100), unique=True, nullable=False) color = Column(String(7)) description = Column(Text) class Bookmark(Base, TimestampMixin): """Bookmark/Link model with Firefox-compatible fields.""" __tablename__ = 'links' id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) url = Column(String(2048), nullable=False, index=True) title = Column(String(255), nullable=False) description = Column(Text) notes = Column(Text) tags = Column(JSON, default=list) favicon_url = Column(String(512)) path = Column(String(512), nullable=True) # Folder structure path created_at = Column(DateTime, server_default=func.now(), nullable=False) updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False) visit_count = Column(Integer, default=0) is_bookmarked = Column(Boolean, default=False) source_set_id = Column(String(36), ForeignKey('links.id')) # Self-reference for duplicate tracking user_id = Column(String(36), ForeignKey('users.id'), nullable=True) # Relationships user = relationship('User', back_populates='bookmarks') source_set = relationship('Bookmark', remote_side=id) class Collection(Base, TimestampMixin): """Collection model for bookmark sets.""" __tablename__ = 'collections' id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) name = Column(String(200), nullable=False, unique=True) description = Column(Text) query_type = Column(String(20), nullable=False) # 'static' or 'dynamic' query_expression = Column(JSON) # Parsed AST for dynamic collections is_public = Column(Boolean, default=False) created_by = Column(String(36), ForeignKey('users.id'), nullable=False) # Relationships user = relationship('User', back_populates='collections') bookmarks = relationship('CollectionBookmark', back_populates='collection') class CollectionBookmark(Base, TimestampMixin): """Junction table for static collections.""" __tablename__ = 'collection_bookmarks' collection_id = Column(String(36), ForeignKey('collections.id'), primary_key=True) bookmark_id = Column(String(36), ForeignKey('links.id'), primary_key=True) # Relationships collection = relationship('Collection', back_populates='bookmarks') bookmark = relationship('Bookmark') class AuditLog(Base, TimestampMixin): """Audit log for tracking changes.""" __tablename__ = 'audit_log' id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) user_id = Column(String(36), ForeignKey('users.id', ondelete='SET NULL'), nullable=True) action = Column(String(100), nullable=False) entity_type = Column(String(50), nullable=False) entity_id = Column(String(36)) old_value = Column(JSON) new_value = Column(JSON) ip_address = Column(String(45)) # Create indexes __all__ = ['Base', 'User', 'ApiKey', 'Tag', 'Bookmark', 'Collection', 'CollectionBookmark', 'AuditLog']