""" LinkSyncServer - Collection CRUD Endpoints with SQLAlchemy """ from fastapi import APIRouter, Depends, HTTPException, status, Query, Request from sqlalchemy.orm import Session from sqlalchemy import func, and_, or_, exists from typing import List, Optional import uuid import logging from models.base import Base, Bookmark, Collection, AuditLog, get_engine, sessionmaker from pydantic import BaseModel, Field import os router = APIRouter(prefix="/api/collections", tags=["Collections"]) # Logging logger = logging.getLogger(__name__) class CollectionCreate(BaseModel): name: str = Field(..., description="Collection name") description: Optional[str] = Field(None, max_length=1024, description="Collection description") query_type: str = Field(default="static", description="Static or dynamic collection") query_expression: Optional[dict] = Field(None, description="Query expression for dynamic collections") is_public: bool = Field(default=False, description="Is collection public") tags: Optional[List[str]] = Field(default_factory=list, description="Collection tags") class CollectionUpdate(BaseModel): name: Optional[str] = Field(None, max_length=255) description: Optional[str] = Field(None, max_length=1024) query_type: Optional[str] = Field(None) query_expression: Optional[dict] = Field(None) is_public: Optional[bool] = None tags: Optional[List[str]] = Field(None) class CollectionResponse(BaseModel): id: str name: str description: Optional[str] query_type: str query_expression: Optional[dict] is_public: bool created_at: str updated_at: str tags: List[str] def get_db(): """Get database session.""" db_session = sessionmaker(get_engine())() return db_session def get_current_user(request: Request): """Get current authenticated user.""" SECRET_KEY = os.environ.get("SECRET_KEY") auth_header = request.headers.get("Authorization") or request.headers.get("authorization") if auth_header and auth_header.startswith("Bearer "): token = auth_header[7:] try: import jwt payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"]) return {"username": payload.get("sub"), "id": payload.get("sub")} except Exception: pass return {"username": "guest"} class CollectionManager: """Collection management helper.""" @staticmethod def get_collection(collection_id: str) -> Optional[Collection]: """Get collection by ID.""" db = get_db() try: collection = db.query(Collection).filter(Collection.id == collection_id).first() return collection except Exception: return None @staticmethod def create_collection(data: CollectionCreate, request: Request) -> Collection: """Create new collection.""" db = get_db() collection = Collection( name=data.name, description=data.description, query_type=data.query_type, query_expression=data.query_expression, is_public=data.is_public, tags=TagCollection(tags=data.tags or []), ) db.add(collection) db.commit() db.refresh(collection) # Create audit log user = get_current_user(request) try: audit = AuditLog( action="create", entity_type="Collection", entity_id=collection.id, old_value=None, new_value=collection.dict(), user_id=user.get("id") ) db.add(audit) db.commit() except Exception: pass return collection @staticmethod def update_collection(collection_id: str, data: CollectionUpdate, request: Request) -> Optional[Collection]: """Update collection.""" db = get_db() collection = db.query(Collection).filter(Collection.id == collection_id).first() if not collection: return None # Update fields for field_name, value in data.dict().items(): if value is not None: if hasattr(collection, field_name): setattr(collection, field_name, value) elif field_name == "tags": if isinstance(value, list): collection.tags.add(*value) else: collection.tags.update(str(value)) db.commit() db.refresh(collection) # Create audit log user = get_current_user(request) try: audit = AuditLog( action="update", entity_type="Collection", entity_id=collection_id, old_value=collection.dict(), new_value=collection.dict(), user_id=user.get("id") ) db.add(audit) db.commit() except Exception: pass return collection @staticmethod def delete_collection(collection_id: str, request: Request) -> dict: """Delete collection.""" db = get_db() collection = db.query(Collection).filter(Collection.id == collection_id).first() if not collection: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Collection not found") db.delete(collection) db.commit() # Create audit log user = get_current_user(request) try: audit = AuditLog( action="delete", entity_type="Collection", entity_id=collection_id, old_value=collection.dict(), new_value=None, user_id=user.get("id") ) db.add(audit) db.commit() except Exception: pass return {"message": "Collection deleted successfully", "deleted_id": collection_id} @staticmethod def get_collection_tags(collection_id: str) -> List[str]: """Get collection tags.""" db = get_db() collection = db.query(Collection).filter(Collection.id == collection_id).first() if not collection: return [] return list(collection.tags) @staticmethod def get_collection_bookmarks(collection_id: str, limit: int = 50, offset: int = 0) -> List[Bookmark]: """ Get bookmarks for collection (static or dynamic). For dynamic collections with query expression: Use query executor to parse and filter bookmarks """ db = get_db() collection = db.query(Collection).filter(Collection.id == collection_id).first() if not collection: return [] if collection.query_type == "static": # Static collection: get all bookmarks bookmarks = db.query(Bookmark).filter(Bookmark.collection_id == collection_id).limit(limit).offset(offset).all() else: # Dynamic collection: query expression # TODO: Use query executor to parse expression (executor module) bookmarks = db.query(Bookmark).limit(limit).offset(offset).all() return bookmarks