arckode-framework 1.0.1 → 1.0.3

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.1",
3
+ "version": "1.0.3",
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",
@@ -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