# Tasks — Starlark Backend (v0.1.0) # # Task management extension using ext_data + notifications + triggers. # # Entry points: # on_request(req) → surface API routes # on_task_event(event) → event trigger (task.*) # on_webhook(req) → webhook trigger for external task creation # # Modules: db, json, notifications, settings # ═══════════════════════════════════════════════ # Helpers # ═══════════════════════════════════════════════ def _resp(status, data): return {"status": status, "body": json.encode(data), "headers": {"Content-Type": "application/json"}} def _str(v): if v == None: return "" return str(v) def _now(): """ISO-ish timestamp from Starlark (no time module — use db auto-created_at or pass from caller).""" return "" def _statuses(): raw = settings.get("statuses") if not raw: return ["todo", "in_progress", "done", "cancelled"] return [s.strip() for s in raw.split(",") if s.strip()] def _priorities(): raw = settings.get("priorities") if not raw: return ["low", "medium", "high", "urgent"] return [s.strip() for s in raw.split(",") if s.strip()] # ═══════════════════════════════════════════════ # Surface API routes (/s/tasks/api/*) # ═══════════════════════════════════════════════ def on_request(req): path = req["path"] method = req["method"] # GET /items — list tasks if method == "GET" and path == "/items": return _list_items(req) # POST /items — create task if method == "POST" and path == "/items": return _create_item(req) # GET /stats — aggregate counts if method == "GET" and path == "/stats": return _get_stats() # GET /items/:id if method == "GET" and path.startswith("/items/"): return _get_item(path[len("/items/"):]) # PUT /items/:id if method == "PUT" and path.startswith("/items/"): return _update_item(path[len("/items/"):], req) # DELETE /items/:id if method == "DELETE" and path.startswith("/items/"): return _delete_item(path[len("/items/"):]) return _resp(404, {"error": "not found"}) def _list_items(req): q = req.get("query", {}) filters = {} status = _str(q.get("status", "")) if status: filters["status"] = status assignee = _str(q.get("assignee_id", "")) if assignee: filters["assignee_id"] = assignee priority = _str(q.get("priority", "")) if priority: filters["priority"] = priority creator = _str(q.get("creator_id", "")) if creator: filters["creator_id"] = creator order = _str(q.get("order", "")) or "created_at" limit_str = _str(q.get("limit", "")) limit = int(limit_str) if limit_str else 100 rows = db.query("items", filters=filters, order=order, limit=limit) return _resp(200, {"data": rows or []}) def _create_item(req): body = json.decode(req.get("body", "{}")) user_id = req.get("user_id", "") title = _str(body.get("title", "")) if not title: return _resp(400, {"error": "title is required"}) statuses = _statuses() status = _str(body.get("status", "")) if not status: status = statuses[0] if statuses else "todo" priorities = _priorities() priority = _str(body.get("priority", "")) if not priority: priority = priorities[0] if priorities else "medium" row = db.insert("items", { "title": title, "description": _str(body.get("description", "")), "status": status, "priority": priority, "assignee_id": _str(body.get("assignee_id", "")) or user_id, "creator_id": user_id, "due_date": _str(body.get("due_date", "")), "tags": _str(body.get("tags", "")), "updated_at": "", "completed_at":"", }) return _resp(201, row) def _get_item(item_id): rows = db.query("items", filters={"id": item_id}, limit=1) if not rows: return _resp(404, {"error": "task not found"}) return _resp(200, rows[0]) def _update_item(item_id, req): body = json.decode(req.get("body", "{}")) user_id = req.get("user_id", "") # Fetch existing to detect transitions existing = db.query("items", filters={"id": item_id}, limit=1) if not existing: return _resp(404, {"error": "task not found"}) old = existing[0] updates = {} for key in ["title", "description", "status", "priority", "assignee_id", "due_date", "tags"]: if key in body: updates[key] = _str(body[key]) # Detect completion transition new_status = updates.get("status", "") old_status = _str(old.get("status", "")) if new_status == "done" and old_status != "done": updates["completed_at"] = "now" # sentinel — db module handles timestamp ok = db.update("items", item_id, updates) if not ok: return _resp(500, {"error": "update failed"}) # Notify creator on completion if assignee is different if new_status == "done" and old_status != "done": creator = _str(old.get("creator_id", "")) assignee = _str(old.get("assignee_id", "")) if creator and assignee and creator != assignee: notifications.send( creator, "Task completed: " + _str(old.get("title", "")), body=_str(old.get("title", "")) + " was marked done by assignee.", ) # Re-fetch updated row rows = db.query("items", filters={"id": item_id}, limit=1) return _resp(200, rows[0] if rows else {}) def _delete_item(item_id): ok = db.delete("items", item_id) if not ok: return _resp(404, {"error": "task not found"}) return _resp(200, {"deleted": True}) def _get_stats(): all_items = db.query("items", limit=10000) items = all_items or [] counts = {} for s in _statuses(): counts[s] = 0 for item in items: st = _str(item.get("status", "")) if st in counts: counts[st] = counts[st] + 1 else: counts[st] = 1 return _resp(200, {"counts": counts, "total": len(items)}) # ═══════════════════════════════════════════════ # Event trigger handler # ═══════════════════════════════════════════════ def on_task_event(event): """Handle task lifecycle events emitted by the platform.""" label = event.get("event_label", "") payload = event.get("event_payload", {}) if type(payload) == "string": payload = json.decode(payload) if payload else {} # Could be used for audit logging, metrics, etc. # For now just a placeholder — actual notifications happen inline in _update_item. pass # ═══════════════════════════════════════════════ # Webhook trigger handler # ═══════════════════════════════════════════════ def on_webhook(req): """ External task creation via webhook. POST /api/v1/hooks/tasks/create with JSON body: { "title": "...", "description": "...", "priority": "...", "assignee_id": "..." } """ raw_body = req.get("body", "{}") body = json.decode(raw_body) if raw_body else {} title = _str(body.get("title", "")) if not title: return {"status": 400, "body": json.encode({"error": "title is required"})} priorities = _priorities() priority = _str(body.get("priority", "")) if not priority: priority = priorities[0] if priorities else "medium" statuses = _statuses() status = statuses[0] if statuses else "todo" row = db.insert("items", { "title": title, "description": _str(body.get("description", "")), "status": status, "priority": priority, "assignee_id": _str(body.get("assignee_id", "")), "creator_id": "", "due_date": _str(body.get("due_date", "")), "tags": _str(body.get("tags", "")), "updated_at": "", "completed_at":"", }) return {"status": 201, "body": json.encode(row), "headers": {"Content-Type": "application/json"}}