outlet-orm 10.0.0 → 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 (36) hide show
  1. package/README.md +32 -0
  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/API.md +30 -0
  6. package/skills/outlet-orm/MODELS.md +144 -2
  7. package/skills/outlet-orm/QUERIES.md +133 -0
  8. package/skills/outlet-orm/RELATIONS.md +44 -0
  9. package/skills/outlet-orm/SKILL.md +4 -2
  10. package/skills/outlet-orm/TYPESCRIPT.md +98 -0
  11. package/src/AI/AIManager.js +58 -58
  12. package/src/AI/AIQueryBuilder.js +2 -2
  13. package/src/AI/AIQueryOptimizer.js +2 -2
  14. package/src/AI/Contracts/AudioProviderContract.js +2 -2
  15. package/src/AI/Contracts/ChatProviderContract.js +3 -2
  16. package/src/AI/Contracts/EmbeddingsProviderContract.js +1 -1
  17. package/src/AI/Contracts/ImageProviderContract.js +1 -1
  18. package/src/AI/Contracts/ModelsProviderContract.js +1 -1
  19. package/src/AI/Contracts/ToolContract.js +1 -1
  20. package/src/AI/MCPServer.js +3 -3
  21. package/src/AI/Providers/CustomOpenAIProvider.js +0 -2
  22. package/src/AI/Providers/GeminiProvider.js +2 -2
  23. package/src/AI/Providers/OpenAIProvider.js +0 -5
  24. package/src/AI/Support/DocumentAttachmentMapper.js +37 -37
  25. package/src/AI/Support/FileSecurity.js +1 -1
  26. package/src/Backup/BackupManager.js +6 -6
  27. package/src/Backup/BackupScheduler.js +1 -1
  28. package/src/Backup/BackupSocketServer.js +2 -2
  29. package/src/DatabaseConnection.js +51 -0
  30. package/src/Model.js +245 -5
  31. package/src/QueryBuilder.js +191 -0
  32. package/src/Relations/HasOneRelation.js +114 -114
  33. package/src/Relations/HasOneThroughRelation.js +105 -105
  34. package/src/Relations/MorphOneRelation.js +4 -2
  35. package/src/Relations/Relation.js +35 -0
  36. package/types/index.d.ts +67 -1
@@ -90,66 +90,66 @@ class AIManager {
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
  }
@@ -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 {
@@ -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');
@@ -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
  }
@@ -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;
@@ -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
  }
@@ -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);
@@ -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