outlet-orm 9.0.2 → 11.1.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.
Files changed (44) hide show
  1. package/README.md +103 -5
  2. package/bin/reverse.js +0 -1
  3. package/package.json +1 -1
  4. package/skills/outlet-orm/ADVANCED.md +29 -0
  5. package/skills/outlet-orm/AI.md +23 -23
  6. package/skills/outlet-orm/API.md +33 -3
  7. package/skills/outlet-orm/MODELS.md +144 -2
  8. package/skills/outlet-orm/QUERIES.md +136 -3
  9. package/skills/outlet-orm/RELATIONS.md +44 -0
  10. package/skills/outlet-orm/SEEDS.md +2 -2
  11. package/skills/outlet-orm/SKILL.md +8 -6
  12. package/skills/outlet-orm/TYPESCRIPT.md +98 -0
  13. package/src/AI/{AiBridgeManager.js → AIManager.js} +61 -61
  14. package/src/AI/AIPromptEnhancer.js +1 -1
  15. package/src/AI/AIQueryBuilder.js +4 -4
  16. package/src/AI/AIQueryOptimizer.js +3 -3
  17. package/src/AI/AISeeder.js +1 -1
  18. package/src/AI/Builders/TextBuilder.js +2 -2
  19. package/src/AI/Contracts/AudioProviderContract.js +2 -2
  20. package/src/AI/Contracts/ChatProviderContract.js +3 -2
  21. package/src/AI/Contracts/EmbeddingsProviderContract.js +1 -1
  22. package/src/AI/Contracts/ImageProviderContract.js +1 -1
  23. package/src/AI/Contracts/ModelsProviderContract.js +1 -1
  24. package/src/AI/Contracts/ToolContract.js +1 -1
  25. package/src/AI/Facades/{AiBridge.js → AI.js} +11 -11
  26. package/src/AI/MCPServer.js +16 -16
  27. package/src/AI/Providers/CustomOpenAIProvider.js +0 -2
  28. package/src/AI/Providers/GeminiProvider.js +2 -2
  29. package/src/AI/Providers/OpenAIProvider.js +0 -5
  30. package/src/AI/Support/DocumentAttachmentMapper.js +37 -37
  31. package/src/AI/Support/FileSecurity.js +1 -1
  32. package/src/AI/Support/ToolChatRunner.js +1 -1
  33. package/src/Backup/BackupManager.js +6 -6
  34. package/src/Backup/BackupScheduler.js +1 -1
  35. package/src/Backup/BackupSocketServer.js +2 -2
  36. package/src/DatabaseConnection.js +51 -0
  37. package/src/Model.js +245 -5
  38. package/src/QueryBuilder.js +191 -0
  39. package/src/Relations/HasOneRelation.js +114 -114
  40. package/src/Relations/HasOneThroughRelation.js +105 -105
  41. package/src/Relations/MorphOneRelation.js +4 -2
  42. package/src/Relations/Relation.js +35 -0
  43. package/src/index.js +6 -6
  44. package/types/index.d.ts +78 -12
