arckode-framework 1.3.2 → 1.4.1

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.
Files changed (64) hide show
  1. package/adapters/jwt.ts +6 -4
  2. package/adapters/mysql.ts +7 -2
  3. package/adapters/postgres.ts +37 -0
  4. package/adapters/sqlite.ts +7 -1
  5. package/adapters/vendor.d.ts +48 -0
  6. package/cli/analyze/checks.ts +333 -0
  7. package/cli/analyze/index.ts +44 -0
  8. package/cli/analyze/report.ts +107 -0
  9. package/cli/analyze/types.ts +46 -0
  10. package/cli/analyze/utils.ts +36 -0
  11. package/cli/analyze.ts +2 -647
  12. package/cli/commands/db-migrate.ts +213 -89
  13. package/cli/commands/db-seed.ts +97 -32
  14. package/cli/commands/db-utils.ts +192 -0
  15. package/cli/commands/new.ts +175 -0
  16. package/cli/commands/routes.ts +94 -0
  17. package/cli/index.ts +57 -404
  18. package/cli/stubs/module/core.ts +162 -0
  19. package/cli/stubs/module/data.ts +171 -0
  20. package/cli/stubs/module/index.ts +5 -0
  21. package/cli/stubs/module/service.ts +198 -0
  22. package/cli/stubs/module/types.ts +12 -0
  23. package/cli/stubs/module-stub.ts +2 -552
  24. package/kernel/auth.ts +114 -0
  25. package/kernel/cache.ts +37 -0
  26. package/kernel/config.ts +129 -0
  27. package/kernel/container.ts +64 -0
  28. package/kernel/db/orm-migrate.ts +136 -0
  29. package/kernel/db/orm-repository.ts +45 -0
  30. package/kernel/db/orm-utils.ts +93 -0
  31. package/kernel/db/orm.ts +254 -0
  32. package/kernel/db/transactor.ts +17 -0
  33. package/kernel/db/types.ts +72 -0
  34. package/kernel/errors.ts +102 -0
  35. package/kernel/framework.default.ts +41 -0
  36. package/kernel/framework.ts +8 -2144
  37. package/kernel/http/router.ts +131 -0
  38. package/kernel/http/server.ts +303 -0
  39. package/kernel/http/types.ts +56 -0
  40. package/kernel/index.ts +25 -0
  41. package/kernel/logger.ts +50 -0
  42. package/kernel/middlewares.ts +19 -7
  43. package/kernel/modules/create-module.ts +5 -0
  44. package/kernel/modules/system.ts +149 -0
  45. package/kernel/modules/types.ts +46 -0
  46. package/kernel/seeds.ts +48 -0
  47. package/kernel/static.ts +11 -2
  48. package/kernel/testing.ts +8 -3
  49. package/kernel/validator.ts +116 -0
  50. package/modules/events/index.ts +19 -3
  51. package/modules/mail/index.ts +14 -2
  52. package/modules/storage/local-adapter.ts +19 -5
  53. package/modules/ws/index.ts +123 -18
  54. package/package.json +8 -11
  55. package/skills/auth/SKILL.md +36 -220
  56. package/skills/cli/SKILL.md +32 -251
  57. package/skills/config/SKILL.md +30 -239
  58. package/skills/connectors/SKILL.md +32 -295
  59. package/skills/helpers/SKILL.md +26 -195
  60. package/skills/middlewares/SKILL.md +30 -280
  61. package/skills/orm/SKILL.md +42 -349
  62. package/skills/realtime/SKILL.md +22 -297
  63. package/skills/services/SKILL.md +40 -183
  64. package/skills/testing/SKILL.md +34 -266
@@ -1,307 +1,32 @@
1
- # SKILL: Arckode Realtime — EventBus, Queue y WebSockets
1
+ # Realtime — WebSockets, SSE, Event Bus
2
2
 
3
- > Activar cuando: comunicación entre módulos broadcast, jobs en background, WebSockets, tiempo real, notificaciones push.
3
+ ## Stack
4
4
 
5
- ---
5
+ - WebSockets nativos (`ws`) o Socket.IO
6
+ - SSE para unidireccional server→client
7
+ - Event Bus interno (`EventEmitter`) para módulos
6
8
 
