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.
- package/README.md +103 -5
- package/bin/reverse.js +0 -1
- package/package.json +1 -1
- package/skills/outlet-orm/ADVANCED.md +29 -0
- package/skills/outlet-orm/AI.md +23 -23
- package/skills/outlet-orm/API.md +33 -3
- package/skills/outlet-orm/MODELS.md +144 -2
- package/skills/outlet-orm/QUERIES.md +136 -3
- package/skills/outlet-orm/RELATIONS.md +44 -0
- package/skills/outlet-orm/SEEDS.md +2 -2
- package/skills/outlet-orm/SKILL.md +8 -6
- package/skills/outlet-orm/TYPESCRIPT.md +98 -0
- package/src/AI/{AiBridgeManager.js → AIManager.js} +61 -61
- package/src/AI/AIPromptEnhancer.js +1 -1
- package/src/AI/AIQueryBuilder.js +4 -4
- package/src/AI/AIQueryOptimizer.js +3 -3
- package/src/AI/AISeeder.js +1 -1
- package/src/AI/Builders/TextBuilder.js +2 -2
- package/src/AI/Contracts/AudioProviderContract.js +2 -2
- package/src/AI/Contracts/ChatProviderContract.js +3 -2
- package/src/AI/Contracts/EmbeddingsProviderContract.js +1 -1
- package/src/AI/Contracts/ImageProviderContract.js +1 -1
- package/src/AI/Contracts/ModelsProviderContract.js +1 -1
- package/src/AI/Contracts/ToolContract.js +1 -1
- package/src/AI/Facades/{AiBridge.js → AI.js} +11 -11
- package/src/AI/MCPServer.js +16 -16
- package/src/AI/Providers/CustomOpenAIProvider.js +0 -2
- package/src/AI/Providers/GeminiProvider.js +2 -2
- package/src/AI/Providers/OpenAIProvider.js +0 -5
- package/src/AI/Support/DocumentAttachmentMapper.js +37 -37
- package/src/AI/Support/FileSecurity.js +1 -1
- package/src/AI/Support/ToolChatRunner.js +1 -1
- package/src/Backup/BackupManager.js +6 -6
- package/src/Backup/BackupScheduler.js +1 -1
- package/src/Backup/BackupSocketServer.js +2 -2
- package/src/DatabaseConnection.js +51 -0
- package/src/Model.js +245 -5
- package/src/QueryBuilder.js +191 -0
- package/src/Relations/HasOneRelation.js +114 -114
- package/src/Relations/HasOneThroughRelation.js +105 -105
- package/src/Relations/MorphOneRelation.js +4 -2
- package/src/Relations/Relation.js +35 -0
- package/src/index.js +6 -6
- 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,
|
|
14
|
+
async generateImage(prompt, _options = {}) {
|
|
15
15
|
throw new Error('Not implemented: generateImage()');
|
|
16
16
|
}
|
|
17
17
|
}
|
|
@@ -19,7 +19,7 @@ class ToolContract {
|
|
|
19
19
|
* @param {Object} args
|
|
20
20
|
* @returns {Promise<string>|string}
|
|
21
21
|
*/
|
|
22
|
-
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
|
-
*
|
|
4
|
+
* AI Facade
|
|
5
5
|
*
|
|
6
|
-
* Convenience entry-point
|
|
7
|
-
* Provides static-like helpers that delegate to an
|
|
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
|
|
16
|
+
const AI = {
|
|
17
17
|
/**
|
|
18
|
-
* Bind an
|
|
19
|
-
* @param {import('../
|
|
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('../
|
|
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('
|
|
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('
|
|
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('
|
|
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 =
|
|
79
|
+
module.exports = AI;
|
package/src/AI/MCPServer.js
CHANGED
|
@@ -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
|
|
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(
|
|
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 =
|
|
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 =
|
|
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.
|
|
648
|
-
if (!manager) throw new Error('
|
|
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.
|
|
676
|
-
if (!manager) throw new Error('
|
|
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
|
-
// ──
|
|
695
|
+
// ── AI manager helper ────────────────────────────────────
|
|
696
696
|
|
|
697
697
|
/**
|
|
698
|
-
* Lazily creates an
|
|
699
|
-
* @returns {import('./
|
|
698
|
+
* Lazily creates an AI manager from environment variables.
|
|
699
|
+
* @returns {import('./AIManager')|null}
|
|
700
700
|
*/
|
|
701
|
-
|
|
702
|
-
if (this.
|
|
701
|
+
_getAIManager() {
|
|
702
|
+
if (this._aiManager) return this._aiManager;
|
|
703
703
|
|
|
704
704
|
try {
|
|
705
|
-
const
|
|
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.
|
|
720
|
-
return this.
|
|
719
|
+
this._aiManager = new AIManager(config);
|
|
720
|
+
return this._aiManager;
|
|
721
721
|
} catch {
|
|
722
722
|
return null;
|
|
723
723
|
}
|
|
@@ -18,7 +18,7 @@ class GeminiProvider {
|
|
|
18
18
|
/** @private */
|
|
19
19
|
_keyQuery() { return `?key=${this.apiKey}`; }
|
|
20
20
|
|
|
21
|
-
async chat(messages,
|
|
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,
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|