167 lines
5.4 KiB
TypeScript
167 lines
5.4 KiB
TypeScript
import type { FastifyInstance } from 'fastify';
|
|
import type { FastifySchema } from 'fastify';
|
|
import type pg from 'pg';
|
|
import { z } from 'zod';
|
|
import { requireRole, type Authenticate } from '../../../shared/auth.js';
|
|
import { AppError } from '../../../shared/errors.js';
|
|
import { errorSchema } from '../../../shared/swagger.js';
|
|
import { parseJson } from '../../../shared/http-input.js';
|
|
import {
|
|
CreateBrand,
|
|
GetBrandBySlug,
|
|
ListBrands,
|
|
UpdateBrand,
|
|
} from '../application/brand-use-cases.js';
|
|
import type { Brand } from '../domain/brand.js';
|
|
import { BrandSlugAlreadyExistsError } from '../domain/errors.js';
|
|
import { PgBrandRepository } from '../infrastructure/pg-brand-repository.js';
|
|
|
|
export interface BrandsRoutesDeps {
|
|
pool: pg.Pool;
|
|
authenticate: Authenticate;
|
|
}
|
|
|
|
const slugSchema = z
|
|
.string()
|
|
.min(1)
|
|
.max(160)
|
|
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/);
|
|
const slugParamSchema = z.object({ slug: slugSchema });
|
|
const idParamSchema = z.object({ id: z.uuid() });
|
|
|
|
const newBrandSchema = z.object({
|
|
name: z.string().min(1).max(200),
|
|
slug: slugSchema,
|
|
seoTitle: z.string().min(1).max(200).optional().nullable(),
|
|
seoDescription: z.string().min(1).max(500).optional().nullable(),
|
|
});
|
|
|
|
const brandPatchSchema = newBrandSchema
|
|
.partial()
|
|
.refine((value) => Object.values(value).some((field) => field !== undefined), {
|
|
message: 'At least one brand field is required',
|
|
});
|
|
|
|
export async function registerBrandsRoutes(
|
|
app: FastifyInstance,
|
|
deps: BrandsRoutesDeps,
|
|
): Promise<void> {
|
|
const repository = new PgBrandRepository(deps.pool);
|
|
const getBySlug = new GetBrandBySlug(repository);
|
|
const listBrands = new ListBrands(repository);
|
|
const createBrand = new CreateBrand(repository);
|
|
const updateBrand = new UpdateBrand(repository);
|
|
|
|
const listBrandsSchema: FastifySchema = {
|
|
tags: ['Brands'],
|
|
summary: 'List brands (público)',
|
|
};
|
|
app.get('/brands', { schema: listBrandsSchema }, async (_request, reply) => {
|
|
const items = await listBrands.execute();
|
|
return reply.send({ items: items.map(serializeBrand) });
|
|
});
|
|
|
|
const publicBrandSchema: FastifySchema = {
|
|
tags: ['Brands'],
|
|
summary: 'Get brand by slug (público)',
|
|
params: { type: 'object', required: ['slug'], properties: { slug: { type: 'string' } } },
|
|
response: { 404: errorSchema },
|
|
};
|
|
app.get('/marca/:slug', { schema: publicBrandSchema }, async (request, reply) => {
|
|
const { slug } = parseJson(slugParamSchema, request.params);
|
|
const brand = await getBySlug.execute(slug);
|
|
if (!brand) {
|
|
throw new AppError(404, 'NOT_FOUND', 'Brand not found');
|
|
}
|
|
return reply.send(serializeBrand(brand));
|
|
});
|
|
|
|
const createBrandSchema: FastifySchema = {
|
|
tags: ['Brands'],
|
|
summary: 'Create brand (admin)',
|
|
body: { type: 'object' },
|
|
response: { 201: { type: 'object' }, 401: errorSchema, 403: errorSchema },
|
|
};
|
|
app.post('/brands', { schema: createBrandSchema }, async (request, reply) => {
|
|
const user = await deps.authenticate(request);
|
|
requireRole(user, 'admin');
|
|
const input = parseJson(newBrandSchema, request.body);
|
|
try {
|
|
const brand = await createBrand.execute(input);
|
|
return reply.code(201).send(serializeBrand(brand));
|
|
} catch (error) {
|
|
throw mapBrandError(error);
|
|
}
|
|
});
|
|
|
|
const updateBrandSchema: FastifySchema = {
|
|
tags: ['Brands'],
|
|
summary: 'Update brand (admin)',
|
|
params: {
|
|
type: 'object',
|
|
required: ['id'],
|
|
properties: { id: { type: 'string', format: 'uuid' } },
|
|
},
|
|
body: { type: 'object' },
|
|
response: { 401: errorSchema, 403: errorSchema, 404: errorSchema },
|
|
};
|
|
app.patch('/brands/:id', { schema: updateBrandSchema }, async (request, reply) => {
|
|
const user = await deps.authenticate(request);
|
|
requireRole(user, 'admin');
|
|
const { id } = parseJson(idParamSchema, request.params);
|
|
const patch = parseJson(brandPatchSchema, request.body);
|
|
try {
|
|
const brand = await updateBrand.execute(id, patch);
|
|
if (!brand) {
|
|
throw new AppError(404, 'NOT_FOUND', 'Brand not found');
|
|
}
|
|
return reply.send(serializeBrand(brand));
|
|
} catch (error) {
|
|
throw mapBrandError(error);
|
|
}
|
|
});
|
|
|
|
// DELETE /brands/:id
|
|
const deleteBrandSchema: FastifySchema = {
|
|
tags: ['Brands'],
|
|
summary: 'Delete brand (admin)',
|
|
params: {
|
|
type: 'object',
|
|
required: ['id'],
|
|
properties: { id: { type: 'string', format: 'uuid' } },
|
|
},
|
|
response: { 204: { type: 'null' }, 401: errorSchema, 403: errorSchema },
|
|
};
|
|
app.delete('/brands/:id', { schema: deleteBrandSchema }, async (request, reply) => {
|
|
const user = await deps.authenticate(request);
|
|
requireRole(user, 'admin');
|
|
const { id } = parseJson(idParamSchema, request.params);
|
|
try {
|
|
await repository.delete(id);
|
|
} catch {
|
|
throw new AppError(404, 'NOT_FOUND', 'Brand not found');
|
|
}
|
|
return reply.code(204).send();
|
|
});
|
|
}
|
|
|
|
function mapBrandError(error: unknown): Error {
|
|
if (error instanceof BrandSlugAlreadyExistsError) {
|
|
return new AppError(409, 'BRAND_SLUG_EXISTS', error.message);
|
|
}
|
|
return error instanceof Error ? error : new Error('Unknown brand error');
|
|
}
|
|
|
|
function serializeBrand(brand: Brand) {
|
|
return {
|
|
id: brand.id,
|
|
name: brand.name,
|
|
slug: brand.slug,
|
|
url: `/marca/${brand.slug}`,
|
|
seoTitle: brand.seoTitle,
|
|
seoDescription: brand.seoDescription,
|
|
createdAt: brand.createdAt.toISOString(),
|
|
updatedAt: brand.updatedAt.toISOString(),
|
|
};
|
|
}
|