Initial commit: Mana Collection NestJS backend.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-07 20:49:55 +03:30
co-authored by Cursor
commit fe78a99b78
87 changed files with 10557 additions and 0 deletions
@@ -0,0 +1,361 @@
-- CreateEnum
CREATE TYPE "UserRole" AS ENUM ('CUSTOMER', 'STORE_ADMIN', 'SUPER_ADMIN');
-- CreateEnum
CREATE TYPE "StoreStatus" AS ENUM ('pending', 'active', 'rejected', 'suspended');
-- CreateEnum
CREATE TYPE "OrderStatus" AS ENUM ('pending', 'confirmed', 'preparing', 'shipped', 'delivered', 'cancelled');
-- CreateEnum
CREATE TYPE "OrderStoreStatus" AS ENUM ('pending', 'confirmed', 'preparing', 'shipped', 'delivered', 'cancelled');
-- CreateEnum
CREATE TYPE "SmsOtpPurpose" AS ENUM ('register', 'login', 'resetPassword', 'storeRegister');
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"firstName" TEXT NOT NULL,
"lastName" TEXT NOT NULL,
"cellNumber" TEXT NOT NULL,
"passwordHash" TEXT NOT NULL,
"email" TEXT,
"instagram" TEXT,
"about" TEXT,
"avatarUrl" TEXT,
"avatarKey" TEXT,
"role" "UserRole" NOT NULL DEFAULT 'CUSTOMER',
"disabled" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "UserAddress" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"label" TEXT NOT NULL DEFAULT '',
"province" TEXT NOT NULL,
"city" TEXT NOT NULL,
"fullAddress" TEXT NOT NULL,
"landline" TEXT,
CONSTRAINT "UserAddress_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "RefreshToken" (
"id" TEXT NOT NULL,
"tokenHash" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "RefreshToken_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "SmsOtp" (
"id" TEXT NOT NULL,
"cellNumber" TEXT NOT NULL,
"purpose" "SmsOtpPurpose" NOT NULL,
"codeHash" TEXT NOT NULL,
"payload" JSONB,
"attempts" INTEGER NOT NULL DEFAULT 0,
"expiresAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SmsOtp_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Store" (
"id" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"nameFa" TEXT NOT NULL,
"nameEn" TEXT NOT NULL DEFAULT '',
"description" TEXT,
"logoUrl" TEXT,
"logoKey" TEXT,
"status" "StoreStatus" NOT NULL DEFAULT 'pending',
"ownerId" TEXT NOT NULL,
"rejectionReason" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Store_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Category" (
"id" TEXT NOT NULL,
"storeId" TEXT NOT NULL,
"parentId" TEXT,
"nameFa" TEXT NOT NULL,
"nameEn" TEXT NOT NULL DEFAULT '',
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"colors" TEXT[] DEFAULT ARRAY[]::TEXT[],
"sizes" TEXT[] DEFAULT ARRAY[]::TEXT[],
"packages" TEXT[] DEFAULT ARRAY[]::TEXT[],
CONSTRAINT "Category_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Brand" (
"id" TEXT NOT NULL,
"storeId" TEXT NOT NULL,
"nameFa" TEXT NOT NULL,
"nameEn" TEXT NOT NULL DEFAULT '',
"imageUrl" TEXT,
"imageKey" TEXT,
CONSTRAINT "Brand_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Product" (
"id" TEXT NOT NULL,
"storeId" TEXT NOT NULL,
"categoryId" TEXT,
"brandId" TEXT,
"nameFa" TEXT NOT NULL,
"nameEn" TEXT NOT NULL DEFAULT '',
"price" INTEGER NOT NULL DEFAULT 0,
"discountPrice" INTEGER,
"imageUrl" TEXT,
"imageKey" TEXT,
"tags" TEXT[] DEFAULT ARRAY[]::TEXT[],
"published" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Product_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ProductVariant" (
"id" TEXT NOT NULL,
"productId" TEXT NOT NULL,
"colorId" TEXT NOT NULL DEFAULT '',
"sizeId" TEXT NOT NULL DEFAULT '',
"packageId" TEXT NOT NULL DEFAULT '',
"price" INTEGER NOT NULL DEFAULT 0,
"discountPrice" INTEGER,
CONSTRAINT "ProductVariant_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "BlogPost" (
"id" TEXT NOT NULL,
"storeId" TEXT NOT NULL,
"title" TEXT NOT NULL,
"summary" TEXT NOT NULL DEFAULT '',
"body" TEXT NOT NULL DEFAULT '',
"author" TEXT NOT NULL DEFAULT '',
"imageUrl" TEXT,
"imageKey" TEXT,
"published" BOOLEAN NOT NULL DEFAULT true,
"metaTitle" TEXT,
"metaDescription" TEXT,
"slug" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "BlogPost_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Order" (
"id" TEXT NOT NULL,
"customerId" TEXT,
"customerName" TEXT NOT NULL,
"customerPhone" TEXT NOT NULL,
"status" "OrderStatus" NOT NULL DEFAULT 'pending',
"itemCount" INTEGER NOT NULL DEFAULT 0,
"totalAmount" INTEGER NOT NULL DEFAULT 0,
"note" TEXT NOT NULL DEFAULT '',
"shippingAddress" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Order_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OrderItem" (
"id" TEXT NOT NULL,
"orderId" TEXT NOT NULL,
"storeId" TEXT NOT NULL,
"productId" TEXT,
"nameFa" TEXT NOT NULL,
"variantLabel" TEXT,
"quantity" INTEGER NOT NULL DEFAULT 1,
"unitPrice" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "OrderItem_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OrderStoreFulfillment" (
"id" TEXT NOT NULL,
"orderId" TEXT NOT NULL,
"storeId" TEXT NOT NULL,
"status" "OrderStoreStatus" NOT NULL DEFAULT 'pending',
CONSTRAINT "OrderStoreFulfillment_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_cellNumber_key" ON "User"("cellNumber");
-- CreateIndex
CREATE INDEX "User_role_idx" ON "User"("role");
-- CreateIndex
CREATE INDEX "User_disabled_idx" ON "User"("disabled");
-- CreateIndex
CREATE INDEX "UserAddress_userId_idx" ON "UserAddress"("userId");
-- CreateIndex
CREATE INDEX "RefreshToken_userId_idx" ON "RefreshToken"("userId");
-- CreateIndex
CREATE INDEX "RefreshToken_tokenHash_idx" ON "RefreshToken"("tokenHash");
-- CreateIndex
CREATE INDEX "SmsOtp_cellNumber_purpose_idx" ON "SmsOtp"("cellNumber", "purpose");
-- CreateIndex
CREATE INDEX "SmsOtp_expiresAt_idx" ON "SmsOtp"("expiresAt");
-- CreateIndex
CREATE UNIQUE INDEX "Store_slug_key" ON "Store"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "Store_ownerId_key" ON "Store"("ownerId");
-- CreateIndex
CREATE INDEX "Store_status_idx" ON "Store"("status");
-- CreateIndex
CREATE INDEX "Category_storeId_idx" ON "Category"("storeId");
-- CreateIndex
CREATE INDEX "Category_parentId_idx" ON "Category"("parentId");
-- CreateIndex
CREATE INDEX "Category_sortOrder_idx" ON "Category"("sortOrder");
-- CreateIndex
CREATE INDEX "Brand_storeId_idx" ON "Brand"("storeId");
-- CreateIndex
CREATE INDEX "Product_storeId_idx" ON "Product"("storeId");
-- CreateIndex
CREATE INDEX "Product_categoryId_idx" ON "Product"("categoryId");
-- CreateIndex
CREATE INDEX "Product_brandId_idx" ON "Product"("brandId");
-- CreateIndex
CREATE INDEX "Product_price_idx" ON "Product"("price");
-- CreateIndex
CREATE INDEX "Product_published_idx" ON "Product"("published");
-- CreateIndex
CREATE INDEX "ProductVariant_productId_idx" ON "ProductVariant"("productId");
-- CreateIndex
CREATE INDEX "BlogPost_storeId_idx" ON "BlogPost"("storeId");
-- CreateIndex
CREATE INDEX "BlogPost_published_idx" ON "BlogPost"("published");
-- CreateIndex
CREATE INDEX "BlogPost_slug_idx" ON "BlogPost"("slug");
-- CreateIndex
CREATE INDEX "Order_customerId_idx" ON "Order"("customerId");
-- CreateIndex
CREATE INDEX "Order_status_idx" ON "Order"("status");
-- CreateIndex
CREATE INDEX "Order_createdAt_idx" ON "Order"("createdAt");
-- CreateIndex
CREATE INDEX "OrderItem_orderId_idx" ON "OrderItem"("orderId");
-- CreateIndex
CREATE INDEX "OrderItem_storeId_idx" ON "OrderItem"("storeId");
-- CreateIndex
CREATE INDEX "OrderItem_productId_idx" ON "OrderItem"("productId");
-- CreateIndex
CREATE INDEX "OrderStoreFulfillment_storeId_idx" ON "OrderStoreFulfillment"("storeId");
-- CreateIndex
CREATE INDEX "OrderStoreFulfillment_status_idx" ON "OrderStoreFulfillment"("status");
-- CreateIndex
CREATE UNIQUE INDEX "OrderStoreFulfillment_orderId_storeId_key" ON "OrderStoreFulfillment"("orderId", "storeId");
-- AddForeignKey
ALTER TABLE "UserAddress" ADD CONSTRAINT "UserAddress_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "RefreshToken" ADD CONSTRAINT "RefreshToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Store" ADD CONSTRAINT "Store_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Category" ADD CONSTRAINT "Category_storeId_fkey" FOREIGN KEY ("storeId") REFERENCES "Store"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Category" ADD CONSTRAINT "Category_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Category"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Brand" ADD CONSTRAINT "Brand_storeId_fkey" FOREIGN KEY ("storeId") REFERENCES "Store"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Product" ADD CONSTRAINT "Product_storeId_fkey" FOREIGN KEY ("storeId") REFERENCES "Store"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Product" ADD CONSTRAINT "Product_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Product" ADD CONSTRAINT "Product_brandId_fkey" FOREIGN KEY ("brandId") REFERENCES "Brand"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ProductVariant" ADD CONSTRAINT "ProductVariant_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BlogPost" ADD CONSTRAINT "BlogPost_storeId_fkey" FOREIGN KEY ("storeId") REFERENCES "Store"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Order" ADD CONSTRAINT "Order_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OrderItem" ADD CONSTRAINT "OrderItem_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OrderItem" ADD CONSTRAINT "OrderItem_storeId_fkey" FOREIGN KEY ("storeId") REFERENCES "Store"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OrderItem" ADD CONSTRAINT "OrderItem_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OrderStoreFulfillment" ADD CONSTRAINT "OrderStoreFulfillment_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OrderStoreFulfillment" ADD CONSTRAINT "OrderStoreFulfillment_storeId_fkey" FOREIGN KEY ("storeId") REFERENCES "Store"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "Product" ADD COLUMN IF NOT EXISTS "summary" TEXT NOT NULL DEFAULT '';
ALTER TABLE "Product" ADD COLUMN IF NOT EXISTS "description" TEXT NOT NULL DEFAULT '';
ALTER TABLE "Product" ADD COLUMN IF NOT EXISTS "galleryUrls" TEXT[] DEFAULT ARRAY[]::TEXT[];
ALTER TABLE "Product" ADD COLUMN IF NOT EXISTS "galleryKeys" TEXT[] DEFAULT ARRAY[]::TEXT[];
+2
View File
@@ -0,0 +1,2 @@
-- Prisma migration lock
provider = "postgresql"
+287
View File
@@ -0,0 +1,287 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum UserRole {
CUSTOMER
STORE_ADMIN
SUPER_ADMIN
}
enum StoreStatus {
pending
active
rejected
suspended
}
enum OrderStatus {
pending
confirmed
preparing
shipped
delivered
cancelled
}
enum OrderStoreStatus {
pending
confirmed
preparing
shipped
delivered
cancelled
}
enum SmsOtpPurpose {
register
login
resetPassword
storeRegister
}
model User {
id String @id @default(cuid())
firstName String
lastName String
cellNumber String @unique
passwordHash String
email String?
instagram String?
about String?
avatarUrl String?
avatarKey String?
role UserRole @default(CUSTOMER)
disabled Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
refreshTokens RefreshToken[]
addresses UserAddress[]
ownedStore Store?
orders Order[] @relation("CustomerOrders")
@@index([role])
@@index([disabled])
}
model UserAddress {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
label String @default("")
province String
city String
fullAddress String
landline String?
@@index([userId])
}
model RefreshToken {
id String @id @default(cuid())
tokenHash String
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
expiresAt DateTime
createdAt DateTime @default(now())
@@index([userId])
@@index([tokenHash])
}
model SmsOtp {
id String @id @default(cuid())
cellNumber String
purpose SmsOtpPurpose
codeHash String
payload Json?
attempts Int @default(0)
expiresAt DateTime
createdAt DateTime @default(now())
@@index([cellNumber, purpose])
@@index([expiresAt])
}
model Store {
id String @id @default(cuid())
slug String @unique
nameFa String
nameEn String @default("")
description String?
logoUrl String?
logoKey String?
status StoreStatus @default(pending)
ownerId String @unique
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
rejectionReason String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
products Product[]
categories Category[]
brands Brand[]
blogs BlogPost[]
orderItems OrderItem[]
fulfillments OrderStoreFulfillment[]
@@index([status])
}
model Category {
id String @id @default(cuid())
storeId String
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
parentId String?
parent Category? @relation("CategoryTree", fields: [parentId], references: [id], onDelete: SetNull)
children Category[] @relation("CategoryTree")
nameFa String
nameEn String @default("")
sortOrder Int @default(0)
colors String[] @default([])
sizes String[] @default([])
packages String[] @default([])
products Product[]
@@index([storeId])
@@index([parentId])
@@index([sortOrder])
}
model Brand {
id String @id @default(cuid())
storeId String
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
nameFa String
nameEn String @default("")
imageUrl String?
imageKey String?
products Product[]
@@index([storeId])
}
model Product {
id String @id @default(cuid())
storeId String
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
categoryId String?
category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
brandId String?
brand Brand? @relation(fields: [brandId], references: [id], onDelete: SetNull)
nameFa String
nameEn String @default("")
summary String @default("")
description String @default("")
price Int @default(0)
discountPrice Int?
imageUrl String?
imageKey String?
galleryUrls String[] @default([])
galleryKeys String[] @default([])
tags String[] @default([])
published Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
variants ProductVariant[]
orderItems OrderItem[]
@@index([storeId])
@@index([categoryId])
@@index([brandId])
@@index([price])
@@index([published])
}
model ProductVariant {
id String @id @default(cuid())
productId String
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
colorId String @default("")
sizeId String @default("")
packageId String @default("")
price Int @default(0)
discountPrice Int?
@@index([productId])
}
model BlogPost {
id String @id @default(cuid())
storeId String
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
title String
summary String @default("")
body String @default("")
author String @default("")
imageUrl String?
imageKey String?
published Boolean @default(true)
metaTitle String?
metaDescription String?
slug String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([storeId])
@@index([published])
@@index([slug])
}
model Order {
id String @id @default(cuid())
customerId String?
customer User? @relation("CustomerOrders", fields: [customerId], references: [id], onDelete: SetNull)
customerName String
customerPhone String
status OrderStatus @default(pending)
itemCount Int @default(0)
totalAmount Int @default(0)
note String @default("")
shippingAddress Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
items OrderItem[]
fulfillments OrderStoreFulfillment[]
@@index([customerId])
@@index([status])
@@index([createdAt])
}
model OrderItem {
id String @id @default(cuid())
orderId String
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
storeId String
store Store @relation(fields: [storeId], references: [id], onDelete: Restrict)
productId String?
product Product? @relation(fields: [productId], references: [id], onDelete: SetNull)
nameFa String
variantLabel String?
quantity Int @default(1)
unitPrice Int @default(0)
@@index([orderId])
@@index([storeId])
@@index([productId])
}
model OrderStoreFulfillment {
id String @id @default(cuid())
orderId String
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
storeId String
store Store @relation(fields: [storeId], references: [id], onDelete: Restrict)
status OrderStoreStatus @default(pending)
@@unique([orderId, storeId])
@@index([storeId])
@@index([status])
}