arckode-framework 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +546 -0
- package/adapters/__tests__/mysql.test.ts +283 -0
- package/adapters/jwt.ts +18 -0
- package/adapters/mysql.ts +98 -0
- package/adapters/postgres.ts +52 -0
- package/adapters/redis-cache.ts +64 -0
- package/adapters/sqlite.ts +73 -0
- package/adapters/vendor.d.ts +48 -0
- package/bin/arckode.js +7 -0
- package/cli/analyze.ts +506 -0
- package/cli/commands/db-migrate.ts +121 -0
- package/cli/commands/db-seed.ts +54 -0
- package/cli/commands/generate-api-client.ts +106 -0
- package/cli/commands/make-adapter.ts +132 -0
- package/cli/commands/make-auth.ts +297 -0
- package/cli/commands/make-frontend-module.ts +271 -0
- package/cli/commands/make-helper.ts +65 -0
- package/cli/commands/make-migration.ts +30 -0
- package/cli/commands/make-seed.ts +29 -0
- package/cli/generate.ts +132 -0
- package/cli/index.ts +604 -0
- package/cli/stubs/frontend-stub.ts +294 -0
- package/cli/stubs/fullstack-stub.ts +46 -0
- package/cli/stubs/module-stub.ts +469 -0
- package/kernel/__tests__/adapters.test.ts +101 -0
- package/kernel/__tests__/analyzer.test.ts +282 -0
- package/kernel/__tests__/framework.test.ts +617 -0
- package/kernel/__tests__/middlewares.test.ts +174 -0
- package/kernel/__tests__/static.test.ts +94 -0
- package/kernel/framework.ts +1851 -0
- package/kernel/middlewares.ts +179 -0
- package/kernel/static.ts +76 -0
- package/kernel/testing.ts +237 -0
- package/modules/events/index.ts +99 -0
- package/modules/mail/index.ts +51 -0
- package/modules/mail/smtp-adapter.ts +42 -0
- package/modules/queue/index.ts +78 -0
- package/modules/storage/index.ts +40 -0
- package/modules/storage/local-adapter.ts +41 -0
- package/modules/ws/__tests__/ws.test.ts +114 -0
- package/modules/ws/index.ts +136 -0
- package/package.json +99 -0
- package/skills/auth/SKILL.md +243 -0
- package/skills/cli/SKILL.md +258 -0
- package/skills/config/SKILL.md +253 -0
- package/skills/connectors/SKILL.md +259 -0
- package/skills/helpers/SKILL.md +206 -0
- package/skills/middlewares/SKILL.md +282 -0
- package/skills/orm/SKILL.md +260 -0
- package/skills/realtime/SKILL.md +307 -0
- package/skills/services/SKILL.md +206 -0
- package/skills/testing/SKILL.md +257 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# SKILL: Arckode Helpers y Static — Código compartido y archivos estáticos
|
|
2
|
+
|
|
3
|
+
> Activar cuando: crear una función utilitaria compartida, servir el frontend compilado, modo monolito, `arckode make:helper`.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. HELPERS PUROS (shared/helpers/)
|
|
8
|
+
|
|
9
|
+
Los helpers son funciones puras — sin estado, sin efectos secundarios, sin imports del framework. Cualquier módulo puede importarlos directamente (la única excepción a "no importar de otros módulos").
|
|
10
|
+
|
|
11
|
+
### Generar un helper
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
arckode make:helper formato-precio
|
|
15
|
+
# Crea: src/shared/helpers/formato-precio.ts
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Estructura generada
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// src/shared/helpers/formato-precio.ts
|
|
22
|
+
// Helpers puros — sin estado, sin efectos secundarios, sin dependencias externas
|
|
23
|
+
|
|
24
|
+
export function formatoPrecioExample(input: string): string {
|
|
25
|
+
return input.trim()
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Implementar el helper real
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// src/shared/helpers/formato-precio.ts
|
|
33
|
+
export function formatearPrecio(monto: number, moneda = 'DOP'): string {
|
|
34
|
+
return new Intl.NumberFormat('es-DO', {
|
|
35
|
+
style: 'currency',
|
|
36
|
+
currency: moneda,
|
|
37
|
+
}).format(monto)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function redondear(valor: number, decimales = 2): number {
|
|
41
|
+
return Math.round(valor * 10 ** decimales) / 10 ** decimales
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Importar desde cualquier módulo
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
// En cualquier service, controller o tipo — import directo permitido
|
|
49
|
+
import { formatearPrecio, redondear } from '../../shared/helpers/formato-precio'
|
|
50
|
+
|
|
51
|
+
class PedidosService {
|
|
52
|
+
calcularTotal(items: LineaDTO[]): number {
|
|
53
|
+
const bruto = items.reduce((sum, i) => sum + i.precio * i.cantidad, 0)
|
|
54
|
+
return redondear(bruto, 2)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 2. TIPOS DE HELPERS (qué va acá vs dónde)
|
|
62
|
+
|
|
63
|
+
| Tipo | Dónde va | Ejemplo |
|
|
64
|
+
|------|---------|---------|
|
|
65
|
+
| Función pura de transformación | `shared/helpers/` | `formatearFecha`, `slugify`, `calcularIVA` |
|
|
66
|
+
| Lógica de negocio | `modules/{modulo}/actions/service.ts` | `calcularDescuento(pedido)` |
|
|
67
|
+
| Validación de dominio | `modules/{modulo}/validators/schema.ts` | Schema de validación |
|
|
68
|
+
| Constante compartida | `shared/constants.ts` | `IVA_RATE = 0.18` |
|
|
69
|
+
| Tipo/interfaz compartida | `shared/types.ts` | `Paginado<T>`, `ApiResponse<T>` |
|
|
70
|
+
|
|
71
|
+
### Señales de que algo NO es un helper
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
// ❌ Tiene efectos secundarios — va en el service
|
|
75
|
+
export async function crearPedidoYNotificar(dto) {
|
|
76
|
+
const pedido = await orm.create(...) // NO — helper no puede tocar ORM
|
|
77
|
+
await mail.send(...) // NO — helper no puede usar servicios
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ❌ Depende de estado externo — va en el service
|
|
81
|
+
export function obtenerPrecioActual(productoId: string) {
|
|
82
|
+
return cache.get(`precio:${productoId}`) // NO — depende de cache
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ✅ Helper puro — sin dependencias
|
|
86
|
+
export function calcularSubtotal(precio: number, cantidad: number, descuento = 0): number {
|
|
87
|
+
return precio * cantidad * (1 - descuento)
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 3. CONSTANTS Y TIPOS COMPARTIDOS
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// src/shared/constants.ts
|
|
97
|
+
export const IVA_RATE = 0.18
|
|
98
|
+
export const ESTADOS_PEDIDO = ['pendiente', 'confirmado', 'enviado', 'entregado', 'cancelado'] as const
|
|
99
|
+
export type EstadoPedido = typeof ESTADOS_PEDIDO[number]
|
|
100
|
+
|
|
101
|
+
export const ROLES = ['user', 'admin', 'superadmin'] as const
|
|
102
|
+
export type Rol = typeof ROLES[number]
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
// src/shared/types.ts
|
|
107
|
+
export interface ApiResponse<T> {
|
|
108
|
+
data: T
|
|
109
|
+
message?: string
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface Paginado<T> {
|
|
113
|
+
data: T[]
|
|
114
|
+
total: number
|
|
115
|
+
page: number
|
|
116
|
+
limit: number
|
|
117
|
+
totalPages: number
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface ErrorResponse {
|
|
121
|
+
error: string
|
|
122
|
+
errors?: Record<string, string>
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 4. STATIC SERVER (modo monolito)
|
|
129
|
+
|
|
130
|
+
Sirve el frontend compilado desde el mismo servidor Node.js. Para proyectos fullstack donde el backend y frontend corren en el mismo proceso.
|
|
131
|
+
|
|
132
|
+
### Setup en composition-root.ts
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { serveStatic } from 'arckode-framework/static'
|
|
136
|
+
|
|
137
|
+
// Después de registrar todos los módulos y rutas del API
|
|
138
|
+
// IMPORTANTE: va al final — no interceptar rutas del API
|
|
139
|
+
serveStatic(router, './frontend/dist', {
|
|
140
|
+
prefix: '', // sin prefijo — sirve desde /
|
|
141
|
+
fallback: 'index.html', // para SPA (Vue Router, React Router)
|
|
142
|
+
cacheControl: 'public, max-age=3600', // 1 hora de cache
|
|
143
|
+
})
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Separar API de archivos estáticos
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
// API bajo /api/* — registrar primero
|
|
150
|
+
router.get('/api/productos', req => ...)
|
|
151
|
+
router.post('/api/auth/login', req => ...)
|
|
152
|
+
|
|
153
|
+
// Luego los estáticos — captura todo lo demás
|
|
154
|
+
serveStatic(router, './frontend/dist', {
|
|
155
|
+
prefix: '',
|
|
156
|
+
fallback: 'index.html',
|
|
157
|
+
})
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Servir uploads públicos
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { serveStatic } from 'arckode-framework/static'
|
|
164
|
+
|
|
165
|
+
// Archivos subidos por usuarios (no del frontend)
|
|
166
|
+
serveStatic(router, './uploads', {
|
|
167
|
+
prefix: '/uploads',
|
|
168
|
+
cacheControl: 'public, max-age=86400', // 24h — cambian por nombre único
|
|
169
|
+
})
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### MIME types soportados automáticamente
|
|
173
|
+
|
|
174
|
+
`.html`, `.css`, `.js`, `.json`, `.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`, `.ico`, `.woff`, `.woff2`, `.webp`, `.pdf`
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 5. ESTRUCTURA RECOMENDADA DEL PROYECTO
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
src/
|
|
182
|
+
├── composition-root.ts
|
|
183
|
+
├── modules/
|
|
184
|
+
│ ├── productos/
|
|
185
|
+
│ └── pedidos/
|
|
186
|
+
├── connectors/
|
|
187
|
+
│ └── pedido-stock.ts
|
|
188
|
+
└── shared/ ← código compartido
|
|
189
|
+
├── constants.ts ← constantes de dominio
|
|
190
|
+
├── types.ts ← tipos/interfaces compartidas
|
|
191
|
+
└── helpers/
|
|
192
|
+
├── formato-precio.ts ← helpers puros
|
|
193
|
+
├── fecha.ts
|
|
194
|
+
└── slugify.ts
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## 6. CHECKLIST HELPERS
|
|
200
|
+
|
|
201
|
+
- [ ] El helper es una función pura — sin imports de ORM, cache, mail, etc.
|
|
202
|
+
- [ ] Generado con `arckode make:helper nombre` para que quede en `shared/helpers/`
|
|
203
|
+
- [ ] Testeable sin mocks (función pura → test directo)
|
|
204
|
+
- [ ] Constantes compartidas en `shared/constants.ts`, no duplicadas en cada módulo
|
|
205
|
+
- [ ] `serveStatic()` registrado DESPUÉS de todas las rutas de API
|
|
206
|
+
- [ ] `fallback: 'index.html'` para proyectos con SPA routing
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# SKILL: Arckode Middlewares — Globales y por Ruta
|
|
2
|
+
|
|
3
|
+
> Activar cuando: agregar CORS, rate limit, timeout, logging, compresión, body limit, auth en rutas, o crear un middleware propio.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. IMPORT
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import {
|
|
11
|
+
cors,
|
|
12
|
+
rateLimit,
|
|
13
|
+
requestLogger,
|
|
14
|
+
bodyLimit,
|
|
15
|
+
timeout,
|
|
16
|
+
compression,
|
|
17
|
+
requireAuth,
|
|
18
|
+
} from 'arckode-framework/middlewares'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 2. MIDDLEWARES GLOBALES (composition-root.ts)
|
|
24
|
+
|
|
25
|
+
Los globales se aplican a TODAS las rutas. Registrar en este orden:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { cors, rateLimit, requestLogger, bodyLimit, timeout, compression } from 'arckode-framework/middlewares'
|
|
29
|
+
|
|
30
|
+
// Orden obligatorio — cada uno depende del anterior
|
|
31
|
+
router.use(timeout(10_000)) // 1. Cortar requests colgados antes que todo
|
|
32
|
+
router.use(cors({ origins: ['https://mi-app.com'], credentials: true })) // 2. CORS
|
|
33
|
+
router.use(bodyLimit(2 * 1024 * 1024)) // 3. Rechazar bodies > 2MB antes de parsear
|
|
34
|
+
router.use(requestLogger(logger)) // 4. Loguear método, path, status, duración
|
|
35
|
+
router.use(compression()) // 5. Gzip para responses > 1KB (opcional)
|
|
36
|
+
// rate limit: ver sección 3 — puede ser global o por ruta
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**El orden importa:** si `timeout` va al final, no puede cortar las fases anteriores.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 3. CORS
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// Permitir cualquier origen (solo para desarrollo/APIs públicas)
|
|
47
|
+
router.use(cors())
|
|
48
|
+
|
|
49
|
+
// Origenes específicos (producción)
|
|
50
|
+
router.use(cors({
|
|
51
|
+
origins: ['https://mi-app.com', 'https://admin.mi-app.com'],
|
|
52
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
53
|
+
headers: ['Content-Type', 'Authorization'],
|
|
54
|
+
credentials: true, // necesario si el frontend envía cookies o Authorization
|
|
55
|
+
}))
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Preflight:** el middleware maneja `OPTIONS` automáticamente y retorna 204.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 4. RATE LIMIT
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// Global: 100 requests por minuto por IP
|
|
66
|
+
const limiter = rateLimit({ windowMs: 60_000, max: 100 })
|
|
67
|
+
router.use(limiter)
|
|
68
|
+
|
|
69
|
+
// Por ruta específica (más restrictivo para endpoints sensibles)
|
|
70
|
+
const loginLimiter = rateLimit({
|
|
71
|
+
windowMs: 15 * 60_000, // 15 minutos
|
|
72
|
+
max: 5, // máximo 5 intentos
|
|
73
|
+
keyBy: (req) => req.headers['x-forwarded-for'] as string ?? 'anon',
|
|
74
|
+
})
|
|
75
|
+
router.post('/auth/login', [loginLimiter], req => controller.login(req))
|
|
76
|
+
|
|
77
|
+
// Rate limit por usuario autenticado (después de que auth middleware corra)
|
|
78
|
+
const userLimiter = rateLimit({
|
|
79
|
+
windowMs: 60_000,
|
|
80
|
+
max: 200,
|
|
81
|
+
keyBy: (req) => req.user?.id ?? req.headers['x-forwarded-for'] as string ?? 'anon',
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// Resetear límite de un usuario (ej: tras verificación de captcha)
|
|
85
|
+
limiter.reset('192.168.1.1')
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Importante:** El rate limiter usa memoria en proceso — no persiste entre reinicios ni entre múltiples instancias. Para producción distribuida → implementar `RateLimitAdapter` con Redis.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 5. REQUEST LOGGER
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
// Loguea: "POST /auth/login", "POST /auth/login 200 43ms"
|
|
96
|
+
router.use(requestLogger(logger))
|
|
97
|
+
|
|
98
|
+
// El logger hijo del módulo también puede usarse
|
|
99
|
+
router.use(requestLogger(logger.child('http')))
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Output en logs:
|
|
103
|
+
```
|
|
104
|
+
[app.http] POST /auth/login { requestId: 'abc-123' }
|
|
105
|
+
[app.http] POST /auth/login 200 43ms { requestId: 'abc-123', status: 200, duration: 43 }
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 6. BODY LIMIT
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
// Default: 1MB
|
|
114
|
+
router.use(bodyLimit())
|
|
115
|
+
|
|
116
|
+
// Custom: 10MB (para endpoints de upload)
|
|
117
|
+
router.use(bodyLimit(10 * 1024 * 1024))
|
|
118
|
+
|
|
119
|
+
// Por ruta (más permisivo solo en upload)
|
|
120
|
+
router.post('/archivos/upload', [bodyLimit(50 * 1024 * 1024)], req => controller.upload(req))
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Retorna **413** si el body supera el límite.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 7. TIMEOUT
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
// Default: 5 segundos
|
|
131
|
+
router.use(timeout())
|
|
132
|
+
|
|
133
|
+
// Custom por aplicación
|
|
134
|
+
router.use(timeout(10_000)) // 10s
|
|
135
|
+
|
|
136
|
+
// Más permisivo para endpoints lentos (reportes, exports)
|
|
137
|
+
router.get('/reportes/export', [timeout(60_000)], req => controller.export(req))
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Lanza `InternalError` si el handler no responde en tiempo.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 8. COMPRESSION (gzip)
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
// Comprime responses JSON > 1KB si el cliente acepta gzip
|
|
148
|
+
router.use(compression())
|
|
149
|
+
|
|
150
|
+
// Custom threshold
|
|
151
|
+
router.use(compression({ threshold: 512 })) // comprimir si > 512 bytes
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**No aplica a:** SSE streams, responses `null`, responses que ya tienen `Content-Encoding`.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 9. MIDDLEWARES POR RUTA
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
// Un middleware
|
|
162
|
+
router.get('/admin', [auth.authenticate('admin')], req => controller.dashboard(req))
|
|
163
|
+
|
|
164
|
+
// Múltiples middlewares (se ejecutan en orden)
|
|
165
|
+
router.post(
|
|
166
|
+
'/auth/login',
|
|
167
|
+
[loginLimiter, bodyLimit(10_000)],
|
|
168
|
+
req => controller.login(req)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
// Auth + role específico
|
|
172
|
+
router.delete(
|
|
173
|
+
'/usuarios/:id',
|
|
174
|
+
[auth.authenticate('admin', 'superadmin')],
|
|
175
|
+
req => controller.eliminar(req)
|
|
176
|
+
)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## 10. CREAR UN MIDDLEWARE PROPIO
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import type { MiddlewareHandler } from 'arckode-framework'
|
|
185
|
+
|
|
186
|
+
// Middleware que verifica un API key en el header
|
|
187
|
+
export function apiKeyAuth(validKeys: string[]): MiddlewareHandler {
|
|
188
|
+
return async (req, next) => {
|
|
189
|
+
const key = req.headers['x-api-key'] as string
|
|
190
|
+
if (!key || !validKeys.includes(key)) {
|
|
191
|
+
return { status: 401, body: { error: 'API key inválida' } }
|
|
192
|
+
}
|
|
193
|
+
return next() // siempre llamar next() para continuar la cadena
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Middleware de auditoría
|
|
198
|
+
export function auditLog(repo: RepositoryAdapter<AuditoriaDTO>): MiddlewareHandler {
|
|
199
|
+
return async (req, next) => {
|
|
200
|
+
const res = await next()
|
|
201
|
+
if (req.method !== 'GET' && res.status < 400) {
|
|
202
|
+
await repo.create({
|
|
203
|
+
userId: req.user?.id ?? 'anon',
|
|
204
|
+
action: `${req.method} ${req.path}`,
|
|
205
|
+
status: res.status,
|
|
206
|
+
} as any)
|
|
207
|
+
}
|
|
208
|
+
return res
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Registrar
|
|
213
|
+
router.use(apiKeyAuth(['key-secreta-1', 'key-secreta-2']))
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Regla:** siempre llamar `await next()` y retornar su resultado — excepto cuando querés cortar la cadena (retornar error directo, como en el ejemplo de apiKeyAuth).
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## 11. CADENA DE EJECUCIÓN
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
Request entrante
|
|
224
|
+
→ timeout (global)
|
|
225
|
+
→ cors (global)
|
|
226
|
+
→ bodyLimit (global)
|
|
227
|
+
→ requestLogger (global)
|
|
228
|
+
→ compression (global)
|
|
229
|
+
→ loginLimiter (por ruta, si aplica)
|
|
230
|
+
→ auth.authenticate (por ruta, si aplica)
|
|
231
|
+
→ handler (controller)
|
|
232
|
+
← Response saliente
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Los middlewares globales corren ANTES que los de ruta. El error handler del router atrapa excepciones de toda la cadena.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## 12. ANTI-PATRONES
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
// ❌ Middleware global que modifica req.body con lógica de negocio
|
|
243
|
+
router.use(async (req, next) => {
|
|
244
|
+
req.body.precio = req.body.precio * 1.21 // impuesto — NO acá, va en el service
|
|
245
|
+
return next()
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
// ❌ Olvidar llamar next() — la request queda colgada
|
|
249
|
+
router.use(async (req, next) => {
|
|
250
|
+
if (req.headers['x-tenant']) {
|
|
251
|
+
// ... pero nunca llama next()
|
|
252
|
+
}
|
|
253
|
+
// cuelga para requests sin x-tenant
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
// ❌ Lógica de autenticación duplicada en cada handler
|
|
257
|
+
router.get('/datos', async (req) => {
|
|
258
|
+
const token = req.headers['authorization']
|
|
259
|
+
if (!token) return { status: 401, body: { error: '...' } } // NO
|
|
260
|
+
// usar auth.authenticate() como middleware por ruta
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
// ✅ Cortar la cadena cuando es correcto (no llamar next)
|
|
264
|
+
router.use(async (req, next) => {
|
|
265
|
+
if (req.method === 'OPTIONS') {
|
|
266
|
+
return { status: 204, body: null } // cortar la cadena intencionalmente
|
|
267
|
+
}
|
|
268
|
+
return next()
|
|
269
|
+
})
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## 13. CHECKLIST MIDDLEWARES
|
|
275
|
+
|
|
276
|
+
- [ ] `timeout()` es el PRIMERO en los globales
|
|
277
|
+
- [ ] `cors()` tiene `origins` específicos en producción (no `'*'`)
|
|
278
|
+
- [ ] `bodyLimit()` registrado antes de endpoints que reciben JSON
|
|
279
|
+
- [ ] Rate limiter en endpoints de login/registro (máximo 5/15min)
|
|
280
|
+
- [ ] Middlewares de ruta van en array: `[auth.authenticate('admin')]`
|
|
281
|
+
- [ ] Todo middleware custom llama `await next()` (excepto cuando corta intencionalmente)
|
|
282
|
+
- [ ] No hay lógica de negocio en middlewares — solo cross-cutting concerns
|