144 lines
5.5 KiB
Python
144 lines
5.5 KiB
Python
"""
|
|
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'] |