--- phase: 18-template-schema-and-crud-api plan: 02 type: execute wave: 2 depends_on: ["18-01"] files_modified: - teressa-copeland-homes/src/app/api/templates/route.ts - teressa-copeland-homes/src/app/api/templates/[id]/route.ts autonomous: true requirements: [TMPL-01, TMPL-02, TMPL-03, TMPL-04] must_haves: truths: - "POST /api/templates creates a document_templates row with name and formTemplateId" - "GET /api/templates returns active templates with form name and field count, excluding archived" - "PATCH /api/templates/[id] can rename a template and update signatureFields" - "DELETE /api/templates/[id] sets archivedAt instead of deleting the row" - "All routes return 401 when unauthenticated" artifacts: - path: "teressa-copeland-homes/src/app/api/templates/route.ts" provides: "GET (list) and POST (create) handlers" exports: ["GET", "POST"] - path: "teressa-copeland-homes/src/app/api/templates/[id]/route.ts" provides: "PATCH (update) and DELETE (soft-delete) handlers" exports: ["PATCH", "DELETE"] key_links: - from: "GET /api/templates" to: "documentTemplates + formTemplates" via: "LEFT JOIN for formName" pattern: "formTemplates.*name" - from: "DELETE /api/templates/[id]" to: "documentTemplates.archivedAt" via: "SET archivedAt = new Date()" pattern: "archivedAt.*new Date" --- Create all four CRUD API routes for document templates: list, create, rename/update, and soft-delete. Purpose: These routes are the interface Phase 19 (template editor UI) and Phase 20 (apply template) will call. Without them, no template can be created, listed, edited, or removed. Output: Two route files implementing GET, POST, PATCH, DELETE at `/api/templates`. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/18-template-schema-and-crud-api/18-CONTEXT.md @.planning/phases/18-template-schema-and-crud-api/18-01-SUMMARY.md From teressa-copeland-homes/src/lib/db/schema.ts (after Plan 01): ```typescript export const documentTemplates = pgTable("document_templates", { id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()), name: text("name").notNull(), formTemplateId: text("form_template_id").notNull().references(() => formTemplates.id), signatureFields: jsonb("signature_fields").$type(), archivedAt: timestamp("archived_at"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }); ``` Auth pattern (from teressa-copeland-homes/src/lib/auth.ts): ```typescript import { auth } from '@/lib/auth'; // Usage in route: const session = await auth(); if (!session) return new Response('Unauthorized', { status: 401 }); ``` Existing PATCH pattern (from teressa-copeland-homes/src/app/api/documents/[id]/route.ts): ```typescript export async function PATCH( req: Request, { params }: { params: Promise<{ id: string }> } ) { const session = await auth(); if (!session) return new Response('Unauthorized', { status: 401 }); const { id } = await params; // ... } ``` Task 1: Create GET and POST routes at /api/templates - teressa-copeland-homes/src/lib/db/schema.ts - teressa-copeland-homes/src/app/api/documents/route.ts - teressa-copeland-homes/src/lib/auth.ts teressa-copeland-homes/src/app/api/templates/route.ts Create `teressa-copeland-homes/src/app/api/templates/route.ts` with two handlers. Per D-10, D-11, D-08, D-12: **GET /api/templates** — list active templates: ```typescript import { auth } from '@/lib/auth'; import { db } from '@/lib/db'; import { documentTemplates, formTemplates } from '@/lib/db/schema'; import type { SignatureFieldData } from '@/lib/db/schema'; import { eq, isNull } from 'drizzle-orm'; export async function GET() { const session = await auth(); if (!session) return new Response('Unauthorized', { status: 401 }); const rows = await db .select({ id: documentTemplates.id, name: documentTemplates.name, formTemplateId: documentTemplates.formTemplateId, formName: formTemplates.name, signatureFields: documentTemplates.signatureFields, createdAt: documentTemplates.createdAt, updatedAt: documentTemplates.updatedAt, }) .from(documentTemplates) .leftJoin(formTemplates, eq(documentTemplates.formTemplateId, formTemplates.id)) .where(isNull(documentTemplates.archivedAt)); const result = rows.map((r) => ({ id: r.id, name: r.name, formTemplateId: r.formTemplateId, formName: r.formName, fieldCount: ((r.signatureFields as SignatureFieldData[] | null) ?? []).length, createdAt: r.createdAt, updatedAt: r.updatedAt, })); return Response.json(result); } ``` Key: `fieldCount` is computed server-side per D-12: `(signatureFields ?? []).length`. The `archivedAt IS NULL` filter per D-08 ensures archived templates are invisible. **POST /api/templates** — create template: ```typescript export async function POST(req: Request) { const session = await auth(); if (!session) return new Response('Unauthorized', { status: 401 }); const body = await req.json() as { name?: string; formTemplateId?: string }; if (!body.name || !body.formTemplateId) { return Response.json( { error: 'name and formTemplateId are required' }, { status: 400 } ); } // Verify form template exists const form = await db.query.formTemplates.findFirst({ where: eq(formTemplates.id, body.formTemplateId), }); if (!form) { return Response.json({ error: 'Form template not found' }, { status: 404 }); } const [template] = await db .insert(documentTemplates) .values({ name: body.name, formTemplateId: body.formTemplateId, }) .returning(); return Response.json(template, { status: 201 }); } ``` Key: `signatureFields` defaults to NULL on insert per D-04 — template starts empty. cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20 - File exists at `src/app/api/templates/route.ts` - Exports `GET` and `POST` functions - GET filters by `isNull(documentTemplates.archivedAt)` per D-08 - GET joins formTemplates for formName per D-10 - GET computes `fieldCount` from `signatureFields.length` per D-12 - POST validates `name` and `formTemplateId` are present - POST verifies formTemplateId FK exists before insert - POST returns 201 with the created template - Both handlers call `auth()` and return 401 if no session per D-11 - `npx tsc --noEmit` passes GET /api/templates returns active templates with formName and fieldCount; POST /api/templates creates a template from a form library entry Task 2: Create PATCH and DELETE routes at /api/templates/[id] - teressa-copeland-homes/src/lib/db/schema.ts - teressa-copeland-homes/src/app/api/documents/[id]/route.ts teressa-copeland-homes/src/app/api/templates/[id]/route.ts Create `teressa-copeland-homes/src/app/api/templates/[id]/route.ts` with two handlers. Per D-10, D-11, D-05, D-07: **PATCH /api/templates/[id]** — rename or save fields: ```typescript import { auth } from '@/lib/auth'; import { db } from '@/lib/db'; import { documentTemplates } from '@/lib/db/schema'; import type { SignatureFieldData } from '@/lib/db/schema'; import { eq, and, isNull } from 'drizzle-orm'; export async function PATCH( req: Request, { params }: { params: Promise<{ id: string }> } ) { const session = await auth(); if (!session) return new Response('Unauthorized', { status: 401 }); const { id } = await params; const body = await req.json() as { name?: string; signatureFields?: SignatureFieldData[]; }; // Only update non-archived templates const existing = await db.query.documentTemplates.findFirst({ where: and(eq(documentTemplates.id, id), isNull(documentTemplates.archivedAt)), }); if (!existing) { return Response.json({ error: 'Not found' }, { status: 404 }); } const updates: Partial = { updatedAt: new Date(), // per D-05: explicit updatedAt on every UPDATE }; if (body.name !== undefined) updates.name = body.name; if (body.signatureFields !== undefined) updates.signatureFields = body.signatureFields; const [updated] = await db .update(documentTemplates) .set(updates) .where(eq(documentTemplates.id, id)) .returning(); return Response.json(updated); } ``` Key: `updatedAt: new Date()` is set explicitly per D-05 — no DB trigger. The PATCH handles both rename (name) and field save (signatureFields) per D-10. Phase 19 will call this with signatureFields. **DELETE /api/templates/[id]** — soft-delete: ```typescript export async function DELETE( _req: Request, { params }: { params: Promise<{ id: string }> } ) { const session = await auth(); if (!session) return new Response('Unauthorized', { status: 401 }); const { id } = await params; // Only soft-delete non-archived templates (idempotent) const existing = await db.query.documentTemplates.findFirst({ where: and(eq(documentTemplates.id, id), isNull(documentTemplates.archivedAt)), }); if (!existing) { return Response.json({ error: 'Not found' }, { status: 404 }); } await db .update(documentTemplates) .set({ archivedAt: new Date(), // per D-07: soft-delete sets archivedAt updatedAt: new Date(), // per D-05: explicit updatedAt }) .where(eq(documentTemplates.id, id)); return new Response(null, { status: 204 }); } ``` Key: Per D-07, DELETE never removes the row — it sets `archivedAt = new Date()`. Per D-09, there is no restore endpoint. Returns 204 No Content on success. cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20 - File exists at `src/app/api/templates/[id]/route.ts` - Exports `PATCH` and `DELETE` functions - PATCH accepts `{ name?, signatureFields? }` body - PATCH always sets `updatedAt: new Date()` per D-05 - PATCH returns 404 for archived or missing templates - DELETE sets `archivedAt: new Date()` — never removes the row per D-07 - DELETE sets `updatedAt: new Date()` per D-05 - DELETE returns 204 on success - Both handlers use `await params` pattern matching existing [id] routes - Both handlers call `auth()` and return 401 if no session per D-11 - `npx tsc --noEmit` passes PATCH /api/templates/[id] renames or updates fields with explicit updatedAt; DELETE /api/templates/[id] soft-deletes via archivedAt 1. `npx tsc --noEmit` passes — both route files compile 2. `grep -r "auth()" src/app/api/templates/` — all handlers auth-gated 3. `grep "archivedAt" src/app/api/templates/` — soft-delete pattern present in both GET (filter) and DELETE (set) 4. `grep "updatedAt.*new Date" src/app/api/templates/[id]/route.ts` — explicit updatedAt in PATCH and DELETE - All four HTTP methods implemented: GET, POST, PATCH, DELETE - GET returns active templates with formName JOIN and computed fieldCount - POST creates template with name + formTemplateId, validates FK exists - PATCH handles name and signatureFields with explicit updatedAt - DELETE soft-deletes via archivedAt, returns 204 - All routes auth-gated, return 401 when unauthenticated - TypeScript compiles with zero errors After completion, create `.planning/phases/18-template-schema-and-crud-api/18-02-SUMMARY.md`