arckode-framework 1.3.2 → 1.4.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/adapters/jwt.ts +6 -4
- package/adapters/mysql.ts +7 -2
- package/adapters/postgres.ts +37 -0
- package/adapters/sqlite.ts +7 -1
- package/adapters/vendor.d.ts +48 -0
- package/cli/analyze/checks.ts +333 -0
- package/cli/analyze/index.ts +44 -0
- package/cli/analyze/report.ts +107 -0
- package/cli/analyze/types.ts +46 -0
- package/cli/analyze/utils.ts +36 -0
- package/cli/analyze.ts +2 -647
- package/cli/commands/db-migrate.ts +213 -89
- package/cli/commands/db-seed.ts +97 -32
- package/cli/commands/db-utils.ts +192 -0
- package/cli/commands/new.ts +175 -0
- package/cli/commands/routes.ts +94 -0
- package/cli/index.ts +57 -404
- package/cli/stubs/module/core.ts +162 -0
- package/cli/stubs/module/data.ts +171 -0
- package/cli/stubs/module/index.ts +5 -0
- package/cli/stubs/module/service.ts +198 -0
- package/cli/stubs/module/types.ts +12 -0
- package/cli/stubs/module-stub.ts +2 -552
- package/kernel/auth.ts +114 -0
- package/kernel/cache.ts +37 -0
- package/kernel/config.ts +129 -0
- package/kernel/container.ts +64 -0
- package/kernel/db/orm-migrate.ts +136 -0
- package/kernel/db/orm-repository.ts +45 -0
- package/kernel/db/orm-utils.ts +93 -0
- package/kernel/db/orm.ts +254 -0
- package/kernel/db/transactor.ts +17 -0
- package/kernel/db/types.ts +72 -0
- package/kernel/errors.ts +102 -0
- package/kernel/framework.default.ts +41 -0
- package/kernel/framework.ts +8 -2144
- package/kernel/http/router.ts +131 -0
- package/kernel/http/server.ts +303 -0
- package/kernel/http/types.ts +56 -0
- package/kernel/index.ts +25 -0
- package/kernel/logger.ts +50 -0
- package/kernel/middlewares.ts +19 -7
- package/kernel/modules/create-module.ts +5 -0
- package/kernel/modules/system.ts +149 -0
- package/kernel/modules/types.ts +46 -0
- package/kernel/seeds.ts +48 -0
- package/kernel/static.ts +11 -2
- package/kernel/testing.ts +8 -3
- package/kernel/validator.ts +116 -0
- package/modules/events/index.ts +19 -3
- package/modules/mail/index.ts +14 -2
- package/modules/storage/local-adapter.ts +19 -5
- package/modules/ws/index.ts +123 -18
- package/package.json +8 -11
- package/skills/auth/SKILL.md +36 -220
- package/skills/cli/SKILL.md +32 -251
- package/skills/config/SKILL.md +30 -239
- package/skills/connectors/SKILL.md +32 -295
- package/skills/helpers/SKILL.md +26 -195
- package/skills/middlewares/SKILL.md +30 -280
- package/skills/orm/SKILL.md +42 -349
- package/skills/realtime/SKILL.md +22 -297
- package/skills/services/SKILL.md +40 -183
- package/skills/testing/SKILL.md +34 -266
package/skills/realtime/SKILL.md
CHANGED
|
@@ -1,307 +1,32 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Realtime — WebSockets, SSE, Event Bus
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
25
|
+
## Errores silenciosos
|
|
301
26
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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 |
|
package/skills/services/SKILL.md
CHANGED
|
@@ -1,206 +1,63 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Services — Lógica de Negocio
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
46
|
+
Composition root (ej. `index.ts` del módulo):
|
|
189
47
|
|
|
190
48
|
```ts
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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>` |
|