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
@@ -211,6 +211,30 @@ if (hasAdmins) {
211
211
  }
212
212
  ```
213
213
 
214
+ ### Sum / Avg / Min / Max (v11.0.0)
215
+
216
+ ```javascript
217
+ const totalBalance = await User.query().sum('balance');
218
+ const averageAge = await User.query().avg('age');
219
+ const youngest = await User.query().min('age');
220
+ const oldest = await User.query().max('age');
221
+
222
+ // With conditions
223
+ const activeTotal = await User.where('status', 'active').sum('balance');
224
+ ```
225
+
226
+ ### Pluck / Value (v11.0.0)
227
+
228
+ ```javascript
229
+ // pluck() — get an array of values from a single column
230
+ const emails = await User.query().pluck('email');
231
+ // ['john@example.com', 'jane@example.com', ...]
232
+
233
+ // value() — get a single value from the first row
234
+ const name = await User.where('id', 1).value('name');
235
+ // 'John Doe'
236
+ ```
237
+
214
238
  ### Group By & Having
215
239
 
216
240
  ```javascript
@@ -280,6 +304,104 @@ await User.where('id', 1).decrement('credits', 50);
280
304
 
281
305
  ---
282
306
 
307
+ ## Batch Processing — chunk() (v11.0.0)
308
+
309
+ Process large datasets in manageable batches:
310
+
311
+ ```javascript
312
+ // Process 100 records at a time
313
+ await User.query().chunk(100, async (users) => {
314
+ for (const user of users) {
315
+ await sendNewsletter(user);
316
+ }
317
+ });
318
+
319
+ // With conditions
320
+ await User.where('status', 'active').chunk(50, async (batch) => {
321
+ console.log(`Processing ${batch.length} users`);
322
+ });
323
+ ```
324
+
325
+ ---
326
+
327
+ ## Conditional Queries — when() / tap() (v11.0.0)
328
+
329
+ ### when()
330
+
331
+ Conditionally apply query clauses:
332
+
333
+ ```javascript
334
+ const status = req.query.status; // may be undefined
335
+
336
+ const users = await User.query()
337
+ .when(status, (query, value) => query.where('status', value))
338
+ .when(req.query.role, (query, value) => query.where('role', value))
339
+ .get();
340
+ ```
341
+
342
+ ### tap()
343
+
344
+ Execute a callback for debugging without modifying the query:
345
+
346
+ ```javascript
347
+ const users = await User.query()
348
+ .where('status', 'active')
349
+ .tap((query) => console.log('Query so far:', query.toSQL()))
350
+ .orderBy('name')
351
+ .get();
352
+ ```
353
+
354
+ ---
355
+
356
+ ## Query Debugging — toSQL() / dd() (v11.0.0)
357
+
358
+ ```javascript
359
+ // toSQL() — get the SQL string and bindings
360
+ const { sql, bindings } = User.where('status', 'active').toSQL();
361
+ console.log(sql); // 'SELECT * FROM users WHERE status = ?'
362
+ console.log(bindings); // ['active']
363
+
364
+ // dd() — dump and die (logs to console and throws)
365
+ User.where('status', 'active').dd();
366
+ // Logs: { sql: '...', bindings: [...] } then throws
367
+ ```
368
+
369
+ ---
370
+
371
+ ## Fluent Local Scopes (v11.0.0)
372
+
373
+ Define reusable query constraints as static methods on the model:
374
+
375
+ ```javascript
376
+ class User extends Model {
377
+ static table = 'users';
378
+
379
+ // Define scope as static scopeXxx(query, ...params)
380
+ static scopeActive(query) {
381
+ return query.where('status', 'active');
382
+ }
383
+
384
+ static scopeRole(query, role) {
385
+ return query.where('role', role);
386
+ }
387
+
388
+ static scopeRecent(query, days = 7) {
389
+ const date = new Date(Date.now() - days * 86400000).toISOString();
390
+ return query.where('created_at', '>', date);
391
+ }
392
+ }
393
+
394
+ // Use fluently on the query builder
395
+ const users = await User.query().active().role('admin').recent(30).get();
396
+
397
+ // Combine with other query methods
398
+ const count = await User.query().active().count();
399
+ ```
400
+
401
+ > See [ADVANCED.md](ADVANCED.md) for more details on global and local scopes.
402
+
403
+ ---
404
+
283
405
  ## Raw Queries
