# API Design Instructions > **Parent Document**: `main-instructions.md` - Read that first! ## ๐Ÿ”Œ API Design Principles ### Rule #1: API Endpoints Serve the UI Every API endpoint must exist to support a specific UI feature or user action. **The Process**: 1. Identify UI interaction (button click, form submit, page load) 2. Determine what data is needed 3. Create API endpoint that provides exactly that data 4. No more, no less ### API Architecture in Next.js ``` src/ app/ api/ # REST API Routes (when needed) auth/ route.ts work-orders/ route.ts [id]/ route.ts [feature]/ actions.ts # Server Actions (preferred for mutations) ``` ## ๐Ÿ”’ Type Safety in Server Actions ### NEVER Use `any` Type **ALWAYS validate inputs with Zod - no exceptions:** ```typescript "use server"; import { z } from "zod"; import type { ActionResult } from "@/lib/types"; // โŒ BAD - using any export async function createProject(data: any): Promise> { // FORBIDDEN! Never use 'any' } // โŒ BAD - unvalidated FormData export async function createProject( formData: FormData, ): Promise> { const name = formData.get("name"); // Type is FormDataEntryValue | null - not safe! // No validation! } // โœ… GOOD - strict Zod schema const createProjectSchema = z.object({ name: z.string().min(1, "Name is required").max(255), description: z.string().optional(), clientId: z.string().uuid("Invalid client ID"), startDate: z.string().datetime(), budget: z.number().positive("Budget must be positive"), }); type CreateProjectInput = z.infer; export async function createProject( input: unknown, // Unknown until validated ): Promise> { // Validate at runtime + get full type safety const result = createProjectSchema.safeParse(input); if (!result.success) { return { success: false, error: result.error.errors .map((e) => `${e.path.join(".")}: ${e.message}`) .join(", "), }; } // result.data is now fully typed as CreateProjectInput const validatedData = result.data; // ... rest of implementation with full type safety } ``` ### Avoid Type Casting with `as` **Use Zod validation instead of unsafe casting:** ```typescript // โŒ BAD - type casting (unsafe!) const project = data as Project; // No validation! const userId = (formData.get("userId") as string).toUpperCase(); // Can crash! // โœ… GOOD - Zod validation const projectSchema = z.object({ id: z.string().uuid(), data: z.object({ name: z.string(), status: z.enum(["active", "completed", "cancelled"]), }), createdAt: z.coerce.date(), // Converts + validates updatedAt: z.coerce.date(), }); const project = projectSchema.parse(data); // Type-safe + runtime validation โœ… const userIdSchema = z.string().min(1); const userId = userIdSchema.parse(formData.get("userId")); // Safe โœ… ``` ### FormData Validation Pattern **Always validate FormData with Zod:** ```typescript "use server"; export async function updateUser( formData: FormData, ): Promise> { // Convert FormData to object const rawData = Object.fromEntries(formData); // Define schema const schema = z.object({ userId: z.string().uuid(), name: z.string().min(1).max(255), email: z.string().email(), age: z.coerce.number().int().positive(), // Coerce string to number }); // Validate const result = schema.safeParse(rawData); if (!result.success) { return { success: false, error: result.error.message }; } // Now result.data is fully typed and validated const validatedData = result.data; // ... safe to use } ``` ## ๐ŸŽฏ ActionResult Pattern ### Use Shared ActionResult Type **ALWAYS use the shared ActionResult type with helper functions:** ```typescript import { type ActionResult, success, failure, toFailure } from "@/lib/types"; // โŒ BAD - inline result objects export async function createProject(input: unknown) { return { success: true as const, data: project }; return { success: false as const, error: "Failed" }; } // โœ… GOOD - use helpers export async function createProject( input: unknown, ): Promise> { try { // ... validation and logic return success({ id: result.id }); } catch (error) { return toFailure(error, "Failed to create project"); } } ``` ### Error Handling with toFailure **Use `toFailure()` for catch blocks - it handles unknown error types safely:** ```typescript // โŒ BAD - manual error handling catch (error) { return failure(error instanceof Error ? error.message : "Unknown error"); } // โœ… GOOD - use toFailure helper catch (error) { console.error("Action error:", error); return toFailure(error, "Failed to perform action"); } ``` ## ๐Ÿ”‘ Authentication Helper ### Use Shared getCurrentUser Helper **ALWAYS use the shared helper instead of duplicating auth logic:** ```typescript // โŒ BAD - duplicated auth logic in each action file async function getCurrentUser() { if (skipAuth) return mockUser; const session = await auth.api.getSession({ headers: await headers() }); return session?.user ?? null; } // โœ… GOOD - import shared helper import { getCurrentUser } from "@/lib/utils/get-current-user"; export async function myAction(input: unknown): Promise> { const user = await getCurrentUser(); if (!user) { return failure("Unauthorized. Please log in."); } // ... } ``` ## ๐Ÿ“‹ Zod Schema Patterns ### Use .partial() at Object Level **Prefer `.partial()` on object level instead of `.optional()` on each field:** ```typescript // โŒ BAD - verbose, repetitive const schema = z.object({ title: z.string().optional(), description: z.string().optional(), status: z.string().optional(), // ... 20 more fields with .optional() }); // โœ… GOOD - concise, maintainable const baseSchema = z.object({ title: z.string(), description: z.string(), status: z.string(), }); const updateSchema = baseSchema.partial(); // All fields optional const createSchema = baseSchema.partial().required({ title: true }); // title required ``` ### Shared Enums **Put reusable enum values in `@/lib/schemas/shared-enums.ts`:** ```typescript // โŒ BAD - duplicated in multiple files // In file A: const statusSchema = z.enum(["New", "In Progress", "Completed"]); // In file B: const STATUS_OPTIONS = ["New", "In Progress", "Completed"]; // โœ… GOOD - single source of truth // shared-enums.ts export const STATUS_VALUES = ["New", "In Progress", "Completed"] as const; export const statusSchema = z.enum(STATUS_VALUES); export type Status = z.infer; // Other files import from shared-enums import { STATUS_VALUES, statusSchema } from "@/lib/schemas/shared-enums"; ``` ## ๐Ÿ—ƒ๏ธ Drizzle Type Inference ### Use $inferSelect for Types **Use Drizzle's `$inferSelect` to avoid manual type definitions and casting:** ```typescript // โŒ BAD - manual type + casting type Project = { id: string; data: ProjectData; createdAt: Date; updatedAt: Date; }; const result = await db.select().from(projects); return result as Project[]; // Unsafe! // โœ… GOOD - inferred type from schema export type Project = typeof projects.$inferSelect; const result = await db.select().from(projects); return result; // Already typed correctly! ``` ## ๐Ÿ›ก๏ธ Security in Server Actions ### 1. Authentication Check (ALWAYS) **EVERY Server Action MUST check authentication:** ```typescript "use server"; import { getServerSession } from "@/lib/auth"; export async function deleteProject( projectId: string, ): Promise> { // โŒ FORBIDDEN - no auth check // await db.delete(projects).where(eq(projects.id, projectId)); // โœ… REQUIRED - check authentication first const session = await getServerSession(); if (!session?.user) { return { success: false, error: "Unauthorized - please log in" }; } // Now proceed with validated user // ... rest of implementation } ``` ### 2. Authorization Check (ALWAYS) **Check if user has permission to perform the action:** ```typescript "use server"; export async function updateProject( projectId: string, updates: unknown, ): Promise> { // 1. Validate input const idSchema = z.string().uuid(); const projectIdResult = idSchema.safeParse(projectId); if (!projectIdResult.success) { return { success: false, error: "Invalid project ID" }; } const updateSchema = z.object({ name: z.string().min(1).max(255).optional(), status: z.enum(["active", "completed", "cancelled"]).optional(), }); const updatesResult = updateSchema.safeParse(updates); if (!updatesResult.success) { return { success: false, error: updatesResult.error.message }; } // 2. Check authentication const session = await getServerSession(); if (!session?.user) { return { success: false, error: "Unauthorized" }; } // 3. Check authorization - user must own the project or be admin const db = await getDatabase(); const [project] = await db .select() .from(projects) .where(eq(projects.id, projectIdResult.data)); if (!project) { return { success: false, error: "Project not found" }; } // Check ownership or admin role const isOwner = project.data.ownerId === session.user.id; const isAdmin = session.user.role === "admin"; if (!isOwner && !isAdmin) { return { success: false, error: "Forbidden - you do not have permission" }; } // 4. Now safe to perform action const [updated] = await db .update(projects) .set({ data: { ...project.data, ...updatesResult.data }, updatedAt: new Date(), }) .where(eq(projects.id, projectIdResult.data)) .returning(); return { success: true, data: updated }; } ``` ### 3. Input Sanitization (ALWAYS) **Validate ALL inputs, even from trusted sources:** ```typescript "use server"; export async function searchProjects( query: unknown, filters: unknown, ): Promise> { // Validate search query const querySchema = z.string().min(1).max(255); const queryResult = querySchema.safeParse(query); if (!queryResult.success) { return { success: false, error: "Invalid search query" }; } // Validate filters const filtersSchema = z.object({ status: z.enum(["active", "completed", "cancelled"]).optional(), clientId: z.string().uuid().optional(), dateFrom: z.string().datetime().optional(), dateTo: z.string().datetime().optional(), }); const filtersResult = filtersSchema.safeParse(filters); if (!filtersResult.success) { return { success: false, error: "Invalid filters" }; } // Now both are fully typed and validated const safeQuery = queryResult.data; const safeFilters = filtersResult.data; // Safe to use in query (Drizzle prevents SQL injection) const db = await getDatabase(); let dbQuery = db.select().from(projects); if (safeFilters.status) { dbQuery = dbQuery.where(sql`data->>'status' = ${safeFilters.status}`); } const results = await dbQuery; return { success: true, data: results }; } ``` ### 4. Rate Limiting (Optional - Add When Needed) **For now: simple validation is enough. Add rate limiting later if abuse occurs.** ```typescript // โœ… NOW - simple validation prevents most abuse export async function sendEmail(input: unknown): Promise> { // Validate + authenticate const schema = z.object({ to: z.string().email() }); const result = schema.safeParse(input); if (!result.success) { return { success: false, error: "Invalid email" }; } const session = await getServerSession(); if (!session?.user) { return { success: false, error: "Unauthorized" }; } // ... send email } // ๐Ÿ”ฎ FUTURE - add rate limiting if users abuse the feature // import { ratelimit } from '@/lib/ratelimit'; // Will use Redis when scaling // const { success } = await ratelimit.limit(`email:${session.user.id}`, 5, '1h'); ``` ## ๐ŸŽฏ Server Actions (Preferred) ### When to Use Server Actions - โœ… Form submissions - โœ… Data mutations (create, update, delete) - โœ… Actions triggered by user interactions - โœ… When you need to revalidate data - โœ… **Data fetching from Client Components** (when you can't use Server Component) **Best Practice**: Prefer Server Components for data fetching. Only fetch from Client Components when: - You need client-side interactivity (filters, search, pagination) that triggers refetch - Data depends on client state - You're polling/refreshing data on interval ### Server Action Template ```typescript // app/projects/actions.ts "use server"; import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; import { db } from "@/lib/getDatabase"; import { projects } from "@/schema/schema"; import { getCurrentUser } from "@/lib/auth-helpers-server"; import { eq } from "drizzle-orm"; // Define return type for consistency type ActionResult = | { success: true; data: T } | { success: false; error: string }; /** * Creates a new project */ export async function createProject( formData: FormData, ): Promise> { try { // 1. Authentication check const user = await getCurrentUser(); if (!user) { return { success: false, error: "Not authenticated" }; } // 2. Validate input const title = formData.get("title") as string; const description = formData.get("description") as string; if (!title || title.trim().length === 0) { return { success: false, error: "Title is required" }; } // 3. Database operation const [newProject] = await db .insert(projects) .values({ title: title.trim(), description: description?.trim() || "", userId: user.id, status: "draft", }) .returning(); // 4. Revalidate affected paths revalidatePath("/projects"); revalidatePath("/"); // 5. Return success return { success: true, data: { id: newProject.id }, }; } catch (error) { console.error("Error creating project:", error); return { success: false, error: "Failed to create project", }; } } /** * Updates an existing project */ export async function updateProject( projectId: string, formData: FormData, ): Promise { try { const user = await getCurrentUser(); if (!user) { return { success: false, error: "Not authenticated" }; } // Check ownership const project = await db.query.projects.findFirst({ where: eq(projects.id, projectId), }); if (!project) { return { success: false, error: "Project not found" }; } if (project.userId !== user.id) { return { success: false, error: "Not authorized" }; } // Update const title = formData.get("title") as string; const description = formData.get("description") as string; await db .update(projects) .set({ title: title.trim(), description: description?.trim() || "", updatedAt: new Date(), }) .where(eq(projects.id, projectId)); revalidatePath("/projects"); revalidatePath(`/projects/${projectId}`); return { success: true, data: undefined }; } catch (error) { console.error("Error updating project:", error); return { success: false, error: "Failed to update project" }; } } /** * Deletes a project (soft delete) */ export async function deleteProject(projectId: string): Promise { try { const user = await getCurrentUser(); if (!user) { return { success: false, error: "Not authenticated" }; } // Check ownership const project = await db.query.projects.findFirst({ where: eq(projects.id, projectId), }); if (!project) { return { success: false, error: "Project not found" }; } if (project.userId !== user.id) { return { success: false, error: "Not authorized" }; } // Soft delete await db .update(projects) .set({ deletedAt: new Date() }) .where(eq(projects.id, projectId)); revalidatePath("/projects"); return { success: true, data: undefined }; } catch (error) { console.error("Error deleting project:", error); return { success: false, error: "Failed to delete project" }; } } ``` ### Using Server Actions in Components ```typescript 'use client' import { useTransition } from 'react' import { useRouter } from 'next/navigation' import { createProject } from './actions' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { useToast } from '@/hooks/use-toast' export function ProjectForm() { const router = useRouter() const { toast } = useToast() const [isPending, startTransition] = useTransition() const handleSubmit = async (formData: FormData) => { startTransition(async () => { const result = await createProject(formData) if (result.success) { toast({ title: 'Success', description: 'Project created successfully', }) router.push(`/projects/${result.data.id}`) } else { toast({ title: 'Error', description: result.error, variant: 'destructive', }) } }) } return (
) } ``` ## ๐ŸŒ REST API Routes (When Needed) ### When to Use API Routes - โœ… Webhooks from third-party services (external systems calling your app) - โœ… Public APIs that need to be consumed by non-Next.js clients **Note**: Even these cases are RARE in this project. Default to Server Actions. ### API Route Template ```typescript // app/api/work-orders/route.ts import { NextRequest, NextResponse } from "next/server"; import { db } from "@/lib/getDatabase"; import { workOrders } from "@/schema/schema"; import { getCurrentUser } from "@/lib/auth-helpers-server"; import { eq, and, desc } from "drizzle-orm"; /** * GET /api/work-orders * Fetch work orders with optional filters */ export async function GET(request: NextRequest) { try { // 1. Authentication const user = await getCurrentUser(); if (!user) { return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); } // 2. Parse query parameters const { searchParams } = new URL(request.url); const status = searchParams.get("status"); const assignedTo = searchParams.get("assignedTo"); // 3. Build query const conditions = []; if (status) { conditions.push(eq(workOrders.status, status)); } if (assignedTo) { conditions.push(eq(workOrders.assignedTo, assignedTo)); } // 4. Fetch data (RLS will filter based on user) const results = await db.query.workOrders.findMany({ where: conditions.length > 0 ? and(...conditions) : undefined, orderBy: [desc(workOrders.createdAt)], with: { user: { columns: { id: true, name: true, email: true, }, }, }, }); // 5. Return response return NextResponse.json({ success: true, data: results, }); } catch (error) { console.error("Error fetching work orders:", error); return NextResponse.json( { error: "Internal server error" }, { status: 500 }, ); } } /** * POST /api/work-orders * Create a new work order */ export async function POST(request: NextRequest) { try { const user = await getCurrentUser(); if (!user) { return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); } // Parse body const body = await request.json(); const { title, description, dueDate, assignedTo } = body; // Validate if (!title || title.trim().length === 0) { return NextResponse.json({ error: "Title is required" }, { status: 400 }); } // Create work order const [newWorkOrder] = await db .insert(workOrders) .values({ title: title.trim(), description: description?.trim() || "", dueDate: dueDate ? new Date(dueDate) : null, assignedTo: assignedTo || null, userId: user.id, status: "draft", }) .returning(); return NextResponse.json( { success: true, data: newWorkOrder, }, { status: 201 }, ); } catch (error) { console.error("Error creating work order:", error); return NextResponse.json( { error: "Internal server error" }, { status: 500 }, ); } } ``` ### Dynamic Route Template ```typescript // app/api/work-orders/[id]/route.ts import { NextRequest, NextResponse } from "next/server"; import { db } from "@/lib/getDatabase"; import { workOrders } from "@/schema/schema"; import { getCurrentUser } from "@/lib/auth-helpers-server"; import { eq } from "drizzle-orm"; type Params = { params: { id: string; }; }; /** * GET /api/work-orders/[id] */ export async function GET(request: NextRequest, { params }: Params) { try { const user = await getCurrentUser(); if (!user) { return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); } const workOrder = await db.query.workOrders.findFirst({ where: eq(workOrders.id, params.id), with: { user: true, }, }); if (!workOrder) { return NextResponse.json({ error: "Not found" }, { status: 404 }); } return NextResponse.json({ success: true, data: workOrder }); } catch (error) { console.error("Error fetching work order:", error); return NextResponse.json( { error: "Internal server error" }, { status: 500 }, ); } } /** * PUT /api/work-orders/[id] */ export async function PUT(request: NextRequest, { params }: Params) { try { const user = await getCurrentUser(); if (!user) { return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); } // Check existence and ownership const existing = await db.query.workOrders.findFirst({ where: eq(workOrders.id, params.id), }); if (!existing) { return NextResponse.json({ error: "Not found" }, { status: 404 }); } if (existing.userId !== user.id) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } // Update const body = await request.json(); const [updated] = await db .update(workOrders) .set({ ...body, updatedAt: new Date(), }) .where(eq(workOrders.id, params.id)) .returning(); return NextResponse.json({ success: true, data: updated }); } catch (error) { console.error("Error updating work order:", error); return NextResponse.json( { error: "Internal server error" }, { status: 500 }, ); } } /** * DELETE /api/work-orders/[id] */ export async function DELETE(request: NextRequest, { params }: Params) { try { const user = await getCurrentUser(); if (!user) { return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); } const existing = await db.query.workOrders.findFirst({ where: eq(workOrders.id, params.id), }); if (!existing) { return NextResponse.json({ error: "Not found" }, { status: 404 }); } if (existing.userId !== user.id) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } // Soft delete await db .update(workOrders) .set({ deletedAt: new Date() }) .where(eq(workOrders.id, params.id)); return NextResponse.json({ success: true }); } catch (error) { console.error("Error deleting work order:", error); return NextResponse.json( { error: "Internal server error" }, { status: 500 }, ); } } ``` ## ๐Ÿ“‹ API Response Standards ### Consistent Response Format ```typescript // Success Response { "success": true, "data": { /* result data */ } } // Error Response { "success": false, "error": "Human-readable error message" } // List Response (with pagination) { "success": true, "data": [ /* items */ ], "pagination": { "total": 100, "page": 1, "limit": 10, "pages": 10 } } ``` ### HTTP Status Codes | Code | Meaning | When to Use | | ---- | --------------------- | --------------------------------------------------- | | 200 | OK | Successful GET, PUT, DELETE | | 201 | Created | Successful POST (resource created) | | 204 | No Content | Successful DELETE with no response body | | 400 | Bad Request | Invalid input, validation error | | 401 | Unauthorized | Not authenticated | | 403 | Forbidden | Authenticated but not authorized | | 404 | Not Found | Resource doesn't exist | | 409 | Conflict | Resource conflict (duplicate, constraint violation) | | 500 | Internal Server Error | Unexpected server error | ## ๐Ÿ” Authentication & Authorization ### Authentication Check ```typescript import { getCurrentUser } from "@/lib/auth-helpers-server"; export async function GET(request: NextRequest) { const user = await getCurrentUser(); if (!user) { return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); } // Continue with authenticated request } ``` ### Authorization Check ```typescript // Check ownership const resource = await db.query.projects.findFirst({ where: eq(projects.id, projectId), }); if (!resource) { return NextResponse.json({ error: "Not found" }, { status: 404 }); } if (resource.userId !== user.id) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } // Check role const hasPermission = user.role === "admin" || user.role === "manager"; if (!hasPermission) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } ``` ## โœ… Input Validation ### Using Zod for Validation ```typescript import { z } from "zod"; const createWorkOrderSchema = z.object({ title: z.string().min(1, "Title is required").max(200), description: z.string().max(2000).optional(), status: z.enum(["draft", "pending", "approved", "in_progress", "completed"]), dueDate: z.string().datetime().optional(), assignedTo: z.string().uuid().optional(), }); export async function POST(request: NextRequest) { try { const body = await request.json(); // Validate const validationResult = createWorkOrderSchema.safeParse(body); if (!validationResult.success) { return NextResponse.json( { error: "Validation failed", details: validationResult.error.issues, }, { status: 400 }, ); } const data = validationResult.data; // Proceed with validated data // ... } catch (error) { // Handle error } } ``` ## ๐Ÿ”„ Data Revalidation ### When to Revalidate ```typescript import { revalidatePath, revalidateTag } from "next/cache"; // After creating/updating/deleting export async function createProject(formData: FormData) { // ... create project // Revalidate specific path revalidatePath("/projects"); revalidatePath("/"); // Or revalidate by tag (if using fetch with tags) revalidateTag("projects"); } ``` --- ## ๐Ÿ’ก Custom API Guidelines ### โš ๏ธ CRITICAL: NO REST API Routes **MANDATORY RULE**: - โŒ **DO NOT create REST API routes** (`app/api/*/route.ts`) - โœ… **USE Server Actions ONLY** for all data operations **ONLY Exception**: Webhooks from external services (e.g., payment providers) or integrations with external service providers **Why?** - Server Actions are more efficient - Better type safety - Automatic revalidation - No API route overhead ### Server Actions with Typed Schema **Working with Properly Typed Database Schema:** ```typescript "use server"; import { revalidatePath } from "next/cache"; import { db } from "@/lib/getDatabase"; import { workOrders } from "@/schema/schema"; import { getCurrentUser } from "@/lib/auth-helpers-server"; import { z } from "zod"; import { eq } from "drizzle-orm"; // Define input schemas with Zod const createWorkOrderSchema = z.object({ title: z.string().min(1, "Title is required").max(200), description: z.string().max(2000).optional(), status: z .enum(["draft", "pending", "approved", "completed"]) .default("draft"), assignedTo: z.string().uuid().nullable().optional(), dueDate: z.coerce.date().nullable().optional(), budget: z.number().positive().nullable().optional(), priority: z.number().int().min(0).max(5).default(0), }); const updateWorkOrderSchema = createWorkOrderSchema.partial(); type ActionResult = | { success: true; data: T } | { success: false; error: string }; /** * Create work order - stores data in typed columns */ export async function createWorkOrder( formData: FormData, ): Promise> { try { const user = await getCurrentUser(); if (!user) { return { success: false, error: "Not authenticated" }; } // Parse and validate data const rawData = { title: formData.get("title"), description: formData.get("description") || undefined, status: formData.get("status") || "draft", assignedTo: formData.get("assignedTo") || null, dueDate: formData.get("dueDate") || null, budget: formData.get("budget") ? Number(formData.get("budget")) : null, priority: formData.get("priority") ? Number(formData.get("priority")) : 0, }; const validationResult = createWorkOrderSchema.safeParse(rawData); if (!validationResult.success) { return { success: false, error: validationResult.error.issues[0].message, }; } // Insert with typed columns const [newWorkOrder] = await db .insert(workOrders) .values({ ...validationResult.data, createdBy: user.id, }) .returning(); revalidatePath("/work-orders"); return { success: true, data: { id: newWorkOrder.id }, }; } catch (error) { console.error("Error creating work order:", error); return { success: false, error: "Failed to create work order" }; } } /** * Update work order - update specific columns */ export async function updateWorkOrder( id: string, formData: FormData, ): Promise { try { const user = await getCurrentUser(); if (!user) { return { success: false, error: "Not authenticated" }; } // Get existing work order const [existing] = await db .select() .from(workOrders) .where(eq(workOrders.id, id)); if (!existing) { return { success: false, error: "Not found" }; } // Parse updates const updates: Record = {}; const title = formData.get("title"); const status = formData.get("status"); const description = formData.get("description"); if (title) updates.title = title; if (status) updates.status = status; if (description !== null) updates.description = description; // Validate updates const validationResult = updateWorkOrderSchema.safeParse(updates); if (!validationResult.success) { return { success: false, error: "Invalid data" }; } // Update typed columns await db .update(workOrders) .set({ ...validationResult.data, updatedAt: new Date(), }) .where(eq(workOrders.id, id)); revalidatePath("/work-orders"); revalidatePath(`/work-orders/${id}`); return { success: true, data: undefined }; } catch (error) { console.error("Error updating work order:", error); return { success: false, error: "Failed to update" }; } } /** * Fetch work orders - query typed columns */ export async function getWorkOrders() { const user = await getCurrentUser(); if (!user) { throw new Error("Not authenticated"); } return await db .select() .from(workOrders) .where(eq(workOrders.deletedAt, null)) .orderBy(workOrders.createdAt); } /** * Filter by status - simple column query */ export async function getWorkOrdersByStatus(status: string) { const user = await getCurrentUser(); if (!user) { throw new Error("Not authenticated"); } return await db .select() .from(workOrders) .where(eq(workOrders.status, status)) .orderBy(workOrders.createdAt); } ``` ### Rate Limiting ### Webhooks ### Third-Party Integrations ### Error Logging --- **Last Updated**: 2026-02-13 **Version**: 1.1.0