Sistema NEXUS v3.4 migrado con: Estructura principal: - core/orchestration: Sistema SIMCO + CAPVED (27 directivas, 28 perfiles) - core/catalog: Catalogo de funcionalidades reutilizables - shared/knowledge-base: Base de conocimiento compartida - devtools/scripts: Herramientas de desarrollo - control-plane/registries: Control de servicios y CI/CD - orchestration/: Configuracion de orquestacion de agentes Proyectos incluidos (11): - gamilit (submodule -> GitHub) - trading-platform (OrbiquanTIA) - erp-suite con 5 verticales: - erp-core, construccion, vidrio-templado - mecanicas-diesel, retail, clinicas - betting-analytics - inmobiliaria-analytics - platform_marketing_content - pos-micro, erp-basico Configuracion: - .gitignore completo para Node.js/Python/Docker - gamilit como submodule (git@github.com:rckrdmrd/gamilit-workspace.git) - Sistema de puertos estandarizado (3005-3199) Generated with NEXUS v3.4 Migration System EPIC-010: Configuracion Git y Repositorios
715 lines
16 KiB
Markdown
715 lines
16 KiB
Markdown
# ANTIPATRONES: Lo que NUNCA Hacer
|
|
|
|
**Versión:** 1.0.0
|
|
**Fecha:** 2025-12-08
|
|
**Prioridad:** OBLIGATORIA - Consultar antes de implementar
|
|
**Sistema:** SIMCO + CAPVED
|
|
|
|
---
|
|
|
|
## PROPÓSITO
|
|
|
|
Documentar antipatrones comunes para que agentes y subagentes los eviten. Cada antipatrón incluye el problema, por qué es malo, y la solución correcta.
|
|
|
|
---
|
|
|
|
## 1. ANTIPATRONES DE DATABASE
|
|
|
|
### DB-001: Crear tabla sin schema
|
|
|
|
```sql
|
|
-- ❌ INCORRECTO
|
|
CREATE TABLE users (
|
|
id UUID PRIMARY KEY
|
|
);
|
|
|
|
-- ✅ CORRECTO
|
|
CREATE TABLE auth.users (
|
|
id UUID PRIMARY KEY
|
|
);
|
|
```
|
|
|
|
**Por qué es malo:** Sin schema, la tabla va a `public`, mezclando dominios y dificultando permisos.
|
|
|
|
---
|
|
|
|
### DB-002: Columna NOT NULL sin DEFAULT en tabla existente
|
|
|
|
```sql
|
|
-- ❌ INCORRECTO (en tabla con datos)
|
|
ALTER TABLE users ADD COLUMN status VARCHAR(20) NOT NULL;
|
|
|
|
-- ✅ CORRECTO
|
|
ALTER TABLE users ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'active';
|
|
```
|
|
|
|
**Por qué es malo:** Falla en INSERT para registros existentes que no tienen valor.
|
|
|
|
---
|
|
|
|
### DB-003: Foreign Key sin ON DELETE
|
|
|
|
```sql
|
|
-- ❌ INCORRECTO
|
|
CREATE TABLE orders (
|
|
user_id UUID REFERENCES users(id)
|
|
);
|
|
|
|
-- ✅ CORRECTO
|
|
CREATE TABLE orders (
|
|
user_id UUID REFERENCES users(id) ON DELETE CASCADE
|
|
-- o ON DELETE SET NULL, ON DELETE RESTRICT según el caso
|
|
);
|
|
```
|
|
|
|
**Por qué es malo:** Al eliminar usuario, los orders quedan huérfanos o el DELETE falla sin explicación clara.
|
|
|
|
---
|
|
|
|
### DB-004: Usar TEXT cuando debería ser ENUM
|
|
|
|
```sql
|
|
-- ❌ INCORRECTO
|
|
CREATE TABLE users (
|
|
status TEXT -- Permite cualquier valor
|
|
);
|
|
|
|
-- ✅ CORRECTO
|
|
CREATE TYPE user_status AS ENUM ('active', 'inactive', 'suspended');
|
|
CREATE TABLE users (
|
|
status user_status DEFAULT 'active'
|
|
);
|
|
|
|
-- O con CHECK constraint
|
|
CREATE TABLE users (
|
|
status VARCHAR(20) CHECK (status IN ('active', 'inactive', 'suspended'))
|
|
);
|
|
```
|
|
|
|
**Por qué es malo:** TEXT permite valores inválidos, causando bugs silenciosos.
|
|
|
|
---
|
|
|
|
### DB-005: Índice faltante en columna de búsqueda frecuente
|
|
|
|
```sql
|
|
-- ❌ INCORRECTO
|
|
CREATE TABLE products (
|
|
sku VARCHAR(50) UNIQUE -- Sin índice adicional para búsquedas
|
|
);
|
|
|
|
-- ✅ CORRECTO
|
|
CREATE TABLE products (
|
|
sku VARCHAR(50) UNIQUE
|
|
);
|
|
CREATE INDEX idx_products_sku ON products(sku);
|
|
-- UNIQUE ya crea índice, pero para búsquedas parciales:
|
|
CREATE INDEX idx_products_sku_pattern ON products(sku varchar_pattern_ops);
|
|
```
|
|
|
|
**Por qué es malo:** Queries lentas en tablas grandes (full table scan).
|
|
|
|
---
|
|
|
|
### DB-006: Guardar contraseñas en texto plano
|
|
|
|
```sql
|
|
-- ❌ INCORRECTO
|
|
CREATE TABLE users (
|
|
password VARCHAR(100) -- Almacena "mipassword123"
|
|
);
|
|
|
|
-- ✅ CORRECTO
|
|
CREATE TABLE users (
|
|
password_hash VARCHAR(255) -- Almacena hash bcrypt
|
|
);
|
|
-- Hashing se hace en backend, no en SQL
|
|
```
|
|
|
|
**Por qué es malo:** Vulnerabilidad de seguridad crítica.
|
|
|
|
---
|
|
|
|
## 2. ANTIPATRONES DE BACKEND
|
|
|
|
### BE-001: Lógica de negocio en Controller
|
|
|
|
```typescript
|
|
// ❌ INCORRECTO
|
|
@Controller('orders')
|
|
export class OrderController {
|
|
@Post()
|
|
async create(@Body() dto: CreateOrderDto) {
|
|
// Lógica de negocio en controller
|
|
const total = dto.items.reduce((sum, item) => sum + item.price * item.qty, 0);
|
|
const tax = total * 0.16;
|
|
const discount = dto.couponCode ? await this.calculateDiscount() : 0;
|
|
// ... más lógica
|
|
}
|
|
}
|
|
|
|
// ✅ CORRECTO
|
|
@Controller('orders')
|
|
export class OrderController {
|
|
@Post()
|
|
async create(@Body() dto: CreateOrderDto) {
|
|
return this.orderService.create(dto); // Delega a service
|
|
}
|
|
}
|
|
|
|
@Injectable()
|
|
export class OrderService {
|
|
async create(dto: CreateOrderDto) {
|
|
const total = this.calculateTotal(dto.items);
|
|
const tax = this.calculateTax(total);
|
|
// Lógica de negocio en service
|
|
}
|
|
}
|
|
```
|
|
|
|
**Por qué es malo:** Controller difícil de testear, lógica no reutilizable, violación de SRP.
|
|
|
|
---
|
|
|
|
### BE-002: Query directo en Service sin Repository
|
|
|
|
```typescript
|
|
// ❌ INCORRECTO
|
|
@Injectable()
|
|
export class UserService {
|
|
async findActive() {
|
|
// Query directo con QueryBuilder
|
|
return this.connection.query(`
|
|
SELECT * FROM users WHERE status = 'active'
|
|
`);
|
|
}
|
|
}
|
|
|
|
// ✅ CORRECTO
|
|
@Injectable()
|
|
export class UserService {
|
|
constructor(
|
|
@InjectRepository(UserEntity)
|
|
private readonly repository: Repository<UserEntity>,
|
|
) {}
|
|
|
|
async findActive() {
|
|
return this.repository.find({
|
|
where: { status: UserStatus.ACTIVE },
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
**Por qué es malo:** No tipado, vulnerable a SQL injection, difícil de mantener.
|
|
|
|
---
|
|
|
|
### BE-003: Validación en Service en lugar de DTO
|
|
|
|
```typescript
|
|
// ❌ INCORRECTO
|
|
@Injectable()
|
|
export class UserService {
|
|
async create(dto: any) {
|
|
if (!dto.email) throw new BadRequestException('Email required');
|
|
if (!dto.email.includes('@')) throw new BadRequestException('Invalid email');
|
|
if (dto.name.length < 2) throw new BadRequestException('Name too short');
|
|
// ... más validaciones manuales
|
|
}
|
|
}
|
|
|
|
// ✅ CORRECTO
|
|
export class CreateUserDto {
|
|
@IsEmail()
|
|
@IsNotEmpty()
|
|
email: string;
|
|
|
|
@IsString()
|
|
@MinLength(2)
|
|
name: string;
|
|
}
|
|
|
|
// Service recibe DTO ya validado
|
|
```
|
|
|
|
**Por qué es malo:** Validación duplicada, inconsistente, no documentada en Swagger.
|
|
|
|
---
|
|
|
|
### BE-004: Catch vacío o silencioso
|
|
|
|
```typescript
|
|
// ❌ INCORRECTO
|
|
async processPayment() {
|
|
try {
|
|
await this.paymentGateway.charge();
|
|
} catch (e) {
|
|
// Silencioso - error perdido
|
|
}
|
|
}
|
|
|
|
// ❌ TAMBIÉN INCORRECTO
|
|
async processPayment() {
|
|
try {
|
|
await this.paymentGateway.charge();
|
|
} catch (e) {
|
|
console.log(e); // Solo log, sin manejar
|
|
}
|
|
}
|
|
|
|
// ✅ CORRECTO
|
|
async processPayment() {
|
|
try {
|
|
await this.paymentGateway.charge();
|
|
} catch (error) {
|
|
this.logger.error('Payment failed', { error, context: 'payment' });
|
|
throw new InternalServerErrorException('Error procesando pago');
|
|
}
|
|
}
|
|
```
|
|
|
|
**Por qué es malo:** Errores silenciosos causan bugs imposibles de debuggear.
|
|
|
|
---
|
|
|
|
### BE-005: Entity no alineada con DDL
|
|
|
|
```typescript
|
|
// DDL tiene:
|
|
// status VARCHAR(20) CHECK (status IN ('active', 'inactive'))
|
|
|
|
// ❌ INCORRECTO
|
|
@Column()
|
|
status: string; // No valida valores
|
|
|
|
// ✅ CORRECTO
|
|
enum UserStatus {
|
|
ACTIVE = 'active',
|
|
INACTIVE = 'inactive',
|
|
}
|
|
|
|
@Column({ type: 'varchar', length: 20 })
|
|
status: UserStatus;
|
|
```
|
|
|
|
**Por qué es malo:** Entity permite valores que BD rechaza, errores en runtime.
|
|
|
|
---
|
|
|
|
### BE-006: Hardcodear configuración
|
|
|
|
```typescript
|
|
// ❌ INCORRECTO
|
|
const API_URL = 'https://api.stripe.com/v1';
|
|
const DB_HOST = '192.168.1.100';
|
|
|
|
// ✅ CORRECTO
|
|
const API_URL = this.configService.get('STRIPE_API_URL');
|
|
const DB_HOST = this.configService.get('DB_HOST');
|
|
|
|
// O usar decorador
|
|
@Injectable()
|
|
export class PaymentService {
|
|
constructor(
|
|
@Inject('STRIPE_CONFIG')
|
|
private readonly config: StripeConfig,
|
|
) {}
|
|
}
|
|
```
|
|
|
|
**Por qué es malo:** Difícil cambiar entre ambientes, secretos expuestos en código.
|
|
|
|
---
|
|
|
|
### BE-007: N+1 Query Problem
|
|
|
|
```typescript
|
|
// ❌ INCORRECTO
|
|
async getOrdersWithItems() {
|
|
const orders = await this.orderRepository.find();
|
|
for (const order of orders) {
|
|
order.items = await this.itemRepository.find({
|
|
where: { orderId: order.id }
|
|
});
|
|
}
|
|
return orders;
|
|
}
|
|
// Resultado: 1 query para orders + N queries para items
|
|
|
|
// ✅ CORRECTO
|
|
async getOrdersWithItems() {
|
|
return this.orderRepository.find({
|
|
relations: ['items'], // JOIN en una query
|
|
});
|
|
}
|
|
// Resultado: 1 query con JOIN
|
|
```
|
|
|
|
**Por qué es malo:** Performance terrible en listas grandes (100 orders = 101 queries).
|
|
|
|
---
|
|
|
|
## 3. ANTIPATRONES DE FRONTEND
|
|
|
|
### FE-001: Fetch directo en componente
|
|
|
|
```typescript
|
|
// ❌ INCORRECTO
|
|
const UserProfile = () => {
|
|
const [user, setUser] = useState(null);
|
|
|
|
useEffect(() => {
|
|
fetch('/api/users/me')
|
|
.then(res => res.json())
|
|
.then(setUser);
|
|
}, []);
|
|
|
|
return <div>{user?.name}</div>;
|
|
};
|
|
|
|
// ✅ CORRECTO
|
|
// hooks/useUser.ts
|
|
const useUser = () => {
|
|
return useQuery({
|
|
queryKey: ['user', 'me'],
|
|
queryFn: () => userService.getMe(),
|
|
});
|
|
};
|
|
|
|
// components/UserProfile.tsx
|
|
const UserProfile = () => {
|
|
const { data: user, isLoading, error } = useUser();
|
|
|
|
if (isLoading) return <Loading />;
|
|
if (error) return <Error error={error} />;
|
|
|
|
return <div>{user.name}</div>;
|
|
};
|
|
```
|
|
|
|
**Por qué es malo:** No cachea, no maneja loading/error, lógica no reutilizable.
|
|
|
|
---
|
|
|
|
### FE-002: Hardcodear URLs de API
|
|
|
|
```typescript
|
|
// ❌ INCORRECTO
|
|
const response = await fetch('http://localhost:3000/api/users');
|
|
|
|
// ✅ CORRECTO
|
|
// config.ts
|
|
export const API_URL = import.meta.env.VITE_API_URL;
|
|
|
|
// services/api.ts
|
|
const api = axios.create({
|
|
baseURL: API_URL,
|
|
});
|
|
```
|
|
|
|
**Por qué es malo:** Rompe en producción, difícil cambiar backend.
|
|
|
|
---
|
|
|
|
### FE-003: Estado global para todo
|
|
|
|
```typescript
|
|
// ❌ INCORRECTO - Redux/Zustand para estado de formulario
|
|
const formStore = create((set) => ({
|
|
name: '',
|
|
email: '',
|
|
setName: (name) => set({ name }),
|
|
setEmail: (email) => set({ email }),
|
|
}));
|
|
|
|
// ✅ CORRECTO - Estado local para formularios
|
|
const UserForm = () => {
|
|
const [name, setName] = useState('');
|
|
const [email, setEmail] = useState('');
|
|
// O usar react-hook-form
|
|
};
|
|
|
|
// Store global SOLO para:
|
|
// - Auth state (usuario logueado)
|
|
// - UI state (tema, sidebar abierto)
|
|
// - Cache de server state (mejor usar React Query)
|
|
```
|
|
|
|
**Por qué es malo:** Complejidad innecesaria, renders innecesarios, difícil de seguir.
|
|
|
|
---
|
|
|
|
### FE-004: Props drilling excesivo
|
|
|
|
```typescript
|
|
// ❌ INCORRECTO
|
|
<App user={user} theme={theme}>
|
|
<Layout user={user} theme={theme}>
|
|
<Sidebar user={user} theme={theme}>
|
|
<UserMenu user={user} theme={theme}>
|
|
<Avatar user={user} /> // Finalmente se usa
|
|
</UserMenu>
|
|
</Sidebar>
|
|
</Layout>
|
|
</App>
|
|
|
|
// ✅ CORRECTO - Context para datos compartidos
|
|
const UserContext = createContext<User | null>(null);
|
|
const useUser = () => useContext(UserContext);
|
|
|
|
<UserProvider value={user}>
|
|
<App>
|
|
<Layout>
|
|
<Sidebar>
|
|
<UserMenu>
|
|
<Avatar /> // Usa useUser() internamente
|
|
</UserMenu>
|
|
</Sidebar>
|
|
</Layout>
|
|
</App>
|
|
</UserProvider>
|
|
```
|
|
|
|
**Por qué es malo:** Componentes intermedios reciben props que no usan, refactoring doloroso.
|
|
|
|
---
|
|
|
|
### FE-005: useEffect para todo
|
|
|
|
```typescript
|
|
// ❌ INCORRECTO
|
|
const ProductList = ({ categoryId }) => {
|
|
const [products, setProducts] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
setLoading(true);
|
|
fetch(`/api/products?category=${categoryId}`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
setProducts(data);
|
|
setLoading(false);
|
|
});
|
|
}, [categoryId]);
|
|
// Race conditions, no cleanup, no error handling
|
|
};
|
|
|
|
// ✅ CORRECTO - React Query
|
|
const ProductList = ({ categoryId }) => {
|
|
const { data: products, isLoading } = useQuery({
|
|
queryKey: ['products', categoryId],
|
|
queryFn: () => productService.getByCategory(categoryId),
|
|
});
|
|
// Maneja cache, loading, error, race conditions automáticamente
|
|
};
|
|
```
|
|
|
|
**Por qué es malo:** Race conditions, memory leaks, re-inventar la rueda.
|
|
|
|
---
|
|
|
|
### FE-006: Tipos no sincronizados con backend
|
|
|
|
```typescript
|
|
// Backend DTO (actualizado)
|
|
class UserDto {
|
|
id: string;
|
|
email: string;
|
|
name: string;
|
|
phone: string; // ← NUEVO CAMPO
|
|
}
|
|
|
|
// ❌ INCORRECTO - Frontend desactualizado
|
|
interface User {
|
|
id: string;
|
|
email: string;
|
|
name: string;
|
|
// Falta phone → undefined en runtime
|
|
}
|
|
|
|
// ✅ CORRECTO - Mantener sincronizado
|
|
interface User {
|
|
id: string;
|
|
email: string;
|
|
name: string;
|
|
phone?: string; // Sincronizado con backend
|
|
}
|
|
```
|
|
|
|
**Por qué es malo:** Bugs silenciosos en runtime cuando backend cambia.
|
|
|
|
---
|
|
|
|
## 4. ANTIPATRONES DE ARQUITECTURA
|
|
|
|
### ARCH-001: Importar de capa incorrecta
|
|
|
|
```typescript
|
|
// ❌ INCORRECTO - Controller importa de otro módulo directamente
|
|
import { ProductEntity } from '../products/entities/product.entity';
|
|
|
|
// ✅ CORRECTO - Usar exports del módulo
|
|
import { ProductService } from '../products/product.module';
|
|
|
|
// O mejor: dependency injection
|
|
@Module({
|
|
imports: [ProductModule],
|
|
})
|
|
export class OrderModule {}
|
|
```
|
|
|
|
**Por qué es malo:** Acopla módulos, rompe encapsulamiento, dependencias circulares.
|
|
|
|
---
|
|
|
|
### ARCH-002: Duplicar código entre módulos
|
|
|
|
```typescript
|
|
// ❌ INCORRECTO - Misma función en dos módulos
|
|
// users/utils.ts
|
|
function formatDate(date: Date) { ... }
|
|
|
|
// orders/utils.ts
|
|
function formatDate(date: Date) { ... } // Duplicado
|
|
|
|
// ✅ CORRECTO - Shared utils
|
|
// shared/utils/date.utils.ts
|
|
export function formatDate(date: Date) { ... }
|
|
```
|
|
|
|
**Por qué es malo:** Cambios deben hacerse en múltiples lugares, inconsistencias.
|
|
|
|
---
|
|
|
|
### ARCH-003: Circular dependencies
|
|
|
|
```typescript
|
|
// ❌ INCORRECTO
|
|
// user.service.ts
|
|
import { OrderService } from '../orders/order.service';
|
|
|
|
// order.service.ts
|
|
import { UserService } from '../users/user.service';
|
|
|
|
// ✅ CORRECTO - Usar forwardRef o reestructurar
|
|
@Injectable()
|
|
export class UserService {
|
|
constructor(
|
|
@Inject(forwardRef(() => OrderService))
|
|
private orderService: OrderService,
|
|
) {}
|
|
}
|
|
|
|
// O mejor: Extraer lógica compartida a un tercer servicio
|
|
```
|
|
|
|
**Por qué es malo:** Errores en runtime, código difícil de entender.
|
|
|
|
---
|
|
|
|
## 5. ANTIPATRONES DE TESTING
|
|
|
|
### TEST-001: Tests que dependen de orden
|
|
|
|
```typescript
|
|
// ❌ INCORRECTO
|
|
describe('UserService', () => {
|
|
it('should create user', () => {
|
|
const user = service.create({ email: 'test@test.com' });
|
|
// Asume que este test corre primero
|
|
});
|
|
|
|
it('should find user', () => {
|
|
const user = service.findByEmail('test@test.com');
|
|
// Depende del test anterior
|
|
});
|
|
});
|
|
|
|
// ✅ CORRECTO - Tests independientes
|
|
describe('UserService', () => {
|
|
beforeEach(async () => {
|
|
// Setup limpio para cada test
|
|
await repository.clear();
|
|
});
|
|
|
|
it('should create user', () => { ... });
|
|
|
|
it('should find user', async () => {
|
|
// Crear datos necesarios en el test
|
|
await repository.save({ email: 'test@test.com' });
|
|
const user = await service.findByEmail('test@test.com');
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
### TEST-002: Mock incorrecto
|
|
|
|
```typescript
|
|
// ❌ INCORRECTO - Mock parcial/inconsistente
|
|
jest.mock('./user.service', () => ({
|
|
findOne: jest.fn().mockResolvedValue({ id: '1' }),
|
|
// Otros métodos no mockeados → undefined
|
|
}));
|
|
|
|
// ✅ CORRECTO - Mock completo
|
|
const mockUserService = {
|
|
findOne: jest.fn(),
|
|
findAll: jest.fn(),
|
|
create: jest.fn(),
|
|
update: jest.fn(),
|
|
delete: jest.fn(),
|
|
};
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 6. CHECKLIST DE REVISIÓN
|
|
|
|
Antes de hacer commit, verificar que NO estás haciendo:
|
|
|
|
```
|
|
Database:
|
|
[ ] Tabla sin schema
|
|
[ ] NOT NULL sin DEFAULT en tabla existente
|
|
[ ] FK sin ON DELETE
|
|
[ ] TEXT donde debería ser ENUM
|
|
[ ] Índices faltantes
|
|
|
|
Backend:
|
|
[ ] Lógica en Controller
|
|
[ ] Queries directas sin Repository
|
|
[ ] Validación en Service
|
|
[ ] Catch vacío
|
|
[ ] Entity desalineada con DDL
|
|
[ ] Config hardcodeada
|
|
[ ] N+1 queries
|
|
|
|
Frontend:
|
|
[ ] Fetch en componente
|
|
[ ] URLs hardcodeadas
|
|
[ ] Store global para estado local
|
|
[ ] Props drilling excesivo
|
|
[ ] useEffect innecesario
|
|
[ ] Tipos desactualizados
|
|
|
|
Arquitectura:
|
|
[ ] Import de capa incorrecta
|
|
[ ] Código duplicado
|
|
[ ] Dependencias circulares
|
|
|
|
Testing:
|
|
[ ] Tests dependientes
|
|
[ ] Mocks incompletos
|
|
```
|
|
|
|
---
|
|
|
|
**Versión:** 1.0.0 | **Sistema:** SIMCO | **Tipo:** Guía de Antipatrones
|