LinkSyncServer: - Fix app.py imports, add CORS middleware, lifespan events - Create api/routes.py router aggregator - Create config/settings.py for centralized configuration - Rewrite models/base.py with proper relationships and serialization - Rewrite all API endpoints with real DB integration (auth, links, collections, sync, queries, tags) - Add admin endpoints (user management, stats, audit log) - Complete query parser with recursive descent and proper precedence - Complete query executor with set operations and field filters - Set up Alembic migrations with initial schema - Create web interface (templates, CSS, JS) - Add 42 passing tests (auth, links, collections, queries) - Add deploy.ps1 and deploy.sh scripts - Update README with deployment workflow LinkSyncExtension: - Create utils/api.js (REST client with retries, auth, error handling) - Create utils/sync.js (3 sync modes + conflict detection) - Create utils/collection.js (collection management) - Create utils/query-engine.js (client-side query parser) - Rewrite background.js (sync loop, bookmark events, message routing) - Rewrite popup.js (tabs, settings modal, notifications, CRUD) - Update popup.html (tabbed interface, query builder, modal) - Update popup.css (full redesign) - Create content/content.js (page metadata extraction) - Create options.html/js (dedicated settings page) - Generate icons (48x48, 96x96) - Update manifest.json (host permissions, content scripts, options) - Create AGENTS.md
101 lines
3.2 KiB
Python
101 lines
3.2 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", "").lower()
|
|
or value in b.get("description", "").lower()
|
|
or value in b.get("url", "").lower()
|
|
or value in b.get("notes", "").lower()
|
|
}
|
|
|
|
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', '')} {b.get('description', '')} {b.get('url', '')} {b.get('notes', '')}"
|
|
).lower()
|
|
if any(term in text 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()
|