Files
imen_pishghaman/apps/api/prisma/schema.prisma
T
S. EsmaeiliandClaude Opus 5.5 789d224ea0 feat: monorepo with NestJS API, admin panel and DB-driven website
Restructure the repo into a pnpm + Turborepo monorepo:

- apps/web: Nuxt 4 site, now server-rendered and reading all content
  from the API; pages cached with SWR and purged on content changes
- apps/api: NestJS 12 + Prisma 7 + PostgreSQL. Auth with short-lived
  access tokens and rotating httpOnly refresh cookies, roles (ADMIN,
  EDITOR), catalog, editable site sections, media upload to S3-compatible
  storage (converted to WebP), contact messages, audit log
- apps/admin: Vue 3 admin panel (Ant Design Vue, Pinia, Tailwind) for
  products, categories, site sections, messages, media and users
- packages/shared: Zod schemas and types shared by API and admin

Also:
- seed script that loads the original 8 categories / 89 products and
  section content
- Docker images for api (with a one-off migrate service), web, admin,
  and an nginx gateway for both domains
- dev and production compose files, daily database backup service
- API e2e tests

Removes the committed static out/ build and the old root Dockerfile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:31:05 +03:30

174 lines
5.1 KiB
Plaintext

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())
email 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[]
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])
}