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,316 +1,53 @@
1
- # SKILL: Arckode Connectors Puentes entre módulos
1
+ # Conectores DB, Cache, Storage, etc.
2
2
 
3
- > Activar cuando: dos módulos necesitan comunicarse, implementar side effects entre módulos, crear eventos cross-módulo.
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
- export function conectarPedidoCompleto(ctx: ConnectorContext): void {
142
- const pedidos = ctx.resolveModule<PedidosService>('pedidos')
143
- const inventario = ctx.resolveModule<InventarioService>('inventario')
144
- const notificaciones = ctx.resolveModule<NotificacionesService>('notificaciones')
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
- ```ts
167
- // citas/service.ts
168
- await this.transactor.run(async () => {
169
- cita = await this.repo.create({ ... })
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
- **Regla crítica:** si un socket dispara una llamada HTTP (WhatsApp, email, push notification), DEBE ser un evento `*Committed` post-transacción.
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
- ## 8. EVENTBUS (alternativa para eventos broadcast)
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
- // En composition-root.ts
213
- import { EventBus } from 'arckode-framework/events'
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
- // Importar desde ./service directamente (viola REGLA #1)
243
- import { PedidosService } from '../modules/pedidos/service' // NO
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
- ## 10. CHECKLIST CONECTORES
47
+ ## Troubleshooting
308
48
 
309
- - [ ] Solo importa tipos (no implementaciones) desde `index.ts` de módulos
310
- - [ ] Sin `if`, `for`, ni lógica de negocio — solo llamadas de delegación
311
- - [ ] `setSockets()` usa el patrón ACUMULATIVO (for loop + chaining) — NUNCA `{ ...this.sockets, ...s }`
312
- - [ ] Sockets con HTTP/WhatsApp/email usan eventos `*Committed` nunca dentro de `transactor.run()`
313
- - [ ] `setSockets()` usa `?.` al llamar los hooks (son opcionales)
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 |
@@ -1,206 +1,37 @@
1
- # SKILL: Arckode Helpers y Static Código compartido y archivos estáticos
1
+ # Helpers — Funciones Puras Compartidas
2
2
 
3
- > Activar cuando: crear una función utilitaria compartida, servir el frontend compilado, modo monolito, `arckode make:helper`.
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
- ## 1. HELPERS PUROS (shared/helpers/)
9
+ ## Built-in
8
10
 
9
- Los helpers son funciones puras sin estado, sin efectos secundarios, sin imports del framework. Cualquier módulo puede importarlos directamente (la única excepción a "no importar de otros módulos").
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
- ### Generar un helper
12
-
13
- ```bash
14
- arckode make:helper formato-precio
15
- # Crea: src/shared/helpers/formato-precio.ts
16
- ```
17
-
18
- ### Estructura generada
19
-
20
- ```ts
21
- // src/shared/helpers/formato-precio.ts
22
- // Helpers puros — sin estado, sin efectos secundarios, sin dependencias externas
23
-
24
- export function formatoPrecioExample(input: string): string {
25
- return input.trim()
26
- }
27
- ```
28
-
29
- ### Implementar el helper real
21
+ ## Helper personalizado
30
22
 
31
23
  ```ts
32
- // src/shared/helpers/formato-precio.ts
33
- export function formatearPrecio(monto: number, moneda = 'DOP'): string {
34
- return new Intl.NumberFormat('es-DO', {
35
- style: 'currency',
36
- currency: moneda,
37
- }).format(monto)
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
- ```ts
106
- // src/shared/types.ts
107
- export interface ApiResponse<T> {
108
- data: T
109
- message?: string
110
- }
111
-
112
- export interface Paginado<T> {
113
- data: T[]
114
- total: number
115
- page: number
116
- limit: number
117
- totalPages: number
118
- }
119
-
120
- export interface ErrorResponse {
121
- error: string
122
- errors?: Record<string, string>
123
- }
124
- ```
125
-
126
- ---
127
-
128
- ## 4. STATIC SERVER (modo monolito)
129
-
130
- Sirve el frontend compilado desde el mismo servidor Node.js. Para proyectos fullstack donde el backend y frontend corren en el mismo proceso.
131
-
132
- ### Setup en composition-root.ts
133
-
134
- ```ts
135
- import { serveStatic } from 'arckode-framework/static'
136
-
137
- // Después de registrar todos los módulos y rutas del API
138
- // IMPORTANTE: va al final — no interceptar rutas del API
139
- serveStatic(router, './frontend/dist', {
140
- prefix: '', // sin prefijo — sirve desde /
141
- fallback: 'index.html', // para SPA (Vue Router, React Router)
142
- cacheControl: 'public, max-age=3600', // 1 hora de cache
143
- })
144
- ```
145
-
146
- ### Separar API de archivos estáticos
147
-
148
- ```ts
149
- // API bajo /api/* — registrar primero
150
- router.get('/api/productos', req => ...)
151
- router.post('/api/auth/login', req => ...)
152
-
153
- // Luego los estáticos — captura todo lo demás
154
- serveStatic(router, './frontend/dist', {
155
- prefix: '',
156
- fallback: 'index.html',
157
- })
158
- ```
159
-
160
- ### Servir uploads públicos
161
-
162
- ```ts
163
- import { serveStatic } from 'arckode-framework/static'
164
-
165
- // Archivos subidos por usuarios (no del frontend)
166
- serveStatic(router, './uploads', {
167
- prefix: '/uploads',
168
- cacheControl: 'public, max-age=86400', // 24h — cambian por nombre único
169
- })
170
- ```
171
-
172
- ### MIME types soportados automáticamente
173
-
174
- `.html`, `.css`, `.js`, `.json`, `.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`, `.ico`, `.woff`, `.woff2`, `.webp`, `.pdf`
175
-
176
- ---
177
-
178
- ## 5. ESTRUCTURA RECOMENDADA DEL PROYECTO
179
-
180
- ```
181
- src/
182
- ├── composition-root.ts
183
- ├── modules/
184
- │ ├── productos/
185
- │ └── pedidos/
186
- ├── connectors/
187
- │ └── pedido-stock.ts
188
- └── shared/ ← código compartido
189
- ├── constants.ts ← constantes de dominio
190
- ├── types.ts ← tipos/interfaces compartidas
191
- └── helpers/
192
- ├── formato-precio.ts ← helpers puros
193
- ├── fecha.ts
194
- └── slugify.ts
195
- ```
196
-
197
- ---
198
-
199
- ## 6. CHECKLIST HELPERS
30
+ ## Errores silenciosos
200
31
 
201
- - [ ] El helper es una función pura — sin imports de ORM, cache, mail, etc.
202
- - [ ] Generado con `arckode make:helper nombre` para que quede en `shared/helpers/`
203
- - [ ] Testeable sin mocks (función pura test directo)
204
- - [ ] Constantes compartidas en `shared/constants.ts`, no duplicadas en cada módulo
205
- - [ ] `serveStatic()` registrado DESPUÉS de todas las rutas de API
206
- - [ ] `fallback: 'index.html'` para proyectos con SPA routing
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` |