feat: add AssetModel validation + CRUD (src/lib/asset-models.ts)
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
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<string, unknown>;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface CreateAssetModelInput {
|
||||
categoryId: string;
|
||||
manufacturer: string;
|
||||
name: string;
|
||||
specs: Record<string, unknown>;
|
||||
}
|
||||
|
||||
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<string, unknown>) ?? {},
|
||||
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<CreateAssetModelResult> {
|
||||
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<string, unknown>), 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<AssetModelRecord[]> {
|
||||
const rows = await prisma.assetModel.findMany({
|
||||
orderBy: [{ createdAt: "desc" }, { id: "desc" }],
|
||||
});
|
||||
return rows.map(toRecord);
|
||||
}
|
||||
Reference in New Issue
Block a user