aiplang 2.11.3 → 2.11.5
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 +198 -3
- 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.5'
|
|
9
9
|
const RUNTIME_DIR = path.join(__dirname, '..', 'runtime')
|
|
10
10
|
const cmd = process.argv[2]
|
|
11
11
|
const args = process.argv.slice(3)
|
|
@@ -60,6 +60,12 @@ if (!cmd||cmd==='--help'||cmd==='-h') {
|
|
|
60
60
|
{{year}} current year
|
|
61
61
|
|
|
62
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)
|
|
63
69
|
~theme accent=#7c3aed radius=1.5rem font=Syne bg=#000 text=#fff
|
|
64
70
|
hero{...} animate:fade-up
|
|
65
71
|
row3{...} class:my-class animate:stagger
|
|
@@ -680,7 +686,7 @@ function generateTypes(app, srcFile) {
|
|
|
680
686
|
}
|
|
681
687
|
|
|
682
688
|
lines.push(`// ── aiplang version ──────────────────────────────────────────`)
|
|
683
|
-
lines.push(`export const AIPLANG_VERSION = '2.11.
|
|
689
|
+
lines.push(`export const AIPLANG_VERSION = '2.11.5'`)
|
|
684
690
|
lines.push(``)
|
|
685
691
|
return lines.join('\n')
|
|
686
692
|
}
|
|
@@ -1034,6 +1040,10 @@ function parseBlock(line) {
|
|
|
1034
1040
|
return{kind:'pricing',plans,extraClass,animate,variant,style,bg}
|
|
1035
1041
|
}
|
|
1036
1042
|
|
|
1043
|
+
if(line.startsWith('code{')) { const m=line.match(/^code\{([^}]*)\}/);if(m){const pts=m[1].split('|');const vm=line.match(/variant:(\S+)/);const am=line.match(/animate:(\S+)/);return{kind:'code',lang:pts[0]?.trim()||'aip',lines:pts.slice(1).map(l=>l.trim()),variant:vm?.[1],animate:am?.[1]}} }
|
|
1044
|
+
if(line.startsWith('benchmark{')) { const m=line.match(/^benchmark\{([^}]*)\}/);if(m){const vm=line.match(/variant:(\S+)/);const am=line.match(/animate:(\S+)/);return{kind:'benchmark',items:m[1].split('|').map(it=>{const p=it.trim().split(':');return{num:p[0]?.trim(),label:p[1]?.trim(),vs:p[2]?.trim(),pct:parseInt(p[3])||0}}),variant:vm?.[1],animate:am?.[1]}} }
|
|
1045
|
+
if(line.startsWith('install{')) { const m=line.match(/^install\{([^}]*)\}/);if(m){const vm=line.match(/variant:(\S+)/);return{kind:'install',cmds:m[1].split('|').map(c=>c.trim()).filter(Boolean),variant:vm?.[1]}} }
|
|
1046
|
+
if(line.startsWith('feature{')) { const b=parseBlock(line.replace(/^feature/,'row3'));if(b){b.variant='feature'};return b }
|
|
1037
1047
|
if(line.startsWith('faq{')) {
|
|
1038
1048
|
const body=line.slice(4,line.lastIndexOf('}')).trim()
|
|
1039
1049
|
const items=body.split('|').map(i=>{const idx=i.indexOf('>');return{q:i.slice(0,idx).trim(),a:i.slice(idx+1).trim()}}).filter(i=>i.q&&i.a)
|
|
@@ -1262,6 +1272,10 @@ function renderBlock(b, page) {
|
|
|
1262
1272
|
case 'select': return rSelectBlock(b)
|
|
1263
1273
|
case 'pricing': return rPricing(b)
|
|
1264
1274
|
case 'faq': return rFaq(b)
|
|
1275
|
+
case 'code': return rCode(b)
|
|
1276
|
+
case 'benchmark': return rBenchmark(b)
|
|
1277
|
+
case 'install': return rInstall(b)
|
|
1278
|
+
case 'feature': return rRow(b)
|
|
1265
1279
|
case 'testimonial': return rTestimonial(b)
|
|
1266
1280
|
case 'gallery': return rGallery(b)
|
|
1267
1281
|
case 'raw': return (b.html||'')+'\n'
|
|
@@ -1341,6 +1355,49 @@ function rNav(b) {
|
|
|
1341
1355
|
return `<nav class="fx-nav">${brand}<button class="fx-hamburger" onclick="this.classList.toggle('open');document.querySelector('.fx-nav-links').classList.toggle('open')" aria-label="Menu"><span></span><span></span><span></span></button><div class="fx-nav-links">${links}</div></nav>\n`
|
|
1342
1356
|
}
|
|
1343
1357
|
|
|
1358
|
+
|
|
1359
|
+
|
|
1360
|
+
// ── Parser: code{lang|linha1|linha2} ─────────────────────────────
|
|
1361
|
+
function parseCode(line) {
|
|
1362
|
+
const m = line.match(/^code\{([^}]*)\}/)
|
|
1363
|
+
if (!m) return null
|
|
1364
|
+
const parts = m[1].split('|')
|
|
1365
|
+
const lang = parts[0]?.trim() || 'aip'
|
|
1366
|
+
const lines = parts.slice(1).map(l => l.trim())
|
|
1367
|
+
const b = parseBlockMeta(line)
|
|
1368
|
+
return { ...b, kind:'code', lang, lines }
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// ── Parser: benchmark{Num:Label:vs texto|...} ─────────────────────
|
|
1372
|
+
function parseBenchmark(line) {
|
|
1373
|
+
const m = line.match(/^benchmark\{([^}]*)\}/)
|
|
1374
|
+
if (!m) return null
|
|
1375
|
+
const items = m[1].split('|').map(item => {
|
|
1376
|
+
const parts = item.trim().split(':')
|
|
1377
|
+
return { num: parts[0]?.trim(), label: parts[1]?.trim(), vs: parts[2]?.trim(), pct: parts[3]?.trim() }
|
|
1378
|
+
})
|
|
1379
|
+
const b = parseBlockMeta(line)
|
|
1380
|
+
return { ...b, kind:'benchmark', items }
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// ── Parser: install{cmd1|cmd2|...} ───────────────────────────────
|
|
1384
|
+
function parseInstall(line) {
|
|
1385
|
+
const m = line.match(/^install\{([^}]*)\}/)
|
|
1386
|
+
if (!m) return null
|
|
1387
|
+
const cmds = m[1].split('|').map(c => c.trim()).filter(Boolean)
|
|
1388
|
+
const b = parseBlockMeta(line)
|
|
1389
|
+
return { ...b, kind:'install', cmds }
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// ── Parser: feature{emoji>Título>Desc | ...} (alias row3 otimizado)
|
|
1393
|
+
function parseFeature(line) {
|
|
1394
|
+
// Converte feature{} em row{} com cols=3 e variant=feature
|
|
1395
|
+
const inner = line.replace(/^feature/, 'row3')
|
|
1396
|
+
const b = parseRow(inner)
|
|
1397
|
+
if (b) b.variant = 'feature'
|
|
1398
|
+
return b
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1344
1401
|
function rHero(b) {
|
|
1345
1402
|
let h1='',sub='',img='',ctas=''
|
|
1346
1403
|
for(const item of (b.items||[])) for(const f of item){
|
|
@@ -1352,6 +1409,10 @@ function rHero(b) {
|
|
|
1352
1409
|
const v = b.variant || (img ? 'split' : 'centered')
|
|
1353
1410
|
const bgStyle = b.bg ? ` style="background:${b.bg}"` : b.style ? ` style="${b.style.replace(/,/g,';')}"` : ''
|
|
1354
1411
|
const inlineStyle = b.style && !b.bg ? ` style="${b.style.replace(/,/g,';')}"` : ''
|
|
1412
|
+
if (v === 'landing') {
|
|
1413
|
+
return `<section class="fx-hero fx-hero-landing"${bgStyle}><div class="fx-hero-inner">${h1}${sub}${ctas}</div></section>
|
|
1414
|
+
`
|
|
1415
|
+
}
|
|
1355
1416
|
if (v === 'minimal') {
|
|
1356
1417
|
return `<section class="fx-hero fx-hero-minimal"${bgStyle}><div class="fx-hero-inner">${h1}${sub}${ctas}</div></section>\n`
|
|
1357
1418
|
}
|
|
@@ -1424,13 +1485,102 @@ function rSect(b) {
|
|
|
1424
1485
|
return `<section class="fx-sect${cls}"${bgStyle}>${inner}</section>\n`
|
|
1425
1486
|
}
|
|
1426
1487
|
|
|
1488
|
+
|
|
1489
|
+
// ── rCode: code window com syntax highlight ───────────────────────
|
|
1490
|
+
function rCode(b) {
|
|
1491
|
+
const lang = b.lang || 'aip'
|
|
1492
|
+
const id = 'code_' + Math.random().toString(36).slice(2,7)
|
|
1493
|
+
const highlighted = (b.lines||[]).map(line => {
|
|
1494
|
+
let l = esc(line)
|
|
1495
|
+
if(lang==='aip'||lang==='aiplang') {
|
|
1496
|
+
l = l.replace(/^(~\w+)/g,'<span class="fx-kw">$1</span>')
|
|
1497
|
+
l = l.replace(/\$([\w.]+)/g,'<span class="fx-nb">$$$1</span>')
|
|
1498
|
+
l = l.replace(/(\{[^}]*\})/g,'<span class="fx-op">$1</span>')
|
|
1499
|
+
l = l.replace(/(#[\w-]+(?:\.\w+)*)/g,'<span class="fx-comment">$1</span>')
|
|
1500
|
+
} else if(lang==='bash'||lang==='sh') {
|
|
1501
|
+
l = l.replace(/^(\$ )/,'<span class="fx-kw">$1</span>')
|
|
1502
|
+
l = l.replace(/(#.*$)/,'<span class="fx-comment">$1</span>')
|
|
1503
|
+
l = l.replace(/(npx aiplang \w+)/g,'<span class="fx-fn">$1</span>')
|
|
1504
|
+
} else if(lang==='js'||lang==='ts') {
|
|
1505
|
+
l = l.replace(/\b(const|let|var|function|async|await|return|import|from|export|if|else|for)\b/g,'<span class="fx-kw">$1</span>')
|
|
1506
|
+
l = l.replace(/(\/\/.*$)/,'<span class="fx-comment">$1</span>')
|
|
1507
|
+
l = l.replace(/(['"`][^'"`]*['"`])/g,'<span class="fx-st">$1</span>')
|
|
1508
|
+
}
|
|
1509
|
+
return `<div class="fx-code-line">${l}</div>`
|
|
1510
|
+
}).join('')
|
|
1511
|
+
const copyBtn = `<button class="fx-code-copy" onclick="(function(b){navigator.clipboard&&navigator.clipboard.writeText(b.querySelectorAll('.fx-code-line').length?Array.from(b.querySelectorAll('.fx-code-line')).map(l=>l.innerText).join('\\n'):'');var t=b.querySelector('.fx-code-copy');t&&(t.textContent='copiado!',setTimeout(()=>t.textContent='copiar',1500))})(this.closest('.fx-code-window'))">copiar</button>`
|
|
1512
|
+
return `<div class="fx-code-window"><div class="fx-code-bar"><div class="fx-dots"><span></span><span></span><span></span></div><span class="fx-code-lang">${esc(lang)}</span>${copyBtn}</div><div class="fx-code-body" id="${id}">${highlighted}</div></div>\n`
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
// ── rBenchmark: cards de benchmark com número + barra ─────────────
|
|
1516
|
+
function rBenchmark(b) {
|
|
1517
|
+
const cards = (b.items||[]).map(item => {
|
|
1518
|
+
const pct = item.pct || (item.num && item.num.includes('%') ? parseInt(item.num) : 85)
|
|
1519
|
+
const n = item.num || ''
|
|
1520
|
+
const isLow = pct < 20
|
|
1521
|
+
return `<div class="fx-bench-card">
|
|
1522
|
+
<div class="fx-bench-label">${esc(item.label||'')}</div>
|
|
1523
|
+
<div class="fx-bench-num">${esc(n)}</div>
|
|
1524
|
+
${item.vs ? `<div class="fx-bench-vs">${esc(item.vs)}</div>` : ''}
|
|
1525
|
+
<div class="fx-bench-bar"><div class="fx-bench-fill" style="width:${isLow?pct+'%':pct+'%'}"></div></div>
|
|
1526
|
+
</div>`
|
|
1527
|
+
}).join('')
|
|
1528
|
+
return `<div class="fx-benchmark">${cards}</div>\n`
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
// ── rInstall: multi-step code box com botões ──────────────────────
|
|
1532
|
+
function rInstall(b) {
|
|
1533
|
+
const steps = (b.cmds||[]).map((cmd,i) => {
|
|
1534
|
+
const isComment = cmd.startsWith('#')
|
|
1535
|
+
if(isComment) return `<div class="fx-install-comment">${esc(cmd.slice(1).trim())}</div>`
|
|
1536
|
+
return `<div class="fx-install-line"><span class="fx-install-prompt">$</span><span class="fx-install-cmd">${esc(cmd)}</span></div>`
|
|
1537
|
+
}).join('')
|
|
1538
|
+
const firstCmd = (b.cmds||[]).find(c => !c.startsWith('#')) || ''
|
|
1539
|
+
const copy = `navigator.clipboard&&navigator.clipboard.writeText(${JSON.stringify(firstCmd)})`
|
|
1540
|
+
return `<div class="fx-install-wrap">
|
|
1541
|
+
<div class="fx-code-bar"><div class="fx-dots"><span></span><span></span><span></span></div><span class="fx-code-lang">terminal</span><button class="fx-code-copy" onclick="${copy};var t=this;t.textContent='copiado!';setTimeout(()=>t.textContent='copiar',1500)">copiar</button></div>
|
|
1542
|
+
<div class="fx-install-body">${steps}</div>
|
|
1543
|
+
</div>\n`
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// ── rStats upgrade: suporte a subtítulo via "val:label:vs" ────────
|
|
1547
|
+
function rStatsUpgraded(b) {
|
|
1548
|
+
const cells = (b.items||[]).map(item => {
|
|
1549
|
+
const raw = item[0]?.text || ''
|
|
1550
|
+
const parts = raw.split(':')
|
|
1551
|
+
const val = parts[0]?.trim()
|
|
1552
|
+
const lbl = parts[1]?.trim()
|
|
1553
|
+
const vs = parts[2]?.trim()
|
|
1554
|
+
const bind = isDyn(val) ? ` data-fx-bind="${esc(val)}"` : ''
|
|
1555
|
+
return `<div class="fx-stat">
|
|
1556
|
+
<div class="fx-stat-val"${bind}>${esc(val)}</div>
|
|
1557
|
+
<div class="fx-stat-lbl">${esc(lbl||'')}</div>
|
|
1558
|
+
${vs ? `<div class="fx-stat-vs">${esc(vs)}</div>` : ''}
|
|
1559
|
+
</div>`
|
|
1560
|
+
}).join('')
|
|
1561
|
+
return `<div class="fx-stats">${cells}</div>\n`
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1427
1564
|
function rFoot(b) {
|
|
1565
|
+
let brand='', links='', note=''
|
|
1566
|
+
let itemIdx = 0
|
|
1567
|
+
for(const item of (b.items||[])) for(const f of item){
|
|
1568
|
+
if(f.isLink) links+=`<a href="${esc(f.path)}" class="fx-footer-link">${esc(f.label)}</a>`
|
|
1569
|
+
else if(itemIdx===0) { brand=`<span class="fx-footer-brand">${esc(f.text)}</span>`; itemIdx++ }
|
|
1570
|
+
else note=`<span class="fx-footer-note">${esc(f.text)}</span>`
|
|
1571
|
+
}
|
|
1572
|
+
if(brand||links){
|
|
1573
|
+
return `<footer class="fx-footer"><div class="fx-footer-inner">${brand}<div class="fx-footer-links">${links}</div>${note}</div></footer>
|
|
1574
|
+
`
|
|
1575
|
+
}
|
|
1576
|
+
// fallback centrado
|
|
1428
1577
|
let inner=''
|
|
1429
1578
|
for(const item of (b.items||[])) for(const f of item){
|
|
1430
1579
|
if(f.isLink) inner+=`<a href="${esc(f.path)}" class="fx-footer-link">${esc(f.label)}</a>`
|
|
1431
1580
|
else inner+=`<p class="fx-footer-text">${esc(f.text)}</p>`
|
|
1432
1581
|
}
|
|
1433
|
-
return `<footer class="fx-footer">${inner}</footer
|
|
1582
|
+
return `<footer class="fx-footer">${inner}</footer>
|
|
1583
|
+
`
|
|
1434
1584
|
}
|
|
1435
1585
|
|
|
1436
1586
|
function rTable(b) {
|
|
@@ -1551,6 +1701,51 @@ function genThemeVarCSS(t) {
|
|
|
1551
1701
|
|
|
1552
1702
|
function css(theme) {
|
|
1553
1703
|
const base=`*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html{scroll-behavior:smooth}body{font-family:-apple-system,'Segoe UI',system-ui,sans-serif;-webkit-font-smoothing:antialiased;min-height:100vh}a{text-decoration:none;color:inherit}input,button,select{font-family:inherit}img{max-width:100%;height:auto}.fx-nav{display:flex;align-items:center;justify-content:space-between;padding:1rem 2.5rem;position:sticky;top:0;z-index:50;backdrop-filter:blur(12px);flex-wrap:wrap;gap:.5rem}.fx-brand{font-size:1.25rem;font-weight:800;letter-spacing:-.03em}.fx-nav-links{display:flex;align-items:center;gap:1.75rem}.fx-nav-link{font-size:.875rem;font-weight:500;opacity:.65;transition:opacity .15s}.fx-nav-link:hover{opacity:1}.fx-hamburger{display:none;flex-direction:column;gap:5px;background:none;border:none;cursor:pointer;padding:.25rem}.fx-hamburger span{display:block;width:22px;height:2px;background:currentColor;transition:all .2s;border-radius:1px}.fx-hamburger.open span:nth-child(1){transform:rotate(45deg) translate(5px,5px)}.fx-hamburger.open span:nth-child(2){opacity:0}.fx-hamburger.open span:nth-child(3){transform:rotate(-45deg) translate(5px,-5px)}@media(max-width:640px){.fx-hamburger{display:flex}.fx-nav-links{display:none;width:100%;flex-direction:column;align-items:flex-start;gap:.75rem;padding:.75rem 0}.fx-nav-links.open{display:flex}}.fx-hero{display:flex;align-items:center;justify-content:center;min-height:92vh;padding:4rem 1.5rem}.fx-hero-split{display:grid;grid-template-columns:1fr 1fr;gap:3rem;align-items:center;padding:4rem 2.5rem;min-height:70vh}@media(max-width:768px){.fx-hero-split{grid-template-columns:1fr}}.fx-hero-img{width:100%;border-radius:1.25rem;object-fit:cover;max-height:500px}.fx-hero-inner{max-width:56rem;text-align:center;display:flex;flex-direction:column;align-items:center;gap:1.5rem}.fx-hero-split .fx-hero-inner{text-align:left;align-items:flex-start;max-width:none}.fx-title{font-size:clamp(2.5rem,8vw,5.5rem);font-weight:900;letter-spacing:-.04em;line-height:1}.fx-sub{font-size:clamp(1rem,2vw,1.25rem);line-height:1.75;max-width:40rem}.fx-cta{display:inline-flex;align-items:center;padding:.875rem 2.5rem;border-radius:.75rem;font-weight:700;font-size:1rem;letter-spacing:-.01em;transition:transform .15s;margin:.25rem}.fx-cta:hover{transform:translateY(-1px)}.fx-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:3rem;padding:5rem 2.5rem;text-align:center}.fx-stat-val{font-size:clamp(2.5rem,5vw,4rem);font-weight:900;letter-spacing:-.04em;line-height:1}.fx-stat-lbl{font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.1em;margin-top:.5rem}.fx-grid{display:grid;gap:1.25rem;padding:1rem 2.5rem 5rem}.fx-grid-2{grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}.fx-grid-3{grid-template-columns:repeat(auto-fit,minmax(240px,1fr))}.fx-grid-4{grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}.fx-card{border-radius:1rem;padding:1.75rem;transition:transform .2s,box-shadow .2s}.fx-card:hover{transform:translateY(-2px)}.fx-card-img{width:100%;border-radius:.75rem;object-fit:cover;height:180px;margin-bottom:1rem}.fx-icon{font-size:2rem;margin-bottom:1rem}.fx-card-title{font-size:1.0625rem;font-weight:700;letter-spacing:-.02em;margin-bottom:.5rem}.fx-card-body{font-size:.875rem;line-height:1.65}.fx-card-link{font-size:.8125rem;font-weight:600;display:inline-block;margin-top:1rem;opacity:.6;transition:opacity .15s}.fx-card-link:hover{opacity:1}.fx-sect{padding:5rem 2.5rem}.fx-sect-title{font-size:clamp(1.75rem,4vw,3rem);font-weight:800;letter-spacing:-.04em;margin-bottom:1.5rem;text-align:center}.fx-sect-body{font-size:1rem;line-height:1.75;text-align:center;max-width:48rem;margin:0 auto}.fx-form-wrap{padding:3rem 2.5rem;display:flex;justify-content:center}.fx-form{width:100%;max-width:28rem;border-radius:1.25rem;padding:2.5rem}.fx-field{margin-bottom:1.25rem}.fx-label{display:block;font-size:.8125rem;font-weight:600;margin-bottom:.5rem}.fx-input{width:100%;padding:.75rem 1rem;border-radius:.625rem;font-size:.9375rem;outline:none;transition:box-shadow .15s}.fx-input:focus{box-shadow:0 0 0 3px rgba(37,99,235,.35)}.fx-btn{width:100%;padding:.875rem 1.5rem;border:none;border-radius:.625rem;font-size:.9375rem;font-weight:700;cursor:pointer;margin-top:.5rem;transition:transform .15s,opacity .15s;letter-spacing:-.01em}.fx-btn:hover{transform:translateY(-1px)}.fx-btn:disabled{opacity:.5;cursor:not-allowed;transform:none}.fx-btn-wrap{padding:0 2.5rem 1.5rem}.fx-standalone-btn{width:auto;padding:.75rem 2rem;margin-top:0}.fx-form-msg{font-size:.8125rem;padding:.5rem 0;min-height:1.5rem;text-align:center}.fx-form-err{color:#f87171}.fx-form-ok{color:#4ade80}.fx-table-wrap{overflow-x:auto;padding:0 2.5rem 4rem}.fx-table{width:100%;border-collapse:collapse;font-size:.875rem}.fx-th{text-align:left;padding:.875rem 1.25rem;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em}.fx-th-actions{opacity:.6}.fx-tr{transition:background .1s}.fx-td{padding:.875rem 1.25rem}.fx-td-empty{padding:2rem 1.25rem;text-align:center;opacity:.4}.fx-td-actions{white-space:nowrap;padding:.5rem 1rem!important}.fx-action-btn{border:none;cursor:pointer;font-size:.75rem;font-weight:600;padding:.3rem .75rem;border-radius:.375rem;margin-right:.375rem;font-family:inherit;transition:opacity .15s}.fx-action-btn:hover{opacity:.85}.fx-edit-btn{background:#1e40af;color:#93c5fd}.fx-delete-btn{background:#7f1d1d;color:#fca5a5}.fx-select-wrap{padding:.5rem 2.5rem}.fx-select-block{width:auto;min-width:200px;margin-top:0}.fx-pricing{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1.5rem;padding:2rem 2.5rem 5rem;align-items:start}.fx-pricing-card{border-radius:1.25rem;padding:2rem;position:relative;transition:transform .2s}.fx-pricing-featured{transform:scale(1.03)}.fx-pricing-badge{position:absolute;top:-12px;left:50%;transform:translateX(-50%);background:#2563eb;color:#fff;font-size:.7rem;font-weight:700;padding:.25rem .875rem;border-radius:999px;white-space:nowrap;letter-spacing:.05em}.fx-pricing-name{font-size:.875rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;margin-bottom:.5rem;opacity:.7}.fx-pricing-price{font-size:3rem;font-weight:900;letter-spacing:-.05em;line-height:1;margin-bottom:.75rem}.fx-pricing-desc{font-size:.875rem;line-height:1.65;margin-bottom:1.5rem;opacity:.7}.fx-pricing-cta{display:block;text-align:center;padding:.75rem;border-radius:.625rem;font-weight:700;font-size:.9rem;transition:opacity .15s}.fx-pricing-cta:hover{opacity:.85}.fx-faq{max-width:48rem;margin:0 auto}.fx-faq-item{border-radius:.75rem;margin-bottom:.625rem;cursor:pointer;overflow:hidden;transition:background .15s}.fx-faq-q{display:flex;justify-content:space-between;align-items:center;padding:1rem 1.25rem;font-size:.9375rem;font-weight:600}.fx-faq-arrow{transition:transform .2s;font-size:.75rem;opacity:.5}.fx-faq-item.open .fx-faq-arrow{transform:rotate(90deg)}.fx-faq-a{max-height:0;overflow:hidden;padding:0 1.25rem;font-size:.875rem;line-height:1.7;transition:max-height .3s,padding .3s}.fx-faq-item.open .fx-faq-a{max-height:300px;padding:.75rem 1.25rem 1.25rem}.fx-testi-wrap{padding:5rem 2.5rem;display:flex;justify-content:center}.fx-testi{max-width:42rem;text-align:center;display:flex;flex-direction:column;align-items:center;gap:1.25rem}.fx-testi-img{width:64px;height:64px;border-radius:50%;object-fit:cover}.fx-testi-avatar{width:64px;height:64px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:1.5rem;font-weight:700;background:#1e293b}.fx-testi-quote{font-size:1.25rem;line-height:1.7;font-style:italic;opacity:.9}.fx-testi-author{font-size:.875rem;font-weight:600;opacity:.5}.fx-gallery{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:.75rem;padding:1rem 2.5rem 4rem}.fx-gallery-item{border-radius:.75rem;overflow:hidden;aspect-ratio:4/3}.fx-gallery-item img{width:100%;height:100%;object-fit:cover;transition:transform .3s}.fx-gallery-item:hover img{transform:scale(1.04)}.fx-if-wrap{display:contents}.fx-footer{padding:3rem 2.5rem;text-align:center}.fx-footer-text{font-size:.8125rem}.fx-footer-link{font-size:.8125rem;margin:0 .75rem;opacity:.5;transition:opacity .15s}.fx-footer-link:hover{opacity:1}
|
|
1704
|
+
/* ── code window ── */
|
|
1705
|
+
.fx-code-window{border-radius:.875rem;overflow:hidden;border:1px solid rgba(255,255,255,.08);margin:0 2.5rem 2rem}
|
|
1706
|
+
.fx-code-bar{display:flex;align-items:center;gap:.75rem;padding:.625rem 1rem;background:rgba(255,255,255,.04);border-bottom:1px solid rgba(255,255,255,.06)}
|
|
1707
|
+
.fx-dots{display:flex;gap:.375rem}
|
|
1708
|
+
.fx-dots span{width:10px;height:10px;border-radius:50%;background:rgba(255,255,255,.15)}
|
|
1709
|
+
.fx-dots span:nth-child(1){background:#ff5f57}.fx-dots span:nth-child(2){background:#febc2e}.fx-dots span:nth-child(3){background:#28c840}
|
|
1710
|
+
.fx-code-lang{font-size:.62rem;letter-spacing:.1em;text-transform:uppercase;opacity:.35;font-family:monospace;margin-left:.25rem}
|
|
1711
|
+
.fx-code-copy{margin-left:auto;font-family:monospace;font-size:.62rem;letter-spacing:.1em;text-transform:uppercase;background:none;border:1px solid rgba(255,255,255,.15);color:rgba(255,255,255,.4);padding:.2rem .625rem;border-radius:.3rem;cursor:pointer;transition:all .15s}
|
|
1712
|
+
.fx-code-copy:hover{border-color:rgba(255,255,255,.3);color:rgba(255,255,255,.7)}
|
|
1713
|
+
.fx-code-body{padding:1.375rem 1.5rem;overflow-x:auto}
|
|
1714
|
+
.fx-code-line{font-family:"JetBrains Mono","Fira Code","Courier New",monospace;font-size:.8rem;line-height:1.75;color:#8899aa;white-space:pre}
|
|
1715
|
+
.fx-kw{color:#c792ea}.fx-st{color:#c3e88d}.fx-fn{color:#82aaff}.fx-nb{color:#f78c6c}.fx-op{color:var(--accent,#ff5722)}.fx-comment{color:#3d5166;font-style:italic}
|
|
1716
|
+
/* ── benchmark ── */
|
|
1717
|
+
.fx-benchmark{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:1rem;padding:1rem 2.5rem 4rem}
|
|
1718
|
+
.fx-bench-card{border-radius:1rem;padding:1.5rem;border:1px solid rgba(255,255,255,.06);background:rgba(255,255,255,.02);position:relative;overflow:hidden}
|
|
1719
|
+
.fx-bench-card::before{content:"";position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,var(--accent,#ff5722),transparent)}
|
|
1720
|
+
.fx-bench-label{font-size:.6rem;letter-spacing:.12em;text-transform:uppercase;opacity:.5;margin-bottom:.5rem;font-family:monospace}
|
|
1721
|
+
.fx-bench-num{font-size:clamp(2rem,5vw,3.5rem);font-weight:900;letter-spacing:-.04em;line-height:1;color:var(--accent,#ff5722)}
|
|
1722
|
+
.fx-bench-vs{font-size:.7rem;opacity:.4;margin-top:.25rem;font-family:monospace}
|
|
1723
|
+
.fx-bench-bar{margin-top:1rem;height:5px;background:rgba(255,255,255,.06);border-radius:3px;overflow:hidden}
|
|
1724
|
+
.fx-bench-fill{height:100%;border-radius:3px;background:var(--accent,#ff5722);transition:width 1.5s cubic-bezier(.4,0,.2,1)}
|
|
1725
|
+
/* ── install ── */
|
|
1726
|
+
.fx-install-wrap{border-radius:.875rem;overflow:hidden;border:1px solid rgba(255,255,255,.08);margin:0 2.5rem 2rem;max-width:540px}
|
|
1727
|
+
.fx-install-body{padding:1.25rem 1.5rem}
|
|
1728
|
+
.fx-install-line{display:flex;gap:.75rem;padding:.25rem 0;font-family:"JetBrains Mono","Courier New",monospace;font-size:.8rem;line-height:1.7}
|
|
1729
|
+
.fx-install-prompt{color:var(--accent,#ff5722);flex-shrink:0}
|
|
1730
|
+
.fx-install-cmd{color:rgba(255,255,255,.85)}
|
|
1731
|
+
.fx-install-comment{font-family:"JetBrains Mono","Courier New",monospace;font-size:.72rem;color:rgba(255,255,255,.25);padding:.25rem 0;font-style:italic}
|
|
1732
|
+
/* ── stats upgrade ── */
|
|
1733
|
+
.fx-stat-vs{font-size:.65rem;opacity:.35;margin-top:.2rem;letter-spacing:.02em}
|
|
1734
|
+
/* ── hero landing (dark variant) grid + glow ── */
|
|
1735
|
+
.fx-hero-landing{position:relative;overflow:hidden}
|
|
1736
|
+
.fx-hero-landing::before{content:"";position:absolute;inset:0;background:linear-gradient(rgba(255,255,255,.05) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.05) 1px,transparent 1px);background-size:60px 60px;mask-image:radial-gradient(ellipse 70% 60% at 50% 50%,black,transparent);pointer-events:none}
|
|
1737
|
+
.fx-hero-landing::after{content:"";position:absolute;width:700px;height:500px;border-radius:50%;background:radial-gradient(ellipse,rgba(255,87,34,.13) 0%,transparent 70%);left:50%;top:50%;transform:translate(-50%,-50%);pointer-events:none;animation:fx-breathe 6s ease-in-out infinite}
|
|
1738
|
+
@keyframes fx-breathe{0%,100%{transform:translate(-50%,-50%) scale(1)}50%{transform:translate(-50%,-50%) scale(1.1)}}
|
|
1739
|
+
/* ── feature grid ── */
|
|
1740
|
+
.fx-grid-feature{gap:1rem}
|
|
1741
|
+
.fx-grid-feature .fx-card{transition:border-color .2s,transform .15s}
|
|
1742
|
+
.fx-grid-feature .fx-card:hover{border-color:rgba(255,87,34,.25)}
|
|
1743
|
+
.fx-grid-feature .fx-icon{width:44px;height:44px;border-radius:.75rem;display:flex;align-items:center;justify-content:center;background:rgba(255,87,34,.08);border:1px solid rgba(255,87,34,.15)}
|
|
1744
|
+
/* ── footer upgrade ── */
|
|
1745
|
+
.fx-footer-inner{max-width:1100px;margin:0 auto;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:1rem}
|
|
1746
|
+
.fx-footer-brand{font-size:1rem;font-weight:800;letter-spacing:-.02em}
|
|
1747
|
+
.fx-footer-links{display:flex;gap:1.5rem;flex-wrap:wrap}
|
|
1748
|
+
.fx-footer-note{font-size:.72rem;opacity:.3;font-family:monospace}
|
|
1554
1749
|
.fx-hero-minimal{min-height:50vh!important}
|
|
1555
1750
|
.fx-hero-minimal .fx-hero-inner{gap:1rem}
|
|
1556
1751
|
.fx-hero-tall{min-height:98vh!important}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiplang",
|
|
3
|
-
"version": "2.11.
|
|
3
|
+
"version": "2.11.5",
|
|
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.5',
|
|
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,
|