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,253 @@
|
|
|
1
|
+
# SKILL: Arckode Config — ConfigStore, Logger y Cache
|
|
2
|
+
|
|
3
|
+
> Activar cuando: configurar variables de entorno, agregar config nueva, usar el logger, trabajar con cache.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. CONFIGSTORE + loadEnv
|
|
8
|
+
|
|
9
|
+
### Setup completo
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { ConfigStore, loadEnv } from 'arckode-framework'
|
|
13
|
+
|
|
14
|
+
// 1. Cargar archivos .env (SIEMPRE await)
|
|
15
|
+
const env = await loadEnv()
|
|
16
|
+
|
|
17
|
+
// 2. Definir schema y cargar valores
|
|
18
|
+
const config = new ConfigStore()
|
|
19
|
+
config
|
|
20
|
+
.define({
|
|
21
|
+
// Servidor
|
|
22
|
+
PORT: { type: 'number', default: 3000 },
|
|
23
|
+
HOST: { type: 'string', default: '0.0.0.0' },
|
|
24
|
+
NODE_ENV: { type: 'string', default: 'development' },
|
|
25
|
+
|
|
26
|
+
// Base de datos
|
|
27
|
+
DB_PATH: { type: 'string', default: './data/db.sqlite' },
|
|
28
|
+
DB_HOST: { type: 'string', required: false },
|
|
29
|
+
DB_PORT: { type: 'number', default: 5432 },
|
|
30
|
+
DB_NAME: { type: 'string', required: false },
|
|
31
|
+
DB_USER: { type: 'string', required: false },
|
|
32
|
+
DB_PASSWORD: { type: 'string', required: false },
|
|
33
|
+
|
|
34
|
+
// Auth
|
|
35
|
+
JWT_SECRET: { type: 'string', required: true }, // fail-fast si falta
|
|
36
|
+
|
|
37
|
+
// Opcionales con defaults
|
|
38
|
+
LOG_LEVEL: { type: 'string', default: 'info' },
|
|
39
|
+
SMTP_HOST: { type: 'string', required: false },
|
|
40
|
+
SMTP_PORT: { type: 'number', default: 587 },
|
|
41
|
+
})
|
|
42
|
+
.load(env)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Tipos de config disponibles
|
|
46
|
+
|
|
47
|
+
| Tipo | Validación | Ejemplo |
|
|
48
|
+
|------|-----------|---------|
|
|
49
|
+
| `string` | Cualquier texto | `'localhost'` |
|
|
50
|
+
| `number` | Parseado de string | `'3000'` → `3000` |
|
|
51
|
+
| `boolean` | `'true'/'false'/'1'/'0'` | `'true'` → `true` |
|
|
52
|
+
| `url` | Válida como URL | `'https://api.com'` |
|
|
53
|
+
| `email` | Válido como email | `'a@b.com'` |
|
|
54
|
+
|
|
55
|
+
### Opciones por campo
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
config.define({
|
|
59
|
+
CAMPO: {
|
|
60
|
+
type: 'string', // tipo (requerido)
|
|
61
|
+
required: true, // fail-fast si no está en .env — default: false
|
|
62
|
+
default: 'valor', // valor si no está en .env
|
|
63
|
+
// required + default no tienen sentido juntos — elegir uno
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Leer valores
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
config.get('PORT') // → string (tipo no inferido)
|
|
72
|
+
config.get<number>('PORT') // → number (con genérico)
|
|
73
|
+
config.get<boolean>('DEBUG') // → boolean
|
|
74
|
+
config.get('JWT_SECRET') // → string
|
|
75
|
+
|
|
76
|
+
// Fail-fast en get(): si la key no fue definida → error en startup
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Archivos .env cargados (en orden de precedencia)
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
process.env ← máxima prioridad (CI/CD, Docker)
|
|
83
|
+
.env.{NODE_ENV}.local ← ej: .env.development.local
|
|
84
|
+
.env.{NODE_ENV} ← ej: .env.production
|
|
85
|
+
.env.local ← local, no commitear
|
|
86
|
+
.env ← base del proyecto
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**.gitignore obligatorio:**
|
|
90
|
+
```gitignore
|
|
91
|
+
.env.local
|
|
92
|
+
.env.*.local
|
|
93
|
+
.env.production
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 2. LOGGER
|
|
99
|
+
|
|
100
|
+
### Crear logger
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { Logger } from 'arckode-framework'
|
|
104
|
+
|
|
105
|
+
// En composition-root
|
|
106
|
+
const logger = new Logger('app', 'info') // nombre, log level
|
|
107
|
+
|
|
108
|
+
// Logger hijo por módulo (recomendado)
|
|
109
|
+
const log = logger.child('productos') // → [app.productos]
|
|
110
|
+
const log = logger.child('productos.stock') // → [app.productos.stock]
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Niveles disponibles: `debug` | `info` | `warn` | `error`
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
log.debug('Mensaje de debug', { userId, queryTime }) // solo si LOG_LEVEL=debug
|
|
117
|
+
log.info('Operación completada', { id: item.id })
|
|
118
|
+
log.warn('Comportamiento inesperado', { field, value })
|
|
119
|
+
log.error('Error crítico', { error: err.message, stack: err.stack })
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Qué loguear y cómo
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
// ✅ Info: eventos de negocio con contexto suficiente para debugging
|
|
126
|
+
log.info('Pedido confirmado', { pedidoId, usuarioId, total })
|
|
127
|
+
log.info('Usuario registrado', { userId, email })
|
|
128
|
+
|
|
129
|
+
// ✅ Warn: comportamiento inusual que no es error
|
|
130
|
+
log.warn('Stock bajo', { productoId, stockActual: 3, umbral: 10 })
|
|
131
|
+
log.warn('Token próximo a expirar', { userId, expiresIn: '5min' })
|
|
132
|
+
|
|
133
|
+
// ✅ Error: errores reales con contexto completo
|
|
134
|
+
log.error('Fallo al enviar email', { to, error: err.message })
|
|
135
|
+
|
|
136
|
+
// ❌ Debug en producción — usar solo en desarrollo
|
|
137
|
+
log.debug('Query ejecutada', { sql, params, duration })
|
|
138
|
+
|
|
139
|
+
// ❌ Nunca loguear datos sensibles
|
|
140
|
+
log.info('Login', { email, password: dto.password }) // NO
|
|
141
|
+
log.info('Login', { email }) // ✅
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Log level por entorno
|
|
145
|
+
|
|
146
|
+
```env
|
|
147
|
+
# .env
|
|
148
|
+
LOG_LEVEL=info
|
|
149
|
+
|
|
150
|
+
# .env.development
|
|
151
|
+
LOG_LEVEL=debug
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## 3. CACHE
|
|
157
|
+
|
|
158
|
+
### Setup
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { MemoryCache } from 'arckode-framework'
|
|
162
|
+
|
|
163
|
+
const cache = new MemoryCache()
|
|
164
|
+
|
|
165
|
+
// Para producción distribuida → Redis
|
|
166
|
+
import { RedisCache } from 'arckode-framework/adapters/redis-cache'
|
|
167
|
+
const cache = new RedisCache({ host: config.get('REDIS_HOST'), port: 6379 })
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### API del Cache
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
// Leer
|
|
174
|
+
const value = await cache.get<ProductoDTO[]>('productos:list') // null si no existe o expiró
|
|
175
|
+
|
|
176
|
+
// Guardar con TTL (en segundos)
|
|
177
|
+
await cache.set('productos:list', items, 60) // expira en 60s
|
|
178
|
+
await cache.set('usuario:123', user, 300) // expira en 5 min
|
|
179
|
+
await cache.set('config:global', data) // sin TTL — persiste hasta flush()
|
|
180
|
+
|
|
181
|
+
// Invalidar
|
|
182
|
+
await cache.delete('productos:list') // borrar una key
|
|
183
|
+
await cache.flush() // borrar todo
|
|
184
|
+
|
|
185
|
+
// Estadísticas
|
|
186
|
+
console.log(cache.stats)
|
|
187
|
+
// { size: 42, hits: 1203, misses: 87, hitRate: '93.27%' }
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Patrón cache-aside (el más común)
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
async listar(): Promise<ProductoDTO[]> {
|
|
194
|
+
const KEY = 'productos:list'
|
|
195
|
+
|
|
196
|
+
const cached = await this.cache.get<ProductoDTO[]>(KEY)
|
|
197
|
+
if (cached) return cached // cache HIT
|
|
198
|
+
|
|
199
|
+
const items = await this.repo.findMany({ activo: true })
|
|
200
|
+
await this.cache.set(KEY, items, 60) // guardar 60s
|
|
201
|
+
return items
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Invalidar al mutar
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
async crear(dto: CreateProductoDTO): Promise<ProductoDTO> {
|
|
209
|
+
const item = await this.repo.create(...)
|
|
210
|
+
await this.cache.delete('productos:list') // invalidar lista
|
|
211
|
+
return item
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async actualizar(id: string, dto: UpdateProductoDTO): Promise<ProductoDTO> {
|
|
215
|
+
const item = await this.repo.update(id, dto)
|
|
216
|
+
await this.cache.delete('productos:list') // invalidar lista
|
|
217
|
+
await this.cache.delete(`producto:${id}`) // invalidar el item específico
|
|
218
|
+
return item!
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Naming convention de keys
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
{entidad}:list → productos:list
|
|
226
|
+
{entidad}:{id} → producto:abc-123
|
|
227
|
+
{entidad}:{id}:{sub-recurso} → usuario:abc-123:pedidos
|
|
228
|
+
{scope}:{entidad}:{filtro} → admin:reportes:2026-05
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### TTL recomendados
|
|
232
|
+
|
|
233
|
+
| Tipo de dato | TTL |
|
|
234
|
+
|-------------|-----|
|
|
235
|
+
| Listas con paginación | 30-60s |
|
|
236
|
+
| Item individual | 2-5 min |
|
|
237
|
+
| Datos de config | 10-30 min |
|
|
238
|
+
| Contadores/stats | 5 min |
|
|
239
|
+
| Tokens/sessions | igual que el token |
|
|
240
|
+
| Datos de referencia (países, categorías) | 1h+ |
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 4. CHECKLIST CONFIG
|
|
245
|
+
|
|
246
|
+
- [ ] `loadEnv()` con `await` — nunca sincrónico
|
|
247
|
+
- [ ] Keys `required: true` para todo lo que cause crash si falta
|
|
248
|
+
- [ ] Nunca `config.get()` sin haber llamado `config.define()` antes
|
|
249
|
+
- [ ] Logger hijo por módulo: `logger.child('nombre-modulo')`
|
|
250
|
+
- [ ] Nunca loguear passwords, tokens ni datos sensibles
|
|
251
|
+
- [ ] Cache invalidado en toda operación de escritura que afecte la key
|
|
252
|
+
- [ ] TTL explícito en `cache.set()` para datos que cambian frecuentemente
|
|
253
|
+
- [ ] `.env.production` en `.gitignore` — nunca commitear secrets
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# SKILL: Arckode Connectors — Puentes entre módulos
|
|
2
|
+
|
|
3
|
+
> Activar cuando: dos módulos necesitan comunicarse, implementar side effects entre módulos, crear eventos cross-módulo.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. QUÉ ES UN CONECTOR (y cuándo crearlo)
|
|
8
|
+
|
|
9
|
+
Un conector es el **único canal legal** entre módulos. Se crea cuando:
|
|
10
|
+
- El módulo A necesita reaccionar a algo que hace el módulo B
|
|
11
|
+
- Un evento en B debe disparar una acción en A
|
|
12
|
+
- B necesita datos de A para completar su operación
|
|
13
|
+
|
|
14
|
+
**NO crear un conector cuando:**
|
|
15
|
+
- Podrías resolver el problema con datos del mismo módulo
|
|
16
|
+
- La comunicación es en un solo sentido sin side effects
|
|
17
|
+
- El módulo B solo necesita mostrar datos del módulo A en una response (usar acción pública directamente en el controller o composition-root)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 2. ANATOMÍA DE UN CONECTOR
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// src/connectors/pedido-inventario.ts
|
|
25
|
+
import type { ConnectorContext } from 'arckode-framework'
|
|
26
|
+
|
|
27
|
+
// SOLO importar tipos — nunca implementaciones internas de módulos
|
|
28
|
+
import type { PedidosService } from '../modules/pedidos' // desde index.ts
|
|
29
|
+
import type { InventarioService } from '../modules/inventario' // desde index.ts
|
|
30
|
+
|
|
31
|
+
export function conectarPedidoConInventario(ctx: ConnectorContext): void {
|
|
32
|
+
// 1. Resolver los módulos que necesitás conectar
|
|
33
|
+
const pedidos = ctx.resolveModule<PedidosService>('pedidos')
|
|
34
|
+
const inventario = ctx.resolveModule<InventarioService>('inventario')
|
|
35
|
+
|
|
36
|
+
// 2. Inyectar los sockets — solo delegar, sin lógica
|
|
37
|
+
pedidos.setSockets({
|
|
38
|
+
onPedidoConfirmado: async (pedido) => {
|
|
39
|
+
await inventario.descontarStock(pedido.productoId, pedido.cantidad)
|
|
40
|
+
},
|
|
41
|
+
onPedidoCancelado: async (pedido) => {
|
|
42
|
+
await inventario.reponerStock(pedido.productoId, pedido.cantidad)
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Regla de oro:** Si hay un `if` o un `for` en el conector — es lógica de negocio. Moverla al módulo correspondiente.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 3. PREPARAR EL SERVICE PARA RECIBIR SOCKETS
|
|
53
|
+
|
|
54
|
+
El módulo que emite eventos debe implementar `SocketsAware`:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// En sockets.ts del módulo pedidos
|
|
58
|
+
export interface PedidosSockets {
|
|
59
|
+
onPedidoConfirmado?: (pedido: PedidoDTO) => Promise<void>
|
|
60
|
+
onPedidoCancelado?: (pedido: PedidoDTO) => Promise<void>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// En actions/service.ts
|
|
64
|
+
import type { PedidosSockets } from '../sockets'
|
|
65
|
+
|
|
66
|
+
export class PedidosService {
|
|
67
|
+
private sockets: PedidosSockets = {}
|
|
68
|
+
|
|
69
|
+
// Requerido para que el conector pueda inyectar
|
|
70
|
+
setSockets(sockets: PedidosSockets): void {
|
|
71
|
+
this.sockets = sockets
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async confirmar(id: string): Promise<PedidoDTO> {
|
|
75
|
+
const pedido = await this.repo.update(id, { estado: 'confirmado' })
|
|
76
|
+
if (!pedido) throw new NotFoundError('Pedido no encontrado')
|
|
77
|
+
|
|
78
|
+
// Disparar el evento — el conector decide qué hacer
|
|
79
|
+
await this.sockets.onPedidoConfirmado?.(pedido)
|
|
80
|
+
|
|
81
|
+
return pedido
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**El `?.` es intencional:** el socket es opcional. Si no hay conector registrado, el módulo funciona igual (solo sin side effects).
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 4. REGISTRAR EN COMPOSITION-ROOT
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
// src/composition-root.ts
|
|
94
|
+
import { conectarPedidoConInventario } from './connectors/pedido-inventario'
|
|
95
|
+
import { conectarPedidoConNotificaciones } from './connectors/pedido-notificaciones'
|
|
96
|
+
|
|
97
|
+
// DESPUÉS de registrar los módulos, ANTES de start()
|
|
98
|
+
system.addModule(PedidosModule())
|
|
99
|
+
system.addModule(InventarioModule())
|
|
100
|
+
system.addModule(NotificacionesModule())
|
|
101
|
+
|
|
102
|
+
// Los conectores van al final — dependen de que los módulos estén inicializados
|
|
103
|
+
system.addConnector('pedido-inventario', conectarPedidoConInventario)
|
|
104
|
+
system.addConnector('pedido-notificaciones', conectarPedidoConNotificaciones)
|
|
105
|
+
|
|
106
|
+
await system.start()
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Orden obligatorio:** módulos → conectores → start.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 5. NAMING CONVENTION
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
src/connectors/
|
|
117
|
+
{módulo-origen}-{módulo-destino}.ts → pedido-inventario.ts
|
|
118
|
+
{evento}-{acción}.ts → nuevo-usuario-bienvenida.ts
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
El nombre del conector en `addConnector()` es solo para logging — puede ser descriptivo:
|
|
122
|
+
```ts
|
|
123
|
+
system.addConnector('pedido→inventario:descontar-stock', conectarPedidoConInventario)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 6. CONECTOR CON MÚLTIPLES MÓDULOS
|
|
129
|
+
|
|
130
|
+
Un conector puede coordinar más de 2 módulos, siempre que solo delegue:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
export function conectarPedidoCompleto(ctx: ConnectorContext): void {
|
|
134
|
+
const pedidos = ctx.resolveModule<PedidosService>('pedidos')
|
|
135
|
+
const inventario = ctx.resolveModule<InventarioService>('inventario')
|
|
136
|
+
const notificaciones = ctx.resolveModule<NotificacionesService>('notificaciones')
|
|
137
|
+
const auditoria = ctx.resolveModule<AuditoriaService>('auditoria')
|
|
138
|
+
|
|
139
|
+
pedidos.setSockets({
|
|
140
|
+
onPedidoConfirmado: async (pedido) => {
|
|
141
|
+
// Cada llamada es una delegación — sin lógica en el medio
|
|
142
|
+
await inventario.descontarStock(pedido.productoId, pedido.cantidad)
|
|
143
|
+
await notificaciones.enviarConfirmacion(pedido.usuarioId, pedido.id)
|
|
144
|
+
await auditoria.registrar('pedido.confirmado', { pedidoId: pedido.id })
|
|
145
|
+
},
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 7. EVENTBUS (alternativa para eventos broadcast)
|
|
153
|
+
|
|
154
|
+
Cuando muchos módulos necesitan reaccionar al mismo evento sin conocerse entre sí:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
// En composition-root.ts
|
|
158
|
+
import { EventBus } from 'arckode-framework/events'
|
|
159
|
+
|
|
160
|
+
const events = new EventBus()
|
|
161
|
+
|
|
162
|
+
// Módulo pedidos publica
|
|
163
|
+
pedidos.setSockets({
|
|
164
|
+
onPedidoConfirmado: async (pedido) => {
|
|
165
|
+
events.emit('pedido.confirmado', pedido, 'pedidos')
|
|
166
|
+
},
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
// Múltiples suscriptores — no se conocen entre sí
|
|
170
|
+
events.on('pedido.confirmado', async (event) => {
|
|
171
|
+
await inventario.descontarStock(event.data.productoId, event.data.cantidad)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
events.on('pedido.confirmado', async (event) => {
|
|
175
|
+
await notificaciones.enviarConfirmacion(event.data.usuarioId, event.data.id)
|
|
176
|
+
})
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Usar EventBus cuando:** múltiples módulos reaccionan al mismo evento y no querés un conector gigante.
|
|
180
|
+
**Usar conectores directos cuando:** la coordinación es entre 2 módulos específicos y el flujo es claro.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 8. ANTI-PATRONES DE CONECTORES
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
// ❌ Importar desde actions/service directamente (viola REGLA #1)
|
|
188
|
+
import { PedidosService } from '../modules/pedidos/actions/service' // NO
|
|
189
|
+
|
|
190
|
+
// ✅ Solo desde index.ts (puerta pública)
|
|
191
|
+
import type { PedidosService } from '../modules/pedidos'
|
|
192
|
+
|
|
193
|
+
// ❌ Lógica de negocio en el conector (viola REGLA #3)
|
|
194
|
+
pedidos.setSockets({
|
|
195
|
+
onPedidoConfirmado: async (pedido) => {
|
|
196
|
+
if (pedido.total > 1000) {
|
|
197
|
+
await inventario.priorizar(pedido.productoId) // decisión de negocio — NO acá
|
|
198
|
+
} else {
|
|
199
|
+
await inventario.descontarStock(pedido.productoId, pedido.cantidad)
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// ✅ Lógica en el módulo, conector solo llama
|
|
205
|
+
pedidos.setSockets({
|
|
206
|
+
onPedidoConfirmado: async (pedido) => {
|
|
207
|
+
await inventario.procesarPedido(pedido) // inventario decide cómo procesar
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
// ❌ Conector escribe directamente en tabla de otro módulo (viola REGLA #4)
|
|
212
|
+
pedidos.setSockets({
|
|
213
|
+
onPedidoConfirmado: async (pedido) => {
|
|
214
|
+
await orm.update('Inventario', pedido.productoId, { stock: nuevoStock }) // NO
|
|
215
|
+
},
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
// ❌ Conector en composition-root antes de módulos
|
|
219
|
+
system.addConnector(...) // NO
|
|
220
|
+
system.addModule(PedidosModule()) // los módulos deben ir primero
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## 9. TESTEAR CONECTORES
|
|
226
|
+
|
|
227
|
+
Los conectores son difíciles de testear en aislamiento — testear los módulos que emiten/reciben:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
// Testear que el service llama a los sockets cuando corresponde
|
|
231
|
+
test('confirmar pedido dispara onPedidoConfirmado', async () => {
|
|
232
|
+
const socketFn = mock(() => Promise.resolve())
|
|
233
|
+
service.setSockets({ onPedidoConfirmado: socketFn })
|
|
234
|
+
|
|
235
|
+
await service.confirmar('pedido-123')
|
|
236
|
+
|
|
237
|
+
expect(socketFn).toHaveBeenCalledTimes(1)
|
|
238
|
+
expect(socketFn).toHaveBeenCalledWith(
|
|
239
|
+
expect.objectContaining({ estado: 'confirmado' })
|
|
240
|
+
)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
// Testear que funciona SIN sockets (optional chaining)
|
|
244
|
+
test('confirmar pedido funciona sin sockets registrados', async () => {
|
|
245
|
+
// sin setSockets() — sockets = {}
|
|
246
|
+
await expect(service.confirmar('pedido-123')).resolves.toBeDefined()
|
|
247
|
+
})
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## 10. CHECKLIST CONECTORES
|
|
253
|
+
|
|
254
|
+
- [ ] Solo importa tipos (no implementaciones) desde `index.ts` de módulos
|
|
255
|
+
- [ ] Sin `if`, `for`, ni lógica de negocio — solo llamadas de delegación
|
|
256
|
+
- [ ] El service emisor implementa `setSockets()` y usa `?.` al llamarlos
|
|
257
|
+
- [ ] Registrado en composition-root DESPUÉS de los módulos
|
|
258
|
+
- [ ] El módulo funciona correctamente sin el conector (sockets son opcionales)
|
|
259
|
+
- [ ] `arckode analyze` detecta: `CONNECTOR_BUSINESS_LOGIC` si hay lógica
|