From 59c4fa464808224567cfa439de69e6b43f5d73d2 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 1 Aug 2026 01:47:46 +0000 Subject: [PATCH] T4/Phase 2C-10: port custom fields routes to Hono (4 routes) --- apps/api/src/routes/custom-fields.ts | 151 +++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 apps/api/src/routes/custom-fields.ts diff --git a/apps/api/src/routes/custom-fields.ts b/apps/api/src/routes/custom-fields.ts new file mode 100644 index 0000000..8dcbe44 --- /dev/null +++ b/apps/api/src/routes/custom-fields.ts @@ -0,0 +1,151 @@ +import { Hono } from "hono"; +import { db, customFields } from "@project-e/db"; +import { and, asc, eq } from "drizzle-orm"; +import { requireAuth, resolveActiveDomain, AuthError } from "../middleware/auth"; +import { recordActivity } from "../middleware/activity"; +import { z } from "zod"; + +export const customFieldRoutes = new Hono(); + +const createFieldSchema = z.object({ + name: z.string().min(1, "Name is required"), + type: z.string().optional().default("text"), + entityType: z.string().min(1, "Entity type is required"), + domain: z.string().min(1, "Domain is required"), + required: z.boolean().optional().default(false), + options: z.array(z.string()).optional().default([]), + defaultValue: z.unknown().optional(), + sortOrder: z.number().int().optional().default(0), +}); + +const updateFieldSchema = z.object({ + name: z.string().min(1).optional(), + type: z.string().optional(), + required: z.boolean().optional(), + options: z.array(z.string()).optional(), + defaultValue: z.unknown().optional(), + sortOrder: z.number().int().optional(), +}); + +// GET /api/custom-fields?entity=... — List for an entity type +customFieldRoutes.get("/", async (c) => { + try { + const user = await requireAuth(c); + const entityType = c.req.query("entity"); + let domainId = c.req.query("domain") || undefined; + if (!domainId) { + const active = await resolveActiveDomain(user); + domainId = active.id; + } + + const conditions: any[] = [eq(customFields.domainId, domainId)]; + if (entityType) { + conditions.push(eq(customFields.entityType, entityType)); + } + + const items = await db.select() + .from(customFields) + .where(and(...conditions)) + .orderBy(asc(customFields.sortOrder), asc(customFields.name)); + + return c.json({ items, totalItems: items.length }); + } catch (error) { + if (error instanceof AuthError) return c.json({ error: { code: error.code, message: error.message } }, error.status as any); + console.error("[custom-fields] GET error:", error); + return c.json({ error: { code: "INTERNAL_ERROR", message: "Failed to list custom fields" } }, 500); + } +}); + +// POST /api/custom-fields — Create +customFieldRoutes.post("/", async (c) => { + try { + const user = await requireAuth(c); + const body = await c.req.json(); + const data = createFieldSchema.parse({ + ...body, + domain: body.domain || (await resolveActiveDomain(user)).id, + }); + + const [field] = await db.insert(customFields).values({ + name: data.name, + type: data.type, + entityType: data.entityType, + domainId: data.domain, + required: data.required, + options: data.options ?? [], + defaultValue: data.defaultValue ?? null, + sortOrder: data.sortOrder ?? 0, + }).returning(); + + await recordActivity({ + actor: user.name, action: "created", entityType: "custom_field", entityId: field.id, + changes: { name: field.name, entityType: field.entityType }, workspaceId: data.domain, + }); + + return c.json(field, 201); + } catch (error) { + if (error instanceof AuthError) return c.json({ error: { code: error.code, message: error.message } }, error.status as any); + if (error instanceof z.ZodError) return c.json({ error: { code: "VALIDATION_ERROR", message: "Invalid input", details: error.issues } }, 400); + console.error("[custom-fields] POST error:", error); + return c.json({ error: { code: "INTERNAL_ERROR", message: "Failed to create custom field" } }, 500); + } +}); + +// PATCH /api/custom-fields/:id — Update +customFieldRoutes.patch("/:id", async (c) => { + try { + const user = await requireAuth(c); + const id = c.req.param("id"); + const body = await c.req.json(); + const data = updateFieldSchema.parse(body); + + const [existing] = await db.select().from(customFields).where(eq(customFields.id, id)).limit(1); + if (!existing) return c.json({ error: { code: "NOT_FOUND", message: "Custom field not found" } }, 404); + + const updateValues: Record = {}; + if (data.name !== undefined) updateValues.name = data.name; + if (data.type !== undefined) updateValues.type = data.type; + if (data.required !== undefined) updateValues.required = data.required; + if (data.options !== undefined) updateValues.options = data.options; + if (data.defaultValue !== undefined) updateValues.defaultValue = data.defaultValue; + if (data.sortOrder !== undefined) updateValues.sortOrder = data.sortOrder; + updateValues.updatedAt = new Date(); + + const [updated] = await db.update(customFields).set(updateValues).where(eq(customFields.id, id)).returning(); + + await recordActivity({ + actor: user.name, action: "updated", entityType: "custom_field", entityId: id, + changes: { name: updated.name }, workspaceId: existing.domainId, + }); + + return c.json(updated); + } catch (error) { + if (error instanceof AuthError) return c.json({ error: { code: error.code, message: error.message } }, error.status as any); + if (error instanceof z.ZodError) return c.json({ error: { code: "VALIDATION_ERROR", message: "Invalid input", details: error.issues } }, 400); + console.error("[custom-fields] PATCH error:", error); + return c.json({ error: { code: "INTERNAL_ERROR", message: "Failed to update custom field" } }, 500); + } +}); + +// DELETE /api/custom-fields/:id — Delete +customFieldRoutes.delete("/:id", async (c) => { + try { + const user = await requireAuth(c); + const id = c.req.param("id"); + const [existing] = await db.select().from(customFields).where(eq(customFields.id, id)).limit(1); + if (!existing) return c.json({ error: { code: "NOT_FOUND", message: "Custom field not found" } }, 404); + + await db.delete(customFields).where(eq(customFields.id, id)); + + await recordActivity({ + actor: user.name, action: "deleted", entityType: "custom_field", entityId: id, + changes: { name: existing.name }, workspaceId: existing.domainId, + }); + + return c.body(null, 204); + } catch (error) { + if (error instanceof AuthError) return c.json({ error: { code: error.code, message: error.message } }, error.status as any); + console.error("[custom-fields] DELETE error:", error); + return c.json({ error: { code: "INTERNAL_ERROR", message: "Failed to delete custom field" } }, 500); + } +});