7
- ## 1. EVENTBUS (pub-sub broadcast)
8
-
9
- **Cuándo usar EventBus vs Conectores:**
10
- - **Conector directo** → 2 módulos específicos, flujo claro y predecible
11
- - **EventBus** → 1 evento, N módulos reaccionan independientemente entre sí
12
-
13
- ### Setup en composition-root.ts
14
-
15
- ```ts
16
- import { EventBus } from 'arckode-framework/events'
17
-
18
- const events = new EventBus()
19
-
20
- // Pasar a los módulos que lo necesiten
21
- const system = new System({ ..., events })
22
- ```
23
-
24
- ### Emitir eventos (en el service)
25
-
26
- ```ts
27
- // El service emite — no sabe quién escucha
28
- class PedidosService {
29
- constructor(private events: EventBus, ...) {}
30
-
31
- async confirmar(id: string): Promise<PedidoDTO> {
32
- const pedido = await this.repo.update(id, { estado: 'confirmado' })
33
- await this.events.emit('pedido.confirmado', pedido, 'pedidos')
34
- return pedido!
35
- }
36
- }
37
- ```
38
-
39
- ### Suscribirse (en composition-root, no en módulos)
9
+ ## Event Bus
40
10
 
41
11
  ```ts
42
- // Los suscriptores van en composition-root — no en los módulos
43
- events.on('pedido.confirmado', async (event) => {
44
- const pedido = event.data as PedidoDTO
45
- await inventario.descontarStock(pedido.productoId, pedido.cantidad)
46
- })
47
-
48
- events.on('pedido.confirmado', async (event) => {
49
- const pedido = event.data as PedidoDTO
50
- await notificaciones.enviar(pedido.usuarioId, 'Pedido confirmado')
51
- })
52
-
53
- // Suscribirse una sola vez
54
- events.once('sistema.iniciado', async (event) => {
55
- await seeds.cargar()
56
- })
57
-
58
- // Cancelar suscripción
59
- const handler = async (event: EventMessage) => { ... }
60
- events.on('pedido.cancelado', handler)
61
- events.off('pedido.cancelado', handler)
62
- ```
63
-
64
- ### Inspeccionar (debug)
65
-
66
- ```ts
67
- // Historial de los últimos 100 eventos
68
- const todos = events.getHistory()
69
- const soloConfirmados = events.getHistory('pedido.confirmado')
70
-
71
- // Cuántos listeners hay por evento
72
- console.log(events.listeners) // { 'pedido.confirmado': 2, 'pedido.cancelado': 1 }
73
-
74
- // Limpiar historial
75
- events.clearHistory()
76
- ```
77
-
78
- ### Nomenclatura de eventos
79
-
80
- ```
81
- {módulo}.{acción} → pedido.confirmado, usuario.registrado
82
- {módulo}.{entidad}.{acción} → inventario.stock.agotado
83
- sistema.{acción} → sistema.iniciado, sistema.detenido
84
- ```
85
-
86
- ---
87
-
88
- ## 2. QUEUE SERVICE (jobs en background)
89
-
90
- **Cuándo usar Queue:**
91
- - Tareas que no deben bloquear la response HTTP (enviar email, generar PDF, procesar imagen)
92
- - Tareas que pueden fallar y necesitan reintentos automáticos
93
- - Tareas que deben ejecutarse con delay
94
-
95
- ### Setup en composition-root.ts
96
-
97
- ```ts
98
- import { QueueService, MemoryQueueAdapter } from 'arckode-framework/modules/queue'
99
-
100
- const queue = new QueueService(new MemoryQueueAdapter())
101
- ```
102
-
103
- ### Registrar handlers (antes de dispatch)
104
-
105
- ```ts
106
- // Registrar los handlers ANTES de empezar a despachar
107
- queue.register('enviar-email', async (job) => {
108
- const { to, subject, html } = job.data as { to: string; subject: string; html: string }
109
- await mail.send({ to: { address: to }, subject, html })
110
- })
111
-
112
- queue.register('generar-reporte', async (job) => {
113
- const { userId, periodo } = job.data as { userId: string; periodo: string }
114
- const pdf = await generarPDF(userId, periodo)
115
- await storage.upload({ ...pdf, fieldName: 'reporte' }, 'reportes')
116
- })
117
-
118
- queue.register('procesar-imagen', async (job) => {
119
- const { imagePath, userId } = job.data as { imagePath: string; userId: string }
120
- await redimensionarYOptimizar(imagePath)
121
- })
122
- ```
123
-
124
- ### Despachar jobs (desde el service)
125
-
126
- ```ts
127
- // Fire-and-forget: no espera que el job termine
128
- async registrar(dto: RegisterDTO): Promise<UserDTO> {
129
- const user = await this.repo.create(...)
130
-
131
- // El email no bloquea — se procesa en background
132
- await this.queue.dispatch('enviar-email', {
133
- to: user.email,
134
- subject: 'Bienvenido',
135
- html: `<h1>Hola ${user.nombre}!</h1>`,
136
- })
137
-
138
- return user
139
- }
140
-
141
- // Con delay (30 segundos)
142
- await queue.dispatch('recordatorio-carrito', { userId }, { delay: 30_000 })
143
-
144
- // Con reintentos personalizados
145
- await queue.dispatch('procesar-pago', { monto, userId }, { maxAttempts: 5 })
146
- ```
147
-
148
- ### Manejo de errores y reintentos
149
-
150
- ```ts
151
- // MemoryQueueAdapter reintenta automáticamente con backoff exponencial:
152
- // intento 1 → falla → espera 1s → intento 2 → falla → espera 2s → intento 3
153
- // maxAttempts default: 3
154
-
155
- // El handler recibe el job con metadatos
156
- queue.register('procesar-pago', async (job) => {
157
- console.log(`Intento ${job.attempts}/${job.maxAttempts}`)
158
- if (job.error) console.log('Error anterior:', job.error)
159
-
160
- const { monto } = job.data as { monto: number }
161
- await procesarPago(monto)
162
- })
163
- ```
164
-
165
- **Nota:** `MemoryQueueAdapter` es solo para desarrollo — los jobs se pierden al reiniciar. Para producción → implementar `QueueAdapter` con Redis o una tabla en BD.
166
-
167
- ---
168
-
169
- ## 3. WEBSOCKETS (tiempo real)
170
-
171
- ### Setup en composition-root.ts
172
-
173
- ```ts
174
- import { WSServer } from 'arckode-framework/ws'
175
-
176
- const ws = new WSServer()
177
-
178
- // Adjuntar al servidor HTTP ANTES de iniciar
179
- ws.attach(http.server)
180
-
181
- await system.start()
182
- ```
183
-
184
- ### Broadcast a todos los clientes
185
-
186
- ```ts
187
- // Desde cualquier service inyectado con ws
188
- ws.broadcast('pedido.nuevo', {
189
- id: pedido.id,
190
- total: pedido.total,
191
- estado: 'pendiente',
192
- })
193
-
194
- ws.broadcast('stock.actualizado', { productoId, stockActual })
195
- ```
196
-
197
- ### Enviar a un cliente específico
198
-
199
- ```ts
200
- ws.sendTo(clientId, 'notificacion', {
201
- tipo: 'success',
202
- mensaje: 'Tu pedido fue confirmado',
203
- })
204
- ```
205
-
206
- ### Gestionar clientes
207
-
208
- ```ts
209
- // Obtener todos los clientes conectados
210
- const clientes = ws.getClients()
211
- console.log(`${clientes.length} clientes conectados`)
212
- ```
213
-
214
- ### Inyectar WebSocket en un módulo
215
-
216
- ```ts
217
- // En index.ts del módulo
218
- create({ logger, orm, router, ws }: ModuleDependencies) {
219
- const service = new PedidosService(repo, ws, logger.child('pedidos'))
220
- ...
221
- }
222
-
223
- // En service.ts (al root del módulo)
224
- class PedidosService {
225
- constructor(
226
- private repo: RepositoryAdapter<PedidoDTO>,
227
- private ws: WSServer,
228
- private logger: Logger,
229
- ) {}
230
-
231
- async confirmar(id: string): Promise<PedidoDTO> {
232
- const pedido = await this.repo.update(id, { estado: 'confirmado' })
233
- this.ws.broadcast('pedido.confirmado', { id, estado: 'confirmado' })
234
- return pedido!
235
- }
236
- }
237
- ```
238
-
239
- ### SSE como alternativa (Server-Sent Events)
240
-
241
- Para notificaciones unidireccionales servidor → cliente sin WebSocket:
242
-
243
- ```ts
244
- // En el controller — SSE es más simple para notificaciones push
245
- router.get('/eventos', [auth.authenticate()], async (req) => {
246
- return {
247
- status: 200,
248
- stream: async function* () {
249
- // Enviar evento inicial
250
- yield `data: ${JSON.stringify({ tipo: 'conectado' })}\n\n`
251
-
252
- // Mantener conexión abierta — enviar heartbeat cada 30s
253
- while (true) {
254
- await new Promise(r => setTimeout(r, 30_000))
255
- yield `data: ${JSON.stringify({ tipo: 'heartbeat' })}\n\n`
256
- }
257
- },
258
- headers: {
259
- 'Content-Type': 'text/event-stream',
260
- 'Cache-Control': 'no-cache',
261
- 'Connection': 'keep-alive',
262
- },
263
- }
264
- })
265
- ```
266
-
267
- ---
268
-
269
- ## 4. COMBINACIONES COMUNES
270
-
271
- ### EventBus + Queue (decoupled async pipeline)
272
-
273
- ```ts
274
- // Módulo emite → EventBus → Queue despacha en background
275
- events.on('usuario.registrado', async (event) => {
276
- const user = event.data as UserDTO
277
- await queue.dispatch('enviar-bienvenida', { email: user.email, nombre: user.nombre })
278
- await queue.dispatch('configurar-defaults', { userId: user.id })
279
- })
280
- ```
281
-
282
- ### Queue + WebSocket (progreso en tiempo real)
283
-
284
- ```ts
285
- queue.register('exportar-reporte', async (job) => {
286
- const { userId } = job.data as { userId: string }
287
-
288
- ws.sendTo(userId, 'reporte.progreso', { paso: 1, total: 3, mensaje: 'Generando...' })
289
- const datos = await obtenerDatos(userId)
290
-
291
- ws.sendTo(userId, 'reporte.progreso', { paso: 2, total: 3, mensaje: 'Formateando...' })
292
- const pdf = await generarPDF(datos)
12
+ import { EventBus } from 'arckode'
13
+ const bus: EventBus
293
14
 
294
- ws.sendTo(userId, 'reporte.listo', { url: pdf.url })
295
- })
15
+ bus.on('pedido:creado', (pedido) => { /* ... */ }) // callback catch
16
+ bus.emit('pedido:creado', pedido) // dispatch
296
17
  ```
