Initial commit: LinkSyncServer and LinkSyncExtension projects with complete documentation, models, API endpoints, tests, and extension implementation

This commit is contained in:
DavidSaylor
2026-05-11 17:37:10 -05:00
parent ad0b12b452
commit aed69afdfd
691 changed files with 181874 additions and 28 deletions

Binary file not shown.

View 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']