docs(18): create phase plan — schema + CRUD API for document templates
This commit is contained in:
184
.planning/phases/18-template-schema-and-crud-api/18-01-PLAN.md
Normal file
184
.planning/phases/18-template-schema-and-crud-api/18-01-PLAN.md
Normal file
@@ -0,0 +1,184 @@
|
||||
---
|
||||
phase: 18-template-schema-and-crud-api
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- teressa-copeland-homes/src/lib/db/schema.ts
|
||||
- teressa-copeland-homes/drizzle/0012_*.sql
|
||||
autonomous: true
|
||||
requirements: [TMPL-01]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "document_templates table exists in the Drizzle schema with all 7 columns"
|
||||
- "Migration file 0012_*.sql exists and contains CREATE TABLE document_templates"
|
||||
- "TypeScript compiles with no errors after schema change"
|
||||
artifacts:
|
||||
- path: "teressa-copeland-homes/src/lib/db/schema.ts"
|
||||
provides: "documentTemplates table definition and relations"
|
||||
contains: "documentTemplates"
|
||||
- path: "teressa-copeland-homes/drizzle/0012_*.sql"
|
||||
provides: "SQL migration creating document_templates table"
|
||||
contains: "document_templates"
|
||||
key_links:
|
||||
- from: "documentTemplates.formTemplateId"
|
||||
to: "formTemplates.id"
|
||||
via: "FK reference"
|
||||
pattern: "references.*formTemplates\\.id"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Add the `document_templates` Drizzle table to schema.ts and generate the SQL migration.
|
||||
|
||||
Purpose: Every subsequent template feature (editor, API, apply-to-document) depends on this table existing. This plan establishes the schema foundation.
|
||||
Output: Updated schema.ts with `documentTemplates` table + `drizzle/0012_*.sql` migration file.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/18-template-schema-and-crud-api/18-CONTEXT.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Existing types and tables the executor needs -->
|
||||
|
||||
From teressa-copeland-homes/src/lib/db/schema.ts:
|
||||
```typescript
|
||||
export interface SignatureFieldData {
|
||||
id: string;
|
||||
page: number;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
type?: SignatureFieldType;
|
||||
signerEmail?: string;
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
export const formTemplates = pgTable("form_templates", {
|
||||
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||
name: text("name").notNull(),
|
||||
filename: text("filename").notNull().unique(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
});
|
||||
```
|
||||
|
||||
Existing migration files: 0000 through 0011. Next migration is 0012.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Add documentTemplates table to schema.ts</name>
|
||||
<read_first>
|
||||
- teressa-copeland-homes/src/lib/db/schema.ts
|
||||
</read_first>
|
||||
<files>teressa-copeland-homes/src/lib/db/schema.ts</files>
|
||||
<action>
|
||||
Add the `documentTemplates` table definition to schema.ts, placed immediately after the `formTemplates` table definition (before the `documents` table). Per D-01, D-02, D-03, D-04, D-05, D-06:
|
||||
|
||||
```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<SignatureFieldData[]>(),
|
||||
archivedAt: timestamp("archived_at"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
});
|
||||
```
|
||||
|
||||
Key points:
|
||||
- `signatureFields` is nullable (per D-04) — template starts empty, editor fills it in Phase 19
|
||||
- `formTemplateId` FK has NO `onDelete` option (per D-06) — no cascade
|
||||
- `archivedAt` is nullable — NULL means active (per D-07)
|
||||
- `updatedAt` must be set explicitly in PATCH queries, not via DB trigger (per D-05)
|
||||
|
||||
Also add a relation from documentTemplates to formTemplates:
|
||||
|
||||
```typescript
|
||||
export const documentTemplatesRelations = relations(documentTemplates, ({ one }) => ({
|
||||
formTemplate: one(formTemplates, { fields: [documentTemplates.formTemplateId], references: [formTemplates.id] }),
|
||||
}));
|
||||
```
|
||||
|
||||
Do NOT add `onDelete: 'cascade'` to the formTemplateId FK. Per D-06, if a form is removed, templates referencing it should remain.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `documentTemplates` export exists in schema.ts
|
||||
- Table has exactly 7 columns: id, name, formTemplateId, signatureFields, archivedAt, createdAt, updatedAt
|
||||
- `formTemplateId` references `formTemplates.id` with no onDelete
|
||||
- `signatureFields` typed as `SignatureFieldData[]` via `$type`
|
||||
- `archivedAt` is nullable timestamp (no `.notNull()`)
|
||||
- `npx tsc --noEmit` passes with zero errors
|
||||
</acceptance_criteria>
|
||||
<done>documentTemplates table and relations defined in schema.ts, TypeScript compiles cleanly</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Generate Drizzle migration</name>
|
||||
<read_first>
|
||||
- teressa-copeland-homes/src/lib/db/schema.ts
|
||||
- teressa-copeland-homes/drizzle.config.ts
|
||||
</read_first>
|
||||
<files>teressa-copeland-homes/drizzle/0012_*.sql</files>
|
||||
<action>
|
||||
Per D-13, run the Drizzle migration generator from within the teressa-copeland-homes directory:
|
||||
|
||||
```bash
|
||||
cd teressa-copeland-homes && npx drizzle-kit generate
|
||||
```
|
||||
|
||||
This will produce `drizzle/0012_*.sql` (Drizzle auto-names it). The migration should contain a `CREATE TABLE "document_templates"` statement with all 7 columns and the FK constraint to `form_templates`.
|
||||
|
||||
After generation, read the migration file to verify it contains the expected CREATE TABLE statement and FK constraint. The migration should NOT contain any DROP or ALTER statements on existing tables.
|
||||
|
||||
Commit both schema.ts and the migration file.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>ls /Users/ccopeland/temp/red/teressa-copeland-homes/drizzle/0012_*.sql && grep -l "document_templates" /Users/ccopeland/temp/red/teressa-copeland-homes/drizzle/0012_*.sql</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- Migration file `drizzle/0012_*.sql` exists
|
||||
- File contains `CREATE TABLE` for `document_templates`
|
||||
- File contains FK constraint referencing `form_templates(id)`
|
||||
- File does NOT alter or drop any existing tables
|
||||
- No other 0012 migration files exist (exactly one)
|
||||
</acceptance_criteria>
|
||||
<done>Migration 0012 generated with CREATE TABLE document_templates, FK to form_templates, all 7 columns</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. `npx tsc --noEmit` passes — schema.ts compiles
|
||||
2. `ls drizzle/0012_*.sql` — exactly one migration file
|
||||
3. `grep "document_templates" drizzle/0012_*.sql` — CREATE TABLE present
|
||||
4. `grep "form_template_id" drizzle/0012_*.sql` — FK column present
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- documentTemplates table defined in schema.ts with all 7 columns per D-02
|
||||
- Drizzle relation to formTemplates exists
|
||||
- Migration 0012_*.sql generated and contains CREATE TABLE
|
||||
- TypeScript compiles with zero errors
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/18-template-schema-and-crud-api/18-01-SUMMARY.md`
|
||||
</output>
|
||||
336
.planning/phases/18-template-schema-and-crud-api/18-02-PLAN.md
Normal file
336
.planning/phases/18-template-schema-and-crud-api/18-02-PLAN.md
Normal file
@@ -0,0 +1,336 @@
|
||||
---
|
||||
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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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`.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.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
|
||||
|
||||
<interfaces>
|
||||
<!-- Schema from Plan 01 — executor reads these, does not recreate -->
|
||||
|
||||
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<SignatureFieldData[]>(),
|
||||
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;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Create GET and POST routes at /api/templates</name>
|
||||
<read_first>
|
||||
- teressa-copeland-homes/src/lib/db/schema.ts
|
||||
- teressa-copeland-homes/src/app/api/documents/route.ts
|
||||
- teressa-copeland-homes/src/lib/auth.ts
|
||||
</read_first>
|
||||
<files>teressa-copeland-homes/src/app/api/templates/route.ts</files>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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
|
||||
</acceptance_criteria>
|
||||
<done>GET /api/templates returns active templates with formName and fieldCount; POST /api/templates creates a template from a form library entry</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Create PATCH and DELETE routes at /api/templates/[id]</name>
|
||||
<read_first>
|
||||
- teressa-copeland-homes/src/lib/db/schema.ts
|
||||
- teressa-copeland-homes/src/app/api/documents/[id]/route.ts
|
||||
</read_first>
|
||||
<files>teressa-copeland-homes/src/app/api/templates/[id]/route.ts</files>
|
||||
<action>
|
||||
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<typeof documentTemplates.$inferInsert> = {
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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
|
||||
</acceptance_criteria>
|
||||
<done>PATCH /api/templates/[id] renames or updates fields with explicit updatedAt; DELETE /api/templates/[id] soft-deletes via archivedAt</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
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
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/18-template-schema-and-crud-api/18-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user