feat(ADM-018): completed feature

This commit is contained in:
chattie
2026-08-17 22:23:10 +02:00
parent cf1c69fc8b
commit d595b4871f
871 changed files with 47411 additions and 281 deletions

View File

@@ -0,0 +1,127 @@
import type { FastifyInstance } 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 { 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);
app.get('/brands', async (_request, reply) => {
const items = await listBrands.execute();
return reply.send({ items: items.map(serializeBrand) });
});
app.get('/marca/:slug', 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));
});
app.post('/brands', 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);
}
});
app.patch('/brands/:id', 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
app.delete('/brands/:id', 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(),
};
}