@@ -11,7 +11,7 @@ class ImageProviderContract {
11
11
  * @param {Object} [options={}]
12
12
  * @returns {Promise<{images: Array, meta?: Object, raw?: Object}>}
13
13
  */
14
- async generateImage(prompt, options = {}) {
14
+ async generateImage(prompt, _options = {}) {
15
15
  throw new Error('Not implemented: generateImage()');
16
16
  }
17
17
  }
@@ -18,7 +18,7 @@ class ModelsProviderContract {
18
18
  * @param {string} id
19
19
  * @returns {Promise<Object>}
20
20
  */
21
- async getModel(id) {
21
+ async getModel(_id) {
22
22
  throw new Error('Not implemented: getModel()');
23
23
  }
24
24
  }
@@ -19,7 +19,7 @@ class ToolContract {
19
19
  * @param {Object} args
20
20
  * @returns {Promise<string>|string}
21
21
  */
22
- execute(args) { throw new Error('Not implemented: execute()'); }
22
+ execute(_args) { throw new Error('Not implemented: execute()'); }
23
23
  }
24
24
 
25
25
  module.exports = ToolContract;
@@ -1,10 +1,10 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * AiBridge Facade
4
+ * AI Facade
5
5
  *
6
- * Convenience entry-point mirroring AiBridge\Facades\AiBridge in PHP.
7
- * Provides static-like helpers that delegate to an AiBridgeManager instance.
6
+ * Convenience entry-point for AI operations.
7
+ * Provides static-like helpers that delegate to an AIManager instance.
8
8
  */
9
9
 
10
10
  const ImageNormalizer = require('../Support/ImageNormalizer');
@@ -13,10 +13,10 @@ const EmbeddingsNormalizer = require('../Support/EmbeddingsNormalizer');
13
13
 
14
14
  let _manager = null;
15
15
 
16
- const AiBridge = {
16
+ const AI = {
17
17
  /**
18
- * Bind an AiBridgeManager instance so all helpers delegate to it.
19
- * @param {import('../AiBridgeManager')} manager
18
+ * Bind an AIManager instance so all helpers delegate to it.
19
+ * @param {import('../AIManager')} manager
20
20
  */
21
21
  setManager(manager) {
22
22
  _manager = manager;
@@ -24,7 +24,7 @@ const AiBridge = {
24
24
 
25
25
  /**
26
26
  * Return the bound manager (or null).
27
- * @returns {import('../AiBridgeManager')|null}
27
+ * @returns {import('../AIManager')|null}
28
28
  */
29
29
  getManager() {
30
30
  return _manager;
@@ -55,7 +55,7 @@ const AiBridge = {
55
55
  * @returns {import('../Builders/TextBuilder')}
56
56
  */
57
57
  text() {
58
- if (!_manager) throw new Error('AiBridge facade: no manager bound. Call AiBridge.setManager(manager) first.');
58
+ if (!_manager) throw new Error('AI facade: no manager bound. Call AI.setManager(manager) first.');
59
59
  return _manager.text();
60
60
  },
61
61
 
@@ -63,7 +63,7 @@ const AiBridge = {
63
63
  * Shorthand for manager.chat()
64
64
  */
65
65
  async chat(messages, opts) {
66
- if (!_manager) throw new Error('AiBridge facade: no manager bound.');
66
+ if (!_manager) throw new Error('AI facade: no manager bound.');
67
67
  return _manager.chat(messages, opts);
68
68
  },
69
69
 
@@ -71,9 +71,9 @@ const AiBridge = {
71
71
  * Shorthand for manager.provider()
72
72
  */
73
73
  provider(name) {
74
- if (!_manager) throw new Error('AiBridge facade: no manager bound.');
74
+ if (!_manager) throw new Error('AI facade: no manager bound.');
75
75
  return _manager.provider(name);
76
76
  },
77
77
  };
78
78
 
79
- module.exports = AiBridge;
79
+ module.exports = AI;
@@ -115,7 +115,7 @@ const TOOL_DEFINITIONS = [
115
115
  },
116
116
  {
117
117
  name: 'ai_query',
118
- description: 'Convert a natural language question into SQL and execute it. Requires an AI provider (AiBridge).',
118
+ description: 'Convert a natural language question into SQL and execute it. Requires an AI provider.',
119
119
  inputSchema: {
120
120
  type: 'object',
121
121
  properties: {
@@ -442,7 +442,7 @@ class MCPServer extends EventEmitter {
442
442
 
443
443
  // ── migrate_reset ──────────────────────────────────────────────
444
444
 
445
- async _toolMigrateReset(args) {
445
+ async _toolMigrateReset(_args) {
446
446
  // Consent already validated in _handleToolCall
447
447
  const conn = await this._getConnection();
448
448
  const { MigrationManager } = require('../../src');
@@ -517,9 +517,9 @@ class MCPServer extends EventEmitter {
517
517
  // Get all tables
518
518
  let query;
519
519
  if (driver === 'sqlite') {
520
- query = "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name";
520
+ query = 'SELECT name FROM sqlite_master WHERE type=\'table\' AND name NOT LIKE \'sqlite_%\' ORDER BY name';
521
521
  } else if (driver === 'postgres') {
522
- query = "SELECT tablename AS name FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename";
522
+ query = 'SELECT tablename AS name FROM pg_tables WHERE schemaname = \'public\' ORDER BY tablename';
523
523
  } else {
524
524
  query = 'SHOW TABLES';
525
525
  }
@@ -644,8 +644,8 @@ class MCPServer extends EventEmitter {
644
644
  async _toolAiQuery(args) {
645
645
  if (!args.question) throw new Error('A natural language question is required.');
646
646
  const conn = await this._getConnection();
647
- const manager = this._getAiBridgeManager();
648
- if (!manager) throw new Error('AiBridge is not configured. Set OPENAI_API_KEY or configure a provider.');
647
+ const manager = this._getAIManager();
648
+ if (!manager) throw new Error('AI is not configured. Set OPENAI_API_KEY or configure a provider.');
649
649
 
650
650
  const AIQueryBuilder = require('./AIQueryBuilder');
651
651
  const builder = new AIQueryBuilder(manager, conn);
@@ -672,8 +672,8 @@ class MCPServer extends EventEmitter {
672
672
  async _toolQueryOptimize(args) {
673
673
  if (!args.sql) throw new Error('SQL query is required.');
674
674
  const conn = await this._getConnection();
675
- const manager = this._getAiBridgeManager();
676
- if (!manager) throw new Error('AiBridge is not configured. Set OPENAI_API_KEY or configure a provider.');
675
+ const manager = this._getAIManager();
676
+ if (!manager) throw new Error('AI is not configured. Set OPENAI_API_KEY or configure a provider.');
677
677
 
678
678
  const AIQueryOptimizer = require('./AIQueryOptimizer');
679
679
  const optimizer = new AIQueryOptimizer(manager, conn);
@@ -692,17 +692,17 @@ class MCPServer extends EventEmitter {
692
692
  };
693
693
  }
694
694
 
695
- // ── AiBridge manager helper ────────────────────────────────────
695
+ // ── AI manager helper ────────────────────────────────────
696
696
 
697
697
  /**
698
- * Lazily creates an AiBridge manager from environment variables.
699
- * @returns {import('./Bridge/AiBridgeManager')|null}
698
+ * Lazily creates an AI manager from environment variables.
699
+ * @returns {import('./AIManager')|null}
700
700
  */
701
- _getAiBridgeManager() {
702
- if (this._aiBridgeManager) return this._aiBridgeManager;
701
+ _getAIManager() {
702
+ if (this._aiManager) return this._aiManager;
703
703
 
704
704
  try {
705
- const AiBridgeManager = require('./AiBridgeManager');
705
+ const AIManager = require('./AIManager');
706
706
  const config = {};
707
707
 
708
708
  // Auto-detect providers from env
@@ -716,8 +716,8 @@ class MCPServer extends EventEmitter {
716
716
 
717
717
  if (Object.keys(config).length === 0) return null;
718
718
 
719
- this._aiBridgeManager = new AiBridgeManager(config);
720
- return this._aiBridgeManager;
719
+ this._aiManager = new AIManager(config);
720
+ return this._aiManager;
721
721
  } catch {
722
722
  return null;
723
723
  }
@@ -1,7 +1,5 @@
1
1
  'use strict';
2
2
 
3
- const JsonSchemaValidator = require('../Support/JsonSchemaValidator');
4
-
5
3
  /**
6
4
  * CustomOpenAIProvider
7
5
  * Fully configurable OpenAI-compatible provider.
@@ -18,7 +18,7 @@ class GeminiProvider {
18
18
  /** @private */
19
19
  _keyQuery() { return `?key=${this.apiKey}`; }
20
20
 
21
- async chat(messages, options = {}) {
21
+ async chat(messages, _options = {}) {
22
22
  const userTexts = messages
23
23
  .filter(m => (m.role || '') !== 'system')
24
24
  .map(m => m.content);
@@ -46,7 +46,7 @@ class GeminiProvider {
46
46
 
47
47
  supportsStreaming() { return true; } // simulated
48
48
 
49
- async embeddings(inputs, options = {}) {
49
+ async embeddings(inputs, _options = {}) {
50
50
  const vectors = [];
51
51
  for (const input of inputs) {
52
52
  const payload = {
@@ -1,10 +1,5 @@
1
1
  'use strict';
2
2
 
3
- const ChatProviderContract = require('../Contracts/ChatProviderContract');
4
- const EmbeddingsProviderContract = require('../Contracts/EmbeddingsProviderContract');
5
- const ImageProviderContract = require('../Contracts/ImageProviderContract');
6
- const AudioProviderContract = require('../Contracts/AudioProviderContract');
7
- const ModelsProviderContract = require('../Contracts/ModelsProviderContract');
8
3
  const JsonSchemaValidator = require('../Support/JsonSchemaValidator');
9
4
  const DocumentAttachmentMapper = require('../Support/DocumentAttachmentMapper');
10
5
  const fs = require('fs');
@@ -26,46 +26,46 @@ class DocumentAttachmentMapper {
26
26
  for (const att of attachments) {
27
27
  if (att instanceof Document) {
28
28
  switch (att.kind) {
29
- case 'text':
30
- if (att.text != null) inlineTexts.push(att.text);
31
- break;
32
- case 'local':
33
- if (att.path && sec.validateFile(att.path)) {
34
- const mime = att.mime || 'application/octet-stream';
35
- const b64 = fs.readFileSync(att.path).toString('base64');
36
- if (mime.startsWith(IMAGE_PREFIX)) {
37
- images.push(b64);
38
- } else {
39
- files.push({ name: path.basename(att.path), type: mime, content: b64 });
40
- }
29
+ case 'text':
30
+ if (att.text != null) inlineTexts.push(att.text);
31
+ break;
32
+ case 'local':
33
+ if (att.path && sec.validateFile(att.path)) {
34
+ const mime = att.mime || 'application/octet-stream';
35
+ const b64 = fs.readFileSync(att.path).toString('base64');
36
+ if (mime.startsWith(IMAGE_PREFIX)) {
37
+ images.push(b64);
38
+ } else {
39
+ files.push({ name: path.basename(att.path), type: mime, content: b64 });
41
40
  }
42
- break;
43
- case 'base64':
44
- if (att.base64 && att.mime) {
45
- if (att.mime.startsWith(IMAGE_PREFIX)) {
46
- images.push(att.base64);
47
- } else {
48
- files.push({ name: att.title || 'document', type: att.mime, content: att.base64 });
49
- }
41
+ }
42
+ break;
43
+ case 'base64':
44
+ if (att.base64 && att.mime) {
45
+ if (att.mime.startsWith(IMAGE_PREFIX)) {
46
+ images.push(att.base64);
47
+ } else {
48
+ files.push({ name: att.title || 'document', type: att.mime, content: att.base64 });
50
49
  }
51
- break;
52
- case 'raw':
53
- if (att.raw && att.mime) {
54
- const b64 = Buffer.from(att.raw).toString('base64');
55
- if (att.mime.startsWith(IMAGE_PREFIX)) {
56
- images.push(b64);
57
- } else {
58
- files.push({ name: att.title || 'document', type: att.mime, content: b64 });
59
- }
50
+ }
51
+ break;
52
+ case 'raw':
53
+ if (att.raw && att.mime) {
54
+ const b64 = Buffer.from(att.raw).toString('base64');
55
+ if (att.mime.startsWith(IMAGE_PREFIX)) {
56
+ images.push(b64);
57
+ } else {
58
+ files.push({ name: att.title || 'document', type: att.mime, content: b64 });
60
59
  }
61
- break;
62
- case 'chunks':
63
- for (const c of att.chunks) inlineTexts.push(String(c));
64
- break;
65
- case 'url':
66
- case 'file_id':
67
- default:
68
- break;
60
+ }
61
+ break;
62
+ case 'chunks':
63
+ for (const c of att.chunks) inlineTexts.push(String(c));
64
+ break;
65
+ case 'url':
66
+ case 'file_id':
67
+ default:
68
+ break;
69
69
  }
70
70
  } else if (typeof att === 'string' && sec.validateFile(att)) {
71
71
  const b64 = fs.readFileSync(att).toString('base64');
@@ -31,7 +31,7 @@ class FileSecurity {
31
31
  * @param {boolean} [image=false]
32
32
  * @returns {boolean}
33
33
  */
34
- validateFile(filePath, image = false) {
34
+ validateFile(filePath, _image = false) {
35
35
  try {
36
36
  if (!fs.existsSync(filePath)) return false;
37
37
  const stats = fs.statSync(filePath);
@@ -8,7 +8,7 @@
8
8
  */
9
9
  class ToolChatRunner {
10
10
  /**
11
- * @param {import('../AiBridgeManager')} manager
11
+ * @param {import('../AIManager')} manager
12
12
  */
13
13
  constructor(manager) {
14
14
  this.manager = manager;
@@ -40,7 +40,7 @@ function quoteValue(val) {
40
40
  if (val === null || val === undefined) return 'NULL';
41
41
  if (typeof val === 'number' || typeof val === 'boolean') return String(val);
42
42
  // Escape single quotes
43
- return `'${String(val).replace(/'/g, "''")}'`;
43
+ return `'${String(val).replace(/'/g, '\'\'')}'`;
44
44
  }
45
45
 
46
46
  /**
@@ -244,7 +244,7 @@ class BackupManager {
244
244
 
245
245
  for (const table of tables) {
246
246
  lines.push(`\n-- Table: ${table}`);
247
- lines.push(`-- -----------------------------------------------------------`);
247
+ lines.push('-- -----------------------------------------------------------');
248
248
 
249
249
  // Schema
250
250
  const createSql = await this._getCreateTable(table);
@@ -302,13 +302,13 @@ class BackupManager {
302
302
  case 'postgres':
303
303
  case 'postgresql':
304
304
  rows = await this.connection.executeRawQuery(
305
- "SELECT tablename AS name FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename"
305
+ 'SELECT tablename AS name FROM pg_tables WHERE schemaname = \'public\' ORDER BY tablename'
306
306
  );
307
307
  return rows.map((r) => r.name);
308
308
 
309
309
  case 'sqlite':
310
310
  rows = await this.connection.executeRawQuery(
311
- "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
311
+ 'SELECT name FROM sqlite_master WHERE type=\'table\' AND name NOT LIKE \'sqlite_%\' ORDER BY name'
312
312
  );
313
313
  return rows.map((r) => r.name);
314
314
 
@@ -382,12 +382,12 @@ class BackupManager {
382
382
  */
383
383
  _sqlHeader(type) {
384
384
  return [
385
- `-- outlet-orm backup`,
385
+ '-- outlet-orm backup',
386
386
  `-- type : ${type}`,
387
387
  `-- driver : ${this.connection.driver}`,
388
388
  `-- database : ${this.connection.config?.database || '(unknown)'}`,
389
389
  `-- generated : ${new Date().toISOString()}`,
390
- `-- ---------------------------------------------------------------`,
390
+ '-- ---------------------------------------------------------------',
391
391
  ].join('\n');
392
392
  }
393
393
 
@@ -74,7 +74,7 @@ class BackupScheduler {
74
74
  throw new Error('BackupScheduler: intervalMs must be >= 1000 (1 second)');
75
75
  }
76
76
  if (type === 'partial' && (!Array.isArray(config.tables) || config.tables.length === 0)) {
77
- throw new Error("BackupScheduler: 'tables' array is required for partial backups");
77
+ throw new Error('BackupScheduler: \'tables\' array is required for partial backups');
78
78
  }
79
79
 
80
80
  const name = config.name || `${type}_${Date.now()}`;
@@ -271,7 +271,7 @@ class BackupSocketServer extends events.EventEmitter {
271
271
  filePath = await runManager.full(options);
272
272
  } else if (type === 'partial') {
273
273
  if (!Array.isArray(tables) || tables.length === 0) {
274
- throw new Error("'tables' array is required for partial backup");
274
+ throw new Error('\'tables\' array is required for partial backup');
275
275
  }
276
276
  filePath = await runManager.partial(tables, options);
277
277
  } else if (type === 'journal') {
@@ -284,7 +284,7 @@ class BackupSocketServer extends events.EventEmitter {
284
284
 
285
285
  case 'restore': {
286
286
  if (!msg.filePath) {
287
- throw new Error("'filePath' is required for restore");
287
+ throw new Error('\'filePath\' is required for restore');
288
288
  }
289
289
  // Build a restore-capable manager (needs encryptionPassword if file is encrypted)
290
290
  const restoreOpts = Object.assign({}, this._managerOptions, {
@@ -750,6 +750,57 @@ class DatabaseConnection {
750
750
  return result;
751
751
  }
752
752
 
753
+ /**
754
+ * Execute an aggregate function (SUM, AVG, MIN, MAX) on a column
755
+ * @param {string} table
756
+ * @param {string} fn - Aggregate function name (SUM, AVG, MIN, MAX)
757
+ * @param {string} column
758
+ * @param {Object} query
759
+ * @returns {Promise<number>}
760
+ */
761
+ async aggregate(table, fn, column, query) {
762
+ await this.connect();
763
+ const allowedFns = ['SUM', 'AVG', 'MIN', 'MAX'];
764
+ const safeFn = fn.toUpperCase();
765
+ if (!allowedFns.includes(safeFn)) {
766
+ throw new Error(`Invalid aggregate function: ${fn}`);
767
+ }
768
+ const safeTable = sanitizeIdentifier(table);
769
+ const safeColumn = sanitizeIdentifier(column);
770
+
771
+ const { whereClause, params } = this.buildWhereClause(query?.wheres || []);
772
+ const sql = `SELECT ${safeFn}(${safeColumn}) as result FROM ${safeTable}${whereClause}`;
773
+ const start = Date.now();
774
+
775
+ let result;
776
+ switch (this.driver) {
777
+ case 'mysql': {
778
+ const conn = this._getConnection();
779
+ const [rows] = await conn.execute(this.convertToDriverPlaceholder(sql), params);
780
+ result = rows[0].result;
781
+ break;
782
+ }
783
+ case 'postgres':
784
+ case 'postgresql': {
785
+ const conn = this._getConnection();
786
+ const pgResult = await conn.query(this.convertToDriverPlaceholder(sql, 'postgres'), params);
787
+ result = parseFloat(pgResult.rows[0].result);
788
+ break;
789
+ }
790
+ case 'sqlite':
791
+ result = await new Promise((resolve, reject) => {
792
+ this.connection.get(sql, params, (err, row) => {
793
+ if (err) reject(new Error(err.message || String(err)));
794
+ else resolve(row.result);
795
+ });
796
+ });
797
+ break;
798
+ }
799
+
800
+ logQuery(sql, params, Date.now() - start);
801
+ return result != null ? Number(result) : 0;
802
+ }
803
+
753
804
  /**
754
805
  * Execute a raw query and return normalized results.
755
806
  * ⚠️ SECURITY WARNING: The `sql` parameter is passed to the database driver without