Zod schema + validate function, Prisma-backed CRUD following the
{ok,value}|{ok,error} pattern from the start. createAssetCategoryAttributeDef()
catches P2002 for the composite (categoryId, key) unique constraint (mirrors
createAssetItem()'s serialNumber handling) and P2003 for the categoryId FK
(mirrors createPurchaseOrder()).
170 lines
6.1 KiB
TypeScript
170 lines
6.1 KiB
TypeScript
import { z } from "zod";
|
|
import { Prisma } from "@prisma/client";
|
|
import { prisma } from "./prisma";
|
|
|
|
export type AssetAttributeDataType = "string" | "number" | "boolean" | "date";
|
|
|
|
export interface AssetCategoryAttributeDefRecord {
|
|
id: string;
|
|
category_id: string;
|
|
key: string;
|
|
label: string;
|
|
data_type: AssetAttributeDataType;
|
|
required: boolean;
|
|
sort_order: number;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface CreateAssetCategoryAttributeDefInput {
|
|
categoryId: string;
|
|
key: string;
|
|
label: string;
|
|
dataType: AssetAttributeDataType;
|
|
required: boolean;
|
|
sortOrder: number;
|
|
}
|
|
|
|
export type ValidationResult =
|
|
| { ok: true; value: CreateAssetCategoryAttributeDefInput }
|
|
| { ok: false; error: string };
|
|
|
|
// Mirrors src/lib/asset-categories.ts's nonEmptyString helper.
|
|
function nonEmptyString(message: string) {
|
|
return z.string({ required_error: message, invalid_type_error: message }).refine(
|
|
(value) => value.trim() !== "",
|
|
{ message }
|
|
);
|
|
}
|
|
|
|
const createAssetCategoryAttributeDefSchema = z.object({
|
|
// FK to AssetCategory — only a UUID *format* check here, whether it
|
|
// references a real category is left to Prisma's own FK constraint at
|
|
// insert time (see createAssetCategoryAttributeDef()'s P2003 handling
|
|
// below). Mirrors src/lib/purchase-orders.ts's vendorId 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"),
|
|
// Slug-style, e.g. "ram_gb" — only unique *within* a category (composite
|
|
// unique (categoryId, key) at the DB layer), not globally. Just requires
|
|
// non-empty here; the DB-level uniqueness is enforced by
|
|
// createAssetCategoryAttributeDef()'s P2002 handling below.
|
|
key: nonEmptyString("key is required and must be a non-empty string"),
|
|
label: nonEmptyString("label is required and must be a non-empty string"),
|
|
// Fixed 4-value set (see docs/asset-license-management.md's "Cập nhật
|
|
// 2026-08-02 (3)" section) — a DEV-defined closed technical set, mirrors
|
|
// src/lib/asset-categories.ts's valueGroup z.enum() pattern.
|
|
dataType: z.enum(["string", "number", "boolean", "date"], {
|
|
errorMap: () => ({
|
|
message: "dataType is required and must be one of 'string', 'number', 'boolean', 'date'",
|
|
}),
|
|
}),
|
|
required: z.boolean({
|
|
required_error: "required is required and must be a boolean",
|
|
invalid_type_error: "required is required and must be a boolean",
|
|
}),
|
|
sortOrder: z.number({
|
|
required_error: "sortOrder is required and must be an integer",
|
|
invalid_type_error: "sortOrder is required and must be an integer",
|
|
})
|
|
.int("sortOrder is required and must be an integer"),
|
|
});
|
|
|
|
/**
|
|
* Validates raw (untyped) asset-category-attribute-def-creation input.
|
|
* categoryId must be a valid UUID string, key/label must be non-empty
|
|
* strings, dataType must be one of the four fixed enum values, required
|
|
* must be a boolean, sortOrder must be an integer. Whether categoryId
|
|
* references a real AssetCategory, and whether (categoryId, key) is
|
|
* actually unique, are both left to Prisma's constraints at insert time
|
|
* (see createAssetCategoryAttributeDef() below).
|
|
*/
|
|
export function validateCreateAssetCategoryAttributeDefInput(input: {
|
|
categoryId?: unknown;
|
|
key?: unknown;
|
|
label?: unknown;
|
|
dataType?: unknown;
|
|
required?: unknown;
|
|
sortOrder?: unknown;
|
|
}): ValidationResult {
|
|
const result = createAssetCategoryAttributeDefSchema.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;
|
|
key: string;
|
|
label: string;
|
|
dataType: AssetAttributeDataType;
|
|
required: boolean;
|
|
sortOrder: number;
|
|
createdAt: Date;
|
|
}): AssetCategoryAttributeDefRecord {
|
|
return {
|
|
id: row.id,
|
|
category_id: row.categoryId,
|
|
key: row.key,
|
|
label: row.label,
|
|
data_type: row.dataType,
|
|
required: row.required,
|
|
sort_order: row.sortOrder,
|
|
created_at: row.createdAt.toISOString(),
|
|
};
|
|
}
|
|
|
|
export type CreateAssetCategoryAttributeDefResult =
|
|
| { ok: true; value: AssetCategoryAttributeDefRecord }
|
|
| { ok: false; error: string };
|
|
|
|
/**
|
|
* Creates an AssetCategoryAttributeDef. If (categoryId, key) collides with
|
|
* an existing row, Prisma raises P2002 (composite unique constraint
|
|
* violation) — caught here and translated into a clean {ok:false,error},
|
|
* same pattern as createAssetItem()'s serialNumber P2002 handling in
|
|
* src/lib/assets.ts. Note this is a *composite* uniqueness (unique within a
|
|
* category, not globally) — the same `key` under a *different* categoryId
|
|
* is expected to succeed, per docs/asset-license-management.md's "Cập nhật
|
|
* 2026-08-02 (3)" section. If categoryId doesn't reference an existing
|
|
* AssetCategory, Prisma raises P2003 (FK constraint violation) — caught the
|
|
* same way, mirrors src/lib/purchase-orders.ts's createPurchaseOrder() P2003
|
|
* handling.
|
|
*/
|
|
export async function createAssetCategoryAttributeDef(
|
|
input: CreateAssetCategoryAttributeDefInput
|
|
): Promise<CreateAssetCategoryAttributeDefResult> {
|
|
try {
|
|
const created = await prisma.assetCategoryAttributeDef.create({
|
|
data: {
|
|
categoryId: input.categoryId,
|
|
key: input.key,
|
|
label: input.label,
|
|
dataType: input.dataType,
|
|
required: input.required,
|
|
sortOrder: input.sortOrder,
|
|
},
|
|
});
|
|
return { ok: true, value: toRecord(created) };
|
|
} catch (error) {
|
|
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
|
|
return { ok: false, error: "key already exists for this category" };
|
|
}
|
|
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2003") {
|
|
return { ok: false, error: "categoryId does not reference an existing asset category" };
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function listAssetCategoryAttributeDefs(): Promise<
|
|
AssetCategoryAttributeDefRecord[]
|
|
> {
|
|
const rows = await prisma.assetCategoryAttributeDef.findMany({
|
|
orderBy: [{ createdAt: "desc" }, { id: "desc" }],
|
|
});
|
|
return rows.map(toRecord);
|
|
}
|