arckode-framework 1.1.1 → 1.2.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.
@@ -1,8 +1,8 @@
1
- // adapters/sqlite.ts — Adapter SQLite
2
- // Responsabilidad ÚNICA: traducir llamadas de DbAdapter a better-sqlite3
1
+ // adapters/sqlite.ts — Adapter SQLite (bun:sqlite)
2
+ // Responsabilidad ÚNICA: traducir llamadas de DbAdapter a bun:sqlite
3
3
  // SOLID: implementa DbAdapter, puede reemplazarse por PostgresAdapter sin cambiar el ORM
4
4
 
5
- import Database from 'better-sqlite3'
5
+ import { Database } from 'bun:sqlite'
6
6
  import type { DbAdapter } from '../kernel/framework'
7
7
 
8
8
  interface SqliteConfig {
@@ -11,7 +11,6 @@ interface SqliteConfig {
11
11
  foreignKeys?: boolean
12
12
  }
13
13
 
14
- // Configuración con valores por defecto
15
14
  const DEFAULT_CONFIG: SqliteConfig = {
16
15
  path: './data/db.sqlite',
17
16
  wal: true,
@@ -19,7 +18,7 @@ const DEFAULT_CONFIG: SqliteConfig = {
19
18
  }
20
19
 
21
20
  export class SqliteAdapter implements DbAdapter {
22
- private db!: Database.Database
21
+ private db!: Database
23
22
  private config: SqliteConfig
24
23
 
25
24
  constructor(config?: Partial<SqliteConfig>) {
@@ -29,19 +28,21 @@ export class SqliteAdapter implements DbAdapter {
29
28
  async connect(): Promise<void> {
30
29
  this.db = new Database(this.config.path)
31
30
 
32
- if (this.config.wal) this.db.pragma('journal_mode = WAL')
33
- if (this.config.foreignKeys) this.db.pragma('foreign_keys = ON')
31
+ if (this.config.wal) this.db.exec('PRAGMA journal_mode = WAL')
32
+ if (this.config.foreignKeys) this.db.exec('PRAGMA foreign_keys = ON')
34
33
  }
35
34
 
36
35
  query(sql: string, params: unknown[] = []): Promise<unknown[]> {
37
- const stmt = this.db.prepare(sql)
38
- const rows = params.length > 0 ? stmt.all(...params) : stmt.all()
39
- return Promise.resolve(rows)
36
+ const stmt = this.db.query(sql)
37
+ const rows = params.length > 0 ? stmt.all(...(params as Parameters<typeof stmt.all>)) : stmt.all()
38
+ return Promise.resolve(rows as unknown[])
40
39
  }
41
40
 
42
41
  run(sql: string, params: unknown[] = []): Promise<{ changes: number; lastId?: string }> {
43
- const stmt = this.db.prepare(sql)
44
- const result = params.length > 0 ? stmt.run(...params) : stmt.run()
42
+ const stmt = this.db.query(sql)
43
+ const result = params.length > 0
44
+ ? stmt.run(...(params as Parameters<typeof stmt.run>))
45
+ : stmt.run()
45
46
  return Promise.resolve({
46
47
  changes: result.changes,
47
48
  lastId: result.lastInsertRowid ? String(result.lastInsertRowid) : undefined,
@@ -49,21 +50,15 @@ export class SqliteAdapter implements DbAdapter {
49
50
  }
50
51
 
51
52
  async transaction<T>(fn: (adapter: DbAdapter) => Promise<T>): Promise<T> {
52
- // better-sqlite3 usa transacciones síncronas — necesitamos un wrapper async
53
- return new Promise((resolve, reject) => {
54
- const txFn = this.db.transaction((args: unknown[]) => args[0])
55
- // Ejecutar la función dentro de BEGIN/COMMIT de better-sqlite3
56
- this.db.exec('BEGIN')
57
- fn(this)
58
- .then((result) => {
59
- this.db.exec('COMMIT')
60
- resolve(result)
61
- })
62
- .catch((err) => {
63
- try { this.db.exec('ROLLBACK') } catch { /* ya hizo rollback */ }
64
- reject(err)
65
- })
66
- })
53
+ this.db.exec('BEGIN')
54
+ try {
55
+ const result = await fn(this)
56
+ this.db.exec('COMMIT')
57
+ return result
58
+ } catch (err) {
59
+ try { this.db.exec('ROLLBACK') } catch { /* ya hizo rollback */ }
60
+ throw err
61
+ }
67
62
  }
68
63
 
69
64
  close(): Promise<void> {
package/cli/analyze.ts CHANGED
@@ -42,6 +42,9 @@ export interface Violation {
42
42
  | 'IDOR_RISK' // findById sin assertOwnership posterior
43
43
  // ── Portabilidad ──
44
44
  | 'SERVICE_DEPENDS_ON_ORM' // service inyecta ORM en lugar de RepositoryAdapter
45
+ // ── Estructura de archivos (Regla #22) ──
46
+ | 'MODEL_IN_TYPES_FILE' // ModelDefinition en types.ts en vez de model.ts
47
+ | 'MISSING_MODEL_TS' // módulo sin model.ts
45
48
  // ── Conectores ──
46
49
  | 'DUPLICATE_CONNECTOR' // mismo nombre lógico con distinto casing (foo-bar.ts y fooBar.ts)
47
50
  | 'UNREGISTERED_CONNECTOR' // archivo existe pero no está en composition-root.ts
@@ -427,7 +430,6 @@ export async function analyzeProject(basePath: string): Promise<AnalysisResult>
427
430
  // 9. SERVICE DEPENDS ON ORM — service inyecta ORM en lugar de RepositoryAdapter (Regla #18)
428
431
  try {
429
432
  const serviceContent = await readFile(servicePath, 'utf-8')
430
- // Detectar: private orm: ORM o private readonly orm: ORM en el constructor
431
433
  if (/private\s+(readonly\s+)?\w+\s*:\s*ORM\b/.test(serviceContent)) {
432
434
  violations.push({
433
435
  type: 'SERVICE_DEPENDS_ON_ORM',
@@ -436,6 +438,28 @@ export async function analyzeProject(basePath: string): Promise<AnalysisResult>
436
438
  })
437
439
  }
438
440
  } catch { /* no service */ }
441
+
442
+ // 10. MODEL IN TYPES — ModelDefinition en types.ts en vez de model.ts (Regla #22)
443
+ const typesFilePath = join(modPath, 'types.ts')
444
+ try {
445
+ const typesContent = await readFile(typesFilePath, 'utf-8')
446
+ if (/\bModelDefinition\b/.test(typesContent)) {
447
+ violations.push({
448
+ type: 'MODEL_IN_TYPES_FILE',
449
+ module: module.name,
450
+ message: `"${module.name}/types.ts" contiene ModelDefinition o schema de DB. Moverlo a model.ts — son conceptos distintos. (CLAUDE #22)`,
451
+ })
452
+ }
453
+ } catch { /* no types.ts */ }
454
+
455
+ // 11. MISSING MODEL.TS — módulo sin model.ts (Regla #22)
456
+ if (!await fileExists(join(modPath, 'model.ts'))) {
457
+ violations.push({
458
+ type: 'MISSING_MODEL_TS',
459
+ module: module.name,
460
+ message: `"${module.name}" no tiene model.ts. El schema de DB debe vivir separado de types.ts. (CLAUDE #22)`,
461
+ })
462
+ }
439
463
  }
440
464
 
441
465
  return {
@@ -525,7 +549,7 @@ export function printAnalysis(result: AnalysisResult): void {
525
549
  }
526
550
 
527
551
  for (const v of result.violations) {
528
- if (['MISSING_INDEX', 'MISSING_SERVICE', 'MISSING_TYPES', 'MISSING_SOCKETS', 'MISSING_VALIDATORS', 'MISSING_TESTS', 'MISSING_CONNECTORS', 'EMPTY_MODULE_DESCRIPTION'].includes(v.type)) {
552
+ if (['MISSING_INDEX', 'MISSING_SERVICE', 'MISSING_TYPES', 'MISSING_SOCKETS', 'MISSING_VALIDATORS', 'MISSING_TESTS', 'MISSING_CONNECTORS', 'EMPTY_MODULE_DESCRIPTION', 'MODEL_IN_TYPES_FILE', 'MISSING_MODEL_TS'].includes(v.type)) {
529
553
  byCategory['Estructura']!.push(v)
530
554
  } else if (['DIRECT_MODULE_IMPORT', 'SERVICE_IMPORTS_OTHER_MODULE', 'CONNECTOR_BUSINESS_LOGIC'].includes(v.type)) {
531
555
  byCategory['Acoplamiento']!.push(v)
@@ -616,6 +640,8 @@ export function buildManifest(result: AnalysisResult): Record<string, unknown> {
616
640
  'REGLA 6: Todo POST/PUT/PATCH requiere validateSchema().',
617
641
  'REGLA 7: Controller NO llama al ORM. Llama al service.',
618
642
  'REGLA 18: Service recibe RepositoryAdapter<T>, no ORM directamente.',
643
+ 'REGLA 22: model.ts separado de types.ts. Schema DB ≠ contrato TS.',
644
+ 'REGLA 23: Un DEFAULT por caso. Escalar SOLO cuando la condición lo justifica. Nivel 1: OrmRepository / service.ts / Conector. Nivel 2+ solo si aplica.',
619
645
  ],
620
646
  }
621
647
  }
package/cli/index.ts CHANGED
@@ -168,12 +168,12 @@ async function main() {
168
168
  version: '1.0.0',
169
169
  type: 'module',
170
170
  scripts: {
171
- dev: `bun --watch src/composition-root.ts`,
172
- start: `bun run src/composition-root.ts`,
173
- test: `bun test`,
174
- 'test:watch': `bun test --watch`,
175
- analyze: `bun run node_modules/arckode-framework/cli/index.ts analyze`,
176
- 'analyze:json': `bun run node_modules/arckode-framework/cli/index.ts analyze --json`,
171
+ dev: `bun --watch src/composition-root.ts`,
172
+ start: `bun run src/composition-root.ts`,
173
+ test: `bun test`,
174
+ 'test:watch': `bun test --watch`,
175
+ analyze: `bun run node_modules/arckode-framework/cli/index.ts analyze`,
176
+ 'analyze:ci': `bun run node_modules/arckode-framework/cli/index.ts analyze --ci`,
177
177
  },
178
178
  dependencies: {
179
179
  'arckode-framework': 'latest',
@@ -260,14 +260,18 @@ data/
260
260
  const basePath = process.cwd()
261
261
  const result = await analyzeProject(basePath)
262
262
 
263
- if (args.includes('--json')) {
264
- const manifest = buildManifest(result)
265
- const dest = join(basePath, 'arckode.json')
266
- await writeFile(dest, JSON.stringify(manifest, null, 2))
267
- const status = result.valid ? '✅ válido' : `❌ ${result.violations.length} violaciones`
268
- console.log(`arckode.json generado — ${status}`)
263
+ // arckode.json siempre se regenera — es el snapshot que la IA lee primero
264
+ const manifest = buildManifest(result)
265
+ const dest = join(basePath, 'arckode.json')
266
+ await writeFile(dest, JSON.stringify(manifest, null, 2))
267
+
268
+ if (args.includes('--ci')) {
269
+ // Modo CI: sin output de consola, exit 1 si hay violaciones
270
+ if (!result.valid) process.exit(1)
269
271
  } else {
270
272
  printAnalysis(result)
273
+ const status = result.valid ? '✅ sin violaciones' : `❌ ${result.violations.length} violación(es)`
274
+ console.log(`→ arckode.json actualizado — ${status}\n`)
271
275
  }
272
276
  break
273
277
  }
@@ -62,7 +62,8 @@ src/modules/mi-modulo/
62
62
  | 19 | Transactor para atomicidad multi-tabla — \`transactor.run(async (repos) => ...)\` | manual |
63
63
  | 20 | Conectores limpios: kebab-case, con prefijo \`connect\`, registrados en composition-root | \`arckode analyze\` |
64
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 |
65
+ | 22 | \`model.ts\` separado de \`types.ts\` (schema DB ≠ contrato TS) | \`arckode analyze\` |
66
+ | 23 | Un DEFAULT por caso. Escalar SOLO cuando la condición lo justifica | \`arckode analyze\` |
66
67
 
67
68
  ---
68
69
 
@@ -152,11 +153,22 @@ seeds/ ← datos de prueba
152
153
  migrations/ ← migraciones explícitas de DB
153
154
  \`\`\`
154
155
 
156
+ ## Escalation ladder — un DEFAULT por caso
157
+
158
+ \`\`\`
159
+ Capa de datos: OrmRepository<T> → repository.ts custom → Transactor
160
+ Lógica de negocio: service.ts → usecases/{caso}.ts (nunca para CRUD)
161
+ Entre módulos: Conector → EventBus (2+ módulos reaccionan)
162
+ DB Adapter: SqliteAdapter → MySQL / Postgres (concurrencia alta)
163
+ \`\`\`
164
+
165
+ Escalar SOLO cuando la condición lo justifica. Si no hay condición → nivel 1.
166
+
155
167
  ## Comandos útiles
156
168
  \`\`\`bash
157
169
  bun run dev # desarrollo con hot reload
158
- bun run analyze # verificar arquitectura (consola)
159
- bun run analyze:json # generar/actualizar arckode.json
170
+ bun run analyze # verificar arquitectura + actualiza arckode.json
171
+ bun run analyze:ci # modo CI: exit 1 si hay violaciones
160
172
  bun arckode make:module Nombre # generar módulo completo
161
173
  bun arckode make:connector nombre-x mod1 mod2 # conectar dos módulos (kebab-case)
162
174
  bun test # correr todos los tests
@@ -166,9 +178,10 @@ bun test # correr todos los tests
166
178
 
167
179
  ## Notas para la IA
168
180
 
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.
181
+ - CRUD simple: usá \`OrmRepository<T>\` directo. NO crees \`repository.ts\` ni \`usecases/\`.
182
+ - 3+ tablas en un módulo: definí todos los modelos en \`model.ts\`, registralos en \`registerXxxModels(orm)\`.
183
+ - Atomicidad multi-tabla: inyectá \`Transactor\` al service, no el ORM.
184
+ - Antes de escribir código: revisá \`arckode.json\` (violations) y \`composition-root.ts\` (estado actual).
185
+ - Si el analyzer muestra \`LEGACY_ACTIONS_FOLDER\`, \`DUPLICATE_CONNECTOR\`, \`UNREGISTERED_CONNECTOR\`, \`MODEL_IN_TYPES_FILE\` o \`MISSING_MODEL_TS\`: arreglarlos ANTES de agregar código nuevo.
173
186
  `
174
187
  }
@@ -487,9 +487,7 @@ ${sampleData.replace(`'${p.name} de ejemplo'`, `'${p.name} de ejemplo 2'`).repla
487
487
  },
488
488
  ]
489
489
 
490
- for (const item of items) {
491
- await orm.create('${p.name}', item)
492
- }
490
+ await Promise.all(items.map(item => orm.create('${p.name}', item)))
493
491
 
494
492
  console.log(' ✓ ${p.name} seeded: ' + items.length + ' items')
495
493
  }
@@ -896,6 +896,23 @@ export class ORM {
896
896
  } catch {
897
897
  // Error esperado: la columna ya existe — ignorar
898
898
  }
899
+
900
+ // 3c. Corregir drift de nullability: si el modelo dice nullable pero la columna es NOT NULL, quitarlo
901
+ // Solo PostgreSQL soporta ALTER COLUMN ... DROP NOT NULL de forma confiable
902
+ if (field.nullable === true) {
903
+ try {
904
+ await this.db.run(`ALTER TABLE ${def.table} ALTER COLUMN ${name} DROP NOT NULL`)
905
+ } catch { /* SQLite no soporta esto — ignorar */ }
906
+ }
907
+ }
908
+
909
+ // 3b. Agregar columnas de timestamps/softDelete si el modelo las requiere pero no existen aún
910
+ if (def.timestamps) {
911
+ try { await this.db.run(`ALTER TABLE ${def.table} ADD COLUMN createdAt TEXT`) } catch { /* ya existe */ }
912
+ try { await this.db.run(`ALTER TABLE ${def.table} ADD COLUMN updatedAt TEXT`) } catch { /* ya existe */ }
913
+ }
914
+ if (def.softDelete) {
915
+ try { await this.db.run(`ALTER TABLE ${def.table} ADD COLUMN deletedAt TEXT`) } catch { /* ya existe */ }
899
916
  }
900
917
 
901
918
  // 4. Detectar drift de schema: columnas en la BD que ya no están en el modelo
@@ -1602,7 +1619,7 @@ export class Auth {
1602
1619
 
1603
1620
  createRefreshToken(payload: { id: string; role: string }): string {
1604
1621
  const token = this.jwt.sign(
1605
- { id: payload.id, role: payload.role, type: 'refresh' },
1622
+ { id: payload.id, role: payload.role, type: 'refresh', jti: crypto.randomUUID() },
1606
1623
  this.refreshSecret,
1607
1624
  this.refreshExpiresIn,
1608
1625
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arckode-framework",
3
- "version": "1.1.1",
3
+ "version": "1.2.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",
@@ -58,16 +58,12 @@
58
58
  "jsonwebtoken": "^9.0.0"
59
59
  },
60
60
  "peerDependencies": {
61
- "better-sqlite3": ">=11.0.0",
62
61
  "mysql2": ">=3.0.0",
63
62
  "pg": "^8.0.0",
64
- "redis": ">=4.0.0",
63
+ "redis": "",
65
64
  "nodemailer": ">=6.0.0"
66
65
  },
67
66
  "peerDependenciesMeta": {
68
- "better-sqlite3": {
69
- "optional": true
70
- },
71
67
  "mysql2": {
72
68
  "optional": true
73
69
  },
@@ -82,12 +78,12 @@
82
78
  }
83
79
  },
84
80
  "devDependencies": {
85
- "@types/better-sqlite3": "^7.6.12",
86
81
  "@types/jsonwebtoken": "^9.0.7",
87
82
  "@types/nodemailer": "^6.4.0",
88
83
  "@types/pg": "^8.11.0",
89
84
  "bun-types": "^1.3.14",
90
85
  "pg": "^8.0.0",
86
+ "redis": ">=4.0.0",
91
87
  "typescript": "^5.6.0"
92
88
  },
93
89
  "keywords": [