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.
@@ -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
- const evalWriter = new ScriptWriterEval(dbgateApi, requirePlugin, dbhan, runid);
631
- await playJsonScriptWriter(script, evalWriter);
632
- process.send({ msgtype: 'runnerDone', runid });
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('end', () => {
297
- writer.close(() => {
298
- process.send({ msgtype: 'done' });
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() {
@@ -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
- // throw err;
88
+ throw err;
88
89
  }
89
90
  }
90
91
 
@@ -1047,6 +1047,10 @@ module.exports = {
1047
1047
  {
1048
1048
  "id": -3,
1049
1049
  "name": "superadmin"
1050
+ },
1051
+ {
1052
+ "id": -4,
1053
+ "name": "mcp"
1050
1054
  }
1051
1055
  ]
1052
1056
  },
@@ -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
- sessionCount: sessions.opened.length,
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,