Files
myworkspace/LinkSyncServer/queries/executor.py
DavidSaylor fe4cbc3537 feat: add web UI, query engine, session management, and 20 E2E tests
- 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)
2026-05-22 07:46:53 -05:00

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()