284
406
 
285
407
  ```javascript
@@ -304,12 +426,12 @@ const native = await db.execute(
304
426
 
305
427
  > Since v8.0.0
306
428
 
307
- Convert natural language into SQL queries using any LLM provider via AiBridge.
429
+ Convert natural language into SQL queries using any LLM provider via AI.
308
430
 
309
431
  ```javascript
310
- const { AiBridgeManager, AIQueryBuilder, DatabaseConnection } = require('outlet-orm');
432
+ const { AIManager, AIQueryBuilder, DatabaseConnection } = require('outlet-orm');
311
433
 
312
- const ai = new AiBridgeManager({ providers: { openai: { api_key: process.env.OPENAI_API_KEY, model: 'gpt-4o-mini' } } });
434
+ const ai = new AIManager({ providers: { openai: { api_key: process.env.OPENAI_API_KEY, model: 'gpt-4o-mini' } } });
313
435
  const db = new DatabaseConnection();
314
436
  const qb = new AIQueryBuilder(ai, db);
315
437
 
@@ -394,6 +516,17 @@ See [AI.md](AI.md) for full details.
394
516
  |`paginate(page, perPage)`| Pagination |
395
517
  |`count()`| Count results |
396
518
  |`exists()`| Check existence |
519
+ |`sum(col)`| Sum of column (v11) |
520
+ |`avg(col)`| Average of column (v11) |
521
+ |`min(col)`| Minimum of column (v11) |
522
+ |`max(col)`| Maximum of column (v11) |
523
+ |`pluck(col)`| Array of column values (v11) |
524
+ |`value(col)`| Single value from first row (v11) |
525
+ |`chunk(size, callback)`| Batch processing (v11) |
526
+ |`when(condition, callback)`| Conditional clause (v11) |
527
+ |`tap(callback)`| Debug callback (v11) |
528
+ |`toSQL()`| Get SQL + bindings (v11) |
529
+ |`dd()`| Dump & die debug (v11) |
397
530
  |`insert(data)`| Insert record(s) |
398
531
  |`update(attrs)`| Update records |
399
532
  |`delete()`| Delete records |
