arckode-framework 1.0.0 → 1.0.2

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.
@@ -5,7 +5,10 @@ import { createClient, type RedisClientType } from 'redis'
5
5
  import type { CacheAdapter } from '../kernel/framework'
6
6
 
7
7
  export interface RedisCacheConfig {
8
- url: string
8
+ /** URL completa: redis://localhost:6379 o rediss://user:pass@host:6380 */
9
+ url?: string
10
+ /** Config por host/port como alternativa a url */
11
+ socket?: { host: string; port: number; password?: string }
9
12
  keyPrefix?: string
10
13
  defaultTTL?: number
11
14
  }
@@ -17,7 +20,10 @@ export class RedisCacheAdapter implements CacheAdapter {
17
20
  constructor(private config: RedisCacheConfig) {}
18
21
 
19
22
  async connect(): Promise<void> {
20
- this.client = createClient({ url: this.config.url })
23
+ const clientConfig = this.config.url
24
+ ? { url: this.config.url }
25
+ : { socket: this.config.socket, password: this.config.socket?.password }
26
+ this.client = createClient(clientConfig)
21
27
  await this.client.connect()
22
28
  this.connected = true
23
29
  }
@@ -27,7 +27,7 @@ export type { ${p.name}DTO, Create${p.name}DTO, Update${p.name}DTO, ${p.name}Que
27
27
  export type { ${p.name}Sockets } from './sockets'
28
28
  export { ${p.name}Validator, Create${p.name}Schema, Update${p.name}Schema } from './validators/schema'
29
29
 
30
- export function ${p.name}Module(sockets?: import('./sockets').${p.name}Sockets) {
30
+ export function ${p.name}Module() {
31
31
  return createModule({
32
32
  name: '${p.module}',
33
33
  version: '1.0.0',
@@ -44,10 +44,10 @@ export function ${p.name}Module(sockets?: import('./sockets').${p.name}Sockets)
44
44
  rules: ['No importar de otros módulos'],
45
45
  },
46
46
 
47
- create({ logger, orm, cache, router }) {
47
+ create({ logger, orm, cache, router, auth }) {
48
48
  const repo = new OrmRepository<${p.name}DTO>(orm, '${p.name}')
49
49
  const log = logger.child('${p.module}')
50
- const service = new ${p.name}Service(repo, log, cache, sockets)
50
+ const service = new ${p.name}Service(repo, log, cache)
51
51
  const controller = new ${p.name}Controller(service, log)
52
52
 
53
53
  // Rutas públicas por defecto — agregar [auth.authenticate()] para proteger
@@ -168,13 +168,18 @@ import type { ${p.name}DTO, Create${p.name}DTO, Update${p.name}DTO, ${p.name}Que
168
168
  import type { ${p.name}Sockets } from '../sockets'
169
169
 
170
170
  export class ${p.name}Service {
171
+ private sockets: ${p.name}Sockets = {}
172
+
171
173
  constructor(
172
174
  private readonly repo: RepositoryAdapter<${p.name}DTO>,
173
175
  private readonly logger: Logger,
174
176
  private readonly cache: CacheAdapter,
175
- private readonly sockets?: ${p.name}Sockets,
176
177
  ) {}
177
178
 
179
+ setSockets(s: Partial<${p.name}Sockets>): void {
180
+ this.sockets = { ...this.sockets, ...s }
181
+ }
182
+
178
183
  async list(query?: ${p.name}Query): Promise<${p.name}Paginated> {
179
184
  this.logger.info('Listando ${p.module}', { query })
180
185
 
@@ -210,7 +215,7 @@ export class ${p.name}Service {
210
215
  async create(dto: Create${p.name}DTO): Promise<${p.name}DTO> {
211
216
  this.logger.info('Creando ${p.module}')
212
217
  const item = await this.repo.create(dto as Omit<${p.name}DTO, 'id'>)
213
- await this.sockets?.on${p.name}Created?.(item)
218
+ await this.sockets.on${p.name}Created?.(item)
214
219
  await this.cache.delete('${p.module}:list')
215
220
  return item
216
221
  }
@@ -219,7 +224,7 @@ export class ${p.name}Service {
219
224
  this.logger.info('Actualizando ${p.module}', { id })
220
225
  const item = await this.repo.update(id, dto as Partial<Omit<${p.name}DTO, 'id'>>)
221
226
  if (!item) throw new NotFoundError('${p.name} no encontrado')
222
- await this.sockets?.on${p.name}Updated?.(item)
227
+ await this.sockets.on${p.name}Updated?.(item)
223
228
  await this.cache.delete('${p.module}:list')
224
229
  return item
225
230
  }
@@ -228,7 +233,7 @@ export class ${p.name}Service {
228
233
  this.logger.info('Eliminando ${p.module}', { id })
229
234
  const deleted = await this.repo.delete(id)
230
235
  if (!deleted) throw new NotFoundError('${p.name} no encontrado')
231
- await this.sockets?.on${p.name}Deleted?.(id)
236
+ await this.sockets.on${p.name}Deleted?.(id)
232
237
  await this.cache.delete('${p.module}:list')
233
238
  }
234
239
  }
@@ -333,13 +338,16 @@ export function testStub(p: ModuleStubParams): string {
333
338
  return `// ${p.module}/tests/service.test.ts — Tests del servicio
334
339
  // Usa RepositoryAdapter mock — sin dependencia de SQLite ni Postgres.
335
340
 
336
- import { describe, it, expect, beforeEach } from 'bun:test'
341
+ import { describe, it, expect } from 'bun:test'
337
342
  import type { RepositoryAdapter, CacheAdapter } from 'arckode-framework'
338
343
  import { silentLogger } from 'arckode-framework/testing'
339
344
  import { ${p.name}Service } from '../actions/service'
340
345
  import type { ${p.name}DTO } from '../types'
341
346
 
342
- // Mock mínimo de RepositoryAdapterdevuelve datos predefinidos
347
+ // silentLogger es una factory function SIEMPRE llamarla con ()
348
+ const log = silentLogger()
349
+ const silentCache: CacheAdapter = { get: async () => null, set: async () => {}, delete: async () => {}, clear: async () => {}, flush: async () => {} }
350
+
343
351
  function makeRepo(overrides: Partial<RepositoryAdapter<${p.name}DTO>> = {}): RepositoryAdapter<${p.name}DTO> {
344
352
  return {
345
353
  findMany: async () => [],
@@ -354,25 +362,23 @@ function makeRepo(overrides: Partial<RepositoryAdapter<${p.name}DTO>> = {}): Rep
354
362
  }
355
363
  }
356
364
 
357
- const silentCache: CacheAdapter = { get: async () => null, set: async () => {}, delete: async () => {}, clear: async () => {} }
358
-
359
365
  describe('${p.name}Service', () => {
360
366
  describe('getById', () => {
361
367
  it('lanza NotFound si el item no existe', async () => {
362
- const service = new ${p.name}Service(makeRepo(), silentLogger, silentCache)
368
+ const service = new ${p.name}Service(makeRepo(), log, silentCache)
363
369
  await expect(service.getById('no-existe')).rejects.toThrow('${p.name} no encontrado')
364
370
  })
365
371
 
366
372
  it('retorna el item si existe', async () => {
367
373
  const item = { id: '1' } as ${p.name}DTO
368
- const service = new ${p.name}Service(makeRepo({ findById: async () => item }), silentLogger, silentCache)
374
+ const service = new ${p.name}Service(makeRepo({ findById: async () => item }), log, silentCache)
369
375
  expect(await service.getById('1')).toEqual(item)
370
376
  })
371
377
  })
372
378
 
373
379
  describe('create', () => {
374
380
  it('crea y retorna el item', async () => {
375
- const service = new ${p.name}Service(makeRepo(), silentLogger, silentCache)
381
+ const service = new ${p.name}Service(makeRepo(), log, silentCache)
376
382
  const result = await service.create({} as any)
377
383
  expect(result.id).toBe('test-id')
378
384
  })
@@ -380,7 +386,7 @@ describe('${p.name}Service', () => {
380
386
 
381
387
  describe('delete', () => {
382
388
  it('lanza NotFound si el item no existe', async () => {
383
- const service = new ${p.name}Service(makeRepo({ delete: async () => false }), silentLogger, silentCache)
389
+ const service = new ${p.name}Service(makeRepo({ delete: async () => false }), log, silentCache)
384
390
  await expect(service.delete('no-existe')).rejects.toThrow('${p.name} no encontrado')
385
391
  })
386
392
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arckode-framework",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "AI-first TypeScript/Bun framework. Modular, SOLID, zero magic. The AI reads the composition root and knows everything.",
5
5
  "type": "module",
6
6
  "main": "./kernel/framework.ts",
@@ -25,9 +25,20 @@
25
25
  },
26
26
  "files": [
27
27
  "bin/",
28
- "kernel/",
29
- "adapters/",
30
- "modules/",
28
+ "kernel/framework.ts",
29
+ "kernel/middlewares.ts",
30
+ "kernel/static.ts",
31
+ "kernel/testing.ts",
32
+ "adapters/jwt.ts",
33
+ "adapters/mysql.ts",
34
+ "adapters/postgres.ts",
35
+ "adapters/redis-cache.ts",
36
+ "adapters/sqlite.ts",
37
+ "modules/events/index.ts",
38
+ "modules/mail/",
39
+ "modules/queue/index.ts",
40
+ "modules/storage/",
41
+ "modules/ws/index.ts",
31
42
  "cli/",
32
43
  "skills/",
33
44
  "README.md"
@@ -66,9 +66,9 @@ import type { PedidosSockets } from '../sockets'
66
66
  export class PedidosService {
67
67
  private sockets: PedidosSockets = {}
68
68
 
69
- // Requerido para que el conector pueda inyectar
70
- setSockets(sockets: PedidosSockets): void {
71
- this.sockets = sockets
69
+ // MERGE no replace. Si dos conectores inyectan sockets, el segundo no pisa al primero.
70
+ setSockets(s: Partial<PedidosSockets>): void {
71
+ this.sockets = { ...this.sockets, ...s }
72
72
  }
73
73
 
74
74
  async confirmar(id: string): Promise<PedidoDTO> {
@@ -253,7 +253,8 @@ test('confirmar pedido funciona sin sockets registrados', async () => {
253
253
 
254
254
  - [ ] Solo importa tipos (no implementaciones) desde `index.ts` de módulos
255
255
  - [ ] Sin `if`, `for`, ni lógica de negocio — solo llamadas de delegación
256
- - [ ] El service emisor implementa `setSockets()` y usa `?.` al llamarlos
256
+ - [ ] El service emisor implementa `setSockets()` con MERGE (`{...this.sockets, ...s}`) — no replace
257
+ - [ ] `setSockets()` usa `?.` al llamar los hooks (son opcionales)
257
258
  - [ ] Registrado en composition-root DESPUÉS de los módulos
258
259
  - [ ] El módulo funciona correctamente sin el conector (sockets son opcionales)
259
260
  - [ ] `arckode analyze` detecta: `CONNECTOR_BUSINESS_LOGIC` si hay lógica
@@ -8,18 +8,36 @@
8
8
 
9
9
  ```ts
10
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'
11
+ // NOMBRES REALES: silentLogger (factory), createRecordingDb, createTestClient
12
+ // createRecordingOrm / createSilentLogger / createNullCache NO existen — nunca usar esos nombres
13
+ import { silentLogger, createRecordingDb } from 'arckode-framework/testing'
14
+ import type { RepositoryAdapter, CacheAdapter } from 'arckode-framework'
13
15
  import { ProductosService } from '../actions/service'
14
16
  import type { ProductoDTO } from '../types'
15
17
 
18
+ // silentLogger es factory function — SIEMPRE llamar con ()
19
+ const log = silentLogger()
20
+ const silentCache: CacheAdapter = { get: async () => null, set: async () => {}, delete: async () => {}, flush: async () => {} }
21
+
22
+ function makeRepo(overrides: Partial<RepositoryAdapter<ProductoDTO>> = {}): RepositoryAdapter<ProductoDTO> {
23
+ return {
24
+ findMany: async () => [],
25
+ findById: async () => null,
26
+ findOne: async () => null,
27
+ create: async (data) => ({ id: 'test-id', ...data } as ProductoDTO),
28
+ update: async (id, data) => ({ id, ...data } as ProductoDTO),
29
+ delete: async () => true,
30
+ count: async () => 0,
31
+ paginate: async () => ({ data: [], total: 0, limit: 20, offset: 0, pages: 0 }),
32
+ ...overrides,
33
+ }
34
+ }
35
+
16
36
  describe('ProductosService', () => {
17
37
  let service: ProductosService
18
38
 
19
39
  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())
40
+ service = new ProductosService(makeRepo(), log, silentCache)
23
41
  })
24
42
  })
25
43
  ```
@@ -31,24 +49,31 @@ describe('ProductosService', () => {
31
49
  ## 2. UTILIDADES DE TESTING
32
50
 
33
51
  ```ts
52
+ // Exports REALES del módulo arckode-framework/testing:
34
53
  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
54
+ silentLogger, // factory: silentLogger() Logger (NO usarla sin ())
55
+ createRecordingDb, // captura SQL sin ejecutar BD real db.calls, db.lastSql()
56
+ createTestClient, // cliente HTTP sin levantar servidor
57
+ createIntegrationClient, // cliente HTTP con servidor real (puerto aleatorio)
58
+ mockAuth, // middleware mock para simular autenticación
39
59
  } from 'arckode-framework/testing'
40
60
 
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')
61
+ // ESTOS NO EXISTEN nunca usar:
62
+ // createRecordingOrm, createSilentLogger, createNullCache
63
+
64
+ // silentLogger factory, siempre con ()
65
+ const log = silentLogger()
66
+
67
+ // createRecordingDb — captura SQL sin ejecutar nada
68
+ const db = createRecordingDb({ queryData: [{ id: '1', nombre: 'Laptop' }] })
69
+ // db.calls → array de { sql, params, type }
70
+ // db.lastSql() → último SQL ejecutado
71
+ // db.reset() → limpia el historial
46
72
 
47
- // TestClient — integration tests con router real
73
+ // TestClient — router real, sin HTTP
48
74
  const client = createTestClient(router)
49
75
  const res = await client.get('/productos')
50
76
  expect(res.status).toBe(200)
51
- expect(res.body).toBeArray()
52
77
  ```
53
78
 
54
79
  ---
@@ -119,9 +144,7 @@ describe('Productos API', () => {
119
144
  beforeEach(() => {
120
145
  const router = new Router()
121
146
  // 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())
147
+ const service = new ProductosService(makeRepo(), log, silentCache)
125
148
  const controller = new ProductosController(service)
126
149
 
127
150
  router.get('/productos', req => controller.index(req))
@@ -161,7 +184,7 @@ describe('Productos API', () => {
161
184
  import { Auth } from 'arckode-framework'
162
185
  import { jwtTokenAdapter } from 'arckode-framework/adapters/jwt'
163
186
 
164
- const auth = new Auth(jwtTokenAdapter, 'test-secret-12345678901234567890', createSilentLogger())
187
+ const auth = new Auth(jwtTokenAdapter, 'test-secret-12345678901234567890', silentLogger())
165
188
  const token = auth.createToken({ id: 'user-1', role: 'user' })
166
189
  const adminToken = auth.createToken({ id: 'admin-1', role: 'admin' })
167
190
 
@@ -249,9 +272,9 @@ test('ORM recibe query INSERT', async () => {
249
272
  ## 10. CHECKLIST TESTING
250
273
 
251
274
  - [ ] 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)
275
+ - [ ] `silentLogger()` con paréntesis, es factory function (no objeto)
276
+ - [ ] Cache mock inline (`get: async () => null`, etc.) — no hay createNullCache
277
+ - [ ] `RepositoryAdapter<T>` mock inline con `makeRepo()` no hay createRecordingOrm
255
278
  - [ ] Todo `expect(...).rejects` tiene `await`
256
279
  - [ ] Los nombres de test describen comportamiento, no implementación
257
280
  - [ ] `bun test` pasa sin errores antes del commit
@@ -1,283 +0,0 @@
1
- // adapters/__tests__/mysql.test.ts
2
- // Testea MySQLAdapter con mocks de mysql2/promise — sin servidor real.
3
- // Para tests de integración real ver el bloque comentado al final.
4
-
5
- import { describe, it, expect, beforeAll, beforeEach, mock } from 'bun:test'
6
- import type { MySQLAdapter } from '../mysql'
7
-
8
- // ── Mocks de conexión (transacciones) ────────────────────────────────────────
9
- const connMock = {
10
- beginTransaction: mock(async () => {}),
11
- execute: mock(async (): Promise<unknown[]> => [[], []]),
12
- commit: mock(async () => {}),
13
- rollback: mock(async () => {}),
14
- release: mock(() => {}),
15
- }
16
-
17
- // ── Mock del pool ─────────────────────────────────────────────────────────────
18
- const poolMock = {
19
- getConnection: mock(async () => connMock),
20
- execute: mock(async (): Promise<unknown[]> => [[], []]),
21
- end: mock(async () => {}),
22
- }
23
-
24
- // Registrar el mock ANTES de que el adapter importe mysql2/promise.
25
- // El dynamic import en beforeAll garantiza que mysql2/promise ya está mockeado.
26
- mock.module('mysql2/promise', () => ({
27
- default: { createPool: mock(() => poolMock) },
28
- }))
29
-
30
- // ── Carga diferida del adapter ────────────────────────────────────────────────
31
- let Adapter: typeof MySQLAdapter
32
-
33
- beforeAll(async () => {
34
- const mod = await import('../mysql')
35
- Adapter = mod.MySQLAdapter
36
- })
37
-
38
- // ── Helpers ───────────────────────────────────────────────────────────────────
39
- function resetMocks() {
40
- connMock.beginTransaction.mockClear()
41
- connMock.execute.mockClear()
42
- connMock.commit.mockClear()
43
- connMock.rollback.mockClear()
44
- connMock.release.mockClear()
45
- poolMock.getConnection.mockClear()
46
- poolMock.execute.mockClear()
47
- poolMock.end.mockClear()
48
- }
49
-
50
- function makeAdapter() {
51
- return new Adapter({ host: 'localhost', user: 'test', password: 'test', database: 'testdb' })
52
- }
53
-
54
- // ─────────────────────────────────────────────────────────────────────────────
55
-
56
- describe('MySQLAdapter', () => {
57
- beforeEach(resetMocks)
58
-
59
- // ── connect() ───────────────────────────────────────────────────────────────
60
- describe('connect()', () => {
61
- it('crea el pool y verifica la conexión con getConnection+release', async () => {
62
- const adapter = makeAdapter()
63
- await adapter.connect()
64
-
65
- expect(poolMock.getConnection).toHaveBeenCalledTimes(1)
66
- expect(connMock.release).toHaveBeenCalledTimes(1)
67
- })
68
- })
69
-
70
- // ── query() ─────────────────────────────────────────────────────────────────
71
- describe('query()', () => {
72
- let adapter: MySQLAdapter
73
-
74
- beforeEach(async () => {
75
- adapter = makeAdapter()
76
- await adapter.connect()
77
- resetMocks() // limpia las calls de connect()
78
- })
79
-
80
- it('retorna el array de filas', async () => {
81
- const filas = [{ id: '1', nombre: 'Laptop' }, { id: '2', nombre: 'Mouse' }]
82
- poolMock.execute.mockImplementation(async () => [filas, []])
83
-
84
- const result = await adapter.query('SELECT * FROM productos')
85
- expect(result).toEqual(filas)
86
- })
87
-
88
- it('pasa los params al execute del pool', async () => {
89
- poolMock.execute.mockImplementation(async () => [[{ id: '1' }], []])
90
-
91
- await adapter.query('SELECT * FROM productos WHERE id = ?', ['1'])
92
- expect(poolMock.execute).toHaveBeenCalledWith(
93
- 'SELECT * FROM productos WHERE id = ?',
94
- ['1'],
95
- )
96
- })
97
-
98
- it('retorna array vacío cuando no hay resultados', async () => {
99
- poolMock.execute.mockImplementation(async () => [[], []])
100
-
101
- const result = await adapter.query('SELECT * FROM productos WHERE id = ?', ['999'])
102
- expect(result).toEqual([])
103
- })
104
- })
105
-
106
- // ── run() ────────────────────────────────────────────────────────────────────
107
- describe('run()', () => {
108
- let adapter: MySQLAdapter
109
-
110
- beforeEach(async () => {
111
- adapter = makeAdapter()
112
- await adapter.connect()
113
- resetMocks()
114
- })
115
-
116
- it('INSERT retorna changes y lastId como string', async () => {
117
- poolMock.execute.mockImplementation(async () => [{ affectedRows: 1, insertId: 42 }, []])
118
-
119
- const result = await adapter.run(
120
- 'INSERT INTO productos (nombre, precio) VALUES (?, ?)',
121
- ['Laptop', 999],
122
- )
123
-
124
- expect(result.changes).toBe(1)
125
- expect(result.lastId).toBe('42')
126
- })
127
-
128
- it('UPDATE retorna changes y lastId undefined (insertId === 0)', async () => {
129
- poolMock.execute.mockImplementation(async () => [{ affectedRows: 3, insertId: 0 }, []])
130
-
131
- const result = await adapter.run(
132
- 'UPDATE productos SET activo = ? WHERE categoria = ?',
133
- [false, 'perifericos'],
134
- )
135
-
136
- expect(result.changes).toBe(3)
137
- expect(result.lastId).toBeUndefined()
138
- })
139
-
140
- it('DELETE retorna changes y sin lastId', async () => {
141
- poolMock.execute.mockImplementation(async () => [{ affectedRows: 1, insertId: 0 }, []])
142
-
143
- const result = await adapter.run('DELETE FROM productos WHERE id = ?', ['1'])
144
- expect(result.changes).toBe(1)
145
- expect(result.lastId).toBeUndefined()
146
- })
147
- })
148
-
149
- // ── transaction() ────────────────────────────────────────────────────────────
150
- describe('transaction()', () => {
151
- let adapter: MySQLAdapter
152
-
153
- beforeEach(async () => {
154
- adapter = makeAdapter()
155
- await adapter.connect()
156
- resetMocks()
157
- })
158
-
159
- it('adquiere conexión, abre transacción, ejecuta fn y hace commit', async () => {
160
- const filas = [{ id: '1' }]
161
- connMock.execute.mockImplementation(async () => [filas, []])
162
-
163
- const result = await adapter.transaction(async (tx) => {
164
- return tx.query('SELECT * FROM productos')
165
- })
166
-
167
- expect(poolMock.getConnection).toHaveBeenCalledTimes(1)
168
- expect(connMock.beginTransaction).toHaveBeenCalledTimes(1)
169
- expect(connMock.commit).toHaveBeenCalledTimes(1)
170
- expect(connMock.rollback).not.toHaveBeenCalled()
171
- expect(connMock.release).toHaveBeenCalledTimes(1)
172
- expect(result).toEqual(filas)
173
- })
174
-
175
- it('el adapter de transacción usa la conexión, no el pool', async () => {
176
- connMock.execute.mockImplementation(async () => [[{ total: 5 }], []])
177
-
178
- await adapter.transaction(async (tx) => {
179
- await tx.query('SELECT COUNT(*) as total FROM productos')
180
- await tx.run('INSERT INTO auditoria (evento) VALUES (?)', ['sync'])
181
- })
182
-
183
- // Ambas llamadas van a connMock.execute, NO a poolMock.execute
184
- expect(connMock.execute).toHaveBeenCalledTimes(2)
185
- expect(poolMock.execute).not.toHaveBeenCalled()
186
- })
187
-
188
- it('hace rollback y lanza el error si fn falla', async () => {
189
- const error = new Error('error de negocio')
190
-
191
- await expect(
192
- adapter.transaction(async () => {
193
- throw error
194
- }),
195
- ).rejects.toThrow('error de negocio')
196
-
197
- expect(connMock.beginTransaction).toHaveBeenCalledTimes(1)
198
- expect(connMock.rollback).toHaveBeenCalledTimes(1)
199
- expect(connMock.commit).not.toHaveBeenCalled()
200
- expect(connMock.release).toHaveBeenCalledTimes(1)
201
- })
202
-
203
- it('libera la conexión aunque el rollback falle (finally garantizado)', async () => {
204
- connMock.rollback.mockImplementation(async () => {
205
- throw new Error('rollback falló')
206
- })
207
-
208
- await expect(
209
- adapter.transaction(async () => {
210
- throw new Error('error original')
211
- }),
212
- ).rejects.toBeDefined()
213
-
214
- // release() debe ejecutarse siempre — está en finally
215
- expect(connMock.release).toHaveBeenCalledTimes(1)
216
-
217
- // Restaurar implementación para tests siguientes
218
- connMock.rollback.mockImplementation(async () => {})
219
- })
220
- })
221
-
222
- // ── close() ──────────────────────────────────────────────────────────────────
223
- describe('close()', () => {
224
- it('termina el pool', async () => {
225
- const adapter = makeAdapter()
226
- await adapter.connect()
227
- resetMocks()
228
-
229
- await adapter.close()
230
- expect(poolMock.end).toHaveBeenCalledTimes(1)
231
- })
232
- })
233
- })
234
-
235
- // ─────────────────────────────────────────────────────────────────────────────
236
- // Test de integración real (requiere MySQL corriendo):
237
- //
238
- // describe('MySQLAdapter — integración', () => {
239
- // let adapter: MySQLAdapter
240
- //
241
- // beforeAll(async () => {
242
- // const { MySQLAdapter } = await import('../mysql')
243
- // adapter = new MySQLAdapter({
244
- // host: 'localhost',
245
- // user: 'test',
246
- // password: 'test',
247
- // database: 'arckode_test',
248
- // })
249
- // await adapter.connect()
250
- // await adapter.run('CREATE TABLE IF NOT EXISTS test_items (id INT AUTO_INCREMENT PRIMARY KEY, nombre VARCHAR(255))')
251
- // })
252
- //
253
- // afterAll(async () => {
254
- // await adapter.run('DROP TABLE IF EXISTS test_items')
255
- // await adapter.close()
256
- // })
257
- //
258
- // it('INSERT + SELECT funcionan con placeholders ?', async () => {
259
- // await adapter.run('INSERT INTO test_items (nombre) VALUES (?)', ['Laptop'])
260
- // const rows = await adapter.query('SELECT * FROM test_items WHERE nombre = ?', ['Laptop'])
261
- // expect(rows).toHaveLength(1)
262
- // expect((rows[0] as any).nombre).toBe('Laptop')
263
- // })
264
- //
265
- // it('transacción commit persiste los datos', async () => {
266
- // await adapter.transaction(async (tx) => {
267
- // await tx.run('INSERT INTO test_items (nombre) VALUES (?)', ['Mouse'])
268
- // })
269
- // const rows = await adapter.query('SELECT * FROM test_items WHERE nombre = ?', ['Mouse'])
270
- // expect(rows).toHaveLength(1)
271
- // })
272
- //
273
- // it('transacción rollback no persiste los datos', async () => {
274
- // await expect(
275
- // adapter.transaction(async (tx) => {
276
- // await tx.run('INSERT INTO test_items (nombre) VALUES (?)', ['Keyboard'])
277
- // throw new Error('forzar rollback')
278
- // })
279
- // ).rejects.toThrow()
280
- // const rows = await adapter.query('SELECT * FROM test_items WHERE nombre = ?', ['Keyboard'])
281
- // expect(rows).toHaveLength(0)
282
- // })
283
- // })
@@ -1,48 +0,0 @@
1
- // adapters/vendor.d.ts — Minimal type stubs for optional peer dependencies.
2
- // pg and redis are optional — users install them only when using those adapters.
3
- // These stubs let TypeScript compile without the packages installed.
4
-
5
- declare module 'pg' {
6
- export interface PoolConfig {
7
- connectionString?: string
8
- min?: number
9
- max?: number
10
- [key: string]: unknown
11
- }
12
-
13
- export interface QueryResult<R = Record<string, unknown>> {
14
- rows: R[]
15
- rowCount: number | null
16
- }
17
-
18
- export interface PoolClient {
19
- release(): void
20
- }
21
-
22
- export class Pool {
23
- constructor(config?: PoolConfig)
24
- connect(): Promise<PoolClient>
25
- query(sql: string, params?: unknown[]): Promise<QueryResult>
26
- end(): Promise<void>
27
- }
28
-
29
- const pg: {
30
- Pool: typeof Pool
31
- } & typeof import('pg')
32
- export default pg
33
- }
34
-
35
- declare module 'redis' {
36
- export type RedisClientType = {
37
- connect(): Promise<void>
38
- get(key: string): Promise<string | null>
39
- set(key: string, value: string): Promise<unknown>
40
- setEx(key: string, seconds: number, value: string): Promise<unknown>
41
- del(keys: string | string[]): Promise<unknown>
42
- keys(pattern: string): Promise<string[]>
43
- flushAll(): Promise<unknown>
44
- quit(): Promise<unknown>
45
- }
46
-
47
- export function createClient(options?: { url?: string; [key: string]: unknown }): RedisClientType
48
- }