import { z } from "zod"; import { Prisma } from "@prisma/client"; import { prisma } from "./prisma"; export interface AssetModelRecord { id: string; category_id: string; manufacturer: string; name: string; specs: Record; created_at: string; } export interface CreateAssetModelInput { categoryId: string; manufacturer: string; name: string; specs: Record; } export type ValidationResult = | { ok: true; value: CreateAssetModelInput } | { ok: false; error: string }; // Mirrors src/lib/asset-category-attribute-defs.ts's nonEmptyString helper. function nonEmptyString(message: string) { return z.string({ required_error: message, invalid_type_error: message }).refine( (value) => value.trim() !== "", { message } ); } const createAssetModelSchema = z.object({ // FK to AssetCategory — only a UUID *format* check here, whether it // references a real category (and whether that category is // valueGroup=high_value) is left to createAssetModel()'s explicit lookup // below — that's a business rule, not something pure Zod schema // validation on this input alone can express. Mirrors // src/lib/asset-category-attribute-defs.ts's categoryId handling. categoryId: z .string({ required_error: "categoryId is required and must be a valid UUID string" }) .uuid("categoryId is required and must be a valid UUID string"), manufacturer: nonEmptyString("manufacturer is required and must be a non-empty string"), name: nonEmptyString("name is required and must be a non-empty string"), // Deliberately LOOSE for this task — just "a plain JSON object" (no // arrays, no primitives at the top level). Does NOT validate keys/types // dynamically against this model's category's AssetCategoryAttributeDef // rows — that's a separate follow-up task per docs/ // asset-license-management.md's "Cập nhật 2026-08-02 (3)" section, out of // scope here. Defaults to {} when omitted, matching the DB column default. specs: z .record(z.unknown(), { invalid_type_error: "specs must be a JSON object", }) .optional() .default({}), }); /** * Validates raw (untyped) asset-model-creation input. categoryId must be a * valid UUID string, manufacturer/name must be non-empty strings, specs * must be a plain JSON object (or omitted, defaulting to {}). Whether * categoryId references a real, high_value AssetCategory is checked in * createAssetModel() below, not here — that requires a DB lookup. */ export function validateCreateAssetModelInput(input: { categoryId?: unknown; manufacturer?: unknown; name?: unknown; specs?: unknown; }): ValidationResult { const result = createAssetModelSchema.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; categoryId: string; manufacturer: string; name: string; specs: Prisma.JsonValue; createdAt: Date; }): AssetModelRecord { return { id: row.id, category_id: row.categoryId, manufacturer: row.manufacturer, name: row.name, specs: (row.specs as Record) ?? {}, created_at: row.createdAt.toISOString(), }; } export type CreateAssetModelResult = | { ok: true; value: AssetModelRecord } | { ok: false; error: string }; /** * Creates an AssetModel. categoryId must reference a real AssetCategory * (checked here via an explicit prisma.assetCategory.findUnique() lookup — * not left to Prisma's P2003 FK-violation catch like other entities' * simple FK checks, e.g. src/lib/asset-category-attribute-defs.ts's * categoryId handling), because there's an additional business rule to * enforce beyond "does this row exist": the referenced category's * valueGroup must be `high_value` (per docs/asset-license-management.md's * "Ràng buộc dữ liệu" section under "Cập nhật 2026-08-02 (2)") — a rule * that can't be expressed as a Postgres FK constraint, so it can't be * caught via a Prisma error code the way a plain missing-FK case can. If * the category doesn't exist at all, or exists but is `low_value`, this * returns a distinct {ok:false,error} for each case rather than a raw * P2003/500. */ export async function createAssetModel( input: CreateAssetModelInput ): Promise { const category = await prisma.assetCategory.findUnique({ where: { id: input.categoryId }, }); if (!category) { return { ok: false, error: "categoryId does not reference an existing asset category" }; } if (category.valueGroup !== "high_value") { return { ok: false, error: "categoryId must reference a category with valueGroup=high_value" }; } const created = await prisma.assetModel.create({ data: { categoryId: input.categoryId, manufacturer: input.manufacturer, name: input.name, // input.specs is validated by Zod to be a plain JSON object // (Record), but Prisma's generated InputJsonValue // type doesn't structurally accept an index-signature Record — cast // is safe here since the Zod schema already guarantees JSON-shaped // values. specs: input.specs as Prisma.InputJsonValue, }, }); return { ok: true, value: toRecord(created) }; } export async function listAssetModels(): Promise { const rows = await prisma.assetModel.findMany({ orderBy: [{ createdAt: "desc" }, { id: "desc" }], }); return rows.map(toRecord); }