297
18
 
298
- ---
19
+ | Clave | `on()` | `once()` | `emit()` |
20
+ |-------|--------|----------|----------|
21
+ | Persiste | ✅ hasta `off()` | ❌ tras 1er fire | — |
22
+ | Async | ✅ callbacks | ✅ callbacks | ✅ await emit |
23
+ | Error | atrapa y log | atrapa y log | lanza a caller |
299
24
 
300
- ## 5. CHECKLIST REALTIME
25
+ ## Errores silenciosos
301
26
 
302
- - [ ] EventBus: suscriptores en composition-root, NO dentro de módulos
303
- - [ ] Queue: handlers registrados ANTES del primer dispatch
304
- - [ ] `MemoryQueueAdapter` solo para dev jobs se pierden al reiniciar
305
- - [ ] WebSocket: `ws.attach(http.server)` ANTES de `system.start()`
306
- - [ ] Eventos nombrados en `{módulo}.{acción}` no strings aleatorios
307
- - [ ] Handlers de queue con try/catch si el error debe notificarse al usuario
27
+ | Error | Señal | Fix |
28
+ |-------|-------|-----|
29
+ | Memory leak en `on()` | Eventos acumulan listeners sin `off()` | `bus.once()` || cleanup en `dispose()` |
30
+ | Socket sin heartbeat | Conexión muerta invisible | `ws.ping()` / `socket.io pingInterval` |
31
+ | Evento sin tipo | `bus.emit('order:paid', whatever)` | Tipar payload con `interface OrderPaidPayload` |
32
+ | Broadcast a todos | Socket envía a todos sin filtro | Room/channel por usuario o grupo |
@@ -1,206 +1,63 @@
1
- # SKILL: Arckode Services — Mail y Storage
1
+ # Services — Lógica de Negocio
2
2
 
