aiplang 2.11.3 → 2.11.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/aiplang.js CHANGED
@@ -5,7 +5,7 @@ const fs = require('fs')
5
5
  const path = require('path')
6
6
  const http = require('http')
7
7
 
8
- const VERSION = '2.11.3'
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)
@@ -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.3'`)
689
+ lines.push(`export const AIPLANG_VERSION = '2.11.4'`)
684
690
  lines.push(``)
685
691
  return lines.join('\n')
686
692
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiplang",
3
- "version": "2.11.3",
3
+ "version": "2.11.4",
4
4
  "description": "AI-first web language. One .aip file = complete app. Frontend + backend + database + auth.",
5
5
  "keywords": [
6
6
  "aiplang",
@@ -43,12 +43,18 @@
43
43
  "@sqlite.org/sqlite-wasm": "^3.51.2-build8",
44
44
  "bcryptjs": "^2.4.3",
45
45
  "better-sqlite3": "^12.8.0",
46
+ "ioredis": "^5.3.2",
46
47
  "jsonwebtoken": "^9.0.2",
48
+ "mongodb": "^6.5.0",
49
+ "mysql2": "^3.9.0",
47
50
  "nodemailer": "^8.0.3",
48
51
  "pg": "^8.11.0",
49
52
  "sql.js": "^1.10.3",
50
53
  "stripe": "^14.0.0",
51
54
  "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.44.0",
52
55
  "ws": "^8.16.0"
56
+ },
57
+ "optionalDependencies": {
58
+ "better-sqlite3": "^9.4.0"
53
59
  }
54
60
  }
package/server/server.js CHANGED
@@ -32,6 +32,12 @@ let SQL, DB_FILE, _db = null
32
32
  let _pgPool = null // PostgreSQL connection pool
33
33
  let _dbDriver = 'sqlite' // 'sqlite' | 'postgres'
34
34
  let _useBetter = false // true when better-sqlite3 is available
35
+ let _mysqlPool = null // MySQL/MariaDB (mysql2)
36
+ let _mongoClient= null // MongoDB client
37
+ let _mongoDB = null // MongoDB database
38
+ let _redisClient= null // Redis (ioredis)
39
+ let _useMongo = false
40
+ let _useRedis = false
35
41
 
