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.
Files changed (2) hide show
  1. package/kernel/framework.ts +53 -16
  2. package/package.json +2 -1
@@ -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
- return (await this.db.query(sql, params)) as ModelResult[]
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
- return (rows as ModelResult[])[0] ?? null
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 keys = Object.keys(record)
625
- const values = Object.values(record)
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 keys = Object.keys(record)
644
- const values = Object.values(record)
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 keys = Object.keys(prepared[0] ?? {})
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 = prepared.map(() => rowPlaceholders).join(', ')
688
- const allValues = prepared.flatMap(r => Object.values(r))
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 setClause = Object.keys(data).map(k => `${k} = ?`).join(', ')
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(data), ...whereParams],
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 ? ['createdAt', 'updatedAt'] : []),
825
- ...(def.softDelete ? ['deletedAt'] : []),
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.4",
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": {