3
- > Activar cuando: enviar emails, subir archivos, gestionar attachments, servir uploads.
3
+ ## Regla cardinal
4
4
 
5
- ---
6
-
7
- ## 1. MAIL SERVICE
8
-
9
- ### Setup en composition-root.ts
10
-
11
- ```ts
12
- import { MailService } from 'arckode-framework/modules/mail'
13
- import { SmtpAdapter } from 'arckode-framework/modules/mail/smtp-adapter'
14
-
15
- const mail = new MailService(
16
- new SmtpAdapter({
17
- host: config.get('SMTP_HOST'),
18
- port: config.get<number>('SMTP_PORT'),
19
- user: config.get('SMTP_USER'),
20
- pass: config.get('SMTP_PASS'),
21
- secure: config.get<number>('SMTP_PORT') === 465,
22
- }),
23
- { address: 'noreply@mi-app.com', name: 'Mi App' }, // from por defecto
24
- )
25
- ```
26
-
27
- **Config requerida en .env:**
28
- ```env
29
- SMTP_HOST=smtp.gmail.com
30
- SMTP_PORT=587
31
- SMTP_USER=mi@gmail.com
32
- SMTP_PASS=app-password-aqui
33
- ```
34
-
35
- ---
36
-
37
- ### API del MailService
5
+ Service recibe `RepositoryAdapter<T>`, NUNCA el ORM.
38
6
 
