dbgate-api-premium 7.1.6 → 7.1.8-alpha.7

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-premium",
3
3
  "main": "src/index.js",
4
- "version": "7.1.6",
4
+ "version": "7.1.8-alpha.7",
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.6",
33
+ "dbgate-datalib": "7.1.8-alpha.7",
34
34
  "dbgate-query-splitter": "^4.12.0",
35
- "dbgate-rest": "7.1.6",
36
- "dbgate-sqltree": "7.1.6",
37
- "dbgate-tools": "7.1.6",
35
+ "dbgate-rest": "7.1.8-alpha.7",
36
+ "dbgate-sqltree": "7.1.8-alpha.7",
37
+ "dbgate-tools": "7.1.8-alpha.7",
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.6",
91
+ "dbgate-types": "7.1.8-alpha.7",
92
92
  "env-cmd": "^10.1.0",
93
93
  "jsdoc-to-markdown": "^9.0.5",
94
94
  "node-loader": "^1.0.2",
@@ -592,7 +592,7 @@ function validateEmail(email) {
592
592
  }
593
593
 
594
594
  function extractEmailFromMsEntraPayload(payload) {
595
- for (const field of ['email', 'upn', 'unique_name']) {
595
+ for (const field of ['email', 'preferred_username', 'upn', 'unique_name']) {
596
596
  const value = payload[field];
597
597
  if (value && validateEmail(value)) {
598
598
  return value;
@@ -492,7 +492,61 @@ module.exports = {
492
492
  return mask && !platformInfo.allowShellConnection ? maskConnection(res) : encryptConnection(res);
493
493
  }
494
494
  const res = await this.datastore.get(conid);
495
- return res || null;
495
+ if (res) return res;
496
+
497
+ // In a forked runner-script child process, ask the parent for connections that may be
498
+ // volatile (in-memory only, e.g. ask-for-password). We only do this when
499
+ // there really is a parent (process.send exists) to avoid an infinite loop
500
+ // when the parent's own getCore falls through here.
501
+ // The check is intentionally narrow: only runner scripts pass
502
+ // --process-display-name script, so connect/session/ssh-forward subprocesses
503
+ // are not affected and continue to return null immediately.
504
+ if (process.send && processArgs.processDisplayName === 'script') {
505
+ const conn = await new Promise(resolve => {
506
+ let resolved = false;
507
+
508
+ const cleanup = () => {
509
+ process.removeListener('message', handler);
510
+ process.removeListener('disconnect', onDisconnect);
511
+ clearTimeout(timeout);
512
+ };
513
+
514
+ const settle = value => {
515
+ if (!resolved) {
516
+ resolved = true;
517
+ cleanup();
518
+ resolve(value);
519
+ }
520
+ };
521
+
522
+ const handler = message => {
523
+ if (message?.msgtype === 'volatile-connection-response' && message.conid === conid) {
524
+ settle(message.conn || null);
525
+ }
526
+ };
527
+
528
+ const onDisconnect = () => settle(null);
529
+
530
+ const timeout = setTimeout(() => settle(null), 5000);
531
+ // Don't let the timer alone keep the process alive if all other work is done
532
+ timeout.unref();
533
+
534
+ process.on('message', handler);
535
+ process.once('disconnect', onDisconnect);
536
+
537
+ try {
538
+ process.send({ msgtype: 'get-volatile-connection', conid });
539
+ } catch {
540
+ settle(null);
541
+ }
542
+ });
543
+ if (conn) {
544
+ volatileConnections[conn._id] = conn; // cache for subsequent calls
545
+ return conn;
546
+ }
547
+ }
548
+
549
+ return null;
496
550
  },
497
551
 
498
552
  get_meta: true,
@@ -1,5 +1,8 @@
1
- const { filterName } = require('dbgate-tools');
1
+ const { filterName, getLogger, extractErrorLogData } = require('dbgate-tools');
2
+ const logger = getLogger('jsldata');
3
+ const { jsldir, archivedir } = require('../utility/directories');
2
4
  const fs = require('fs');
5
+ const path = require('path');
3
6
  const lineReader = require('line-reader');
4
7
  const _ = require('lodash');
5
8
  const { __ } = require('lodash/fp');
@@ -149,6 +152,10 @@ module.exports = {
149
152
 
150
153
  getRows_meta: true,
151
154
  async getRows({ jslid, offset, limit, filters, sort, formatterFunction }) {
155
+ const fileName = getJslFileName(jslid);
156
+ if (!fs.existsSync(fileName)) {
157
+ return [];
158
+ }
152
159
  const datastore = await this.ensureDatastore(jslid, formatterFunction);
153
160
  return datastore.getRows(offset, limit, _.isEmpty(filters) ? null : filters, _.isEmpty(sort) ? null : sort);
154
161
  },
@@ -159,6 +166,72 @@ module.exports = {
159
166
  return fs.existsSync(fileName);
160
167
  },
161
168
 
169
+ streamRows_meta: {
170
+ method: 'get',
171
+ raw: true,
172
+ },
173
+ streamRows(req, res) {
174
+ const { jslid } = req.query;
175
+ if (!jslid) {
176
+ res.status(400).json({ apiErrorMessage: 'Missing jslid' });
177
+ return;
178
+ }
179
+
180
+ // Reject file:// jslids — they resolve to arbitrary server-side paths
181
+ if (jslid.startsWith('file://')) {
182
+ res.status(403).json({ apiErrorMessage: 'Forbidden jslid scheme' });
183
+ return;
184
+ }
185
+
186
+ const fileName = getJslFileName(jslid);
187
+
188
+ if (!fs.existsSync(fileName)) {
189
+ res.status(404).json({ apiErrorMessage: 'File not found' });
190
+ return;
191
+ }
192
+
193
+ // Dereference symlinks and normalize case (Windows) before the allow-list check.
194
+ // realpathSync is safe here because existsSync confirmed the file is present.
195
+ // path.resolve() alone cannot dereference symlinks, so a symlink inside an allowed
196
+ // root could otherwise point to an arbitrary external path.
197
+ const normalize = p => (process.platform === 'win32' ? p.toLowerCase() : p);
198
+ const resolveRoot = r => { try { return fs.realpathSync(r); } catch { return path.resolve(r); } };
199
+
200
+ let realFile;
201
+ try {
202
+ realFile = fs.realpathSync(fileName);
203
+ } catch {
204
+ res.status(403).json({ apiErrorMessage: 'Forbidden path' });
205
+ return;
206
+ }
207
+
208
+ const allowedRoots = [jsldir(), archivedir()].map(r => normalize(resolveRoot(r)) + path.sep);
209
+ const isAllowed = allowedRoots.some(root => normalize(realFile).startsWith(root));
210
+ if (!isAllowed) {
211
+ logger.warn({ jslid, realFile }, 'DBGM-00000 streamRows rejected path outside allowed roots');
212
+ res.status(403).json({ apiErrorMessage: 'Forbidden path' });
213
+ return;
214
+ }
215
+ res.setHeader('Content-Type', 'application/x-ndjson');
216
+ res.setHeader('Cache-Control', 'no-cache');
217
+ const stream = fs.createReadStream(realFile, 'utf-8');
218
+
219
+ req.on('close', () => {
220
+ stream.destroy();
221
+ });
222
+
223
+ stream.on('error', err => {
224
+ logger.error(extractErrorLogData(err), 'DBGM-00000 Error streaming JSONL file');
225
+ if (!res.headersSent) {
226
+ res.status(500).json({ apiErrorMessage: 'Stream error' });
227
+ } else {
228
+ res.end();
229
+ }
230
+ });
231
+
232
+ stream.pipe(res);
233
+ },
234
+
162
235
  getStats_meta: true,
163
236
  getStats({ jslid }) {
164
237
  const file = `${getJslFileName(jslid)}.stats`;
@@ -33,19 +33,35 @@ function readCore(reader, skip, limit, filter) {
33
33
  });
34
34
  }
35
35
 
36
- module.exports = {
37
- read_meta: true,
38
- async read({ skip, limit, filter }) {
36
+ function readJsonl({ skip, limit, filter }) {
37
+ return new Promise(async (resolve, reject) => {
39
38
  const fileName = path.join(datadir(), 'query-history.jsonl');
40
39
  // @ts-ignore
41
- if (!(await fs.exists(fileName))) return [];
40
+ if (!(await fs.exists(fileName))) return resolve([]);
42
41
  const reader = fsReverse(fileName);
43
42
  const res = await readCore(reader, skip, limit, filter);
44
- return res;
43
+ resolve(res);
44
+ });
45
+ }
46
+
47
+ module.exports = {
48
+ read_meta: true,
49
+ async read({ skip, limit, filter }, req) {
50
+ const storage = require('./storage');
51
+ const storageResult = await storage.readQueryHistory({ skip, limit, filter }, req);
52
+ if (storageResult) return storageResult;
53
+ return readJsonl({ skip, limit, filter });
45
54
  },
46
55
 
47
56
  write_meta: true,
48
- async write({ data }) {
57
+ async write({ data }, req) {
58
+ const storage = require('./storage');
59
+ const written = await storage.writeQueryHistory({ data }, req);
60
+ if (written) {
61
+ socket.emit('query-history-changed');
62
+ return 'OK';
63
+ }
64
+
49
65
  const fileName = path.join(datadir(), 'query-history.jsonl');
50
66
  await fs.appendFile(fileName, JSON.stringify(data) + '\n');
51
67
  socket.emit('query-history-changed');
@@ -196,6 +196,27 @@ module.exports = {
196
196
  // @ts-ignore
197
197
  const { msgtype } = message;
198
198
  if (handleProcessCommunication(message, subprocess)) return;
199
+ if (msgtype === 'get-volatile-connection') {
200
+ const connections = require('./connections');
201
+ // @ts-ignore
202
+ const conid = message.conid;
203
+ if (!conid || typeof conid !== 'string') return;
204
+ const trySend = payload => {
205
+ if (!subprocess.connected) return;
206
+ try {
207
+ subprocess.send(payload);
208
+ } catch {
209
+ // child disconnected between the check and the send — ignore
210
+ }
211
+ };
212
+ connections.getCore({ conid }).then(conn => {
213
+ trySend({ msgtype: 'volatile-connection-response', conid, conn: conn?.unsaved ? conn : null });
214
+ }).catch(err => {
215
+ logger.error({ ...extractErrorLogData(err), conid }, 'DBGM-00000 Error resolving volatile connection for child process');
216
+ trySend({ msgtype: 'volatile-connection-response', conid, conn: null });
217
+ });
218
+ return;
219
+ }
199
220
  this[`handle_${msgtype}`](runid, message);
200
221
  });
201
222
  return _.pick(newOpened, ['runid']);
@@ -1282,4 +1282,111 @@ DbGate Team
1282
1282
 
1283
1283
  return { success: true };
1284
1284
  },
1285
+
1286
+ async readQueryHistory({ skip, limit, filter }, req) {
1287
+ if (!process.env.STORAGE_DATABASE) {
1288
+ return null;
1289
+ }
1290
+
1291
+ const [conn, driver] = await getStorageConnection();
1292
+ if (!conn) return null;
1293
+
1294
+ const userId = req?.user?.userId;
1295
+ const roleId = req?.user?.roleId;
1296
+
1297
+ const conditions = [];
1298
+
1299
+ if (userId) {
1300
+ conditions.push({
1301
+ conditionType: 'binary',
1302
+ operator: '=',
1303
+ left: { exprType: 'column', columnName: 'user_id' },
1304
+ right: { exprType: 'value', value: userId },
1305
+ });
1306
+ } else {
1307
+ conditions.push({
1308
+ conditionType: 'binary',
1309
+ operator: '=',
1310
+ left: { exprType: 'column', columnName: 'role_id' },
1311
+ right: { exprType: 'value', value: roleId ?? -1 },
1312
+ });
1313
+ }
1314
+
1315
+ if (filter) {
1316
+ conditions.push({
1317
+ conditionType: 'or',
1318
+ conditions: [
1319
+ {
1320
+ conditionType: 'like',
1321
+ left: { exprType: 'column', columnName: 'sql' },
1322
+ right: { exprType: 'value', value: `%${filter}%` },
1323
+ },
1324
+ {
1325
+ conditionType: 'like',
1326
+ left: { exprType: 'column', columnName: 'database' },
1327
+ right: { exprType: 'value', value: `%${filter}%` },
1328
+ },
1329
+ ],
1330
+ });
1331
+ }
1332
+
1333
+ const select = {
1334
+ commandType: 'select',
1335
+ from: {
1336
+ name: { pureName: 'query_history' },
1337
+ },
1338
+ columns: ['id', 'created', 'user_id', 'role_id', 'sql', 'conid', 'database'].map(columnName => ({
1339
+ exprType: 'column',
1340
+ columnName,
1341
+ })),
1342
+ orderBy: [{ exprType: 'column', columnName: 'id', direction: 'desc' }],
1343
+ };
1344
+
1345
+ if (conditions.length > 1) {
1346
+ select.where = { conditionType: 'and', conditions };
1347
+ } else {
1348
+ select.where = conditions[0];
1349
+ }
1350
+
1351
+ if (limit || skip != null) {
1352
+ select.range = {
1353
+ limit,
1354
+ offset: skip ?? 0,
1355
+ };
1356
+ }
1357
+
1358
+ const dmp = driver.createDumper();
1359
+ dumpSqlSelect(dmp, select);
1360
+ const resp = await driver.query(conn, dmp.s);
1361
+
1362
+ if (!resp || !resp.rows) return [];
1363
+
1364
+ return resp.rows.map(row => ({
1365
+ sql: row.sql,
1366
+ conid: row.conid,
1367
+ database: row.database,
1368
+ date: Number(row.created),
1369
+ }));
1370
+ },
1371
+
1372
+ async writeQueryHistory({ data }, req) {
1373
+ if (!process.env.STORAGE_DATABASE) {
1374
+ return false;
1375
+ }
1376
+
1377
+ const userId = req?.user?.userId;
1378
+ const roleId = req?.user?.roleId;
1379
+
1380
+ await storageSqlCommandFmt(
1381
+ `^insert ^into ~query_history (~created, ~user_id, ~role_id, ~sql, ~conid, ~database) values (%v, %v, %v, %v, %v, %v)`,
1382
+ data.date || new Date().getTime(),
1383
+ userId || null,
1384
+ userId ? null : roleId ?? -1,
1385
+ data.sql || null,
1386
+ data.conid || null,
1387
+ data.database || null
1388
+ );
1389
+
1390
+ return true;
1391
+ },
1285
1392
  };
@@ -83,15 +83,42 @@ module.exports = {
83
83
  const readAll = hasPermission(`all-team-files/read`, loadedPermissions);
84
84
  const writeAll = hasPermission(`all-team-files/write`, loadedPermissions);
85
85
  const useAll = hasPermission(`all-team-files/use`, loadedPermissions);
86
- if (readAll || writeAll || useAll) {
87
- res = await storageListAllTeamFiles();
88
- res = res?.map(item => ({ ...item, allow_read: readAll, allow_write: writeAll, allow_use: useAll }));
86
+ const hasAllGlobal = readAll && writeAll && useAll;
87
+ const hasAnyGlobal = readAll || writeAll || useAll;
88
+
89
+ if (hasAllGlobal) {
90
+ // All global flags granted, no need to query per-file permissions
91
+ res = (await storageListAllTeamFiles()) || [];
92
+ res = res.map(item => ({ ...item, allow_read: true, allow_write: true, allow_use: true }));
93
+ } else if (hasAnyGlobal) {
94
+ // Partial global permissions - merge with per-file permissions
95
+ const userId = req?.user?.userId;
96
+ let perFileRes = [];
97
+ if (userId) {
98
+ perFileRes = (await storageListTeamFilesForUser(userId)) || [];
99
+ } else {
100
+ perFileRes = (await storageListTeamFilesForRole(getBuiltinRoleIdFromRequest(req))) || [];
101
+ }
102
+ const allFiles = (await storageListAllTeamFiles()) || [];
103
+ const perFileMap = {};
104
+ for (const f of perFileRes) {
105
+ perFileMap[f.id] = f;
106
+ }
107
+ res = allFiles.map(item => {
108
+ const perFile = perFileMap[item.id];
109
+ return {
110
+ ...item,
111
+ allow_read: readAll || !!perFile?.allow_read,
112
+ allow_write: writeAll || !!perFile?.allow_write,
113
+ allow_use: useAll || !!perFile?.allow_use,
114
+ };
115
+ });
89
116
  } else {
90
117
  const userId = req?.user?.userId;
91
118
  if (userId) {
92
- res = await storageListTeamFilesForUser(userId);
119
+ res = (await storageListTeamFilesForUser(userId)) || [];
93
120
  } else {
94
- res = await storageListTeamFilesForRole(getBuiltinRoleIdFromRequest(req));
121
+ res = (await storageListTeamFilesForRole(getBuiltinRoleIdFromRequest(req))) || [];
95
122
  }
96
123
  }
97
124
  return (
@@ -120,21 +147,43 @@ module.exports = {
120
147
  const readAll = hasPermission(`all-team-files/read`, loadedPermissions);
121
148
  const writeAll = hasPermission(`all-team-files/write`, loadedPermissions);
122
149
  const useAll = hasPermission(`all-team-files/use`, loadedPermissions);
123
- if (readAll || writeAll || useAll || createAll) {
124
- res = await storageListAllTeamFolders();
125
- res = res?.map(item => ({
126
- ...item,
127
- allow_create: createAll,
128
- allow_read: readAll,
129
- allow_write: writeAll,
130
- allow_use: useAll,
131
- }));
150
+ const hasAllGlobal = createAll && readAll && writeAll && useAll;
151
+ const hasAnyGlobal = readAll || writeAll || useAll || createAll;
152
+
153
+ if (hasAllGlobal) {
154
+ // All global flags granted, no need to query per-folder permissions
155
+ res = (await storageListAllTeamFolders()) || [];
156
+ res = res.map(item => ({ ...item, allow_create: true, allow_read: true, allow_write: true, allow_use: true }));
157
+ } else if (hasAnyGlobal) {
158
+ // Partial global permissions - merge with per-folder permissions
159
+ const userId = req?.user?.userId;
160
+ let perFolderRes = [];
161
+ if (userId) {
162
+ perFolderRes = (await storageListTeamFoldersForUser(userId)) || [];
163
+ } else {
164
+ perFolderRes = (await storageListTeamFoldersForRole(getBuiltinRoleIdFromRequest(req))) || [];
165
+ }
166
+ const allFolders = (await storageListAllTeamFolders()) || [];
167
+ const perFolderMap = {};
168
+ for (const f of perFolderRes) {
169
+ perFolderMap[f.id] = f;
170
+ }
171
+ res = allFolders.map(item => {
172
+ const perFolder = perFolderMap[item.id];
173
+ return {
174
+ ...item,
175
+ allow_create: createAll || !!perFolder?.allow_create,
176
+ allow_read: readAll || !!perFolder?.allow_read,
177
+ allow_write: writeAll || !!perFolder?.allow_write,
178
+ allow_use: useAll || !!perFolder?.allow_use,
179
+ };
180
+ });
132
181
  } else {
133
182
  const userId = req?.user?.userId;
134
183
  if (userId) {
135
- res = await storageListTeamFoldersForUser(userId);
184
+ res = (await storageListTeamFoldersForUser(userId)) || [];
136
185
  } else {
137
- res = await storageListTeamFoldersForRole(getBuiltinRoleIdFromRequest(req));
186
+ res = (await storageListTeamFoldersForRole(getBuiltinRoleIdFromRequest(req))) || [];
138
187
  }
139
188
  }
140
189
  return (
@@ -1,5 +1,5 @@
1
1
 
2
2
  module.exports = {
3
- version: '7.1.6',
4
- buildTime: '2026-03-26T11:49:35.154Z'
3
+ version: '7.1.8-alpha.7',
4
+ buildTime: '2026-04-09T13:29:00.133Z'
5
5
  };
@@ -7,6 +7,7 @@ async function runScript(func) {
7
7
  if (processArgs.checkParent) {
8
8
  childProcessChecker();
9
9
  }
10
+
10
11
  try {
11
12
  await func();
12
13
  process.exit(0);
@@ -874,6 +874,114 @@ module.exports = {
874
874
  }
875
875
  ]
876
876
  },
877
+ {
878
+ "pureName": "query_history",
879
+ "columns": [
880
+ {
881
+ "pureName": "query_history",
882
+ "columnName": "id",
883
+ "dataType": "int",
884
+ "autoIncrement": true,
885
+ "notNull": true
886
+ },
887
+ {
888
+ "pureName": "query_history",
889
+ "columnName": "created",
890
+ "dataType": "bigint",
891
+ "notNull": true
892
+ },
893
+ {
894
+ "pureName": "query_history",
895
+ "columnName": "user_id",
896
+ "dataType": "int",
897
+ "notNull": false
898
+ },
899
+ {
900
+ "pureName": "query_history",
901
+ "columnName": "role_id",
902
+ "dataType": "int",
903
+ "notNull": false
904
+ },
905
+ {
906
+ "pureName": "query_history",
907
+ "columnName": "sql",
908
+ "dataType": "text",
909
+ "notNull": false
910
+ },
911
+ {
912
+ "pureName": "query_history",
913
+ "columnName": "conid",
914
+ "dataType": "varchar(100)",
915
+ "notNull": false
916
+ },
917
+ {
918
+ "pureName": "query_history",
919
+ "columnName": "database",
920
+ "dataType": "varchar(200)",
921
+ "notNull": false
922
+ }
923
+ ],
924
+ "foreignKeys": [
925
+ {
926
+ "constraintType": "foreignKey",
927
+ "constraintName": "FK_query_history_user_id",
928
+ "pureName": "query_history",
929
+ "refTableName": "users",
930
+ "deleteAction": "CASCADE",
931
+ "columns": [
932
+ {
933
+ "columnName": "user_id",
934
+ "refColumnName": "id"
935
+ }
936
+ ]
937
+ },
938
+ {
939
+ "constraintType": "foreignKey",
940
+ "constraintName": "FK_query_history_role_id",
941
+ "pureName": "query_history",
942
+ "refTableName": "roles",
943
+ "deleteAction": "CASCADE",
944
+ "columns": [
945
+ {
946
+ "columnName": "role_id",
947
+ "refColumnName": "id"
948
+ }
949
+ ]
950
+ }
951
+ ],
952
+ "indexes": [
953
+ {
954
+ "constraintName": "idx_query_history_user_id",
955
+ "pureName": "query_history",
956
+ "constraintType": "index",
957
+ "columns": [
958
+ {
959
+ "columnName": "user_id"
960
+ }
961
+ ]
962
+ },
963
+ {
964
+ "constraintName": "idx_query_history_role_id",
965
+ "pureName": "query_history",
966
+ "constraintType": "index",
967
+ "columns": [
968
+ {
969
+ "columnName": "role_id"
970
+ }
971
+ ]
972
+ }
973
+ ],
974
+ "primaryKey": {
975
+ "pureName": "query_history",
976
+ "constraintType": "primaryKey",
977
+ "constraintName": "PK_query_history",
978
+ "columns": [
979
+ {
980
+ "columnName": "id"
981
+ }
982
+ ]
983
+ }
984
+ },
877
985
  {
878
986
  "pureName": "roles",
879
987
  "columns": [