dbgate-api 5.1.6 → 5.1.7-alpha.14

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.
@@ -0,0 +1,14 @@
1
+ DEVMODE=1
2
+
3
+ CONNECTIONS=mysql
4
+ SINGLE_CONNECTION=mysql
5
+ # SINGLE_DATABASE=Chinook
6
+
7
+ LABEL_mysql=MySql localhost
8
+ SERVER_mysql=localhost
9
+ # USER_mysql=root
10
+ PORT_mysql=3306
11
+ # PASSWORD_mysql=Pwd2020Db
12
+ ENGINE_mysql=mysql@dbgate-plugin-mysql
13
+ # PASSWORD_MODE_mysql=askPassword
14
+ PASSWORD_MODE_mysql=askUser
package/env/singledb/.env CHANGED
@@ -5,8 +5,8 @@ CONNECTIONS=mysql
5
5
  LABEL_mysql=MySql localhost
6
6
  SERVER_mysql=localhost
7
7
  USER_mysql=root
8
- PASSWORD_mysql=test
9
- PORT_mysql=3307
8
+ PASSWORD_mysql=Pwd2020Db
9
+ PORT_mysql=3306
10
10
  ENGINE_mysql=mysql@dbgate-plugin-mysql
11
11
  DBCONFIG_mysql=[{"name":"Chinook","connectionColor":"cyan"}]
12
12
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dbgate-api",
3
3
  "main": "src/index.js",
4
- "version": "5.1.6",
4
+ "version": "5.1.7-alpha.14",
5
5
  "homepage": "https://dbgate.org/",
6
6
  "repository": {
7
7
  "type": "git",
@@ -17,6 +17,7 @@
17
17
  "dbgate"
18
18
  ],
19
19
  "dependencies": {
20
+ "activedirectory2": "^2.1.0",
20
21
  "async-lock": "^1.2.4",
21
22
  "axios": "^0.21.1",
22
23
  "body-parser": "^1.19.0",
@@ -25,9 +26,9 @@
25
26
  "compare-versions": "^3.6.0",
26
27
  "cors": "^2.8.5",
27
28
  "cross-env": "^6.0.3",
28
- "dbgate-query-splitter": "^4.9.2",
29
- "dbgate-sqltree": "^5.1.6",
30
- "dbgate-tools": "^5.1.6",
29
+ "dbgate-query-splitter": "^4.9.3",
30
+ "dbgate-sqltree": "^5.1.7-alpha.14",
31
+ "dbgate-tools": "^5.1.7-alpha.14",
31
32
  "debug": "^4.3.4",
32
33
  "diff": "^5.0.0",
33
34
  "diff2html": "^3.4.13",
@@ -42,6 +43,7 @@
42
43
  "is-electron": "^2.2.1",
43
44
  "js-yaml": "^4.1.0",
44
45
  "json-stable-stringify": "^1.0.1",
46
+ "jsonwebtoken": "^8.5.1",
45
47
  "line-reader": "^0.4.0",
46
48
  "lodash": "^4.17.21",
47
49
  "ncp": "^2.0.0",
@@ -57,6 +59,8 @@
57
59
  "start": "env-cmd node src/index.js --listen-api",
58
60
  "start:portal": "env-cmd -f env/portal/.env node src/index.js --listen-api",
59
61
  "start:singledb": "env-cmd -f env/singledb/.env node src/index.js --listen-api",
62
+ "start:auth": "env-cmd -f env/auth/.env node src/index.js --listen-api",
63
+ "start:dblogin": "env-cmd -f env/dblogin/.env node src/index.js --listen-api",
60
64
  "start:filedb": "env-cmd node src/index.js /home/jena/test/chinook/Chinook.db --listen-api",
61
65
  "start:singleconn": "env-cmd node src/index.js --server localhost --user root --port 3307 --engine mysql@dbgate-plugin-mysql --password test --listen-api",
62
66
  "ts": "tsc",
@@ -65,7 +69,7 @@
65
69
  "devDependencies": {
66
70
  "@types/fs-extra": "^9.0.11",
67
71
  "@types/lodash": "^4.14.149",
68
- "dbgate-types": "^5.1.6",
72
+ "dbgate-types": "^5.1.7-alpha.14",
69
73
  "env-cmd": "^10.1.0",
70
74
  "node-loader": "^1.0.2",
71
75
  "nodemon": "^2.0.2",
@@ -75,6 +79,7 @@
75
79
  },
76
80
  "optionalDependencies": {
77
81
  "better-sqlite3": "7.6.2",
82
+ "oracledb": "^5.5.0",
78
83
  "msnodesqlv8": "^2.6.0"
79
84
  }
80
85
  }
