Feat v0.4.1 folders (#23)
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-go-pg (push) Successful in 2m38s
CI/CD / test-sqlite (push) Successful in 2m48s
CI/CD / build-and-deploy (push) Successful in 28s

Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #23.
This commit is contained in:
2026-03-29 12:23:47 +00:00
committed by xcaliber
parent 2c8dc59284
commit 03c182b9d1
7 changed files with 521 additions and 23 deletions

View File

@@ -1,4 +1,4 @@
# Notes — Starlark Backend (v0.1.0)
# Notes — Starlark Backend (v0.2.0)
#
# Markdown notes surface using ext_data.
#
@@ -76,6 +76,27 @@ def on_request(req):
if method == "DELETE" and path.startswith("/notes/"):
return _delete_note(path[len("/notes/"):], req)
# POST /notes/move
if method == "POST" and path == "/notes/move":
return _move_note(req)
# ── Folder routes ──────────────────────────
# GET /folders
if method == "GET" and path == "/folders":
return _list_folders()
# POST /folders
if method == "POST" and path == "/folders":
return _create_folder(req)
# PUT /folders/:id
if method == "PUT" and path.startswith("/folders/"):
return _update_folder(path[len("/folders/"):], req)
# DELETE /folders/:id
if method == "DELETE" and path.startswith("/folders/"):
return _delete_folder(path[len("/folders/"):])
return _resp(404, {"error": "not found"})
@@ -200,6 +221,102 @@ def _delete_note(note_id, req):
return _resp(200, {"archived": True})
# ═══════════════════════════════════════════════
# Move note between folders
# ═══════════════════════════════════════════════
def _move_note(req):
body = json.decode(req.get("body", "{}"))
note_id = _str(body.get("note_id", ""))
folder_id = _str(body.get("folder_id", ""))
if not note_id:
return _resp(400, {"error": "note_id required"})
existing = db.query("notes", filters={"id": note_id}, limit=1)
if not existing:
return _resp(404, {"error": "note not found"})
ok = db.update("notes", note_id, {"folder_id": folder_id, "updated_at": "now"})
if not ok:
return _resp(500, {"error": "move failed"})
rows = db.query("notes", filters={"id": note_id}, limit=1)
return _resp(200, rows[0] if rows else {})
# ═══════════════════════════════════════════════
# Folder CRUD
# ═══════════════════════════════════════════════
def _list_folders():
rows = db.query("folders", order="sort_order", limit=500)
items = []
for r in (rows or []):
items.append({
"id": r.get("id", ""),
"name": r.get("name", ""),
"parent_id": r.get("parent_id", ""),
"sort_order": _int(r.get("sort_order", 0)),
"creator_id": r.get("creator_id", ""),
"created_at": r.get("created_at", ""),
})
return _resp(200, {"data": items})
def _create_folder(req):
body = json.decode(req.get("body", "{}"))
user_id = req.get("user_id", "")
name = _str(body.get("name", ""))
if not name:
return _resp(400, {"error": "name required"})
row = db.insert("folders", {
"name": name,
"parent_id": _str(body.get("parent_id", "")),
"creator_id": user_id,
"sort_order": _int(body.get("sort_order", 0)),
})
return _resp(201, row)
def _update_folder(folder_id, req):
body = json.decode(req.get("body", "{}"))
existing = db.query("folders", filters={"id": folder_id}, limit=1)
if not existing:
return _resp(404, {"error": "folder not found"})
updates = {}
for key in ["name", "parent_id"]:
if key in body:
updates[key] = _str(body[key])
if "sort_order" in body:
updates["sort_order"] = _int(body["sort_order"])
if not updates:
return _resp(200, existing[0])
ok = db.update("folders", folder_id, updates)
if not ok:
return _resp(500, {"error": "update failed"})
rows = db.query("folders", filters={"id": folder_id}, limit=1)
return _resp(200, rows[0] if rows else {})
def _delete_folder(folder_id):
existing = db.query("folders", filters={"id": folder_id}, limit=1)
if not existing:
return _resp(404, {"error": "folder not found"})
parent_id = _str(existing[0].get("parent_id", ""))
# reparent child folders to deleted folder's parent
children = db.query("folders", filters={"parent_id": folder_id}, limit=500)
for child in (children or []):
db.update("folders", child["id"], {"parent_id": parent_id})
# orphan notes in this folder (move to unfiled)
notes_in_folder = db.query("notes", filters={"folder_id": folder_id}, limit=1000)
for n in (notes_in_folder or []):
db.update("notes", n["id"], {"folder_id": ""})
ok = db.delete("folders", folder_id)
if not ok:
return _resp(500, {"error": "delete failed"})
return _resp(200, {"deleted": True, "orphaned_notes": len(notes_in_folder or [])})
# ═══════════════════════════════════════════════
# Search
# ═══════════════════════════════════════════════
@@ -241,6 +358,7 @@ def _get_stats():
total = 0
pinned = 0
archived = 0
unfiled = 0
for n in notes:
if _int(n.get("archived", 0)) == 1:
archived = archived + 1
@@ -248,4 +366,8 @@ def _get_stats():
total = total + 1
if _int(n.get("pinned", 0)) == 1:
pinned = pinned + 1
return _resp(200, {"total": total, "pinned": pinned, "archived": archived})
if not _str(n.get("folder_id", "")):
unfiled = unfiled + 1
all_folders = db.query("folders", limit=10000)
folder_count = len(all_folders or [])
return _resp(200, {"total": total, "pinned": pinned, "archived": archived, "unfiled": unfiled, "folders": folder_count})