@@ -546,6 +546,50 @@ class Category extends Model {
546
546
  |`attach(ids)`| Attach (many-to-many) |
547
547
  |`detach(ids?)`| Detach (many-to-many) |
548
548
  |`sync(ids)`| Sync (many-to-many) |
549
+ |`withDefault(attrs?)`| Default for empty HasOne/MorphOne (v11) |
550
+
551
+ ---
552
+
553
+ ## Relation Defaults — withDefault() (v11.0.0)
554
+
555
+ Return a default model instead of `null` when a `hasOne`, `morphOne` or `hasOneThrough` relationship is empty:
556
+
557
+ ```javascript
558
+ class User extends Model {
559
+ static table = 'users';
560
+
561
+ profile() {
562
+ // Returns empty Profile instance instead of null
563
+ return this.hasOne(Profile, 'user_id').withDefault();
564
+ }
565
+
566
+ avatar() {
567
+ // Returns a Profile with default values
568
+ return this.morphOne(Image, 'imageable').withDefault({
569
+ url: '/images/default-avatar.png'
570
+ });
571
+ }
572
+
573
+ settings() {
574
+ // Dynamic defaults via callback
575
+ return this.hasOne(Settings, 'user_id').withDefault((model) => {
576
+ model.setAttribute('theme', 'light');
577
+ model.setAttribute('locale', 'en');
578
+ });
579
+ }
580
+ }
581
+
582
+ // Usage
583
+ const user = await User.with('profile').find(1);
584
+ console.log(user.relationships.profile); // Profile instance (never null)
585
+ ```
586
+
587
+ **Signatures:**
588
+ - `withDefault()` — empty model instance
589
+ - `withDefault({ key: value, ... })` — model with attributes
590
+ - `withDefault((model) => { ... })` — dynamic defaults via callback
591
+
592
+ **Supported on:** `hasOne`, `morphOne`, `hasOneThrough`
549
593
 
550
594
  ---
551
595
 
@@ -106,9 +106,9 @@ outlet-migrate seed
106
106
  Generate realistic domain-specific seed data using AI instead of generic lorem ipsum.
107
107
 
108
108
  ```javascript
109
- const { AiBridgeManager, AISeeder, DatabaseConnection } = require('outlet-orm');
109
+ const { AIManager, AISeeder, DatabaseConnection } = require('outlet-orm');
110
110
 
111
- const ai = new AiBridgeManager({
111
+ const ai = new AIManager({
112
112
  providers: { openai: { api_key: process.env.OPENAI_API_KEY, model: 'gpt-4o-mini' } }
113
113
  });
114
114
  const seeder = new AISeeder(ai, new DatabaseConnection());
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: outlet-orm-best-practices
3
- description: Outlet ORM is a Laravel Eloquent-inspired ORM for Node.js with MySQL, PostgreSQL, and SQLite support. Use this skill when working with Outlet ORM models, queries, relationships, migrations, backup, AI integration (AiBridge multi-provider LLM, AI Query Builder, AI Seeder, AI Optimizer, MCP Server) and database operations.
3
+ description: Outlet ORM is a Laravel Eloquent-inspired ORM for Node.js with MySQL, PostgreSQL, and SQLite support. Use this skill when working with Outlet ORM models, queries, relationships, migrations, backup, AI integration (AI multi-provider LLM, AI Query Builder, AI Seeder, AI Optimizer, MCP Server) and database operations.
4
4
  license: MIT
5
5
  metadata:
6
6
  author: omgbwa-yasse
7
- version: "9.0.0"
7
+ version: "11.0.0"
8
8
  source: https://github.com/omgbwa-yasse/outlet-orm
9
9
  npm: https://www.npmjs.com/package/outlet-orm
10
10
  ---
@@ -13,9 +13,11 @@ npm: https://www.npmjs.com/package/outlet-orm
13
13
 
14
14
  Comprehensive guide for using Outlet ORM - a Laravel Eloquent-inspired ORM for Node.js/TypeScript with support for MySQL, PostgreSQL, and SQLite.
15
15
 
16
- > 🆕 **v9.0.0**: Complete AI documentation AiBridge multi-provider LLM, AI Query Builder, AI Seeder, AI Optimizer, AI Prompt Enhancer, AI Safety Guardrails. See [AI.md](AI.md).
16
+ > 🆕 **v11.0.0**: Proxy property access (`user.name`), Eloquent-style model methods (`fresh`, `refresh`, `replicate`, `is`, `isNot`, `only`, `except`, `makeVisible`, `makeHidden`, `wasChanged`, `getChanges`), computed `appends`, QueryBuilder enhancements (`value`, `chunk`, `when`, `tap`, `toSQL`, `dd`), fluent local scopes (`static scopeActive(query)` → `User.query().active()`), relation `withDefault()`. See [MODELS.md](MODELS.md), [QUERIES.md](QUERIES.md), [ADVANCED.md](ADVANCED.md), [RELATIONS.md](RELATIONS.md).
17
17
  >
18
- > 🔖 **v8.0.0**: AiBridge multi-provider LLM abstraction (9 providers), AI Query Builder, AI Seeder, AI Query Optimizer, AI Prompt Enhancer.
18
+ > 🔖 **v9.0.0**: Complete AI documentation — AI multi-provider LLM, AI Query Builder, AI Seeder, AI Optimizer, AI Prompt Enhancer, AI Safety Guardrails. See [AI.md](AI.md).
19
+ >
20
+ > 🔖 **v8.0.0**: AI multi-provider LLM abstraction (9 providers), AI Query Builder, AI Seeder, AI Query Optimizer, AI Prompt Enhancer.
19
21
  >
20
22
  > 🔖 **v7.0.0**: AI Integration — MCP Server (Model Context Protocol), AI Safety Guardrails, Prompt-based project initialization.
21
23
  >
@@ -37,7 +39,7 @@ Comprehensive guide for using Outlet ORM - a Laravel Eloquent-inspired ORM for N
37
39
  | **[TYPESCRIPT.md](TYPESCRIPT.md)** | TypeScript types, generics, typed models, migrations |
38
40
  | **[SECURITY.md](SECURITY.md)** | 🔐 Security best practices, authentication, authorisation |
39
41
  | **[BACKUP.md](BACKUP.md)** | 🗄️ Backups, scheduling, AES-256-GCM encryption, TCP daemon, restore |
40
- | **[AI.md](AI.md)** | 🤖 AiBridge (9 LLM providers), AI Query Builder, AI Seeder, AI Optimizer, AI Prompt Enhancer, MCP Server, AI Safety Guardrails |
42
+ | **[AI.md](AI.md)** | 🤖 AI (9 LLM providers), AI Query Builder, AI Seeder, AI Optimizer, AI Prompt Enhancer, MCP Server, AI Safety Guardrails |
41
43
  | **[API.md](API.md)** | Complete API Reference |
42
44
 
43
45
  ---
@@ -56,7 +58,7 @@ Reference these guidelines when:
56
58
  - Scheduling or encrypting database backups [BACKUP.md](BACKUP.md)
57
59
  - Restoring a database from a backup file [BACKUP.md](BACKUP.md)
58
60
  - Running a long-lived backup daemon over TCP [BACKUP.md](BACKUP.md)
59
- - Configuring LLM providers (AiBridge) [AI.md](AI.md)
61
+ - Configuring LLM providers (AI) [AI.md](AI.md)
60
62
  - Converting natural language to SQL (AI Query Builder) [AI.md](AI.md)
61
63
  - Generating realistic seed data with AI [AI.md](AI.md)
62
64
  - Optimizing SQL queries with AI [AI.md](AI.md)
@@ -384,6 +384,104 @@ table.foreign('user_id')
384
384
 
385
385
  ---
386
386
 
387
+ ## v11.0.0 Type Additions
388
+
389
+ ### Property-Style Attribute Access
390
+
391
+ Model instances are wrapped in a `Proxy`, so attributes can be read and written as direct properties:
392
+
393
+ ```typescript
394
+ const user = await User.find(1);
395
+
396
+ // Property access (via Proxy)
397
+ console.log(user.name); // equivalent to user.getAttribute('name')
398
+ user.email = 'new@mail.com'; // equivalent to user.setAttribute('email', ...)
399
+
400
+ // Both styles coexist
401
+ user.getAttribute('name'); // still works
402
+ ```
403
+
404
+ The index signature `[K: string]: any` on `Model` enables this in TypeScript.
405
+
406
+ ### Computed Appends
407
+
408
+ ```typescript
409
+ class User extends Model<UserAttributes> {
410
+ static table = 'users';
411
+ static appends = ['full_name'] as const;
412
+
413
+ getFullNameAttribute(): string {
414
+ return `${this.attributes.first_name} ${this.attributes.last_name}`;
415
+ }
416
+ }
417
+
418
+ // full_name is included in toJSON() output
419
+ ```
420
+
421
+ ### New Model Instance Methods
422
+
423
+ ```typescript
424
+ // Reload from DB
425
+ const freshUser: User | null = await user.fresh('posts');
426
+ await user.refresh(); // mutates in place
427
+
428
+ // Clone without primary key
429
+ const clone: User = user.replicate('id', 'created_at');
430
+
431
+ // Identity comparison
432
+ user.is(otherUser); // same table + same PK
433
+ user.isNot(otherUser);
434
+
435
+ // Attribute subsets
436
+ const partial: Partial<UserAttributes> = user.only('name', 'email');
437
+ const rest: Partial<UserAttributes> = user.except('password');
438
+
439
+ // Instance-level visibility
440
+ user.makeVisible('password');
441
+ user.makeHidden('email');
442
+
443
+ // Change tracking (after save)
444
+ user.wasChanged('name'); // boolean
445
+ user.getChanges(); // { name: 'new value' }
446
+ ```
447
+
448
+ ### New QueryBuilder Methods
449
+
450
+ ```typescript
451
+ // Single column value
452
+ const email: any = await User.where('id', 1).value('email');
453
+
454
+ // Batch processing
455
+ await User.where('active', true).chunk(100, (users, page) => {
456
+ console.log(`Page ${page}:`, users.length);
457
+ // return false to stop
458
+ });
459
+
460
+ // Conditional query building
461
+ User.query()
462
+ .when(role, (qb, val) => qb.where('role', val))
463
+ .tap(qb => console.log(qb.toSQL()))
464
+ .get();
465
+
466
+ // Debug
467
+ const sql = User.where('active', true).toSQL();
468
+ User.where('active', true).dd(); // dumps SQL + throws
469
+ ```
470
+
471
+ ### withDefault on Relations
472
+
473
+ ```typescript
474
+ class User extends Model<UserAttributes> {
475
+ profile(): HasOneRelation<Profile> {
476
+ return this.hasOne(Profile, 'user_id').withDefault();
477
+ // or .withDefault({ bio: 'N/A' })
478
+ // or .withDefault(() => new Profile({ bio: 'N/A' }))
479
+ }
480
+ }
481
+ ```
482
+
483
+ ---
484
+
387
485
  ## Common Patterns
388
486
 
389
487
  ### Repository Pattern
@@ -16,12 +16,12 @@ const ToolChatRunner = require('./Support/ToolChatRunner');
16
16
  const BEARER_PREFIX = 'Bearer ';
17
17
 
18
18
  /**
19
- * AiBridgeManager
19
+ * AIManager
20
20
  * Central orchestrator for all AI providers. Supports provider registration,
21
21
  * per-call overrides, capability delegation (chat, stream, embeddings, images,
22
22
  * audio, models), tool registration, and chatWithTools loop.
23
23
  */
24
- class AiBridgeManager {
24
+ class AIManager {
25
25
  /**
26
26
  * @param {Object} config
27
27
  */
@@ -90,66 +90,66 @@ class AiBridgeManager {
90
90
  /** @private */
91
91
  _buildProviderFromOptions(name, options) {
92
92
  switch (name) {
93
- case 'openai': {
94
- const api = options.api_key;
95
- if (api) return new OpenAIProvider(api, options.chat_endpoint || 'https://api.openai.com/v1/chat/completions');
96
- break;
97
- }
98
- case 'ollama': return new OllamaProvider(options.endpoint || 'http://localhost:11434');
99
- case 'ollama_turbo': {
100
- const api = options.api_key;
101
- if (api) return new OllamaTurboProvider(api, options.endpoint || 'https://ollama.com');
102
- break;
103
- }
104
- case 'onn': {
105
- const api = options.api_key;
106
- if (api) return new OnnProvider(api, options.endpoint || 'https://api.onn.ai/v1/chat');
107
- break;
108
- }
109
- case 'gemini': {
110
- const api = options.api_key;
111
- if (api) return new GeminiProvider(api, options.endpoint);
112
- break;
113
- }
114
- case 'grok': {
115
- const api = options.api_key;
116
- if (api) return new GrokProvider(api, options.endpoint);
117
- break;
118
- }
119
- case 'claude': {
120
- const api = options.api_key;
121
- if (api) return new ClaudeProvider(api, options.endpoint);
122
- break;
123
- }
124
- case 'mistral': {
125
- const api = options.api_key;
126
- if (api) return new MistralProvider(api, options.endpoint || 'https://api.mistral.ai/v1/chat/completions');
127
- break;
128
- }
129
- case 'openai_custom': {
130
- const api = options.api_key;
131
- const base = options.base_url;
132
- if (api && base) {
133
- return new CustomOpenAIProvider(api, base, options.paths || {},
134
- options.auth_header || 'Authorization', options.auth_prefix || BEARER_PREFIX,
135
- options.extra_headers || {});
136
- }
137
- break;
93
+ case 'openai': {
94
+ const api = options.api_key;
95
+ if (api) return new OpenAIProvider(api, options.chat_endpoint || 'https://api.openai.com/v1/chat/completions');
96
+ break;
97
+ }
98
+ case 'ollama': return new OllamaProvider(options.endpoint || 'http://localhost:11434');
99
+ case 'ollama_turbo': {
100
+ const api = options.api_key;
101
+ if (api) return new OllamaTurboProvider(api, options.endpoint || 'https://ollama.com');
102
+ break;
103
+ }
104
+ case 'onn': {
105
+ const api = options.api_key;
106
+ if (api) return new OnnProvider(api, options.endpoint || 'https://api.onn.ai/v1/chat');
107
+ break;
108
+ }
109
+ case 'gemini': {
110
+ const api = options.api_key;
111
+ if (api) return new GeminiProvider(api, options.endpoint);
112
+ break;
113
+ }
114
+ case 'grok': {
115
+ const api = options.api_key;
116
+ if (api) return new GrokProvider(api, options.endpoint);
117
+ break;
118
+ }
119
+ case 'claude': {
120
+ const api = options.api_key;
121
+ if (api) return new ClaudeProvider(api, options.endpoint);
122
+ break;
123
+ }
124
+ case 'mistral': {
125
+ const api = options.api_key;
126
+ if (api) return new MistralProvider(api, options.endpoint || 'https://api.mistral.ai/v1/chat/completions');
127
+ break;
128
+ }
129
+ case 'openai_custom': {
130
+ const api = options.api_key;
131
+ const base = options.base_url;
132
+ if (api && base) {
133
+ return new CustomOpenAIProvider(api, base, options.paths || {},
134
+ options.auth_header || 'Authorization', options.auth_prefix || BEARER_PREFIX,
135
+ options.extra_headers || {});
138
136
  }
139
- case 'openrouter': {
140
- const api = options.api_key;
141
- if (api) {
142
- const base = options.base_url || 'https://openrouter.ai/api/v1';
143
- const hdrs = {};
144
- if (options.referer) hdrs['HTTP-Referer'] = options.referer;
145
- if (options.title) hdrs['X-Title'] = options.title;
146
- return new CustomOpenAIProvider(api, base,
147
- { chat: '/chat/completions', embeddings: '/embeddings', image: '/images/generations', tts: '/audio/speech', stt: '/audio/transcriptions' },
148
- 'Authorization', BEARER_PREFIX, hdrs
149
- );
150
- }
151
- break;
137
+ break;
138
+ }
139
+ case 'openrouter': {
140
+ const api = options.api_key;
141
+ if (api) {
142
+ const base = options.base_url || 'https://openrouter.ai/api/v1';
143
+ const hdrs = {};
144
+ if (options.referer) hdrs['HTTP-Referer'] = options.referer;
145
+ if (options.title) hdrs['X-Title'] = options.title;
146
+ return new CustomOpenAIProvider(api, base,
147
+ { chat: '/chat/completions', embeddings: '/embeddings', image: '/images/generations', tts: '/audio/speech', stt: '/audio/transcriptions' },
148
+ 'Authorization', BEARER_PREFIX, hdrs
149
+ );
152
150
  }
151
+ break;
152
+ }
153
153
  }
154
154
  return null;
155
155
  }
@@ -284,4 +284,4 @@ class AiBridgeManager {
284
284
  }
285
285
  }
286
286
 
287
- module.exports = AiBridgeManager;
287
+ module.exports = AIManager;
@@ -10,7 +10,7 @@
10
10
  */
11
11
  class AIPromptEnhancer {
12
12
  /**
13
- * @param {import('./AiBridgeManager')} manager
13
+ * @param {import('./AIManager')} manager
14
14
  */
15
15
  constructor(manager) {
16
16
  this._manager = manager;
@@ -2,7 +2,7 @@
2
2
 
3
3
  /**
4
4
  * AIQueryBuilder
5
- * Natural Language → SQL conversion using any AiBridge provider.
5
+ * Natural Language → SQL conversion using any AI provider.
6
6
  * Introspects the database schema, sends it with the NL prompt to an LLM,
7
7
  * and returns a safe, parameterized SQL query.
8
8
  *
@@ -10,7 +10,7 @@
10
10
  */
11
11
  class AIQueryBuilder {
12
12
  /**
13
- * @param {import('./AiBridgeManager')} manager - AiBridge manager instance
13
+ * @param {import('./AIManager')} manager - AI manager instance
14
14
  * @param {Object} connection - DatabaseConnection instance (outlet-orm)
15
15
  */
16
16
  constructor(manager, connection) {
@@ -153,12 +153,12 @@ class AIQueryBuilder {
153
153
  let tables = [];
154
154
  if (dialect === 'pg' || dialect === 'postgresql') {
155
155
  const res = await this._connection.raw(
156
- "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE'"
156
+ 'SELECT table_name FROM information_schema.tables WHERE table_schema = \'public\' AND table_type = \'BASE TABLE\''
157
157
  );
158
158
  tables = (res.rows || res).map(r => r.table_name);
159
159
  } else if (dialect === 'sqlite' || dialect === 'sqlite3') {
160
160
  const res = await this._connection.raw(
161
- "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
161
+ 'SELECT name FROM sqlite_master WHERE type=\'table\' AND name NOT LIKE \'sqlite_%\''
162
162
  );
163
163
  tables = (Array.isArray(res) ? res : (res.rows || [])).map(r => r.name);
164
164
  } else {
@@ -9,7 +9,7 @@
9
9
  */
10
10
  class AIQueryOptimizer {
11
11
  /**
12
- * @param {import('./AiBridgeManager')} manager
12
+ * @param {import('./AIManager')} manager
13
13
  * @param {Object} [connection] - DatabaseConnection instance (optional, for schema introspection)
14
14
  */
15
15
  constructor(manager, connection = null) {
@@ -143,10 +143,10 @@ RULES:
143
143
  const dialect = this._connection.config?.client || 'mysql';
144
144
  let tables = [];
145
145
  if (dialect === 'pg' || dialect === 'postgresql') {
146
- const res = await this._connection.raw("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'");
146
+ const res = await this._connection.raw('SELECT table_name FROM information_schema.tables WHERE table_schema = \'public\'');
147
147
  tables = (res.rows || res).map(r => r.table_name);
148
148
  } else if (dialect === 'sqlite' || dialect === 'sqlite3') {
149
- const res = await this._connection.raw("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'");
149
+ const res = await this._connection.raw('SELECT name FROM sqlite_master WHERE type=\'table\' AND name NOT LIKE \'sqlite_%\'');
150
150
  tables = (Array.isArray(res) ? res : []).map(r => r.name);
151
151
  } else {
152
152
  const res = await this._connection.raw('SHOW TABLES');
@@ -9,7 +9,7 @@
9
9
  */
10
10
  class AISeeder {
11
11
  /**
12
- * @param {import('./AiBridgeManager')} manager
12
+ * @param {import('./AIManager')} manager
13
13
  * @param {Object} connection - DatabaseConnection instance
14
14
  */
15
15
  constructor(manager, connection) {
@@ -5,7 +5,7 @@ const StreamChunk = require('../Support/StreamChunk');
5
5
 
6
6
  /**
7
7
  * TextBuilder
8
- * Fluent builder for text generation over AiBridge providers.
8
+ * Fluent builder for text generation over AI providers.
9
9
  * Keeps method names short and explicit, reducing array option errors.
10
10
  *
11
11
  * @example
@@ -19,7 +19,7 @@ class TextBuilder {
19
19
  static ERR_MISSING_USING = 'Provider and model must be set via using().';
20
20
 
21
21
  /**
22
- * @param {import('../AiBridgeManager')} manager
22
+ * @param {import('../AIManager')} manager
23
23
  */
24
24
  constructor(manager) {
25
25
  this._manager = manager;
@@ -11,7 +11,7 @@ class AudioProviderContract {
11
11
  * @param {Object} [options={}]
12
12
  * @returns {Promise<{audio: string, mime: string}>} audio is base64-encoded
13
13
  */
14
- async textToSpeech(text, options = {}) {
14
+ async textToSpeech(text, _options = {}) {
15
15
  throw new Error('Not implemented: textToSpeech()');
16
16
  }
17
17
 
@@ -21,7 +21,7 @@ class AudioProviderContract {
21
21
  * @param {Object} [options={}]
22
22
  * @returns {Promise<{text: string, raw?: Object}>}
23
23
  */
24
- async speechToText(filePath, options = {}) {
24
+ async speechToText(filePath, _options = {}) {
25
25
  throw new Error('Not implemented: speechToText()');
26
26
  }
27
27
  }
@@ -12,7 +12,7 @@ class ChatProviderContract {
12
12
  * @param {Object} [options={}]
13
13
  * @returns {Promise<Object>}
14
14
  */
15
- async chat(messages, options = {}) {
15
+ async chat(messages, _options = {}) {
16
16
  throw new Error('Not implemented: chat()');
17
17
  }
18
18
 
@@ -22,7 +22,8 @@ class ChatProviderContract {
22
22
  * @param {Object} [options={}]
23
23
  * @yields {string|Object}
24
24
  */
25
- async *stream(messages, options = {}) {
25
+ // eslint-disable-next-line require-yield
26
+ async *stream(messages, _options = {}) {
26
27
  throw new Error('Not implemented: stream()');
27
28
  }
28
29
 
@@ -11,7 +11,7 @@ class EmbeddingsProviderContract {
11
11
  * @param {Object} [options={}]
12
12
  * @returns {Promise<{embeddings: number[][], usage?: Object, raw?: Object}>}
13
13
  */
14
- async embeddings(inputs, options = {}) {
14
+ async embeddings(inputs, _options = {}) {
15
15
  throw new Error('Not implemented: embeddings()');
16
16
  }
17
17
  }