aiplang 2.11.2 → 2.11.4

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/bin/aiplang.js CHANGED
@@ -5,7 +5,7 @@ const fs = require('fs')
5
5
  const path = require('path')
6
6
  const http = require('http')
7
7
 
8
- const VERSION = '2.11.2'
8
+ const VERSION = '2.11.4'
9
9
  const RUNTIME_DIR = path.join(__dirname, '..', 'runtime')
10
10
  const cmd = process.argv[2]
11
11
  const args = process.argv.slice(3)
@@ -37,6 +37,7 @@ if (!cmd||cmd==='--help'||cmd==='-h') {
37
37
  npx aiplang serve [dir] dev server + hot reload
38
38
  npx aiplang build [dir/file] compile → static HTML
39
39
  npx aiplang validate <app.aip> validate syntax with AI-friendly errors
40
+ npx aiplang types <app.aip> generate TypeScript types (.d.ts)
40
41
  npx aiplang context [app.aip] dump minimal AI context (<500 tokens)
41
42
  npx aiplang new <page> new page template
42
43
  npx aiplang --version
@@ -59,6 +60,12 @@ if (!cmd||cmd==='--help'||cmd==='-h') {
59
60
  {{year}} current year
60
61
 
61
62
  Customization:
63
+ # Bancos de dados suportados:
64
+ # ~db sqlite ./app.db (padrão — sem configuração)
65
+ # ~db pg $DATABASE_URL (PostgreSQL)
66
+ # ~db mysql $MYSQL_URL (MySQL / MariaDB)
67
+ # ~db mongodb $MONGODB_URL (MongoDB)
68
+ # ~db redis $REDIS_URL (Redis — cache/session)
62
69
  ~theme accent=#7c3aed radius=1.5rem font=Syne bg=#000 text=#fff
63
70
  hero{...} animate:fade-up
64
71
  row3{...} class:my-class animate:stagger
@@ -451,6 +458,242 @@ const _KNOWN_TYPES = new Set([
451
458
  'bigint','smallint','tinyint','currency','money','price'
452
459
  ])
453
460
 
461
+
462
+
463
+ function _parseForTypes(src) {
464
+ const app = { models:[], apis:[], auth:null }
465
+ const _ta = {
466
+ integer:'int',boolean:'bool',double:'float',number:'float',string:'text',
467
+ varchar:'text',datetime:'timestamp',email:'email',url:'url',uri:'url',
468
+ phone:'phone',currency:'float',money:'float',price:'float',
469
+ json:'json',jsonb:'json',bigint:'int',smallint:'int',tinyint:'int'
470
+ }
471
+ const norm = t => _ta[(t||'').toLowerCase()] || t || 'text'
472
+
473
+ function parseField(line) {
474
+ const p = line.split(/\s*:\s*/)
475
+ const f = { name:(p[0]||'').trim(), type:norm(p[1]), modifiers:[], enumVals:[], constraints:{}, default:null }
476
+ if (f.type === 'enum') {
477
+ const ev = p.slice(2).find(x => x && !x.startsWith('default=') && !['required','unique','hashed','pk','auto','index'].includes(x.trim()))
478
+ if (ev) f.enumVals = ev.includes('|') ? ev.split('|').map(v=>v.trim()) : ev.split(',').map(v=>v.trim())
479
+ }
480
+ for (let j=2; j<p.length; j++) {
481
+ const x = (p[j]||'').trim()
482
+ if (!x) continue
483
+ if (x.startsWith('default=')) f.default = x.slice(8)
484
+ else if (x.startsWith('min=')) f.constraints.min = Number(x.slice(4))
485
+ else if (x.startsWith('max=')) f.constraints.max = Number(x.slice(4))
486
+ else if (x.startsWith('minLen=')) f.constraints.minLen = Number(x.slice(7))
487
+ else if (x.startsWith('maxLen=')) f.constraints.maxLen = Number(x.slice(7))
488
+ else if (x.startsWith('enum:')) f.enumVals = x.slice(5).includes('|') ? x.slice(5).split('|').map(v=>v.trim()) : x.slice(5).split(',').map(v=>v.trim())
489
+ else if (x && !x.includes('=')) f.modifiers.push(x)
490
+ }
491
+ return f
492
+ }
493
+
494
+ // Juntar todas as linhas e tokenizar por estado
495
+ const lines = src.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'))
496
+ let inModel=false, inAPI=false, curModel=null, depth=0
497
+
498
+ for (let i=0; i<lines.length; i++) {
499
+ const line = lines[i]
500
+
501
+ // Auth
502
+ if (line.startsWith('~auth ')) { app.auth = { type: line.includes('jwt') ? 'jwt' : 'basic' }; continue }
503
+
504
+ // Model
505
+ if (line.startsWith('model ') || (line.startsWith('model') && line.includes('{'))) {
506
+ if (inModel && curModel) app.models.push(curModel)
507
+ curModel = { name: line.replace(/^model\s+/, '').replace(/\s*\{.*$/, '').trim(), fields:[] }
508
+ inModel=true; inAPI=false; depth=0
509
+ if (line.includes('{')) depth++
510
+ continue
511
+ }
512
+
513
+ // API
514
+ if (line.startsWith('api ')) {
515
+ if (inModel && curModel) { app.models.push(curModel); curModel=null; inModel=false }
516
+ const m = line.match(/^api\s+(\w+)\s+(\S+)/)
517
+ if (m) {
518
+ const guards = []
519
+ // Procura guard tanto na linha atual quanto nas próximas linhas do bloco
520
+ const blockLines = []
521
+ let j=i; let bd=0
522
+ while(j<lines.length) {
523
+ const bl = lines[j]
524
+ for(const ch of bl) { if(ch==='{') bd++; else if(ch==='}') bd-- }
525
+ blockLines.push(bl)
526
+ if(bd===0 && j>i) break
527
+ j++
528
+ }
529
+ const blockStr = blockLines.join(' ')
530
+ if (blockStr.includes('~guard auth')) guards.push('auth')
531
+ if (blockStr.includes('~guard admin')) guards.push('admin')
532
+ if (blockStr.includes('=> auth')) guards.push('auth')
533
+ if (blockStr.includes('=> admin')) guards.push('admin')
534
+ app.apis.push({ method:m[1].toUpperCase(), path:m[2], guards })
535
+ }
536
+ inAPI=true; inModel=false
537
+ // Contar profundidade das chaves para saber quando o bloco fecha
538
+ depth=0
539
+ for(const ch of line) { if(ch==='{') depth++; else if(ch==='}') depth-- }
540
+ if(depth<=0) { inAPI=false; depth=0 }
541
+ continue
542
+ }
543
+
544
+ // Dentro de api block: rastrear chaves
545
+ if (inAPI) {
546
+ for(const ch of line) { if(ch==='{') depth++; else if(ch==='}') depth-- }
547
+ if(depth<=0) { inAPI=false; depth=0 }
548
+ continue
549
+ }
550
+
551
+ // Fechar model
552
+ if (line === '}' && inModel) {
553
+ if (curModel) { app.models.push(curModel); curModel=null }
554
+ inModel=false; continue
555
+ }
556
+
557
+ // Campos do model
558
+ if (inModel && curModel && line && line !== '{' && !line.startsWith('~') && line.includes(':')) {
559
+ const f = parseField(line)
560
+ if (f.name) curModel.fields.push(f)
561
+ }
562
+ }
563
+ if (inModel && curModel) app.models.push(curModel)
564
+ return app
565
+ }
566
+
567
+ // ── TypeScript type generator ────────────────────────────────────
568
+ // Maps aiplang types to TypeScript equivalents
569
+ const _AIP_TO_TS = {
570
+ text:'string', string:'string', email:'string', url:'string',
571
+ phone:'string', slug:'string', color:'string', file:'string', image:'string',
572
+ int:'number', integer:'number', float:'number', double:'number',
573
+ number:'number', currency:'number', money:'number', price:'number',
574
+ bool:'boolean', boolean:'boolean',
575
+ uuid:'string', date:'string', timestamp:'string', datetime:'string',
576
+ json:'Record<string,unknown>', jsonb:'Record<string,unknown>',
577
+ }
578
+
579
+ function generateTypes(app, srcFile) {
580
+ const lines = [
581
+ '// ─────────────────────────────────────────────────────────────',
582
+ `// aiplang generated types — ${srcFile || 'app.aip'}`,
583
+ `// Generated: ${new Date().toISOString()}`,
584
+ '// DO NOT EDIT — regenerate with: npx aiplang types <app.aip>',
585
+ '// ─────────────────────────────────────────────────────────────',
586
+ '',
587
+ ]
588
+
589
+ // Model interfaces
590
+ for (const model of (app.models || [])) {
591
+ const name = model.name
592
+ lines.push(`// Model: ${name}`)
593
+
594
+ // Enum types first
595
+ for (const f of (model.fields || [])) {
596
+ if (f.type === 'enum' && f.enumVals && f.enumVals.length) {
597
+ lines.push(`export type ${name}${_cap(f.name)} = ${f.enumVals.map(v => `'${v}'`).join(' | ')}`)
598
+ }
599
+ }
600
+
601
+ // Main interface
602
+ lines.push(`export interface ${name} {`)
603
+ for (const f of (model.fields || [])) {
604
+ const req = f.modifiers && f.modifiers.includes('required')
605
+ const pk = f.modifiers && f.modifiers.includes('pk')
606
+ const auto = f.modifiers && f.modifiers.includes('auto')
607
+ const opt = !req || pk || auto
608
+ let tsType
609
+ if (f.type === 'enum' && f.enumVals && f.enumVals.length) {
610
+ tsType = `${name}${_cap(f.name)}`
611
+ } else {
612
+ tsType = _AIP_TO_TS[f.type] || 'string'
613
+ }
614
+ // Constraint comments
615
+ const constraints = []
616
+ if (f.constraints) {
617
+ if (f.constraints.min != null) constraints.push(`min:${f.constraints.min}`)
618
+ if (f.constraints.max != null) constraints.push(`max:${f.constraints.max}`)
619
+ if (f.constraints.minLen != null) constraints.push(`minLen:${f.constraints.minLen}`)
620
+ if (f.constraints.maxLen != null) constraints.push(`maxLen:${f.constraints.maxLen}`)
621
+ if (f.constraints.format) constraints.push(`format:${f.constraints.format}`)
622
+ }
623
+ const comment = constraints.length ? ` // ${constraints.join(', ')}` : ''
624
+ const mods = []
625
+ if (f.modifiers) {
626
+ if (f.modifiers.includes('unique')) mods.push('@unique')
627
+ if (f.modifiers.includes('hashed')) mods.push('@hashed')
628
+ if (pk) mods.push('@pk')
629
+ if (auto) mods.push('@auto')
630
+ }
631
+ const modStr = mods.length ? ` /** ${mods.join(' ')} */` : ''
632
+ lines.push(` ${f.name}${opt ? '?' : ''}: ${tsType}${modStr}${comment}`)
633
+ }
634
+ lines.push(`}`)
635
+ lines.push(``)
636
+
637
+ // Input type (for POST/PUT — excludes pk/auto, optional for patches)
638
+ const inputFields = (model.fields || []).filter(f =>
639
+ !(f.modifiers && f.modifiers.includes('pk')) &&
640
+ !(f.modifiers && f.modifiers.includes('auto')) &&
641
+ f.name !== 'created_at' && f.name !== 'updated_at'
642
+ )
643
+ if (inputFields.length) {
644
+ lines.push(`export interface ${name}Input {`)
645
+ for (const f of inputFields) {
646
+ const req = f.modifiers && f.modifiers.includes('required')
647
+ let tsType
648
+ if (f.type === 'enum' && f.enumVals && f.enumVals.length) {
649
+ tsType = `${name}${_cap(f.name)}`
650
+ } else {
651
+ tsType = _AIP_TO_TS[f.type] || 'string'
652
+ }
653
+ lines.push(` ${f.name}${req ? '' : '?'}: ${tsType}`)
654
+ }
655
+ lines.push(`}`)
656
+ lines.push(``)
657
+ }
658
+ }
659
+
660
+ // API route types
661
+ if ((app.apis || []).length) {
662
+ lines.push(`// ── API Route types ──────────────────────────────────────────`)
663
+ lines.push(``)
664
+ lines.push(`export interface AiplangRoutes {`)
665
+ for (const api of (app.apis || [])) {
666
+ const method = api.method.toUpperCase()
667
+ const path = api.path.replace(/\//g, '_').replace(/[^a-zA-Z0-9_]/g,'').replace(/^_/,'')
668
+ const guards = (api.guards || []).join(', ')
669
+ const guardComment = guards ? ` /** guards: ${guards} */` : ''
670
+ lines.push(` '${method} ${api.path}': {${guardComment}}`)
671
+ }
672
+ lines.push(`}`)
673
+ lines.push(``)
674
+ }
675
+
676
+ // Convenience type for auth user
677
+ const hasAuth = app.auth && app.auth.type
678
+ if (hasAuth) {
679
+ const userModel = (app.models || []).find(m => m.name === 'User' || m.name === 'user')
680
+ if (userModel) {
681
+ lines.push(`// ── Auth types ───────────────────────────────────────────────`)
682
+ lines.push(`export type AuthUser = Pick<User, 'id' | 'email'${(userModel.fields||[]).some(f=>f.name==='role')?" | 'role'":''}> & { type: 'access' | 'refresh', iat: number, exp: number }`)
683
+ lines.push(`declare global { namespace Express { interface Request { user?: AuthUser } } }`)
684
+ lines.push(``)
685
+ }
686
+ }
687
+
688
+ lines.push(`// ── aiplang version ──────────────────────────────────────────`)
689
+ lines.push(`export const AIPLANG_VERSION = '2.11.4'`)
690
+ lines.push(``)
691
+ return lines.join('\n')
692
+ }
693
+
694
+ function _cap(s) { return s ? s[0].toUpperCase() + s.slice(1) : s }
695
+
696
+
454
697
  function validateAipSrc(source) {
455
698
  const errors = []
456
699
  const lines = source.split('\n')
@@ -490,6 +733,34 @@ function validateAipSrc(source) {
490
733
  return errors
491
734
  }
492
735
 
736
+ if (cmd==='types'||cmd==='type'||cmd==='dts') {
737
+ const file = args[0]
738
+ if (!file) { console.error('\n Usage: aiplang types <app.aip> [--out types.d.ts]\n'); process.exit(1) }
739
+ if (!require('fs').existsSync(file)) { console.error(`\n ✗ Arquivo não encontrado: ${file}\n`); process.exit(1) }
740
+ const src = require('fs').readFileSync(file,'utf8')
741
+ const errs = validateAipSrc(src)
742
+ if (errs.some(e => e.severity === 'error')) {
743
+ console.error('\n ✗ Corrija os erros antes de gerar tipos:\n')
744
+ errs.filter(e=>e.severity==='error').forEach(e=>console.error(` Line ${e.line}: ${e.message}`))
745
+ process.exit(1)
746
+ }
747
+ const serverPath = require('path').join(__dirname,'../server/server.js')
748
+ // Parse models and routes from .aip source for type generation
749
+ // Lightweight inline parser — no DB, no server init required
750
+ const app = _parseForTypes(src)
751
+ const _outIdx = args.indexOf('--out')
752
+ const outFile = (_outIdx >= 0 && args[_outIdx+1]) ? args[_outIdx+1] : file.replace(/\.aip$/,'') + '.d.ts'
753
+ const dts = generateTypes(app, require('path').basename(file))
754
+ require('fs').writeFileSync(outFile, dts)
755
+ console.log(`\n ✅ Tipos gerados: ${outFile}`)
756
+ console.log(` ${(app.models||[]).length} models · ${(app.apis||[]).length} routes\n`)
757
+ // Also show preview
758
+ const preview = dts.split('\n').slice(0,30).join('\n')
759
+ console.log(preview)
760
+ if (dts.split('\n').length > 30) console.log(' ...')
761
+ process.exit(0)
762
+ }
763
+
493
764
  if (cmd==='validate'||cmd==='check'||cmd==='lint') {
494
765
  const file = args[0]
495
766
  if (!file) { console.error('\n Usage: aiplang validate <app.aip>\n'); process.exit(1) }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiplang",
3
- "version": "2.11.2",
3
+ "version": "2.11.4",
4
4
  "description": "AI-first web language. One .aip file = complete app. Frontend + backend + database + auth.",
5
5
  "keywords": [
6
6
  "aiplang",
@@ -43,12 +43,18 @@
43
43
  "@sqlite.org/sqlite-wasm": "^3.51.2-build8",
44
44
  "bcryptjs": "^2.4.3",
45
45
  "better-sqlite3": "^12.8.0",
46
+ "ioredis": "^5.3.2",
46
47
  "jsonwebtoken": "^9.0.2",
48
+ "mongodb": "^6.5.0",
49
+ "mysql2": "^3.9.0",
47
50
  "nodemailer": "^8.0.3",
48
51
  "pg": "^8.11.0",
49
52
  "sql.js": "^1.10.3",
50
53
  "stripe": "^14.0.0",
51
54
  "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.44.0",
52
55
  "ws": "^8.16.0"
56
+ },
57
+ "optionalDependencies": {
58
+ "better-sqlite3": "^9.4.0"
53
59
  }
54
60
  }
package/server/server.js CHANGED
@@ -32,6 +32,12 @@ let SQL, DB_FILE, _db = null
32
32
  let _pgPool = null // PostgreSQL connection pool
33
33
  let _dbDriver = 'sqlite' // 'sqlite' | 'postgres'
34
34
  let _useBetter = false // true when better-sqlite3 is available
35
+ let _mysqlPool = null // MySQL/MariaDB (mysql2)
36
+ let _mongoClient= null // MongoDB client
37
+ let _mongoDB = null // MongoDB database
38
+ let _redisClient= null // Redis (ioredis)
39
+ let _useMongo = false
40
+ let _useRedis = false
35
41
 
36
42
  async function getDB(dbConfig = { driver: 'sqlite', dsn: ':memory:' }) {
37
43
  if (_db || _pgPool) return _db || _pgPool
@@ -39,16 +45,72 @@ async function getDB(dbConfig = { driver: 'sqlite', dsn: ':memory:' }) {
39
45
  const dsn = dbConfig.dsn || ':memory:'
40
46
  _dbDriver = driver
41
47
 
42
- if (driver === 'postgres' || driver === 'postgresql' || dsn.startsWith('postgres')) {
48
+ // ── PostgreSQL ────────────────────────────────────────────────
49
+ if (driver === 'postgres' || dsn.startsWith('postgres')) {
43
50
  try {
44
51
  const { Pool } = require('pg')
45
52
  _pgPool = new Pool({ connectionString: dsn, ssl: dsn.includes('ssl=true') ? { rejectUnauthorized: false } : false })
46
- await _pgPool.query('SELECT 1') // test connection
53
+ await _pgPool.query('SELECT 1')
47
54
  console.log('[aiplang] DB: PostgreSQL ✓')
48
55
  return _pgPool
49
56
  } catch (e) {
50
- console.error('[aiplang] PostgreSQL connection failed:', e.message)
51
- console.log('[aiplang] Falling back to SQLite :memory:')
57
+ console.error('[aiplang] PostgreSQL falhou:', e.message)
58
+ _dbDriver = 'sqlite'
59
+ }
60
+ }
61
+
62
+ // ── MySQL / MariaDB ───────────────────────────────────────────
63
+ if (driver === 'mysql' || dsn.startsWith('mysql') || dsn.startsWith('mariadb')) {
64
+ try {
65
+ const mysql = require('mysql2/promise')
66
+ _mysqlPool = await mysql.createPool({
67
+ uri: dsn.startsWith('mariadb') ? dsn.replace('mariadb://', 'mysql://') : dsn,
68
+ waitForConnections: true,
69
+ connectionLimit: 10,
70
+ queueLimit: 0,
71
+ ssl: dsn.includes('ssl=true') ? { rejectUnauthorized: false } : undefined
72
+ })
73
+ await _mysqlPool.query('SELECT 1')
74
+ _dbDriver = 'mysql'
75
+ console.log('[aiplang] DB: MySQL/MariaDB ✓')
76
+ return _mysqlPool
77
+ } catch (e) {
78
+ console.error('[aiplang] MySQL falhou:', e.message)
79
+ _dbDriver = 'sqlite'
80
+ }
81
+ }
82
+
83
+ // ── MongoDB ───────────────────────────────────────────────────
84
+ if (driver === 'mongodb' || dsn.startsWith('mongodb')) {
85
+ try {
86
+ const { MongoClient } = require('mongodb')
87
+ _mongoClient = new MongoClient(dsn)
88
+ await _mongoClient.connect()
89
+ // Extrair nome do banco da URL
90
+ const dbName = dsn.split('/').pop()?.split('?')[0] || 'aiplang'
91
+ _mongoDB = _mongoClient.db(dbName)
92
+ _useMongo = true
93
+ _dbDriver = 'mongodb'
94
+ console.log('[aiplang] DB: MongoDB ✓ (' + dbName + ')')
95
+ return _mongoDB
96
+ } catch (e) {
97
+ console.error('[aiplang] MongoDB falhou:', e.message)
98
+ _dbDriver = 'sqlite'
99
+ }
100
+ }
101
+
102
+ // ── Redis ─────────────────────────────────────────────────────
103
+ if (driver === 'redis' || dsn.startsWith('redis')) {
104
+ try {
105
+ const Redis = require('ioredis')
106
+ _redisClient = new Redis(dsn, { lazyConnect: false, maxRetriesPerRequest: 3 })
107
+ await _redisClient.ping()
108
+ _useRedis = true
109
+ _dbDriver = 'redis'
110
+ console.log('[aiplang] DB: Redis ✓')
111
+ return _redisClient
112
+ } catch (e) {
113
+ console.error('[aiplang] Redis falhou:', e.message)
52
114
  _dbDriver = 'sqlite'
53
115
  }
54
116
  }
@@ -89,7 +151,48 @@ function persistDB() {
89
151
  }
90
152
  let _dirty = false, _persistTimer = null
91
153
 
154
+ // ── MongoDB helpers ───────────────────────────────────────────────
155
+ function _sqlWhereToMongo(sql, params) {
156
+ // Converte WHERE simples: id = ? / email = ? → {id: val, email: val}
157
+ const filter = {}
158
+ const whereMatch = sql.match(/WHERE\s+(.+?)(?:ORDER|LIMIT|$)/is)
159
+ if (!whereMatch) return filter
160
+ const conditions = whereMatch[1].trim()
161
+ const parts = conditions.split(/\s+AND\s+/i)
162
+ let paramIdx = 0
163
+ for (const part of parts) {
164
+ const m = part.trim().match(/^(\w+)\s*=\s*\?$/)
165
+ if (m && paramIdx < params.length) {
166
+ filter[m[1]] = params[paramIdx++]
167
+ }
168
+ }
169
+ return filter
170
+ }
171
+
172
+ function _mongoToObj(doc) {
173
+ if (!doc) return null
174
+ const { _id, ...rest } = doc
175
+ return { id: _id?.toString() || rest.id, ...rest }
176
+ }
177
+
178
+ // ── MySQL: CREATE TABLE equivalente ──────────────────────────────
179
+ function _mysqlType(sqliteType) {
180
+ return {
181
+ 'TEXT': 'TEXT',
182
+ 'INTEGER': 'INT',
183
+ 'REAL': 'DOUBLE',
184
+ 'BLOB': 'BLOB'
185
+ }[sqliteType] || 'TEXT'
186
+ }
187
+
188
+
92
189
  function dbRun(sql, params = []) {
190
+ if (_mysqlPool) {
191
+ // MySQL: async, mas dbRun é chamado sync em alguns contextos
192
+ // Enfileirar de forma segura
193
+ _mysqlPool.execute(sql, params).catch(e => console.debug('[aiplang:mysql]', e?.message))
194
+ return
195
+ }
93
196
  if (_useBetter && !_pgPool) {
94
197
  // better-sqlite3: synchronous, native — 30x faster than sql.js writes
95
198
  _getStmt(sql).run(...params)
@@ -117,7 +220,8 @@ function convertPlaceholders(sql) {
117
220
  }
118
221
 
119
222
  async function dbRunAsync(sql, params = []) {
120
- if (_pgPool) return _pgPool.query(convertPlaceholders(sql), params)
223
+ if (_mysqlPool) return _mysqlPool.execute(sql, params)
224
+ if (_pgPool) return _pgPool.query(convertPlaceholders(sql), params)
121
225
  dbRun(sql, params)
122
226
  }
123
227
 
@@ -181,18 +285,18 @@ function _getStmt(sql) {
181
285
  }
182
286
 
183
287
  function dbAll(sql, params = [], _cacheKey = null, _cacheTables = null) {
184
- if (_pgPool) return []
288
+ if (_pgPool) return [] // PostgreSQL usa dbAllAsync
289
+ if (_mysqlPool) return [] // MySQL usa dbAllAsync
290
+ if (_useMongo) return [] // MongoDB usa dbAllAsync
185
291
  if (_cacheKey && !params.length) {
186
292
  const cached = _cacheGet(_cacheKey)
187
293
  if (cached !== null) return { __cached: true, __body: cached }
188
294
  }
189
295
  let rows
190
296
  if (_useBetter) {
191
- // better-sqlite3: synchronous, native, 7x faster
192
297
  const stmt = _getStmt(sql)
193
298
  rows = params.length ? stmt.all(...params) : stmt.all()
194
299
  } else {
195
- // sql.js fallback
196
300
  const stmt = _db.prepare(sql)
197
301
  stmt.bind(params); rows = []
198
302
  while (stmt.step()) rows.push(stmt.getAsObject())
@@ -202,12 +306,33 @@ function dbAll(sql, params = [], _cacheKey = null, _cacheTables = null) {
202
306
  return rows
203
307
  }
204
308
 
205
- async function dbAllAsync(sql, params = []) {
309
+ async function dbAllAsync(sql, params = [], _cacheKey = null, _cacheTables = null) {
310
+ // Cache check (universal)
311
+ if (_cacheKey && !params.length) {
312
+ const cached = _cacheGet(_cacheKey)
313
+ if (cached !== null) return { __cached: true, __body: cached }
314
+ }
315
+ let rows
206
316
  if (_pgPool) {
207
317
  const r = await _pgPool.query(convertPlaceholders(sql), params)
208
- return r.rows
318
+ rows = r.rows
319
+ } else if (_mysqlPool) {
320
+ const [result] = await _mysqlPool.execute(sql, params)
321
+ rows = result
322
+ } else if (_useMongo) {
323
+ // MongoDB: extrair collection do SQL "SELECT * FROM table WHERE..."
324
+ const tableMatch = sql.match(/FROM\s+(\w+)/i)
325
+ const collection = tableMatch ? tableMatch[1] : null
326
+ if (!collection) return []
327
+ const filter = _sqlWhereToMongo(sql, params)
328
+ rows = await _mongoDB.collection(collection).find(filter).toArray()
329
+ } else {
330
+ rows = dbAll(sql, params)
331
+ }
332
+ if (Array.isArray(rows) && _cacheKey && !params.length) {
333
+ _cacheSet(_cacheKey, JSON.stringify(rows), _cacheTables)
209
334
  }
210
- return dbAll(sql, params)
335
+ return rows
211
336
  }
212
337
 
213
338
  function dbGet(sql, params = []) { return dbAll(sql, params)[0] || null }
@@ -460,9 +585,15 @@ function validateAip(source) {
460
585
  }
461
586
 
462
587
  function cacheSet(key, value, ttlMs = 60000) {
588
+ // Redis: persistir em redis além do cache em memória
589
+ if (_useRedis) {
590
+ try { _redisClient.set('aip:' + key, JSON.stringify(value), 'PX', ttlMs).catch(() => {}) } catch {}
591
+ }
463
592
  _cache.set(key, { value, expires: Date.now() + ttlMs })
464
593
  }
465
594
  function cacheGet(key) {
595
+ // Redis: verificar redis se não tiver na memória
596
+ // (retorna null para busca assíncrona — o caller deve usar cacheGetAsync se precisar)
466
597
  const item = _cache.get(key)
467
598
  if (!item) return null
468
599
  if (item.expires < Date.now()) { _cache.delete(key); return null }
@@ -621,6 +752,19 @@ class Model {
621
752
 
622
753
  // ── Core queries ────────────────────────────────────────────────
623
754
  all(opts = {}) {
755
+ // MongoDB
756
+ if (_useMongo) {
757
+ const filter = this.softDelete ? { deleted_at: { $exists: false } } : {}
758
+ const mongoOpts = {}
759
+ if (opts.limit) mongoOpts.limit = parseInt(opts.limit)
760
+ if (opts.offset) mongoOpts.skip = parseInt(opts.offset)
761
+ if (opts.order) {
762
+ const p = String(opts.order).trim().split(/\s+/)
763
+ mongoOpts.sort = { [p[0]]: p[1]?.toLowerCase()==='desc' ? -1 : 1 }
764
+ }
765
+ return _mongoDB.collection(this.tableName).find(filter, mongoOpts).toArray()
766
+ .then(docs => docs.map(d => { const { _id, ...rest } = d; return { id: _id?.toString(), ...rest } }))
767
+ }
624
768
  let sql = `SELECT * FROM ${this.tableName}`
625
769
  const params = [], conditions = []
626
770
  if (this.softDelete) conditions.push('deleted_at IS NULL')
@@ -684,6 +828,11 @@ class Model {
684
828
  if (!row.updated_at) row.updated_at = now()
685
829
  }
686
830
  const keys = Object.keys(row), vals = Object.values(row)
831
+ // MongoDB: usar insertOne
832
+ if (_useMongo) {
833
+ _mongoDB.collection(this.tableName).insertOne({ ...row }).catch(e => console.debug('[aiplang:mongo]', e?.message))
834
+ return row
835
+ }
687
836
  dbRun(`INSERT INTO ${this.tableName} (${keys.join(',')}) VALUES (${keys.map(()=>'?').join(',')})`, vals)
688
837
  emit(`${this.modelName}.created`, row)
689
838
  return row
@@ -767,13 +916,39 @@ class Model {
767
916
  // MIGRATION
768
917
  // ═══════════════════════════════════════════════════════════════════
769
918
  function migrateModels(models) {
919
+ // ── Seleção de mapa de tipos por banco ───────────────────────────
920
+ const _sqliteTypes = { uuid:'TEXT',int:'INTEGER',integer:'INTEGER',float:'REAL',bool:'INTEGER',timestamp:'TEXT',date:'TEXT',json:'TEXT',enum:'TEXT',text:'TEXT',email:'TEXT',url:'TEXT',phone:'TEXT' }
921
+ const _mysqlTypes = { uuid:'VARCHAR(36)',int:'INT',integer:'INT',float:'DOUBLE',bool:'TINYINT(1)',timestamp:'DATETIME',date:'DATE',json:'JSON',enum:'TEXT',text:'TEXT',email:'VARCHAR(255)',url:'TEXT',phone:'VARCHAR(20)' }
922
+ const _pgTypes = { uuid:'UUID',int:'INTEGER',integer:'INTEGER',float:'DOUBLE PRECISION',bool:'BOOLEAN',timestamp:'TIMESTAMPTZ',date:'DATE',json:'JSONB',enum:'TEXT',text:'TEXT',email:'TEXT',url:'TEXT',phone:'TEXT' }
923
+ const typeMap = _mysqlPool ? _mysqlTypes : (_pgPool ? _pgTypes : _sqliteTypes)
924
+
770
925
  for (const model of models) {
771
926
  const table = toTable(model.name)
927
+
928
+ // ── MongoDB: criar collection com índices ──────────────────────
929
+ if (_useMongo) {
930
+ _mongoDB.collection(table) // cria collection implicitamente
931
+ for (const f of model.fields) {
932
+ const colName = toCol(f.name)
933
+ if (f.modifiers.includes('unique')) {
934
+ _mongoDB.collection(table).createIndex({ [colName]: 1 }, { unique: true }).catch(() => {})
935
+ }
936
+ }
937
+ console.log(`[aiplang] ✓ ${table} (MongoDB)`)
938
+ continue
939
+ }
940
+
772
941
  const cols = []
773
942
  for (const f of model.fields) {
774
- let sqlType = { uuid:'TEXT',int:'INTEGER',integer:'INTEGER',float:'REAL',bool:'INTEGER',timestamp:'TEXT',date:'TEXT',json:'TEXT',enum:'TEXT',text:'TEXT',email:'TEXT',url:'TEXT',phone:'TEXT' }[f.type] || 'TEXT'
943
+ let sqlType = typeMap[f.type] || 'TEXT'
775
944
  let def = `${toCol(f.name)} ${sqlType}`
776
- if (f.modifiers.includes('pk')) def += ' PRIMARY KEY'
945
+ if (f.modifiers.includes('pk')) {
946
+ if (_mysqlPool) def += ' PRIMARY KEY'
947
+ else def += ' PRIMARY KEY'
948
+ }
949
+ if (f.modifiers.includes('auto') && f.type !== 'uuid') {
950
+ if (_mysqlPool) def += ' AUTO_INCREMENT'
951
+ }
777
952
  if (f.modifiers.includes('required')) def += ' NOT NULL'
778
953
  if (f.modifiers.includes('unique')) def += ' UNIQUE'
779
954
  if (f.default !== null) def += ` DEFAULT '${f.default}'`
@@ -905,7 +1080,18 @@ function parseApp(src) {
905
1080
  }
906
1081
 
907
1082
  function parseEnvLine(s) { const p=s.split(/\s+/); const ev={name:'',required:false,default:null}; for(const x of p){if(x==='required')ev.required=true;else if(x.includes('=')){const[k,v]=x.split('=');ev.name=k;ev.default=v}else ev.name=x}; return ev }
908
- function parseDBLine(s) { const p=s.split(/\s+/); const d=p[0]||'sqlite'; return{driver:d==='pg'||d==='psql'?'postgres':d,dsn:p[1]||'./app.db'} }
1083
+ function parseDBLine(s) {
1084
+ const p = s.split(/\s+/)
1085
+ let d = (p[0]||'sqlite').toLowerCase()
1086
+ // Normalizar aliases
1087
+ if (d==='pg'||d==='psql'||d==='postgresql') d='postgres'
1088
+ if (d==='mariadb') d='mysql'
1089
+ if (d==='mongo') d='mongodb'
1090
+ if (d==='redis'||d==='cache') d='redis'
1091
+ if (d==='sqlite3') d='sqlite'
1092
+ const dsn = p[1] || (d==='sqlite'?'./app.db':d==='redis'?'redis://localhost:6379':'')
1093
+ return { driver:d, dsn }
1094
+ }
909
1095
  function parseAuthLine(s) { const p=s.split(/\s+/); const a={provider:'jwt',secret:p[1]||'$JWT_SECRET',expire:'7d',refresh:'30d'}; for(const x of p){if(x.startsWith('expire='))a.expire=x.slice(7);if(x.startsWith('refresh='))a.refresh=x.slice(8);if(x==='google')a.oauth=['google'];if(x==='github')a.oauth=[...(a.oauth||[]),'google']}; return a }
910
1096
  function parseMailLine(s) { const parts=s.split(/\s+/); const m={driver:parts[0]||'smtp'}; for(const x of parts.slice(1)){const[k,v]=x.split('='); m[k]=v}; return m }
911
1097
  function parseStripeLine(s) {
@@ -2246,7 +2432,7 @@ async function startServer(aipFile, port = 3000) {
2246
2432
  })
2247
2433
 
2248
2434
  srv.addRoute('GET', '/health', (req, res) => res.json(200, {
2249
- status:'ok', version:'2.11.2',
2435
+ status:'ok', version:'2.11.4',
2250
2436
  models: app.models.map(m=>m.name),
2251
2437
  routes: app.apis.length, pages: app.pages.length,
2252
2438
  admin: app.admin?.prefix || null,