@@ -58,7 +58,7 @@ module.exports = {
58
58
 
59
59
  refreshFiles_meta: true,
60
60
  async refreshFiles({ folder }) {
61
- socket.emitChanged(`app-files-changed-${folder}`);
61
+ socket.emitChanged('app-files-changed', { app: folder });
62
62
  },
63
63
 
64
64
  refreshFolders_meta: true,
@@ -69,7 +69,7 @@ module.exports = {
69
69
  deleteFile_meta: true,
70
70
  async deleteFile({ folder, file, fileType }) {
71
71
  await fs.unlink(path.join(appdir(), folder, `${file}.${fileType}`));
72
- socket.emitChanged(`app-files-changed-${folder}`);
72
+ socket.emitChanged('app-files-changed', { app: folder });
73
73
  this.emitChangedDbApp(folder);
74
74
  },
75
75
 
@@ -79,7 +79,7 @@ module.exports = {
79
79
  path.join(path.join(appdir(), folder), `${file}.${fileType}`),
80
80
  path.join(path.join(appdir(), folder), `${newFile}.${fileType}`)
81
81
  );
82
- socket.emitChanged(`app-files-changed-${folder}`);
82
+ socket.emitChanged('app-files-changed', { app: folder });
83
83
  this.emitChangedDbApp(folder);
84
84
  },
85
85
 
@@ -95,7 +95,7 @@ module.exports = {
95
95
  if (!folder) throw new Error('Missing folder parameter');
96
96
  await fs.rmdir(path.join(appdir(), folder), { recursive: true });
97
97
  socket.emitChanged(`app-folders-changed`);
98
- socket.emitChanged(`app-files-changed-${folder}`);
98
+ socket.emitChanged('app-files-changed', { app: folder });
99
99
  socket.emitChanged('used-apps-changed');
100
100
  },
101
101
 
@@ -219,7 +219,7 @@ module.exports = {
219
219
 
220
220
  await fs.writeFile(file, JSON.stringify(json, undefined, 2));
221
221
 
222
- socket.emitChanged(`app-files-changed-${appFolder}`);
222
+ socket.emitChanged('app-files-changed', { app: appFolder });
223
223
  socket.emitChanged('used-apps-changed');
224
224
  },
225
225
 
@@ -271,7 +271,7 @@ module.exports = {
271
271
  const file = path.join(appdir(), appFolder, fileName);
272
272
  if (!(await fs.exists(file))) {
273
273
  await fs.writeFile(file, JSON.stringify(content, undefined, 2));
274
- socket.emitChanged(`app-files-changed-${appFolder}`);
274
+ socket.emitChanged('app-files-changed', { app: appFolder });
275
275
  socket.emitChanged('used-apps-changed');
276
276
  return true;
277
277
  }
@@ -5,6 +5,7 @@ const { archivedir, clearArchiveLinksCache, resolveArchiveFolder } = require('..
5
5
  const socket = require('../utility/socket');
6
6
  const { saveFreeTableData } = require('../utility/freeTableStorage');
7
7
  const loadFilesRecursive = require('../utility/loadFilesRecursive');
8
+ const getJslFileName = require('../utility/getJslFileName');
8
9
 
9
10
  module.exports = {
10
11
  folders_meta: true,
@@ -74,7 +75,7 @@ module.exports = {
74
75
 
75
76
  refreshFiles_meta: true,
76
77
  async refreshFiles({ folder }) {
77
- socket.emitChanged(`archive-files-changed-${folder}`);
78
+ socket.emitChanged('archive-files-changed', { folder });
78
79
  },
79
80
 
80
81
  refreshFolders_meta: true,
@@ -85,7 +86,7 @@ module.exports = {
85
86
  deleteFile_meta: true,
86
87
  async deleteFile({ folder, file, fileType }) {
87
88
  await fs.unlink(path.join(resolveArchiveFolder(folder), `${file}.${fileType}`));
88
- socket.emitChanged(`archive-files-changed-${folder}`);
89
+ socket.emitChanged(`archive-files-changed`, { folder });
89
90
  },
90
91
 
91
92
  renameFile_meta: true,
@@ -94,7 +95,7 @@ module.exports = {
94
95
  path.join(resolveArchiveFolder(folder), `${file}.${fileType}`),
95
96
  path.join(resolveArchiveFolder(folder), `${newFile}.${fileType}`)
96
97
  );
97
- socket.emitChanged(`archive-files-changed-${folder}`);
98
+ socket.emitChanged(`archive-files-changed`, { folder });
98
99
  },
99
100
 
100
101
  renameFolder_meta: true,
@@ -118,7 +119,7 @@ module.exports = {
118
119
  saveFreeTable_meta: true,
119
120
  async saveFreeTable({ folder, file, data }) {
120
121
  await saveFreeTableData(path.join(resolveArchiveFolder(folder), `${file}.jsonl`), data);
121
- socket.emitChanged(`archive-files-changed-${folder}`);
122
+ socket.emitChanged(`archive-files-changed`, { folder });
122
123
  return true;
123
124
  },
124
125
 
@@ -146,7 +147,16 @@ module.exports = {
146
147
  saveText_meta: true,
147
148
  async saveText({ folder, file, text }) {
148
149
  await fs.writeFile(path.join(resolveArchiveFolder(folder), `${file}.jsonl`), text);
149
- socket.emitChanged(`archive-files-changed-${folder}`);
150
+ socket.emitChanged(`archive-files-changed`, { folder });
151
+ return true;
152
+ },
153
+
154
+ saveJslData_meta: true,
155
+ async saveJslData({ folder, file, jslid }) {
156
+ const source = getJslFileName(jslid);
157
+ const target = path.join(resolveArchiveFolder(folder), `${file}.jsonl`);
158
+ await fs.copyFile(source, target);
159
+ socket.emitChanged(`archive-files-changed`, { folder });
150
160
  return true;
151
161
  },
152
162
 
@@ -0,0 +1,143 @@
1
+ const axios = require('axios');
2
+ const jwt = require('jsonwebtoken');
3
+ const getExpressPath = require('../utility/getExpressPath');
4
+ const uuidv1 = require('uuid/v1');
5
+ const { getLogins } = require('../utility/hasPermission');
6
+ const AD = require('activedirectory2').promiseWrapper;
7
+
8
+ const tokenSecret = uuidv1();
9
+
10
+ function shouldAuthorizeApi() {
11
+ const logins = getLogins();
12
+ return !!process.env.OAUTH_AUTH || !!process.env.AD_URL || (!!logins && !process.env.BASIC_AUTH);
13
+ }
14
+
15
+ function getTokenLifetime() {
16
+ return process.env.TOKEN_LIFETIME || '1d';
17
+ }
18
+
19
+ function unauthorizedResponse(req, res, text) {
20
+ // if (req.path == getExpressPath('/config/get-settings')) {
21
+ // return res.json({});
22
+ // }
23
+ // if (req.path == getExpressPath('/connections/list')) {
24
+ // return res.json([]);
25
+ // }
26
+ return res.sendStatus(401).send(text);
27
+ }
28
+
29
+ function authMiddleware(req, res, next) {
30
+ const SKIP_AUTH_PATHS = ['/config/get', '/auth/oauth-token', '/auth/login', '/stream'];
31
+
32
+ if (!shouldAuthorizeApi()) {
33
+ return next();
34
+ }
35
+ let skipAuth = !!SKIP_AUTH_PATHS.find(x => req.path == getExpressPath(x));
36
+
37
+ const authHeader = req.headers.authorization;
38
+ if (!authHeader) {
39
+ if (skipAuth) {
40
+ return next();
41
+ }
42
+ return unauthorizedResponse(req, res, 'missing authorization header');
43
+ }
44
+ const token = authHeader.split(' ')[1];
45
+ try {
46
+ const decoded = jwt.verify(token, tokenSecret);
47
+ req.user = decoded;
48
+ return next();
49
+ } catch (err) {
50
+ if (skipAuth) {
51
+ return next();
52
+ }
53
+
54
+ console.log('Sending invalid token error', err.message);
55
+
56
+ return unauthorizedResponse(req, res, 'invalid token');
57
+ }
58
+ }
59
+
60
+ module.exports = {
61
+ oauthToken_meta: true,
62
+ async oauthToken(params) {
63
+ const { redirectUri, code } = params;
64
+
65
+ const resp = await axios.default.post(
66
+ `${process.env.OAUTH_TOKEN}`,
67
+ `grant_type=authorization_code&code=${encodeURIComponent(code)}&redirect_uri=${encodeURIComponent(
68
+ redirectUri
69
+ )}&client_id=${process.env.OAUTH_CLIENT_ID}&client_secret=${process.env.OAUTH_CLIENT_SECRET}`
70
+ );
71
+
72
+ const { access_token, refresh_token } = resp.data;
73
+
74
+ const payload = jwt.decode(access_token);
75
+
76
+ console.log('User payload returned from OAUTH:', payload);
77
+
78
+ const login = process.env.OAUTH_LOGIN_FIELD ? payload[process.env.OAUTH_LOGIN_FIELD] : 'oauth';
79
+
80
+ if (
81
+ process.env.OAUTH_ALLOWED_LOGINS &&
82
+ !process.env.OAUTH_ALLOWED_LOGINS.split(',').find(x => x.toLowerCase().trim() == login.toLowerCase().trim())
83
+ ) {
84
+ return { error: `Username ${login} not allowed to log in` };
85
+ }
86
+ if (access_token) {
87
+ return {
88
+ accessToken: jwt.sign({ login }, tokenSecret, { expiresIn: getTokenLifetime() }),
89
+ };
90
+ }
91
+
92
+ return { error: 'Token not found' };
93
+ },
94
+ login_meta: true,
95
+ async login(params) {
96
+ const { login, password } = params;
97
+
98
+ if (process.env.AD_URL) {
99
+ const adConfig = {
100
+ url: process.env.AD_URL,
101
+ baseDN: process.env.AD_BASEDN,
102
+ username: process.env.AD_USERNAME,
103
+ password: process.env.AD_PASSOWRD,
104
+ };
105
+ const ad = new AD(adConfig);
106
+ try {
107
+ const res = await ad.authenticate(login, password);
108
+ if (!res) {
109
+ return { error: 'Login failed' };
110
+ }
111
+ if (
112
+ process.env.AD_ALLOWED_LOGINS &&
113
+ !process.env.AD_ALLOWED_LOGINS.split(',').find(x => x.toLowerCase().trim() == login.toLowerCase().trim())
114
+ ) {
115
+ return { error: `Username ${login} not allowed to log in` };
116
+ }
117
+ return {
118
+ accessToken: jwt.sign({ login }, tokenSecret, { expiresIn: getTokenLifetime() }),
119
+ };
120
+ } catch (err) {
121
+ console.log('Failed active directory authentization', err.message);
122
+ return {
123
+ error: err.message,
124
+ };
125
+ }
126
+ }
127
+
128
+ const logins = getLogins();
129
+ if (!logins) {
130
+ return { error: 'Logins not configured' };
131
+ }
132
+ const foundLogin = logins.find(x => x.login == login)
133
+ if (foundLogin && foundLogin.password == password) {
134
+ return {
135
+ accessToken: jwt.sign({ login }, tokenSecret, { expiresIn: getTokenLifetime() }),
136
+ };
137
+ }
138
+ return { error: 'Invalid credentials' };
139
+ },
140
+
141
+ authMiddleware,
142
+ shouldAuthorizeApi,
143
+ };
@@ -28,18 +28,27 @@ module.exports = {
28
28
  get_meta: true,
29
29
  async get(_params, req) {
30
30
  const logins = getLogins();
31
- const login = logins ? logins.find(x => x.login == (req.auth && req.auth.user)) : null;
31
+ const login =
32
+ req && req.user
33
+ ? req.user.login
34
+ : logins
35
+ ? logins.find(x => x.login == (req && req.auth && req.auth.user))
36
+ : null;
32
37
  const permissions = login ? login.permissions : process.env.PERMISSIONS;
33
38
 
34
39
  return {
35
40
  runAsPortal: !!connections.portalConnections,
36
- singleDatabase: connections.singleDatabase,
41
+ singleDbConnection: connections.singleDbConnection,
42
+ singleConnection: connections.singleConnection,
37
43
  // hideAppEditor: !!process.env.HIDE_APP_EDITOR,
38
44
  allowShellConnection: platformInfo.allowShellConnection,
39
45
  allowShellScripting: platformInfo.allowShellScripting,
40
46
  isDocker: platformInfo.isDocker,
41
47
  permissions,
42
48
  login,
49
+ oauth: process.env.OAUTH_AUTH,
50
+ oauthLogout: process.env.OAUTH_LOGOUT,
51
+ isLoginForm: !!process.env.AD_URL || (!!logins && !process.env.BASIC_AUTH),
43
52
  ...currentVersion,
44
53
  };
45
54
  },
@@ -2,6 +2,7 @@ const path = require('path');
2
2
  const { fork } = require('child_process');
3
3
  const _ = require('lodash');
4
4
  const fs = require('fs-extra');
5
+ const crypto = require('crypto');
5
6
 
6
7
  const { datadir, filesdir } = require('../utility/directories');
7
8
  const socket = require('../utility/socket');
@@ -15,6 +16,8 @@ const { safeJsonParse } = require('dbgate-tools');
15
16
  const platformInfo = require('../utility/platformInfo');
16
17
  const { connectionHasPermission, testConnectionPermission } = require('../utility/hasPermission');
17
18
 
19
+ let volatileConnections = {};
20
+
18
21
  function getNamedArgs() {
19
22
  const res = {};
20
23
  for (let i = 0; i < process.argv.length; i++) {
@@ -49,6 +52,7 @@ function getPortalCollections() {
49
52
  server: process.env[`SERVER_${id}`],
50
53
  user: process.env[`USER_${id}`],
51
54
  password: process.env[`PASSWORD_${id}`],
55
+ passwordMode: process.env[`PASSWORD_MODE_${id}`],
52
56
  port: process.env[`PORT_${id}`],
53
57
  databaseUrl: process.env[`URL_${id}`],
54
58
  useDatabaseUrl: !!process.env[`URL_${id}`],
@@ -126,9 +130,10 @@ function getPortalCollections() {
126
130
 
127
131
  return null;
128
132
  }
133
+
129
134
  const portalConnections = getPortalCollections();
130
135
 
131
- function getSingleDatabase() {
136
+ function getSingleDbConnection() {
132
137
  if (process.env.SINGLE_CONNECTION && process.env.SINGLE_DATABASE) {
133
138
  // @ts-ignore
134
139
  const connection = portalConnections.find(x => x._id == process.env.SINGLE_CONNECTION);
@@ -152,12 +157,31 @@ function getSingleDatabase() {
152
157
  return null;
153
158
  }
154
159
 
155
- const singleDatabase = getSingleDatabase();
160
+ function getSingleConnection() {
161
+ if (getSingleDbConnection()) return null;
162
+ if (process.env.SINGLE_CONNECTION) {
163
+ // @ts-ignore
164
+ const connection = portalConnections.find(x => x._id == process.env.SINGLE_CONNECTION);
165
+ if (connection) {
166
+ return connection;
167
+ }
168
+ }
169
+ // @ts-ignore
170
+ const arg0 = (portalConnections || []).find(x => x._id == 'argv');
171
+ if (arg0) {
172
+ return arg0;
173
+ }
174
+ return null;
175
+ }
176
+
177
+ const singleDbConnection = getSingleDbConnection();
178
+ const singleConnection = getSingleConnection();
156
179
 
157
180
  module.exports = {
158
181
  datastore: null,
159
182
  opened: [],
160
- singleDatabase,
183
+ singleDbConnection,
184
+ singleConnection,
161
185
  portalConnections,
162
186
 
163
187
  async _init() {
@@ -199,6 +223,36 @@ module.exports = {
199
223
  });
200
224
  },
201
225
 
226
+ saveVolatile_meta: true,
227
+ async saveVolatile({ conid, user, password, test }) {
228
+ const old = await this.getCore({ conid });
229
+ const res = {
230
+ ...old,
231
+ _id: crypto.randomUUID(),
232
+ password,
233
+ passwordMode: undefined,
234
+ unsaved: true,
235
+ };
236
+ if (old.passwordMode == 'askUser') {
237
+ res.user = user;
238
+ }
239
+
240
+ if (test) {
241
+ const testRes = await this.test(res);
242
+ if (testRes.msgtype == 'connected') {
243
+ volatileConnections[res._id] = res;
244
+ return {
245
+ ...res,
246
+ msgtype: 'connected',
247
+ };
248
+ }
249
+ return testRes;
250
+ } else {
251
+ volatileConnections[res._id] = res;
252
+ return res;
253
+ }
254
+ },
255
+
202
256
  save_meta: true,
203
257
  async save(connection) {
204
258
  if (portalConnections) return;
@@ -229,6 +283,14 @@ module.exports = {
229
283
  return res;
230
284
  },
231
285
 
286
+ batchChangeFolder_meta: true,
287
+ async batchChangeFolder({ folder, newFolder }, req) {
288
+ // const updated = await this.datastore.find(x => x.parent == folder);
289
+ const res = await this.datastore.updateAll(x => (x.parent == folder ? { ...x, parent: newFolder } : x));
290
+ socket.emitChanged('connection-list-changed');
291
+ return res;
292
+ },
293
+
232
294
  updateDatabase_meta: true,
233
295
  async updateDatabase({ conid, database, values }, req) {
234
296
  if (portalConnections) return;
@@ -258,6 +320,10 @@ module.exports = {
258
320
 
259
321
  async getCore({ conid, mask = false }) {
260
322
  if (!conid) return null;
323
+ const volatile = volatileConnections[conid];
324
+ if (volatile) {
325
+ return volatile;
326
+ }
261
327
  if (portalConnections) {
262
328
  const res = portalConnections.find(x => x._id == conid) || null;
263
329
  return mask && !platformInfo.allowShellConnection ? maskConnection(res) : res;
@@ -27,6 +27,7 @@ const { createTwoFilesPatch } = require('diff');
27
27
  const diff2htmlPage = require('../utility/diff2htmlPage');
28
28
  const processArgs = require('../utility/processArgs');
29
29
  const { testConnectionPermission } = require('../utility/hasPermission');
30
+ const { MissingCredentialsError } = require('../utility/exceptions');
30
31
 
31
32
  module.exports = {
32
33
  /** @type {import('dbgate-types').OpenedDatabaseConnection[]} */
@@ -42,19 +43,19 @@ module.exports = {
42
43
  const existing = this.opened.find(x => x.conid == conid && x.database == database);
43
44
  if (!existing) return;
44
45
  existing.structure = structure;
45
- socket.emitChanged(`database-structure-changed-${conid}-${database}`);
46
+ socket.emitChanged('database-structure-changed', { conid, database });
46
47
  },
47
48
  handle_structureTime(conid, database, { analysedTime }) {
48
49
  const existing = this.opened.find(x => x.conid == conid && x.database == database);
49
50
  if (!existing) return;
50
51
  existing.analysedTime = analysedTime;
51
- socket.emitChanged(`database-status-changed-${conid}-${database}`);
52
+ socket.emitChanged(`database-status-changed`, { conid, database });
52
53
  },
53
54
  handle_version(conid, database, { version }) {
54
55
  const existing = this.opened.find(x => x.conid == conid && x.database == database);
55
56
  if (!existing) return;
56
57
  existing.serverVersion = version;
57
- socket.emitChanged(`database-server-version-changed-${conid}-${database}`);
58
+ socket.emitChanged(`database-server-version-changed`, { conid, database });
58
59
  },
59
60
 
60
61
  handle_error(conid, database, props) {
@@ -72,7 +73,7 @@ module.exports = {
72
73
  if (!existing) return;
73
74
  if (existing.status && status && existing.status.counter > status.counter) return;
74
75
  existing.status = status;
75
- socket.emitChanged(`database-status-changed-${conid}-${database}`);
76
+ socket.emitChanged(`database-status-changed`, { conid, database });
76
77
  },
77
78
 
78
79
  handle_ping() {},
@@ -81,6 +82,9 @@ module.exports = {
81
82
  const existing = this.opened.find(x => x.conid == conid && x.database == database);
82
83
  if (existing) return existing;
83
84
  const connection = await connections.getCore({ conid });
85
+ if (connection.passwordMode == 'askPassword' || connection.passwordMode == 'askUser') {
86
+ throw new MissingCredentialsError({ conid, passwordMode: connection.passwordMode });
87
+ }
84
88
  const subprocess = fork(global['API_PACKAGE'] || process.argv[1], [
85
89
  '--is-forked-api',
86
90
  '--start-process',
@@ -313,7 +317,7 @@ module.exports = {
313
317
  },
314
318
  structure: existing.structure,
315
319
  };
316
- socket.emitChanged(`database-status-changed-${conid}-${database}`);
320
+ socket.emitChanged(`database-status-changed`, { conid, database });
317
321
  }
318
322
  },
319
323
 
@@ -49,7 +49,7 @@ module.exports = {
49
49
  async delete({ folder, file }, req) {
50
50
  if (!hasPermission(`files/${folder}/write`, req)) return false;
51
51
  await fs.unlink(path.join(filesdir(), folder, file));
52
- socket.emitChanged(`files-changed-${folder}`);
52
+ socket.emitChanged(`files-changed`, { folder });
53
53
  socket.emitChanged(`all-files-changed`);
54
54
  return true;
55
55
  },
@@ -58,7 +58,7 @@ module.exports = {
58
58
  async rename({ folder, file, newFile }, req) {
59
59
  if (!hasPermission(`files/${folder}/write`, req)) return false;
60
60
  await fs.rename(path.join(filesdir(), folder, file), path.join(filesdir(), folder, newFile));
61
- socket.emitChanged(`files-changed-${folder}`);
61
+ socket.emitChanged(`files-changed`, { folder });
62
62
  socket.emitChanged(`all-files-changed`);
63
63
  return true;
64
64
  },
@@ -66,7 +66,7 @@ module.exports = {
66
66
  refresh_meta: true,
67
67
  async refresh({ folders }, req) {
68
68
  for (const folder of folders) {
69
- socket.emitChanged(`files-changed-${folder}`);
69
+ socket.emitChanged(`files-changed`, { folder });
70
70
  socket.emitChanged(`all-files-changed`);
71
71
  }
72
72
  return true;
@@ -76,7 +76,7 @@ module.exports = {
76
76
  async copy({ folder, file, newFile }, req) {
77
77
  if (!hasPermission(`files/${folder}/write`, req)) return false;
78
78
  await fs.copyFile(path.join(filesdir(), folder, file), path.join(filesdir(), folder, newFile));
79
- socket.emitChanged(`files-changed-${folder}`);
79
+ socket.emitChanged(`files-changed`, { folder });
80
80
  socket.emitChanged(`all-files-changed`);
81
81
  return true;
82
82
  },
@@ -112,13 +112,13 @@ module.exports = {
112
112
  if (!hasPermission(`archive/write`, req)) return false;
113
113
  const dir = resolveArchiveFolder(folder.substring('archive:'.length));
114
114
  await fs.writeFile(path.join(dir, file), serialize(format, data));
115
- socket.emitChanged(`archive-files-changed-${folder.substring('archive:'.length)}`);
115
+ socket.emitChanged(`archive-files-changed`, { folder: folder.substring('archive:'.length) });
116
116
  return true;
117
117
  } else if (folder.startsWith('app:')) {
118
118
  if (!hasPermission(`apps/write`, req)) return false;
119
119
  const app = folder.substring('app:'.length);
120
120
  await fs.writeFile(path.join(appdir(), app, file), serialize(format, data));
121
- socket.emitChanged(`app-files-changed-${app}`);
121
+ socket.emitChanged(`app-files-changed`, { app });
122
122
  socket.emitChanged('used-apps-changed');
123
123
  apps.emitChangedDbApp(folder);
124
124
  return true;
@@ -129,7 +129,7 @@ module.exports = {
129
129
  await fs.mkdir(dir);
130
130
  }
131
131
  await fs.writeFile(path.join(dir, file), serialize(format, data));
132
- socket.emitChanged(`files-changed-${folder}`);
132
+ socket.emitChanged(`files-changed`, { folder });
133
133
  socket.emitChanged(`all-files-changed`);
134
134
  if (folder == 'shell') {
135
135
  scheduler.reload();
@@ -7,6 +7,7 @@ const DatastoreProxy = require('../utility/DatastoreProxy');
7
7
  const { saveFreeTableData } = require('../utility/freeTableStorage');
8
8
  const getJslFileName = require('../utility/getJslFileName');
9
9
  const JsonLinesDatastore = require('../utility/JsonLinesDatastore');
10
+ const requirePluginFunction = require('../utility/requirePluginFunction');
10
11
  const socket = require('../utility/socket');
11
12
 
12
13
  function readFirstLine(file) {
@@ -99,10 +100,13 @@ module.exports = {
99
100
  // return readerInfo;
100
101
  // },
101
102
 
102
- async ensureDatastore(jslid) {
103
+ async ensureDatastore(jslid, formatterFunction) {
103
104
  let datastore = this.datastores[jslid];
104
- if (!datastore) {
105
- datastore = new JsonLinesDatastore(getJslFileName(jslid));
105
+ if (!datastore || datastore.formatterFunction != formatterFunction) {
106
+ if (datastore) {
107
+ datastore._closeReader();
108
+ }
109
+ datastore = new JsonLinesDatastore(getJslFileName(jslid), formatterFunction);
106
110
  // datastore = new DatastoreProxy(getJslFileName(jslid));
107
111
  this.datastores[jslid] = datastore;
108
112
  }
@@ -131,8 +135,8 @@ module.exports = {
131
135
  },
132
136
 
133
137
  getRows_meta: true,
134
- async getRows({ jslid, offset, limit, filters }) {
135
- const datastore = await this.ensureDatastore(jslid);
138
+ async getRows({ jslid, offset, limit, filters, formatterFunction }) {
139
+ const datastore = await this.ensureDatastore(jslid, formatterFunction);
136
140
  return datastore.getRows(offset, limit, _.isEmpty(filters) ? null : filters);
137
141
  },
138
142
 
@@ -150,8 +154,8 @@ module.exports = {
150
154
  },
151
155
 
152
156
  loadFieldValues_meta: true,
153
- async loadFieldValues({ jslid, field, search }) {
154
- const datastore = await this.ensureDatastore(jslid);
157
+ async loadFieldValues({ jslid, field, search, formatterFunction }) {
158
+ const datastore = await this.ensureDatastore(jslid, formatterFunction);
155
159
  const res = new Set();
156
160
  await datastore.enumRows(row => {
157
161
  if (!filterName(search, row[field])) return true;
@@ -188,4 +192,85 @@ module.exports = {
188
192
  await fs.promises.writeFile(getJslFileName(jslid), text);
189
193
  return true;
190
194
  },
195
+
196
+ extractTimelineChart_meta: true,
197
+ async extractTimelineChart({ jslid, timestampFunction, aggregateFunction, measures }) {
198
+ const timestamp = requirePluginFunction(timestampFunction);
199
+ const aggregate = requirePluginFunction(aggregateFunction);
200
+ const datastore = new JsonLinesDatastore(getJslFileName(jslid));
201
+ let mints = null;
202
+ let maxts = null;
203
+ // pass 1 - counts stats, time range
204
+ await datastore.enumRows(row => {
205
+ const ts = timestamp(row);
206
+ if (!mints || ts < mints) mints = ts;
207
+ if (!maxts || ts > maxts) maxts = ts;
208
+ return true;
209
+ });
210
+ const minTime = new Date(mints).getTime();
211
+ const maxTime = new Date(maxts).getTime();
212
+ const duration = maxTime - minTime;
213
+ const STEPS = 100;
214
+ let stepCount = duration > 100 * 1000 ? STEPS : Math.round((maxTime - minTime) / 1000);
215
+ if (stepCount < 2) {
216
+ stepCount = 2;
217
+ }
218
+ const stepDuration = duration / stepCount;
219
+ const labels = _.range(stepCount).map(i => new Date(minTime + stepDuration / 2 + stepDuration * i));
220
+
221
+ // const datasets = measures.map(m => ({
222
+ // label: m.label,
223
+ // data: Array(stepCount).fill(0),
224
+ // }));
225
+
226
+ const mproc = measures.map(m => ({
227
+ ...m,
228
+ }));
229
+
230
+ const data = Array(stepCount)
231
+ .fill(0)
232
+ .map(() => ({}));
233
+
234
+ // pass 2 - count measures
235
+ await datastore.enumRows(row => {
236
+ const ts = timestamp(row);
237
+ let part = Math.round((new Date(ts).getTime() - minTime) / stepDuration);
238
+ if (part < 0) part = 0;
239
+ if (part >= stepCount) part - stepCount - 1;
240
+ if (data[part]) {
241
+ data[part] = aggregate(data[part], row, stepDuration);
242
+ }
243
+ return true;
244
+ });
245
+
246
+ datastore._closeReader();
247
+
248
+ // const measureByField = _.fromPairs(measures.map((m, i) => [m.field, i]));
249
+
250
+ // for (let mindex = 0; mindex < measures.length; mindex++) {
251
+ // for (let stepIndex = 0; stepIndex < stepCount; stepIndex++) {
252
+ // const measure = measures[mindex];
253
+ // if (measure.perSecond) {
254
+ // datasets[mindex].data[stepIndex] /= stepDuration / 1000;
255
+ // }
256
+ // if (measure.perField) {
257
+ // datasets[mindex].data[stepIndex] /= datasets[measureByField[measure.perField]].data[stepIndex];
258
+ // }
259
+ // }
260
+ // }
261
+
262
+ // for (let i = 0; i < measures.length; i++) {
263
+ // if (measures[i].hidden) {
264
+ // datasets[i] = null;
265
+ // }
266
+ // }
267
+
268
+ return {
269
+ labels,
270
+ datasets: mproc.map(m => ({
271
+ label: m.label,
272
+ data: data.map(d => d[m.field] || 0),
273
+ })),
274
+ };
275
+ },
191
276
  };
@@ -1,6 +1,7 @@
1
1
  const connections = require('./connections');
2
2
  const socket = require('../utility/socket');
3
3
  const { fork } = require('child_process');
4
+ const uuidv1 = require('uuid/v1');
4
5
  const _ = require('lodash');
5
6
  const AsyncLock = require('async-lock');
6
7
  const { handleProcessCommunication } = require('../utility/processComm');
@@ -8,23 +9,25 @@ const lock = new AsyncLock();
8
9
  const config = require('./config');
9
10
  const processArgs = require('../utility/processArgs');
10
11
  const { testConnectionPermission } = require('../utility/hasPermission');
12
+ const { MissingCredentialsError } = require('../utility/exceptions');
11
13
 
12
14
  module.exports = {
13
15
  opened: [],
14
16
  closed: {},
15
17
  lastPinged: {},
18
+ requests: {},
16
19
 
17
20
  handle_databases(conid, { databases }) {
18
21
  const existing = this.opened.find(x => x.conid == conid);
19
22
  if (!existing) return;
20
23
  existing.databases = databases;
21
- socket.emitChanged(`database-list-changed-${conid}`);
24
+ socket.emitChanged(`database-list-changed`, { conid });
22
25
  },
23
26
  handle_version(conid, { version }) {
24
27
  const existing = this.opened.find(x => x.conid == conid);
25
28
  if (!existing) return;
26
29
  existing.version = version;
27
- socket.emitChanged(`server-version-changed-${conid}`);
30
+ socket.emitChanged(`server-version-changed`, { conid });
28
31
  },
29
32
  handle_status(conid, { status }) {
30
33
  const existing = this.opened.find(x => x.conid == conid);
@@ -33,12 +36,20 @@ module.exports = {
33
36
  socket.emitChanged(`server-status-changed`);
34
37
  },
35
38
  handle_ping() {},
39
+ handle_response(conid, { msgid, ...response }) {
40
+ const [resolve, reject] = this.requests[msgid];
41
+ resolve(response);
42
+ delete this.requests[msgid];
43
+ },
36
44
 
37
45
  async ensureOpened(conid) {
38
46
  const res = await lock.acquire(conid, async () => {
39
47
  const existing = this.opened.find(x => x.conid == conid);
40
48
  if (existing) return existing;
41
49
  const connection = await connections.getCore({ conid });
50
+ if (connection.passwordMode == 'askPassword' || connection.passwordMode == 'askUser') {
51
+ throw new MissingCredentialsError({ conid, passwordMode: connection.passwordMode });
52
+ }
42
53
  const subprocess = fork(global['API_PACKAGE'] || process.argv[1], [
43
54
  '--is-forked-api',
44
55
  '--start-process',
@@ -120,9 +131,9 @@ module.exports = {
120
131
  },
121
132
 
122
133
  ping_meta: true,
123
- async ping({ connections }) {
134
+ async ping({ conidArray }) {
124
135
  await Promise.all(
125
- _.uniq(connections).map(async conid => {
136
+ _.uniq(conidArray).map(async conid => {
126
137
  const last = this.lastPinged[conid];
127
138
  if (last && new Date().getTime() - last < 30 * 1000) {
128
139
  return Promise.resolve();
@@ -161,4 +172,41 @@ module.exports = {
161
172
  opened.subprocess.send({ msgtype: 'dropDatabase', name });
162
173
  return { status: 'ok' };
163
174
  },
175
+
176
+ sendRequest(conn, message) {
177
+ const msgid = uuidv1();
178
+ const promise = new Promise((resolve, reject) => {
179
+ this.requests[msgid] = [resolve, reject];
180
+ conn.subprocess.send({ msgid, ...message });
181
+ });
182
+ return promise;
183
+ },
184
+
185
+ async loadDataCore(msgtype, { conid, ...args }, req) {
186
+ testConnectionPermission(conid, req);
187
+ const opened = await this.ensureOpened(conid);
188
+ const res = await this.sendRequest(opened, { msgtype, ...args });
189
+ if (res.errorMessage) {
190
+ console.error(res.errorMessage);
191
+
192
+ return {
193
+ errorMessage: res.errorMessage,
194
+ };
195
+ }
196
+ return res.result || null;
197
+ },
198
+
199
+ serverSummary_meta: true,
200
+ async serverSummary({ conid }, req) {
201
+ testConnectionPermission(conid, req);
202
+ return this.loadDataCore('serverSummary', { conid });
203
+ },
204
+
205
+ summaryCommand_meta: true,
206
+ async summaryCommand({ conid, command, row }, req) {
207
+ testConnectionPermission(conid, req);
208
+ const opened = await this.ensureOpened(conid);
209
+ if (opened.connection.isReadOnly) return false;
210
+ return this.loadDataCore('summaryCommand', { conid, command, row });
211
+ },
164
212
  };
@@ -150,6 +150,31 @@ module.exports = {
150
150
  return true;
151
151
  },
152
152
 
153
+ startProfiler_meta: true,
154
+ async startProfiler({ sesid }) {
155
+ const jslid = uuidv1();
156
+ const session = this.opened.find(x => x.sesid == sesid);
157
+ if (!session) {
158
+ throw new Error('Invalid session');
159
+ }
160
+
161
+ console.log(`Starting profiler, sesid=${sesid}`);
162
+ session.loadingReader_jslid = jslid;
163
+ session.subprocess.send({ msgtype: 'startProfiler', jslid });
164
+
165
+ return { state: 'ok', jslid };
166
+ },
167
+
168
+ stopProfiler_meta: true,
169
+ async stopProfiler({ sesid }) {
170
+ const session = this.opened.find(x => x.sesid == sesid);
171
+ if (!session) {
172
+ throw new Error('Invalid session');
173
+ }
174
+ session.subprocess.send({ msgtype: 'stopProfiler' });
175
+ return { state: 'ok' };
176
+ },
177
+
153
178
  // cancel_meta: true,
154
179
  // async cancel({ sesid }) {
155
180
  // const session = this.opened.find((x) => x.sesid == sesid);
@@ -1,5 +1,5 @@
1
1
 
2
2
  module.exports = {
3
- version: '5.1.6',
4
- buildTime: '2022-11-14T18:39:13.832Z'
3
+ version: '5.1.7-alpha.14',
4
+ buildTime: '2023-01-02T17:51:07.060Z'
5
5
  };
package/src/main.js CHANGED
@@ -20,6 +20,7 @@ const jsldata = require('./controllers/jsldata');
20
20
  const config = require('./controllers/config');
21
21
  const archive = require('./controllers/archive');
22
22
  const apps = require('./controllers/apps');
23
+ const auth = require('./controllers/auth');
23
24
  const uploads = require('./controllers/uploads');
24
25
  const plugins = require('./controllers/plugins');
25
26
  const files = require('./controllers/files');
@@ -41,7 +42,7 @@ function start() {
41
42
  const server = http.createServer(app);
42
43
 
43
44
  const logins = getLogins();
44
- if (logins) {
45
+ if (logins && process.env.BASIC_AUTH) {
45
46
  app.use(
46
47
  basicAuth({
47
48
  users: _.fromPairs(logins.map(x => [x.login, x.password])),
@@ -53,6 +54,25 @@ function start() {
53
54
 
54
55
  app.use(cors());
55
56
 
57
+ if (platformInfo.isDocker) {
58
+ // server static files inside docker container
59
+ app.use(getExpressPath('/'), express.static('/home/dbgate-docker/public'));
60
+ } else if (platformInfo.isNpmDist) {
61
+ app.use(getExpressPath('/'), express.static(path.join(__dirname, '../../dbgate-web/public')));
62
+ } else if (process.env.DEVWEB) {
63
+ console.log('__dirname', __dirname);
64
+ console.log(path.join(__dirname, '../../web/public/build'));
65
+ app.use(getExpressPath('/'), express.static(path.join(__dirname, '../../web/public')));
66
+ } else {
67
+ app.get(getExpressPath('/'), (req, res) => {
68
+ res.send('DbGate API');
69
+ });
70
+ }
71
+
72
+ if (auth.shouldAuthorizeApi()) {
73
+ app.use(auth.authMiddleware);
74
+ }
75
+
56
76
  app.get(getExpressPath('/stream'), async function (req, res) {
57
77
  res.set({
58
78
  'Cache-Control': 'no-cache',
@@ -88,14 +108,10 @@ function start() {
88
108
  app.use(getExpressPath('/runners/data'), express.static(rundir()));
89
109
 
90
110
  if (platformInfo.isDocker) {
91
- // server static files inside docker container
92
- app.use(getExpressPath('/'), express.static('/home/dbgate-docker/public'));
93
-
94
111
  const port = process.env.PORT || 3000;
95
112
  console.log('DbGate API listening on port (docker build)', port);
96
113
  server.listen(port);
97
114
  } else if (platformInfo.isNpmDist) {
98
- app.use(getExpressPath('/'), express.static(path.join(__dirname, '../../dbgate-web/public')));
99
115
  getPort({
100
116
  port: parseInt(
101
117
  // @ts-ignore
@@ -107,18 +123,10 @@ function start() {
107
123
  });
108
124
  });
109
125
  } else if (process.env.DEVWEB) {
110
- console.log('__dirname', __dirname);
111
- console.log(path.join(__dirname, '../../web/public/build'));
112
- app.use(getExpressPath('/'), express.static(path.join(__dirname, '../../web/public')));
113
-
114
126
  const port = process.env.PORT || 3000;
115
127
  console.log('DbGate API & web listening on port (dev web build)', port);
116
128
  server.listen(port);
117
129
  } else {
118
- app.get(getExpressPath('/'), (req, res) => {
119
- res.send('DbGate API');
120
- });
121
-
122
130
  const port = process.env.PORT || 3000;
123
131
  console.log('DbGate API listening on port (dev API build)', port);
124
132
  server.listen(port);
@@ -157,6 +165,7 @@ function useAllControllers(app, electron) {
157
165
  useController(app, electron, '/scheduler', scheduler);
158
166
  useController(app, electron, '/query-history', queryHistory);
159
167
  useController(app, electron, '/apps', apps);
168
+ useController(app, electron, '/auth', auth);
160
169
  }
161
170
 
162
171
  function setElectronSender(electronSender) {
@@ -2,6 +2,6 @@
2
2
  // this file is generated automatically by script fillNativeModules.js, do not edit it manually
3
3
  const content = {};
4
4
 
5
- content['better-sqlite3'] = () => require('better-sqlite3');
5
+ content.oracledb = () => require('oracledb');content['better-sqlite3'] = () => require('better-sqlite3');
6
6
 
7
7
  module.exports = content;
@@ -10,6 +10,7 @@ let storedConnection;
10
10
  let lastDatabases = null;
11
11
  let lastStatus = null;
12
12
  let lastPing = null;
13
+ let afterConnectCallbacks = [];
13
14
 
14
15
  async function handleRefresh() {
15
16
  const driver = requireEngineDriver(storedConnection);
@@ -74,6 +75,18 @@ async function handleConnect(connection) {
74
75
  // console.error(err);
75
76
  setTimeout(() => process.exit(1), 1000);
76
77
  }
78
+
79
+ for (const [resolve] of afterConnectCallbacks) {
80
+ resolve();
81
+ }
82
+ afterConnectCallbacks = [];
83
+ }
84
+
85
+ function waitConnected() {
86
+ if (systemConnection) return Promise.resolve();
87
+ return new Promise((resolve, reject) => {
88
+ afterConnectCallbacks.push([resolve, reject]);
89
+ });
77
90
  }
78
91
 
79
92
  function handlePing() {
@@ -94,9 +107,30 @@ async function handleDatabaseOp(op, { name }) {
94
107
  await handleRefresh();
95
108
  }
96
109
 
110
+ async function handleDriverDataCore(msgid, callMethod) {
111
+ await waitConnected();
112
+ const driver = requireEngineDriver(storedConnection);
113
+ try {
114
+ const result = await callMethod(driver);
115
+ process.send({ msgtype: 'response', msgid, result });
116
+ } catch (err) {
117
+ process.send({ msgtype: 'response', msgid, errorMessage: err.message });
118
+ }
119
+ }
120
+
121
+ async function handleServerSummary({ msgid }) {
122
+ return handleDriverDataCore(msgid, driver => driver.serverSummary(systemConnection));
123
+ }
124
+
125
+ async function handleSummaryCommand({ msgid, command, row }) {
126
+ return handleDriverDataCore(msgid, driver => driver.summaryCommand(systemConnection, command, row));
127
+ }
128
+
97
129
  const messageHandlers = {
98
130
  connect: handleConnect,
99
131
  ping: handlePing,
132
+ serverSummary: handleServerSummary,
133
+ summaryCommand: handleSummaryCommand,
100
134
  createDatabase: props => handleDatabaseOp('createDatabase', props),
101
135
  dropDatabase: props => handleDatabaseOp('dropDatabase', props),
102
136
  };
@@ -16,6 +16,7 @@ let storedConnection;
16
16
  let afterConnectCallbacks = [];
17
17
  // let currentHandlers = [];
18
18
  let lastPing = null;
19
+ let currentProfiler = null;
19
20
 
20
21
  class TableWriter {
21
22
  constructor() {
@@ -210,6 +211,31 @@ function waitConnected() {
210
211
  });
211
212
  }
212
213
 
214
+ async function handleStartProfiler({ jslid }) {
215
+ await waitConnected();
216
+ const driver = requireEngineDriver(storedConnection);
217
+
218
+ if (!allowExecuteCustomScript(driver)) {
219
+ process.send({ msgtype: 'done' });
220
+ return;
221
+ }
222
+
223
+ const writer = new TableWriter();
224
+ writer.initializeFromReader(jslid);
225
+
226
+ currentProfiler = await driver.startProfiler(systemConnection, {
227
+ row: data => writer.rowFromReader(data),
228
+ });
229
+ currentProfiler.writer = writer;
230
+ }
231
+
232
+ async function handleStopProfiler({ jslid }) {
233
+ const driver = requireEngineDriver(storedConnection);
234
+ currentProfiler.writer.close();
235
+ driver.stopProfiler(systemConnection, currentProfiler);
236
+ currentProfiler = null;
237
+ }
238
+
213
239
  async function handleExecuteQuery({ sql }) {
214
240
  await waitConnected();
215
241
  const driver = requireEngineDriver(storedConnection);
@@ -280,6 +306,8 @@ const messageHandlers = {
280
306
  connect: handleConnect,
281
307
  executeQuery: handleExecuteQuery,
282
308
  executeReader: handleExecuteReader,
309
+ startProfiler: handleStartProfiler,
310
+ stopProfiler: handleStopProfiler,
283
311
  ping: handlePing,
284
312
  // cancel: handleCancel,
285
313
  };
@@ -90,6 +90,12 @@ class JsonLinesDatabase {
90
90
  return obj;
91
91
  }
92
92
 
93
+ async updateAll(mapFunction) {
94
+ await this._ensureLoaded();
95
+ this.data = this.data.map(mapFunction);
96
+ await this._save();
97
+ }
98
+
93
99
  async patch(id, values) {
94
100
  await this._ensureLoaded();
95
101
  this.data = this.data.map(x => (x._id == id ? { ...x, ...values } : x));
@@ -3,6 +3,7 @@ const AsyncLock = require('async-lock');
3
3
  const lock = new AsyncLock();
4
4
  const stableStringify = require('json-stable-stringify');
5
5
  const { evaluateCondition } = require('dbgate-sqltree');
6
+ const requirePluginFunction = require('./requirePluginFunction');
6
7
 
7
8
  function fetchNextLineFromReader(reader) {
8
9
  return new Promise((resolve, reject) => {
@@ -22,14 +23,16 @@ function fetchNextLineFromReader(reader) {
22
23
  }
23
24
 
24
25
  class JsonLinesDatastore {
25
- constructor(file) {
26
+ constructor(file, formatterFunction) {
26
27
  this.file = file;
28
+ this.formatterFunction = formatterFunction;
27
29
  this.reader = null;
28
30
  this.readedDataRowCount = 0;
29
31
  this.readedSchemaRow = false;
30
32
  // this.firstRowToBeReturned = null;
31
33
  this.notifyChangedCallback = null;
32
34
  this.currentFilter = null;
35
+ this.rowFormatter = requirePluginFunction(formatterFunction);
33
36
  }
34
37
 
35
38
  _closeReader() {
@@ -62,6 +65,11 @@ class JsonLinesDatastore {
62
65
  );
63
66
  }
64
67
 
68
+ parseLine(line) {
69
+ const res = JSON.parse(line);
70
+ return this.rowFormatter ? this.rowFormatter(res) : res;
71
+ }
72
+
65
73
  async _readLine(parse) {
66
74
  // if (this.firstRowToBeReturned) {
67
75
  // const res = this.firstRowToBeReturned;
@@ -84,14 +92,14 @@ class JsonLinesDatastore {
84
92
  }
85
93
  }
86
94
  if (this.currentFilter) {
87
- const parsedLine = JSON.parse(line);
95
+ const parsedLine = this.parseLine(line);
88
96
  if (evaluateCondition(this.currentFilter, parsedLine)) {
89
97
  this.readedDataRowCount += 1;
90
98
  return parse ? parsedLine : true;
91
99
  }
92
100
  } else {
93
101
  this.readedDataRowCount += 1;
94
- return parse ? JSON.parse(line) : true;
102
+ return parse ? this.parseLine(line) : true;
95
103
  }
96
104
  }
97
105
 
@@ -0,0 +1,9 @@
1
+ class MissingCredentialsError {
2
+ constructor(detail) {
3
+ this.detail = detail;
4
+ }
5
+ }
6
+
7
+ module.exports = {
8
+ MissingCredentialsError,
9
+ };
@@ -0,0 +1,16 @@
1
+ const _ = require('lodash');
2
+ const requirePlugin = require('../shell/requirePlugin');
3
+
4
+ function requirePluginFunction(functionName) {
5
+ if (!functionName) return null;
6
+ if (functionName.includes('@')) {
7
+ const [shortName, packageName] = functionName.split('@');
8
+ const plugin = requirePlugin(packageName);
9
+ if (plugin.functions) {
10
+ return plugin.functions[shortName];
11
+ }
12
+ }
13
+ return null;
14
+ }
15
+
16
+ module.exports = requirePluginFunction;
@@ -1,4 +1,5 @@
1
1
  const _ = require('lodash');
2
+ const stableStringify = require('json-stable-stringify');
2
3
 
3
4
  const sseResponses = [];
4
5
  let electronSender = null;
@@ -27,12 +28,12 @@ module.exports = {
27
28
  electronSender.send(message, data == null ? null : data);
28
29
  }
29
30
  for (const res of sseResponses) {
30
- res.write(`event: ${message}\ndata: ${JSON.stringify(data == null ? null : data)}\n\n`);
31
+ res.write(`event: ${message}\ndata: ${stableStringify(data == null ? null : data)}\n\n`);
31
32
  }
32
33
  },
33
- emitChanged(key) {
34
+ emitChanged(key, params = undefined) {
34
35
  // console.log('EMIT CHANGED', key);
35
- this.emit('changed-cache', key);
36
+ this.emit('changed-cache', { key, ...params });
36
37
  // this.emit(key);
37
38
  },
38
39
  };
@@ -1,6 +1,7 @@
1
1
  const _ = require('lodash');
2
2
  const express = require('express');
3
3
  const getExpressPath = require('./getExpressPath');
4
+ const { MissingCredentialsError } = require('./exceptions');
4
5
 
5
6
  /**
6
7
  * @param {string} route
@@ -37,6 +38,13 @@ module.exports = function useController(app, electron, route, controller) {
37
38
  if (data === undefined) return null;
38
39
  return data;
39
40
  } catch (err) {
41
+ if (err instanceof MissingCredentialsError) {
42
+ return {
43
+ missingCredentials: true,
44
+ apiErrorMessage: 'Missing credentials',
45
+ detail: err.detail,
46
+ };
47
+ }
40
48
  return { apiErrorMessage: err.message };
41
49
  }
42
50
  });
@@ -69,7 +77,15 @@ module.exports = function useController(app, electron, route, controller) {
69
77
  res.json(data);
70
78
  } catch (e) {
71
79
  console.log(e);
72
- res.status(500).json({ apiErrorMessage: e.message });
80
+ if (e instanceof MissingCredentialsError) {
81
+ res.json({
82
+ missingCredentials: true,
83
+ apiErrorMessage: 'Missing credentials',
84
+ detail: e.detail,
85
+ });
86
+ } else {
87
+ res.status(500).json({ apiErrorMessage: e.message });
88
+ }
73
89
  }
74
90
  });
75
91
  }