feat: add AssetCategory validation + CRUD (src/lib/asset-categories.ts)

This commit is contained in:
toanhoang
2026-08-02 20:31:22 -04:00
parent 69fa00012b
commit 20ec7a11df
+107
View File
@@ -0,0 +1,107 @@
import { z } from "zod";
import { prisma } from "./prisma";
export type AssetCategoryValueGroup = "high_value" | "low_value";
export interface AssetCategoryRecord {
id: string;
name: string;
value_group: AssetCategoryValueGroup;
created_at: string;
}
export interface CreateAssetCategoryInput {
name: string;
valueGroup: AssetCategoryValueGroup;
}
export type ValidationResult =
| { ok: true; value: CreateAssetCategoryInput }
| { ok: false; error: string };
// Mirrors src/lib/vendors.ts's nonEmptyString helper.
function nonEmptyString(message: string) {
return z.string({ required_error: message, invalid_type_error: message }).refine(
(value) => value.trim() !== "",
{ message }
);
}
const createAssetCategorySchema = z.object({
name: nonEmptyString("name is required and must be a non-empty string"),
// Fixed 2-value set (see docs/asset-license-management.md's "## Phân
// loại tài sản" section) — mirrors src/lib/licenses.ts's licenseType
// z.enum() pattern.
valueGroup: z.enum(["high_value", "low_value"], {
errorMap: () => ({
message: "valueGroup is required and must be either 'high_value' or 'low_value'",
}),
}),
});
/**
* Validates raw (untyped) asset-category-creation input. name must be a
* non-empty string, valueGroup must be one of the two fixed enum values
* (per the "## Phân loại tài sản" spec in docs/asset-license-management.md).
*/
export function validateCreateAssetCategoryInput(input: {
name?: unknown;
valueGroup?: unknown;
}): ValidationResult {
const result = createAssetCategorySchema.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;
valueGroup: AssetCategoryValueGroup;
createdAt: Date;
}): AssetCategoryRecord {
return {
id: row.id,
name: row.name,
value_group: row.valueGroup,
created_at: row.createdAt.toISOString(),
};
}
export type CreateAssetCategoryResult =
| { ok: true; value: AssetCategoryRecord }
| { ok: false; error: string };
/**
* Creates an AssetCategory. No unique constraint on any field, no FKs
* either (see docs/asset-license-management.md's "## Phân loại tài sản"
* spec — standalone entity, same starting shape as Vendor before it grew
* relations) — so there's no known Prisma error code to catch/translate
* here today. Still returns the `{ok,value}|{ok,error}` shape (rather than
* a bare Promise<AssetCategoryRecord> like createVendor()) to match the
* convention used across the newer entities (AssetItem/License) — see the
* code-reviewer finding on createLicense() in CLAUDE.md's "Trạng thái hiện
* tại": adding this shape up front avoids a silent 500 if a unique
* constraint is added later (e.g. on `name`) without remembering to also
* add P2002 handling here.
*/
export async function createAssetCategory(
input: CreateAssetCategoryInput
): Promise<CreateAssetCategoryResult> {
const created = await prisma.assetCategory.create({
data: {
name: input.name,
valueGroup: input.valueGroup,
},
});
return { ok: true, value: toRecord(created) };
}
export async function listAssetCategories(): Promise<AssetCategoryRecord[]> {
const rows = await prisma.assetCategory.findMany({
orderBy: [{ createdAt: "desc" }, { id: "desc" }],
});
return rows.map(toRecord);
}