dbgate-api-premium 7.1.12 → 7.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dbgate-api-premium",
3
3
  "main": "src/index.js",
4
- "version": "7.1.12",
4
+ "version": "7.2.0",
5
5
  "homepage": "https://www.dbgate.io/",
6
6
  "repository": {
7
7
  "type": "git",
@@ -30,11 +30,11 @@
30
30
  "compare-versions": "^3.6.0",
31
31
  "cors": "^2.8.5",
32
32
  "cross-env": "^6.0.3",
33
- "dbgate-datalib": "7.1.12",
33
+ "dbgate-datalib": "7.2.0",
34
34
  "dbgate-query-splitter": "^4.12.0",
35
- "dbgate-rest": "7.1.12",
36
- "dbgate-sqltree": "7.1.12",
37
- "dbgate-tools": "7.1.12",
35
+ "dbgate-rest": "7.2.0",
36
+ "dbgate-sqltree": "7.2.0",
37
+ "dbgate-tools": "7.2.0",
38
38
  "debug": "^4.3.4",
39
39
  "diff": "^5.0.0",
40
40
  "diff2html": "^3.4.13",
@@ -96,7 +96,7 @@
96
96
  "@types/fs-extra": "^9.0.11",
97
97
  "@types/jest": "^30.0.0",
98
98
  "@types/lodash": "^4.14.149",
99
- "dbgate-types": "7.1.12",
99
+ "dbgate-types": "7.2.0",
100
100
  "env-cmd": "^10.1.0",
101
101
  "jest": "^30.4.2",
102
102
  "jsdoc-to-markdown": "^9.0.5",
@@ -517,6 +517,21 @@ module.exports = {
517
517
  return res.result || null;
518
518
  },
519
519
 
520
+ saveQueryResultData_meta: true,
521
+ async saveQueryResultData({ conid, database, changeSet, sql }, req) {
522
+ await testConnectionPermission(conid, req);
523
+ await testDatabaseRolePermission(conid, database, 'run_script', req);
524
+
525
+ const opened = await this.ensureOpened(conid, database);
526
+ const res = await this.sendRequest(opened, { msgtype: 'saveQueryResultData', changeSet, sql });
527
+ if (res.errorMessage) {
528
+ return {
529
+ errorMessage: res.errorMessage,
530
+ };
531
+ }
532
+ return res.result || { state: 'ok' };
533
+ },
534
+
520
535
  multiCallMethod_meta: true,
521
536
  async multiCallMethod({ conid, database, callList }, req) {
522
537
  await testConnectionPermission(conid, req);
@@ -21,6 +21,7 @@ const logger = getLogger('sessions');
21
21
  module.exports = {
22
22
  /** @type {import('dbgate-types').OpenedSession[]} */
23
23
  opened: [],
24
+ requests: {},
24
25
 
25
26
  // handle_error(sesid, props) {
26
27
  // const { error } = props;
@@ -115,6 +116,44 @@ module.exports = {
115
116
 
116
117
  handle_ping() {},
117
118
 
119
+ handle_response(sesid, props) {
120
+ const { msgid } = props;
121
+ const request = this.requests[msgid];
122
+ if (!request) return;
123
+ delete this.requests[msgid];
124
+ request.resolve(_.omit(props, ['msgtype', 'msgid', 'sesid']));
125
+ },
126
+
127
+ sendRequest(session, message) {
128
+ const msgid = crypto.randomUUID();
129
+ return new Promise((resolve, reject) => {
130
+ const cleanup = () => {
131
+ session.subprocess.off('exit', handleExit);
132
+ delete this.requests[msgid];
133
+ };
134
+ const handleExit = () => {
135
+ cleanup();
136
+ reject(new Error('DBGM-00000 Session process exited before response was received'));
137
+ };
138
+ this.requests[msgid] = {
139
+ resolve: value => {
140
+ cleanup();
141
+ resolve(value);
142
+ },
143
+ reject: error => {
144
+ cleanup();
145
+ reject(error);
146
+ },
147
+ };
148
+ session.subprocess.once('exit', handleExit);
149
+ session.subprocess.send({ ...message, msgid }, err => {
150
+ if (err && this.requests[msgid]) {
151
+ this.requests[msgid].reject(err);
152
+ }
153
+ });
154
+ });
155
+ },
156
+
118
157
  create_meta: true,
119
158
  async create({ conid, database }) {
120
159
  const sesid = crypto.randomUUID();
@@ -202,6 +241,12 @@ module.exports = {
202
241
  message: 'Query execution started',
203
242
  sql,
204
243
  });
244
+ let dbinfo = null;
245
+ try {
246
+ dbinfo = (await require('./databaseConnections').ensureOpened(session.conid, session.database))?.structure;
247
+ } catch (err) {
248
+ logger.warn(extractErrorLogData(err), 'DBGM-00000 Error loading structure for query result metadata');
249
+ }
205
250
  session.subprocess.send({
206
251
  msgtype: 'executeQuery',
207
252
  sql,
@@ -209,6 +254,7 @@ module.exports = {
209
254
  autoDetectCharts: autoDetectCharts || !!frontMatter?.['selected-chart'],
210
255
  limitRows,
211
256
  frontMatter,
257
+ dbinfo,
212
258
  });
213
259
 
214
260
  return { state: 'ok' };
@@ -228,6 +274,24 @@ module.exports = {
228
274
  return { state: 'ok' };
229
275
  },
230
276
 
277
+ saveQueryResultData_meta: true,
278
+ async saveQueryResultData({ sesid, changeSet, sql, autoCommit }, req) {
279
+ await testStandardPermission('dbops/query', req);
280
+ const session = this.opened.find(x => x.sesid == sesid);
281
+ if (!session) {
282
+ throw new Error('Invalid session');
283
+ }
284
+ await testDatabaseRolePermission(session.conid, session.database, 'run_script', req);
285
+
286
+ const res = await this.sendRequest(session, { msgtype: 'saveQueryResultData', changeSet, sql, autoCommit });
287
+ if (res.errorMessage) {
288
+ return {
289
+ errorMessage: res.errorMessage,
290
+ };
291
+ }
292
+ return res.result || { state: 'ok' };
293
+ },
294
+
231
295
  setIsolationLevel_meta: true,
232
296
  async setIsolationLevel({ sesid, level }) {
233
297
  const session = this.opened.find(x => x.sesid == sesid);
@@ -1,5 +1,5 @@
1
1
 
2
2
  module.exports = {
3
- version: '7.1.12',
4
- buildTime: '2026-05-20T09:12:47.123Z'
3
+ version: '7.2.0',
4
+ buildTime: '2026-06-08T11:54:20.800Z'
5
5
  };
@@ -19,12 +19,14 @@ const { handleProcessCommunication } = require('../utility/processComm');
19
19
  const generateDeploySql = require('../shell/generateDeploySql');
20
20
  const { dumpSqlSelect, scriptToSql } = require('dbgate-sqltree');
21
21
  const { allowExecuteCustomScript, handleQueryStream } = require('../utility/handleQueryStream');
22
+ const { enrichQueryResultColumns } = require('../utility/queryResultMetadata');
22
23
  const dbgateApi = require('../shell');
23
24
  const requirePlugin = require('../shell/requirePlugin');
24
25
  const path = require('path');
25
26
  const { rundir } = require('../utility/directories');
26
27
  const fs = require('fs-extra');
27
28
  const { changeSetToSql } = require('dbgate-datalib');
29
+ const _ = require('lodash');
28
30
 
29
31
  const logger = getLogger('dbconnProcess');
30
32
 
@@ -253,6 +255,9 @@ async function handleQueryData({ msgid, sql, range, commandTimeout }, skipReadon
253
255
  try {
254
256
  if (!skipReadonlyCheck) ensureExecuteCustomScript(driver);
255
257
  const res = await driver.query(dbhan, sql, { range, commandTimeout });
258
+ if (res?.columns) {
259
+ res.columns = await enrichQueryResultColumns({ dbhan, driver, sql, columns: res.columns, dbinfo: analysedStructure });
260
+ }
256
261
  process.send({ msgtype: 'response', msgid, ...serializeJsTypesForJsonStringify(res) });
257
262
  } catch (err) {
258
263
  process.send({
@@ -381,6 +386,55 @@ async function handleSaveTableData({ msgid, changeSet }) {
381
386
  }
382
387
  }
383
388
 
389
+ function validateQueryResultChangeSet(driver, changeSet) {
390
+ if (!driver.databaseEngineTypes?.includes('sql') || !driver.supportsEditableQueryResults) {
391
+ throw new Error('DBGM-00000 Editable query results are not supported by this driver');
392
+ }
393
+ if (changeSet?.inserts?.length > 0 || changeSet?.deletes?.length > 0) {
394
+ throw new Error('DBGM-00000 Query result saving supports UPDATE operations only');
395
+ }
396
+ for (const update of changeSet?.updates || []) {
397
+ if (!update.pureName) {
398
+ throw new Error('DBGM-00000 Query result update is missing target table');
399
+ }
400
+ if (_.isEmpty(update.fields)) {
401
+ throw new Error('DBGM-00000 Query result update is missing changed fields');
402
+ }
403
+ if (_.isEmpty(update.condition)) {
404
+ throw new Error('DBGM-00000 Query result update is missing row condition');
405
+ }
406
+ if (Object.values(update.condition).some(value => value === null || value === undefined)) {
407
+ throw new Error('DBGM-00000 Query result update has incomplete row condition');
408
+ }
409
+ }
410
+ }
411
+
412
+ async function handleSaveQueryResultData({ msgid, changeSet, sql }) {
413
+ await waitConnected();
414
+ const driver = requireEngineDriver(storedConnection);
415
+ try {
416
+ ensureExecuteCustomScript(driver);
417
+ validateQueryResultChangeSet(driver, changeSet);
418
+ if (!sql) {
419
+ const script = changeSetToSql({ ...changeSet, inserts: [], deletes: [] }, null, driver.dialect);
420
+ if (script.some(command => command.commandType != 'update')) {
421
+ throw new Error('DBGM-00000 Query result saving supports UPDATE operations only');
422
+ }
423
+ sql = scriptToSql(driver, script);
424
+ }
425
+ if (sql) {
426
+ await driver.script(dbhan, sql, { useTransaction: false });
427
+ }
428
+ process.send({ msgtype: 'response', msgid, result: { state: 'ok' } });
429
+ } catch (err) {
430
+ process.send({
431
+ msgtype: 'response',
432
+ msgid,
433
+ errorMessage: extractErrorMessage(err, 'DBGM-00000 Error saving query result data'),
434
+ });
435
+ }
436
+ }
437
+
384
438
  async function handleMultiCallMethod({ msgid, callList }) {
385
439
  try {
386
440
  const driver = requireEngineDriver(storedConnection);
@@ -557,7 +611,7 @@ async function handleExecuteSessionQuery({ sesid, sql }) {
557
611
  ...driver.getQuerySplitterOptions('stream'),
558
612
  returnRichInfo: true,
559
613
  })) {
560
- await handleQueryStream(dbhan, driver, queryStreamInfoHolder, sqlItem, sesid);
614
+ await handleQueryStream(dbhan, driver, queryStreamInfoHolder, sqlItem, sesid, undefined, undefined, false, analysedStructure);
561
615
  if (queryStreamInfoHolder.canceled) {
562
616
  break;
563
617
  }
@@ -599,6 +653,7 @@ const messageHandlers = {
599
653
  runOperation: handleRunOperation,
600
654
  updateCollection: handleUpdateCollection,
601
655
  saveTableData: handleSaveTableData,
656
+ saveQueryResultData: handleSaveQueryResultData,
602
657
  collectionData: handleCollectionData,
603
658
  loadKeys: handleLoadKeys,
604
659
  scanKeys: handleScanKeys,
@@ -10,7 +10,9 @@ const requireEngineDriver = require('../utility/requireEngineDriver');
10
10
  const { decryptConnection } = require('../utility/crypting');
11
11
  const { connectUtility } = require('../utility/connectUtility');
12
12
  const { handleProcessCommunication } = require('../utility/processComm');
13
- const { getLogger, extractIntSettingsValue, extractBoolSettingsValue } = require('dbgate-tools');
13
+ const { getLogger, extractIntSettingsValue, extractBoolSettingsValue, extractErrorMessage } = require('dbgate-tools');
14
+ const { changeSetToSql } = require('dbgate-datalib');
15
+ const { scriptToSql } = require('dbgate-sqltree');
14
16
  const { handleQueryStream, QueryStreamTableWriter, allowExecuteCustomScript } = require('../utility/handleQueryStream');
15
17
 
16
18
  const logger = getLogger('sessionProcess');
@@ -119,7 +121,7 @@ async function handleExecuteControlCommand({ command }) {
119
121
  process.send({
120
122
  msgtype: 'info',
121
123
  info: {
122
- message: 'Connection without read-only sessions is read only',
124
+ message: 'Connection is read-only',
123
125
  severity: 'error',
124
126
  },
125
127
  });
@@ -149,7 +151,7 @@ async function handleExecuteControlCommand({ command }) {
149
151
  }
150
152
  }
151
153
 
152
- async function handleExecuteQuery({ sql, autoCommit, autoDetectCharts, limitRows, frontMatter }) {
154
+ async function handleExecuteQuery({ sql, autoCommit, autoDetectCharts, limitRows, frontMatter, dbinfo }) {
153
155
  lastActivity = new Date().getTime();
154
156
 
155
157
  await waitConnected();
@@ -159,7 +161,7 @@ async function handleExecuteQuery({ sql, autoCommit, autoDetectCharts, limitRows
159
161
  process.send({
160
162
  msgtype: 'info',
161
163
  info: {
162
- message: 'Connection without read-only sessions is read only',
164
+ message: 'Connection is read-only',
163
165
  severity: 'error',
164
166
  },
165
167
  });
@@ -186,7 +188,8 @@ async function handleExecuteQuery({ sql, autoCommit, autoDetectCharts, limitRows
186
188
  undefined,
187
189
  limitRows,
188
190
  frontMatter,
189
- autoDetectCharts
191
+ autoDetectCharts,
192
+ dbinfo
190
193
  );
191
194
  // const handler = new StreamHandler(resultIndex);
192
195
  // const stream = await driver.stream(systemConnection, sqlItem, handler);
@@ -203,6 +206,69 @@ async function handleExecuteQuery({ sql, autoCommit, autoDetectCharts, limitRows
203
206
  }
204
207
  }
205
208
 
209
+ function validateQueryResultChangeSet(driver, changeSet) {
210
+ if (!driver.databaseEngineTypes?.includes('sql') || !driver.supportsEditableQueryResults) {
211
+ throw new Error('DBGM-00000 Editable query results are not supported by this driver');
212
+ }
213
+ if (changeSet?.inserts?.length > 0 || changeSet?.deletes?.length > 0) {
214
+ throw new Error('DBGM-00000 Query result saving supports UPDATE operations only');
215
+ }
216
+ for (const update of changeSet?.updates || []) {
217
+ if (!update.pureName) {
218
+ throw new Error('DBGM-00000 Query result update is missing target table');
219
+ }
220
+ if (_.isEmpty(update.fields)) {
221
+ throw new Error('DBGM-00000 Query result update is missing changed fields');
222
+ }
223
+ if (_.isEmpty(update.condition)) {
224
+ throw new Error('DBGM-00000 Query result update is missing row condition');
225
+ }
226
+ if (Object.values(update.condition).some(value => value === null || value === undefined)) {
227
+ throw new Error('DBGM-00000 Query result update has incomplete row condition');
228
+ }
229
+ }
230
+ }
231
+
232
+ async function handleSaveQueryResultData({ msgid, changeSet, sql, autoCommit }) {
233
+ lastActivity = new Date().getTime();
234
+
235
+ await waitConnected();
236
+ const driver = requireEngineDriver(storedConnection);
237
+ try {
238
+ if (!allowExecuteCustomScript(storedConnection, driver)) {
239
+ throw new Error('DBGM-00000 Connection is read-only');
240
+ }
241
+ validateQueryResultChangeSet(driver, changeSet);
242
+ if (!sql) {
243
+ const script = changeSetToSql({ ...changeSet, inserts: [], deletes: [] }, null, driver.dialect);
244
+ if (script.some(command => command.commandType != 'update')) {
245
+ throw new Error('DBGM-00000 Query result saving supports UPDATE operations only');
246
+ }
247
+ sql = scriptToSql(driver, script);
248
+ }
249
+ if (sql) {
250
+ executingScripts++;
251
+ try {
252
+ await driver.script(dbhan, sql, { useTransaction: false });
253
+ if (autoCommit) {
254
+ const dmp = driver.createDumper();
255
+ dmp.commitTransaction();
256
+ await driver.query(dbhan, dmp.s, { discardResult: true });
257
+ }
258
+ } finally {
259
+ executingScripts--;
260
+ }
261
+ }
262
+ process.send({ msgtype: 'response', msgid, result: { state: 'ok' } });
263
+ } catch (err) {
264
+ process.send({
265
+ msgtype: 'response',
266
+ msgid,
267
+ errorMessage: extractErrorMessage(err, 'DBGM-00000 Error saving query result data'),
268
+ });
269
+ }
270
+ }
271
+
206
272
  async function handleExecuteReader({ jslid, sql, fileName }) {
207
273
  lastActivity = new Date().getTime();
208
274
 
@@ -244,6 +310,7 @@ const messageHandlers = {
244
310
  executeControlCommand: handleExecuteControlCommand,
245
311
  setIsolationLevel: handleSetIsolationLevel,
246
312
  executeReader: handleExecuteReader,
313
+ saveQueryResultData: handleSaveQueryResultData,
247
314
  startProfiler: handleStartProfiler,
248
315
  stopProfiler: handleStopProfiler,
249
316
  ping: handlePing,
@@ -12,24 +12,24 @@ const logger = getLogger('authProxy');
12
12
  const AUTH_PROXY_URL = process.env.LOCAL_AUTH_PROXY
13
13
  ? 'http://localhost:3110'
14
14
  : process.env.PROD_AUTH_PROXY
15
- ? 'https://auth.dbgate.eu'
15
+ ? 'https://api.dbgate.cloud'
16
16
  : process.env.DEVWEB || process.env.DEVMODE
17
- ? 'https://auth-proxy.dbgate.udolni.net'
18
- : 'https://auth.dbgate.eu';
17
+ ? 'https://dev.dbgate.cloud'
18
+ : 'https://api.dbgate.cloud';
19
19
 
20
20
  const AI_GATEWAY_URL = process.env.LOCAL_AI_GATEWAY
21
21
  ? 'http://localhost:3110'
22
22
  : process.env.DEVWEB || process.env.DEVMODE
23
- ? 'https://aigw.dbgate.udolni.net'
23
+ ? 'https://dev.dbgate.cloud'
24
24
  : 'https://api.dbgate.cloud';
25
25
 
26
26
  const DBGATE_API_URL = process.env.LOCAL_DBGATE_API
27
27
  ? 'http://localhost:3115'
28
28
  : process.env.PROD_DBGATE_API
29
- ? 'https://api.dbgate.io'
29
+ ? 'https://api.dbgate.cloud'
30
30
  : process.env.DEVWEB || process.env.DEVMODE
31
- ? 'https://api.dbgate.udolni.net'
32
- : 'https://api.dbgate.io';
31
+ ? 'https://dev.dbgate.cloud'
32
+ : 'https://api.dbgate.cloud';
33
33
 
34
34
  let licenseKey = null;
35
35
 
@@ -26,20 +26,16 @@ const DBGATE_IDENTITY_URL = process.env.LOCAL_DBGATE_IDENTITY
26
26
  : process.env.PROD_DBGATE_IDENTITY
27
27
  ? 'https://identity.dbgate.cloud'
28
28
  : process.env.DEVWEB || process.env.DEVMODE
29
- ? 'https://identity.dbgate.udolni.net'
29
+ ? 'https://identity.dbgate.cloud'
30
30
  : 'https://identity.dbgate.cloud';
31
31
 
32
32
  const DBGATE_CLOUD_URL = process.env.LOCAL_DBGATE_CLOUD
33
33
  ? 'http://localhost:3110'
34
34
  : process.env.PROD_DBGATE_CLOUD
35
- ? 'https://cloud.dbgate.io'
35
+ ? 'https://api.dbgate.cloud'
36
36
  : process.env.DEVWEB || process.env.DEVMODE
37
37
  ? 'https://dev.dbgate.cloud'
38
- : 'https://cloud.dbgate.io';
39
-
40
-
41
- const DBGATE_PUBLIC_CLOUD_URL =
42
- DBGATE_CLOUD_URL === 'https://cloud.dbgate.io' ? 'https://api.dbgate.cloud' : DBGATE_CLOUD_URL;
38
+ : 'https://api.dbgate.cloud';
43
39
 
44
40
  const stageAxiosConfig =
45
41
  !process.env.PROD_DBGATE_CLOUD && (process.env.DEVWEB || process.env.DEVMODE)
@@ -224,7 +220,7 @@ async function updateCloudFiles(isRefresh, language) {
224
220
  logger.info({ tags, lastCheckedTm }, 'DBGM-00082 Downloading cloud files');
225
221
 
226
222
  const resp = await axios.default.get(
227
- `${DBGATE_PUBLIC_CLOUD_URL}/public-cloud-updates?lastCheckedTm=${lastCheckedTm}&tags=${tags}&isRefresh=${
223
+ `${DBGATE_CLOUD_URL}/public-cloud-updates?lastCheckedTm=${lastCheckedTm}&tags=${tags}&isRefresh=${
228
224
  isRefresh ? 1 : 0
229
225
  }}`,
230
226
  {
@@ -269,7 +265,7 @@ async function getPublicCloudFiles() {
269
265
  }
270
266
 
271
267
  async function getPublicFileData(path) {
272
- const resp = await axios.default.get(`${DBGATE_PUBLIC_CLOUD_URL}/public/${path}`, {
268
+ const resp = await axios.default.get(`${DBGATE_CLOUD_URL}/public/${path}`, {
273
269
  headers: {
274
270
  ...getLicenseHttpHeaders(),
275
271
  },
@@ -297,7 +293,7 @@ async function updatePremiumPromoWidget(language) {
297
293
  const tags = (await collectCloudFilesSearchTags()).join(',');
298
294
 
299
295
  const resp = await axios.default.get(
300
- `${DBGATE_PUBLIC_CLOUD_URL}/premium-promo-widget?identifier=${promoWidgetData?.identifier ?? 'empty'}&tags=${tags}`,
296
+ `${DBGATE_CLOUD_URL}/premium-promo-widget?identifier=${promoWidgetData?.identifier ?? 'empty'}&tags=${tags}`,
301
297
  {
302
298
  headers: {
303
299
  ...getLicenseHttpHeaders(),
@@ -346,7 +342,7 @@ async function callCloudApiGet(endpoint, signinHolder = null, additionalHeaders
346
342
  }
347
343
  const signinHeaders = await getCloudSigninHeaders(signinHolder);
348
344
 
349
- const resp = await axios.default.get(`${DBGATE_PUBLIC_CLOUD_URL}/${endpoint}`, {
345
+ const resp = await axios.default.get(`${DBGATE_CLOUD_URL}/${endpoint}`, {
350
346
  headers: {
351
347
  ...getLicenseHttpHeaders(),
352
348
  ...signinHeaders,
@@ -386,7 +382,7 @@ async function callCloudApiPost(endpoint, body, signinHolder = null) {
386
382
  }
387
383
  const signinHeaders = await getCloudSigninHeaders(signinHolder);
388
384
 
389
- const resp = await axios.default.post(`${DBGATE_PUBLIC_CLOUD_URL}/${endpoint}`, body, {
385
+ const resp = await axios.default.post(`${DBGATE_CLOUD_URL}/${endpoint}`, body, {
390
386
  headers: {
391
387
  ...getLicenseHttpHeaders(),
392
388
  ...signinHeaders,
@@ -491,7 +487,7 @@ function removeCloudCachedConnection(folid, cntid) {
491
487
 
492
488
  async function getPublicIpInfo() {
493
489
  try {
494
- const resp = await axios.default.get(`${DBGATE_PUBLIC_CLOUD_URL}/ipinfo`, stageAxiosConfig);
490
+ const resp = await axios.default.get(`${DBGATE_CLOUD_URL}/ipinfo`, stageAxiosConfig);
495
491
  if (!resp.data?.ip) {
496
492
  return { ip: 'unknown-ip' };
497
493
  }
@@ -508,14 +504,14 @@ async function getPromoWidgetData() {
508
504
 
509
505
  async function getPromoWidgetPreview(campaign, variant) {
510
506
  const resp = await axios.default.get(
511
- `${DBGATE_PUBLIC_CLOUD_URL}/premium-promo-widget-preview/${campaign}/${variant}`,
507
+ `${DBGATE_CLOUD_URL}/premium-promo-widget-preview/${campaign}/${variant}`,
512
508
  stageAxiosConfig
513
509
  );
514
510
  return resp.data;
515
511
  }
516
512
 
517
513
  async function getPromoWidgetList() {
518
- const resp = await axios.default.get(`${DBGATE_PUBLIC_CLOUD_URL}/promo-widget-list`, stageAxiosConfig);
514
+ const resp = await axios.default.get(`${DBGATE_CLOUD_URL}/promo-widget-list`, stageAxiosConfig);
519
515
  return resp.data;
520
516
  }
521
517
 
@@ -4,9 +4,12 @@ const fs = require('fs');
4
4
  const _ = require('lodash');
5
5
 
6
6
  const { jsldir } = require('../utility/directories');
7
- const { serializeJsTypesReplacer } = require('dbgate-tools');
7
+ const { serializeJsTypesReplacer, getLogger } = require('dbgate-tools');
8
8
  const { ChartProcessor } = require('dbgate-datalib');
9
9
  const { isProApp } = require('./checkLicense');
10
+ const { enrichQueryResultColumns } = require('./queryResultMetadata');
11
+
12
+ const logger = getLogger('handleQueryStream');
10
13
 
11
14
  class QueryStreamTableWriter {
12
15
  constructor(sesid = undefined) {
@@ -144,13 +147,21 @@ class StreamHandler {
144
147
  sesid = undefined,
145
148
  limitRows = undefined,
146
149
  frontMatter = undefined,
147
- autoDetectCharts = false
150
+ autoDetectCharts = false,
151
+ supportsEditableQueryResults = false,
152
+ dbhan = null,
153
+ driver = null,
154
+ dbinfo = null
148
155
  ) {
149
156
  this.recordset = this.recordset.bind(this);
150
157
  this.startLine = startLine;
151
158
  this.sesid = sesid;
152
159
  this.frontMatter = frontMatter;
153
160
  this.autoDetectCharts = autoDetectCharts;
161
+ this.supportsEditableQueryResults = supportsEditableQueryResults;
162
+ this.dbhan = dbhan;
163
+ this.driver = driver;
164
+ this.dbinfo = dbinfo;
154
165
  this.limitRows = limitRows;
155
166
  this.rowsLimitOverflow = false;
156
167
  this.row = this.row.bind(this);
@@ -165,7 +176,8 @@ class StreamHandler {
165
176
  this.plannedStats = false;
166
177
  this.queryStreamInfoHolder = queryStreamInfoHolder;
167
178
  this.resolve = resolve;
168
- this.rowCounter = 0;
179
+ this.currentRecordset = null;
180
+ this.recordsetQueuePromise = Promise.resolve();
169
181
  // currentHandlers = [...currentHandlers, this];
170
182
  }
171
183
 
@@ -180,21 +192,37 @@ class StreamHandler {
180
192
  process.send({ msgtype: 'changedCurrentDatabase', database, sesid: this.sesid });
181
193
  }
182
194
 
183
- recordset(columns, options) {
195
+ async prepareRecordset(recordsetContext, columns, options) {
184
196
  if (this.rowsLimitOverflow) {
185
197
  return;
186
198
  }
187
199
  this.closeCurrentWriter();
188
- this.currentWriter = new QueryStreamTableWriter(this.sesid);
189
- this.currentWriter.initializeFromQuery(
190
- Array.isArray(columns) ? { columns } : columns,
191
- this.queryStreamInfoHolder.resultIndex,
192
- this.frontMatter?.[`chart-${this.queryStreamInfoHolder.resultIndex + 1}`],
200
+ const writer = new QueryStreamTableWriter(this.sesid);
201
+ const structure = Array.isArray(columns) ? { columns } : columns;
202
+ const enrichedColumns = await enrichQueryStreamColumns(
203
+ structure?.columns,
204
+ this.sql,
205
+ this.supportsEditableQueryResults,
206
+ this.driver,
207
+ this.dbhan,
208
+ this.dbinfo
209
+ );
210
+ writer.initializeFromQuery(
211
+ {
212
+ ...structure,
213
+ columns: enrichedColumns,
214
+ },
215
+ recordsetContext.resultIndex,
216
+ this.frontMatter?.[`chart-${recordsetContext.resultIndex + 1}`],
193
217
  this.autoDetectCharts,
194
218
  options
195
219
  );
196
- this.queryStreamInfoHolder.resultIndex += 1;
197
- this.rowCounter = 0;
220
+ recordsetContext.writer = writer;
221
+ this.currentWriter = writer;
222
+ for (const row of recordsetContext.pendingRows) {
223
+ this.writeRow(recordsetContext, row, false);
224
+ }
225
+ recordsetContext.pendingRows = [];
198
226
 
199
227
  // this.writeCurrentStats();
200
228
 
@@ -204,15 +232,50 @@ class StreamHandler {
204
232
  // }
205
233
  // }, 500);
206
234
  }
235
+
236
+ recordset(columns, options) {
237
+ const recordsetContext = {
238
+ pendingRows: [],
239
+ resultIndex: this.queryStreamInfoHolder.resultIndex,
240
+ rowCounter: 0,
241
+ writer: null,
242
+ };
243
+ this.queryStreamInfoHolder.resultIndex += 1;
244
+ this.currentRecordset = recordsetContext;
245
+
246
+ const recordsetReadyPromise = this.recordsetQueuePromise
247
+ .then(() => this.prepareRecordset(recordsetContext, columns, options))
248
+ .catch(err => {
249
+ recordsetContext.pendingRows = [];
250
+ this.info({
251
+ message: err?.message || `${err}`,
252
+ severity: 'error',
253
+ });
254
+ })
255
+ .finally(() => {
256
+ if (this.recordsetReadyPromise == recordsetReadyPromise) {
257
+ this.recordsetReadyPromise = null;
258
+ }
259
+ });
260
+ this.recordsetReadyPromise = recordsetReadyPromise;
261
+ this.recordsetQueuePromise = recordsetReadyPromise;
262
+ }
263
+
264
+ writeRow(recordsetContext, row, incrementCounter = true) {
265
+ recordsetContext.writer.row(row);
266
+ if (incrementCounter) recordsetContext.rowCounter += 1;
267
+ }
268
+
207
269
  row(row) {
208
270
  if (this.rowsLimitOverflow) {
209
271
  return;
210
272
  }
211
273
 
212
- if (this.limitRows && this.rowCounter >= this.limitRows) {
274
+ const recordsetContext = this.currentRecordset;
275
+ if (this.limitRows && recordsetContext?.rowCounter >= this.limitRows) {
213
276
  process.send({
214
277
  msgtype: 'info',
215
- info: { message: `Rows limit overflow, loaded ${this.rowCounter} rows, canceling query`, severity: 'error' },
278
+ info: { message: `Rows limit overflow, loaded ${recordsetContext.rowCounter} rows, canceling query`, severity: 'error' },
216
279
  sesid: this.sesid,
217
280
  });
218
281
  this.rowsLimitOverflow = true;
@@ -229,9 +292,11 @@ class StreamHandler {
229
292
  return;
230
293
  }
231
294
 
232
- if (this.currentWriter) {
233
- this.currentWriter.row(row);
234
- this.rowCounter += 1;
295
+ if (recordsetContext?.writer) {
296
+ this.writeRow(recordsetContext, row);
297
+ } else if (recordsetContext) {
298
+ recordsetContext.pendingRows.push(row);
299
+ recordsetContext.rowCounter += 1;
235
300
  } else if (row.message) {
236
301
  process.send({ msgtype: 'info', info: { message: row.message }, sesid: this.sesid });
237
302
  }
@@ -241,9 +306,12 @@ class StreamHandler {
241
306
  // process.send({ msgtype: 'error', error });
242
307
  // }
243
308
  done(result) {
244
- this.closeCurrentWriter();
245
- // currentHandlers = currentHandlers.filter((x) => x != this);
246
- this.resolve();
309
+ const finish = () => {
310
+ this.closeCurrentWriter();
311
+ // currentHandlers = currentHandlers.filter((x) => x != this);
312
+ this.resolve();
313
+ };
314
+ this.recordsetQueuePromise.then(finish);
247
315
  }
248
316
  info(info) {
249
317
  if (info && info.line != null) {
@@ -259,6 +327,19 @@ class StreamHandler {
259
327
  }
260
328
  }
261
329
 
330
+ async function enrichQueryStreamColumns(columns, sql, supportsEditableQueryResults, driver, dbhan, dbinfo) {
331
+ if (!supportsEditableQueryResults) return columns;
332
+ return enrichQueryResultColumns({
333
+ columns,
334
+ sql,
335
+ driver,
336
+ dbhan,
337
+ dbinfo,
338
+ onNativeMetadataError: err => logger.warn('Error enriching query stream columns', err),
339
+ onFallbackMetadataError: err => logger.warn('Error parsing query stream column metadata', err),
340
+ });
341
+ }
342
+
262
343
  function handleQueryStream(
263
344
  dbhan,
264
345
  driver,
@@ -267,7 +348,8 @@ function handleQueryStream(
267
348
  sesid = undefined,
268
349
  limitRows = undefined,
269
350
  frontMatter = undefined,
270
- autoDetectCharts = false
351
+ autoDetectCharts = false,
352
+ dbinfo = null
271
353
  ) {
272
354
  return new Promise((resolve, reject) => {
273
355
  const start = sqlItem.trimStart || sqlItem.start;
@@ -278,8 +360,13 @@ function handleQueryStream(
278
360
  sesid,
279
361
  limitRows,
280
362
  frontMatter,
281
- autoDetectCharts
363
+ autoDetectCharts,
364
+ driver.databaseEngineTypes?.includes('sql') && driver.supportsEditableQueryResults,
365
+ dbhan,
366
+ driver,
367
+ dbinfo
282
368
  );
369
+ handler.sql = sqlItem.text;
283
370
  driver.stream(dbhan, sqlItem.text, handler);
284
371
  });
285
372
  }
@@ -0,0 +1,70 @@
1
+ const {
2
+ extractSingleTableFromSql,
3
+ extractColumnSourcesFromSql,
4
+ extractColumnMetadataFromSql,
5
+ } = require('dbgate-tools');
6
+
7
+ function getColumnSourceName(columnSources, columnName) {
8
+ if (!columnSources || !columnName) return null;
9
+ if (columnSources[columnName]) return columnSources[columnName];
10
+
11
+ const matchingKeys = Object.keys(columnSources).filter(key => key.toLowerCase() == columnName.toLowerCase());
12
+ return matchingKeys.length == 1 ? columnSources[matchingKeys[0]] : null;
13
+ }
14
+
15
+ function getColumnMetadata(columnMetadata, columnName) {
16
+ if (!columnMetadata || !columnName) return null;
17
+ if (columnMetadata[columnName]) return columnMetadata[columnName];
18
+
19
+ const matchingKeys = Object.keys(columnMetadata).filter(key => key.toLowerCase() == columnName.toLowerCase());
20
+ return matchingKeys.length == 1 ? columnMetadata[matchingKeys[0]] : null;
21
+ }
22
+
23
+ async function enrichQueryResultColumns({ columns, sql, driver, dbhan, dbinfo, onNativeMetadataError, onFallbackMetadataError }) {
24
+ if (!columns?.length || !driver?.databaseEngineTypes?.includes('sql') || !driver?.supportsEditableQueryResults) {
25
+ return columns;
26
+ }
27
+
28
+ if (driver.enrichColumnMetadata) {
29
+ try {
30
+ const enriched = await driver.enrichColumnMetadata(dbhan, sql, columns, dbinfo);
31
+ if (enriched?.every(column => column.tableName && column.sourceColumnName)) return enriched;
32
+ if (enriched?.length == columns.length) columns = enriched;
33
+ } catch (err) {
34
+ onNativeMetadataError?.(err);
35
+ }
36
+ }
37
+
38
+ try {
39
+ const columnMetadata = extractColumnMetadataFromSql(sql);
40
+ if (columnMetadata) {
41
+ return columns.map(column => {
42
+ const metadata = getColumnMetadata(columnMetadata, column.columnName);
43
+ return {
44
+ ...column,
45
+ tableName: column.tableName || metadata?.tableName,
46
+ tableSchema: column.tableSchema || metadata?.schemaName,
47
+ sourceColumnName: column.sourceColumnName || metadata?.sourceColumnName,
48
+ };
49
+ });
50
+ }
51
+
52
+ const table = extractSingleTableFromSql(sql);
53
+ if (!table) return columns;
54
+ const columnSources = extractColumnSourcesFromSql(sql);
55
+
56
+ return columns.map(column => ({
57
+ ...column,
58
+ tableName: column.tableName || table.tableName,
59
+ tableSchema: column.tableSchema || table.schemaName,
60
+ sourceColumnName: column.sourceColumnName || getColumnSourceName(columnSources, column.columnName) || column.columnName,
61
+ }));
62
+ } catch (err) {
63
+ onFallbackMetadataError?.(err);
64
+ return columns;
65
+ }
66
+ }
67
+
68
+ module.exports = {
69
+ enrichQueryResultColumns,
70
+ };