SPB Git
3.8 KB · 47 lines typescript
1import { z } from "zod";2import { and, eq, isNull, sql as dsql } from "drizzle-orm";3import { handler, parseBody, uuid, HttpError } from "@/lib/api";4import { db, schema } from "@/db";5import { audit } from "@/lib/audit";67const cols = { id: schema.tasks.id, title: schema.tasks.title, status: schema.tasks.status, priority: schema.tasks.priority, dueAt: schema.tasks.dueAt, completedAt: schema.tasks.completedAt, project: schema.tasks.project, pageId: schema.tasks.pageId, createdAt: schema.tasks.createdAt, updatedAt: schema.tasks.updatedAt, pageTitle: schema.pages.title, pageIcon: schema.pages.icon };8const priority = z.enum(["low", "medium", "high"]);9const dueAt = z.string().datetime({ offset: true }).nullable();1011/** GET ?done=1 → all open tasks (+ done ones when done=1), with linked page title. */12export const GET = handler(async ({ req, ws }) => {13  const withDone = new URL(req.url).searchParams.get("done") === "1";14  const rows = await db.select(cols).from(schema.tasks).leftJoin(schema.pages, eq(schema.pages.id, schema.tasks.pageId))15    .where(and(eq(schema.tasks.workspaceId, ws.id), isNull(schema.tasks.deletedAt), withDone ? undefined : dsql`${schema.tasks.status} <> 'done'`))16    .orderBy(dsql`case when ${schema.tasks.status} = 'done' then 1 else 0 end`, dsql`${schema.tasks.dueAt} asc nulls last`, dsql`${schema.tasks.createdAt} desc`).limit(500);17  return Response.json(rows);18});19export const POST = handler(async ({ req, ws, user }) => {20  const b = await parseBody(req, z.object({ title: z.string().trim().min(1).max(500), dueAt: dueAt.optional(), priority: priority.optional(), project: z.string().trim().max(100).nullable().optional(), pageId: uuid.nullable().optional() }));21  const [t] = await db.insert(schema.tasks).values({ workspaceId: ws.id, title: b.title, dueAt: b.dueAt ? new Date(b.dueAt) : null, priority: b.priority ?? "medium", project: b.project || null, pageId: b.pageId ?? null }).returning();22  await audit(user.id, "task.create", { type: "task", id: t.id }, { title: t.title });23  return Response.json(t, { status: 201 });24});25export const PATCH = handler(async ({ req, ws, user }) => {26  const b = await parseBody(req, z.object({ id: uuid, title: z.string().trim().min(1).max(500).optional(), dueAt: dueAt.optional(), priority: priority.optional(), project: z.string().trim().max(100).nullable().optional(), pageId: uuid.nullable().optional(), status: z.enum(["todo", "done"]).optional() }));27  const { id, ...patch } = b;28  const set: Partial<typeof schema.tasks.$inferInsert> = { updatedAt: new Date() };29  if (patch.title !== undefined) set.title = patch.title;30  if (patch.dueAt !== undefined) set.dueAt = patch.dueAt ? new Date(patch.dueAt) : null;31  if (patch.priority !== undefined) set.priority = patch.priority;32  if (patch.project !== undefined) set.project = patch.project || null;33  if (patch.pageId !== undefined) set.pageId = patch.pageId;34  if (patch.status !== undefined) { set.status = patch.status; set.completedAt = patch.status === "done" ? new Date() : null; }35  const [t] = await db.update(schema.tasks).set(set).where(and(eq(schema.tasks.id, id), eq(schema.tasks.workspaceId, ws.id), isNull(schema.tasks.deletedAt))).returning();36  if (!t) throw new HttpError(404, "Task not found");37  if (patch.status !== undefined) await audit(user.id, patch.status === "done" ? "task.complete" : "task.reopen", { type: "task", id });38  return Response.json(t);39});40export const DELETE = handler(async ({ req, ws, user }) => {41  const id = uuid.parse(new URL(req.url).searchParams.get("id"));42  const [t] = await db.update(schema.tasks).set({ deletedAt: new Date() }).where(and(eq(schema.tasks.id, id), eq(schema.tasks.workspaceId, ws.id))).returning({ id: schema.tasks.id });43  if (!t) throw new HttpError(404, "Task not found");44  await audit(user.id, "task.delete", { type: "task", id });45  return Response.json({ ok: true });46});47
Files · main
Menu

Theme