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 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.5-alpha.1",
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.4.5-alpha.1",
29
- "dbgate-sqltree": "^4.4.5-alpha.1",
30
- "dbgate-tools": "^4.4.5-alpha.1",
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.4.5-alpha.1",
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
- return _.compact(process.env.CONNECTIONS.split(',')).map(id => ({
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 = nedb.create(path.join(dir, 'connections.jsonl'));
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
- method: 'post',
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.on('message', resp => {
150
- if (handleProcessCommunication(resp, subprocess)) return;
151
- // @ts-ignore
152
- const { msgtype } = resp;
153
- if (msgtype == 'connected' || msgtype == 'error') {
154
- res.json(resp);
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(_.pick(connection, '_id'), encrypted);
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.update({ _id }, { $set: values });
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.find({ _id: conid });
186
- let databases = conn[0].databases || [];
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.update({ _id: conid }, { $set: { databases } });
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(_.pick(connection, '_id'));
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.find({ _id: conid });
209
- return res[0] || null;
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,
@@ -252,6 +252,7 @@ module.exports = {
252
252
 
253
253
  serverVersion_meta: true,
254
254
  async serverVersion({ conid, database }) {
255
+ if (!conid) return null;
255
256
  const opened = await this.ensureOpened(conid, database);
256
257
  return opened.serverVersion || null;
257
258
  },
@@ -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
  };
@@ -1,5 +1,5 @@
1
1
 
2
2
  module.exports = {
3
- version: '4.4.5-alpha.1',
4
- buildTime: '2021-12-30T08:13:02.622Z'
3
+ version: '4.6.1',
4
+ buildTime: '2022-01-31T19:49:04.817Z'
5
5
  };
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
- async function fetchNextLine(reader) {
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 fetchNextLine(this.reader);
65
+ const line = await fetchNextLineFromReader(this.reader);
66
66
  if (!line) {
67
67
  // EOF
68
68
  return null;
@@ -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('Could not get driver from connection');
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 found engine driver ${engine}`);
21
+ throw new Error(`Could not find engine driver ${engine}`);
21
22
  }
22
23
 
23
24
  module.exports = requireEngineDriver;