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:
@@ -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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user