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,206 @@
1
+ # SKILL: Arckode Helpers y Static — Código compartido y archivos estáticos
2
+
3
+ > Activar cuando: crear una función utilitaria compartida, servir el frontend compilado, modo monolito, `arckode make:helper`.
4
+
5
+ ---
6
+
7
+ ## 1. HELPERS PUROS (shared/helpers/)
8
+
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").
10
+
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
30
+
31
+ ```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)
38
+ }
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}/actions/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
+ ```
104
+
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
200
+
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
@@ -0,0 +1,282 @@
1
+ # SKILL: Arckode Middlewares — Globales y por Ruta
2
+
3
+ > Activar cuando: agregar CORS, rate limit, timeout, logging, compresión, body limit, auth en rutas, o crear un middleware propio.
4
+
5
+ ---
6
+
7
+ ## 1. IMPORT
8
+
9
+ ```ts
10
+ import {
11
+ cors,
12
+ rateLimit,
13
+ requestLogger,
14
+ bodyLimit,
15
+ timeout,
16
+ compression,
17
+ requireAuth,
18
+ } from 'arckode-framework/middlewares'
19
+ ```
20
+
21
+ ---
22
+
23
+ ## 2. MIDDLEWARES GLOBALES (composition-root.ts)
24
+
25
+ Los globales se aplican a TODAS las rutas. Registrar en este orden:
26
+
27
+ ```ts
28
+ import { cors, rateLimit, requestLogger, bodyLimit, timeout, compression } from 'arckode-framework/middlewares'
29
+
30
+ // Orden obligatorio — cada uno depende del anterior
31
+ router.use(timeout(10_000)) // 1. Cortar requests colgados antes que todo
32
+ router.use(cors({ origins: ['https://mi-app.com'], credentials: true })) // 2. CORS
33
+ router.use(bodyLimit(2 * 1024 * 1024)) // 3. Rechazar bodies > 2MB antes de parsear
34
+ router.use(requestLogger(logger)) // 4. Loguear método, path, status, duración
35
+ router.use(compression()) // 5. Gzip para responses > 1KB (opcional)
36
+ // rate limit: ver sección 3 — puede ser global o por ruta
37
+ ```
38
+
39
+ **El orden importa:** si `timeout` va al final, no puede cortar las fases anteriores.
40
+
41
+ ---
42
+
43
+ ## 3. CORS
44
+
45
+ ```ts
46
+ // Permitir cualquier origen (solo para desarrollo/APIs públicas)
47
+ router.use(cors())
48
+
49
+ // Origenes específicos (producción)
50
+ router.use(cors({
51
+ origins: ['https://mi-app.com', 'https://admin.mi-app.com'],
52
+ methods: ['GET', 'POST', 'PUT', 'DELETE'],
53
+ headers: ['Content-Type', 'Authorization'],
54
+ credentials: true, // necesario si el frontend envía cookies o Authorization
55
+ }))
56
+ ```
57
+
58
+ **Preflight:** el middleware maneja `OPTIONS` automáticamente y retorna 204.
59
+
60
+ ---
61
+
62
+ ## 4. RATE LIMIT
63
+
64
+ ```ts
65
+ // Global: 100 requests por minuto por IP
66
+ const limiter = rateLimit({ windowMs: 60_000, max: 100 })
67
+ router.use(limiter)
68
+
69
+ // Por ruta específica (más restrictivo para endpoints sensibles)
70
+ const loginLimiter = rateLimit({
71
+ windowMs: 15 * 60_000, // 15 minutos
72
+ max: 5, // máximo 5 intentos
73
+ keyBy: (req) => req.headers['x-forwarded-for'] as string ?? 'anon',
74
+ })
75
+ router.post('/auth/login', [loginLimiter], req => controller.login(req))
76
+
77
+ // Rate limit por usuario autenticado (después de que auth middleware corra)
78
+ const userLimiter = rateLimit({
79
+ windowMs: 60_000,
80
+ max: 200,
81
+ keyBy: (req) => req.user?.id ?? req.headers['x-forwarded-for'] as string ?? 'anon',
82
+ })
83
+
84
+ // Resetear límite de un usuario (ej: tras verificación de captcha)
85
+ limiter.reset('192.168.1.1')
86
+ ```
87
+
88
+ **Importante:** El rate limiter usa memoria en proceso — no persiste entre reinicios ni entre múltiples instancias. Para producción distribuida → implementar `RateLimitAdapter` con Redis.
89
+
90
+ ---
91
+
92
+ ## 5. REQUEST LOGGER
93
+
94
+ ```ts
95
+ // Loguea: "POST /auth/login", "POST /auth/login 200 43ms"
96
+ router.use(requestLogger(logger))
97
+
98
+ // El logger hijo del módulo también puede usarse
99
+ router.use(requestLogger(logger.child('http')))
100
+ ```
101
+
102
+ Output en logs:
103
+ ```
104
+ [app.http] POST /auth/login { requestId: 'abc-123' }
105
+ [app.http] POST /auth/login 200 43ms { requestId: 'abc-123', status: 200, duration: 43 }
106
+ ```
107
+
108
+ ---
109
+
110
+ ## 6. BODY LIMIT
111
+
112
+ ```ts
113
+ // Default: 1MB
114
+ router.use(bodyLimit())
115
+
116
+ // Custom: 10MB (para endpoints de upload)
117
+ router.use(bodyLimit(10 * 1024 * 1024))
118
+
119
+ // Por ruta (más permisivo solo en upload)
120
+ router.post('/archivos/upload', [bodyLimit(50 * 1024 * 1024)], req => controller.upload(req))
121
+ ```
122
+
123
+ Retorna **413** si el body supera el límite.
124
+
125
+ ---
126
+
127
+ ## 7. TIMEOUT
128
+
129
+ ```ts
130
+ // Default: 5 segundos
131
+ router.use(timeout())
132
+
133
+ // Custom por aplicación
134
+ router.use(timeout(10_000)) // 10s
135
+
136
+ // Más permisivo para endpoints lentos (reportes, exports)
137
+ router.get('/reportes/export', [timeout(60_000)], req => controller.export(req))
138
+ ```
139
+
140
+ Lanza `InternalError` si el handler no responde en tiempo.
141
+
142
+ ---
143
+
144
+ ## 8. COMPRESSION (gzip)
145
+
146
+ ```ts
147
+ // Comprime responses JSON > 1KB si el cliente acepta gzip
148
+ router.use(compression())
149
+
150
+ // Custom threshold
151
+ router.use(compression({ threshold: 512 })) // comprimir si > 512 bytes
152
+ ```
153
+
154
+ **No aplica a:** SSE streams, responses `null`, responses que ya tienen `Content-Encoding`.
155
+
156
+ ---
157
+
158
+ ## 9. MIDDLEWARES POR RUTA
159
+
160
+ ```ts
161
+ // Un middleware
162
+ router.get('/admin', [auth.authenticate('admin')], req => controller.dashboard(req))
163
+
164
+ // Múltiples middlewares (se ejecutan en orden)
165
+ router.post(
166
+ '/auth/login',
167
+ [loginLimiter, bodyLimit(10_000)],
168
+ req => controller.login(req)
169
+ )
170
+
171
+ // Auth + role específico
172
+ router.delete(
173
+ '/usuarios/:id',
174
+ [auth.authenticate('admin', 'superadmin')],
175
+ req => controller.eliminar(req)
176
+ )
177
+ ```
178
+
179
+ ---
180
+
181
+ ## 10. CREAR UN MIDDLEWARE PROPIO
182
+
183
+ ```ts
184
+ import type { MiddlewareHandler } from 'arckode-framework'
185
+
186
+ // Middleware que verifica un API key en el header
187
+ export function apiKeyAuth(validKeys: string[]): MiddlewareHandler {
188
+ return async (req, next) => {
189
+ const key = req.headers['x-api-key'] as string
190
+ if (!key || !validKeys.includes(key)) {
191
+ return { status: 401, body: { error: 'API key inválida' } }
192
+ }
193
+ return next() // siempre llamar next() para continuar la cadena
194
+ }
195
+ }
196
+
197
+ // Middleware de auditoría
198
+ export function auditLog(repo: RepositoryAdapter<AuditoriaDTO>): MiddlewareHandler {
199
+ return async (req, next) => {
200
+ const res = await next()
201
+ if (req.method !== 'GET' && res.status < 400) {
202
+ await repo.create({
203
+ userId: req.user?.id ?? 'anon',
204
+ action: `${req.method} ${req.path}`,
205
+ status: res.status,
206
+ } as any)
207
+ }
208
+ return res
209
+ }
210
+ }
211
+
212
+ // Registrar
213
+ router.use(apiKeyAuth(['key-secreta-1', 'key-secreta-2']))
214
+ ```
215
+
216
+ **Regla:** siempre llamar `await next()` y retornar su resultado — excepto cuando querés cortar la cadena (retornar error directo, como en el ejemplo de apiKeyAuth).
217
+
218
+ ---
219
+
220
+ ## 11. CADENA DE EJECUCIÓN
221
+
222
+ ```
223
+ Request entrante
224
+ → timeout (global)
225
+ → cors (global)
226
+ → bodyLimit (global)
227
+ → requestLogger (global)
228
+ → compression (global)
229
+ → loginLimiter (por ruta, si aplica)
230
+ → auth.authenticate (por ruta, si aplica)
231
+ → handler (controller)
232
+ ← Response saliente
233
+ ```
234
+
235
+ Los middlewares globales corren ANTES que los de ruta. El error handler del router atrapa excepciones de toda la cadena.
236
+
237
+ ---
238
+
239
+ ## 12. ANTI-PATRONES
240
+
241
+ ```ts
242
+ // ❌ Middleware global que modifica req.body con lógica de negocio
243
+ router.use(async (req, next) => {
244
+ req.body.precio = req.body.precio * 1.21 // impuesto — NO acá, va en el service
245
+ return next()
246
+ })
247
+
248
+ // ❌ Olvidar llamar next() — la request queda colgada
249
+ router.use(async (req, next) => {
250
+ if (req.headers['x-tenant']) {
251
+ // ... pero nunca llama next()
252
+ }
253
+ // cuelga para requests sin x-tenant
254
+ })
255
+
256
+ // ❌ Lógica de autenticación duplicada en cada handler
257
+ router.get('/datos', async (req) => {
258
+ const token = req.headers['authorization']
259
+ if (!token) return { status: 401, body: { error: '...' } } // NO
260
+ // usar auth.authenticate() como middleware por ruta
261
+ })
262
+
263
+ // ✅ Cortar la cadena cuando es correcto (no llamar next)
264
+ router.use(async (req, next) => {
265
+ if (req.method === 'OPTIONS') {
266
+ return { status: 204, body: null } // cortar la cadena intencionalmente
267
+ }
268
+ return next()
269
+ })
270
+ ```
271
+
272
+ ---
273
+
274
+ ## 13. CHECKLIST MIDDLEWARES
275
+
276
+ - [ ] `timeout()` es el PRIMERO en los globales
277
+ - [ ] `cors()` tiene `origins` específicos en producción (no `'*'`)
278
+ - [ ] `bodyLimit()` registrado antes de endpoints que reciben JSON
279
+ - [ ] Rate limiter en endpoints de login/registro (máximo 5/15min)
280
+ - [ ] Middlewares de ruta van en array: `[auth.authenticate('admin')]`
281
+ - [ ] Todo middleware custom llama `await next()` (excepto cuando corta intencionalmente)
282
+ - [ ] No hay lógica de negocio en middlewares — solo cross-cutting concerns