dbgate-api 7.1.3-alpha.3 → 7.1.4-alpha.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/databaseConnections.js +40 -8
- package/src/controllers/files.js +38 -4
- package/src/controllers/sessions.js +13 -0
- package/src/currentVersion.js +2 -2
- package/src/proc/databaseConnectionProcess.js +4 -4
- package/src/proc/sessionProcess.js +33 -0
- package/src/storageModel.js +24 -0
- package/src/utility/connectUtility.js +29 -1
- package/src/utility/crypting.js +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dbgate-api",
|
|
3
3
|
"main": "src/index.js",
|
|
4
|
-
"version": "7.1.
|
|
4
|
+
"version": "7.1.4-alpha.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.1.
|
|
33
|
+
"dbgate-datalib": "7.1.4-alpha.1",
|
|
34
34
|
"dbgate-query-splitter": "^4.12.0",
|
|
35
|
-
"dbgate-rest": "7.1.
|
|
36
|
-
"dbgate-sqltree": "7.1.
|
|
37
|
-
"dbgate-tools": "7.1.
|
|
35
|
+
"dbgate-rest": "7.1.4-alpha.1",
|
|
36
|
+
"dbgate-sqltree": "7.1.4-alpha.1",
|
|
37
|
+
"dbgate-tools": "7.1.4-alpha.1",
|
|
38
38
|
"debug": "^4.3.4",
|
|
39
39
|
"diff": "^5.0.0",
|
|
40
40
|
"diff2html": "^3.4.13",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"devDependencies": {
|
|
89
89
|
"@types/fs-extra": "^9.0.11",
|
|
90
90
|
"@types/lodash": "^4.14.149",
|
|
91
|
-
"dbgate-types": "7.1.
|
|
91
|
+
"dbgate-types": "7.1.4-alpha.1",
|
|
92
92
|
"env-cmd": "^10.1.0",
|
|
93
93
|
"jsdoc-to-markdown": "^9.0.5",
|
|
94
94
|
"node-loader": "^1.0.2",
|
|
@@ -95,10 +95,12 @@ module.exports = {
|
|
|
95
95
|
}
|
|
96
96
|
},
|
|
97
97
|
handle_response(conid, database, { msgid, ...response }) {
|
|
98
|
-
const [resolve, reject, additionalData] = this.requests[msgid];
|
|
99
|
-
resolve
|
|
100
|
-
|
|
101
|
-
additionalData?.auditLogger
|
|
98
|
+
const [resolve, reject, additionalData] = this.requests[msgid] || [];
|
|
99
|
+
if (resolve) {
|
|
100
|
+
resolve(response);
|
|
101
|
+
if (additionalData?.auditLogger) {
|
|
102
|
+
additionalData?.auditLogger(response);
|
|
103
|
+
}
|
|
102
104
|
}
|
|
103
105
|
delete this.requests[msgid];
|
|
104
106
|
},
|
|
@@ -239,7 +241,7 @@ module.exports = {
|
|
|
239
241
|
sendRequest(conn, message, additionalData = {}) {
|
|
240
242
|
const msgid = crypto.randomUUID();
|
|
241
243
|
const promise = new Promise((resolve, reject) => {
|
|
242
|
-
this.requests[msgid] = [resolve, reject, additionalData];
|
|
244
|
+
this.requests[msgid] = [resolve, reject, additionalData, conn.conid, conn.database];
|
|
243
245
|
try {
|
|
244
246
|
const serializedMessage = serializeJsTypesForJsonStringify({ msgid, ...message });
|
|
245
247
|
conn.subprocess.send(serializedMessage);
|
|
@@ -264,12 +266,12 @@ module.exports = {
|
|
|
264
266
|
},
|
|
265
267
|
|
|
266
268
|
sqlSelect_meta: true,
|
|
267
|
-
async sqlSelect({ conid, database, select, auditLogSessionGroup }, req) {
|
|
269
|
+
async sqlSelect({ conid, database, select, commandTimeout, auditLogSessionGroup }, req) {
|
|
268
270
|
await testConnectionPermission(conid, req);
|
|
269
271
|
const opened = await this.ensureOpened(conid, database);
|
|
270
272
|
const res = await this.sendRequest(
|
|
271
273
|
opened,
|
|
272
|
-
{ msgtype: 'sqlSelect', select },
|
|
274
|
+
{ msgtype: 'sqlSelect', select, commandTimeout },
|
|
273
275
|
{
|
|
274
276
|
auditLogger:
|
|
275
277
|
auditLogSessionGroup && select?.from?.name?.pureName
|
|
@@ -344,9 +346,12 @@ module.exports = {
|
|
|
344
346
|
},
|
|
345
347
|
|
|
346
348
|
collectionData_meta: true,
|
|
347
|
-
async collectionData({ conid, database, options, auditLogSessionGroup }, req) {
|
|
349
|
+
async collectionData({ conid, database, options, commandTimeout, auditLogSessionGroup }, req) {
|
|
348
350
|
await testConnectionPermission(conid, req);
|
|
349
351
|
const opened = await this.ensureOpened(conid, database);
|
|
352
|
+
if (commandTimeout && options) {
|
|
353
|
+
options.commandTimeout = commandTimeout;
|
|
354
|
+
}
|
|
350
355
|
const res = await this.sendRequest(
|
|
351
356
|
opened,
|
|
352
357
|
{ msgtype: 'collectionData', options },
|
|
@@ -580,6 +585,24 @@ module.exports = {
|
|
|
580
585
|
};
|
|
581
586
|
},
|
|
582
587
|
|
|
588
|
+
pingDatabases_meta: true,
|
|
589
|
+
async pingDatabases({ databases }, req) {
|
|
590
|
+
if (!databases || !Array.isArray(databases)) return { status: 'ok' };
|
|
591
|
+
for (const { conid, database } of databases) {
|
|
592
|
+
if (!conid || !database) continue;
|
|
593
|
+
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
|
594
|
+
if (existing) {
|
|
595
|
+
try {
|
|
596
|
+
existing.subprocess.send({ msgtype: 'ping' });
|
|
597
|
+
} catch (err) {
|
|
598
|
+
logger.error(extractErrorLogData(err), 'DBGM-00308 Error pinging DB connection');
|
|
599
|
+
this.close(conid, database);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return { status: 'ok' };
|
|
604
|
+
},
|
|
605
|
+
|
|
583
606
|
refresh_meta: true,
|
|
584
607
|
async refresh({ conid, database, keepOpen }, req) {
|
|
585
608
|
await testConnectionPermission(conid, req);
|
|
@@ -622,6 +645,15 @@ module.exports = {
|
|
|
622
645
|
structure: existing.structure,
|
|
623
646
|
};
|
|
624
647
|
socket.emitChanged(`database-status-changed`, { conid, database });
|
|
648
|
+
|
|
649
|
+
// Reject all pending requests for this connection
|
|
650
|
+
for (const [msgid, entry] of Object.entries(this.requests)) {
|
|
651
|
+
const [resolve, reject, additionalData, reqConid, reqDatabase] = entry;
|
|
652
|
+
if (reqConid === conid && reqDatabase === database) {
|
|
653
|
+
reject('DBGM-00309 Database connection closed');
|
|
654
|
+
delete this.requests[msgid];
|
|
655
|
+
}
|
|
656
|
+
}
|
|
625
657
|
}
|
|
626
658
|
},
|
|
627
659
|
|
package/src/controllers/files.js
CHANGED
|
@@ -15,7 +15,8 @@ const getDiagramExport = require('../utility/getDiagramExport');
|
|
|
15
15
|
const apps = require('./apps');
|
|
16
16
|
const getMapExport = require('../utility/getMapExport');
|
|
17
17
|
const dbgateApi = require('../shell');
|
|
18
|
-
const { getLogger } = require('dbgate-tools');
|
|
18
|
+
const { getLogger, getSqlFrontMatter } = require('dbgate-tools');
|
|
19
|
+
const yaml = require('js-yaml');
|
|
19
20
|
const platformInfo = require('../utility/platformInfo');
|
|
20
21
|
const { checkSecureFilePathsWithoutDirectory, checkSecureDirectories } = require('../utility/security');
|
|
21
22
|
const { copyAppLogsIntoFile, getRecentAppLogRecords } = require('../utility/appLogStore');
|
|
@@ -35,13 +36,46 @@ function deserialize(format, text) {
|
|
|
35
36
|
|
|
36
37
|
module.exports = {
|
|
37
38
|
list_meta: true,
|
|
38
|
-
async list({ folder }, req) {
|
|
39
|
+
async list({ folder, parseFrontMatter }, req) {
|
|
39
40
|
const loadedPermissions = await loadPermissionsFromRequest(req);
|
|
40
41
|
if (!hasPermission(`files/${folder}/read`, loadedPermissions)) return [];
|
|
41
42
|
const dir = path.join(filesdir(), folder);
|
|
42
43
|
if (!(await fs.exists(dir))) return [];
|
|
43
|
-
const
|
|
44
|
-
|
|
44
|
+
const fileNames = await fs.readdir(dir);
|
|
45
|
+
if (!parseFrontMatter) {
|
|
46
|
+
return fileNames.map(file => ({ folder, file }));
|
|
47
|
+
}
|
|
48
|
+
const result = [];
|
|
49
|
+
for (const file of fileNames) {
|
|
50
|
+
const item = { folder, file };
|
|
51
|
+
let fh;
|
|
52
|
+
try {
|
|
53
|
+
fh = await require('fs').promises.open(path.join(dir, file), 'r');
|
|
54
|
+
const buf = new Uint8Array(512);
|
|
55
|
+
const { bytesRead } = await fh.read(buf, 0, 512, 0);
|
|
56
|
+
let text = Buffer.from(buf.buffer, 0, bytesRead).toString('utf-8');
|
|
57
|
+
|
|
58
|
+
if (text.includes('-- >>>') && !text.includes('-- <<<')) {
|
|
59
|
+
const stat = await fh.stat();
|
|
60
|
+
const fullSize = Math.min(stat.size, 4096);
|
|
61
|
+
if (fullSize > 512) {
|
|
62
|
+
const fullBuf = new Uint8Array(fullSize);
|
|
63
|
+
const { bytesRead: fullBytesRead } = await fh.read(fullBuf, 0, fullSize, 0);
|
|
64
|
+
text = Buffer.from(fullBuf.buffer, 0, fullBytesRead).toString('utf-8');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const fm = getSqlFrontMatter(text, yaml);
|
|
69
|
+
if (fm?.connectionId) item.connectionId = fm.connectionId;
|
|
70
|
+
if (fm?.databaseName) item.databaseName = fm.databaseName;
|
|
71
|
+
} catch (e) {
|
|
72
|
+
// ignore read errors for individual files
|
|
73
|
+
} finally {
|
|
74
|
+
if (fh) await fh.close().catch(() => {});
|
|
75
|
+
}
|
|
76
|
+
result.push(item);
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
45
79
|
},
|
|
46
80
|
|
|
47
81
|
listAll_meta: true,
|
|
@@ -228,6 +228,19 @@ module.exports = {
|
|
|
228
228
|
return { state: 'ok' };
|
|
229
229
|
},
|
|
230
230
|
|
|
231
|
+
setIsolationLevel_meta: true,
|
|
232
|
+
async setIsolationLevel({ sesid, level }) {
|
|
233
|
+
const session = this.opened.find(x => x.sesid == sesid);
|
|
234
|
+
if (!session) {
|
|
235
|
+
throw new Error('Invalid session');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
logger.info({ sesid, level }, 'DBGM-00315 Setting transaction isolation level');
|
|
239
|
+
session.subprocess.send({ msgtype: 'setIsolationLevel', level });
|
|
240
|
+
|
|
241
|
+
return { state: 'ok' };
|
|
242
|
+
},
|
|
243
|
+
|
|
231
244
|
executeReader_meta: true,
|
|
232
245
|
async executeReader({ conid, database, sql, queryName, appFolder }) {
|
|
233
246
|
const { sesid } = await this.create({ conid, database });
|
package/src/currentVersion.js
CHANGED
|
@@ -234,12 +234,12 @@ async function handleRunOperation({ msgid, operation, useTransaction }, skipRead
|
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
async function handleQueryData({ msgid, sql, range }, skipReadonlyCheck = false) {
|
|
237
|
+
async function handleQueryData({ msgid, sql, range, commandTimeout }, skipReadonlyCheck = false) {
|
|
238
238
|
await waitConnected();
|
|
239
239
|
const driver = requireEngineDriver(storedConnection);
|
|
240
240
|
try {
|
|
241
241
|
if (!skipReadonlyCheck) ensureExecuteCustomScript(driver);
|
|
242
|
-
const res = await driver.query(dbhan, sql, { range });
|
|
242
|
+
const res = await driver.query(dbhan, sql, { range, commandTimeout });
|
|
243
243
|
process.send({ msgtype: 'response', msgid, ...serializeJsTypesForJsonStringify(res) });
|
|
244
244
|
} catch (err) {
|
|
245
245
|
process.send({
|
|
@@ -250,11 +250,11 @@ async function handleQueryData({ msgid, sql, range }, skipReadonlyCheck = false)
|
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
async function handleSqlSelect({ msgid, select }) {
|
|
253
|
+
async function handleSqlSelect({ msgid, select, commandTimeout }) {
|
|
254
254
|
const driver = requireEngineDriver(storedConnection);
|
|
255
255
|
const dmp = driver.createDumper();
|
|
256
256
|
dumpSqlSelect(dmp, select);
|
|
257
|
-
return handleQueryData({ msgid, sql: dmp.s, range: select.range }, true);
|
|
257
|
+
return handleQueryData({ msgid, sql: dmp.s, range: select.range, commandTimeout }, true);
|
|
258
258
|
}
|
|
259
259
|
|
|
260
260
|
async function handleDriverDataCore(msgid, callMethod, { logName }) {
|
|
@@ -77,6 +77,38 @@ async function handleStopProfiler({ jslid }) {
|
|
|
77
77
|
currentProfiler = null;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
async function handleSetIsolationLevel({ level }) {
|
|
81
|
+
lastActivity = new Date().getTime();
|
|
82
|
+
|
|
83
|
+
await waitConnected();
|
|
84
|
+
const driver = requireEngineDriver(storedConnection);
|
|
85
|
+
|
|
86
|
+
if (!driver.setTransactionIsolationLevel) {
|
|
87
|
+
process.send({ msgtype: 'done', skipFinishedMessage: true });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (driver.isolationLevels && level && !driver.isolationLevels.includes(level)) {
|
|
92
|
+
process.send({
|
|
93
|
+
msgtype: 'info',
|
|
94
|
+
info: {
|
|
95
|
+
message: `Isolation level "${level}" is not supported by this driver. Supported levels: ${driver.isolationLevels.join(', ')}`,
|
|
96
|
+
severity: 'error',
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
process.send({ msgtype: 'done', skipFinishedMessage: true });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
executingScripts++;
|
|
104
|
+
try {
|
|
105
|
+
await driver.setTransactionIsolationLevel(dbhan, level);
|
|
106
|
+
process.send({ msgtype: 'done', controlCommand: 'setIsolationLevel' });
|
|
107
|
+
} finally {
|
|
108
|
+
executingScripts--;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
80
112
|
async function handleExecuteControlCommand({ command }) {
|
|
81
113
|
lastActivity = new Date().getTime();
|
|
82
114
|
|
|
@@ -210,6 +242,7 @@ const messageHandlers = {
|
|
|
210
242
|
connect: handleConnect,
|
|
211
243
|
executeQuery: handleExecuteQuery,
|
|
212
244
|
executeControlCommand: handleExecuteControlCommand,
|
|
245
|
+
setIsolationLevel: handleSetIsolationLevel,
|
|
213
246
|
executeReader: handleExecuteReader,
|
|
214
247
|
startProfiler: handleStartProfiler,
|
|
215
248
|
stopProfiler: handleStopProfiler,
|
package/src/storageModel.js
CHANGED
|
@@ -698,6 +698,30 @@ module.exports = {
|
|
|
698
698
|
"columnName": "id_original",
|
|
699
699
|
"dataType": "varchar(250)",
|
|
700
700
|
"notNull": false
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
"pureName": "connections",
|
|
704
|
+
"columnName": "httpProxyUrl",
|
|
705
|
+
"dataType": "varchar(250)",
|
|
706
|
+
"notNull": false
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
"pureName": "connections",
|
|
710
|
+
"columnName": "httpProxyUser",
|
|
711
|
+
"dataType": "varchar(250)",
|
|
712
|
+
"notNull": false
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
"pureName": "connections",
|
|
716
|
+
"columnName": "httpProxyPassword",
|
|
717
|
+
"dataType": "varchar(250)",
|
|
718
|
+
"notNull": false
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
"pureName": "connections",
|
|
722
|
+
"columnName": "defaultIsolationLevel",
|
|
723
|
+
"dataType": "varchar(250)",
|
|
724
|
+
"notNull": false
|
|
701
725
|
}
|
|
702
726
|
],
|
|
703
727
|
"foreignKeys": [
|
|
@@ -132,7 +132,35 @@ async function connectUtility(driver, storedConnection, connectionMode, addition
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
connection.ssl = await extractConnectionSslParams(connection);
|
|
135
|
-
|
|
135
|
+
|
|
136
|
+
const proxyUrl = String(connection.httpProxyUrl ?? '').trim();
|
|
137
|
+
const proxyUser = String(connection.httpProxyUser ?? '').trim();
|
|
138
|
+
const proxyPassword = String(connection.httpProxyPassword ?? '').trim();
|
|
139
|
+
if (!proxyUrl && (proxyUser || proxyPassword)) {
|
|
140
|
+
throw new Error('DBGM-00329 Proxy user or password is set but proxy URL is missing');
|
|
141
|
+
}
|
|
142
|
+
if (proxyUrl) {
|
|
143
|
+
let parsedProxy;
|
|
144
|
+
try {
|
|
145
|
+
const parsed = new URL(proxyUrl.includes('://') ? proxyUrl : `http://${proxyUrl}`);
|
|
146
|
+
parsedProxy = {
|
|
147
|
+
protocol: parsed.protocol.replace(':', ''),
|
|
148
|
+
host: parsed.hostname,
|
|
149
|
+
port: parsed.port ? parseInt(parsed.port, 10) : (parsed.protocol === 'https:' ? 443 : 80),
|
|
150
|
+
};
|
|
151
|
+
const username = connection.httpProxyUser ?? parsed.username;
|
|
152
|
+
const rawPassword = connection.httpProxyPassword ?? parsed.password;
|
|
153
|
+
const password = decryptPasswordString(rawPassword);
|
|
154
|
+
if (username) {
|
|
155
|
+
parsedProxy.auth = { username, password: password ?? '' };
|
|
156
|
+
}
|
|
157
|
+
} catch (err) {
|
|
158
|
+
throw new Error(`DBGM-00334 Invalid proxy URL "${proxyUrl}": ${err && err.message ? err.message : err}`);
|
|
159
|
+
}
|
|
160
|
+
connection.axios = axios.default.create({ proxy: parsedProxy });
|
|
161
|
+
} else {
|
|
162
|
+
connection.axios = axios.default;
|
|
163
|
+
}
|
|
136
164
|
|
|
137
165
|
const conn = await driver.connect({ conid: connectionLoaded?._id, ...connection, ...additionalOptions });
|
|
138
166
|
return conn;
|
package/src/utility/crypting.js
CHANGED
|
@@ -101,7 +101,7 @@ function decryptObjectPasswordField(obj, field, encryptor = null) {
|
|
|
101
101
|
return obj;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
const fieldsToEncrypt = ['password', 'sshPassword', 'sshKeyfilePassword', 'connectionDefinition'];
|
|
104
|
+
const fieldsToEncrypt = ['password', 'sshPassword', 'sshKeyfilePassword', 'connectionDefinition', 'httpProxyPassword'];
|
|
105
105
|
const additionalFieldsToMask = [
|
|
106
106
|
'databaseUrl',
|
|
107
107
|
'server',
|