Merge branch 'master' into develop

This commit is contained in:
Jan Prochazka
2022-07-18 22:46:31 +02:00
21 changed files with 395 additions and 135 deletions

View File

@@ -8,6 +8,23 @@ Builds:
- linux - application for linux - linux - application for linux
- win - application for Windows - win - application for Windows
### 5.0.8
- ADDED: SQL Server - support using domain logins under Linux and Mac #305
- ADDED: Permissions for connections #318
- ADDED: Ability to change editor front #308
- ADDED: Custom expression in query designer #306
- ADDED: OR conditions in query designer #321
- ADDED: Ability to configure settings view environment variables #304
### 5.0.7
- FIXED: Fixed some problems with SSH tunnel (upgraded SSH client) #315
- FIXED: Fixed MognoDB executing find query #312
- ADDED: Interval filters for date/time columns #311
- ADDED: Ability to clone rows #309
- ADDED: connecting option Trust server certificate for SQL Server #305
- ADDED: Autorefresh, reload table every x second #303
- FIXED(app): Changing editor theme and font size in Editor Themes #300
### 5.0.6 ### 5.0.6
- ADDED: Search in columns - ADDED: Search in columns
- CHANGED: Upgraded mongodb driver - CHANGED: Upgraded mongodb driver

View File

@@ -1,6 +1,6 @@
{ {
"private": true, "private": true,
"version": "5.0.7-beta.4", "version": "5.0.8",
"name": "dbgate-all", "name": "dbgate-all",
"workspaces": [ "workspaces": [
"packages/*", "packages/*",

View File

@@ -48,4 +48,15 @@ PASSWORD_relational=relational
ENGINE_relational=mariadb@dbgate-plugin-mysql ENGINE_relational=mariadb@dbgate-plugin-mysql
READONLY_relational=1 READONLY_relational=1
# SETTINGS_dataGrid.showHintColumns=1
# docker run -p 3000:3000 -e CONNECTIONS=mongo -e URL_mongo=mongodb://localhost:27017 -e ENGINE_mongo=mongo@dbgate-plugin-mongo -e LABEL_mongo=mongo dbgate/dbgate:beta # docker run -p 3000:3000 -e CONNECTIONS=mongo -e URL_mongo=mongodb://localhost:27017 -e ENGINE_mongo=mongo@dbgate-plugin-mongo -e LABEL_mongo=mongo dbgate/dbgate:beta
# LOGINS=x,y
# LOGIN_PASSWORD_x=x
# LOGIN_PASSWORD_y=LOGIN_PASSWORD_y
# LOGIN_PERMISSIONS_x=~*
# LOGIN_PERMISSIONS_y=~*
# PERMISSIONS=~*,connections/relational
# PERMISSIONS=~*

View File

@@ -29,7 +29,7 @@ module.exports = {
async get(_params, req) { async get(_params, req) {
const logins = getLogins(); const logins = getLogins();
const login = logins ? logins.find(x => x.login == (req.auth && req.auth.user)) : null; const login = logins ? logins.find(x => x.login == (req.auth && req.auth.user)) : null;
const permissions = login ? login.permissions : null; const permissions = login ? login.permissions : process.env.PERMISSIONS;
return { return {
runAsPortal: !!connections.portalConnections, runAsPortal: !!connections.portalConnections,
@@ -73,6 +73,14 @@ module.exports = {
// res['app.useNativeMenu'] = os.platform() == 'darwin' ? true : false; // res['app.useNativeMenu'] = os.platform() == 'darwin' ? true : false;
res['app.useNativeMenu'] = false; res['app.useNativeMenu'] = false;
} }
for (const envVar in process.env) {
if (envVar.startsWith('SETTINGS_')) {
const key = envVar.substring('SETTINGS_'.length);
if (!res[key]) {
res[key] = process.env[envVar];
}
}
}
return res; return res;
}, },

View File

@@ -13,6 +13,7 @@ const JsonLinesDatabase = require('../utility/JsonLinesDatabase');
const processArgs = require('../utility/processArgs'); const processArgs = require('../utility/processArgs');
const { safeJsonParse } = require('dbgate-tools'); const { safeJsonParse } = require('dbgate-tools');
const platformInfo = require('../utility/platformInfo'); const platformInfo = require('../utility/platformInfo');
const { connectionHasPermission, testConnectionPermission } = require('../utility/hasPermission');
function getNamedArgs() { function getNamedArgs() {
const res = {}; const res = {};
@@ -165,12 +166,12 @@ module.exports = {
}, },
list_meta: true, list_meta: true,
async list() { async list(_params, req) {
if (portalConnections) { if (portalConnections) {
if (platformInfo.allowShellConnection) return portalConnections; if (platformInfo.allowShellConnection) return portalConnections;
return portalConnections.map(maskConnection); return portalConnections.map(maskConnection).filter(x => connectionHasPermission(x, req));
} }
return this.datastore.find(); return (await this.datastore.find()).filter(x => connectionHasPermission(x, req));
}, },
test_meta: true, test_meta: true,
@@ -217,16 +218,18 @@ module.exports = {
}, },
update_meta: true, update_meta: true,
async update({ _id, values }) { async update({ _id, values }, req) {
if (portalConnections) return; if (portalConnections) return;
testConnectionPermission(_id, req);
const res = await this.datastore.patch(_id, values); const res = await this.datastore.patch(_id, values);
socket.emitChanged('connection-list-changed'); socket.emitChanged('connection-list-changed');
return res; return res;
}, },
updateDatabase_meta: true, updateDatabase_meta: true,
async updateDatabase({ conid, database, values }) { async updateDatabase({ conid, database, values }, req) {
if (portalConnections) return; if (portalConnections) return;
testConnectionPermission(conid, req);
const conn = await this.datastore.get(conid); const conn = await this.datastore.get(conid);
let databases = (conn && conn.databases) || []; let databases = (conn && conn.databases) || [];
if (databases.find(x => x.name == database)) { if (databases.find(x => x.name == database)) {
@@ -242,8 +245,9 @@ module.exports = {
}, },
delete_meta: true, delete_meta: true,
async delete(connection) { async delete(connection, req) {
if (portalConnections) return; if (portalConnections) return;
testConnectionPermission(connection, req);
const res = await this.datastore.remove(connection._id); const res = await this.datastore.remove(connection._id);
socket.emitChanged('connection-list-changed'); socket.emitChanged('connection-list-changed');
return res; return res;
@@ -260,7 +264,8 @@ module.exports = {
}, },
get_meta: true, get_meta: true,
async get({ conid }) { async get({ conid }, req) {
testConnectionPermission(conid, req);
return this.getCore({ conid, mask: true }); return this.getCore({ conid, mask: true });
}, },

View File

@@ -26,6 +26,7 @@ const generateDeploySql = require('../shell/generateDeploySql');
const { createTwoFilesPatch } = require('diff'); const { createTwoFilesPatch } = require('diff');
const diff2htmlPage = require('../utility/diff2htmlPage'); const diff2htmlPage = require('../utility/diff2htmlPage');
const processArgs = require('../utility/processArgs'); const processArgs = require('../utility/processArgs');
const { testConnectionPermission } = require('../utility/hasPermission');
module.exports = { module.exports = {
/** @type {import('dbgate-types').OpenedDatabaseConnection[]} */ /** @type {import('dbgate-types').OpenedDatabaseConnection[]} */
@@ -130,7 +131,8 @@ module.exports = {
}, },
queryData_meta: true, queryData_meta: true,
async queryData({ conid, database, sql }) { async queryData({ conid, database, sql }, req) {
testConnectionPermission(conid, req);
console.log(`Processing query, conid=${conid}, database=${database}, sql=${sql}`); console.log(`Processing query, conid=${conid}, database=${database}, sql=${sql}`);
const opened = await this.ensureOpened(conid, database); const opened = await this.ensureOpened(conid, database);
// if (opened && opened.status && opened.status.name == 'error') { // if (opened && opened.status && opened.status.name == 'error') {
@@ -141,14 +143,16 @@ module.exports = {
}, },
sqlSelect_meta: true, sqlSelect_meta: true,
async sqlSelect({ conid, database, select }) { async sqlSelect({ conid, database, select }, req) {
testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid, database); const opened = await this.ensureOpened(conid, database);
const res = await this.sendRequest(opened, { msgtype: 'sqlSelect', select }); const res = await this.sendRequest(opened, { msgtype: 'sqlSelect', select });
return res; return res;
}, },
runScript_meta: true, runScript_meta: true,
async runScript({ conid, database, sql }) { async runScript({ conid, database, sql }, req) {
testConnectionPermission(conid, req);
console.log(`Processing script, conid=${conid}, database=${database}, sql=${sql}`); console.log(`Processing script, conid=${conid}, database=${database}, sql=${sql}`);
const opened = await this.ensureOpened(conid, database); const opened = await this.ensureOpened(conid, database);
const res = await this.sendRequest(opened, { msgtype: 'runScript', sql }); const res = await this.sendRequest(opened, { msgtype: 'runScript', sql });
@@ -156,13 +160,15 @@ module.exports = {
}, },
collectionData_meta: true, collectionData_meta: true,
async collectionData({ conid, database, options }) { async collectionData({ conid, database, options }, req) {
testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid, database); const opened = await this.ensureOpened(conid, database);
const res = await this.sendRequest(opened, { msgtype: 'collectionData', options }); const res = await this.sendRequest(opened, { msgtype: 'collectionData', options });
return res.result || null; return res.result || null;
}, },
async loadDataCore(msgtype, { conid, database, ...args }) { async loadDataCore(msgtype, { conid, database, ...args }, req) {
testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid, database); const opened = await this.ensureOpened(conid, database);
const res = await this.sendRequest(opened, { msgtype, ...args }); const res = await this.sendRequest(opened, { msgtype, ...args });
if (res.errorMessage) { if (res.errorMessage) {
@@ -176,32 +182,38 @@ module.exports = {
}, },
loadKeys_meta: true, loadKeys_meta: true,
async loadKeys({ conid, database, root, filter }) { async loadKeys({ conid, database, root, filter }, req) {
testConnectionPermission(conid, req);
return this.loadDataCore('loadKeys', { conid, database, root, filter }); return this.loadDataCore('loadKeys', { conid, database, root, filter });
}, },
exportKeys_meta: true, exportKeys_meta: true,
async exportKeys({ conid, database, options }) { async exportKeys({ conid, database, options }, req) {
testConnectionPermission(conid, req);
return this.loadDataCore('exportKeys', { conid, database, options }); return this.loadDataCore('exportKeys', { conid, database, options });
}, },
loadKeyInfo_meta: true, loadKeyInfo_meta: true,
async loadKeyInfo({ conid, database, key }) { async loadKeyInfo({ conid, database, key }, req) {
testConnectionPermission(conid, req);
return this.loadDataCore('loadKeyInfo', { conid, database, key }); return this.loadDataCore('loadKeyInfo', { conid, database, key });
}, },
loadKeyTableRange_meta: true, loadKeyTableRange_meta: true,
async loadKeyTableRange({ conid, database, key, cursor, count }) { async loadKeyTableRange({ conid, database, key, cursor, count }, req) {
testConnectionPermission(conid, req);
return this.loadDataCore('loadKeyTableRange', { conid, database, key, cursor, count }); return this.loadDataCore('loadKeyTableRange', { conid, database, key, cursor, count });
}, },
loadFieldValues_meta: true, loadFieldValues_meta: true,
async loadFieldValues({ conid, database, schemaName, pureName, field, search }) { async loadFieldValues({ conid, database, schemaName, pureName, field, search }, req) {
testConnectionPermission(conid, req);
return this.loadDataCore('loadFieldValues', { conid, database, schemaName, pureName, field, search }); return this.loadDataCore('loadFieldValues', { conid, database, schemaName, pureName, field, search });
}, },
callMethod_meta: true, callMethod_meta: true,
async callMethod({ conid, database, method, args }) { async callMethod({ conid, database, method, args }, req) {
testConnectionPermission(conid, req);
return this.loadDataCore('callMethod', { conid, database, method, args }); return this.loadDataCore('callMethod', { conid, database, method, args });
// const opened = await this.ensureOpened(conid, database); // const opened = await this.ensureOpened(conid, database);
@@ -213,7 +225,8 @@ module.exports = {
}, },
updateCollection_meta: true, updateCollection_meta: true,
async updateCollection({ conid, database, changeSet }) { async updateCollection({ conid, database, changeSet }, req) {
testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid, database); const opened = await this.ensureOpened(conid, database);
const res = await this.sendRequest(opened, { msgtype: 'updateCollection', changeSet }); const res = await this.sendRequest(opened, { msgtype: 'updateCollection', changeSet });
if (res.errorMessage) { if (res.errorMessage) {
@@ -225,7 +238,14 @@ module.exports = {
}, },
status_meta: true, status_meta: true,
async status({ conid, database }) { async status({ conid, database }, req) {
if (!conid) {
return {
name: 'error',
message: 'No connection',
};
}
testConnectionPermission(conid, req);
const existing = this.opened.find(x => x.conid == conid && x.database == database); const existing = this.opened.find(x => x.conid == conid && x.database == database);
if (existing) { if (existing) {
return { return {
@@ -247,7 +267,8 @@ module.exports = {
}, },
ping_meta: true, ping_meta: true,
async ping({ conid, database }) { async ping({ conid, database }, req) {
testConnectionPermission(conid, req);
let existing = this.opened.find(x => x.conid == conid && x.database == database); let existing = this.opened.find(x => x.conid == conid && x.database == database);
if (existing) { if (existing) {
@@ -263,7 +284,8 @@ module.exports = {
}, },
refresh_meta: true, refresh_meta: true,
async refresh({ conid, database, keepOpen }) { async refresh({ conid, database, keepOpen }, req) {
testConnectionPermission(conid, req);
if (!keepOpen) this.close(conid, database); if (!keepOpen) this.close(conid, database);
await this.ensureOpened(conid, database); await this.ensureOpened(conid, database);
@@ -271,7 +293,8 @@ module.exports = {
}, },
syncModel_meta: true, syncModel_meta: true,
async syncModel({ conid, database, isFullRefresh }) { async syncModel({ conid, database, isFullRefresh }, req) {
testConnectionPermission(conid, req);
const conn = await this.ensureOpened(conid, database); const conn = await this.ensureOpened(conid, database);
conn.subprocess.send({ msgtype: 'syncModel', isFullRefresh }); conn.subprocess.send({ msgtype: 'syncModel', isFullRefresh });
return { status: 'ok' }; return { status: 'ok' };
@@ -301,13 +324,15 @@ module.exports = {
}, },
disconnect_meta: true, disconnect_meta: true,
async disconnect({ conid, database }) { async disconnect({ conid, database }, req) {
testConnectionPermission(conid, req);
await this.close(conid, database, true); await this.close(conid, database, true);
return { status: 'ok' }; return { status: 'ok' };
}, },
structure_meta: true, structure_meta: true,
async structure({ conid, database }) { async structure({ conid, database }, req) {
testConnectionPermission(conid, req);
if (conid == '__model') { if (conid == '__model') {
const model = await importDbModel(database); const model = await importDbModel(database);
return model; return model;
@@ -324,14 +349,19 @@ module.exports = {
}, },
serverVersion_meta: true, serverVersion_meta: true,
async serverVersion({ conid, database }) { async serverVersion({ conid, database }, req) {
if (!conid) {
return null;
}
testConnectionPermission(conid, req);
if (!conid) return null; if (!conid) return null;
const opened = await this.ensureOpened(conid, database); const opened = await this.ensureOpened(conid, database);
return opened.serverVersion || null; return opened.serverVersion || null;
}, },
sqlPreview_meta: true, sqlPreview_meta: true,
async sqlPreview({ conid, database, objects, options }) { async sqlPreview({ conid, database, objects, options }, req) {
testConnectionPermission(conid, req);
// wait for structure // wait for structure
await this.structure({ conid, database }); await this.structure({ conid, database });
@@ -341,7 +371,8 @@ module.exports = {
}, },
exportModel_meta: true, exportModel_meta: true,
async exportModel({ conid, database }) { async exportModel({ conid, database }, req) {
testConnectionPermission(conid, req);
const archiveFolder = await archive.getNewArchiveFolder({ database }); const archiveFolder = await archive.getNewArchiveFolder({ database });
await fs.mkdir(path.join(archivedir(), archiveFolder)); await fs.mkdir(path.join(archivedir(), archiveFolder));
const model = await this.structure({ conid, database }); const model = await this.structure({ conid, database });
@@ -351,7 +382,8 @@ module.exports = {
}, },
generateDeploySql_meta: true, generateDeploySql_meta: true,
async generateDeploySql({ conid, database, archiveFolder }) { async generateDeploySql({ conid, database, archiveFolder }, req) {
testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid, database); const opened = await this.ensureOpened(conid, database);
const res = await this.sendRequest(opened, { const res = await this.sendRequest(opened, {
msgtype: 'generateDeploySql', msgtype: 'generateDeploySql',

View File

@@ -7,6 +7,7 @@ const { handleProcessCommunication } = require('../utility/processComm');
const lock = new AsyncLock(); const lock = new AsyncLock();
const config = require('./config'); const config = require('./config');
const processArgs = require('../utility/processArgs'); const processArgs = require('../utility/processArgs');
const { testConnectionPermission } = require('../utility/hasPermission');
module.exports = { module.exports = {
opened: [], opened: [],
@@ -90,19 +91,22 @@ module.exports = {
}, },
disconnect_meta: true, disconnect_meta: true,
async disconnect({ conid }) { async disconnect({ conid }, req) {
testConnectionPermission(conid, req);
await this.close(conid, true); await this.close(conid, true);
return { status: 'ok' }; return { status: 'ok' };
}, },
listDatabases_meta: true, listDatabases_meta: true,
async listDatabases({ conid }) { async listDatabases({ conid }, req) {
testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid); const opened = await this.ensureOpened(conid);
return opened.databases; return opened.databases;
}, },
version_meta: true, version_meta: true,
async version({ conid }) { async version({ conid }, req) {
testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid); const opened = await this.ensureOpened(conid);
return opened.version; return opened.version;
}, },
@@ -132,7 +136,8 @@ module.exports = {
}, },
refresh_meta: true, refresh_meta: true,
async refresh({ conid, keepOpen }) { async refresh({ conid, keepOpen }, req) {
testConnectionPermission(conid, req);
if (!keepOpen) this.close(conid); if (!keepOpen) this.close(conid);
await this.ensureOpened(conid); await this.ensureOpened(conid);
@@ -140,7 +145,8 @@ module.exports = {
}, },
createDatabase_meta: true, createDatabase_meta: true,
async createDatabase({ conid, name }) { async createDatabase({ conid, name }, req) {
testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid); const opened = await this.ensureOpened(conid);
if (opened.connection.isReadOnly) return false; if (opened.connection.isReadOnly) return false;
opened.subprocess.send({ msgtype: 'createDatabase', name }); opened.subprocess.send({ msgtype: 'createDatabase', name });

View File

@@ -4,12 +4,21 @@ const _ = require('lodash');
const userPermissions = {}; const userPermissions = {};
function hasPermission(tested, req) { function hasPermission(tested, req) {
if (!req) {
// request object not available, allow all
return true;
}
const { user } = (req && req.auth) || {}; const { user } = (req && req.auth) || {};
const key = user || ''; const key = user || '';
const logins = getLogins(); const logins = getLogins();
if (!userPermissions[key] && logins) {
const login = logins.find(x => x.login == user); if (!userPermissions[key]) {
userPermissions[key] = compilePermissions(login ? login.permissions : null); if (logins) {
const login = logins.find(x => x.login == user);
userPermissions[key] = compilePermissions(login ? login.permissions : null);
} else {
userPermissions[key] = compilePermissions(process.env.PERMISSIONS);
}
} }
return testPermission(tested, userPermissions[key]); return testPermission(tested, userPermissions[key]);
} }
@@ -50,7 +59,26 @@ function getLogins() {
return loginsCache; return loginsCache;
} }
function connectionHasPermission(connection, req) {
if (!connection) {
return true;
}
if (_.isString(connection)) {
return hasPermission(`connections/${connection}`, req);
} else {
return hasPermission(`connections/${connection._id}`, req);
}
}
function testConnectionPermission(connection, req) {
if (!connectionHasPermission(connection, req)) {
throw new Error('Connection permission not granted');
}
}
module.exports = { module.exports = {
hasPermission, hasPermission,
getLogins, getLogins,
connectionHasPermission,
testConnectionPermission,
}; };

View File

@@ -47,7 +47,6 @@ module.exports = function useController(app, electron, route, controller) {
let method = 'post'; let method = 'post';
let raw = false; let raw = false;
let rawParams = false;
// if (_.isString(meta)) { // if (_.isString(meta)) {
// method = meta; // method = meta;
@@ -55,7 +54,6 @@ module.exports = function useController(app, electron, route, controller) {
if (_.isPlainObject(meta)) { if (_.isPlainObject(meta)) {
method = meta.method; method = meta.method;
raw = meta.raw; raw = meta.raw;
rawParams = meta.rawParams;
} }
if (raw) { if (raw) {
@@ -67,9 +65,7 @@ module.exports = function useController(app, electron, route, controller) {
// controller._init_called = true; // controller._init_called = true;
// } // }
try { try {
let params = [{ ...req.body, ...req.query }, req]; const data = await controller[key]({ ...req.body, ...req.query }, req);
if (rawParams) params = [req, res];
const data = await controller[key](...params);
res.json(data); res.json(data);
} catch (e) { } catch (e) {
console.log(e); console.log(e);

View File

@@ -56,7 +56,7 @@
id: 'dataGrid.cloneRows', id: 'dataGrid.cloneRows',
category: 'Data grid', category: 'Data grid',
name: 'Clone rows', name: 'Clone rows',
toolbarName: 'Clone', toolbarName: 'Clone row(s)',
keyText: 'CtrlOrCommand+Shift+C', keyText: 'CtrlOrCommand+Shift+C',
testEnabled: () => getCurrentDataGrid()?.getGrider()?.editable, testEnabled: () => getCurrentDataGrid()?.getGrider()?.editable,
onClick: () => getCurrentDataGrid().cloneRows(), onClick: () => getCurrentDataGrid().cloneRows(),

View File

@@ -167,8 +167,8 @@
async function detectSize(tables, domTables) { async function detectSize(tables, domTables) {
await tick(); await tick();
const rects = _.values(domTables).map(x => x.getRect()); const rects = _.values(domTables).map(x => x.getRect());
const maxX = _.max(rects.map(x => x.right)); const maxX = rects.length > 0 ? _.max(rects.map(x => x.right)) : 0;
const maxY = _.max(rects.map(x => x.bottom)); const maxY = rects.length > 0 ? _.max(rects.map(x => x.bottom)) : 0;
canvasWidth = Math.max(3000, maxX + 50); canvasWidth = Math.max(3000, maxX + 50);
canvasHeight = Math.max(3000, maxY + 50); canvasHeight = Math.max(3000, maxY + 50);

View File

@@ -8,6 +8,7 @@ import {
mergeConditions, mergeConditions,
Source, Source,
ResultField, ResultField,
Expression,
} from 'dbgate-sqltree'; } from 'dbgate-sqltree';
import { EngineDriver } from 'dbgate-types'; import { EngineDriver } from 'dbgate-types';
import { DesignerInfo, DesignerTableInfo, DesignerReferenceInfo, DesignerJoinType } from './types'; import { DesignerInfo, DesignerTableInfo, DesignerReferenceInfo, DesignerJoinType } from './types';
@@ -78,25 +79,27 @@ export class DesignerQueryDumper {
return select; return select;
} }
addConditions(select: Select, tables: DesignerTableInfo[]) { buildConditionFromFilterField(tables: DesignerTableInfo[], filterField: string, getExpression?: Function): Condition {
const conditions = [];
for (const column of this.designer.columns || []) { for (const column of this.designer.columns || []) {
if (!column.filter) continue; if (!column[filterField]) continue;
const table = (this.designer.tables || []).find(x => x.designerId == column.designerId);
if (!table) continue; if (!column.isCustomExpression) {
if (!tables.find(x => x.designerId == table.designerId)) continue; const table = (this.designer.tables || []).find(x => x.designerId == column.designerId);
if (!table) continue;
if (!tables.find(x => x.designerId == table.designerId)) continue;
}
try { try {
const condition = parseFilter(column.filter, findDesignerFilterType(column, this.designer)); const condition = parseFilter(column[filterField], findDesignerFilterType(column, this.designer));
if (condition) { if (condition) {
select.where = mergeConditions( conditions.push(
select.where,
_.cloneDeepWith(condition, expr => { _.cloneDeepWith(condition, expr => {
if (expr.exprType == 'placeholder') if (expr.exprType == 'placeholder') {
return { if (getExpression) return getExpression(column);
exprType: 'column', return this.getColumnExpression(column);
columnName: column.columnName, }
source: findQuerySource(this.designer, column.designerId),
};
}) })
); );
} }
@@ -105,33 +108,79 @@ export class DesignerQueryDumper {
continue; continue;
} }
} }
if (conditions.length == 0) {
return null;
}
if (conditions.length == 1) {
return conditions[0];
}
return {
conditionType: 'and',
conditions,
};
}
addConditionsCore(select: Select, tables: DesignerTableInfo[], filterFields, selectField, getExpression?) {
const conditions: Condition[] = _.compact(
filterFields.map(field => this.buildConditionFromFilterField(tables, field, getExpression))
);
if (conditions.length == 0) {
return;
}
if (conditions.length == 0) {
select[selectField] = mergeConditions(select[selectField], conditions[0]);
return;
}
select[selectField] = mergeConditions(select[selectField], {
conditionType: 'or',
conditions,
});
}
addConditions(select: Select, tables: DesignerTableInfo[]) {
const additionalFilterCount = this.designer.settings?.additionalFilterCount || 0;
const filterFields = ['filter', ..._.range(additionalFilterCount).map(index => `additionalFilter${index + 1}`)];
this.addConditionsCore(select, tables, filterFields, 'where');
} }
addGroupConditions(select: Select, tables: DesignerTableInfo[], selectIsGrouped: boolean) { addGroupConditions(select: Select, tables: DesignerTableInfo[], selectIsGrouped: boolean) {
for (const column of this.designer.columns || []) { const additionalGroupFilterCount = this.designer.settings?.additionalGroupFilterCount || 0;
if (!column.groupFilter) continue; const filterFields = [
const table = (this.designer.tables || []).find(x => x.designerId == column.designerId); 'groupFilter',
if (!table) continue; ..._.range(additionalGroupFilterCount).map(index => `additionalGroupFilter${index + 1}`),
if (!tables.find(x => x.designerId == table.designerId)) continue; ];
this.addConditionsCore(select, tables, filterFields, 'having', column =>
const condition = parseFilter(column.groupFilter, findDesignerFilterType(column, this.designer)); this.getColumnResultField(column, selectIsGrouped)
if (condition) { );
select.having = mergeConditions(
select.having,
_.cloneDeepWith(condition, expr => {
if (expr.exprType == 'placeholder') {
return this.getColumnOutputExpression(column, selectIsGrouped);
}
})
);
}
}
} }
getColumnOutputExpression(col, selectIsGrouped): ResultField { getColumnExpression(col): Expression {
const source = findQuerySource(this.designer, col.designerId); const source = findQuerySource(this.designer, col.designerId);
const { columnName, isCustomExpression, customExpression } = col;
const res: Expression = isCustomExpression
? {
exprType: 'raw',
sql: customExpression,
}
: {
exprType: 'column',
columnName,
source,
};
return res;
}
getColumnResultField(col, selectIsGrouped): ResultField {
const { columnName } = col; const { columnName } = col;
let { alias } = col; let { alias } = col;
const exprCore = this.getColumnExpression(col);
if (selectIsGrouped && !col.isGrouped) { if (selectIsGrouped && !col.isGrouped) {
// use aggregate // use aggregate
const aggregate = col.aggregate == null || col.aggregate == '---' ? 'MAX' : col.aggregate; const aggregate = col.aggregate == null || col.aggregate == '---' ? 'MAX' : col.aggregate;
@@ -142,20 +191,12 @@ export class DesignerQueryDumper {
func: aggregate == 'COUNT DISTINCT' ? 'COUNT' : aggregate, func: aggregate == 'COUNT DISTINCT' ? 'COUNT' : aggregate,
argsPrefix: aggregate == 'COUNT DISTINCT' ? 'DISTINCT' : null, argsPrefix: aggregate == 'COUNT DISTINCT' ? 'DISTINCT' : null,
alias, alias,
args: [ args: [exprCore],
{
exprType: 'column',
columnName,
source,
},
],
}; };
} else { } else {
return { return {
exprType: 'column', ...exprCore,
columnName,
alias, alias,
source,
}; };
} }
} }
@@ -179,24 +220,21 @@ export class DesignerQueryDumper {
} }
} }
const topLevelColumns = (this.designer.columns || []).filter(col => const topLevelColumns = (this.designer.columns || []).filter(
topLevelTables.find(tbl => tbl.designerId == col.designerId) col =>
topLevelTables.find(tbl => tbl.designerId == col.designerId) || (col.isCustomExpression && col.customExpression)
); );
const selectIsGrouped = !!topLevelColumns.find(x => x.isGrouped || (x.aggregate && x.aggregate != '---')); const selectIsGrouped = !!topLevelColumns.find(x => x.isGrouped || (x.aggregate && x.aggregate != '---'));
const outputColumns = topLevelColumns.filter(x => x.isOutput); const outputColumns = topLevelColumns.filter(x => x.isOutput);
if (outputColumns.length == 0) { if (outputColumns.length == 0) {
res.selectAll = true; res.selectAll = true;
} else { } else {
res.columns = outputColumns.map(col => this.getColumnOutputExpression(col, selectIsGrouped)); res.columns = outputColumns.map(col => this.getColumnResultField(col, selectIsGrouped));
} }
const groupedColumns = topLevelColumns.filter(x => x.isGrouped); const groupedColumns = topLevelColumns.filter(x => x.isGrouped);
if (groupedColumns.length > 0) { if (groupedColumns.length > 0) {
res.groupBy = groupedColumns.map(col => ({ res.groupBy = groupedColumns.map(col => this.getColumnExpression(col));
exprType: 'column',
columnName: col.columnName,
source: findQuerySource(this.designer, col.designerId),
}));
} }
const orderColumns = _.sortBy( const orderColumns = _.sortBy(
@@ -205,10 +243,8 @@ export class DesignerQueryDumper {
); );
if (orderColumns.length > 0) { if (orderColumns.length > 0) {
res.orderBy = orderColumns.map(col => ({ res.orderBy = orderColumns.map(col => ({
exprType: 'column', ...this.getColumnExpression(col),
direction: col.sortOrder < 0 ? 'DESC' : 'ASC', direction: col.sortOrder < 0 ? 'DESC' : 'ASC',
columnName: col.columnName,
source: findQuerySource(this.designer, col.designerId),
})); }));
} }

View File

@@ -31,10 +31,14 @@ export type DesignerColumnInfo = {
sortOrder?: number; sortOrder?: number;
filter?: string; filter?: string;
groupFilter?: string; groupFilter?: string;
isCustomExpression?: boolean;
customExpression?: string;
}; };
export type DesignerSettings = { export type DesignerSettings = {
isDistinct?: boolean; isDistinct?: boolean;
additionalFilterCount?: number;
additionalGroupFilterCount?: number;
}; };
export type DesignerInfo = { export type DesignerInfo = {

View File

@@ -13,8 +13,11 @@
import SelectField from '../forms/SelectField.svelte'; import SelectField from '../forms/SelectField.svelte';
import TextField from '../forms/TextField.svelte'; import TextField from '../forms/TextField.svelte';
import InlineButton from '../buttons/InlineButton.svelte'; import InlineButton from '../buttons/InlineButton.svelte';
import uuidv1 from 'uuid/v1';
import TableControl from './TableControl.svelte'; import TableControl from './TableControl.svelte';
import FormStyledButton from '../buttons/FormStyledButton.svelte';
import _ from 'lodash';
export let value; export let value;
export let onChange; export let onChange;
@@ -35,8 +38,56 @@
})); }));
}; };
const addExpressionColumn = () => {
onChange(current => ({
...current,
columns: [...(current.columns || []), { isCustomExpression: true, isOutput: true, designerId: uuidv1() }],
}));
};
const addOrCondition = () => {
onChange(current => ({
...current,
settings: {
...current?.settings,
additionalFilterCount: (current?.settings?.additionalFilterCount ?? 0) + 1,
},
}));
};
const removeOrCondition = () => {
onChange(current => ({
...current,
settings: {
...current?.settings,
additionalFilterCount: (current?.settings?.additionalFilterCount ?? 1) - 1,
},
}));
};
const addGroupOrCondition = () => {
onChange(current => ({
...current,
settings: {
...current?.settings,
additionalGroupFilterCount: (current?.settings?.additionalGroupFilterCount ?? 0) + 1,
},
}));
};
const removeGroupOrCondition = () => {
onChange(current => ({
...current,
settings: {
...current?.settings,
additionalGroupFilterCount: (current?.settings?.additionalGroupFilterCount ?? 1) - 1,
},
}));
};
$: columns = value?.columns; $: columns = value?.columns;
$: tables = value?.tables; $: tables = value?.tables;
$: settings = value?.settings;
$: hasGroupedColumn = !!(columns || []).find(x => x.isGrouped); $: hasGroupedColumn = !!(columns || []).find(x => x.isGrouped);
</script> </script>
@@ -44,18 +95,49 @@
<TableControl <TableControl
rows={columns || []} rows={columns || []}
columns={[ columns={[
{ fieldName: 'columnName', header: 'Column/Expression' }, { fieldName: 'columnName', slot: 8, header: 'Column/Expression' },
{ fieldName: 'tableDisplayName', header: 'Table', formatter: row => getTableDisplayName(row, tables) }, { fieldName: 'tableDisplayName', header: 'Table', formatter: row => getTableDisplayName(row, tables) },
{ fieldName: 'isOutput', header: 'Output', slot: 0 }, { fieldName: 'isOutput', header: 'Output', slot: 0 },
{ fieldName: 'alias', header: 'Alias', slot: 1 }, { fieldName: 'alias', header: 'Alias', slot: 1 },
{ fieldName: 'isGrouped', header: 'Group by', slot: 2 }, { fieldName: 'isGrouped', header: 'Group by', slot: 2 },
{ fieldName: 'aggregate', header: 'Aggregate', slot: 3 }, { fieldName: 'aggregate', header: 'Aggregate', slot: 3 },
{ fieldName: 'sortOrder', header: 'Sort order', slot: 4 }, { fieldName: 'sortOrder', header: 'Sort order', slot: 4 },
{ fieldName: 'filter', header: 'Filter', slot: 5 }, { fieldName: 'filter', header: 'Filter', slot: 5, props: { filterField: 'filter' } },
hasGroupedColumn && { fieldName: 'groupFilter', header: 'Group filter', slot: 6 }, ..._.range(settings?.additionalFilterCount || 0).map(index => ({
fieldName: `additionalFilter${index + 1}`,
header: `OR Filter ${index + 2}`,
slot: 5,
props: { filterField: `additionalFilter${index + 1}` },
})),
hasGroupedColumn && {
fieldName: 'groupFilter',
header: 'Group filter',
slot: 5,
props: { filterField: 'groupFilter' },
},
..._.range(hasGroupedColumn ? settings?.additionalGroupFilterCount || 0 : 0).map(index => ({
fieldName: `additionalGroupFilter${index + 1}`,
header: `OR group filter ${index + 2}`,
slot: 5,
props: { filterField: `additionalGroupFilter${index + 1}` },
})),
{ fieldName: 'actions', header: '', slot: 7 }, { fieldName: 'actions', header: '', slot: 7 },
]} ]}
> >
<svelte:fragment slot="8" let:row>
{#if row.isCustomExpression}
<TextField
style="min-width:calc(100% - 9px)"
value={row.customExpression}
on:input={e => {
changeColumn({ ...row, customExpression: e.target.value });
}}
/>
{:else}
{row.columnName}
{/if}
</svelte:fragment>
<svelte:fragment slot="0" let:row> <svelte:fragment slot="0" let:row>
<CheckboxField <CheckboxField
checked={row.isOutput} checked={row.isOutput}
@@ -67,6 +149,7 @@
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="1" let:row> <svelte:fragment slot="1" let:row>
<TextField <TextField
style="min-width:calc(100% - 9px)"
value={row.alias} value={row.alias}
on:input={e => { on:input={e => {
changeColumn({ ...row, alias: e.target.value }); changeColumn({ ...row, alias: e.target.value });
@@ -86,6 +169,7 @@
{#if !row.isGrouped} {#if !row.isGrouped}
<SelectField <SelectField
isNative isNative
style="min-width:calc(100% - 9px)"
value={row.aggregate} value={row.aggregate}
on:change={e => { on:change={e => {
changeColumn({ ...row, aggregate: e.detail }); changeColumn({ ...row, aggregate: e.detail });
@@ -97,6 +181,7 @@
<svelte:fragment slot="4" let:row> <svelte:fragment slot="4" let:row>
<SelectField <SelectField
isNative isNative
style="min-width:calc(100% - 9px)"
value={row.sortOrder} value={row.sortOrder}
on:change={e => { on:change={e => {
changeColumn({ ...row, sortOrder: parseInt(e.detail) }); changeColumn({ ...row, sortOrder: parseInt(e.detail) });
@@ -112,21 +197,12 @@
]} ]}
/> />
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="5" let:row> <svelte:fragment slot="5" let:row let:filterField>
<DataFilterControl <DataFilterControl
filterType={findDesignerFilterType(row, value)} filterType={findDesignerFilterType(row, value)}
filter={row.filter} filter={row[filterField]}
setFilter={filter => { setFilter={filter => {
changeColumn({ ...row, filter }); changeColumn({ ...row, [filterField]: filter });
}}
/>
</svelte:fragment>
<svelte:fragment slot="6" let:row>
<DataFilterControl
filterType={findDesignerFilterType(row, value)}
filter={row.groupFilter}
setFilter={groupFilter => {
changeColumn({ ...row, groupFilter });
}} }}
/> />
</svelte:fragment> </svelte:fragment>
@@ -134,6 +210,17 @@
<InlineButton on:click={() => removeColumn(row)}>Remove</InlineButton> <InlineButton on:click={() => removeColumn(row)}>Remove</InlineButton>
</svelte:fragment> </svelte:fragment>
</TableControl> </TableControl>
<FormStyledButton value="Add custom expression" on:click={addExpressionColumn} style="width:200px" />
<FormStyledButton value="Add OR condition" on:click={addOrCondition} style="width:200px" />
{#if settings?.additionalFilterCount > 0}
<FormStyledButton value="Remove OR condition" on:click={removeOrCondition} style="width:200px" />
{/if}
{#if hasGroupedColumn}
<FormStyledButton value="Add group OR condition" on:click={addGroupOrCondition} style="width:200px" />
{/if}
{#if hasGroupedColumn && settings?.additionalGroupFilterCount > 0}
<FormStyledButton value="Remove group OR condition" on:click={removeGroupOrCondition} style="width:200px" />
{/if}
</div> </div>
<style> <style>
@@ -141,4 +228,4 @@
overflow: auto; overflow: auto;
flex: 1; flex: 1;
} }
</style> </style>

View File

@@ -4,6 +4,7 @@
header: string; header: string;
component?: any; component?: any;
getProps?: any; getProps?: any;
props?: any;
formatter?: any; formatter?: any;
slot?: number; slot?: number;
isHighlighted?: Function; isHighlighted?: Function;
@@ -25,6 +26,7 @@
export let clickable = false; export let clickable = false;
export let disableFocusOutline = false; export let disableFocusOutline = false;
export let emptyMessage = null; export let emptyMessage = null;
export let noCellPadding = false;
export let domTable = undefined; export let domTable = undefined;
@@ -77,21 +79,24 @@
}} }}
> >
{#each columnList as col} {#each columnList as col}
<td class:isHighlighted={col.isHighlighted && col.isHighlighted(row)}> {@const rowProps = { ...col.props, ...(col.getProps ? col.getProps(row) : null) }}
<td class:isHighlighted={col.isHighlighted && col.isHighlighted(row)} class:noCellPadding>
{#if col.component} {#if col.component}
<svelte:component this={col.component} {...col.getProps(row)} /> <svelte:component this={col.component} {...rowProps} />
{:else if col.formatter} {:else if col.formatter}
{col.formatter(row)} {col.formatter(row)}
{:else if col.slot != null} {:else if col.slot != null}
{#if col.slot == -1}<slot name="-1" {row} {index} /> {#if col.slot == -1}<slot name="-1" {row} {index} />
{:else if col.slot == 0}<slot name="0" {row} {index} /> {:else if col.slot == 0}<slot name="0" {row} {index} {...rowProps} />
{:else if col.slot == 1}<slot name="1" {row} {index} /> {:else if col.slot == 1}<slot name="1" {row} {index} {...rowProps} />
{:else if col.slot == 2}<slot name="2" {row} {index} /> {:else if col.slot == 2}<slot name="2" {row} {index} {...rowProps} />
{:else if col.slot == 3}<slot name="3" {row} {index} /> {:else if col.slot == 3}<slot name="3" {row} {index} {...rowProps} />
{:else if col.slot == 4}<slot name="4" {row} {index} /> {:else if col.slot == 4}<slot name="4" {row} {index} {...rowProps} />
{:else if col.slot == 5}<slot name="5" {row} {index} /> {:else if col.slot == 5}<slot name="5" {row} {index} {...rowProps} />
{:else if col.slot == 6}<slot name="6" {row} {index} /> {:else if col.slot == 6}<slot name="6" {row} {index} {...rowProps} />
{:else if col.slot == 7}<slot name="7" {row} {index} /> {:else if col.slot == 7}<slot name="7" {row} {index} {...rowProps} />
{:else if col.slot == 8}<slot name="8" {row} {index} {...rowProps} />
{:else if col.slot == 9}<slot name="9" {row} {index} {...rowProps} />
{/if} {/if}
{:else} {:else}
{row[col.fieldName] || ''} {row[col.fieldName] || ''}
@@ -136,6 +141,9 @@
} }
tbody td { tbody td {
border: 1px solid var(--theme-border); border: 1px solid var(--theme-border);
}
tbody td:not(.noCellPadding) {
padding: 5px; padding: 5px;
} }

View File

@@ -114,7 +114,13 @@
import 'ace-builds/src-noconflict/theme-tomorrow_night'; import 'ace-builds/src-noconflict/theme-tomorrow_night';
import 'ace-builds/src-noconflict/theme-twilight'; import 'ace-builds/src-noconflict/theme-twilight';
import { currentDropDownMenu, currentEditorFontSize, currentEditorTheme, currentThemeDefinition } from '../stores'; import {
currentDropDownMenu,
currentEditorFontSize,
currentEditorFont,
currentEditorTheme,
currentThemeDefinition,
} from '../stores';
import _ from 'lodash'; import _ from 'lodash';
import { handleCommandKeyDown } from '../commands/CommandListener.svelte'; import { handleCommandKeyDown } from '../commands/CommandListener.svelte';
import resizeObserver from '../utility/resizeObserver'; import resizeObserver from '../utility/resizeObserver';
@@ -223,12 +229,15 @@
} }
} }
$: watchOptions(options); $: watchOptions(options, $currentEditorFont);
function watchOptions(newOption: any) { function watchOptions(newOption: any, fontFamily) {
if (editor) { if (editor) {
editor.setOptions({ editor.setOptions({
...stdOptions, ...stdOptions,
...newOption, ...newOption,
fontFamily: fontFamily || 'Menlo, Monaco, Ubuntu Mono, Consolas, source-code-pro, monospace',
// fontFamily: 'tahoma,Menlo',
// fontSize: '10pt',
}); });
} }
} }

View File

@@ -164,6 +164,10 @@
/> />
{/if} {/if}
{#if driver?.showConnectionField('windowsDomain', $values)}
<FormTextField label="Domain (specify to use NTLM authentication)" name="windowsDomain" disabled={isConnected} />
{/if}
{#if driver?.showConnectionField('isReadOnly', $values)} {#if driver?.showConnectionField('isReadOnly', $values)}
<FormCheckboxField label="Is read only" name="isReadOnly" disabled={isConnected} /> <FormCheckboxField label="Is read only" name="isReadOnly" disabled={isConnected} />
{/if} {/if}

View File

@@ -127,7 +127,7 @@ ORDER BY
<div class="heading">Editor theme</div> <div class="heading">Editor theme</div>
<div class="flex"> <div class="flex">
<div class="col-6"> <div class="col-4">
<FormFieldTemplateLarge label="Theme" type="combo"> <FormFieldTemplateLarge label="Theme" type="combo">
<SelectField <SelectField
isNative isNative
@@ -139,7 +139,7 @@ ORDER BY
</FormFieldTemplateLarge> </FormFieldTemplateLarge>
</div> </div>
<div class="col-6"> <div class="col-4">
<FormFieldTemplateLarge label="Font size " type="combo"> <FormFieldTemplateLarge label="Font size " type="combo">
<SelectField <SelectField
isNative isNative
@@ -150,6 +150,10 @@ ORDER BY
/> />
</FormFieldTemplateLarge> </FormFieldTemplateLarge>
</div> </div>
<div class="col-4">
<FormTextField name="editor.fontFamily" label="Editor font family" />
</div>
</div> </div>
<div class="editor"> <div class="editor">

View File

@@ -68,6 +68,7 @@ export const currentEditorTheme = getElectron()
export const currentEditorFontSize = getElectron() export const currentEditorFontSize = getElectron()
? writableSettingsValue(null, 'currentEditorFontSize') ? writableSettingsValue(null, 'currentEditorFontSize')
: writableWithStorage(null, 'currentEditorFontSize'); : writableWithStorage(null, 'currentEditorFontSize');
export const currentEditorFont = writableSettingsValue(null, 'editor.fontFamily');
export const activeTabId = derived([openedTabs], ([$openedTabs]) => $openedTabs.find(x => x.selected)?.tabid); export const activeTabId = derived([openedTabs], ([$openedTabs]) => $openedTabs.find(x => x.selected)?.tabid);
export const activeTab = derived([openedTabs], ([$openedTabs]) => $openedTabs.find(x => x.selected)); export const activeTab = derived([openedTabs], ([$openedTabs]) => $openedTabs.find(x => x.selected));
export const recentDatabases = writableWithStorage([], 'recentDatabases'); export const recentDatabases = writableWithStorage([], 'recentDatabases');

View File

@@ -22,7 +22,7 @@ function extractTediousColumns(columns, addDriverNativeColumn = false) {
return res; return res;
} }
async function tediousConnect({ server, port, user, password, database, ssl, trustServerCertificate }) { async function tediousConnect({ server, port, user, password, database, ssl, trustServerCertificate, windowsDomnain }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const connectionOptions = { const connectionOptions = {
encrypt: !!ssl, encrypt: !!ssl,
@@ -43,10 +43,11 @@ async function tediousConnect({ server, port, user, password, database, ssl, tru
server, server,
authentication: { authentication: {
type: 'default', type: windowsDomnain ? 'ntlm' : 'default',
options: { options: {
userName: user, userName: user,
password: password, password: password,
...(windowsDomnain ? { domain: windowsDomnain } : {}),
}, },
}, },

View File

@@ -127,13 +127,16 @@ const driver = {
['authType', 'server', 'port', 'user', 'password', 'defaultDatabase', 'singleDatabase', 'isReadOnly'].includes( ['authType', 'server', 'port', 'user', 'password', 'defaultDatabase', 'singleDatabase', 'isReadOnly'].includes(
field field
) || ) ||
(field == 'trustServerCertificate' && values.authType != 'sql' && values.authType != 'sspi'), (field == 'trustServerCertificate' && values.authType != 'sql' && values.authType != 'sspi') ||
(field == 'windowsDomain' && values.authType != 'sql' && values.authType != 'sspi'),
// (field == 'useDatabaseUrl' && values.authType != 'sql' && values.authType != 'sspi')
getQuerySplitterOptions: () => mssqlSplitterOptions, getQuerySplitterOptions: () => mssqlSplitterOptions,
engine: 'mssql@dbgate-plugin-mssql', engine: 'mssql@dbgate-plugin-mssql',
title: 'Microsoft SQL Server', title: 'Microsoft SQL Server',
defaultPort: 1433, defaultPort: 1433,
defaultAuthTypeName: 'tedious', defaultAuthTypeName: 'tedious',
// databaseUrlPlaceholder: 'e.g. server=localhost&authentication.type=default&authentication.type.user=myuser&authentication.type.password=pwd&options.database=mydb',
getNewObjectTemplates() { getNewObjectTemplates() {
return [ return [