x
This commit is contained in:
1 parent
2d39edbd57
commit
78b722e120
24 files changed
+2130
-65
No files matched your search
@@ -2,8 +2,68 @@ import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
mergeSectionTranslations,
|
||||
transformLocalizedPaths,
|
||||
validateSectionPayload,
|
||||
} from "./pages";
|
||||
|
||||
describe("Page section payload validation", () => {
|
||||
test("accepts a DIVIDER payload", () => {
|
||||
const result = validateSectionPayload("DIVIDER", {
|
||||
width: "narrow",
|
||||
style: "dotted",
|
||||
thickness: 2,
|
||||
spacing: "xl",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
test.each(["#123", "#AbC8", "#12aBcD", "#12aBcD80"])(
|
||||
"accepts DIVIDER color %s",
|
||||
(color) => {
|
||||
const result = validateSectionPayload("DIVIDER", {
|
||||
width: "container",
|
||||
style: "solid",
|
||||
thickness: 1,
|
||||
spacing: "md",
|
||||
color,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
test.each(["red", "#12", "#12345", "#123456789", "#fff; background: url(javascript:alert(1))"])(
|
||||
"rejects unsafe DIVIDER color %s",
|
||||
(color) => {
|
||||
const result = validateSectionPayload("DIVIDER", {
|
||||
width: "container",
|
||||
style: "solid",
|
||||
thickness: 1,
|
||||
spacing: "md",
|
||||
color,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.issues?.[0]?.path).toEqual(["color"]);
|
||||
expect(result.issues?.[0]?.message).toBe(
|
||||
"Color must be a hex value in #RGB, #RGBA, #RRGGBB, or #RRGGBBAA format",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test("rejects an unsupported DIVIDER width", () => {
|
||||
const result = validateSectionPayload("DIVIDER", {
|
||||
width: "wide",
|
||||
style: "solid",
|
||||
thickness: 1,
|
||||
spacing: "sm",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.issues?.[0]?.path).toEqual(["width"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Page section localization", () => {
|
||||
const paths = ["title", "items[].title", "items[].note"];
|
||||
const payload = {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
FeaturedTilesSectionPayloadSchema,
|
||||
EditorialCarouselSectionPayloadSchema,
|
||||
HeroCarouselSectionPayloadSchema,
|
||||
DividerSectionPayloadSchema,
|
||||
HeroArticleSectionPayloadSchema,
|
||||
RichTextSectionPayloadSchema,
|
||||
ArticleHeaderSectionPayloadSchema,
|
||||
@@ -1335,6 +1336,9 @@ export function validateSectionPayload(
|
||||
case "HERO_CAROUSEL":
|
||||
schema = HeroCarouselSectionPayloadSchema;
|
||||
break;
|
||||
case "DIVIDER":
|
||||
schema = DividerSectionPayloadSchema;
|
||||
break;
|
||||
case "HERO_ARTICLE":
|
||||
schema = HeroArticleSectionPayloadSchema;
|
||||
break;
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { Elysia, t } from "elysia";
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import prisma from "../lib/prisma";
|
||||
import { localizeProductAttribute } from "../lib/productAttributes";
|
||||
import { hydrateSectionPayload, validateSectionPayload } from "./pages";
|
||||
import {
|
||||
createCustomProductDetailSection,
|
||||
createProductMainDetailSection,
|
||||
defaultProductDetailSections,
|
||||
validateProductDetailSectionPayload,
|
||||
} from "../lib/productDetailSections";
|
||||
import { hydrateSectionPayload } from "./pages";
|
||||
|
||||
function summarizeValidationIssues(issues: any[]) {
|
||||
return issues.map((issue) => ({
|
||||
@@ -11,16 +18,6 @@ function summarizeValidationIssues(issues: any[]) {
|
||||
}));
|
||||
}
|
||||
|
||||
function defaultProductDetailSections(productId: number) {
|
||||
return [
|
||||
{ productId, type: "PRODUCT_MAIN", order: 1, payload: { layoutMode: "standard", galleryBehavior: "carousel", purchaseCardSticky: true, showStickyPurchaseBarAfterMain: true, showBreadcrumbs: true, showBrandMeta: true }, isActive: true, isLocked: true },
|
||||
{ productId, type: "PRODUCT_DETAILS", order: 2, payload: { title: { th: "รายละเอียดสินค้า", en: "Product Details" }, showHeading: true }, isActive: true },
|
||||
{ productId, type: "PRODUCT_SPECS", order: 3, payload: { title: { th: "ข้อมูลสินค้า", en: "Specifications" }, showHeading: true }, isActive: true },
|
||||
{ productId, type: "PRODUCT_REVIEWS", order: 4, payload: { title: { th: "รีวิว", en: "Reviews" }, showHeading: true }, isActive: true },
|
||||
{ productId, type: "RELATED_PRODUCTS", order: 5, payload: { title: { th: "สินค้าที่เกี่ยวข้อง", en: "Related Products" }, showHeading: true, showViewAllLink: true }, isActive: true },
|
||||
];
|
||||
}
|
||||
|
||||
async function assertActiveCategory(categoryId: number, client: any = prisma) {
|
||||
const category = await client.category.findFirst({
|
||||
where: { id: categoryId, deletedAt: null },
|
||||
@@ -508,7 +505,7 @@ export const productsRoute = new Elysia({ prefix: "/products" })
|
||||
const payload = body as any;
|
||||
const type = String(payload.type || "").toUpperCase();
|
||||
const sectionPayload = payload.payload || {};
|
||||
const validationResult = validateSectionPayload(type, sectionPayload);
|
||||
const validationResult = validateProductDetailSectionPayload(type, sectionPayload);
|
||||
if (!validationResult.success) {
|
||||
return status(400, {
|
||||
success: false,
|
||||
@@ -520,7 +517,8 @@ export const productsRoute = new Elysia({ prefix: "/products" })
|
||||
const product = await prisma.product.findUnique({ where: { id }, select: { id: true } });
|
||||
if (!product) return status(404, { message: "Product not found" });
|
||||
|
||||
const isProductMain = type === "PRODUCT_MAIN";
|
||||
const validatedType = validationResult.type;
|
||||
const isProductMain = validatedType === "PRODUCT_MAIN";
|
||||
if (isProductMain) {
|
||||
const existingMain = await prisma.productDetailSection.count({
|
||||
where: { productId: id, type: "PRODUCT_MAIN" },
|
||||
@@ -530,38 +528,16 @@ export const productsRoute = new Elysia({ prefix: "/products" })
|
||||
}
|
||||
}
|
||||
|
||||
const lastSection = await prisma.productDetailSection.findFirst({
|
||||
where: { productId: id },
|
||||
orderBy: { order: "desc" },
|
||||
});
|
||||
|
||||
const section = isProductMain
|
||||
? (await prisma.$transaction([
|
||||
prisma.productDetailSection.updateMany({
|
||||
where: { productId: id },
|
||||
data: { order: { increment: 1 } },
|
||||
}),
|
||||
prisma.productDetailSection.create({
|
||||
data: {
|
||||
productId: id,
|
||||
type,
|
||||
order: 1,
|
||||
payload: validationResult.data,
|
||||
isActive: true,
|
||||
isLocked: true,
|
||||
},
|
||||
}),
|
||||
]))[1]
|
||||
: await prisma.productDetailSection.create({
|
||||
data: {
|
||||
productId: id,
|
||||
type,
|
||||
order: Number.isFinite(Number(payload.order)) ? Number(payload.order) : (lastSection?.order || 0) + 1,
|
||||
payload: validationResult.data,
|
||||
isActive: payload.isActive !== false,
|
||||
isLocked: Boolean(payload.isLocked),
|
||||
},
|
||||
});
|
||||
? await prisma.$transaction((tx: Prisma.TransactionClient) => createProductMainDetailSection(tx, id, validationResult.data))
|
||||
: await prisma.$transaction((tx: Prisma.TransactionClient) => createCustomProductDetailSection(tx, {
|
||||
productId: id,
|
||||
type: validatedType,
|
||||
payload: validationResult.data,
|
||||
...(Number.isFinite(Number(payload.order)) ? { order: Number(payload.order) } : {}),
|
||||
isActive: payload.isActive !== false,
|
||||
isLocked: Boolean(payload.isLocked),
|
||||
}));
|
||||
|
||||
return { success: true, section };
|
||||
})
|
||||
@@ -575,7 +551,7 @@ export const productsRoute = new Elysia({ prefix: "/products" })
|
||||
const payload = body as any;
|
||||
const type = String(payload.type || existing.type).toUpperCase();
|
||||
const sectionPayload = payload.payload === undefined ? existing.payload : payload.payload;
|
||||
const validationResult = validateSectionPayload(type, sectionPayload);
|
||||
const validationResult = validateProductDetailSectionPayload(type, sectionPayload);
|
||||
if (!validationResult.success) {
|
||||
return status(400, {
|
||||
success: false,
|
||||
@@ -584,8 +560,9 @@ export const productsRoute = new Elysia({ prefix: "/products" })
|
||||
});
|
||||
}
|
||||
|
||||
const validatedType = validationResult.type;
|
||||
if (existing.isLocked) {
|
||||
if (payload.type && type !== existing.type) {
|
||||
if (payload.type && validatedType !== existing.type) {
|
||||
return status(409, { success: false, message: "Locked product detail sections cannot change type" });
|
||||
}
|
||||
if (payload.isActive === false) {
|
||||
@@ -606,7 +583,7 @@ export const productsRoute = new Elysia({ prefix: "/products" })
|
||||
const section = await prisma.productDetailSection.update({
|
||||
where: { id: sectionId },
|
||||
data: {
|
||||
type,
|
||||
type: validatedType,
|
||||
payload: validationResult.data,
|
||||
...(payload.order !== undefined ? { order: Number(payload.order) } : {}),
|
||||
...(payload.isActive !== undefined ? { isActive: Boolean(payload.isActive) } : {}),
|
||||
|
||||
Reference in new issue
Block a user