generator client { provider = "prisma-client" output = "../src/generated/prisma" moduleFormat = "esm" importFileExtension = "js" } datasource db { provider = "postgresql" } enum Role { ADMIN EDITOR } enum MessageStatus { NEW READ ARCHIVED } model AdminUser { id Int @id @default(autoincrement()) phone String @unique name String passwordHash String role Role @default(EDITOR) isActive Boolean @default(true) lastLoginAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt refreshTokens RefreshToken[] auditLogs AuditLog[] } model RefreshToken { id Int @id @default(autoincrement()) userId Int user AdminUser @relation(fields: [userId], references: [id], onDelete: Cascade) /// sha256 of the opaque token stored in the httpOnly cookie tokenHash String @unique expiresAt DateTime revokedAt DateTime? userAgent String @default("") createdAt DateTime @default(now()) @@index([userId]) } model Media { id Int @id @default(autoincrement()) /// object key in the S3 bucket; the public URL is MEDIA_PUBLIC_URL + "/" + key key String @unique mime String size Int width Int? height Int? alt String @default("") originalName String @default("") createdAt DateTime @default(now()) categoryCovers Category[] @relation("CategoryCover") productImages Product[] @relation("ProductImage") productGallery ProductImage[] } model Category { id Int @id @default(autoincrement()) slug String @unique code String name String nameEn String @default("") shortDesc String @default("") fullDesc String @default("") /// Lucide icon name icon String @default("") coverImageId Int? coverImage Media? @relation("CategoryCover", fields: [coverImageId], references: [id], onDelete: SetNull) sortOrder Int @default(0) isPublished Boolean @default(true) seoTitle String @default("") seoDescription String @default("") groups ProductGroup[] products Product[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([sortOrder]) } model ProductGroup { id Int @id @default(autoincrement()) categoryId Int category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade) slug String name String desc String @default("") sortOrder Int @default(0) products Product[] @@unique([categoryId, slug]) } model Product { id Int @id @default(autoincrement()) categoryId Int category Category @relation(fields: [categoryId], references: [id], onDelete: Restrict) groupId Int? group ProductGroup? @relation(fields: [groupId], references: [id], onDelete: SetNull) code String @unique slug String @unique name String nameEn String @default("") desc String @default("") specs String[] variants String[] /// long description for the product page; a blank line starts a new paragraph details String @default("") brand String @default("") /// country of manufacture origin String @default("") sizes String[] @default([]) standards String[] @default([]) features String[] @default([]) applications String[] @default([]) /// technical specification table: [{ "name": "...", "value": "..." }] attributes Json @default("[]") packaging String @default("") minOrder String @default("") seoTitle String @default("") seoDescription String @default("") imageId Int? image Media? @relation("ProductImage", fields: [imageId], references: [id], onDelete: SetNull) gallery ProductImage[] sortOrder Int @default(0) isPublished Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([categoryId, sortOrder]) } model ProductImage { productId Int mediaId Int sortOrder Int @default(0) product Product @relation(fields: [productId], references: [id], onDelete: Cascade) media Media @relation(fields: [mediaId], references: [id], onDelete: Cascade) @@id([productId, mediaId]) } /// One row per editable website section; `data` is validated by the zod schema for `key` (@imen/shared) model SiteSection { key String @id data Json updatedById Int? updatedAt DateTime @updatedAt } model ContactMessage { id Int @id @default(autoincrement()) name String org String phone String message String @default("") status MessageStatus @default(NEW) ip String @default("") userAgent String @default("") createdAt DateTime @default(now()) @@index([status, createdAt]) } model AuditLog { id Int @id @default(autoincrement()) userId Int? user AdminUser? @relation(fields: [userId], references: [id], onDelete: SetNull) /// create | update | delete | login ... action String /// product | category | section | media | user | message entity String entityId String @default("") summary String @default("") createdAt DateTime @default(now()) @@index([createdAt]) }