arckode-framework 1.0.8 → 1.1.1

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>()
@@ -990,6 +1019,31 @@ export class OrmRepository<T extends object = Record<string, unknown>>
990
1019
  }
991
1020
  }
992
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
+
993
1047
  // ═══════════════════════════════════════════════════════════════
994
1048
  // 6. ROUTER + HTTP — Enrutamiento con middlewares y error handler
995
1049
  // ═══════════════════════════════════════════════════════════════
@@ -1198,6 +1252,55 @@ export interface ServerAdapter {
1198
1252
  stop(): Promise<void>
1199
1253
  }
1200
1254
 
1255
+ // ── Envelope estándar de respuesta API ──────────────────────────
1256
+ // Todas las respuestas siguen este contrato. Un solo lugar para cambiar el formato.
1257
+ // Inspirado en JSend + convenciones de GitHub/Laravel:
1258
+ // success → { success: true, data: T, meta: { pagination? } | null, error: null }
1259
+ // error → { success: false, data: null, meta: null, error: { code, message, details } }
1260
+ export interface ApiResponse<T = unknown> {
1261
+ success: boolean
1262
+ data: T | null
1263
+ meta: { pagination?: unknown } | null
1264
+ error: { code: string; message: string; details?: unknown } | null
1265
+ }
1266
+
1267
+ function buildEnvelope(status: number, body: unknown): string {
1268
+ // Errores 4xx / 5xx
1269
+ if (status >= 400) {
1270
+ const b = (body ?? {}) as Record<string, unknown>
1271
+ return JSON.stringify({
1272
+ success: false,
1273
+ data: null,
1274
+ meta: null,
1275
+ error: {
1276
+ code: b.code ?? 'ERROR',
1277
+ message: b.error ?? 'Error',
1278
+ details: b.details ?? null,
1279
+ },
1280
+ } satisfies ApiResponse)
1281
+ }
1282
+
1283
+ // Sin contenido (DELETE 204)
1284
+ if (body === null || body === undefined) {
1285
+ return JSON.stringify({ success: true, data: null, meta: null, error: null } satisfies ApiResponse)
1286
+ }
1287
+
1288
+ const b = body as Record<string, unknown>
1289
+
1290
+ // Lista paginada — el service retorna { data: [], pagination: {} }
1291
+ if (Array.isArray(b.data) && b.pagination !== undefined) {
1292
+ return JSON.stringify({
1293
+ success: true,
1294
+ data: b.data,
1295
+ meta: { pagination: b.pagination },
1296
+ error: null,
1297
+ } satisfies ApiResponse)
1298
+ }
1299
+
1300
+ // Recurso único o cualquier otro payload exitoso
1301
+ return JSON.stringify({ success: true, data: body, meta: null, error: null } satisfies ApiResponse)
1302
+ }
1303
+
1201
1304
  export class NodeServer implements ServerAdapter {
1202
1305
  private server?: ReturnType<typeof createNodeServer>
1203
1306
  private maxBodyBytes: number
@@ -1260,8 +1363,10 @@ export class NodeServer implements ServerAdapter {
1260
1363
 
1261
1364
  // ── Respuesta normal (JSON o binario comprimido) ──
1262
1365
  const isBuffer = Buffer.isBuffer(res.body)
1263
- const responseBody = isBuffer ? res.body : JSON.stringify(res.body)
1264
- nodeRes.writeHead(res.status, {
1366
+ // 204 prohíbe body por spec HTTP — con envelope usamos 200 para "no content"
1367
+ const effectiveStatus = res.status === 204 ? 200 : res.status
1368
+ const responseBody = isBuffer ? res.body : buildEnvelope(effectiveStatus, res.body)
1369
+ nodeRes.writeHead(effectiveStatus, {
1265
1370
  'Content-Type': 'application/json',
1266
1371
  'X-Request-Id': req.id,
1267
1372
  ...res.headers,
@@ -1271,13 +1376,13 @@ export class NodeServer implements ServerAdapter {
1271
1376
  const httpStatus = (error as any)?.httpStatus
1272
1377
  if (httpStatus) {
1273
1378
  nodeRes.writeHead(httpStatus, { 'Content-Type': 'application/json' })
1274
- nodeRes.end(JSON.stringify({ error: (error as Error).message, code: 'REQUEST_ERROR' }))
1379
+ nodeRes.end(buildEnvelope(httpStatus, { error: (error as Error).message, code: 'REQUEST_ERROR' }))
1275
1380
  return
1276
1381
  }
1277
1382
  const stack = error instanceof Error ? error.stack : String(error)
1278
1383
  this.logger.error('Error no manejado en HTTP', { error: String(error), stack })
1279
1384
  nodeRes.writeHead(500, { 'Content-Type': 'application/json' })
1280
- nodeRes.end(JSON.stringify({ error: 'Error interno', code: 'INTERNAL_ERROR' }))
1385
+ nodeRes.end(buildEnvelope(500, { error: 'Error interno', code: 'INTERNAL_ERROR' }))
1281
1386
  } finally {
1282
1387
  this.activeRequests--
1283
1388
  }
@@ -1903,7 +2008,7 @@ export default {
1903
2008
  Logger, ConsoleTransport,
1904
2009
  ConfigStore, loadEnv,
1905
2010
  Container,
1906
- ORM, OrmRepository,
2011
+ ORM, OrmRepository, OrmTransactor,
1907
2012
  Router, NodeServer,
1908
2013
  validateSchema,
1909
2014
  Auth, MemoryCache,
@@ -1914,6 +2019,6 @@ export default {
1914
2019
  // Seeds
1915
2020
  SeedRunner,
1916
2021
 
1917
- // Types (re-exported for convenience)
1918
- // All interfaces are exported above
2022
+ // Response envelope
2023
+ // ApiResponse interface is exported as named export above
1919
2024
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arckode-framework",
3
- "version": "1.0.8",
3
+ "version": "1.1.1",
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/ |