dbgate-api-premium 7.2.0 → 7.2.1
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/controllers/config.js +17 -0
- package/src/controllers/databaseConnections.js +9 -2
- package/src/controllers/storage.js +46 -0
- package/src/currentVersion.js +2 -2
- package/src/proc/databaseConnectionProcess.js +40 -3
- package/src/proc/sessionProcess.js +20 -3
- package/src/shell/copyStream.js +2 -1
- package/src/utility/healthStatus.js +3 -1
- package/src/utility/loginchecker.js +7 -0
- package/src/utility/queryResultMetadata.js +268 -5
- package/src/utility/storageReplicatorItems.js +12 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dbgate-api-premium",
|
|
3
3
|
"main": "src/index.js",
|
|
4
|
-
"version": "7.2.
|
|
4
|
+
"version": "7.2.1",
|
|
5
5
|
"homepage": "https://www.dbgate.io/",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
"compare-versions": "^3.6.0",
|
|
31
31
|
"cors": "^2.8.5",
|
|
32
32
|
"cross-env": "^6.0.3",
|
|
33
|
-
"dbgate-datalib": "7.2.
|
|
33
|
+
"dbgate-datalib": "7.2.1",
|
|
34
34
|
"dbgate-query-splitter": "^4.12.0",
|
|
35
|
-
"dbgate-rest": "7.2.
|
|
36
|
-
"dbgate-sqltree": "7.2.
|
|
37
|
-
"dbgate-tools": "7.2.
|
|
35
|
+
"dbgate-rest": "7.2.1",
|
|
36
|
+
"dbgate-sqltree": "7.2.1",
|
|
37
|
+
"dbgate-tools": "7.2.1",
|
|
38
38
|
"debug": "^4.3.4",
|
|
39
39
|
"diff": "^5.0.0",
|
|
40
40
|
"diff2html": "^3.4.13",
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"@types/fs-extra": "^9.0.11",
|
|
97
97
|
"@types/jest": "^30.0.0",
|
|
98
98
|
"@types/lodash": "^4.14.149",
|
|
99
|
-
"dbgate-types": "7.2.
|
|
99
|
+
"dbgate-types": "7.2.1",
|
|
100
100
|
"env-cmd": "^10.1.0",
|
|
101
101
|
"jest": "^30.4.2",
|
|
102
102
|
"jsdoc-to-markdown": "^9.0.5",
|
|
@@ -16,6 +16,7 @@ const connections = require('../controllers/connections');
|
|
|
16
16
|
const { getAuthProviderFromReq } = require('../auth/authProvider');
|
|
17
17
|
const { checkLicense, checkLicenseKey } = require('../utility/checkLicense');
|
|
18
18
|
const storage = require('./storage');
|
|
19
|
+
const dbgateApi = require('../shell');
|
|
19
20
|
const { getAuthProxyUrl, tryToGetRefreshedLicense } = require('../utility/authProxy');
|
|
20
21
|
const { getPublicHardwareFingerprint } = require('../utility/hardwareFingerprint');
|
|
21
22
|
const { extractErrorMessage } = require('dbgate-tools');
|
|
@@ -438,4 +439,20 @@ module.exports = {
|
|
|
438
439
|
|
|
439
440
|
return true;
|
|
440
441
|
},
|
|
442
|
+
|
|
443
|
+
createConnectionsAndSettingsZip_meta: true,
|
|
444
|
+
async createConnectionsAndSettingsZip({ db, filePath }, req) {
|
|
445
|
+
const loadedPermissions = await loadPermissionsFromRequest(req);
|
|
446
|
+
if (!hasPermission(`admin/config`, loadedPermissions)) {
|
|
447
|
+
throw new Error('Permission denied: admin/config');
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (connections.portalConnections) {
|
|
451
|
+
throw new Error('Not allowed');
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const exportDb = process.env.STORAGE_DATABASE ? await storage.fillTeamFileContentForExport(db) : db;
|
|
455
|
+
await dbgateApi.zipJsonLinesData(exportDb, filePath);
|
|
456
|
+
return true;
|
|
457
|
+
},
|
|
441
458
|
};
|
|
@@ -157,8 +157,15 @@ module.exports = {
|
|
|
157
157
|
|
|
158
158
|
handle_copyStreamError(conid, database, { copyStreamError }) {
|
|
159
159
|
const { progressName } = copyStreamError;
|
|
160
|
-
const
|
|
161
|
-
logger.error(
|
|
160
|
+
const runid = progressName?.runid;
|
|
161
|
+
logger.error({ conid, database, copyStreamError }, 'DBGM-00000 Error in database connection copy stream');
|
|
162
|
+
if (!runid) return;
|
|
163
|
+
if (copyStreamError.dbgateCopyStreamErrorReported) return;
|
|
164
|
+
socket.emit(`runner-progress-${runid}`, {
|
|
165
|
+
progressName: progressName?.name,
|
|
166
|
+
status: 'error',
|
|
167
|
+
errorMessage: copyStreamError.message,
|
|
168
|
+
});
|
|
162
169
|
socket.emit(`runner-done-${runid}`);
|
|
163
170
|
},
|
|
164
171
|
|
|
@@ -1091,6 +1091,18 @@ module.exports = {
|
|
|
1091
1091
|
const auth_methods_config = await storageSelectFmt(`select * from ~auth_methods_config`);
|
|
1092
1092
|
const role_permissions = await storageSelectFmt(`select * from ~role_permissions`);
|
|
1093
1093
|
const roles = await storageSelectFmt(`select * from ~roles`);
|
|
1094
|
+
const team_folders = await storageSelectFmt(`select * from ~team_folders`);
|
|
1095
|
+
const team_files = (
|
|
1096
|
+
await storageSelectFmt(
|
|
1097
|
+
`select ~team_files.~id, ~team_files.~file_type_id, ~team_file_types.~name as ~type_name, ~team_files.~file_name,
|
|
1098
|
+
~team_files.~owner_user_id, ~team_files.~metadata, ~team_files.~team_folder_id
|
|
1099
|
+
from ~team_files
|
|
1100
|
+
inner join ~team_file_types on ~team_files.~file_type_id = ~team_file_types.~id`
|
|
1101
|
+
)
|
|
1102
|
+
).map(file => ({
|
|
1103
|
+
...file,
|
|
1104
|
+
owner_user_id: null,
|
|
1105
|
+
}));
|
|
1094
1106
|
|
|
1095
1107
|
return {
|
|
1096
1108
|
connections,
|
|
@@ -1104,6 +1116,34 @@ module.exports = {
|
|
|
1104
1116
|
auth_methods_config,
|
|
1105
1117
|
role_permissions,
|
|
1106
1118
|
roles,
|
|
1119
|
+
team_folders,
|
|
1120
|
+
team_files,
|
|
1121
|
+
};
|
|
1122
|
+
},
|
|
1123
|
+
|
|
1124
|
+
async fillTeamFileContentForExport(db) {
|
|
1125
|
+
if (!db.team_files?.length) {
|
|
1126
|
+
return db;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
const teamFileIds = _.uniq(db.team_files.map(file => file.id).filter(id => id != null));
|
|
1130
|
+
if (teamFileIds.length == 0) {
|
|
1131
|
+
return db;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
const fileContentRows = await storageSelectFmt(
|
|
1135
|
+
`select ~id, ~file_content from ~team_files where ~id in (%,v)`,
|
|
1136
|
+
teamFileIds
|
|
1137
|
+
);
|
|
1138
|
+
const fileContentById = _.keyBy(fileContentRows, 'id');
|
|
1139
|
+
|
|
1140
|
+
return {
|
|
1141
|
+
...db,
|
|
1142
|
+
team_files: db.team_files.map(file => ({
|
|
1143
|
+
..._.omit(file, ['type_name']),
|
|
1144
|
+
owner_user_id: null,
|
|
1145
|
+
file_content: fileContentById[file.id]?.file_content ?? file.file_content,
|
|
1146
|
+
})),
|
|
1107
1147
|
};
|
|
1108
1148
|
},
|
|
1109
1149
|
|
|
@@ -1121,6 +1161,12 @@ module.exports = {
|
|
|
1121
1161
|
.filter(x => x.jsonArray),
|
|
1122
1162
|
});
|
|
1123
1163
|
socket.emitChanged('connection-list-changed');
|
|
1164
|
+
if (db.team_files) {
|
|
1165
|
+
socket.emitChanged('team-files-changed');
|
|
1166
|
+
}
|
|
1167
|
+
if (db.team_folders) {
|
|
1168
|
+
socket.emitChanged('team-folders-changed');
|
|
1169
|
+
}
|
|
1124
1170
|
},
|
|
1125
1171
|
|
|
1126
1172
|
async getUsedEngines() {
|
package/src/currentVersion.js
CHANGED
|
@@ -623,13 +623,50 @@ async function handleEvalJsonScript({ script, runid }) {
|
|
|
623
623
|
const directory = path.join(rundir(), runid);
|
|
624
624
|
fs.mkdirSync(directory);
|
|
625
625
|
const originalCwd = process.cwd();
|
|
626
|
+
let scriptError = null;
|
|
627
|
+
let finalizerError = null;
|
|
626
628
|
|
|
627
629
|
try {
|
|
628
630
|
process.chdir(directory);
|
|
629
631
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
632
|
+
try {
|
|
633
|
+
const evalWriter = new ScriptWriterEval(dbgateApi, requirePlugin, dbhan, runid);
|
|
634
|
+
await playJsonScriptWriter(script, evalWriter);
|
|
635
|
+
} catch (err) {
|
|
636
|
+
scriptError = err;
|
|
637
|
+
} finally {
|
|
638
|
+
try {
|
|
639
|
+
await dbgateApi.finalizer.run();
|
|
640
|
+
} catch (err) {
|
|
641
|
+
finalizerError = err;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const shouldReportScriptError = scriptError && !scriptError.dbgateCopyStreamErrorReported;
|
|
646
|
+
|
|
647
|
+
if (shouldReportScriptError || finalizerError) {
|
|
648
|
+
if (shouldReportScriptError) {
|
|
649
|
+
logger.error(extractErrorLogData(scriptError), 'DBGM-00000 Error running JSON script on database connection');
|
|
650
|
+
}
|
|
651
|
+
if (finalizerError) {
|
|
652
|
+
logger.error(extractErrorLogData(finalizerError), 'DBGM-00000 Error running JSON script finalizers');
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
process.send({
|
|
656
|
+
msgtype: 'copyStreamError',
|
|
657
|
+
copyStreamError: {
|
|
658
|
+
message: [
|
|
659
|
+
shouldReportScriptError && extractErrorMessage(scriptError),
|
|
660
|
+
finalizerError && `Finalizer failed: ${extractErrorMessage(finalizerError)}`,
|
|
661
|
+
]
|
|
662
|
+
.filter(Boolean)
|
|
663
|
+
.join('\n'),
|
|
664
|
+
progressName: { name: 'script', runid },
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
} else {
|
|
668
|
+
process.send({ msgtype: 'runnerDone', runid });
|
|
669
|
+
}
|
|
633
670
|
} finally {
|
|
634
671
|
process.chdir(originalCwd);
|
|
635
672
|
}
|
|
@@ -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
|
|
|
@@ -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
|
};
|
|
@@ -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,
|