arckode-framework 1.0.4 → 1.0.6
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/kernel/framework.ts +53 -16
- package/package.json +2 -1
package/kernel/framework.ts
CHANGED
|
@@ -386,8 +386,9 @@ export interface DbAdapter {
|
|
|
386
386
|
|
|
387
387
|
// ── Definición de campo ──
|
|
388
388
|
export interface FieldDefinition {
|
|
389
|
-
type: 'string' | 'number' | 'boolean' | 'json' | 'date'
|
|
389
|
+
type: 'string' | 'text' | 'number' | 'boolean' | 'json' | 'date'
|
|
390
390
|
required?: boolean
|
|
391
|
+
nullable?: boolean // alias semántico de !required — campo puede ser NULL
|
|
391
392
|
default?: unknown
|
|
392
393
|
unique?: boolean
|
|
393
394
|
indexed?: boolean
|
|
@@ -503,6 +504,28 @@ export class ORM {
|
|
|
503
504
|
|
|
504
505
|
// ── Helpers privados ──
|
|
505
506
|
|
|
507
|
+
private serializeForDb(def: ModelDefinition, record: Record<string, unknown>): Record<string, unknown> {
|
|
508
|
+
const out: Record<string, unknown> = {}
|
|
509
|
+
for (const [k, v] of Object.entries(record)) {
|
|
510
|
+
if (def.fields[k]?.type === 'json' && v !== null && v !== undefined && typeof v !== 'string') {
|
|
511
|
+
out[k] = JSON.stringify(v)
|
|
512
|
+
} else {
|
|
513
|
+
out[k] = v
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return out
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
private deserializeFromDb(def: ModelDefinition, row: ModelResult): ModelResult {
|
|
520
|
+
const result = { ...row } as Record<string, unknown>
|
|
521
|
+
for (const [k, field] of Object.entries(def.fields)) {
|
|
522
|
+
if (field.type === 'json' && typeof result[k] === 'string') {
|
|
523
|
+
try { result[k] = JSON.parse(result[k] as string) } catch { /* no es JSON válido */ }
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return result as ModelResult
|
|
527
|
+
}
|
|
528
|
+
|
|
506
529
|
private getAllowedFields(def: ModelDefinition): Set<string> {
|
|
507
530
|
const allowed = new Set(['id', ...Object.keys(def.fields)])
|
|
508
531
|
if (def.timestamps) { allowed.add('createdAt'); allowed.add('updatedAt') }
|
|
@@ -566,7 +589,8 @@ export class ORM {
|
|
|
566
589
|
}
|
|
567
590
|
}
|
|
568
591
|
|
|
569
|
-
|
|
592
|
+
const rows = (await this.db.query(sql, params)) as ModelResult[]
|
|
593
|
+
return rows.map(r => this.deserializeFromDb(def, r))
|
|
570
594
|
}
|
|
571
595
|
|
|
572
596
|
async findById(name: string, id: string, select?: string[]): Promise<ModelResult | null> {
|
|
@@ -575,7 +599,8 @@ export class ORM {
|
|
|
575
599
|
let sql = `SELECT ${selectClause} FROM ${def.table} WHERE id = ?`
|
|
576
600
|
if (def.softDelete) sql += ' AND deletedAt IS NULL'
|
|
577
601
|
const rows = await this.db.query(sql, [id])
|
|
578
|
-
|
|
602
|
+
const row = (rows as ModelResult[])[0] ?? null
|
|
603
|
+
return row ? this.deserializeFromDb(def, row) : null
|
|
579
604
|
}
|
|
580
605
|
|
|
581
606
|
async findOne(name: string, filters: Record<string, unknown>): Promise<ModelResult | null> {
|
|
@@ -621,8 +646,9 @@ export class ORM {
|
|
|
621
646
|
record.updatedAt = now
|
|
622
647
|
}
|
|
623
648
|
|
|
624
|
-
const
|
|
625
|
-
const
|
|
649
|
+
const dbRecord = this.serializeForDb(def, record)
|
|
650
|
+
const keys = Object.keys(dbRecord)
|
|
651
|
+
const values = Object.values(dbRecord)
|
|
626
652
|
const placeholders = keys.map(() => '?').join(', ')
|
|
627
653
|
|
|
628
654
|
await this.db.run(
|
|
@@ -640,8 +666,9 @@ export class ORM {
|
|
|
640
666
|
const record = { ...data }
|
|
641
667
|
if (def.timestamps) record.updatedAt = now
|
|
642
668
|
|
|
643
|
-
const
|
|
644
|
-
const
|
|
669
|
+
const dbRecord = this.serializeForDb(def, record)
|
|
670
|
+
const keys = Object.keys(dbRecord)
|
|
671
|
+
const values = Object.values(dbRecord)
|
|
645
672
|
const setClause = keys.map(k => `${k} = ?`).join(', ')
|
|
646
673
|
|
|
647
674
|
await this.db.run(`UPDATE ${def.table} SET ${setClause} WHERE id = ?`, [...values, id])
|
|
@@ -682,10 +709,11 @@ export class ORM {
|
|
|
682
709
|
return record
|
|
683
710
|
})
|
|
684
711
|
|
|
685
|
-
const
|
|
712
|
+
const dbPrepared = prepared.map(r => this.serializeForDb(def, r))
|
|
713
|
+
const keys = Object.keys(dbPrepared[0] ?? {})
|
|
686
714
|
const rowPlaceholders = `(${keys.map(() => '?').join(', ')})`
|
|
687
|
-
const allPlaceholders =
|
|
688
|
-
const allValues =
|
|
715
|
+
const allPlaceholders = dbPrepared.map(() => rowPlaceholders).join(', ')
|
|
716
|
+
const allValues = dbPrepared.flatMap(r => Object.values(r))
|
|
689
717
|
|
|
690
718
|
await this.db.run(
|
|
691
719
|
`INSERT INTO ${def.table} (${keys.join(', ')}) VALUES ${allPlaceholders}`,
|
|
@@ -711,10 +739,11 @@ export class ORM {
|
|
|
711
739
|
const data = { ...changes }
|
|
712
740
|
if (def.timestamps) data.updatedAt = now
|
|
713
741
|
|
|
714
|
-
const
|
|
742
|
+
const dbData = this.serializeForDb(def, data)
|
|
743
|
+
const setClause = Object.keys(dbData).map(k => `${k} = ?`).join(', ')
|
|
715
744
|
const result = await this.db.run(
|
|
716
745
|
`UPDATE ${def.table} SET ${setClause}${clause}`,
|
|
717
|
-
[...Object.values(
|
|
746
|
+
[...Object.values(dbData), ...whereParams],
|
|
718
747
|
)
|
|
719
748
|
return result.changes
|
|
720
749
|
}
|
|
@@ -769,6 +798,7 @@ export class ORM {
|
|
|
769
798
|
}
|
|
770
799
|
|
|
771
800
|
// 1. Crear tabla si no existe
|
|
801
|
+
const hasExplicitId = Object.keys(def.fields).includes('id')
|
|
772
802
|
const columns = Object.entries(def.fields).map(([name, field]) => {
|
|
773
803
|
const sqlType = this.fieldTypeToSQL(field.type)
|
|
774
804
|
const parts = [name, sqlType]
|
|
@@ -783,6 +813,11 @@ export class ORM {
|
|
|
783
813
|
return parts.join(' ')
|
|
784
814
|
})
|
|
785
815
|
|
|
816
|
+
// Auto-añadir id TEXT PRIMARY KEY si el modelo no lo declara explícitamente
|
|
817
|
+
if (!hasExplicitId) {
|
|
818
|
+
columns.unshift('id TEXT PRIMARY KEY')
|
|
819
|
+
}
|
|
820
|
+
|
|
786
821
|
if (def.timestamps) {
|
|
787
822
|
columns.push('createdAt TEXT')
|
|
788
823
|
columns.push('updatedAt TEXT')
|
|
@@ -818,11 +853,12 @@ export class ORM {
|
|
|
818
853
|
}
|
|
819
854
|
|
|
820
855
|
// 4. Detectar drift de schema: columnas en la BD que ya no están en el modelo
|
|
856
|
+
// Normalizar a lowercase para compatibilidad con PostgreSQL (retorna nombres en minúscula)
|
|
821
857
|
const definedCols = new Set([
|
|
822
858
|
'id',
|
|
823
|
-
...Object.keys(def.fields),
|
|
824
|
-
...(def.timestamps ? ['
|
|
825
|
-
...(def.softDelete ? ['
|
|
859
|
+
...Object.keys(def.fields).map(k => k.toLowerCase()),
|
|
860
|
+
...(def.timestamps ? ['createdat', 'updatedat'] : []),
|
|
861
|
+
...(def.softDelete ? ['deletedat'] : []),
|
|
826
862
|
])
|
|
827
863
|
|
|
828
864
|
const dbCols = await this.getTableColumns(def.table)
|
|
@@ -860,7 +896,7 @@ export class ORM {
|
|
|
860
896
|
// Postgres / estándar SQL
|
|
861
897
|
try {
|
|
862
898
|
const rows = await this.db.query(
|
|
863
|
-
`SELECT column_name FROM information_schema.columns WHERE table_name = '${table}'`
|
|
899
|
+
`SELECT column_name FROM information_schema.columns WHERE table_name = '${table}' AND table_schema = 'public'`
|
|
864
900
|
)
|
|
865
901
|
if (Array.isArray(rows) && rows.length > 0) {
|
|
866
902
|
return (rows as { column_name: string }[]).map(r => r.column_name)
|
|
@@ -873,6 +909,7 @@ export class ORM {
|
|
|
873
909
|
private fieldTypeToSQL(type: FieldDefinition['type']): string {
|
|
874
910
|
const map: Record<FieldDefinition['type'], string> = {
|
|
875
911
|
string: 'TEXT',
|
|
912
|
+
text: 'TEXT', // alias de string para TEXT largo
|
|
876
913
|
number: 'REAL',
|
|
877
914
|
boolean: 'BOOLEAN', // INTEGER falla en Postgres con DEFAULT false/true
|
|
878
915
|
json: 'TEXT',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arckode-framework",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
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,6 +55,7 @@
|
|
|
55
55
|
"analyze:framework": "bun run cli/index.ts analyze"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
+
"arckode-framework": "^1.0.3",
|
|
58
59
|
"jsonwebtoken": "^9.0.0"
|
|
59
60
|
},
|
|
60
61
|
"peerDependencies": {
|