fix: retrofit AssetItem.model to modelId FK
AssetItem.model was a free-text string with no referential integrity. Now a required FK to AssetModel, mirroring the Request.employeeId and License.vendorId retrofit precedent (no backfill needed, no real AssetItem data existed).
This commit is contained in:
@@ -22,11 +22,11 @@ export async function POST(request: Request): Promise<NextResponse> {
|
||||
return NextResponse.json({ error: "Request body must be a JSON object" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { serialNumber, model, purchaseDate, warrantyEndDate, purchaseOrderId } =
|
||||
const { serialNumber, modelId, purchaseDate, warrantyEndDate, purchaseOrderId } =
|
||||
body as Record<string, unknown>;
|
||||
const validation = validateCreateAssetItemInput({
|
||||
serialNumber,
|
||||
model,
|
||||
modelId,
|
||||
purchaseDate,
|
||||
warrantyEndDate,
|
||||
purchaseOrderId,
|
||||
|
||||
+35
-19
@@ -7,7 +7,7 @@ export type AssetItemStatus = "in_stock" | "assigned" | "repair" | "retired";
|
||||
export interface AssetItemRecord {
|
||||
id: string;
|
||||
serial_number: string;
|
||||
model: string;
|
||||
model_id: string;
|
||||
purchase_date: string;
|
||||
warranty_end_date: string | null;
|
||||
status: AssetItemStatus;
|
||||
@@ -17,7 +17,7 @@ export interface AssetItemRecord {
|
||||
|
||||
export interface CreateAssetItemInput {
|
||||
serialNumber: string;
|
||||
model: string;
|
||||
modelId: string;
|
||||
purchaseDate: Date;
|
||||
warrantyEndDate?: Date;
|
||||
purchaseOrderId?: string | null;
|
||||
@@ -47,7 +47,15 @@ function dateString(message: string) {
|
||||
|
||||
const createAssetItemSchema = z.object({
|
||||
serialNumber: nonEmptyString("serialNumber is required and must be a non-empty string"),
|
||||
model: nonEmptyString("model is required and must be a non-empty string"),
|
||||
// Required FK to AssetModel — replaced the free-text `model` String (see
|
||||
// schema.prisma / docs/asset-license-management.md's "Cập nhật 2026-08-02
|
||||
// (2)" section). Only a UUID *format* check here, whether it references a
|
||||
// real AssetModel is left to Prisma's own FK constraint at insert time
|
||||
// (see createAssetItem()'s P2003 handling below). Mirrors
|
||||
// src/lib/licenses.ts's vendorId handling.
|
||||
modelId: z
|
||||
.string({ required_error: "modelId is required and must be a valid UUID string" })
|
||||
.uuid("modelId is required and must be a valid UUID string"),
|
||||
purchaseDate: dateString("purchaseDate is required and must be a valid date string"),
|
||||
warrantyEndDate: dateString("warrantyEndDate must be a valid date string").optional(),
|
||||
// FK to PurchaseOrder — nullable/optional, supply-side traceability (not
|
||||
@@ -64,17 +72,17 @@ const createAssetItemSchema = z.object({
|
||||
});
|
||||
|
||||
/**
|
||||
* Validates raw (untyped) asset-item-creation input. serialNumber/model
|
||||
* must be non-empty strings, purchaseDate must be a valid date string,
|
||||
* warrantyEndDate is optional but must be a valid date string if present.
|
||||
* purchaseOrderId is optional/nullable but must be a valid UUID if present.
|
||||
* status is not accepted here — new assets always start "in_stock" (same
|
||||
* pattern as Request always starting "Draft"), later states are set via
|
||||
* other operations, not at creation.
|
||||
* Validates raw (untyped) asset-item-creation input. serialNumber must be a
|
||||
* non-empty string, modelId must be a valid UUID string, purchaseDate must
|
||||
* be a valid date string, warrantyEndDate is optional but must be a valid
|
||||
* date string if present. purchaseOrderId is optional/nullable but must be
|
||||
* a valid UUID if present. status is not accepted here — new assets always
|
||||
* start "in_stock" (same pattern as Request always starting "Draft"), later
|
||||
* states are set via other operations, not at creation.
|
||||
*/
|
||||
export function validateCreateAssetItemInput(input: {
|
||||
serialNumber?: unknown;
|
||||
model?: unknown;
|
||||
modelId?: unknown;
|
||||
purchaseDate?: unknown;
|
||||
warrantyEndDate?: unknown;
|
||||
purchaseOrderId?: unknown;
|
||||
@@ -90,7 +98,7 @@ export function validateCreateAssetItemInput(input: {
|
||||
function toRecord(row: {
|
||||
id: string;
|
||||
serialNumber: string;
|
||||
model: string;
|
||||
modelId: string;
|
||||
purchaseDate: Date;
|
||||
warrantyEndDate: Date | null;
|
||||
status: AssetItemStatus;
|
||||
@@ -100,7 +108,7 @@ function toRecord(row: {
|
||||
return {
|
||||
id: row.id,
|
||||
serial_number: row.serialNumber,
|
||||
model: row.model,
|
||||
model_id: row.modelId,
|
||||
purchase_date: row.purchaseDate.toISOString(),
|
||||
warranty_end_date: row.warrantyEndDate ? row.warrantyEndDate.toISOString() : null,
|
||||
status: row.status,
|
||||
@@ -119,10 +127,14 @@ export type CreateAssetItemResult =
|
||||
* caught here and translated into the same `{ok:false,error}` shape as
|
||||
* input validation failures, rather than letting a raw Prisma error leak
|
||||
* to the API layer. Mirrors createEmployee()/createRequest()'s P2003
|
||||
* handling for FK violations. If purchaseOrderId doesn't reference an
|
||||
* existing PurchaseOrder, Prisma raises P2003 (FK constraint violation) —
|
||||
* caught the same way, mirrors src/lib/purchase-orders.ts's
|
||||
* createPurchaseOrder() P2003 handling.
|
||||
* handling for FK violations. Two separate FKs can fail here (modelId ->
|
||||
* AssetModel, purchaseOrderId -> PurchaseOrder) — Prisma raises the same
|
||||
* error code (P2003) for either, so the FK constraint name in
|
||||
* `error.meta.field_name` is used to tell them apart and return a targeted
|
||||
* message, rather than a raw Prisma error leaking to the API layer. Mirrors
|
||||
* src/lib/licenses.ts's createLicense() / src/lib/purchase-orders.ts's
|
||||
* createPurchaseOrder() pattern for disambiguating between two FKs on the
|
||||
* same model.
|
||||
*/
|
||||
export async function createAssetItem(
|
||||
input: CreateAssetItemInput
|
||||
@@ -131,7 +143,7 @@ export async function createAssetItem(
|
||||
const created = await prisma.assetItem.create({
|
||||
data: {
|
||||
serialNumber: input.serialNumber,
|
||||
model: input.model,
|
||||
modelId: input.modelId,
|
||||
purchaseDate: input.purchaseDate,
|
||||
warrantyEndDate: input.warrantyEndDate ?? null,
|
||||
purchaseOrderId: input.purchaseOrderId ?? null,
|
||||
@@ -143,7 +155,11 @@ export async function createAssetItem(
|
||||
return { ok: false, error: "serialNumber already exists" };
|
||||
}
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2003") {
|
||||
return { ok: false, error: "purchaseOrderId does not reference an existing purchase order" };
|
||||
const fieldName = typeof error.meta?.field_name === "string" ? error.meta.field_name : "";
|
||||
if (fieldName.includes("purchase_order")) {
|
||||
return { ok: false, error: "purchaseOrderId does not reference an existing purchase order" };
|
||||
}
|
||||
return { ok: false, error: "modelId does not reference an existing asset model" };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user