dbgate-api 4.4.5-alpha.1 → 4.6.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/.env-portal +14 -1
- package/package.json +5 -6
- package/src/controllers/apps.js +264 -0
- package/src/controllers/connections.js +59 -25
- package/src/controllers/databaseConnections.js +1 -0
- package/src/controllers/files.js +23 -2
- package/src/currentVersion.js +2 -2
- package/src/main.js +2 -0
- package/src/proc/connectProcess.js +1 -9
- package/src/utility/JsonLinesDatabase.js +142 -0
- package/src/utility/JsonLinesDatastore.js +2 -2
- package/src/utility/crypting.js +11 -0
- package/src/utility/directories.js +2 -0
- package/src/utility/getChartExport.js +3 -0
- package/src/utility/getDiagramExport.js +25 -0
- package/src/utility/requireEngineDriver.js +3 -2
package/.env-portal
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
DEVMODE=1
|
|
2
2
|
|
|
3
|
-
CONNECTIONS=mysql,postgres,mongo,mongo2
|
|
3
|
+
CONNECTIONS=mysql,postgres,mongo,mongo2,mysqlssh
|
|
4
4
|
|
|
5
5
|
LABEL_mysql=MySql localhost
|
|
6
6
|
SERVER_mysql=localhost
|
|
@@ -24,4 +24,17 @@ LABEL_mongo2=Mongo Server
|
|
|
24
24
|
SERVER_mongo2=localhost
|
|
25
25
|
ENGINE_mongo2=mongo@dbgate-plugin-mongo
|
|
26
26
|
|
|
27
|
+
LABEL_mysqlssh=MySql SSH
|
|
28
|
+
SERVER_mysqlssh=localhost
|
|
29
|
+
USER_mysqlssh=root
|
|
30
|
+
PASSWORD_mysqlssh=xxx
|
|
31
|
+
PORT_mysqlssh=3316
|
|
32
|
+
ENGINE_mysqlssh=mysql@dbgate-plugin-mysql
|
|
33
|
+
USE_SSH_mysqlssh=1
|
|
34
|
+
SSH_HOST_mysqlssh=demo.dbgate.org
|
|
35
|
+
SSH_PORT_mysqlssh=22
|
|
36
|
+
SSH_MODE_mysqlssh=userPassword
|
|
37
|
+
SSH_LOGIN_mysqlssh=root
|
|
38
|
+
SSH_PASSWORD_mysqlssh=xxx
|
|
39
|
+
|
|
27
40
|
# docker run -p 3000:3000 -e CONNECTIONS=mongo -e URL_mongo=mongodb://localhost:27017 -e ENGINE_mongo=mongo@dbgate-plugin-mongo -e LABEL_mongo=mongo dbgate/dbgate:beta
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dbgate-api",
|
|
3
3
|
"main": "src/index.js",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.6.1",
|
|
5
5
|
"homepage": "https://dbgate.org/",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"compare-versions": "^3.6.0",
|
|
26
26
|
"cors": "^2.8.5",
|
|
27
27
|
"cross-env": "^6.0.3",
|
|
28
|
-
"dbgate-query-splitter": "^4.
|
|
29
|
-
"dbgate-sqltree": "^4.
|
|
30
|
-
"dbgate-tools": "^4.
|
|
28
|
+
"dbgate-query-splitter": "^4.6.1",
|
|
29
|
+
"dbgate-sqltree": "^4.6.1",
|
|
30
|
+
"dbgate-tools": "^4.6.1",
|
|
31
31
|
"diff": "^5.0.0",
|
|
32
32
|
"diff2html": "^3.4.13",
|
|
33
33
|
"eslint": "^6.8.0",
|
|
@@ -44,7 +44,6 @@
|
|
|
44
44
|
"line-reader": "^0.4.0",
|
|
45
45
|
"lodash": "^4.17.21",
|
|
46
46
|
"ncp": "^2.0.0",
|
|
47
|
-
"nedb-promises": "^4.0.1",
|
|
48
47
|
"node-cron": "^2.0.3",
|
|
49
48
|
"node-ssh-forward": "^0.7.2",
|
|
50
49
|
"portfinder": "^1.0.28",
|
|
@@ -64,7 +63,7 @@
|
|
|
64
63
|
"devDependencies": {
|
|
65
64
|
"@types/fs-extra": "^9.0.11",
|
|
66
65
|
"@types/lodash": "^4.14.149",
|
|
67
|
-
"dbgate-types": "^4.
|
|
66
|
+
"dbgate-types": "^4.6.1",
|
|
68
67
|
"env-cmd": "^10.1.0",
|
|
69
68
|
"node-loader": "^1.0.2",
|
|
70
69
|
"nodemon": "^2.0.2",
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const _ = require('lodash');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { appdir } = require('../utility/directories');
|
|
5
|
+
const socket = require('../utility/socket');
|
|
6
|
+
const connections = require('./connections');
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
folders_meta: true,
|
|
10
|
+
async folders() {
|
|
11
|
+
const folders = await fs.readdir(appdir());
|
|
12
|
+
return [
|
|
13
|
+
...folders.map(name => ({
|
|
14
|
+
name,
|
|
15
|
+
})),
|
|
16
|
+
];
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
createFolder_meta: true,
|
|
20
|
+
async createFolder({ folder }) {
|
|
21
|
+
const name = await this.getNewAppFolder({ name: folder });
|
|
22
|
+
await fs.mkdir(path.join(appdir(), name));
|
|
23
|
+
socket.emitChanged('app-folders-changed');
|
|
24
|
+
return name;
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
files_meta: true,
|
|
28
|
+
async files({ folder }) {
|
|
29
|
+
const dir = path.join(appdir(), folder);
|
|
30
|
+
if (!(await fs.exists(dir))) return [];
|
|
31
|
+
const files = await fs.readdir(dir);
|
|
32
|
+
|
|
33
|
+
function fileType(ext, type) {
|
|
34
|
+
return files
|
|
35
|
+
.filter(name => name.endsWith(ext))
|
|
36
|
+
.map(name => ({
|
|
37
|
+
name: name.slice(0, -ext.length),
|
|
38
|
+
label: path.parse(name.slice(0, -ext.length)).base,
|
|
39
|
+
type,
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return [
|
|
44
|
+
...fileType('.command.sql', 'command.sql'),
|
|
45
|
+
...fileType('.query.sql', 'query.sql'),
|
|
46
|
+
...fileType('.config.json', 'config.json'),
|
|
47
|
+
];
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
async emitChangedDbApp(folder) {
|
|
51
|
+
const used = await this.getUsedAppFolders();
|
|
52
|
+
if (used.includes(folder)) {
|
|
53
|
+
socket.emitChanged('used-apps-changed');
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
refreshFiles_meta: true,
|
|
58
|
+
async refreshFiles({ folder }) {
|
|
59
|
+
socket.emitChanged(`app-files-changed-${folder}`);
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
refreshFolders_meta: true,
|
|
63
|
+
async refreshFolders() {
|
|
64
|
+
socket.emitChanged(`app-folders-changed`);
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
deleteFile_meta: true,
|
|
68
|
+
async deleteFile({ folder, file, fileType }) {
|
|
69
|
+
await fs.unlink(path.join(appdir(), folder, `${file}.${fileType}`));
|
|
70
|
+
socket.emitChanged(`app-files-changed-${folder}`);
|
|
71
|
+
this.emitChangedDbApp(folder);
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
renameFile_meta: true,
|
|
75
|
+
async renameFile({ folder, file, newFile, fileType }) {
|
|
76
|
+
await fs.rename(
|
|
77
|
+
path.join(path.join(appdir(), folder), `${file}.${fileType}`),
|
|
78
|
+
path.join(path.join(appdir(), folder), `${newFile}.${fileType}`)
|
|
79
|
+
);
|
|
80
|
+
socket.emitChanged(`app-files-changed-${folder}`);
|
|
81
|
+
this.emitChangedDbApp(folder);
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
renameFolder_meta: true,
|
|
85
|
+
async renameFolder({ folder, newFolder }) {
|
|
86
|
+
const uniqueName = await this.getNewAppFolder({ name: newFolder });
|
|
87
|
+
await fs.rename(path.join(appdir(), folder), path.join(appdir(), uniqueName));
|
|
88
|
+
socket.emitChanged(`app-folders-changed`);
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
deleteFolder_meta: true,
|
|
92
|
+
async deleteFolder({ folder }) {
|
|
93
|
+
if (!folder) throw new Error('Missing folder parameter');
|
|
94
|
+
await fs.rmdir(path.join(appdir(), folder), { recursive: true });
|
|
95
|
+
socket.emitChanged(`app-folders-changed`);
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
async getNewAppFolder({ name }) {
|
|
99
|
+
if (!(await fs.exists(path.join(appdir(), name)))) return name;
|
|
100
|
+
let index = 2;
|
|
101
|
+
while (await fs.exists(path.join(appdir(), `${name}${index}`))) {
|
|
102
|
+
index += 1;
|
|
103
|
+
}
|
|
104
|
+
return `${name}${index}`;
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
getUsedAppFolders_meta: true,
|
|
108
|
+
async getUsedAppFolders() {
|
|
109
|
+
const list = await connections.list();
|
|
110
|
+
const apps = [];
|
|
111
|
+
|
|
112
|
+
for (const connection of list) {
|
|
113
|
+
for (const db of connection.databases || []) {
|
|
114
|
+
for (const key of _.keys(db || {})) {
|
|
115
|
+
if (key.startsWith('useApp:') && db[key]) {
|
|
116
|
+
apps.push(key.substring('useApp:'.length));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return _.intersection(_.uniq(apps), await fs.readdir(appdir()));
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
getUsedApps_meta: true,
|
|
126
|
+
async getUsedApps() {
|
|
127
|
+
const apps = await this.getUsedAppFolders();
|
|
128
|
+
const res = [];
|
|
129
|
+
|
|
130
|
+
for (const folder of apps) {
|
|
131
|
+
res.push(await this.loadApp({ folder }));
|
|
132
|
+
}
|
|
133
|
+
return res;
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
// getAppsForDb_meta: true,
|
|
137
|
+
// async getAppsForDb({ conid, database }) {
|
|
138
|
+
// const connection = await connections.get({ conid });
|
|
139
|
+
// if (!connection) return [];
|
|
140
|
+
// const db = (connection.databases || []).find(x => x.name == database);
|
|
141
|
+
// const apps = [];
|
|
142
|
+
// const res = [];
|
|
143
|
+
// if (db) {
|
|
144
|
+
// for (const key of _.keys(db || {})) {
|
|
145
|
+
// if (key.startsWith('useApp:') && db[key]) {
|
|
146
|
+
// apps.push(key.substring('useApp:'.length));
|
|
147
|
+
// }
|
|
148
|
+
// }
|
|
149
|
+
// }
|
|
150
|
+
// for (const folder of apps) {
|
|
151
|
+
// res.push(await this.loadApp({ folder }));
|
|
152
|
+
// }
|
|
153
|
+
// return res;
|
|
154
|
+
// },
|
|
155
|
+
|
|
156
|
+
loadApp_meta: true,
|
|
157
|
+
async loadApp({ folder }) {
|
|
158
|
+
const res = {
|
|
159
|
+
queries: [],
|
|
160
|
+
commands: [],
|
|
161
|
+
name: folder,
|
|
162
|
+
};
|
|
163
|
+
const dir = path.join(appdir(), folder);
|
|
164
|
+
if (await fs.exists(dir)) {
|
|
165
|
+
const files = await fs.readdir(dir);
|
|
166
|
+
|
|
167
|
+
async function processType(ext, field) {
|
|
168
|
+
for (const file of files) {
|
|
169
|
+
if (file.endsWith(ext)) {
|
|
170
|
+
res[field].push({
|
|
171
|
+
name: file.slice(0, -ext.length),
|
|
172
|
+
sql: await fs.readFile(path.join(dir, file), { encoding: 'utf-8' }),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await processType('.command.sql', 'commands');
|
|
179
|
+
await processType('.query.sql', 'queries');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
res.virtualReferences = JSON.parse(
|
|
184
|
+
await fs.readFile(path.join(dir, 'virtual-references.config.json'), { encoding: 'utf-8' })
|
|
185
|
+
);
|
|
186
|
+
} catch (err) {
|
|
187
|
+
res.virtualReferences = [];
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
res.dictionaryDescriptions = JSON.parse(
|
|
191
|
+
await fs.readFile(path.join(dir, 'dictionary-descriptions.config.json'), { encoding: 'utf-8' })
|
|
192
|
+
);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
res.dictionaryDescriptions = [];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return res;
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
async saveConfigFile(appFolder, filename, filterFunc, newItem) {
|
|
201
|
+
const file = path.join(appdir(), appFolder, filename);
|
|
202
|
+
|
|
203
|
+
let json;
|
|
204
|
+
try {
|
|
205
|
+
json = JSON.parse(await fs.readFile(file, { encoding: 'utf-8' }));
|
|
206
|
+
} catch (err) {
|
|
207
|
+
json = [];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (filterFunc) {
|
|
211
|
+
json = json.filter(filterFunc);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
json = [...json, newItem];
|
|
215
|
+
|
|
216
|
+
await fs.writeFile(file, JSON.stringify(json, undefined, 2));
|
|
217
|
+
|
|
218
|
+
socket.emitChanged(`app-files-changed-${appFolder}`);
|
|
219
|
+
socket.emitChanged('used-apps-changed');
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
saveVirtualReference_meta: true,
|
|
223
|
+
async saveVirtualReference({ appFolder, schemaName, pureName, refSchemaName, refTableName, columns }) {
|
|
224
|
+
await this.saveConfigFile(
|
|
225
|
+
appFolder,
|
|
226
|
+
'virtual-references.config.json',
|
|
227
|
+
columns.length == 1
|
|
228
|
+
? x =>
|
|
229
|
+
!(
|
|
230
|
+
x.schemaName == schemaName &&
|
|
231
|
+
x.pureName == pureName &&
|
|
232
|
+
x.columns.length == 1 &&
|
|
233
|
+
x.columns[0].columnName == columns[0].columnName
|
|
234
|
+
)
|
|
235
|
+
: null,
|
|
236
|
+
{
|
|
237
|
+
schemaName,
|
|
238
|
+
pureName,
|
|
239
|
+
refSchemaName,
|
|
240
|
+
refTableName,
|
|
241
|
+
columns,
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
return true;
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
saveDictionaryDescription_meta: true,
|
|
248
|
+
async saveDictionaryDescription({ appFolder, pureName, schemaName, expression, columns, delimiter }) {
|
|
249
|
+
await this.saveConfigFile(
|
|
250
|
+
appFolder,
|
|
251
|
+
'dictionary-descriptions.config.json',
|
|
252
|
+
x => !(x.schemaName == schemaName && x.pureName == pureName),
|
|
253
|
+
{
|
|
254
|
+
schemaName,
|
|
255
|
+
pureName,
|
|
256
|
+
expression,
|
|
257
|
+
columns,
|
|
258
|
+
delimiter,
|
|
259
|
+
}
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
return true;
|
|
263
|
+
},
|
|
264
|
+
};
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const { fork } = require('child_process');
|
|
3
3
|
const _ = require('lodash');
|
|
4
|
-
const nedb = require('nedb-promises');
|
|
5
4
|
const fs = require('fs-extra');
|
|
6
5
|
|
|
7
6
|
const { datadir, filesdir } = require('../utility/directories');
|
|
8
7
|
const socket = require('../utility/socket');
|
|
9
8
|
const { encryptConnection } = require('../utility/crypting');
|
|
10
9
|
const { handleProcessCommunication } = require('../utility/processComm');
|
|
10
|
+
const { pickSafeConnectionInfo } = require('../utility/crypting');
|
|
11
|
+
const JsonLinesDatabase = require('../utility/JsonLinesDatabase');
|
|
11
12
|
|
|
12
13
|
const processArgs = require('../utility/processArgs');
|
|
13
14
|
|
|
@@ -39,7 +40,7 @@ function getDatabaseFileLabel(databaseFile) {
|
|
|
39
40
|
|
|
40
41
|
function getPortalCollections() {
|
|
41
42
|
if (process.env.CONNECTIONS) {
|
|
42
|
-
|
|
43
|
+
const connections = _.compact(process.env.CONNECTIONS.split(',')).map(id => ({
|
|
43
44
|
_id: id,
|
|
44
45
|
engine: process.env[`ENGINE_${id}`],
|
|
45
46
|
server: process.env[`SERVER_${id}`],
|
|
@@ -52,7 +53,35 @@ function getPortalCollections() {
|
|
|
52
53
|
defaultDatabase: process.env[`DATABASE_${id}`],
|
|
53
54
|
singleDatabase: !!process.env[`DATABASE_${id}`],
|
|
54
55
|
displayName: process.env[`LABEL_${id}`],
|
|
56
|
+
|
|
57
|
+
// SSH tunnel
|
|
58
|
+
useSshTunnel: process.env[`USE_SSH_${id}`],
|
|
59
|
+
sshHost: process.env[`SSH_HOST_${id}`],
|
|
60
|
+
sshPort: process.env[`SSH_PORT_${id}`],
|
|
61
|
+
sshMode: process.env[`SSH_MODE_${id}`],
|
|
62
|
+
sshLogin: process.env[`SSH_LOGIN_${id}`],
|
|
63
|
+
sshPassword: process.env[`SSH_PASSWORD_${id}`],
|
|
64
|
+
sshKeyfile: process.env[`SSH_KEY_FILE_${id}`],
|
|
65
|
+
sshKeyfilePassword: process.env[`SSH_KEY_FILE_PASSWORD_${id}`],
|
|
66
|
+
|
|
67
|
+
// SSL
|
|
68
|
+
useSsl: process.env[`USE_SSL_${id}`],
|
|
69
|
+
sslCaFile: process.env[`SSL_CA_FILE_${id}`],
|
|
70
|
+
sslCertFile: process.env[`SSL_CERT_FILE_${id}`],
|
|
71
|
+
sslCertFilePassword: process.env[`SSL_CERT_FILE_PASSWORD_${id}`],
|
|
72
|
+
sslKeyFile: process.env[`SSL_KEY_FILE_${id}`],
|
|
73
|
+
sslRejectUnauthorized: process.env[`SSL_REJECT_UNAUTHORIZED_${id}`],
|
|
55
74
|
}));
|
|
75
|
+
console.log('Using connections from ENV variables:');
|
|
76
|
+
console.log(JSON.stringify(connections.map(pickSafeConnectionInfo), undefined, 2));
|
|
77
|
+
const noengine = connections.filter(x => !x.engine);
|
|
78
|
+
if (noengine.length > 0) {
|
|
79
|
+
console.log(
|
|
80
|
+
'Warning: Invalid CONNECTIONS configutation, missing ENGINE for connection ID:',
|
|
81
|
+
noengine.map(x => x._id)
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return connections;
|
|
56
85
|
}
|
|
57
86
|
|
|
58
87
|
const args = getNamedArgs();
|
|
@@ -125,7 +154,7 @@ module.exports = {
|
|
|
125
154
|
const dir = datadir();
|
|
126
155
|
if (!portalConnections) {
|
|
127
156
|
// @ts-ignore
|
|
128
|
-
this.datastore =
|
|
157
|
+
this.datastore = new JsonLinesDatabase(path.join(dir, 'connections.jsonl'));
|
|
129
158
|
}
|
|
130
159
|
},
|
|
131
160
|
|
|
@@ -134,11 +163,8 @@ module.exports = {
|
|
|
134
163
|
return portalConnections || this.datastore.find();
|
|
135
164
|
},
|
|
136
165
|
|
|
137
|
-
test_meta:
|
|
138
|
-
|
|
139
|
-
raw: true,
|
|
140
|
-
},
|
|
141
|
-
test(req, res) {
|
|
166
|
+
test_meta: true,
|
|
167
|
+
test(connection) {
|
|
142
168
|
const subprocess = fork(global['API_PACKAGE'] || process.argv[1], [
|
|
143
169
|
'--is-forked-api',
|
|
144
170
|
'--start-process',
|
|
@@ -146,15 +172,17 @@ module.exports = {
|
|
|
146
172
|
...processArgs.getPassArgs(),
|
|
147
173
|
// ...process.argv.slice(3),
|
|
148
174
|
]);
|
|
149
|
-
subprocess.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
175
|
+
subprocess.send(connection);
|
|
176
|
+
return new Promise(resolve => {
|
|
177
|
+
subprocess.on('message', resp => {
|
|
178
|
+
if (handleProcessCommunication(resp, subprocess)) return;
|
|
179
|
+
// @ts-ignore
|
|
180
|
+
const { msgtype } = resp;
|
|
181
|
+
if (msgtype == 'connected' || msgtype == 'error') {
|
|
182
|
+
resolve(resp);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
156
185
|
});
|
|
157
|
-
subprocess.send(req.body);
|
|
158
186
|
},
|
|
159
187
|
|
|
160
188
|
save_meta: true,
|
|
@@ -163,18 +191,22 @@ module.exports = {
|
|
|
163
191
|
let res;
|
|
164
192
|
const encrypted = encryptConnection(connection);
|
|
165
193
|
if (connection._id) {
|
|
166
|
-
res = await this.datastore.update(
|
|
194
|
+
res = await this.datastore.update(encrypted);
|
|
167
195
|
} else {
|
|
168
196
|
res = await this.datastore.insert(encrypted);
|
|
169
197
|
}
|
|
170
198
|
socket.emitChanged('connection-list-changed');
|
|
199
|
+
socket.emitChanged('used-apps-changed');
|
|
200
|
+
// for (const db of connection.databases || []) {
|
|
201
|
+
// socket.emitChanged(`db-apps-changed-${connection._id}-${db.name}`);
|
|
202
|
+
// }
|
|
171
203
|
return res;
|
|
172
204
|
},
|
|
173
205
|
|
|
174
206
|
update_meta: true,
|
|
175
207
|
async update({ _id, values }) {
|
|
176
208
|
if (portalConnections) return;
|
|
177
|
-
const res = await this.datastore.
|
|
209
|
+
const res = await this.datastore.patch(_id, values);
|
|
178
210
|
socket.emitChanged('connection-list-changed');
|
|
179
211
|
return res;
|
|
180
212
|
},
|
|
@@ -182,31 +214,33 @@ module.exports = {
|
|
|
182
214
|
updateDatabase_meta: true,
|
|
183
215
|
async updateDatabase({ conid, database, values }) {
|
|
184
216
|
if (portalConnections) return;
|
|
185
|
-
const conn = await this.datastore.
|
|
186
|
-
let databases = conn
|
|
217
|
+
const conn = await this.datastore.get(conid);
|
|
218
|
+
let databases = (conn && conn.databases) || [];
|
|
187
219
|
if (databases.find(x => x.name == database)) {
|
|
188
220
|
databases = databases.map(x => (x.name == database ? { ...x, ...values } : x));
|
|
189
221
|
} else {
|
|
190
222
|
databases = [...databases, { name: database, ...values }];
|
|
191
223
|
}
|
|
192
|
-
const res = await this.datastore.
|
|
224
|
+
const res = await this.datastore.patch(conid, { databases });
|
|
193
225
|
socket.emitChanged('connection-list-changed');
|
|
226
|
+
socket.emitChanged('used-apps-changed');
|
|
227
|
+
// socket.emitChanged(`db-apps-changed-${conid}-${database}`);
|
|
194
228
|
return res;
|
|
195
229
|
},
|
|
196
230
|
|
|
197
231
|
delete_meta: true,
|
|
198
232
|
async delete(connection) {
|
|
199
233
|
if (portalConnections) return;
|
|
200
|
-
const res = await this.datastore.remove(
|
|
234
|
+
const res = await this.datastore.remove(connection._id);
|
|
201
235
|
socket.emitChanged('connection-list-changed');
|
|
202
236
|
return res;
|
|
203
237
|
},
|
|
204
238
|
|
|
205
239
|
get_meta: true,
|
|
206
240
|
async get({ conid }) {
|
|
207
|
-
if (portalConnections) return portalConnections.find(x => x._id == conid);
|
|
208
|
-
const res = await this.datastore.
|
|
209
|
-
return res
|
|
241
|
+
if (portalConnections) return portalConnections.find(x => x._id == conid) || null;
|
|
242
|
+
const res = await this.datastore.get(conid);
|
|
243
|
+
return res || null;
|
|
210
244
|
},
|
|
211
245
|
|
|
212
246
|
newSqliteDatabase_meta: true,
|
package/src/controllers/files.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
const uuidv1 = require('uuid/v1');
|
|
2
2
|
const fs = require('fs-extra');
|
|
3
3
|
const path = require('path');
|
|
4
|
-
const { filesdir, archivedir, resolveArchiveFolder, uploadsdir } = require('../utility/directories');
|
|
4
|
+
const { filesdir, archivedir, resolveArchiveFolder, uploadsdir, appdir } = require('../utility/directories');
|
|
5
5
|
const getChartExport = require('../utility/getChartExport');
|
|
6
6
|
const hasPermission = require('../utility/hasPermission');
|
|
7
7
|
const socket = require('../utility/socket');
|
|
8
8
|
const scheduler = require('./scheduler');
|
|
9
|
+
const getDiagramExport = require('../utility/getDiagramExport');
|
|
10
|
+
const apps = require('./apps');
|
|
9
11
|
|
|
10
12
|
function serialize(format, data) {
|
|
11
13
|
if (format == 'text') return data;
|
|
@@ -73,6 +75,11 @@ module.exports = {
|
|
|
73
75
|
encoding: 'utf-8',
|
|
74
76
|
});
|
|
75
77
|
return deserialize(format, text);
|
|
78
|
+
} else if (folder.startsWith('app:')) {
|
|
79
|
+
const text = await fs.readFile(path.join(appdir(), folder.substring('app:'.length), file), {
|
|
80
|
+
encoding: 'utf-8',
|
|
81
|
+
});
|
|
82
|
+
return deserialize(format, text);
|
|
76
83
|
} else {
|
|
77
84
|
if (!hasPermission(`files/${folder}/read`)) return null;
|
|
78
85
|
const text = await fs.readFile(path.join(filesdir(), folder, file), { encoding: 'utf-8' });
|
|
@@ -86,8 +93,15 @@ module.exports = {
|
|
|
86
93
|
const dir = resolveArchiveFolder(folder.substring('archive:'.length));
|
|
87
94
|
await fs.writeFile(path.join(dir, file), serialize(format, data));
|
|
88
95
|
socket.emitChanged(`archive-files-changed-${folder.substring('archive:'.length)}`);
|
|
96
|
+
return true;
|
|
97
|
+
} else if (folder.startsWith('app:')) {
|
|
98
|
+
const app = folder.substring('app:'.length);
|
|
99
|
+
await fs.writeFile(path.join(appdir(), app, file), serialize(format, data));
|
|
100
|
+
socket.emitChanged(`app-files-changed-${app}`);
|
|
101
|
+
apps.emitChangedDbApp(folder);
|
|
102
|
+
return true;
|
|
89
103
|
} else {
|
|
90
|
-
if (!hasPermission(`files/${folder}/write`)) return;
|
|
104
|
+
if (!hasPermission(`files/${folder}/write`)) return false;
|
|
91
105
|
const dir = path.join(filesdir(), folder);
|
|
92
106
|
if (!(await fs.exists(dir))) {
|
|
93
107
|
await fs.mkdir(dir);
|
|
@@ -98,6 +112,7 @@ module.exports = {
|
|
|
98
112
|
if (folder == 'shell') {
|
|
99
113
|
scheduler.reload();
|
|
100
114
|
}
|
|
115
|
+
return true;
|
|
101
116
|
}
|
|
102
117
|
},
|
|
103
118
|
|
|
@@ -150,4 +165,10 @@ module.exports = {
|
|
|
150
165
|
}
|
|
151
166
|
return true;
|
|
152
167
|
},
|
|
168
|
+
|
|
169
|
+
exportDiagram_meta: true,
|
|
170
|
+
async exportDiagram({ filePath, html, css, themeType, themeClassName }) {
|
|
171
|
+
await fs.writeFile(filePath, getDiagramExport(html, css, themeType, themeClassName));
|
|
172
|
+
return true;
|
|
173
|
+
},
|
|
153
174
|
};
|
package/src/currentVersion.js
CHANGED
package/src/main.js
CHANGED
|
@@ -19,6 +19,7 @@ const runners = require('./controllers/runners');
|
|
|
19
19
|
const jsldata = require('./controllers/jsldata');
|
|
20
20
|
const config = require('./controllers/config');
|
|
21
21
|
const archive = require('./controllers/archive');
|
|
22
|
+
const apps = require('./controllers/apps');
|
|
22
23
|
const uploads = require('./controllers/uploads');
|
|
23
24
|
const plugins = require('./controllers/plugins');
|
|
24
25
|
const files = require('./controllers/files');
|
|
@@ -157,6 +158,7 @@ function useAllControllers(app, electron) {
|
|
|
157
158
|
useController(app, electron, '/files', files);
|
|
158
159
|
useController(app, electron, '/scheduler', scheduler);
|
|
159
160
|
useController(app, electron, '/query-history', queryHistory);
|
|
161
|
+
useController(app, electron, '/apps', apps);
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
function initializeElectronSender(electronSender) {
|
|
@@ -2,17 +2,9 @@ const childProcessChecker = require('../utility/childProcessChecker');
|
|
|
2
2
|
const requireEngineDriver = require('../utility/requireEngineDriver');
|
|
3
3
|
const connectUtility = require('../utility/connectUtility');
|
|
4
4
|
const { handleProcessCommunication } = require('../utility/processComm');
|
|
5
|
+
const { pickSafeConnectionInfo } = require('../utility/crypting');
|
|
5
6
|
const _ = require('lodash');
|
|
6
7
|
|
|
7
|
-
function pickSafeConnectionInfo(connection) {
|
|
8
|
-
return _.mapValues(connection, (v, k) => {
|
|
9
|
-
if (k == 'engine' || k == 'port' || k == 'authType' || k == 'sshMode' || k == 'passwordMode') return v;
|
|
10
|
-
if (v === null || v === true || v === false) return v;
|
|
11
|
-
if (v) return '***';
|
|
12
|
-
return undefined;
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
|
|
16
8
|
const formatErrorDetail = (e, connection) => `${e.stack}
|
|
17
9
|
|
|
18
10
|
Error JSON: ${JSON.stringify(e, undefined, 2)}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
const AsyncLock = require('async-lock');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const uuidv1 = require('uuid/v1');
|
|
4
|
+
|
|
5
|
+
const lock = new AsyncLock();
|
|
6
|
+
|
|
7
|
+
// const lineReader = require('line-reader');
|
|
8
|
+
// const { fetchNextLineFromReader } = require('./JsonLinesDatastore');
|
|
9
|
+
|
|
10
|
+
class JsonLinesDatabase {
|
|
11
|
+
constructor(filename) {
|
|
12
|
+
this.filename = filename;
|
|
13
|
+
this.data = [];
|
|
14
|
+
this.loadedOk = false;
|
|
15
|
+
this.loadPerformed = false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async _save() {
|
|
19
|
+
if (!this.loadedOk) {
|
|
20
|
+
// don't override data
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
await fs.writeFile(this.filename, this.data.map(x => JSON.stringify(x)).join('\n'));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async _ensureLoaded() {
|
|
27
|
+
if (!this.loadPerformed) {
|
|
28
|
+
await lock.acquire('reader', async () => {
|
|
29
|
+
if (!this.loadPerformed) {
|
|
30
|
+
if (!(await fs.exists(this.filename))) {
|
|
31
|
+
this.loadedOk = true;
|
|
32
|
+
this.loadPerformed = true;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const text = await fs.readFile(this.filename, { encoding: 'utf-8' });
|
|
37
|
+
this.data = text
|
|
38
|
+
.split('\n')
|
|
39
|
+
.filter(x => x.trim())
|
|
40
|
+
.map(x => JSON.parse(x));
|
|
41
|
+
this.loadedOk = true;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error(`Error loading file ${this.filename}`, err);
|
|
44
|
+
}
|
|
45
|
+
this.loadPerformed = true;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async insert(obj) {
|
|
52
|
+
await this._ensureLoaded();
|
|
53
|
+
if (obj._id && (await this.get(obj._id))) {
|
|
54
|
+
throw new Error(`Cannot insert duplicate ID ${obj._id} into ${this.filename}`);
|
|
55
|
+
}
|
|
56
|
+
const elem = obj._id
|
|
57
|
+
? obj
|
|
58
|
+
: {
|
|
59
|
+
...obj,
|
|
60
|
+
_id: uuidv1(),
|
|
61
|
+
};
|
|
62
|
+
this.data.push(elem);
|
|
63
|
+
await this._save();
|
|
64
|
+
return elem;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async get(id) {
|
|
68
|
+
await this._ensureLoaded();
|
|
69
|
+
return this.data.find(x => x._id == id);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async find(cond) {
|
|
73
|
+
await this._ensureLoaded();
|
|
74
|
+
if (cond) {
|
|
75
|
+
return this.data.filter(x => {
|
|
76
|
+
for (const key of Object.keys(cond)) {
|
|
77
|
+
if (x[key] != cond[key]) return false;
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
});
|
|
81
|
+
} else {
|
|
82
|
+
return this.data;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async update(obj) {
|
|
87
|
+
await this._ensureLoaded();
|
|
88
|
+
this.data = this.data.map(x => (x._id == obj._id ? obj : x));
|
|
89
|
+
await this._save();
|
|
90
|
+
return obj;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async patch(id, values) {
|
|
94
|
+
await this._ensureLoaded();
|
|
95
|
+
this.data = this.data.map(x => (x._id == id ? { ...x, ...values } : x));
|
|
96
|
+
await this._save();
|
|
97
|
+
return this.data.find(x => x._id == id);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async remove(id) {
|
|
101
|
+
await this._ensureLoaded();
|
|
102
|
+
const removed = this.data.find(x => x._id == id);
|
|
103
|
+
this.data = this.data.filter(x => x._id != id);
|
|
104
|
+
await this._save();
|
|
105
|
+
return removed;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// async _openReader() {
|
|
109
|
+
// return new Promise((resolve, reject) =>
|
|
110
|
+
// lineReader.open(this.filename, (err, reader) => {
|
|
111
|
+
// if (err) reject(err);
|
|
112
|
+
// resolve(reader);
|
|
113
|
+
// })
|
|
114
|
+
// );
|
|
115
|
+
// }
|
|
116
|
+
|
|
117
|
+
// async _read() {
|
|
118
|
+
// this.data = [];
|
|
119
|
+
// if (!(await fs.exists(this.filename))) return;
|
|
120
|
+
// try {
|
|
121
|
+
// const reader = await this._openReader();
|
|
122
|
+
// for (;;) {
|
|
123
|
+
// const line = await fetchNextLineFromReader(reader);
|
|
124
|
+
// if (!line) break;
|
|
125
|
+
// this.data.push(JSON.parse(line));
|
|
126
|
+
// }
|
|
127
|
+
// } catch (err) {
|
|
128
|
+
// console.error(`Error loading file ${this.filename}`, err);
|
|
129
|
+
// }
|
|
130
|
+
// }
|
|
131
|
+
|
|
132
|
+
// async _write() {
|
|
133
|
+
// const fw = fs.createWriteStream(this.filename);
|
|
134
|
+
// for (const obj of this.data) {
|
|
135
|
+
// await fw.write(JSON.stringify(obj));
|
|
136
|
+
// await fw.write('\n');
|
|
137
|
+
// }
|
|
138
|
+
// await fw.end();
|
|
139
|
+
// }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = JsonLinesDatabase;
|
|
@@ -4,7 +4,7 @@ const lock = new AsyncLock();
|
|
|
4
4
|
const stableStringify = require('json-stable-stringify');
|
|
5
5
|
const { evaluateCondition } = require('dbgate-sqltree');
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
function fetchNextLineFromReader(reader) {
|
|
8
8
|
return new Promise((resolve, reject) => {
|
|
9
9
|
if (!reader.hasNextLine()) {
|
|
10
10
|
resolve(null);
|
|
@@ -62,7 +62,7 @@ class JsonLinesDatastore {
|
|
|
62
62
|
|
|
63
63
|
async _readLine(parse) {
|
|
64
64
|
for (;;) {
|
|
65
|
-
const line = await
|
|
65
|
+
const line = await fetchNextLineFromReader(this.reader);
|
|
66
66
|
if (!line) {
|
|
67
67
|
// EOF
|
|
68
68
|
return null;
|
package/src/utility/crypting.js
CHANGED
|
@@ -2,6 +2,7 @@ const crypto = require('crypto');
|
|
|
2
2
|
const simpleEncryptor = require('simple-encryptor');
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const _ = require('lodash');
|
|
5
6
|
|
|
6
7
|
const { datadir } = require('./directories');
|
|
7
8
|
|
|
@@ -81,8 +82,18 @@ function decryptConnection(connection) {
|
|
|
81
82
|
return connection;
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
function pickSafeConnectionInfo(connection) {
|
|
86
|
+
return _.mapValues(connection, (v, k) => {
|
|
87
|
+
if (k == 'engine' || k == 'port' || k == 'authType' || k == 'sshMode' || k == 'passwordMode') return v;
|
|
88
|
+
if (v === null || v === true || v === false) return v;
|
|
89
|
+
if (v) return '***';
|
|
90
|
+
return undefined;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
84
94
|
module.exports = {
|
|
85
95
|
loadEncryptionKey,
|
|
86
96
|
encryptConnection,
|
|
87
97
|
decryptConnection,
|
|
98
|
+
pickSafeConnectionInfo,
|
|
88
99
|
};
|
|
@@ -38,6 +38,7 @@ const rundir = dirFunc('run', true);
|
|
|
38
38
|
const uploadsdir = dirFunc('uploads', true);
|
|
39
39
|
const pluginsdir = dirFunc('plugins');
|
|
40
40
|
const archivedir = dirFunc('archive');
|
|
41
|
+
const appdir = dirFunc('apps');
|
|
41
42
|
const filesdir = dirFunc('files');
|
|
42
43
|
|
|
43
44
|
function packagedPluginsDir() {
|
|
@@ -103,6 +104,7 @@ module.exports = {
|
|
|
103
104
|
rundir,
|
|
104
105
|
uploadsdir,
|
|
105
106
|
archivedir,
|
|
107
|
+
appdir,
|
|
106
108
|
ensureDirectory,
|
|
107
109
|
pluginsdir,
|
|
108
110
|
filesdir,
|
|
@@ -7,6 +7,9 @@ const getChartExport = (title, config, imageFile) => {
|
|
|
7
7
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.6.0/chart.min.js" integrity="sha512-GMGzUEevhWh8Tc/njS0bDpwgxdCJLQBWG3Z2Ct+JGOpVnEmjvNx6ts4v6A2XJf1HOrtOsfhv3hBKpK9kE5z8AQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
|
8
8
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js" integrity="sha512-qTXRIMyZIFb8iQcfjXWCO8+M5Tbc38Qi5WzdPOYZHIlZpzBHG3L3by84BBBOiRGiEb7KKtAOAs5qYdUiZiQNNQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
|
9
9
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-adapter-moment/1.0.0/chartjs-adapter-moment.min.js" integrity="sha512-oh5t+CdSBsaVVAvxcZKy3XJdP7ZbYUBSRCXDTVn0ODewMDDNnELsrG9eDm8rVZAQg7RsDD/8K3MjPAFB13o6eA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
|
10
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js" integrity="sha512-UXumZrZNiOwnTcZSHLOfcTs0aos2MzBWHXOHOuB0J/R44QB0dwY5JgfbvljXcklVf65Gc4El6RjZ+lnwd2az2g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
|
11
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-zoom/1.2.0/chartjs-plugin-zoom.min.js" integrity="sha512-TT0wAMqqtjXVzpc48sI0G84rBP+oTkBZPgeRYIOVRGUdwJsyS3WPipsNh///ay2LJ+onCM23tipnz6EvEy2/UA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
|
12
|
+
|
|
10
13
|
<style>
|
|
11
14
|
a { text-decoration: none }
|
|
12
15
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const getDiagramExport = (html, css, themeType, themeClassName) => {
|
|
2
|
+
return `<html>
|
|
3
|
+
<meta charset='utf-8'>
|
|
4
|
+
|
|
5
|
+
<head>
|
|
6
|
+
<style>
|
|
7
|
+
${css}
|
|
8
|
+
|
|
9
|
+
body {
|
|
10
|
+
background: var(--theme-bg-1);
|
|
11
|
+
color: var(--theme-font-1);
|
|
12
|
+
}
|
|
13
|
+
</style>
|
|
14
|
+
|
|
15
|
+
<link rel="stylesheet" href='https://cdn.jsdelivr.net/npm/@mdi/font@6.5.95/css/materialdesignicons.css' />
|
|
16
|
+
</head>
|
|
17
|
+
|
|
18
|
+
<body class='${themeType == 'dark' ? 'theme-type-dark' : 'theme-type-light'} ${themeClassName}'>
|
|
19
|
+
${html}
|
|
20
|
+
</body>
|
|
21
|
+
|
|
22
|
+
</html>`;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
module.exports = getDiagramExport;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
2
|
const requirePlugin = require('../shell/requirePlugin');
|
|
3
|
+
const { pickSafeConnectionInfo } = require('./crypting');
|
|
3
4
|
|
|
4
5
|
/** @returns {import('dbgate-types').EngineDriver} */
|
|
5
6
|
function requireEngineDriver(connection) {
|
|
@@ -10,14 +11,14 @@ function requireEngineDriver(connection) {
|
|
|
10
11
|
engine = connection.engine;
|
|
11
12
|
}
|
|
12
13
|
if (!engine) {
|
|
13
|
-
throw new Error(
|
|
14
|
+
throw new Error(`Could not get driver from connection ${JSON.stringify(pickSafeConnectionInfo(connection))}`);
|
|
14
15
|
}
|
|
15
16
|
if (engine.includes('@')) {
|
|
16
17
|
const [shortName, packageName] = engine.split('@');
|
|
17
18
|
const plugin = requirePlugin(packageName);
|
|
18
19
|
return plugin.drivers.find(x => x.engine == engine);
|
|
19
20
|
}
|
|
20
|
-
throw new Error(`Could not
|
|
21
|
+
throw new Error(`Could not find engine driver ${engine}`);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
module.exports = requireEngineDriver;
|