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.
- 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
|
@@ -1,316 +1,53 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Conectores — DB, Cache, Storage, etc.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Regla cardinal
|
|
4
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 service.ts (al root del módulo)
|
|
64
|
-
import type { PedidosSockets } from './sockets'
|
|
65
|
-
|
|
66
|
-
export class PedidosService {
|
|
67
|
-
private sockets: PedidosSockets = {}
|
|
68
|
-
|
|
69
|
-
// ACUMULA — si dos conectores registran el mismo evento, ambos corren en cadena.
|
|
70
|
-
// NUNCA usar { ...this.sockets, ...s } — pisa silenciosamente el handler anterior.
|
|
71
|
-
setSockets(s: Partial<PedidosSockets>): void {
|
|
72
|
-
const next = s as Record<string, any>
|
|
73
|
-
const cur = this.sockets as Record<string, any>
|
|
74
|
-
for (const key of Object.keys(next)) {
|
|
75
|
-
const h = next[key]
|
|
76
|
-
if (!h) continue
|
|
77
|
-
const prev = cur[key]
|
|
78
|
-
cur[key] = prev ? async (...a: any[]) => { await prev(...a); await h(...a) } : h
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async confirmar(id: string): Promise<PedidoDTO> {
|
|
83
|
-
const pedido = await this.repo.update(id, { estado: 'confirmado' })
|
|
84
|
-
if (!pedido) throw new NotFoundError('Pedido no encontrado')
|
|
85
|
-
|
|
86
|
-
// Disparar el evento — el conector decide qué hacer
|
|
87
|
-
await this.sockets.onPedidoConfirmado?.(pedido)
|
|
88
|
-
|
|
89
|
-
return pedido
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
**El `?.` es intencional:** el socket es opcional. Si no hay conector registrado, el módulo funciona igual (solo sin side effects).
|
|
95
|
-
|
|
96
|
-
---
|
|
97
|
-
|
|
98
|
-
## 4. REGISTRAR EN COMPOSITION-ROOT
|
|
99
|
-
|
|
100
|
-
```ts
|
|
101
|
-
// src/composition-root.ts
|
|
102
|
-
import { conectarPedidoConInventario } from './connectors/pedido-inventario'
|
|
103
|
-
import { conectarPedidoConNotificaciones } from './connectors/pedido-notificaciones'
|
|
104
|
-
|
|
105
|
-
// DESPUÉS de registrar los módulos, ANTES de start()
|
|
106
|
-
system.addModule(PedidosModule())
|
|
107
|
-
system.addModule(InventarioModule())
|
|
108
|
-
system.addModule(NotificacionesModule())
|
|
109
|
-
|
|
110
|
-
// Los conectores van al final — dependen de que los módulos estén inicializados
|
|
111
|
-
system.addConnector('pedido-inventario', conectarPedidoConInventario)
|
|
112
|
-
system.addConnector('pedido-notificaciones', conectarPedidoConNotificaciones)
|
|
113
|
-
|
|
114
|
-
await system.start()
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
**Orden obligatorio:** módulos → conectores → start.
|
|
118
|
-
|
|
119
|
-
---
|
|
120
|
-
|
|
121
|
-
## 5. NAMING CONVENTION
|
|
122
|
-
|
|
123
|
-
```
|
|
124
|
-
src/connectors/
|
|
125
|
-
{módulo-origen}-{módulo-destino}.ts → pedido-inventario.ts
|
|
126
|
-
{evento}-{acción}.ts → nuevo-usuario-bienvenida.ts
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
El nombre del conector en `addConnector()` es solo para logging — puede ser descriptivo:
|
|
130
|
-
```ts
|
|
131
|
-
system.addConnector('pedido→inventario:descontar-stock', conectarPedidoConInventario)
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
---
|
|
135
|
-
|
|
136
|
-
## 6. CONECTOR CON MÚLTIPLES MÓDULOS
|
|
137
|
-
|
|
138
|
-
Un conector puede coordinar más de 2 módulos, siempre que solo delegue:
|
|
5
|
+
Los conectores SOLO inyectan dependencia, NO tienen lógica de negocio ni adaptación. Un conector que parsea, transforma, o valida está mal.
|
|
139
6
|
|
|
140
7
|
```ts
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const auditoria = ctx.resolveModule<AuditoriaService>('auditoria')
|
|
146
|
-
|
|
147
|
-
pedidos.setSockets({
|
|
148
|
-
onPedidoConfirmado: async (pedido) => {
|
|
149
|
-
// Cada llamada es una delegación — sin lógica en el medio
|
|
150
|
-
await inventario.descontarStock(pedido.productoId, pedido.cantidad)
|
|
151
|
-
await notificaciones.enviarConfirmacion(pedido.usuarioId, pedido.id)
|
|
152
|
-
await auditoria.registrar('pedido.confirmado', { pedidoId: pedido.id })
|
|
153
|
-
},
|
|
154
|
-
})
|
|
8
|
+
// ✅ bien
|
|
9
|
+
class MariaDBConnector {
|
|
10
|
+
readonly pool: Pool
|
|
11
|
+
constructor() { this.pool = new Pool({ host: 'localhost' }) }
|
|
155
12
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
---
|
|
159
|
-
|
|
160
|
-
## 7. SOCKETS DENTRO VS FUERA DE TRANSACCIÓN
|
|
161
|
-
|
|
162
|
-
Este es el error más silencioso del patrón sockets. **Hay dos tipos de socket calls:**
|
|
163
|
-
|
|
164
|
-
### Dentro de `transactor.run()` — SOLO lógica DB crítica
|
|
13
|
+
export const db = new MariaDBConnector()
|
|
165
14
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
await this.sockets.onCitaCreada?.(cita) // ← SOLO ops DB que deben revertirse si fallan
|
|
171
|
-
})
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
Reglas:
|
|
175
|
-
- El handler DEBE poder fallar y revertir la transacción completa
|
|
176
|
-
- NO hacer llamadas HTTP (WhatsApp, email) — si el HTTP falla, la transacción falla pero el booking ya ocurrió en la DB
|
|
177
|
-
- Solo poner lógica que tenga sentido dentro de una transacción (ej: reservar un slot)
|
|
178
|
-
|
|
179
|
-
### Post-commit — notificaciones y side effects
|
|
180
|
-
|
|
181
|
-
```ts
|
|
182
|
-
// Después del transactor — la cita ya existe, no se puede revertir
|
|
183
|
-
await this.sockets.onCitaCreadaCommitted?.(cita) // WhatsApp, email, CRM, etc.
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
Reglas:
|
|
187
|
-
- Si el handler falla, NO revierte la transacción (ya committeó)
|
|
188
|
-
- Ideal para notificaciones, actualizaciones de otros sistemas
|
|
189
|
-
- Puede hacer HTTP, enviar mensajes, actualizar servicios externos
|
|
190
|
-
|
|
191
|
-
### Convención de nombres
|
|
192
|
-
|
|
193
|
-
```ts
|
|
194
|
-
// sockets.ts — separar explícitamente por contexto de ejecución
|
|
195
|
-
export interface MiModuloSockets {
|
|
196
|
-
// Corre DENTRO de transactor.run() — para lógica DB crítica
|
|
197
|
-
onItemCreado?: (item: ItemDTO) => Promise<void>
|
|
198
|
-
// Corre DESPUÉS del commit — para notificaciones y side effects
|
|
199
|
-
onItemCreadoCommitted?: (item: ItemDTO) => Promise<void>
|
|
15
|
+
// ❌ mal — lógica aquí
|
|
16
|
+
class MariaDBConnector {
|
|
17
|
+
getConnection() { return this.pool }
|
|
18
|
+
async query(sql) { /* no */ }
|
|
200
19
|
}
|
|
201
20
|
```
|
|
202
21
|
|
|
203
|
-
|
|
22
|
+
## Conectores incluidos
|
|
204
23
|
|
|
205
|
-
|
|
24
|
+
| Conector | Archivo | Propósito |
|
|
25
|
+
|----------|---------|-----------|
|
|
26
|
+
| `DatabaseConnector` | `/connectors/database.ts` | Pool de conexión DB |
|
|
27
|
+
| `CacheConnector` | `/connectors/cache.ts` | Redis/Memoria |
|
|
28
|
+
| `StorageConnector` | `/connectors/storage.ts` | S3/local |
|
|
29
|
+
| `QueueConnector` | `/connectors/queue.ts` | RabbitMQ/Redis |
|
|
206
30
|
|
|
207
|
-
##
|
|
208
|
-
|
|
209
|
-
Cuando muchos módulos necesitan reaccionar al mismo evento sin conocerse entre sí:
|
|
31
|
+
## Error común
|
|
210
32
|
|
|
211
33
|
```ts
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const events = new EventBus()
|
|
216
|
-
|
|
217
|
-
// Módulo pedidos publica
|
|
218
|
-
pedidos.setSockets({
|
|
219
|
-
onPedidoConfirmado: async (pedido) => {
|
|
220
|
-
events.emit('pedido.confirmado', pedido, 'pedidos')
|
|
221
|
-
},
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
// Múltiples suscriptores — no se conocen entre sí
|
|
225
|
-
events.on('pedido.confirmado', async (event) => {
|
|
226
|
-
await inventario.descontarStock(event.data.productoId, event.data.cantidad)
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
events.on('pedido.confirmado', async (event) => {
|
|
230
|
-
await notificaciones.enviarConfirmacion(event.data.usuarioId, event.data.id)
|
|
231
|
-
})
|
|
34
|
+
class MySQLConnector { /* ... */ }
|
|
35
|
+
export const mysql = new MySQLConnector() // ← export con nombre concreto
|
|
232
36
|
```
|
|
233
37
|
|
|
234
|
-
**Usar EventBus cuando:** múltiples módulos reaccionan al mismo evento y no querés un conector gigante.
|
|
235
|
-
**Usar conectores directos cuando:** la coordinación es entre 2 módulos específicos y el flujo es claro.
|
|
236
|
-
|
|
237
|
-
---
|
|
238
|
-
|
|
239
|
-
## 8. ANTI-PATRONES DE CONECTORES
|
|
240
|
-
|
|
241
38
|
```ts
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
// ✅ Solo desde index.ts (puerta pública)
|
|
246
|
-
import type { PedidosService } from '../modules/pedidos'
|
|
247
|
-
|
|
248
|
-
// ❌ Lógica de negocio en el conector (viola REGLA #3)
|
|
249
|
-
pedidos.setSockets({
|
|
250
|
-
onPedidoConfirmado: async (pedido) => {
|
|
251
|
-
if (pedido.total > 1000) {
|
|
252
|
-
await inventario.priorizar(pedido.productoId) // decisión de negocio — NO acá
|
|
253
|
-
} else {
|
|
254
|
-
await inventario.descontarStock(pedido.productoId, pedido.cantidad)
|
|
255
|
-
}
|
|
256
|
-
},
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
// ✅ Lógica en el módulo, conector solo llama
|
|
260
|
-
pedidos.setSockets({
|
|
261
|
-
onPedidoConfirmado: async (pedido) => {
|
|
262
|
-
await inventario.procesarPedido(pedido) // inventario decide cómo procesar
|
|
263
|
-
},
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
// ❌ Conector escribe directamente en tabla de otro módulo (viola REGLA #4)
|
|
267
|
-
pedidos.setSockets({
|
|
268
|
-
onPedidoConfirmado: async (pedido) => {
|
|
269
|
-
await orm.update('Inventario', pedido.productoId, { stock: nuevoStock }) // NO
|
|
270
|
-
},
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
// ❌ Conector en composition-root antes de módulos
|
|
274
|
-
system.addConnector(...) // NO
|
|
275
|
-
system.addModule(PedidosModule()) // los módulos deben ir primero
|
|
39
|
+
class DatabaseConnector { /* ... */ }
|
|
40
|
+
export const db = new DatabaseConnector() // ← export genérico (desacoplado)
|
|
276
41
|
```
|
|
277
42
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
## 9. TESTEAR CONECTORES
|
|
281
|
-
|
|
282
|
-
Los conectores son difíciles de testear en aislamiento — testear los módulos que emiten/reciben:
|
|
283
|
-
|
|
284
|
-
```ts
|
|
285
|
-
// Testear que el service llama a los sockets cuando corresponde
|
|
286
|
-
test('confirmar pedido dispara onPedidoConfirmado', async () => {
|
|
287
|
-
const socketFn = mock(() => Promise.resolve())
|
|
288
|
-
service.setSockets({ onPedidoConfirmado: socketFn })
|
|
289
|
-
|
|
290
|
-
await service.confirmar('pedido-123')
|
|
291
|
-
|
|
292
|
-
expect(socketFn).toHaveBeenCalledTimes(1)
|
|
293
|
-
expect(socketFn).toHaveBeenCalledWith(
|
|
294
|
-
expect.objectContaining({ estado: 'confirmado' })
|
|
295
|
-
)
|
|
296
|
-
})
|
|
297
|
-
|
|
298
|
-
// Testear que funciona SIN sockets (optional chaining)
|
|
299
|
-
test('confirmar pedido funciona sin sockets registrados', async () => {
|
|
300
|
-
// sin setSockets() — sockets = {}
|
|
301
|
-
await expect(service.confirmar('pedido-123')).resolves.toBeDefined()
|
|
302
|
-
})
|
|
303
|
-
```
|
|
43
|
+
## Cómo extender
|
|
304
44
|
|
|
305
|
-
|
|
45
|
+
Crear archivo en `/connectors/`, export default, registrar en composition-root. Sin lógica de negocio.
|
|
306
46
|
|
|
307
|
-
##
|
|
47
|
+
## Troubleshooting
|
|
308
48
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
- [ ] Registrado en composition-root DESPUÉS de los módulos
|
|
315
|
-
- [ ] El módulo funciona correctamente sin el conector (sockets son opcionales)
|
|
316
|
-
- [ ] `arckode analyze` detecta: `CONNECTOR_BUSINESS_LOGIC` si hay lógica
|
|
49
|
+
| Problema | Causa | Fix |
|
|
50
|
+
|----------|-------|-----|
|
|
51
|
+
| Pool timeout | Sin `max` en pool config | `pool: new Pool({ max: 10 })` |
|
|
52
|
+
| Reconexión no automática | Driver no la soporta | Envolver en wrapper que reconecta |
|
|
53
|
+
| Conector con lógica | Parseo/validación dentro | Mover a helper/utils |
|
package/skills/helpers/SKILL.md
CHANGED
|
@@ -1,206 +1,37 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Helpers — Funciones Puras Compartidas
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Reglas
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
1. Función pura → mismo input = mismo output (sin I/O)
|
|
6
|
+
2. Sin estado, sin side effects
|
|
7
|
+
3. Test solo si lógica compleja (regex, cálculos)
|
|
6
8
|
|
|
7
|
-
##
|
|
9
|
+
## Built-in
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
| Helper | Propósito | Ejemplo |
|
|
12
|
+
|--------|-----------|---------|
|
|
13
|
+
| `formatDate` | ISO→legible | `formatDate('2024-01-15')` → `'15 Ene 2024'` |
|
|
14
|
+
| `slugify` | String→URL slug | `slugify('Hola Mundo')` → `'hola-mundo'` |
|
|
15
|
+
| `paginate` | Offset calc | `paginate(page, limit)` → `{ skip, take }` |
|
|
16
|
+
| `maskEmail` | Privacy | `maskEmail('user@test.com')` → `'us***@test.com'` |
|
|
17
|
+
| `sleep` | Async delay | `await sleep(1000)` |
|
|
18
|
+
| `generateId` | UUID | `generateId()` |
|
|
19
|
+
| `deepClone` | Deep copy | `deepClone(obj)` |
|
|
10
20
|
|
|
11
|
-
|
|
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
|
|
21
|
+
## Helper personalizado
|
|
30
22
|
|
|
31
23
|
```ts
|
|
32
|
-
//
|
|
33
|
-
export function
|
|
34
|
-
return
|
|
35
|
-
style: 'currency',
|
|
36
|
-
currency: moneda,
|
|
37
|
-
}).format(monto)
|
|
24
|
+
// helpers/string.utils.ts — no helpers/index.ts
|
|
25
|
+
export function truncate(str: string, max: number): string {
|
|
26
|
+
return str.length > max ? str.slice(0, max) + '...' : str
|
|
38
27
|
}
|
|
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}/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
28
|
```
|
|
104
29
|
|
|
105
|
-
|
|
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
|
|
30
|
+
## Errores silenciosos
|
|
200
31
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
-
|
|
32
|
+
| Error | Señal | Fix |
|
|
33
|
+
|-------|-------|-----|
|
|
34
|
+
| Helper con side effect | Llama API o escribe archivo | Separar en service |
|
|
35
|
+
| Test innecesario | Helper simple (`add(a,b)`) | Confiar en tipo |
|
|
36
|
+
| Export default | Import ambiguo | Export named |
|
|
37
|
+
| Over-engineering | `slugify` en 30 líneas con regex complejo | Usar librería `slug` |
|