39
7
  ```ts
40
- // Email simple
41
- await mail.send({
42
- to: { address: 'user@example.com', name: 'Juan' },
43
- subject: 'Bienvenido',
44
- html: '<h1>Hola Juan!</h1>',
45
- text: 'Hola Juan!', // fallback plain text (recomendado)
46
- })
47
-
48
- // Múltiples destinatarios
49
- await mail.send({
50
- to: [{ address: 'a@x.com' }, { address: 'b@x.com' }],
51
- cc: { address: 'cc@x.com' },
52
- bcc: { address: 'bcc@x.com' },
53
- subject: 'Notificación',
54
- html: '<p>Mensaje masivo</p>',
55
- })
56
-
57
- // Con adjunto
58
- await mail.send({
59
- to: { address: 'user@example.com' },
60
- subject: 'Tu factura',
61
- html: '<p>Adjunto tu factura</p>',
62
- attachments: [{
63
- filename: 'factura.pdf',
64
- content: pdfBuffer, // Buffer
65
- contentType: 'application/pdf',
66
- }],
67
- })
68
-
69
- // Helpers pre-construidos
70
- await mail.sendWelcome('user@example.com', 'Juan')
71
- await mail.sendPasswordReset('user@example.com', resetToken)
72
- ```
73
-
74
- ---
75
-
76
- ### Inyectar MailService en un módulo
77
-
78
- ```ts
79
- // En index.ts del módulo
80
- create({ logger, orm, cache, router, auth, mail }: ModuleDependencies) {
81
- const service = new AuthService(repo, auth, mail, logger.child('auth'))
82
- ...
83
- }
84
-
85
- // En service.ts (al root del módulo)
86
- class AuthService {
87
- constructor(
88
- private repo: RepositoryAdapter<UserDTO>,
89
- private auth: Auth,
90
- private mail: MailService,
91
- private logger: Logger,
92
- ) {}
93
-
94
- async registrar(dto: RegisterDTO) {
95
- const user = await this.repo.create(...)
96
- await this.mail.sendWelcome(user.email, user.nombre) // no bloquear si falla
97
- return user
8
+ // bien
9
+ class WalletsService {
10
+ constructor(private repo: RepositoryAdapter<WalletDTO>) {}
11
+ async credit(userId: string, amount: number) {
12
+ return this.repo.update(userId, { balance: { increment: amount } })
98
13
  }
99
14
  }
100
- ```
101
-
102
- **Importante:** Envolver `mail.send()` en try/catch si el email no es crítico — el registro no debería fallar si el correo de bienvenida no llega.
103
-
104
- ---
105
-
106
- ### Testear con mock
107
15
 
108
- ```ts
109
- const mockMail = { send: mock(() => Promise.resolve()), sendWelcome: mock(() => Promise.resolve()) }
110
- const service = new AuthService(repo, auth, mockMail as any, logger)
111
-
112
- test('registrar envía email de bienvenida', async () => {
113
- await service.registrar({ email: 'a@b.com', password: '12345678', nombre: 'Juan' })
114
- expect(mockMail.sendWelcome).toHaveBeenCalledWith('a@b.com', 'Juan')
115
- })
116
- ```
117
-
118
- ---
119
-
120
- ## 2. STORAGE SERVICE
121
-
122
- ### Setup en composition-root.ts
123
-
124
- ```ts
125
- import { StorageService } from 'arckode-framework/modules/storage'
126
- import { LocalStorageAdapter } from 'arckode-framework/modules/storage/local-adapter'
127
-
128
- const storage = new StorageService(
129
- new LocalStorageAdapter(
130
- './uploads', // directorio en disco
131
- '/uploads', // URL base pública
132
- )
133
- )
134
- ```
135
-
136
- **Para servir los archivos subidos**, agregar el static server:
137
- ```ts
138
- import { serveStatic } from 'arckode-framework/static'
139
- serveStatic(router, './uploads', { prefix: '/uploads' })
16
+ // ❌ mal — service acoplado al ORM
17
+ class WalletsService {
18
+ constructor(private orm: ORM) {} // swap DB requiere cambiar esto
19
+ }
140
20
  ```
