From 9d376961f475a6870d347372a1d973e1874b5bd3 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Tue, 29 Apr 2025 17:15:04 +0200 Subject: [PATCH 001/129] API: queryReader accepts systemConnection (fixes duckDb export filtered data) --- packages/api/src/shell/queryReader.js | 13 ++++++++++--- plugins/dbgate-plugin-duckdb/src/backend/driver.js | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/api/src/shell/queryReader.js b/packages/api/src/shell/queryReader.js index 0d9a16296..5fe820d99 100644 --- a/packages/api/src/shell/queryReader.js +++ b/packages/api/src/shell/queryReader.js @@ -7,6 +7,8 @@ const logger = getLogger('queryReader'); * Returns reader object for {@link copyStream} function. This reader object reads data from query. * @param {object} options * @param {connectionType} options.connection - connection object + * @param {object} options.systemConnection - system connection (result of driver.connect). If not provided, new connection will be created + * @param {object} options.driver - driver object. If not provided, it will be loaded from connection * @param {string} options.query - SQL query * @param {string} [options.queryType] - query type * @param {string} [options.sql] - SQL query. obsolete; use query instead @@ -16,6 +18,8 @@ async function queryReader({ connection, query, queryType, + systemConnection, + driver, // obsolete; use query instead sql, }) { @@ -28,10 +32,13 @@ async function queryReader({ logger.info({ sql: query || sql }, `Reading query`); // else console.log(`Reading query ${JSON.stringify(json)}`); - const driver = requireEngineDriver(connection); - const pool = await connectUtility(driver, connection, queryType == 'json' ? 'read' : 'script'); + if (!driver) { + driver = requireEngineDriver(connection); + } + const dbhan = systemConnection || (await connectUtility(driver, connection, queryType == 'json' ? 'read' : 'script')); + const reader = - queryType == 'json' ? await driver.readJsonQuery(pool, query) : await driver.readQuery(pool, query || sql); + queryType == 'json' ? await driver.readJsonQuery(dbhan, query) : await driver.readQuery(dbhan, query || sql); return reader; } diff --git a/plugins/dbgate-plugin-duckdb/src/backend/driver.js b/plugins/dbgate-plugin-duckdb/src/backend/driver.js index d78a4a0a7..c9fc270c0 100644 --- a/plugins/dbgate-plugin-duckdb/src/backend/driver.js +++ b/plugins/dbgate-plugin-duckdb/src/backend/driver.js @@ -9,7 +9,7 @@ const sql = require('./sql'); const { mapSchemaRowToSchemaInfo } = require('./Analyser.helpers'); const { zipObject } = require('lodash'); -const logger = getLogger('sqliteDriver'); +const logger = getLogger('duckdbDriver'); /** * @type {import('@duckdb/node-api')} From 62ddbb20acaf9be8dd56aa36b9708b51afda014c Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Wed, 30 Apr 2025 08:36:56 +0200 Subject: [PATCH 002/129] mongodb - filter by objectId imrpoved --- packages/filterparser/src/parseFilter.ts | 24 +++++++++++++++---- .../src/frontend/convertToMongoCondition.js | 19 ++++++++++++--- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/packages/filterparser/src/parseFilter.ts b/packages/filterparser/src/parseFilter.ts index d5ccd2909..fbc224280 100644 --- a/packages/filterparser/src/parseFilter.ts +++ b/packages/filterparser/src/parseFilter.ts @@ -52,6 +52,18 @@ const binaryCondition = }; }; +const simpleEqualCondition = () => value => ({ + conditionType: 'binary', + operator: '=', + left: { + exprType: 'placeholder', + }, + right: { + exprType: 'value', + value, + }, +}); + const likeCondition = (conditionType, likeString) => value => ({ conditionType, left: { @@ -348,6 +360,8 @@ const createParser = (filterBehaviour: FilterBehaviour) => { objectid: () => token(P.regexp(/ObjectId\(['"]?[0-9a-f]{24}['"]?\)/)).desc('ObjectId'), + objectidstr: () => token(P.regexp(/[0-9a-f]{24}/)).desc('ObjectId string'), + hexstring: () => token(P.regexp(/0x(([0-9a-fA-F][0-9a-fA-F])+)/, 1)) .map(x => ({ @@ -366,6 +380,7 @@ const createParser = (filterBehaviour: FilterBehaviour) => { value: r => P.alt(...allowedValues.map(x => r[x])), valueTestEq: r => r.value.map(binaryCondition('=')), hexTestEq: r => r.hexstring.map(binaryCondition('=')), + valueTestObjectIdStr: r => r.objectidstr.map(simpleEqualCondition()), valueTestStr: r => r.value.map(likeCondition('like', '%#VALUE#%')), valueTestNum: r => r.number.map(numberTestCondition()), valueTestObjectId: r => r.objectid.map(objectIdTestCondition()), @@ -546,12 +561,13 @@ const createParser = (filterBehaviour: FilterBehaviour) => { } } - if (filterBehaviour.allowNumberDualTesting) { - allowedElements.push('valueTestNum'); + if (filterBehaviour.allowObjectIdTesting) { + allowedElements.push('valueTestObjectIdStr'); + allowedElements.push('valueTestObjectId'); } - if (filterBehaviour.allowObjectIdTesting) { - allowedElements.push('valueTestObjectId'); + if (filterBehaviour.allowNumberDualTesting) { + allowedElements.push('valueTestNum'); } // must be last diff --git a/plugins/dbgate-plugin-mongo/src/frontend/convertToMongoCondition.js b/plugins/dbgate-plugin-mongo/src/frontend/convertToMongoCondition.js index f197152ad..5de7c3ef4 100644 --- a/plugins/dbgate-plugin-mongo/src/frontend/convertToMongoCondition.js +++ b/plugins/dbgate-plugin-mongo/src/frontend/convertToMongoCondition.js @@ -11,6 +11,21 @@ function convertRightOperandToMongoValue(right) { throw new Error(`Unknown right operand type ${right.exprType}`); } +function convertRightEqualOperandToMongoCondition(right) { + if (right.exprType != 'value') { + throw new Error(`Unknown right operand type ${right.exprType}`); + } + const { value } = right; + if (/^[0-9a-fA-F]{24}$/.test(value)) { + return { + $in: [value, { $oid: value }], + }; + } + return { + $eq: value, + }; +} + function convertToMongoCondition(filter) { if (!filter) { return null; @@ -28,9 +43,7 @@ function convertToMongoCondition(filter) { switch (filter.operator) { case '=': return { - [convertLeftOperandToMongoColumn(filter.left)]: { - $eq: convertRightOperandToMongoValue(filter.right), - }, + [convertLeftOperandToMongoColumn(filter.left)]: convertRightEqualOperandToMongoCondition(filter.right), }; case '!=': case '<>': From bca5514a76c8d0a1813c7b44ab3e399e2795bbc0 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Wed, 30 Apr 2025 08:46:18 +0200 Subject: [PATCH 003/129] v6.3.4-beta.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 210a25d0a..dbc304f24 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "6.3.4-premium-beta.3", + "version": "6.3.4-beta.4", "name": "dbgate-all", "workspaces": [ "packages/*", From 87fbd7e5da1c4df0c2bbb67a6993ad86812f975e Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Wed, 30 Apr 2025 08:49:58 +0200 Subject: [PATCH 004/129] v6.3.4-premium-beta.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dbc304f24..f1d00edee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "6.3.4-beta.4", + "version": "6.3.4-premium-beta.5", "name": "dbgate-all", "workspaces": [ "packages/*", From 40a9ced0f77957c5b93ef9dd49dbd8ab0fce5e46 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Wed, 30 Apr 2025 09:30:54 +0200 Subject: [PATCH 005/129] changelog --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b28a3f21..71f5f0033 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Builds: - win - application for Windows ## 6.4.0 - not released yet +- ADDED: DuckDB support - ADDED: Data deployer (Premium) - ADDED: Compare data between JSON lines file in archive and database table - CHANGED: Data Duplicator => Data Replicator (suitable for update, create and delete data, much more customizable) @@ -18,6 +19,14 @@ Builds: - ADDED: Upload SQLite files - ADDED: Upload archive as ZIP folder (Premium) - ADDED: Compress, uncompress archive folder (Premium) +- ADDED: Export connections and settings #357 +- ADDED: Filtering by MongoDB ObjectId works now also without ObjectId(...) wrapper +- ADDED: Split queries using blank lines #1089 +- FIXED: JSON-to-Grid only works if there is no newline #1085 +- CHANGED: When running multiple commands in script, stop execution after first error #1070 +- FIXED: Selection rectangle remains visible after closing JSONB edit cell value form #1031 +- FIXED: Diplaying numeric FK column with right alignement #1021 + ## 6.3.3 - CHANGED: New administration UI, redesigned administration of users, connections and roles From 9f029b892b9103d569d32d1da624e805f0e9fd80 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Wed, 30 Apr 2025 09:40:48 +0200 Subject: [PATCH 006/129] SYNC: fixed opening data deployer from dataabase --- packages/web/src/appobj/DatabaseAppObject.svelte | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/web/src/appobj/DatabaseAppObject.svelte b/packages/web/src/appobj/DatabaseAppObject.svelte index e316cfd64..28ac3c572 100644 --- a/packages/web/src/appobj/DatabaseAppObject.svelte +++ b/packages/web/src/appobj/DatabaseAppObject.svelte @@ -338,7 +338,7 @@ await dbgateApi.dropAllDbObjects(${JSON.stringify( { title: archiveFolder, icon: 'img replicator', - tabComponent: 'DataDeployerTab', + tabComponent: 'DataDeployTab', props: { conid: connection?._id, database: name, @@ -347,6 +347,8 @@ await dbgateApi.dropAllDbObjects(${JSON.stringify( { editor: { archiveFolder, + conid: connection?._id, + database: name, }, } ); From e87ae31a513fc390a47c2fab3dea274901278f3b Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Wed, 30 Apr 2025 09:54:56 +0200 Subject: [PATCH 007/129] changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71f5f0033..128786d96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ Builds: - linux - application for linux - win - application for Windows -## 6.4.0 - not released yet +## 6.4.0 - ADDED: DuckDB support - ADDED: Data deployer (Premium) - ADDED: Compare data between JSON lines file in archive and database table @@ -26,7 +26,7 @@ Builds: - CHANGED: When running multiple commands in script, stop execution after first error #1070 - FIXED: Selection rectangle remains visible after closing JSONB edit cell value form #1031 - FIXED: Diplaying numeric FK column with right alignement #1021 - +- ADDED: Additional arguments for MySQL and PostgreSQL backup #1092 ## 6.3.3 - CHANGED: New administration UI, redesigned administration of users, connections and roles From 5d953da26724e73fcdfbf7afbc60813a9fc36d19 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Wed, 30 Apr 2025 09:55:35 +0200 Subject: [PATCH 008/129] v6.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f1d00edee..298fb4aeb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "6.3.4-premium-beta.5", + "version": "6.4.0", "name": "dbgate-all", "workspaces": [ "packages/*", From 6b751eb7153a33ed6a19035661b7e769c723732b Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Wed, 30 Apr 2025 10:48:56 +0200 Subject: [PATCH 009/129] SYNC: export connections modal screenshot --- CHANGELOG.md | 1 + e2e-tests/cypress/e2e/add-connection.cy.js | 7 +++++++ packages/web/src/commands/stdCommands.ts | 4 ++-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 128786d96..e85824304 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ Builds: - FIXED: Selection rectangle remains visible after closing JSONB edit cell value form #1031 - FIXED: Diplaying numeric FK column with right alignement #1021 - ADDED: Additional arguments for MySQL and PostgreSQL backup #1092 +- CHANGED: Amazon and Azure instalations are not auto-upgraded by default ## 6.3.3 - CHANGED: New administration UI, redesigned administration of users, connections and roles diff --git a/e2e-tests/cypress/e2e/add-connection.cy.js b/e2e-tests/cypress/e2e/add-connection.cy.js index 06b9916f1..d8dad00f8 100644 --- a/e2e-tests/cypress/e2e/add-connection.cy.js +++ b/e2e-tests/cypress/e2e/add-connection.cy.js @@ -112,4 +112,11 @@ describe('Add connection', () => { cy.contains('performance_schema'); }); + + it('export connections', () => { + cy.testid('WidgetIconPanel_menu').click(); + cy.contains('Tools').click(); + cy.contains('Export connections').click(); + cy.themeshot('export-connections'); + }); }); diff --git a/packages/web/src/commands/stdCommands.ts b/packages/web/src/commands/stdCommands.ts index fbe314a26..85c97b632 100644 --- a/packages/web/src/commands/stdCommands.ts +++ b/packages/web/src/commands/stdCommands.ts @@ -535,7 +535,7 @@ registerCommand({ id: 'app.exportConnections', category: 'Settings', name: 'Export connections', - testEnabled: () => getElectron() != null, + testEnabled: () => !getCurrentConfig()?.runAsPortal && !getCurrentConfig()?.storageDatabase, onClick: () => { showModal(ExportImportConnectionsModal, { mode: 'export', @@ -547,7 +547,7 @@ registerCommand({ id: 'app.importConnections', category: 'Settings', name: 'Import connections', - testEnabled: () => getElectron() != null, + testEnabled: () => !getCurrentConfig()?.runAsPortal && !getCurrentConfig()?.storageDatabase, onClick: async () => { const files = await electron.showOpenDialog({ properties: ['showHiddenFiles', 'openFile'], From c9f3e8cb9ffcb87952a66db337ec94713855e0f6 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Thu, 1 May 2025 07:27:03 +0200 Subject: [PATCH 010/129] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4d62be5a5..a08b55a81 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ DbGate is licensed under GPL-3.0 license and is free to use for any purpose. * CosmosDB (Premium) * ClickHouse * Apache Cassandra +* libSQL/Turso (Premium) +* DuckDB @@ -184,4 +186,4 @@ yarn plugin # this compiles plugin and copies it into existing DbGate installati After restarting DbGate, you could use your new plugin from DbGate. ## Logging -DbGate uses [pinomin logger](https://github.com/dbgate/pinomin). So by default, it produces JSON log messages into console and log files. If you want to see formatted logs, please use [pino-pretty](https://github.com/pinojs/pino-pretty) log formatter. \ No newline at end of file +DbGate uses [pinomin logger](https://github.com/dbgate/pinomin). So by default, it produces JSON log messages into console and log files. If you want to see formatted logs, please use [pino-pretty](https://github.com/pinojs/pino-pretty) log formatter. From 28f62623bf39d78064c9ce7acc2cdeedeea6838e Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sat, 3 May 2025 09:59:29 +0200 Subject: [PATCH 011/129] SYNC: Added option to dump CREATE/DROP database in mysql dump #1103 --- .../src/frontend/drivers.js | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/plugins/dbgate-plugin-mysql/src/frontend/drivers.js b/plugins/dbgate-plugin-mysql/src/frontend/drivers.js index d73a13298..3adfa6155 100644 --- a/plugins/dbgate-plugin-mysql/src/frontend/drivers.js +++ b/plugins/dbgate-plugin-mysql/src/frontend/drivers.js @@ -253,7 +253,14 @@ const mysqlDriverBase = { const customArgs = options.customArgs.split(/\s+/).filter(arg => arg.trim() != ''); args.push(...customArgs); } - args.push(database); + if (options.createDatabase) { + args.push('--databases', database); + if (options.dropDatabase) { + args.push('--add-drop-database'); + } + } else { + args.push(database); + } return { command, args }; }, restoreDatabaseCommand(connection, settings, externalTools) { @@ -346,6 +353,19 @@ const mysqlDriverBase = { default: false, disabledFn: values => values.lockTables || values.skipLockTables, }, + { + type: 'checkbox', + label: 'Create database', + name: 'createDatabase', + default: false, + }, + { + type: 'checkbox', + label: 'Drop database before import', + name: 'dropDatabase', + default: false, + disabledFn: values => !values.createDatabase, + }, { type: 'text', label: 'Custom arguments', From 2e3b770bea90839a7e3942258d9d47b2f0376a8f Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sat, 3 May 2025 17:30:48 +0200 Subject: [PATCH 012/129] v6.4.1-beta.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 298fb4aeb..bceb46b2d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "6.4.0", + "version": "6.4.1-beta.2", "name": "dbgate-all", "workspaces": [ "packages/*", From 16990bd0c30662c198c9f64541dcbacfaaeb86c7 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sat, 3 May 2025 19:03:06 +0200 Subject: [PATCH 013/129] changelog fix --- packages/api/src/controllers/config.js | 8 ++++++-- packages/web/src/tabs/ChangelogTab.svelte | 5 +++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/api/src/controllers/config.js b/packages/api/src/controllers/config.js index d3d777576..1c476183f 100644 --- a/packages/api/src/controllers/config.js +++ b/packages/api/src/controllers/config.js @@ -298,8 +298,12 @@ module.exports = { changelog_meta: true, async changelog() { - const resp = await axios.default.get('https://raw.githubusercontent.com/dbgate/dbgate/master/CHANGELOG.md'); - return resp.data; + try { + const resp = await axios.default.get('https://raw.githubusercontent.com/dbgate/dbgate/master/CHANGELOG.md'); + return resp.data; + } catch (err) { + return '' + } }, checkLicense_meta: true, diff --git a/packages/web/src/tabs/ChangelogTab.svelte b/packages/web/src/tabs/ChangelogTab.svelte index fc9e75d93..dc5f2531a 100644 --- a/packages/web/src/tabs/ChangelogTab.svelte +++ b/packages/web/src/tabs/ChangelogTab.svelte @@ -7,9 +7,10 @@ import LoadingInfo from '../elements/LoadingInfo.svelte'; import Markdown from '../elements/Markdown.svelte'; import { apiCall } from '../utility/api'; + import _ from 'lodash'; let isLoading = false; - let text = null; + let text = ''; const handleLoad = async () => { isLoading = true; @@ -27,7 +28,7 @@ {:else}
- +
{/if} From 23db34575644310db1b1b8e69b3b3908c5efca3e Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sat, 3 May 2025 19:09:19 +0200 Subject: [PATCH 014/129] v6.4.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bceb46b2d..0e328c98f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "6.4.1-beta.2", + "version": "6.4.1", "name": "dbgate-all", "workspaces": [ "packages/*", From cd817714cd4f8a526625ac32e53e0fd072ad03d7 Mon Sep 17 00:00:00 2001 From: Moshe Brevda Date: Mon, 5 May 2025 12:39:30 +0300 Subject: [PATCH 015/129] Add source label to container --- docker/Dockerfile | 2 ++ docker/Dockerfile-alpine | 2 ++ 2 files changed, 4 insertions(+) diff --git a/docker/Dockerfile b/docker/Dockerfile index 728f01891..3e5c04e6b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,5 +1,7 @@ FROM node:22 +LABEL org.opencontainers.image.source="https://github.com/dbgate/dbgate" + RUN apt-get update && apt-get install -y \ iputils-ping \ iproute2 \ diff --git a/docker/Dockerfile-alpine b/docker/Dockerfile-alpine index cfd148b43..d19642cc1 100644 --- a/docker/Dockerfile-alpine +++ b/docker/Dockerfile-alpine @@ -1,5 +1,7 @@ FROM node:18-alpine +LABEL org.opencontainers.image.source="https://github.com/dbgate/dbgate" + WORKDIR /home/dbgate-docker RUN apk --no-cache upgrade \ From 110d87e5129818ee5fd2c254454af0bc5b87379d Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Mon, 5 May 2025 16:04:21 +0200 Subject: [PATCH 016/129] bigint support #1087 --- .../api/src/proc/databaseConnectionProcess.js | 3 +- packages/api/src/utility/handleQueryStream.js | 3 +- packages/filterparser/src/filterTool.ts | 1 + packages/filterparser/src/parseFilter.ts | 16 +++--- packages/sqltree/src/evaluateCondition.ts | 21 ++++++-- packages/tools/src/SqlDumper.ts | 1 + packages/tools/src/stringTools.ts | 51 +++++++++++++++++++ packages/web/src/datagrid/DataGridCell.svelte | 3 +- packages/web/src/datagrid/gridutil.ts | 1 + packages/web/src/utility/api.ts | 3 +- .../src/backend/drivers.js | 5 ++ 11 files changed, 93 insertions(+), 15 deletions(-) diff --git a/packages/api/src/proc/databaseConnectionProcess.js b/packages/api/src/proc/databaseConnectionProcess.js index ad1155258..2d7d613c5 100644 --- a/packages/api/src/proc/databaseConnectionProcess.js +++ b/packages/api/src/proc/databaseConnectionProcess.js @@ -12,6 +12,7 @@ const { ScriptWriterEval, SqlGenerator, playJsonScriptWriter, + serializeJsTypesForJsonStringify, } = require('dbgate-tools'); const requireEngineDriver = require('../utility/requireEngineDriver'); const { connectUtility } = require('../utility/connectUtility'); @@ -232,7 +233,7 @@ async function handleQueryData({ msgid, sql, range }, skipReadonlyCheck = false) try { if (!skipReadonlyCheck) ensureExecuteCustomScript(driver); const res = await driver.query(dbhan, sql, { range }); - process.send({ msgtype: 'response', msgid, ...res }); + process.send({ msgtype: 'response', msgid, ...serializeJsTypesForJsonStringify(res) }); } catch (err) { process.send({ msgtype: 'response', diff --git a/packages/api/src/utility/handleQueryStream.js b/packages/api/src/utility/handleQueryStream.js index e0f64ba73..76e573a57 100644 --- a/packages/api/src/utility/handleQueryStream.js +++ b/packages/api/src/utility/handleQueryStream.js @@ -4,6 +4,7 @@ const fs = require('fs'); const _ = require('lodash'); const { jsldir } = require('../utility/directories'); +const { serializeJsTypesReplacer } = require('dbgate-tools'); class QueryStreamTableWriter { constructor(sesid = undefined) { @@ -38,7 +39,7 @@ class QueryStreamTableWriter { row(row) { // console.log('ACCEPT ROW', row); - this.currentStream.write(JSON.stringify(row) + '\n'); + this.currentStream.write(JSON.stringify(row, serializeJsTypesReplacer) + '\n'); this.currentRowCount += 1; if (!this.plannedStats) { diff --git a/packages/filterparser/src/filterTool.ts b/packages/filterparser/src/filterTool.ts index 2e502db96..864e16021 100644 --- a/packages/filterparser/src/filterTool.ts +++ b/packages/filterparser/src/filterTool.ts @@ -20,6 +20,7 @@ export function getFilterValueExpression(value, dataType?) { if (value === true) return 'TRUE'; if (value === false) return 'FALSE'; if (value.$oid) return `ObjectId("${value.$oid}")`; + if (value.$bigint) return value.$bigint; if (value.type == 'Buffer' && Array.isArray(value.data)) { return '0x' + arrayToHexString(value.data); } diff --git a/packages/filterparser/src/parseFilter.ts b/packages/filterparser/src/parseFilter.ts index fbc224280..144b40f28 100644 --- a/packages/filterparser/src/parseFilter.ts +++ b/packages/filterparser/src/parseFilter.ts @@ -2,14 +2,18 @@ import P from 'parsimmon'; import moment from 'moment'; import { Condition } from 'dbgate-sqltree'; import { interpretEscapes, token, word, whitespace } from './common'; -import { hexStringToArray } from 'dbgate-tools'; +import { hexStringToArray, parseNumberSafe } from 'dbgate-tools'; import { FilterBehaviour, TransformType } from 'dbgate-types'; const binaryCondition = (operator, numberDualTesting = false) => value => { - const numValue = parseFloat(value); - if (numberDualTesting && !isNaN(numValue)) { + const numValue = parseNumberSafe(value); + if ( + numberDualTesting && + // @ts-ignore + !isNaN(numValue) + ) { return { conditionType: 'or', conditions: [ @@ -345,17 +349,17 @@ const createParser = (filterBehaviour: FilterBehaviour) => { string1Num: () => token(P.regexp(/"-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?"/, 1)) - .map(Number) + .map(parseNumberSafe) .desc('numer quoted'), string2Num: () => token(P.regexp(/'-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?'/, 1)) - .map(Number) + .map(parseNumberSafe) .desc('numer quoted'), number: () => token(P.regexp(/-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?/)) - .map(Number) + .map(parseNumberSafe) .desc('number'), objectid: () => token(P.regexp(/ObjectId\(['"]?[0-9a-f]{24}['"]?\)/)).desc('ObjectId'), diff --git a/packages/sqltree/src/evaluateCondition.ts b/packages/sqltree/src/evaluateCondition.ts index 795fe4965..d52d1365e 100644 --- a/packages/sqltree/src/evaluateCondition.ts +++ b/packages/sqltree/src/evaluateCondition.ts @@ -16,11 +16,17 @@ function isLike(value, test) { return res; } +function extractRawValue(value) { + if (value?.$bigint) return value.$bigint; + if (value?.$oid) return value.$oid; + return value; +} + export function evaluateCondition(condition: Condition, values) { switch (condition.conditionType) { case 'binary': - const left = evaluateExpression(condition.left, values); - const right = evaluateExpression(condition.right, values); + const left = extractRawValue(evaluateExpression(condition.left, values)); + const right = extractRawValue(evaluateExpression(condition.right, values)); switch (condition.operator) { case '=': return left == right; @@ -50,10 +56,15 @@ export function evaluateCondition(condition: Condition, values) { case 'or': return condition.conditions.some(cond => evaluateCondition(cond, values)); case 'like': - return isLike(evaluateExpression(condition.left, values), evaluateExpression(condition.right, values)); - break; + return isLike( + extractRawValue(evaluateExpression(condition.left, values)), + extractRawValue(evaluateExpression(condition.right, values)) + ); case 'notLike': - return !isLike(evaluateExpression(condition.left, values), evaluateExpression(condition.right, values)); + return !isLike( + extractRawValue(evaluateExpression(condition.left, values)), + extractRawValue(evaluateExpression(condition.right, values)) + ); case 'not': return !evaluateCondition(condition.condition, values); case 'anyColumnPass': diff --git a/packages/tools/src/SqlDumper.ts b/packages/tools/src/SqlDumper.ts index 371fb75fe..d5bc11ec6 100644 --- a/packages/tools/src/SqlDumper.ts +++ b/packages/tools/src/SqlDumper.ts @@ -78,6 +78,7 @@ export class SqlDumper implements AlterProcessor { else if (_isNumber(value)) this.putRaw(value.toString()); else if (_isDate(value)) this.putStringValue(new Date(value).toISOString()); else if (value?.type == 'Buffer' && _isArray(value?.data)) this.putByteArrayValue(value?.data); + else if (value?.$bigint) this.putRaw(value?.$bigint); else if (_isPlainObject(value) || _isArray(value)) this.putStringValue(JSON.stringify(value)); else this.put('^null'); } diff --git a/packages/tools/src/stringTools.ts b/packages/tools/src/stringTools.ts index 1bd65e653..adf6aec3c 100644 --- a/packages/tools/src/stringTools.ts +++ b/packages/tools/src/stringTools.ts @@ -4,6 +4,7 @@ import _isDate from 'lodash/isDate'; import _isNumber from 'lodash/isNumber'; import _isPlainObject from 'lodash/isPlainObject'; import _pad from 'lodash/pad'; +import _cloneDeepWith from 'lodash/cloneDeepWith'; import { DataEditorTypesBehaviour } from 'dbgate-types'; export type EditorDataType = @@ -208,6 +209,12 @@ export function stringifyCellValue( } } } + if (value?.$bigint) { + return { + value: value.$bigint, + gridStyle: 'valueCellStyle', + }; + } if (editorTypes?.parseDateAsDollar) { if (value?.$date) { @@ -343,6 +350,9 @@ export function shouldOpenMultilineDialog(value) { if (value?.$date) { return false; } + if (value?.$bigint) { + return false; + } if (_isPlainObject(value) || _isArray(value)) { return true; } @@ -573,3 +583,44 @@ export function jsonLinesParse(jsonLines: string): any[] { }) .filter(x => x); } + +export function serializeJsTypesForJsonStringify(obj) { + return _cloneDeepWith(obj, value => { + if (typeof value === 'bigint') { + return { $bigint: value.toString() }; + } + }); +} + +export function deserializeJsTypesFromJsonParse(obj) { + return _cloneDeepWith(obj, value => { + if (value?.$bigint) { + return BigInt(value.$bigint); + } + }); +} + +export function serializeJsTypesReplacer(key, value) { + if (typeof value === 'bigint') { + return { $bigint: value.toString() }; + } + return value; +} + +export function deserializeJsTypesReviver(key, value) { + if (value?.$bigint) { + return BigInt(value.$bigint); + } + return value; +} + +export function parseNumberSafe(value) { + if (/^-?[0-9]+$/.test(value)) { + const parsed = parseInt(value); + if (Number.isSafeInteger(parsed)) { + return parsed; + } + return BigInt(value); + } + return parseFloat(value); +} diff --git a/packages/web/src/datagrid/DataGridCell.svelte b/packages/web/src/datagrid/DataGridCell.svelte index ba618460c..f7596cb69 100644 --- a/packages/web/src/datagrid/DataGridCell.svelte +++ b/packages/web/src/datagrid/DataGridCell.svelte @@ -54,7 +54,8 @@ $: style = computeStyle(maxWidth, col); - $: isJson = _.isPlainObject(value) && !(value?.type == 'Buffer' && _.isArray(value.data)) && !value.$oid; + $: isJson = + _.isPlainObject(value) && !(value?.type == 'Buffer' && _.isArray(value.data)) && !value.$oid && !value.$bigint; // don't parse JSON for explicit data types $: jsonParsedValue = !editorTypes?.explicitDataType && isJsonLikeLongString(value) ? safeJsonParse(value) : null; diff --git a/packages/web/src/datagrid/gridutil.ts b/packages/web/src/datagrid/gridutil.ts index a631cd512..0d155d87b 100644 --- a/packages/web/src/datagrid/gridutil.ts +++ b/packages/web/src/datagrid/gridutil.ts @@ -72,6 +72,7 @@ export function countColumnSizes(grider: Grider, columns, containerWidth, displa let text = value; if (_.isArray(value)) text = `[${value.length} items]`; else if (value?.$oid) text = `ObjectId("${value.$oid}")`; + else if (value?.$bigint) text = value.$bigint; else if (isJsonLikeLongString(value) && safeJsonParse(value)) text = '(JSON)'; const width = context.measureText(text).width + 8; // console.log('colName', colName, text, width); diff --git a/packages/web/src/utility/api.ts b/packages/web/src/utility/api.ts index bcb72a31f..bfe827cfe 100644 --- a/packages/web/src/utility/api.ts +++ b/packages/web/src/utility/api.ts @@ -13,6 +13,7 @@ import { callServerPing } from './connectionsPinger'; import { batchDispatchCacheTriggers, dispatchCacheChange } from './cache'; import { isAdminPage, isOneOfPage } from './pageDefs'; import { openWebLink } from './simpleTools'; +import { serializeJsTypesReplacer } from 'dbgate-tools'; export const strmid = uuidv1(); @@ -177,7 +178,7 @@ export async function apiCall( 'Content-Type': 'application/json', ...resolveApiHeaders(), }, - body: JSON.stringify(args), + body: JSON.stringify(args, serializeJsTypesReplacer), }); if (resp.status == 401 && !apiDisabled) { diff --git a/plugins/dbgate-plugin-postgres/src/backend/drivers.js b/plugins/dbgate-plugin-postgres/src/backend/drivers.js index 54dcd146e..3413915b6 100644 --- a/plugins/dbgate-plugin-postgres/src/backend/drivers.js +++ b/plugins/dbgate-plugin-postgres/src/backend/drivers.js @@ -21,6 +21,11 @@ const logger = getLogger('postreDriver'); pg.types.setTypeParser(1082, 'text', val => val); // date pg.types.setTypeParser(1114, 'text', val => val); // timestamp without timezone pg.types.setTypeParser(1184, 'text', val => val); // timestamp +pg.types.setTypeParser(20, 'text', val => { + const parsed = parseInt(val); + if (Number.isSafeInteger(parsed)) return parsed; + return BigInt(val); +}); // timestamp function extractGeographyDate(value) { try { From ce7559087e00360a5e73204a5abb9562b06f1cc3 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Mon, 5 May 2025 16:24:55 +0200 Subject: [PATCH 017/129] duckdb - fixed bigint processing --- plugins/dbgate-plugin-duckdb/src/backend/helpers.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/plugins/dbgate-plugin-duckdb/src/backend/helpers.js b/plugins/dbgate-plugin-duckdb/src/backend/helpers.js index 5e5b85047..987c537f4 100644 --- a/plugins/dbgate-plugin-duckdb/src/backend/helpers.js +++ b/plugins/dbgate-plugin-duckdb/src/backend/helpers.js @@ -31,7 +31,14 @@ function _normalizeValue(value) { } if (typeof value === 'bigint') { - return parseInt(value); + const parsed = parseInt(value); + if (Number.isSafeInteger(parsed)) { + return parsed; + } else { + return { + $bigint: value.toString(), + }; + } } if (value instanceof DuckDBTimestampValue) { From b9d4197b5cbe9689e4bc45207b427580b3535087 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Mon, 5 May 2025 17:16:46 +0200 Subject: [PATCH 018/129] mongoDB - bigint support WIP --- packages/api/src/proc/databaseConnectionProcess.js | 2 +- packages/tools/src/stringTools.ts | 8 +++++++- packages/web/src/datagrid/CellValue.svelte | 2 ++ plugins/dbgate-plugin-mongo/src/backend/driver.js | 5 ++++- plugins/dbgate-plugin-mongo/src/frontend/driver.js | 14 ++++++++++---- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/packages/api/src/proc/databaseConnectionProcess.js b/packages/api/src/proc/databaseConnectionProcess.js index 2d7d613c5..2bb0c1375 100644 --- a/packages/api/src/proc/databaseConnectionProcess.js +++ b/packages/api/src/proc/databaseConnectionProcess.js @@ -255,7 +255,7 @@ async function handleDriverDataCore(msgid, callMethod, { logName }) { const driver = requireEngineDriver(storedConnection); try { const result = await callMethod(driver); - process.send({ msgtype: 'response', msgid, result }); + process.send({ msgtype: 'response', msgid, result: serializeJsTypesForJsonStringify(result) }); } catch (err) { logger.error(extractErrorLogData(err, { logName }), `Error when handling message ${logName}`); process.send({ msgtype: 'response', msgid, errorMessage: extractErrorMessage(err, 'Error executing DB data') }); diff --git a/packages/tools/src/stringTools.ts b/packages/tools/src/stringTools.ts index adf6aec3c..41a27e5ff 100644 --- a/packages/tools/src/stringTools.ts +++ b/packages/tools/src/stringTools.ts @@ -81,7 +81,7 @@ export function parseCellValue(value, editorTypes?: DataEditorTypesBehaviour) { if (editorTypes?.parseNumber) { if (/^-?[0-9]+(?:\.[0-9]+)?$/.test(value)) { - return parseFloat(value); + return parseNumberSafe(value); } } @@ -215,6 +215,12 @@ export function stringifyCellValue( gridStyle: 'valueCellStyle', }; } + if (typeof value === 'bigint') { + return { + value: value.toString(), + gridStyle: 'valueCellStyle', + }; + } if (editorTypes?.parseDateAsDollar) { if (value?.$date) { diff --git a/packages/web/src/datagrid/CellValue.svelte b/packages/web/src/datagrid/CellValue.svelte index 013a5ff99..8d006962e 100644 --- a/packages/web/src/datagrid/CellValue.svelte +++ b/packages/web/src/datagrid/CellValue.svelte @@ -16,6 +16,8 @@ { useThousandsSeparator: getBoolSettingsValue('dataGrid.thousandsSeparator', false) }, jsonParsedValue ); + + // $: console.log('CellValue', value, stringified); {#if rowData == null} diff --git a/plugins/dbgate-plugin-mongo/src/backend/driver.js b/plugins/dbgate-plugin-mongo/src/backend/driver.js index 9c10ba8eb..b2d400377 100644 --- a/plugins/dbgate-plugin-mongo/src/backend/driver.js +++ b/plugins/dbgate-plugin-mongo/src/backend/driver.js @@ -5,6 +5,7 @@ const driverBase = require('../frontend/driver'); const Analyser = require('./Analyser'); const { MongoClient, ObjectId, AbstractCursor } = require('mongodb'); const { EJSON } = require('bson'); +const { serializeJsTypesForJsonStringify } = require('dbgate-tools'); const createBulkInsertStream = require('./createBulkInsertStream'); const { convertToMongoCondition, @@ -13,7 +14,9 @@ const { } = require('../frontend/convertToMongoCondition'); function transformMongoData(row) { - return EJSON.serialize(row); + // TODO process LONG type + // console.log('ROW', row); + return EJSON.serialize(serializeJsTypesForJsonStringify(row)); } async function readCursor(cursor, options) { diff --git a/plugins/dbgate-plugin-mongo/src/frontend/driver.js b/plugins/dbgate-plugin-mongo/src/frontend/driver.js index 45d3f66b3..8b93696db 100644 --- a/plugins/dbgate-plugin-mongo/src/frontend/driver.js +++ b/plugins/dbgate-plugin-mongo/src/frontend/driver.js @@ -5,11 +5,17 @@ const { mongoSplitterOptions } = require('dbgate-query-splitter/lib/options'); const _pickBy = require('lodash/pickBy'); const _fromPairs = require('lodash/fromPairs'); +function mongoReplacer(key, value) { + if (typeof value === 'bigint') { + return { $bigint: value.toString() }; + } + return value; +} + function jsonStringifyWithObjectId(obj) { - return JSON.stringify(obj, undefined, 2).replace( - /\{\s*\"\$oid\"\s*\:\s*\"([0-9a-f]+)\"\s*\}/g, - (m, id) => `ObjectId("${id}")` - ); + return JSON.stringify(obj, mongoReplacer, 2) + .replace(/\{\s*\"\$oid\"\s*\:\s*\"([0-9a-f]+)\"\s*\}/g, (m, id) => `ObjectId("${id}")`) + .replace(/\{\s*\"\$bigint\"\s*\:\s*\"([0-9]+)\"\s*\}/g, (m, num) => `${num}n`); } /** @type {import('dbgate-types').SqlDialect} */ From a71c4fe7ec7b085a089956e22c0945d3254008e5 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Tue, 6 May 2025 12:51:37 +0200 Subject: [PATCH 019/129] mognoDB bigint support --- packages/tools/src/stringTools.ts | 5 +- .../dbgate-plugin-mongo/src/backend/driver.js | 55 +++++++++++-------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/packages/tools/src/stringTools.ts b/packages/tools/src/stringTools.ts index 41a27e5ff..281d914af 100644 --- a/packages/tools/src/stringTools.ts +++ b/packages/tools/src/stringTools.ts @@ -590,11 +590,14 @@ export function jsonLinesParse(jsonLines: string): any[] { .filter(x => x); } -export function serializeJsTypesForJsonStringify(obj) { +export function serializeJsTypesForJsonStringify(obj, replacer = null) { return _cloneDeepWith(obj, value => { if (typeof value === 'bigint') { return { $bigint: value.toString() }; } + if (replacer) { + return replacer(value); + } }); } diff --git a/plugins/dbgate-plugin-mongo/src/backend/driver.js b/plugins/dbgate-plugin-mongo/src/backend/driver.js index b2d400377..f6b17ea81 100644 --- a/plugins/dbgate-plugin-mongo/src/backend/driver.js +++ b/plugins/dbgate-plugin-mongo/src/backend/driver.js @@ -3,9 +3,9 @@ const stream = require('stream'); const isPromise = require('is-promise'); const driverBase = require('../frontend/driver'); const Analyser = require('./Analyser'); -const { MongoClient, ObjectId, AbstractCursor } = require('mongodb'); +const { MongoClient, ObjectId, AbstractCursor, Long } = require('mongodb'); const { EJSON } = require('bson'); -const { serializeJsTypesForJsonStringify } = require('dbgate-tools'); +const { serializeJsTypesForJsonStringify, deserializeJsTypesFromJsonParse } = require('dbgate-tools'); const createBulkInsertStream = require('./createBulkInsertStream'); const { convertToMongoCondition, @@ -13,21 +13,30 @@ const { convertToMongoSort, } = require('../frontend/convertToMongoCondition'); -function transformMongoData(row) { - // TODO process LONG type - // console.log('ROW', row); - return EJSON.serialize(serializeJsTypesForJsonStringify(row)); +function serializeMongoData(row) { + return EJSON.serialize( + serializeJsTypesForJsonStringify(row, (value) => { + if (value instanceof Long) { + if (Number.isSafeInteger(value.toNumber())) { + return value.toNumber(); + } + return { + $bigint: value.toString(), + }; + } + }) + ); } async function readCursor(cursor, options) { options.recordset({ __isDynamicStructure: true }); await cursor.forEach((row) => { - options.row(transformMongoData(row)); + options.row(serializeMongoData(row)); }); } -function convertObjectId(condition) { - return EJSON.deserialize(condition); +function deserializeMongoData(value) { + return deserializeJsTypesFromJsonParse(EJSON.deserialize(value)); } function findArrayResult(resValue) { @@ -266,7 +275,7 @@ const driver = { const cursorStream = exprValue.stream(); cursorStream.on('data', (row) => { - pass.write(transformMongoData(row)); + pass.write(serializeMongoData(row)); }); // propagate error @@ -320,26 +329,26 @@ const driver = { const collection = dbhan.getDatabase().collection(options.pureName); if (options.countDocuments) { - const count = await collection.countDocuments(convertObjectId(mongoCondition) || {}); + const count = await collection.countDocuments(deserializeMongoData(mongoCondition) || {}); return { count }; } else if (options.aggregate) { - let cursor = await collection.aggregate(convertObjectId(convertToMongoAggregate(options.aggregate))); + let cursor = await collection.aggregate(deserializeMongoData(convertToMongoAggregate(options.aggregate))); const rows = await cursor.toArray(); return { - rows: rows.map(transformMongoData).map((x) => ({ + rows: rows.map(serializeMongoData).map((x) => ({ ...x._id, ..._.omit(x, ['_id']), })), }; } else { // console.log('options.condition', JSON.stringify(options.condition, undefined, 2)); - let cursor = await collection.find(convertObjectId(mongoCondition) || {}); + let cursor = await collection.find(deserializeMongoData(mongoCondition) || {}); if (options.sort) cursor = cursor.sort(convertToMongoSort(options.sort)); if (options.skip) cursor = cursor.skip(options.skip); if (options.limit) cursor = cursor.limit(options.limit); const rows = await cursor.toArray(); return { - rows: rows.map(transformMongoData), + rows: rows.map(serializeMongoData), }; } } catch (err) { @@ -361,7 +370,7 @@ const driver = { ...insert.document, ...insert.fields, }; - const resdoc = await collection.insertOne(convertObjectId(document)); + const resdoc = await collection.insertOne(deserializeMongoData(document)); res.inserted.push(resdoc._id); } for (const update of changeSet.updates) { @@ -371,16 +380,16 @@ const driver = { ...update.document, ...update.fields, }; - const doc = await collection.findOne(convertObjectId(update.condition)); + const doc = await collection.findOne(deserializeMongoData(update.condition)); if (doc) { - const resdoc = await collection.replaceOne(convertObjectId(update.condition), { - ...convertObjectId(document), + const resdoc = await collection.replaceOne(deserializeMongoData(update.condition), { + ...deserializeMongoData(document), _id: doc._id, }); res.replaced.push(resdoc._id); } } else { - const set = convertObjectId(_.pickBy(update.fields, (v, k) => !v?.$$undefined$$)); + const set = deserializeMongoData(_.pickBy(update.fields, (v, k) => !v?.$$undefined$$)); const unset = _.fromPairs( Object.keys(update.fields) .filter((k) => update.fields[k]?.$$undefined$$) @@ -390,13 +399,13 @@ const driver = { if (!_.isEmpty(set)) updates.$set = set; if (!_.isEmpty(unset)) updates.$unset = unset; - const resdoc = await collection.updateOne(convertObjectId(update.condition), updates); + const resdoc = await collection.updateOne(deserializeMongoData(update.condition), updates); res.updated.push(resdoc._id); } } for (const del of changeSet.deletes) { const collection = db.collection(del.pureName); - const resdoc = await collection.deleteOne(convertObjectId(del.condition)); + const resdoc = await collection.deleteOne(deserializeMongoData(del.condition)); res.deleted.push(resdoc._id); } return res; @@ -452,7 +461,7 @@ const driver = { ]); const rows = await cursor.toArray(); return _.uniqBy( - rows.map(transformMongoData).map(({ _id }) => { + rows.map(serializeMongoData).map(({ _id }) => { if (_.isArray(_id) || _.isPlainObject(_id)) return { value: null }; return { value: _id }; }), From 8c1b51b7e9fa77696977ca7438d3eb5274adcbb5 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Tue, 6 May 2025 14:04:29 +0200 Subject: [PATCH 020/129] v6.4.2-beta.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0e328c98f..3722a7a85 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "6.4.1", + "version": "6.4.2-beta.1", "name": "dbgate-all", "workspaces": [ "packages/*", From 541af0b77e78ed168bff34c0afc61d38560275c3 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Mon, 12 May 2025 10:28:28 +0200 Subject: [PATCH 021/129] fixed loading keys in redis --- packages/tools/src/dbKeysLoader.ts | 8 ++++++-- packages/web/src/widgets/DbKeysSubTree.svelte | 15 ++++++++++++++- packages/web/src/widgets/DbKeysTreeNode.svelte | 4 +++- plugins/dbgate-plugin-redis/src/backend/driver.js | 2 +- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/tools/src/dbKeysLoader.ts b/packages/tools/src/dbKeysLoader.ts index 428ca10ab..42c36923d 100644 --- a/packages/tools/src/dbKeysLoader.ts +++ b/packages/tools/src/dbKeysLoader.ts @@ -176,7 +176,7 @@ export function dbKeys_reloadFolder(tree: DbKeysTreeModel, root: string): DbKeys }; } -function addFlatItems(tree: DbKeysTreeModel, root: string, res: DbKeysNodeModel[]) { +function addFlatItems(tree: DbKeysTreeModel, root: string, res: DbKeysNodeModel[], visitedRoots: string[] = []) { const item = tree.dirsByKey[root]; if (!item.isExpanded) { return false; @@ -185,7 +185,11 @@ function addFlatItems(tree: DbKeysTreeModel, root: string, res: DbKeysNodeModel[ for (const child of children) { res.push(child); if (child.type == 'dir') { - addFlatItems(tree, child.root, res); + if (visitedRoots.includes(child.root)) { + console.warn('Redis: preventing infinite loop for root', child.root); + return false; + } + addFlatItems(tree, child.root, res, [...visitedRoots, root]); } } } diff --git a/packages/web/src/widgets/DbKeysSubTree.svelte b/packages/web/src/widgets/DbKeysSubTree.svelte index 5138474f2..b8e026839 100644 --- a/packages/web/src/widgets/DbKeysSubTree.svelte +++ b/packages/web/src/widgets/DbKeysSubTree.svelte @@ -17,11 +17,24 @@ export let model: DbKeysTreeModel; export let changeModel: DbKeysChangeModelFunction; + export let parentRoots = []; + $: items = model.childrenByKey[root] ?? []; {#each items as item} - + {/each} {#if model.dirsByKey[root]?.shouldLoadNext} diff --git a/packages/web/src/widgets/DbKeysTreeNode.svelte b/packages/web/src/widgets/DbKeysTreeNode.svelte index afb968be6..10b869b59 100644 --- a/packages/web/src/widgets/DbKeysTreeNode.svelte +++ b/packages/web/src/widgets/DbKeysTreeNode.svelte @@ -31,6 +31,7 @@ export let item; export let indentLevel = 0; export let filter; + export let parentRoots = []; export let model: DbKeysTreeModel; export let changeModel: DbKeysChangeModelFunction; @@ -179,7 +180,7 @@ {item.text} --> -{#if isExpanded} +{#if isExpanded && !parentRoots.includes(item.root)} {/if} diff --git a/plugins/dbgate-plugin-redis/src/backend/driver.js b/plugins/dbgate-plugin-redis/src/backend/driver.js index aab53f939..b1a7d1b98 100644 --- a/plugins/dbgate-plugin-redis/src/backend/driver.js +++ b/plugins/dbgate-plugin-redis/src/backend/driver.js @@ -260,7 +260,7 @@ const driver = { extractKeysFromLevel(dbhan, root, keys) { const prefix = root ? `${root}${dbhan.treeKeySeparator}` : ''; - const rootSplit = _.compact(root.split(dbhan.treeKeySeparator)); + const rootSplit = root == '' ? [] : root.split(dbhan.treeKeySeparator); const res = {}; for (const key of keys) { if (!key.startsWith(prefix)) continue; From 861ea7ef949269a47dab8b77022890dc10f952c6 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Mon, 12 May 2025 10:31:39 +0200 Subject: [PATCH 022/129] v6.4.2-premium-beta.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3722a7a85..8f504aaac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "6.4.2-beta.1", + "version": "6.4.2-premium-beta.2", "name": "dbgate-all", "workspaces": [ "packages/*", From a4518ce261e3f712639acd3c105f354dfb9f6d6b Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Mon, 12 May 2025 12:47:14 +0200 Subject: [PATCH 023/129] SYNC: handled double quote strings in MySQL #1107 --- packages/api/package.json | 2 +- packages/tools/package.json | 2 +- packages/web/package.json | 2 +- plugins/dbgate-plugin-duckdb/package.json | 2 +- plugins/dbgate-plugin-mongo/package.json | 2 +- plugins/dbgate-plugin-mssql/package.json | 2 +- plugins/dbgate-plugin-mysql/package.json | 2 +- plugins/dbgate-plugin-oracle/package.json | 2 +- plugins/dbgate-plugin-postgres/package.json | 2 +- plugins/dbgate-plugin-redis/package.json | 2 +- plugins/dbgate-plugin-sqlite/package.json | 2 +- yarn.lock | 127 ++++++++++++++++++-- 12 files changed, 127 insertions(+), 22 deletions(-) diff --git a/packages/api/package.json b/packages/api/package.json index 24d1b844f..9352b5b73 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -31,7 +31,7 @@ "cors": "^2.8.5", "cross-env": "^6.0.3", "dbgate-datalib": "^6.0.0-alpha.1", - "dbgate-query-splitter": "^4.11.4", + "dbgate-query-splitter": "^4.11.5", "dbgate-sqltree": "^6.0.0-alpha.1", "dbgate-tools": "^6.0.0-alpha.1", "debug": "^4.3.4", diff --git a/packages/tools/package.json b/packages/tools/package.json index 146c7a58f..a569a91a2 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -32,7 +32,7 @@ "typescript": "^4.4.3" }, "dependencies": { - "dbgate-query-splitter": "^4.11.4", + "dbgate-query-splitter": "^4.11.5", "dbgate-sqltree": "^6.0.0-alpha.1", "debug": "^4.3.4", "json-stable-stringify": "^1.0.1", diff --git a/packages/web/package.json b/packages/web/package.json index bc756de37..eb5223a84 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -26,7 +26,7 @@ "chartjs-adapter-moment": "^1.0.0", "cross-env": "^7.0.3", "dbgate-datalib": "^6.0.0-alpha.1", - "dbgate-query-splitter": "^4.11.4", + "dbgate-query-splitter": "^4.11.5", "dbgate-sqltree": "^6.0.0-alpha.1", "dbgate-tools": "^6.0.0-alpha.1", "dbgate-types": "^6.0.0-alpha.1", diff --git a/plugins/dbgate-plugin-duckdb/package.json b/plugins/dbgate-plugin-duckdb/package.json index fd1b853c1..77b0e5f48 100644 --- a/plugins/dbgate-plugin-duckdb/package.json +++ b/plugins/dbgate-plugin-duckdb/package.json @@ -37,7 +37,7 @@ "dependencies": { "dbgate-tools": "^6.0.0-alpha.1", "lodash": "^4.17.21", - "dbgate-query-splitter": "^4.11.3" + "dbgate-query-splitter": "^4.11.5" }, "optionalDependencies": { "@duckdb/node-api": "^1.2.1-alpha.16" diff --git a/plugins/dbgate-plugin-mongo/package.json b/plugins/dbgate-plugin-mongo/package.json index 8d26925f8..91076b030 100644 --- a/plugins/dbgate-plugin-mongo/package.json +++ b/plugins/dbgate-plugin-mongo/package.json @@ -37,7 +37,7 @@ }, "dependencies": { "bson": "^6.8.0", - "dbgate-query-splitter": "^4.11.4", + "dbgate-query-splitter": "^4.11.5", "dbgate-tools": "^6.0.0-alpha.1", "is-promise": "^4.0.0", "lodash": "^4.17.21", diff --git a/plugins/dbgate-plugin-mssql/package.json b/plugins/dbgate-plugin-mssql/package.json index 1de07ca9b..1e0e21477 100644 --- a/plugins/dbgate-plugin-mssql/package.json +++ b/plugins/dbgate-plugin-mssql/package.json @@ -38,7 +38,7 @@ "dependencies": { "@azure/identity": "^4.6.0", "async-lock": "^1.2.6", - "dbgate-query-splitter": "^4.11.4", + "dbgate-query-splitter": "^4.11.5", "dbgate-tools": "^6.0.0-alpha.1", "lodash": "^4.17.21", "tedious": "^18.6.1" diff --git a/plugins/dbgate-plugin-mysql/package.json b/plugins/dbgate-plugin-mysql/package.json index 09b74f0a1..b98ebbc44 100644 --- a/plugins/dbgate-plugin-mysql/package.json +++ b/plugins/dbgate-plugin-mysql/package.json @@ -36,7 +36,7 @@ "webpack-cli": "^5.1.4" }, "dependencies": { - "dbgate-query-splitter": "^4.11.4", + "dbgate-query-splitter": "^4.11.5", "dbgate-tools": "^6.0.0-alpha.1", "lodash": "^4.17.21", "mysql2": "^3.11.3" diff --git a/plugins/dbgate-plugin-oracle/package.json b/plugins/dbgate-plugin-oracle/package.json index 38544c880..bdec6926a 100644 --- a/plugins/dbgate-plugin-oracle/package.json +++ b/plugins/dbgate-plugin-oracle/package.json @@ -35,7 +35,7 @@ "webpack-cli": "^5.1.4" }, "dependencies": { - "dbgate-query-splitter": "^4.11.4", + "dbgate-query-splitter": "^4.11.5", "dbgate-tools": "^6.0.0-alpha.1", "lodash": "^4.17.21" }, diff --git a/plugins/dbgate-plugin-postgres/package.json b/plugins/dbgate-plugin-postgres/package.json index db0892ccf..fe38f9a72 100644 --- a/plugins/dbgate-plugin-postgres/package.json +++ b/plugins/dbgate-plugin-postgres/package.json @@ -37,7 +37,7 @@ "dependencies": { "wkx": "^0.5.0", "pg-copy-streams": "^6.0.6", - "dbgate-query-splitter": "^4.11.4", + "dbgate-query-splitter": "^4.11.5", "dbgate-tools": "^6.0.0-alpha.1", "lodash": "^4.17.21", "pg": "^8.11.5" diff --git a/plugins/dbgate-plugin-redis/package.json b/plugins/dbgate-plugin-redis/package.json index 89ef1a763..e0581f65c 100644 --- a/plugins/dbgate-plugin-redis/package.json +++ b/plugins/dbgate-plugin-redis/package.json @@ -34,7 +34,7 @@ "webpack-cli": "^5.1.4" }, "dependencies": { - "dbgate-query-splitter": "^4.11.4", + "dbgate-query-splitter": "^4.11.5", "dbgate-tools": "^6.0.0-alpha.1", "lodash": "^4.17.21", "async": "^3.2.3", diff --git a/plugins/dbgate-plugin-sqlite/package.json b/plugins/dbgate-plugin-sqlite/package.json index 9c45ac25d..6c599ccae 100644 --- a/plugins/dbgate-plugin-sqlite/package.json +++ b/plugins/dbgate-plugin-sqlite/package.json @@ -37,7 +37,7 @@ "dependencies": { "dbgate-tools": "^6.0.0-alpha.1", "lodash": "^4.17.21", - "dbgate-query-splitter": "^4.11.4" + "dbgate-query-splitter": "^4.11.5" }, "optionalDependencies": { "libsql": "0.5.0-pre.6", diff --git a/yarn.lock b/yarn.lock index 92632a452..3d12564cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -542,7 +542,7 @@ "@azure/core-util" "^1.1.0" tslib "^2.6.2" -"@azure/core-auth@^1.7.2", "@azure/core-auth@^1.8.0", "@azure/core-auth@^1.9.0": +"@azure/core-auth@^1.7.1", "@azure/core-auth@^1.7.2", "@azure/core-auth@^1.8.0", "@azure/core-auth@^1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@azure/core-auth/-/core-auth-1.9.0.tgz#ac725b03fabe3c892371065ee9e2041bee0fd1ac" integrity sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw== @@ -590,6 +590,19 @@ dependencies: tslib "^2.6.2" +"@azure/core-rest-pipeline@^1.15.1", "@azure/core-rest-pipeline@^1.8.0": + version "1.20.0" + resolved "https://registry.yarnpkg.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.20.0.tgz#916d8d6c9cff6b556f0b0bfd5b923526d590e2d9" + integrity sha512-ASoP8uqZBS3H/8N8at/XwFr6vYrRP3syTK0EUjDXQy0Y1/AUS+QeIRThKmTNJO2RggvBBxaXDPM7YoIwDGeA0g== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-auth" "^1.8.0" + "@azure/core-tracing" "^1.0.1" + "@azure/core-util" "^1.11.0" + "@azure/logger" "^1.0.0" + "@typespec/ts-http-runtime" "^0.2.2" + tslib "^2.6.2" + "@azure/core-rest-pipeline@^1.17.0": version "1.18.2" resolved "https://registry.yarnpkg.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.18.2.tgz#fa3a83b412d4b3e33edca30a71b1d5838306c075" @@ -625,6 +638,13 @@ dependencies: tslib "^2.6.2" +"@azure/core-tracing@^1.1.1": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@azure/core-tracing/-/core-tracing-1.2.0.tgz#7be5d53c3522d639cf19042cbcdb19f71bc35ab2" + integrity sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg== + dependencies: + tslib "^2.6.2" + "@azure/core-util@^1.0.0", "@azure/core-util@^1.1.0", "@azure/core-util@^1.2.0", "@azure/core-util@^1.6.1", "@azure/core-util@^1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.9.0.tgz#469afd7e6452d5388b189f90d33f7756b0b210d1" @@ -633,6 +653,15 @@ "@azure/abort-controller" "^2.0.0" tslib "^2.6.2" +"@azure/core-util@^1.10.0", "@azure/core-util@^1.8.1": + version "1.12.0" + resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.12.0.tgz#0b8c2837e6d67c3fbaeae20df34cf07f66b3480d" + integrity sha512-13IyjTQgABPARvG90+N2dXpC+hwp466XCdQXPCRlbWHgd3SJd5Q1VvaBGv6k1BIa4MQm6hAF1UBU1m8QUxV8sQ== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@typespec/ts-http-runtime" "^0.2.2" + tslib "^2.6.2" + "@azure/core-util@^1.11.0": version "1.11.0" resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.11.0.tgz#f530fc67e738aea872fbdd1cc8416e70219fada7" @@ -641,6 +670,23 @@ "@azure/abort-controller" "^2.0.0" tslib "^2.6.2" +"@azure/cosmos@^4.1.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@azure/cosmos/-/cosmos-4.3.0.tgz#aeb809f2c7837ea0f5613f2376b9b756ab7f348d" + integrity sha512-0Ls3l1uWBBSphx6YRhnM+w7rSvq8qVugBCdO6kSiNuRYXEf6+YWLjbzz4e7L2kkz/6ScFdZIOJYP+XtkiRYOhA== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-auth" "^1.7.1" + "@azure/core-rest-pipeline" "^1.15.1" + "@azure/core-tracing" "^1.1.1" + "@azure/core-util" "^1.8.1" + "@azure/keyvault-keys" "^4.8.0" + fast-json-stable-stringify "^2.1.0" + jsbi "^4.3.0" + priorityqueuejs "^2.0.0" + semaphore "^1.1.0" + tslib "^2.6.2" + "@azure/identity@^4.2.1": version "4.9.1" resolved "https://registry.yarnpkg.com/@azure/identity/-/identity-4.9.1.tgz#ee4b9435f1b96bea5985e7dec989760a67d9a119" @@ -678,6 +724,20 @@ stoppable "^1.1.0" tslib "^2.2.0" +"@azure/keyvault-common@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@azure/keyvault-common/-/keyvault-common-2.0.0.tgz#91e50df01d9bfa8f55f107bb9cdbc57586b2b2a4" + integrity sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-auth" "^1.3.0" + "@azure/core-client" "^1.5.0" + "@azure/core-rest-pipeline" "^1.8.0" + "@azure/core-tracing" "^1.0.0" + "@azure/core-util" "^1.10.0" + "@azure/logger" "^1.1.4" + tslib "^2.2.0" + "@azure/keyvault-keys@^4.4.0": version "4.8.0" resolved "https://registry.yarnpkg.com/@azure/keyvault-keys/-/keyvault-keys-4.8.0.tgz#1513b3a187bb3a9a372b5980c593962fb793b2ad" @@ -695,6 +755,24 @@ "@azure/logger" "^1.0.0" tslib "^2.2.0" +"@azure/keyvault-keys@^4.8.0": + version "4.9.0" + resolved "https://registry.yarnpkg.com/@azure/keyvault-keys/-/keyvault-keys-4.9.0.tgz#83ad2370429d1f576e6c5c59ff165761e2d8feab" + integrity sha512-ZBP07+K4Pj3kS4TF4XdkqFcspWwBHry3vJSOFM5k5ZABvf7JfiMonvaFk2nBF6xjlEbMpz5PE1g45iTMme0raQ== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-auth" "^1.3.0" + "@azure/core-client" "^1.5.0" + "@azure/core-http-compat" "^2.0.1" + "@azure/core-lro" "^2.2.0" + "@azure/core-paging" "^1.1.1" + "@azure/core-rest-pipeline" "^1.8.1" + "@azure/core-tracing" "^1.0.0" + "@azure/core-util" "^1.0.0" + "@azure/keyvault-common" "^2.0.0" + "@azure/logger" "^1.0.0" + tslib "^2.2.0" + "@azure/logger@^1.0.0": version "1.1.2" resolved "https://registry.yarnpkg.com/@azure/logger/-/logger-1.1.2.tgz#3f4b876cefad328dc14aff8b850d63b611e249dc" @@ -702,6 +780,14 @@ dependencies: tslib "^2.6.2" +"@azure/logger@^1.1.4": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@azure/logger/-/logger-1.2.0.tgz#a79aefcdd57d2a96603fab59c9a66e0d9022a564" + integrity sha512-0hKEzLhpw+ZTAfNJyRrn6s+V0nDWzXk9OjBr2TiGIu0OfMr5s2V4FpKLTAK3Ca5r5OKLbf4hkOGDPyiRjie/jA== + dependencies: + "@typespec/ts-http-runtime" "^0.2.2" + tslib "^2.6.2" + "@azure/msal-browser@^4.0.1": version "4.2.0" resolved "https://registry.yarnpkg.com/@azure/msal-browser/-/msal-browser-4.2.0.tgz#3d817357cfb0e6aef68bb708df7ccce9fe14ca65" @@ -2576,6 +2662,15 @@ dependencies: "@types/yargs-parser" "*" +"@typespec/ts-http-runtime@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@typespec/ts-http-runtime/-/ts-http-runtime-0.2.2.tgz#a0c7458ed99aae6d7eb22efc17a839cec0b4a1b3" + integrity sha512-Gz/Sm64+Sq/vklJu1tt9t+4R2lvnud8NbTD/ZfpZtMiUX7YeVpCA8j6NSW8ptwcoLL+NmYANwqP8DV0q/bwl2w== + dependencies: + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.0" + tslib "^2.6.2" + "@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" @@ -4275,15 +4370,10 @@ dbgate-plugin-tools@^1.0.4, dbgate-plugin-tools@^1.0.7, dbgate-plugin-tools@^1.0 pacote "^11.1.13" rimraf "^3.0.2" -dbgate-query-splitter@^4.11.3: - version "4.11.3" - resolved "https://registry.yarnpkg.com/dbgate-query-splitter/-/dbgate-query-splitter-4.11.3.tgz#8391363be4cac1bd41793e1aebb8c85b5b296f28" - integrity sha512-rdAGiaQ3f02gvN2SPMX5j3DqojIL/WE+EArvc7OkVk5QuCDNojWvDjqSxJoOBG593+Ob3lQ8/FYbKRCOYhAVYg== - -dbgate-query-splitter@^4.11.4: - version "4.11.4" - resolved "https://registry.yarnpkg.com/dbgate-query-splitter/-/dbgate-query-splitter-4.11.4.tgz#9a137329b84a5b353aedf41bdf58721fd9e32614" - integrity sha512-t0lsumpMsX0WSAAjsiFoYBSgj/bVOmmt6yFaPdmQcDBp1nq1wVt1iLS6oYYH9d20nrCwjhdUHBxuJFJ+bhimRw== +dbgate-query-splitter@^4.11.5: + version "4.11.5" + resolved "https://registry.yarnpkg.com/dbgate-query-splitter/-/dbgate-query-splitter-4.11.5.tgz#ed57b570303146258bc5e727c6d7f76d19082e29" + integrity sha512-xr4rWhuLeaaDdfdDpMOesCAnZGPUKTH9WPU/GSCHzoKR6NI60IyPjMccShLY+rPIsNw2SZQtbic2aCoFcwI4kg== debug@2.6.9, debug@^2.2.0, debug@^2.3.3: version "2.6.9" @@ -5153,7 +5243,7 @@ fast-glob@^3.0.3, fast-glob@^3.3.2: merge2 "^1.3.0" micromatch "^4.0.4" -fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -7365,6 +7455,11 @@ js2xmlparser@^4.0.2: dependencies: xmlcreate "^2.0.4" +jsbi@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/jsbi/-/jsbi-4.3.2.tgz#8a4d05d4e09907d73042135b6aa55a6d161ee955" + integrity sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew== + jsbn@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" @@ -9254,6 +9349,11 @@ printj@~1.1.0: resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222" integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ== +priorityqueuejs@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/priorityqueuejs/-/priorityqueuejs-2.0.0.tgz#96064040edd847ee9dd3013d8e16297399a6bd4f" + integrity sha512-19BMarhgpq3x4ccvVi8k2QpJZcymo/iFUcrhPd4V96kYGovOdTsWwy7fxChYi4QY+m2EnGBWSX9Buakz+tWNQQ== + process-nextick-args@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" @@ -9905,6 +10005,11 @@ secure-json-parse@^2.4.0: resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.7.0.tgz#5a5f9cd6ae47df23dba3151edd06855d47e09862" integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw== +semaphore@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/semaphore/-/semaphore-1.1.0.tgz#aaad8b86b20fe8e9b32b16dc2ee682a8cd26a8aa" + integrity sha512-O4OZEaNtkMd/K0i6js9SL+gqy0ZCBMgUvlSqHKi4IBdjhe7wB8pwztUk1BbZ1fmrvpwFrPbHzqd2w5pTcJH6LA== + semiver@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/semiver/-/semiver-1.1.0.tgz#9c97fb02c21c7ce4fcf1b73e2c7a24324bdddd5f" From 36ae07074d6150ebd47aff9cba379a42cbfd0457 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Mon, 12 May 2025 13:01:53 +0200 Subject: [PATCH 024/129] SYNC: View PostgreSQL server output #1108 --- .../dbgate-plugin-postgres/src/backend/drivers.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/plugins/dbgate-plugin-postgres/src/backend/drivers.js b/plugins/dbgate-plugin-postgres/src/backend/drivers.js index 3413915b6..282f347dc 100644 --- a/plugins/dbgate-plugin-postgres/src/backend/drivers.js +++ b/plugins/dbgate-plugin-postgres/src/backend/drivers.js @@ -164,6 +164,16 @@ const drivers = driverBases.map(driverBase => ({ return { rows: (res.rows || []).map(row => zipDataRow(row, columns)), columns }; }, stream(dbhan, sql, options) { + const handleNotice = notice => { + const { message, where } = notice; + options.info({ + message, + procedure: where, + time: new Date(), + severity: 'info', + }); + }; + const query = new pg.Query({ text: sql, rowMode: 'array', @@ -171,6 +181,7 @@ const drivers = driverBases.map(driverBase => ({ let wasHeader = false; let columnsToTransform = null; + dbhan.client.on('notice', handleNotice); query.on('row', row => { if (!wasHeader) { @@ -211,6 +222,7 @@ const drivers = driverBases.map(driverBase => ({ wasHeader = true; } + dbhan.client.off('notice', handleNotice); options.done(); }); @@ -228,6 +240,7 @@ const drivers = driverBases.map(driverBase => ({ time: new Date(), severity: 'error', }); + dbhan.client.off('notice', handleNotice); options.done(); }); From c3e09ddab02d34122c5a9b23e700417274938455 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Mon, 12 May 2025 13:05:49 +0200 Subject: [PATCH 025/129] SYNC: pgsql: added notice detail #1108 --- plugins/dbgate-plugin-postgres/src/backend/drivers.js | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/dbgate-plugin-postgres/src/backend/drivers.js b/plugins/dbgate-plugin-postgres/src/backend/drivers.js index 282f347dc..1daf620da 100644 --- a/plugins/dbgate-plugin-postgres/src/backend/drivers.js +++ b/plugins/dbgate-plugin-postgres/src/backend/drivers.js @@ -171,6 +171,7 @@ const drivers = driverBases.map(driverBase => ({ procedure: where, time: new Date(), severity: 'info', + detail: notice, }); }; From fb036935e66837a0e3586bce5b18b0a6b2cadc55 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Mon, 12 May 2025 15:55:19 +0200 Subject: [PATCH 026/129] SYNC: Limit query result rows #1098 --- packages/api/src/controllers/sessions.js | 4 +- packages/api/src/proc/sessionProcess.js | 4 +- packages/api/src/utility/handleQueryStream.js | 62 +++++++++++++++---- packages/web/src/icons/FontIcon.svelte | 1 + packages/web/src/modals/RowsLimitModal.svelte | 41 ++++++++++++ .../web/src/settings/SettingsModal.svelte | 6 ++ packages/web/src/tabs/QueryTab.svelte | 33 ++++++++++ 7 files changed, 136 insertions(+), 15 deletions(-) create mode 100644 packages/web/src/modals/RowsLimitModal.svelte diff --git a/packages/api/src/controllers/sessions.js b/packages/api/src/controllers/sessions.js index 08e3e11e2..4f2751483 100644 --- a/packages/api/src/controllers/sessions.js +++ b/packages/api/src/controllers/sessions.js @@ -141,7 +141,7 @@ module.exports = { }, executeQuery_meta: true, - async executeQuery({ sesid, sql, autoCommit }) { + async executeQuery({ sesid, sql, autoCommit, limitRows }) { const session = this.opened.find(x => x.sesid == sesid); if (!session) { throw new Error('Invalid session'); @@ -149,7 +149,7 @@ module.exports = { logger.info({ sesid, sql }, 'Processing query'); this.dispatchMessage(sesid, 'Query execution started'); - session.subprocess.send({ msgtype: 'executeQuery', sql, autoCommit }); + session.subprocess.send({ msgtype: 'executeQuery', sql, autoCommit, limitRows }); return { state: 'ok' }; }, diff --git a/packages/api/src/proc/sessionProcess.js b/packages/api/src/proc/sessionProcess.js index 7560f30f9..8dd193db5 100644 --- a/packages/api/src/proc/sessionProcess.js +++ b/packages/api/src/proc/sessionProcess.js @@ -117,7 +117,7 @@ async function handleExecuteControlCommand({ command }) { } } -async function handleExecuteQuery({ sql, autoCommit }) { +async function handleExecuteQuery({ sql, autoCommit, limitRows }) { lastActivity = new Date().getTime(); await waitConnected(); @@ -146,7 +146,7 @@ async function handleExecuteQuery({ sql, autoCommit }) { ...driver.getQuerySplitterOptions('stream'), returnRichInfo: true, })) { - await handleQueryStream(dbhan, driver, queryStreamInfoHolder, sqlItem); + await handleQueryStream(dbhan, driver, queryStreamInfoHolder, sqlItem, undefined, limitRows); // const handler = new StreamHandler(resultIndex); // const stream = await driver.stream(systemConnection, sqlItem, handler); // handler.stream = stream; diff --git a/packages/api/src/utility/handleQueryStream.js b/packages/api/src/utility/handleQueryStream.js index 76e573a57..f57cb46fd 100644 --- a/packages/api/src/utility/handleQueryStream.js +++ b/packages/api/src/utility/handleQueryStream.js @@ -82,20 +82,27 @@ class QueryStreamTableWriter { } close(afterClose) { - if (this.currentStream) { - this.currentStream.end(() => { - this.writeCurrentStats(true, true); - if (afterClose) afterClose(); - }); - } + return new Promise(resolve => { + if (this.currentStream) { + this.currentStream.end(() => { + this.writeCurrentStats(true, true); + if (afterClose) afterClose(); + resolve(); + }); + } else { + resolve(); + } + }); } } class StreamHandler { - constructor(queryStreamInfoHolder, resolve, startLine, sesid = undefined) { + constructor(queryStreamInfoHolder, resolve, startLine, sesid = undefined, limitRows = undefined) { this.recordset = this.recordset.bind(this); this.startLine = startLine; this.sesid = sesid; + this.limitRows = limitRows; + this.rowsLimitOverflow = false; this.row = this.row.bind(this); // this.error = this.error.bind(this); this.done = this.done.bind(this); @@ -107,6 +114,7 @@ class StreamHandler { this.plannedStats = false; this.queryStreamInfoHolder = queryStreamInfoHolder; this.resolve = resolve; + this.rowCounter = 0; // currentHandlers = [...currentHandlers, this]; } @@ -118,6 +126,9 @@ class StreamHandler { } recordset(columns) { + if (this.rowsLimitOverflow) { + return; + } this.closeCurrentWriter(); this.currentWriter = new QueryStreamTableWriter(this.sesid); this.currentWriter.initializeFromQuery( @@ -125,6 +136,7 @@ class StreamHandler { this.queryStreamInfoHolder.resultIndex ); this.queryStreamInfoHolder.resultIndex += 1; + this.rowCounter = 0; // this.writeCurrentStats(); @@ -135,8 +147,36 @@ class StreamHandler { // }, 500); } row(row) { - if (this.currentWriter) this.currentWriter.row(row); - else if (row.message) process.send({ msgtype: 'info', info: { message: row.message }, sesid: this.sesid }); + if (this.rowsLimitOverflow) { + return; + } + + if (this.limitRows && this.rowCounter >= this.limitRows) { + process.send({ + msgtype: 'info', + info: { message: `Rows limit overflow, loaded ${this.rowCounter} rows, canceling query`, severity: 'error' }, + sesid: this.sesid, + }); + this.rowsLimitOverflow = true; + + this.queryStreamInfoHolder.canceled = true; + if (this.currentWriter) { + this.currentWriter.close().then(() => { + process.exit(0); + }); + } else { + process.exit(0); + } + + return; + } + + if (this.currentWriter) { + this.currentWriter.row(row); + this.rowCounter += 1; + } else if (row.message) { + process.send({ msgtype: 'info', info: { message: row.message }, sesid: this.sesid }); + } // this.onRow(this.jslid); } // error(error) { @@ -161,10 +201,10 @@ class StreamHandler { } } -function handleQueryStream(dbhan, driver, queryStreamInfoHolder, sqlItem, sesid = undefined) { +function handleQueryStream(dbhan, driver, queryStreamInfoHolder, sqlItem, sesid = undefined, limitRows = undefined) { return new Promise((resolve, reject) => { const start = sqlItem.trimStart || sqlItem.start; - const handler = new StreamHandler(queryStreamInfoHolder, resolve, start && start.line, sesid); + const handler = new StreamHandler(queryStreamInfoHolder, resolve, start && start.line, sesid, limitRows); driver.stream(dbhan, sqlItem.text, handler); }); } diff --git a/packages/web/src/icons/FontIcon.svelte b/packages/web/src/icons/FontIcon.svelte index 8190919fc..bd9d0df51 100644 --- a/packages/web/src/icons/FontIcon.svelte +++ b/packages/web/src/icons/FontIcon.svelte @@ -222,6 +222,7 @@ 'icon premium': 'mdi mdi-star', 'icon upload': 'mdi mdi-upload', + 'icon limit': 'mdi mdi-car-speed-limiter', 'img ok': 'mdi mdi-check-circle color-icon-green', 'img ok-inv': 'mdi mdi-check-circle color-icon-inv-green', diff --git a/packages/web/src/modals/RowsLimitModal.svelte b/packages/web/src/modals/RowsLimitModal.svelte new file mode 100644 index 000000000..12f62606f --- /dev/null +++ b/packages/web/src/modals/RowsLimitModal.svelte @@ -0,0 +1,41 @@ + + + + + Rows limit + + + + + handleSubmit(parseInt(e.detail.value) || null)} + data-testid="RowsLimitModal_setLimit" + /> + handleSubmit(null)} data-testid="RowsLimitModal_setNoLimit" /> + + + + diff --git a/packages/web/src/settings/SettingsModal.svelte b/packages/web/src/settings/SettingsModal.svelte index 0cec20629..6e66932ab 100644 --- a/packages/web/src/settings/SettingsModal.svelte +++ b/packages/web/src/settings/SettingsModal.svelte @@ -227,6 +227,12 @@ ORDER BY + +
Connection
diff --git a/packages/web/src/tabs/QueryTab.svelte b/packages/web/src/tabs/QueryTab.svelte index 41ae484d2..da772adb9 100644 --- a/packages/web/src/tabs/QueryTab.svelte +++ b/packages/web/src/tabs/QueryTab.svelte @@ -144,6 +144,9 @@ import HorizontalSplitter from '../elements/HorizontalSplitter.svelte'; import QueryAiAssistant from '../query/QueryAiAssistant.svelte'; import uuidv1 from 'uuid/v1'; + import ToolStripButton from '../buttons/ToolStripButton.svelte'; + import { getIntSettingsValue } from '../settings/settingsTools'; + import RowsLimitModal from '../modals/RowsLimitModal.svelte'; export let tabid; export let conid; @@ -197,6 +200,21 @@ let isInTransaction = false; let isAutocommit = false; + const queryRowsLimitLocalStorageKey = `tabdata_limitRows_${tabid}`; + function getInitialRowsLimit() { + const storageValue = localStorage.getItem(queryRowsLimitLocalStorageKey); + if (storageValue == 'nolimit') { + return null; + } + if (storageValue) { + return parseInt(storageValue) ?? null; + } + return getIntSettingsValue('sqlEditor.limitRows', null, 1); + } + + let queryRowsLimit = getInitialRowsLimit(); + $: localStorage.setItem(queryRowsLimitLocalStorageKey, queryRowsLimit ? queryRowsLimit.toString() : 'nolimit'); + onMount(() => { intervalId = setInterval(() => { if (!driver?.singleConnectionOnly && sessionId) { @@ -362,6 +380,7 @@ sesid, sql, autoCommit: driver?.implicitTransactions && isAutocommit, + limitRows: queryRowsLimit ? queryRowsLimit : undefined, }); } await apiCall('query-history/write', { @@ -713,6 +732,20 @@ + {#if !driver?.singleConnectionOnly} + + showModal(RowsLimitModal, { + value: queryRowsLimit, + onConfirm: value => { + queryRowsLimit = value; + }, + })} + > + {queryRowsLimit ? `Limit ${queryRowsLimit} rows` : 'Unlimited rows'} + {/if} {#if resultCount == 1} {/if} From 2b101844e94c68c06b9506ead876d13679071311 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Tue, 13 May 2025 09:51:00 +0200 Subject: [PATCH 027/129] Shell: Run script --- packages/api/src/shell/executeQuery.js | 8 ++++-- .../web/src/appobj/DatabaseAppObject.svelte | 28 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/api/src/shell/executeQuery.js b/packages/api/src/shell/executeQuery.js index 3f54267f0..cdd8124e0 100644 --- a/packages/api/src/shell/executeQuery.js +++ b/packages/api/src/shell/executeQuery.js @@ -14,6 +14,7 @@ const logger = getLogger('execQuery'); * @param {string} [options.sql] - SQL query * @param {string} [options.sqlFile] - SQL file * @param {boolean} [options.logScriptItems] - whether to log script items instead of whole script + * @param {boolean} [options.skipLogging] - whether to skip logging */ async function executeQuery({ connection = undefined, @@ -22,8 +23,9 @@ async function executeQuery({ sql, sqlFile = undefined, logScriptItems = false, + skipLogging = false, }) { - if (!logScriptItems) { + if (!logScriptItems && !skipLogging) { logger.info({ sql: getLimitedQuery(sql) }, `Execute query`); } @@ -36,7 +38,9 @@ async function executeQuery({ } try { - logger.debug(`Running SQL query, length: ${sql.length}`); + if (!skipLogging) { + logger.debug(`Running SQL query, length: ${sql.length}`); + } await driver.script(dbhan, sql, { logScriptItems }); } finally { diff --git a/packages/web/src/appobj/DatabaseAppObject.svelte b/packages/web/src/appobj/DatabaseAppObject.svelte index 28ac3c572..b9b9e11d1 100644 --- a/packages/web/src/appobj/DatabaseAppObject.svelte +++ b/packages/web/src/appobj/DatabaseAppObject.svelte @@ -330,6 +330,29 @@ await dbgateApi.dropAllDbObjects(${JSON.stringify( }); }; + const handleGenerateRunScript = () => { + openNewTab( + { + title: 'Shell #', + icon: 'img shell', + tabComponent: 'ShellTab', + }, + { + editor: `// @require ${extractPackageName(connection.engine)} + +await dbgateApi.executeQuery(${JSON.stringify( + { + connection: extractShellConnection(connection, name), + sql: 'your script here', + }, + undefined, + 2 + )}); +`, + } + ); + }; + const handleShowDataDeployer = () => { showModal(ChooseArchiveFolderModal, { message: 'Choose archive folder for data deployer', @@ -439,6 +462,11 @@ await dbgateApi.dropAllDbObjects(${JSON.stringify( text: 'Shell: Drop all objects', }, + { + onClick: handleGenerateRunScript, + text: 'Shell: Run script', + }, + driver?.databaseEngineTypes?.includes('sql') && hasPermission(`dbops/import`) && { onClick: handleShowDataDeployer, From f6699ad93bfe009801dec8f8bb75f7c0c3631482 Mon Sep 17 00:00:00 2001 From: Nybkox Date: Tue, 13 May 2025 12:35:24 +0200 Subject: [PATCH 028/129] fix: add triggers to mysql snapshot --- .../src/backend/Analyser.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/plugins/dbgate-plugin-mysql/src/backend/Analyser.js b/plugins/dbgate-plugin-mysql/src/backend/Analyser.js index 27402eca8..5b97f15d2 100644 --- a/plugins/dbgate-plugin-mysql/src/backend/Analyser.js +++ b/plugins/dbgate-plugin-mysql/src/backend/Analyser.js @@ -245,6 +245,7 @@ class Analyser extends DatabaseAnalyser { parameters: functionNameToParameters[x.pureName], })), triggers: triggers.rows.map(row => ({ + objectId: 'triggers:' + row.triggerName, contentHash: row.modifyDate, pureName: row.triggerName, eventType: row.eventType, @@ -277,6 +278,7 @@ class Analyser extends DatabaseAnalyser { const procedureModificationsQueryData = await this.analyserQuery('procedureModifications'); const functionModificationsQueryData = await this.analyserQuery('functionModifications'); const schedulerEvents = await this.analyserQuery('schedulerEvents'); + const triggers = await this.analyserQuery('triggers'); return { tables: tableModificationsQueryData.rows @@ -307,17 +309,13 @@ class Analyser extends DatabaseAnalyser { schedulerEvents: schedulerEvents.rows.map(row => ({ contentHash: _.isDate(row.LAST_ALTERED) ? row.LAST_ALTERED.toISOString() : row.LAST_ALTERED, pureName: row.EVENT_NAME, - createSql: row.CREATE_SQL, objectId: row.EVENT_NAME, - intervalValue: row.INTERVAL_VALUE, - intervalField: row.INTERVAL_FIELD, - starts: row.STARTS, - status: row.STATUS, - executeAt: row.EXECUTE_AT, - lastExecuted: row.LAST_EXECUTED, - eventType: row.EVENT_TYPE, - definer: row.DEFINER, - objectTypeField: 'schedulerEvents', + })), + triggers: triggers.rows.map(row => ({ + contentHash: row.modifyDate, + objectId: 'triggers:' + row.triggerName, + pureName: row.triggerName, + tableName: row.tableName, })), }; } From 31a6f7b621ea97c51b0ff1db7077c48b74bbbddb Mon Sep 17 00:00:00 2001 From: Nybkox Date: Tue, 13 May 2025 13:03:59 +0200 Subject: [PATCH 029/129] fix: add triggersModifications query --- plugins/dbgate-plugin-mysql/src/backend/Analyser.js | 2 +- plugins/dbgate-plugin-mysql/src/backend/sql/index.js | 2 ++ .../src/backend/sql/triggersModifications.js | 9 +++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 plugins/dbgate-plugin-mysql/src/backend/sql/triggersModifications.js diff --git a/plugins/dbgate-plugin-mysql/src/backend/Analyser.js b/plugins/dbgate-plugin-mysql/src/backend/Analyser.js index 5b97f15d2..ced1f327b 100644 --- a/plugins/dbgate-plugin-mysql/src/backend/Analyser.js +++ b/plugins/dbgate-plugin-mysql/src/backend/Analyser.js @@ -278,7 +278,7 @@ class Analyser extends DatabaseAnalyser { const procedureModificationsQueryData = await this.analyserQuery('procedureModifications'); const functionModificationsQueryData = await this.analyserQuery('functionModifications'); const schedulerEvents = await this.analyserQuery('schedulerEvents'); - const triggers = await this.analyserQuery('triggers'); + const triggers = await this.analyserQuery('triggersModifications'); return { tables: tableModificationsQueryData.rows diff --git a/plugins/dbgate-plugin-mysql/src/backend/sql/index.js b/plugins/dbgate-plugin-mysql/src/backend/sql/index.js index ab23eeaca..37c4234f3 100644 --- a/plugins/dbgate-plugin-mysql/src/backend/sql/index.js +++ b/plugins/dbgate-plugin-mysql/src/backend/sql/index.js @@ -12,6 +12,7 @@ const uniqueNames = require('./uniqueNames'); const viewTexts = require('./viewTexts'); const parameters = require('./parameters'); const triggers = require('./triggers'); +const triggersModifications = require('./triggersModifications'); const schedulerEvents = require('./schedulerEvents.js'); module.exports = { @@ -29,5 +30,6 @@ module.exports = { uniqueNames, viewTexts, triggers, + triggersModifications, schedulerEvents, }; diff --git a/plugins/dbgate-plugin-mysql/src/backend/sql/triggersModifications.js b/plugins/dbgate-plugin-mysql/src/backend/sql/triggersModifications.js new file mode 100644 index 000000000..f52334b07 --- /dev/null +++ b/plugins/dbgate-plugin-mysql/src/backend/sql/triggersModifications.js @@ -0,0 +1,9 @@ +module.exports = ` +SELECT + TRIGGER_NAME AS triggerName, + EVENT_OBJECT_TABLE AS tableName, + CREATED as modifyDate +FROM + INFORMATION_SCHEMA.TRIGGERS + WHERE EVENT_OBJECT_SCHEMA = '#DATABASE#' AND TRIGGER_NAME =OBJECT_ID_CONDITION +`; From 660e76145ed4a1498013e6fa2c5bc1ca3de90244 Mon Sep 17 00:00:00 2001 From: Nybkox Date: Tue, 13 May 2025 13:04:10 +0200 Subject: [PATCH 030/129] fix: add schedulerEventsModifications query --- plugins/dbgate-plugin-mysql/src/backend/Analyser.js | 2 +- plugins/dbgate-plugin-mysql/src/backend/sql/index.js | 2 ++ .../src/backend/sql/schedulerEventsModifications.js | 7 +++++++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 plugins/dbgate-plugin-mysql/src/backend/sql/schedulerEventsModifications.js diff --git a/plugins/dbgate-plugin-mysql/src/backend/Analyser.js b/plugins/dbgate-plugin-mysql/src/backend/Analyser.js index ced1f327b..ba94d39bb 100644 --- a/plugins/dbgate-plugin-mysql/src/backend/Analyser.js +++ b/plugins/dbgate-plugin-mysql/src/backend/Analyser.js @@ -277,7 +277,7 @@ class Analyser extends DatabaseAnalyser { const tableModificationsQueryData = await this.analyserQuery('tableModifications'); const procedureModificationsQueryData = await this.analyserQuery('procedureModifications'); const functionModificationsQueryData = await this.analyserQuery('functionModifications'); - const schedulerEvents = await this.analyserQuery('schedulerEvents'); + const schedulerEvents = await this.analyserQuery('schedulerEventsModifications'); const triggers = await this.analyserQuery('triggersModifications'); return { diff --git a/plugins/dbgate-plugin-mysql/src/backend/sql/index.js b/plugins/dbgate-plugin-mysql/src/backend/sql/index.js index 37c4234f3..41450cb6d 100644 --- a/plugins/dbgate-plugin-mysql/src/backend/sql/index.js +++ b/plugins/dbgate-plugin-mysql/src/backend/sql/index.js @@ -14,6 +14,7 @@ const parameters = require('./parameters'); const triggers = require('./triggers'); const triggersModifications = require('./triggersModifications'); const schedulerEvents = require('./schedulerEvents.js'); +const schedulerEventsModifications = require('./schedulerEventsModifications.js'); module.exports = { columns, @@ -32,4 +33,5 @@ module.exports = { triggers, triggersModifications, schedulerEvents, + schedulerEventsModifications, }; diff --git a/plugins/dbgate-plugin-mysql/src/backend/sql/schedulerEventsModifications.js b/plugins/dbgate-plugin-mysql/src/backend/sql/schedulerEventsModifications.js new file mode 100644 index 000000000..540806ebf --- /dev/null +++ b/plugins/dbgate-plugin-mysql/src/backend/sql/schedulerEventsModifications.js @@ -0,0 +1,7 @@ +module.exports = ` +SELECT + EVENT_NAME, + LAST_ALTERED +FROM INFORMATION_SCHEMA.EVENTS +WHERE EVENT_SCHEMA = '#DATABASE#' AND EVENT_NAME =OBJECT_ID_CONDITION +`; From 170cf4753e9e731568ed2672bc7f5c5cafee7198 Mon Sep 17 00:00:00 2001 From: Nybkox Date: Tue, 13 May 2025 14:10:00 +0200 Subject: [PATCH 031/129] fix: remove object conditions from modification queries --- .../src/backend/sql/schedulerEventsModifications.js | 2 +- .../src/backend/sql/triggersModifications.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/dbgate-plugin-mysql/src/backend/sql/schedulerEventsModifications.js b/plugins/dbgate-plugin-mysql/src/backend/sql/schedulerEventsModifications.js index 540806ebf..b66612417 100644 --- a/plugins/dbgate-plugin-mysql/src/backend/sql/schedulerEventsModifications.js +++ b/plugins/dbgate-plugin-mysql/src/backend/sql/schedulerEventsModifications.js @@ -3,5 +3,5 @@ SELECT EVENT_NAME, LAST_ALTERED FROM INFORMATION_SCHEMA.EVENTS -WHERE EVENT_SCHEMA = '#DATABASE#' AND EVENT_NAME =OBJECT_ID_CONDITION +WHERE EVENT_SCHEMA = '#DATABASE#' `; diff --git a/plugins/dbgate-plugin-mysql/src/backend/sql/triggersModifications.js b/plugins/dbgate-plugin-mysql/src/backend/sql/triggersModifications.js index f52334b07..cc2807481 100644 --- a/plugins/dbgate-plugin-mysql/src/backend/sql/triggersModifications.js +++ b/plugins/dbgate-plugin-mysql/src/backend/sql/triggersModifications.js @@ -5,5 +5,5 @@ SELECT CREATED as modifyDate FROM INFORMATION_SCHEMA.TRIGGERS - WHERE EVENT_OBJECT_SCHEMA = '#DATABASE#' AND TRIGGER_NAME =OBJECT_ID_CONDITION + WHERE EVENT_OBJECT_SCHEMA = '#DATABASE#' `; From e8d5412e14731f35ce72c208bfa5dbd4e6e36703 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Tue, 13 May 2025 16:22:13 +0200 Subject: [PATCH 032/129] survey link --- packages/web/src/widgets/AdminPremiumPromoWidget.svelte | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/web/src/widgets/AdminPremiumPromoWidget.svelte b/packages/web/src/widgets/AdminPremiumPromoWidget.svelte index b7560eeb8..c47cbdb62 100644 --- a/packages/web/src/widgets/AdminPremiumPromoWidget.svelte +++ b/packages/web/src/widgets/AdminPremiumPromoWidget.svelte @@ -40,6 +40,13 @@
openWebLink('https://dbgate.io/purchase/premium')} value="Purchase" />
+ +

Give us feedback

+

Your feedback is very valuable for us. It helps us to improve DbGate and make it more useful for you.

+ +
+ openWebLink('https://dbgate.org/survey')} value="Fill out the survey" skipWidth /> +
diff --git a/packages/web/src/icons/FontIcon.svelte b/packages/web/src/icons/FontIcon.svelte index bd9d0df51..86bee43eb 100644 --- a/packages/web/src/icons/FontIcon.svelte +++ b/packages/web/src/icons/FontIcon.svelte @@ -151,6 +151,7 @@ 'icon text': 'mdi mdi-text', 'icon ai': 'mdi mdi-head-lightbulb', 'icon wait': 'mdi mdi-timer-sand', + 'icon more': 'mdi mdi-more', 'icon run': 'mdi mdi-play', 'icon chevron-down': 'mdi mdi-chevron-down', diff --git a/packages/web/src/utility/metadataLoaders.ts b/packages/web/src/utility/metadataLoaders.ts index 67f9d7465..2fc374a02 100644 --- a/packages/web/src/utility/metadataLoaders.ts +++ b/packages/web/src/utility/metadataLoaders.ts @@ -83,12 +83,6 @@ const databaseListLoader = ({ conid }) => ({ errorValue: [], }); -// const databaseKeysLoader = ({ conid, database, root }) => ({ -// url: 'database-connections/load-keys', -// params: { conid, database, root }, -// reloadTrigger: `database-keys-changed-${conid}-${database}`, -// }); - const serverVersionLoader = ({ conid }) => ({ url: 'server-connections/version', params: { conid }, diff --git a/packages/web/src/widgets/DatabaseWidget.svelte b/packages/web/src/widgets/DatabaseWidget.svelte index 7a295761e..7e525c090 100644 --- a/packages/web/src/widgets/DatabaseWidget.svelte +++ b/packages/web/src/widgets/DatabaseWidget.svelte @@ -80,7 +80,7 @@ storageName="dbObjectsWidget" skip={!(conid && (database || singleDatabase) && driver?.databaseEngineTypes?.includes('keyvalue'))} > - + import { dbKeys_getFlatList, - dbKeys_loadMissing, + dbKeys_loadNext, dbKeys_markNodeExpanded, dbKeys_refreshAll, findEngineDriver, @@ -36,6 +36,7 @@ export let conid; export let database; + export let treeKeySeparator = ':'; let domListHandler; let domContainer = null; @@ -43,10 +44,10 @@ let filter; - let model = dbKeys_refreshAll(); + let model = dbKeys_refreshAll(treeKeySeparator); function handleRefreshDatabase() { - changeModel(model => dbKeys_refreshAll(model)); + changeModel(model => dbKeys_refreshAll(treeKeySeparator, model)); } function handleAddKey() { @@ -80,22 +81,26 @@ $: connection = useConnectionInfo({ conid }); - async function changeModel(modelUpdate) { + function changeModel(modelUpdate) { model = modelUpdate(model); - model = await dbKeys_loadMissing(model, async (root, limit) => { - const result = await apiCall('database-connections/load-keys', { + } + + async function loadNextPage() { + model = await dbKeys_loadNext(model, async (cursor, count) => { + const result = await apiCall('database-connections/scan-keys', { conid, database, - root, - filter, - limit, + pattern: filter, + cursor, + count, }); return result; }); } function reloadModel() { - changeModel(model => dbKeys_refreshAll(model)); + changeModel(model => dbKeys_refreshAll(treeKeySeparator, model)); + loadNextPage(); } $: { @@ -104,11 +109,13 @@ filter; reloadModel(); } + + $: console.log('DbKeysTree MODEL', model); - + +
+
Scanned 10/20 keys
+ + Scan more + +
{#if differentFocusedDb} {/if} diff --git a/plugins/dbgate-plugin-redis/src/backend/driver.js b/plugins/dbgate-plugin-redis/src/backend/driver.js index b1a7d1b98..b2baf6eb5 100644 --- a/plugins/dbgate-plugin-redis/src/backend/driver.js +++ b/plugins/dbgate-plugin-redis/src/backend/driver.js @@ -201,6 +201,18 @@ const driver = { return _.range(16).map((index) => ({ name: `db${index}`, extInfo: info[`db${index}`], sortOrder: index })); }, + async scanKeys(dbhan, pattern, cursor = 0, count) { + const [nextCursor, keys] = await dbhan.client.scan(cursor, 'MATCH', pattern || '*', 'COUNT', count); + const keysMapped = keys.map((key) => ({ + key, + })); + await this.enrichKeyInfo(dbhan, keysMapped); + return { + nextCursor, + keys: keysMapped, + }; + }, + async loadKeys(dbhan, root = '', filter = null, limit = null) { const keys = await this.getKeys(dbhan, root ? `${root}${dbhan.treeKeySeparator}*` : '*'); const keysFiltered = keys.filter((x) => filterName(filter, x)); @@ -310,9 +322,9 @@ const driver = { item.count = await this.getKeyCardinality(dbhan, item.key, item.type); }, - async enrichKeyInfo(dbhan, levelInfo) { + async enrichKeyInfo(dbhan, keyObjects) { await async.eachLimit( - levelInfo.filter((x) => x.key), + keyObjects.filter((x) => x.key), 10, async (item) => await this.enrichOneKeyInfo(dbhan, item) ); From b16b02c3f188c4049871bc94b21a9852eb3a58c9 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Wed, 14 May 2025 10:32:42 +0200 Subject: [PATCH 034/129] tree loader --- packages/tools/src/dbKeysLoader.ts | 29 +++++++++------ packages/web/src/widgets/DbKeysSubTree.svelte | 14 ++++---- packages/web/src/widgets/DbKeysTree.svelte | 35 ++++++++++--------- .../web/src/widgets/DbKeysTreeNode.svelte | 18 +++++----- .../dbgate-plugin-redis/src/backend/driver.js | 2 ++ 5 files changed, 56 insertions(+), 42 deletions(-) diff --git a/packages/tools/src/dbKeysLoader.ts b/packages/tools/src/dbKeysLoader.ts index 76696a99d..4aa5c711b 100644 --- a/packages/tools/src/dbKeysLoader.ts +++ b/packages/tools/src/dbKeysLoader.ts @@ -1,4 +1,5 @@ import _omit from 'lodash/omit'; +import _sortBy from 'lodash/sortBy'; const SHOW_INCREMENT = 100; @@ -29,8 +30,10 @@ export interface DbKeysTreeModel { childrenByKey: { [key: string]: DbKeysNodeModel[] }; keyObjectsByKey: { [key: string]: DbKeysNodeModel }; scannedKeys: number; + loadCount: number; + dbsize: number; cursor: string; - loadedAll: false; + loadedAll: boolean; // refreshAll?: boolean; } @@ -46,9 +49,10 @@ export interface DbKeyLoadedModel { export interface DbKeysLoadResult { nextCursor: string; keys: DbKeyLoadedModel[]; + dbsize: number; } -export type DbKeysLoadFunction = (root: string, limit: number) => Promise; +// export type DbKeysLoadFunction = (root: string, limit: number) => Promise; export type DbKeysChangeModelFunction = (func: (model: DbKeysTreeModel) => DbKeysTreeModel) => void; @@ -140,13 +144,10 @@ export type DbKeysChangeModelFunction = (func: (model: DbKeysTreeModel) => DbKey // }; // } -export async function dbKeys_loadNext(tree: DbKeysTreeModel, loader: DbKeysLoadFunction): Promise { - const count = 2000; +export function dbKeys_mergeNextPage(tree: DbKeysTreeModel, nextPage: DbKeysLoadResult): DbKeysTreeModel { const keyObjectsByKey = { ...tree.keyObjectsByKey }; - const loaded = await loader(tree.cursor, count); - - for (const keyObj of loaded.keys) { + for (const keyObj of nextPage.keys) { const keyPath = keyObj.key.split(tree.treeKeySeparator); keyObjectsByKey[keyObj.key] = { ...keyObj, @@ -198,20 +199,26 @@ export async function dbKeys_loadNext(tree: DbKeysTreeModel, loader: DbKeysLoadF if (dirObj.key == '') { continue; } - + if (!childrenByKey[dirObj.parentKey]) { childrenByKey[dirObj.parentKey] = []; } childrenByKey[dirObj.parentKey].push(dirObj); } + for (const key in childrenByKey) { + childrenByKey[key] = _sortBy(childrenByKey[key], 'text'); + } + return { ...tree, - cursor: loaded.nextCursor, + cursor: nextPage.nextCursor, dirsByKey, childrenByKey, keyObjectsByKey, - scannedKeys: tree.scannedKeys + count, + scannedKeys: tree.scannedKeys + tree.loadCount, + loadedAll: nextPage.nextCursor == '0', + dbsize: nextPage.dbsize, }; } @@ -251,6 +258,8 @@ export function dbKeys_refreshAll(treeKeySeparator: string, tree?: DbKeysTreeMod '': root, }, scannedKeys: 0, + dbsize: 0, + loadCount: 2000, cursor: '0', root, loadedAll: false, diff --git a/packages/web/src/widgets/DbKeysSubTree.svelte b/packages/web/src/widgets/DbKeysSubTree.svelte index b8e026839..6a1f3959b 100644 --- a/packages/web/src/widgets/DbKeysSubTree.svelte +++ b/packages/web/src/widgets/DbKeysSubTree.svelte @@ -6,7 +6,7 @@ import DbKeysTreeNode from './DbKeysTreeNode.svelte'; import { dbKeys_markNodeExpanded, DbKeysChangeModelFunction, DbKeysTreeModel } from 'dbgate-tools'; - export let root; + export let key; export let connection; export let database; export let conid; @@ -19,34 +19,34 @@ export let parentRoots = []; - $: items = model.childrenByKey[root] ?? []; + $: items = model.childrenByKey[key] ?? []; {#each items as item} {/each} -{#if model.dirsByKey[root]?.shouldLoadNext} +{#if model.dirsByKey[key]?.shouldLoadNext} -{:else if model.dirsByKey[root]?.hasNext} +{:else if model.dirsByKey[key]?.hasNext} { - changeModel(model => dbKeys_markNodeExpanded(model, root, true)); + changeModel(model => dbKeys_markNodeExpanded(model, key, true)); }} /> {/if} diff --git a/packages/web/src/widgets/DbKeysTree.svelte b/packages/web/src/widgets/DbKeysTree.svelte index ed39398d1..3e25d2ea9 100644 --- a/packages/web/src/widgets/DbKeysTree.svelte +++ b/packages/web/src/widgets/DbKeysTree.svelte @@ -1,8 +1,8 @@ @@ -126,20 +125,32 @@ - + -
- {#if model} -
- Scanned {Math.min(model?.scannedKeys, model?.dbsize) ?? '???'}/{model?.dbsize ?? '???'} -
- {/if} - - Scan more - -
+{#if !model?.loadedAll} +
+ {#if model} +
+ {#if isLoading} + Loading... + {:else} + Scanned {Math.min(model?.scannedKeys, model?.dbsize) ?? '???'}/{model?.dbsize ?? '???'} + {/if} +
+ {/if} + {#if isLoading} +
+ +
+ {:else} + + Scan more + + {/if} +
+{/if} {#if differentFocusedDb} {/if} @@ -172,11 +183,11 @@ }; } if (data.key && clickAction == 'keyEnter') { - changeModel(model => dbKeys_markNodeExpanded(model, data.key, !model.dirsByKey[data.key]?.isExpanded)); + changeModel(model => dbKeys_markNodeExpanded(model, data.key, !model.dirsByKey[data.key]?.isExpanded), false); } }} handleExpansion={(data, value) => { - changeModel(model => dbKeys_markNodeExpanded(model, data.key, value)); + changeModel(model => dbKeys_markNodeExpanded(model, data.key, value), false); }} onScrollTop={() => { domContainer?.scrollTop(); diff --git a/packages/web/src/widgets/DbKeysTreeNode.svelte b/packages/web/src/widgets/DbKeysTreeNode.svelte index ed00b0f1e..162fbf820 100644 --- a/packages/web/src/widgets/DbKeysTreeNode.svelte +++ b/packages/web/src/widgets/DbKeysTreeNode.svelte @@ -1,7 +1,7 @@ -{#each items as item} +{#each items.slice(0, visibleCount) as item} {/each} -{#if model.dirsByKey[key]?.shouldLoadNext} - -{:else if model.dirsByKey[key]?.hasNext} +{#if model.childrenByKey[key]?.length > visibleCount} { - changeModel(model => dbKeys_markNodeExpanded(model, key, true), false); + changeModel(model => dbKeys_showNextItems(model, key), false); }} /> {/if} From 1b297fed9013f0e674602b0d2ebe867e3728c8cc Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Wed, 14 May 2025 13:00:08 +0200 Subject: [PATCH 037/129] fix sorting --- packages/tools/src/dbKeysLoader.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/tools/src/dbKeysLoader.ts b/packages/tools/src/dbKeysLoader.ts index 80c93c9c0..b7556db57 100644 --- a/packages/tools/src/dbKeysLoader.ts +++ b/packages/tools/src/dbKeysLoader.ts @@ -5,6 +5,7 @@ export const DB_KEYS_SHOW_INCREMENT = 100; export interface DbKeysNodeModelBase { text?: string; + sortKey: string; key: string; count?: number; level: number; @@ -163,6 +164,7 @@ export function dbKeys_mergeNextPage(tree: DbKeysTreeModel, nextPage: DbKeysLoad ...keyObj, level: keyPath.length, text: keyPath[keyPath.length - 1], + sortKey: keyPath[keyPath.length - 1], keyPath, parentKey: keyPath.slice(0, -1).join(tree.treeKeySeparator), }; @@ -190,6 +192,7 @@ export function dbKeys_mergeNextPage(tree: DbKeysTreeModel, nextPage: DbKeysLoad type: 'dir', key: newDirKey, text: `${newDirPath[newDirPath.length - 1]}${tree.treeKeySeparator}*`, + sortKey: newDirPath[newDirPath.length - 1], }; } @@ -218,7 +221,7 @@ export function dbKeys_mergeNextPage(tree: DbKeysTreeModel, nextPage: DbKeysLoad } for (const key in childrenByKey) { - childrenByKey[key] = _sortBy(childrenByKey[key], 'text'); + childrenByKey[key] = _sortBy(childrenByKey[key], 'sortKey'); } return { From 9d924f8d1c13c5d4b74dc97bf43a3bc99628d498 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Wed, 14 May 2025 13:27:19 +0200 Subject: [PATCH 038/129] optimalized loading redis keys info - using pipeline --- .../dbgate-plugin-redis/src/backend/driver.js | 57 ++++++++++++++++--- 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/plugins/dbgate-plugin-redis/src/backend/driver.js b/plugins/dbgate-plugin-redis/src/backend/driver.js index a24e0e272..ea8991c40 100644 --- a/plugins/dbgate-plugin-redis/src/backend/driver.js +++ b/plugins/dbgate-plugin-redis/src/backend/driver.js @@ -320,17 +320,56 @@ const driver = { } }, - async enrichOneKeyInfo(dbhan, item) { - item.type = await dbhan.client.type(item.key); - item.count = await this.getKeyCardinality(dbhan, item.key, item.type); - }, + // async enrichOneKeyInfo(dbhan, item) { + // item.type = await dbhan.client.type(item.key); + // item.count = await this.getKeyCardinality(dbhan, item.key, item.type); + // }, async enrichKeyInfo(dbhan, keyObjects) { - await async.eachLimit( - keyObjects.filter((x) => x.key), - 10, - async (item) => await this.enrichOneKeyInfo(dbhan, item) - ); + // 1. get type + const typePipeline = dbhan.client.pipeline(); + for (const item of keyObjects) { + typePipeline.type(item.key); + } + const resultType = await typePipeline.exec(); + for (let i = 0; i < resultType.length; i++) { + if (resultType[i][0] == null) { + keyObjects[i].type = resultType[i][1]; + } + } + + // 2. get cardinality + const cardinalityPipeline = dbhan.client.pipeline(); + for (const item of keyObjects) { + switch (item.type) { + case 'list': + cardinalityPipeline.llen(item.key); + case 'set': + cardinalityPipeline.scard(item.key); + case 'zset': + cardinalityPipeline.zcard(item.key); + case 'stream': + cardinalityPipeline.xlen(item.key); + case 'hash': + cardinalityPipeline.hlen(item.key); + } + } + const resultCardinality = await cardinalityPipeline.exec(); + let resIndex = 0; + for (const item of keyObjects) { + if ( + item.type == 'list' || + item.type == 'set' || + item.type == 'zset' || + item.type == 'stream' || + item.type == 'hash' + ) { + if (resultCardinality[resIndex][0] == null) { + item.count = resultCardinality[resIndex][1]; + resIndex++; + } + } + } }, async loadKeyInfo(dbhan, key) { From 5bbdb66eb21bf4676e1b37795c6facd3858488cb Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Wed, 14 May 2025 13:32:29 +0200 Subject: [PATCH 039/129] v6.4.2-premium-beta.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8f504aaac..470cea516 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "6.4.2-premium-beta.2", + "version": "6.4.2-premium-beta.3", "name": "dbgate-all", "workspaces": [ "packages/*", From 727523eb3fbd8e1a1768862e0cafb590706952b7 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Wed, 14 May 2025 13:41:22 +0200 Subject: [PATCH 040/129] ts fix --- packages/tools/src/dbKeysLoader.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/tools/src/dbKeysLoader.ts b/packages/tools/src/dbKeysLoader.ts index b7556db57..44ea41b4b 100644 --- a/packages/tools/src/dbKeysLoader.ts +++ b/packages/tools/src/dbKeysLoader.ts @@ -271,6 +271,7 @@ export function dbKeys_createNewModel(treeKeySeparator: string): DbKeysTreeModel keyPath: [], parentKey: '', key: '', + sortKey: '', }; return { treeKeySeparator, From 762547d0e9e8bc6b9ecd9783564f86062bdcd5b1 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Wed, 14 May 2025 13:53:26 +0200 Subject: [PATCH 041/129] v4.6.2-beta.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 470cea516..ae9c40d43 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "6.4.2-premium-beta.3", + "version": "4.6.2-beta.4", "name": "dbgate-all", "workspaces": [ "packages/*", From 2b5812155218b51289b694fe11d8830bc000c158 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Wed, 14 May 2025 14:53:20 +0200 Subject: [PATCH 042/129] v6.4.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ae9c40d43..273369105 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "4.6.2-beta.4", + "version": "6.4.2", "name": "dbgate-all", "workspaces": [ "packages/*", From f826b9eb6e83267f9d48aa640e45b3a28ad1ebfe Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Wed, 14 May 2025 14:59:49 +0200 Subject: [PATCH 043/129] 6.4.2 changelog --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e85824304..a24f8cefb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,17 @@ Builds: - linux - application for linux - win - application for Windows +## 6.4.2 + +- ADDED: Source label to docker container #1105 +- FIXED: DbGate restart needed to take effect after trigger is created/deleted on mariadb #1112 +- ADDED: View PostgreSQL query console output #1108 +- FIXED: Single quote generete MySql error #1107 +- ADDED: Ability to limit query result count #1098 +- CHANGED: Correct processing of bigint columns #1087 #1055 #583 +- CHANGED: Improved and optimalized algorithm of loading redis keys #1062, #1034 +- FIXED: Fixed loading Redis keys with :: in key name + ## 6.4.0 - ADDED: DuckDB support - ADDED: Data deployer (Premium) From b33198d1bfc61089ac2118a9c1defbd8a669f338 Mon Sep 17 00:00:00 2001 From: ProjectInfinity Date: Sun, 20 Apr 2025 03:06:39 +0200 Subject: [PATCH 044/129] feat: redesign CommandPalette --- .../web/src/commands/CommandPalette.svelte | 182 ++++++++++++------ 1 file changed, 124 insertions(+), 58 deletions(-) diff --git a/packages/web/src/commands/CommandPalette.svelte b/packages/web/src/commands/CommandPalette.svelte index 7b9fa7a8c..9e04dbe37 100644 --- a/packages/web/src/commands/CommandPalette.svelte +++ b/packages/web/src/commands/CommandPalette.svelte @@ -164,109 +164,175 @@ }} data-testid='CommandPalette_main' > -
-
{ - $visibleCommandPalette = 'menu'; - domInput.focus(); - }} - > - Commands +
{ + $visibleCommandPalette = null; + }} + /> +
+
+
{ + $visibleCommandPalette = 'menu'; + domInput.focus(); + }} + > + Commands +
+
{ + $visibleCommandPalette = 'database'; + domInput.focus(); + }} + > + Database +
-
{ - $visibleCommandPalette = 'database'; - domInput.focus(); - }} - > - Database -
-
-
- -
- {#each filteredItems as command, index} -
handleCommand(command)} - bind:this={domItems[index]} - > -
- {#if command.icon} - +
+ +
+ {#each filteredItems as command, index} +
handleCommand(command)} + bind:this={domItems[index]} + > +
+ {#if command.icon} + + {/if} + {@html command.text} +
+ {#if command.keyText} +
{formatKeyText(command.keyText)}
{/if} - {@html command.text}
- {#if command.keyText} -
{formatKeyText(command.keyText)}
- {/if} -
- {/each} + {/each} +
From 4dc2627da2768e3a00df746f028997f83f795ac8 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Thu, 15 May 2025 16:01:51 +0200 Subject: [PATCH 045/129] cloud login WIP --- packages/api/.env | 1 + packages/api/src/controllers/auth.js | 9 +++++ packages/api/src/utility/authProxy.js | 9 +++++ packages/api/src/utility/cloudIntf.js | 34 +++++++++++++++++++ packages/web/src/icons/FontIcon.svelte | 3 ++ .../web/src/widgets/WidgetIconPanel.svelte | 14 +++++++- 6 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 packages/api/src/utility/cloudIntf.js diff --git a/packages/api/.env b/packages/api/.env index dc654e025..790defd5d 100644 --- a/packages/api/.env +++ b/packages/api/.env @@ -1,5 +1,6 @@ DEVMODE=1 SHELL_SCRIPTING=1 +# LOCAL_DBGATE_IDENTITY=1 # CLOUD_UPGRADE_FILE=c:\test\upg\upgrade.zip diff --git a/packages/api/src/controllers/auth.js b/packages/api/src/controllers/auth.js index 15600f156..0d3b81a00 100644 --- a/packages/api/src/controllers/auth.js +++ b/packages/api/src/controllers/auth.js @@ -13,6 +13,7 @@ const { } = require('../auth/authProvider'); const storage = require('./storage'); const { decryptPasswordString } = require('../utility/crypting'); +const { createDbGateIdentitySession, getIdentitySigninUrl } = require('../utility/cloudIntf'); const logger = getLogger('auth'); @@ -135,5 +136,13 @@ module.exports = { return getAuthProviderById(amoid).redirect(params); }, + createCloudLoginSession_meta: true, + async createCloudLoginSession({ client }) { + const sid = await createDbGateIdentitySession(client); + return { + url: getIdentitySigninUrl(sid), + }; + }, + authMiddleware, }; diff --git a/packages/api/src/utility/authProxy.js b/packages/api/src/utility/authProxy.js index 0d998fec2..744536177 100644 --- a/packages/api/src/utility/authProxy.js +++ b/packages/api/src/utility/authProxy.js @@ -36,6 +36,14 @@ async function callRefactorSqlQueryApi(query, task, structure, dialect) { return null; } +function getExternalParamsWithLicense() { + return { + headers: { + 'Content-Type': 'application/json', + }, + }; +} + module.exports = { isAuthProxySupported, authProxyGetRedirectUrl, @@ -47,4 +55,5 @@ module.exports = { callTextToSqlApi, callCompleteOnCursorApi, callRefactorSqlQueryApi, + getExternalParamsWithLicense, }; diff --git a/packages/api/src/utility/cloudIntf.js b/packages/api/src/utility/cloudIntf.js new file mode 100644 index 000000000..3258167e3 --- /dev/null +++ b/packages/api/src/utility/cloudIntf.js @@ -0,0 +1,34 @@ +const axios = require('axios'); +const { getExternalParamsWithLicense } = require('./authProxy'); + +const DBGATE_IDENTITY_URL = process.env.LOCAL_DBGATE_IDENTITY + ? 'http://localhost:3001' + : process.env.DEVWEB || process.env.DEVMODE + ? 'https://identity.dbgate.udolni.net' + : 'https://identity.dbgate.io'; + +const DBGATE_CLOUD_URL = process.env.LOCAL_DBGATE_CLOUD + ? 'http://localhost:3109' + : process.env.DEVWEB || process.env.DEVMODE + ? 'https://cloud.dbgate.udolni.net' + : 'https://cloud.dbgate.io'; + +async function createDbGateIdentitySession(client) { + const resp = await axios.default.post( + `${DBGATE_IDENTITY_URL}/api/create-session`, + { + client, + }, + getExternalParamsWithLicense() + ); + return resp.data.sid; +} + +function getIdentitySigninUrl(sid) { + return `${DBGATE_IDENTITY_URL}/signin/${sid}`; +} + +module.exports = { + createDbGateIdentitySession, + getIdentitySigninUrl, +}; diff --git a/packages/web/src/icons/FontIcon.svelte b/packages/web/src/icons/FontIcon.svelte index 86bee43eb..1dc0f9e98 100644 --- a/packages/web/src/icons/FontIcon.svelte +++ b/packages/web/src/icons/FontIcon.svelte @@ -112,6 +112,9 @@ 'icon square': 'mdi mdi-square', 'icon data-deploy': 'mdi mdi-database-settings', + 'icon cloud-account': 'mdi mdi-account-remove-outline', + 'icon cloud-account-connected': 'mdi mdi-account-check-outline', + 'icon edit': 'mdi mdi-pencil', 'icon delete': 'mdi mdi-delete', 'icon arrow-up': 'mdi mdi-arrow-up', diff --git a/packages/web/src/widgets/WidgetIconPanel.svelte b/packages/web/src/widgets/WidgetIconPanel.svelte index dec384ed0..0245a5f28 100644 --- a/packages/web/src/widgets/WidgetIconPanel.svelte +++ b/packages/web/src/widgets/WidgetIconPanel.svelte @@ -13,6 +13,9 @@ import mainMenuDefinition from '../../../../app/src/mainMenuDefinition'; import hasPermission from '../utility/hasPermission'; import { isProApp } from '../utility/proTools'; + import { openWebLink } from '../utility/simpleTools'; + import { apiCall } from '../utility/api'; + import getElectron from '../utility/getElectron'; let domSettings; let domMainMenu; @@ -103,6 +106,11 @@ const items = mainMenuDefinition({ editMenu: false }); currentDropDownMenu.set({ left, top, items }); } + + async function handleOpenCloudLogin() { + const { url, sid } = await apiCall('auth/create-cloud-login-session', { client: getElectron() ? 'app' : 'web' }); + openWebLink(url); + }
@@ -129,7 +137,7 @@
 
-
{ @@ -138,6 +146,10 @@ data-testid="WidgetIconPanel_lockDb" > +
--> + +
+
From 22577c5f879aa8d0f176dd018d70458fd26482fa Mon Sep 17 00:00:00 2001 From: CI workflows Date: Thu, 15 May 2025 14:07:52 +0000 Subject: [PATCH 046/129] Update pro ref --- workflow-templates/includes.tpl.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflow-templates/includes.tpl.yaml b/workflow-templates/includes.tpl.yaml index e05960c2c..7d4d0e5eb 100644 --- a/workflow-templates/includes.tpl.yaml +++ b/workflow-templates/includes.tpl.yaml @@ -7,7 +7,7 @@ checkout-and-merge-pro: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: 55cf42d58b843c4f1ffd6ab9b808f5f971bc3c8b + ref: 5cc7d79f7e3f5f33cad605e16df7570f25f36978 - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro From 55640470013f19d88280d60a7f3a56b4603b3350 Mon Sep 17 00:00:00 2001 From: CI workflows Date: Thu, 15 May 2025 14:08:12 +0000 Subject: [PATCH 047/129] chore: auto-update github workflows --- .github/workflows/build-app-pro-beta.yaml | 2 +- .github/workflows/build-app-pro.yaml | 2 +- .github/workflows/build-cloud-pro.yaml | 2 +- .github/workflows/build-docker-pro.yaml | 2 +- .github/workflows/build-npm-pro.yaml | 2 +- .github/workflows/e2e-pro.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-app-pro-beta.yaml b/.github/workflows/build-app-pro-beta.yaml index 219b97675..8f7899bd1 100644 --- a/.github/workflows/build-app-pro-beta.yaml +++ b/.github/workflows/build-app-pro-beta.yaml @@ -39,7 +39,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: 55cf42d58b843c4f1ffd6ab9b808f5f971bc3c8b + ref: 5cc7d79f7e3f5f33cad605e16df7570f25f36978 - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/build-app-pro.yaml b/.github/workflows/build-app-pro.yaml index dfeac852d..d684d3642 100644 --- a/.github/workflows/build-app-pro.yaml +++ b/.github/workflows/build-app-pro.yaml @@ -39,7 +39,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: 55cf42d58b843c4f1ffd6ab9b808f5f971bc3c8b + ref: 5cc7d79f7e3f5f33cad605e16df7570f25f36978 - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/build-cloud-pro.yaml b/.github/workflows/build-cloud-pro.yaml index 56f37a0f1..c989edbbe 100644 --- a/.github/workflows/build-cloud-pro.yaml +++ b/.github/workflows/build-cloud-pro.yaml @@ -39,7 +39,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: 55cf42d58b843c4f1ffd6ab9b808f5f971bc3c8b + ref: 5cc7d79f7e3f5f33cad605e16df7570f25f36978 - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/build-docker-pro.yaml b/.github/workflows/build-docker-pro.yaml index 029dde3d4..46ec00e7d 100644 --- a/.github/workflows/build-docker-pro.yaml +++ b/.github/workflows/build-docker-pro.yaml @@ -44,7 +44,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: 55cf42d58b843c4f1ffd6ab9b808f5f971bc3c8b + ref: 5cc7d79f7e3f5f33cad605e16df7570f25f36978 - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/build-npm-pro.yaml b/.github/workflows/build-npm-pro.yaml index c6c83f7f3..116de2991 100644 --- a/.github/workflows/build-npm-pro.yaml +++ b/.github/workflows/build-npm-pro.yaml @@ -32,7 +32,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: 55cf42d58b843c4f1ffd6ab9b808f5f971bc3c8b + ref: 5cc7d79f7e3f5f33cad605e16df7570f25f36978 - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/e2e-pro.yaml b/.github/workflows/e2e-pro.yaml index 3b3d87b7e..3c5594553 100644 --- a/.github/workflows/e2e-pro.yaml +++ b/.github/workflows/e2e-pro.yaml @@ -26,7 +26,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: 55cf42d58b843c4f1ffd6ab9b808f5f971bc3c8b + ref: 5cc7d79f7e3f5f33cad605e16df7570f25f36978 - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro From e8cb87ae3d3f518a8eef12ed0cf19812e0ac91b9 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Fri, 16 May 2025 08:00:33 +0200 Subject: [PATCH 048/129] feedback link --- packages/web/src/widgets/AdminPremiumPromoWidget.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/widgets/AdminPremiumPromoWidget.svelte b/packages/web/src/widgets/AdminPremiumPromoWidget.svelte index c47cbdb62..ee06418c0 100644 --- a/packages/web/src/widgets/AdminPremiumPromoWidget.svelte +++ b/packages/web/src/widgets/AdminPremiumPromoWidget.svelte @@ -45,7 +45,7 @@

Your feedback is very valuable for us. It helps us to improve DbGate and make it more useful for you.

- openWebLink('https://dbgate.org/survey')} value="Fill out the survey" skipWidth /> + openWebLink('https://dbgate.org/feedback')} value="Give us feedback" skipWidth />
From 4a3491e0b518161008a5f73d838f7bb4b0ed824d Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Fri, 16 May 2025 08:03:08 +0200 Subject: [PATCH 049/129] feedback menu link --- app/src/mainMenuDefinition.js | 1 + packages/web/src/commands/stdCommands.ts | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/app/src/mainMenuDefinition.js b/app/src/mainMenuDefinition.js index 6d0ab52a8..c463af4bf 100644 --- a/app/src/mainMenuDefinition.js +++ b/app/src/mainMenuDefinition.js @@ -108,6 +108,7 @@ module.exports = ({ editMenu, isMac }) => [ { command: 'app.openWeb', hideDisabled: true }, { command: 'app.openIssue', hideDisabled: true }, { command: 'app.openSponsoring', hideDisabled: true }, + { command: 'app.giveFeedback', hideDisabled: true }, { divider: true }, { command: 'settings.commands', hideDisabled: true }, { command: 'tabs.changelog', hideDisabled: true }, diff --git a/packages/web/src/commands/stdCommands.ts b/packages/web/src/commands/stdCommands.ts index 85c97b632..44be01135 100644 --- a/packages/web/src/commands/stdCommands.ts +++ b/packages/web/src/commands/stdCommands.ts @@ -929,9 +929,17 @@ registerCommand({ id: 'app.openSponsoring', category: 'Application', name: 'Become sponsor', + testEnabled: () => !isProApp(), onClick: () => openWebLink('https://opencollective.com/dbgate'), }); +registerCommand({ + id: 'app.giveFeedback', + category: 'Application', + name: 'Give us feedback', + onClick: () => openWebLink('https://dbgate.org/feedback'), +}); + registerCommand({ id: 'app.zoomIn', category: 'Application', From 5590aa7234dfd7aeaa1b80e553a13937448dc55e Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Fri, 16 May 2025 08:04:44 +0200 Subject: [PATCH 050/129] feedback URL --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a08b55a81..44298c6d4 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ DbGate is licensed under GPL-3.0 license and is free to use for any purpose. * Run web version as [NPM package](https://www.npmjs.com/package/dbgate-serve) or as [docker image](https://hub.docker.com/r/dbgate/dbgate) * Use nodeJs [scripting interface](https://docs.dbgate.io/scripting) ([API documentation](https://docs.dbgate.io/apidoc)) * [Recommend DbGate](https://testimonial.to/dbgate) | [Rate on G2](https://www.g2.com/products/dbgate/reviews) +* [Give us feedback](https://dbgate.org/feedback) ## Supported databases * MySQL From c71c32b363ee58447a25fd51283436000a2a6294 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Fri, 16 May 2025 08:10:20 +0200 Subject: [PATCH 051/129] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 44298c6d4..5c12c1e35 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ DbGate is licensed under GPL-3.0 license and is free to use for any purpose. * Run web version as [NPM package](https://www.npmjs.com/package/dbgate-serve) or as [docker image](https://hub.docker.com/r/dbgate/dbgate) * Use nodeJs [scripting interface](https://docs.dbgate.io/scripting) ([API documentation](https://docs.dbgate.io/apidoc)) * [Recommend DbGate](https://testimonial.to/dbgate) | [Rate on G2](https://www.g2.com/products/dbgate/reviews) -* [Give us feedback](https://dbgate.org/feedback) +* [Give us feedback](https://dbgate.org/feedback) - it will help us to decide, how to improve DbGate in future ## Supported databases * MySQL From 9329345d98c0259cf7a4b67ba51a2d13249e39fc Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Fri, 16 May 2025 12:19:26 +0200 Subject: [PATCH 052/129] basic cloud signin workflow --- packages/api/src/controllers/auth.js | 12 +++-- packages/api/src/utility/cloudIntf.js | 36 ++++++++++++-- packages/web/src/App.svelte | 3 +- packages/web/src/commands/stdCommands.ts | 10 ++++ packages/web/src/icons/FontIcon.svelte | 1 + packages/web/src/stores.ts | 2 + packages/web/src/utility/api.ts | 7 +++ .../web/src/widgets/WidgetIconPanel.svelte | 48 ++++++++++++++++--- 8 files changed, 101 insertions(+), 18 deletions(-) diff --git a/packages/api/src/controllers/auth.js b/packages/api/src/controllers/auth.js index 0d3b81a00..fbaa74f3d 100644 --- a/packages/api/src/controllers/auth.js +++ b/packages/api/src/controllers/auth.js @@ -13,7 +13,8 @@ const { } = require('../auth/authProvider'); const storage = require('./storage'); const { decryptPasswordString } = require('../utility/crypting'); -const { createDbGateIdentitySession, getIdentitySigninUrl } = require('../utility/cloudIntf'); +const { createDbGateIdentitySession, startCloudTokenChecking } = require('../utility/cloudIntf'); +const socket = require('../utility/socket'); const logger = getLogger('auth'); @@ -138,10 +139,11 @@ module.exports = { createCloudLoginSession_meta: true, async createCloudLoginSession({ client }) { - const sid = await createDbGateIdentitySession(client); - return { - url: getIdentitySigninUrl(sid), - }; + const res = await createDbGateIdentitySession(client); + startCloudTokenChecking(res.sid, token => { + socket.emit('got-cloud-token', { token }); + }); + return res; }, authMiddleware, diff --git a/packages/api/src/utility/cloudIntf.js b/packages/api/src/utility/cloudIntf.js index 3258167e3..ee2b8301f 100644 --- a/packages/api/src/utility/cloudIntf.js +++ b/packages/api/src/utility/cloudIntf.js @@ -1,8 +1,11 @@ const axios = require('axios'); const { getExternalParamsWithLicense } = require('./authProxy'); +const { getLogger, extractErrorLogData } = require('dbgate-tools'); + +const logger = getLogger('cloudIntf'); const DBGATE_IDENTITY_URL = process.env.LOCAL_DBGATE_IDENTITY - ? 'http://localhost:3001' + ? 'http://localhost:3103' : process.env.DEVWEB || process.env.DEVMODE ? 'https://identity.dbgate.udolni.net' : 'https://identity.dbgate.io'; @@ -21,14 +24,37 @@ async function createDbGateIdentitySession(client) { }, getExternalParamsWithLicense() ); - return resp.data.sid; + return { + sid: resp.data.sid, + url: `${DBGATE_IDENTITY_URL}/api/signin/${resp.data.sid}`, + }; } -function getIdentitySigninUrl(sid) { - return `${DBGATE_IDENTITY_URL}/signin/${sid}`; +function startCloudTokenChecking(sid, callback) { + const started = Date.now(); + const interval = setInterval(async () => { + if (Date.now() - started > 60 * 1000) { + clearInterval(interval); + return; + } + + try { + const resp = await axios.default.get( + `${DBGATE_IDENTITY_URL}/api/get-token/${sid}`, + getExternalParamsWithLicense() + ); + + if (resp.data.status == 'ok') { + clearInterval(interval); + callback(resp.data.token); + } + } catch (err) { + logger.error(extractErrorLogData(err), 'Error checking cloud token'); + } + }, 500); } module.exports = { createDbGateIdentitySession, - getIdentitySigninUrl, + startCloudTokenChecking, }; diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 487880df7..a0787efa0 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -14,7 +14,7 @@ // import { shouldWaitForElectronInitialize } from './utility/getElectron'; import { subscribeConnectionPingers } from './utility/connectionsPinger'; import { subscribePermissionCompiler } from './utility/hasPermission'; - import { apiCall, installNewVolatileConnectionListener } from './utility/api'; + import { apiCall, installNewCloudTokenListener, installNewVolatileConnectionListener } from './utility/api'; import { getConfig, getSettings, getUsedApps } from './utility/metadataLoaders'; import AppTitleProvider from './utility/AppTitleProvider.svelte'; import getElectron from './utility/getElectron'; @@ -51,6 +51,7 @@ subscribeConnectionPingers(); subscribePermissionCompiler(); installNewVolatileConnectionListener(); + installNewCloudTokenListener(); initializeAppUpdates(); } diff --git a/packages/web/src/commands/stdCommands.ts b/packages/web/src/commands/stdCommands.ts index 85c97b632..8b3418422 100644 --- a/packages/web/src/commands/stdCommands.ts +++ b/packages/web/src/commands/stdCommands.ts @@ -1,4 +1,5 @@ import { + cloudSigninToken, currentDatabase, currentTheme, emptyConnectionGroupNames, @@ -662,6 +663,15 @@ if (hasPermission('settings/change')) { }); } +registerCommand({ + id: 'cloud.logout', + category: 'Cloud', + name: 'Logout', + onClick: () => { + cloudSigninToken.set(null); + }, +}); + registerCommand({ id: 'file.exit', category: 'File', diff --git a/packages/web/src/icons/FontIcon.svelte b/packages/web/src/icons/FontIcon.svelte index 1dc0f9e98..22dc64d7b 100644 --- a/packages/web/src/icons/FontIcon.svelte +++ b/packages/web/src/icons/FontIcon.svelte @@ -39,6 +39,7 @@ 'icon minus-thick': 'mdi mdi-minus-thick', 'icon invisible-box': 'mdi mdi-minus-box-outline icon-invisible', 'icon cloud-upload': 'mdi mdi-cloud-upload', + 'icon cloud': 'mdi mdi-cloud', 'icon import': 'mdi mdi-application-import', 'icon export': 'mdi mdi-application-export', 'icon new-connection': 'mdi mdi-database-plus', diff --git a/packages/web/src/stores.ts b/packages/web/src/stores.ts index dac8c4f02..c79e0de7f 100644 --- a/packages/web/src/stores.ts +++ b/packages/web/src/stores.ts @@ -182,6 +182,8 @@ export const focusedConnectionOrDatabase = writable<{ conid: string; database?: export const focusedTreeDbKey = writable<{ key: string; root: string; type: string; text: string }>(null); +export const cloudSigninToken = writableWithStorage(null, 'cloudSigninToken'); + export const DEFAULT_OBJECT_SEARCH_SETTINGS = { pureName: true, schemaName: false, diff --git a/packages/web/src/utility/api.ts b/packages/web/src/utility/api.ts index bfe827cfe..0314cb047 100644 --- a/packages/web/src/utility/api.ts +++ b/packages/web/src/utility/api.ts @@ -14,6 +14,7 @@ import { batchDispatchCacheTriggers, dispatchCacheChange } from './cache'; import { isAdminPage, isOneOfPage } from './pageDefs'; import { openWebLink } from './simpleTools'; import { serializeJsTypesReplacer } from 'dbgate-tools'; +import { cloudSigninToken } from '../stores'; export const strmid = uuidv1(); @@ -279,6 +280,12 @@ export function installNewVolatileConnectionListener() { }); } +export function installNewCloudTokenListener() { + apiOn('got-cloud-token', async ({ token }) => { + cloudSigninToken.set(token); + }); +} + export function getAuthCategory(config) { if (config.isBasicAuth) { return 'basic'; diff --git a/packages/web/src/widgets/WidgetIconPanel.svelte b/packages/web/src/widgets/WidgetIconPanel.svelte index 0245a5f28..778f5eb8f 100644 --- a/packages/web/src/widgets/WidgetIconPanel.svelte +++ b/packages/web/src/widgets/WidgetIconPanel.svelte @@ -9,6 +9,7 @@ visibleHamburgerMenuWidget, lockedDatabaseMode, getCurrentConfig, + cloudSigninToken, } from '../stores'; import mainMenuDefinition from '../../../../app/src/mainMenuDefinition'; import hasPermission from '../utility/hasPermission'; @@ -18,6 +19,7 @@ import getElectron from '../utility/getElectron'; let domSettings; + let domCloudAccount; let domMainMenu; const widgets = [ @@ -61,9 +63,10 @@ title: 'Selected cell data detail view', }, { - icon: 'icon app', - name: 'app', - title: 'Application layers', + icon: 'icon cloud', + name: 'cloud', + title: 'DbGate Cloud', + isCloud: true, }, { icon: 'icon premium', @@ -95,7 +98,26 @@ const rect = domSettings.getBoundingClientRect(); const left = rect.right; const top = rect.bottom; - const items = [{ command: 'settings.show' }, { command: 'theme.changeTheme' }, { command: 'settings.commands' }]; + const items = [ + { command: 'settings.show' }, + { command: 'theme.changeTheme' }, + { command: 'settings.commands' }, + { + text: 'View applications', + onClick: () => { + $selectedWidget = 'app'; + $visibleWidgetSideBar = true; + }, + }, + ]; + currentDropDownMenu.set({ left, top, items }); + } + + function handleCloudAccountMenu() { + const rect = domCloudAccount.getBoundingClientRect(); + const left = rect.right; + const top = rect.bottom; + const items = [{ command: 'cloud.logout' }]; currentDropDownMenu.set({ left, top, items }); } @@ -121,6 +143,7 @@ {/if} {#each widgets .filter(x => x && hasPermission(`widgets/${x.name}`)) + .filter(x => !x.isCloud || $cloudSigninToken) .filter(x => !x.isPremiumPromo || !isProApp()) as item}
--> -
- -
+ {#if $cloudSigninToken} +
+ +
+ {:else} +
+ +
+ {/if}
From a50f223fe3785b5b56741e850c72b66e9a558e9a Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Fri, 16 May 2025 13:54:08 +0200 Subject: [PATCH 053/129] cloud icons WIP --- packages/web/src/utility/simpleTools.ts | 33 +++++++++++++++++-- .../web/src/widgets/WidgetIconPanel.svelte | 2 +- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/packages/web/src/utility/simpleTools.ts b/packages/web/src/utility/simpleTools.ts index f655445ab..77164e211 100644 --- a/packages/web/src/utility/simpleTools.ts +++ b/packages/web/src/utility/simpleTools.ts @@ -1,11 +1,40 @@ import getElectron from './getElectron'; -export function openWebLink(href) { +export function openWebLink(href, usePopup = false) { const electron = getElectron(); if (electron) { electron.send('open-link', href); } else { - window.open(href, '_blank'); + if (usePopup) { + const w = 500; + const h = 650; + + const dualScreenLeft = window.screenLeft ?? window.screenX; // X of parent + const dualScreenTop = window.screenTop ?? window.screenY; // Y of parent + + // 2. How big is the parent window? + const parentWidth = window.outerWidth; + const parentHeight = window.outerHeight; + + // 3. Centre the popup inside that rectangle + const left = dualScreenLeft + (parentWidth - w) / 2; + const top = dualScreenTop + (parentHeight - h) / 2; + + const features = [ + `width=${w}`, + `height=${h}`, + `left=${left}`, + `top=${top}`, + 'scrollbars=yes', + 'resizable=yes', + 'noopener', + 'noreferrer', + ]; + + window.open(href, 'dbgateCloudLoginPopup', features.join(',')); + } else { + window.open(href, '_blank'); + } } } diff --git a/packages/web/src/widgets/WidgetIconPanel.svelte b/packages/web/src/widgets/WidgetIconPanel.svelte index 778f5eb8f..16a7aaaf9 100644 --- a/packages/web/src/widgets/WidgetIconPanel.svelte +++ b/packages/web/src/widgets/WidgetIconPanel.svelte @@ -131,7 +131,7 @@ async function handleOpenCloudLogin() { const { url, sid } = await apiCall('auth/create-cloud-login-session', { client: getElectron() ? 'app' : 'web' }); - openWebLink(url); + openWebLink(url, true); } From 23150815a09bde6f7464ed0cab36af7d4bc10dc6 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Fri, 16 May 2025 13:55:14 +0200 Subject: [PATCH 054/129] use default target schema in dbDeploy --- packages/api/src/shell/generateDeploySql.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/api/src/shell/generateDeploySql.js b/packages/api/src/shell/generateDeploySql.js index dad7a9e0d..b3e9e4210 100644 --- a/packages/api/src/shell/generateDeploySql.js +++ b/packages/api/src/shell/generateDeploySql.js @@ -52,7 +52,10 @@ async function generateDeploySql({ dbdiffOptionsExtra?.['schemaMode'] !== 'ignore' && dbdiffOptionsExtra?.['schemaMode'] !== 'ignoreImplicit' ) { - throw new Error('targetSchema is required for databases with multiple schemas'); + if (!driver?.dialect?.defaultSchemaName) { + throw new Error('targetSchema is required for databases with multiple schemas'); + } + targetSchema = driver.dialect.defaultSchemaName; } try { From 05e8f6ed78336ca6243263cf1bef2ab706c62dba Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Fri, 16 May 2025 13:55:45 +0200 Subject: [PATCH 055/129] v6.4.3-alpha.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 273369105..8bfdbd903 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "6.4.2", + "version": "6.4.3-alpha.1", "name": "dbgate-all", "workspaces": [ "packages/*", From f3ff910821e1ea162afba8b8752fa32d9696a237 Mon Sep 17 00:00:00 2001 From: CI workflows Date: Mon, 19 May 2025 08:11:00 +0000 Subject: [PATCH 056/129] Update pro ref --- workflow-templates/includes.tpl.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflow-templates/includes.tpl.yaml b/workflow-templates/includes.tpl.yaml index 7d4d0e5eb..fa29cdefb 100644 --- a/workflow-templates/includes.tpl.yaml +++ b/workflow-templates/includes.tpl.yaml @@ -7,7 +7,7 @@ checkout-and-merge-pro: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: 5cc7d79f7e3f5f33cad605e16df7570f25f36978 + ref: 25b7aff6b07b5dee69cf1b9932364b9470d7967e - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro From a9ab864cbb69acabea4463fb73e075bba758e6c7 Mon Sep 17 00:00:00 2001 From: CI workflows Date: Mon, 19 May 2025 08:11:23 +0000 Subject: [PATCH 057/129] chore: auto-update github workflows --- .github/workflows/build-app-pro-beta.yaml | 2 +- .github/workflows/build-app-pro.yaml | 2 +- .github/workflows/build-cloud-pro.yaml | 2 +- .github/workflows/build-docker-pro.yaml | 2 +- .github/workflows/build-npm-pro.yaml | 2 +- .github/workflows/e2e-pro.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-app-pro-beta.yaml b/.github/workflows/build-app-pro-beta.yaml index 8f7899bd1..80e826a32 100644 --- a/.github/workflows/build-app-pro-beta.yaml +++ b/.github/workflows/build-app-pro-beta.yaml @@ -39,7 +39,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: 5cc7d79f7e3f5f33cad605e16df7570f25f36978 + ref: 25b7aff6b07b5dee69cf1b9932364b9470d7967e - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/build-app-pro.yaml b/.github/workflows/build-app-pro.yaml index d684d3642..5169293f9 100644 --- a/.github/workflows/build-app-pro.yaml +++ b/.github/workflows/build-app-pro.yaml @@ -39,7 +39,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: 5cc7d79f7e3f5f33cad605e16df7570f25f36978 + ref: 25b7aff6b07b5dee69cf1b9932364b9470d7967e - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/build-cloud-pro.yaml b/.github/workflows/build-cloud-pro.yaml index c989edbbe..70f26a0a8 100644 --- a/.github/workflows/build-cloud-pro.yaml +++ b/.github/workflows/build-cloud-pro.yaml @@ -39,7 +39,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: 5cc7d79f7e3f5f33cad605e16df7570f25f36978 + ref: 25b7aff6b07b5dee69cf1b9932364b9470d7967e - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/build-docker-pro.yaml b/.github/workflows/build-docker-pro.yaml index 46ec00e7d..6f1e5e139 100644 --- a/.github/workflows/build-docker-pro.yaml +++ b/.github/workflows/build-docker-pro.yaml @@ -44,7 +44,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: 5cc7d79f7e3f5f33cad605e16df7570f25f36978 + ref: 25b7aff6b07b5dee69cf1b9932364b9470d7967e - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/build-npm-pro.yaml b/.github/workflows/build-npm-pro.yaml index 116de2991..ee2c9fb97 100644 --- a/.github/workflows/build-npm-pro.yaml +++ b/.github/workflows/build-npm-pro.yaml @@ -32,7 +32,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: 5cc7d79f7e3f5f33cad605e16df7570f25f36978 + ref: 25b7aff6b07b5dee69cf1b9932364b9470d7967e - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/e2e-pro.yaml b/.github/workflows/e2e-pro.yaml index 3c5594553..aa56a9ec7 100644 --- a/.github/workflows/e2e-pro.yaml +++ b/.github/workflows/e2e-pro.yaml @@ -26,7 +26,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: 5cc7d79f7e3f5f33cad605e16df7570f25f36978 + ref: 25b7aff6b07b5dee69cf1b9932364b9470d7967e - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro From 281de5196e771951f192dcea127c69aa2dff3641 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Mon, 19 May 2025 10:39:35 +0200 Subject: [PATCH 058/129] update cloud files --- packages/api/src/controllers/connections.js | 13 +++ packages/api/src/controllers/storage.js | 4 + packages/api/src/main.js | 3 + packages/api/src/utility/cloudIntf.js | 103 +++++++++++++++++++- 4 files changed, 122 insertions(+), 1 deletion(-) diff --git a/packages/api/src/controllers/connections.js b/packages/api/src/controllers/connections.js index 13a452a64..e221e8411 100644 --- a/packages/api/src/controllers/connections.js +++ b/packages/api/src/controllers/connections.js @@ -239,6 +239,19 @@ module.exports = { return (await this.datastore.find()).filter(x => connectionHasPermission(x, req)); }, + async getUsedEngines() { + const storage = require('./storage'); + + const storageEngines = await storage.getUsedEngines(); + if (storageEngines) { + return storageEngines; + } + if (portalConnections) { + return _.uniq(_.compact(portalConnections.map(x => x.engine))); + } + return _.uniq((await this.datastore.find()).map(x => x.engine)); + }, + test_meta: true, test({ connection, requestDbList = false }) { const subprocess = fork( diff --git a/packages/api/src/controllers/storage.js b/packages/api/src/controllers/storage.js index 6d498f869..f7066eb22 100644 --- a/packages/api/src/controllers/storage.js +++ b/packages/api/src/controllers/storage.js @@ -32,4 +32,8 @@ module.exports = { }, startRefreshLicense() {}, + + async getUsedEngines() { + return null; + }, }; diff --git a/packages/api/src/main.js b/packages/api/src/main.js index ac0c33ef5..3304d6a08 100644 --- a/packages/api/src/main.js +++ b/packages/api/src/main.js @@ -39,6 +39,7 @@ const { getDefaultAuthProvider } = require('./auth/authProvider'); const startCloudUpgradeTimer = require('./utility/cloudUpgrade'); const { isProApp } = require('./utility/checkLicense'); const { getHealthStatus, getHealthStatusSprinx } = require('./utility/healthStatus'); +const { startCloudFiles } = require('./utility/cloudIntf'); const logger = getLogger('main'); @@ -200,6 +201,8 @@ function start() { if (process.env.CLOUD_UPGRADE_FILE) { startCloudUpgradeTimer(); } + + startCloudFiles(); } function useAllControllers(app, electron) { diff --git a/packages/api/src/utility/cloudIntf.js b/packages/api/src/utility/cloudIntf.js index ee2b8301f..9d56f8edf 100644 --- a/packages/api/src/utility/cloudIntf.js +++ b/packages/api/src/utility/cloudIntf.js @@ -1,8 +1,16 @@ const axios = require('axios'); +const fs = require('fs-extra'); +const _ = require('lodash'); +const path = require('path'); const { getExternalParamsWithLicense } = require('./authProxy'); -const { getLogger, extractErrorLogData } = require('dbgate-tools'); +const { getLogger, extractErrorLogData, jsonLinesParse } = require('dbgate-tools'); +const { datadir } = require('./directories'); +const platformInfo = require('./platformInfo'); +const connections = require('../controllers/connections'); +const { isProApp } = require('./checkLicense'); const logger = getLogger('cloudIntf'); +let cloudFiles = null; const DBGATE_IDENTITY_URL = process.env.LOCAL_DBGATE_IDENTITY ? 'http://localhost:3103' @@ -54,7 +62,100 @@ function startCloudTokenChecking(sid, callback) { }, 500); } +async function loadCloudFiles() { + try { + const fileContent = await fs.readFile(path.join(datadir(), 'cloud-files.jsonl'), 'utf-8'); + const parsedJson = jsonLinesParse(fileContent); + cloudFiles = parsedJson; + } catch (err) { + cloudFiles = []; + } +} + +async function collectCloudFilesSearchTags() { + const res = []; + if (platformInfo.isElectron) { + res.push('app'); + } else { + res.push('web'); + } + if (platformInfo.isWindows) { + res.push('windows'); + } + if (platformInfo.isMac) { + res.push('mac'); + } + if (platformInfo.isLinux) { + res.push('linux'); + } + if (platformInfo.isAwsUbuntuLayout) { + res.push('aws'); + } + if (platformInfo.isAzureUbuntuLayout) { + res.push('azure'); + } + if (platformInfo.isSnap) { + res.push('snap'); + } + const engines = await connections.getUsedEngines(); + const engineTags = engines.map(engine => engine.split('@')[0]); + res.push(...engineTags); + + // team-premium and trials will return the same cloud files as premium - no need to check + res.push(isProApp() ? 'premium' : 'community'); + + return res; +} + +async function updateCloudFiles() { + let lastCloudFilesTags; + try { + const fileContent = await fs.readFile(path.join(datadir(), 'cloud-files-tags.json'), 'utf-8'); + cloudFiles = JSON.parse(fileContent); + } catch (err) { + lastCloudFilesTags = []; + } + + let lastCheckedTm = 0; + if (_.isEqual(cloudFiles, lastCloudFilesTags) && cloudFiles.length > 0) { + lastCheckedTm = _.max(cloudFiles.map(x => x.modifiedTm)); + } + const tags = await collectCloudFilesSearchTags(); + + const resp = await axios.default.post( + `${DBGATE_CLOUD_URL}/public-cloud-updates`, + { + lastCheckedTm, + tags, + }, + getExternalParamsWithLicense() + ); + + const filesByPath = _.keyBy(cloudFiles, 'path'); + for(const file of resp.data) { + filesByPath[file.path] = file; + } + + cloudFiles = Object.values(filesByPath); + + await fs.writeFile( + path.join(datadir(), 'cloud-files.jsonl'), + cloudFiles.map(x => JSON.stringify(x)).join('\n') + ); + + await fs.writeFile( + path.join(datadir(), 'cloud-files-tags.json'), + JSON.stringify(tags) + ); +} + +async function startCloudFiles() { + await loadCloudFiles(); + await updateCloudFiles(); +} + module.exports = { createDbGateIdentitySession, startCloudTokenChecking, + startCloudFiles, }; From 41ebd39810fcef673dbd4e5cf4b5f25c80ffc81b Mon Sep 17 00:00:00 2001 From: CI workflows Date: Mon, 19 May 2025 13:24:06 +0000 Subject: [PATCH 059/129] Update pro ref --- workflow-templates/includes.tpl.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflow-templates/includes.tpl.yaml b/workflow-templates/includes.tpl.yaml index fa29cdefb..774773256 100644 --- a/workflow-templates/includes.tpl.yaml +++ b/workflow-templates/includes.tpl.yaml @@ -7,7 +7,7 @@ checkout-and-merge-pro: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: 25b7aff6b07b5dee69cf1b9932364b9470d7967e + ref: 7edf19fb4f980e31ef440838b311d5ddcc13c2b6 - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro From 903a26a330b9abf978a6053282f208f6a79f1bad Mon Sep 17 00:00:00 2001 From: CI workflows Date: Mon, 19 May 2025 13:24:23 +0000 Subject: [PATCH 060/129] chore: auto-update github workflows --- .github/workflows/build-app-pro-beta.yaml | 2 +- .github/workflows/build-app-pro.yaml | 2 +- .github/workflows/build-cloud-pro.yaml | 2 +- .github/workflows/build-docker-pro.yaml | 2 +- .github/workflows/build-npm-pro.yaml | 2 +- .github/workflows/e2e-pro.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-app-pro-beta.yaml b/.github/workflows/build-app-pro-beta.yaml index 80e826a32..2a75a989c 100644 --- a/.github/workflows/build-app-pro-beta.yaml +++ b/.github/workflows/build-app-pro-beta.yaml @@ -39,7 +39,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: 25b7aff6b07b5dee69cf1b9932364b9470d7967e + ref: 7edf19fb4f980e31ef440838b311d5ddcc13c2b6 - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/build-app-pro.yaml b/.github/workflows/build-app-pro.yaml index 5169293f9..15b387549 100644 --- a/.github/workflows/build-app-pro.yaml +++ b/.github/workflows/build-app-pro.yaml @@ -39,7 +39,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: 25b7aff6b07b5dee69cf1b9932364b9470d7967e + ref: 7edf19fb4f980e31ef440838b311d5ddcc13c2b6 - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/build-cloud-pro.yaml b/.github/workflows/build-cloud-pro.yaml index 70f26a0a8..d8737976e 100644 --- a/.github/workflows/build-cloud-pro.yaml +++ b/.github/workflows/build-cloud-pro.yaml @@ -39,7 +39,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: 25b7aff6b07b5dee69cf1b9932364b9470d7967e + ref: 7edf19fb4f980e31ef440838b311d5ddcc13c2b6 - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/build-docker-pro.yaml b/.github/workflows/build-docker-pro.yaml index 6f1e5e139..ffb1f02bc 100644 --- a/.github/workflows/build-docker-pro.yaml +++ b/.github/workflows/build-docker-pro.yaml @@ -44,7 +44,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: 25b7aff6b07b5dee69cf1b9932364b9470d7967e + ref: 7edf19fb4f980e31ef440838b311d5ddcc13c2b6 - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/build-npm-pro.yaml b/.github/workflows/build-npm-pro.yaml index ee2c9fb97..b6e67a61c 100644 --- a/.github/workflows/build-npm-pro.yaml +++ b/.github/workflows/build-npm-pro.yaml @@ -32,7 +32,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: 25b7aff6b07b5dee69cf1b9932364b9470d7967e + ref: 7edf19fb4f980e31ef440838b311d5ddcc13c2b6 - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/e2e-pro.yaml b/.github/workflows/e2e-pro.yaml index aa56a9ec7..8672ae4df 100644 --- a/.github/workflows/e2e-pro.yaml +++ b/.github/workflows/e2e-pro.yaml @@ -26,7 +26,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: 25b7aff6b07b5dee69cf1b9932364b9470d7967e + ref: 7edf19fb4f980e31ef440838b311d5ddcc13c2b6 - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro From 310f8bf6f7e8a4d96550615518159b2495043610 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Mon, 19 May 2025 16:33:04 +0200 Subject: [PATCH 061/129] public cloud widget --- packages/api/src/controllers/cloud.js | 15 +++++ packages/api/src/main.js | 2 + packages/api/src/utility/authProxy.js | 10 ++-- packages/api/src/utility/cloudIntf.js | 58 +++++++++++-------- .../web/src/appobj/CloudFileAppObject.svelte | 29 ++++++++++ packages/web/src/icons/FontIcon.svelte | 2 + packages/web/src/utility/metadataLoaders.ts | 13 +++++ .../web/src/widgets/CloudItemsWidget.svelte | 25 ++++++++ .../web/src/widgets/WidgetContainer.svelte | 4 ++ .../web/src/widgets/WidgetIconPanel.svelte | 6 +- 10 files changed, 133 insertions(+), 31 deletions(-) create mode 100644 packages/api/src/controllers/cloud.js create mode 100644 packages/web/src/appobj/CloudFileAppObject.svelte create mode 100644 packages/web/src/widgets/CloudItemsWidget.svelte diff --git a/packages/api/src/controllers/cloud.js b/packages/api/src/controllers/cloud.js new file mode 100644 index 000000000..3ecc8a3ae --- /dev/null +++ b/packages/api/src/controllers/cloud.js @@ -0,0 +1,15 @@ +const fs = require('fs-extra'); +const _ = require('lodash'); +const path = require('path'); +const { appdir } = require('../utility/directories'); +const socket = require('../utility/socket'); +const connections = require('./connections'); +const { getPublicCloudFiles } = require('../utility/cloudIntf'); + +module.exports = { + publicFiles_meta: true, + async publicFiles() { + const res = await getPublicCloudFiles(); + return res; + }, +}; diff --git a/packages/api/src/main.js b/packages/api/src/main.js index 3304d6a08..571593bd1 100644 --- a/packages/api/src/main.js +++ b/packages/api/src/main.js @@ -27,6 +27,7 @@ const plugins = require('./controllers/plugins'); const files = require('./controllers/files'); const scheduler = require('./controllers/scheduler'); const queryHistory = require('./controllers/queryHistory'); +const cloud = require('./controllers/cloud'); const onFinished = require('on-finished'); const processArgs = require('./utility/processArgs'); @@ -223,6 +224,7 @@ function useAllControllers(app, electron) { useController(app, electron, '/query-history', queryHistory); useController(app, electron, '/apps', apps); useController(app, electron, '/auth', auth); + useController(app, electron, '/cloud', cloud); } function setElectronSender(electronSender) { diff --git a/packages/api/src/utility/authProxy.js b/packages/api/src/utility/authProxy.js index 744536177..913c8c82f 100644 --- a/packages/api/src/utility/authProxy.js +++ b/packages/api/src/utility/authProxy.js @@ -36,11 +36,13 @@ async function callRefactorSqlQueryApi(query, task, structure, dialect) { return null; } -function getExternalParamsWithLicense() { +function getExternalParamsWithLicense(isPost = false) { return { - headers: { - 'Content-Type': 'application/json', - }, + headers: isPost + ? { + 'Content-Type': 'application/json', + } + : {}, }; } diff --git a/packages/api/src/utility/cloudIntf.js b/packages/api/src/utility/cloudIntf.js index 9d56f8edf..b39fab441 100644 --- a/packages/api/src/utility/cloudIntf.js +++ b/packages/api/src/utility/cloudIntf.js @@ -8,8 +8,10 @@ const { datadir } = require('./directories'); const platformInfo = require('./platformInfo'); const connections = require('../controllers/connections'); const { isProApp } = require('./checkLicense'); +const socket = require('./socket'); const logger = getLogger('cloudIntf'); + let cloudFiles = null; const DBGATE_IDENTITY_URL = process.env.LOCAL_DBGATE_IDENTITY @@ -30,7 +32,7 @@ async function createDbGateIdentitySession(client) { { client, }, - getExternalParamsWithLicense() + getExternalParamsWithLicense(true) ); return { sid: resp.data.sid, @@ -49,7 +51,7 @@ function startCloudTokenChecking(sid, callback) { try { const resp = await axios.default.get( `${DBGATE_IDENTITY_URL}/api/get-token/${sid}`, - getExternalParamsWithLicense() + getExternalParamsWithLicense(false) ); if (resp.data.status == 'ok') { @@ -110,52 +112,58 @@ async function collectCloudFilesSearchTags() { async function updateCloudFiles() { let lastCloudFilesTags; try { - const fileContent = await fs.readFile(path.join(datadir(), 'cloud-files-tags.json'), 'utf-8'); - cloudFiles = JSON.parse(fileContent); + lastCloudFilesTags = await fs.readFile(path.join(datadir(), 'cloud-files-tags.txt'), 'utf-8'); } catch (err) { - lastCloudFilesTags = []; + lastCloudFilesTags = ''; } + const tags = (await collectCloudFilesSearchTags()).join(','); let lastCheckedTm = 0; - if (_.isEqual(cloudFiles, lastCloudFilesTags) && cloudFiles.length > 0) { - lastCheckedTm = _.max(cloudFiles.map(x => x.modifiedTm)); + if (tags == lastCloudFilesTags && cloudFiles.length > 0) { + lastCheckedTm = _.max(cloudFiles.map(x => parseInt(x.modifiedTm))); } - const tags = await collectCloudFilesSearchTags(); - const resp = await axios.default.post( - `${DBGATE_CLOUD_URL}/public-cloud-updates`, - { - lastCheckedTm, - tags, - }, - getExternalParamsWithLicense() + logger.info({ tags, lastCheckedTm }, 'Downloading cloud files'); + + const resp = await axios.default.get( + `${DBGATE_CLOUD_URL}/public-cloud-updates?lastCheckedTm=${lastCheckedTm}&tags=${tags}`, + getExternalParamsWithLicense(false) ); + logger.info(`Downloaded ${resp.data.length} cloud files`); + const filesByPath = _.keyBy(cloudFiles, 'path'); - for(const file of resp.data) { + for (const file of resp.data) { filesByPath[file.path] = file; } cloudFiles = Object.values(filesByPath); - await fs.writeFile( - path.join(datadir(), 'cloud-files.jsonl'), - cloudFiles.map(x => JSON.stringify(x)).join('\n') - ); + await fs.writeFile(path.join(datadir(), 'cloud-files.jsonl'), cloudFiles.map(x => JSON.stringify(x)).join('\n')); + await fs.writeFile(path.join(datadir(), 'cloud-files-tags.txt'), tags); - await fs.writeFile( - path.join(datadir(), 'cloud-files-tags.json'), - JSON.stringify(tags) - ); + socket.emitChanged(`public-cloud-changed`); } async function startCloudFiles() { await loadCloudFiles(); - await updateCloudFiles(); + try { + await updateCloudFiles(); + } catch (err) { + logger.error(extractErrorLogData(err), 'Error updating cloud files'); + } +} + +async function getPublicCloudFiles() { + if (!loadCloudFiles) { + await loadCloudFiles(); + } + return cloudFiles; } module.exports = { createDbGateIdentitySession, startCloudTokenChecking, startCloudFiles, + getPublicCloudFiles, }; diff --git a/packages/web/src/appobj/CloudFileAppObject.svelte b/packages/web/src/appobj/CloudFileAppObject.svelte new file mode 100644 index 000000000..de547b2a2 --- /dev/null +++ b/packages/web/src/appobj/CloudFileAppObject.svelte @@ -0,0 +1,29 @@ + + + + + diff --git a/packages/web/src/icons/FontIcon.svelte b/packages/web/src/icons/FontIcon.svelte index 22dc64d7b..babcd45ed 100644 --- a/packages/web/src/icons/FontIcon.svelte +++ b/packages/web/src/icons/FontIcon.svelte @@ -40,6 +40,8 @@ 'icon invisible-box': 'mdi mdi-minus-box-outline icon-invisible', 'icon cloud-upload': 'mdi mdi-cloud-upload', 'icon cloud': 'mdi mdi-cloud', + 'icon cloud-public': 'mdi mdi-cloud-search', + 'icon cloud-logged': 'mdi mdi-cloud-key', 'icon import': 'mdi mdi-application-import', 'icon export': 'mdi mdi-application-export', 'icon new-connection': 'mdi mdi-database-plus', diff --git a/packages/web/src/utility/metadataLoaders.ts b/packages/web/src/utility/metadataLoaders.ts index 2fc374a02..fd3fd2a21 100644 --- a/packages/web/src/utility/metadataLoaders.ts +++ b/packages/web/src/utility/metadataLoaders.ts @@ -166,6 +166,12 @@ const authTypesLoader = ({ engine }) => ({ errorValue: null, }); +const publicCloudFilesLoader = () => ({ + url: 'cloud/public-files', + params: {}, + reloadTrigger: { key: `public-cloud-changed` }, +}); + async function getCore(loader, args) { const { url, params, reloadTrigger, transform, onLoaded, errorValue } = loader(args); const key = stableStringify({ url, ...params }); @@ -456,3 +462,10 @@ export function getSchemaList(args) { export function useSchemaList(args) { return useCore(schemaListLoader, args); } + +export function getPublicCloudFiles(args) { + return getCore(publicCloudFilesLoader, args); +} +export function usePublicCloudFiles(args = {}) { + return useCore(publicCloudFilesLoader, args); +} diff --git a/packages/web/src/widgets/CloudItemsWidget.svelte b/packages/web/src/widgets/CloudItemsWidget.svelte new file mode 100644 index 000000000..6c6a2036e --- /dev/null +++ b/packages/web/src/widgets/CloudItemsWidget.svelte @@ -0,0 +1,25 @@ + + + + + + data.folder} /> + + + + + diff --git a/packages/web/src/widgets/WidgetContainer.svelte b/packages/web/src/widgets/WidgetContainer.svelte index 814a60c96..38c1de040 100644 --- a/packages/web/src/widgets/WidgetContainer.svelte +++ b/packages/web/src/widgets/WidgetContainer.svelte @@ -9,6 +9,7 @@ import AppWidget from './AppWidget.svelte'; import AdminMenuWidget from './AdminMenuWidget.svelte'; import AdminPremiumPromoWidget from './AdminPremiumPromoWidget.svelte'; + import CloudItemsWidget from './CloudItemsWidget.svelte';
{/if} - {#if $currentArchive} + {#if $currentArchive && $currentArchive != 'default'}
{/if} + {#if $cloudSigninTokenHolder?.email} +
+ + {$cloudSigninTokenHolder?.email} +
+ {/if} + {#if $appUpdateStatus}
From cc930a3ff92fd079d9c5a3ea249edc818970857e Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Mon, 26 May 2025 15:50:48 +0200 Subject: [PATCH 085/129] cloud connections expansion fix --- packages/web/src/appobj/CloudContentAppObject.svelte | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/web/src/appobj/CloudContentAppObject.svelte b/packages/web/src/appobj/CloudContentAppObject.svelte index 656626057..8ed50b909 100644 --- a/packages/web/src/appobj/CloudContentAppObject.svelte +++ b/packages/web/src/appobj/CloudContentAppObject.svelte @@ -43,6 +43,8 @@ ...$cloudConnectionsStore[data.conid], status: data.status, }} + on:dblclick + on:expand /> {:else} {/if} From afde0a742390606457a4536305ff23bb8ec36647 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Mon, 26 May 2025 16:46:04 +0200 Subject: [PATCH 086/129] cloud connection save --- packages/api/src/controllers/cloud.js | 20 +++++++- packages/api/src/proc/connectProcess.js | 9 +--- packages/api/src/utility/cloudIntf.js | 8 +++- packages/web/src/tabs/ConnectionTab.svelte | 48 ++++++++++++++----- .../web/src/widgets/PrivateCloudWidget.svelte | 27 ++++++++--- 5 files changed, 83 insertions(+), 29 deletions(-) diff --git a/packages/api/src/controllers/cloud.js b/packages/api/src/controllers/cloud.js index 50cfd61c5..c5db7fb37 100644 --- a/packages/api/src/controllers/cloud.js +++ b/packages/api/src/controllers/cloud.js @@ -7,6 +7,7 @@ const { getCloudFolderEncryptor, getCloudContent, putCloudContent, + removeCloudCachedConnection, } = require('../utility/cloudIntf'); const connections = require('./connections'); const socket = require('../utility/socket'); @@ -128,17 +129,32 @@ module.exports = { saveConnection_meta: true, async saveConnection({ folid, connection }) { + let cntid = undefined; + if (connection._id) { + const m = connection._id.match(/^cloud\:\/\/(.+)\/(.+)$/); + if (!m) { + throw new Error('Invalid cloud connection ID format'); + } + folid = m[1]; + cntid = m[2]; + } + + if (!folid) { + throw new Error('Missing cloud folder ID'); + } + const folderEncryptor = await getCloudFolderEncryptor(folid); const recryptedConn = encryptConnection(connection, folderEncryptor); const resp = await putCloudContent( folid, - undefined, + cntid, JSON.stringify(recryptedConn), getConnectionLabel(recryptedConn), 'connection' ); - const { cntid } = resp; + removeCloudCachedConnection(folid, resp.cntid); + cntid = resp.cntid; socket.emitChanged('cloud-content-changed'); return { ...recryptedConn, diff --git a/packages/api/src/proc/connectProcess.js b/packages/api/src/proc/connectProcess.js index 6d375c596..58d2ac454 100644 --- a/packages/api/src/proc/connectProcess.js +++ b/packages/api/src/proc/connectProcess.js @@ -28,14 +28,7 @@ function start() { let version = { version: 'Unknown', }; - try { - version = await driver.getVersion(dbhan); - } catch (err) { - logger.error(extractErrorLogData(err), 'Error getting DB server version'); - version = { - version: 'Unknown', - }; - } + version = await driver.getVersion(dbhan); let databases = undefined; if (requestDbList) { databases = await driver.listDatabases(dbhan); diff --git a/packages/api/src/utility/cloudIntf.js b/packages/api/src/utility/cloudIntf.js index a12980bf6..d1dfbdc7d 100644 --- a/packages/api/src/utility/cloudIntf.js +++ b/packages/api/src/utility/cloudIntf.js @@ -271,7 +271,7 @@ async function callCloudApiPost(endpoint, body, signinHolder = null) { async function getCloudFolderEncryptor(folid) { const { encryptionKey } = await callCloudApiGet(`folder-key/${folid}`); if (!encryptionKey) { - throw new Error('No encryption key'); + throw new Error('No encryption key for folder: ' + folid); } return simpleEncryptor.createEncryptor(encryptionKey); } @@ -336,6 +336,11 @@ async function loadCachedCloudConnection(folid, cntid) { return cloudConnectionCache[cacheKey]; } +function removeCloudCachedConnection(folid, cntid) { + const cacheKey = `${folid}|${cntid}`; + delete cloudConnectionCache[cacheKey]; +} + module.exports = { createDbGateIdentitySession, startCloudTokenChecking, @@ -349,4 +354,5 @@ module.exports = { getCloudContent, loadCachedCloudConnection, putCloudContent, + removeCloudCachedConnection, }; diff --git a/packages/web/src/tabs/ConnectionTab.svelte b/packages/web/src/tabs/ConnectionTab.svelte index ebc5d2fa3..5df80d76f 100644 --- a/packages/web/src/tabs/ConnectionTab.svelte +++ b/packages/web/src/tabs/ConnectionTab.svelte @@ -159,7 +159,7 @@ $: currentConnection = getCurrentConnectionCore($values, driver); async function handleSave() { - if (saveOnCloud) { + if (saveOnCloud && !getCurrentConnection()?._id) { showModal(ChooseCloudFolderModal, { requiredRoleVariants: ['write', 'admin'], message: 'Choose cloud folder to saved connection', @@ -184,6 +184,17 @@ } }, }); + } else if ( + // @ts-ignore + getCurrentConnection()?._id?.startsWith('cloud://') + ) { + let connection = getCurrentConnection(); + await apiCall('cloud/save-connection', { connection }); + showSnackbarSuccess('Connection saved'); + changeTab(tabid, tab => ({ + ...tab, + title: getConnectionLabel(connection), + })); } else { let connection = getCurrentConnection(); connection = { @@ -210,19 +221,32 @@ async function handleConnect() { let connection = getCurrentConnection(); - if (!connection._id) { - connection = { - ...connection, - unsaved: true, + + if ( + // @ts-ignore + connection?._id?.startsWith('cloud://') + ) { + const saved = await apiCall('cloud/save-connection', { connection }); + changeTab(tabid, tab => ({ + ...tab, + title: getConnectionLabel(connection), + })); + openConnection(saved); + } else { + if (!connection._id) { + connection = { + ...connection, + unsaved: true, + }; + } + const saved = await apiCall('connections/save', connection); + $values = { + ...$values, + unsaved: connection.unsaved, + _id: saved._id, }; + openConnection(saved); } - const saved = await apiCall('connections/save', connection); - $values = { - ...$values, - unsaved: connection.unsaved, - _id: saved._id, - }; - openConnection(saved); // closeMultipleTabs(x => x.tabid == tabid, true); } diff --git a/packages/web/src/widgets/PrivateCloudWidget.svelte b/packages/web/src/widgets/PrivateCloudWidget.svelte index e67dcb5c6..49d2c01e9 100644 --- a/packages/web/src/widgets/PrivateCloudWidget.svelte +++ b/packages/web/src/widgets/PrivateCloudWidget.svelte @@ -20,6 +20,7 @@ currentDatabase, expandedConnections, openedConnections, + openedSingleDatabaseConnections, } from '../stores'; import _ from 'lodash'; import { plusExpandIcon } from '../icons/expandIcons'; @@ -74,12 +75,26 @@ }; } - onMount(() => { - const currentConid = $currentDatabase?.connection?._id; - if (currentConid?.startsWith('cloud://') && !$cloudConnectionsStore[currentConid]) { - loadCloudConnection(currentConid); - } - }); + function ensureCloudConnectionsLoaded(...conids) { + _.uniq(conids).forEach(conid => { + if (conid?.startsWith('cloud://') && !$cloudConnectionsStore[conid]) { + loadCloudConnection(conid); + } + }); + } + + $: ensureCloudConnectionsLoaded( + $currentDatabase?.connection?._id, + ...$openedSingleDatabaseConnections, + ...$openedConnections + ); + + // onMount(() => { + // const currentConid = $currentDatabase?.connection?._id; + // if (currentConid?.startsWith('cloud://') && !$cloudConnectionsStore[currentConid]) { + // loadCloudConnection(currentConid); + // } + // }); function createAddMenu() { return [ From d26db7096d4284f5631ddfca90aced8f405fd7df Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Mon, 26 May 2025 17:02:09 +0200 Subject: [PATCH 087/129] refactor - handle cloud listeners --- packages/api/src/controllers/cloud.js | 7 +++ packages/api/src/utility/cloudIntf.js | 1 + packages/web/src/App.svelte | 2 + packages/web/src/stores.ts | 6 +++ packages/web/src/utility/cloudListeners.ts | 48 +++++++++++++++++++ .../web/src/widgets/PrivateCloudWidget.svelte | 38 +++++++-------- 6 files changed, 83 insertions(+), 19 deletions(-) create mode 100644 packages/web/src/utility/cloudListeners.ts diff --git a/packages/api/src/controllers/cloud.js b/packages/api/src/controllers/cloud.js index c5db7fb37..4888cc826 100644 --- a/packages/api/src/controllers/cloud.js +++ b/packages/api/src/controllers/cloud.js @@ -59,6 +59,7 @@ module.exports = { async putContent({ folid, cntid, content, name, type }) { const resp = await putCloudContent(folid, cntid, content, name, type); socket.emitChanged('cloud-content-changed'); + socket.emit('cloud-content-updated'); return resp; }, @@ -66,6 +67,7 @@ module.exports = { async createFolder({ name }) { const resp = await callCloudApiPost(`folders/create`, { name }); socket.emitChanged('cloud-content-changed'); + socket.emit('cloud-content-updated'); return resp; }, @@ -80,6 +82,7 @@ module.exports = { const resp = await callCloudApiPost(`folders/grant/${mode}`, { invite }); socket.emitChanged('cloud-content-changed'); + socket.emit('cloud-content-updated'); return resp; }, @@ -87,6 +90,7 @@ module.exports = { async renameFolder({ folid, name }) { const resp = await callCloudApiPost(`folders/rename`, { folid, name }); socket.emitChanged('cloud-content-changed'); + socket.emit('cloud-content-updated'); return resp; }, @@ -94,6 +98,7 @@ module.exports = { async deleteFolder({ folid }) { const resp = await callCloudApiPost(`folders/delete`, { folid }); socket.emitChanged('cloud-content-changed'); + socket.emit('cloud-content-updated'); return resp; }, @@ -106,6 +111,7 @@ module.exports = { refreshContent_meta: true, async refreshContent() { socket.emitChanged('cloud-content-changed'); + socket.emit('cloud-content-updated'); return { status: 'ok', }; @@ -156,6 +162,7 @@ module.exports = { removeCloudCachedConnection(folid, resp.cntid); cntid = resp.cntid; socket.emitChanged('cloud-content-changed'); + socket.emit('cloud-content-updated'); return { ...recryptedConn, _id: `cloud://${folid}/${cntid}`, diff --git a/packages/api/src/utility/cloudIntf.js b/packages/api/src/utility/cloudIntf.js index d1dfbdc7d..6e845b212 100644 --- a/packages/api/src/utility/cloudIntf.js +++ b/packages/api/src/utility/cloudIntf.js @@ -320,6 +320,7 @@ async function putCloudContent(folid, cntid, content, name, type) { signinHolder ); socket.emitChanged('cloud-content-changed'); + socket.emit('cloud-content-updated'); return resp; } diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index e5acac607..124be6c23 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -28,6 +28,7 @@ import { handleAuthOnStartup } from './clientAuth'; import { initializeAppUpdates } from './utility/appUpdate'; import { _t } from './translations'; + import { installCloudListeners } from './utility/cloudListeners'; export let isAdminPage = false; @@ -58,6 +59,7 @@ installNewVolatileConnectionListener(); installNewCloudTokenListener(); initializeAppUpdates(); + installCloudListeners(); } refreshPublicCloudFiles(); diff --git a/packages/web/src/stores.ts b/packages/web/src/stores.ts index 1de2cdc71..5dfdb4c99 100644 --- a/packages/web/src/stores.ts +++ b/packages/web/src/stores.ts @@ -457,4 +457,10 @@ focusedTreeDbKey.subscribe(value => { }); export const getFocusedTreeDbKey = () => focusedTreeDbKeyValue; +let cloudConnectionsStoreValue = {}; +cloudConnectionsStore.subscribe(value => { + cloudConnectionsStoreValue = value; +}); +export const getCloudConnectionsStore = () => cloudConnectionsStoreValue; + window['__changeCurrentTheme'] = theme => currentTheme.set(theme); diff --git a/packages/web/src/utility/cloudListeners.ts b/packages/web/src/utility/cloudListeners.ts new file mode 100644 index 000000000..501cc2f65 --- /dev/null +++ b/packages/web/src/utility/cloudListeners.ts @@ -0,0 +1,48 @@ +import { derived } from 'svelte/store'; +import { + cloudConnectionsStore, + currentDatabase, + getCloudConnectionsStore, + openedConnections, + openedSingleDatabaseConnections, +} from '../stores'; +import { apiCall, apiOn } from './api'; +import _ from 'lodash'; + +export const possibleCloudConnectionSources = derived( + [currentDatabase, openedSingleDatabaseConnections, openedConnections], + ([$currentDatabase, $openedSingleDatabaseConnections, $openedConnections]) => { + const conids = new Set(); + if ($currentDatabase?.connection?._id) { + conids.add($currentDatabase.connection._id); + } + $openedSingleDatabaseConnections.forEach(x => conids.add(x)); + $openedConnections.forEach(x => conids.add(x)); + return Array.from(conids).filter(x => x?.startsWith('cloud://')); + } +); + +async function loadCloudConnection(conid) { + const conn = await apiCall('connections/get', { conid }); + cloudConnectionsStore.update(store => ({ + ...store, + [conid]: conn, + })); +} + +function ensureCloudConnectionsLoaded(...conids) { + const conns = getCloudConnectionsStore(); + _.uniq(conids).forEach(conid => { + if (!conns[conid]) { + loadCloudConnection(conid); + } + }); +} + +export function installCloudListeners() { + possibleCloudConnectionSources.subscribe(conids => { + ensureCloudConnectionsLoaded(...conids); + }); + + apiOn('cloud-content-updated', () => cloudConnectionsStore.set({})); +} diff --git a/packages/web/src/widgets/PrivateCloudWidget.svelte b/packages/web/src/widgets/PrivateCloudWidget.svelte index 49d2c01e9..5b6cf38c0 100644 --- a/packages/web/src/widgets/PrivateCloudWidget.svelte +++ b/packages/web/src/widgets/PrivateCloudWidget.svelte @@ -67,27 +67,27 @@ await apiCall('cloud/refresh-content'); } - async function loadCloudConnection(conid) { - const conn = await apiCall('connections/get', { conid }); - $cloudConnectionsStore = { - ...$cloudConnectionsStore, - [conid]: conn, - }; - } + // async function loadCloudConnection(conid) { + // const conn = await apiCall('connections/get', { conid }); + // $cloudConnectionsStore = { + // ...$cloudConnectionsStore, + // [conid]: conn, + // }; + // } - function ensureCloudConnectionsLoaded(...conids) { - _.uniq(conids).forEach(conid => { - if (conid?.startsWith('cloud://') && !$cloudConnectionsStore[conid]) { - loadCloudConnection(conid); - } - }); - } + // function ensureCloudConnectionsLoaded(...conids) { + // _.uniq(conids).forEach(conid => { + // if (conid?.startsWith('cloud://') && !$cloudConnectionsStore[conid]) { + // loadCloudConnection(conid); + // } + // }); + // } - $: ensureCloudConnectionsLoaded( - $currentDatabase?.connection?._id, - ...$openedSingleDatabaseConnections, - ...$openedConnections - ); + // $: ensureCloudConnectionsLoaded( + // $currentDatabase?.connection?._id, + // ...$openedSingleDatabaseConnections, + // ...$openedConnections + // ); // onMount(() => { // const currentConid = $currentDatabase?.connection?._id; From f94bf3f8ce9efd082c62873e7c48771a45894467 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Mon, 26 May 2025 17:24:13 +0200 Subject: [PATCH 088/129] cloud fixes --- packages/api/src/controllers/cloud.js | 4 ++++ packages/web/src/tabs/ConnectionTab.svelte | 14 ++++++++------ packages/web/src/widgets/PrivateCloudWidget.svelte | 2 +- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/api/src/controllers/cloud.js b/packages/api/src/controllers/cloud.js index 4888cc826..b1e92ae4d 100644 --- a/packages/api/src/controllers/cloud.js +++ b/packages/api/src/controllers/cloud.js @@ -159,6 +159,10 @@ module.exports = { 'connection' ); + if (resp.apiErrorMessage) { + return resp; + } + removeCloudCachedConnection(folid, resp.cntid); cntid = resp.cntid; socket.emitChanged('cloud-content-changed'); diff --git a/packages/web/src/tabs/ConnectionTab.svelte b/packages/web/src/tabs/ConnectionTab.svelte index 5df80d76f..d15209e07 100644 --- a/packages/web/src/tabs/ConnectionTab.svelte +++ b/packages/web/src/tabs/ConnectionTab.svelte @@ -189,12 +189,14 @@ getCurrentConnection()?._id?.startsWith('cloud://') ) { let connection = getCurrentConnection(); - await apiCall('cloud/save-connection', { connection }); - showSnackbarSuccess('Connection saved'); - changeTab(tabid, tab => ({ - ...tab, - title: getConnectionLabel(connection), - })); + const resp = await apiCall('cloud/save-connection', { connection }); + if (resp?._id) { + showSnackbarSuccess('Connection saved'); + changeTab(tabid, tab => ({ + ...tab, + title: getConnectionLabel(connection), + })); + } } else { let connection = getCurrentConnection(); connection = { diff --git a/packages/web/src/widgets/PrivateCloudWidget.svelte b/packages/web/src/widgets/PrivateCloudWidget.svelte index 5b6cf38c0..6527af042 100644 --- a/packages/web/src/widgets/PrivateCloudWidget.svelte +++ b/packages/web/src/widgets/PrivateCloudWidget.svelte @@ -222,7 +222,7 @@ module={cloudContentAppObject} emptyGroupNames={emptyCloudContent} groupFunc={data => data.folid} - mapGroupTitle={folid => contentGroupMap[folid]?.name} + mapGroupTitle={folid => `${contentGroupMap[folid]?.name} - ${contentGroupMap[folid]?.role}`} filter={publicFilter} subItemsComponent={() => SubCloudItemsList} expandIconFunc={plusExpandIcon} From 74560c3289cf1a3152c66afa562933c5c3423695 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Mon, 26 May 2025 17:59:03 +0200 Subject: [PATCH 089/129] duplicate cloud connection --- packages/api/src/controllers/cloud.js | 18 +++++++++ .../src/appobj/CloudContentAppObject.svelte | 38 +++++++++++++++---- .../web/src/appobj/ConnectionAppObject.svelte | 4 +- packages/web/src/utility/cloudListeners.ts | 5 ++- 4 files changed, 55 insertions(+), 10 deletions(-) diff --git a/packages/api/src/controllers/cloud.js b/packages/api/src/controllers/cloud.js index b1e92ae4d..775582539 100644 --- a/packages/api/src/controllers/cloud.js +++ b/packages/api/src/controllers/cloud.js @@ -172,4 +172,22 @@ module.exports = { _id: `cloud://${folid}/${cntid}`, }; }, + + duplicateConnection_meta: true, + async duplicateConnection({ conid }) { + const m = conid.match(/^cloud\:\/\/(.+)\/(.+)$/); + if (!m) { + throw new Error('Invalid cloud connection ID format'); + } + const folid = m[1]; + const cntid = m[2]; + const respGet = await getCloudContent(folid, cntid); + const conn = JSON.parse(respGet.content); + const conn2 = { + ...conn, + displayName: getConnectionLabel(conn) + ' - copy', + }; + const respPut = await putCloudContent(folid, undefined, JSON.stringify(conn2), conn2.displayName, 'connection'); + return respPut; + }, }; diff --git a/packages/web/src/appobj/CloudContentAppObject.svelte b/packages/web/src/appobj/CloudContentAppObject.svelte index 8ed50b909..b2a915b6f 100644 --- a/packages/web/src/appobj/CloudContentAppObject.svelte +++ b/packages/web/src/appobj/CloudContentAppObject.svelte @@ -13,23 +13,47 @@ - + Save file + {#if $cloudSigninTokenHolder} + + {/if} + {#if electron} @@ -79,4 +116,4 @@ {/if} - + From 741b942dea3402f11fca2b2209f0e93add7a8f70 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Wed, 28 May 2025 08:25:10 +0200 Subject: [PATCH 091/129] cloud files WIP --- packages/api/src/controllers/cloud.js | 9 ++- packages/api/src/utility/cloudIntf.js | 20 ++++-- .../src/appobj/CloudContentAppObject.svelte | 14 ++++ .../web/src/appobj/SavedFileAppObject.svelte | 17 ++++- .../src/forms/FormCloudFolderSelect.svelte | 16 +++-- packages/web/src/modals/SaveFileModal.svelte | 70 ++++++++++--------- .../web/src/widgets/PrivateCloudWidget.svelte | 9 ++- .../web/src/widgets/PublicCloudWidget.svelte | 8 +-- 8 files changed, 109 insertions(+), 54 deletions(-) diff --git a/packages/api/src/controllers/cloud.js b/packages/api/src/controllers/cloud.js index 821470add..37fd9856f 100644 --- a/packages/api/src/controllers/cloud.js +++ b/packages/api/src/controllers/cloud.js @@ -205,6 +205,11 @@ module.exports = { return resp; }, - // saveFile_meta: true, - // async saveFile({folid, file, data, folder, format}) + saveFile_meta: true, + async saveFile({ folid, cntid, fileName, data, contentFolder, format }) { + const resp = await putCloudContent(folid, cntid, data, fileName, 'file', contentFolder, format); + socket.emitChanged('cloud-content-changed'); + socket.emit('cloud-content-updated'); + return resp; + }, }; diff --git a/packages/api/src/utility/cloudIntf.js b/packages/api/src/utility/cloudIntf.js index 6e845b212..da67425b8 100644 --- a/packages/api/src/utility/cloudIntf.js +++ b/packages/api/src/utility/cloudIntf.js @@ -284,9 +284,13 @@ async function getCloudContent(folid, cntid) { const encryptor = simpleEncryptor.createEncryptor(signinHolder.encryptionKey); - const { content, name, type, apiErrorMessage } = await callCloudApiGet(`content/${folid}/${cntid}`, signinHolder, { - 'x-kehid': signinHolder.kehid, - }); + const { content, name, type, contentFolder, contentType, apiErrorMessage } = await callCloudApiGet( + `content/${folid}/${cntid}`, + signinHolder, + { + 'x-kehid': signinHolder.kehid, + } + ); if (apiErrorMessage) { return { apiErrorMessage }; @@ -296,10 +300,16 @@ async function getCloudContent(folid, cntid) { content: encryptor.decrypt(content), name, type, + contentFolder, + contentType, }; } -async function putCloudContent(folid, cntid, content, name, type) { +/** + * + * @returns Promise<{ cntid: string } | { apiErrorMessage: string }> + */ +async function putCloudContent(folid, cntid, content, name, type, contentFolder = null, contentType = null) { const signinHolder = await getCloudSigninHolder(); if (!signinHolder) { throw new Error('No signed in'); @@ -316,6 +326,8 @@ async function putCloudContent(folid, cntid, content, name, type) { type, kehid: signinHolder.kehid, content: encryptor.encrypt(content), + contentFolder, + contentType, }, signinHolder ); diff --git a/packages/web/src/appobj/CloudContentAppObject.svelte b/packages/web/src/appobj/CloudContentAppObject.svelte index aaff32814..819949e6e 100644 --- a/packages/web/src/appobj/CloudContentAppObject.svelte +++ b/packages/web/src/appobj/CloudContentAppObject.svelte @@ -17,6 +17,7 @@ import openNewTab from '../utility/openNewTab'; import { showModal } from '../modals/modalTools'; import ConfirmModal from '../modals/ConfirmModal.svelte'; + import SavedFileAppObject from './SavedFileAppObject.svelte'; export let data; export let passProps; @@ -102,6 +103,19 @@ on:dblclick on:expand /> +{:else if data.type == 'file'} + {:else} diff --git a/packages/web/src/forms/FormCloudFolderSelect.svelte b/packages/web/src/forms/FormCloudFolderSelect.svelte index a6c215743..28d5bf08f 100644 --- a/packages/web/src/forms/FormCloudFolderSelect.svelte +++ b/packages/web/src/forms/FormCloudFolderSelect.svelte @@ -6,14 +6,22 @@ export let name; export let requiredRoleVariants = ['read', 'write', 'admin']; + export let prependFolders = []; + const cloudContentList = useCloudContentList(); - $: folderOptions = ($cloudContentList || []) - .filter(folder => requiredRoleVariants.find(role => folder.role == role)) - .map(folder => ({ + $: folderOptions = [ + ...prependFolders.map(folder => ({ value: folder.folid, label: folder.name, - })); + })), + ...($cloudContentList || []) + .filter(folder => requiredRoleVariants.find(role => folder.role == role)) + .map(folder => ({ + value: folder.folid, + label: folder.name, + })), + ]; diff --git a/packages/web/src/modals/SaveFileModal.svelte b/packages/web/src/modals/SaveFileModal.svelte index 16b9d7dd2..4665880fa 100644 --- a/packages/web/src/modals/SaveFileModal.svelte +++ b/packages/web/src/modals/SaveFileModal.svelte @@ -23,20 +23,42 @@ export let filePath; export let onSave = undefined; - const values = writable({ name }); + const values = writable({ name, cloudFolder: '__local' }); const electron = getElectron(); const handleSubmit = async e => { - const { name } = e.detail; - await apiCall('files/save', { folder, file: name, data, format }); - closeCurrentModal(); - if (onSave) { - onSave(name, { - savedFile: name, - savedFolder: folder, - savedFilePath: null, + const { name, cloudFolder } = e.detail; + if (cloudFolder === '__local') { + await apiCall('files/save', { folder, file: name, data, format }); + closeCurrentModal(); + if (onSave) { + onSave(name, { + savedFile: name, + savedFolder: folder, + savedFilePath: null, + }); + } + } else { + const resp = await apiCall('cloud/save-file', { + folid: cloudFolder, + fileName: name, + data, + contentFolder: folder, + format, }); + if (resp.cntid) { + closeCurrentModal(); + if (onSave) { + onSave(name, { + savedFile: name, + savedFolder: folder, + savedFilePath: null, + savedCloudFolderId: cloudFolder, + savedCloudContentId: resp.cntid, + }); + } + } } }; @@ -56,28 +78,6 @@ }); } }; - - const handleSaveToCloud = async folid => { - const resp = await apiCall('cloud/save-file', { - folid, - fileName: $values.name, - data, - contentFolder: folder, - format, - }); - if (resp.cntid) { - closeCurrentModal(); - if (onSave) { - onSave(name, { - savedFile: name, - savedFolder: folder, - savedFilePath: null, - savedCloudFolderId: folid, - savedCloudContentId: resp.cntid, - }); - } - } - }; @@ -86,10 +86,16 @@ {#if $cloudSigninTokenHolder} {/if} diff --git a/packages/web/src/widgets/PrivateCloudWidget.svelte b/packages/web/src/widgets/PrivateCloudWidget.svelte index 6527af042..8fdb35f5d 100644 --- a/packages/web/src/widgets/PrivateCloudWidget.svelte +++ b/packages/web/src/widgets/PrivateCloudWidget.svelte @@ -34,8 +34,7 @@ import ConfirmModal from '../modals/ConfirmModal.svelte'; import { showSnackbarInfo } from '../utility/snackbar'; - let publicFilter = ''; - let cloudFilter = ''; + let filter = ''; let domSqlObjectList = null; const cloudContentList = useCloudContentList(); @@ -205,8 +204,8 @@ skip={!$cloudSigninTokenHolder} > - - + + data.folid} mapGroupTitle={folid => `${contentGroupMap[folid]?.name} - ${contentGroupMap[folid]?.role}`} - filter={publicFilter} + {filter} subItemsComponent={() => SubCloudItemsList} expandIconFunc={plusExpandIcon} isExpandable={data => diff --git a/packages/web/src/widgets/PublicCloudWidget.svelte b/packages/web/src/widgets/PublicCloudWidget.svelte index e0383c94a..9b137f2fb 100644 --- a/packages/web/src/widgets/PublicCloudWidget.svelte +++ b/packages/web/src/widgets/PublicCloudWidget.svelte @@ -18,7 +18,7 @@ import FontIcon from '../icons/FontIcon.svelte'; import { apiCall } from '../utility/api'; import _ from 'lodash'; - let publicFilter = ''; + let filter = ''; const publicFiles = usePublicCloudFiles(); @@ -31,8 +31,8 @@ - - + + data.folder || undefined} - filter={publicFilter} + {filter} /> From 7b50a19b2c7648ec12185c2b7680acfb6ff35a4e Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Wed, 28 May 2025 10:46:35 +0200 Subject: [PATCH 092/129] cloud file, folder operations --- packages/api/src/controllers/cloud.js | 35 +++++++++++++ .../web/src/appobj/ConnectionAppObject.svelte | 14 ++++-- .../web/src/appobj/SavedFileAppObject.svelte | 49 +++++++++++++++---- packages/web/src/modals/SaveFileModal.svelte | 12 +++-- packages/web/src/utility/cloudListeners.ts | 8 ++- packages/web/src/utility/openElectronFile.ts | 2 + packages/web/src/utility/openNewTab.ts | 7 ++- packages/web/src/utility/saveTabFile.ts | 33 ++++++++++--- .../web/src/widgets/PrivateCloudWidget.svelte | 31 ++++++------ 9 files changed, 150 insertions(+), 41 deletions(-) diff --git a/packages/api/src/controllers/cloud.js b/packages/api/src/controllers/cloud.js index 37fd9856f..c2cc1af2d 100644 --- a/packages/api/src/controllers/cloud.js +++ b/packages/api/src/controllers/cloud.js @@ -15,6 +15,7 @@ const { recryptConnection, getInternalEncryptor, encryptConnection } = require(' const { getConnectionLabel, getLogger, extractErrorLogData } = require('dbgate-tools'); const logger = getLogger('cloud'); const _ = require('lodash'); +const fs = require('fs-extra'); module.exports = { publicFiles_meta: true, @@ -205,6 +206,22 @@ module.exports = { return resp; }, + deleteContent_meta: true, + async deleteContent({ folid, cntid }) { + const resp = await callCloudApiPost(`content/delete/${folid}/${cntid}`); + socket.emitChanged('cloud-content-changed'); + socket.emit('cloud-content-updated'); + return resp; + }, + + renameContent_meta: true, + async renameContent({ folid, cntid, name }) { + const resp = await callCloudApiPost(`content/rename/${folid}/${cntid}`, { name }); + socket.emitChanged('cloud-content-changed'); + socket.emit('cloud-content-updated'); + return resp; + }, + saveFile_meta: true, async saveFile({ folid, cntid, fileName, data, contentFolder, format }) { const resp = await putCloudContent(folid, cntid, data, fileName, 'file', contentFolder, format); @@ -212,4 +229,22 @@ module.exports = { socket.emit('cloud-content-updated'); return resp; }, + + copyFile_meta: true, + async copyFile({ folid, cntid, name }) { + const resp = await callCloudApiPost(`content/duplicate/${folid}/${cntid}`, { name }); + socket.emitChanged('cloud-content-changed'); + socket.emit('cloud-content-updated'); + return resp; + }, + + exportFile_meta: true, + async exportFile({ folid, cntid, filePath }, req) { + const { content } = await getCloudContent(folid, cntid); + if (!content) { + throw new Error('File not found'); + } + await fs.writeFile(filePath, content); + return true; + }, }; diff --git a/packages/web/src/appobj/ConnectionAppObject.svelte b/packages/web/src/appobj/ConnectionAppObject.svelte index 531bb0788..7ed4d83ff 100644 --- a/packages/web/src/appobj/ConnectionAppObject.svelte +++ b/packages/web/src/appobj/ConnectionAppObject.svelte @@ -262,11 +262,15 @@ }); }; const handleDuplicate = () => { - apiCall('connections/save', { - ...data, - _id: undefined, - displayName: `${getConnectionLabel(data)} - copy`, - }); + if (data._id.startsWith('cloud://')) { + apiCall('cloud/duplicate-connection', { conid: data._id }); + } else { + apiCall('connections/save', { + ...data, + _id: undefined, + displayName: `${getConnectionLabel(data)} - copy`, + }); + } }; const handleCreateDatabase = () => { showModal(InputTextModal, { diff --git a/packages/web/src/appobj/SavedFileAppObject.svelte b/packages/web/src/appobj/SavedFileAppObject.svelte index 89f2ce970..2cd5e70bc 100644 --- a/packages/web/src/appobj/SavedFileAppObject.svelte +++ b/packages/web/src/appobj/SavedFileAppObject.svelte @@ -206,7 +206,14 @@ showModal(ConfirmModal, { message: `Really delete file ${data.file}?`, onConfirm: () => { - apiCall('files/delete', data); + if (data.folid && data.cntid) { + apiCall('cloud/delete-content', { + folid: data.folid, + cntid: data.cntid, + }); + } else { + apiCall('files/delete', data); + } }, }); }; @@ -217,7 +224,15 @@ label: 'New file name', header: 'Rename file', onConfirm: newFile => { - apiCall('files/rename', { ...data, newFile }); + if (data.folid && data.cntid) { + apiCall('cloud/rename-content', { + folid: data.folid, + cntid: data.cntid, + name: newFile, + }); + } else { + apiCall('files/rename', { ...data, newFile }); + } }, }); }; @@ -226,9 +241,17 @@ showModal(InputTextModal, { value: data.file, label: 'New file name', - header: 'Rename file', + header: 'Copy file', onConfirm: newFile => { - apiCall('files/copy', { ...data, newFile }); + if (data.folid && data.cntid) { + apiCall('cloud/copy-file', { + folid: data.folid, + cntid: data.cntid, + name: newFile, + }); + } else { + apiCall('files/copy', { ...data, newFile }); + } }, }); }; @@ -236,11 +259,19 @@ const handleDownload = () => { saveFileToDisk( async filePath => { - await apiCall('files/export-file', { - folder, - file: data.file, - filePath, - }); + if (data.folid && data.cntid) { + await apiCall('cloud/export-file', { + folid: data.folid, + cntid: data.cntid, + filePath, + }); + } else { + await apiCall('files/export-file', { + folder, + file: data.file, + filePath, + }); + } }, { formatLabel: handler.label, formatExtension: handler.format, defaultFileName: data.file } ); diff --git a/packages/web/src/modals/SaveFileModal.svelte b/packages/web/src/modals/SaveFileModal.svelte index 4665880fa..15ebd67e1 100644 --- a/packages/web/src/modals/SaveFileModal.svelte +++ b/packages/web/src/modals/SaveFileModal.svelte @@ -10,7 +10,6 @@ import { writable } from 'svelte/store'; import getElectron from '../utility/getElectron'; - import ChooseCloudFolderModal from './ChooseCloudFolderModal.svelte'; import ModalBase from './ModalBase.svelte'; import { closeCurrentModal, showModal } from './modalTools'; import FormCloudFolderSelect from '../forms/FormCloudFolderSelect.svelte'; @@ -22,8 +21,10 @@ export let fileExtension; export let filePath; export let onSave = undefined; + export let folid; + // export let cntid; - const values = writable({ name, cloudFolder: '__local' }); + const values = writable({ name, cloudFolder: folid ?? '__local' }); const electron = getElectron(); @@ -37,6 +38,8 @@ savedFile: name, savedFolder: folder, savedFilePath: null, + savedCloudFolderId: null, + savedCloudContentId: null, }); } } else { @@ -46,6 +49,7 @@ data, contentFolder: folder, format, + // cntid, }); if (resp.cntid) { closeCurrentModal(); @@ -55,7 +59,7 @@ savedFolder: folder, savedFilePath: null, savedCloudFolderId: cloudFolder, - savedCloudContentId: resp.cntid, + // savedCloudContentId: resp.cntid, }); } } @@ -75,6 +79,8 @@ savedFile: null, savedFolder: null, savedFilePath: filePath, + savedCloudFolderId: null, + savedCloudContentId: null, }); } }; diff --git a/packages/web/src/utility/cloudListeners.ts b/packages/web/src/utility/cloudListeners.ts index 7eef1cd46..bdb3b1bc6 100644 --- a/packages/web/src/utility/cloudListeners.ts +++ b/packages/web/src/utility/cloudListeners.ts @@ -47,5 +47,11 @@ export function installCloudListeners() { ensureCloudConnectionsLoaded(...conids); }); - apiOn('cloud-content-updated', () => cloudConnectionsStore.set({})); + apiOn('cloud-content-updated', () => { + const conids = Object.keys(getCloudConnectionsStore()); + cloudConnectionsStore.set({}); + for (const conn of conids) { + loadCloudConnection(conn); + } + }); } diff --git a/packages/web/src/utility/openElectronFile.ts b/packages/web/src/utility/openElectronFile.ts index f771cdcdf..51c265f8e 100644 --- a/packages/web/src/utility/openElectronFile.ts +++ b/packages/web/src/utility/openElectronFile.ts @@ -111,6 +111,8 @@ async function openSavedElectronFile(filePath, parsed, folder) { props: { savedFile: null, savedFolder: null, + savedCloudFolderId: null, + savedCloudContentId: null, savedFilePath: filePath, savedFormat: handler.format, ...connProps, diff --git a/packages/web/src/utility/openNewTab.ts b/packages/web/src/utility/openNewTab.ts index 59fab9db4..6b4070ded 100644 --- a/packages/web/src/utility/openNewTab.ts +++ b/packages/web/src/utility/openNewTab.ts @@ -30,7 +30,8 @@ export default async function openNewTab(newTab, initialData: any = undefined, o }; } - const { savedFile, savedFolder, savedFilePath, conid, database } = newTab.props || {}; + const { savedFile, savedFolder, savedFilePath, savedCloudFolderId, savedCloudContentId, conid, database } = + newTab.props || {}; if (conid && database) { const connection = await getConnectionInfo({ conid }); @@ -49,7 +50,9 @@ export default async function openNewTab(newTab, initialData: any = undefined, o x.closedTime == null && x.props.savedFile == savedFile && x.props.savedFolder == savedFolder && - x.props.savedFilePath == savedFilePath + x.props.savedFilePath == savedFilePath && + x.props.savedCloudFolderId == savedCloudFolderId && + x.props.savedCloudContentId == savedCloudContentId ); } diff --git a/packages/web/src/utility/saveTabFile.ts b/packages/web/src/utility/saveTabFile.ts index 60351ecdb..b4519fc9d 100644 --- a/packages/web/src/utility/saveTabFile.ts +++ b/packages/web/src/utility/saveTabFile.ts @@ -15,16 +15,31 @@ export default async function saveTabFile(editor, saveMode, folder, format, file const tabs = get(openedTabs); const tabid = editor.activator.tabid; const data = editor.getData(); - const { savedFile, savedFilePath, savedFolder } = tabs.find(x => x.tabid == tabid).props || {}; + const { savedFile, savedFilePath, savedFolder, savedCloudFolderId, savedCloudContentId } = + tabs.find(x => x.tabid == tabid).props || {}; const handleSave = async () => { - if (savedFile) { - await apiCall('files/save', { folder: savedFolder || folder, file: savedFile, data, format }); + if (savedCloudFolderId && savedCloudContentId) { + const resp = await apiCall('cloud/save-file', { + folid: savedCloudFolderId, + fileName: savedFile, + data, + contentFolder: folder, + format, + cntid: savedCloudContentId, + }); + if (resp.cntid) { + markTabSaved(tabid); + } + } else { + if (savedFile) { + await apiCall('files/save', { folder: savedFolder || folder, file: savedFile, data, format }); + } + if (savedFilePath) { + await apiCall('files/save-as', { filePath: savedFilePath, data, format }); + } + markTabSaved(tabid); } - if (savedFilePath) { - await apiCall('files/save-as', { filePath: savedFilePath, data, format }); - } - markTabSaved(tabid); }; const onSave = (title, newProps) => { @@ -60,6 +75,8 @@ export default async function saveTabFile(editor, saveMode, folder, format, file savedFile: null, savedFolder: null, savedFilePath: file, + savedCloudFolderId: null, + savedCloudContentId: null, }); } } else if ((savedFile || savedFilePath) && saveMode == 'save') { @@ -73,6 +90,8 @@ export default async function saveTabFile(editor, saveMode, folder, format, file name: savedFile || 'newFile', filePath: savedFilePath, onSave, + folid: savedCloudFolderId, + // cntid: savedCloudContentId, }); } } diff --git a/packages/web/src/widgets/PrivateCloudWidget.svelte b/packages/web/src/widgets/PrivateCloudWidget.svelte index 8fdb35f5d..1a53663d5 100644 --- a/packages/web/src/widgets/PrivateCloudWidget.svelte +++ b/packages/web/src/widgets/PrivateCloudWidget.svelte @@ -41,22 +41,25 @@ const serverStatus = useServerStatus(); $: emptyCloudContent = ($cloudContentList || []).filter(x => !x.items?.length).map(x => x.folid); - $: cloudContentFlat = ($cloudContentList || []) - .flatMap(fld => fld.items ?? []) - .map(data => { - if (data.type == 'connection') { - const conid = `cloud://${data.folid}/${data.cntid}`; - const status = $serverStatus ? $serverStatus[$volatileConnectionMapStore[conid] || conid] : undefined; + $: cloudContentFlat = _.sortBy( + ($cloudContentList || []) + .flatMap(fld => fld.items ?? []) + .map(data => { + if (data.type == 'connection') { + const conid = `cloud://${data.folid}/${data.cntid}`; + const status = $serverStatus ? $serverStatus[$volatileConnectionMapStore[conid] || conid] : undefined; - return { - ...data, - conid, - status, - }; - } + return { + ...data, + conid, + status, + }; + } - return data; - }); + return data; + }), + 'name' + ); $: contentGroupMap = _.keyBy($cloudContentList || [], x => x.folid); // $: console.log('cloudContentFlat', cloudContentFlat); From 7a3b27227ad5ef3a3c961a1f75f1dd9d66464908 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Wed, 28 May 2025 13:21:52 +0200 Subject: [PATCH 093/129] stats fixed --- packages/api/src/utility/cloudIntf.js | 7 ++++++- packages/api/src/utility/hardwareFingerprint.js | 1 + packages/web/src/commands/stdCommands.ts | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/api/src/utility/cloudIntf.js b/packages/api/src/utility/cloudIntf.js index da67425b8..3e64de812 100644 --- a/packages/api/src/utility/cloudIntf.js +++ b/packages/api/src/utility/cloudIntf.js @@ -11,6 +11,8 @@ const { isProApp } = require('./checkLicense'); const socket = require('./socket'); const config = require('../controllers/config'); const simpleEncryptor = require('simple-encryptor'); +const currentVersion = require('../currentVersion'); +const { getPublicIpInfo } = require('./hardwareFingerprint'); const logger = getLogger('cloudIntf'); @@ -151,6 +153,8 @@ async function updateCloudFiles(isRefresh) { lastCloudFilesTags = ''; } + const ipInfo = await getPublicIpInfo(); + const tags = (await collectCloudFilesSearchTags()).join(','); let lastCheckedTm = 0; if (tags == lastCloudFilesTags && cloudFiles.length > 0) { @@ -162,11 +166,12 @@ async function updateCloudFiles(isRefresh) { const resp = await axios.default.get( `${DBGATE_CLOUD_URL}/public-cloud-updates?lastCheckedTm=${lastCheckedTm}&tags=${tags}&isRefresh=${ isRefresh ? 1 : 0 - }`, + }&country=${ipInfo?.country || ''}`, { headers: { ...getLicenseHttpHeaders(), ...(await getCloudSigninHeaders()), + 'x-app-version': currentVersion.version, }, } ); diff --git a/packages/api/src/utility/hardwareFingerprint.js b/packages/api/src/utility/hardwareFingerprint.js index 1be04fbb2..c99d86967 100644 --- a/packages/api/src/utility/hardwareFingerprint.js +++ b/packages/api/src/utility/hardwareFingerprint.js @@ -87,4 +87,5 @@ module.exports = { getHardwareFingerprint, getHardwareFingerprintHash, getPublicHardwareFingerprint, + getPublicIpInfo, }; diff --git a/packages/web/src/commands/stdCommands.ts b/packages/web/src/commands/stdCommands.ts index 4c1d822de..10f22e56c 100644 --- a/packages/web/src/commands/stdCommands.ts +++ b/packages/web/src/commands/stdCommands.ts @@ -132,7 +132,7 @@ registerCommand({ category: 'New', toolbarOrder: 1, name: 'Connection on Cloud', - testEnabled: () => !getCurrentConfig()?.runAsPortal && !getCurrentConfig()?.storageDatabase, + testEnabled: () => !getCurrentConfig()?.runAsPortal && !getCurrentConfig()?.storageDatabase && isProApp(), onClick: () => { openNewTab({ title: 'New Connection on Cloud', From 45d82dce041a41e8a55f651f6a79f85bd098606c Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Wed, 28 May 2025 15:55:53 +0200 Subject: [PATCH 094/129] tmp change --- packages/api/src/utility/cloudIntf.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api/src/utility/cloudIntf.js b/packages/api/src/utility/cloudIntf.js index 3e64de812..c51a4d910 100644 --- a/packages/api/src/utility/cloudIntf.js +++ b/packages/api/src/utility/cloudIntf.js @@ -21,13 +21,13 @@ let cloudFiles = null; const DBGATE_IDENTITY_URL = process.env.LOCAL_DBGATE_IDENTITY ? 'http://localhost:3103' : process.env.DEVWEB || process.env.DEVMODE - ? 'https://identity.dbgate.udolni.net' + ? 'https://identity.dbgate.io' // 'https://identity.dbgate.udolni.net' : 'https://identity.dbgate.io'; const DBGATE_CLOUD_URL = process.env.LOCAL_DBGATE_CLOUD ? 'http://localhost:3110' : process.env.DEVWEB || process.env.DEVMODE - ? 'https://cloud.dbgate.udolni.net' + ? 'https://cloud.dbgate.io' // 'https://cloud.dbgate.udolni.net' : 'https://cloud.dbgate.io'; async function createDbGateIdentitySession(client) { From aff7125914da771c1f2617b7a1beef091b02f08e Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Wed, 28 May 2025 16:44:58 +0200 Subject: [PATCH 095/129] Revert "tmp change" This reverts commit 45d82dce041a41e8a55f651f6a79f85bd098606c. --- packages/api/src/utility/cloudIntf.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api/src/utility/cloudIntf.js b/packages/api/src/utility/cloudIntf.js index c51a4d910..3e64de812 100644 --- a/packages/api/src/utility/cloudIntf.js +++ b/packages/api/src/utility/cloudIntf.js @@ -21,13 +21,13 @@ let cloudFiles = null; const DBGATE_IDENTITY_URL = process.env.LOCAL_DBGATE_IDENTITY ? 'http://localhost:3103' : process.env.DEVWEB || process.env.DEVMODE - ? 'https://identity.dbgate.io' // 'https://identity.dbgate.udolni.net' + ? 'https://identity.dbgate.udolni.net' : 'https://identity.dbgate.io'; const DBGATE_CLOUD_URL = process.env.LOCAL_DBGATE_CLOUD ? 'http://localhost:3110' : process.env.DEVWEB || process.env.DEVMODE - ? 'https://cloud.dbgate.io' // 'https://cloud.dbgate.udolni.net' + ? 'https://cloud.dbgate.udolni.net' : 'https://cloud.dbgate.io'; async function createDbGateIdentitySession(client) { From cb50d2838aec648de5b7ef4a7162801e6ec7f5a8 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Wed, 28 May 2025 17:37:55 +0200 Subject: [PATCH 096/129] license limit modal --- packages/api/src/utility/cloudIntf.js | 4 +- .../modals/LicenseLimitMessageModal.svelte | 62 +++++++++++++++++++ packages/web/src/utility/api.ts | 9 ++- 3 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 packages/web/src/modals/LicenseLimitMessageModal.svelte diff --git a/packages/api/src/utility/cloudIntf.js b/packages/api/src/utility/cloudIntf.js index 3e64de812..78d28a53a 100644 --- a/packages/api/src/utility/cloudIntf.js +++ b/packages/api/src/utility/cloudIntf.js @@ -266,9 +266,9 @@ async function callCloudApiPost(endpoint, body, signinHolder = null) { }, validateStatus: status => status < 500, }); - const { errorMessage } = resp.data; + const { errorMessage, isLicenseLimit } = resp.data; if (errorMessage) { - return { apiErrorMessage: errorMessage }; + return { apiErrorMessage: errorMessage, apiErrorIsLicenseLimit: isLicenseLimit }; } return resp.data; } diff --git a/packages/web/src/modals/LicenseLimitMessageModal.svelte b/packages/web/src/modals/LicenseLimitMessageModal.svelte new file mode 100644 index 000000000..624b632b5 --- /dev/null +++ b/packages/web/src/modals/LicenseLimitMessageModal.svelte @@ -0,0 +1,62 @@ + + + + +
License limit error
+ +
+
+ +
+
+

+ Cloud operation ended with error:
+ {message} +

+ +

+ This is a limitation of the free version of DbGate. To continue using cloud operations, please purchase DbGate + Premium. +

+

Free version limit:

+
    +
  • max 5 connections
  • +
  • plus max 5 files
  • +
+
+
+ +
+ + +
+
+
+ + diff --git a/packages/web/src/utility/api.ts b/packages/web/src/utility/api.ts index 8b80c794a..96b88c060 100644 --- a/packages/web/src/utility/api.ts +++ b/packages/web/src/utility/api.ts @@ -15,6 +15,7 @@ import { isAdminPage, isOneOfPage } from './pageDefs'; import { openWebLink } from './simpleTools'; import { serializeJsTypesReplacer } from 'dbgate-tools'; import { cloudSigninTokenHolder } from '../stores'; +import LicenseLimitMessageModal from '../modals/LicenseLimitMessageModal.svelte'; export const strmid = uuidv1(); @@ -121,7 +122,13 @@ async function processApiResponse(route, args, resp) { // missingCredentials: true, // }; } else if (resp?.apiErrorMessage) { - showSnackbarError('API error:' + resp?.apiErrorMessage); + if (resp?.apiErrorIsLicenseLimit) { + showModal(LicenseLimitMessageModal, { + message: resp.apiErrorMessage, + }); + } else { + showSnackbarError('API error:' + resp?.apiErrorMessage); + } return { errorMessage: resp.apiErrorMessage, }; From e836fa3d3811837511ccb3bcc62c4de84ce0194d Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Thu, 29 May 2025 12:44:31 +0200 Subject: [PATCH 097/129] show license - better UX --- packages/api/src/utility/cloudIntf.js | 8 +++-- .../modals/LicenseLimitMessageModal.svelte | 29 ++++++++++++------- packages/web/src/utility/api.ts | 1 + 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/packages/api/src/utility/cloudIntf.js b/packages/api/src/utility/cloudIntf.js index 78d28a53a..329a55fc0 100644 --- a/packages/api/src/utility/cloudIntf.js +++ b/packages/api/src/utility/cloudIntf.js @@ -266,9 +266,13 @@ async function callCloudApiPost(endpoint, body, signinHolder = null) { }, validateStatus: status => status < 500, }); - const { errorMessage, isLicenseLimit } = resp.data; + const { errorMessage, isLicenseLimit, limitedLicenseLimits } = resp.data; if (errorMessage) { - return { apiErrorMessage: errorMessage, apiErrorIsLicenseLimit: isLicenseLimit }; + return { + apiErrorMessage: errorMessage, + apiErrorIsLicenseLimit: isLicenseLimit, + apiErrorLimitedLicenseLimits: limitedLicenseLimits, + }; } return resp.data; } diff --git a/packages/web/src/modals/LicenseLimitMessageModal.svelte b/packages/web/src/modals/LicenseLimitMessageModal.svelte index 624b632b5..4121159cc 100644 --- a/packages/web/src/modals/LicenseLimitMessageModal.svelte +++ b/packages/web/src/modals/LicenseLimitMessageModal.svelte @@ -3,17 +3,14 @@ import FormProvider from '../forms/FormProvider.svelte'; import FormSubmit from '../forms/FormSubmit.svelte'; import FontIcon from '../icons/FontIcon.svelte'; + import { isProApp } from '../utility/proTools'; import { openWebLink } from '../utility/simpleTools'; import ModalBase from './ModalBase.svelte'; import { closeCurrentModal } from './modalTools'; export let message; - - function handlePurchase() { - closeCurrentModal(); - openWebLink('https://dbgate.io/purchase/premium/', '_blank'); - } + export let licenseLimits; @@ -31,20 +28,32 @@

- This is a limitation of the free version of DbGate. To continue using cloud operations, please purchase DbGate - Premium. + This is a limitation of the free version of DbGate. To continue using cloud operations, please {#if !isProApp()}download + and{/if} purchase DbGate Premium.

Free version limit:

    -
  • max 5 connections
  • -
  • plus max 5 files
  • + {#each licenseLimits || [] as limit} +
  • {limit}
  • + {/each}
- + {#if !isProApp()} + openWebLink('https://dbgate.io/download/')} + skipWidth + /> + {/if} + openWebLink('https://dbgate.io/purchase/premium/')} + skipWidth + />
diff --git a/packages/web/src/utility/api.ts b/packages/web/src/utility/api.ts index 96b88c060..043699ff0 100644 --- a/packages/web/src/utility/api.ts +++ b/packages/web/src/utility/api.ts @@ -125,6 +125,7 @@ async function processApiResponse(route, args, resp) { if (resp?.apiErrorIsLicenseLimit) { showModal(LicenseLimitMessageModal, { message: resp.apiErrorMessage, + licenseLimits: resp.apiErrorLimitedLicenseLimits, }); } else { showSnackbarError('API error:' + resp?.apiErrorMessage); From a9958af818877da23c0202a89f6477285b1c4cbf Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Thu, 29 May 2025 13:21:17 +0200 Subject: [PATCH 098/129] v6.4.3-beta.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8bfdbd903..8266178bd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "6.4.3-alpha.1", + "version": "6.4.3-beta.3", "name": "dbgate-all", "workspaces": [ "packages/*", From 356d25e54809c5df3c6ac054ce520d421f8fce94 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Thu, 29 May 2025 13:59:53 +0200 Subject: [PATCH 099/129] build app check --- workflow-templates/build-app.tpl.yaml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/workflow-templates/build-app.tpl.yaml b/workflow-templates/build-app.tpl.yaml index 37598e53a..9784e2be7 100644 --- a/workflow-templates/build-app.tpl.yaml +++ b/workflow-templates/build-app.tpl.yaml @@ -5,6 +5,7 @@ _templates: - _community - _beta - _channel + - _publish string-replace: "<>": '' "<>": '--community' @@ -16,12 +17,22 @@ _templates: defs: - _community - _stable + - _publish string-replace: "<>": '' "<>": '--community' "<>": 'app/dist' "<>": 'latest' - + _community_check: + file: build-app-check.yaml + defs: + - _community + - _beta + string-replace: + "<>": '' + "<>": '--community' + "<>": 'app/dist' + "<>": 'check' _premium_beta: file: build-app-pro-beta.yaml @@ -29,6 +40,7 @@ _templates: - _premium - _beta - _channel + - _publish string-replace: "<>": | cd .. @@ -37,12 +49,14 @@ _templates: "<>": '../dbgate-merged/app/dist' "<>": 'premium-beta' "<>": 'premium-beta' + _premium_stable: file: build-app-pro.yaml defs: - _premium - _stable - _channel + - _publish string-replace: "<>": | cd .. @@ -57,6 +71,7 @@ name: _community_stable: Electron app _premium_beta: Electron app PREMIUM BETA _premium_stable: Electron app PREMIUM + _community_check: Electron app check build on: push: @@ -64,6 +79,7 @@ on: - _community_beta: 'v[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+' _premium_beta: 'v[0-9]+.[0-9]+.[0-9]+-premium-beta.[0-9]+' _stable: 'v[0-9]+.[0-9]+.[0-9]+' + _community_check: 'check-[0-9]+-[0-9]+-[0-9]+-[0-9]+' # - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 # branches: @@ -197,12 +213,14 @@ jobs: mv app/dist/dbgate-pad.xml artifacts/ || true - name: Upload artifacts + _if: _publish uses: actions/upload-artifact@v4 with: name: ${{ matrix.os }} path: artifacts - name: Release + _if: _publish uses: softprops/action-gh-release@v1 if: startsWith(github.ref, 'refs/tags/') with: From 5b04adb21f6b6747adf8a237e0a17afb6f5308e4 Mon Sep 17 00:00:00 2001 From: CI workflows Date: Thu, 29 May 2025 12:00:15 +0000 Subject: [PATCH 100/129] chore: auto-update github workflows --- .github/workflows/build-app-check.yaml | 111 +++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 .github/workflows/build-app-check.yaml diff --git a/.github/workflows/build-app-check.yaml b/.github/workflows/build-app-check.yaml new file mode 100644 index 000000000..7cde6c46d --- /dev/null +++ b/.github/workflows/build-app-check.yaml @@ -0,0 +1,111 @@ +# -------------------------------------------------------------------------------------------- +# This file is generated. Do not edit manually +# -------------------------------------------------------------------------------------------- +name: Electron app check build +'on': + push: + tags: + - check-[0-9]+-[0-9]+-[0-9]+-[0-9]+ +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - macos-14 + - windows-2022 + - ubuntu-22.04 + steps: + - name: Install python 3.11 (MacOS) + if: matrix.os == 'macos-14' + run: | + brew install python@3.11 + echo "PYTHON=/opt/homebrew/bin/python3.11" >> $GITHUB_ENV + - name: Context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@v2 + with: + fetch-depth: 1 + - name: Use Node.js 22.x + uses: actions/setup-node@v1 + with: + node-version: 22.x + - name: adjustPackageJson + run: | + + node adjustPackageJson --community + - name: yarn set timeout + run: | + + yarn config set network-timeout 100000 + - name: yarn install + run: | + + yarn install + - name: setCurrentVersion + run: | + + yarn setCurrentVersion + - name: printSecrets + run: | + + yarn printSecrets + env: + GIST_UPLOAD_SECRET: ${{secrets.GIST_UPLOAD_SECRET}} + - name: fillPackagedPlugins + run: | + + yarn fillPackagedPlugins + - name: Install Snapcraft + if: matrix.os == 'ubuntu-22.04' + uses: samuelmeuli/action-snapcraft@v1 + - name: Publish + run: | + + yarn run build:app + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + WIN_CSC_LINK: ${{ secrets.WINCERT_2025 }} + WIN_CSC_KEY_PASSWORD: ${{ secrets.WINCERT_2025_PASSWORD }} + CSC_LINK: ${{ secrets.APPLECERT_CERTIFICATE }} + CSC_KEY_PASSWORD: ${{ secrets.APPLECERT_PASSWORD }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} + SNAPCRAFT_STORE_CREDENTIALS: ${{secrets.SNAPCRAFT_LOGIN}} + APPLE_APP_SPECIFIC_PASSWORD: ${{secrets.APPLE_APP_SPECIFIC_PASSWORD}} + - name: Copy artifacts + run: | + mkdir artifacts + + cp app/dist/*.deb artifacts/dbgate-check.deb || true + cp app/dist/*x86*.AppImage artifacts/dbgate-check.AppImage || true + cp app/dist/*arm64*.AppImage artifacts/dbgate-check-arm64.AppImage || true + cp app/dist/*armv7l*.AppImage artifacts/dbgate-check-armv7l.AppImage || true + cp app/dist/*win*.exe artifacts/dbgate-check.exe || true + cp app/dist/*win_x64.zip artifacts/dbgate-windows-check.zip || true + cp app/dist/*win_arm64.zip artifacts/dbgate-windows-check-arm64.zip || true + cp app/dist/*-mac_universal.dmg artifacts/dbgate-check.dmg || true + cp app/dist/*-mac_x64.dmg artifacts/dbgate-check-x64.dmg || true + cp app/dist/*-mac_arm64.dmg artifacts/dbgate-check-arm64.dmg || true + mv app/dist/*.snap artifacts/dbgate-check.snap || true + + mv app/dist/*.exe artifacts/ || true + mv app/dist/*.zip artifacts/ || true + mv app/dist/*.tar.gz artifacts/ || true + mv app/dist/*.AppImage artifacts/ || true + mv app/dist/*.deb artifacts/ || true + mv app/dist/*.snap artifacts/ || true + mv app/dist/*.dmg artifacts/ || true + mv app/dist/*.blockmap artifacts/ || true + + mv app/dist/*.yml artifacts/ || true + rm artifacts/builder-debug.yml + - name: Print content of notarization-error.log + if: failure() && matrix.os == 'macos-14' + run: | + + find . -type f -name "notarization-error.log" -exec echo "=== Start of {} ===" \; -exec cat {} \; -exec echo "=== End of {} ===" \; From 9dc847b72f5a2ced05c3a514922b24d18d720039 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Thu, 29 May 2025 14:05:10 +0200 Subject: [PATCH 101/129] fix --- workflow-templates/build-app.tpl.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflow-templates/build-app.tpl.yaml b/workflow-templates/build-app.tpl.yaml index 9784e2be7..e45e9040c 100644 --- a/workflow-templates/build-app.tpl.yaml +++ b/workflow-templates/build-app.tpl.yaml @@ -79,7 +79,7 @@ on: - _community_beta: 'v[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+' _premium_beta: 'v[0-9]+.[0-9]+.[0-9]+-premium-beta.[0-9]+' _stable: 'v[0-9]+.[0-9]+.[0-9]+' - _community_check: 'check-[0-9]+-[0-9]+-[0-9]+-[0-9]+' + _community_check: 'check-[0-9]+-[0-9]+-[0-9]+.[0-9]+' # - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 # branches: From d1c52548b093f3dcf7d4bf719357ff18622e5ab0 Mon Sep 17 00:00:00 2001 From: CI workflows Date: Thu, 29 May 2025 12:05:29 +0000 Subject: [PATCH 102/129] chore: auto-update github workflows --- .github/workflows/build-app-check.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-app-check.yaml b/.github/workflows/build-app-check.yaml index 7cde6c46d..6ee1a1e6c 100644 --- a/.github/workflows/build-app-check.yaml +++ b/.github/workflows/build-app-check.yaml @@ -5,7 +5,7 @@ name: Electron app check build 'on': push: tags: - - check-[0-9]+-[0-9]+-[0-9]+-[0-9]+ + - check-[0-9]+-[0-9]+-[0-9]+.[0-9]+ jobs: build: runs-on: ${{ matrix.os }} From 6ad218f354e69a696f758a6b2d88e276989a27a1 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Thu, 29 May 2025 14:31:08 +0200 Subject: [PATCH 103/129] upload artifacts forr check build --- workflow-templates/build-app.tpl.yaml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/workflow-templates/build-app.tpl.yaml b/workflow-templates/build-app.tpl.yaml index e45e9040c..380804273 100644 --- a/workflow-templates/build-app.tpl.yaml +++ b/workflow-templates/build-app.tpl.yaml @@ -5,7 +5,7 @@ _templates: - _community - _beta - _channel - - _publish + - _release string-replace: "<>": '' "<>": '--community' @@ -17,7 +17,7 @@ _templates: defs: - _community - _stable - - _publish + - _release string-replace: "<>": '' "<>": '--community' @@ -40,7 +40,7 @@ _templates: - _premium - _beta - _channel - - _publish + - _release string-replace: "<>": | cd .. @@ -56,7 +56,7 @@ _templates: - _premium - _stable - _channel - - _publish + - _release string-replace: "<>": | cd .. @@ -213,7 +213,6 @@ jobs: mv app/dist/dbgate-pad.xml artifacts/ || true - name: Upload artifacts - _if: _publish uses: actions/upload-artifact@v4 with: name: ${{ matrix.os }} From 25060c14775c7ec085b755507ce9cab24feb4ae2 Mon Sep 17 00:00:00 2001 From: CI workflows Date: Thu, 29 May 2025 12:31:31 +0000 Subject: [PATCH 104/129] chore: auto-update github workflows --- .github/workflows/build-app-beta.yaml | 8 -------- .github/workflows/build-app-check.yaml | 5 +++++ .github/workflows/build-app-pro-beta.yaml | 8 -------- .github/workflows/build-app-pro.yaml | 8 -------- .github/workflows/build-app.yaml | 8 -------- 5 files changed, 5 insertions(+), 32 deletions(-) diff --git a/.github/workflows/build-app-beta.yaml b/.github/workflows/build-app-beta.yaml index b238d00c0..da50fbb69 100644 --- a/.github/workflows/build-app-beta.yaml +++ b/.github/workflows/build-app-beta.yaml @@ -113,14 +113,6 @@ jobs: with: name: ${{ matrix.os }} path: artifacts - - name: Release - uses: softprops/action-gh-release@v1 - if: startsWith(github.ref, 'refs/tags/') - with: - files: artifacts/** - prerelease: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Print content of notarization-error.log if: failure() && matrix.os == 'macos-14' run: | diff --git a/.github/workflows/build-app-check.yaml b/.github/workflows/build-app-check.yaml index 6ee1a1e6c..d59b10815 100644 --- a/.github/workflows/build-app-check.yaml +++ b/.github/workflows/build-app-check.yaml @@ -104,6 +104,11 @@ jobs: mv app/dist/*.yml artifacts/ || true rm artifacts/builder-debug.yml + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.os }} + path: artifacts - name: Print content of notarization-error.log if: failure() && matrix.os == 'macos-14' run: | diff --git a/.github/workflows/build-app-pro-beta.yaml b/.github/workflows/build-app-pro-beta.yaml index f9f088189..b9d65660a 100644 --- a/.github/workflows/build-app-pro-beta.yaml +++ b/.github/workflows/build-app-pro-beta.yaml @@ -144,14 +144,6 @@ jobs: with: name: ${{ matrix.os }} path: artifacts - - name: Release - uses: softprops/action-gh-release@v1 - if: startsWith(github.ref, 'refs/tags/') - with: - files: artifacts/** - prerelease: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Print content of notarization-error.log if: failure() && matrix.os == 'macos-14' run: | diff --git a/.github/workflows/build-app-pro.yaml b/.github/workflows/build-app-pro.yaml index a95c852ed..d763001d8 100644 --- a/.github/workflows/build-app-pro.yaml +++ b/.github/workflows/build-app-pro.yaml @@ -144,14 +144,6 @@ jobs: with: name: ${{ matrix.os }} path: artifacts - - name: Release - uses: softprops/action-gh-release@v1 - if: startsWith(github.ref, 'refs/tags/') - with: - files: artifacts/** - prerelease: false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Print content of notarization-error.log if: failure() && matrix.os == 'macos-14' run: | diff --git a/.github/workflows/build-app.yaml b/.github/workflows/build-app.yaml index db0785eac..da2dd7db2 100644 --- a/.github/workflows/build-app.yaml +++ b/.github/workflows/build-app.yaml @@ -116,14 +116,6 @@ jobs: with: name: ${{ matrix.os }} path: artifacts - - name: Release - uses: softprops/action-gh-release@v1 - if: startsWith(github.ref, 'refs/tags/') - with: - files: artifacts/** - prerelease: false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Print content of notarization-error.log if: failure() && matrix.os == 'macos-14' run: | From f405124ce43822c936db6dddb22bb24f4926c831 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Thu, 29 May 2025 14:40:20 +0200 Subject: [PATCH 105/129] fix --- workflow-templates/build-app.tpl.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflow-templates/build-app.tpl.yaml b/workflow-templates/build-app.tpl.yaml index 380804273..61ba294c9 100644 --- a/workflow-templates/build-app.tpl.yaml +++ b/workflow-templates/build-app.tpl.yaml @@ -219,7 +219,7 @@ jobs: path: artifacts - name: Release - _if: _publish + _if: _release uses: softprops/action-gh-release@v1 if: startsWith(github.ref, 'refs/tags/') with: From 1794b86041c7170de0d2009271080969e7ecae76 Mon Sep 17 00:00:00 2001 From: CI workflows Date: Thu, 29 May 2025 12:40:42 +0000 Subject: [PATCH 106/129] chore: auto-update github workflows --- .github/workflows/build-app-beta.yaml | 8 ++++++++ .github/workflows/build-app-pro-beta.yaml | 8 ++++++++ .github/workflows/build-app-pro.yaml | 8 ++++++++ .github/workflows/build-app.yaml | 8 ++++++++ 4 files changed, 32 insertions(+) diff --git a/.github/workflows/build-app-beta.yaml b/.github/workflows/build-app-beta.yaml index da50fbb69..b238d00c0 100644 --- a/.github/workflows/build-app-beta.yaml +++ b/.github/workflows/build-app-beta.yaml @@ -113,6 +113,14 @@ jobs: with: name: ${{ matrix.os }} path: artifacts + - name: Release + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + files: artifacts/** + prerelease: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Print content of notarization-error.log if: failure() && matrix.os == 'macos-14' run: | diff --git a/.github/workflows/build-app-pro-beta.yaml b/.github/workflows/build-app-pro-beta.yaml index b9d65660a..f9f088189 100644 --- a/.github/workflows/build-app-pro-beta.yaml +++ b/.github/workflows/build-app-pro-beta.yaml @@ -144,6 +144,14 @@ jobs: with: name: ${{ matrix.os }} path: artifacts + - name: Release + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + files: artifacts/** + prerelease: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Print content of notarization-error.log if: failure() && matrix.os == 'macos-14' run: | diff --git a/.github/workflows/build-app-pro.yaml b/.github/workflows/build-app-pro.yaml index d763001d8..a95c852ed 100644 --- a/.github/workflows/build-app-pro.yaml +++ b/.github/workflows/build-app-pro.yaml @@ -144,6 +144,14 @@ jobs: with: name: ${{ matrix.os }} path: artifacts + - name: Release + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + files: artifacts/** + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Print content of notarization-error.log if: failure() && matrix.os == 'macos-14' run: | diff --git a/.github/workflows/build-app.yaml b/.github/workflows/build-app.yaml index da2dd7db2..db0785eac 100644 --- a/.github/workflows/build-app.yaml +++ b/.github/workflows/build-app.yaml @@ -116,6 +116,14 @@ jobs: with: name: ${{ matrix.os }} path: artifacts + - name: Release + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + files: artifacts/** + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Print content of notarization-error.log if: failure() && matrix.os == 'macos-14' run: | From db6b7f52eb3ddbe3d7168a0cb675c7ac5fd58d37 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Thu, 29 May 2025 14:54:08 +0200 Subject: [PATCH 107/129] removed charts & profiler --- packages/web/src/charts/ChartCore.svelte | 87 ------- packages/web/src/charts/ChartEditor.svelte | 170 -------------- packages/web/src/charts/DataChart.svelte | 198 ---------------- packages/web/src/charts/chartDataLoader.ts | 133 ----------- packages/web/src/tabs/ChartTab.svelte | 118 ---------- packages/web/src/tabs/ProfilerTab.svelte | 250 --------------------- packages/web/src/tabs/index.js | 4 - 7 files changed, 960 deletions(-) delete mode 100644 packages/web/src/charts/ChartCore.svelte delete mode 100644 packages/web/src/charts/ChartEditor.svelte delete mode 100644 packages/web/src/charts/DataChart.svelte delete mode 100644 packages/web/src/charts/chartDataLoader.ts delete mode 100644 packages/web/src/tabs/ChartTab.svelte delete mode 100644 packages/web/src/tabs/ProfilerTab.svelte diff --git a/packages/web/src/charts/ChartCore.svelte b/packages/web/src/charts/ChartCore.svelte deleted file mode 100644 index 1fc893003..000000000 --- a/packages/web/src/charts/ChartCore.svelte +++ /dev/null @@ -1,87 +0,0 @@ - - - - - diff --git a/packages/web/src/charts/ChartEditor.svelte b/packages/web/src/charts/ChartEditor.svelte deleted file mode 100644 index aadf922b1..000000000 --- a/packages/web/src/charts/ChartEditor.svelte +++ /dev/null @@ -1,170 +0,0 @@ - - - - -
- - - - - - - - - {#if $configStore.chartType == 'line'} - - {/if} - - - - - - {#if availableColumnNames.length > 0} - ({ value: col, label: col }))} - /> - {/if} - - {#each availableColumnNames as col (col)} - - {#if config[`dataColumn_${col}`]} - - - {/if} - {/each} - - - -
- - - {#if errorLoadingColumns} - - {:else if errorLoadingData} - - {:else} - - {/if} - -
-
- - diff --git a/packages/web/src/charts/DataChart.svelte b/packages/web/src/charts/DataChart.svelte deleted file mode 100644 index 62d429b0a..000000000 --- a/packages/web/src/charts/DataChart.svelte +++ /dev/null @@ -1,198 +0,0 @@ - - - - -
- {#if chartData} - {#key `${$values.chartType}|${clientWidth}|${clientHeight}`} - - {/key} - {/if} -
- - diff --git a/packages/web/src/charts/chartDataLoader.ts b/packages/web/src/charts/chartDataLoader.ts deleted file mode 100644 index 440b8c956..000000000 --- a/packages/web/src/charts/chartDataLoader.ts +++ /dev/null @@ -1,133 +0,0 @@ -import type { Select } from 'dbgate-sqltree'; -import type { EngineDriver } from 'dbgate-types'; -import _ from 'lodash'; -import { apiCall } from '../utility/api'; - -export async function loadChartStructure(driver: EngineDriver, conid, database, sql) { - const select: Select = { - commandType: 'select', - selectAll: true, - topRecords: 1, - from: { - subQueryString: sql, - alias: 'subq', - }, - }; - - const resp = await apiCall('database-connections/sql-select', { conid, database, select }); - if (resp.errorMessage) throw new Error(resp.errorMessage); - return resp.columns.map(x => x.columnName); -} - -export async function loadChartData(driver: EngineDriver, conid, database, sql, config) { - const dataColumns = extractDataColumns(config); - const { labelColumn, truncateFrom, truncateLimit, showRelativeValues } = config; - if (!labelColumn || !dataColumns || dataColumns.length == 0) return null; - - const select: Select = { - commandType: 'select', - - columns: [ - { - exprType: 'column', - source: { alias: 'subq' }, - columnName: labelColumn, - alias: labelColumn, - }, - // @ts-ignore - ...dataColumns.map(columnName => ({ - exprType: 'call', - func: 'SUM', - args: [ - { - exprType: 'column', - columnName, - source: { alias: 'subq' }, - }, - ], - alias: columnName, - })), - ], - topRecords: truncateLimit || 100, - from: { - subQueryString: sql, - alias: 'subq', - }, - groupBy: [ - { - exprType: 'column', - source: { alias: 'subq' }, - columnName: labelColumn, - }, - ], - orderBy: [ - { - exprType: 'column', - source: { alias: 'subq' }, - columnName: labelColumn, - direction: truncateFrom == 'end' ? 'DESC' : 'ASC', - }, - ], - }; - - const resp = await apiCall('database-connections/sql-select', { conid, database, select }); - let { rows, columns, errorMessage } = resp; - if (errorMessage) { - throw new Error(errorMessage); - } - if (truncateFrom == 'end' && rows) { - rows = _.reverse([...rows]); - } - if (showRelativeValues) { - const maxValues = dataColumns.map(col => _.max(rows.map(row => row[col]))); - for (const [col, max] of _.zip(dataColumns, maxValues)) { - if (!max) continue; - if (!_.isNumber(max)) continue; - if (!(max > 0)) continue; - rows = rows.map(row => ({ - ...row, - [col]: (row[col] / max) * 100, - })); - // columns = columns.map((x) => { - // if (x.columnName == col) { - // return { columnName: `${col} %` }; - // } - // return x; - // }); - } - } - - console.log('Loaded chart data', { columns, rows }); - - return { - columns, - rows, - }; -} - -export function extractDataColumns(values) { - const dataColumns = []; - for (const key in values) { - if (key.startsWith('dataColumn_') && values[key]) { - dataColumns.push(key.substring('dataColumn_'.length)); - } - } - return dataColumns; -} -export function extractDataColumnColors(values, dataColumns) { - const res = {}; - for (const column of dataColumns) { - const color = values[`dataColumnColor_${column}`]; - if (color) res[column] = color; - } - return res; -} - -export function extractDataColumnLabels(values, dataColumns) { - const res = {}; - for (const column of dataColumns) { - const label = values[`dataColumnLabel_${column}`]; - if (label) res[column] = label; - } - return res; -} diff --git a/packages/web/src/tabs/ChartTab.svelte b/packages/web/src/tabs/ChartTab.svelte deleted file mode 100644 index b0efeec5c..000000000 --- a/packages/web/src/tabs/ChartTab.svelte +++ /dev/null @@ -1,118 +0,0 @@ - - - - -{#if $editorState.isLoading} - -{:else if $editorState.errorMessage} - -{:else} - -{/if} diff --git a/packages/web/src/tabs/ProfilerTab.svelte b/packages/web/src/tabs/ProfilerTab.svelte deleted file mode 100644 index 774f6d4de..000000000 --- a/packages/web/src/tabs/ProfilerTab.svelte +++ /dev/null @@ -1,250 +0,0 @@ - - - - - - {#if jslid} - - - {#key jslid} - - {/key} - - - {#if isLoadingChart} - - {:else} - - {/if} - - - {:else} - - {/if} - - - - - - - diff --git a/packages/web/src/tabs/index.js b/packages/web/src/tabs/index.js index e0baa54b8..c8ac250f1 100644 --- a/packages/web/src/tabs/index.js +++ b/packages/web/src/tabs/index.js @@ -6,7 +6,6 @@ import * as QueryTab from './QueryTab.svelte'; import * as ShellTab from './ShellTab.svelte'; import * as ArchiveFileTab from './ArchiveFileTab.svelte'; import * as PluginTab from './PluginTab.svelte'; -import * as ChartTab from './ChartTab.svelte'; import * as MarkdownEditorTab from './MarkdownEditorTab.svelte'; import * as MarkdownViewTab from './MarkdownViewTab.svelte'; import * as MarkdownPreviewTab from './MarkdownPreviewTab.svelte'; @@ -23,7 +22,6 @@ import * as QueryDataTab from './QueryDataTab.svelte'; import * as ConnectionTab from './ConnectionTab.svelte'; import * as MapTab from './MapTab.svelte'; import * as ServerSummaryTab from './ServerSummaryTab.svelte'; -import * as ProfilerTab from './ProfilerTab.svelte'; import * as ImportExportTab from './ImportExportTab.svelte'; import * as SqlObjectTab from './SqlObjectTab.svelte'; @@ -38,7 +36,6 @@ export default { ShellTab, ArchiveFileTab, PluginTab, - ChartTab, MarkdownEditorTab, MarkdownViewTab, MarkdownPreviewTab, @@ -55,7 +52,6 @@ export default { ConnectionTab, MapTab, ServerSummaryTab, - ProfilerTab, ImportExportTab, SqlObjectTab, ...protabs, From b4ef640052f391923b2d35f00fde9c7f29c0b5e6 Mon Sep 17 00:00:00 2001 From: CI workflows Date: Thu, 29 May 2025 12:54:50 +0000 Subject: [PATCH 108/129] Update pro ref --- workflow-templates/includes.tpl.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflow-templates/includes.tpl.yaml b/workflow-templates/includes.tpl.yaml index 8b7a5a21f..2c3f84f30 100644 --- a/workflow-templates/includes.tpl.yaml +++ b/workflow-templates/includes.tpl.yaml @@ -7,7 +7,7 @@ checkout-and-merge-pro: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: e75fc4967ca3a4c379aee89d7911c5ec909306ce + ref: 1b503a70268bcaac850697db1fc1dc0237caf5cc - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro From c0b41987aa861212a8737dfb031dcbfc4a2df3fc Mon Sep 17 00:00:00 2001 From: CI workflows Date: Thu, 29 May 2025 12:55:11 +0000 Subject: [PATCH 109/129] chore: auto-update github workflows --- .github/workflows/build-app-pro-beta.yaml | 2 +- .github/workflows/build-app-pro.yaml | 2 +- .github/workflows/build-cloud-pro.yaml | 2 +- .github/workflows/build-docker-pro.yaml | 2 +- .github/workflows/build-npm-pro.yaml | 2 +- .github/workflows/e2e-pro.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-app-pro-beta.yaml b/.github/workflows/build-app-pro-beta.yaml index f9f088189..5be4b4e6c 100644 --- a/.github/workflows/build-app-pro-beta.yaml +++ b/.github/workflows/build-app-pro-beta.yaml @@ -39,7 +39,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: e75fc4967ca3a4c379aee89d7911c5ec909306ce + ref: 1b503a70268bcaac850697db1fc1dc0237caf5cc - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/build-app-pro.yaml b/.github/workflows/build-app-pro.yaml index a95c852ed..b880a1754 100644 --- a/.github/workflows/build-app-pro.yaml +++ b/.github/workflows/build-app-pro.yaml @@ -39,7 +39,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: e75fc4967ca3a4c379aee89d7911c5ec909306ce + ref: 1b503a70268bcaac850697db1fc1dc0237caf5cc - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/build-cloud-pro.yaml b/.github/workflows/build-cloud-pro.yaml index 16a170c1c..b65f6566b 100644 --- a/.github/workflows/build-cloud-pro.yaml +++ b/.github/workflows/build-cloud-pro.yaml @@ -39,7 +39,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: e75fc4967ca3a4c379aee89d7911c5ec909306ce + ref: 1b503a70268bcaac850697db1fc1dc0237caf5cc - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/build-docker-pro.yaml b/.github/workflows/build-docker-pro.yaml index 69f8a0b80..4634b97a4 100644 --- a/.github/workflows/build-docker-pro.yaml +++ b/.github/workflows/build-docker-pro.yaml @@ -44,7 +44,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: e75fc4967ca3a4c379aee89d7911c5ec909306ce + ref: 1b503a70268bcaac850697db1fc1dc0237caf5cc - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/build-npm-pro.yaml b/.github/workflows/build-npm-pro.yaml index be73f5cc0..b6807ca2b 100644 --- a/.github/workflows/build-npm-pro.yaml +++ b/.github/workflows/build-npm-pro.yaml @@ -32,7 +32,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: e75fc4967ca3a4c379aee89d7911c5ec909306ce + ref: 1b503a70268bcaac850697db1fc1dc0237caf5cc - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/e2e-pro.yaml b/.github/workflows/e2e-pro.yaml index c1d2c56d9..8a53b58b3 100644 --- a/.github/workflows/e2e-pro.yaml +++ b/.github/workflows/e2e-pro.yaml @@ -26,7 +26,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: e75fc4967ca3a4c379aee89d7911c5ec909306ce + ref: 1b503a70268bcaac850697db1fc1dc0237caf5cc - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro From 212b26b960723ab3adb31b65e746a36a5270bb50 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Thu, 29 May 2025 14:56:07 +0200 Subject: [PATCH 110/129] temporatily disable MognoDB profiler support --- packages/web/src/appobj/ArchiveFileAppObject.svelte | 2 ++ packages/web/src/appobj/DatabaseAppObject.svelte | 1 + plugins/dbgate-plugin-mongo/src/frontend/driver.js | 3 ++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/web/src/appobj/ArchiveFileAppObject.svelte b/packages/web/src/appobj/ArchiveFileAppObject.svelte index 172bd33eb..62f09e66a 100644 --- a/packages/web/src/appobj/ArchiveFileAppObject.svelte +++ b/packages/web/src/appobj/ArchiveFileAppObject.svelte @@ -81,6 +81,7 @@ import ConfirmModal from '../modals/ConfirmModal.svelte'; import { apiCall } from '../utility/api'; import { openImportExportTab } from '../utility/importExportTools'; + import { isProApp } from '../utility/proTools'; export let data; $: isZipped = data.folderName?.endsWith('.zip'); @@ -187,6 +188,7 @@ data.fileType.endsWith('.sql') && { text: 'Open SQL', onClick: handleOpenSqlFile }, data.fileType.endsWith('.yaml') && { text: 'Open YAML', onClick: handleOpenYamlFile }, !isZipped && + isProApp() && data.fileType == 'jsonl' && { text: 'Open in profiler', submenu: getExtensions() diff --git a/packages/web/src/appobj/DatabaseAppObject.svelte b/packages/web/src/appobj/DatabaseAppObject.svelte index b9b9e11d1..100c643b2 100644 --- a/packages/web/src/appobj/DatabaseAppObject.svelte +++ b/packages/web/src/appobj/DatabaseAppObject.svelte @@ -430,6 +430,7 @@ await dbgateApi.executeQuery(${JSON.stringify( driver?.databaseEngineTypes?.includes('sql') && hasPermission(`dbops/sql-generator`) && { onClick: handleSqlGenerator, text: 'SQL Generator' }, driver?.supportsDatabaseProfiler && + isProApp() && hasPermission(`dbops/profiler`) && { onClick: handleDatabaseProfiler, text: 'Database profiler' }, // isSqlOrDoc && // isSqlOrDoc && diff --git a/plugins/dbgate-plugin-mongo/src/frontend/driver.js b/plugins/dbgate-plugin-mongo/src/frontend/driver.js index 8b93696db..028eb7706 100644 --- a/plugins/dbgate-plugin-mongo/src/frontend/driver.js +++ b/plugins/dbgate-plugin-mongo/src/frontend/driver.js @@ -42,7 +42,8 @@ const driver = { defaultPort: 27017, supportsDatabaseUrl: true, supportsServerSummary: true, - supportsDatabaseProfiler: true, + // temporatily disable MongoDB profiler support + supportsDatabaseProfiler: false, profilerFormatterFunction: 'formatProfilerEntry@dbgate-plugin-mongo', profilerTimestampFunction: 'extractProfileTimestamp@dbgate-plugin-mongo', profilerChartAggregateFunction: 'aggregateProfileChartEntry@dbgate-plugin-mongo', From 80597039f50772df6d31e3460304c7fd49c80b25 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Thu, 29 May 2025 18:10:09 +0200 Subject: [PATCH 111/129] SYNC: charts --- .../web/src/datagrid/FreeTableDataGrid.svelte | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 packages/web/src/datagrid/FreeTableDataGrid.svelte diff --git a/packages/web/src/datagrid/FreeTableDataGrid.svelte b/packages/web/src/datagrid/FreeTableDataGrid.svelte new file mode 100644 index 000000000..4b581b185 --- /dev/null +++ b/packages/web/src/datagrid/FreeTableDataGrid.svelte @@ -0,0 +1,27 @@ + + +{#if !model} + +{:else if errorMessage} + +{:else if grider} + +{/if} From 5d37280643768db7a77c2fb54b47ad5640d68e20 Mon Sep 17 00:00:00 2001 From: CI workflows Date: Thu, 29 May 2025 16:10:50 +0000 Subject: [PATCH 112/129] Update pro ref --- workflow-templates/includes.tpl.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflow-templates/includes.tpl.yaml b/workflow-templates/includes.tpl.yaml index 2c3f84f30..2a0745434 100644 --- a/workflow-templates/includes.tpl.yaml +++ b/workflow-templates/includes.tpl.yaml @@ -7,7 +7,7 @@ checkout-and-merge-pro: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: 1b503a70268bcaac850697db1fc1dc0237caf5cc + ref: fe0399b3cc319a2f751451d7b56b0bdf222662a5 - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro From af1eccde8e864071ebd45c1b4f7b1ca43c25074e Mon Sep 17 00:00:00 2001 From: CI workflows Date: Thu, 29 May 2025 16:11:09 +0000 Subject: [PATCH 113/129] chore: auto-update github workflows --- .github/workflows/build-app-pro-beta.yaml | 2 +- .github/workflows/build-app-pro.yaml | 2 +- .github/workflows/build-cloud-pro.yaml | 2 +- .github/workflows/build-docker-pro.yaml | 2 +- .github/workflows/build-npm-pro.yaml | 2 +- .github/workflows/e2e-pro.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-app-pro-beta.yaml b/.github/workflows/build-app-pro-beta.yaml index 5be4b4e6c..078aaf978 100644 --- a/.github/workflows/build-app-pro-beta.yaml +++ b/.github/workflows/build-app-pro-beta.yaml @@ -39,7 +39,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: 1b503a70268bcaac850697db1fc1dc0237caf5cc + ref: fe0399b3cc319a2f751451d7b56b0bdf222662a5 - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/build-app-pro.yaml b/.github/workflows/build-app-pro.yaml index b880a1754..93fddf6cd 100644 --- a/.github/workflows/build-app-pro.yaml +++ b/.github/workflows/build-app-pro.yaml @@ -39,7 +39,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: 1b503a70268bcaac850697db1fc1dc0237caf5cc + ref: fe0399b3cc319a2f751451d7b56b0bdf222662a5 - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/build-cloud-pro.yaml b/.github/workflows/build-cloud-pro.yaml index b65f6566b..a071c5a49 100644 --- a/.github/workflows/build-cloud-pro.yaml +++ b/.github/workflows/build-cloud-pro.yaml @@ -39,7 +39,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: 1b503a70268bcaac850697db1fc1dc0237caf5cc + ref: fe0399b3cc319a2f751451d7b56b0bdf222662a5 - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/build-docker-pro.yaml b/.github/workflows/build-docker-pro.yaml index 4634b97a4..39c1f3043 100644 --- a/.github/workflows/build-docker-pro.yaml +++ b/.github/workflows/build-docker-pro.yaml @@ -44,7 +44,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: 1b503a70268bcaac850697db1fc1dc0237caf5cc + ref: fe0399b3cc319a2f751451d7b56b0bdf222662a5 - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/build-npm-pro.yaml b/.github/workflows/build-npm-pro.yaml index b6807ca2b..1e8853eaf 100644 --- a/.github/workflows/build-npm-pro.yaml +++ b/.github/workflows/build-npm-pro.yaml @@ -32,7 +32,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: 1b503a70268bcaac850697db1fc1dc0237caf5cc + ref: fe0399b3cc319a2f751451d7b56b0bdf222662a5 - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/e2e-pro.yaml b/.github/workflows/e2e-pro.yaml index 8a53b58b3..8a73bc930 100644 --- a/.github/workflows/e2e-pro.yaml +++ b/.github/workflows/e2e-pro.yaml @@ -26,7 +26,7 @@ jobs: repository: dbgate/dbgate-pro token: ${{ secrets.GH_TOKEN }} path: dbgate-pro - ref: 1b503a70268bcaac850697db1fc1dc0237caf5cc + ref: fe0399b3cc319a2f751451d7b56b0bdf222662a5 - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro From d54f7293b7832693db5d650b1b1b5d60b09122da Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Fri, 30 May 2025 08:16:18 +0200 Subject: [PATCH 114/129] db2 test container config --- integration-tests/docker-compose.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/integration-tests/docker-compose.yaml b/integration-tests/docker-compose.yaml index 6ea85877c..029fd4792 100644 --- a/integration-tests/docker-compose.yaml +++ b/integration-tests/docker-compose.yaml @@ -17,6 +17,16 @@ services: environment: - MYSQL_ROOT_PASSWORD=Pwd2020Db + # db2: + # image: icr.io/db2_community/db2:11.5.8.0 + # ports: + # - "50000:50000" + # environment: + # - LICENSE=accept + # - DB2INST1_PASSWORD=Pwd2020Db + # - DBNAME=testdb + + # mysql: # image: mysql:8.0.18 # command: --default-authentication-plugin=mysql_native_password From 8166da548c7f1058045e2e3da597de03ad8288b4 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Fri, 30 May 2025 10:56:18 +0200 Subject: [PATCH 115/129] db2 config --- integration-tests/docker-compose.yaml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/integration-tests/docker-compose.yaml b/integration-tests/docker-compose.yaml index 029fd4792..277dc4c72 100644 --- a/integration-tests/docker-compose.yaml +++ b/integration-tests/docker-compose.yaml @@ -17,15 +17,16 @@ services: environment: - MYSQL_ROOT_PASSWORD=Pwd2020Db - # db2: - # image: icr.io/db2_community/db2:11.5.8.0 - # ports: - # - "50000:50000" - # environment: - # - LICENSE=accept - # - DB2INST1_PASSWORD=Pwd2020Db - # - DBNAME=testdb - + db2: + image: icr.io/db2_community/db2:11.5.8.0 + privileged: true + ports: + - "15055:50000" + environment: + LICENSE: accept + DB2INST1_PASSWORD: Pwd2020Db + DBNAME: testdb + DB2INSTANCE: db2inst1 # mysql: # image: mysql:8.0.18 From 6f69205818fa2ed3dd27c673ffd5871da0e18410 Mon Sep 17 00:00:00 2001 From: "SPRINX0\\prochazka" Date: Fri, 30 May 2025 13:21:10 +0200 Subject: [PATCH 116/129] v6.4.3-premium-beta.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8266178bd..c03e8a323 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "6.4.3-beta.3", + "version": "6.4.3-premium-beta.4", "name": "dbgate-all", "workspaces": [ "packages/*", From ff1b58ebd816998da05d5d893071ded88f8c8643 Mon Sep 17 00:00:00 2001 From: Pavel Date: Thu, 5 Jun 2025 20:23:34 +0200 Subject: [PATCH 117/129] fix: correctly map DuckDBDateValue to string --- plugins/dbgate-plugin-duckdb/src/backend/helpers.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/plugins/dbgate-plugin-duckdb/src/backend/helpers.js b/plugins/dbgate-plugin-duckdb/src/backend/helpers.js index 987c537f4..d9f0a1c71 100644 --- a/plugins/dbgate-plugin-duckdb/src/backend/helpers.js +++ b/plugins/dbgate-plugin-duckdb/src/backend/helpers.js @@ -51,10 +51,7 @@ function _normalizeValue(value) { } if (value instanceof DuckDBDateValue) { - const year = value.year; - const month = String(value.month).padStart(2, '0'); - const day = String(value.day).padStart(2, '0'); - return `${year}-${month}-${day}`; + return value.toString(); } if (value instanceof DuckDBTimeValue) { From ecda226949c8af069c6fde3f68785cf2df864665 Mon Sep 17 00:00:00 2001 From: Pavel Date: Thu, 5 Jun 2025 20:26:33 +0200 Subject: [PATCH 118/129] fix: correctly map DuckDBTimeValue to string --- plugins/dbgate-plugin-duckdb/src/backend/helpers.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/dbgate-plugin-duckdb/src/backend/helpers.js b/plugins/dbgate-plugin-duckdb/src/backend/helpers.js index d9f0a1c71..133326b0c 100644 --- a/plugins/dbgate-plugin-duckdb/src/backend/helpers.js +++ b/plugins/dbgate-plugin-duckdb/src/backend/helpers.js @@ -55,10 +55,11 @@ function _normalizeValue(value) { } if (value instanceof DuckDBTimeValue) { - const hour = String(value.hour).padStart(2, '0'); - const minute = String(value.min).padStart(2, '0'); - const second = String(value.sec).padStart(2, '0'); - const micros = String(value.micros).padStart(6, '0').substring(0, 3); + const parts = value.toParts(); + const hour = String(parts.hour).padStart(2, '0'); + const minute = String(parts.min).padStart(2, '0'); + const second = String(parts.sec).padStart(2, '0'); + const micros = String(parts.micros).padStart(6, '0').substring(0, 3); return `${hour}:${minute}:${second}.${micros}`; } From 809dca184eff129371ffa751cff02959c4733032 Mon Sep 17 00:00:00 2001 From: Pavel Date: Thu, 5 Jun 2025 20:26:41 +0200 Subject: [PATCH 119/129] chore: add start:api:watch script --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index c03e8a323..fc7bbc39e 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ ], "scripts": { "start:api": "yarn workspace dbgate-api start | pino-pretty", + "start:api:watch": "nodemon --watch 'src/**' --ext 'ts,json,js' --exec yarn start:api", "start:api:json": "yarn workspace dbgate-api start", "start:app": "cd app && yarn start | pino-pretty", "start:app:singledb": "CONNECTIONS=con1 SERVER_con1=localhost ENGINE_con1=mysql@dbgate-plugin-mysql USER_con1=root PASSWORD_con1=Pwd2020Db SINGLE_CONNECTION=con1 SINGLE_DATABASE=Chinook yarn start:app", From f03cffe3f8523dc6c02b29ad860808035f8ceab9 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Mon, 9 Jun 2025 09:15:21 +0200 Subject: [PATCH 120/129] SYNC: Merge pull request #4 from dbgate/feature/charts --- packages/api/src/controllers/jsldata.js | 26 + packages/api/src/controllers/sessions.js | 9 +- packages/api/src/proc/sessionProcess.js | 4 +- packages/api/src/utility/handleQueryStream.js | 68 ++- packages/datalib/package.json | 8 +- packages/datalib/src/chartDefinitions.ts | 84 +++ packages/datalib/src/chartProcessor.ts | 374 ++++++++++++ packages/datalib/src/chartScoring.ts | 23 + packages/datalib/src/chartTools.ts | 542 ++++++++++++++++++ packages/datalib/src/index.ts | 2 + .../datalib/src/tests/chartProcessor.test.ts | 376 ++++++++++++ packages/tools/src/stringTools.ts | 38 ++ packages/tools/tsconfig.json | 2 +- .../src/appobj/DatabaseObjectAppObject.svelte | 35 +- .../web/src/appobj/SavedFileAppObject.svelte | 11 - packages/web/src/datagrid/DataGridCore.svelte | 31 +- .../web/src/datagrid/SqlDataGridCore.svelte | 30 - .../src/elements/HorizontalSplitter.svelte | 2 +- packages/web/src/elements/TabControl.svelte | 14 +- packages/web/src/icons/FontIcon.svelte | 1 + packages/web/src/query/ResultTabs.svelte | 67 ++- packages/web/src/tabs/QueryTab.svelte | 62 +- 22 files changed, 1687 insertions(+), 122 deletions(-) create mode 100644 packages/datalib/src/chartDefinitions.ts create mode 100644 packages/datalib/src/chartProcessor.ts create mode 100644 packages/datalib/src/chartScoring.ts create mode 100644 packages/datalib/src/chartTools.ts create mode 100644 packages/datalib/src/tests/chartProcessor.test.ts diff --git a/packages/api/src/controllers/jsldata.js b/packages/api/src/controllers/jsldata.js index be6e1bf84..ece59f270 100644 --- a/packages/api/src/controllers/jsldata.js +++ b/packages/api/src/controllers/jsldata.js @@ -10,6 +10,7 @@ const requirePluginFunction = require('../utility/requirePluginFunction'); const socket = require('../utility/socket'); const crypto = require('crypto'); const dbgateApi = require('../shell'); +const { ChartProcessor } = require('dbgate-datalib'); function readFirstLine(file) { return new Promise((resolve, reject) => { @@ -302,4 +303,29 @@ module.exports = { await dbgateApi.download(uri, { targetFile: getJslFileName(jslid) }); return { jslid }; }, + + buildChart_meta: true, + async buildChart({ jslid, definition }) { + const datastore = new JsonLinesDatastore(getJslFileName(jslid)); + const processor = new ChartProcessor(definition ? [definition] : undefined); + await datastore.enumRows(row => { + processor.addRow(row); + return true; + }); + processor.finalize(); + return processor.charts; + }, + + detectChartColumns_meta: true, + async detectChartColumns({ jslid }) { + const datastore = new JsonLinesDatastore(getJslFileName(jslid)); + const processor = new ChartProcessor(); + processor.autoDetectCharts = false; + await datastore.enumRows(row => { + processor.addRow(row); + return true; + }); + processor.finalize(); + return processor.availableColumns; + }, }; diff --git a/packages/api/src/controllers/sessions.js b/packages/api/src/controllers/sessions.js index 4f2751483..bebd09039 100644 --- a/packages/api/src/controllers/sessions.js +++ b/packages/api/src/controllers/sessions.js @@ -83,6 +83,11 @@ module.exports = { jsldata.notifyChangedStats(stats); }, + handle_charts(sesid, props) { + const { jslid, charts, resultIndex } = props; + socket.emit(`session-charts-${sesid}`, { jslid, resultIndex, charts }); + }, + handle_initializeFile(sesid, props) { const { jslid } = props; socket.emit(`session-initialize-file-${jslid}`); @@ -141,7 +146,7 @@ module.exports = { }, executeQuery_meta: true, - async executeQuery({ sesid, sql, autoCommit, limitRows }) { + async executeQuery({ sesid, sql, autoCommit, limitRows, frontMatter }) { const session = this.opened.find(x => x.sesid == sesid); if (!session) { throw new Error('Invalid session'); @@ -149,7 +154,7 @@ module.exports = { logger.info({ sesid, sql }, 'Processing query'); this.dispatchMessage(sesid, 'Query execution started'); - session.subprocess.send({ msgtype: 'executeQuery', sql, autoCommit, limitRows }); + session.subprocess.send({ msgtype: 'executeQuery', sql, autoCommit, limitRows, frontMatter }); return { state: 'ok' }; }, diff --git a/packages/api/src/proc/sessionProcess.js b/packages/api/src/proc/sessionProcess.js index 8dd193db5..0c05d670c 100644 --- a/packages/api/src/proc/sessionProcess.js +++ b/packages/api/src/proc/sessionProcess.js @@ -117,7 +117,7 @@ async function handleExecuteControlCommand({ command }) { } } -async function handleExecuteQuery({ sql, autoCommit, limitRows }) { +async function handleExecuteQuery({ sql, autoCommit, limitRows, frontMatter }) { lastActivity = new Date().getTime(); await waitConnected(); @@ -146,7 +146,7 @@ async function handleExecuteQuery({ sql, autoCommit, limitRows }) { ...driver.getQuerySplitterOptions('stream'), returnRichInfo: true, })) { - await handleQueryStream(dbhan, driver, queryStreamInfoHolder, sqlItem, undefined, limitRows); + await handleQueryStream(dbhan, driver, queryStreamInfoHolder, sqlItem, undefined, limitRows, frontMatter); // const handler = new StreamHandler(resultIndex); // const stream = await driver.stream(systemConnection, sqlItem, handler); // handler.stream = stream; diff --git a/packages/api/src/utility/handleQueryStream.js b/packages/api/src/utility/handleQueryStream.js index f57cb46fd..7e0239f59 100644 --- a/packages/api/src/utility/handleQueryStream.js +++ b/packages/api/src/utility/handleQueryStream.js @@ -5,6 +5,8 @@ const _ = require('lodash'); const { jsldir } = require('../utility/directories'); const { serializeJsTypesReplacer } = require('dbgate-tools'); +const { ChartProcessor } = require('dbgate-datalib'); +const { isProApp } = require('./checkLicense'); class QueryStreamTableWriter { constructor(sesid = undefined) { @@ -12,9 +14,12 @@ class QueryStreamTableWriter { this.currentChangeIndex = 1; this.initializedFile = false; this.sesid = sesid; + if (isProApp()) { + this.chartProcessor = new ChartProcessor(); + } } - initializeFromQuery(structure, resultIndex) { + initializeFromQuery(structure, resultIndex, chartDefinition) { this.jslid = crypto.randomUUID(); this.currentFile = path.join(jsldir(), `${this.jslid}.jsonl`); fs.writeFileSync( @@ -28,6 +33,9 @@ class QueryStreamTableWriter { this.writeCurrentStats(false, false); this.resultIndex = resultIndex; this.initializedFile = true; + if (isProApp() && chartDefinition) { + this.chartProcessor = new ChartProcessor([chartDefinition]); + } process.send({ msgtype: 'recordset', jslid: this.jslid, resultIndex, sesid: this.sesid }); } @@ -40,6 +48,15 @@ class QueryStreamTableWriter { row(row) { // console.log('ACCEPT ROW', row); this.currentStream.write(JSON.stringify(row, serializeJsTypesReplacer) + '\n'); + try { + if (this.chartProcessor) { + this.chartProcessor.addRow(row); + } + } catch (e) { + console.error('Error processing chart row', e); + this.chartProcessor = null; + } + this.currentRowCount += 1; if (!this.plannedStats) { @@ -87,6 +104,23 @@ class QueryStreamTableWriter { this.currentStream.end(() => { this.writeCurrentStats(true, true); if (afterClose) afterClose(); + if (this.chartProcessor) { + try { + this.chartProcessor.finalize(); + if (this.chartProcessor.charts.length > 0) { + process.send({ + msgtype: 'charts', + sesid: this.sesid, + jslid: this.jslid, + charts: this.chartProcessor.charts, + resultIndex: this.resultIndex, + }); + } + } catch (e) { + console.error('Error finalizing chart processor', e); + this.chartProcessor = null; + } + } resolve(); }); } else { @@ -97,10 +131,18 @@ class QueryStreamTableWriter { } class StreamHandler { - constructor(queryStreamInfoHolder, resolve, startLine, sesid = undefined, limitRows = undefined) { + constructor( + queryStreamInfoHolder, + resolve, + startLine, + sesid = undefined, + limitRows = undefined, + frontMatter = undefined + ) { this.recordset = this.recordset.bind(this); this.startLine = startLine; this.sesid = sesid; + this.frontMatter = frontMatter; this.limitRows = limitRows; this.rowsLimitOverflow = false; this.row = this.row.bind(this); @@ -133,7 +175,8 @@ class StreamHandler { this.currentWriter = new QueryStreamTableWriter(this.sesid); this.currentWriter.initializeFromQuery( Array.isArray(columns) ? { columns } : columns, - this.queryStreamInfoHolder.resultIndex + this.queryStreamInfoHolder.resultIndex, + this.frontMatter?.[`chart-${this.queryStreamInfoHolder.resultIndex + 1}`] ); this.queryStreamInfoHolder.resultIndex += 1; this.rowCounter = 0; @@ -201,10 +244,25 @@ class StreamHandler { } } -function handleQueryStream(dbhan, driver, queryStreamInfoHolder, sqlItem, sesid = undefined, limitRows = undefined) { +function handleQueryStream( + dbhan, + driver, + queryStreamInfoHolder, + sqlItem, + sesid = undefined, + limitRows = undefined, + frontMatter = undefined +) { return new Promise((resolve, reject) => { const start = sqlItem.trimStart || sqlItem.start; - const handler = new StreamHandler(queryStreamInfoHolder, resolve, start && start.line, sesid, limitRows); + const handler = new StreamHandler( + queryStreamInfoHolder, + resolve, + start && start.line, + sesid, + limitRows, + frontMatter + ); driver.stream(dbhan, sqlItem.text, handler); }); } diff --git a/packages/datalib/package.json b/packages/datalib/package.json index 50b9564d3..b82b4585a 100644 --- a/packages/datalib/package.json +++ b/packages/datalib/package.json @@ -6,6 +6,7 @@ "scripts": { "build": "tsc", "test": "jest", + "test:charts": "jest -t \"Chart processor\"", "test:ci": "jest --json --outputFile=result.json --testLocationInResults", "start": "tsc --watch" }, @@ -13,16 +14,17 @@ "lib" ], "dependencies": { + "date-fns": "^4.1.0", + "dbgate-filterparser": "^6.0.0-alpha.1", "dbgate-sqltree": "^6.0.0-alpha.1", "dbgate-tools": "^6.0.0-alpha.1", - "dbgate-filterparser": "^6.0.0-alpha.1", "uuid": "^3.4.0" }, "devDependencies": { - "dbgate-types": "^6.0.0-alpha.1", "@types/node": "^13.7.0", + "dbgate-types": "^6.0.0-alpha.1", "jest": "^28.1.3", "ts-jest": "^28.0.7", "typescript": "^4.4.3" } -} \ No newline at end of file +} diff --git a/packages/datalib/src/chartDefinitions.ts b/packages/datalib/src/chartDefinitions.ts new file mode 100644 index 000000000..72831ec07 --- /dev/null +++ b/packages/datalib/src/chartDefinitions.ts @@ -0,0 +1,84 @@ +export type ChartTypeEnum = 'bar' | 'line' | 'pie' | 'polarArea'; +export type ChartXTransformFunction = + | 'identity' + | 'date:minute' + | 'date:hour' + | 'date:day' + | 'date:month' + | 'date:year'; +export type ChartYAggregateFunction = 'sum' | 'first' | 'last' | 'min' | 'max' | 'count' | 'avg'; + +export const ChartConstDefaults = { + sortOrder: ' asc', + windowAlign: 'end', + windowSize: 100, + parentAggregateLimit: 200, +}; + +export const ChartLimits = { + AUTODETECT_CHART_LIMIT: 10, // limit for auto-detecting charts, to avoid too many charts + AUTODETECT_MEASURES_LIMIT: 10, // limit for auto-detecting measures, to avoid too many measures + APPLY_LIMIT_AFTER_ROWS: 100, + MAX_DISTINCT_VALUES: 10, // max number of distinct values to keep in topDistinctValues + VALID_VALUE_RATIO_LIMIT: 0.5, // limit for valid value ratio, y defs below this will not be used in auto-detect + PIE_RATIO_LIMIT: 0.05, // limit for other values in pie chart, if the value is below this, it will be grouped into "Other" + PIE_COUNT_LIMIT: 10, // limit for number of pie chart slices, if the number of slices is above this, it will be grouped into "Other" +}; + +export interface ChartXFieldDefinition { + field: string; + title?: string; + transformFunction: ChartXTransformFunction; + sortOrder?: 'natural' | 'ascKeys' | 'descKeys' | 'ascValues' | 'descValues'; + windowAlign?: 'start' | 'end'; + windowSize?: number; + parentAggregateLimit?: number; +} + +export interface ChartYFieldDefinition { + field: string; + title?: string; + aggregateFunction: ChartYAggregateFunction; +} + +export interface ChartDefinition { + chartType: ChartTypeEnum; + title?: string; + pieRatioLimit?: number; // limit for pie chart, if the value is below this, it will be grouped into "Other" + pieCountLimit?: number; // limit for number of pie chart slices, if the number of slices is above this, it will be grouped into "Other" + + xdef: ChartXFieldDefinition; + ydefs: ChartYFieldDefinition[]; +} + +export interface ChartDateParsed { + year: number; + month?: number; + day?: number; + hour?: number; + minute?: number; + second?: number; + fraction?: string; +} + +export interface ChartAvailableColumn { + field: string; +} + +export interface ProcessedChart { + minX?: string; + maxX?: string; + rowsAdded: number; + buckets: { [key: string]: any }; // key is the bucket key, value is aggregated data + bucketKeysOrdered: string[]; + bucketKeyDateParsed: { [key: string]: ChartDateParsed }; // key is the bucket key, value is parsed date + isGivenDefinition: boolean; // true if the chart was created with a given definition, false if it was created from raw data + invalidXRows: number; + invalidYRows: { [key: string]: number }; // key is the y field, value is the count of invalid rows + validYRows: { [key: string]: number }; // key is the field, value is the count of valid rows + + topDistinctValues: { [key: string]: Set }; // key is the field, value is the set of distinct values + availableColumns: ChartAvailableColumn[]; + + definition: ChartDefinition; +} diff --git a/packages/datalib/src/chartProcessor.ts b/packages/datalib/src/chartProcessor.ts new file mode 100644 index 000000000..4a1cdc26a --- /dev/null +++ b/packages/datalib/src/chartProcessor.ts @@ -0,0 +1,374 @@ +import { + ChartAvailableColumn, + ChartDateParsed, + ChartDefinition, + ChartLimits, + ProcessedChart, +} from './chartDefinitions'; +import _sortBy from 'lodash/sortBy'; +import _sum from 'lodash/sum'; +import { + aggregateChartNumericValuesFromSource, + autoAggregateCompactTimelineChart, + computeChartBucketCardinality, + computeChartBucketKey, + fillChartTimelineBuckets, + tryParseChartDate, +} from './chartTools'; +import { getChartScore, getChartYFieldScore } from './chartScoring'; + +export class ChartProcessor { + chartsProcessing: ProcessedChart[] = []; + charts: ProcessedChart[] = []; + availableColumnsDict: { [field: string]: ChartAvailableColumn } = {}; + availableColumns: ChartAvailableColumn[] = []; + autoDetectCharts = false; + rowsAdded = 0; + + constructor(public givenDefinitions: ChartDefinition[] = []) { + for (const definition of givenDefinitions) { + this.chartsProcessing.push({ + definition, + rowsAdded: 0, + bucketKeysOrdered: [], + buckets: {}, + bucketKeyDateParsed: {}, + isGivenDefinition: true, + invalidXRows: 0, + invalidYRows: {}, + availableColumns: [], + validYRows: {}, + topDistinctValues: {}, + }); + } + this.autoDetectCharts = this.givenDefinitions.length == 0; + } + + // findOrCreateChart(definition: ChartDefinition, isGivenDefinition: boolean): ProcessedChart { + // const signatureItems = [ + // definition.chartType, + // definition.xdef.field, + // definition.xdef.transformFunction, + // definition.ydefs.map(y => y.field).join(','), + // ]; + // const signature = signatureItems.join('::'); + + // if (this.chartsBySignature[signature]) { + // return this.chartsBySignature[signature]; + // } + // const chart: ProcessedChart = { + // definition, + // rowsAdded: 0, + // bucketKeysOrdered: [], + // buckets: {}, + // bucketKeyDateParsed: {}, + // isGivenDefinition, + // }; + // this.chartsBySignature[signature] = chart; + // return chart; + // } + + addRow(row: any) { + const dateColumns: { [key: string]: ChartDateParsed } = {}; + const numericColumns: { [key: string]: number } = {}; + const numericColumnsForAutodetect: { [key: string]: number } = {}; + const stringColumns: { [key: string]: string } = {}; + + for (const [key, value] of Object.entries(row)) { + const number: number = typeof value == 'string' ? Number(value) : typeof value == 'number' ? value : NaN; + this.availableColumnsDict[key] = { + field: key, + }; + + const keyLower = key.toLowerCase(); + const keyIsId = keyLower.endsWith('_id') || keyLower == 'id' || key.endsWith('Id'); + + const parsedDate = tryParseChartDate(value); + if (parsedDate) { + dateColumns[key] = parsedDate; + continue; + } + + if (!isNaN(number) && isFinite(number)) { + numericColumns[key] = number; + if (!keyIsId) { + numericColumnsForAutodetect[key] = number; // for auto-detecting charts + } + continue; + } + + if (typeof value === 'string' && isNaN(number) && value.length < 100) { + stringColumns[key] = value; + } + } + + // const sortedNumericColumnns = Object.keys(numericColumns).sort(); + + if (this.autoDetectCharts) { + // create charts from data, if there are no given definitions + for (const datecol in dateColumns) { + let usedChart = this.chartsProcessing.find( + chart => + !chart.isGivenDefinition && + chart.definition.xdef.field === datecol && + chart.definition.xdef.transformFunction?.startsWith('date:') + ); + + if ( + !usedChart && + (this.rowsAdded < ChartLimits.APPLY_LIMIT_AFTER_ROWS || + this.chartsProcessing.length < ChartLimits.AUTODETECT_CHART_LIMIT) + ) { + usedChart = { + definition: { + chartType: 'line', + xdef: { + field: datecol, + transformFunction: 'date:day', + }, + ydefs: [], + }, + rowsAdded: 0, + bucketKeysOrdered: [], + buckets: {}, + bucketKeyDateParsed: {}, + isGivenDefinition: false, + invalidXRows: 0, + invalidYRows: {}, + availableColumns: [], + validYRows: {}, + topDistinctValues: {}, + }; + this.chartsProcessing.push(usedChart); + } + + for (const [key, value] of Object.entries(row)) { + if (value == null) continue; + if (key == datecol) continue; // skip date column itself + let existingYDef = usedChart.definition.ydefs.find(y => y.field === key); + if ( + !existingYDef && + (this.rowsAdded < ChartLimits.APPLY_LIMIT_AFTER_ROWS || + usedChart.definition.ydefs.length < ChartLimits.AUTODETECT_MEASURES_LIMIT) + ) { + existingYDef = { + field: key, + aggregateFunction: 'sum', + }; + usedChart.definition.ydefs.push(existingYDef); + } + } + } + } + + // apply on all charts with this date column + for (const chart of this.chartsProcessing) { + this.applyRawData( + chart, + row, + dateColumns[chart.definition.xdef.field], + chart.isGivenDefinition ? numericColumns : numericColumnsForAutodetect, + stringColumns + ); + } + + for (let i = 0; i < this.chartsProcessing.length; i++) { + this.chartsProcessing[i] = autoAggregateCompactTimelineChart(this.chartsProcessing[i]); + } + + this.rowsAdded += 1; + if (this.rowsAdded == ChartLimits.APPLY_LIMIT_AFTER_ROWS) { + this.applyLimitsOnCharts(); + } + } + + applyLimitsOnCharts() { + const autodetectProcessingCharts = this.chartsProcessing.filter(chart => !chart.isGivenDefinition); + if (autodetectProcessingCharts.length > ChartLimits.AUTODETECT_CHART_LIMIT) { + const newAutodetectProcessingCharts = _sortBy( + this.chartsProcessing.slice(0, ChartLimits.AUTODETECT_CHART_LIMIT), + chart => -getChartScore(chart) + ); + + for (const chart of autodetectProcessingCharts) { + chart.definition.ydefs = _sortBy(chart.definition.ydefs, yfield => -getChartYFieldScore(chart, yfield)).slice( + 0, + ChartLimits.AUTODETECT_MEASURES_LIMIT + ); + } + + this.chartsProcessing = [ + ...this.chartsProcessing.filter(chart => chart.isGivenDefinition), + ...newAutodetectProcessingCharts, + ]; + } + } + + addRows(...rows: any[]) { + for (const row of rows) { + this.addRow(row); + } + } + + finalize() { + this.applyLimitsOnCharts(); + this.availableColumns = Object.values(this.availableColumnsDict); + for (const chart of this.chartsProcessing) { + let addedChart: ProcessedChart = chart; + if (chart.rowsAdded == 0) { + continue; // skip empty charts + } + const sortOrder = chart.definition.xdef.sortOrder ?? 'ascKeys'; + if (sortOrder != 'natural') { + if (sortOrder == 'ascKeys' || sortOrder == 'descKeys') { + if (chart.definition.xdef.transformFunction.startsWith('date:')) { + addedChart = autoAggregateCompactTimelineChart(addedChart); + fillChartTimelineBuckets(addedChart); + } + + addedChart.bucketKeysOrdered = _sortBy(Object.keys(addedChart.buckets)); + if (sortOrder == 'descKeys') { + addedChart.bucketKeysOrdered.reverse(); + } + } + + if (sortOrder == 'ascValues' || sortOrder == 'descValues') { + addedChart.bucketKeysOrdered = _sortBy(Object.keys(addedChart.buckets), key => + computeChartBucketCardinality(addedChart.buckets[key]) + ); + if (sortOrder == 'descValues') { + addedChart.bucketKeysOrdered.reverse(); + } + } + } + + if (!addedChart.isGivenDefinition) { + addedChart = { + ...addedChart, + definition: { + ...addedChart.definition, + ydefs: addedChart.definition.ydefs.filter( + y => + !addedChart.invalidYRows[y.field] && + addedChart.validYRows[y.field] / addedChart.rowsAdded >= ChartLimits.VALID_VALUE_RATIO_LIMIT + ), + }, + }; + } + + if (addedChart) { + addedChart.availableColumns = this.availableColumns; + this.charts.push(addedChart); + } + + this.groupPieOtherBuckets(addedChart); + } + + this.charts = [ + ...this.charts.filter(x => x.isGivenDefinition), + ..._sortBy( + this.charts.filter(x => !x.isGivenDefinition), + chart => -getChartScore(chart) + ), + ]; + } + groupPieOtherBuckets(chart: ProcessedChart) { + if (chart.definition.chartType !== 'pie') { + return; // only for pie charts + } + const ratioLimit = chart.definition.pieRatioLimit ?? ChartLimits.PIE_RATIO_LIMIT; + const countLimit = chart.definition.pieCountLimit ?? ChartLimits.PIE_COUNT_LIMIT; + if (ratioLimit == 0 && countLimit == 0) { + return; // no grouping if limit is 0 + } + const otherBucket: any = {}; + let newBuckets: any = {}; + const cardSum = _sum(Object.values(chart.buckets).map(bucket => computeChartBucketCardinality(bucket))); + + if (cardSum == 0) { + return; // no buckets to process + } + + for (const [bucketKey, bucket] of Object.entries(chart.buckets)) { + if (computeChartBucketCardinality(bucket) / cardSum < ratioLimit) { + for (const field in bucket) { + otherBucket[field] = (otherBucket[field] ?? 0) + bucket[field]; + } + } else { + newBuckets[bucketKey] = bucket; + } + } + + if (Object.keys(newBuckets).length > countLimit) { + const sortedBucketKeys = _sortBy( + Object.entries(newBuckets), + ([, bucket]) => -computeChartBucketCardinality(bucket) + ).map(([key]) => key); + const newBuckets2 = {}; + sortedBucketKeys.forEach((key, index) => { + if (index < countLimit) { + newBuckets2[key] = newBuckets[key]; + } else { + for (const field in newBuckets[key]) { + otherBucket[field] = (otherBucket[field] ?? 0) + newBuckets[key][field]; + } + } + }); + newBuckets = newBuckets2; + } + + if (Object.keys(otherBucket).length > 0) { + newBuckets['Other'] = otherBucket; + } + chart.buckets = newBuckets; + chart.bucketKeysOrdered = [...chart.bucketKeysOrdered, 'Other'].filter(key => key in newBuckets); + } + + applyRawData( + chart: ProcessedChart, + row: any, + dateParsed: ChartDateParsed, + numericColumns: { [key: string]: number }, + stringColumns: { [key: string]: string } + ) { + if (chart.definition.xdef == null) { + return; + } + + if (row[chart.definition.xdef.field] == null) { + return; + } + + if (dateParsed == null && chart.definition.xdef.transformFunction.startsWith('date:')) { + chart.invalidXRows += 1; + return; // skip if date is invalid + } + + const [bucketKey, bucketKeyParsed] = computeChartBucketKey(dateParsed, chart, row); + + if (!bucketKey) { + return; // skip if no bucket key + } + + if (bucketKeyParsed) { + chart.bucketKeyDateParsed[bucketKey] = bucketKeyParsed; + } + + if (chart.minX == null || bucketKey < chart.minX) { + chart.minX = bucketKey; + } + if (chart.maxX == null || bucketKey > chart.maxX) { + chart.maxX = bucketKey; + } + + if (!chart.buckets[bucketKey]) { + chart.buckets[bucketKey] = {}; + if (chart.definition.xdef.sortOrder == 'natural') { + chart.bucketKeysOrdered.push(bucketKey); + } + } + + aggregateChartNumericValuesFromSource(chart, bucketKey, numericColumns, row); + chart.rowsAdded += 1; + } +} diff --git a/packages/datalib/src/chartScoring.ts b/packages/datalib/src/chartScoring.ts new file mode 100644 index 000000000..b4c10861d --- /dev/null +++ b/packages/datalib/src/chartScoring.ts @@ -0,0 +1,23 @@ +import _sortBy from 'lodash/sortBy'; +import _sum from 'lodash/sum'; +import { ChartLimits, ChartYFieldDefinition, ProcessedChart } from './chartDefinitions'; + +export function getChartScore(chart: ProcessedChart): number { + let res = 0; + res += chart.rowsAdded * 5; + + const ydefScores = chart.definition.ydefs.map(yField => getChartYFieldScore(chart, yField)); + const sorted = _sortBy(ydefScores).reverse(); + res += _sum(sorted.slice(0, ChartLimits.AUTODETECT_MEASURES_LIMIT)); + return res; +} + +export function getChartYFieldScore(chart: ProcessedChart, yField: ChartYFieldDefinition): number { + let res = 0; + res += chart.validYRows[yField.field] * 5; // score for valid Y rows + res += (chart.topDistinctValues[yField.field]?.size ?? 0) * 20; // score for distinct values in Y field + res += chart.rowsAdded * 2; // base score for rows added + res -= (chart.invalidYRows[yField.field] ?? 0) * 50; // penalty for invalid Y rows + + return res; +} diff --git a/packages/datalib/src/chartTools.ts b/packages/datalib/src/chartTools.ts new file mode 100644 index 000000000..387d34c40 --- /dev/null +++ b/packages/datalib/src/chartTools.ts @@ -0,0 +1,542 @@ +import _toPairs from 'lodash/toPairs'; +import _sumBy from 'lodash/sumBy'; +import { + ChartConstDefaults, + ChartDateParsed, + ChartLimits, + ChartXTransformFunction, + ProcessedChart, +} from './chartDefinitions'; +import { addMinutes, addHours, addDays, addMonths, addYears } from 'date-fns'; + +export function getChartDebugPrint(chart: ProcessedChart) { + let res = ''; + res += `Chart: ${chart.definition.chartType} (${chart.definition.xdef.transformFunction})\n`; + for (const key of chart.bucketKeysOrdered) { + res += `${key}: ${_toPairs(chart.buckets[key]) + .map(([k, v]) => `${k}=${v}`) + .join(', ')}\n`; + } + return res; +} + +export function tryParseChartDate(dateInput: any): ChartDateParsed | null { + if (dateInput instanceof Date) { + return { + year: dateInput.getFullYear(), + month: dateInput.getMonth() + 1, + day: dateInput.getDate(), + hour: dateInput.getHours(), + minute: dateInput.getMinutes(), + second: dateInput.getSeconds(), + fraction: undefined, // Date object does not have fraction + }; + } + + if (typeof dateInput !== 'string') return null; + const m = dateInput.match( + /^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(Z|[+-]\d{2}:\d{2})?)?$/ + ); + if (!m) return null; + + const [_notUsed, year, month, day, hour, minute, second, fraction] = m; + + return { + year: parseInt(year, 10), + month: parseInt(month, 10), + day: parseInt(day, 10), + hour: parseInt(hour, 10) || 0, + minute: parseInt(minute, 10) || 0, + second: parseInt(second, 10) || 0, + fraction: fraction || undefined, + }; +} + +function pad2Digits(number) { + return ('00' + number).slice(-2); +} + +export function stringifyChartDate(value: ChartDateParsed, transform: ChartXTransformFunction): string { + switch (transform) { + case 'date:year': + return `${value.year}`; + case 'date:month': + return `${value.year}-${pad2Digits(value.month)}`; + case 'date:day': + return `${value.year}-${pad2Digits(value.month)}-${pad2Digits(value.day)}`; + case 'date:hour': + return `${value.year}-${pad2Digits(value.month)}-${pad2Digits(value.day)} ${pad2Digits(value.hour)}`; + case 'date:minute': + return `${value.year}-${pad2Digits(value.month)}-${pad2Digits(value.day)} ${pad2Digits(value.hour)}:${pad2Digits( + value.minute + )}`; + default: + return ''; + } +} + +export function incrementChartDate(value: ChartDateParsed, transform: ChartXTransformFunction): ChartDateParsed { + const dateRepresentation = new Date( + value.year, + (value.month ?? 1) - 1, + value.day ?? 1, + value.hour ?? 0, + value.minute ?? 0 + ); + let newDateRepresentation: Date; + switch (transform) { + case 'date:year': + newDateRepresentation = addYears(dateRepresentation, 1); + break; + case 'date:month': + newDateRepresentation = addMonths(dateRepresentation, 1); + break; + case 'date:day': + newDateRepresentation = addDays(dateRepresentation, 1); + break; + case 'date:hour': + newDateRepresentation = addHours(dateRepresentation, 1); + break; + case 'date:minute': + newDateRepresentation = addMinutes(dateRepresentation, 1); + break; + } + switch (transform) { + case 'date:year': + return { year: newDateRepresentation.getFullYear() }; + case 'date:month': + return { + year: newDateRepresentation.getFullYear(), + month: newDateRepresentation.getMonth() + 1, + }; + case 'date:day': + return { + year: newDateRepresentation.getFullYear(), + month: newDateRepresentation.getMonth() + 1, + day: newDateRepresentation.getDate(), + }; + case 'date:hour': + return { + year: newDateRepresentation.getFullYear(), + month: newDateRepresentation.getMonth() + 1, + day: newDateRepresentation.getDate(), + hour: newDateRepresentation.getHours(), + }; + case 'date:minute': + return { + year: newDateRepresentation.getFullYear(), + month: newDateRepresentation.getMonth() + 1, + day: newDateRepresentation.getDate(), + hour: newDateRepresentation.getHours(), + minute: newDateRepresentation.getMinutes(), + }; + } +} + +export function computeChartBucketKey( + dateParsed: ChartDateParsed, + chart: ProcessedChart, + row: any +): [string, ChartDateParsed] { + switch (chart.definition.xdef.transformFunction) { + case 'date:year': + return [dateParsed ? `${dateParsed.year}` : null, { year: dateParsed.year }]; + case 'date:month': + return [ + dateParsed ? `${dateParsed.year}-${pad2Digits(dateParsed.month)}` : null, + { + year: dateParsed.year, + month: dateParsed.month, + }, + ]; + case 'date:day': + return [ + dateParsed ? `${dateParsed.year}-${pad2Digits(dateParsed.month)}-${pad2Digits(dateParsed.day)}` : null, + { + year: dateParsed.year, + month: dateParsed.month, + day: dateParsed.day, + }, + ]; + case 'date:hour': + return [ + dateParsed + ? `${dateParsed.year}-${pad2Digits(dateParsed.month)}-${pad2Digits(dateParsed.day)} ${pad2Digits( + dateParsed.hour + )}` + : null, + { + year: dateParsed.year, + month: dateParsed.month, + day: dateParsed.day, + hour: dateParsed.hour, + }, + ]; + case 'date:minute': + return [ + dateParsed + ? `${dateParsed.year}-${pad2Digits(dateParsed.month)}-${pad2Digits(dateParsed.day)} ${pad2Digits( + dateParsed.hour + )}:${pad2Digits(dateParsed.minute)}` + : null, + { + year: dateParsed.year, + month: dateParsed.month, + day: dateParsed.day, + hour: dateParsed.hour, + minute: dateParsed.minute, + }, + ]; + case 'identity': + default: + return [row[chart.definition.xdef.field], null]; + } +} + +export function computeDateBucketDistance( + begin: ChartDateParsed, + end: ChartDateParsed, + transform: ChartXTransformFunction +): number { + switch (transform) { + case 'date:year': + return end.year - begin.year; + case 'date:month': + return (end.year - begin.year) * 12 + (end.month - begin.month); + case 'date:day': + return ( + (end.year - begin.year) * 365 + + (end.month - begin.month) * 30 + // rough approximation + (end.day - begin.day) + ); + case 'date:hour': + return ( + (end.year - begin.year) * 365 * 24 + + (end.month - begin.month) * 30 * 24 + // rough approximation + (end.day - begin.day) * 24 + + (end.hour - begin.hour) + ); + case 'date:minute': + return ( + (end.year - begin.year) * 365 * 24 * 60 + + (end.month - begin.month) * 30 * 24 * 60 + // rough approximation + (end.day - begin.day) * 24 * 60 + + (end.hour - begin.hour) * 60 + + (end.minute - begin.minute) + ); + case 'identity': + default: + return NaN; + } +} + +export function compareChartDatesParsed( + a: ChartDateParsed, + b: ChartDateParsed, + transform: ChartXTransformFunction +): number { + switch (transform) { + case 'date:year': + return a.year - b.year; + case 'date:month': + return a.year === b.year ? a.month - b.month : a.year - b.year; + case 'date:day': + return a.year === b.year && a.month === b.month + ? a.day - b.day + : a.year === b.year + ? a.month - b.month + : a.year - b.year; + case 'date:hour': + return a.year === b.year && a.month === b.month && a.day === b.day + ? a.hour - b.hour + : a.year === b.year && a.month === b.month + ? a.day - b.day + : a.year === b.year + ? a.month - b.month + : a.year - b.year; + + case 'date:minute': + return a.year === b.year && a.month === b.month && a.day === b.day && a.hour === b.hour + ? a.minute - b.minute + : a.year === b.year && a.month === b.month && a.day === b.day + ? a.hour - b.hour + : a.year === b.year && a.month === b.month + ? a.day - b.day + : a.year === b.year + ? a.month - b.month + : a.year - b.year; + } +} + +function getParentDateBucketKey(bucketKey: string, transform: ChartXTransformFunction): string | null { + switch (transform) { + case 'date:year': + return null; // no parent for year + case 'date:month': + return bucketKey.slice(0, 4); + case 'date:day': + return bucketKey.slice(0, 7); + case 'date:hour': + return bucketKey.slice(0, 10); + case 'date:minute': + return bucketKey.slice(0, 13); + } +} + +function getParentDateBucketTransform(transform: ChartXTransformFunction): ChartXTransformFunction | null { + switch (transform) { + case 'date:year': + return null; // no parent for year + case 'date:month': + return 'date:year'; + case 'date:day': + return 'date:month'; + case 'date:hour': + return 'date:day'; + case 'date:minute': + return 'date:hour'; + default: + return null; + } +} + +function getParentKeyParsed(date: ChartDateParsed, transform: ChartXTransformFunction): ChartDateParsed | null { + switch (transform) { + case 'date:year': + return null; // no parent for year + case 'date:month': + return { year: date.year }; + case 'date:day': + return { year: date.year, month: date.month }; + case 'date:hour': + return { year: date.year, month: date.month, day: date.day }; + case 'date:minute': + return { year: date.year, month: date.month, day: date.day, hour: date.hour }; + default: + return null; + } +} + +function createParentChartAggregation(chart: ProcessedChart): ProcessedChart | null { + if (chart.isGivenDefinition) { + // if the chart is created with a given definition, we cannot create a parent aggregation + return null; + } + const parentTransform = getParentDateBucketTransform(chart.definition.xdef.transformFunction); + if (!parentTransform) { + return null; + } + + const res: ProcessedChart = { + definition: { + ...chart.definition, + xdef: { + ...chart.definition.xdef, + transformFunction: parentTransform, + }, + }, + rowsAdded: chart.rowsAdded, + bucketKeysOrdered: [], + buckets: {}, + bucketKeyDateParsed: {}, + isGivenDefinition: false, + invalidXRows: chart.invalidXRows, + invalidYRows: { ...chart.invalidYRows }, // copy invalid Y rows + validYRows: { ...chart.validYRows }, // copy valid Y rows + topDistinctValues: { ...chart.topDistinctValues }, // copy top distinct values + availableColumns: chart.availableColumns, + }; + + for (const [bucketKey, bucketValues] of Object.entries(chart.buckets)) { + const parentKey = getParentDateBucketKey(bucketKey, chart.definition.xdef.transformFunction); + if (!parentKey) { + // skip if the bucket is already a parent + continue; + } + res.bucketKeyDateParsed[parentKey] = getParentKeyParsed( + chart.bucketKeyDateParsed[bucketKey], + chart.definition.xdef.transformFunction + ); + aggregateChartNumericValuesFromChild(res, parentKey, bucketValues); + } + + const bucketKeys = Object.keys(res.buckets).sort(); + res.minX = bucketKeys.length > 0 ? bucketKeys[0] : null; + res.maxX = bucketKeys.length > 0 ? bucketKeys[bucketKeys.length - 1] : null; + + return res; +} + +export function autoAggregateCompactTimelineChart(chart: ProcessedChart) { + while (true) { + const fromParsed = chart.bucketKeyDateParsed[chart.minX]; + const toParsed = chart.bucketKeyDateParsed[chart.maxX]; + + if (!fromParsed || !toParsed) { + return chart; // cannot fill timeline buckets without valid date range + } + const transform = chart.definition.xdef.transformFunction; + if (!transform.startsWith('date:')) { + return chart; // cannot aggregate non-date charts + } + const dateDistance = computeDateBucketDistance(fromParsed, toParsed, transform); + if (dateDistance < (chart.definition.xdef.parentAggregateLimit ?? ChartConstDefaults.parentAggregateLimit)) { + return chart; // no need to aggregate further, the distance is less than the limit + } + + const parentChart = createParentChartAggregation(chart); + if (!parentChart) { + return chart; // cannot create parent aggregation + } + + chart = parentChart; + } +} + +export function aggregateChartNumericValuesFromSource( + chart: ProcessedChart, + bucketKey: string, + numericColumns: { [key: string]: number }, + row: any +) { + for (const ydef of chart.definition.ydefs) { + if (numericColumns[ydef.field] == null) { + if (row[ydef.field]) { + chart.invalidYRows[ydef.field] = (chart.invalidYRows[ydef.field] || 0) + 1; // increment invalid row count if the field is not numeric + } + continue; + } + chart.validYRows[ydef.field] = (chart.validYRows[ydef.field] || 0) + 1; // increment valid row count + + let distinctValues = chart.topDistinctValues[ydef.field]; + if (!distinctValues) { + distinctValues = new Set(); + chart.topDistinctValues[ydef.field] = distinctValues; + } + if (distinctValues.size < ChartLimits.MAX_DISTINCT_VALUES) { + chart.topDistinctValues[ydef.field].add(numericColumns[ydef.field]); + } + + switch (ydef.aggregateFunction) { + case 'sum': + chart.buckets[bucketKey][ydef.field] = + (chart.buckets[bucketKey][ydef.field] || 0) + (numericColumns[ydef.field] || 0); + break; + case 'first': + if (chart.buckets[bucketKey][ydef.field] === undefined) { + chart.buckets[bucketKey][ydef.field] = numericColumns[ydef.field]; + } + break; + case 'last': + chart.buckets[bucketKey][ydef.field] = numericColumns[ydef.field]; + break; + case 'min': + if (chart.buckets[bucketKey][ydef.field] === undefined) { + chart.buckets[bucketKey][ydef.field] = numericColumns[ydef.field]; + } else { + chart.buckets[bucketKey][ydef.field] = Math.min( + chart.buckets[bucketKey][ydef.field], + numericColumns[ydef.field] + ); + } + break; + case 'max': + if (chart.buckets[bucketKey][ydef.field] === undefined) { + chart.buckets[bucketKey][ydef.field] = numericColumns[ydef.field]; + } else { + chart.buckets[bucketKey][ydef.field] = Math.max( + chart.buckets[bucketKey][ydef.field], + numericColumns[ydef.field] + ); + } + break; + case 'count': + chart.buckets[bucketKey][ydef.field] = (chart.buckets[bucketKey][ydef.field] || 0) + 1; + break; + case 'avg': + if (chart.buckets[bucketKey][ydef.field] === undefined) { + chart.buckets[bucketKey][ydef.field] = [numericColumns[ydef.field], 1]; // [sum, count] + } else { + chart.buckets[bucketKey][ydef.field][0] += numericColumns[ydef.field]; + chart.buckets[bucketKey][ydef.field][1] += 1; + } + break; + } + } +} + +export function aggregateChartNumericValuesFromChild( + chart: ProcessedChart, + bucketKey: string, + childBucketValues: { [key: string]: any } +) { + for (const ydef of chart.definition.ydefs) { + if (childBucketValues[ydef.field] == undefined) { + continue; // skip if the field is not present in the child bucket + } + if (!chart.buckets[bucketKey]) { + chart.buckets[bucketKey] = {}; + } + switch (ydef.aggregateFunction) { + case 'sum': + case 'count': + chart.buckets[bucketKey][ydef.field] = + (chart.buckets[bucketKey][ydef.field] || 0) + (childBucketValues[ydef.field] || 0); + break; + case 'min': + if (chart.buckets[bucketKey][ydef.field] === undefined) { + chart.buckets[bucketKey][ydef.field] = childBucketValues[ydef.field]; + } else { + chart.buckets[bucketKey][ydef.field] = Math.min( + chart.buckets[bucketKey][ydef.field], + childBucketValues[ydef.field] + ); + } + break; + case 'max': + if (chart.buckets[bucketKey][ydef.field] === undefined) { + chart.buckets[bucketKey][ydef.field] = childBucketValues[ydef.field]; + } else { + chart.buckets[bucketKey][ydef.field] = Math.max( + chart.buckets[bucketKey][ydef.field], + childBucketValues[ydef.field] + ); + } + break; + case 'avg': + if (chart.buckets[bucketKey][ydef.field] === undefined) { + chart.buckets[bucketKey][ydef.field] = childBucketValues[ydef.field]; + } else { + chart.buckets[bucketKey][ydef.field][0] += childBucketValues[ydef.field][0]; + chart.buckets[bucketKey][ydef.field][1] += childBucketValues[ydef.field][1]; + } + break; + case 'first': + case 'last': + throw new Error(`Cannot aggregate ${ydef.aggregateFunction} for ${ydef.field} in child bucket`); + } + } +} + +export function fillChartTimelineBuckets(chart: ProcessedChart) { + const fromParsed = chart.bucketKeyDateParsed[chart.minX]; + const toParsed = chart.bucketKeyDateParsed[chart.maxX]; + if (!fromParsed || !toParsed) { + return; // cannot fill timeline buckets without valid date range + } + const transform = chart.definition.xdef.transformFunction; + + let currentParsed = fromParsed; + while (compareChartDatesParsed(currentParsed, toParsed, transform) <= 0) { + const bucketKey = stringifyChartDate(currentParsed, transform); + if (!chart.buckets[bucketKey]) { + chart.buckets[bucketKey] = {}; + chart.bucketKeyDateParsed[bucketKey] = currentParsed; + } + currentParsed = incrementChartDate(currentParsed, transform); + } +} + +export function computeChartBucketCardinality(bucket: { [key: string]: any }): number { + return _sumBy(Object.keys(bucket), field => bucket[field]); +} diff --git a/packages/datalib/src/index.ts b/packages/datalib/src/index.ts index 6ef5c50a0..7012e70d2 100644 --- a/packages/datalib/src/index.ts +++ b/packages/datalib/src/index.ts @@ -23,3 +23,5 @@ export * from './FreeTableGridDisplay'; export * from './FreeTableModel'; export * from './CustomGridDisplay'; export * from './ScriptDrivedDeployer'; +export * from './chartDefinitions'; +export * from './chartProcessor'; diff --git a/packages/datalib/src/tests/chartProcessor.test.ts b/packages/datalib/src/tests/chartProcessor.test.ts new file mode 100644 index 000000000..d7601239a --- /dev/null +++ b/packages/datalib/src/tests/chartProcessor.test.ts @@ -0,0 +1,376 @@ +import exp from 'constants'; +import { ChartProcessor } from '../chartProcessor'; +import { getChartDebugPrint } from '../chartTools'; + +const DS1 = [ + { + timestamp: '2023-10-01T12:00:00Z', + value: 42.5, + category: 'B', + related_id: 12, + }, + { + timestamp: '2023-10-02T10:05:00Z', + value: 12, + category: 'A', + related_id: 13, + }, + { + timestamp: '2023-10-03T07:10:00Z', + value: 57, + category: 'A', + related_id: 5, + }, + { + timestamp: '2024-08-03T07:10:00Z', + value: 33, + category: 'B', + related_id: 22, + }, +]; + +const DS2 = [ + { + ts1: '2023-10-01T12:00:00Z', + ts2: '2024-10-01T12:00:00Z', + dummy1: 1, + dummy2: 1, + dummy3: 1, + dummy4: 1, + dummy5: 1, + dummy6: 1, + dummy7: 1, + dummy8: 1, + dummy9: 1, + dummy10: 1, + price1: '11', + price2: '22', + }, + { + ts1: '2023-10-02T10:05:00Z', + ts2: '2024-10-02T10:05:00Z', + price1: '12', + price2: '23', + }, + { + ts1: '2023-10-03T07:10:00Z', + ts2: '2024-10-03T07:10:00Z', + price1: '13', + price2: '24', + }, + { + ts1: '2023-11-04T12:00:00Z', + ts2: '2024-11-04T12:00:00Z', + price1: 1, + price2: 2, + }, +]; + +const DS3 = [ + { + timestamp: '2023-10-01T12:00:00Z', + value: 42.5, + bitval: true, + }, + { + timestamp: '2023-10-02T10:05:00Z', + value: 12, + bitval: false, + }, + { + timestamp: '2023-10-03T07:10:00Z', + value: 57, + bitval: null, + }, +]; + +const DS4 = [ + { + object_id: 710293590, + ObjectName: 'Journal', + Total_Reserved_kb: '68696', + RowsCount: '405452', + }, + { + object_id: 182291709, + ObjectName: 'Employee', + Total_Reserved_kb: '732008', + RowsCount: '1980067', + }, + { + object_id: 23432525, + ObjectName: 'User', + Total_Reserved_kb: '325352', + RowsCount: '2233', + }, + { + object_id: 4985159, + ObjectName: 'Project', + Total_Reserved_kb: '293523', + RowsCount: '1122', + }, +]; + +describe('Chart processor', () => { + test('Simple by day test, autodetected', () => { + const processor = new ChartProcessor(); + processor.addRows(...DS1.slice(0, 3)); + processor.finalize(); + expect(processor.charts.length).toEqual(1); + const chart = processor.charts[0]; + expect(chart.definition.xdef.transformFunction).toEqual('date:day'); + expect(chart.definition.ydefs).toEqual([ + expect.objectContaining({ + field: 'value', + }), + ]); + expect(chart.bucketKeysOrdered).toEqual(['2023-10-01', '2023-10-02', '2023-10-03']); + }); + test('By month grouped, autedetected', () => { + const processor = new ChartProcessor(); + processor.addRows(...DS1.slice(0, 4)); + processor.finalize(); + expect(processor.charts.length).toEqual(1); + const chart = processor.charts[0]; + expect(chart.definition.xdef.transformFunction).toEqual('date:month'); + expect(chart.bucketKeysOrdered).toEqual([ + '2023-10', + '2023-11', + '2023-12', + '2024-01', + '2024-02', + '2024-03', + '2024-04', + '2024-05', + '2024-06', + '2024-07', + '2024-08', + ]); + }); + test('Detect columns', () => { + const processor = new ChartProcessor(); + processor.autoDetectCharts = false; + processor.addRows(...DS1); + processor.finalize(); + expect(processor.charts.length).toEqual(0); + expect(processor.availableColumns).toEqual([ + expect.objectContaining({ + field: 'timestamp', + }), + expect.objectContaining({ + field: 'value', + }), + expect.objectContaining({ + field: 'category', + }), + expect.objectContaining({ + field: 'related_id', + }), + ]); + }); + test('Explicit definition', () => { + const processor = new ChartProcessor([ + { + chartType: 'pie', + xdef: { + field: 'category', + transformFunction: 'identity', + sortOrder: 'natural', + }, + ydefs: [ + { + field: 'related_id', + aggregateFunction: 'sum', + }, + ], + }, + ]); + processor.addRows(...DS1); + processor.finalize(); + expect(processor.charts.length).toEqual(1); + const chart = processor.charts[0]; + expect(chart.definition.xdef.transformFunction).toEqual('identity'); + expect(chart.bucketKeysOrdered).toEqual(['B', 'A']); + expect(chart.buckets).toEqual({ + B: { related_id: 34 }, + A: { related_id: 18 }, + }); + }); + + test('Two data sets with different date columns', () => { + const processor = new ChartProcessor(); + processor.addRows(...DS2); + processor.finalize(); + expect(processor.charts.length).toEqual(2); + expect(processor.charts[0].definition).toEqual( + expect.objectContaining({ + xdef: expect.objectContaining({ + field: 'ts1', + transformFunction: 'date:day', + }), + ydefs: [ + expect.objectContaining({ + field: 'price1', + aggregateFunction: 'sum', + }), + expect.objectContaining({ + field: 'price2', + aggregateFunction: 'sum', + }), + ], + }) + ); + expect(processor.charts[1].definition).toEqual( + expect.objectContaining({ + xdef: expect.objectContaining({ + field: 'ts2', + transformFunction: 'date:day', + }), + ydefs: [ + expect.objectContaining({ + field: 'price1', + aggregateFunction: 'sum', + }), + expect.objectContaining({ + field: 'price2', + aggregateFunction: 'sum', + }), + ], + }) + ); + }); + + test('Exclude boolean fields in autodetected', () => { + const processor = new ChartProcessor(); + processor.addRows(...DS3); + processor.finalize(); + expect(processor.charts.length).toEqual(1); + const chart = processor.charts[0]; + expect(chart.definition.xdef.transformFunction).toEqual('date:day'); + expect(chart.definition.ydefs).toEqual([ + expect.objectContaining({ + field: 'value', + }), + ]); + }); + + test('Added field manual from GUI', () => { + const processor = new ChartProcessor([ + { + chartType: 'bar', + xdef: { + field: 'object_id', + transformFunction: 'identity', + }, + ydefs: [ + { + field: 'object_id', + aggregateFunction: 'sum', + }, + ], + }, + ]); + processor.addRows(...DS4); + processor.finalize(); + expect(processor.charts.length).toEqual(1); + const chart = processor.charts[0]; + expect(chart.definition.xdef.transformFunction).toEqual('identity'); + expect(chart.definition.ydefs).toEqual([ + expect.objectContaining({ + field: 'object_id', + aggregateFunction: 'sum', + }), + ]); + }); + + const PieMainTestData = [ + ['natural', ['Journal', 'Employee', 'User', 'Project']], + ['ascKeys', ['Employee', 'Journal', 'Project', 'User']], + ['descKeys', ['User', 'Project', 'Journal', 'Employee']], + ['ascValues', ['Project', 'User', 'Journal', 'Employee']], + ['descValues', ['Employee', 'Journal', 'User', 'Project']], + ]; + + test.each(PieMainTestData)('Pie chart - used space for DB objects (%s)', (sortOrder, expectedOrder) => { + const processor = new ChartProcessor([ + { + chartType: 'bar', + xdef: { + field: 'ObjectName', + transformFunction: 'identity', + sortOrder: sortOrder as any, + }, + ydefs: [ + { + field: 'RowsCount', + aggregateFunction: 'sum', + }, + ], + }, + ]); + processor.addRows(...DS4); + processor.finalize(); + expect(processor.charts.length).toEqual(1); + const chart = processor.charts[0]; + expect(chart.bucketKeysOrdered).toEqual(expectedOrder); + expect(chart.buckets).toEqual({ + Employee: { RowsCount: 1980067 }, + Journal: { RowsCount: 405452 }, + Project: { RowsCount: 1122 }, + User: { RowsCount: 2233 }, + }); + }); + + const PieOtherTestData = [ + [ + 'ratio', + 0.1, + 5, + ['Employee', 'Journal', 'Other'], + { + Employee: { RowsCount: 1980067 }, + Journal: { RowsCount: 405452 }, + Other: { RowsCount: 3355 }, + }, + ], + [ + 'count', + 0, + 1, + ['Employee', 'Other'], + { + Employee: { RowsCount: 1980067 }, + Other: { RowsCount: 408807 }, + }, + ], + ]; + + test.each(PieOtherTestData)( + 'Pie limit test - %s', + (_description, pieRatioLimit, pieCountLimit, expectedOrder, expectedBuckets) => { + const processor = new ChartProcessor([ + { + chartType: 'pie', + pieRatioLimit: pieRatioLimit as number, + pieCountLimit: pieCountLimit as number, + xdef: { + field: 'ObjectName', + transformFunction: 'identity', + }, + ydefs: [ + { + field: 'RowsCount', + aggregateFunction: 'sum', + }, + ], + }, + ]); + processor.addRows(...DS4); + processor.finalize(); + expect(processor.charts.length).toEqual(1); + const chart = processor.charts[0]; + expect(chart.bucketKeysOrdered).toEqual(expectedOrder); + expect(chart.buckets).toEqual(expectedBuckets); + } + ); +}); diff --git a/packages/tools/src/stringTools.ts b/packages/tools/src/stringTools.ts index 281d914af..19a867b0f 100644 --- a/packages/tools/src/stringTools.ts +++ b/packages/tools/src/stringTools.ts @@ -5,7 +5,10 @@ import _isNumber from 'lodash/isNumber'; import _isPlainObject from 'lodash/isPlainObject'; import _pad from 'lodash/pad'; import _cloneDeepWith from 'lodash/cloneDeepWith'; +import _isEmpty from 'lodash/isEmpty'; +import _omitBy from 'lodash/omitBy'; import { DataEditorTypesBehaviour } from 'dbgate-types'; +import isPlainObject from 'lodash/isPlainObject'; export type EditorDataType = | 'null' @@ -633,3 +636,38 @@ export function parseNumberSafe(value) { } return parseFloat(value); } + +const frontMatterRe = /^--\ >>>[ \t]*\n(.*)\n-- <<<[ \t]*\n/s; + +export function getSqlFrontMatter(text: string, yamlModule) { + const match = text.match(frontMatterRe); + if (!match) return null; + const yamlContentMapped = match[1].replace(/^--[ ]?/gm, ''); + return yamlModule.load(yamlContentMapped); +} + +export function removeSqlFrontMatter(text: string) { + return text.replace(frontMatterRe, ''); +} + +export function setSqlFrontMatter(text: string, data: { [key: string]: any }, yamlModule) { + const textClean = removeSqlFrontMatter(text); + + if (!isPlainObject(data)) { + return textClean; + } + + const dataClean = _omitBy(data, v => v === undefined); + + if (_isEmpty(dataClean)) { + return textClean; + } + const yamlContent = yamlModule.dump(dataClean); + const yamlContentMapped = yamlContent + .trimRight() + .split('\n') + .map(line => '-- ' + line) + .join('\n'); + const frontMatterContent = `-- >>>\n${yamlContentMapped}\n-- <<<\n`; + return frontMatterContent + textClean; +} diff --git a/packages/tools/tsconfig.json b/packages/tools/tsconfig.json index b2671e70a..a2fd185f1 100644 --- a/packages/tools/tsconfig.json +++ b/packages/tools/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES2015", + "target": "ES2018", "module": "commonjs", "declaration": true, "skipLibCheck": true, diff --git a/packages/web/src/appobj/DatabaseObjectAppObject.svelte b/packages/web/src/appobj/DatabaseObjectAppObject.svelte index 484e50269..948d2311b 100644 --- a/packages/web/src/appobj/DatabaseObjectAppObject.svelte +++ b/packages/web/src/appobj/DatabaseObjectAppObject.svelte @@ -185,10 +185,6 @@ isImport: true, requiresWriteAccess: true, }, - hasPermission('dbops/charts') && { - label: 'Open active chart', - isActiveChart: true, - }, ]; case 'views': return [ @@ -245,10 +241,6 @@ isExport: true, functionName: 'tableReader', }, - { - label: 'Open active chart', - isActiveChart: true, - }, ]; case 'matviews': return [ @@ -299,10 +291,6 @@ isExport: true, functionName: 'tableReader', }, - { - label: 'Open active chart', - isActiveChart: true, - }, ]; case 'queries': return [ @@ -472,28 +460,7 @@ return driver; }; - if (menu.isActiveChart) { - const driver = await getDriver(); - const dmp = driver.createDumper(); - dmp.put('^select * from %f', data); - openNewTab( - { - title: data.pureName, - icon: 'img chart', - tabComponent: 'ChartTab', - props: { - conid: data.conid, - database: data.database, - }, - }, - { - editor: { - config: { chartType: 'bar' }, - sql: dmp.s, - }, - } - ); - } else if (menu.isQueryDesigner) { + if (menu.isQueryDesigner) { openNewTab( { title: 'Query #', diff --git a/packages/web/src/appobj/SavedFileAppObject.svelte b/packages/web/src/appobj/SavedFileAppObject.svelte index 2cd5e70bc..1cf2ec306 100644 --- a/packages/web/src/appobj/SavedFileAppObject.svelte +++ b/packages/web/src/appobj/SavedFileAppObject.svelte @@ -41,16 +41,6 @@ label: 'Markdown file', }; - const charts: FileTypeHandler = { - icon: 'img chart', - format: 'json', - tabComponent: 'ChartTab', - folder: 'charts', - currentConnection: true, - extension: 'json', - label: 'Chart file', - }; - const query: FileTypeHandler = { icon: 'img query-design', format: 'json', @@ -139,7 +129,6 @@ sql, shell, markdown, - charts, query, sqlite, diagrams, diff --git a/packages/web/src/datagrid/DataGridCore.svelte b/packages/web/src/datagrid/DataGridCore.svelte index aff9751d0..fe79b787d 100644 --- a/packages/web/src/datagrid/DataGridCore.svelte +++ b/packages/web/src/datagrid/DataGridCore.svelte @@ -261,13 +261,6 @@ testEnabled: () => getCurrentDataGrid() != null, onClick: () => getCurrentDataGrid().openFreeTable(), }); - registerCommand({ - id: 'dataGrid.openChartFromSelection', - category: 'Data grid', - name: 'Open chart from selection', - testEnabled: () => getCurrentDataGrid() != null, - onClick: () => getCurrentDataGrid().openChartFromSelection(), - }); registerCommand({ id: 'dataGrid.newJson', category: 'Data grid', @@ -469,6 +462,7 @@ export let hideGridLeftColumn = false; export let overlayDefinition = null; export let onGetSelectionMenu = null; + export let onOpenChart = null; export const activator = createActivator('DataGridCore', false); @@ -715,23 +709,6 @@ openJsonLinesData(getSelectedFreeDataRows()); } - export function openChartFromSelection() { - openNewTab( - { - title: 'Chart #', - icon: 'img chart', - tabComponent: 'ChartTab', - props: {}, - }, - { - editor: { - data: getSelectedFreeData(), - config: { chartType: 'bar' }, - }, - } - ); - } - export function viewJsonDocumentEnabled() { return isDynamicStructure && _.uniq(selectedCells.map(x => x[0])).length == 1; } @@ -1869,9 +1846,13 @@ // ], // }, isProApp() && { command: 'dataGrid.sendToDataDeploy' }, + isProApp() && + onOpenChart && { + text: 'Open chart', + onClick: () => onOpenChart(), + }, { command: 'dataGrid.generateSqlFromData' }, { command: 'dataGrid.openFreeTable' }, - { command: 'dataGrid.openChartFromSelection' }, { command: 'dataGrid.openSelectionInMap', hideDisabled: true }, { placeTag: 'chart' } ); diff --git a/packages/web/src/datagrid/SqlDataGridCore.svelte b/packages/web/src/datagrid/SqlDataGridCore.svelte index 0e192b38b..2acbfce70 100644 --- a/packages/web/src/datagrid/SqlDataGridCore.svelte +++ b/packages/web/src/datagrid/SqlDataGridCore.svelte @@ -1,14 +1,6 @@
diff --git a/packages/web/src/elements/TabControl.svelte b/packages/web/src/elements/TabControl.svelte index a89d6bfa3..4c7ee15aa 100644 --- a/packages/web/src/elements/TabControl.svelte +++ b/packages/web/src/elements/TabControl.svelte @@ -18,6 +18,7 @@ export let flex1 = true; export let contentTestId = undefined; export let inlineTabs = false; + export let onUserChange = null; export function setValue(index) { value = index; @@ -30,8 +31,16 @@
{#each _.compact(tabs) as tab, index} -
(value = index)} data-testid={tab.testid}> - +
{ + value = index; + onUserChange?.(index); + }} + data-testid={tab.testid} + > + {tab.label}
@@ -139,5 +148,4 @@ .container.isInline:not(.tabVisible) { display: none; } - diff --git a/packages/web/src/icons/FontIcon.svelte b/packages/web/src/icons/FontIcon.svelte index dcc1a68a5..83a51c23e 100644 --- a/packages/web/src/icons/FontIcon.svelte +++ b/packages/web/src/icons/FontIcon.svelte @@ -71,6 +71,7 @@ 'icon trigger': 'mdi mdi-lightning-bolt', 'icon scheduler-event': 'mdi mdi-calendar-blank', 'icon arrow-link': 'mdi mdi-arrow-top-right-thick', + 'icon reset': 'mdi mdi-cancel', 'icon window-restore': 'mdi mdi-window-restore', 'icon window-maximize': 'mdi mdi-window-maximize', diff --git a/packages/web/src/query/ResultTabs.svelte b/packages/web/src/query/ResultTabs.svelte index 8597cc652..b57384441 100644 --- a/packages/web/src/query/ResultTabs.svelte +++ b/packages/web/src/query/ResultTabs.svelte @@ -1,5 +1,5 @@ setOneTabValue(false) } : { text: 'All results in one tab', onClick: () => setOneTabValue(true) }, ]} + onUserChange={value => { + if (allTabs[value].isChart) { + onSetFrontMatterField?.(`selected-chart`, allTabs[value].resultIndex + 1); + } else { + onSetFrontMatterField?.(`selected-chart`, undefined); + } + }} > diff --git a/packages/web/src/tabs/QueryTab.svelte b/packages/web/src/tabs/QueryTab.svelte index da772adb9..d77ad9f72 100644 --- a/packages/web/src/tabs/QueryTab.svelte +++ b/packages/web/src/tabs/QueryTab.svelte @@ -1,6 +1,7 @@