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 +272 -1
- package/package.json +7 -1
- package/server/server.js +201 -15
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.
|
|
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.
|
|
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
|
-
|
|
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')
|
|
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
|
|
51
|
-
|
|
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 (
|
|
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)
|
|
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
|
-
|
|
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
|
|
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 =
|
|
943
|
+
let sqlType = typeMap[f.type] || 'TEXT'
|
|
775
944
|
let def = `${toCol(f.name)} ${sqlType}`
|
|
776
|
-
if (f.modifiers.includes('pk'))
|
|
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) {
|
|
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.
|
|
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,
|