arckode-framework 1.1.0 → 1.1.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.
@@ -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> {
@@ -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
@@ -1252,6 +1269,55 @@ export interface ServerAdapter {
1252
1269
  stop(): Promise<void>
1253
1270
  }
1254
1271
 
1272
+ // ── Envelope estándar de respuesta API ──────────────────────────
1273
+ // Todas las respuestas siguen este contrato. Un solo lugar para cambiar el formato.
1274
+ // Inspirado en JSend + convenciones de GitHub/Laravel:
1275
+ // success → { success: true, data: T, meta: { pagination? } | null, error: null }
1276
+ // error → { success: false, data: null, meta: null, error: { code, message, details } }
1277
+ export interface ApiResponse<T = unknown> {
1278
+ success: boolean
1279
+ data: T | null
1280
+ meta: { pagination?: unknown } | null
1281
+ error: { code: string; message: string; details?: unknown } | null
1282
+ }
1283
+
1284
+ function buildEnvelope(status: number, body: unknown): string {
1285
+ // Errores 4xx / 5xx
1286
+ if (status >= 400) {
1287
+ const b = (body ?? {}) as Record<string, unknown>
1288
+ return JSON.stringify({
1289
+ success: false,
1290
+ data: null,
1291
+ meta: null,
1292
+ error: {
1293
+ code: b.code ?? 'ERROR',
1294
+ message: b.error ?? 'Error',
1295
+ details: b.details ?? null,
1296
+ },
1297
+ } satisfies ApiResponse)
1298
+ }
1299
+
1300
+ // Sin contenido (DELETE 204)
1301
+ if (body === null || body === undefined) {
1302
+ return JSON.stringify({ success: true, data: null, meta: null, error: null } satisfies ApiResponse)
1303
+ }
1304
+
1305
+ const b = body as Record<string, unknown>
1306
+
1307
+ // Lista paginada — el service retorna { data: [], pagination: {} }
1308
+ if (Array.isArray(b.data) && b.pagination !== undefined) {
1309
+ return JSON.stringify({
1310
+ success: true,
1311
+ data: b.data,
1312
+ meta: { pagination: b.pagination },
1313
+ error: null,
1314
+ } satisfies ApiResponse)
1315
+ }
1316
+
1317
+ // Recurso único o cualquier otro payload exitoso
1318
+ return JSON.stringify({ success: true, data: body, meta: null, error: null } satisfies ApiResponse)
1319
+ }
1320
+
1255
1321
  export class NodeServer implements ServerAdapter {
1256
1322
  private server?: ReturnType<typeof createNodeServer>
1257
1323
  private maxBodyBytes: number
@@ -1314,8 +1380,10 @@ export class NodeServer implements ServerAdapter {
1314
1380
 
1315
1381
  // ── Respuesta normal (JSON o binario comprimido) ──
1316
1382
  const isBuffer = Buffer.isBuffer(res.body)
1317
- const responseBody = isBuffer ? res.body : JSON.stringify(res.body)
1318
- nodeRes.writeHead(res.status, {
1383
+ // 204 prohíbe body por spec HTTP — con envelope usamos 200 para "no content"
1384
+ const effectiveStatus = res.status === 204 ? 200 : res.status
1385
+ const responseBody = isBuffer ? res.body : buildEnvelope(effectiveStatus, res.body)
1386
+ nodeRes.writeHead(effectiveStatus, {
1319
1387
  'Content-Type': 'application/json',
1320
1388
  'X-Request-Id': req.id,
1321
1389
  ...res.headers,
@@ -1325,13 +1393,13 @@ export class NodeServer implements ServerAdapter {
1325
1393
  const httpStatus = (error as any)?.httpStatus
1326
1394
  if (httpStatus) {
1327
1395
  nodeRes.writeHead(httpStatus, { 'Content-Type': 'application/json' })
1328
- nodeRes.end(JSON.stringify({ error: (error as Error).message, code: 'REQUEST_ERROR' }))
1396
+ nodeRes.end(buildEnvelope(httpStatus, { error: (error as Error).message, code: 'REQUEST_ERROR' }))
1329
1397
  return
1330
1398
  }
1331
1399
  const stack = error instanceof Error ? error.stack : String(error)
1332
1400
  this.logger.error('Error no manejado en HTTP', { error: String(error), stack })
1333
1401
  nodeRes.writeHead(500, { 'Content-Type': 'application/json' })
1334
- nodeRes.end(JSON.stringify({ error: 'Error interno', code: 'INTERNAL_ERROR' }))
1402
+ nodeRes.end(buildEnvelope(500, { error: 'Error interno', code: 'INTERNAL_ERROR' }))
1335
1403
  } finally {
1336
1404
  this.activeRequests--
1337
1405
  }
@@ -1551,7 +1619,7 @@ export class Auth {
1551
1619
 
1552
1620
  createRefreshToken(payload: { id: string; role: string }): string {
1553
1621
  const token = this.jwt.sign(
1554
- { id: payload.id, role: payload.role, type: 'refresh' },
1622
+ { id: payload.id, role: payload.role, type: 'refresh', jti: crypto.randomUUID() },
1555
1623
  this.refreshSecret,
1556
1624
  this.refreshExpiresIn,
1557
1625
  )
@@ -1968,6 +2036,6 @@ export default {
1968
2036
  // Seeds
1969
2037
  SeedRunner,
1970
2038
 
1971
- // Types (re-exported for convenience)
1972
- // All interfaces are exported above
2039
+ // Response envelope
2040
+ // ApiResponse interface is exported as named export above
1973
2041
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arckode-framework",
3
- "version": "1.1.0",
3
+ "version": "1.1.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",
@@ -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
63
  "redis": ">=4.0.0",
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,7 +78,6 @@
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",