Compare commits

...

25 Commits

Author SHA1 Message Date
Stela Augustinova 2824681bff Refactor cloudIdToLabel assignment to use lodash's fromPairs for improved readability 2026-03-18 13:47:45 +01:00
Stela Augustinova 073a3e3946 Add cloud content list integration for connection label resolution 2026-03-18 11:23:31 +01:00
Stela Augustinova f99994085a Refactor connection selection to use a dropdown instead of a button for improved usability 2026-03-17 09:22:18 +01:00
Stela Augustinova a548b0d543 Refactor connection label assignment to use logical OR for fallback 2026-03-16 15:05:45 +01:00
Stela Augustinova de94f15383 Fix file reading to correctly handle bytes read from file 2026-03-16 14:41:38 +01:00
Stela Augustinova 7045d986ef Fix file handle management to ensure proper closure in file reading process 2026-03-16 14:31:43 +01:00
Stela Augustinova de7ae9cf09 Refactor connection filter options 2026-03-16 14:17:06 +01:00
Stela Augustinova ab3d6888dc Enhance file reading and connection filtering in SavedFilesList component 2026-03-16 14:08:19 +01:00
Stela Augustinova 98a70891f3 Refactor file reading 2026-03-16 08:12:35 +01:00
Stela Augustinova 52e7326a2c Enhance file listing to support front matter parsing and connection filtering 2026-03-16 08:02:03 +01:00
Jan Prochazka bfd2e3b07a Merge pull request #1382 from dbgate/feature/add-files-button
Enhance drag-and-drop functionality to support Electron file paths
2026-03-12 12:41:31 +01:00
Stela Augustinova 799f5e30d3 Enhance drag-and-drop functionality to support Electron file paths 2026-03-12 10:14:47 +01:00
SPRINX0\prochazka d3e544c3c0 v7.1.3-premium-beta.4 2026-03-11 08:55:53 +01:00
CI workflows 866fd55834 chore: auto-update github workflows 2026-03-10 10:17:13 +00:00
CI workflows 74ce1fba32 Update pro ref 2026-03-10 10:16:57 +00:00
Jan Prochazka a11b93b4cc SYNC: Merge pull request #80 from dbgate/feature/loading-fix 2026-03-10 10:16:46 +00:00
CI workflows 066f2baa03 chore: auto-update github workflows 2026-03-10 09:50:36 +00:00
Stela Augustinova e02396280f SYNC: Add port mappings for DynamoDB and fix formatting in e2e-pro.yaml 2026-03-10 09:50:18 +00:00
CI workflows a654c80746 chore: auto-update github workflows 2026-03-10 09:32:53 +00:00
CI workflows 3b50f4bd7c Update pro ref 2026-03-10 09:32:34 +00:00
CI workflows cc1f77f5bc chore: auto-update github workflows 2026-03-10 08:23:51 +00:00
CI workflows 381fce4a82 Update pro ref 2026-03-10 08:23:35 +00:00
Jan Prochazka bc3be97cee SYNC: Merge pull request #81 from dbgate/feature/dynamo-e2e 2026-03-10 08:22:32 +00:00
Jan Prochazka 1c389208a7 Merge pull request #1378 from dbgate/feature/add-files-button
Import getElectron in ElectronFilesInput component
2026-03-10 09:19:34 +01:00
Stela Augustinova e1ead2519a Import getElectron in ElectronFilesInput component 2026-03-09 07:35:34 +01:00
45 changed files with 1974 additions and 1434 deletions
+1 -1
View File
@@ -47,7 +47,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: c33e71a6ddc30d8ce59cf0351e04e08f6be272a3
ref: 0bc104411450ea2849bcf4d6ef3dfc4b48ed7821
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro
+1 -1
View File
@@ -47,7 +47,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: c33e71a6ddc30d8ce59cf0351e04e08f6be272a3
ref: 0bc104411450ea2849bcf4d6ef3dfc4b48ed7821
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro
+1 -1
View File
@@ -39,7 +39,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: c33e71a6ddc30d8ce59cf0351e04e08f6be272a3
ref: 0bc104411450ea2849bcf4d6ef3dfc4b48ed7821
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro
+1 -1
View File
@@ -44,7 +44,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: c33e71a6ddc30d8ce59cf0351e04e08f6be272a3
ref: 0bc104411450ea2849bcf4d6ef3dfc4b48ed7821
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: c33e71a6ddc30d8ce59cf0351e04e08f6be272a3
ref: 0bc104411450ea2849bcf4d6ef3dfc4b48ed7821
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro
-5
View File
@@ -81,11 +81,6 @@ jobs:
working-directory: packages/dbmodel
run: |
npm publish --tag "$NPM_TAG"
- name: Publish rest
working-directory: packages/rest
run: |
npm publish --tag "$NPM_TAG"
- name: Publish dbgate-plugin-csv
working-directory: plugins/dbgate-plugin-csv
run: |
+5 -1
View File
@@ -30,7 +30,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: c33e71a6ddc30d8ce59cf0351e04e08f6be272a3
ref: 0bc104411450ea2849bcf4d6ef3dfc4b48ed7821
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro
@@ -132,6 +132,10 @@ jobs:
image: redis
ports:
- '16011:6379'
dynamodb:
image: amazon/dynamodb-local
ports:
- '16015:8000'
mssql:
image: mcr.microsoft.com/mssql/server
ports:
+2 -1
View File
@@ -4,5 +4,6 @@ module.exports = {
mssql: true,
oracle: true,
sqlite: true,
mongo: true
mongo: true,
dynamo: true,
};
+39
View File
@@ -512,4 +512,43 @@ describe('Data browser data', () => {
cy.testid('DataFilterControl_input_ArtistId.Name').type('mich{enter}');
cy.themeshot('data-browser-filter-by-expanded');
});
it('DynamoDB', () => {
cy.contains('Dynamo-connection').click();
cy.contains('us-east-1').click();
cy.contains('Album').click();
cy.contains('Pearl Jam').click();
cy.themeshot('dynamodb-table-data');
cy.contains('Switch to JSON').click();
cy.themeshot('dynamodb-json-view');
cy.contains('Customer').click();
cy.testid('DataFilterControl_input_CustomerId').type('<=10{enter}');
cy.contains('Rows: 10');
cy.wait(1000);
cy.contains('Helena').click().rightclick();
cy.contains('Show cell data').click();
cy.contains('City: "Prague"');
cy.themeshot('dynamodb-query-json-view');
cy.contains('Switch to JSON').click();
cy.contains('Leonie').rightclick();
cy.contains('Edit document').click();
Array.from({ length: 11 }).forEach(() => cy.realPress('ArrowDown'));
Array.from({ length: 14 }).forEach(() => cy.realPress('ArrowRight'));
Array.from({ length: 7 }).forEach(() => cy.realPress('Delete'));
cy.realType('Italy');
cy.testid('EditJsonModal_saveButton').click();
cy.contains('Helena').rightclick();
cy.contains('Delete document').click();
cy.contains('Save').click();
cy.themeshot('dynamodb-save-changes');
cy.testid('SqlObjectList_addButton').click();
cy.contains('New collection/container').click();
cy.themeshot('dynamodb-new-collection');
});
});
+3
View File
@@ -52,6 +52,9 @@ function multiTest(testProps, testDefinition) {
if (localconfig.mongo && !testProps.skipMongo) {
it('MongoDB', () => testDefinition('Mongo-connection', 'my_guitar_shop', 'mongo@dbgate-plugin-mongo'));
}
if (localconfig.dynamo && !testProps.skipMongo) {
it('DynamoDB', () => testDefinition('Dynamo-connection', null, 'dynamodb@dbgate-plugin-dynamodb'));
}
}
describe('Transactions', () => {
+12 -7
View File
@@ -5,14 +5,14 @@ services:
restart: always
environment:
POSTGRES_PASSWORD: Pwd2020Db
ports:
ports:
- 16000:5432
mariadb:
image: mariadb
command: --default-authentication-plugin=mysql_native_password
restart: always
ports:
ports:
- 16004:3306
environment:
- MYSQL_ROOT_PASSWORD=Pwd2020Db
@@ -20,21 +20,21 @@ services:
mysql-ssh-login:
build: containers/mysql-ssh-login
restart: always
ports:
ports:
- 16017:3306
- "16012:22"
- '16012:22'
mysql-ssh-keyfile:
build: containers/mysql-ssh-keyfile
restart: always
ports:
ports:
- 16007:3306
- "16008:22"
- '16008:22'
dex:
build: containers/dex
ports:
- "16009:5556"
- '16009:5556'
mongo:
image: mongo:4.4.29
@@ -50,6 +50,11 @@ services:
ports:
- 16011:6379
dynamodb:
image: amazon/dynamodb-local
ports:
- 16015:8000
mssql:
image: mcr.microsoft.com/mssql/server
restart: always
+7 -1
View File
@@ -1,4 +1,4 @@
CONNECTIONS=mysql,postgres,mongo
CONNECTIONS=mysql,postgres,mongo,dynamo
LABEL_mysql=MySql-connection
SERVER_mysql=localhost
@@ -22,3 +22,9 @@ USER_mongo=root
PASSWORD_mongo=Pwd2020Db
PORT_mongo=16010
ENGINE_mongo=mongo@dbgate-plugin-mongo
LABEL_dynamo=Dynamo-connection
SERVER_dynamo=localhost
PORT_dynamo=16015
AUTH_TYPE_dynamo=onpremise
ENGINE_dynamo=dynamodb@dbgate-plugin-dynamodb
+8 -1
View File
@@ -1,4 +1,4 @@
CONNECTIONS=mysql,postgres,mssql,oracle,sqlite,mongo
CONNECTIONS=mysql,postgres,mssql,oracle,sqlite,mongo,dynamo
LOG_CONNECTION_SENSITIVE_VALUES=true
LABEL_mysql=MySql-connection
@@ -43,3 +43,10 @@ PASSWORD_mongo=Pwd2020Db
PORT_mongo=16010
ENGINE_mongo=mongo@dbgate-plugin-mongo
LABEL_dynamo=Dynamo-connection
SERVER_dynamo=localhost
PORT_dynamo=16015
AUTH_TYPE_dynamo=onpremise
DATABASE_dynamo=localhost
ENGINE_dynamo=dynamodb@dbgate-plugin-dynamodb
+32
View File
@@ -8,6 +8,8 @@ const dbgatePluginMysql = require('dbgate-plugin-mysql');
dbgateApi.registerPlugins(dbgatePluginMysql);
const dbgatePluginPostgres = require('dbgate-plugin-postgres');
dbgateApi.registerPlugins(dbgatePluginPostgres);
const dbgatePluginDynamodb = require('dbgate-plugin-dynamodb');
dbgateApi.registerPlugins(dbgatePluginDynamodb);
async function initMySqlDatabase(dbname, inputFile) {
await dbgateApi.executeQuery({
@@ -125,6 +127,34 @@ async function initMongoDatabase(dbname, inputDirectory) {
// });
}
async function initDynamoDatabase(inputDirectory) {
const dynamodbConnection = {
server: process.env.SERVER_dynamo,
port: process.env.PORT_dynamo,
authType: 'onpremise',
engine: 'dynamodb@dbgate-plugin-dynamodb',
};
const driver = dbgatePluginDynamodb.drivers.find(d => d.engine === 'dynamodb@dbgate-plugin-dynamodb');
const pool = await driver.connect(dynamodbConnection);
const collections = await driver.listCollections(pool);
for (const collection of collections) {
await driver.dropTable(pool, collection);
}
await driver.disconnect(pool);
for (const file of fs.readdirSync(inputDirectory)) {
const pureName = path.parse(file).name;
const src = await dbgateApi.jsonLinesReader({ fileName: path.join(inputDirectory, file) });
const dst = await dbgateApi.tableWriter({
connection: dynamodbConnection,
pureName,
createIfNotExists: true,
});
await dbgateApi.copyStream(src, dst);
}
}
const baseDir = path.join(os.homedir(), '.dbgate');
async function copyFolder(source, target) {
@@ -148,6 +178,8 @@ async function run() {
await initMongoDatabase('MgChinook', path.resolve(path.join(__dirname, '../data/chinook-jsonl')));
await initMongoDatabase('MgRivers', path.resolve(path.join(__dirname, '../data/rivers-jsonl')));
await initDynamoDatabase(path.resolve(path.join(__dirname, '../data/chinook-jsonl')));
await copyFolder(
path.resolve(path.join(__dirname, '../data/chinook-jsonl')),
path.join(baseDir, 'archive-e2etests', 'default')
+24
View File
@@ -7,6 +7,8 @@ const dbgatePluginMysql = require('dbgate-plugin-mysql');
dbgateApi.registerPlugins(dbgatePluginMysql);
const dbgatePluginPostgres = require('dbgate-plugin-postgres');
dbgateApi.registerPlugins(dbgatePluginPostgres);
const dbgatePluginDynamodb = require('dbgate-plugin-dynamodb');
dbgateApi.registerPlugins(dbgatePluginDynamodb);
async function createDb(connection, dropDbSql, createDbSql, database = 'my_guitar_shop', { dropDatabaseName } = {}) {
if (dropDbSql) {
@@ -125,6 +127,28 @@ async function run() {
{ dropDatabaseName: 'my_guitar_shop' }
);
}
if (localconfig.dynamo) {
const dynamodbConnection = {
server: process.env.SERVER_dynamo,
port: process.env.PORT_dynamo,
authType: 'onpremise',
engine: 'dynamodb@dbgate-plugin-dynamodb',
};
const driver = dbgatePluginDynamodb.drivers.find(d => d.engine === 'dynamodb@dbgate-plugin-dynamodb');
const pool = await driver.connect(dynamodbConnection);
const collections = await driver.listCollections(pool);
for (const collection of collections) {
await driver.dropTable(pool, collection);
}
await driver.disconnect(pool);
await dbgateApi.importDbFromFolder({
connection: dynamodbConnection,
folder: path.resolve(path.join(__dirname, '../data/my-guitar-shop')),
});
}
}
dbgateApi.runScript(run);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"private": true,
"version": "7.1.3-alpha.3",
"version": "7.1.3-premium-beta.4",
"name": "dbgate-all",
"workspaces": [
"packages/*",
@@ -95,10 +95,12 @@ module.exports = {
}
},
handle_response(conid, database, { msgid, ...response }) {
const [resolve, reject, additionalData] = this.requests[msgid];
resolve(response);
if (additionalData?.auditLogger) {
additionalData?.auditLogger(response);
const [resolve, reject, additionalData] = this.requests[msgid] || [];
if (resolve) {
resolve(response);
if (additionalData?.auditLogger) {
additionalData?.auditLogger(response);
}
}
delete this.requests[msgid];
},
@@ -239,7 +241,7 @@ module.exports = {
sendRequest(conn, message, additionalData = {}) {
const msgid = crypto.randomUUID();
const promise = new Promise((resolve, reject) => {
this.requests[msgid] = [resolve, reject, additionalData];
this.requests[msgid] = [resolve, reject, additionalData, conn.conid, conn.database];
try {
const serializedMessage = serializeJsTypesForJsonStringify({ msgid, ...message });
conn.subprocess.send(serializedMessage);
@@ -264,12 +266,12 @@ module.exports = {
},
sqlSelect_meta: true,
async sqlSelect({ conid, database, select, auditLogSessionGroup }, req) {
async sqlSelect({ conid, database, select, commandTimeout, auditLogSessionGroup }, req) {
await testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid, database);
const res = await this.sendRequest(
opened,
{ msgtype: 'sqlSelect', select },
{ msgtype: 'sqlSelect', select, commandTimeout },
{
auditLogger:
auditLogSessionGroup && select?.from?.name?.pureName
@@ -344,9 +346,12 @@ module.exports = {
},
collectionData_meta: true,
async collectionData({ conid, database, options, auditLogSessionGroup }, req) {
async collectionData({ conid, database, options, commandTimeout, auditLogSessionGroup }, req) {
await testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid, database);
if (commandTimeout && options) {
options.commandTimeout = commandTimeout;
}
const res = await this.sendRequest(
opened,
{ msgtype: 'collectionData', options },
@@ -580,6 +585,24 @@ module.exports = {
};
},
pingDatabases_meta: true,
async pingDatabases({ databases }, req) {
if (!databases || !Array.isArray(databases)) return { status: 'ok' };
for (const { conid, database } of databases) {
if (!conid || !database) continue;
const existing = this.opened.find(x => x.conid == conid && x.database == database);
if (existing) {
try {
existing.subprocess.send({ msgtype: 'ping' });
} catch (err) {
logger.error(extractErrorLogData(err), 'DBGM-00000 Error pinging DB connection');
this.close(conid, database);
}
}
}
return { status: 'ok' };
},
refresh_meta: true,
async refresh({ conid, database, keepOpen }, req) {
await testConnectionPermission(conid, req);
@@ -622,6 +645,15 @@ module.exports = {
structure: existing.structure,
};
socket.emitChanged(`database-status-changed`, { conid, database });
// Reject all pending requests for this connection
for (const [msgid, entry] of Object.entries(this.requests)) {
const [resolve, reject, additionalData, reqConid, reqDatabase] = entry;
if (reqConid === conid && reqDatabase === database) {
reject('DBGM-00000 Database connection closed');
delete this.requests[msgid];
}
}
}
},
+38 -4
View File
@@ -15,7 +15,8 @@ const getDiagramExport = require('../utility/getDiagramExport');
const apps = require('./apps');
const getMapExport = require('../utility/getMapExport');
const dbgateApi = require('../shell');
const { getLogger } = require('dbgate-tools');
const { getLogger, getSqlFrontMatter } = require('dbgate-tools');
const yaml = require('js-yaml');
const platformInfo = require('../utility/platformInfo');
const { checkSecureFilePathsWithoutDirectory, checkSecureDirectories } = require('../utility/security');
const { copyAppLogsIntoFile, getRecentAppLogRecords } = require('../utility/appLogStore');
@@ -35,13 +36,46 @@ function deserialize(format, text) {
module.exports = {
list_meta: true,
async list({ folder }, req) {
async list({ folder, parseFrontMatter }, req) {
const loadedPermissions = await loadPermissionsFromRequest(req);
if (!hasPermission(`files/${folder}/read`, loadedPermissions)) return [];
const dir = path.join(filesdir(), folder);
if (!(await fs.exists(dir))) return [];
const files = (await fs.readdir(dir)).map(file => ({ folder, file }));
return files;
const fileNames = await fs.readdir(dir);
if (!parseFrontMatter) {
return fileNames.map(file => ({ folder, file }));
}
const result = [];
for (const file of fileNames) {
const item = { folder, file };
let fh;
try {
fh = await require('fs').promises.open(path.join(dir, file), 'r');
const buf = new Uint8Array(512);
const { bytesRead } = await fh.read(buf, 0, 512, 0);
let text = Buffer.from(buf.buffer, 0, bytesRead).toString('utf-8');
if (text.includes('-- >>>') && !text.includes('-- <<<')) {
const stat = await fh.stat();
const fullSize = Math.min(stat.size, 4096);
if (fullSize > 512) {
const fullBuf = new Uint8Array(fullSize);
const { bytesRead: fullBytesRead } = await fh.read(fullBuf, 0, fullSize, 0);
text = Buffer.from(fullBuf.buffer, 0, fullBytesRead).toString('utf-8');
}
}
const fm = getSqlFrontMatter(text, yaml);
if (fm?.connectionId) item.connectionId = fm.connectionId;
if (fm?.databaseName) item.databaseName = fm.databaseName;
} catch (e) {
// ignore read errors for individual files
} finally {
if (fh) await fh.close().catch(() => {});
}
result.push(item);
}
return result;
},
listAll_meta: true,
@@ -234,12 +234,12 @@ async function handleRunOperation({ msgid, operation, useTransaction }, skipRead
}
}
async function handleQueryData({ msgid, sql, range }, skipReadonlyCheck = false) {
async function handleQueryData({ msgid, sql, range, commandTimeout }, skipReadonlyCheck = false) {
await waitConnected();
const driver = requireEngineDriver(storedConnection);
try {
if (!skipReadonlyCheck) ensureExecuteCustomScript(driver);
const res = await driver.query(dbhan, sql, { range });
const res = await driver.query(dbhan, sql, { range, commandTimeout });
process.send({ msgtype: 'response', msgid, ...serializeJsTypesForJsonStringify(res) });
} catch (err) {
process.send({
@@ -250,11 +250,11 @@ async function handleQueryData({ msgid, sql, range }, skipReadonlyCheck = false)
}
}
async function handleSqlSelect({ msgid, select }) {
async function handleSqlSelect({ msgid, select, commandTimeout }) {
const driver = requireEngineDriver(storedConnection);
const dmp = driver.createDumper();
dumpSqlSelect(dmp, select);
return handleQueryData({ msgid, sql: dmp.s, range: select.range }, true);
return handleQueryData({ msgid, sql: dmp.s, range: select.range, commandTimeout }, true);
}
async function handleDriverDataCore(msgid, callMethod, { logName }) {
+1
View File
@@ -59,6 +59,7 @@ export interface QueryOptions {
importSqlDump?: boolean;
range?: { offset: number; limit: number };
readonly?: boolean;
commandTimeout?: number;
}
export interface WriteTableOptions {
@@ -1,286 +1,307 @@
<script context="module" lang="ts">
import { __t } from '../translations';
const getCurrentEditor = () => getActiveComponent('CollectionDataGridCore');
registerCommand({
id: 'collectionDataGrid.openQuery',
category: __t('command.dataGrid', { defaultMessage: 'Data grid' }),
name: __t('command.dataGrid.openQuery', { defaultMessage: 'Open query' }),
testEnabled: () => getCurrentEditor() != null,
onClick: () => getCurrentEditor().openQuery(),
});
registerCommand({
id: 'collectionDataGrid.export',
category: __t('command.dataGrid', { defaultMessage: 'Data grid' }),
name: __t('command.dataGrid.export', { defaultMessage: 'Export' }),
keyText: 'CtrlOrCommand+E',
icon: 'icon export',
testEnabled: () => getCurrentEditor() != null,
onClick: () => getCurrentEditor().exportGrid(),
});
function buildConditionForGrid(props) {
const filters = props?.display?.config?.filters;
const filterBehaviour =
props?.display?.driver?.getFilterBehaviour(null, standardFilterBehaviours) ?? mongoFilterBehaviour;
// console.log('USED FILTER BEHAVIOUR', filterBehaviour);
const conditions = [];
for (const uniqueName in filters || {}) {
if (!filters[uniqueName]) continue;
try {
const ast = parseFilter(filters[uniqueName], filterBehaviour);
// console.log('AST', ast);
const cond = _.cloneDeepWith(ast, expr => {
if (expr.exprType == 'placeholder') {
return {
exprType: 'column',
columnName: uniqueName,
};
}
// if (expr.__placeholder__) {
// return {
// [uniqueName]: expr.__placeholder__,
// };
// }
});
conditions.push(cond);
} catch (err) {
// error in filter
}
}
return conditions.length > 0
? {
conditionType: 'and',
conditions,
}
: undefined;
}
function buildSortForGrid(props) {
const sort = props?.display?.config?.sort;
if (sort?.length > 0) {
return sort.map(col => ({
columnName: col.uniqueName,
direction: col.order,
}));
}
return null;
}
export async function loadCollectionDataPage(props, offset, limit) {
const { conid, database } = props;
const response = await apiCall('database-connections/collection-data', {
conid,
database,
options: {
pureName: props.pureName,
limit,
skip: offset,
condition: buildConditionForGrid(props),
sort: buildSortForGrid(props),
},
auditLogSessionGroup: 'data-grid',
});
if (response.errorMessage) return response;
return response.rows;
}
function dataPageAvailable(props) {
return true;
// const { display } = props;
// const sql = display.getPageQuery(0, 1);
// return !!sql;
}
async function loadRowCount(props) {
const { conid, database } = props;
const response = await apiCall('database-connections/collection-data', {
conid,
database,
options: {
pureName: props.pureName,
countDocuments: true,
condition: buildConditionForGrid(props),
},
});
return response.count;
}
</script>
<script lang="ts">
import { parseFilter } from 'dbgate-filterparser';
import _ from 'lodash';
import { registerQuickExportHandler } from '../buttons/ToolStripExportButton.svelte';
import registerCommand from '../commands/registerCommand';
import {
extractShellConnection,
extractShellConnectionHostable,
extractShellHostConnection,
} from '../impexp/createImpExpScript';
import { apiCall } from '../utility/api';
import { registerMenu } from '../utility/contextMenu';
import createActivator, { getActiveComponent } from '../utility/createActivator';
import createQuickExportMenu from '../utility/createQuickExportMenu';
import { exportQuickExportFile } from '../utility/exportFileTools';
import { getConnectionInfo } from '../utility/metadataLoaders';
import openNewTab from '../utility/openNewTab';
import ChangeSetGrider from './ChangeSetGrider';
import LoadingDataGridCore from './LoadingDataGridCore.svelte';
import { mongoFilterBehaviour, standardFilterBehaviours } from 'dbgate-tools';
import { openImportExportTab } from '../utility/importExportTools';
export let conid;
export let display;
export let database;
export let schemaName;
export let pureName;
export let config;
export let changeSetState;
export let dispatchChangeSet;
export let macroPreview;
export let macroValues;
export let setLoadedRows = null;
export let onPublishedCellsChanged;
// export let onChangeGrider = undefined;
let loadedRows = [];
let publishedCells = [];
export const activator = createActivator('CollectionDataGridCore', false);
// $: console.log('loadedRows BIND', loadedRows);
$: grider = new ChangeSetGrider(
loadedRows,
changeSetState,
dispatchChangeSet,
display,
macroPreview,
macroValues,
publishedCells
);
// $: console.log('GRIDER', grider);
// $: if (onChangeGrider) onChangeGrider(grider);
function getExportQuery() {
return display?.driver?.getCollectionExportQueryScript?.(
pureName,
buildConditionForGrid($$props),
buildSortForGrid($$props)
);
// return `db.collection('${pureName}')
// .find(${JSON.stringify(buildConditionForGrid($$props) || {})})
// .sort(${JSON.stringify(buildMongoSort($$props) || {})})`;
}
function getExportQueryJson() {
return display?.driver?.getCollectionExportQueryJson?.(
pureName,
buildConditionForGrid($$props),
buildSortForGrid($$props)
);
// return {
// collection: pureName,
// condition: buildConditionForGrid($$props) || {},
// sort: buildMongoSort($$props) || {},
// };
}
export async function exportGrid() {
const coninfo = await getConnectionInfo({ conid });
const initialValues: any = {};
initialValues.sourceStorageType = 'query';
initialValues.sourceConnectionId = conid;
initialValues.sourceDatabaseName = database;
initialValues.sourceQuery = coninfo.isReadOnly
? JSON.stringify(getExportQueryJson(), undefined, 2)
: getExportQuery();
initialValues.sourceQueryType = coninfo.isReadOnly ? 'json' : 'native';
initialValues.sourceList = [pureName];
initialValues[`columns_${pureName}`] = display.getExportColumnMap();
openImportExportTab(initialValues);
// showModal(ImportExportModal, { initialValues });
}
export function openQuery() {
openNewTab(
{
title: 'Query #',
icon: 'img sql-file',
tabComponent: 'QueryTab',
focused: true,
props: {
conid,
database,
},
},
{
editor: getExportQuery(),
}
);
}
const quickExportHandler = fmt => async () => {
const coninfo = await getConnectionInfo({ conid });
exportQuickExportFile(
pureName || 'Data',
{
functionName: 'queryReader',
props: {
...extractShellConnectionHostable(coninfo, database),
queryType: coninfo.isReadOnly ? 'json' : 'native',
query: coninfo.isReadOnly ? getExportQueryJson() : getExportQuery(),
},
hostConnection: extractShellHostConnection(coninfo, database),
},
fmt,
display.getExportColumnMap()
);
};
registerQuickExportHandler(quickExportHandler);
registerMenu({ command: 'collectionDataGrid.openQuery', tag: 'export' }, () =>
createQuickExportMenu(
quickExportHandler,
{
command: 'collectionDataGrid.export',
},
{ tag: 'export' }
)
);
function handleSetLoadedRows(rows) {
loadedRows = rows;
if (setLoadedRows) setLoadedRows(rows);
}
</script>
<LoadingDataGridCore
{...$$props}
loadDataPage={loadCollectionDataPage}
{dataPageAvailable}
{loadRowCount}
setLoadedRows={handleSetLoadedRows}
onPublishedCellsChanged={value => {
publishedCells = value;
if (onPublishedCellsChanged) {
onPublishedCellsChanged(value);
}
}}
frameSelection={!!macroPreview}
onOpenQuery={openQuery}
{grider}
const getCurrentEditor = () => getActiveComponent('CollectionDataGridCore');
registerCommand({
id: 'collectionDataGrid.openQuery',
category: __t('command.dataGrid', { defaultMessage: 'Data grid' }),
name: __t('command.dataGrid.openQuery', { defaultMessage: 'Open query' }),
testEnabled: () => getCurrentEditor() != null,
onClick: () => getCurrentEditor().openQuery(),
});
registerCommand({
id: 'collectionDataGrid.export',
category: __t('command.dataGrid', { defaultMessage: 'Data grid' }),
name: __t('command.dataGrid.export', { defaultMessage: 'Export' }),
keyText: 'CtrlOrCommand+E',
icon: 'icon export',
testEnabled: () => getCurrentEditor() != null,
onClick: () => getCurrentEditor().exportGrid(),
});
function buildConditionForGrid(props) {
const filters = props?.display?.config?.filters;
const filterBehaviour =
props?.display?.driver?.getFilterBehaviour(null, standardFilterBehaviours) ?? mongoFilterBehaviour;
// console.log('USED FILTER BEHAVIOUR', filterBehaviour);
const conditions = [];
for (const uniqueName in filters || {}) {
if (!filters[uniqueName]) continue;
try {
const ast = parseFilter(filters[uniqueName], filterBehaviour);
// console.log('AST', ast);
const cond = _.cloneDeepWith(ast, expr => {
if (expr.exprType == 'placeholder') {
return {
exprType: 'column',
columnName: uniqueName,
};
}
// if (expr.__placeholder__) {
// return {
// [uniqueName]: expr.__placeholder__,
// };
// }
});
conditions.push(cond);
} catch (err) {
// error in filter
}
}
return conditions.length > 0
? {
conditionType: 'and',
conditions,
}
: undefined;
}
function buildSortForGrid(props) {
const sort = props?.display?.config?.sort;
if (sort?.length > 0) {
return sort.map(col => ({
columnName: col.uniqueName,
direction: col.order,
}));
}
return null;
}
export async function loadCollectionDataPage(props, offset, limit) {
const { conid, database } = props;
const response = await apiCall('database-connections/collection-data', {
conid,
database,
options: {
pureName: props.pureName,
limit,
skip: offset,
condition: buildConditionForGrid(props),
sort: buildSortForGrid(props),
},
auditLogSessionGroup: 'data-grid',
});
if (response.errorMessage) return response;
return response.rows;
}
function dataPageAvailable(props) {
return true;
// const { display } = props;
// const sql = display.getPageQuery(0, 1);
// return !!sql;
}
async function loadRowCount(props) {
const { conid, database } = props;
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Row count query timed out')), 3000)
);
try {
const response = await Promise.race([
apiCall('database-connections/collection-data', {
conid,
database,
commandTimeout: 3000,
options: {
pureName: props.pureName,
countDocuments: true,
condition: buildConditionForGrid(props),
},
}),
timeoutPromise,
]);
if (response && typeof response === 'object' && (response as any).errorMessage) {
return { errorMessage: (response as any).errorMessage };
}
if (response && typeof response === 'object' && typeof (response as any).count === 'number') {
return (response as any).count;
}
return { errorMessage: 'Error loading row count' };
} catch (err) {
return { errorMessage: err.message || 'Error loading row count' };
}
}
</script>
<script lang="ts">
import { parseFilter } from 'dbgate-filterparser';
import _ from 'lodash';
import { registerQuickExportHandler } from '../buttons/ToolStripExportButton.svelte';
import registerCommand from '../commands/registerCommand';
import {
extractShellConnection,
extractShellConnectionHostable,
extractShellHostConnection,
} from '../impexp/createImpExpScript';
import { apiCall } from '../utility/api';
import { registerMenu } from '../utility/contextMenu';
import createActivator, { getActiveComponent } from '../utility/createActivator';
import createQuickExportMenu from '../utility/createQuickExportMenu';
import { exportQuickExportFile } from '../utility/exportFileTools';
import { getConnectionInfo } from '../utility/metadataLoaders';
import openNewTab from '../utility/openNewTab';
import ChangeSetGrider from './ChangeSetGrider';
import LoadingDataGridCore from './LoadingDataGridCore.svelte';
import { mongoFilterBehaviour, standardFilterBehaviours } from 'dbgate-tools';
import { openImportExportTab } from '../utility/importExportTools';
export let conid;
export let display;
export let database;
export let schemaName;
export let pureName;
export let config;
export let changeSetState;
export let dispatchChangeSet;
export let macroPreview;
export let macroValues;
export let setLoadedRows = null;
export let onPublishedCellsChanged;
// export let onChangeGrider = undefined;
let loadedRows = [];
let publishedCells = [];
export const activator = createActivator('CollectionDataGridCore', false);
// $: console.log('loadedRows BIND', loadedRows);
$: grider = new ChangeSetGrider(
loadedRows,
changeSetState,
dispatchChangeSet,
display,
macroPreview,
macroValues,
publishedCells
);
// $: console.log('GRIDER', grider);
// $: if (onChangeGrider) onChangeGrider(grider);
function getExportQuery() {
return display?.driver?.getCollectionExportQueryScript?.(
pureName,
buildConditionForGrid($$props),
buildSortForGrid($$props)
);
// return `db.collection('${pureName}')
// .find(${JSON.stringify(buildConditionForGrid($$props) || {})})
// .sort(${JSON.stringify(buildMongoSort($$props) || {})})`;
}
function getExportQueryJson() {
return display?.driver?.getCollectionExportQueryJson?.(
pureName,
buildConditionForGrid($$props),
buildSortForGrid($$props)
);
// return {
// collection: pureName,
// condition: buildConditionForGrid($$props) || {},
// sort: buildMongoSort($$props) || {},
// };
}
export async function exportGrid() {
const coninfo = await getConnectionInfo({ conid });
const initialValues: any = {};
initialValues.sourceStorageType = 'query';
initialValues.sourceConnectionId = conid;
initialValues.sourceDatabaseName = database;
initialValues.sourceQuery = coninfo.isReadOnly
? JSON.stringify(getExportQueryJson(), undefined, 2)
: getExportQuery();
initialValues.sourceQueryType = coninfo.isReadOnly ? 'json' : 'native';
initialValues.sourceList = [pureName];
initialValues[`columns_${pureName}`] = display.getExportColumnMap();
openImportExportTab(initialValues);
// showModal(ImportExportModal, { initialValues });
}
export function openQuery() {
openNewTab(
{
title: 'Query #',
icon: 'img sql-file',
tabComponent: 'QueryTab',
focused: true,
props: {
conid,
database,
},
},
{
editor: getExportQuery(),
}
);
}
const quickExportHandler = fmt => async () => {
const coninfo = await getConnectionInfo({ conid });
exportQuickExportFile(
pureName || 'Data',
{
functionName: 'queryReader',
props: {
...extractShellConnectionHostable(coninfo, database),
queryType: coninfo.isReadOnly ? 'json' : 'native',
query: coninfo.isReadOnly ? getExportQueryJson() : getExportQuery(),
},
hostConnection: extractShellHostConnection(coninfo, database),
},
fmt,
display.getExportColumnMap()
);
};
registerQuickExportHandler(quickExportHandler);
registerMenu({ command: 'collectionDataGrid.openQuery', tag: 'export' }, () =>
createQuickExportMenu(
quickExportHandler,
{
command: 'collectionDataGrid.export',
},
{ tag: 'export' }
)
);
function handleSetLoadedRows(rows) {
loadedRows = rows;
if (setLoadedRows) setLoadedRows(rows);
}
</script>
<LoadingDataGridCore
{...$$props}
loadDataPage={loadCollectionDataPage}
{dataPageAvailable}
{loadRowCount}
setLoadedRows={handleSetLoadedRows}
onPublishedCellsChanged={value => {
publishedCells = value;
if (onPublishedCellsChanged) {
onPublishedCellsChanged(value);
}
}}
frameSelection={!!macroPreview}
onOpenQuery={openQuery}
{grider}
/>
@@ -461,6 +461,8 @@
export let frameSelection = undefined;
export let isLoading = false;
export let allRowCount = undefined;
export let allRowCountError = undefined;
export let onReloadRowCount = undefined;
export let onReferenceSourceChanged = undefined;
export let onPublishedCellsChanged = undefined;
export let onReferenceClick = undefined;
@@ -2400,6 +2402,15 @@
<div class="row-count-label">
{_t('datagrid.rows', { defaultMessage: 'Rows' })}: {allRowCount.toLocaleString()}
</div>
{:else if allRowCountError && multipleGridsOnTab}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="row-count-label row-count-error"
title={allRowCountError}
on:click={onReloadRowCount}
>
{_t('datagrid.rows', { defaultMessage: 'Rows' })}: {_t('datagrid.rowCountMany', { defaultMessage: 'Many' })}
</div>
{/if}
{#if isLoading}
@@ -2408,6 +2419,13 @@
{#if !tabControlHiddenTab && !multipleGridsOnTab && allRowCount != null}
<StatusBarTabItem text={`${_t('datagrid.rows', { defaultMessage: 'Rows' })}: ${allRowCount.toLocaleString()}`} />
{:else if !tabControlHiddenTab && !multipleGridsOnTab && allRowCountError}
<StatusBarTabItem
text={`${_t('datagrid.rows', { defaultMessage: 'Rows' })}: ${_t('datagrid.rowCountMany', { defaultMessage: 'Many' })}`}
title={allRowCountError}
clickable
onClick={onReloadRowCount}
/>
{/if}
</div>
{/if}
@@ -2472,6 +2490,15 @@
opacity: 1;
}
.row-count-error {
cursor: pointer;
color: var(--theme-font-3);
}
.row-count-error:hover {
text-decoration: underline;
}
.selection-menu {
position: absolute;
background-color: var(--theme-datagrid-corner-label-background);
@@ -25,6 +25,7 @@
let isLoadedAll = false;
let loadedTime = new Date().getTime();
let allRowCount = null;
let allRowCountError = null;
let errorMessage = null;
let domGrid;
@@ -37,8 +38,14 @@
}
const handleLoadRowCount = async () => {
const rowCount = await loadRowCount($$props);
allRowCount = rowCount;
const result = await loadRowCount($$props);
if (result != null && typeof result === 'object' && result.errorMessage) {
allRowCount = null;
allRowCountError = result.errorMessage;
} else {
allRowCount = result;
allRowCountError = null;
}
};
async function loadNextData() {
@@ -103,6 +110,7 @@
function reload() {
allRowCount = null;
allRowCountError = null;
isLoading = false;
loadedRows = [];
isLoadedAll = false;
@@ -132,6 +140,8 @@
{errorMessage}
{isLoading}
allRowCount={rowCountLoaded || allRowCount}
allRowCountError={allRowCountError}
onReloadRowCount={handleLoadRowCount}
{isLoadedAll}
{loadedTime}
{grider}
+250 -235
View File
@@ -2,238 +2,253 @@
import { getActiveComponent } from '../utility/createActivator';
import registerCommand from '../commands/registerCommand';
import hasPermission from '../utility/hasPermission';
import { __t, _t } from '../translations'
const getCurrentEditor = () => getActiveComponent('SqlDataGridCore');
registerCommand({
id: 'sqlDataGrid.openQuery',
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
name: __t('command.openQuery', { defaultMessage : 'Open query' }),
testEnabled: () => getCurrentEditor() != null && hasPermission('dbops/query'),
onClick: () => getCurrentEditor().openQuery(),
});
registerCommand({
id: 'sqlDataGrid.export',
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
name: __t('common.export', { defaultMessage : 'Export' }),
icon: 'icon export',
keyText: 'CtrlOrCommand+E',
testEnabled: () => getCurrentEditor() != null && hasPermission('dbops/export'),
onClick: () => getCurrentEditor().exportGrid(),
});
</script>
<script lang="ts">
import _ from 'lodash';
import { registerQuickExportHandler } from '../buttons/ToolStripExportButton.svelte';
import {
extractShellConnection,
extractShellConnectionHostable,
extractShellHostConnection,
} from '../impexp/createImpExpScript';
import { apiCall } from '../utility/api';
import { registerMenu } from '../utility/contextMenu';
import createActivator from '../utility/createActivator';
import createQuickExportMenu from '../utility/createQuickExportMenu';
import { exportQuickExportFile } from '../utility/exportFileTools';
import { getConnectionInfo } from '../utility/metadataLoaders';
import openNewTab from '../utility/openNewTab';
import ChangeSetGrider from './ChangeSetGrider';
import LoadingDataGridCore from './LoadingDataGridCore.svelte';
import { openImportExportTab } from '../utility/importExportTools';
import { getIntSettingsValue } from '../settings/settingsTools';
import OverlayDiffGrider from './OverlayDiffGrider';
export let conid;
export let display;
export let database;
export let schemaName;
export let pureName;
export let config;
export let changeSetState;
export let dispatchChangeSet;
export let overlayDefinition = null;
export let macroPreview;
export let macroValues;
export let onPublishedCellsChanged;
let publishedCells = [];
// export let onChangeGrider = undefined;
export const activator = createActivator('SqlDataGridCore', false);
let loadedRows = [];
let grider;
// $: console.log('loadedRows BIND', loadedRows);
$: {
if (!overlayDefinition && macroPreview) {
grider = new ChangeSetGrider(
loadedRows,
changeSetState,
dispatchChangeSet,
display,
macroPreview,
macroValues,
publishedCells
);
}
}
// prevent recreate grider, if no macro is selected, so there is no need to selectedcells in macro
$: {
if (!overlayDefinition && !macroPreview) {
grider = new ChangeSetGrider(loadedRows, changeSetState, dispatchChangeSet, display);
}
}
// $: console.log('GRIDER', grider);
// $: if (onChangeGrider) onChangeGrider(grider);
$: {
if (overlayDefinition) {
grider = new OverlayDiffGrider(
loadedRows,
display,
overlayDefinition.matchColumns,
overlayDefinition.overlayData,
overlayDefinition.matchedDbKeys
);
}
}
export async function exportGrid() {
const coninfo = await getConnectionInfo({ conid });
const initialValues: any = {};
initialValues.sourceStorageType = 'query';
initialValues.sourceConnectionId = conid;
initialValues.sourceDatabaseName = database;
initialValues.sourceQuery = coninfo.isReadOnly
? JSON.stringify(display.getExportQueryJson(), undefined, 2)
: display.getExportQuery();
initialValues.sourceQueryType = coninfo.isReadOnly ? 'json' : 'native';
initialValues.sourceList = display.baseTableOrSimilar ? [display.baseTableOrSimilar.pureName] : [];
initialValues[`columns_${pureName}`] = display.getExportColumnMap();
openImportExportTab(initialValues);
// showModal(ImportExportModal, { initialValues });
}
export function openQuery(sql?) {
openNewTab(
{
title: _t('common.queryNumber', { defaultMessage: 'Query #' }),
icon: 'img sql-file',
tabComponent: 'QueryTab',
focused: true,
props: {
schemaName: display.baseTableOrSimilar?.schemaName,
pureName: display.baseTableOrSimilar?.pureName,
conid,
database,
},
},
{
editor: sql ?? display.getExportQuery(),
}
);
}
function openQueryOnError() {
openQuery(display.getPageQueryText(0, getIntSettingsValue('dataGrid.pageSize', 100, 5, 50000)));
}
const quickExportHandler = fmt => async () => {
const coninfo = await getConnectionInfo({ conid });
exportQuickExportFile(
pureName || 'Data',
{
functionName: 'queryReader',
props: {
...extractShellConnectionHostable(coninfo, database),
queryType: coninfo.isReadOnly ? 'json' : 'native',
query: coninfo.isReadOnly ? display.getExportQueryJson() : display.getExportQuery(),
},
hostConnection: extractShellHostConnection(coninfo, database),
},
fmt,
display.getExportColumnMap()
);
};
registerQuickExportHandler(quickExportHandler);
registerMenu(
{ command: 'sqlDataGrid.openActiveChart', tag: 'chart' },
{ command: 'sqlDataGrid.openQuery', tag: 'export' },
() =>
createQuickExportMenu(
quickExportHandler,
{
command: 'sqlDataGrid.export',
},
{ tag: 'export' }
)
);
function handleSetLoadedRows(rows) {
loadedRows = rows;
}
async function loadDataPage(props, offset, limit) {
const { display, conid, database } = props;
const select = display.getPageQuery(offset, limit);
const response = await apiCall('database-connections/sql-select', {
conid,
database,
select,
auditLogSessionGroup: 'data-grid',
});
if (response.errorMessage) return response;
return response.rows;
}
function dataPageAvailable(props) {
const { display } = props;
const select = display.getPageQuery(0, 1);
return !!select;
}
async function loadRowCount(props) {
const { display, conid, database } = props;
const select = display.getCountQuery();
const response = await apiCall('database-connections/sql-select', {
conid,
database,
select,
});
return parseInt(response.rows[0].count);
}
</script>
<LoadingDataGridCore
{...$$props}
{loadDataPage}
{dataPageAvailable}
{loadRowCount}
setLoadedRows={handleSetLoadedRows}
onPublishedCellsChanged={value => {
publishedCells = value;
if (onPublishedCellsChanged) {
onPublishedCellsChanged(value);
}
}}
frameSelection={!!macroPreview}
{grider}
{display}
onOpenQuery={openQuery}
import { __t, _t } from '../translations'
const getCurrentEditor = () => getActiveComponent('SqlDataGridCore');
registerCommand({
id: 'sqlDataGrid.openQuery',
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
name: __t('command.openQuery', { defaultMessage : 'Open query' }),
testEnabled: () => getCurrentEditor() != null && hasPermission('dbops/query'),
onClick: () => getCurrentEditor().openQuery(),
});
registerCommand({
id: 'sqlDataGrid.export',
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
name: __t('common.export', { defaultMessage : 'Export' }),
icon: 'icon export',
keyText: 'CtrlOrCommand+E',
testEnabled: () => getCurrentEditor() != null && hasPermission('dbops/export'),
onClick: () => getCurrentEditor().exportGrid(),
});
</script>
<script lang="ts">
import _ from 'lodash';
import { registerQuickExportHandler } from '../buttons/ToolStripExportButton.svelte';
import {
extractShellConnection,
extractShellConnectionHostable,
extractShellHostConnection,
} from '../impexp/createImpExpScript';
import { apiCall } from '../utility/api';
import { registerMenu } from '../utility/contextMenu';
import createActivator from '../utility/createActivator';
import createQuickExportMenu from '../utility/createQuickExportMenu';
import { exportQuickExportFile } from '../utility/exportFileTools';
import { getConnectionInfo } from '../utility/metadataLoaders';
import openNewTab from '../utility/openNewTab';
import ChangeSetGrider from './ChangeSetGrider';
import LoadingDataGridCore from './LoadingDataGridCore.svelte';
import { openImportExportTab } from '../utility/importExportTools';
import { getIntSettingsValue } from '../settings/settingsTools';
import OverlayDiffGrider from './OverlayDiffGrider';
export let conid;
export let display;
export let database;
export let schemaName;
export let pureName;
export let config;
export let changeSetState;
export let dispatchChangeSet;
export let overlayDefinition = null;
export let macroPreview;
export let macroValues;
export let onPublishedCellsChanged;
let publishedCells = [];
// export let onChangeGrider = undefined;
export const activator = createActivator('SqlDataGridCore', false);
let loadedRows = [];
let grider;
// $: console.log('loadedRows BIND', loadedRows);
$: {
if (!overlayDefinition && macroPreview) {
grider = new ChangeSetGrider(
loadedRows,
changeSetState,
dispatchChangeSet,
display,
macroPreview,
macroValues,
publishedCells
);
}
}
// prevent recreate grider, if no macro is selected, so there is no need to selectedcells in macro
$: {
if (!overlayDefinition && !macroPreview) {
grider = new ChangeSetGrider(loadedRows, changeSetState, dispatchChangeSet, display);
}
}
// $: console.log('GRIDER', grider);
// $: if (onChangeGrider) onChangeGrider(grider);
$: {
if (overlayDefinition) {
grider = new OverlayDiffGrider(
loadedRows,
display,
overlayDefinition.matchColumns,
overlayDefinition.overlayData,
overlayDefinition.matchedDbKeys
);
}
}
export async function exportGrid() {
const coninfo = await getConnectionInfo({ conid });
const initialValues: any = {};
initialValues.sourceStorageType = 'query';
initialValues.sourceConnectionId = conid;
initialValues.sourceDatabaseName = database;
initialValues.sourceQuery = coninfo.isReadOnly
? JSON.stringify(display.getExportQueryJson(), undefined, 2)
: display.getExportQuery();
initialValues.sourceQueryType = coninfo.isReadOnly ? 'json' : 'native';
initialValues.sourceList = display.baseTableOrSimilar ? [display.baseTableOrSimilar.pureName] : [];
initialValues[`columns_${pureName}`] = display.getExportColumnMap();
openImportExportTab(initialValues);
// showModal(ImportExportModal, { initialValues });
}
export function openQuery(sql?) {
openNewTab(
{
title: _t('common.queryNumber', { defaultMessage: 'Query #' }),
icon: 'img sql-file',
tabComponent: 'QueryTab',
focused: true,
props: {
schemaName: display.baseTableOrSimilar?.schemaName,
pureName: display.baseTableOrSimilar?.pureName,
conid,
database,
},
},
{
editor: sql ?? display.getExportQuery(),
}
);
}
function openQueryOnError() {
openQuery(display.getPageQueryText(0, getIntSettingsValue('dataGrid.pageSize', 100, 5, 50000)));
}
const quickExportHandler = fmt => async () => {
const coninfo = await getConnectionInfo({ conid });
exportQuickExportFile(
pureName || 'Data',
{
functionName: 'queryReader',
props: {
...extractShellConnectionHostable(coninfo, database),
queryType: coninfo.isReadOnly ? 'json' : 'native',
query: coninfo.isReadOnly ? display.getExportQueryJson() : display.getExportQuery(),
},
hostConnection: extractShellHostConnection(coninfo, database),
},
fmt,
display.getExportColumnMap()
);
};
registerQuickExportHandler(quickExportHandler);
registerMenu(
{ command: 'sqlDataGrid.openActiveChart', tag: 'chart' },
{ command: 'sqlDataGrid.openQuery', tag: 'export' },
() =>
createQuickExportMenu(
quickExportHandler,
{
command: 'sqlDataGrid.export',
},
{ tag: 'export' }
)
);
function handleSetLoadedRows(rows) {
loadedRows = rows;
}
async function loadDataPage(props, offset, limit) {
const { display, conid, database } = props;
const select = display.getPageQuery(offset, limit);
const response = await apiCall('database-connections/sql-select', {
conid,
database,
select,
auditLogSessionGroup: 'data-grid',
});
if (response.errorMessage) return response;
return response.rows;
}
function dataPageAvailable(props) {
const { display } = props;
const select = display.getPageQuery(0, 1);
return !!select;
}
async function loadRowCount(props) {
const { display, conid, database } = props;
const select = display.getCountQuery();
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Row count query timed out')), 3000)
);
try {
const response = await Promise.race([
apiCall('database-connections/sql-select', {
conid,
database,
select,
commandTimeout: 3000,
}),
timeoutPromise,
]);
if (response.errorMessage) return { errorMessage: response.errorMessage };
return parseInt(response.rows[0].count);
} catch (err) {
return { errorMessage: err.message || 'Error loading row count' };
}
}
</script>
<LoadingDataGridCore
{...$$props}
{loadDataPage}
{dataPageAvailable}
{loadRowCount}
setLoadedRows={handleSetLoadedRows}
onPublishedCellsChanged={value => {
publishedCells = value;
if (onPublishedCellsChanged) {
onPublishedCellsChanged(value);
}
}}
frameSelection={!!macroPreview}
{grider}
{display}
onOpenQuery={openQuery}
onOpenQueryOnError={openQueryOnError}
/>
File diff suppressed because it is too large Load Diff
@@ -22,6 +22,7 @@
let isLoadedCount = false;
let loadedTime = new Date().getTime();
let allRowCount = null;
let allRowCountError = null;
let errorMessage = null;
const handleLoadCurrentRow = async () => {
@@ -38,7 +39,14 @@
const handleLoadRowCount = async () => {
isLoadingCount = true;
allRowCount = await loadRowCountFunc();
const result = await loadRowCountFunc();
if (result != null && typeof result === 'object' && result.errorMessage) {
allRowCount = null;
allRowCountError = result.errorMessage;
} else {
allRowCount = result;
allRowCountError = null;
}
isLoadedCount = true;
isLoadingCount = false;
};
@@ -55,6 +63,7 @@
rowData = null;
loadedTime = new Date().getTime();
allRowCount = null;
allRowCountError = null;
errorMessage = null;
}
@@ -82,4 +91,4 @@
$: if (onReferenceSourceChanged && rowData) onReferenceSourceChanged([rowData], loadedTime);
</script>
<FormView {...$$props} {grider} isLoading={isLoadingData} {allRowCount} onNavigate={handleNavigate} />
<FormView {...$$props} {grider} isLoading={isLoadingData} {allRowCount} {allRowCountError} onReloadRowCount={handleLoadRowCount} onNavigate={handleNavigate} />
+45 -33
View File
@@ -1,35 +1,47 @@
<script lang="ts" context="module">
import { apiCall } from '../utility/api';
async function loadRow(props, select) {
const { conid, database } = props;
if (!select) return null;
const response = await apiCall('database-connections/sql-select', {
conid,
database,
select,
auditLogSessionGroup: 'data-form',
});
if (response.errorMessage) return response;
return response.rows[0];
}
</script>
<script lang="ts"> import _ from 'lodash';
import LoadingFormView from './LoadingFormView.svelte';
export let display;
async function handleLoadRow() {
return await loadRow($$props, display.getPageQuery(display.config.formViewRecordNumber || 0, 1));
}
async function handleLoadRowCount() {
const countRow = await loadRow($$props, display.getCountQuery());
return countRow ? parseInt(countRow.count) : null;
}
</script>
<LoadingFormView {...$$props} loadRowFunc={handleLoadRow} loadRowCountFunc={handleLoadRowCount} />
async function loadRow(props, select, options = {}) {
const { conid, database } = props;
if (!select) return null;
const response = await apiCall('database-connections/sql-select', {
conid,
database,
select,
auditLogSessionGroup: 'data-form',
...options,
});
if (response.errorMessage) return response;
return response.rows[0];
}
</script>
<script lang="ts">
import _ from 'lodash';
import LoadingFormView from './LoadingFormView.svelte';
export let display;
async function handleLoadRow() {
return await loadRow($$props, display.getPageQuery(display.config.formViewRecordNumber || 0, 1));
}
async function handleLoadRowCount() {
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Row count query timed out')), 3000)
);
try {
const countRow = await Promise.race([
loadRow($$props, display.getCountQuery(), { commandTimeout: 3000 }),
timeoutPromise,
]);
return countRow ? parseInt(countRow.count) : null;
} catch (err) {
return { errorMessage: err.message || 'Error loading row count' };
}
}
</script>
<LoadingFormView {...$$props} loadRowFunc={handleLoadRow} loadRowCountFunc={handleLoadRowCount} />
@@ -15,6 +15,7 @@
import LoadingInfo from '../elements/LoadingInfo.svelte';
import { getFormContext } from '../forms/FormProviderCore.svelte';
import { addFilesToSourceList } from './ImportExportConfigurator.svelte';
import getElectron from '../utility/getElectron';
let isLoading = false;
+32 -1
View File
@@ -1,5 +1,11 @@
import _ from 'lodash';
import { currentDatabase, openedConnectionsWithTemporary, getCurrentConfig, getOpenedConnections } from '../stores';
import {
currentDatabase,
openedConnectionsWithTemporary,
getCurrentConfig,
getOpenedConnections,
getOpenedTabs,
} from '../stores';
import { apiCall, getVolatileConnections, strmid } from './api';
import hasPermission from '../utility/hasPermission';
import { getConfig } from './metadataLoaders';
@@ -37,9 +43,29 @@ const doDatabasePing = value => {
}
};
function pingAllOpenedDatabases() {
const tabs = getOpenedTabs() || [];
const allDbs = tabs
.filter(tab => !tab.closedTime && tab.props?.conid && tab.props?.database)
.map(tab => ({ conid: tab.props.conid as string, database: tab.props.database as string }));
const seen = new Set<string>();
const databases: { conid: string; database: string }[] = [];
for (const db of allDbs) {
const key = `${db.conid}/${db.database}`;
if (!seen.has(key)) {
seen.add(key);
databases.push(db);
}
}
if (databases.length > 0) {
apiCall('database-connections/ping-databases', { databases });
}
}
let openedConnectionsHandle = null;
let currentDatabaseHandle = null;
let allDatabasesHandle = null;
export function subscribeConnectionPingers() {
openedConnectionsWithTemporary.subscribe(value => {
@@ -53,6 +79,11 @@ export function subscribeConnectionPingers() {
if (currentDatabaseHandle) window.clearInterval(currentDatabaseHandle);
currentDatabaseHandle = window.setInterval(() => doDatabasePing(value), 20 * 1000);
});
// Ping all databases that have open (non-closed) tabs, not just the current one
pingAllOpenedDatabases();
if (allDatabasesHandle) window.clearInterval(allDatabasesHandle);
allDatabasesHandle = window.setInterval(() => pingAllOpenedDatabases(), 20 * 1000);
}
export function callServerPing() {
+17 -2
View File
@@ -2,6 +2,7 @@ import _ from 'lodash';
import { isFileDragActive } from '../stores';
import { fromEvent } from 'file-selector';
import uploadFiles from './uploadFiles';
import getElectron from './getElectron';
function isEvtWithFiles(event) {
if (!event.dataTransfer) {
@@ -65,8 +66,22 @@ export default function dragDropFileTarget(node, items) {
isFileDragActive.set(false);
if (isEvtWithFiles(event)) {
const files = await fromEvent(event);
uploadFiles(files);
const electron = getElectron();
if (electron && event.dataTransfer?.files?.length) {
// Electron 37+ removed File.path in favour of webUtils.getPathForFile().
// Older Electron sets file.path automatically; newer requires webUtils.
const electronModule = window['require']('electron');
const files = Array.from(event.dataTransfer.files).map((file: any) => {
if (!file.path && electronModule?.webUtils) {
file.path = electronModule.webUtils.getPathForFile(file);
}
return file;
});
uploadFiles(files);
} else {
const files = await fromEvent(event);
uploadFiles(files);
}
}
}
+2 -2
View File
@@ -171,9 +171,9 @@ const installedPluginsLoader = () => ({
reloadTrigger: { key: `installed-plugins-changed` },
});
const filesLoader = ({ folder }) => ({
const filesLoader = ({ folder, parseFrontMatter = false }) => ({
url: 'files/list',
params: { folder },
params: parseFrontMatter ? { folder, parseFrontMatter: true } : { folder },
reloadTrigger: { key: `files-changed`, folder },
});
const allFilesLoader = () => ({
+166 -18
View File
@@ -8,16 +8,20 @@
import SearchInput from '../elements/SearchInput.svelte';
import FontIcon from '../icons/FontIcon.svelte';
import { apiCall } from '../utility/api';
import { useFiles, useTeamFiles } from '../utility/metadataLoaders';
import { useFiles, useTeamFiles, useConnectionList, useCloudContentList } from '../utility/metadataLoaders';
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
import { isProApp } from '../utility/proTools';
import InlineUploadButton from '../buttons/InlineUploadButton.svelte';
import { DATA_FOLDER_NAMES } from 'dbgate-tools';
import { DATA_FOLDER_NAMES, getConnectionLabel } from 'dbgate-tools';
import { _t } from '../translations';
import { currentDatabase } from '../stores';
let filter = '';
let selectedConnectionId = '';
const sqlFiles = useFiles({ folder: 'sql' });
const connectionList = useConnectionList();
const cloudContentList = useCloudContentList();
const sqlFiles = useFiles({ folder: 'sql', parseFrontMatter: true });
const shellFiles = useFiles({ folder: 'shell' });
const markdownFiles = useFiles({ folder: 'markdown' });
const chartFiles = useFiles({ folder: 'charts' });
@@ -33,22 +37,105 @@
const appFiles = useFiles({ folder: 'apps' });
const teamFiles = useTeamFiles({});
function makeConnectionKey(connectionId: string, databaseName: string | undefined) {
return databaseName ? `${connectionId}::${databaseName}` : connectionId;
}
$: cloudIdToLabel = _.fromPairs(
(($cloudContentList || []) as any[])
.flatMap(fld => fld.items ?? [])
.filter(item => item.type === 'connection' && item.folid && item.cntid)
.map(item => [`cloud://${item.folid}/${item.cntid}`, item.name as string])
);
$: connectionDbOptions = _.uniqBy(
(($sqlFiles || []) as any[])
.filter(f => f.connectionId)
.map(f => {
const conn = (($connectionList || []) as any[]).find(c => c._id === f.connectionId);
const connLabel = (conn && getConnectionLabel(conn)) || cloudIdToLabel[f.connectionId] || f.connectionId;
const label = f.databaseName ? `${connLabel} - ${f.databaseName}` : connLabel;
return {
value: makeConnectionKey(f.connectionId, f.databaseName),
label: label as string,
connectionId: f.connectionId,
databaseName: f.databaseName,
};
}),
x => x.value
);
$: connectionOptions = (() => {
const grouped = _.groupBy(connectionDbOptions, o => o.connectionId);
const connectionGroups = Object.entries(grouped)
.map(([connId, items]) => {
const conn = (($connectionList || []) as any[]).find(c => c._id === connId);
const connLabel = (conn && getConnectionLabel(conn)) || cloudIdToLabel[connId] || connId;
const hasMultipleDbs = items.length > 1 || items.some(i => i.databaseName);
const dbItems = [...items].sort((a, b) => (a.databaseName || '').localeCompare(b.databaseName || ''));
return {
sortLabel: connLabel || connId,
options: [
...(hasMultipleDbs
? [
{
value: `conn-all::${connId}`,
label: `${connLabel} - ${_t('files.allDatabases', { defaultMessage: 'all databases' })}`,
},
]
: []),
...dbItems,
],
};
})
.sort((a, b) => a.sortLabel.localeCompare(b.sortLabel));
return connectionGroups.flatMap(group => group.options);
})();
$: currentDbFilterOption = (() => {
if (!$currentDatabase?.connection?._id) return null;
const connId = $currentDatabase.connection._id;
const dbName = $currentDatabase.name;
const hasFiles = (($sqlFiles || []) as any[]).some(f => f.connectionId === connId && f.databaseName === dbName);
if (!hasFiles) return null;
const conn = (($connectionList || []) as any[]).find(c => c._id === connId);
const connLabel = (conn && getConnectionLabel(conn)) || cloudIdToLabel[connId] || connId;
const label = dbName ? `${connLabel} - ${dbName}` : connLabel;
return { value: `current-db`, label };
})();
$: filteredSqlFiles = (() => {
if (!selectedConnectionId) return $sqlFiles || [];
if (selectedConnectionId === 'current-db') {
const connId = $currentDatabase?.connection?._id;
const dbName = $currentDatabase?.name;
return ($sqlFiles || []).filter(f => f.connectionId === connId && f.databaseName === dbName);
}
if (selectedConnectionId.startsWith('conn-all::')) {
const connId = selectedConnectionId.slice('conn-all::'.length);
return ($sqlFiles || []).filter(f => f.connectionId === connId);
}
return ($sqlFiles || []).filter(f => makeConnectionKey(f.connectionId, f.databaseName) === selectedConnectionId);
})();
$: files = [
...($sqlFiles || []),
...($shellFiles || []),
...($markdownFiles || []),
...($chartFiles || []),
...($queryFiles || []),
...($sqliteFiles || []),
...($diagramFiles || []),
...($perspectiveFiles || []),
...($importExportJobFiles || []),
...($modelTransformFiles || []),
...($themeFiles || []),
...((isProApp() && $dataDeployJobFiles) || []),
...((isProApp() && $dbCompareJobFiles) || []),
...((isProApp() && $appFiles) || []),
...($teamFiles || []),
...filteredSqlFiles,
...(selectedConnectionId ? [] : $shellFiles || []),
...(selectedConnectionId ? [] : $markdownFiles || []),
...(selectedConnectionId ? [] : $chartFiles || []),
...(selectedConnectionId ? [] : $queryFiles || []),
...(selectedConnectionId ? [] : $sqliteFiles || []),
...(selectedConnectionId ? [] : $diagramFiles || []),
...(selectedConnectionId ? [] : $perspectiveFiles || []),
...(selectedConnectionId ? [] : $importExportJobFiles || []),
...(selectedConnectionId ? [] : $modelTransformFiles || []),
...(selectedConnectionId ? [] : $themeFiles || []),
...((isProApp() && !selectedConnectionId && $dataDeployJobFiles) || []),
...((isProApp() && !selectedConnectionId && $dbCompareJobFiles) || []),
...((isProApp() && !selectedConnectionId && $appFiles) || []),
...(selectedConnectionId ? [] : $teamFiles || []),
];
function handleRefreshFiles() {
@@ -92,6 +179,41 @@
</InlineButton>
</SearchBoxWrapper>
{#if connectionOptions.length > 0}
<div class="connection-filter">
<div class="mr-1">{_t('files.connection', { defaultMessage: 'Connection' })}:</div>
<select
class="connection-select"
value={selectedConnectionId}
on:change={e => {
selectedConnectionId = e.currentTarget.value;
}}
data-testid="SavedFilesList_connectionFilter"
>
<option value="">{_t('files.allConnections', { defaultMessage: 'All connections' })}</option>
{#if currentDbFilterOption}
<option value="current-db"
>{_t('files.currentDatabase', { defaultMessage: 'Current database' })}: {currentDbFilterOption.label}</option
>
{/if}
{#each connectionOptions as opt}
<option value={opt.value}>{opt.label}</option>
{/each}
</select>
{#if selectedConnectionId}
<InlineButton
on:click={() => {
selectedConnectionId = '';
}}
title={_t('files.clearConnectionFilter', { defaultMessage: 'Clear connection filter' })}
data-testid="SavedFilesList_clearConnectionFilter"
>
<FontIcon icon="icon close" />
</InlineButton>
{/if}
</div>
{/if}
<WidgetsInnerContainer>
<AppObjectList
list={files}
@@ -103,3 +225,29 @@
{filter}
/>
</WidgetsInnerContainer>
<style>
.connection-filter {
display: flex;
border-bottom: var(--theme-card-border);
margin-bottom: 5px;
align-items: center;
padding-left: 5px;
gap: 2px;
}
.connection-select {
flex: 1 1 0%;
min-width: 0;
padding: 0;
height: 24px;
background-color: var(--theme-searchbox-background);
border: var(--theme-searchbox-border);
box-shadow: none;
cursor: pointer;
color: var(--theme-font-1);
font-size: inherit;
font-family: inherit;
margin-right: 4px;
}
</style>
@@ -310,7 +310,7 @@
data-testid="SqlObjectList_searchMenuDropDown"
/>
{#if !filter}
<DropDownButton icon="icon plus-thick" menu={createAddMenu} />
<DropDownButton icon="icon plus-thick" menu={createAddMenu} data-testid="SqlObjectList_addButton" />
{/if}
<DropDownButton
menu={createRefreshDatabaseMenu}
+1 -1
View File
@@ -181,7 +181,7 @@
</div>
<div class="container">
{#each contextItems || [] as item}
<div class="item" class:clickable={item.clickable} on:click={item.onClick}>
<div class="item" class:clickable={item.clickable} on:click={item.onClick} title={item.title || null}>
{#if item.icon}
<FontIcon icon={item.icon} padRight />
{/if}
@@ -8,14 +8,15 @@
export let clickable = false;
export let icon = null;
export let onClick = null;
export let title = null;
const key = uuidv1();
const tabid = getContext('tabid');
onMount(() => {
updateStatuBarInfoItem(tabid, key, { text, icon, clickable, onClick });
updateStatuBarInfoItem(tabid, key, { text, icon, clickable, onClick, title });
});
onDestroy(() => updateStatuBarInfoItem(tabid, key, null));
$: updateStatuBarInfoItem(tabid, key, { text, icon, clickable, onClick });
$: updateStatuBarInfoItem(tabid, key, { text, icon, clickable, onClick, title });
</script>
@@ -71,14 +71,19 @@ const driver = {
// called for retrieve data (eg. browse in data grid) and for update database
async query(dbhan, query, options) {
const offset = options?.range?.offset;
const commandTimeout = options?.commandTimeout;
const executeOptions = {};
if (commandTimeout) {
executeOptions.readTimeout = parseInt(commandTimeout);
}
if (options?.discardResult) {
await dbhan.client.execute(query);
await dbhan.client.execute(query, [], executeOptions);
return {
rows: [],
columns: [],
};
}
const result = await dbhan.client.execute(query);
const result = await dbhan.client.execute(query, [], executeOptions);
if (!result.rows?.[0]) {
return {
rows: [],
@@ -25,6 +25,7 @@ const driver = {
},
// called for retrieve data (eg. browse in data grid) and for update database
async query(dbhan, query, options) {
const commandTimeout = options?.commandTimeout;
if (options?.discardResult) {
await dbhan.client.command({
query,
@@ -34,10 +35,14 @@ const driver = {
columns: [],
};
} else {
const resultSet = await dbhan.client.query({
const queryOptions = {
query,
format: 'JSONCompactEachRowWithNamesAndTypes',
});
};
if (commandTimeout) {
queryOptions.settings = { max_execution_time: Math.ceil(parseInt(commandTimeout) / 1000) };
}
const resultSet = await dbhan.client.query(queryOptions);
const dataSet = await resultSet.json();
if (!dataSet?.[0]) {
@@ -487,7 +487,11 @@ const drivers = driverBases.map((driverBase) => ({
const collection = dbhan.getDatabase().collection(options.pureName);
if (options.countDocuments) {
const count = await collection.countDocuments(deserializeMongoData(mongoCondition) || {});
const countOptions = {};
if (options.commandTimeout) {
countOptions.maxTimeMS = parseInt(options.commandTimeout);
}
const count = await collection.countDocuments(deserializeMongoData(mongoCondition) || {}, countOptions);
return { count };
} else if (options.aggregate) {
let cursor = await collection.aggregate(deserializeMongoData(convertToMongoAggregate(options.aggregate)));
@@ -119,7 +119,7 @@ async function tediousQueryCore(dbhan, sql, options) {
columns: [],
});
}
const { addDriverNativeColumn, discardResult } = options || {};
const { addDriverNativeColumn, discardResult, commandTimeout } = options || {};
return new Promise((resolve, reject) => {
const result = {
rows: [],
@@ -129,6 +129,9 @@ async function tediousQueryCore(dbhan, sql, options) {
if (err) reject(err);
else resolve(result);
});
if (commandTimeout) {
request.setTimeout(parseInt(commandTimeout));
}
request.on('columnMetadata', function (columns) {
result.columns = extractTediousColumns(columns, addDriverNativeColumn);
});
@@ -105,9 +105,18 @@ const drivers = driverBases.map(driverBase => ({
};
}
const commandTimeout = options?.commandTimeout;
const queryOptions = {};
if (commandTimeout) {
queryOptions.timeout = parseInt(commandTimeout);
}
return new Promise((resolve, reject) => {
dbhan.client.query(sql, function (error, results, fields) {
if (error) reject(error);
dbhan.client.query({ sql, ...queryOptions }, function (error, results, fields) {
if (error) {
reject(error);
return;
}
const columns = extractColumns(fields);
resolve({ rows: results && columns && results.map && results.map(row => modifyRow(zipDataRow(row, columns), columns)), columns });
});
@@ -107,7 +107,7 @@ const driver = {
async close(dbhan) {
return dbhan.client.close();
},
async query(dbhan, sql) {
async query(dbhan, sql, options) {
if (sql == null || sql.trim() == '') {
return {
rows: [],
@@ -120,7 +120,21 @@ const driver = {
sql = mtrim[1];
}
const res = await dbhan.client.execute(sql);
const commandTimeout = options?.commandTimeout;
let previousCallTimeout;
if (commandTimeout) {
previousCallTimeout = dbhan.client.callTimeout;
dbhan.client.callTimeout = parseInt(commandTimeout);
}
let res;
try {
res = await dbhan.client.execute(sql);
} finally {
if (commandTimeout) {
dbhan.client.callTimeout = previousCallTimeout || 0;
}
}
try {
const columns = extractOracleColumns(res.metaData);
return { rows: (res.rows || []).map(row => modifyRow(zipDataRow(row, columns), columns)), columns };
@@ -178,14 +178,25 @@ const drivers = driverBases.map(driverBase => ({
async close(dbhan) {
return dbhan.client.end();
},
async query(dbhan, sql) {
async query(dbhan, sql, options) {
if (sql == null) {
return {
rows: [],
columns: [],
};
}
const res = await dbhan.client.query({ text: sql, rowMode: 'array' });
const commandTimeout = options?.commandTimeout;
if (commandTimeout) {
await dbhan.client.query({ text: `SET statement_timeout = ${parseInt(commandTimeout)}` });
}
let res;
try {
res = await dbhan.client.query({ text: sql, rowMode: 'array' });
} finally {
if (commandTimeout) {
await dbhan.client.query({ text: 'SET statement_timeout = 0' }).catch(() => {});
}
}
const columns = extractPostgresColumns(res, dbhan);
const transormableTypeNames = Object.values(dbhan.typeIdToName ?? {});
+8 -3
View File
@@ -101,12 +101,12 @@ jobs:
--health-retries 5
env:
POSTGRES_PASSWORD: Pwd2020Db
ports:
ports:
- 16000:5432
mysql-cypress:
image: mysql:8.0.18
ports:
ports:
- 16004:3306
env:
MYSQL_ROOT_PASSWORD: Pwd2020Db
@@ -118,7 +118,7 @@ jobs:
mysql-ssh-keyfile:
image: ghcr.io/dbgate/mysql-ssh-keyfile:latest
ports:
ports:
- '16008:22'
dex:
@@ -139,6 +139,11 @@ jobs:
ports:
- 16011:6379
dynamodb:
image: amazon/dynamodb-local
ports:
- 16015:8000
mssql:
image: mcr.microsoft.com/mssql/server
ports:
+1 -1
View File
@@ -7,7 +7,7 @@ checkout-and-merge-pro:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: c33e71a6ddc30d8ce59cf0351e04e08f6be272a3
ref: 0bc104411450ea2849bcf4d6ef3dfc4b48ed7821
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro
+5
View File
@@ -7247,6 +7247,11 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
dom-to-image@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/dom-to-image/-/dom-to-image-2.6.0.tgz#8a503608088c87b1c22f9034ae032e1898955867"
integrity sha512-Dt0QdaHmLpjURjU7Tnu3AgYSF2LuOmksSGsUcE6ItvJoCWTBEmiMXcqBdNSAm9+QbbwD7JMoVsuuKX6ZVQv1qA==
dotenv@^16.0.0:
version "16.4.5"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"