From c02235f381d6ef3c950f10d594ec0d495c854393 Mon Sep 17 00:00:00 2001 From: toanhoang Date: Sun, 2 Aug 2026 20:46:48 -0400 Subject: [PATCH] feat: add AssetModel Prisma model + migration --- .../migration.sql | 14 +++++++ prisma/schema.prisma | 38 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 prisma/migrations/20260803004634_add_asset_model/migration.sql diff --git a/prisma/migrations/20260803004634_add_asset_model/migration.sql b/prisma/migrations/20260803004634_add_asset_model/migration.sql new file mode 100644 index 0000000..7a0694b --- /dev/null +++ b/prisma/migrations/20260803004634_add_asset_model/migration.sql @@ -0,0 +1,14 @@ +-- CreateTable +CREATE TABLE "asset_models" ( + "id" UUID NOT NULL, + "category_id" UUID NOT NULL, + "manufacturer" TEXT NOT NULL, + "name" TEXT NOT NULL, + "specs" JSONB NOT NULL DEFAULT '{}', + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "asset_models_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "asset_models" ADD CONSTRAINT "asset_models_category_id_fkey" FOREIGN KEY ("category_id") REFERENCES "asset_categories"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e5f45f3..4e13dfd 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -242,6 +242,13 @@ model AssetCategory { valueGroup AssetCategoryValueGroup @map("value_group") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) attributeDefs AssetCategoryAttributeDef[] + // Back-relation for AssetModel.categoryId — see "Cập nhật 2026-08-02 (2)" + // in docs/asset-license-management.md. Only categories with + // valueGroup=high_value are ever actually referenced by an AssetModel (a + // business rule enforced in src/lib/asset-models.ts's createAssetModel(), + // not expressible as a Prisma-level constraint), but the relation itself + // is not restricted to high_value categories at the schema level. + assetModels AssetModel[] @@map("asset_categories") } @@ -293,6 +300,37 @@ model AssetCategoryAttributeDef { @@map("asset_category_attribute_defs") } +// Sits between AssetCategory and AssetItem — see docs/ +// asset-license-management.md's "Cập nhật 2026-08-02 (2)" section and the +// `AssetModel` row in the "Data model (sơ bộ)" table. 1 AssetModel has many +// AssetItem serials (AssetItem.modelId FK, not built in this task). +// `manufacturer`/`name` are deliberately two separate String fields (not +// merged into one), per the doc, to allow filtering/reporting by +// manufacturer alone (e.g. "Dell") without string-parsing a combined field. +// `categoryId` is a required FK to AssetCategory — the DB layer doesn't +// enforce that the referenced category is `high_value` (Prisma/Postgres FK +// constraints can't express that), that business rule is checked in +// src/lib/asset-models.ts's createAssetModel() via an explicit lookup +// before the create, per the doc's "Ràng buộc dữ liệu" section. `specs` is +// JSONB, defaulting to an empty object — the real per-category dynamic +// attribute *values* (whose *shape* is defined by +// AssetCategoryAttributeDef rows for this model's category). This task +// deliberately does NOT validate specs against the category's attribute +// defs (loose z.record(z.unknown()) validation only, see +// src/lib/asset-models.ts) — that dynamic cross-validation is a separate +// follow-up task per the doc's "Cập nhật 2026-08-02 (3)" section. +model AssetModel { + id String @id @default(uuid()) @db.Uuid + categoryId String @map("category_id") @db.Uuid + category AssetCategory @relation(fields: [categoryId], references: [id]) + manufacturer String + name String + specs Json @default("{}") + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) + + @@map("asset_models") +} + // Minimal internal auth accounts — NOT company SSO (deliberately ruled out, // see CLAUDE.md / auth decision notes). A handful of trusted IT staff // accounts, created by hand via scripts/create-user.ts, no self-registration.