dbgate-api-premium 7.2.0 → 7.2.2
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/package.json +6 -6
- package/src/auth/authProvider.js +3 -0
- package/src/auth/storageAuthProvider.js +37 -0
- package/src/controllers/auth.js +80 -2
- package/src/controllers/config.js +29 -3
- package/src/controllers/connections.js +10 -3
- package/src/controllers/databaseConnections.js +18 -2
- package/src/controllers/mcpAdmin.js +156 -0
- package/src/controllers/storage.js +72 -17
- package/src/controllers/storageDb.js +6 -0
- package/src/currentVersion.js +2 -2
- package/src/main.js +15 -0
- package/src/mcp.js +1719 -0
- package/src/proc/databaseConnectionProcess.js +50 -3
- package/src/proc/sessionProcess.js +20 -3
- package/src/shell/copyStream.js +2 -1
- package/src/storageModel.js +4 -0
- package/src/utility/envtools.js +5 -0
- package/src/utility/healthStatus.js +3 -1
- package/src/utility/loginchecker.js +7 -0
- package/src/utility/mcpAuth.js +137 -0
- package/src/utility/queryResultMetadata.js +268 -5
- package/src/utility/storageReplicatorItems.js +12 -0
|
@@ -210,6 +210,15 @@ function waitStructure() {
|
|
|
210
210
|
});
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
async function handleGetStructure({ msgid }) {
|
|
214
|
+
await waitStructure();
|
|
215
|
+
process.send({
|
|
216
|
+
msgtype: 'response',
|
|
217
|
+
msgid,
|
|
218
|
+
structure: serializeJsTypesForJsonStringify(analysedStructure),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
213
222
|
function resolveAnalysedPromises() {
|
|
214
223
|
for (const [resolve] of afterAnalyseCallbacks) {
|
|
215
224
|
resolve();
|
|
@@ -623,13 +632,50 @@ async function handleEvalJsonScript({ script, runid }) {
|
|
|
623
632
|
const directory = path.join(rundir(), runid);
|
|
624
633
|
fs.mkdirSync(directory);
|
|
625
634
|
const originalCwd = process.cwd();
|
|
635
|
+
let scriptError = null;
|
|
636
|
+
let finalizerError = null;
|
|
626
637
|
|
|
627
638
|
try {
|
|
628
639
|
process.chdir(directory);
|
|
629
640
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
641
|
+
try {
|
|
642
|
+
const evalWriter = new ScriptWriterEval(dbgateApi, requirePlugin, dbhan, runid);
|
|
643
|
+
await playJsonScriptWriter(script, evalWriter);
|
|
644
|
+
} catch (err) {
|
|
645
|
+
scriptError = err;
|
|
646
|
+
} finally {
|
|
647
|
+
try {
|
|
648
|
+
await dbgateApi.finalizer.run();
|
|
649
|
+
} catch (err) {
|
|
650
|
+
finalizerError = err;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const shouldReportScriptError = scriptError && !scriptError.dbgateCopyStreamErrorReported;
|
|
655
|
+
|
|
656
|
+
if (shouldReportScriptError || finalizerError) {
|
|
657
|
+
if (shouldReportScriptError) {
|
|
658
|
+
logger.error(extractErrorLogData(scriptError), 'DBGM-00000 Error running JSON script on database connection');
|
|
659
|
+
}
|
|
660
|
+
if (finalizerError) {
|
|
661
|
+
logger.error(extractErrorLogData(finalizerError), 'DBGM-00000 Error running JSON script finalizers');
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
process.send({
|
|
665
|
+
msgtype: 'copyStreamError',
|
|
666
|
+
copyStreamError: {
|
|
667
|
+
message: [
|
|
668
|
+
shouldReportScriptError && extractErrorMessage(scriptError),
|
|
669
|
+
finalizerError && `Finalizer failed: ${extractErrorMessage(finalizerError)}`,
|
|
670
|
+
]
|
|
671
|
+
.filter(Boolean)
|
|
672
|
+
.join('\n'),
|
|
673
|
+
progressName: { name: 'script', runid },
|
|
674
|
+
},
|
|
675
|
+
});
|
|
676
|
+
} else {
|
|
677
|
+
process.send({ msgtype: 'runnerDone', runid });
|
|
678
|
+
}
|
|
633
679
|
} finally {
|
|
634
680
|
process.chdir(originalCwd);
|
|
635
681
|
}
|
|
@@ -668,6 +714,7 @@ const messageHandlers = {
|
|
|
668
714
|
sqlSelect: handleSqlSelect,
|
|
669
715
|
exportKeys: handleExportKeys,
|
|
670
716
|
schemaList: handleSchemaList,
|
|
717
|
+
getStructure: handleGetStructure,
|
|
671
718
|
executeSessionQuery: handleExecuteSessionQuery,
|
|
672
719
|
evalJsonScript: handleEvalJsonScript,
|
|
673
720
|
multiCallMethod: handleMultiCallMethod,
|
|
@@ -290,14 +290,31 @@ async function handleExecuteReader({ jslid, sql, fileName }) {
|
|
|
290
290
|
|
|
291
291
|
const reader = await driver.readQuery(dbhan, sql);
|
|
292
292
|
|
|
293
|
+
let isFinished = false;
|
|
294
|
+
const finishReader = () => {
|
|
295
|
+
if (isFinished) return;
|
|
296
|
+
isFinished = true;
|
|
297
|
+
writer.close().then(() => {
|
|
298
|
+
process.send({ msgtype: 'done' });
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
|
|
293
302
|
reader.on('data', data => {
|
|
303
|
+
if (isFinished) return;
|
|
294
304
|
writer.rowFromReader(data);
|
|
295
305
|
});
|
|
296
|
-
reader.on('
|
|
297
|
-
|
|
298
|
-
|
|
306
|
+
reader.on('error', err => {
|
|
307
|
+
process.send({
|
|
308
|
+
msgtype: 'info',
|
|
309
|
+
info: {
|
|
310
|
+
message: extractErrorMessage(err),
|
|
311
|
+
severity: 'error',
|
|
312
|
+
time: new Date(),
|
|
313
|
+
},
|
|
299
314
|
});
|
|
315
|
+
finishReader();
|
|
300
316
|
});
|
|
317
|
+
reader.on('end', finishReader);
|
|
301
318
|
}
|
|
302
319
|
|
|
303
320
|
function handlePing() {
|
package/src/shell/copyStream.js
CHANGED
|
@@ -66,6 +66,7 @@ async function copyStream(input, output, options) {
|
|
|
66
66
|
}
|
|
67
67
|
} catch (err) {
|
|
68
68
|
logger.error(extractErrorLogData(err, { progressName }), 'DBGM-00157 Import/export job failed');
|
|
69
|
+
err.dbgateCopyStreamErrorReported = true;
|
|
69
70
|
|
|
70
71
|
process.send({
|
|
71
72
|
msgtype: 'copyStreamError',
|
|
@@ -84,7 +85,7 @@ async function copyStream(input, output, options) {
|
|
|
84
85
|
errorMessage: extractErrorMessage(err),
|
|
85
86
|
});
|
|
86
87
|
}
|
|
87
|
-
|
|
88
|
+
throw err;
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
91
|
|
package/src/storageModel.js
CHANGED
package/src/utility/envtools.js
CHANGED
|
@@ -3,6 +3,10 @@ const _ = require('lodash');
|
|
|
3
3
|
const { safeJsonParse, getDatabaseFileLabel } = require('dbgate-tools');
|
|
4
4
|
const crypto = require('crypto');
|
|
5
5
|
|
|
6
|
+
function isTrueValue(value) {
|
|
7
|
+
return [true, 1, '1', 'true'].includes(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
6
10
|
function extractConnectionsFromEnv(env) {
|
|
7
11
|
if (!env?.CONNECTIONS) {
|
|
8
12
|
return null;
|
|
@@ -34,6 +38,7 @@ function extractConnectionsFromEnv(env) {
|
|
|
34
38
|
: null),
|
|
35
39
|
singleDatabase: !!env[`DATABASE_${id}`] || !!env[`FILE_${id}`] || !!env[`APISERVERURL1_${id}`],
|
|
36
40
|
displayName: env[`LABEL_${id}`],
|
|
41
|
+
mcpEnabled: isTrueValue(env[`MCP_ENABLED_${id}`]),
|
|
37
42
|
isReadOnly: env[`READONLY_${id}`],
|
|
38
43
|
databases: env[`DBCONFIG_${id}`] ? safeJsonParse(env[`DBCONFIG_${id}`]) : null,
|
|
39
44
|
allowedDatabases: env[`ALLOWED_DATABASES_${id}`]?.replace(/\|/g, '\n'),
|
|
@@ -4,6 +4,7 @@ const databaseConnections = require('../controllers/databaseConnections');
|
|
|
4
4
|
const serverConnections = require('../controllers/serverConnections');
|
|
5
5
|
const sessions = require('../controllers/sessions');
|
|
6
6
|
const runners = require('../controllers/runners');
|
|
7
|
+
const { getLoggedUserCount } = require('./loginchecker');
|
|
7
8
|
|
|
8
9
|
async function getHealthStatus() {
|
|
9
10
|
const memory = process.memoryUsage();
|
|
@@ -13,7 +14,8 @@ async function getHealthStatus() {
|
|
|
13
14
|
status: 'ok',
|
|
14
15
|
databaseConnectionCount: databaseConnections.opened.length,
|
|
15
16
|
serverConnectionCount: serverConnections.opened.length,
|
|
16
|
-
|
|
17
|
+
querySessionCount: sessions.opened.length,
|
|
18
|
+
loggedUserCount: getLoggedUserCount(),
|
|
17
19
|
runProcessCount: runners.opened.length,
|
|
18
20
|
memory,
|
|
19
21
|
cpuUsage,
|
|
@@ -60,10 +60,17 @@ function markLoginAsLoggedOut(licenseUid) {
|
|
|
60
60
|
delete activeLoggedTokens[licenseUid];
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
function getLoggedUserCount() {
|
|
64
|
+
activeLoggedUsers = _.pickBy(activeLoggedUsers, value => value > Date.now());
|
|
65
|
+
activeLoggedTokens = _.pickBy(activeLoggedTokens, value => value > Date.now());
|
|
66
|
+
return _.uniq([...Object.keys(activeLoggedUsers), ...Object.keys(activeLoggedTokens)]).length;
|
|
67
|
+
}
|
|
68
|
+
|
|
63
69
|
module.exports = {
|
|
64
70
|
markUserAsActive,
|
|
65
71
|
markTokenAsLoggedIn,
|
|
66
72
|
isLoginLicensed,
|
|
67
73
|
LOGIN_LIMIT_ERROR,
|
|
68
74
|
markLoginAsLoggedOut,
|
|
75
|
+
getLoggedUserCount,
|
|
69
76
|
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const getExpressPath = require('./getExpressPath');
|
|
3
|
+
|
|
4
|
+
const MCP_ROLE_ID = -4;
|
|
5
|
+
const MCP_AUTH_MODES = ['token', 'oauth', 'none'];
|
|
6
|
+
|
|
7
|
+
function isLocalHostname(hostname) {
|
|
8
|
+
return ['localhost', '127.0.0.1', '::1'].includes(hostname.toLowerCase());
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function hashMcpToken(token) {
|
|
12
|
+
return crypto.createHash('sha256').update(String(token)).digest('hex');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isEnabledValue(value) {
|
|
16
|
+
return [true, 1, '1', 'true'].includes(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sanitizeMcpConfig(config) {
|
|
20
|
+
const authMode = MCP_AUTH_MODES.includes(config?.authMode) ? config.authMode : 'none';
|
|
21
|
+
return {
|
|
22
|
+
enabled: isEnabledValue(config?.enabled),
|
|
23
|
+
authMode,
|
|
24
|
+
tokenHash: config?.tokenHash || null,
|
|
25
|
+
tokenEncrypted: config?.tokenEncrypted || null,
|
|
26
|
+
tokenSuffix: config?.tokenSuffix || null,
|
|
27
|
+
tokenGeneratedAt: config?.tokenGeneratedAt || null,
|
|
28
|
+
oauthClientId: config?.oauthClientId || null,
|
|
29
|
+
oauthClientSecretHash: config?.oauthClientSecretHash || null,
|
|
30
|
+
oauthClientSecretEncrypted: config?.oauthClientSecretEncrypted || null,
|
|
31
|
+
oauthClientSecretSuffix: config?.oauthClientSecretSuffix || null,
|
|
32
|
+
oauthClientGeneratedAt: config?.oauthClientGeneratedAt || null,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function readMcpConfig() {
|
|
37
|
+
if (process.env.STORAGE_DATABASE) {
|
|
38
|
+
const storage = require('../controllers/storage');
|
|
39
|
+
return sanitizeMcpConfig((await storage.readConfig({ group: 'mcp' })) || {});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const token = process.env.MCP_TOKEN?.trim();
|
|
43
|
+
return {
|
|
44
|
+
enabled: !!token,
|
|
45
|
+
authMode: 'token',
|
|
46
|
+
tokenHash: token ? hashMcpToken(token) : null,
|
|
47
|
+
tokenEncrypted: null,
|
|
48
|
+
tokenSuffix: token ? token.slice(-6) : null,
|
|
49
|
+
tokenGeneratedAt: null,
|
|
50
|
+
oauthClientId: null,
|
|
51
|
+
oauthClientSecretHash: null,
|
|
52
|
+
oauthClientSecretEncrypted: null,
|
|
53
|
+
oauthClientSecretSuffix: null,
|
|
54
|
+
oauthClientGeneratedAt: null,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getPublicBaseUrl(req) {
|
|
59
|
+
const configuredUrl = process.env.MCP_PUBLIC_URL?.trim();
|
|
60
|
+
if (configuredUrl) {
|
|
61
|
+
let parsed;
|
|
62
|
+
try {
|
|
63
|
+
parsed = new URL(configuredUrl);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw new Error('DBGM-00000 MCP_PUBLIC_URL must be a valid HTTP(S) origin');
|
|
66
|
+
}
|
|
67
|
+
if (
|
|
68
|
+
!['http:', 'https:'].includes(parsed.protocol) ||
|
|
69
|
+
parsed.username ||
|
|
70
|
+
parsed.password ||
|
|
71
|
+
parsed.pathname !== '/' ||
|
|
72
|
+
parsed.search ||
|
|
73
|
+
parsed.hash
|
|
74
|
+
) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
'DBGM-00000 MCP_PUBLIC_URL must be an HTTP(S) origin without a path, credentials, query, or fragment'
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (parsed.protocol !== 'https:' && !isLocalHostname(parsed.hostname)) {
|
|
80
|
+
throw new Error('DBGM-00000 MCP_PUBLIC_URL must use HTTPS for a non-local origin');
|
|
81
|
+
}
|
|
82
|
+
return parsed.origin;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const protocol = req?.protocol || (req?.socket?.encrypted ? 'https' : 'http');
|
|
86
|
+
const host = req?.headers?.host || `localhost:${process.env.PORT || 3000}`;
|
|
87
|
+
if (!['http', 'https'].includes(protocol)) {
|
|
88
|
+
throw new Error('DBGM-00000 Could not determine a valid MCP public protocol');
|
|
89
|
+
}
|
|
90
|
+
let requestUrl;
|
|
91
|
+
try {
|
|
92
|
+
requestUrl = new URL(`${protocol}://${host}`);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
throw new Error('DBGM-00000 Could not determine a valid MCP public URL');
|
|
95
|
+
}
|
|
96
|
+
if (requestUrl.username || requestUrl.password || requestUrl.pathname !== '/' || requestUrl.search || requestUrl.hash) {
|
|
97
|
+
throw new Error('DBGM-00000 Could not determine a valid MCP public URL');
|
|
98
|
+
}
|
|
99
|
+
if (requestUrl.protocol !== 'https:' && !isLocalHostname(requestUrl.hostname)) {
|
|
100
|
+
throw new Error('DBGM-00000 MCP OAuth requires HTTPS for a non-local public URL');
|
|
101
|
+
}
|
|
102
|
+
return requestUrl.origin;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getMcpResourceUrl(req) {
|
|
106
|
+
return `${getPublicBaseUrl(req)}${getExpressPath('/mcp')}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function setMcpIdentity(req, details = {}) {
|
|
110
|
+
req.user = {
|
|
111
|
+
...details,
|
|
112
|
+
amoid: 'mcp',
|
|
113
|
+
roleId: MCP_ROLE_ID,
|
|
114
|
+
licenseUid: details.licenseUid || 'mcp',
|
|
115
|
+
login: details.login || 'mcp',
|
|
116
|
+
tokenUse: 'mcp',
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function safeTokenHashEquals(token, expectedHash) {
|
|
121
|
+
if (!token || !expectedHash) return false;
|
|
122
|
+
const actual = Buffer.from(hashMcpToken(token), 'hex');
|
|
123
|
+
const expected = Buffer.from(expectedHash, 'hex');
|
|
124
|
+
return actual.length === expected.length && crypto.timingSafeEqual(actual, expected);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
MCP_ROLE_ID,
|
|
129
|
+
MCP_AUTH_MODES,
|
|
130
|
+
hashMcpToken,
|
|
131
|
+
readMcpConfig,
|
|
132
|
+
sanitizeMcpConfig,
|
|
133
|
+
getPublicBaseUrl,
|
|
134
|
+
getMcpResourceUrl,
|
|
135
|
+
setMcpIdentity,
|
|
136
|
+
safeTokenHashEquals,
|
|
137
|
+
};
|
|
@@ -20,6 +20,268 @@ function getColumnMetadata(columnMetadata, columnName) {
|
|
|
20
20
|
return matchingKeys.length == 1 ? columnMetadata[matchingKeys[0]] : null;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
function namesEqual(left, right) {
|
|
24
|
+
return left != null && right != null && left.toLowerCase() == right.toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function findTable(dbinfo, schemaName, pureName) {
|
|
28
|
+
if (!dbinfo?.tables?.length || !pureName) return null;
|
|
29
|
+
const schemaMatches = table => !schemaName || namesEqual(table.schemaName, schemaName);
|
|
30
|
+
const exactTables = dbinfo.tables.filter(table => schemaMatches(table) && table.pureName == pureName);
|
|
31
|
+
if (exactTables.length == 1) return exactTables[0];
|
|
32
|
+
const matchingTables = dbinfo.tables.filter(table => schemaMatches(table) && namesEqual(table.pureName, pureName));
|
|
33
|
+
if (matchingTables.length == 1) return matchingTables[0];
|
|
34
|
+
const uniqueNameTables = dbinfo.tables.filter(table => namesEqual(table.pureName, pureName));
|
|
35
|
+
return uniqueNameTables.length == 1 ? uniqueNameTables[0] : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function findView(dbinfo, schemaName, pureName) {
|
|
39
|
+
const views = [...(dbinfo?.views || []), ...(dbinfo?.matviews || [])];
|
|
40
|
+
if (!views.length || !pureName) return null;
|
|
41
|
+
if (schemaName) {
|
|
42
|
+
const exactView = views.find(view => view.schemaName == schemaName && view.pureName == pureName);
|
|
43
|
+
if (exactView) return exactView;
|
|
44
|
+
const schemaViews = views.filter(view => namesEqual(view.schemaName, schemaName) && namesEqual(view.pureName, pureName));
|
|
45
|
+
if (schemaViews.length == 1) return schemaViews[0];
|
|
46
|
+
const schemaLessViews = views.filter(view => !view.schemaName && namesEqual(view.pureName, pureName));
|
|
47
|
+
return schemaLessViews.length == 1 ? schemaLessViews[0] : null;
|
|
48
|
+
}
|
|
49
|
+
const matchingViews = views.filter(view => namesEqual(view.pureName, pureName));
|
|
50
|
+
return matchingViews.length == 1 ? matchingViews[0] : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function extractSelectFromCreateView(createSql) {
|
|
54
|
+
if (!createSql) return null;
|
|
55
|
+
const match = createSql.match(/\bas\s+(select\b[\s\S]*)/i);
|
|
56
|
+
return match ? match[1] : createSql;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function unquoteIdentifier(identifier) {
|
|
60
|
+
const trimmed = identifier?.trim();
|
|
61
|
+
if (!trimmed) return null;
|
|
62
|
+
if (
|
|
63
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
64
|
+
(trimmed.startsWith('`') && trimmed.endsWith('`')) ||
|
|
65
|
+
(trimmed.startsWith('[') && trimmed.endsWith(']'))
|
|
66
|
+
) {
|
|
67
|
+
return trimmed.slice(1, -1).replace(/""/g, '"').replace(/``/g, '`').replace(/]]/g, ']');
|
|
68
|
+
}
|
|
69
|
+
return trimmed;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function extractCreateViewColumnNames(createSql) {
|
|
73
|
+
if (!createSql) return null;
|
|
74
|
+
const asMatch = createSql.match(/\bas\b/i);
|
|
75
|
+
if (!asMatch) return null;
|
|
76
|
+
const prefix = createSql.substring(0, asMatch.index);
|
|
77
|
+
const match = prefix.match(/\(([^()]*)\)\s*$/);
|
|
78
|
+
if (!match) return null;
|
|
79
|
+
const columns = match[1]
|
|
80
|
+
.split(',')
|
|
81
|
+
.map(column => unquoteIdentifier(column))
|
|
82
|
+
.filter(Boolean);
|
|
83
|
+
return columns.length > 0 ? columns : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getViewColumnNames(view) {
|
|
87
|
+
const analysedColumns = view?.columns?.map(column => column.columnName).filter(Boolean);
|
|
88
|
+
if (analysedColumns?.length) return analysedColumns;
|
|
89
|
+
return extractCreateViewColumnNames(view?.createSql);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isSelectStarFromSingleTable(selectSql) {
|
|
93
|
+
return /^\s*select\s+\*\s+from\b/i.test(selectSql || '');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getSelectListItemCount(selectSql) {
|
|
97
|
+
const selectMatch = selectSql?.match(/^\s*select\s+([\s\S]+?)\s+from\s/i);
|
|
98
|
+
return selectMatch ? splitTopLevelCommaList(selectMatch[1]).length : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const identifierPattern = '`(?:``|[^`])+`|"(?:""|[^"])+"|\\[[^\\]]+\\]|[A-Za-z_@$#][A-Za-z0-9_@$#]*';
|
|
102
|
+
const qualifiedIdentifierPattern = `(?:${identifierPattern})(?:\\s*\\.\\s*(?:${identifierPattern}))*`;
|
|
103
|
+
const qualifiedIdentifierOnlyRegex = new RegExp(`^\\s*${qualifiedIdentifierPattern}\\s*$`, 'i');
|
|
104
|
+
const aliasStopWordPattern =
|
|
105
|
+
'on|where|join|inner|left|right|full|outer|cross|straight_join|group|order|having|limit|union';
|
|
106
|
+
|
|
107
|
+
function splitTopLevelCommaList(text) {
|
|
108
|
+
const result = [];
|
|
109
|
+
let current = '';
|
|
110
|
+
let quote = null;
|
|
111
|
+
let depth = 0;
|
|
112
|
+
|
|
113
|
+
for (let index = 0; index < text.length; index++) {
|
|
114
|
+
const ch = text[index];
|
|
115
|
+
const next = text[index + 1];
|
|
116
|
+
|
|
117
|
+
if (quote) {
|
|
118
|
+
current += ch;
|
|
119
|
+
if (ch == quote) {
|
|
120
|
+
if ((quote == '"' || quote == '`') && next == quote) {
|
|
121
|
+
current += next;
|
|
122
|
+
index++;
|
|
123
|
+
} else {
|
|
124
|
+
quote = null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (ch == '"' || ch == '`' || ch == '[') {
|
|
131
|
+
quote = ch == '[' ? ']' : ch;
|
|
132
|
+
current += ch;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (ch == '(') depth++;
|
|
136
|
+
if (ch == ')' && depth > 0) depth--;
|
|
137
|
+
if (ch == ',' && depth == 0) {
|
|
138
|
+
result.push(current.trim());
|
|
139
|
+
current = '';
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
current += ch;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (current.trim()) result.push(current.trim());
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function extractIdentifierParts(text) {
|
|
150
|
+
const identifierRegex = new RegExp(identifierPattern, 'g');
|
|
151
|
+
return (text.match(identifierRegex) || []).map(identifier => unquoteIdentifier(identifier));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function extractMySqlStyleViewColumnMetadata(selectSql, dbinfo) {
|
|
155
|
+
if (!selectSql) return null;
|
|
156
|
+
const selectMatch = selectSql.match(/^\s*select\s+([\s\S]+?)\s+from\s/i);
|
|
157
|
+
if (!selectMatch) return null;
|
|
158
|
+
|
|
159
|
+
const tableByAlias = {};
|
|
160
|
+
const tableRegex = new RegExp(
|
|
161
|
+
`\\b(?:from|join)\\s+\\(*\\s*(${qualifiedIdentifierPattern})(?:\\s+(?:as\\s+)?(?!\\b(?:${aliasStopWordPattern})\\b)(${identifierPattern}))?`,
|
|
162
|
+
'gi'
|
|
163
|
+
);
|
|
164
|
+
let tableMatch;
|
|
165
|
+
while ((tableMatch = tableRegex.exec(selectSql))) {
|
|
166
|
+
const tableParts = extractIdentifierParts(tableMatch[1]);
|
|
167
|
+
const alias = unquoteIdentifier(tableMatch[2]);
|
|
168
|
+
if (!tableParts.length) continue;
|
|
169
|
+
const metadata = {
|
|
170
|
+
schemaName: tableParts.length >= 2 ? tableParts[tableParts.length - 2] : undefined,
|
|
171
|
+
tableName: tableParts[tableParts.length - 1],
|
|
172
|
+
};
|
|
173
|
+
if (alias) tableByAlias[alias.toLowerCase()] = metadata;
|
|
174
|
+
tableByAlias[metadata.tableName.toLowerCase()] = metadata;
|
|
175
|
+
}
|
|
176
|
+
if (Object.keys(tableByAlias).length == 0) return null;
|
|
177
|
+
|
|
178
|
+
const result = {};
|
|
179
|
+
for (const item of splitTopLevelCommaList(selectMatch[1])) {
|
|
180
|
+
const [sourceText, aliasText] = item.split(/\s+\bas\b\s+/i);
|
|
181
|
+
if (!qualifiedIdentifierOnlyRegex.test(sourceText)) continue;
|
|
182
|
+
const sourceParts = extractIdentifierParts(sourceText);
|
|
183
|
+
if (sourceParts.length < 2) continue;
|
|
184
|
+
const sourceColumnName = sourceParts[sourceParts.length - 1];
|
|
185
|
+
const qualifier = sourceParts[sourceParts.length - 2];
|
|
186
|
+
const table = tableByAlias[qualifier.toLowerCase()];
|
|
187
|
+
if (!table) continue;
|
|
188
|
+
const aliasParts = aliasText ? extractIdentifierParts(aliasText) : [];
|
|
189
|
+
const resultColumnName = aliasParts[0] || sourceColumnName;
|
|
190
|
+
result[resultColumnName] = resolveMetadataTable(
|
|
191
|
+
{
|
|
192
|
+
...table,
|
|
193
|
+
sourceColumnName,
|
|
194
|
+
},
|
|
195
|
+
dbinfo
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return Object.keys(result).length == 0 ? null : result;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function resolveMetadataTable(metadata, dbinfo) {
|
|
203
|
+
if (!metadata?.tableName) return metadata;
|
|
204
|
+
const table = findTable(dbinfo, metadata.schemaName, metadata.tableName);
|
|
205
|
+
if (!table) return metadata;
|
|
206
|
+
return {
|
|
207
|
+
...metadata,
|
|
208
|
+
tableName: table.pureName,
|
|
209
|
+
schemaName: table.schemaName,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function createViewColumnMetadata(view, dbinfo) {
|
|
214
|
+
const selectSql = extractSelectFromCreateView(view?.createSql);
|
|
215
|
+
if (!selectSql) return null;
|
|
216
|
+
const viewColumnNames = getViewColumnNames(view);
|
|
217
|
+
const columnMetadata = extractColumnMetadataFromSql(selectSql) || extractMySqlStyleViewColumnMetadata(selectSql, dbinfo);
|
|
218
|
+
|
|
219
|
+
if (columnMetadata && Object.keys(columnMetadata).length > 0) {
|
|
220
|
+
if (!viewColumnNames?.length) {
|
|
221
|
+
return Object.fromEntries(
|
|
222
|
+
Object.entries(columnMetadata).map(([columnName, metadata]) => [
|
|
223
|
+
columnName,
|
|
224
|
+
resolveMetadataTable(metadata, dbinfo),
|
|
225
|
+
])
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const metadataValues = Object.values(columnMetadata).map(metadata => resolveMetadataTable(metadata, dbinfo));
|
|
230
|
+
const allowPositionalFallback =
|
|
231
|
+
metadataValues.length == viewColumnNames.length && metadataValues.length == getSelectListItemCount(selectSql);
|
|
232
|
+
const result = {};
|
|
233
|
+
for (let index = 0; index < viewColumnNames.length; index++) {
|
|
234
|
+
const columnName = viewColumnNames[index];
|
|
235
|
+
const metadata = getColumnMetadata(columnMetadata, columnName);
|
|
236
|
+
const resolvedMetadata = metadata
|
|
237
|
+
? resolveMetadataTable(metadata, dbinfo)
|
|
238
|
+
: allowPositionalFallback
|
|
239
|
+
? metadataValues[index]
|
|
240
|
+
: null;
|
|
241
|
+
if (resolvedMetadata) result[columnName] = resolvedMetadata;
|
|
242
|
+
}
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!viewColumnNames?.length || !isSelectStarFromSingleTable(selectSql)) return columnMetadata;
|
|
247
|
+
const sourceTable = extractSingleTableFromSql(selectSql);
|
|
248
|
+
const table = findTable(dbinfo, sourceTable?.schemaName, sourceTable?.tableName);
|
|
249
|
+
if (!table?.columns?.length) return columnMetadata;
|
|
250
|
+
|
|
251
|
+
const result = {};
|
|
252
|
+
for (let index = 0; index < viewColumnNames.length; index++) {
|
|
253
|
+
const sourceColumn = table.columns[index];
|
|
254
|
+
if (!sourceColumn?.columnName) continue;
|
|
255
|
+
result[viewColumnNames[index]] = {
|
|
256
|
+
tableName: table.pureName,
|
|
257
|
+
schemaName: table.schemaName,
|
|
258
|
+
sourceColumnName: sourceColumn.columnName,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function resolveViewResultColumns(columns, dbinfo) {
|
|
265
|
+
if (!columns?.length) return columns;
|
|
266
|
+
if (!dbinfo?.views?.length && !dbinfo?.matviews?.length) return columns;
|
|
267
|
+
|
|
268
|
+
return columns.map(column => {
|
|
269
|
+
const view = findView(dbinfo, column.tableSchema, column.tableName);
|
|
270
|
+
if (!view?.createSql) return column;
|
|
271
|
+
|
|
272
|
+
const columnMetadata = createViewColumnMetadata(view, dbinfo);
|
|
273
|
+
const metadata = getColumnMetadata(columnMetadata, column.columnName);
|
|
274
|
+
if (!metadata) return column;
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
...column,
|
|
278
|
+
tableName: metadata.tableName,
|
|
279
|
+
tableSchema: metadata.schemaName,
|
|
280
|
+
sourceColumnName: metadata.sourceColumnName,
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
23
285
|
async function enrichQueryResultColumns({ columns, sql, driver, dbhan, dbinfo, onNativeMetadataError, onFallbackMetadataError }) {
|
|
24
286
|
if (!columns?.length || !driver?.databaseEngineTypes?.includes('sql') || !driver?.supportsEditableQueryResults) {
|
|
25
287
|
return columns;
|
|
@@ -27,7 +289,7 @@ async function enrichQueryResultColumns({ columns, sql, driver, dbhan, dbinfo, o
|
|
|
27
289
|
|
|
28
290
|
if (driver.enrichColumnMetadata) {
|
|
29
291
|
try {
|
|
30
|
-
const enriched = await driver.enrichColumnMetadata(dbhan, sql, columns, dbinfo);
|
|
292
|
+
const enriched = resolveViewResultColumns(await driver.enrichColumnMetadata(dbhan, sql, columns, dbinfo), dbinfo);
|
|
31
293
|
if (enriched?.every(column => column.tableName && column.sourceColumnName)) return enriched;
|
|
32
294
|
if (enriched?.length == columns.length) columns = enriched;
|
|
33
295
|
} catch (err) {
|
|
@@ -38,7 +300,7 @@ async function enrichQueryResultColumns({ columns, sql, driver, dbhan, dbinfo, o
|
|
|
38
300
|
try {
|
|
39
301
|
const columnMetadata = extractColumnMetadataFromSql(sql);
|
|
40
302
|
if (columnMetadata) {
|
|
41
|
-
return columns.map(column => {
|
|
303
|
+
return resolveViewResultColumns(columns.map(column => {
|
|
42
304
|
const metadata = getColumnMetadata(columnMetadata, column.columnName);
|
|
43
305
|
return {
|
|
44
306
|
...column,
|
|
@@ -46,19 +308,19 @@ async function enrichQueryResultColumns({ columns, sql, driver, dbhan, dbinfo, o
|
|
|
46
308
|
tableSchema: column.tableSchema || metadata?.schemaName,
|
|
47
309
|
sourceColumnName: column.sourceColumnName || metadata?.sourceColumnName,
|
|
48
310
|
};
|
|
49
|
-
});
|
|
311
|
+
}), dbinfo);
|
|
50
312
|
}
|
|
51
313
|
|
|
52
314
|
const table = extractSingleTableFromSql(sql);
|
|
53
315
|
if (!table) return columns;
|
|
54
316
|
const columnSources = extractColumnSourcesFromSql(sql);
|
|
55
317
|
|
|
56
|
-
return columns.map(column => ({
|
|
318
|
+
return resolveViewResultColumns(columns.map(column => ({
|
|
57
319
|
...column,
|
|
58
320
|
tableName: column.tableName || table.tableName,
|
|
59
321
|
tableSchema: column.tableSchema || table.schemaName,
|
|
60
322
|
sourceColumnName: column.sourceColumnName || getColumnSourceName(columnSources, column.columnName) || column.columnName,
|
|
61
|
-
}));
|
|
323
|
+
})), dbinfo);
|
|
62
324
|
} catch (err) {
|
|
63
325
|
onFallbackMetadataError?.(err);
|
|
64
326
|
return columns;
|
|
@@ -67,4 +329,5 @@ async function enrichQueryResultColumns({ columns, sql, driver, dbhan, dbinfo, o
|
|
|
67
329
|
|
|
68
330
|
module.exports = {
|
|
69
331
|
enrichQueryResultColumns,
|
|
332
|
+
resolveViewResultColumns,
|
|
70
333
|
};
|
|
@@ -31,6 +31,18 @@ module.exports = [
|
|
|
31
31
|
updateExisting: true,
|
|
32
32
|
matchColumns: ['conid'],
|
|
33
33
|
},
|
|
34
|
+
{
|
|
35
|
+
name: 'team_folders',
|
|
36
|
+
findExisting: true,
|
|
37
|
+
createNew: true,
|
|
38
|
+
updateExisting: true,
|
|
39
|
+
matchColumns: ['folder_name'],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'team_files',
|
|
43
|
+
createNew: true,
|
|
44
|
+
//matchColumns: ['file_name', 'team_folder_id'],
|
|
45
|
+
},
|
|
34
46
|
{
|
|
35
47
|
name: 'role_connections',
|
|
36
48
|
findExisting: true,
|