aiplang 2.9.3 → 2.10.0
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 +44 -8
- package/package.json +1 -1
- package/server/server.js +12 -4
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.
|
|
8
|
+
const VERSION = '2.10.0'
|
|
9
9
|
const RUNTIME_DIR = path.join(__dirname, '..', 'runtime')
|
|
10
10
|
const cmd = process.argv[2]
|
|
11
11
|
const args = process.argv.slice(3)
|
|
@@ -648,9 +648,15 @@ function parseBlock(line) {
|
|
|
648
648
|
const binding=line.slice(start,idx).trim().replace(/^@/,'@')
|
|
649
649
|
const content=line.slice(idx+1,line.lastIndexOf('}')).trim()
|
|
650
650
|
const em=content.match(/edit\s+(PUT|PATCH)\s+(\S+)/), dm=content.match(/delete\s+(?:DELETE\s+)?(\S+)/)
|
|
651
|
-
const
|
|
651
|
+
const fallbackM=content.match(/fallback\s*:\s*([^|]+)/)
|
|
652
|
+
const retryM=content.match(/retry\s*:\s*(\S+)/)
|
|
653
|
+
const clean=content
|
|
654
|
+
.replace(/edit\s+(PUT|PATCH)\s+\S+/g,'')
|
|
655
|
+
.replace(/delete\s+(?:DELETE\s+)?\S+/g,'')
|
|
656
|
+
.replace(/fallback\s*:[^|]+/g,'')
|
|
657
|
+
.replace(/retry\s*:\s*\S+/g,'')
|
|
652
658
|
const cols=parseCols(clean)
|
|
653
|
-
return{kind:'table',binding,cols:Array.isArray(cols)?cols:[],empty:parseEmpty(clean),editPath:em?.[2]||null,editMethod:em?.[1]||'PUT',deletePath:dm?.[1]||null,deleteKey:'id',extraClass,animate,variant,style,bg}
|
|
659
|
+
return{kind:'table',binding,cols:Array.isArray(cols)?cols:[],empty:parseEmpty(clean),editPath:em?.[2]||null,editMethod:em?.[1]||'PUT',deletePath:dm?.[1]||null,deleteKey:'id',fallback:fallbackM?.[1]?.trim()||null,retry:retryM?.[1]||null,extraClass,animate,variant,style,bg}
|
|
654
660
|
}
|
|
655
661
|
|
|
656
662
|
// ── form ────────────────────────────────────────────────────
|
|
@@ -658,12 +664,17 @@ function parseBlock(line) {
|
|
|
658
664
|
const bi=line.indexOf('{');if(bi===-1) return null
|
|
659
665
|
let head=line.slice(line.startsWith('form{')?4:5,bi).trim()
|
|
660
666
|
const content=line.slice(bi+1,line.lastIndexOf('}')).trim()
|
|
661
|
-
let action=''; const ai=head.indexOf('=>')
|
|
662
|
-
if(ai!==-1){
|
|
667
|
+
let action='', optimistic=false; const ai=head.indexOf('=>')
|
|
668
|
+
if(ai!==-1){
|
|
669
|
+
action=head.slice(ai+2).trim()
|
|
670
|
+
// Optimistic: => @list.optimistic($result)
|
|
671
|
+
if(action.includes('.optimistic(')){optimistic=true;action=action.replace('.optimistic','')}
|
|
672
|
+
head=head.slice(0,ai).trim()
|
|
673
|
+
}
|
|
663
674
|
const parts=head.trim().split(/\s+/)
|
|
664
675
|
const method=parts[0]&&['GET','POST','PUT','PATCH','DELETE'].includes(parts[0].toUpperCase())?parts[0].toUpperCase():'POST'
|
|
665
676
|
const bpath=parts[method===parts[0].toUpperCase()?1:0]||''
|
|
666
|
-
return{kind:'form',method,bpath,action,fields:parseFields(content)||[],extraClass,animate,variant,style,bg}
|
|
677
|
+
return{kind:'form',method,bpath,action,optimistic,fields:parseFields(content)||[],extraClass,animate,variant,style,bg}
|
|
667
678
|
}
|
|
668
679
|
|
|
669
680
|
// ── pricing ─────────────────────────────────────────────────
|
|
@@ -858,11 +869,33 @@ function renderBlock(b, page) {
|
|
|
858
869
|
case 'testimonial': return rTestimonial(b)
|
|
859
870
|
case 'gallery': return rGallery(b)
|
|
860
871
|
case 'raw': return (b.html||'')+'\n'
|
|
872
|
+
case 'html': return `<div class="fx-html">${b.content||''}</div>\n`
|
|
873
|
+
case 'spacer': return `<div class="fx-spacer" style="height:${esc(b.height||'2rem')}"></div>\n`
|
|
874
|
+
case 'divider': return b.label?`<div class="fx-divider"><span class="fx-divider-label">${esc(b.label)}</span></div>\n`:`<hr class="fx-hr">\n`
|
|
875
|
+
case 'badge': return `<div class="fx-badge-row"><span class="fx-badge-tag">${esc(b.content||'')}</span></div>\n`
|
|
876
|
+
case 'card': return rCardBlock(b)
|
|
877
|
+
case 'cols': return rColsBlock(b)
|
|
878
|
+
case 'each': return `<div class="fx-each fx-each-${b.variant||'list'}" data-fx-each="${esc(b.binding||'')}" data-fx-tpl="${esc(b.tpl||'')}"${b.style?` style="${b.style.replace(/,/g,';')}"`:''}>\n<div class="fx-each-empty fx-td-empty">Loading...</div></div>\n`
|
|
861
879
|
case 'if': return `<div class="fx-if-wrap" data-fx-if="${esc(b.cond)}" style="display:none"></div>\n`
|
|
862
880
|
default: return ''
|
|
863
881
|
}
|
|
864
882
|
}
|
|
865
883
|
|
|
884
|
+
function rCardBlock(b) {
|
|
885
|
+
const img=b.img?`<img src="${esc(b.img)}" class="fx-card-img" alt="${esc(b.title||'')}" loading="lazy">`:'';
|
|
886
|
+
const badge=b.badge?`<span class="fx-card-badge">${esc(b.badge)}</span>`:'';
|
|
887
|
+
const title=b.title?`<h3 class="fx-card-title">${esc(b.title)}</h3>`:'';
|
|
888
|
+
const sub=b.subtitle?`<p class="fx-card-body">${esc(b.subtitle)}</p>`:'';
|
|
889
|
+
const link=b.link?`<a href="${esc(b.link.split(':')[0])}" class="fx-card-link">${esc(b.link.split(':')[1]||'View')} →</a>`:'';
|
|
890
|
+
const bg=b.bg?` style="background:${b.bg}"`:b.style?` style="${b.style.replace(/,/g,';')}"`:''
|
|
891
|
+
return`<div class="fx-card"${bg}>${img}${badge}${title}${sub}${link}</div>\n`
|
|
892
|
+
}
|
|
893
|
+
function rColsBlock(b) {
|
|
894
|
+
const cols=(b.items||[]).map(col=>`<div class="fx-col">${col}</div>`).join('')
|
|
895
|
+
const style=b.style?` style="${b.style.replace(/,/g,';')}"`:''
|
|
896
|
+
return`<div class="fx-cols fx-cols-${b.n||2}"${style}>${cols}</div>\n`
|
|
897
|
+
}
|
|
898
|
+
|
|
866
899
|
function rNav(b) {
|
|
867
900
|
if(!b.items?.[0]) return ''
|
|
868
901
|
const it=b.items[0]
|
|
@@ -973,7 +1006,9 @@ function rTable(b) {
|
|
|
973
1006
|
const da=b.deletePath?` data-fx-delete="${esc(b.deletePath)}"`:''
|
|
974
1007
|
const at=(b.editPath||b.deletePath)?'<th class="fx-th fx-th-actions">Actions</th>':''
|
|
975
1008
|
const span=cols.length+((b.editPath||b.deletePath)?1:0)
|
|
976
|
-
|
|
1009
|
+
const fallbackAttr=b.fallback?` data-fx-fallback="${esc(b.fallback)}"`:''
|
|
1010
|
+
const retryAttr=b.retry?` data-fx-retry="${esc(b.retry)}"`:''
|
|
1011
|
+
return `<div class="fx-table-wrap"><table class="fx-table" data-fx-table="${esc(b.binding)}" data-fx-cols='${keys}' data-fx-col-map='${cm}'${ea}${da}${fallbackAttr}${retryAttr}><thead><tr>${ths}${at}</tr></thead><tbody class="fx-tbody"><tr><td colspan="${span}" class="fx-td-empty">${esc(b.empty)}</td></tr></tbody></table></div>\n`
|
|
977
1012
|
}
|
|
978
1013
|
|
|
979
1014
|
function rForm(b) {
|
|
@@ -993,7 +1028,8 @@ function rForm(b) {
|
|
|
993
1028
|
if(v==='minimal') {
|
|
994
1029
|
return `<div class="fx-form-minimal"><form data-fx-form="${esc(b.bpath)}" data-fx-method="${esc(b.method)}" data-fx-action="${esc(b.action)}">${fields}<div class="fx-form-msg"></div><button type="submit" class="fx-btn">${esc(label)}</button></form></div>\n`
|
|
995
1030
|
}
|
|
996
|
-
|
|
1031
|
+
const optAttr=b.optimistic?' data-fx-optimistic="true"':''
|
|
1032
|
+
return `<div class="fx-form-wrap"><form class="fx-form"${bgStyle}${optAttr} data-fx-form="${esc(b.bpath)}" data-fx-method="${esc(b.method)}" data-fx-action="${esc(b.action)}">${fields}<div class="fx-form-msg"></div><button type="submit" class="fx-btn">${esc(label)}</button></form></div>\n`
|
|
997
1033
|
}
|
|
998
1034
|
|
|
999
1035
|
function rBtn(b) {
|
package/package.json
CHANGED
package/server/server.js
CHANGED
|
@@ -325,7 +325,9 @@ function validateAndCoerce(data, schema) {
|
|
|
325
325
|
|
|
326
326
|
// Coerce type
|
|
327
327
|
if (val !== undefined && val !== null) {
|
|
328
|
-
if (def.type
|
|
328
|
+
if (Array.isArray(val) || (typeof val === 'object' && val !== null && def.type !== 'json')) {
|
|
329
|
+
errors.push(`${col}: expected ${def.type}, got ${Array.isArray(val)?'array':'object'}`)
|
|
330
|
+
} else if (def.type === 'int') {
|
|
329
331
|
const n = parseInt(val)
|
|
330
332
|
if (isNaN(n)) errors.push(`${col}: expected integer, got "${val}"`)
|
|
331
333
|
else out[col] = n
|
|
@@ -706,7 +708,13 @@ function parseEventLine(s) { const m=s.match(/^(\S+)\s*=>\s*(.+)$/); return{even
|
|
|
706
708
|
function parseField(line) {
|
|
707
709
|
const p=line.split(':').map(s=>s.trim())
|
|
708
710
|
const f={name:p[0],type:p[1]||'text',modifiers:[],enumVals:[],default:null}
|
|
709
|
-
|
|
711
|
+
// If type is enum, p[2] contains comma-separated values directly
|
|
712
|
+
if (f.type === 'enum' && p[2] && !p[2].startsWith('default=') && !['required','unique','hashed','pk','auto','index'].includes(p[2])) {
|
|
713
|
+
f.enumVals = p[2].split(',').map(v=>v.trim()).filter(Boolean)
|
|
714
|
+
for(let j=3;j<p.length;j++){const x=p[j];if(x.startsWith('default='))f.default=x.slice(8);else if(x)f.modifiers.push(x)}
|
|
715
|
+
} else {
|
|
716
|
+
for(let j=2;j<p.length;j++){const x=p[j];if(x.startsWith('default='))f.default=x.slice(8);else if(x.startsWith('enum:'))f.enumVals=x.slice(5).split(',').map(v=>v.trim());else if(x)f.modifiers.push(x)}
|
|
717
|
+
}
|
|
710
718
|
return f
|
|
711
719
|
}
|
|
712
720
|
function parseAPILine(line, route) {
|
|
@@ -1710,7 +1718,7 @@ async function startServer(aipFile, port = 3000) {
|
|
|
1710
1718
|
migrateModels(app.models)
|
|
1711
1719
|
|
|
1712
1720
|
// Register models
|
|
1713
|
-
for (const m of app.models) srv.registerModel(m.name, { softDelete: m.softDelete, timestamps: true })
|
|
1721
|
+
for (const m of app.models) srv.registerModel(m.name, MODEL_DEFS[m.name] || { softDelete: m.softDelete, timestamps: true })
|
|
1714
1722
|
|
|
1715
1723
|
// Events
|
|
1716
1724
|
for (const ev of app.events) on(ev.event, (data) => console.log(`[aiplang:event] ${ev.event}:`, ev.action))
|
|
@@ -1789,7 +1797,7 @@ async function startServer(aipFile, port = 3000) {
|
|
|
1789
1797
|
|
|
1790
1798
|
// Health
|
|
1791
1799
|
srv.addRoute('GET', '/health', (req, res) => res.json(200, {
|
|
1792
|
-
status:'ok', version:'2.
|
|
1800
|
+
status:'ok', version:'2.10.0',
|
|
1793
1801
|
models: app.models.map(m=>m.name),
|
|
1794
1802
|
routes: app.apis.length, pages: app.pages.length,
|
|
1795
1803
|
admin: app.admin?.prefix || null,
|