arckode-framework 1.0.7 → 1.1.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.
@@ -0,0 +1,174 @@
1
+ // cli/stubs/claude-md-stub.ts — Plantilla del CLAUDE.md generado por `arckode new`
2
+ //
3
+ // IMPORTANTE: Este archivo debe mantenerse SINCRONIZADO con CLAUDE.md del framework.
4
+ // Si agregás/modificás una regla en /CLAUDE.md → actualizá también este stub.
5
+ // Los proyectos NUEVOS lo reciben hardcoded en su raíz al ejecutar `arckode new`.
6
+
7
+ export interface ClaudeMdStubParams {
8
+ projectName: string
9
+ db: 'sqlite' | 'mysql' | 'postgres'
10
+ }
11
+
12
+ export function claudeMdStub(p: ClaudeMdStubParams): string {
13
+ return `# CLAUDE.md — ${p.projectName}
14
+
15
+ ## Lee esto ANTES de escribir cualquier línea de código.
16
+
17
+ ### PASO 0 — Cargar contexto
18
+ 1. Si existe \`arckode.json\` → leelo (snapshot del sistema: módulos, conectores, violaciones).
19
+ Si no existe (proyecto recién creado), generalo con: \`bun run analyze:json\`
20
+ 2. Leer \`src/composition-root.ts\` → es la única fuente de verdad del sistema.
21
+ 3. Leer este CLAUDE.md → reglas inmutables y estructura obligatoria.
22
+
23
+ ---
24
+
25
+ ## Estructura obligatoria de un módulo
26
+
27
+ \`\`\`
28
+ src/modules/mi-modulo/
29
+ index.ts ← OBLIGATORIO — factory: registra modelo via registerXxxModels, wirea rutas
30
+ model.ts ← OBLIGATORIO — ModelDefinition (schema DB) + registerXxxModels(orm)
31
+ types.ts ← OBLIGATORIO — DTOs y tipos de queries (sin schema DB)
32
+ sockets.ts ← OBLIGATORIO — interfaz de eventos hacia otros módulos
33
+ service.ts ← OBLIGATORIO — facade del módulo (lógica de negocio)
34
+ controller.ts ← OBLIGATORIO — adaptador HTTP
35
+ repository.ts ← OPCIONAL — queries con nombres del dominio (ver Regla #21)
36
+ usecases/ ← OPCIONAL — solo si hay 3+ flujos DIFERENTES (no CRUD)
37
+ validators/schema.ts ← OBLIGATORIO — esquemas de validateSchema()
38
+ tests/service.test.ts ← OBLIGATORIO — mínimo 2 casos (happy path + error)
39
+ \`\`\`
40
+
41
+ **\`service.ts\` y \`controller.ts\` van al ROOT del módulo, NO en \`actions/\`.**
42
+ **\`model.ts\` separado de \`types.ts\`** porque schema DB y contrato TS son conceptos distintos.
43
+
44
+ ---
45
+
46
+ ## Reglas INMUTABLES (no se negocian, no se omiten)
47
+
48
+ | # | Regla | Detección |
49
+ |---|-------|-----------|
50
+ | 1 | Módulos NO importan de otros módulos → usar conectores | \`arckode analyze\` |
51
+ | 2 | index.ts es APPEND-ONLY → no eliminar exports existentes | manual |
52
+ | 3 | Conectores NO tienen lógica de negocio → solo delegan | \`arckode analyze\` |
53
+ | 4 | Cada tabla pertenece a UN módulo | manual |
54
+ | 5 | Si no está en composition-root.ts, no existe | \`arckode analyze\` |
55
+ | 6 | Todo POST/PUT/PATCH requiere validateSchema() | \`arckode analyze\` |
56
+ | 7 | Controller NO llama al ORM → llama al service | \`arckode analyze\` |
57
+ | 8 | Toda operación atómica multi-tabla usa Transactor (no inyectar ORM) | manual |
58
+ | 9 | findById → verificar ownership inmediatamente (IDOR) | \`arckode analyze\` |
59
+ | 10 | ORM dentro de un loop → N+1 prohibido | \`arckode analyze\` |
60
+ | 11 | Service recibe RepositoryAdapter<T> o Repository del dominio, NO ORM directo | \`arckode analyze\` |
61
+ | 15 | Extraer a usecases/ solo si hay 3+ flujos DIFERENTES (CRUD nunca se fragmenta) | manual |
62
+ | 19 | Transactor para atomicidad multi-tabla — \`transactor.run(async (repos) => ...)\` | manual |
63
+ | 20 | Conectores limpios: kebab-case, con prefijo \`connect\`, registrados en composition-root | \`arckode analyze\` |
64
+ | 21 | Si el service repite filtros 3+ veces o necesita JOIN/IN/LIKE → crear \`repository.ts\` | manual |
65
+ | 22 | \`model.ts\` separado de \`types.ts\` (schema DB ≠ contrato TS) | manual |
66
+
67
+ ---
68
+
69
+ ## Protocolo de la IA (4 pasos obligatorios)
70
+
71
+ \`\`\`
72
+ PASO 1 — IDENTIFICAR ALCANCE
73
+ ¿Toca 1 módulo? → Seguir
74
+ ¿Toca 1 conector? → Seguir
75
+ ¿Toca 2+ módulos? → DETENERSE. Dividir la tarea.
76
+
77
+ PASO 2 — VERIFICAR REGLAS
78
+ ¿Importo de otro módulo? → NO (usar conector)
79
+ ¿Modifico index.ts? → Solo append
80
+ ¿Pongo lógica en el conector? → NO
81
+ ¿Comparto una tabla con otro módulo? → NO
82
+ ¿Inyecto ORM al service? → NO (RepositoryAdapter<T> o Repository del dominio)
83
+
84
+ PASO 3 — GENERAR (estructura nueva)
85
+ Obligatorios: index.ts, model.ts, types.ts, sockets.ts,
86
+ service.ts, controller.ts (al root),
87
+ validators/schema.ts, tests/service.test.ts
88
+ Opcionales: repository.ts (si hay queries del dominio repetidas)
89
+ usecases/ (si hay 3+ flujos DIFERENTES, no CRUD)
90
+
91
+ PASO 4 — VERIFICAR
92
+ bun run analyze --json → violations: 0
93
+ bun test → 0 fallos
94
+ \`\`\`
95
+
96
+ ---
97
+
98
+ ## Patrones de código
99
+
100
+ ### Atomicidad multi-tabla — usar Transactor
101
+ \`\`\`ts
102
+ import type { Transactor, RepositoryAdapter } from 'arckode-framework'
103
+
104
+ class WalletsService {
105
+ constructor(
106
+ private walletRepo: RepositoryAdapter<WalletDTO>,
107
+ private transactor: Transactor,
108
+ ) {}
109
+
110
+ async credit(userId: string, amount: number) {
111
+ await this.transactor.run(async (repos) => {
112
+ const wallets = repos.for<WalletDTO>('Wallet')
113
+ const txs = repos.for<TransactionDTO>('Transaction')
114
+ await wallets.update(id, { balance })
115
+ await txs.create({ ... })
116
+ })
117
+ }
118
+ }
119
+
120
+ // composition-root:
121
+ const transactor = new OrmTransactor(orm)
122
+ \`\`\`
123
+
124
+ ### Repository del dominio (cuando aplique)
125
+ \`\`\`ts
126
+ // repository.ts — opcional, encapsula queries con nombres del dominio
127
+ export class WalletsRepository {
128
+ constructor(private base: RepositoryAdapter<WalletDTO>) {}
129
+
130
+ findByUserAndCurrency(userId: string, currency: WalletCurrency) {
131
+ return this.base.findOne({ user_id: userId, currency })
132
+ }
133
+ }
134
+
135
+ // service.ts — habla en lenguaje del dominio, no de columnas
136
+ const wallet = await this.wallets.findByUserAndCurrency(userId, currency)
137
+ \`\`\`
138
+
139
+ ---
140
+
141
+ ## DB Adapter activo: \`${p.db}\`
142
+
143
+ ## Estructura del proyecto
144
+ \`\`\`
145
+ src/
146
+ composition-root.ts ← TODO el sistema (módulos, conectores, infra)
147
+ modules/ ← módulos independientes (NO se importan entre sí)
148
+ connectors/ ← puentes entre módulos (sin lógica de negocio)
149
+ shared/helpers/ ← funciones puras sin estado
150
+ shared/services/ ← servicios con estado compartidos
151
+ seeds/ ← datos de prueba
152
+ migrations/ ← migraciones explícitas de DB
153
+ \`\`\`
154
+
155
+ ## Comandos útiles
156
+ \`\`\`bash
157
+ bun run dev # desarrollo con hot reload
158
+ bun run analyze # verificar arquitectura (consola)
159
+ bun run analyze:json # generar/actualizar arckode.json
160
+ bun arckode make:module Nombre # generar módulo completo
161
+ bun arckode make:connector nombre-x mod1 mod2 # conectar dos módulos (kebab-case)
162
+ bun test # correr todos los tests
163
+ \`\`\`
164
+
165
+ ---
166
+
167
+ ## Notas para la IA
168
+
169
+ - Si el módulo es CRUD simple: NO crees \`repository.ts\` ni \`usecases/\`. Pasás \`OrmRepository<T>\` directo al service.
170
+ - Si el módulo tiene 3+ tablas: definí TODOS los modelos en \`model.ts\` y registralos en \`registerXxxModels(orm)\` (helper único). El \`index.ts\` queda limpio.
171
+ - Si necesitás atomicidad: inyectá \`Transactor\` al service, no el ORM. Mantiene la dependency inversion.
172
+ - Si el analyzer muestra \`LEGACY_ACTIONS_FOLDER\`, \`DUPLICATE_CONNECTOR\` o \`UNREGISTERED_CONNECTOR\`: arreglarlos ANTES de pushear código nuevo.
173
+ `
174
+ }
@@ -18,8 +18,9 @@ export function indexStub(p: ModuleStubParams): string {
18
18
  // ⚠ REGLA: Append-only. No sacar ni modificar exports existentes.
19
19
 
20
20
  import { createModule, OrmRepository } from 'arckode-framework'
21
- import { ${p.name}Service } from './actions/service'
22
- import { ${p.name}Controller } from './actions/controller'
21
+ import { register${p.name}Models } from './model'
22
+ import { ${p.name}Service } from './service'
23
+ import { ${p.name}Controller } from './controller'
23
24
  import type { ${p.name}DTO } from './types'
24
25
 
25
26
  export { ${p.name}Service }
@@ -45,6 +46,9 @@ export function ${p.name}Module() {
45
46
  },
46
47
 
47
48
  create({ logger, orm, cache, router, auth }) {
49
+ // Registrar modelo(s) — delegado a model.ts
50
+ register${p.name}Models(orm)
51
+
48
52
  const repo = new OrmRepository<${p.name}DTO>(orm, '${p.name}')
49
53
  const log = logger.child('${p.module}')
50
54
  const service = new ${p.name}Service(repo, log, cache)
@@ -65,10 +69,10 @@ export function ${p.name}Module() {
65
69
  `
66
70
  }
67
71
 
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
+ // ─── model.ts — Definición de DB (schema) ───
73
+ // Separado de types.ts porque conceptualmente describe la PERSISTENCIA (la DB),
74
+ // no el contrato de TypeScript (DTOs). Cambia con migraciones, no con la API.
75
+ export function modelStub(p: ModuleStubParams): string {
72
76
  const modelFields = p.fields
73
77
  .filter(f => !['id', 'createdAt', 'updatedAt', 'deletedAt'].includes(f.name))
74
78
  .map(f => {
@@ -80,13 +84,14 @@ export function typesStub(p: ModuleStubParams): string {
80
84
  })
81
85
  .join(',\n')
82
86
 
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í.
87
+ return `// ${p.module}/model.ts — Schema de base de datos
88
+ // Responsabilidad ÚNICA: describir la estructura física de la tabla.
89
+ // Importado por index.ts orm.define() para registrar el modelo.
90
+ //
91
+ // Si el módulo tiene 3+ tablas, agregá registerModels(orm) acá y delegá desde index.ts.
86
92
 
87
- import type { ModelDefinition } from 'arckode-framework'
93
+ import type { ModelDefinition, ORM } from 'arckode-framework'
88
94
 
89
- // Definición del modelo para ORM.define() — importar en composition-root.ts
90
95
  export const ${p.name}Model: ModelDefinition = {
91
96
  table: '${p.module}',
92
97
  fields: {
@@ -96,6 +101,22 @@ ${modelFields}
96
101
  ${p.softDelete ? ' softDelete: true,' : ''}
97
102
  }
98
103
 
104
+ // Helper opcional — usalo si el módulo registra 3+ tablas
105
+ // (mantiene index.ts limpio, enfocado en wiring)
106
+ export function register${p.name}Models(orm: ORM): void {
107
+ orm.define('${p.name}', ${p.name}Model)
108
+ }
109
+ `
110
+ }
111
+
112
+ export function typesStub(p: ModuleStubParams): string {
113
+ const fields = p.fields.map(f => ` ${f.name}${f.required ? '' : '?'}: ${mapTS(f.type)}`).join('\n')
114
+ const filterFields = p.fields.map(f => ` ${f.name}?: ${mapTS(f.type)}`).join('\n')
115
+
116
+ return `// ${p.module}/types.ts — DTOs y tipos de queries
117
+ // Responsabilidad ÚNICA: contrato TypeScript del módulo (cómo se ven los datos).
118
+ // El schema de DB vive en ./model.ts — son conceptos distintos.
119
+
99
120
  export interface ${p.name}DTO {
100
121
  id: string
101
122
  ${fields}
@@ -154,18 +175,21 @@ export interface ${p.name}Sockets {
154
175
  export function serviceStub(p: ModuleStubParams): string {
155
176
  const hasSearch = p.fields.some(f => f.type === 'string')
156
177
 
157
- return `// ${p.module}/actions/service.ts — Lógica de negocio
178
+ return `// ${p.module}/service.ts — Facade pública del módulo
158
179
  // Responsabilidad ÚNICA: casos de uso del módulo.
159
180
  // NO sabe de HTTP. NO importa de otros módulos.
160
181
  // Recibe dependencias por constructor (Dependency Inversion).
161
182
  //
183
+ // Si este archivo supera 200 líneas → extraer casos de uso a ./usecases/{caso}.ts
184
+ // y dejar acá solo el orquestador que delega.
185
+ //
162
186
  // IMPORTANTE: depende de RepositoryAdapter<${p.name}DTO>, no del ORM directamente.
163
187
  // Esto permite swapear SQL → MongoDB → Prisma en composition-root.ts sin tocar este archivo.
164
188
 
165
189
  import type { RepositoryAdapter, Logger, CacheAdapter } from 'arckode-framework'
166
190
  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'
191
+ import type { ${p.name}DTO, Create${p.name}DTO, Update${p.name}DTO, ${p.name}Query, ${p.name}Paginated } from './types'
192
+ import type { ${p.name}Sockets } from './sockets'
169
193
 
170
194
  export class ${p.name}Service {
171
195
  private sockets: ${p.name}Sockets = {}
@@ -209,6 +233,8 @@ export class ${p.name}Service {
209
233
  this.logger.info('Obteniendo ${p.module}', { id })
210
234
  const item = await this.repo.findById(id)
211
235
  if (!item) throw new NotFoundError('${p.name} no encontrado')
236
+ // IDOR: si el recurso tiene userId u ownerId, descomentar y adaptar:
237
+ // auth.assertOwnership(item.userId as string, currentUser.id, currentUser.role)
212
238
  return item
213
239
  }
214
240
 
@@ -241,14 +267,15 @@ export class ${p.name}Service {
241
267
  }
242
268
 
243
269
  export function controllerStub(p: ModuleStubParams): string {
244
- return `// ${p.module}/actions/controller.ts — Capa HTTP
245
- // Responsabilidad ÚNICA: traducir request/response.
246
- // SIN lógica de negocio. SIN validación directa.
270
+ return `// ${p.module}/controller.ts — Adaptador HTTP del módulo
271
+ // Responsabilidad ÚNICA: traducir request → service → response.
272
+ // SIN lógica de negocio. SIN llamadas directas al ORM. (REGLA #12)
273
+ // Toda mutación (POST/PUT/PATCH) DEBE pasar por validateSchema(). (REGLA #11)
247
274
 
248
275
  import type { HttpRequest, Logger } from 'arckode-framework'
249
276
  import { validateSchema } from 'arckode-framework'
250
277
  import type { ${p.name}Service } from './service'
251
- import { Create${p.name}Schema, Update${p.name}Schema } from '../validators/schema'
278
+ import { Create${p.name}Schema, Update${p.name}Schema } from './validators/schema'
252
279
 
253
280
  export class ${p.name}Controller {
254
281
  constructor(
@@ -301,7 +328,7 @@ export function validatorStub(p: ModuleStubParams): string {
301
328
  parts.push(' }')
302
329
  return parts.join('')
303
330
  })
304
- .join('\n')
331
+ .join(',\n')
305
332
 
306
333
  const updateRules = p.fields
307
334
  .filter(f => f.name !== 'id' && f.name !== 'createdAt')
@@ -311,7 +338,7 @@ export function validatorStub(p: ModuleStubParams): string {
311
338
  parts.push(' }')
312
339
  return parts.join('')
313
340
  })
314
- .join('\n')
341
+ .join(',\n')
315
342
 
316
343
  return `// ${p.module}/validators/schema.ts — Validación de entrada
317
344
  // Schemas planos, sin dependencias externas.
@@ -341,7 +368,7 @@ export function testStub(p: ModuleStubParams): string {
341
368
  import { describe, it, expect } from 'bun:test'
342
369
  import type { RepositoryAdapter, CacheAdapter } from 'arckode-framework'
343
370
  import { silentLogger } from 'arckode-framework/testing'
344
- import { ${p.name}Service } from '../actions/service'
371
+ import { ${p.name}Service } from '../service'
345
372
  import type { ${p.name}DTO } from '../types'
346
373
 
347
374
  // silentLogger es una factory function — SIEMPRE llamarla con ()
@@ -469,6 +496,48 @@ ${sampleData.replace(`'${p.name} de ejemplo'`, `'${p.name} de ejemplo 2'`).repla
469
496
  `
470
497
  }
471
498
 
499
+ // ─── repository.ts — OPCIONAL ────────────────────────────
500
+ // NO se genera por default. Crealo a mano cuando:
501
+ // 1. El service repite los mismos filtros en varios métodos
502
+ // (ej: findOne({ user_id, currency }) en 4 lugares)
503
+ // 2. Necesitás queries con JOIN, IN, LIKE — el ORM builtin no las soporta
504
+ // 3. Querés expresar la query en lenguaje del DOMINIO
505
+ // (ej: findActiveByUser vs findMany({ user_id, status: 'active' }))
506
+ //
507
+ // Si solo hacés CRUD pelado → NO necesitás repository.ts.
508
+ // Pasás el OrmRepository<T> directo al service.
509
+ export function repositoryStub(p: ModuleStubParams): string {
510
+ return `// ${p.module}/repository.ts — Repository de dominio (OPCIONAL)
511
+ // Encapsula queries con NOMBRES DEL DOMINIO en lugar de nombres de columna.
512
+ //
513
+ // Antes: service.findOne({ user_id: userId, currency }) ← service conoce la DB
514
+ // Ahora: service.findByUserAndCurrency(userId, currency) ← service habla el dominio
515
+ //
516
+ // Implementa RepositoryAdapter<${p.name}DTO> si necesitás queries complejas (JOIN, IN).
517
+
518
+ import type { RepositoryAdapter } from 'arckode-framework'
519
+ import type { ${p.name}DTO } from './types'
520
+
521
+ export class ${p.name}Repository {
522
+ constructor(private readonly base: RepositoryAdapter<${p.name}DTO>) {}
523
+
524
+ // Métodos genéricos delegados al adapter base
525
+ findById(id: string) { return this.base.findById(id) }
526
+ findMany(filters?: Record<string, unknown>) { return this.base.findMany(filters) }
527
+ create(data: Omit<${p.name}DTO, 'id'>) { return this.base.create(data) }
528
+ update(id: string, data: Partial<Omit<${p.name}DTO, 'id'>>) { return this.base.update(id, data) }
529
+ delete(id: string) { return this.base.delete(id) }
530
+
531
+ // ─── Métodos específicos del dominio ───
532
+ // Agregá acá las queries que se repiten en el service
533
+ //
534
+ // async findActiveByUser(userId: string): Promise<${p.name}DTO[]> {
535
+ // return this.base.findMany({ user_id: userId, active: true })
536
+ // }
537
+ }
538
+ `
539
+ }
540
+
472
541
  function mapTS(type: string): string {
473
542
  const map: Record<string, string> = { string: 'string', number: 'number', boolean: 'boolean', date: 'string' }
474
543
  return map[type] ?? 'unknown'
@@ -463,6 +463,35 @@ export interface RepositoryAdapter<T extends object = Record<string, unknown>> {
463
463
  ): Promise<PageResult<T>>
464
464
  }
465
465
 
466
+ // ── Transactor — atomicidad cross-modelo SIN exponer ORM al service ──
467
+ //
468
+ // Los services NO reciben ORM. Para operaciones atómicas multi-tabla
469
+ // (ledger doble entrada, transferencias, etc.), reciben Transactor.
470
+ //
471
+ // El service obtiene repos transaccionales por modelo dentro del run():
472
+ //
473
+ // await this.transactor.run(async (repos) => {
474
+ // const walletRepo = repos.for<WalletDTO>('Wallet')
475
+ // const txRepo = repos.for<TransactionDTO>('Transaction')
476
+ // await walletRepo.update(id, { balance })
477
+ // await txRepo.create({ ... })
478
+ // })
479
+ //
480
+ // Si una operación dentro de run() falla, todo se rolleabackea automáticamente.
481
+ //
482
+ // Implementaciones:
483
+ // OrmTransactor → built-in, delega a ORM.transaction()
484
+ // Custom → implementar para Prisma, Drizzle, MongoDB, etc.
485
+
486
+ export interface TransactionalRepos {
487
+ /** Crea un RepositoryAdapter<T> ligado a la transacción activa para el modelo dado. */
488
+ for<T extends object = Record<string, unknown>>(modelName: string): RepositoryAdapter<T>
489
+ }
490
+
491
+ export interface Transactor {
492
+ run<R>(fn: (repos: TransactionalRepos) => Promise<R>): Promise<R>
493
+ }
494
+
466
495
  // ── ORM — SOLID: S = solo ORM, D = depende de DbAdapter ──
467
496
  export class ORM {
468
497
  private models = new Map<string, ModelDefinition>()
@@ -518,11 +547,28 @@ export class ORM {
518
547
 
519
548
  private deserializeFromDb(def: ModelDefinition, row: ModelResult): ModelResult {
520
549
  const result = { ...row } as Record<string, unknown>
550
+
551
+ // Deserializar campos JSON almacenados como string
521
552
  for (const [k, field] of Object.entries(def.fields)) {
522
553
  if (field.type === 'json' && typeof result[k] === 'string') {
523
554
  try { result[k] = JSON.parse(result[k] as string) } catch { /* no es JSON válido */ }
524
555
  }
525
556
  }
557
+
558
+ // Normalizar timestamps que PostgreSQL devuelve en minúscula
559
+ // PostgreSQL baja el case de identificadores no-quoted: createdAt → createdat
560
+ const tsMap: Record<string, string> = {
561
+ createdat: 'createdAt',
562
+ updatedat: 'updatedAt',
563
+ deletedat: 'deletedAt',
564
+ }
565
+ for (const [lower, camel] of Object.entries(tsMap)) {
566
+ if (lower in result && !(camel in result)) {
567
+ result[camel] = result[lower]
568
+ delete result[lower]
569
+ }
570
+ }
571
+
526
572
  return result as ModelResult
527
573
  }
528
574
 
@@ -973,6 +1019,31 @@ export class OrmRepository<T extends object = Record<string, unknown>>
973
1019
  }
974
1020
  }
975
1021
 
1022
+ // ── OrmTransactor — implementación SQL del Transactor ──
1023
+ //
1024
+ // Composition root:
1025
+ // const transactor = new OrmTransactor(orm)
1026
+ // const service = new WalletsService(walletRepo, txRepo, transactor, logger, cache)
1027
+ //
1028
+ // El service recibe Transactor (interfaz). Para swapear a Prisma:
1029
+ // class PrismaTransactor implements Transactor { ... }
1030
+ // const transactor = new PrismaTransactor(prisma)
1031
+ // El service NO cambia.
1032
+
1033
+ export class OrmTransactor implements Transactor {
1034
+ constructor(private readonly orm: ORM) {}
1035
+
1036
+ run<R>(fn: (repos: TransactionalRepos) => Promise<R>): Promise<R> {
1037
+ return this.orm.transaction(async (txOrm) => {
1038
+ const repos: TransactionalRepos = {
1039
+ for: <T extends object = Record<string, unknown>>(modelName: string) =>
1040
+ new OrmRepository<T>(txOrm, modelName) as RepositoryAdapter<T>,
1041
+ }
1042
+ return fn(repos)
1043
+ })
1044
+ }
1045
+ }
1046
+
976
1047
  // ═══════════════════════════════════════════════════════════════
977
1048
  // 6. ROUTER + HTTP — Enrutamiento con middlewares y error handler
978
1049
  // ═══════════════════════════════════════════════════════════════
@@ -1886,7 +1957,7 @@ export default {
1886
1957
  Logger, ConsoleTransport,
1887
1958
  ConfigStore, loadEnv,
1888
1959
  Container,
1889
- ORM, OrmRepository,
1960
+ ORM, OrmRepository, OrmTransactor,
1890
1961
  Router, NodeServer,
1891
1962
  validateSchema,
1892
1963
  Auth, MemoryCache,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arckode-framework",
3
- "version": "1.0.7",
3
+ "version": "1.1.0",
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",
@@ -55,13 +55,12 @@
55
55
  "analyze:framework": "bun run cli/index.ts analyze"
56
56
  },
57
57
  "dependencies": {
58
- "arckode-framework": "^1.0.3",
59
58
  "jsonwebtoken": "^9.0.0"
60
59
  },
61
60
  "peerDependencies": {
62
61
  "better-sqlite3": ">=11.0.0",
63
62
  "mysql2": ">=3.0.0",
64
- "pg": ">=8.0.0",
63
+ "pg": "^8.0.0",
65
64
  "redis": ">=4.0.0",
66
65
  "nodemailer": ">=6.0.0"
67
66
  },
@@ -88,6 +87,7 @@
88
87
  "@types/nodemailer": "^6.4.0",
89
88
  "@types/pg": "^8.11.0",
90
89
  "bun-types": "^1.3.14",
90
+ "pg": "^8.0.0",
91
91
  "typescript": "^5.6.0"
92
92
  },
93
93
  "keywords": [
@@ -36,7 +36,7 @@ JWT_REFRESH_SECRET=otra-cadena-distinta-para-refresh-tokens
36
36
  ## 2. REGISTRO Y LOGIN (patrón completo)
37
37
 
38
38
  ```ts
39
- // En el módulo de autenticación — actions/service.ts
39
+ // En el módulo de autenticación — service.ts (al root del módulo)
40
40
  export class AuthService {
41
41
  constructor(
42
42
  private repo: RepositoryAdapter<UserDTO>,
@@ -28,20 +28,26 @@ arckode db:migrate --rollback # Revertir última migración
28
28
  arckode make:module Producto
29
29
  ```
30
30
 
31
- Genera en `src/modules/producto/`:
31
+ Genera en `src/modules/producto/` (nueva estructura):
32
32
  ```
33
- index.ts ← ProductoModule() con createModule + OrmRepository + rutas
34
- types.ts ← ProductoModel (ModelDefinition) + ProductoDTO + CreateProductoDTO
35
- sockets.ts ProductosSockets interface + SocketsAware
36
- actions/
37
- service.ts ← ProductosService con RepositoryAdapter<ProductoDTO>
38
- controller.ts ← ProductosController con validateSchema en store/update
33
+ index.ts ← ProductoModule() registra modelos, crea repos, wirea rutas
34
+ model.ts ← ProductoModel (ModelDefinition) + registerProductoModels(orm)
35
+ types.ts ProductoDTO + CreateProductoDTO + UpdateProductoDTO + queries
36
+ sockets.ts ← ProductosSockets interface
37
+ service.ts ← ProductosService con RepositoryAdapter<ProductoDTO> (al root)
38
+ controller.ts ← ProductosController con validateSchema en store/update (al root)
39
39
  validators/
40
40
  schema.ts ← crearProductoSchema + actualizarProductoSchema
41
41
  tests/
42
42
  service.test.ts ← 2 test cases: listar + crear con error
43
43
  ```
44
44
 
45
+ **Opcionales (no generados — agregar a mano cuando aplique):**
46
+ - `repository.ts` → queries con nombres del dominio (si el service repite filtros)
47
+ - `usecases/{caso}.ts` → solo si hay 3+ flujos DIFERENTES (no CRUD)
48
+
49
+ `service.ts` y `controller.ts` van al ROOT del módulo. `model.ts` está separado de `types.ts` por convención: schema DB vs contrato TypeScript.
50
+
45
51
  **Importante:** El nombre se usa en PascalCase para clases, camelCase para variables, kebab-case para rutas:
46
52
  - `arckode make:module LineaPedido` → clase `LineaPedidoService`, tabla `linea_pedidos`, ruta `/linea-pedidos`
47
53
 
@@ -102,11 +108,11 @@ Output ejemplo:
102
108
  ```
103
109
  🔍 Analizando proyecto...
104
110
 
105
- ❌ DIRECT_MODULE_IMPORT modules/pedidos/actions/service.ts:5
106
- "import { ProductosService } from '../productos/actions/service'"
111
+ ❌ DIRECT_MODULE_IMPORT modules/pedidos/service.ts:5
112
+ "import { ProductosService } from '../productos/service'"
107
113
  → Los módulos no pueden importar de otros módulos directamente.
108
114
 
109
- ❌ CONTROLLER_MISSING_VALIDATION modules/productos/actions/controller.ts:23
115
+ ❌ CONTROLLER_MISSING_VALIDATION modules/productos/controller.ts:23
110
116
  "router.post sin validateSchema()"
111
117
  → Todo POST debe validar el body antes de llamar al service.
112
118
 
@@ -126,8 +132,11 @@ Resumen: 2 errores, 1 advertencia
126
132
  | Código | Severidad | Descripción |
127
133
  |--------|-----------|-------------|
128
134
  | `MISSING_INDEX` | ❌ | Módulo sin index.ts |
129
- | `MISSING_SERVICE` | ❌ | Sin actions/service.ts |
130
- | `MISSING_CONTROLLER` | ❌ | Sin actions/controller.ts |
135
+ | `MISSING_SERVICE` | ❌ | Sin service.ts (al root del módulo) |
136
+ | `MISSING_CONTROLLER` | ❌ | Sin controller.ts (al root del módulo) |
137
+ | `LEGACY_ACTIONS_FOLDER` | ⚠️ | Módulo usa estructura legacy actions/ — mover al root |
138
+ | `DUPLICATE_CONNECTOR` | ❌ | Conectores con mismo nombre lógico (kebab + camelCase) |
139
+ | `UNREGISTERED_CONNECTOR` | ❌ | Archivo en connectors/ no importado en composition-root |
131
140
  | `MISSING_TYPES` | ❌ | Sin types.ts |
132
141
  | `MISSING_VALIDATORS` | ❌ | Sin validators/schema.ts |
133
142
  | `MISSING_TESTS` | ❌ | Sin directorio tests/ |
@@ -60,8 +60,8 @@ export interface PedidosSockets {
60
60
  onPedidoCancelado?: (pedido: PedidoDTO) => Promise<void>
61
61
  }
62
62
 
63
- // En actions/service.ts
64
- import type { PedidosSockets } from '../sockets'
63
+ // En service.ts (al root del módulo)
64
+ import type { PedidosSockets } from './sockets'
65
65
 
66
66
  export class PedidosService {
67
67
  private sockets: PedidosSockets = {}
@@ -184,8 +184,8 @@ events.on('pedido.confirmado', async (event) => {
184
184
  ## 8. ANTI-PATRONES DE CONECTORES
185
185
 
186
186
  ```ts
187
- // ❌ Importar desde actions/service directamente (viola REGLA #1)
188
- import { PedidosService } from '../modules/pedidos/actions/service' // NO
187
+ // ❌ Importar desde ./service directamente (viola REGLA #1)
188
+ import { PedidosService } from '../modules/pedidos/service' // NO
189
189
 
190
190
  // ✅ Solo desde index.ts (puerta pública)
191
191
  import type { PedidosService } from '../modules/pedidos'
@@ -63,7 +63,7 @@ class PedidosService {
63
63
  | Tipo | Dónde va | Ejemplo |
64
64
  |------|---------|---------|
65
65
  | Función pura de transformación | `shared/helpers/` | `formatearFecha`, `slugify`, `calcularIVA` |
66
- | Lógica de negocio | `modules/{modulo}/actions/service.ts` | `calcularDescuento(pedido)` |
66
+ | Lógica de negocio | `modules/{modulo}/service.ts` | `calcularDescuento(pedido)` |
67
67
  | Validación de dominio | `modules/{modulo}/validators/schema.ts` | Schema de validación |
68
68
  | Constante compartida | `shared/constants.ts` | `IVA_RATE = 0.18` |
69
69
  | Tipo/interfaz compartida | `shared/types.ts` | `Paginado<T>`, `ApiResponse<T>` |