userdo
Version:
A Durable Object base class for building applications on Cloudflare Workers.
127 lines (126 loc) • 4.26 kB
JavaScript
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);
}
};