outlet-orm 7.0.0 → 9.0.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/README.md +130 -2
- package/package.json +1 -1
- package/src/AI/AIPromptEnhancer.js +170 -0
- package/src/AI/AIQueryBuilder.js +234 -0
- package/src/AI/AIQueryOptimizer.js +185 -0
- package/src/AI/AISeeder.js +181 -0
- package/src/AI/AiBridgeManager.js +287 -0
- package/src/AI/Builders/TextBuilder.js +170 -0
- package/src/AI/Contracts/AudioProviderContract.js +29 -0
- package/src/AI/Contracts/ChatProviderContract.js +38 -0
- package/src/AI/Contracts/EmbeddingsProviderContract.js +19 -0
- package/src/AI/Contracts/ImageProviderContract.js +19 -0
- package/src/AI/Contracts/ModelsProviderContract.js +26 -0
- package/src/AI/Contracts/ToolContract.js +25 -0
- package/src/AI/Facades/AiBridge.js +79 -0
- package/src/AI/MCPServer.js +113 -0
- package/src/AI/Providers/ClaudeProvider.js +64 -0
- package/src/AI/Providers/CustomOpenAIProvider.js +238 -0
- package/src/AI/Providers/GeminiProvider.js +68 -0
- package/src/AI/Providers/GrokProvider.js +46 -0
- package/src/AI/Providers/MistralProvider.js +21 -0
- package/src/AI/Providers/OllamaProvider.js +249 -0
- package/src/AI/Providers/OllamaTurboProvider.js +32 -0
- package/src/AI/Providers/OnnProvider.js +46 -0
- package/src/AI/Providers/OpenAIProvider.js +471 -0
- package/src/AI/Support/AudioNormalizer.js +37 -0
- package/src/AI/Support/ChatNormalizer.js +42 -0
- package/src/AI/Support/Document.js +77 -0
- package/src/AI/Support/DocumentAttachmentMapper.js +101 -0
- package/src/AI/Support/EmbeddingsNormalizer.js +30 -0
- package/src/AI/Support/Exceptions/ProviderError.js +22 -0
- package/src/AI/Support/FileSecurity.js +56 -0
- package/src/AI/Support/ImageNormalizer.js +62 -0
- package/src/AI/Support/JsonSchemaValidator.js +73 -0
- package/src/AI/Support/Message.js +40 -0
- package/src/AI/Support/StreamChunk.js +45 -0
- package/src/AI/Support/ToolChatRunner.js +160 -0
- package/src/AI/Support/ToolRegistry.js +62 -0
- package/src/AI/Tools/SystemInfoTool.js +25 -0
- package/src/index.js +67 -1
- package/types/index.d.ts +326 -0
package/README.md
CHANGED
|
@@ -197,7 +197,14 @@ async store(req, res) {
|
|
|
197
197
|
- **Raw queries**: `executeRawQuery()` and `execute()` (native driver results)
|
|
198
198
|
- **Complete Migrations** (create/alter/drop, index, foreign keys, batch tracking)
|
|
199
199
|
- **Database Backup** (v6.0.0): full/partial/journal backups, recurring scheduler, AES-256-GCM encryption, TCP daemon + remote client, automatic restore
|
|
200
|
-
- **
|
|
200
|
+
- **🤖 AiBridge** (v8.0.0): Multi-provider LLM abstraction — chat, stream, embeddings, images, TTS, STT with 9+ providers
|
|
201
|
+
- **🤖 AI Query Builder** (v8.0.0): Natural language → SQL with schema introspection
|
|
202
|
+
- **🤖 AI Seeder** (v8.0.0): LLM-powered realistic, domain-specific data generation
|
|
203
|
+
- **🤖 AI Query Optimizer** (v8.0.0): SQL analysis, optimization, and index recommendations
|
|
204
|
+
- **🤖 AI Prompt Enhancer** (v8.0.0): Schema/model/migration generation from natural language
|
|
205
|
+
- **🤖 MCP Server** (v7.0.0): Model Context Protocol for AI agent integration (13 tools)
|
|
206
|
+
- **🤖 AI Safety Guardrails** (v7.0.0): Automatic AI agent detection + destructive operation protection
|
|
207
|
+
- **Handy CLI tools**: `outlet-init`, `outlet-migrate`, `outlet-convert`, `outlet-mcp`
|
|
201
208
|
- **`.env` configuration** (loaded automatically)
|
|
202
209
|
- **Multi-database**: MySQL, PostgreSQL, and SQLite
|
|
203
210
|
- **Complete TypeScript types** with Generic Model and typed Schema Builder (v4.0.0+)
|
|
@@ -1220,6 +1227,126 @@ outlet-convert
|
|
|
1220
1227
|
- ✅ Automatic timestamps support
|
|
1221
1228
|
- ✅ Class names converted to PascalCase
|
|
1222
1229
|
|
|
1230
|
+
## 🤖 AI Integration
|
|
1231
|
+
|
|
1232
|
+
Outlet ORM includes a complete AI subsystem with multi-provider LLM support and ORM-specific AI features.
|
|
1233
|
+
|
|
1234
|
+
📚 **[Complete AI documentation available in `/docs`](./docs/AI_BRIDGE.md)**
|
|
1235
|
+
|
|
1236
|
+
### AiBridge — Multi-Provider LLM Abstraction
|
|
1237
|
+
|
|
1238
|
+
```javascript
|
|
1239
|
+
const { AiBridgeManager } = require('outlet-orm');
|
|
1240
|
+
|
|
1241
|
+
const ai = new AiBridgeManager({
|
|
1242
|
+
providers: {
|
|
1243
|
+
openai: { api_key: process.env.OPENAI_API_KEY, model: 'gpt-4o' },
|
|
1244
|
+
claude: { api_key: process.env.ANTHROPIC_API_KEY, model: 'claude-sonnet-4-20250514' },
|
|
1245
|
+
ollama: { endpoint: 'http://localhost:11434', model: 'llama3' }
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
// Chat with any provider
|
|
1250
|
+
const response = await ai.chat('openai', [
|
|
1251
|
+
{ role: 'user', content: 'What is Node.js?' }
|
|
1252
|
+
]);
|
|
1253
|
+
|
|
1254
|
+
// Fluent TextBuilder
|
|
1255
|
+
const { text } = await ai.text()
|
|
1256
|
+
.using('openai', 'gpt-4o')
|
|
1257
|
+
.withSystemPrompt('You are a helpful assistant.')
|
|
1258
|
+
.withPrompt('Explain closures in JavaScript.')
|
|
1259
|
+
.asText();
|
|
1260
|
+
|
|
1261
|
+
// Stream responses
|
|
1262
|
+
for await (const chunk of ai.stream('claude', messages)) {
|
|
1263
|
+
process.stdout.write(chunk.text || '');
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// Embeddings, images, TTS, STT
|
|
1267
|
+
const embeddings = await ai.embeddings('openai', ['Hello world']);
|
|
1268
|
+
const image = await ai.image('openai', 'A sunset over mountains');
|
|
1269
|
+
```
|
|
1270
|
+
|
|
1271
|
+
**Supported providers**: OpenAI, Claude, Gemini, Ollama, Grok, Mistral, ONN, Custom OpenAI, OpenRouter
|
|
1272
|
+
|
|
1273
|
+
### AI Query Builder — Natural Language → SQL
|
|
1274
|
+
|
|
1275
|
+
```javascript
|
|
1276
|
+
const { AIQueryBuilder } = require('outlet-orm');
|
|
1277
|
+
|
|
1278
|
+
const qb = new AIQueryBuilder(ai, db);
|
|
1279
|
+
|
|
1280
|
+
// Ask in natural language, get SQL + results
|
|
1281
|
+
const result = await qb.query('How many users signed up last month?');
|
|
1282
|
+
console.log(result.sql); // SELECT COUNT(*) FROM users WHERE ...
|
|
1283
|
+
console.log(result.results); // [{ count: 42 }]
|
|
1284
|
+
|
|
1285
|
+
// Generate SQL without executing
|
|
1286
|
+
const { sql } = await qb.toSql('Show me the top 5 users by post count');
|
|
1287
|
+
```
|
|
1288
|
+
|
|
1289
|
+
### AI Seeder — Realistic Data Generation
|
|
1290
|
+
|
|
1291
|
+
```javascript
|
|
1292
|
+
const { AISeeder } = require('outlet-orm');
|
|
1293
|
+
|
|
1294
|
+
const seeder = new AISeeder(ai, db);
|
|
1295
|
+
|
|
1296
|
+
// Generate and insert realistic data
|
|
1297
|
+
await seeder.seed('products', 20, {
|
|
1298
|
+
domain: 'e-commerce',
|
|
1299
|
+
locale: 'fr_FR',
|
|
1300
|
+
description: 'Fashion store for young adults'
|
|
1301
|
+
});
|
|
1302
|
+
```
|
|
1303
|
+
|
|
1304
|
+
### AI Query Optimizer
|
|
1305
|
+
|
|
1306
|
+
```javascript
|
|
1307
|
+
const { AIQueryOptimizer } = require('outlet-orm');
|
|
1308
|
+
|
|
1309
|
+
const optimizer = new AIQueryOptimizer(ai, db);
|
|
1310
|
+
const result = await optimizer.optimize(
|
|
1311
|
+
'SELECT * FROM orders WHERE user_id IN (SELECT id FROM users WHERE status = "active")'
|
|
1312
|
+
);
|
|
1313
|
+
|
|
1314
|
+
console.log(result.optimized); // Rewritten SQL
|
|
1315
|
+
console.log(result.suggestions); // [{ type: 'index', impact: 'high', ... }]
|
|
1316
|
+
console.log(result.indexes); // ['CREATE INDEX idx_...']
|
|
1317
|
+
```
|
|
1318
|
+
|
|
1319
|
+
### MCP Server — AI Agent Integration
|
|
1320
|
+
|
|
1321
|
+
```bash
|
|
1322
|
+
# Start MCP server for AI editors
|
|
1323
|
+
npx outlet-mcp
|
|
1324
|
+
```
|
|
1325
|
+
|
|
1326
|
+
Configure your AI editor:
|
|
1327
|
+
|
|
1328
|
+
```json
|
|
1329
|
+
{
|
|
1330
|
+
"mcpServers": {
|
|
1331
|
+
"outlet-orm": {
|
|
1332
|
+
"command": "npx",
|
|
1333
|
+
"args": ["outlet-mcp"]
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
```
|
|
1338
|
+
|
|
1339
|
+
**13 MCP tools**: migrations, schema introspection, queries, seeds, backups, AI query, query optimization
|
|
1340
|
+
|
|
1341
|
+
📖 Full documentation:
|
|
1342
|
+
- [AiBridge Manager](docs/AI_BRIDGE.md) — Multi-provider LLM abstraction
|
|
1343
|
+
- [AI Query Builder](docs/AI_QUERY.md) — Natural language to SQL
|
|
1344
|
+
- [AI Seeder](docs/AI_SEEDER.md) — Realistic data generation
|
|
1345
|
+
- [AI Query Optimizer](docs/AI_OPTIMIZER.md) — SQL optimization
|
|
1346
|
+
- [AI Prompt Enhancer](docs/AI_PROMPT.md) — Schema/code generation
|
|
1347
|
+
- [MCP Server](docs/MCP.md) — AI agent integration
|
|
1348
|
+
- [AI Safety Guardrails](docs/AI_SAFETY.md) — Destructive operation protection
|
|
1349
|
+
|
|
1223
1350
|
## 📚 Documentation
|
|
1224
1351
|
|
|
1225
1352
|
- [Migrations Guide](docs/MIGRATIONS.md)
|
|
@@ -1227,7 +1354,8 @@ outlet-convert
|
|
|
1227
1354
|
- [Relation Detection](docs/RELATIONS_DETECTION.md)
|
|
1228
1355
|
- [Quick Start Guide](docs/QUICKSTART.md)
|
|
1229
1356
|
- [Architecture](docs/ARCHITECTURE.md)
|
|
1230
|
-
- [**TypeScript (
|
|
1357
|
+
- [**TypeScript (complete)**](docs/TYPESCRIPT.md)
|
|
1358
|
+
- [**AI Integration (complete)**](docs/AI_BRIDGE.md)
|
|
1231
1359
|
|
|
1232
1360
|
## 📘 TypeScript Support
|
|
1233
1361
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AIPromptEnhancer
|
|
5
|
+
* LLM-powered enhancement of the existing regex-based PromptGenerator.
|
|
6
|
+
* Takes a natural language description and uses AI to generate richer schemas,
|
|
7
|
+
* model code, migrations, and seeders — beyond what pattern matching can do.
|
|
8
|
+
*
|
|
9
|
+
* @since 8.0.0
|
|
10
|
+
*/
|
|
11
|
+
class AIPromptEnhancer {
|
|
12
|
+
/**
|
|
13
|
+
* @param {import('./AiBridgeManager')} manager
|
|
14
|
+
*/
|
|
15
|
+
constructor(manager) {
|
|
16
|
+
this._manager = manager;
|
|
17
|
+
this._provider = 'openai';
|
|
18
|
+
this._model = 'gpt-4o-mini';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} provider
|
|
23
|
+
* @param {string} model
|
|
24
|
+
* @returns {this}
|
|
25
|
+
*/
|
|
26
|
+
using(provider, model) {
|
|
27
|
+
this._provider = provider;
|
|
28
|
+
this._model = model;
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Generate a full project schema from a natural language description.
|
|
34
|
+
* Returns tables, columns, relations, and seed hints.
|
|
35
|
+
* @param {string} description - e.g., "A recipe sharing app with users, recipes, ingredients, and reviews"
|
|
36
|
+
* @param {Object} [options={}]
|
|
37
|
+
* @returns {Promise<{tables: Object, relations: Array, seedHints: Object}>}
|
|
38
|
+
*/
|
|
39
|
+
async generateSchema(description, options = {}) {
|
|
40
|
+
const systemPrompt = `You are an expert database architect. Given an application description, design a complete relational database schema.
|
|
41
|
+
|
|
42
|
+
RULES:
|
|
43
|
+
- Return a JSON object with:
|
|
44
|
+
"tables" — object where each key is a table name, value is an object with "columns" (array of "name:type:modifiers" strings, using outlet-orm format like "name:string", "email:string:unique", "user_id:foreignId", "content:text:nullable", "price:decimal(10,2)", "status:string:default(active)").
|
|
45
|
+
"relations" — array of objects with "type" (hasOne, hasMany, belongsTo, belongsToMany), "from", "to", and optionally "pivot" table name.
|
|
46
|
+
"seedHints" — object where each key is a table name, value is a short description of the kind of seed data to generate.
|
|
47
|
+
- Always include id, created_at, updated_at columns implicitly (don't list them).
|
|
48
|
+
- Use snake_case for column/table names.
|
|
49
|
+
- Include foreign keys where appropriate.
|
|
50
|
+
- Design for at least 3rd normal form.`;
|
|
51
|
+
|
|
52
|
+
const messages = [
|
|
53
|
+
{ role: 'system', content: systemPrompt },
|
|
54
|
+
{ role: 'user', content: `Design a database schema for: ${description}` },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const res = await this._manager.chat(this._provider, messages, {
|
|
58
|
+
model: options.model || this._model,
|
|
59
|
+
temperature: 0.5,
|
|
60
|
+
max_tokens: 4096,
|
|
61
|
+
response_format: 'json',
|
|
62
|
+
json_schema: {
|
|
63
|
+
name: 'schema_design',
|
|
64
|
+
schema: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
properties: {
|
|
67
|
+
tables: { type: 'object' },
|
|
68
|
+
relations: { type: 'array', items: { type: 'object' } },
|
|
69
|
+
seedHints: { type: 'object' },
|
|
70
|
+
},
|
|
71
|
+
required: ['tables'],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return this._extractJson(res);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Generate model source code for a given table schema.
|
|
81
|
+
* @param {string} tableName
|
|
82
|
+
* @param {Object} tableSchema - { columns: [...] }
|
|
83
|
+
* @param {Array} [relations=[]]
|
|
84
|
+
* @returns {Promise<string>}
|
|
85
|
+
*/
|
|
86
|
+
async generateModelCode(tableName, tableSchema, relations = []) {
|
|
87
|
+
const systemPrompt = `You are an expert in outlet-orm (a Node.js Eloquent-inspired ORM).
|
|
88
|
+
Generate a complete model class for the given table. Use CommonJS (module.exports).
|
|
89
|
+
|
|
90
|
+
Example format:
|
|
91
|
+
const { Model } = require('outlet-orm');
|
|
92
|
+
class User extends Model {
|
|
93
|
+
static tableName = 'users';
|
|
94
|
+
static fillable = ['name', 'email', 'password'];
|
|
95
|
+
static hidden = ['password'];
|
|
96
|
+
static casts = { created_at: 'datetime' };
|
|
97
|
+
posts() { return this.hasMany('Post', 'user_id'); }
|
|
98
|
+
}
|
|
99
|
+
module.exports = User;
|
|
100
|
+
|
|
101
|
+
Include fillable, hidden (for sensitive fields), casts, and relation methods.`;
|
|
102
|
+
|
|
103
|
+
const messages = [
|
|
104
|
+
{ role: 'system', content: systemPrompt },
|
|
105
|
+
{
|
|
106
|
+
role: 'user',
|
|
107
|
+
content: `Generate an outlet-orm model for table "${tableName}":\nColumns: ${JSON.stringify(tableSchema.columns || [])}\nRelations: ${JSON.stringify(relations)}`,
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
const res = await this._manager.chat(this._provider, messages, {
|
|
112
|
+
model: this._model,
|
|
113
|
+
temperature: 0.3,
|
|
114
|
+
max_tokens: 2048,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return res?.output_text || res?.choices?.[0]?.message?.content || res?.content?.[0]?.text || '';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Generate migration code for a given table schema.
|
|
122
|
+
* @param {string} tableName
|
|
123
|
+
* @param {Object} tableSchema
|
|
124
|
+
* @returns {Promise<string>}
|
|
125
|
+
*/
|
|
126
|
+
async generateMigrationCode(tableName, tableSchema) {
|
|
127
|
+
const systemPrompt = `You are an expert in outlet-orm migrations. Generate a migration file.
|
|
128
|
+
|
|
129
|
+
Example format:
|
|
130
|
+
const { Migration, Schema } = require('outlet-orm');
|
|
131
|
+
class CreateUsersTable extends Migration {
|
|
132
|
+
async up(schema) {
|
|
133
|
+
await schema.create('users', (table) => {
|
|
134
|
+
table.increments('id');
|
|
135
|
+
table.string('name');
|
|
136
|
+
table.string('email').unique();
|
|
137
|
+
table.timestamps();
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
async down(schema) {
|
|
141
|
+
await schema.dropIfExists('users');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
module.exports = CreateUsersTable;
|
|
145
|
+
|
|
146
|
+
Use proper column types: string, text, integer, bigInteger, decimal, boolean, date, datetime, timestamp, json, foreignId.`;
|
|
147
|
+
|
|
148
|
+
const messages = [
|
|
149
|
+
{ role: 'system', content: systemPrompt },
|
|
150
|
+
{ role: 'user', content: `Generate a migration for table "${tableName}":\nColumns: ${JSON.stringify(tableSchema.columns || [])}` },
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
const res = await this._manager.chat(this._provider, messages, {
|
|
154
|
+
model: this._model,
|
|
155
|
+
temperature: 0.2,
|
|
156
|
+
max_tokens: 2048,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return res?.output_text || res?.choices?.[0]?.message?.content || res?.content?.[0]?.text || '';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** @private */
|
|
163
|
+
_extractJson(res) {
|
|
164
|
+
let content = res?.output_text || res?.choices?.[0]?.message?.content || res?.content?.[0]?.text || res?.message?.content || '';
|
|
165
|
+
if (typeof content !== 'string') content = JSON.stringify(content);
|
|
166
|
+
try { return JSON.parse(content); } catch { return { tables: {}, relations: [], seedHints: {} }; }
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = AIPromptEnhancer;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AIQueryBuilder
|
|
5
|
+
* Natural Language → SQL conversion using any AiBridge provider.
|
|
6
|
+
* Introspects the database schema, sends it with the NL prompt to an LLM,
|
|
7
|
+
* and returns a safe, parameterized SQL query.
|
|
8
|
+
*
|
|
9
|
+
* @since 8.0.0
|
|
10
|
+
*/
|
|
11
|
+
class AIQueryBuilder {
|
|
12
|
+
/**
|
|
13
|
+
* @param {import('./AiBridgeManager')} manager - AiBridge manager instance
|
|
14
|
+
* @param {Object} connection - DatabaseConnection instance (outlet-orm)
|
|
15
|
+
*/
|
|
16
|
+
constructor(manager, connection) {
|
|
17
|
+
this._manager = manager;
|
|
18
|
+
this._connection = connection;
|
|
19
|
+
this._provider = 'openai';
|
|
20
|
+
this._model = 'gpt-4o-mini';
|
|
21
|
+
this._safeMode = true; // Only SELECT by default
|
|
22
|
+
this._maxTokens = 1024;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Set the provider and model to use.
|
|
27
|
+
* @param {string} provider
|
|
28
|
+
* @param {string} model
|
|
29
|
+
* @returns {this}
|
|
30
|
+
*/
|
|
31
|
+
using(provider, model) {
|
|
32
|
+
this._provider = provider;
|
|
33
|
+
this._model = model;
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Enable/disable safe mode (SELECT only).
|
|
39
|
+
* @param {boolean} safe
|
|
40
|
+
* @returns {this}
|
|
41
|
+
*/
|
|
42
|
+
safeMode(safe) {
|
|
43
|
+
this._safeMode = safe;
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Convert a natural language question to SQL and execute it.
|
|
49
|
+
* @param {string} question
|
|
50
|
+
* @param {Object} [options={}]
|
|
51
|
+
* @returns {Promise<{sql: string, params: Array, results: Array, raw_response: Object}>}
|
|
52
|
+
*/
|
|
53
|
+
async query(question, options = {}) {
|
|
54
|
+
const schema = await this._introspectSchema();
|
|
55
|
+
const systemPrompt = this._buildSystemPrompt(schema);
|
|
56
|
+
const messages = [
|
|
57
|
+
{ role: 'system', content: systemPrompt },
|
|
58
|
+
{ role: 'user', content: question },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const chatOptions = {
|
|
62
|
+
model: options.model || this._model,
|
|
63
|
+
max_tokens: options.max_tokens || this._maxTokens,
|
|
64
|
+
temperature: options.temperature || 0.1, // Low temp for SQL accuracy
|
|
65
|
+
response_format: 'json',
|
|
66
|
+
json_schema: {
|
|
67
|
+
name: 'sql_response',
|
|
68
|
+
schema: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: {
|
|
71
|
+
sql: { type: 'string' },
|
|
72
|
+
params: { type: 'array', items: {} },
|
|
73
|
+
explanation: { type: 'string' },
|
|
74
|
+
},
|
|
75
|
+
required: ['sql'],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const res = await this._manager.chat(this._provider, messages, chatOptions);
|
|
81
|
+
const parsed = this._extractSqlFromResponse(res);
|
|
82
|
+
|
|
83
|
+
// Safety check
|
|
84
|
+
if (this._safeMode && parsed.sql) {
|
|
85
|
+
const upper = parsed.sql.trim().toUpperCase();
|
|
86
|
+
if (!upper.startsWith('SELECT') && !upper.startsWith('WITH')) {
|
|
87
|
+
throw new Error(`AIQueryBuilder safe mode: only SELECT/WITH queries are allowed. Got: ${upper.slice(0, 20)}...`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Execute the query
|
|
92
|
+
let results = [];
|
|
93
|
+
if (parsed.sql && this._connection) {
|
|
94
|
+
try {
|
|
95
|
+
results = await this._connection.raw(parsed.sql, parsed.params || []);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
return { sql: parsed.sql, params: parsed.params || [], results: [], error: err.message, raw_response: res };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
sql: parsed.sql || '',
|
|
103
|
+
params: parsed.params || [],
|
|
104
|
+
results,
|
|
105
|
+
explanation: parsed.explanation || '',
|
|
106
|
+
raw_response: res,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Generate SQL without executing it.
|
|
112
|
+
* @param {string} question
|
|
113
|
+
* @param {Object} [options={}]
|
|
114
|
+
* @returns {Promise<{sql: string, params: Array, explanation: string}>}
|
|
115
|
+
*/
|
|
116
|
+
async toSql(question, options = {}) {
|
|
117
|
+
const schema = await this._introspectSchema();
|
|
118
|
+
const systemPrompt = this._buildSystemPrompt(schema);
|
|
119
|
+
const messages = [
|
|
120
|
+
{ role: 'system', content: systemPrompt },
|
|
121
|
+
{ role: 'user', content: question },
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
const chatOptions = {
|
|
125
|
+
model: options.model || this._model,
|
|
126
|
+
max_tokens: options.max_tokens || this._maxTokens,
|
|
127
|
+
temperature: 0.1,
|
|
128
|
+
response_format: 'json',
|
|
129
|
+
json_schema: {
|
|
130
|
+
name: 'sql_response',
|
|
131
|
+
schema: {
|
|
132
|
+
type: 'object',
|
|
133
|
+
properties: {
|
|
134
|
+
sql: { type: 'string' },
|
|
135
|
+
params: { type: 'array', items: {} },
|
|
136
|
+
explanation: { type: 'string' },
|
|
137
|
+
},
|
|
138
|
+
required: ['sql'],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const res = await this._manager.chat(this._provider, messages, chatOptions);
|
|
144
|
+
const parsed = this._extractSqlFromResponse(res);
|
|
145
|
+
return { sql: parsed.sql || '', params: parsed.params || [], explanation: parsed.explanation || '' };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** @private */
|
|
149
|
+
async _introspectSchema() {
|
|
150
|
+
if (!this._connection) return 'No database connection available.';
|
|
151
|
+
try {
|
|
152
|
+
const dialect = this._connection.config?.client || 'mysql';
|
|
153
|
+
let tables = [];
|
|
154
|
+
if (dialect === 'pg' || dialect === 'postgresql') {
|
|
155
|
+
const res = await this._connection.raw(
|
|
156
|
+
"SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE'"
|
|
157
|
+
);
|
|
158
|
+
tables = (res.rows || res).map(r => r.table_name);
|
|
159
|
+
} else if (dialect === 'sqlite' || dialect === 'sqlite3') {
|
|
160
|
+
const res = await this._connection.raw(
|
|
161
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
|
|
162
|
+
);
|
|
163
|
+
tables = (Array.isArray(res) ? res : (res.rows || [])).map(r => r.name);
|
|
164
|
+
} else {
|
|
165
|
+
// MySQL
|
|
166
|
+
const res = await this._connection.raw('SHOW TABLES');
|
|
167
|
+
tables = (Array.isArray(res) ? res[0] || res : res).map(r => Object.values(r)[0]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const schemaInfo = {};
|
|
171
|
+
for (const table of tables) {
|
|
172
|
+
try {
|
|
173
|
+
let cols;
|
|
174
|
+
if (dialect === 'pg' || dialect === 'postgresql') {
|
|
175
|
+
const cRes = await this._connection.raw(
|
|
176
|
+
`SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_name = '${table}'`
|
|
177
|
+
);
|
|
178
|
+
cols = (cRes.rows || cRes).map(c => `${c.column_name} ${c.data_type}${c.is_nullable === 'YES' ? ' NULL' : ''}`);
|
|
179
|
+
} else if (dialect === 'sqlite' || dialect === 'sqlite3') {
|
|
180
|
+
const cRes = await this._connection.raw(`PRAGMA table_info("${table}")`);
|
|
181
|
+
cols = (Array.isArray(cRes) ? cRes : []).map(c => `${c.name} ${c.type}${c.notnull ? '' : ' NULL'}`);
|
|
182
|
+
} else {
|
|
183
|
+
const cRes = await this._connection.raw(`DESCRIBE \`${table}\``);
|
|
184
|
+
cols = (Array.isArray(cRes) ? cRes[0] || cRes : cRes).map(c => `${c.Field} ${c.Type}${c.Null === 'YES' ? ' NULL' : ''}`);
|
|
185
|
+
}
|
|
186
|
+
schemaInfo[table] = cols;
|
|
187
|
+
} catch { schemaInfo[table] = ['(unable to read columns)']; }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return Object.entries(schemaInfo)
|
|
191
|
+
.map(([t, cols]) => `TABLE ${t}:\n ${cols.join('\n ')}`)
|
|
192
|
+
.join('\n\n');
|
|
193
|
+
} catch (err) {
|
|
194
|
+
return `Schema introspection error: ${err.message}`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** @private */
|
|
199
|
+
_buildSystemPrompt(schema) {
|
|
200
|
+
let prompt = `You are a SQL assistant. Given a natural language question and a database schema, generate a single SQL query that answers the question.
|
|
201
|
+
|
|
202
|
+
DATABASE SCHEMA:
|
|
203
|
+
${schema}
|
|
204
|
+
|
|
205
|
+
RULES:
|
|
206
|
+
- Return ONLY a JSON object with keys: "sql" (the query), "params" (array of parameterized values, can be empty), "explanation" (brief explanation).
|
|
207
|
+
- Use parameterized queries (? placeholders) when appropriate for safety.
|
|
208
|
+
- Do NOT use DROP, TRUNCATE, or ALTER statements.`;
|
|
209
|
+
|
|
210
|
+
if (this._safeMode) {
|
|
211
|
+
prompt += '\n- ONLY generate SELECT or WITH (CTE) queries. No INSERT, UPDATE, DELETE.';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return prompt;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** @private */
|
|
218
|
+
_extractSqlFromResponse(res) {
|
|
219
|
+
// Try various response formats
|
|
220
|
+
let content = res?.output_text || res?.choices?.[0]?.message?.content || res?.content?.[0]?.text || res?.message?.content || '';
|
|
221
|
+
if (typeof content !== 'string') content = JSON.stringify(content);
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const parsed = JSON.parse(content);
|
|
225
|
+
return { sql: parsed.sql || '', params: parsed.params || [], explanation: parsed.explanation || '' };
|
|
226
|
+
} catch {
|
|
227
|
+
// Try to extract SQL from plain text
|
|
228
|
+
const sqlMatch = content.match(/```sql\s*([\s\S]*?)```/i) || content.match(/SELECT[\s\S]+?;/i);
|
|
229
|
+
return { sql: sqlMatch ? (sqlMatch[1] || sqlMatch[0]).trim() : content.trim(), params: [], explanation: '' };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = AIQueryBuilder;
|