Add public catalog APIs, website slots, featured slots, and int category/product IDs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-27 18:30:36 +03:30
co-authored by Cursor
parent 15dd3597f0
commit 835c8a0ac1
25 changed files with 1399 additions and 41 deletions
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "Category" ADD COLUMN "technicalFields" JSONB NOT NULL DEFAULT '[]';
-- AlterTable
ALTER TABLE "Product" ADD COLUMN "technicalData" JSONB NOT NULL DEFAULT '{}';
@@ -0,0 +1,30 @@
-- CreateTable
CREATE TABLE "WebsiteSlotItem" (
"id" TEXT NOT NULL,
"storeId" TEXT NOT NULL,
"slotKey" TEXT NOT NULL,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"nameFa" TEXT NOT NULL DEFAULT '',
"nameEn" TEXT NOT NULL DEFAULT '',
"subtext" TEXT NOT NULL DEFAULT '',
"linkText" TEXT NOT NULL DEFAULT '',
"link" TEXT NOT NULL DEFAULT '',
"imageUrl" TEXT,
"imageKey" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "WebsiteSlotItem_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "WebsiteSlotItem_storeId_idx" ON "WebsiteSlotItem"("storeId");
-- CreateIndex
CREATE INDEX "WebsiteSlotItem_storeId_slotKey_idx" ON "WebsiteSlotItem"("storeId", "slotKey");
-- CreateIndex
CREATE INDEX "WebsiteSlotItem_storeId_slotKey_sortOrder_idx" ON "WebsiteSlotItem"("storeId", "slotKey", "sortOrder");
-- AddForeignKey
ALTER TABLE "WebsiteSlotItem" ADD CONSTRAINT "WebsiteSlotItem_storeId_fkey" FOREIGN KEY ("storeId") REFERENCES "Store"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "Product" ADD COLUMN "featuredSlot" TEXT;
-- CreateIndex
CREATE INDEX "Product_featuredSlot_idx" ON "Product"("featuredSlot");
@@ -0,0 +1,92 @@
-- Convert Category.id (cuid text) → serial integer; remap Product.categoryId + parentId.
ALTER TABLE "Product" DROP CONSTRAINT IF EXISTS "Product_categoryId_fkey";
ALTER TABLE "Category" DROP CONSTRAINT IF EXISTS "Category_parentId_fkey";
ALTER TABLE "Category" DROP CONSTRAINT IF EXISTS "Category_storeId_fkey";
DROP INDEX IF EXISTS "Category_storeId_idx";
DROP INDEX IF EXISTS "Category_parentId_idx";
DROP INDEX IF EXISTS "Category_sortOrder_idx";
DROP INDEX IF EXISTS "Product_categoryId_idx";
CREATE TABLE "_category_id_map" (
"old_id" TEXT PRIMARY KEY,
"new_id" INTEGER NOT NULL UNIQUE
);
INSERT INTO "_category_id_map" ("old_id", "new_id")
SELECT "id", ROW_NUMBER() OVER (ORDER BY "id")::INTEGER
FROM "Category";
CREATE TABLE "Category_new" (
"id" INTEGER NOT NULL,
"storeId" TEXT NOT NULL,
"parentId" INTEGER,
"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[],
"technicalFields" JSONB NOT NULL DEFAULT '[]',
CONSTRAINT "Category_new_pkey" PRIMARY KEY ("id")
);
INSERT INTO "Category_new" (
"id", "storeId", "parentId", "nameFa", "nameEn", "sortOrder",
"colors", "sizes", "packages", "technicalFields"
)
SELECT
m."new_id",
c."storeId",
mp."new_id",
c."nameFa",
c."nameEn",
c."sortOrder",
c."colors",
c."sizes",
c."packages",
COALESCE(c."technicalFields", '[]'::jsonb)
FROM "Category" c
JOIN "_category_id_map" m ON m."old_id" = c."id"
LEFT JOIN "_category_id_map" mp ON mp."old_id" = c."parentId";
ALTER TABLE "Product" ADD COLUMN "categoryId_new" INTEGER;
UPDATE "Product" AS p
SET "categoryId_new" = m."new_id"
FROM "_category_id_map" AS m
WHERE p."categoryId" = m."old_id";
ALTER TABLE "Product" DROP COLUMN "categoryId";
ALTER TABLE "Product" RENAME COLUMN "categoryId_new" TO "categoryId";
DROP TABLE "Category";
ALTER TABLE "Category_new" RENAME TO "Category";
ALTER TABLE "Category" RENAME CONSTRAINT "Category_new_pkey" TO "Category_pkey";
CREATE SEQUENCE "Category_id_seq" OWNED BY "Category"."id";
SELECT setval(
'"Category_id_seq"',
COALESCE((SELECT MAX("id") FROM "Category"), 1)
);
ALTER TABLE "Category" ALTER COLUMN "id" SET DEFAULT nextval('"Category_id_seq"');
CREATE INDEX "Category_storeId_idx" ON "Category"("storeId");
CREATE INDEX "Category_parentId_idx" ON "Category"("parentId");
CREATE INDEX "Category_sortOrder_idx" ON "Category"("sortOrder");
CREATE INDEX "Product_categoryId_idx" ON "Product"("categoryId");
ALTER TABLE "Category"
ADD CONSTRAINT "Category_storeId_fkey"
FOREIGN KEY ("storeId") REFERENCES "Store"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Category"
ADD CONSTRAINT "Category_parentId_fkey"
FOREIGN KEY ("parentId") REFERENCES "Category"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "Product"
ADD CONSTRAINT "Product_categoryId_fkey"
FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE SET NULL ON UPDATE CASCADE;
DROP TABLE "_category_id_map";
@@ -0,0 +1,154 @@
-- Convert Product.id (cuid text) → serial integer; remap ProductVariant + OrderItem FKs.
ALTER TABLE "ProductVariant" DROP CONSTRAINT IF EXISTS "ProductVariant_productId_fkey";
ALTER TABLE "OrderItem" DROP CONSTRAINT IF EXISTS "OrderItem_productId_fkey";
ALTER TABLE "Product" DROP CONSTRAINT IF EXISTS "Product_storeId_fkey";
ALTER TABLE "Product" DROP CONSTRAINT IF EXISTS "Product_categoryId_fkey";
ALTER TABLE "Product" DROP CONSTRAINT IF EXISTS "Product_brandId_fkey";
DROP INDEX IF EXISTS "Product_storeId_idx";
DROP INDEX IF EXISTS "Product_categoryId_idx";
DROP INDEX IF EXISTS "Product_brandId_idx";
DROP INDEX IF EXISTS "Product_price_idx";
DROP INDEX IF EXISTS "Product_published_idx";
DROP INDEX IF EXISTS "Product_featuredSlot_idx";
DROP INDEX IF EXISTS "Product_vipEnabled_idx";
DROP INDEX IF EXISTS "Product_vipOnly_idx";
DROP INDEX IF EXISTS "ProductVariant_productId_idx";
DROP INDEX IF EXISTS "OrderItem_productId_idx";
CREATE TABLE "_product_id_map" (
"old_id" TEXT PRIMARY KEY,
"new_id" INTEGER NOT NULL UNIQUE
);
INSERT INTO "_product_id_map" ("old_id", "new_id")
SELECT "id", ROW_NUMBER() OVER (ORDER BY "createdAt", "id")::INTEGER
FROM "Product";
CREATE TABLE "Product_new" (
"id" INTEGER NOT NULL,
"storeId" TEXT NOT NULL,
"categoryId" INTEGER,
"brandId" TEXT,
"nameFa" TEXT NOT NULL,
"nameEn" TEXT NOT NULL DEFAULT '',
"summary" TEXT NOT NULL DEFAULT '',
"description" TEXT NOT NULL DEFAULT '',
"price" INTEGER NOT NULL DEFAULT 0,
"discountPrice" INTEGER,
"imageUrl" TEXT,
"imageKey" TEXT,
"galleryUrls" TEXT[] DEFAULT ARRAY[]::TEXT[],
"galleryKeys" TEXT[] DEFAULT ARRAY[]::TEXT[],
"tags" TEXT[] DEFAULT ARRAY[]::TEXT[],
"published" BOOLEAN NOT NULL DEFAULT true,
"featuredSlot" TEXT,
"vipEnabled" BOOLEAN NOT NULL DEFAULT false,
"vipOnly" BOOLEAN NOT NULL DEFAULT false,
"vipSpecialPrice" BOOLEAN NOT NULL DEFAULT false,
"vipPrice" INTEGER,
"technicalData" JSONB NOT NULL DEFAULT '{}',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Product_new_pkey" PRIMARY KEY ("id")
);
INSERT INTO "Product_new" (
"id", "storeId", "categoryId", "brandId", "nameFa", "nameEn", "summary", "description",
"price", "discountPrice", "imageUrl", "imageKey", "galleryUrls", "galleryKeys", "tags",
"published", "featuredSlot", "vipEnabled", "vipOnly", "vipSpecialPrice", "vipPrice",
"technicalData", "createdAt", "updatedAt"
)
SELECT
m."new_id",
p."storeId",
p."categoryId",
p."brandId",
p."nameFa",
p."nameEn",
p."summary",
p."description",
p."price",
p."discountPrice",
p."imageUrl",
p."imageKey",
p."galleryUrls",
p."galleryKeys",
p."tags",
p."published",
p."featuredSlot",
p."vipEnabled",
p."vipOnly",
p."vipSpecialPrice",
p."vipPrice",
COALESCE(p."technicalData", '{}'::jsonb),
p."createdAt",
p."updatedAt"
FROM "Product" p
JOIN "_product_id_map" m ON m."old_id" = p."id";
ALTER TABLE "ProductVariant" ADD COLUMN "productId_new" INTEGER;
UPDATE "ProductVariant" AS v
SET "productId_new" = m."new_id"
FROM "_product_id_map" AS m
WHERE v."productId" = m."old_id";
ALTER TABLE "ProductVariant" DROP COLUMN "productId";
ALTER TABLE "ProductVariant" RENAME COLUMN "productId_new" TO "productId";
ALTER TABLE "ProductVariant" ALTER COLUMN "productId" SET NOT NULL;
ALTER TABLE "OrderItem" ADD COLUMN "productId_new" INTEGER;
UPDATE "OrderItem" AS oi
SET "productId_new" = m."new_id"
FROM "_product_id_map" AS m
WHERE oi."productId" = m."old_id";
ALTER TABLE "OrderItem" DROP COLUMN "productId";
ALTER TABLE "OrderItem" RENAME COLUMN "productId_new" TO "productId";
DROP TABLE "Product";
ALTER TABLE "Product_new" RENAME TO "Product";
ALTER TABLE "Product" RENAME CONSTRAINT "Product_new_pkey" TO "Product_pkey";
CREATE SEQUENCE "Product_id_seq" OWNED BY "Product"."id";
SELECT setval(
'"Product_id_seq"',
COALESCE((SELECT MAX("id") FROM "Product"), 1)
);
ALTER TABLE "Product" ALTER COLUMN "id" SET DEFAULT nextval('"Product_id_seq"');
CREATE INDEX "Product_storeId_idx" ON "Product"("storeId");
CREATE INDEX "Product_categoryId_idx" ON "Product"("categoryId");
CREATE INDEX "Product_brandId_idx" ON "Product"("brandId");
CREATE INDEX "Product_price_idx" ON "Product"("price");
CREATE INDEX "Product_published_idx" ON "Product"("published");
CREATE INDEX "Product_featuredSlot_idx" ON "Product"("featuredSlot");
CREATE INDEX "Product_vipEnabled_idx" ON "Product"("vipEnabled");
CREATE INDEX "Product_vipOnly_idx" ON "Product"("vipOnly");
CREATE INDEX "ProductVariant_productId_idx" ON "ProductVariant"("productId");
CREATE INDEX "OrderItem_productId_idx" ON "OrderItem"("productId");
ALTER TABLE "Product"
ADD CONSTRAINT "Product_storeId_fkey"
FOREIGN KEY ("storeId") REFERENCES "Store"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Product"
ADD CONSTRAINT "Product_categoryId_fkey"
FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "Product"
ADD CONSTRAINT "Product_brandId_fkey"
FOREIGN KEY ("brandId") REFERENCES "Brand"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "ProductVariant"
ADD CONSTRAINT "ProductVariant_productId_fkey"
FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "OrderItem"
ADD CONSTRAINT "OrderItem_productId_fkey"
FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE SET NULL ON UPDATE CASCADE;
DROP TABLE "_product_id_map";
+41 -10
View File
@@ -131,24 +131,27 @@ model Store {
blogs BlogPost[]
orderItems OrderItem[]
fulfillments OrderStoreFulfillment[]
websiteSlots WebsiteSlotItem[]
@@index([status])
}
model Category {
id String @id @default(cuid())
id Int @id @default(autoincrement())
storeId String
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
parentId String?
parentId Int?
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[]
colors String[] @default([])
sizes String[] @default([])
packages String[] @default([])
/// Ordered form-builder defs: [{ id, labelFa, labelEn?, type: "text", required? }]
technicalFields Json @default("[]")
products Product[]
@@index([storeId])
@@index([parentId])
@@ -169,10 +172,10 @@ model Brand {
}
model Product {
id String @id @default(cuid())
id Int @id @default(autoincrement())
storeId String
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
categoryId String?
categoryId Int?
category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
brandId String?
brand Brand? @relation(fields: [brandId], references: [id], onDelete: SetNull)
@@ -188,10 +191,14 @@ model Product {
galleryKeys String[] @default([])
tags String[] @default([])
published Boolean @default(true)
/// Home carousel featured slot: promo-1 | promo-2 | promo-3
featuredSlot String?
vipEnabled Boolean @default(false)
vipOnly Boolean @default(false)
vipSpecialPrice Boolean @default(false)
vipPrice Int?
/// Values keyed by category technical field id: { [fieldId]: string }
technicalData Json @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
variants ProductVariant[]
@@ -202,13 +209,14 @@ model Product {
@@index([brandId])
@@index([price])
@@index([published])
@@index([featuredSlot])
@@index([vipEnabled])
@@index([vipOnly])
}
model ProductVariant {
id String @id @default(cuid())
productId String
productId Int
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
colorId String @default("")
sizeId String @default("")
@@ -289,7 +297,7 @@ model OrderItem {
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
storeId String
store Store @relation(fields: [storeId], references: [id], onDelete: Restrict)
productId String?
productId Int?
product Product? @relation(fields: [productId], references: [id], onDelete: SetNull)
nameFa String
variantLabel String?
@@ -313,3 +321,26 @@ model OrderStoreFulfillment {
@@index([storeId])
@@index([status])
}
/// Homepage / storefront image slots (hero slides, banners, category thumbs, …)
model WebsiteSlotItem {
id String @id @default(cuid())
storeId String
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
/// e.g. home.hero | home.category.shirts | home.banner.leather
slotKey String
sortOrder Int @default(0)
nameFa String @default("")
nameEn String @default("")
subtext String @default("")
linkText String @default("")
link String @default("")
imageUrl String?
imageKey String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([storeId])
@@index([storeId, slotKey])
@@index([storeId, slotKey, sortOrder])
}