arckode-framework 1.0.0

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