253 lines
6.7 KiB
Python
253 lines
6.7 KiB
Python
"""
|
|
LinkSyncServer - Query Engine
|
|
"""
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
from typing import List, Optional, Dict, Any
|
|
import re
|
|
import uuid
|
|
|
|
router = APIRouter(prefix="/api/queries", tags=["Queries"])
|
|
|
|
|
|
def tokenize(query: str) -> List[str]:
|
|
"""Tokenize query string."""
|
|
# Remove parentheses first, tokenize, then track nesting
|
|
tokens = []
|
|
current_token = ""
|
|
paren_depth = 0
|
|
i = 0
|
|
while i < len(query):
|
|
c = query[i]
|
|
if c == '(':
|
|
paren_depth += 1
|
|
current_token += c
|
|
elif c == ')':
|
|
paren_depth -= 1
|
|
current_token += c
|
|
elif c in ' \t\n' or paren_depth == 0 and c in ' ,':
|
|
if current_token:
|
|
tokens.append(current_token)
|
|
current_token = ""
|
|
else:
|
|
current_token += c
|
|
i += 1
|
|
if current_token:
|
|
tokens.append(current_token)
|
|
return tokens
|
|
|
|
|
|
class TermSet:
|
|
"""Term set: ('term1', 'term2') -> OR operation"""
|
|
def __init__(self, terms: List[str]):
|
|
self.terms = terms
|
|
self.operation = "OR"
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
"type": "term_set",
|
|
"terms": self.terms,
|
|
"operation": self.operation
|
|
}
|
|
|
|
|
|
class TagFilter:
|
|
"""Tag-based filter"""
|
|
def __init__(self, tag_name: str):
|
|
self.tag_name = tag_name
|
|
self.operation = "TAG"
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
"type": "tag_filter",
|
|
"tag_name": self.tag_name,
|
|
"operation": self.operation
|
|
}
|
|
|
|
|
|
class FieldFilter:
|
|
"""Field-based filter (e.g., url:example.com)"""
|
|
def __init__(self, field: str, value: str):
|
|
self.field = field
|
|
self.value = value
|
|
self.operation = "FIELD"
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
"type": "field_filter",
|
|
"field": self.field,
|
|
"value": self.value,
|
|
"operation": self.operation
|
|
}
|
|
|
|
|
|
class ANDNode:
|
|
"""AND operation node"""
|
|
def __init__(self, left, right):
|
|
self.left = left
|
|
self.right = right
|
|
self.operation = "AND"
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
"type": "binary",
|
|
"operation": self.operation,
|
|
"left": self.left.to_dict(),
|
|
"right": self.right.to_dict()
|
|
}
|
|
|
|
|
|
class ORNode:
|
|
"""OR operation node"""
|
|
def __init__(self, left, right):
|
|
self.left = left
|
|
self.right = right
|
|
self.operation = "OR"
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
"type": "binary",
|
|
"operation": self.operation,
|
|
"left": self.left.to_dict(),
|
|
"right": self.right.to_dict()
|
|
}
|
|
|
|
|
|
class XORNode:
|
|
"""XOR operation node"""
|
|
def __init__(self, left, right):
|
|
self.left = left
|
|
self.right = right
|
|
self.operation = "XOR"
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
"type": "binary",
|
|
"operation": self.operation,
|
|
"left": self.left.to_dict(),
|
|
"right": self.right.to_dict()
|
|
}
|
|
|
|
|
|
class NOTNode:
|
|
"""NOT operation node"""
|
|
def __init__(self, child):
|
|
self.child = child
|
|
self.operation = "NOT"
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
"type": "unary",
|
|
"operation": self.operation,
|
|
"child": self.child.to_dict()
|
|
}
|
|
|
|
|
|
def parse_query(query: str) -> Dict[str, Any]:
|
|
"""
|
|
Parse query expression: ('term1', 'term2') OR tagA AND tagB XOR url:example.com
|
|
Precedence: () > XOR > AND > OR
|
|
"""
|
|
tokens = tokenize(query)
|
|
|
|
# Remove parentheses and tokenize
|
|
tokens = tokenize(query)
|
|
|
|
# Simple parser for basic queries
|
|
# For full parser, would need recursive descent
|
|
|
|
# Handle term sets: ('term1', 'term2')
|
|
term_set = None
|
|
i = 0
|
|
while i < len(tokens):
|
|
token = tokens[i]
|
|
if token.startswith('(') and tokens[i].endswith(')'):
|
|
# Extract terms from tuple
|
|
inner = token[1:-1]
|
|
terms = [t.strip("'\"") for t in inner.split(',')]
|
|
term_set = TermSet(terms)
|
|
i += 1
|
|
else:
|
|
break
|
|
|
|
if not term_set:
|
|
# Parse as simple expression
|
|
# This is a simplified parser for demo
|
|
return {"type": "term_set", "terms": []}
|
|
|
|
return term_set.to_dict()
|
|
|
|
|
|
def execute_query(query_expression: dict, all_bookmarks: List[dict]) -> List[dict]:
|
|
"""
|
|
Execute query expression against bookmark list.
|
|
For demo, returns mock results.
|
|
"""
|
|
# Query AST evaluation would go here
|
|
# For now, return mock results
|
|
return [
|
|
{
|
|
"id": str(uuid.uuid4()),
|
|
"url": "https://example.com/result",
|
|
"title": "Query Result",
|
|
"description": "A result from the query",
|
|
"notes": "",
|
|
"tags": ["query", "result"],
|
|
"favicon_url": None,
|
|
"path": "/Query Result",
|
|
"created_at": "2026-05-11T00:00:00Z",
|
|
"updated_at": "2026-05-11T00:00:00Z",
|
|
"visit_count": 0,
|
|
"is_bookmarked": False,
|
|
"source_set_id": None
|
|
}
|
|
]
|
|
|
|
|
|
@router.post("/parse", response_model=Dict[str, Any])
|
|
async def parse_expression(query: str):
|
|
"""Parse and validate query expression."""
|
|
parsed = parse_query(query)
|
|
return {
|
|
"expression": query,
|
|
"parsed": parsed,
|
|
"valid": True
|
|
}
|
|
|
|
|
|
@router.post("/execute", response_model=List[dict])
|
|
async def execute(query_expression: dict, limit: int = 20):
|
|
"""Execute query against bookmarks."""
|
|
# For demo, return mock results
|
|
return [
|
|
{
|
|
"id": str(uuid.uuid4()),
|
|
"url": "https://example.com/queried",
|
|
"title": "Queried Item",
|
|
"description": "Item from query",
|
|
"notes": "",
|
|
"tags": ["queried"],
|
|
"favicon_url": None,
|
|
"path": "/Queried",
|
|
"created_at": "2026-05-11T00:00:00Z",
|
|
"updated_at": "2026-05-11T00:00:00Z",
|
|
"visit_count": 0,
|
|
"is_bookmarked": False,
|
|
"source_set_id": None
|
|
}
|
|
]
|
|
|
|
|
|
@router.get("/{query_id}", response_model=Dict[str, Any])
|
|
async def get_saved_query(query_id: str):
|
|
"""Get saved query by ID."""
|
|
return {
|
|
"id": query_id,
|
|
"name": "Example Query",
|
|
"description": "Example query description",
|
|
"expression": "('work', 'dev') OR tag:work",
|
|
"query_type": "dynamic",
|
|
"is_public": False,
|
|
"created_at": "2026-05-11T00:00:00Z",
|
|
"updated_at": "2026-05-11T00:00:00Z"
|
|
} |