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,469 @@
1
+ // cli/stubs/module-stub.ts — Plantillas para generar módulos completos
2
+ // Usadas por el generador. Cada stub produce código 100% correcto.
3
+
4
+ export interface ModuleStubParams {
5
+ name: string // PascalCase: Producto
6
+ module: string // kebab-case: productos
7
+ fields: { name: string; type: 'string' | 'number' | 'boolean' | 'date'; required: boolean; default?: unknown }[]
8
+ relations?: { type: 'hasMany' | 'belongsTo'; model: string; foreignKey: string }[]
9
+ softDelete?: boolean
10
+ }
11
+
12
+ export function indexStub(p: ModuleStubParams): string {
13
+ const actions = ['list', 'getById', 'create', 'update', 'delete']
14
+ const events = [`on${p.name}Created`, `on${p.name}Updated`, `on${p.name}Deleted`]
15
+
16
+ return `// ${p.module}/index.ts — PUERTA PÚBLICA
17
+ // Solo esto es visible para otros módulos y conectores.
18
+ // ⚠ REGLA: Append-only. No sacar ni modificar exports existentes.
19
+
20
+ import { createModule, OrmRepository } from 'arckode-framework'
21
+ import { ${p.name}Service } from './actions/service'
22
+ import { ${p.name}Controller } from './actions/controller'
23
+ import type { ${p.name}DTO } from './types'
24
+
25
+ export { ${p.name}Service }
26
+ export type { ${p.name}DTO, Create${p.name}DTO, Update${p.name}DTO, ${p.name}Query, ${p.name}Paginated } from './types'
27
+ export type { ${p.name}Sockets } from './sockets'
28
+ export { ${p.name}Validator, Create${p.name}Schema, Update${p.name}Schema } from './validators/schema'
29
+
30
+ export function ${p.name}Module(sockets?: import('./sockets').${p.name}Sockets) {
31
+ return createModule({
32
+ name: '${p.module}',
33
+ version: '1.0.0',
34
+ description: 'Módulo de ${p.module}',
35
+
36
+ contract: {
37
+ name: '${p.module}',
38
+ version: '1.0.0',
39
+ description: 'Módulo de ${p.module}',
40
+ actions: ${JSON.stringify(actions)},
41
+ events: ${JSON.stringify(events)},
42
+ tables: ['${p.module}'],
43
+ dependencies: [],
44
+ rules: ['No importar de otros módulos'],
45
+ },
46
+
47
+ create({ logger, orm, cache, router }) {
48
+ const repo = new OrmRepository<${p.name}DTO>(orm, '${p.name}')
49
+ const log = logger.child('${p.module}')
50
+ const service = new ${p.name}Service(repo, log, cache, sockets)
51
+ const controller = new ${p.name}Controller(service, log)
52
+
53
+ // Rutas públicas por defecto — agregar [auth.authenticate()] para proteger
54
+ router.get('/${p.module}', (req) => controller.index(req))
55
+ router.get('/${p.module}/:id', (req) => controller.show(req))
56
+ router.post('/${p.module}', (req) => controller.store(req))
57
+ router.put('/${p.module}/:id', (req) => controller.update(req))
58
+ router.delete('/${p.module}/:id', (req) => controller.destroy(req))
59
+
60
+ log.info('Módulo ${p.module} listo')
61
+ return service
62
+ },
63
+ })
64
+ }
65
+ `
66
+ }
67
+
68
+ export function typesStub(p: ModuleStubParams): string {
69
+ const fields = p.fields.map(f => ` ${f.name}${f.required ? '' : '?'}: ${mapTS(f.type)}`).join('\n')
70
+ const filterFields = p.fields.map(f => ` ${f.name}?: ${mapTS(f.type)}`).join('\n')
71
+
72
+ const modelFields = p.fields
73
+ .filter(f => !['id', 'createdAt', 'updatedAt', 'deletedAt'].includes(f.name))
74
+ .map(f => {
75
+ const parts = [` ${f.name}: { type: '${f.type}'`]
76
+ if (f.required) parts.push(`, required: true`)
77
+ if (f.default !== undefined) parts.push(`, default: ${JSON.stringify(f.default)}`)
78
+ parts.push(` }`)
79
+ return parts.join('')
80
+ })
81
+ .join(',\n')
82
+
83
+ return `// ${p.module}/types.ts — DTOs y definición del modelo
84
+ // Responsabilidad ÚNICA: definir la forma de los datos.
85
+ // El módulo es dueño de su ModelDefinition — composition-root la importa desde aquí.
86
+
87
+ import type { ModelDefinition } from 'arckode-framework'
88
+
89
+ // Definición del modelo para ORM.define() — importar en composition-root.ts
90
+ export const ${p.name}Model: ModelDefinition = {
91
+ table: '${p.module}',
92
+ fields: {
93
+ ${modelFields}
94
+ },
95
+ timestamps: true,
96
+ ${p.softDelete ? ' softDelete: true,' : ''}
97
+ }
98
+
99
+ export interface ${p.name}DTO {
100
+ id: string
101
+ ${fields}
102
+ createdAt: string
103
+ updatedAt: string
104
+ ${p.softDelete ? ' deletedAt: string | null' : ''}
105
+ }
106
+
107
+ export interface Create${p.name}DTO {
108
+ ${p.fields.filter(f => !f.name.match(/^(id|createdAt|updatedAt|deletedAt)$/)).map(f => ` ${f.name}${f.required ? '' : '?'}: ${mapTS(f.type)}`).join('\n')}
109
+ }
110
+
111
+ export interface Update${p.name}DTO {
112
+ ${p.fields.filter(f => !f.name.match(/^(id|createdAt|updatedAt|deletedAt)$/)).map(f => ` ${f.name}?: ${mapTS(f.type)}`).join('\n')}
113
+ }
114
+
115
+ // ─── Consultas y paginación ────────────────────────────
116
+ export interface ${p.name}Query {
117
+ ${filterFields}
118
+ page?: number
119
+ limit?: number
120
+ sortBy?: string
121
+ sortOrder?: 'asc' | 'desc'
122
+ search?: string
123
+ }
124
+
125
+ export interface ${p.name}Paginated {
126
+ data: ${p.name}DTO[]
127
+ pagination: {
128
+ page: number
129
+ limit: number
130
+ total: number
131
+ totalPages: number
132
+ hasNext: boolean
133
+ hasPrev: boolean
134
+ }
135
+ }
136
+ `
137
+ }
138
+
139
+ export function socketsStub(p: ModuleStubParams): string {
140
+ return `// ${p.module}/sockets.ts — Hooks OPCIONALES hacia otros módulos
141
+ // Los sockets son opcionales. El módulo funciona sin ellos.
142
+ // Un conector puede pasar sockets para reaccionar a eventos del módulo.
143
+
144
+ import type { ${p.name}DTO } from './types'
145
+
146
+ export interface ${p.name}Sockets {
147
+ on${p.name}Created?: (data: ${p.name}DTO) => Promise<void>
148
+ on${p.name}Updated?: (data: ${p.name}DTO) => Promise<void>
149
+ on${p.name}Deleted?: (id: string) => Promise<void>
150
+ }
151
+ `
152
+ }
153
+
154
+ export function serviceStub(p: ModuleStubParams): string {
155
+ const hasSearch = p.fields.some(f => f.type === 'string')
156
+
157
+ return `// ${p.module}/actions/service.ts — Lógica de negocio
158
+ // Responsabilidad ÚNICA: casos de uso del módulo.
159
+ // NO sabe de HTTP. NO importa de otros módulos.
160
+ // Recibe dependencias por constructor (Dependency Inversion).
161
+ //
162
+ // IMPORTANTE: depende de RepositoryAdapter<${p.name}DTO>, no del ORM directamente.
163
+ // Esto permite swapear SQL → MongoDB → Prisma en composition-root.ts sin tocar este archivo.
164
+
165
+ import type { RepositoryAdapter, Logger, CacheAdapter } from 'arckode-framework'
166
+ import { NotFoundError } from 'arckode-framework'
167
+ import type { ${p.name}DTO, Create${p.name}DTO, Update${p.name}DTO, ${p.name}Query, ${p.name}Paginated } from '../types'
168
+ import type { ${p.name}Sockets } from '../sockets'
169
+
170
+ export class ${p.name}Service {
171
+ constructor(
172
+ private readonly repo: RepositoryAdapter<${p.name}DTO>,
173
+ private readonly logger: Logger,
174
+ private readonly cache: CacheAdapter,
175
+ private readonly sockets?: ${p.name}Sockets,
176
+ ) {}
177
+
178
+ async list(query?: ${p.name}Query): Promise<${p.name}Paginated> {
179
+ this.logger.info('Listando ${p.module}', { query })
180
+
181
+ const page = query?.page ?? 1
182
+ const limit = query?.limit ?? 20
183
+ const offset = (page - 1) * limit
184
+ const filters: Record<string, unknown> = {}
185
+
186
+ ${p.fields.filter(f => f.name !== 'id').slice(0, 3).map(f => `if (query?.${f.name} !== undefined) filters.${f.name} = query.${f.name}`).join('\n ')}
187
+
188
+ const result = await this.repo.paginate(filters, { limit, offset })
189
+
190
+ return {
191
+ data: result.data,
192
+ pagination: {
193
+ page,
194
+ limit,
195
+ total: result.total,
196
+ totalPages: result.pages,
197
+ hasNext: offset + limit < result.total,
198
+ hasPrev: page > 1,
199
+ },
200
+ }
201
+ }
202
+
203
+ async getById(id: string): Promise<${p.name}DTO> {
204
+ this.logger.info('Obteniendo ${p.module}', { id })
205
+ const item = await this.repo.findById(id)
206
+ if (!item) throw new NotFoundError('${p.name} no encontrado')
207
+ return item
208
+ }
209
+
210
+ async create(dto: Create${p.name}DTO): Promise<${p.name}DTO> {
211
+ this.logger.info('Creando ${p.module}')
212
+ const item = await this.repo.create(dto as Omit<${p.name}DTO, 'id'>)
213
+ await this.sockets?.on${p.name}Created?.(item)
214
+ await this.cache.delete('${p.module}:list')
215
+ return item
216
+ }
217
+
218
+ async update(id: string, dto: Update${p.name}DTO): Promise<${p.name}DTO> {
219
+ this.logger.info('Actualizando ${p.module}', { id })
220
+ const item = await this.repo.update(id, dto as Partial<Omit<${p.name}DTO, 'id'>>)
221
+ if (!item) throw new NotFoundError('${p.name} no encontrado')
222
+ await this.sockets?.on${p.name}Updated?.(item)
223
+ await this.cache.delete('${p.module}:list')
224
+ return item
225
+ }
226
+
227
+ async delete(id: string): Promise<void> {
228
+ this.logger.info('Eliminando ${p.module}', { id })
229
+ const deleted = await this.repo.delete(id)
230
+ if (!deleted) throw new NotFoundError('${p.name} no encontrado')
231
+ await this.sockets?.on${p.name}Deleted?.(id)
232
+ await this.cache.delete('${p.module}:list')
233
+ }
234
+ }
235
+ `
236
+ }
237
+
238
+ export function controllerStub(p: ModuleStubParams): string {
239
+ return `// ${p.module}/actions/controller.ts — Capa HTTP
240
+ // Responsabilidad ÚNICA: traducir request/response.
241
+ // SIN lógica de negocio. SIN validación directa.
242
+
243
+ import type { HttpRequest, Logger } from 'arckode-framework'
244
+ import { validateSchema } from 'arckode-framework'
245
+ import type { ${p.name}Service } from './service'
246
+ import { Create${p.name}Schema, Update${p.name}Schema } from '../validators/schema'
247
+
248
+ export class ${p.name}Controller {
249
+ constructor(
250
+ private readonly service: ${p.name}Service,
251
+ private readonly logger: Logger,
252
+ ) {}
253
+
254
+ async index(req: HttpRequest) {
255
+ this.logger.info('GET /${p.module}')
256
+ const result = await this.service.list(req.query as any)
257
+ return { status: 200, body: result }
258
+ }
259
+
260
+ async show(req: HttpRequest) {
261
+ this.logger.info('GET /${p.module}/:id', { id: req.params.id })
262
+ const item = await this.service.getById(req.params.id)
263
+ return { status: 200, body: item }
264
+ }
265
+
266
+ async store(req: HttpRequest) {
267
+ this.logger.info('POST /${p.module}')
268
+ const data = validateSchema(Create${p.name}Schema, req.body)
269
+ const item = await this.service.create(data as any)
270
+ return { status: 201, body: item }
271
+ }
272
+
273
+ async update(req: HttpRequest) {
274
+ this.logger.info('PUT /${p.module}/:id', { id: req.params.id })
275
+ const data = validateSchema(Update${p.name}Schema, req.body)
276
+ const item = await this.service.update(req.params.id, data as any)
277
+ return { status: 200, body: item }
278
+ }
279
+
280
+ async destroy(req: HttpRequest) {
281
+ this.logger.info('DELETE /${p.module}/:id', { id: req.params.id })
282
+ await this.service.delete(req.params.id)
283
+ return { status: 204, body: null }
284
+ }
285
+ }
286
+ `
287
+ }
288
+
289
+ export function validatorStub(p: ModuleStubParams): string {
290
+ const createRules = p.fields
291
+ .filter(f => f.name !== 'id')
292
+ .map(f => {
293
+ const parts = [` ${f.name}: { type: '${f.type}' as const`]
294
+ if (f.required && f.name !== 'createdAt' && f.name !== 'updatedAt') parts.push(', required: true')
295
+ if (f.type === 'string' && f.required) parts.push(', min: 2, max: 200')
296
+ parts.push(' }')
297
+ return parts.join('')
298
+ })
299
+ .join('\n')
300
+
301
+ const updateRules = p.fields
302
+ .filter(f => f.name !== 'id' && f.name !== 'createdAt')
303
+ .map(f => {
304
+ const parts = [` ${f.name}: { type: '${f.type}' as const`]
305
+ if (f.type === 'string') parts.push(', min: 2, max: 200')
306
+ parts.push(' }')
307
+ return parts.join('')
308
+ })
309
+ .join('\n')
310
+
311
+ return `// ${p.module}/validators/schema.ts — Validación de entrada
312
+ // Schemas planos, sin dependencias externas.
313
+
314
+ import type { ValidationRule } from 'arckode-framework'
315
+
316
+ export const Create${p.name}Schema: Record<string, ValidationRule> = {
317
+ ${createRules}
318
+ }
319
+
320
+ export const Update${p.name}Schema: Record<string, ValidationRule> = {
321
+ ${updateRules}
322
+ }
323
+
324
+ // Schema compuesto para usar en validación directa
325
+ export const ${p.name}Validator = {
326
+ create: Create${p.name}Schema,
327
+ update: Update${p.name}Schema,
328
+ }
329
+ `
330
+ }
331
+
332
+ export function testStub(p: ModuleStubParams): string {
333
+ return `// ${p.module}/tests/service.test.ts — Tests del servicio
334
+ // Usa RepositoryAdapter mock — sin dependencia de SQLite ni Postgres.
335
+
336
+ import { describe, it, expect, beforeEach } from 'bun:test'
337
+ import type { RepositoryAdapter, CacheAdapter } from 'arckode-framework'
338
+ import { silentLogger } from 'arckode-framework/testing'
339
+ import { ${p.name}Service } from '../actions/service'
340
+ import type { ${p.name}DTO } from '../types'
341
+
342
+ // Mock mínimo de RepositoryAdapter — devuelve datos predefinidos
343
+ function makeRepo(overrides: Partial<RepositoryAdapter<${p.name}DTO>> = {}): RepositoryAdapter<${p.name}DTO> {
344
+ return {
345
+ findMany: async () => [],
346
+ findById: async () => null,
347
+ findOne: async () => null,
348
+ create: async (data) => ({ id: 'test-id', ...data } as ${p.name}DTO),
349
+ update: async (id, data) => ({ id, ...data } as ${p.name}DTO),
350
+ delete: async () => true,
351
+ count: async () => 0,
352
+ paginate: async () => ({ data: [], total: 0, limit: 20, offset: 0, pages: 0 }),
353
+ ...overrides,
354
+ }
355
+ }
356
+
357
+ const silentCache: CacheAdapter = { get: async () => null, set: async () => {}, delete: async () => {}, clear: async () => {} }
358
+
359
+ describe('${p.name}Service', () => {
360
+ describe('getById', () => {
361
+ it('lanza NotFound si el item no existe', async () => {
362
+ const service = new ${p.name}Service(makeRepo(), silentLogger, silentCache)
363
+ await expect(service.getById('no-existe')).rejects.toThrow('${p.name} no encontrado')
364
+ })
365
+
366
+ it('retorna el item si existe', async () => {
367
+ const item = { id: '1' } as ${p.name}DTO
368
+ const service = new ${p.name}Service(makeRepo({ findById: async () => item }), silentLogger, silentCache)
369
+ expect(await service.getById('1')).toEqual(item)
370
+ })
371
+ })
372
+
373
+ describe('create', () => {
374
+ it('crea y retorna el item', async () => {
375
+ const service = new ${p.name}Service(makeRepo(), silentLogger, silentCache)
376
+ const result = await service.create({} as any)
377
+ expect(result.id).toBe('test-id')
378
+ })
379
+ })
380
+
381
+ describe('delete', () => {
382
+ it('lanza NotFound si el item no existe', async () => {
383
+ const service = new ${p.name}Service(makeRepo({ delete: async () => false }), silentLogger, silentCache)
384
+ await expect(service.delete('no-existe')).rejects.toThrow('${p.name} no encontrado')
385
+ })
386
+ })
387
+ })
388
+ `
389
+ }
390
+
391
+ export function migrationStub(p: ModuleStubParams): string {
392
+ // Tipos ANSI SQL — compatibles con SQLite, MySQL y Postgres sin modificaciones
393
+ const sqlType = (f: ModuleStubParams['fields'][0]): string => {
394
+ if (f.name === 'id') return 'VARCHAR(36)'
395
+ const map: Record<string, string> = {
396
+ string: 'VARCHAR(255)',
397
+ number: 'DECIMAL(15,4)',
398
+ boolean: 'BOOLEAN',
399
+ date: 'TIMESTAMP',
400
+ json: 'TEXT',
401
+ }
402
+ return map[f.type] ?? 'TEXT'
403
+ }
404
+
405
+ const cols = p.fields.map(f => {
406
+ const pk = f.name === 'id' ? ' PRIMARY KEY' : ''
407
+ const req = f.required && f.name !== 'id' ? ' NOT NULL' : ''
408
+ const def = f.default !== undefined ? ` DEFAULT ${typeof f.default === 'string' ? `'${f.default}'` : f.default}` : ''
409
+ return ` ${f.name} ${sqlType(f)}${pk}${req}${def}`
410
+ }).join('\n')
411
+
412
+ return `// migrations/${Date.now()}_create_${p.module}.ts
413
+ // Migración generada por arckode make:module
414
+ // SQL ANSI — compatible con SQLite, MySQL y Postgres sin modificaciones.
415
+
416
+ export async function up(db: any): Promise<void> {
417
+ await db.run(\`
418
+ CREATE TABLE IF NOT EXISTS ${p.module} (
419
+ ${cols},
420
+ createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
421
+ updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP${p.softDelete ? ',\n deletedAt TIMESTAMP NULL' : ''}
422
+ )
423
+ \`)
424
+ }
425
+
426
+ export async function down(db: any): Promise<void> {
427
+ await db.run(\`DROP TABLE IF EXISTS ${p.module}\`)
428
+ }
429
+ `
430
+ }
431
+
432
+ export function seedStub(p: ModuleStubParams): string {
433
+ const sampleData = p.fields
434
+ .filter(f => f.name !== 'id' && f.name !== 'createdAt' && f.name !== 'updatedAt')
435
+ .map(f => {
436
+ if (f.name === 'nombre' || f.name === 'name') return ` ${f.name}: '${p.name} de ejemplo'`
437
+ if (f.name === 'email') return ` ${f.name}: 'ejemplo@correo.com'`
438
+ if (f.name === 'activo' || f.name === 'active') return ` ${f.name}: true`
439
+ if (f.type === 'number') return ` ${f.name}: 100`
440
+ if (f.type === 'boolean') return ` ${f.name}: false`
441
+ return ` ${f.name}: 'valor'`
442
+ })
443
+ .join('\n')
444
+
445
+ return `// seeds/${p.module}.ts — Datos de prueba
446
+
447
+ export async function seed${p.name}(orm: any): Promise<void> {
448
+ const items = [
449
+ {
450
+ ${sampleData}
451
+ },
452
+ {
453
+ ${sampleData.replace(`'${p.name} de ejemplo'`, `'${p.name} de ejemplo 2'`).replace(`'ejemplo@correo.com'`, `'ejemplo2@correo.com'`)}
454
+ },
455
+ ]
456
+
457
+ for (const item of items) {
458
+ await orm.create('${p.name}', item)
459
+ }
460
+
461
+ console.log(' ✓ ${p.name} seeded: ' + items.length + ' items')
462
+ }
463
+ `
464
+ }
465
+
466
+ function mapTS(type: string): string {
467
+ const map: Record<string, string> = { string: 'string', number: 'number', boolean: 'boolean', date: 'string' }
468
+ return map[type] ?? 'unknown'
469
+ }
@@ -0,0 +1,101 @@
1
+ // kernel/__tests__/adapters.test.ts — Tests de adapters reales
2
+ // Bun:test. Requiere: better-sqlite3 (SQLite), pg (Postgres), redis (Redis)
3
+
4
+ import { describe, it, expect, beforeAll, afterAll } from 'bun:test'
5
+
6
+ // ─── SQLite Adapter ────────────────────────────────────
7
+ describe('SQLiteAdapter', () => {
8
+ let adapter: any
9
+
10
+ beforeAll(async () => {
11
+ const { SqliteAdapter } = await import('../../adapters/sqlite')
12
+ adapter = new SqliteAdapter({ path: ':memory:' })
13
+ await adapter.connect()
14
+ })
15
+
16
+ afterAll(async () => {
17
+ await adapter.close()
18
+ })
19
+
20
+ it('crea tabla e inserta datos', async () => {
21
+ await adapter.run('CREATE TABLE IF NOT EXISTS test (id TEXT PRIMARY KEY, nombre TEXT)')
22
+ await adapter.run('INSERT INTO test (id, nombre) VALUES (?, ?)', ['1', 'Test'])
23
+ const rows = await adapter.query('SELECT * FROM test WHERE id = ?', ['1'])
24
+ expect(rows).toHaveLength(1)
25
+ expect((rows[0] as any).nombre).toBe('Test')
26
+ })
27
+
28
+ it('query con múltiples resultados', async () => {
29
+ await adapter.run('INSERT INTO test (id, nombre) VALUES (?, ?)', ['2', 'Otro'])
30
+ const rows = await adapter.query('SELECT * FROM test ORDER BY id')
31
+ expect(rows).toHaveLength(2)
32
+ })
33
+
34
+ it('run devuelve changes', async () => {
35
+ const result = await adapter.run('DELETE FROM test WHERE id = ?', ['1'])
36
+ expect(result.changes).toBeGreaterThan(0)
37
+ expect(result.lastId).toBeDefined()
38
+ })
39
+
40
+ it('query sin resultados devuelve array vacío', async () => {
41
+ const rows = await adapter.query('SELECT * FROM test WHERE id = ?', ['999'])
42
+ expect(rows).toEqual([])
43
+ })
44
+ })
45
+
46
+ // ─── JWT Adapter ───────────────────────────────────────
47
+ describe('JwtAdapter', () => {
48
+ it('sign y verify funcionan', async () => {
49
+ const { jwtTokenAdapter } = await import('../../adapters/jwt')
50
+ const token = jwtTokenAdapter.sign({ id: '1', role: 'admin' }, 'secret', '1h')
51
+ expect(token).toBeTruthy()
52
+ expect(typeof token).toBe('string')
53
+
54
+ const payload = jwtTokenAdapter.verify(token, 'secret')
55
+ expect(payload.id).toBe('1')
56
+ expect(payload.role).toBe('admin')
57
+ })
58
+
59
+ it('verify rechaza token con secret incorrecto', async () => {
60
+ const { jwtTokenAdapter } = await import('../../adapters/jwt')
61
+ const token = jwtTokenAdapter.sign({ id: '1' }, 'correct', '1h')
62
+ expect(() => jwtTokenAdapter.verify(token, 'wrong')).toThrow()
63
+ })
64
+
65
+ it('verify rechaza token expirado', async () => {
66
+ const { jwtTokenAdapter } = await import('../../adapters/jwt')
67
+ const token = jwtTokenAdapter.sign({ id: '1' }, 'secret', '0s')
68
+ expect(() => jwtTokenAdapter.verify(token, 'secret')).toThrow()
69
+ })
70
+ })
71
+
72
+ // ═════════════════════════════════════════════════════════
73
+ // Nota: PostgresAdapter y RedisCacheAdapter requieren
74
+ // servidores externos. Se testean con mock o con docker.
75
+ //
76
+ // Test Postgres (requiere pg instalado y servidor postgres):
77
+ // describe('PostgresAdapter', () => {
78
+ // it('conecta y ejecuta query', async () => {
79
+ // const { PostgresAdapter } = await import('../../adapters/postgres')
80
+ // const adapter = new PostgresAdapter({
81
+ // connectionString: 'postgres://test:test@localhost:5432/test'
82
+ // })
83
+ // await adapter.connect()
84
+ // const rows = await adapter.query('SELECT 1 as num')
85
+ // expect((rows[0] as any).num).toBe(1)
86
+ // await adapter.close()
87
+ // })
88
+ // })
89
+ //
90
+ // Test Redis (requiere redis instalado y servidor redis):
91
+ // describe('RedisCacheAdapter', () => {
92
+ // it('set y get', async () => {
93
+ // const { RedisCacheAdapter } = await import('../../adapters/redis-cache')
94
+ // const cache = new RedisCacheAdapter({ url: 'redis://localhost:6379' })
95
+ // await cache.connect()
96
+ // await cache.set('test', { ok: true })
97
+ // const val = await cache.get('test')
98
+ // expect(val).toEqual({ ok: true })
99
+ // await cache.delete('test')
100
+ // })
101
+ // })