M6 1902a: richer facet query + saved-filters storage (backend)
GET /api/notes gains combinable, AND-ed facets alongside the existing filter/date/sort: multiple ?label= (notes with ALL), ?color, ?kind, ?has_reminder, ?has_attachment, and ?q (full-text over title+body, ranked) — so the facet bar's text box searches, not just filters. All optional; invalid color/kind → 400. saved_filters table (migration 0021) + /api/saved-filters CRUD (list / create / rename+repoint / delete, owner-scoped). `params` is a JSON facet dict mirroring the query surface; clean_params() whitelists facet keys so a saved view can't accumulate junk. Tests (DB-free): _truthy, clean_params key-whitelisting, saved-filters auth-guards. UI (facet bar + saved-views sidebar) lands next. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
@@ -6,6 +6,7 @@ from thoughtsync.notes import (
|
||||
_attachment_ext,
|
||||
_escape_like,
|
||||
_header_filename,
|
||||
_truthy,
|
||||
_keep_spec,
|
||||
_native_spec,
|
||||
_parse_iso_dt,
|
||||
@@ -259,6 +260,13 @@ def test_header_filename():
|
||||
assert _header_filename("") == "file"
|
||||
|
||||
|
||||
def test_truthy():
|
||||
assert _truthy("true") and _truthy("1") and _truthy("yes") and _truthy("on")
|
||||
assert not _truthy("false")
|
||||
assert not _truthy(None)
|
||||
assert not _truthy("")
|
||||
|
||||
|
||||
def test_usec_to_dt():
|
||||
# Google Keep timestamps are microseconds since the epoch (UTC).
|
||||
d = _usec_to_dt(1600000000000000)
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import pytest
|
||||
|
||||
from thoughtsync.app import create_app
|
||||
from thoughtsync.saved_filters import clean_params
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
return create_app()
|
||||
|
||||
|
||||
def test_clean_params_whitelists_facet_keys():
|
||||
raw = {
|
||||
"q": "hi",
|
||||
"color": "yellow",
|
||||
"labels": ["a"],
|
||||
"has_reminder": True,
|
||||
"junk": 1,
|
||||
"__proto__": 2,
|
||||
}
|
||||
assert clean_params(raw) == {"q": "hi", "color": "yellow", "labels": ["a"], "has_reminder": True}
|
||||
assert clean_params("nope") == {}
|
||||
assert clean_params(None) == {}
|
||||
|
||||
|
||||
async def test_list_saved_requires_auth(app):
|
||||
client = app.test_client()
|
||||
resp = await client.get("/api/saved-filters")
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
async def test_create_saved_requires_auth(app):
|
||||
client = app.test_client()
|
||||
resp = await client.post("/api/saved-filters", json={"name": "x"})
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
async def test_delete_saved_requires_auth(app):
|
||||
client = app.test_client()
|
||||
resp = await client.delete("/api/saved-filters/00000000-0000-0000-0000-000000000000")
|
||||
assert resp.status_code == 401
|
||||
Reference in New Issue
Block a user