From eeaf9467127d52c6c281b01ca3435c37f48a11f6 Mon Sep 17 00:00:00 2001 From: Toan Hoang Minh Date: Fri, 31 Jul 2026 14:54:48 +0700 Subject: [PATCH] feat: add Employee validation + CRUD (src/lib/employees.ts) Mirrors src/lib/requests.ts's shape: Zod schema, ValidationResult discriminated union, CRUD via Prisma, toRecord() camelCase->snake_case mapping. managerId is only format-checked as a UUID here; a nonexistent managerId is caught via Prisma's P2003 FK-violation error in createEmployee() and translated into a friendly {ok:false,error} instead of leaking a raw Prisma error. --- src/lib/employees.ts | 116 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 src/lib/employees.ts diff --git a/src/lib/employees.ts b/src/lib/employees.ts new file mode 100644 index 0000000..71ae65a --- /dev/null +++ b/src/lib/employees.ts @@ -0,0 +1,116 @@ +import { z } from "zod"; +import { Prisma } from "@prisma/client"; +import { prisma } from "./prisma"; + +export interface EmployeeRecord { + id: string; + name: string; + department: string; + manager_id: string | null; + created_at: string; +} + +export interface CreateEmployeeInput { + name: string; + department: string; + managerId?: string; +} + +export type ValidationResult = + | { ok: true; value: CreateEmployeeInput } + | { ok: false; error: string }; + +// A string field that must be present and not just whitespace. Mirrors the +// same helper in src/lib/requests.ts: the value itself is NOT +// trimmed/transformed, only validated — the raw input is still what gets +// stored. +function nonEmptyString(message: string) { + return z.string({ required_error: message, invalid_type_error: message }).refine( + (value) => value.trim() !== "", + { message } + ); +} + +const createEmployeeSchema = z.object({ + name: nonEmptyString("name is required and must be a non-empty string"), + department: nonEmptyString("department is required and must be a non-empty string"), + // Only a UUID *format* check here — whether it references a real employee + // is left to Prisma's own FK constraint at insert time (see + // createEmployee()'s P2003 handling below), rather than an extra + // pre-check query. + managerId: z + .string({ invalid_type_error: "managerId must be a valid UUID string" }) + .uuid("managerId must be a valid UUID string") + .optional(), +}); + +/** + * Validates raw (untyped) employee-creation input. + * name and department must be non-empty strings; managerId, if provided, + * must be a valid UUID string (existence of the referenced employee is + * enforced by the DB FK constraint, not here). + */ +export function validateCreateEmployeeInput(input: { + name?: unknown; + department?: unknown; + managerId?: unknown; +}): ValidationResult { + const result = createEmployeeSchema.safeParse(input); + if (!result.success) { + const message = result.error.issues[0]?.message ?? "Invalid input"; + return { ok: false, error: message }; + } + return { ok: true, value: result.data }; +} + +function toRecord(row: { + id: string; + name: string; + department: string; + managerId: string | null; + createdAt: Date; +}): EmployeeRecord { + return { + id: row.id, + name: row.name, + department: row.department, + manager_id: row.managerId, + created_at: row.createdAt.toISOString(), + }; +} + +export type CreateEmployeeResult = + | { ok: true; value: EmployeeRecord } + | { ok: false; error: string }; + +/** + * Creates an Employee. If managerId is provided but doesn't reference an + * existing employee, Prisma raises a known error (code P2003, FK + * violation) — caught here and translated into the same + * `{ok:false,error}` shape as input validation failures, rather than + * letting a raw Prisma error leak to the API layer. + */ +export async function createEmployee(input: CreateEmployeeInput): Promise { + try { + const created = await prisma.employee.create({ + data: { + name: input.name, + department: input.department, + managerId: input.managerId ?? null, + }, + }); + return { ok: true, value: toRecord(created) }; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2003") { + return { ok: false, error: "managerId does not reference an existing employee" }; + } + throw error; + } +} + +export async function listEmployees(): Promise { + const rows = await prisma.employee.findMany({ + orderBy: [{ createdAt: "desc" }, { id: "desc" }], + }); + return rows.map(toRecord); +}