Files
myworkspace/LinkSyncServer/api/endpoints/collections.py

234 lines
7.4 KiB
Python

"""
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