Files
website/apps/api/src/routes/brands.ts
T

179 lines
5.8 KiB
TypeScript

import type { FastifyPluginAsync } from "fastify";
import { VerificationStatus } from "@prisma/client";
import { z } from "zod";
import { slugify } from "../lib/slug.js";
const contactSchema = z.object({
type: z.enum([
"phone",
"landline",
"email",
"instagram",
"website",
"whatsapp",
"other",
]),
value: z.string(),
});
const createSchema = z.object({
title: z.string().min(1),
slug: z.string().min(1).optional(),
abstract: z.string().optional(),
content: z.string().optional(),
imageUrl: z.string().url().nullable().optional(),
galleryUrls: z.array(z.string().url()).optional(),
tags: z.array(z.string()).optional(),
country: z.string().min(1),
city: z.string().optional(),
address: z.string().optional(),
contacts: z.array(contactSchema).optional(),
categoryIds: z.array(z.string().cuid()).optional(),
authorId: z.string().cuid(),
verificationStatus: z.nativeEnum(VerificationStatus).optional(),
publishedAt: z.coerce.date().nullable().optional(),
});
function assertNoContentImages(content: string | undefined, reply: {
status: (code: number) => { send: (body: unknown) => unknown };
}) {
if (content && /<img\b/i.test(content)) {
return reply.status(400).send({
error: "Brand main text cannot include images. Use the gallery instead.",
});
}
return null;
}
export const brandRoutes: FastifyPluginAsync = async (app) => {
app.get("/", async () => {
return app.prisma.brand.findMany({
orderBy: { createdAt: "desc" },
include: {
author: { select: { id: true, email: true, name: true } },
categories: { include: { category: true } },
},
});
});
app.get("/:id", async (request, reply) => {
const { id } = request.params as { id: string };
const brand = await app.prisma.brand.findUnique({
where: { id },
include: {
author: { select: { id: true, email: true, name: true } },
categories: { include: { category: true } },
},
});
if (!brand) {
return reply.status(404).send({ error: "Brand not found" });
}
return brand;
});
app.post("/", async (request, reply) => {
const parsed = createSchema.safeParse(request.body);
if (!parsed.success) {
return reply.status(400).send({ error: parsed.error.flatten() });
}
const contentBlocked = assertNoContentImages(parsed.data.content, reply);
if (contentBlocked) return contentBlocked;
const slug = slugify(parsed.data.slug ?? parsed.data.title);
const categoryIds = parsed.data.categoryIds ?? [];
const galleryUrls = parsed.data.galleryUrls ?? [];
const brand = await app.prisma.brand.create({
data: {
title: parsed.data.title,
slug,
abstract: parsed.data.abstract,
content: parsed.data.content ?? "",
imageUrl: parsed.data.imageUrl ?? null,
galleryUrls,
tags: parsed.data.tags ?? [],
country: parsed.data.country,
city: parsed.data.city,
address: parsed.data.address,
contacts: parsed.data.contacts ?? [],
authorId: parsed.data.authorId,
verificationStatus:
parsed.data.verificationStatus ?? VerificationStatus.PENDING,
publishedAt: parsed.data.publishedAt ?? null,
categories: {
create: categoryIds.map((categoryId) => ({ categoryId })),
},
},
include: {
author: { select: { id: true, email: true, name: true } },
categories: { include: { category: true } },
},
});
return reply.status(201).send(brand);
});
app.patch("/:id", async (request, reply) => {
const { id } = request.params as { id: string };
const updateSchema = createSchema.partial().omit({ authorId: true });
const parsed = updateSchema.safeParse(request.body);
if (!parsed.success) {
return reply.status(400).send({ error: parsed.error.flatten() });
}
const contentBlocked = assertNoContentImages(parsed.data.content, reply);
if (contentBlocked) return contentBlocked;
const existing = await app.prisma.brand.findUnique({ where: { id } });
if (!existing) {
return reply.status(404).send({ error: "Brand not found" });
}
const data = parsed.data;
const categoryIds = data.categoryIds;
const galleryUrls = data.galleryUrls;
const brand = await app.prisma.brand.update({
where: { id },
data: {
...(data.title !== undefined ? { title: data.title } : {}),
...(data.title !== undefined || data.slug !== undefined
? { slug: slugify(data.slug ?? data.title ?? existing.title) }
: {}),
...(data.abstract !== undefined ? { abstract: data.abstract } : {}),
...(data.content !== undefined ? { content: data.content } : {}),
...(data.imageUrl !== undefined ? { imageUrl: data.imageUrl } : {}),
...(galleryUrls !== undefined ? { galleryUrls } : {}),
...(data.tags !== undefined ? { tags: data.tags } : {}),
...(data.country !== undefined ? { country: data.country } : {}),
...(data.city !== undefined ? { city: data.city } : {}),
...(data.address !== undefined ? { address: data.address } : {}),
...(data.contacts !== undefined ? { contacts: data.contacts } : {}),
...(data.verificationStatus !== undefined
? { verificationStatus: data.verificationStatus }
: {}),
...(data.publishedAt !== undefined
? { publishedAt: data.publishedAt }
: {}),
...(categoryIds
? {
categories: {
deleteMany: {},
create: categoryIds.map((categoryId) => ({ categoryId })),
},
}
: {}),
},
include: {
author: { select: { id: true, email: true, name: true } },
categories: { include: { category: true } },
},
});
return brand;
});
};