# Cursor Rules - EMERGES TES ## Arquitectura Clean Architecture + TypeScript + PostgreSQL **Última actualización:** 2025-01-25 **Versión:** 2.0 --- ## 🎯 DECISIONES TÉCNICAS CONSOLIDADAS ### 1. Value Objects (Híbrido) - ✅ Entidades usan **tipos simples** (`ContentStatusType`, `ContentPriorityType`) - ✅ Value Objects (`ContentStatus`, `ContentPriority`) se usan en **servicios** para validación - ✅ Domain Layer mantiene entidades como POJOs inmutables ### 2. Serialización (Mappers) - ✅ **Mappers separados** en `infrastructure/mappers/` - ✅ Domain Layer NO tiene métodos `toJSON`/`fromJSON` - ✅ Mappers convierten entre Domain ↔ Persistence ### 3. IDs (Application Layer) - ✅ UUIDs generados en **Application Layer** (Use Cases) - ✅ Pasados como parámetro a métodos `create` de entidades - ✅ Permite inyección de IDs en tests ### 4. Validación (Híbrido) - ✅ **Validaciones básicas** en métodos `create` de entidades (formato, longitud) - ✅ **Validaciones complejas** en Application Services (unicidad, dependencias) - ✅ **Zod** en Application Layer para validar esquemas de entrada ### 5. Fechas (ISO 8601 Strings) - ✅ Usar `string` con formato ISO 8601: `"2025-01-25T10:00:00Z"` - ✅ NO usar `Date` nativo en entidades - ✅ Mappers convierten strings ↔ PostgreSQL TIMESTAMPTZ ### 6. Arrays (readonly T[]) - ✅ Usar `readonly string[]` para arrays inmutables - ✅ NO usar `ReadonlyArray` (más verboso) - ✅ Mantener inmutabilidad por defecto ### 7. Opcionales (Híbrido) - ✅ `?` para campos opcionales: `readonly description?: string` - ✅ `| null` cuando null tiene significado: `readonly validatedAt: string | null` - ✅ Distinguir entre "no proporcionado" vs "explícitamente null" ### 8. Errores (Personalizados) - ✅ Usar `DomainError`, `ValidationError`, `BusinessRuleError` - ✅ NO usar errores genéricos de JavaScript - ✅ Errores con código y contexto ### 9. Versionado (Números Enteros) - ✅ `version: number` y `latestVersion: number` - ✅ NO usar semantic versioning (`"1.0.0"`) - ✅ Incrementales simples para comparación fácil ### 10. JSONB (Union Types) - ✅ Union types para `content`: `ProtocolContent | GuideContent | ManualContent` - ✅ NO usar `Record` genérico - ✅ Type safety completo con narrowing automático --- ## 📐 ARQUITECTURA ### Estructura de Capas ``` domain/ → Entidades, Value Objects, Repository Interfaces application/ → Services, Use Cases, DTOs infrastructure/ → Repository Implementations, Mappers, Database presentation/ → Routes, Middleware, Validators (Zod) shared/ → Types, Errors, Utils ``` ### Reglas de Dependencias - ✅ Domain: NO depende de nadie - ✅ Application: Solo depende de Domain - ✅ Infrastructure: Depende de Domain y Application - ✅ Presentation: Depende de Application y Domain --- ## 🔒 REGLAS DE CÓDIGO ### TypeScript - ✅ Usar tipos explícitos, evitar `any` - ✅ Usar `readonly` para propiedades inmutables - ✅ Preferir `interface` sobre `type` para objetos extensibles - ✅ Usar `type` para unions, intersections, primitives - ✅ NO usar `@ts-ignore` sin comentario explicativo ### Entidades de Dominio - ✅ Todas las propiedades `readonly` - ✅ Tipos simples (no clases) en interfaces - ✅ Métodos estáticos `create()` para construcción - ✅ Validaciones básicas en `create()` - ✅ IDs recibidos como parámetro (no generados internamente) ```typescript // ✅ CORRECTO interface ContentItem { readonly id: string; readonly title: string; readonly status: ContentStatusType; // Tipo simple readonly createdAt: string; // ISO 8601 readonly tags: readonly string[]; // Array inmutable } static create( id: string, // ID inyectado title: string, // ... ): ContentItem { // Validación básica if (!title || title.trim().length === 0) { throw new ValidationError('Título es obligatorio'); } return { id, title: title.trim(), ... }; } ``` ### Value Objects - ✅ Clases con constructor privado - ✅ Métodos estáticos `fromString()`, `create()` - ✅ Métodos `toString()`, `equals()`, `canTransitionTo()` - ✅ Usados en Services, NO en entidades ```typescript // ✅ CORRECTO export class ContentStatus { private constructor(private readonly value: string) {} static fromString(value: string): ContentStatus { // Validación return new ContentStatus(value); } toString(): string { return this.value; } } ``` ### Mappers - ✅ En `infrastructure/mappers/` - ✅ Métodos estáticos `toDomain()` y `toPersistence()` - ✅ Conversión de tipos (string ↔ Date, snake_case ↔ camelCase) ```typescript // ✅ CORRECTO class ContentItemMapper { static toDomain(row: any): ContentItem { return { id: row.id, status: row.status as ContentStatusType, createdAt: row.created_at, // Ya es string ISO // ... }; } static toPersistence(item: ContentItem): any { return { id: item.id, status: item.status, created_at: item.createdAt, // String ISO // ... }; } } ``` ### Validación - ✅ Zod en Application Layer para esquemas de entrada - ✅ Validaciones básicas en Domain (`create()`) - ✅ Validaciones complejas en Application Services - ✅ Mensajes de error claros y específicos ### Errores - ✅ Usar `DomainError`, `ValidationError`, `BusinessRuleError` - ✅ Incluir código y contexto - ✅ NO silenciar errores con `catch` vacío ```typescript // ✅ CORRECTO throw new ValidationError('Título es obligatorio', { field: 'title', value: title }); throw new BusinessRuleError('Slug ya existe', { slug, existingId: existing.id }); ``` ### Funciones - ✅ Máximo 20-30 líneas - ✅ Una sola responsabilidad - ✅ Nombres descriptivos - ✅ Parámetros máximo 3-4, usar objetos si hay más ### Base de Datos - ✅ Usar parámetros preparados (nunca concatenar SQL) - ✅ Validar datos antes de insertar/actualizar - ✅ Usar transacciones para operaciones múltiples - ✅ Índices apropiados para queries frecuentes --- ## 📁 CONVENCIONES DE ARCHIVOS ### Nomenclatura - **Archivos TypeScript:** `kebab-case.ts` o `PascalCase.ts` para componentes - **Carpetas:** `kebab-case` - **Tipos/Interfaces:** `PascalCase` - **Funciones:** `camelCase` - **Constantes:** `UPPER_SNAKE_CASE` ### Estructura ``` domain/entities/ └── ContentItem.ts # Entidad de dominio application/services/ └── ContentService.ts # Servicio de aplicación infrastructure/repositories/ └── ContentRepository.ts # Repositorio de infraestructura infrastructure/mappers/ └── ContentItemMapper.ts # Mapper de infraestructura presentation/routes/ └── content.ts # Rutas Express ``` --- ## 🧪 TESTING ### Estructura - ✅ Tests unitarios en `tests/unit/` - ✅ Tests de integración en `tests/integration/` - ✅ Tests de API en `tests/api/` - ✅ Mocks en `tests/mocks/` - ✅ Fixtures en `tests/fixtures/` ### Buenas Prácticas - ✅ Un test por caso de uso - ✅ Tests independientes - ✅ Usar mocks para dependencias externas - ✅ Arrange-Act-Assert claro - ✅ Cobertura mínima: 80% --- ## 🚫 ANTI-PATRONES A EVITAR ### ❌ NO hacer: - Funciones de más de 30 líneas - Mutar objetos directamente (usar inmutabilidad) - Validar solo en frontend - Usar `any` en TypeScript - Código duplicado (DRY) - Imports no usados - Código comentado (eliminar o documentar) - Catch vacío sin logging - SQL concatenado (usar parámetros) - Tests que dependen de otros tests - Métodos `toJSON`/`fromJSON` en entidades - Generar IDs dentro de entidades - Usar `Date` nativo en entidades - Usar `ReadonlyArray` (usar `readonly T[]`) --- ## 📚 PATRONES RECOMENDADOS ### Repository Pattern ```typescript interface IContentRepository { findById(id: string): Promise; findAll(filters: ContentFilters): Promise<{ items: ContentItem[]; total: number }>; save(content: ContentItem): Promise; delete(id: string): Promise; } ``` ### Service Layer ```typescript class ContentService { constructor( private readonly repository: IContentRepository, private readonly mapper: ContentItemMapper ) {} async createContent(input: CreateContentDTO): Promise { // 1. Validar con Zod const validated = createContentSchema.parse(input); // 2. Validaciones complejas if (await this.repository.existsBySlug(validated.slug)) { throw new BusinessRuleError('Slug ya existe'); } // 3. Generar ID const id = randomUUID(); // 4. Crear entidad (validaciones básicas aquí) const content = ContentItem.create(id, validated.title, ...); // 5. Persistir return await this.repository.save(content); } } ``` ### Use Case Pattern ```typescript class CreateContentUseCase { constructor( private readonly repository: IContentRepository, private readonly validator: ContentValidator ) {} async execute(input: CreateContentDTO): Promise { // Orquestación del caso de uso } } ``` --- ## 🔍 CODE REVIEW CHECKLIST Antes de hacer commit, verificar: - [ ] Funciones <30 líneas - [ ] Validación con Zod en todos los inputs - [ ] Tests escritos y pasando - [ ] Sin código duplicado - [ ] Sin imports no usados - [ ] Sin `any` en TypeScript - [ ] Entidades inmutables (`readonly`) - [ ] Manejo de errores apropiado - [ ] Logs de auditoría donde corresponda - [ ] Documentación de funciones complejas - [ ] IDs generados en Application Layer - [ ] Fechas como strings ISO 8601 - [ ] Arrays como `readonly T[]` - [ ] Mappers separados en Infrastructure --- ## 📖 REFERENCIAS - SPEC.md: Arquitectura completa del proyecto - Clean Architecture: Robert C. Martin - Domain-Driven Design: Eric Evans - Zod: https://zod.dev --- **Fin de Cursor Rules**