382 lines
9.7 KiB
Plaintext
382 lines
9.7 KiB
Plaintext
|
|
# 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<T>` (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<string, unknown>` 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<T>` (usar `readonly T[]`)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📚 PATRONES RECOMENDADOS
|
||
|
|
|
||
|
|
### Repository Pattern
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
interface IContentRepository {
|
||
|
|
findById(id: string): Promise<ContentItem | null>;
|
||
|
|
findAll(filters: ContentFilters): Promise<{ items: ContentItem[]; total: number }>;
|
||
|
|
save(content: ContentItem): Promise<ContentItem>;
|
||
|
|
delete(id: string): Promise<void>;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Service Layer
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
class ContentService {
|
||
|
|
constructor(
|
||
|
|
private readonly repository: IContentRepository,
|
||
|
|
private readonly mapper: ContentItemMapper
|
||
|
|
) {}
|
||
|
|
|
||
|
|
async createContent(input: CreateContentDTO): Promise<ContentItem> {
|
||
|
|
// 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<ContentItem> {
|
||
|
|
// 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**
|