- Web UI: login, dashboard, links CRUD, collections, API keys, admin pages - Query engine: AND/OR/XOR with field filters, tag search, preview endpoint - Session management: token expiry detection, 401 interceptor, expiry banner - Links search: tags included, multi-word AND, query mode with set operations - Collections: static/dynamic, query builder with preview, public tree view - Save as Collection: convert search results (static) or query (dynamic) - Dashboard stats: resilient loading with allSettled pattern - Login page: redesigned with public collections tree view - Bug fix: query executor None fields crash (notes/description/url/title) - E2E tests: 20 Playwright tests covering all critical user flows - All 104 tests passing (84 unit/integration + 20 E2E)
103 lines
3.5 KiB
Python
103 lines
3.5 KiB
Python
"""
|
|
LinkSyncServer - Query Executor
|
|
"""
|
|
|
|
import logging
|
|
from typing import Any, Dict, List
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def execute_query(parsed: Dict[str, Any], bookmarks: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
if not parsed or not bookmarks:
|
|
return []
|
|
|
|
result_ids = _evaluate_node(parsed, bookmarks)
|
|
return [b for b in bookmarks if b["id"] in result_ids]
|
|
|
|
|
|
def _evaluate_node(node: Dict[str, Any], bookmarks: List[Dict[str, Any]]) -> set:
|
|
operation = node.get("operation", "")
|
|
|
|
if operation == "OR":
|
|
operands = node.get("operands", [])
|
|
if not operands:
|
|
return set()
|
|
result = _evaluate_node(operands[0], bookmarks)
|
|
for operand in operands[1:]:
|
|
result |= _evaluate_node(operand, bookmarks)
|
|
return result
|
|
|
|
if operation == "AND":
|
|
operands = node.get("operands", [])
|
|
if not operands:
|
|
return set()
|
|
result = _evaluate_node(operands[0], bookmarks)
|
|
for operand in operands[1:]:
|
|
result &= _evaluate_node(operand, bookmarks)
|
|
return result
|
|
|
|
if operation == "XOR":
|
|
operands = node.get("operands", [])
|
|
if not operands:
|
|
return set()
|
|
result = _evaluate_node(operands[0], bookmarks)
|
|
for operand in operands[1:]:
|
|
result ^= _evaluate_node(operand, bookmarks)
|
|
return result
|
|
|
|
if operation == "TERM":
|
|
value = node.get("value", "").lower()
|
|
return {
|
|
b["id"]
|
|
for b in bookmarks
|
|
if value in (b.get("title") or "").lower()
|
|
or value in (b.get("description") or "").lower()
|
|
or value in (b.get("url") or "").lower()
|
|
or value in (b.get("notes") or "").lower()
|
|
or any(value in t.lower() for t in (b.get("tags") or []))
|
|
}
|
|
|
|
if operation == "TERM_SET":
|
|
terms = node.get("value", [])
|
|
terms_lower = [t.lower() for t in terms]
|
|
result = set()
|
|
for b in bookmarks:
|
|
text = (
|
|
f"{(b.get('title') or '')} {(b.get('description') or '')} {(b.get('url') or '')} {(b.get('notes') or '')}"
|
|
).lower()
|
|
tags_lower = [t.lower() for t in (b.get("tags") or [])]
|
|
if any(term in text for term in terms_lower) or any(term in tags_lower for term in terms_lower):
|
|
result.add(b["id"])
|
|
return result
|
|
|
|
if operation.startswith("FIELD:"):
|
|
field = operation.split(":", 1)[1].upper()
|
|
value = node.get("value", "").lower()
|
|
return _evaluate_field(field, value, bookmarks)
|
|
|
|
logger.warning(f"Unknown operation: {operation}")
|
|
return set()
|
|
|
|
|
|
def _evaluate_field(field: str, value: str, bookmarks: List[Dict[str, Any]]) -> set:
|
|
if field == "URL":
|
|
return {b["id"] for b in bookmarks if value in b.get("url", "").lower()}
|
|
if field == "TAG":
|
|
return {
|
|
b["id"]
|
|
for b in bookmarks
|
|
if any(value in t.lower() for t in (b.get("tags") or []))
|
|
}
|
|
if field == "TITLE":
|
|
return {b["id"] for b in bookmarks if value in b.get("title", "").lower()}
|
|
if field == "DESCRIPTION":
|
|
return {b["id"] for b in bookmarks if value in b.get("description", "").lower()}
|
|
if field == "PATH":
|
|
return {b["id"] for b in bookmarks if value in (b.get("path") or "").lower()}
|
|
if field == "ID":
|
|
return {b["id"] for b in bookmarks if b.get("id") == value}
|
|
|
|
logger.warning(f"Unknown field: {field}")
|
|
return set()
|