Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2824681bff | |||
| 073a3e3946 | |||
| f99994085a | |||
| a548b0d543 | |||
| de94f15383 | |||
| 7045d986ef | |||
| de7ae9cf09 | |||
| ab3d6888dc | |||
| 98a70891f3 | |||
| 52e7326a2c | |||
| bfd2e3b07a | |||
| 799f5e30d3 | |||
| d3e544c3c0 | |||
| 866fd55834 | |||
| 74ce1fba32 | |||
| a11b93b4cc | |||
| 066f2baa03 | |||
| e02396280f | |||
| a654c80746 | |||
| 3b50f4bd7c | |||
| cc1f77f5bc | |||
| 381fce4a82 | |||
| bc3be97cee | |||
| 1c389208a7 | |||
| e1ead2519a |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -4,5 +4,6 @@ module.exports = {
|
||||
mssql: true,
|
||||
oracle: true,
|
||||
sqlite: true,
|
||||
mongo: true
|
||||
mongo: true,
|
||||
dynamo: true,
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
Vendored
+7
-1
@@ -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
|
||||
|
||||
Vendored
+8
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
Vendored
+1
@@ -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}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = () => ({
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 ?? {});
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user