141
21
 
142
- ---
143
-
144
- ### API del StorageService
22
+ ## Patrones
145
23
 
146
24
  ```ts
147
- // Subir archivo
148
- const stored = await storage.upload(fileUpload, 'avatars')
149
- // → { url: '/uploads/avatars/1234-abc.png', path: 'avatars/1234-abc.png', ... }
150
-
151
- // Eliminar archivo
152
- await storage.delete('avatars/1234-abc.png')
153
-
154
- // Obtener URL pública
155
- const url = storage.getUrl('avatars/1234-abc.png')
156
- ```
157
-
158
- ---
159
-
160
- ### Recibir archivo desde HTTP request
25
+ class PedidosService {
26
+ constructor(
27
+ private repo: RepositoryAdapter<PedidoDTO>,
28
+ private wallets: WalletsService, // otro service (no ORM)
29
+ private bus: EventBus,
30
+ ) {}
161
31
 
162
- ```ts
163
- // En el controller — el framework parsea multipart/form-data automáticamente
164
- async uploadAvatar(req: HttpRequest): Promise<HttpResponse> {
165
- const file = req.files?.['avatar'] // FileUpload
166
- if (!file) throw new ValidationError('Archivo requerido', { avatar: 'requerido' })
167
- if (!file.mimeType.startsWith('image/')) {
168
- throw new ValidationError('Formato inválido', { avatar: 'debe ser una imagen' })
169
- }
170
- if (file.size > 5 * 1024 * 1024) {
171
- throw new ValidationError('Archivo muy grande', { avatar: 'máximo 5MB' })
32
+ async create(data: CreatePedidoDTO): Promise<PedidoDTO> {
33
+ // validación de dominio
34
+ if (data.total <= 0) throw new Error('Total debe ser > 0')
35
+ // delegar persistencia
36
+ const pedido = await this.repo.create(data)
37
+ // emitir evento
38
+ this.bus.emit('pedido:creado', pedido)
39
+ return pedido
172
40
  }
173
-
174
- const stored = await this.service.subirAvatar(req.user!.id, file)
175
- return { status: 200, body: { url: stored.url } }
176
- }
177
-
178
- // En el service
179
- async subirAvatar(userId: string, file: FileUpload): Promise<StoredFile> {
180
- const stored = await this.storage.upload(file, `avatars/${userId}`)
181
- await this.repo.update(userId, { avatarUrl: stored.url })
182
- return stored
183
41
  }
184
42
  ```
185
43
 
186
- ---
44
+ ## Inyección
187
45
 
188
- ### Inyectar StorageService en un módulo
46
+ Composition root (ej. `index.ts` del módulo):
189
47
 
190
48
  ```ts
191
- // ModuleDependencies incluye storage si fue pasado al System
192
- create({ logger, orm, router, storage }: ModuleDependencies) {
193
- const service = new UsuariosService(repo, storage, logger.child('usuarios'))
194
- ...
49
+ create({ orm, bus }) {
50
+ const repo = new OrmRepository<PedidoDTO>(orm, 'Pedido')
51
+ const walletsService = new WalletsService(new OrmRepository<WalletDTO>(orm, 'Wallet'))
52
+ return new PedidosService(repo, walletsService, bus)
195
53
  }
196
54
  ```
197
55
 
198
- ---
199
-
200
- ### Checklist Mail + Storage
56
+ ## Errores silenciosos
201
57
 
202
- - [ ] `SMTP_*` variables en .env (nunca hardcodeadas)
203
- - [ ] `mail.send()` con try/catch si el email no es crítico para la operación
204
- - [ ] Validar `mimeType` y `size` antes de `storage.upload()`
205
- - [ ] `serveStatic()` configurado si se necesita acceso público a los archivos
206
- - [ ] Mockear ambos servicios en tests (no enviar emails reales en tests)
58
+ | Error | Señal | Fix |
59
+ |-------|-------|-----|
60
+ | Service recibe ORM directo | `constructor(private orm: ORM)` | Cambiar a `RepositoryAdapter<T>` |
61
+ | Service con lógica HTTP | Validación de request en service | Mover a controller/validateSchema |
62
+ | Lógica repetida en services | Mismo filtro en 3 services | Domain repository |
63
+ | Service sin interface | Acoplamiento directo | Usar `RepositoryAdapter<T>` |