dbgate-api 7.1.5 → 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 +6 -6
- package/src/controllers/connections.js +55 -1
- package/src/controllers/jsldata.js +74 -1
- package/src/controllers/queryHistory.js +22 -6
- package/src/controllers/runners.js +21 -0
- package/src/currentVersion.js +2 -2
- package/src/shell/runScript.js +1 -0
- package/src/storageModel.js +108 -0
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.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.
|
|
33
|
+
"dbgate-datalib": "7.1.8-alpha.7",
|
|
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.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.
|
|
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",
|
|
@@ -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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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']);
|
package/src/currentVersion.js
CHANGED
package/src/shell/runScript.js
CHANGED
package/src/storageModel.js
CHANGED
|
@@ -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": [
|