Files
sass-mohfarawati/lib/portfolio-validation.ts
T
Moh ba63f75ea8
CI / quality (push) Waiting to run
REFACTORED - Replace Prisma with Drizzle ORM
- Add lib/db (schema, postgres.js client, enums, seed, migrations) on Drizzle
- Rewrite all lib and admin action queries from Prisma to Drizzle
- Keep existing table/column names so no data migration is needed
- Preserve signed-cookie admin auth unchanged
- Map unique-violation handling from Prisma P2002 to SQLSTATE 23505
- Swap deps, scripts, Makefile, and Dockerfile from Prisma to Drizzle
2026-08-08 02:09:03 +02:00

126 lines
4.5 KiB
TypeScript

import { PortfolioSectionType } from "@/lib/db/enums";
import { z } from "zod";
import { mediaFieldInputSchema } from "./media-validation";
const requiredText = (label: string) =>
z
.string()
.trim()
.min(1, `${label} is required.`);
const optionalTrimmedText = z.string().trim().optional().transform((value) => value ?? "");
const portfolioProjectViewModes = ["GRID", "STORY", "CASE_STUDY"] as const;
export const categoryInputSchema = z.object({
id: z.string().trim().optional(),
slug: requiredText("Category slug")
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Category slug must be lowercase and hyphenated."),
nameAr: requiredText("Category nameAr"),
nameEn: requiredText("Category nameEn"),
nameDe: requiredText("Category nameDe"),
descriptionAr: requiredText("Category descriptionAr"),
descriptionEn: requiredText("Category descriptionEn"),
descriptionDe: requiredText("Category descriptionDe"),
sortOrder: z.coerce.number().int().min(0).max(9999),
isActive: z.boolean(),
});
export const sectionInputSchema = z
.object({
id: z.string().trim().optional(),
type: z.nativeEnum(PortfolioSectionType),
titleAr: requiredText("Section titleAr"),
titleEn: requiredText("Section titleEn"),
titleDe: requiredText("Section titleDe"),
bodyAr: optionalTrimmedText,
bodyEn: optionalTrimmedText,
bodyDe: optionalTrimmedText,
imagePath: optionalTrimmedText,
media: mediaFieldInputSchema.optional(),
linkUrl: optionalTrimmedText.refine(
(value) => value === "" || /^https?:\/\//.test(value) || value.startsWith("/"),
"Section linkUrl must be an absolute URL or start with /.",
),
sortOrder: z.coerce.number().int().min(0).max(9999),
})
.superRefine((value, context) => {
const hasBody = Boolean(value.bodyAr && value.bodyEn && value.bodyDe);
const hasImage = value.media?.mode === "library"
? Boolean(value.media.assetId)
: value.media?.mode === "upload"
? true
: Boolean(value.imagePath);
if (
(value.type === PortfolioSectionType.RICH_TEXT ||
value.type === PortfolioSectionType.STATS ||
value.type === PortfolioSectionType.DELIVERABLES) &&
!hasBody
) {
context.addIssue({
code: z.ZodIssueCode.custom,
path: ["bodyAr"],
message: "This section type requires body content in all languages.",
});
}
if (value.type === PortfolioSectionType.GALLERY && !hasImage) {
context.addIssue({
code: z.ZodIssueCode.custom,
path: ["media"],
message: "Single image sections require an image.",
});
}
if (value.type === PortfolioSectionType.LINK && !value.linkUrl) {
context.addIssue({
code: z.ZodIssueCode.custom,
path: ["linkUrl"],
message: "Link sections require a link URL.",
});
}
});
export const assetInputSchema = z.object({
id: z.string().trim().optional(),
kind: z.literal("IMAGE"),
filePath: optionalTrimmedText,
fileFieldName: optionalTrimmedText,
media: mediaFieldInputSchema.optional(),
altAr: requiredText("Asset altAr"),
altEn: requiredText("Asset altEn"),
altDe: requiredText("Asset altDe"),
sortOrder: z.coerce.number().int().min(0).max(9999),
});
export const projectInputSchema = z.object({
id: z.string().trim().optional(),
categoryId: requiredText("Project categoryId"),
slug: requiredText("Project slug")
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Project slug must be lowercase and hyphenated."),
viewMode: z.enum(portfolioProjectViewModes).default("GRID"),
titleAr: requiredText("Project titleAr"),
titleEn: requiredText("Project titleEn"),
titleDe: requiredText("Project titleDe"),
summaryAr: requiredText("Project summaryAr"),
summaryEn: requiredText("Project summaryEn"),
summaryDe: requiredText("Project summaryDe"),
clientName: requiredText("Project clientName"),
projectYear: z.coerce.number().int().min(2000).max(2100),
serviceLabelAr: requiredText("Project serviceLabelAr"),
serviceLabelEn: requiredText("Project serviceLabelEn"),
serviceLabelDe: requiredText("Project serviceLabelDe"),
previewUrl: optionalTrimmedText.refine(
(value) => value === "" || /^https?:\/\//.test(value),
"Project previewUrl must be an absolute URL.",
),
currentCoverImagePath: optionalTrimmedText,
coverMedia: mediaFieldInputSchema.optional(),
sortOrder: z.coerce.number().int().min(0).max(9999),
isFeatured: z.boolean(),
isPublished: z.boolean(),
sections: z.array(sectionInputSchema),
assets: z.array(assetInputSchema),
});