dbgate-api 7.1.3-alpha.3 → 7.1.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dbgate-api",
3
3
  "main": "src/index.js",
4
- "version": "7.1.3-alpha.3",
4
+ "version": "7.1.3",
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.3-alpha.3",
33
+ "dbgate-datalib": "7.1.3",
34
34
  "dbgate-query-splitter": "^4.12.0",
35
- "dbgate-rest": "7.1.3-alpha.3",
36
- "dbgate-sqltree": "7.1.3-alpha.3",
37
- "dbgate-tools": "7.1.3-alpha.3",
35
+ "dbgate-rest": "7.1.3",
36
+ "dbgate-sqltree": "7.1.3",
37
+ "dbgate-tools": "7.1.3",
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.3-alpha.3",
91
+ "dbgate-types": "7.1.3",
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(response);
100
- if (additionalData?.auditLogger) {
101
- additionalData?.auditLogger(response);
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
 
@@ -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 files = (await fs.readdir(dir)).map(file => ({ folder, file }));
44
- return files;
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 });
@@ -1,5 +1,5 @@
1
1
 
2
2
  module.exports = {
3
- version: '7.1.3-alpha.3',
4
- buildTime: '2026-03-09T09:22:37.014Z'
3
+ version: '7.1.3',
4
+ buildTime: '2026-03-18T14:33:28.088Z'
5
5
  };
@@ -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,
@@ -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
- connection.axios = axios.default;
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;
@@ -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',