Initial commit: LinkSyncServer and LinkSyncExtension projects with complete documentation, models, API endpoints, tests, and extension implementation
This commit is contained in:
BIN
LinkSyncServer/models/__pycache__/base.cpython-313.pyc
Normal file
BIN
LinkSyncServer/models/__pycache__/base.cpython-313.pyc
Normal file
Binary file not shown.
144
LinkSyncServer/models/base.py
Normal file
144
LinkSyncServer/models/base.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""
|
||||
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']
|
||||
Reference in New Issue
Block a user