aiplang 2.11.2 → 2.11.3
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 +266 -1
- package/package.json +1 -1
- package/server/server.js +1 -1
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.3'
|
|
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
|
|
@@ -451,6 +452,242 @@ const _KNOWN_TYPES = new Set([
|
|
|
451
452
|
'bigint','smallint','tinyint','currency','money','price'
|
|
452
453
|
])
|
|
453
454
|
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
function _parseForTypes(src) {
|
|
458
|
+
const app = { models:[], apis:[], auth:null }
|
|
459
|
+
const _ta = {
|
|
460
|
+
integer:'int',boolean:'bool',double:'float',number:'float',string:'text',
|
|
461
|
+
varchar:'text',datetime:'timestamp',email:'email',url:'url',uri:'url',
|
|
462
|
+
phone:'phone',currency:'float',money:'float',price:'float',
|
|
463
|
+
json:'json',jsonb:'json',bigint:'int',smallint:'int',tinyint:'int'
|
|
464
|
+
}
|
|
465
|
+
const norm = t => _ta[(t||'').toLowerCase()] || t || 'text'
|
|
466
|
+
|
|
467
|
+
function parseField(line) {
|
|
468
|
+
const p = line.split(/\s*:\s*/)
|
|
469
|
+
const f = { name:(p[0]||'').trim(), type:norm(p[1]), modifiers:[], enumVals:[], constraints:{}, default:null }
|
|
470
|
+
if (f.type === 'enum') {
|
|
471
|
+
const ev = p.slice(2).find(x => x && !x.startsWith('default=') && !['required','unique','hashed','pk','auto','index'].includes(x.trim()))
|
|
472
|
+
if (ev) f.enumVals = ev.includes('|') ? ev.split('|').map(v=>v.trim()) : ev.split(',').map(v=>v.trim())
|
|
473
|
+
}
|
|
474
|
+
for (let j=2; j<p.length; j++) {
|
|
475
|
+
const x = (p[j]||'').trim()
|
|
476
|
+
if (!x) continue
|
|
477
|
+
if (x.startsWith('default=')) f.default = x.slice(8)
|
|
478
|
+
else if (x.startsWith('min=')) f.constraints.min = Number(x.slice(4))
|
|
479
|
+
else if (x.startsWith('max=')) f.constraints.max = Number(x.slice(4))
|
|
480
|
+
else if (x.startsWith('minLen=')) f.constraints.minLen = Number(x.slice(7))
|
|
481
|
+
else if (x.startsWith('maxLen=')) f.constraints.maxLen = Number(x.slice(7))
|
|
482
|
+
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())
|
|
483
|
+
else if (x && !x.includes('=')) f.modifiers.push(x)
|
|
484
|
+
}
|
|
485
|
+
return f
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Juntar todas as linhas e tokenizar por estado
|
|
489
|
+
const lines = src.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'))
|
|
490
|
+
let inModel=false, inAPI=false, curModel=null, depth=0
|
|
491
|
+
|
|
492
|
+
for (let i=0; i<lines.length; i++) {
|
|
493
|
+
const line = lines[i]
|
|
494
|
+
|
|
495
|
+
// Auth
|
|
496
|
+
if (line.startsWith('~auth ')) { app.auth = { type: line.includes('jwt') ? 'jwt' : 'basic' }; continue }
|
|
497
|
+
|
|
498
|
+
// Model
|
|
499
|
+
if (line.startsWith('model ') || (line.startsWith('model') && line.includes('{'))) {
|
|
500
|
+
if (inModel && curModel) app.models.push(curModel)
|
|
501
|
+
curModel = { name: line.replace(/^model\s+/, '').replace(/\s*\{.*$/, '').trim(), fields:[] }
|
|
502
|
+
inModel=true; inAPI=false; depth=0
|
|
503
|
+
if (line.includes('{')) depth++
|
|
504
|
+
continue
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// API
|
|
508
|
+
if (line.startsWith('api ')) {
|
|
509
|
+
if (inModel && curModel) { app.models.push(curModel); curModel=null; inModel=false }
|
|
510
|
+
const m = line.match(/^api\s+(\w+)\s+(\S+)/)
|
|
511
|
+
if (m) {
|
|
512
|
+
const guards = []
|
|
513
|
+
// Procura guard tanto na linha atual quanto nas próximas linhas do bloco
|
|
514
|
+
const blockLines = []
|
|
515
|
+
let j=i; let bd=0
|
|
516
|
+
while(j<lines.length) {
|
|
517
|
+
const bl = lines[j]
|
|
518
|
+
for(const ch of bl) { if(ch==='{') bd++; else if(ch==='}') bd-- }
|
|
519
|
+
blockLines.push(bl)
|
|
520
|
+
if(bd===0 && j>i) break
|
|
521
|
+
j++
|
|
522
|
+
}
|
|
523
|
+
const blockStr = blockLines.join(' ')
|
|
524
|
+
if (blockStr.includes('~guard auth')) guards.push('auth')
|
|
525
|
+
if (blockStr.includes('~guard admin')) guards.push('admin')
|
|
526
|
+
if (blockStr.includes('=> auth')) guards.push('auth')
|
|
527
|
+
if (blockStr.includes('=> admin')) guards.push('admin')
|
|
528
|
+
app.apis.push({ method:m[1].toUpperCase(), path:m[2], guards })
|
|
529
|
+
}
|
|
530
|
+
inAPI=true; inModel=false
|
|
531
|
+
// Contar profundidade das chaves para saber quando o bloco fecha
|
|
532
|
+
depth=0
|
|
533
|
+
for(const ch of line) { if(ch==='{') depth++; else if(ch==='}') depth-- }
|
|
534
|
+
if(depth<=0) { inAPI=false; depth=0 }
|
|
535
|
+
continue
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Dentro de api block: rastrear chaves
|
|
539
|
+
if (inAPI) {
|
|
540
|
+
for(const ch of line) { if(ch==='{') depth++; else if(ch==='}') depth-- }
|
|
541
|
+
if(depth<=0) { inAPI=false; depth=0 }
|
|
542
|
+
continue
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Fechar model
|
|
546
|
+
if (line === '}' && inModel) {
|
|
547
|
+
if (curModel) { app.models.push(curModel); curModel=null }
|
|
548
|
+
inModel=false; continue
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Campos do model
|
|
552
|
+
if (inModel && curModel && line && line !== '{' && !line.startsWith('~') && line.includes(':')) {
|
|
553
|
+
const f = parseField(line)
|
|
554
|
+
if (f.name) curModel.fields.push(f)
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (inModel && curModel) app.models.push(curModel)
|
|
558
|
+
return app
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// ── TypeScript type generator ────────────────────────────────────
|
|
562
|
+
// Maps aiplang types to TypeScript equivalents
|
|
563
|
+
const _AIP_TO_TS = {
|
|
564
|
+
text:'string', string:'string', email:'string', url:'string',
|
|
565
|
+
phone:'string', slug:'string', color:'string', file:'string', image:'string',
|
|
566
|
+
int:'number', integer:'number', float:'number', double:'number',
|
|
567
|
+
number:'number', currency:'number', money:'number', price:'number',
|
|
568
|
+
bool:'boolean', boolean:'boolean',
|
|
569
|
+
uuid:'string', date:'string', timestamp:'string', datetime:'string',
|
|
570
|
+
json:'Record<string,unknown>', jsonb:'Record<string,unknown>',
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function generateTypes(app, srcFile) {
|
|
574
|
+
const lines = [
|
|
575
|
+
'// ─────────────────────────────────────────────────────────────',
|
|
576
|
+
`// aiplang generated types — ${srcFile || 'app.aip'}`,
|
|
577
|
+
`// Generated: ${new Date().toISOString()}`,
|
|
578
|
+
'// DO NOT EDIT — regenerate with: npx aiplang types <app.aip>',
|
|
579
|
+
'// ─────────────────────────────────────────────────────────────',
|
|
580
|
+
'',
|
|
581
|
+
]
|
|
582
|
+
|
|
583
|
+
// Model interfaces
|
|
584
|
+
for (const model of (app.models || [])) {
|
|
585
|
+
const name = model.name
|
|
586
|
+
lines.push(`// Model: ${name}`)
|
|
587
|
+
|
|
588
|
+
// Enum types first
|
|
589
|
+
for (const f of (model.fields || [])) {
|
|
590
|
+
if (f.type === 'enum' && f.enumVals && f.enumVals.length) {
|
|
591
|
+
lines.push(`export type ${name}${_cap(f.name)} = ${f.enumVals.map(v => `'${v}'`).join(' | ')}`)
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Main interface
|
|
596
|
+
lines.push(`export interface ${name} {`)
|
|
597
|
+
for (const f of (model.fields || [])) {
|
|
598
|
+
const req = f.modifiers && f.modifiers.includes('required')
|
|
599
|
+
const pk = f.modifiers && f.modifiers.includes('pk')
|
|
600
|
+
const auto = f.modifiers && f.modifiers.includes('auto')
|
|
601
|
+
const opt = !req || pk || auto
|
|
602
|
+
let tsType
|
|
603
|
+
if (f.type === 'enum' && f.enumVals && f.enumVals.length) {
|
|
604
|
+
tsType = `${name}${_cap(f.name)}`
|
|
605
|
+
} else {
|
|
606
|
+
tsType = _AIP_TO_TS[f.type] || 'string'
|
|
607
|
+
}
|
|
608
|
+
// Constraint comments
|
|
609
|
+
const constraints = []
|
|
610
|
+
if (f.constraints) {
|
|
611
|
+
if (f.constraints.min != null) constraints.push(`min:${f.constraints.min}`)
|
|
612
|
+
if (f.constraints.max != null) constraints.push(`max:${f.constraints.max}`)
|
|
613
|
+
if (f.constraints.minLen != null) constraints.push(`minLen:${f.constraints.minLen}`)
|
|
614
|
+
if (f.constraints.maxLen != null) constraints.push(`maxLen:${f.constraints.maxLen}`)
|
|
615
|
+
if (f.constraints.format) constraints.push(`format:${f.constraints.format}`)
|
|
616
|
+
}
|
|
617
|
+
const comment = constraints.length ? ` // ${constraints.join(', ')}` : ''
|
|
618
|
+
const mods = []
|
|
619
|
+
if (f.modifiers) {
|
|
620
|
+
if (f.modifiers.includes('unique')) mods.push('@unique')
|
|
621
|
+
if (f.modifiers.includes('hashed')) mods.push('@hashed')
|
|
622
|
+
if (pk) mods.push('@pk')
|
|
623
|
+
if (auto) mods.push('@auto')
|
|
624
|
+
}
|
|
625
|
+
const modStr = mods.length ? ` /** ${mods.join(' ')} */` : ''
|
|
626
|
+
lines.push(` ${f.name}${opt ? '?' : ''}: ${tsType}${modStr}${comment}`)
|
|
627
|
+
}
|
|
628
|
+
lines.push(`}`)
|
|
629
|
+
lines.push(``)
|
|
630
|
+
|
|
631
|
+
// Input type (for POST/PUT — excludes pk/auto, optional for patches)
|
|
632
|
+
const inputFields = (model.fields || []).filter(f =>
|
|
633
|
+
!(f.modifiers && f.modifiers.includes('pk')) &&
|
|
634
|
+
!(f.modifiers && f.modifiers.includes('auto')) &&
|
|
635
|
+
f.name !== 'created_at' && f.name !== 'updated_at'
|
|
636
|
+
)
|
|
637
|
+
if (inputFields.length) {
|
|
638
|
+
lines.push(`export interface ${name}Input {`)
|
|
639
|
+
for (const f of inputFields) {
|
|
640
|
+
const req = f.modifiers && f.modifiers.includes('required')
|
|
641
|
+
let tsType
|
|
642
|
+
if (f.type === 'enum' && f.enumVals && f.enumVals.length) {
|
|
643
|
+
tsType = `${name}${_cap(f.name)}`
|
|
644
|
+
} else {
|
|
645
|
+
tsType = _AIP_TO_TS[f.type] || 'string'
|
|
646
|
+
}
|
|
647
|
+
lines.push(` ${f.name}${req ? '' : '?'}: ${tsType}`)
|
|
648
|
+
}
|
|
649
|
+
lines.push(`}`)
|
|
650
|
+
lines.push(``)
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// API route types
|
|
655
|
+
if ((app.apis || []).length) {
|
|
656
|
+
lines.push(`// ── API Route types ──────────────────────────────────────────`)
|
|
657
|
+
lines.push(``)
|
|
658
|
+
lines.push(`export interface AiplangRoutes {`)
|
|
659
|
+
for (const api of (app.apis || [])) {
|
|
660
|
+
const method = api.method.toUpperCase()
|
|
661
|
+
const path = api.path.replace(/\//g, '_').replace(/[^a-zA-Z0-9_]/g,'').replace(/^_/,'')
|
|
662
|
+
const guards = (api.guards || []).join(', ')
|
|
663
|
+
const guardComment = guards ? ` /** guards: ${guards} */` : ''
|
|
664
|
+
lines.push(` '${method} ${api.path}': {${guardComment}}`)
|
|
665
|
+
}
|
|
666
|
+
lines.push(`}`)
|
|
667
|
+
lines.push(``)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Convenience type for auth user
|
|
671
|
+
const hasAuth = app.auth && app.auth.type
|
|
672
|
+
if (hasAuth) {
|
|
673
|
+
const userModel = (app.models || []).find(m => m.name === 'User' || m.name === 'user')
|
|
674
|
+
if (userModel) {
|
|
675
|
+
lines.push(`// ── Auth types ───────────────────────────────────────────────`)
|
|
676
|
+
lines.push(`export type AuthUser = Pick<User, 'id' | 'email'${(userModel.fields||[]).some(f=>f.name==='role')?" | 'role'":''}> & { type: 'access' | 'refresh', iat: number, exp: number }`)
|
|
677
|
+
lines.push(`declare global { namespace Express { interface Request { user?: AuthUser } } }`)
|
|
678
|
+
lines.push(``)
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
lines.push(`// ── aiplang version ──────────────────────────────────────────`)
|
|
683
|
+
lines.push(`export const AIPLANG_VERSION = '2.11.3'`)
|
|
684
|
+
lines.push(``)
|
|
685
|
+
return lines.join('\n')
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function _cap(s) { return s ? s[0].toUpperCase() + s.slice(1) : s }
|
|
689
|
+
|
|
690
|
+
|
|
454
691
|
function validateAipSrc(source) {
|
|
455
692
|
const errors = []
|
|
456
693
|
const lines = source.split('\n')
|
|
@@ -490,6 +727,34 @@ function validateAipSrc(source) {
|
|
|
490
727
|
return errors
|
|
491
728
|
}
|
|
492
729
|
|
|
730
|
+
if (cmd==='types'||cmd==='type'||cmd==='dts') {
|
|
731
|
+
const file = args[0]
|
|
732
|
+
if (!file) { console.error('\n Usage: aiplang types <app.aip> [--out types.d.ts]\n'); process.exit(1) }
|
|
733
|
+
if (!require('fs').existsSync(file)) { console.error(`\n ✗ Arquivo não encontrado: ${file}\n`); process.exit(1) }
|
|
734
|
+
const src = require('fs').readFileSync(file,'utf8')
|
|
735
|
+
const errs = validateAipSrc(src)
|
|
736
|
+
if (errs.some(e => e.severity === 'error')) {
|
|
737
|
+
console.error('\n ✗ Corrija os erros antes de gerar tipos:\n')
|
|
738
|
+
errs.filter(e=>e.severity==='error').forEach(e=>console.error(` Line ${e.line}: ${e.message}`))
|
|
739
|
+
process.exit(1)
|
|
740
|
+
}
|
|
741
|
+
const serverPath = require('path').join(__dirname,'../server/server.js')
|
|
742
|
+
// Parse models and routes from .aip source for type generation
|
|
743
|
+
// Lightweight inline parser — no DB, no server init required
|
|
744
|
+
const app = _parseForTypes(src)
|
|
745
|
+
const _outIdx = args.indexOf('--out')
|
|
746
|
+
const outFile = (_outIdx >= 0 && args[_outIdx+1]) ? args[_outIdx+1] : file.replace(/\.aip$/,'') + '.d.ts'
|
|
747
|
+
const dts = generateTypes(app, require('path').basename(file))
|
|
748
|
+
require('fs').writeFileSync(outFile, dts)
|
|
749
|
+
console.log(`\n ✅ Tipos gerados: ${outFile}`)
|
|
750
|
+
console.log(` ${(app.models||[]).length} models · ${(app.apis||[]).length} routes\n`)
|
|
751
|
+
// Also show preview
|
|
752
|
+
const preview = dts.split('\n').slice(0,30).join('\n')
|
|
753
|
+
console.log(preview)
|
|
754
|
+
if (dts.split('\n').length > 30) console.log(' ...')
|
|
755
|
+
process.exit(0)
|
|
756
|
+
}
|
|
757
|
+
|
|
493
758
|
if (cmd==='validate'||cmd==='check'||cmd==='lint') {
|
|
494
759
|
const file = args[0]
|
|
495
760
|
if (!file) { console.error('\n Usage: aiplang validate <app.aip>\n'); process.exit(1) }
|
package/package.json
CHANGED
package/server/server.js
CHANGED
|
@@ -2246,7 +2246,7 @@ async function startServer(aipFile, port = 3000) {
|
|
|
2246
2246
|
})
|
|
2247
2247
|
|
|
2248
2248
|
srv.addRoute('GET', '/health', (req, res) => res.json(200, {
|
|
2249
|
-
status:'ok', version:'2.11.
|
|
2249
|
+
status:'ok', version:'2.11.3',
|
|
2250
2250
|
models: app.models.map(m=>m.name),
|
|
2251
2251
|
routes: app.apis.length, pages: app.pages.length,
|
|
2252
2252
|
admin: app.admin?.prefix || null,
|