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.
- package/adapters/sqlite.ts +22 -27
- package/kernel/framework.ts +75 -7
- package/package.json +1 -6
package/adapters/sqlite.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// adapters/sqlite.ts — Adapter SQLite
|
|
2
|
-
// Responsabilidad ÚNICA: traducir llamadas de DbAdapter a
|
|
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 '
|
|
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
|
|
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.
|
|
33
|
-
if (this.config.foreignKeys) this.db.
|
|
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.
|
|
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.
|
|
44
|
-
const result = params.length > 0
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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/kernel/framework.ts
CHANGED
|
@@ -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
|
-
|
|
1318
|
-
|
|
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(
|
|
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(
|
|
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
|
-
//
|
|
1972
|
-
//
|
|
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.
|
|
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",
|