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.
This commit is contained in:
2026-07-31 14:54:48 +07:00
parent 4c69bcf2c1
commit eeaf946712
+116
View File
@@ -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<CreateEmployeeResult> {
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<EmployeeRecord[]> {
const rows = await prisma.employee.findMany({
orderBy: [{ createdAt: "desc" }, { id: "desc" }],
});
return rows.map(toRecord);
}