36
42
  async function getDB(dbConfig = { driver: 'sqlite', dsn: ':memory:' }) {
37
43
  if (_db || _pgPool) return _db || _pgPool
@@ -39,16 +45,72 @@ async function getDB(dbConfig = { driver: 'sqlite', dsn: ':memory:' }) {
39
45
  const dsn = dbConfig.dsn || ':memory:'
40
46
  _dbDriver = driver
41
47
 
42
- if (driver === 'postgres' || driver === 'postgresql' || dsn.startsWith('postgres')) {
48
+ // ── PostgreSQL ────────────────────────────────────────────────
49
+ if (driver === 'postgres' || dsn.startsWith('postgres')) {
43
50
  try {
44
51
  const { Pool } = require('pg')
45
52
  _pgPool = new Pool({ connectionString: dsn, ssl: dsn.includes('ssl=true') ? { rejectUnauthorized: false } : false })
46
- await _pgPool.query('SELECT 1') // test connection
53
+ await _pgPool.query('SELECT 1')
47
54
  console.log('[aiplang] DB: PostgreSQL ✓')
48
55
  return _pgPool
49
56
  } catch (e) {
50
- console.error('[aiplang] PostgreSQL connection failed:', e.message)
51
- console.log('[aiplang] Falling back to SQLite :memory:')
57
+ console.error('[aiplang] PostgreSQL falhou:', e.message)
58
+ _dbDriver = 'sqlite'
59
+ }
60
+ }
61
+
62
+ // ── MySQL / MariaDB ───────────────────────────────────────────
63
+ if (driver === 'mysql' || dsn.startsWith('mysql') || dsn.startsWith('mariadb')) {
64
+ try {
65
+ const mysql = require('mysql2/promise')
66
+ _mysqlPool = await mysql.createPool({
67
+ uri: dsn.startsWith('mariadb') ? dsn.replace('mariadb://', 'mysql://') : dsn,
68
+ waitForConnections: true,
69
+ connectionLimit: 10,
70
+ queueLimit: 0,
71
+ ssl: dsn.includes('ssl=true') ? { rejectUnauthorized: false } : undefined
72
+ })
73
+ await _mysqlPool.query('SELECT 1')
74
+ _dbDriver = 'mysql'
75
+ console.log('[aiplang] DB: MySQL/MariaDB ✓')
76
+ return _mysqlPool
77
+ } catch (e) {
78
+ console.error('[aiplang] MySQL falhou:', e.message)
79
+ _dbDriver = 'sqlite'
80
+ }
81
+ }
82
+
83
+ // ── MongoDB ───────────────────────────────────────────────────
84
+ if (driver === 'mongodb' || dsn.startsWith('mongodb')) {
85
+ try {
86
+ const { MongoClient } = require('mongodb')
87
+ _mongoClient = new MongoClient(dsn)
88
+ await _mongoClient.connect()
89
+ // Extrair nome do banco da URL
90
+ const dbName = dsn.split('/').pop()?.split('?')[0] || 'aiplang'
91
+ _mongoDB = _mongoClient.db(dbName)
92
+ _useMongo = true
93
+ _dbDriver = 'mongodb'
94
+ console.log('[aiplang] DB: MongoDB ✓ (' + dbName + ')')
95
+ return _mongoDB
96
+ } catch (e) {
97
+ console.error('[aiplang] MongoDB falhou:', e.message)
98
+ _dbDriver = 'sqlite'
99
+ }
100
+ }
101
+
102
+ // ── Redis ─────────────────────────────────────────────────────
103
+ if (driver === 'redis' || dsn.startsWith('redis')) {
104
+ try {
105
+ const Redis = require('ioredis')
106
+ _redisClient = new Redis(dsn, { lazyConnect: false, maxRetriesPerRequest: 3 })
107
+ await _redisClient.ping()
108
+ _useRedis = true
109
+ _dbDriver = 'redis'
110
+ console.log('[aiplang] DB: Redis ✓')
111
+ return _redisClient
112
+ } catch (e) {
113
+ console.error('[aiplang] Redis falhou:', e.message)
52
114
  _dbDriver = 'sqlite'
53
115
  }
54
116
  }
@@ -89,7 +151,48 @@ function persistDB() {
89
151
  }
90
152
  let _dirty = false, _persistTimer = null
91
153
 
154
+ // ── MongoDB helpers ───────────────────────────────────────────────
155
+ function _sqlWhereToMongo(sql, params) {
156
+ // Converte WHERE simples: id = ? / email = ? → {id: val, email: val}
157
+ const filter = {}
158
+ const whereMatch = sql.match(/WHERE\s+(.+?)(?:ORDER|LIMIT|$)/is)
159
+ if (!whereMatch) return filter
160
+ const conditions = whereMatch[1].trim()
161
+ const parts = conditions.split(/\s+AND\s+/i)
162
+ let paramIdx = 0
163
+ for (const part of parts) {
164
+ const m = part.trim().match(/^(\w+)\s*=\s*\?$/)
165
+ if (m && paramIdx < params.length) {
166
+ filter[m[1]] = params[paramIdx++]
167
+ }
168
+ }
169
+ return filter
170
+ }
171
+
172
+ function _mongoToObj(doc) {
173
+ if (!doc) return null
174
+ const { _id, ...rest } = doc
175
+ return { id: _id?.toString() || rest.id, ...rest }
176
+ }
177
+
178
+ // ── MySQL: CREATE TABLE equivalente ──────────────────────────────
179
+ function _mysqlType(sqliteType) {
180
+ return {
181
+ 'TEXT': 'TEXT',
182
+ 'INTEGER': 'INT',
183
+ 'REAL': 'DOUBLE',
184
+ 'BLOB': 'BLOB'
185
+ }[sqliteType] || 'TEXT'
186
+ }
187
+
188
+
92
189
  function dbRun(sql, params = []) {
190
+ if (_mysqlPool) {
191
+ // MySQL: async, mas dbRun é chamado sync em alguns contextos
192
+ // Enfileirar de forma segura
193
+ _mysqlPool.execute(sql, params).catch(e => console.debug('[aiplang:mysql]', e?.message))
194
+ return
195
+ }
93
196
  if (_useBetter && !_pgPool) {
94
197
  // better-sqlite3: synchronous, native — 30x faster than sql.js writes
95
198
  _getStmt(sql).run(...params)
@@ -117,7 +220,8 @@ function convertPlaceholders(sql) {
117
220
  }
118
221
 
119
222
  async function dbRunAsync(sql, params = []) {
120
- if (_pgPool) return _pgPool.query(convertPlaceholders(sql), params)
223
+ if (_mysqlPool) return _mysqlPool.execute(sql, params)
224
+ if (_pgPool) return _pgPool.query(convertPlaceholders(sql), params)
121
225
  dbRun(sql, params)
122
226
  }
123
227
 
@@ -181,18 +285,18 @@ function _getStmt(sql) {
181
285
  }
182
286
 
183
287
  function dbAll(sql, params = [], _cacheKey = null, _cacheTables = null) {
184
- if (_pgPool) return []
288
+ if (_pgPool) return [] // PostgreSQL usa dbAllAsync
289
+ if (_mysqlPool) return [] // MySQL usa dbAllAsync
290
+ if (_useMongo) return [] // MongoDB usa dbAllAsync
185
291
  if (_cacheKey && !params.length) {
186
292
  const cached = _cacheGet(_cacheKey)
187
293
  if (cached !== null) return { __cached: true, __body: cached }
188
294
  }
189
295
  let rows
190
296
  if (_useBetter) {
191
- // better-sqlite3: synchronous, native, 7x faster
192
297
  const stmt = _getStmt(sql)
193
298
  rows = params.length ? stmt.all(...params) : stmt.all()
194
299
  } else {
195
- // sql.js fallback
196
300
  const stmt = _db.prepare(sql)
197
301
  stmt.bind(params); rows = []
198
302
  while (stmt.step()) rows.push(stmt.getAsObject())
@@ -202,12 +306,33 @@ function dbAll(sql, params = [], _cacheKey = null, _cacheTables = null) {
202
306
  return rows
203
307
  }
204
308
 
205
- async function dbAllAsync(sql, params = []) {
309
+ async function dbAllAsync(sql, params = [], _cacheKey = null, _cacheTables = null) {
310
+ // Cache check (universal)
311
+ if (_cacheKey && !params.length) {
312
+ const cached = _cacheGet(_cacheKey)
313
+ if (cached !== null) return { __cached: true, __body: cached }
314
+ }
315
+ let rows
206
316
  if (_pgPool) {
207
317
  const r = await _pgPool.query(convertPlaceholders(sql), params)
208
- return r.rows
318
+ rows = r.rows
319
+ } else if (_mysqlPool) {
320
+ const [result] = await _mysqlPool.execute(sql, params)
321
+ rows = result
322
+ } else if (_useMongo) {
323
+ // MongoDB: extrair collection do SQL "SELECT * FROM table WHERE..."
324
+ const tableMatch = sql.match(/FROM\s+(\w+)/i)
325
+ const collection = tableMatch ? tableMatch[1] : null
326
+ if (!collection) return []
327
+ const filter = _sqlWhereToMongo(sql, params)
328
+ rows = await _mongoDB.collection(collection).find(filter).toArray()
329
+ } else {
330
+ rows = dbAll(sql, params)
331
+ }
332
+ if (Array.isArray(rows) && _cacheKey && !params.length) {
333
+ _cacheSet(_cacheKey, JSON.stringify(rows), _cacheTables)
209
334
  }
210
- return dbAll(sql, params)
335
+ return rows
211
336
  }
212
337
 
213
338
  function dbGet(sql, params = []) { return dbAll(sql, params)[0] || null }
@@ -460,9 +585,15 @@ function validateAip(source) {
460
585
  }
461
586
 
462
587
  function cacheSet(key, value, ttlMs = 60000) {
588
+ // Redis: persistir em redis além do cache em memória
589
+ if (_useRedis) {
590
+ try { _redisClient.set('aip:' + key, JSON.stringify(value), 'PX', ttlMs).catch(() => {}) } catch {}
591
+ }
463
592
  _cache.set(key, { value, expires: Date.now() + ttlMs })
464
593
  }
465
594
  function cacheGet(key) {
595
+ // Redis: verificar redis se não tiver na memória
596
+ // (retorna null para busca assíncrona — o caller deve usar cacheGetAsync se precisar)
466
597
  const item = _cache.get(key)
467
598
  if (!item) return null
468
599
  if (item.expires < Date.now()) { _cache.delete(key); return null }
@@ -621,6 +752,19 @@ class Model {
621
752
 
622
753
  // ── Core queries ────────────────────────────────────────────────
623
754
  all(opts = {}) {
755
+ // MongoDB
756
+ if (_useMongo) {
757
+ const filter = this.softDelete ? { deleted_at: { $exists: false } } : {}
758
+ const mongoOpts = {}
759
+ if (opts.limit) mongoOpts.limit = parseInt(opts.limit)
760
+ if (opts.offset) mongoOpts.skip = parseInt(opts.offset)
761
+ if (opts.order) {
762
+ const p = String(opts.order).trim().split(/\s+/)
763
+ mongoOpts.sort = { [p[0]]: p[1]?.toLowerCase()==='desc' ? -1 : 1 }
764
+ }
765
+ return _mongoDB.collection(this.tableName).find(filter, mongoOpts).toArray()
766
+ .then(docs => docs.map(d => { const { _id, ...rest } = d; return { id: _id?.toString(), ...rest } }))
767
+ }
624
768
  let sql = `SELECT * FROM ${this.tableName}`
625
769
  const params = [], conditions = []
626
770
  if (this.softDelete) conditions.push('deleted_at IS NULL')
@@ -684,6 +828,11 @@ class Model {
684
828
  if (!row.updated_at) row.updated_at = now()
685
829
  }
686
830
  const keys = Object.keys(row), vals = Object.values(row)
831
+ // MongoDB: usar insertOne
832
+ if (_useMongo) {
833
+ _mongoDB.collection(this.tableName).insertOne({ ...row }).catch(e => console.debug('[aiplang:mongo]', e?.message))
834
+ return row
835
+ }
687
836
  dbRun(`INSERT INTO ${this.tableName} (${keys.join(',')}) VALUES (${keys.map(()=>'?').join(',')})`, vals)
688
837
  emit(`${this.modelName}.created`, row)
689
838
  return row
@@ -767,13 +916,39 @@ class Model {
767
916
  // MIGRATION
768
917
  // ═══════════════════════════════════════════════════════════════════
769
918
  function migrateModels(models) {
919
+ // ── Seleção de mapa de tipos por banco ───────────────────────────
920
+ const _sqliteTypes = { uuid:'TEXT',int:'INTEGER',integer:'INTEGER',float:'REAL',bool:'INTEGER',timestamp:'TEXT',date:'TEXT',json:'TEXT',enum:'TEXT',text:'TEXT',email:'TEXT',url:'TEXT',phone:'TEXT' }
921
+ const _mysqlTypes = { uuid:'VARCHAR(36)',int:'INT',integer:'INT',float:'DOUBLE',bool:'TINYINT(1)',timestamp:'DATETIME',date:'DATE',json:'JSON',enum:'TEXT',text:'TEXT',email:'VARCHAR(255)',url:'TEXT',phone:'VARCHAR(20)' }
922
+ const _pgTypes = { uuid:'UUID',int:'INTEGER',integer:'INTEGER',float:'DOUBLE PRECISION',bool:'BOOLEAN',timestamp:'TIMESTAMPTZ',date:'DATE',json:'JSONB',enum:'TEXT',text:'TEXT',email:'TEXT',url:'TEXT',phone:'TEXT' }
923
+ const typeMap = _mysqlPool ? _mysqlTypes : (_pgPool ? _pgTypes : _sqliteTypes)
924
+
770
925
  for (const model of models) {
771
926
  const table = toTable(model.name)
927
+
928
+ // ── MongoDB: criar collection com índices ──────────────────────
929
+ if (_useMongo) {
930
+ _mongoDB.collection(table) // cria collection implicitamente
931
+ for (const f of model.fields) {
932
+ const colName = toCol(f.name)
933
+ if (f.modifiers.includes('unique')) {
934
+ _mongoDB.collection(table).createIndex({ [colName]: 1 }, { unique: true }).catch(() => {})
935
+ }
936
+ }
937
+ console.log(`[aiplang] ✓ ${table} (MongoDB)`)
938
+ continue
939
+ }
940
+
772
941
  const cols = []
773
942
  for (const f of model.fields) {
774
- let sqlType = { uuid:'TEXT',int:'INTEGER',integer:'INTEGER',float:'REAL',bool:'INTEGER',timestamp:'TEXT',date:'TEXT',json:'TEXT',enum:'TEXT',text:'TEXT',email:'TEXT',url:'TEXT',phone:'TEXT' }[f.type] || 'TEXT'
943
+ let sqlType = typeMap[f.type] || 'TEXT'
775
944
  let def = `${toCol(f.name)} ${sqlType}`
776
- if (f.modifiers.includes('pk')) def += ' PRIMARY KEY'
945
+ if (f.modifiers.includes('pk')) {
946
+ if (_mysqlPool) def += ' PRIMARY KEY'
947
+ else def += ' PRIMARY KEY'
948
+ }
949
+ if (f.modifiers.includes('auto') && f.type !== 'uuid') {
950
+ if (_mysqlPool) def += ' AUTO_INCREMENT'
951
+ }
777
952
  if (f.modifiers.includes('required')) def += ' NOT NULL'
778
953
  if (f.modifiers.includes('unique')) def += ' UNIQUE'
779
954
  if (f.default !== null) def += ` DEFAULT '${f.default}'`
@@ -905,7 +1080,18 @@ function parseApp(src) {
905
1080
  }
906
1081
 
907
1082
  function parseEnvLine(s) { const p=s.split(/\s+/); const ev={name:'',required:false,default:null}; for(const x of p){if(x==='required')ev.required=true;else if(x.includes('=')){const[k,v]=x.split('=');ev.name=k;ev.default=v}else ev.name=x}; return ev }
908
- function parseDBLine(s) { const p=s.split(/\s+/); const d=p[0]||'sqlite'; return{driver:d==='pg'||d==='psql'?'postgres':d,dsn:p[1]||'./app.db'} }
1083
+ function parseDBLine(s) {
1084
+ const p = s.split(/\s+/)
1085
+ let d = (p[0]||'sqlite').toLowerCase()
1086
+ // Normalizar aliases
1087
+ if (d==='pg'||d==='psql'||d==='postgresql') d='postgres'
1088
+ if (d==='mariadb') d='mysql'
1089
+ if (d==='mongo') d='mongodb'
1090
+ if (d==='redis'||d==='cache') d='redis'
1091
+ if (d==='sqlite3') d='sqlite'
1092
+ const dsn = p[1] || (d==='sqlite'?'./app.db':d==='redis'?'redis://localhost:6379':'')
1093
+ return { driver:d, dsn }
1094
+ }
909
1095
  function parseAuthLine(s) { const p=s.split(/\s+/); const a={provider:'jwt',secret:p[1]||'$JWT_SECRET',expire:'7d',refresh:'30d'}; for(const x of p){if(x.startsWith('expire='))a.expire=x.slice(7);if(x.startsWith('refresh='))a.refresh=x.slice(8);if(x==='google')a.oauth=['google'];if(x==='github')a.oauth=[...(a.oauth||[]),'google']}; return a }
910
1096
  function parseMailLine(s) { const parts=s.split(/\s+/); const m={driver:parts[0]||'smtp'}; for(const x of parts.slice(1)){const[k,v]=x.split('='); m[k]=v}; return m }
911
1097
  function parseStripeLine(s) {
@@ -2246,7 +2432,7 @@ async function startServer(aipFile, port = 3000) {
2246
2432
  })
2247
2433
 
2248
2434
  srv.addRoute('GET', '/health', (req, res) => res.json(200, {
2249
- status:'ok', version:'2.11.3',
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,