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 Services — Mail y Storage
2
+
3
+ > Activar cuando: enviar emails, subir archivos, gestionar attachments, servir uploads.
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
38
+
39
+ ```ts
40
+ // Email simple
41
+ await mail.send({
42
+ to: { address: 'user@example.com', name: 'Juan' },
43
+ subject: 'Bienvenido',
44
+ html: '<h1>Hola Juan!</h1>',
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 actions/service.ts
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
98
+ }
99
+ }
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
+
108
+ ```ts
109
+ const mockMail = { send: mock(() => Promise.resolve()), sendWelcome: mock(() => Promise.resolve()) }
110
+ const service = new AuthService(repo, auth, mockMail as any, logger)
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' })
140
+ ```
141
+
142
+ ---
143
+
144
+ ### API del StorageService
145
+
146
+ ```ts
147
+ // Subir archivo
148
+ const stored = await storage.upload(fileUpload, 'avatars')
149
+ // → { url: '/uploads/avatars/1234-abc.png', path: 'avatars/1234-abc.png', ... }
150
+
151
+ // Eliminar archivo
152
+ await storage.delete('avatars/1234-abc.png')
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
161
+
162
+ ```ts
163
+ // En el controller — el framework parsea multipart/form-data automáticamente
164
+ async uploadAvatar(req: HttpRequest): Promise<HttpResponse> {
165
+ const file = req.files?.['avatar'] // FileUpload
166
+ if (!file) throw new ValidationError('Archivo requerido', { avatar: 'requerido' })
167
+ if (!file.mimeType.startsWith('image/')) {
168
+ throw new ValidationError('Formato inválido', { avatar: 'debe ser una imagen' })
169
+ }
170
+ if (file.size > 5 * 1024 * 1024) {
171
+ throw new ValidationError('Archivo muy grande', { avatar: 'máximo 5MB' })
172
+ }
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
+ }
184
+ ```
185
+
186
+ ---
187
+
188
+ ### Inyectar StorageService en un módulo
189
+
190
+ ```ts
191
+ // ModuleDependencies incluye storage si fue pasado al System
192
+ create({ logger, orm, router, storage }: ModuleDependencies) {
193
+ const service = new UsuariosService(repo, storage, logger.child('usuarios'))
194
+ ...
195
+ }
196
+ ```
197
+
198
+ ---
199
+
200
+ ### Checklist Mail + Storage
201
+
202
+ - [ ] `SMTP_*` variables en .env (nunca hardcodeadas)
203
+ - [ ] `mail.send()` con try/catch si el email no es crítico para la operación
204
+ - [ ] Validar `mimeType` y `size` antes de `storage.upload()`
205
+ - [ ] `serveStatic()` configurado si se necesita acceso público a los archivos
206
+ - [ ] Mockear ambos servicios en tests (no enviar emails reales en tests)
@@ -0,0 +1,257 @@
1
+ # SKILL: Arckode Testing — Patrones, Utilidades y Estructura
2
+
3
+ > Activar cuando: escribir tests, configurar test suite, debuggear tests fallidos, agregar cobertura.
4
+
5
+ ---
6
+
7
+ ## 1. SETUP DEL TEST (sin BD real)
8
+
9
+ ```ts
10
+ import { describe, test, expect, beforeEach, mock } from 'bun:test'
11
+ import { createRecordingOrm, createSilentLogger, createNullCache } from 'arckode-framework/testing'
12
+ import { OrmRepository } from 'arckode-framework'
13
+ import { ProductosService } from '../actions/service'
14
+ import type { ProductoDTO } from '../types'
15
+
16
+ describe('ProductosService', () => {
17
+ let service: ProductosService
18
+
19
+ beforeEach(() => {
20
+ const orm = createRecordingOrm() // captura SQL sin ejecutar BD
21
+ const repo = new OrmRepository<ProductoDTO>(orm, 'Producto')
22
+ service = new ProductosService(repo, createSilentLogger(), createNullCache())
23
+ })
24
+ })
25
+ ```
26
+
27
+ **Regla de oro:** Testear el service, no el ORM. El ORM ya tiene sus propios tests.
28
+
29
+ ---
30
+
31
+ ## 2. UTILIDADES DE TESTING
32
+
33
+ ```ts
34
+ import {
35
+ createRecordingOrm, // ORM que captura queries sin ejecutar nada
36
+ createSilentLogger, // Logger que descarta todo (sin ruido en tests)
37
+ createNullCache, // Cache que nunca cachea (siempre miss)
38
+ createTestClient, // Cliente HTTP para integration tests
39
+ } from 'arckode-framework/testing'
40
+
41
+ // RecordingOrm — verificar queries generadas sin BD
42
+ const orm = createRecordingOrm()
43
+ await service.listar()
44
+ const queries = orm.getRecordedQueries()
45
+ expect(queries[0]).toContain('SELECT')
46
+
47
+ // TestClient — integration tests con router real
48
+ const client = createTestClient(router)
49
+ const res = await client.get('/productos')
50
+ expect(res.status).toBe(200)
51
+ expect(res.body).toBeArray()
52
+ ```
53
+
54
+ ---
55
+
56
+ ## 3. ESTRUCTURA MÍNIMA OBLIGATORIA
57
+
58
+ Cada módulo necesita al menos 2 casos:
59
+
60
+ ```ts
61
+ describe('ProductosService', () => {
62
+
63
+ // CASO 1: Happy path
64
+ test('listar retorna array (vacío si no hay datos)', async () => {
65
+ const result = await service.listar()
66
+ expect(Array.isArray(result)).toBe(true)
67
+ })
68
+
69
+ // CASO 2: Error esperado (validación, not found, conflict, etc.)
70
+ test('crear lanza error si el nombre está vacío', async () => {
71
+ await expect(service.crear({ nombre: '', precio: 100 }))
72
+ .rejects.toThrow()
73
+ })
74
+
75
+ // CASO 3 (recomendado): IDOR / autorización
76
+ test('getById lanza NotFoundError si no existe', async () => {
77
+ await expect(service.getById('id-inexistente', { id: 'u1', role: 'user' }))
78
+ .rejects.toThrow('no encontrado')
79
+ })
80
+ })
81
+ ```
82
+
83
+ **`arckode analyze` detecta:** `TESTS_WITHOUT_CASES` — archivo test sin ningún `test()`.
84
+
85
+ ---
86
+
87
+ ## 4. MOCKEAR DEPENDENCIAS EXTERNAS
88
+
89
+ ```ts
90
+ import { mock } from 'bun:test'
91
+
92
+ // Mockear el mail service en tests que lo usan
93
+ const mockMail = {
94
+ send: mock(() => Promise.resolve()),
95
+ }
96
+
97
+ const service = new AuthService(repo, auth, mockMail as any)
98
+
99
+ test('registro envía email de bienvenida', async () => {
100
+ await service.registrar({ email: 'test@test.com', password: '12345678' })
101
+ expect(mockMail.send).toHaveBeenCalledTimes(1)
102
+ expect(mockMail.send).toHaveBeenCalledWith(
103
+ expect.objectContaining({ to: 'test@test.com' })
104
+ )
105
+ })
106
+ ```
107
+
108
+ ---
109
+
110
+ ## 5. INTEGRATION TESTS (router + controller)
111
+
112
+ ```ts
113
+ import { Router } from 'arckode-framework'
114
+ import { createTestClient } from 'arckode-framework/testing'
115
+
116
+ describe('Productos API', () => {
117
+ let client: ReturnType<typeof createTestClient>
118
+
119
+ beforeEach(() => {
120
+ const router = new Router()
121
+ // montar el módulo de prueba
122
+ const orm = createRecordingOrm()
123
+ const repo = new OrmRepository<ProductoDTO>(orm, 'Producto')
124
+ const service = new ProductosService(repo, createSilentLogger(), createNullCache())
125
+ const controller = new ProductosController(service)
126
+
127
+ router.get('/productos', req => controller.index(req))
128
+ router.post('/productos', req => controller.store(req))
129
+ router.delete('/productos/:id', req => controller.destroy(req))
130
+
131
+ client = createTestClient(router)
132
+ })
133
+
134
+ test('GET /productos → 200', async () => {
135
+ const res = await client.get('/productos')
136
+ expect(res.status).toBe(200)
137
+ expect(Array.isArray(res.body)).toBe(true)
138
+ })
139
+
140
+ test('POST /productos sin nombre → 400', async () => {
141
+ const res = await client.post('/productos', { body: { precio: 100 } })
142
+ expect(res.status).toBe(400)
143
+ expect(res.body).toHaveProperty('errors')
144
+ })
145
+
146
+ test('POST /productos con datos válidos → 201', async () => {
147
+ const res = await client.post('/productos', {
148
+ body: { nombre: 'Zapato', precio: 999 }
149
+ })
150
+ expect(res.status).toBe(201)
151
+ expect(res.body).toHaveProperty('id')
152
+ })
153
+ })
154
+ ```
155
+
156
+ ---
157
+
158
+ ## 6. TESTEAR AUTH (con middleware)
159
+
160
+ ```ts
161
+ import { Auth } from 'arckode-framework'
162
+ import { jwtTokenAdapter } from 'arckode-framework/adapters/jwt'
163
+
164
+ const auth = new Auth(jwtTokenAdapter, 'test-secret-12345678901234567890', createSilentLogger())
165
+ const token = auth.createToken({ id: 'user-1', role: 'user' })
166
+ const adminToken = auth.createToken({ id: 'admin-1', role: 'admin' })
167
+
168
+ test('GET /pedidos sin token → 401', async () => {
169
+ const res = await client.get('/pedidos')
170
+ expect(res.status).toBe(401)
171
+ })
172
+
173
+ test('GET /pedidos con token → 200', async () => {
174
+ const res = await client.get('/pedidos', {
175
+ headers: { Authorization: `Bearer ${token}` }
176
+ })
177
+ expect(res.status).toBe(200)
178
+ })
179
+
180
+ test('DELETE /admin/algo sin admin → 403', async () => {
181
+ const res = await client.delete('/admin/algo', {
182
+ headers: { Authorization: `Bearer ${token}` } // user, no admin
183
+ })
184
+ expect(res.status).toBe(403)
185
+ })
186
+ ```
187
+
188
+ ---
189
+
190
+ ## 7. CORRER TESTS
191
+
192
+ ```bash
193
+ # Todos los tests
194
+ bun test
195
+
196
+ # Tests de un módulo específico
197
+ bun test modules/productos
198
+
199
+ # Un archivo específico
200
+ bun test modules/productos/tests/service.test.ts
201
+
202
+ # Con watch (re-corre al cambiar archivos)
203
+ bun test --watch
204
+
205
+ # Con cobertura
206
+ bun test --coverage
207
+ ```
208
+
209
+ ---
210
+
211
+ ## 8. QUÉ NO TESTEAR
212
+
213
+ - **Los adapters del framework** (SqliteAdapter, jwtTokenAdapter): ya tienen tests propios
214
+ - **El ORM** directamente: testealo via RepositoryAdapter en tu service
215
+ - **El router**: testear el controller con `createTestClient`, no el router directamente
216
+ - **Casos imposibles**: no testear `if` que nunca puede ser `false` con TypeScript strict
217
+
218
+ ---
219
+
220
+ ## 9. ERRORES COMUNES EN TESTS
221
+
222
+ ```ts
223
+ // ❌ Olvidar el await — el test pasa siempre (false positive)
224
+ test('lanza error', () => {
225
+ expect(service.crear({})).rejects.toThrow() // sin await → siempre pasa
226
+ })
227
+
228
+ // ✅ Siempre await en expects async
229
+ test('lanza error', async () => {
230
+ await expect(service.crear({})).rejects.toThrow()
231
+ })
232
+
233
+ // ❌ Describir implementación en lugar de comportamiento
234
+ test('llama a repo.create con los datos correctos', ...)
235
+
236
+ // ✅ Describir el comportamiento observable
237
+ test('crear retorna el producto con id asignado', ...)
238
+
239
+ // ❌ Test que testea el ORM en lugar del service
240
+ test('ORM recibe query INSERT', async () => {
241
+ const orm = createRecordingOrm()
242
+ await service.crear(...)
243
+ expect(orm.getRecordedQueries()[0]).toContain('INSERT') // testear el framework, no tu código
244
+ })
245
+ ```
246
+
247
+ ---
248
+
249
+ ## 10. CHECKLIST TESTING
250
+
251
+ - [ ] Mínimo 2 test cases: happy path + error esperado
252
+ - [ ] Usar `createRecordingOrm` (sin BD real en unit tests)
253
+ - [ ] Usar `createSilentLogger` (sin ruido en consola)
254
+ - [ ] Usar `createNullCache` (cache predecible)
255
+ - [ ] Todo `expect(...).rejects` tiene `await`
256
+ - [ ] Los nombres de test describen comportamiento, no implementación
257
+ - [ ] `bun test` pasa sin errores antes del commit