feat: add Area validation + CRUD (src/lib/areas.ts)
Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
3078d9e8c5
commit
1e6dec9cf2
@@ -0,0 +1,92 @@
|
||||
// Area — physical location tag for the office-move inventory walk (see
|
||||
// prisma/schema.prisma's Area model comment). Same shape/conventions as
|
||||
// src/lib/employees.ts: validate -> create (P2002 on duplicate name) ->
|
||||
// list.
|
||||
import { z } from "zod";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { prisma } from "./prisma";
|
||||
|
||||
export interface AreaRecord {
|
||||
id: string;
|
||||
name: string;
|
||||
team: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface CreateAreaInput {
|
||||
name: string;
|
||||
team?: string;
|
||||
}
|
||||
|
||||
export type ValidationResult =
|
||||
| { ok: true; value: CreateAreaInput }
|
||||
| { ok: false; error: string };
|
||||
|
||||
function nonEmptyString(message: string) {
|
||||
return z.string({ required_error: message, invalid_type_error: message }).refine(
|
||||
(value) => value.trim() !== "",
|
||||
{ message }
|
||||
);
|
||||
}
|
||||
|
||||
const createAreaSchema = z.object({
|
||||
name: nonEmptyString("name is required and must be a non-empty string"),
|
||||
team: z.string({ invalid_type_error: "team must be a string" }).optional(),
|
||||
});
|
||||
|
||||
export function validateCreateAreaInput(input: {
|
||||
name?: unknown;
|
||||
team?: unknown;
|
||||
}): ValidationResult {
|
||||
const result = createAreaSchema.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;
|
||||
team: string | null;
|
||||
createdAt: Date;
|
||||
}): AreaRecord {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
team: row.team,
|
||||
created_at: row.createdAt.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
export type CreateAreaResult = { ok: true; value: AreaRecord } | { ok: false; error: string };
|
||||
|
||||
/**
|
||||
* Creates an Area, or returns the existing one if `name` already exists
|
||||
* (idempotent-by-name) — this is deliberate, not just P2002 error handling:
|
||||
* the bot's "reuse existing or type a new one" flow calls this every time a
|
||||
* user picks/types an Area name, and re-submitting an Area the bot already
|
||||
* showed as an existing option must succeed, not 409.
|
||||
*/
|
||||
export async function createOrGetArea(input: CreateAreaInput): Promise<CreateAreaResult> {
|
||||
try {
|
||||
const created = await prisma.area.create({
|
||||
data: { name: input.name, team: input.team ?? null },
|
||||
});
|
||||
return { ok: true, value: toRecord(created) };
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
|
||||
const existing = await prisma.area.findUnique({ where: { name: input.name } });
|
||||
if (existing) {
|
||||
return { ok: true, value: toRecord(existing) };
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function listAreas(): Promise<AreaRecord[]> {
|
||||
const rows = await prisma.area.findMany({ orderBy: [{ name: "asc" }] });
|
||||
return rows.map(toRecord);
|
||||
}
|
||||
Reference in New Issue
Block a user