UNPKG

userdo

Version:

A Durable Object base class for building applications on Cloudflare Workers.

127 lines (126 loc) 4.26 kB
import { createUserDOWorker, createWebSocketHandler, getUserDOFromContext, UserDO, broadcastToUser } from 'userdo'; import { z } from 'zod'; // Define our task data schema const TaskSchema = z.object({ title: z.string(), description: z.string(), completed: z.boolean(), createdAt: z.string(), }); // Extend UserDO with our business logic export class TaskAppDO extends UserDO { constructor(state, env) { super(state, env); this.tasks = this.table('tasks', TaskSchema, { userScoped: true }); } async createTask(title, description) { const taskData = { title, description, completed: false, createdAt: new Date().toISOString(), }; const result = await this.tasks.create(taskData); return result; } async getTasks() { return await this.tasks.orderBy('createdAt', 'desc').get(); } async updateTask(id, updates) { await this.tasks.update(id, updates); return { ok: true }; } async deleteTask(id) { await this.tasks.delete(id); return { ok: true }; } async toggleTask(id) { const task = await this.tasks.findById(id); if (!task) throw new Error('Task not found'); await this.tasks.update(id, { completed: !task.completed }); return { ok: true }; } } // Export TaskAppDO as UserDO for Durable Object binding (required by Cloudflare) export { TaskAppDO as UserDO }; // Create the worker with our custom binding name const taskWorker = createUserDOWorker('TASK_APP_DO'); // Create WebSocket handler for real-time features const webSocketHandler = createWebSocketHandler('TASK_APP_DO'); // Helper functions const getTaskAppDO = (c, email) => { return getUserDOFromContext(c, email, 'TASK_APP_DO'); }; const requireAuth = (c) => { const user = c.get('user'); if (!user) { return { error: c.json({ error: 'Unauthorized' }, 401), user: null }; } return { user, error: null }; }; const broadcastTaskChange = (email, action, data, env) => { broadcastToUser(email, { event: 'table:tasks', data: { action, ...data }, timestamp: Date.now() }, 'TASK_APP_DO', env); }; // --- API ENDPOINTS --- // Get all tasks taskWorker.get("/api/tasks", async (c) => { const { user, error } = requireAuth(c); if (error) return error; const taskAppDO = getTaskAppDO(c, user.email); const tasks = await taskAppDO.getTasks(); return c.json({ ok: true, tasks }); }); // Create a new task taskWorker.post("/api/tasks", async (c) => { const { user, error } = requireAuth(c); if (error) return error; const { title, description } = await c.req.json(); if (!title) { return c.json({ error: "Title is required" }, 400); } const taskAppDO = getTaskAppDO(c, user.email); const task = await taskAppDO.createTask(title, description || ''); // Broadcast WebSocket notification broadcastTaskChange(user.email, 'create', { data: task }, c.env); return c.json({ ok: true, data: task }); }); // Toggle task completion taskWorker.post("/api/tasks/:id/toggle", async (c) => { const { user, error } = requireAuth(c); if (error) return error; const taskId = c.req.param('id'); const taskAppDO = getTaskAppDO(c, user.email); await taskAppDO.toggleTask(taskId); // Broadcast WebSocket notification broadcastTaskChange(user.email, 'toggle', { id: taskId }, c.env); return c.json({ ok: true }); }); // Delete a task taskWorker.delete("/api/tasks/:id", async (c) => { const { user, error } = requireAuth(c); if (error) return error; const taskId = c.req.param('id'); const taskAppDO = getTaskAppDO(c, user.email); await taskAppDO.deleteTask(taskId); // Broadcast WebSocket notification broadcastTaskChange(user.email, 'delete', { id: taskId }, c.env); return c.json({ ok: true }); }); // Export the worker export default { async fetch(request, env, ctx) { if (request.headers.get('upgrade') === 'websocket') { return webSocketHandler.fetch(request, env, ctx); } return taskWorker.fetch(request, env, ctx); } };