mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-17 23:45:59 +00:00
Merge branch 'master' into feature/svelte4
This commit is contained in:
@@ -2,6 +2,7 @@ DEVMODE=1
|
||||
SHELL_SCRIPTING=1
|
||||
ALLOW_DBGATE_PRIVATE_CLOUD=1
|
||||
DEVWEB=1
|
||||
# LOCAL_AUTH_PROXY=1
|
||||
# LOCAL_AI_GATEWAY=true
|
||||
|
||||
# REDIRECT_TO_DBGATE_CLOUD_LOGIN=1
|
||||
|
||||
46
packages/api/env/sfill/.env
vendored
Normal file
46
packages/api/env/sfill/.env
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
DEVMODE=1
|
||||
DEVWEB=1
|
||||
|
||||
STORAGE_SERVER=localhost
|
||||
STORAGE_USER=root
|
||||
STORAGE_PASSWORD=Pwd2020Db
|
||||
STORAGE_PORT=3306
|
||||
STORAGE_DATABASE=dbgate-filled
|
||||
STORAGE_ENGINE=mysql@dbgate-plugin-mysql
|
||||
|
||||
CONNECTIONS=mysql,postgres,mongo,redis
|
||||
|
||||
LABEL_mysql=MySql
|
||||
SERVER_mysql=dbgatedckstage1.sprinx.cz
|
||||
USER_mysql=root
|
||||
PASSWORD_mysql=Pwd2020Db
|
||||
PORT_mysql=3306
|
||||
ENGINE_mysql=mysql@dbgate-plugin-mysql
|
||||
|
||||
LABEL_postgres=Postgres
|
||||
SERVER_postgres=dbgatedckstage1.sprinx.cz
|
||||
USER_postgres=postgres
|
||||
PASSWORD_postgres=Pwd2020Db
|
||||
PORT_postgres=5432
|
||||
ENGINE_postgres=postgres@dbgate-plugin-postgres
|
||||
|
||||
LABEL_mongo=Mongo
|
||||
SERVER_mongo=dbgatedckstage1.sprinx.cz
|
||||
USER_mongo=root
|
||||
PASSWORD_mongo=Pwd2020Db
|
||||
PORT_mongo=27017
|
||||
ENGINE_mongo=mongo@dbgate-plugin-mongo
|
||||
|
||||
LABEL_redis=Redis
|
||||
SERVER_redis=dbgatedckstage1.sprinx.cz
|
||||
ENGINE_redis=redis@dbgate-plugin-redis
|
||||
PORT_redis=6379
|
||||
|
||||
ROLE_test1_CONNECTIONS=mysql
|
||||
ROLE_test1_PERMISSIONS=widgets/*
|
||||
ROLE_test1_DATABASES_db1_CONNECTION=mysql
|
||||
ROLE_test1_DATABASES_db1_PERMISSION=run_script
|
||||
ROLE_test1_DATABASES_db1_DATABASES=db1
|
||||
ROLE_test1_DATABASES_db2_CONNECTION=redis
|
||||
ROLE_test1_DATABASES_db2_PERMISSION=run_script
|
||||
ROLE_test1_DATABASES_db2_DATABASES=db2
|
||||
@@ -31,7 +31,7 @@
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^6.0.3",
|
||||
"dbgate-datalib": "^6.0.0-alpha.1",
|
||||
"dbgate-query-splitter": "^4.11.5",
|
||||
"dbgate-query-splitter": "^4.11.9",
|
||||
"dbgate-sqltree": "^6.0.0-alpha.1",
|
||||
"dbgate-tools": "^6.0.0-alpha.1",
|
||||
"debug": "^4.3.4",
|
||||
@@ -75,6 +75,7 @@
|
||||
"start:dblogin": "env-cmd -f env/dblogin/.env node src/index.js --listen-api",
|
||||
"start:filedb": "env-cmd node src/index.js /home/jena/test/chinook/Chinook.db --listen-api",
|
||||
"start:storage": "env-cmd -f env/storage/.env node src/index.js --listen-api",
|
||||
"start:sfill": "env-cmd -f env/sfill/.env node src/index.js --listen-api",
|
||||
"start:storage:built": "env-cmd -f env/storage/.env cross-env DEVMODE= BUILTWEBMODE=1 node dist/bundle.js --listen-api",
|
||||
"start:singleconn": "env-cmd node src/index.js --server localhost --user root --port 3307 --engine mysql@dbgate-plugin-mysql --password test --listen-api",
|
||||
"start:azure": "env-cmd -f env/azure/.env node src/index.js --listen-api",
|
||||
|
||||
@@ -10,7 +10,13 @@ function getTokenSecret() {
|
||||
return tokenSecret;
|
||||
}
|
||||
|
||||
function getStaticTokenSecret() {
|
||||
// TODO static not fixed
|
||||
return '14813c43-a91b-4ad1-9dcd-a81bd7dbb05f';
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getTokenLifetime,
|
||||
getTokenSecret,
|
||||
getStaticTokenSecret,
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ const logger = getLogger('authProvider');
|
||||
|
||||
class AuthProviderBase {
|
||||
amoid = 'none';
|
||||
skipInList = false;
|
||||
|
||||
async login(login, password, options = undefined, req = undefined) {
|
||||
return {
|
||||
@@ -36,12 +37,28 @@ class AuthProviderBase {
|
||||
return !!req?.user || !!req?.auth;
|
||||
}
|
||||
|
||||
getCurrentPermissions(req) {
|
||||
async getCurrentPermissions(req) {
|
||||
const login = this.getCurrentLogin(req);
|
||||
const permissions = process.env[`LOGIN_PERMISSIONS_${login}`];
|
||||
return permissions || process.env.PERMISSIONS;
|
||||
}
|
||||
|
||||
async checkCurrentConnectionPermission(req, conid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
async getCurrentDatabasePermissions(req) {
|
||||
return [];
|
||||
}
|
||||
|
||||
async getCurrentTablePermissions(req) {
|
||||
return [];
|
||||
}
|
||||
|
||||
async getCurrentFilePermissions(req) {
|
||||
return [];
|
||||
}
|
||||
|
||||
getLoginPageConnections() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,233 +1,99 @@
|
||||
const fs = require('fs-extra');
|
||||
const _ = require('lodash');
|
||||
const path = require('path');
|
||||
const { appdir } = require('../utility/directories');
|
||||
const { appdir, filesdir } = require('../utility/directories');
|
||||
const socket = require('../utility/socket');
|
||||
const connections = require('./connections');
|
||||
const {
|
||||
loadPermissionsFromRequest,
|
||||
loadFilePermissionsFromRequest,
|
||||
hasPermission,
|
||||
getFilePermissionRole,
|
||||
} = require('../utility/hasPermission');
|
||||
|
||||
module.exports = {
|
||||
folders_meta: true,
|
||||
async folders() {
|
||||
const folders = await fs.readdir(appdir());
|
||||
return [
|
||||
...folders.map(name => ({
|
||||
name,
|
||||
})),
|
||||
];
|
||||
},
|
||||
|
||||
createFolder_meta: true,
|
||||
async createFolder({ folder }) {
|
||||
const name = await this.getNewAppFolder({ name: folder });
|
||||
await fs.mkdir(path.join(appdir(), name));
|
||||
socket.emitChanged('app-folders-changed');
|
||||
this.emitChangedDbApp(folder);
|
||||
return name;
|
||||
},
|
||||
|
||||
files_meta: true,
|
||||
async files({ folder }) {
|
||||
if (!folder) return [];
|
||||
const dir = path.join(appdir(), folder);
|
||||
getAllApps_meta: true,
|
||||
async getAllApps({}, req) {
|
||||
const dir = path.join(filesdir(), 'apps');
|
||||
if (!(await fs.exists(dir))) return [];
|
||||
const files = await fs.readdir(dir);
|
||||
|
||||
function fileType(ext, type) {
|
||||
return files
|
||||
.filter(name => name.endsWith(ext))
|
||||
.map(name => ({
|
||||
name: name.slice(0, -ext.length),
|
||||
label: path.parse(name.slice(0, -ext.length)).base,
|
||||
type,
|
||||
}));
|
||||
}
|
||||
|
||||
return [
|
||||
...fileType('.command.sql', 'command.sql'),
|
||||
...fileType('.query.sql', 'query.sql'),
|
||||
...fileType('.config.json', 'config.json'),
|
||||
];
|
||||
},
|
||||
|
||||
async emitChangedDbApp(folder) {
|
||||
const used = await this.getUsedAppFolders();
|
||||
if (used.includes(folder)) {
|
||||
socket.emitChanged('used-apps-changed');
|
||||
}
|
||||
},
|
||||
|
||||
refreshFiles_meta: true,
|
||||
async refreshFiles({ folder }) {
|
||||
socket.emitChanged('app-files-changed', { app: folder });
|
||||
},
|
||||
|
||||
refreshFolders_meta: true,
|
||||
async refreshFolders() {
|
||||
socket.emitChanged(`app-folders-changed`);
|
||||
},
|
||||
|
||||
deleteFile_meta: true,
|
||||
async deleteFile({ folder, file, fileType }) {
|
||||
await fs.unlink(path.join(appdir(), folder, `${file}.${fileType}`));
|
||||
socket.emitChanged('app-files-changed', { app: folder });
|
||||
this.emitChangedDbApp(folder);
|
||||
},
|
||||
|
||||
renameFile_meta: true,
|
||||
async renameFile({ folder, file, newFile, fileType }) {
|
||||
await fs.rename(
|
||||
path.join(path.join(appdir(), folder), `${file}.${fileType}`),
|
||||
path.join(path.join(appdir(), folder), `${newFile}.${fileType}`)
|
||||
);
|
||||
socket.emitChanged('app-files-changed', { app: folder });
|
||||
this.emitChangedDbApp(folder);
|
||||
},
|
||||
|
||||
renameFolder_meta: true,
|
||||
async renameFolder({ folder, newFolder }) {
|
||||
const uniqueName = await this.getNewAppFolder({ name: newFolder });
|
||||
await fs.rename(path.join(appdir(), folder), path.join(appdir(), uniqueName));
|
||||
socket.emitChanged(`app-folders-changed`);
|
||||
},
|
||||
|
||||
deleteFolder_meta: true,
|
||||
async deleteFolder({ folder }) {
|
||||
if (!folder) throw new Error('Missing folder parameter');
|
||||
await fs.rmdir(path.join(appdir(), folder), { recursive: true });
|
||||
socket.emitChanged(`app-folders-changed`);
|
||||
socket.emitChanged('app-files-changed', { app: folder });
|
||||
socket.emitChanged('used-apps-changed');
|
||||
},
|
||||
|
||||
async getNewAppFolder({ name }) {
|
||||
if (!(await fs.exists(path.join(appdir(), name)))) return name;
|
||||
let index = 2;
|
||||
while (await fs.exists(path.join(appdir(), `${name}${index}`))) {
|
||||
index += 1;
|
||||
}
|
||||
return `${name}${index}`;
|
||||
},
|
||||
|
||||
getUsedAppFolders_meta: true,
|
||||
async getUsedAppFolders() {
|
||||
const list = await connections.list();
|
||||
const apps = [];
|
||||
|
||||
for (const connection of list) {
|
||||
for (const db of connection.databases || []) {
|
||||
for (const key of _.keys(db || {})) {
|
||||
if (key.startsWith('useApp:') && db[key]) {
|
||||
apps.push(key.substring('useApp:'.length));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _.intersection(_.uniq(apps), await fs.readdir(appdir()));
|
||||
},
|
||||
|
||||
getUsedApps_meta: true,
|
||||
async getUsedApps() {
|
||||
const apps = await this.getUsedAppFolders();
|
||||
const res = [];
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
const filePermissions = await loadFilePermissionsFromRequest(req);
|
||||
|
||||
for (const folder of apps) {
|
||||
res.push(await this.loadApp({ folder }));
|
||||
}
|
||||
return res;
|
||||
},
|
||||
|
||||
// getAppsForDb_meta: true,
|
||||
// async getAppsForDb({ conid, database }) {
|
||||
// const connection = await connections.get({ conid });
|
||||
// if (!connection) return [];
|
||||
// const db = (connection.databases || []).find(x => x.name == database);
|
||||
// const apps = [];
|
||||
// const res = [];
|
||||
// if (db) {
|
||||
// for (const key of _.keys(db || {})) {
|
||||
// if (key.startsWith('useApp:') && db[key]) {
|
||||
// apps.push(key.substring('useApp:'.length));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// for (const folder of apps) {
|
||||
// res.push(await this.loadApp({ folder }));
|
||||
// }
|
||||
// return res;
|
||||
// },
|
||||
|
||||
loadApp_meta: true,
|
||||
async loadApp({ folder }) {
|
||||
const res = {
|
||||
queries: [],
|
||||
commands: [],
|
||||
name: folder,
|
||||
};
|
||||
const dir = path.join(appdir(), folder);
|
||||
if (await fs.exists(dir)) {
|
||||
const files = await fs.readdir(dir);
|
||||
|
||||
async function processType(ext, field) {
|
||||
for (const file of files) {
|
||||
if (file.endsWith(ext)) {
|
||||
res[field].push({
|
||||
name: file.slice(0, -ext.length),
|
||||
sql: await fs.readFile(path.join(dir, file), { encoding: 'utf-8' }),
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const file of await fs.readdir(dir)) {
|
||||
if (!hasPermission(`all-disk-files`, loadedPermissions)) {
|
||||
const role = getFilePermissionRole('apps', file, filePermissions);
|
||||
if (role == 'deny') continue;
|
||||
}
|
||||
const content = await fs.readFile(path.join(dir, file), { encoding: 'utf-8' });
|
||||
const appJson = JSON.parse(content);
|
||||
// const app = {
|
||||
// appid: file,
|
||||
// name: appJson.applicationName,
|
||||
// usageRules: appJson.usageRules || [],
|
||||
// icon: appJson.applicationIcon || 'img app',
|
||||
// color: appJson.applicationColor,
|
||||
// queries: Object.values(appJson.files || {})
|
||||
// .filter(x => x.type == 'query')
|
||||
// .map(x => ({
|
||||
// name: x.label,
|
||||
// sql: x.sql,
|
||||
// })),
|
||||
// commands: Object.values(appJson.files || {})
|
||||
// .filter(x => x.type == 'command')
|
||||
// .map(x => ({
|
||||
// name: x.label,
|
||||
// sql: x.sql,
|
||||
// })),
|
||||
// virtualReferences: appJson.virtualReferences,
|
||||
// dictionaryDescriptions: appJson.dictionaryDescriptions,
|
||||
// };
|
||||
const app = {
|
||||
...appJson,
|
||||
appid: file,
|
||||
};
|
||||
|
||||
await processType('.command.sql', 'commands');
|
||||
await processType('.query.sql', 'queries');
|
||||
res.push(app);
|
||||
}
|
||||
|
||||
try {
|
||||
res.virtualReferences = JSON.parse(
|
||||
await fs.readFile(path.join(dir, 'virtual-references.config.json'), { encoding: 'utf-8' })
|
||||
);
|
||||
} catch (err) {
|
||||
res.virtualReferences = [];
|
||||
}
|
||||
try {
|
||||
res.dictionaryDescriptions = JSON.parse(
|
||||
await fs.readFile(path.join(dir, 'dictionary-descriptions.config.json'), { encoding: 'utf-8' })
|
||||
);
|
||||
} catch (err) {
|
||||
res.dictionaryDescriptions = [];
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
|
||||
async saveConfigFile(appFolder, filename, filterFunc, newItem) {
|
||||
const file = path.join(appdir(), appFolder, filename);
|
||||
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(await fs.readFile(file, { encoding: 'utf-8' }));
|
||||
} catch (err) {
|
||||
json = [];
|
||||
createAppFromDb_meta: true,
|
||||
async createAppFromDb({ appName, server, database }, req) {
|
||||
const appdir = path.join(filesdir(), 'apps');
|
||||
if (!fs.existsSync(appdir)) {
|
||||
await fs.mkdir(appdir);
|
||||
}
|
||||
|
||||
if (filterFunc) {
|
||||
json = json.filter(filterFunc);
|
||||
const appId = _.kebabCase(appName);
|
||||
let suffix = undefined;
|
||||
while (fs.existsSync(path.join(appdir, `${appId}${suffix || ''}`))) {
|
||||
if (!suffix) suffix = 2;
|
||||
else suffix++;
|
||||
}
|
||||
const finalAppId = `${appId}${suffix || ''}`;
|
||||
|
||||
json = [...json, newItem];
|
||||
const appJson = {
|
||||
applicationName: appName,
|
||||
usageRules: [
|
||||
{
|
||||
serverHostsList: server,
|
||||
databaseNamesList: database,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await fs.writeFile(file, JSON.stringify(json, undefined, 2));
|
||||
await fs.writeFile(path.join(appdir, `${finalAppId}`), JSON.stringify(appJson, undefined, 2));
|
||||
|
||||
socket.emitChanged('app-files-changed', { app: appFolder });
|
||||
socket.emitChanged('used-apps-changed');
|
||||
socket.emitChanged(`files-changed`, { folder: 'apps' });
|
||||
|
||||
return finalAppId;
|
||||
},
|
||||
|
||||
saveVirtualReference_meta: true,
|
||||
async saveVirtualReference({ appFolder, schemaName, pureName, refSchemaName, refTableName, columns }) {
|
||||
await this.saveConfigFile(
|
||||
appFolder,
|
||||
'virtual-references.config.json',
|
||||
async saveVirtualReference({ appid, schemaName, pureName, refSchemaName, refTableName, columns }) {
|
||||
await this.saveConfigItem(
|
||||
appid,
|
||||
'virtualReferences',
|
||||
columns.length == 1
|
||||
? x =>
|
||||
!(
|
||||
@@ -245,14 +111,17 @@ module.exports = {
|
||||
columns,
|
||||
}
|
||||
);
|
||||
|
||||
socket.emitChanged(`files-changed`, { folder: 'apps' });
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
saveDictionaryDescription_meta: true,
|
||||
async saveDictionaryDescription({ appFolder, pureName, schemaName, expression, columns, delimiter }) {
|
||||
await this.saveConfigFile(
|
||||
appFolder,
|
||||
'dictionary-descriptions.config.json',
|
||||
async saveDictionaryDescription({ appid, pureName, schemaName, expression, columns, delimiter }) {
|
||||
await this.saveConfigItem(
|
||||
appid,
|
||||
'dictionaryDescriptions',
|
||||
x => !(x.schemaName == schemaName && x.pureName == pureName),
|
||||
{
|
||||
schemaName,
|
||||
@@ -263,18 +132,271 @@ module.exports = {
|
||||
}
|
||||
);
|
||||
|
||||
socket.emitChanged(`files-changed`, { folder: 'apps' });
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
createConfigFile_meta: true,
|
||||
async createConfigFile({ appFolder, fileName, content }) {
|
||||
const file = path.join(appdir(), appFolder, fileName);
|
||||
if (!(await fs.exists(file))) {
|
||||
await fs.writeFile(file, JSON.stringify(content, undefined, 2));
|
||||
socket.emitChanged('app-files-changed', { app: appFolder });
|
||||
socket.emitChanged('used-apps-changed');
|
||||
return true;
|
||||
async saveConfigItem(appid, fieldName, filterFunc, newItem) {
|
||||
const file = path.join(filesdir(), 'apps', appid);
|
||||
|
||||
const appJson = JSON.parse(await fs.readFile(file, { encoding: 'utf-8' }));
|
||||
let json = appJson[fieldName] || [];
|
||||
|
||||
if (filterFunc) {
|
||||
json = json.filter(filterFunc);
|
||||
}
|
||||
return false;
|
||||
|
||||
json = [...json, newItem];
|
||||
|
||||
await fs.writeFile(
|
||||
file,
|
||||
JSON.stringify(
|
||||
{
|
||||
...appJson,
|
||||
[fieldName]: json,
|
||||
},
|
||||
undefined,
|
||||
2
|
||||
)
|
||||
);
|
||||
|
||||
socket.emitChanged('files-changed', { folder: 'apps' });
|
||||
},
|
||||
|
||||
// folders_meta: true,
|
||||
// async folders() {
|
||||
// const folders = await fs.readdir(appdir());
|
||||
// return [
|
||||
// ...folders.map(name => ({
|
||||
// name,
|
||||
// })),
|
||||
// ];
|
||||
// },
|
||||
|
||||
// createFolder_meta: true,
|
||||
// async createFolder({ folder }) {
|
||||
// const name = await this.getNewAppFolder({ name: folder });
|
||||
// await fs.mkdir(path.join(appdir(), name));
|
||||
// socket.emitChanged('app-folders-changed');
|
||||
// this.emitChangedDbApp(folder);
|
||||
// return name;
|
||||
// },
|
||||
|
||||
// files_meta: true,
|
||||
// async files({ folder }) {
|
||||
// if (!folder) return [];
|
||||
// const dir = path.join(appdir(), folder);
|
||||
// if (!(await fs.exists(dir))) return [];
|
||||
// const files = await fs.readdir(dir);
|
||||
|
||||
// function fileType(ext, type) {
|
||||
// return files
|
||||
// .filter(name => name.endsWith(ext))
|
||||
// .map(name => ({
|
||||
// name: name.slice(0, -ext.length),
|
||||
// label: path.parse(name.slice(0, -ext.length)).base,
|
||||
// type,
|
||||
// }));
|
||||
// }
|
||||
|
||||
// return [
|
||||
// ...fileType('.command.sql', 'command.sql'),
|
||||
// ...fileType('.query.sql', 'query.sql'),
|
||||
// ...fileType('.config.json', 'config.json'),
|
||||
// ];
|
||||
// },
|
||||
|
||||
// async emitChangedDbApp(folder) {
|
||||
// const used = await this.getUsedAppFolders();
|
||||
// if (used.includes(folder)) {
|
||||
// socket.emitChanged('used-apps-changed');
|
||||
// }
|
||||
// },
|
||||
|
||||
// refreshFiles_meta: true,
|
||||
// async refreshFiles({ folder }) {
|
||||
// socket.emitChanged('app-files-changed', { app: folder });
|
||||
// },
|
||||
|
||||
// refreshFolders_meta: true,
|
||||
// async refreshFolders() {
|
||||
// socket.emitChanged(`app-folders-changed`);
|
||||
// },
|
||||
|
||||
// deleteFile_meta: true,
|
||||
// async deleteFile({ folder, file, fileType }) {
|
||||
// await fs.unlink(path.join(appdir(), folder, `${file}.${fileType}`));
|
||||
// socket.emitChanged('app-files-changed', { app: folder });
|
||||
// this.emitChangedDbApp(folder);
|
||||
// },
|
||||
|
||||
// renameFile_meta: true,
|
||||
// async renameFile({ folder, file, newFile, fileType }) {
|
||||
// await fs.rename(
|
||||
// path.join(path.join(appdir(), folder), `${file}.${fileType}`),
|
||||
// path.join(path.join(appdir(), folder), `${newFile}.${fileType}`)
|
||||
// );
|
||||
// socket.emitChanged('app-files-changed', { app: folder });
|
||||
// this.emitChangedDbApp(folder);
|
||||
// },
|
||||
|
||||
// renameFolder_meta: true,
|
||||
// async renameFolder({ folder, newFolder }) {
|
||||
// const uniqueName = await this.getNewAppFolder({ name: newFolder });
|
||||
// await fs.rename(path.join(appdir(), folder), path.join(appdir(), uniqueName));
|
||||
// socket.emitChanged(`app-folders-changed`);
|
||||
// },
|
||||
|
||||
// deleteFolder_meta: true,
|
||||
// async deleteFolder({ folder }) {
|
||||
// if (!folder) throw new Error('Missing folder parameter');
|
||||
// await fs.rmdir(path.join(appdir(), folder), { recursive: true });
|
||||
// socket.emitChanged(`app-folders-changed`);
|
||||
// socket.emitChanged('app-files-changed', { app: folder });
|
||||
// socket.emitChanged('used-apps-changed');
|
||||
// },
|
||||
|
||||
// async getNewAppFolder({ name }) {
|
||||
// if (!(await fs.exists(path.join(appdir(), name)))) return name;
|
||||
// let index = 2;
|
||||
// while (await fs.exists(path.join(appdir(), `${name}${index}`))) {
|
||||
// index += 1;
|
||||
// }
|
||||
// return `${name}${index}`;
|
||||
// },
|
||||
|
||||
// getUsedAppFolders_meta: true,
|
||||
// async getUsedAppFolders() {
|
||||
// const list = await connections.list();
|
||||
// const apps = [];
|
||||
|
||||
// for (const connection of list) {
|
||||
// for (const db of connection.databases || []) {
|
||||
// for (const key of _.keys(db || {})) {
|
||||
// if (key.startsWith('useApp:') && db[key]) {
|
||||
// apps.push(key.substring('useApp:'.length));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// return _.intersection(_.uniq(apps), await fs.readdir(appdir()));
|
||||
// },
|
||||
|
||||
// // getAppsForDb_meta: true,
|
||||
// // async getAppsForDb({ conid, database }) {
|
||||
// // const connection = await connections.get({ conid });
|
||||
// // if (!connection) return [];
|
||||
// // const db = (connection.databases || []).find(x => x.name == database);
|
||||
// // const apps = [];
|
||||
// // const res = [];
|
||||
// // if (db) {
|
||||
// // for (const key of _.keys(db || {})) {
|
||||
// // if (key.startsWith('useApp:') && db[key]) {
|
||||
// // apps.push(key.substring('useApp:'.length));
|
||||
// // }
|
||||
// // }
|
||||
// // }
|
||||
// // for (const folder of apps) {
|
||||
// // res.push(await this.loadApp({ folder }));
|
||||
// // }
|
||||
// // return res;
|
||||
// // },
|
||||
|
||||
// loadApp_meta: true,
|
||||
// async loadApp({ folder }) {
|
||||
// const res = {
|
||||
// queries: [],
|
||||
// commands: [],
|
||||
// name: folder,
|
||||
// };
|
||||
// const dir = path.join(appdir(), folder);
|
||||
// if (await fs.exists(dir)) {
|
||||
// const files = await fs.readdir(dir);
|
||||
|
||||
// async function processType(ext, field) {
|
||||
// for (const file of files) {
|
||||
// if (file.endsWith(ext)) {
|
||||
// res[field].push({
|
||||
// name: file.slice(0, -ext.length),
|
||||
// sql: await fs.readFile(path.join(dir, file), { encoding: 'utf-8' }),
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// await processType('.command.sql', 'commands');
|
||||
// await processType('.query.sql', 'queries');
|
||||
// }
|
||||
|
||||
// try {
|
||||
// res.virtualReferences = JSON.parse(
|
||||
// await fs.readFile(path.join(dir, 'virtual-references.config.json'), { encoding: 'utf-8' })
|
||||
// );
|
||||
// } catch (err) {
|
||||
// res.virtualReferences = [];
|
||||
// }
|
||||
// try {
|
||||
// res.dictionaryDescriptions = JSON.parse(
|
||||
// await fs.readFile(path.join(dir, 'dictionary-descriptions.config.json'), { encoding: 'utf-8' })
|
||||
// );
|
||||
// } catch (err) {
|
||||
// res.dictionaryDescriptions = [];
|
||||
// }
|
||||
|
||||
// return res;
|
||||
// },
|
||||
|
||||
// async saveConfigFile(appFolder, filename, filterFunc, newItem) {
|
||||
// const file = path.join(appdir(), appFolder, filename);
|
||||
|
||||
// let json;
|
||||
// try {
|
||||
// json = JSON.parse(await fs.readFile(file, { encoding: 'utf-8' }));
|
||||
// } catch (err) {
|
||||
// json = [];
|
||||
// }
|
||||
|
||||
// if (filterFunc) {
|
||||
// json = json.filter(filterFunc);
|
||||
// }
|
||||
|
||||
// json = [...json, newItem];
|
||||
|
||||
// await fs.writeFile(file, JSON.stringify(json, undefined, 2));
|
||||
|
||||
// socket.emitChanged('app-files-changed', { app: appFolder });
|
||||
// socket.emitChanged('used-apps-changed');
|
||||
// },
|
||||
|
||||
// saveDictionaryDescription_meta: true,
|
||||
// async saveDictionaryDescription({ appFolder, pureName, schemaName, expression, columns, delimiter }) {
|
||||
// await this.saveConfigFile(
|
||||
// appFolder,
|
||||
// 'dictionary-descriptions.config.json',
|
||||
// x => !(x.schemaName == schemaName && x.pureName == pureName),
|
||||
// {
|
||||
// schemaName,
|
||||
// pureName,
|
||||
// expression,
|
||||
// columns,
|
||||
// delimiter,
|
||||
// }
|
||||
// );
|
||||
|
||||
// return true;
|
||||
// },
|
||||
|
||||
// createConfigFile_meta: true,
|
||||
// async createConfigFile({ appFolder, fileName, content }) {
|
||||
// const file = path.join(appdir(), appFolder, fileName);
|
||||
// if (!(await fs.exists(file))) {
|
||||
// await fs.writeFile(file, JSON.stringify(content, undefined, 2));
|
||||
// socket.emitChanged('app-files-changed', { app: appFolder });
|
||||
// socket.emitChanged('used-apps-changed');
|
||||
// return true;
|
||||
// }
|
||||
// return false;
|
||||
// },
|
||||
};
|
||||
|
||||
@@ -51,6 +51,7 @@ function authMiddleware(req, res, next) {
|
||||
'/auth/oauth-token',
|
||||
'/auth/login',
|
||||
'/auth/redirect',
|
||||
'/redirect',
|
||||
'/stream',
|
||||
'/storage/get-connections-for-login-page',
|
||||
'/storage/set-admin-password',
|
||||
@@ -139,9 +140,9 @@ module.exports = {
|
||||
const accessToken = jwt.sign(
|
||||
{
|
||||
login: 'superadmin',
|
||||
permissions: await storage.loadSuperadminPermissions(),
|
||||
roleId: -3,
|
||||
licenseUid,
|
||||
amoid: 'superadmin',
|
||||
},
|
||||
getTokenSecret(),
|
||||
{
|
||||
@@ -173,7 +174,9 @@ module.exports = {
|
||||
getProviders_meta: true,
|
||||
getProviders() {
|
||||
return {
|
||||
providers: getAuthProviders().map(x => x.toJson()),
|
||||
providers: getAuthProviders()
|
||||
.filter(x => !x.skipInList)
|
||||
.map(x => x.toJson()),
|
||||
default: getDefaultAuthProvider()?.amoid,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -8,6 +8,9 @@ const {
|
||||
getCloudContent,
|
||||
putCloudContent,
|
||||
removeCloudCachedConnection,
|
||||
getPromoWidgetData,
|
||||
getPromoWidgetList,
|
||||
getPromoWidgetPreview,
|
||||
} = require('../utility/cloudIntf');
|
||||
const connections = require('./connections');
|
||||
const socket = require('../utility/socket');
|
||||
@@ -32,8 +35,8 @@ module.exports = {
|
||||
},
|
||||
|
||||
refreshPublicFiles_meta: true,
|
||||
async refreshPublicFiles({ isRefresh }) {
|
||||
await refreshPublicFiles(isRefresh);
|
||||
async refreshPublicFiles({ isRefresh }, req) {
|
||||
await refreshPublicFiles(isRefresh, req?.headers?.['x-ui-language']);
|
||||
return {
|
||||
status: 'ok',
|
||||
};
|
||||
@@ -283,6 +286,28 @@ module.exports = {
|
||||
return getAiGatewayServer();
|
||||
},
|
||||
|
||||
premiumPromoWidget_meta: true,
|
||||
async premiumPromoWidget() {
|
||||
const data = await getPromoWidgetData();
|
||||
if (data?.state != 'data') {
|
||||
return null;
|
||||
}
|
||||
if (data.validTo && new Date().getTime() > new Date(data.validTo).getTime()) {
|
||||
return null;
|
||||
}
|
||||
return data;
|
||||
},
|
||||
|
||||
promoWidgetList_meta: true,
|
||||
async promoWidgetList() {
|
||||
return getPromoWidgetList();
|
||||
},
|
||||
|
||||
promoWidgetPreview_meta: true,
|
||||
async promoWidgetPreview({ campaign, variant }) {
|
||||
return getPromoWidgetPreview(campaign, variant);
|
||||
},
|
||||
|
||||
// chatStream_meta: {
|
||||
// raw: true,
|
||||
// method: 'post',
|
||||
|
||||
@@ -3,7 +3,7 @@ const os = require('os');
|
||||
const path = require('path');
|
||||
const axios = require('axios');
|
||||
const { datadir, getLogsFilePath } = require('../utility/directories');
|
||||
const { hasPermission } = require('../utility/hasPermission');
|
||||
const { hasPermission, loadPermissionsFromRequest } = require('../utility/hasPermission');
|
||||
const socket = require('../utility/socket');
|
||||
const _ = require('lodash');
|
||||
const AsyncLock = require('async-lock');
|
||||
@@ -46,7 +46,7 @@ module.exports = {
|
||||
async get(_params, req) {
|
||||
const authProvider = getAuthProviderFromReq(req);
|
||||
const login = authProvider.getCurrentLogin(req);
|
||||
const permissions = authProvider.getCurrentPermissions(req);
|
||||
const permissions = await authProvider.getCurrentPermissions(req);
|
||||
const isUserLoggedIn = authProvider.isUserLoggedIn(req);
|
||||
|
||||
const singleConid = authProvider.getSingleConnectionId(req);
|
||||
@@ -71,6 +71,7 @@ module.exports = {
|
||||
const isLicenseValid = checkedLicense?.status == 'ok';
|
||||
const logoutUrl = storageConnectionError ? null : await authProvider.getLogoutUrl();
|
||||
const adminConfig = storageConnectionError ? null : await storage.readConfig({ group: 'admin' });
|
||||
const settingsConfig = storageConnectionError ? null : await storage.readConfig({ group: 'settings' });
|
||||
|
||||
storage.startRefreshLicense();
|
||||
|
||||
@@ -121,6 +122,7 @@ module.exports = {
|
||||
allowPrivateCloud: platformInfo.isElectron || !!process.env.ALLOW_DBGATE_PRIVATE_CLOUD,
|
||||
...currentVersion,
|
||||
redirectToDbGateCloudLogin: !!process.env.REDIRECT_TO_DBGATE_CLOUD_LOGIN,
|
||||
preferrendLanguage: settingsConfig?.['storage.language'] || process.env.LANGUAGE || null,
|
||||
};
|
||||
|
||||
return configResult;
|
||||
@@ -280,22 +282,18 @@ module.exports = {
|
||||
|
||||
updateSettings_meta: true,
|
||||
async updateSettings(values, req) {
|
||||
if (!hasPermission(`settings/change`, req)) return false;
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!hasPermission(`settings/change`, loadedPermissions)) return false;
|
||||
cachedSettingsValue = null;
|
||||
|
||||
const res = await lock.acquire('settings', async () => {
|
||||
const currentValue = await this.loadSettings();
|
||||
try {
|
||||
let updated = currentValue;
|
||||
let updated = {
|
||||
...currentValue,
|
||||
...values,
|
||||
};
|
||||
if (process.env.STORAGE_DATABASE) {
|
||||
updated = {
|
||||
...currentValue,
|
||||
..._.mapValues(values, v => {
|
||||
if (v === true) return 'true';
|
||||
if (v === false) return 'false';
|
||||
return v;
|
||||
}),
|
||||
};
|
||||
await storage.writeConfig({
|
||||
group: 'settings',
|
||||
config: updated,
|
||||
@@ -392,7 +390,8 @@ module.exports = {
|
||||
|
||||
exportConnectionsAndSettings_meta: true,
|
||||
async exportConnectionsAndSettings(_params, req) {
|
||||
if (!hasPermission(`admin/config`, req)) {
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!hasPermission(`admin/config`, loadedPermissions)) {
|
||||
throw new Error('Permission denied: admin/config');
|
||||
}
|
||||
|
||||
@@ -416,7 +415,8 @@ module.exports = {
|
||||
|
||||
importConnectionsAndSettings_meta: true,
|
||||
async importConnectionsAndSettings({ db }, req) {
|
||||
if (!hasPermission(`admin/config`, req)) {
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!hasPermission(`admin/config`, loadedPermissions)) {
|
||||
throw new Error('Permission denied: admin/config');
|
||||
}
|
||||
|
||||
|
||||
@@ -14,11 +14,16 @@ const JsonLinesDatabase = require('../utility/JsonLinesDatabase');
|
||||
const processArgs = require('../utility/processArgs');
|
||||
const { safeJsonParse, getLogger, extractErrorLogData } = require('dbgate-tools');
|
||||
const platformInfo = require('../utility/platformInfo');
|
||||
const { connectionHasPermission, testConnectionPermission } = require('../utility/hasPermission');
|
||||
const {
|
||||
connectionHasPermission,
|
||||
testConnectionPermission,
|
||||
loadPermissionsFromRequest,
|
||||
} = require('../utility/hasPermission');
|
||||
const pipeForkLogs = require('../utility/pipeForkLogs');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const { getAuthProviderById } = require('../auth/authProvider');
|
||||
const { startTokenChecking } = require('../utility/authProxy');
|
||||
const { extractConnectionsFromEnv } = require('../utility/envtools');
|
||||
|
||||
const logger = getLogger('connections');
|
||||
|
||||
@@ -57,55 +62,7 @@ function getDatabaseFileLabel(databaseFile) {
|
||||
|
||||
function getPortalCollections() {
|
||||
if (process.env.CONNECTIONS) {
|
||||
const connections = _.compact(process.env.CONNECTIONS.split(',')).map(id => ({
|
||||
_id: id,
|
||||
engine: process.env[`ENGINE_${id}`],
|
||||
server: process.env[`SERVER_${id}`],
|
||||
user: process.env[`USER_${id}`],
|
||||
password: process.env[`PASSWORD_${id}`],
|
||||
passwordMode: process.env[`PASSWORD_MODE_${id}`],
|
||||
port: process.env[`PORT_${id}`],
|
||||
databaseUrl: process.env[`URL_${id}`],
|
||||
useDatabaseUrl: !!process.env[`URL_${id}`],
|
||||
databaseFile: process.env[`FILE_${id}`]?.replace(
|
||||
'%%E2E_TEST_DATA_DIRECTORY%%',
|
||||
path.join(path.dirname(path.dirname(__dirname)), 'e2e-tests', 'tmpdata')
|
||||
),
|
||||
socketPath: process.env[`SOCKET_PATH_${id}`],
|
||||
serviceName: process.env[`SERVICE_NAME_${id}`],
|
||||
authType: process.env[`AUTH_TYPE_${id}`] || (process.env[`SOCKET_PATH_${id}`] ? 'socket' : undefined),
|
||||
defaultDatabase:
|
||||
process.env[`DATABASE_${id}`] ||
|
||||
(process.env[`FILE_${id}`] ? getDatabaseFileLabel(process.env[`FILE_${id}`]) : null),
|
||||
singleDatabase: !!process.env[`DATABASE_${id}`] || !!process.env[`FILE_${id}`],
|
||||
displayName: process.env[`LABEL_${id}`],
|
||||
isReadOnly: process.env[`READONLY_${id}`],
|
||||
databases: process.env[`DBCONFIG_${id}`] ? safeJsonParse(process.env[`DBCONFIG_${id}`]) : null,
|
||||
allowedDatabases: process.env[`ALLOWED_DATABASES_${id}`]?.replace(/\|/g, '\n'),
|
||||
allowedDatabasesRegex: process.env[`ALLOWED_DATABASES_REGEX_${id}`],
|
||||
parent: process.env[`PARENT_${id}`] || undefined,
|
||||
useSeparateSchemas: !!process.env[`USE_SEPARATE_SCHEMAS_${id}`],
|
||||
localDataCenter: process.env[`LOCAL_DATA_CENTER_${id}`],
|
||||
|
||||
// SSH tunnel
|
||||
useSshTunnel: process.env[`USE_SSH_${id}`],
|
||||
sshHost: process.env[`SSH_HOST_${id}`],
|
||||
sshPort: process.env[`SSH_PORT_${id}`],
|
||||
sshMode: process.env[`SSH_MODE_${id}`],
|
||||
sshLogin: process.env[`SSH_LOGIN_${id}`],
|
||||
sshPassword: process.env[`SSH_PASSWORD_${id}`],
|
||||
sshKeyfile: process.env[`SSH_KEY_FILE_${id}`],
|
||||
sshKeyfilePassword: process.env[`SSH_KEY_FILE_PASSWORD_${id}`],
|
||||
|
||||
// SSL
|
||||
useSsl: process.env[`USE_SSL_${id}`],
|
||||
sslCaFile: process.env[`SSL_CA_FILE_${id}`],
|
||||
sslCertFile: process.env[`SSL_CERT_FILE_${id}`],
|
||||
sslCertFilePassword: process.env[`SSL_CERT_FILE_PASSWORD_${id}`],
|
||||
sslKeyFile: process.env[`SSL_KEY_FILE_${id}`],
|
||||
sslRejectUnauthorized: process.env[`SSL_REJECT_UNAUTHORIZED_${id}`],
|
||||
trustServerCertificate: process.env[`SSL_TRUST_CERTIFICATE_${id}`],
|
||||
}));
|
||||
const connections = extractConnectionsFromEnv(process.env);
|
||||
|
||||
for (const conn of connections) {
|
||||
for (const prop in process.env) {
|
||||
@@ -116,7 +73,10 @@ function getPortalCollections() {
|
||||
}
|
||||
}
|
||||
|
||||
logger.info({ connections: connections.map(pickSafeConnectionInfo) }, 'DBGM-00005 Using connections from ENV variables');
|
||||
logger.info(
|
||||
{ connections: connections.map(pickSafeConnectionInfo) },
|
||||
'DBGM-00005 Using connections from ENV variables'
|
||||
);
|
||||
const noengine = connections.filter(x => !x.engine);
|
||||
if (noengine.length > 0) {
|
||||
logger.warn(
|
||||
@@ -222,11 +182,21 @@ module.exports = {
|
||||
);
|
||||
}
|
||||
await this.checkUnsavedConnectionsLimit();
|
||||
|
||||
if (process.env.STORAGE_DATABASE && process.env.CONNECTIONS) {
|
||||
const storage = require('./storage');
|
||||
try {
|
||||
await storage.fillStorageConnectionsFromEnv();
|
||||
} catch (err) {
|
||||
logger.error(extractErrorLogData(err), 'DBGM-00268 Error filling storage connections from env');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
list_meta: true,
|
||||
async list(_params, req) {
|
||||
const storage = require('./storage');
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
|
||||
const storageConnections = await storage.connections(req);
|
||||
if (storageConnections) {
|
||||
@@ -234,9 +204,9 @@ module.exports = {
|
||||
}
|
||||
if (portalConnections) {
|
||||
if (platformInfo.allowShellConnection) return portalConnections;
|
||||
return portalConnections.map(maskConnection).filter(x => connectionHasPermission(x, req));
|
||||
return portalConnections.map(maskConnection).filter(x => connectionHasPermission(x, loadedPermissions));
|
||||
}
|
||||
return (await this.datastore.find()).filter(x => connectionHasPermission(x, req));
|
||||
return (await this.datastore.find()).filter(x => connectionHasPermission(x, loadedPermissions));
|
||||
},
|
||||
|
||||
async getUsedEngines() {
|
||||
@@ -375,7 +345,7 @@ module.exports = {
|
||||
update_meta: true,
|
||||
async update({ _id, values }, req) {
|
||||
if (portalConnections) return;
|
||||
testConnectionPermission(_id, req);
|
||||
await testConnectionPermission(_id, req);
|
||||
const res = await this.datastore.patch(_id, values);
|
||||
socket.emitChanged('connection-list-changed');
|
||||
return res;
|
||||
@@ -392,7 +362,7 @@ module.exports = {
|
||||
updateDatabase_meta: true,
|
||||
async updateDatabase({ conid, database, values }, req) {
|
||||
if (portalConnections) return;
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
const conn = await this.datastore.get(conid);
|
||||
let databases = (conn && conn.databases) || [];
|
||||
if (databases.find(x => x.name == database)) {
|
||||
@@ -410,7 +380,7 @@ module.exports = {
|
||||
delete_meta: true,
|
||||
async delete(connection, req) {
|
||||
if (portalConnections) return;
|
||||
testConnectionPermission(connection, req);
|
||||
await testConnectionPermission(connection, req);
|
||||
const res = await this.datastore.remove(connection._id);
|
||||
socket.emitChanged('connection-list-changed');
|
||||
return res;
|
||||
@@ -452,7 +422,7 @@ module.exports = {
|
||||
_id: '__model',
|
||||
};
|
||||
}
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
return this.getCore({ conid, mask: true });
|
||||
},
|
||||
|
||||
@@ -501,7 +471,11 @@ module.exports = {
|
||||
state,
|
||||
client: 'web',
|
||||
});
|
||||
res.redirect(authResp.url);
|
||||
if (authResp?.url) {
|
||||
res.redirect(authResp.url);
|
||||
return;
|
||||
}
|
||||
res.json({ error: 'No URL returned from auth provider' });
|
||||
},
|
||||
|
||||
dbloginApp_meta: true,
|
||||
|
||||
@@ -29,7 +29,17 @@ const generateDeploySql = require('../shell/generateDeploySql');
|
||||
const { createTwoFilesPatch } = require('diff');
|
||||
const diff2htmlPage = require('../utility/diff2htmlPage');
|
||||
const processArgs = require('../utility/processArgs');
|
||||
const { testConnectionPermission } = require('../utility/hasPermission');
|
||||
const {
|
||||
testConnectionPermission,
|
||||
hasPermission,
|
||||
loadPermissionsFromRequest,
|
||||
loadTablePermissionsFromRequest,
|
||||
getTablePermissionRole,
|
||||
loadDatabasePermissionsFromRequest,
|
||||
getDatabasePermissionRole,
|
||||
getTablePermissionRoleLevelIndex,
|
||||
testDatabaseRolePermission,
|
||||
} = require('../utility/hasPermission');
|
||||
const { MissingCredentialsError } = require('../utility/exceptions');
|
||||
const pipeForkLogs = require('../utility/pipeForkLogs');
|
||||
const crypto = require('crypto');
|
||||
@@ -235,7 +245,7 @@ module.exports = {
|
||||
|
||||
queryData_meta: true,
|
||||
async queryData({ conid, database, sql }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
logger.info({ conid, database, sql }, 'DBGM-00007 Processing query');
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
// if (opened && opened.status && opened.status.name == 'error') {
|
||||
@@ -247,7 +257,7 @@ module.exports = {
|
||||
|
||||
sqlSelect_meta: true,
|
||||
async sqlSelect({ conid, database, select, auditLogSessionGroup }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(
|
||||
opened,
|
||||
@@ -282,7 +292,9 @@ module.exports = {
|
||||
|
||||
runScript_meta: true,
|
||||
async runScript({ conid, database, sql, useTransaction, logMessage }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
await testConnectionPermission(conid, req, loadedPermissions);
|
||||
await testDatabaseRolePermission(conid, database, 'run_script', req);
|
||||
logger.info({ conid, database, sql }, 'DBGM-00008 Processing script');
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
sendToAuditLog(req, {
|
||||
@@ -303,7 +315,7 @@ module.exports = {
|
||||
|
||||
runOperation_meta: true,
|
||||
async runOperation({ conid, database, operation, useTransaction }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
logger.info({ conid, database, operation }, 'DBGM-00009 Processing operation');
|
||||
|
||||
sendToAuditLog(req, {
|
||||
@@ -325,7 +337,7 @@ module.exports = {
|
||||
|
||||
collectionData_meta: true,
|
||||
async collectionData({ conid, database, options, auditLogSessionGroup }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(
|
||||
opened,
|
||||
@@ -356,7 +368,7 @@ module.exports = {
|
||||
},
|
||||
|
||||
async loadDataCore(msgtype, { conid, database, ...args }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(opened, { msgtype, ...args });
|
||||
if (res.errorMessage) {
|
||||
@@ -371,7 +383,7 @@ module.exports = {
|
||||
|
||||
schemaList_meta: true,
|
||||
async schemaList({ conid, database }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
return this.loadDataCore('schemaList', { conid, database });
|
||||
},
|
||||
|
||||
@@ -383,43 +395,43 @@ module.exports = {
|
||||
|
||||
loadKeys_meta: true,
|
||||
async loadKeys({ conid, database, root, filter, limit }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
return this.loadDataCore('loadKeys', { conid, database, root, filter, limit });
|
||||
},
|
||||
|
||||
scanKeys_meta: true,
|
||||
async scanKeys({ conid, database, root, pattern, cursor, count }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
return this.loadDataCore('scanKeys', { conid, database, root, pattern, cursor, count });
|
||||
},
|
||||
|
||||
exportKeys_meta: true,
|
||||
async exportKeys({ conid, database, options }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
return this.loadDataCore('exportKeys', { conid, database, options });
|
||||
},
|
||||
|
||||
loadKeyInfo_meta: true,
|
||||
async loadKeyInfo({ conid, database, key }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
return this.loadDataCore('loadKeyInfo', { conid, database, key });
|
||||
},
|
||||
|
||||
loadKeyTableRange_meta: true,
|
||||
async loadKeyTableRange({ conid, database, key, cursor, count }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
return this.loadDataCore('loadKeyTableRange', { conid, database, key, cursor, count });
|
||||
},
|
||||
|
||||
loadFieldValues_meta: true,
|
||||
async loadFieldValues({ conid, database, schemaName, pureName, field, search, dataType }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
return this.loadDataCore('loadFieldValues', { conid, database, schemaName, pureName, field, search, dataType });
|
||||
},
|
||||
|
||||
callMethod_meta: true,
|
||||
async callMethod({ conid, database, method, args }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
return this.loadDataCore('callMethod', { conid, database, method, args });
|
||||
|
||||
// const opened = await this.ensureOpened(conid, database);
|
||||
@@ -432,7 +444,8 @@ module.exports = {
|
||||
|
||||
updateCollection_meta: true,
|
||||
async updateCollection({ conid, database, changeSet }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(opened, { msgtype: 'updateCollection', changeSet });
|
||||
if (res.errorMessage) {
|
||||
@@ -443,6 +456,44 @@ module.exports = {
|
||||
return res.result || null;
|
||||
},
|
||||
|
||||
saveTableData_meta: true,
|
||||
async saveTableData({ conid, database, changeSet }, req) {
|
||||
await testConnectionPermission(conid, req);
|
||||
|
||||
const databasePermissions = await loadDatabasePermissionsFromRequest(req);
|
||||
const tablePermissions = await loadTablePermissionsFromRequest(req);
|
||||
const fieldsAndRoles = [
|
||||
[changeSet.inserts, 'create_update_delete'],
|
||||
[changeSet.deletes, 'create_update_delete'],
|
||||
[changeSet.updates, 'update_only'],
|
||||
];
|
||||
for (const [operations, requiredRole] of fieldsAndRoles) {
|
||||
for (const operation of operations) {
|
||||
const role = getTablePermissionRole(
|
||||
conid,
|
||||
database,
|
||||
'tables',
|
||||
operation.schemaName,
|
||||
operation.pureName,
|
||||
tablePermissions,
|
||||
databasePermissions
|
||||
);
|
||||
if (getTablePermissionRoleLevelIndex(role) < getTablePermissionRoleLevelIndex(requiredRole)) {
|
||||
throw new Error('DBGM-00262 Permission not granted');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(opened, { msgtype: 'saveTableData', changeSet });
|
||||
if (res.errorMessage) {
|
||||
return {
|
||||
errorMessage: res.errorMessage,
|
||||
};
|
||||
}
|
||||
return res.result || null;
|
||||
},
|
||||
|
||||
status_meta: true,
|
||||
async status({ conid, database }, req) {
|
||||
if (!conid) {
|
||||
@@ -451,7 +502,7 @@ module.exports = {
|
||||
message: 'No connection',
|
||||
};
|
||||
}
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
if (existing) {
|
||||
return {
|
||||
@@ -474,7 +525,7 @@ module.exports = {
|
||||
|
||||
ping_meta: true,
|
||||
async ping({ conid, database }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
let existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
|
||||
if (existing) {
|
||||
@@ -502,7 +553,7 @@ module.exports = {
|
||||
|
||||
refresh_meta: true,
|
||||
async refresh({ conid, database, keepOpen }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
if (!keepOpen) this.close(conid, database);
|
||||
|
||||
await this.ensureOpened(conid, database);
|
||||
@@ -516,7 +567,7 @@ module.exports = {
|
||||
return { status: 'ok' };
|
||||
}
|
||||
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
const conn = await this.ensureOpened(conid, database);
|
||||
conn.subprocess.send({ msgtype: 'syncModel', isFullRefresh });
|
||||
return { status: 'ok' };
|
||||
@@ -553,7 +604,7 @@ module.exports = {
|
||||
|
||||
disconnect_meta: true,
|
||||
async disconnect({ conid, database }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
await this.close(conid, database, true);
|
||||
return { status: 'ok' };
|
||||
},
|
||||
@@ -563,8 +614,9 @@ module.exports = {
|
||||
if (!conid || !database) {
|
||||
return {};
|
||||
}
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req, loadedPermissions);
|
||||
if (conid == '__model') {
|
||||
const model = await importDbModel(database);
|
||||
const trans = await loadModelTransform(modelTransFile);
|
||||
@@ -586,6 +638,46 @@ module.exports = {
|
||||
message: `Loaded database structure for ${database}`,
|
||||
});
|
||||
|
||||
if (process.env.STORAGE_DATABASE && !hasPermission(`all-tables`, loadedPermissions)) {
|
||||
// filter databases by permissions
|
||||
const tablePermissions = await loadTablePermissionsFromRequest(req);
|
||||
const databasePermissions = await loadDatabasePermissionsFromRequest(req);
|
||||
const databasePermissionRole = getDatabasePermissionRole(conid, database, databasePermissions);
|
||||
|
||||
function applyTablePermissionRole(list, objectTypeField) {
|
||||
const res = [];
|
||||
for (const item of list ?? []) {
|
||||
const tablePermissionRole = getTablePermissionRole(
|
||||
conid,
|
||||
database,
|
||||
objectTypeField,
|
||||
item.schemaName,
|
||||
item.pureName,
|
||||
tablePermissions,
|
||||
databasePermissionRole
|
||||
);
|
||||
if (tablePermissionRole != 'deny') {
|
||||
res.push({
|
||||
...item,
|
||||
tablePermissionRole,
|
||||
});
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
const res = {
|
||||
...opened.structure,
|
||||
tables: applyTablePermissionRole(opened.structure.tables, 'tables'),
|
||||
views: applyTablePermissionRole(opened.structure.views, 'views'),
|
||||
procedures: applyTablePermissionRole(opened.structure.procedures, 'procedures'),
|
||||
functions: applyTablePermissionRole(opened.structure.functions, 'functions'),
|
||||
triggers: applyTablePermissionRole(opened.structure.triggers, 'triggers'),
|
||||
collections: applyTablePermissionRole(opened.structure.collections, 'collections'),
|
||||
};
|
||||
return res;
|
||||
}
|
||||
|
||||
return opened.structure;
|
||||
// const existing = this.opened.find((x) => x.conid == conid && x.database == database);
|
||||
// if (existing) return existing.status;
|
||||
@@ -600,7 +692,7 @@ module.exports = {
|
||||
if (!conid) {
|
||||
return null;
|
||||
}
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
if (!conid) return null;
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
return opened.serverVersion || null;
|
||||
@@ -608,7 +700,7 @@ module.exports = {
|
||||
|
||||
sqlPreview_meta: true,
|
||||
async sqlPreview({ conid, database, objects, options }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
// wait for structure
|
||||
await this.structure({ conid, database });
|
||||
|
||||
@@ -619,7 +711,7 @@ module.exports = {
|
||||
|
||||
exportModel_meta: true,
|
||||
async exportModel({ conid, database, outputFolder, schema }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
|
||||
const realFolder = outputFolder.startsWith('archive:')
|
||||
? resolveArchiveFolder(outputFolder.substring('archive:'.length))
|
||||
@@ -637,7 +729,7 @@ module.exports = {
|
||||
|
||||
exportModelSql_meta: true,
|
||||
async exportModelSql({ conid, database, outputFolder, outputFile, schema }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
|
||||
const connection = await connections.getCore({ conid });
|
||||
const driver = requireEngineDriver(connection);
|
||||
@@ -651,7 +743,7 @@ module.exports = {
|
||||
|
||||
generateDeploySql_meta: true,
|
||||
async generateDeploySql({ conid, database, archiveFolder }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(opened, {
|
||||
msgtype: 'generateDeploySql',
|
||||
@@ -923,9 +1015,12 @@ module.exports = {
|
||||
|
||||
executeSessionQuery_meta: true,
|
||||
async executeSessionQuery({ sesid, conid, database, sql }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
logger.info({ sesid, sql }, 'DBGM-00010 Processing query');
|
||||
sessions.dispatchMessage(sesid, 'Query execution started');
|
||||
sessions.dispatchMessage(sesid, {
|
||||
message: 'Query execution started',
|
||||
sql,
|
||||
});
|
||||
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
opened.subprocess.send({ msgtype: 'executeSessionQuery', sql, sesid });
|
||||
@@ -935,7 +1030,7 @@ module.exports = {
|
||||
|
||||
evalJsonScript_meta: true,
|
||||
async evalJsonScript({ conid, database, script, runid }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
|
||||
opened.subprocess.send({ msgtype: 'evalJsonScript', script, runid });
|
||||
|
||||
@@ -3,7 +3,12 @@ const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const { filesdir, archivedir, resolveArchiveFolder, uploadsdir, appdir, jsldir } = require('../utility/directories');
|
||||
const getChartExport = require('../utility/getChartExport');
|
||||
const { hasPermission } = require('../utility/hasPermission');
|
||||
const {
|
||||
hasPermission,
|
||||
loadPermissionsFromRequest,
|
||||
loadFilePermissionsFromRequest,
|
||||
getFilePermissionRole,
|
||||
} = require('../utility/hasPermission');
|
||||
const socket = require('../utility/socket');
|
||||
const scheduler = require('./scheduler');
|
||||
const getDiagramExport = require('../utility/getDiagramExport');
|
||||
@@ -31,7 +36,8 @@ function deserialize(format, text) {
|
||||
module.exports = {
|
||||
list_meta: true,
|
||||
async list({ folder }, req) {
|
||||
if (!hasPermission(`files/${folder}/read`, req)) return [];
|
||||
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 }));
|
||||
@@ -40,10 +46,11 @@ module.exports = {
|
||||
|
||||
listAll_meta: true,
|
||||
async listAll(_params, req) {
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
const folders = await fs.readdir(filesdir());
|
||||
const res = [];
|
||||
for (const folder of folders) {
|
||||
if (!hasPermission(`files/${folder}/read`, req)) continue;
|
||||
if (!hasPermission(`files/${folder}/read`, loadedPermissions)) continue;
|
||||
const dir = path.join(filesdir(), folder);
|
||||
const files = (await fs.readdir(dir)).map(file => ({ folder, file }));
|
||||
res.push(...files);
|
||||
@@ -53,7 +60,8 @@ module.exports = {
|
||||
|
||||
delete_meta: true,
|
||||
async delete({ folder, file }, req) {
|
||||
if (!hasPermission(`files/${folder}/write`, req)) return false;
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!hasPermission(`files/${folder}/write`, loadedPermissions)) return false;
|
||||
if (!checkSecureFilePathsWithoutDirectory(folder, file)) {
|
||||
return false;
|
||||
}
|
||||
@@ -65,7 +73,8 @@ module.exports = {
|
||||
|
||||
rename_meta: true,
|
||||
async rename({ folder, file, newFile }, req) {
|
||||
if (!hasPermission(`files/${folder}/write`, req)) return false;
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!hasPermission(`files/${folder}/write`, loadedPermissions)) return false;
|
||||
if (!checkSecureFilePathsWithoutDirectory(folder, file, newFile)) {
|
||||
return false;
|
||||
}
|
||||
@@ -86,10 +95,11 @@ module.exports = {
|
||||
|
||||
copy_meta: true,
|
||||
async copy({ folder, file, newFile }, req) {
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!checkSecureFilePathsWithoutDirectory(folder, file, newFile)) {
|
||||
return false;
|
||||
}
|
||||
if (!hasPermission(`files/${folder}/write`, req)) return false;
|
||||
if (!hasPermission(`files/${folder}/write`, loadedPermissions)) return false;
|
||||
await fs.copyFile(path.join(filesdir(), folder, file), path.join(filesdir(), folder, newFile));
|
||||
socket.emitChanged(`files-changed`, { folder });
|
||||
socket.emitChanged(`all-files-changed`);
|
||||
@@ -113,7 +123,8 @@ module.exports = {
|
||||
});
|
||||
return deserialize(format, text);
|
||||
} else {
|
||||
if (!hasPermission(`files/${folder}/read`, req)) return null;
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!hasPermission(`files/${folder}/read`, loadedPermissions)) return null;
|
||||
const text = await fs.readFile(path.join(filesdir(), folder, file), { encoding: 'utf-8' });
|
||||
return deserialize(format, text);
|
||||
}
|
||||
@@ -131,18 +142,19 @@ module.exports = {
|
||||
|
||||
save_meta: true,
|
||||
async save({ folder, file, data, format }, req) {
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!checkSecureFilePathsWithoutDirectory(folder, file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (folder.startsWith('archive:')) {
|
||||
if (!hasPermission(`archive/write`, req)) return false;
|
||||
if (!hasPermission(`archive/write`, loadedPermissions)) return false;
|
||||
const dir = resolveArchiveFolder(folder.substring('archive:'.length));
|
||||
await fs.writeFile(path.join(dir, file), serialize(format, data));
|
||||
socket.emitChanged(`archive-files-changed`, { folder: folder.substring('archive:'.length) });
|
||||
return true;
|
||||
} else if (folder.startsWith('app:')) {
|
||||
if (!hasPermission(`apps/write`, req)) return false;
|
||||
if (!hasPermission(`apps/write`, loadedPermissions)) return false;
|
||||
const app = folder.substring('app:'.length);
|
||||
await fs.writeFile(path.join(appdir(), app, file), serialize(format, data));
|
||||
socket.emitChanged(`app-files-changed`, { app });
|
||||
@@ -150,7 +162,7 @@ module.exports = {
|
||||
apps.emitChangedDbApp(folder);
|
||||
return true;
|
||||
} else {
|
||||
if (!hasPermission(`files/${folder}/write`, req)) return false;
|
||||
if (!hasPermission(`files/${folder}/write`, loadedPermissions)) return false;
|
||||
const dir = path.join(filesdir(), folder);
|
||||
if (!(await fs.exists(dir))) {
|
||||
await fs.mkdir(dir);
|
||||
@@ -177,7 +189,8 @@ module.exports = {
|
||||
|
||||
favorites_meta: true,
|
||||
async favorites(_params, req) {
|
||||
if (!hasPermission(`files/favorites/read`, req)) return [];
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!hasPermission(`files/favorites/read`, loadedPermissions)) return [];
|
||||
const dir = path.join(filesdir(), 'favorites');
|
||||
if (!(await fs.exists(dir))) return [];
|
||||
const files = await fs.readdir(dir);
|
||||
@@ -234,16 +247,17 @@ module.exports = {
|
||||
|
||||
getFileRealPath_meta: true,
|
||||
async getFileRealPath({ folder, file }, req) {
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (folder.startsWith('archive:')) {
|
||||
if (!hasPermission(`archive/write`, req)) return false;
|
||||
if (!hasPermission(`archive/write`, loadedPermissions)) return false;
|
||||
const dir = resolveArchiveFolder(folder.substring('archive:'.length));
|
||||
return path.join(dir, file);
|
||||
} else if (folder.startsWith('app:')) {
|
||||
if (!hasPermission(`apps/write`, req)) return false;
|
||||
if (!hasPermission(`apps/write`, loadedPermissions)) return false;
|
||||
const app = folder.substring('app:'.length);
|
||||
return path.join(appdir(), app, file);
|
||||
} else {
|
||||
if (!hasPermission(`files/${folder}/write`, req)) return false;
|
||||
if (!hasPermission(`files/${folder}/write`, loadedPermissions)) return false;
|
||||
const dir = path.join(filesdir(), folder);
|
||||
if (!(await fs.exists(dir))) {
|
||||
await fs.mkdir(dir);
|
||||
@@ -297,7 +311,8 @@ module.exports = {
|
||||
|
||||
exportFile_meta: true,
|
||||
async exportFile({ folder, file, filePath }, req) {
|
||||
if (!hasPermission(`files/${folder}/read`, req)) return false;
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!hasPermission(`files/${folder}/read`, loadedPermissions)) return false;
|
||||
await fs.copyFile(path.join(filesdir(), folder, file), filePath);
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ const socket = require('../utility/socket');
|
||||
const compareVersions = require('compare-versions');
|
||||
const requirePlugin = require('../shell/requirePlugin');
|
||||
const downloadPackage = require('../utility/downloadPackage');
|
||||
const { hasPermission } = require('../utility/hasPermission');
|
||||
const { hasPermission, loadPermissionsFromRequest } = require('../utility/hasPermission');
|
||||
const _ = require('lodash');
|
||||
const packagedPluginsContent = require('../packagedPluginsContent');
|
||||
|
||||
@@ -118,7 +118,8 @@ module.exports = {
|
||||
|
||||
install_meta: true,
|
||||
async install({ packageName }, req) {
|
||||
if (!hasPermission(`plugins/install`, req)) return;
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!hasPermission(`plugins/install`, loadedPermissions)) return;
|
||||
const dir = path.join(pluginsdir(), packageName);
|
||||
// @ts-ignore
|
||||
if (!(await fs.exists(dir))) {
|
||||
@@ -132,7 +133,8 @@ module.exports = {
|
||||
|
||||
uninstall_meta: true,
|
||||
async uninstall({ packageName }, req) {
|
||||
if (!hasPermission(`plugins/install`, req)) return;
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!hasPermission(`plugins/install`, loadedPermissions)) return;
|
||||
const dir = path.join(pluginsdir(), packageName);
|
||||
await fs.rmdir(dir, { recursive: true });
|
||||
socket.emitChanged(`installed-plugins-changed`);
|
||||
@@ -143,7 +145,8 @@ module.exports = {
|
||||
|
||||
upgrade_meta: true,
|
||||
async upgrade({ packageName }, req) {
|
||||
if (!hasPermission(`plugins/install`, req)) return;
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!hasPermission(`plugins/install`, loadedPermissions)) return;
|
||||
const dir = path.join(pluginsdir(), packageName);
|
||||
// @ts-ignore
|
||||
if (await fs.exists(dir)) {
|
||||
|
||||
@@ -21,6 +21,7 @@ const processArgs = require('../utility/processArgs');
|
||||
const platformInfo = require('../utility/platformInfo');
|
||||
const { checkSecureDirectories, checkSecureDirectoriesInScript } = require('../utility/security');
|
||||
const { sendToAuditLog, logJsonRunnerScript } = require('../utility/auditlog');
|
||||
const { testStandardPermission } = require('../utility/hasPermission');
|
||||
const logger = getLogger('runners');
|
||||
|
||||
function extractPlugins(script) {
|
||||
@@ -288,6 +289,8 @@ module.exports = {
|
||||
return this.startCore(runid, scriptTemplate(js, false));
|
||||
}
|
||||
|
||||
await testStandardPermission('run-shell-script', req);
|
||||
|
||||
if (!platformInfo.allowShellScripting) {
|
||||
sendToAuditLog(req, {
|
||||
category: 'shell',
|
||||
|
||||
@@ -3,7 +3,7 @@ const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const cron = require('node-cron');
|
||||
const runners = require('./runners');
|
||||
const { hasPermission } = require('../utility/hasPermission');
|
||||
const { hasPermission, loadPermissionsFromRequest } = require('../utility/hasPermission');
|
||||
const { getLogger } = require('dbgate-tools');
|
||||
|
||||
const logger = getLogger('scheduler');
|
||||
@@ -30,7 +30,8 @@ module.exports = {
|
||||
},
|
||||
|
||||
async reload(_params, req) {
|
||||
if (!hasPermission('files/shell/read', req)) return;
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!hasPermission('files/shell/read', loadedPermissions)) return;
|
||||
const shellDir = path.join(filesdir(), 'shell');
|
||||
await this.unload();
|
||||
if (!(await fs.exists(shellDir))) return;
|
||||
|
||||
@@ -8,7 +8,13 @@ const { handleProcessCommunication } = require('../utility/processComm');
|
||||
const lock = new AsyncLock();
|
||||
const config = require('./config');
|
||||
const processArgs = require('../utility/processArgs');
|
||||
const { testConnectionPermission } = require('../utility/hasPermission');
|
||||
const {
|
||||
testConnectionPermission,
|
||||
loadPermissionsFromRequest,
|
||||
hasPermission,
|
||||
loadDatabasePermissionsFromRequest,
|
||||
getDatabasePermissionRole,
|
||||
} = require('../utility/hasPermission');
|
||||
const { MissingCredentialsError } = require('../utility/exceptions');
|
||||
const pipeForkLogs = require('../utility/pipeForkLogs');
|
||||
const { getLogger, extractErrorLogData } = require('dbgate-tools');
|
||||
@@ -135,7 +141,7 @@ module.exports = {
|
||||
|
||||
disconnect_meta: true,
|
||||
async disconnect({ conid }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
await this.close(conid, true);
|
||||
return { status: 'ok' };
|
||||
},
|
||||
@@ -144,7 +150,9 @@ module.exports = {
|
||||
async listDatabases({ conid }, req) {
|
||||
if (!conid) return [];
|
||||
if (conid == '__model') return [];
|
||||
testConnectionPermission(conid, req);
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
|
||||
await testConnectionPermission(conid, req, loadedPermissions);
|
||||
const opened = await this.ensureOpened(conid);
|
||||
sendToAuditLog(req, {
|
||||
category: 'serverop',
|
||||
@@ -157,12 +165,29 @@ module.exports = {
|
||||
sessionGroup: 'listDatabases',
|
||||
message: `Loaded databases for connection`,
|
||||
});
|
||||
|
||||
if (process.env.STORAGE_DATABASE && !hasPermission(`all-databases`, loadedPermissions)) {
|
||||
// filter databases by permissions
|
||||
const databasePermissions = await loadDatabasePermissionsFromRequest(req);
|
||||
const res = [];
|
||||
for (const db of opened?.databases ?? []) {
|
||||
const databasePermissionRole = getDatabasePermissionRole(db.id, db.name, databasePermissions);
|
||||
if (databasePermissionRole != 'deny') {
|
||||
res.push({
|
||||
...db,
|
||||
databasePermissionRole,
|
||||
});
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
return opened?.databases ?? [];
|
||||
},
|
||||
|
||||
version_meta: true,
|
||||
async version({ conid }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid);
|
||||
return opened?.version ?? null;
|
||||
},
|
||||
@@ -184,11 +209,11 @@ module.exports = {
|
||||
return Promise.resolve();
|
||||
}
|
||||
this.lastPinged[conid] = new Date().getTime();
|
||||
const opened = await this.ensureOpened(conid);
|
||||
if (!opened) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
try {
|
||||
const opened = await this.ensureOpened(conid);
|
||||
if (!opened) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
opened.subprocess.send({ msgtype: 'ping' });
|
||||
} catch (err) {
|
||||
logger.error(extractErrorLogData(err), 'DBGM-00121 Error pinging server connection');
|
||||
@@ -202,7 +227,7 @@ module.exports = {
|
||||
|
||||
refresh_meta: true,
|
||||
async refresh({ conid, keepOpen }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
if (!keepOpen) this.close(conid);
|
||||
|
||||
await this.ensureOpened(conid);
|
||||
@@ -210,7 +235,7 @@ module.exports = {
|
||||
},
|
||||
|
||||
async sendDatabaseOp({ conid, msgtype, name }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid);
|
||||
if (!opened) {
|
||||
return null;
|
||||
@@ -252,7 +277,7 @@ module.exports = {
|
||||
},
|
||||
|
||||
async loadDataCore(msgtype, { conid, ...args }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid);
|
||||
if (!opened) {
|
||||
return null;
|
||||
@@ -270,13 +295,43 @@ module.exports = {
|
||||
|
||||
serverSummary_meta: true,
|
||||
async serverSummary({ conid }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
logger.info({ conid }, 'DBGM-00260 Processing server summary');
|
||||
return this.loadDataCore('serverSummary', { conid });
|
||||
},
|
||||
|
||||
listDatabaseProcesses_meta: true,
|
||||
async listDatabaseProcesses(ctx, req) {
|
||||
const { conid } = ctx;
|
||||
// logger.info({ conid }, 'DBGM-00261 Listing processes of database server');
|
||||
testConnectionPermission(conid, req);
|
||||
|
||||
const opened = await this.ensureOpened(conid);
|
||||
if (!opened) {
|
||||
return null;
|
||||
}
|
||||
if (opened.connection.isReadOnly) return false;
|
||||
|
||||
return this.sendRequest(opened, { msgtype: 'listDatabaseProcesses' });
|
||||
},
|
||||
|
||||
killDatabaseProcess_meta: true,
|
||||
async killDatabaseProcess(ctx, req) {
|
||||
const { conid, pid } = ctx;
|
||||
testConnectionPermission(conid, req);
|
||||
|
||||
const opened = await this.ensureOpened(conid);
|
||||
if (!opened) {
|
||||
return null;
|
||||
}
|
||||
if (opened.connection.isReadOnly) return false;
|
||||
|
||||
return this.sendRequest(opened, { msgtype: 'killDatabaseProcess', pid });
|
||||
},
|
||||
|
||||
summaryCommand_meta: true,
|
||||
async summaryCommand({ conid, command, row }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid);
|
||||
if (!opened) {
|
||||
return null;
|
||||
|
||||
@@ -8,10 +8,13 @@ const path = require('path');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
const processArgs = require('../utility/processArgs');
|
||||
const { appdir } = require('../utility/directories');
|
||||
const { getLogger, extractErrorLogData } = require('dbgate-tools');
|
||||
const { getLogger, extractErrorLogData, removeSqlFrontMatter } = require('dbgate-tools');
|
||||
const pipeForkLogs = require('../utility/pipeForkLogs');
|
||||
const config = require('./config');
|
||||
const { sendToAuditLog } = require('../utility/auditlog');
|
||||
const { testStandardPermission, testDatabaseRolePermission } = require('../utility/hasPermission');
|
||||
const { getStaticTokenSecret } = require('../auth/authCommon');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const logger = getLogger('sessions');
|
||||
|
||||
@@ -80,6 +83,16 @@ module.exports = {
|
||||
socket.emit(`session-recordset-${sesid}`, { jslid, resultIndex });
|
||||
},
|
||||
|
||||
handle_endrecordset(sesid, props) {
|
||||
const { jslid, rowCount, durationMs } = props;
|
||||
this.dispatchMessage(sesid, {
|
||||
message: `Query returned ${rowCount} rows in ${durationMs} ms`,
|
||||
rowCount,
|
||||
durationMs,
|
||||
jslid,
|
||||
});
|
||||
},
|
||||
|
||||
handle_stats(sesid, stats) {
|
||||
jsldata.notifyChangedStats(stats);
|
||||
},
|
||||
@@ -94,6 +107,12 @@ module.exports = {
|
||||
socket.emit(`session-initialize-file-${jslid}`);
|
||||
},
|
||||
|
||||
handle_changedCurrentDatabase(sesid, props) {
|
||||
const { database } = props;
|
||||
this.dispatchMessage(sesid, `Current database changed to ${database}`);
|
||||
socket.emit(`session-changedb-${sesid}`, { database });
|
||||
},
|
||||
|
||||
handle_ping() {},
|
||||
|
||||
create_meta: true,
|
||||
@@ -148,10 +167,23 @@ module.exports = {
|
||||
|
||||
executeQuery_meta: true,
|
||||
async executeQuery({ sesid, sql, autoCommit, autoDetectCharts, limitRows, frontMatter }, req) {
|
||||
let useTokenIsOk = false;
|
||||
if (frontMatter?.useToken) {
|
||||
const decoded = jwt.verify(frontMatter.useToken, getStaticTokenSecret());
|
||||
if (decoded?.['contentHash'] == crypto.createHash('md5').update(removeSqlFrontMatter(sql)).digest('hex')) {
|
||||
useTokenIsOk = true;
|
||||
}
|
||||
}
|
||||
if (!useTokenIsOk) {
|
||||
await testStandardPermission('dbops/query', req);
|
||||
}
|
||||
const session = this.opened.find(x => x.sesid == sesid);
|
||||
if (!session) {
|
||||
throw new Error('Invalid session');
|
||||
}
|
||||
if (!useTokenIsOk) {
|
||||
await testDatabaseRolePermission(session.conid, session.database, 'run_script', req);
|
||||
}
|
||||
|
||||
sendToAuditLog(req, {
|
||||
category: 'dbop',
|
||||
@@ -166,7 +198,10 @@ module.exports = {
|
||||
});
|
||||
|
||||
logger.info({ sesid, sql }, 'DBGM-00019 Processing query');
|
||||
this.dispatchMessage(sesid, 'Query execution started');
|
||||
this.dispatchMessage(sesid, {
|
||||
message: 'Query execution started',
|
||||
sql,
|
||||
});
|
||||
session.subprocess.send({
|
||||
msgtype: 'executeQuery',
|
||||
sql,
|
||||
|
||||
6
packages/api/src/controllers/teamFiles.js
Normal file
6
packages/api/src/controllers/teamFiles.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
list_meta: true,
|
||||
async list(req) {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
@@ -1,19 +1,8 @@
|
||||
const crypto = require('crypto');
|
||||
const path = require('path');
|
||||
const { uploadsdir, getLogsFilePath, filesdir } = require('../utility/directories');
|
||||
const { getLogger, extractErrorLogData } = require('dbgate-tools');
|
||||
const { uploadsdir } = require('../utility/directories');
|
||||
const { getLogger } = require('dbgate-tools');
|
||||
const logger = getLogger('uploads');
|
||||
const axios = require('axios');
|
||||
const os = require('os');
|
||||
const fs = require('fs/promises');
|
||||
const { read } = require('./queryHistory');
|
||||
const platformInfo = require('../utility/platformInfo');
|
||||
const _ = require('lodash');
|
||||
const serverConnections = require('./serverConnections');
|
||||
const config = require('./config');
|
||||
const gistSecret = require('../gistSecret');
|
||||
const currentVersion = require('../currentVersion');
|
||||
const socket = require('../utility/socket');
|
||||
|
||||
module.exports = {
|
||||
upload_meta: {
|
||||
@@ -51,88 +40,70 @@ module.exports = {
|
||||
res.sendFile(path.join(uploadsdir(), req.query.file));
|
||||
},
|
||||
|
||||
async getGistToken() {
|
||||
const settings = await config.getSettings();
|
||||
// uploadErrorToGist_meta: true,
|
||||
// async uploadErrorToGist() {
|
||||
// const logs = await fs.readFile(getLogsFilePath(), { encoding: 'utf-8' });
|
||||
// const connections = await serverConnections.getOpenedConnectionReport();
|
||||
// try {
|
||||
// const response = await axios.default.post(
|
||||
// 'https://api.github.com/gists',
|
||||
// {
|
||||
// description: `DbGate ${currentVersion.version} error report`,
|
||||
// public: false,
|
||||
// files: {
|
||||
// 'logs.jsonl': {
|
||||
// content: logs,
|
||||
// },
|
||||
// 'os.json': {
|
||||
// content: JSON.stringify(
|
||||
// {
|
||||
// release: os.release(),
|
||||
// arch: os.arch(),
|
||||
// machine: os.machine(),
|
||||
// platform: os.platform(),
|
||||
// type: os.type(),
|
||||
// },
|
||||
// null,
|
||||
// 2
|
||||
// ),
|
||||
// },
|
||||
// 'platform.json': {
|
||||
// content: JSON.stringify(
|
||||
// _.omit(
|
||||
// {
|
||||
// ...platformInfo,
|
||||
// },
|
||||
// ['defaultKeyfile', 'sshAuthSock']
|
||||
// ),
|
||||
// null,
|
||||
// 2
|
||||
// ),
|
||||
// },
|
||||
// 'connections.json': {
|
||||
// content: JSON.stringify(connections, null, 2),
|
||||
// },
|
||||
// 'version.json': {
|
||||
// content: JSON.stringify(currentVersion, null, 2),
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// headers: {
|
||||
// Authorization: `token ${await this.getGistToken()}`,
|
||||
// 'Content-Type': 'application/json',
|
||||
// Accept: 'application/vnd.github.v3+json',
|
||||
// },
|
||||
// }
|
||||
// );
|
||||
|
||||
return settings['other.gistCreateToken'] || gistSecret;
|
||||
},
|
||||
// return response.data;
|
||||
// } catch (err) {
|
||||
// logger.error(extractErrorLogData(err), 'DBGM-00148 Error uploading gist');
|
||||
|
||||
uploadErrorToGist_meta: true,
|
||||
async uploadErrorToGist() {
|
||||
const logs = await fs.readFile(getLogsFilePath(), { encoding: 'utf-8' });
|
||||
const connections = await serverConnections.getOpenedConnectionReport();
|
||||
try {
|
||||
const response = await axios.default.post(
|
||||
'https://api.github.com/gists',
|
||||
{
|
||||
description: `DbGate ${currentVersion.version} error report`,
|
||||
public: false,
|
||||
files: {
|
||||
'logs.jsonl': {
|
||||
content: logs,
|
||||
},
|
||||
'os.json': {
|
||||
content: JSON.stringify(
|
||||
{
|
||||
release: os.release(),
|
||||
arch: os.arch(),
|
||||
machine: os.machine(),
|
||||
platform: os.platform(),
|
||||
type: os.type(),
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
'platform.json': {
|
||||
content: JSON.stringify(
|
||||
_.omit(
|
||||
{
|
||||
...platformInfo,
|
||||
},
|
||||
['defaultKeyfile', 'sshAuthSock']
|
||||
),
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
'connections.json': {
|
||||
content: JSON.stringify(connections, null, 2),
|
||||
},
|
||||
'version.json': {
|
||||
content: JSON.stringify(currentVersion, null, 2),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `token ${await this.getGistToken()}`,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
logger.error(extractErrorLogData(err), 'DBGM-00148 Error uploading gist');
|
||||
|
||||
return {
|
||||
apiErrorMessage: err.message,
|
||||
};
|
||||
// console.error('Error creating gist:', error.response ? error.response.data : error.message);
|
||||
}
|
||||
},
|
||||
|
||||
deleteGist_meta: true,
|
||||
async deleteGist({ url }) {
|
||||
const response = await axios.default.delete(url, {
|
||||
headers: {
|
||||
Authorization: `token ${await this.getGistToken()}`,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
},
|
||||
});
|
||||
return true;
|
||||
},
|
||||
// return {
|
||||
// apiErrorMessage: err.message,
|
||||
// };
|
||||
// // console.error('Error creating gist:', error.response ? error.response.data : error.message);
|
||||
// }
|
||||
// },
|
||||
};
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = process.env.GIST_UPLOAD_SECRET;
|
||||
@@ -5,6 +5,7 @@ const moment = require('moment');
|
||||
const path = require('path');
|
||||
const { logsdir, setLogsFilePath, getLogsFilePath } = require('./utility/directories');
|
||||
const currentVersion = require('./currentVersion');
|
||||
const _ = require('lodash');
|
||||
|
||||
const logger = getLogger('apiIndex');
|
||||
|
||||
@@ -68,7 +69,7 @@ function configureLogger() {
|
||||
}
|
||||
const additionals = {};
|
||||
const finalMsg =
|
||||
msg.msg && msg.msg.match(/^DBGM-\d\d\d\d\d/)
|
||||
_.isString(msg.msg) && msg.msg.match(/^DBGM-\d\d\d\d\d/)
|
||||
? {
|
||||
...msg,
|
||||
msg: msg.msg.substring(10).trimStart(),
|
||||
|
||||
@@ -29,6 +29,8 @@ const files = require('./controllers/files');
|
||||
const scheduler = require('./controllers/scheduler');
|
||||
const queryHistory = require('./controllers/queryHistory');
|
||||
const cloud = require('./controllers/cloud');
|
||||
const teamFiles = require('./controllers/teamFiles');
|
||||
|
||||
const onFinished = require('on-finished');
|
||||
const processArgs = require('./utility/processArgs');
|
||||
|
||||
@@ -264,6 +266,7 @@ function useAllControllers(app, electron) {
|
||||
useController(app, electron, '/apps', apps);
|
||||
useController(app, electron, '/auth', auth);
|
||||
useController(app, electron, '/cloud', cloud);
|
||||
useController(app, electron, '/team-files', teamFiles);
|
||||
}
|
||||
|
||||
function setElectronSender(electronSender) {
|
||||
|
||||
@@ -17,13 +17,14 @@ const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const { connectUtility } = require('../utility/connectUtility');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
const generateDeploySql = require('../shell/generateDeploySql');
|
||||
const { dumpSqlSelect } = require('dbgate-sqltree');
|
||||
const { dumpSqlSelect, scriptToSql } = require('dbgate-sqltree');
|
||||
const { allowExecuteCustomScript, handleQueryStream } = require('../utility/handleQueryStream');
|
||||
const dbgateApi = require('../shell');
|
||||
const requirePlugin = require('../shell/requirePlugin');
|
||||
const path = require('path');
|
||||
const { rundir } = require('../utility/directories');
|
||||
const fs = require('fs-extra');
|
||||
const { changeSetToSql } = require('dbgate-datalib');
|
||||
|
||||
const logger = getLogger('dbconnProcess');
|
||||
|
||||
@@ -348,6 +349,25 @@ async function handleUpdateCollection({ msgid, changeSet }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveTableData({ msgid, changeSet }) {
|
||||
await waitStructure();
|
||||
try {
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
const script = driver.createSaveChangeSetScript(changeSet, analysedStructure, () =>
|
||||
changeSetToSql(changeSet, analysedStructure, driver.dialect)
|
||||
);
|
||||
const sql = scriptToSql(driver, script);
|
||||
await driver.script(dbhan, sql, { useTransaction: true });
|
||||
process.send({ msgtype: 'response', msgid });
|
||||
} catch (err) {
|
||||
process.send({
|
||||
msgtype: 'response',
|
||||
msgid,
|
||||
errorMessage: extractErrorMessage(err, 'Error executing SQL script'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSqlPreview({ msgid, objects, options }) {
|
||||
await waitStructure();
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
@@ -464,6 +484,7 @@ const messageHandlers = {
|
||||
runScript: handleRunScript,
|
||||
runOperation: handleRunOperation,
|
||||
updateCollection: handleUpdateCollection,
|
||||
saveTableData: handleSaveTableData,
|
||||
collectionData: handleCollectionData,
|
||||
loadKeys: handleLoadKeys,
|
||||
scanKeys: handleScanKeys,
|
||||
|
||||
@@ -146,6 +146,30 @@ async function handleServerSummary({ msgid }) {
|
||||
return handleDriverDataCore(msgid, driver => driver.serverSummary(dbhan));
|
||||
}
|
||||
|
||||
async function handleKillDatabaseProcess({ msgid, pid }) {
|
||||
await waitConnected();
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
|
||||
try {
|
||||
const result = await driver.killProcess(dbhan, Number(pid));
|
||||
process.send({ msgtype: 'response', msgid, result });
|
||||
} catch (err) {
|
||||
process.send({ msgtype: 'response', msgid, errorMessage: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleListDatabaseProcesses({ msgid }) {
|
||||
await waitConnected();
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
|
||||
try {
|
||||
const result = await driver.listProcesses(dbhan);
|
||||
process.send({ msgtype: 'response', msgid, result });
|
||||
} catch (err) {
|
||||
process.send({ msgtype: 'response', msgid, errorMessage: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSummaryCommand({ msgid, command, row }) {
|
||||
return handleDriverDataCore(msgid, driver => driver.summaryCommand(dbhan, command, row));
|
||||
}
|
||||
@@ -154,6 +178,8 @@ const messageHandlers = {
|
||||
connect: handleConnect,
|
||||
ping: handlePing,
|
||||
serverSummary: handleServerSummary,
|
||||
killDatabaseProcess: handleKillDatabaseProcess,
|
||||
listDatabaseProcesses: handleListDatabaseProcesses,
|
||||
summaryCommand: handleSummaryCommand,
|
||||
createDatabase: props => handleDatabaseOp('createDatabase', props),
|
||||
dropDatabase: props => handleDatabaseOp('dropDatabase', props),
|
||||
|
||||
@@ -65,6 +65,8 @@ async function copyStream(input, output, options) {
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(extractErrorLogData(err, { progressName }), 'DBGM-00157 Import/export job failed');
|
||||
|
||||
process.send({
|
||||
msgtype: 'copyStreamError',
|
||||
copyStreamError: {
|
||||
@@ -82,8 +84,6 @@ async function copyStream(input, output, options) {
|
||||
errorMessage: extractErrorMessage(err),
|
||||
});
|
||||
}
|
||||
|
||||
logger.error(extractErrorLogData(err, { progressName }), 'DBGM-00157 Import/export job failed');
|
||||
// throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ async function dataReplicator({
|
||||
createNew: compileOperationFunction(item.createNew, item.createCondition),
|
||||
updateExisting: compileOperationFunction(item.updateExisting, item.updateCondition),
|
||||
deleteMissing: !!item.deleteMissing,
|
||||
skipUpdateColumns: item.skipUpdateColumns,
|
||||
deleteRestrictionColumns: item.deleteRestrictionColumns ?? [],
|
||||
openStream: item.openStream
|
||||
? item.openStream
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,11 +13,12 @@ const socket = require('./socket');
|
||||
const config = require('../controllers/config');
|
||||
const simpleEncryptor = require('simple-encryptor');
|
||||
const currentVersion = require('../currentVersion');
|
||||
const { getPublicIpInfo } = require('./hardwareFingerprint');
|
||||
|
||||
const logger = getLogger('cloudIntf');
|
||||
|
||||
let cloudFiles = null;
|
||||
let promoWidgetData = null;
|
||||
let promoWidgetDataLoaded = false;
|
||||
|
||||
const DBGATE_IDENTITY_URL = process.env.LOCAL_DBGATE_IDENTITY
|
||||
? 'http://localhost:3103'
|
||||
@@ -192,7 +193,7 @@ async function getCloudSigninHeaders(holder = null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function updateCloudFiles(isRefresh) {
|
||||
async function updateCloudFiles(isRefresh, language) {
|
||||
let lastCloudFilesTags;
|
||||
try {
|
||||
lastCloudFilesTags = await fs.readFile(path.join(datadir(), 'cloud-files-tags.txt'), 'utf-8');
|
||||
@@ -200,8 +201,6 @@ async function updateCloudFiles(isRefresh) {
|
||||
lastCloudFilesTags = '';
|
||||
}
|
||||
|
||||
const ipInfo = await getPublicIpInfo();
|
||||
|
||||
const tags = (await collectCloudFilesSearchTags()).join(',');
|
||||
let lastCheckedTm = 0;
|
||||
if (tags == lastCloudFilesTags && cloudFiles.length > 0) {
|
||||
@@ -213,12 +212,13 @@ async function updateCloudFiles(isRefresh) {
|
||||
const resp = await axios.default.get(
|
||||
`${DBGATE_CLOUD_URL}/public-cloud-updates?lastCheckedTm=${lastCheckedTm}&tags=${tags}&isRefresh=${
|
||||
isRefresh ? 1 : 0
|
||||
}&country=${ipInfo?.country || ''}`,
|
||||
}`,
|
||||
{
|
||||
headers: {
|
||||
...getLicenseHttpHeaders(),
|
||||
...(await getCloudInstanceHeaders()),
|
||||
'x-app-version': currentVersion.version,
|
||||
'x-app-language': language || 'en',
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -262,15 +262,62 @@ async function getPublicFileData(path) {
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
async function refreshPublicFiles(isRefresh) {
|
||||
async function ensurePromoWidgetDataLoaded() {
|
||||
if (promoWidgetDataLoaded) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const fileContent = await fs.readFile(path.join(datadir(), 'promo-widget.json'), 'utf-8');
|
||||
promoWidgetData = JSON.parse(fileContent);
|
||||
} catch (err) {
|
||||
promoWidgetData = null;
|
||||
}
|
||||
promoWidgetDataLoaded = true;
|
||||
}
|
||||
|
||||
async function updatePremiumPromoWidget(language) {
|
||||
await ensurePromoWidgetDataLoaded();
|
||||
|
||||
const tags = (await collectCloudFilesSearchTags()).join(',');
|
||||
|
||||
const resp = await axios.default.get(
|
||||
`${DBGATE_CLOUD_URL}/premium-promo-widget?identifier=${promoWidgetData?.identifier ?? 'empty'}&tags=${tags}`,
|
||||
{
|
||||
headers: {
|
||||
...getLicenseHttpHeaders(),
|
||||
...(await getCloudInstanceHeaders()),
|
||||
'x-app-version': currentVersion.version,
|
||||
'x-app-language': language || 'en',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!resp.data || resp.data?.state == 'unchanged') {
|
||||
return;
|
||||
}
|
||||
|
||||
promoWidgetData = resp.data;
|
||||
await fs.writeFile(path.join(datadir(), 'promo-widget.json'), JSON.stringify(promoWidgetData, null, 2));
|
||||
|
||||
socket.emitChanged(`promo-widget-changed`);
|
||||
}
|
||||
|
||||
async function refreshPublicFiles(isRefresh, uiLanguage) {
|
||||
const language = platformInfo.isElectron
|
||||
? (await config.getCachedSettings())?.['localization.language'] || 'en'
|
||||
: uiLanguage;
|
||||
if (!cloudFiles) {
|
||||
await loadCloudFiles();
|
||||
}
|
||||
try {
|
||||
await updateCloudFiles(isRefresh);
|
||||
await updateCloudFiles(isRefresh, language);
|
||||
} catch (err) {
|
||||
logger.error(extractErrorLogData(err), 'DBGM-00166 Error updating cloud files');
|
||||
}
|
||||
const configSettings = await config.get();
|
||||
if (!isProApp() || configSettings?.trialDaysLeft != null) {
|
||||
await updatePremiumPromoWidget(language);
|
||||
}
|
||||
}
|
||||
|
||||
async function callCloudApiGet(endpoint, signinHolder = null, additionalHeaders = {}) {
|
||||
@@ -423,6 +470,33 @@ function removeCloudCachedConnection(folid, cntid) {
|
||||
delete cloudConnectionCache[cacheKey];
|
||||
}
|
||||
|
||||
async function getPublicIpInfo() {
|
||||
try {
|
||||
const resp = await axios.default.get(`${DBGATE_CLOUD_URL}/ipinfo`);
|
||||
if (!resp.data?.ip) {
|
||||
return { ip: 'unknown-ip' };
|
||||
}
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
return { ip: 'unknown-ip' };
|
||||
}
|
||||
}
|
||||
|
||||
async function getPromoWidgetData() {
|
||||
await ensurePromoWidgetDataLoaded();
|
||||
return promoWidgetData;
|
||||
}
|
||||
|
||||
async function getPromoWidgetPreview(campaign, variant) {
|
||||
const resp = await axios.default.get(`${DBGATE_CLOUD_URL}/premium-promo-widget-preview/${campaign}/${variant}`);
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
async function getPromoWidgetList() {
|
||||
const resp = await axios.default.get(`${DBGATE_CLOUD_URL}/promo-widget-list`);
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createDbGateIdentitySession,
|
||||
startCloudTokenChecking,
|
||||
@@ -439,4 +513,8 @@ module.exports = {
|
||||
removeCloudCachedConnection,
|
||||
readCloudTokenHolder,
|
||||
readCloudTestTokenHolder,
|
||||
getPublicIpInfo,
|
||||
getPromoWidgetData,
|
||||
getPromoWidgetPreview,
|
||||
getPromoWidgetList,
|
||||
};
|
||||
|
||||
445
packages/api/src/utility/envtools.js
Normal file
445
packages/api/src/utility/envtools.js
Normal file
@@ -0,0 +1,445 @@
|
||||
const path = require('path');
|
||||
const _ = require('lodash');
|
||||
const { safeJsonParse, getDatabaseFileLabel } = require('dbgate-tools');
|
||||
const crypto = require('crypto');
|
||||
|
||||
function extractConnectionsFromEnv(env) {
|
||||
if (!env?.CONNECTIONS) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const connections = _.compact(env.CONNECTIONS.split(',')).map(id => ({
|
||||
_id: id,
|
||||
engine: env[`ENGINE_${id}`],
|
||||
server: env[`SERVER_${id}`],
|
||||
user: env[`USER_${id}`],
|
||||
password: env[`PASSWORD_${id}`],
|
||||
passwordMode: env[`PASSWORD_MODE_${id}`],
|
||||
port: env[`PORT_${id}`],
|
||||
databaseUrl: env[`URL_${id}`],
|
||||
useDatabaseUrl: !!env[`URL_${id}`],
|
||||
databaseFile: env[`FILE_${id}`]?.replace(
|
||||
'%%E2E_TEST_DATA_DIRECTORY%%',
|
||||
path.join(path.dirname(path.dirname(__dirname)), 'e2e-tests', 'tmpdata')
|
||||
),
|
||||
socketPath: env[`SOCKET_PATH_${id}`],
|
||||
serviceName: env[`SERVICE_NAME_${id}`],
|
||||
authType: env[`AUTH_TYPE_${id}`] || (env[`SOCKET_PATH_${id}`] ? 'socket' : undefined),
|
||||
defaultDatabase: env[`DATABASE_${id}`] || (env[`FILE_${id}`] ? getDatabaseFileLabel(env[`FILE_${id}`]) : null),
|
||||
singleDatabase: !!env[`DATABASE_${id}`] || !!env[`FILE_${id}`],
|
||||
displayName: env[`LABEL_${id}`],
|
||||
isReadOnly: env[`READONLY_${id}`],
|
||||
databases: env[`DBCONFIG_${id}`] ? safeJsonParse(env[`DBCONFIG_${id}`]) : null,
|
||||
allowedDatabases: env[`ALLOWED_DATABASES_${id}`]?.replace(/\|/g, '\n'),
|
||||
allowedDatabasesRegex: env[`ALLOWED_DATABASES_REGEX_${id}`],
|
||||
parent: env[`PARENT_${id}`] || undefined,
|
||||
useSeparateSchemas: !!env[`USE_SEPARATE_SCHEMAS_${id}`],
|
||||
localDataCenter: env[`LOCAL_DATA_CENTER_${id}`],
|
||||
|
||||
// SSH tunnel
|
||||
useSshTunnel: env[`USE_SSH_${id}`],
|
||||
sshHost: env[`SSH_HOST_${id}`],
|
||||
sshPort: env[`SSH_PORT_${id}`],
|
||||
sshMode: env[`SSH_MODE_${id}`],
|
||||
sshLogin: env[`SSH_LOGIN_${id}`],
|
||||
sshPassword: env[`SSH_PASSWORD_${id}`],
|
||||
sshKeyfile: env[`SSH_KEY_FILE_${id}`],
|
||||
sshKeyfilePassword: env[`SSH_KEY_FILE_PASSWORD_${id}`],
|
||||
|
||||
// SSL
|
||||
useSsl: env[`USE_SSL_${id}`],
|
||||
sslCaFile: env[`SSL_CA_FILE_${id}`],
|
||||
sslCertFile: env[`SSL_CERT_FILE_${id}`],
|
||||
sslCertFilePassword: env[`SSL_CERT_FILE_PASSWORD_${id}`],
|
||||
sslKeyFile: env[`SSL_KEY_FILE_${id}`],
|
||||
sslRejectUnauthorized: env[`SSL_REJECT_UNAUTHORIZED_${id}`],
|
||||
trustServerCertificate: env[`SSL_TRUST_CERTIFICATE_${id}`],
|
||||
}));
|
||||
|
||||
return connections;
|
||||
}
|
||||
|
||||
function extractImportEntitiesFromEnv(env) {
|
||||
const portalConnections = extractConnectionsFromEnv(env) || [];
|
||||
|
||||
const connections = portalConnections.map((conn, index) => ({
|
||||
...conn,
|
||||
id_original: conn._id,
|
||||
import_source_id: -1,
|
||||
conid: crypto.randomUUID(),
|
||||
_id: undefined,
|
||||
id: index + 1, // autoincrement id
|
||||
}));
|
||||
|
||||
const connectionEnvIdToDbId = {};
|
||||
for (const conn of connections) {
|
||||
connectionEnvIdToDbId[conn.id_original] = conn.id;
|
||||
}
|
||||
|
||||
const connectionsRegex = /^ROLE_(.+)_CONNECTIONS$/;
|
||||
const permissionsRegex = /^ROLE_(.+)_PERMISSIONS$/;
|
||||
|
||||
const dbConnectionRegex = /^ROLE_(.+)_DATABASES_(.+)_CONNECTION$/;
|
||||
const dbDatabasesRegex = /^ROLE_(.+)_DATABASES_(.+)_DATABASES$/;
|
||||
const dbDatabasesRegexRegex = /^ROLE_(.+)_DATABASES_(.+)_DATABASES_REGEX$/;
|
||||
const dbPermissionRegex = /^ROLE_(.+)_DATABASES_(.+)_PERMISSION$/;
|
||||
|
||||
const tableConnectionRegex = /^ROLE_(.+)_TABLES_(.+)_CONNECTION$/;
|
||||
const tableDatabasesRegex = /^ROLE_(.+)_TABLES_(.+)_DATABASES$/;
|
||||
const tableDatabasesRegexRegex = /^ROLE_(.+)_TABLES_(.+)_DATABASES_REGEX$/;
|
||||
const tableSchemasRegex = /^ROLE_(.+)_TABLES_(.+)_SCHEMAS$/;
|
||||
const tableSchemasRegexRegex = /^ROLE_(.+)_TABLES_(.+)_SCHEMAS_REGEX$/;
|
||||
const tableTablesRegex = /^ROLE_(.+)_TABLES_(.+)_TABLES$/;
|
||||
const tableTablesRegexRegex = /^ROLE_(.+)_TABLES_(.+)_TABLES_REGEX$/;
|
||||
const tablePermissionRegex = /^ROLE_(.+)_TABLES_(.+)_PERMISSION$/;
|
||||
const tableScopeRegex = /^ROLE_(.+)_TABLES_(.+)_SCOPE$/;
|
||||
|
||||
const roles = [];
|
||||
const role_connections = [];
|
||||
const role_permissions = [];
|
||||
const role_databases = [];
|
||||
const role_tables = [];
|
||||
|
||||
// Permission name to ID mappings
|
||||
const databasePermissionMap = {
|
||||
view: -1,
|
||||
read_content: -2,
|
||||
write_data: -3,
|
||||
run_script: -4,
|
||||
deny: -5,
|
||||
};
|
||||
|
||||
const tablePermissionMap = {
|
||||
read: -1,
|
||||
update_only: -2,
|
||||
create_update_delete: -3,
|
||||
run_script: -4,
|
||||
deny: -5,
|
||||
};
|
||||
|
||||
const tableScopeMap = {
|
||||
all_objects: -1,
|
||||
tables: -2,
|
||||
views: -3,
|
||||
tables_views_collections: -4,
|
||||
procedures: -5,
|
||||
functions: -6,
|
||||
triggers: -7,
|
||||
sql_objects: -8,
|
||||
collections: -9,
|
||||
};
|
||||
|
||||
// Collect database and table permissions data
|
||||
const databasePermissions = {};
|
||||
const tablePermissions = {};
|
||||
|
||||
// First pass: collect all database and table permission data
|
||||
for (const key in env) {
|
||||
const dbConnMatch = key.match(dbConnectionRegex);
|
||||
const dbDatabasesMatch = key.match(dbDatabasesRegex);
|
||||
const dbDatabasesRegexMatch = key.match(dbDatabasesRegexRegex);
|
||||
const dbPermMatch = key.match(dbPermissionRegex);
|
||||
|
||||
const tableConnMatch = key.match(tableConnectionRegex);
|
||||
const tableDatabasesMatch = key.match(tableDatabasesRegex);
|
||||
const tableDatabasesRegexMatch = key.match(tableDatabasesRegexRegex);
|
||||
const tableSchemasMatch = key.match(tableSchemasRegex);
|
||||
const tableSchemasRegexMatch = key.match(tableSchemasRegexRegex);
|
||||
const tableTablesMatch = key.match(tableTablesRegex);
|
||||
const tableTablesRegexMatch = key.match(tableTablesRegexRegex);
|
||||
const tablePermMatch = key.match(tablePermissionRegex);
|
||||
const tableScopeMatch = key.match(tableScopeRegex);
|
||||
|
||||
// Database permissions
|
||||
if (dbConnMatch) {
|
||||
const [, roleName, permId] = dbConnMatch;
|
||||
if (!databasePermissions[roleName]) databasePermissions[roleName] = {};
|
||||
if (!databasePermissions[roleName][permId]) databasePermissions[roleName][permId] = {};
|
||||
databasePermissions[roleName][permId].connection = env[key];
|
||||
}
|
||||
if (dbDatabasesMatch) {
|
||||
const [, roleName, permId] = dbDatabasesMatch;
|
||||
if (!databasePermissions[roleName]) databasePermissions[roleName] = {};
|
||||
if (!databasePermissions[roleName][permId]) databasePermissions[roleName][permId] = {};
|
||||
databasePermissions[roleName][permId].databases = env[key]?.replace(/\|/g, '\n');
|
||||
}
|
||||
if (dbDatabasesRegexMatch) {
|
||||
const [, roleName, permId] = dbDatabasesRegexMatch;
|
||||
if (!databasePermissions[roleName]) databasePermissions[roleName] = {};
|
||||
if (!databasePermissions[roleName][permId]) databasePermissions[roleName][permId] = {};
|
||||
databasePermissions[roleName][permId].databasesRegex = env[key];
|
||||
}
|
||||
if (dbPermMatch) {
|
||||
const [, roleName, permId] = dbPermMatch;
|
||||
if (!databasePermissions[roleName]) databasePermissions[roleName] = {};
|
||||
if (!databasePermissions[roleName][permId]) databasePermissions[roleName][permId] = {};
|
||||
databasePermissions[roleName][permId].permission = env[key];
|
||||
}
|
||||
|
||||
// Table permissions
|
||||
if (tableConnMatch) {
|
||||
const [, roleName, permId] = tableConnMatch;
|
||||
if (!tablePermissions[roleName]) tablePermissions[roleName] = {};
|
||||
if (!tablePermissions[roleName][permId]) tablePermissions[roleName][permId] = {};
|
||||
tablePermissions[roleName][permId].connection = env[key];
|
||||
}
|
||||
if (tableDatabasesMatch) {
|
||||
const [, roleName, permId] = tableDatabasesMatch;
|
||||
if (!tablePermissions[roleName]) tablePermissions[roleName] = {};
|
||||
if (!tablePermissions[roleName][permId]) tablePermissions[roleName][permId] = {};
|
||||
tablePermissions[roleName][permId].databases = env[key]?.replace(/\|/g, '\n');
|
||||
}
|
||||
if (tableDatabasesRegexMatch) {
|
||||
const [, roleName, permId] = tableDatabasesRegexMatch;
|
||||
if (!tablePermissions[roleName]) tablePermissions[roleName] = {};
|
||||
if (!tablePermissions[roleName][permId]) tablePermissions[roleName][permId] = {};
|
||||
tablePermissions[roleName][permId].databasesRegex = env[key];
|
||||
}
|
||||
if (tableSchemasMatch) {
|
||||
const [, roleName, permId] = tableSchemasMatch;
|
||||
if (!tablePermissions[roleName]) tablePermissions[roleName] = {};
|
||||
if (!tablePermissions[roleName][permId]) tablePermissions[roleName][permId] = {};
|
||||
tablePermissions[roleName][permId].schemas = env[key];
|
||||
}
|
||||
if (tableSchemasRegexMatch) {
|
||||
const [, roleName, permId] = tableSchemasRegexMatch;
|
||||
if (!tablePermissions[roleName]) tablePermissions[roleName] = {};
|
||||
if (!tablePermissions[roleName][permId]) tablePermissions[roleName][permId] = {};
|
||||
tablePermissions[roleName][permId].schemasRegex = env[key];
|
||||
}
|
||||
if (tableTablesMatch) {
|
||||
const [, roleName, permId] = tableTablesMatch;
|
||||
if (!tablePermissions[roleName]) tablePermissions[roleName] = {};
|
||||
if (!tablePermissions[roleName][permId]) tablePermissions[roleName][permId] = {};
|
||||
tablePermissions[roleName][permId].tables = env[key]?.replace(/\|/g, '\n');
|
||||
}
|
||||
if (tableTablesRegexMatch) {
|
||||
const [, roleName, permId] = tableTablesRegexMatch;
|
||||
if (!tablePermissions[roleName]) tablePermissions[roleName] = {};
|
||||
if (!tablePermissions[roleName][permId]) tablePermissions[roleName][permId] = {};
|
||||
tablePermissions[roleName][permId].tablesRegex = env[key];
|
||||
}
|
||||
if (tablePermMatch) {
|
||||
const [, roleName, permId] = tablePermMatch;
|
||||
if (!tablePermissions[roleName]) tablePermissions[roleName] = {};
|
||||
if (!tablePermissions[roleName][permId]) tablePermissions[roleName][permId] = {};
|
||||
tablePermissions[roleName][permId].permission = env[key];
|
||||
}
|
||||
if (tableScopeMatch) {
|
||||
const [, roleName, permId] = tableScopeMatch;
|
||||
if (!tablePermissions[roleName]) tablePermissions[roleName] = {};
|
||||
if (!tablePermissions[roleName][permId]) tablePermissions[roleName][permId] = {};
|
||||
tablePermissions[roleName][permId].scope = env[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: process roles, connections, and permissions
|
||||
for (const key in env) {
|
||||
const connMatch = key.match(connectionsRegex);
|
||||
const permMatch = key.match(permissionsRegex);
|
||||
if (connMatch) {
|
||||
const roleName = connMatch[1];
|
||||
let role = roles.find(r => r.name === roleName);
|
||||
if (!role) {
|
||||
role = {
|
||||
id: roles.length + 1,
|
||||
name: roleName,
|
||||
import_source_id: -1,
|
||||
};
|
||||
roles.push(role);
|
||||
}
|
||||
const connIds = env[key]
|
||||
.split(',')
|
||||
.map(id => id.trim())
|
||||
.filter(id => id.length > 0);
|
||||
for (const connId of connIds) {
|
||||
const dbId = connectionEnvIdToDbId[connId];
|
||||
if (dbId) {
|
||||
role_connections.push({
|
||||
role_id: role.id,
|
||||
connection_id: dbId,
|
||||
import_source_id: -1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (permMatch) {
|
||||
const roleName = permMatch[1];
|
||||
let role = roles.find(r => r.name === roleName);
|
||||
if (!role) {
|
||||
role = {
|
||||
id: roles.length + 1,
|
||||
name: roleName,
|
||||
import_source_id: -1,
|
||||
};
|
||||
roles.push(role);
|
||||
}
|
||||
const permissions = env[key]
|
||||
.split(',')
|
||||
.map(p => p.trim())
|
||||
.filter(p => p.length > 0);
|
||||
for (const permission of permissions) {
|
||||
role_permissions.push({
|
||||
role_id: role.id,
|
||||
permission,
|
||||
import_source_id: -1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process database permissions
|
||||
for (const roleName in databasePermissions) {
|
||||
let role = roles.find(r => r.name === roleName);
|
||||
if (!role) {
|
||||
role = {
|
||||
id: roles.length + 1,
|
||||
name: roleName,
|
||||
import_source_id: -1,
|
||||
};
|
||||
roles.push(role);
|
||||
}
|
||||
|
||||
for (const permId in databasePermissions[roleName]) {
|
||||
const perm = databasePermissions[roleName][permId];
|
||||
if (perm.connection && perm.permission) {
|
||||
const dbId = connectionEnvIdToDbId[perm.connection];
|
||||
const permissionId = databasePermissionMap[perm.permission];
|
||||
if (dbId && permissionId) {
|
||||
role_databases.push({
|
||||
role_id: role.id,
|
||||
connection_id: dbId,
|
||||
database_names_list: perm.databases || null,
|
||||
database_names_regex: perm.databasesRegex || null,
|
||||
database_permission_role_id: permissionId,
|
||||
id_original: permId,
|
||||
import_source_id: -1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process table permissions
|
||||
for (const roleName in tablePermissions) {
|
||||
let role = roles.find(r => r.name === roleName);
|
||||
if (!role) {
|
||||
role = {
|
||||
id: roles.length + 1,
|
||||
name: roleName,
|
||||
import_source_id: -1,
|
||||
};
|
||||
roles.push(role);
|
||||
}
|
||||
|
||||
for (const permId in tablePermissions[roleName]) {
|
||||
const perm = tablePermissions[roleName][permId];
|
||||
if (perm.connection && perm.permission) {
|
||||
const dbId = connectionEnvIdToDbId[perm.connection];
|
||||
const permissionId = tablePermissionMap[perm.permission];
|
||||
const scopeId = tableScopeMap[perm.scope || 'all_objects'];
|
||||
if (dbId && permissionId && scopeId) {
|
||||
role_tables.push({
|
||||
role_id: role.id,
|
||||
connection_id: dbId,
|
||||
database_names_list: perm.databases || null,
|
||||
database_names_regex: perm.databasesRegex || null,
|
||||
schema_names_list: perm.schemas || null,
|
||||
schema_names_regex: perm.schemasRegex || null,
|
||||
table_names_list: perm.tables || null,
|
||||
table_names_regex: perm.tablesRegex || null,
|
||||
table_permission_role_id: permissionId,
|
||||
table_permission_scope_id: scopeId,
|
||||
id_original: permId,
|
||||
import_source_id: -1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (connections.length == 0 && roles.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
connections,
|
||||
roles,
|
||||
role_connections,
|
||||
role_permissions,
|
||||
role_databases,
|
||||
role_tables,
|
||||
};
|
||||
}
|
||||
|
||||
function createStorageFromEnvReplicatorItems(importEntities) {
|
||||
return [
|
||||
{
|
||||
name: 'connections',
|
||||
findExisting: true,
|
||||
createNew: true,
|
||||
updateExisting: true,
|
||||
matchColumns: ['id_original', 'import_source_id'],
|
||||
deleteMissing: true,
|
||||
deleteRestrictionColumns: ['import_source_id'],
|
||||
skipUpdateColumns: ['conid'],
|
||||
jsonArray: importEntities.connections,
|
||||
},
|
||||
{
|
||||
name: 'roles',
|
||||
findExisting: true,
|
||||
createNew: true,
|
||||
updateExisting: true,
|
||||
matchColumns: ['name', 'import_source_id'],
|
||||
deleteMissing: true,
|
||||
deleteRestrictionColumns: ['import_source_id'],
|
||||
jsonArray: importEntities.roles,
|
||||
},
|
||||
{
|
||||
name: 'role_connections',
|
||||
findExisting: true,
|
||||
createNew: true,
|
||||
updateExisting: false,
|
||||
deleteMissing: true,
|
||||
matchColumns: ['role_id', 'connection_id', 'import_source_id'],
|
||||
jsonArray: importEntities.role_connections,
|
||||
deleteRestrictionColumns: ['import_source_id'],
|
||||
},
|
||||
{
|
||||
name: 'role_permissions',
|
||||
findExisting: true,
|
||||
createNew: true,
|
||||
updateExisting: false,
|
||||
deleteMissing: true,
|
||||
matchColumns: ['role_id', 'permission', 'import_source_id'],
|
||||
jsonArray: importEntities.role_permissions,
|
||||
deleteRestrictionColumns: ['import_source_id'],
|
||||
},
|
||||
{
|
||||
name: 'role_databases',
|
||||
findExisting: true,
|
||||
createNew: true,
|
||||
updateExisting: true,
|
||||
deleteMissing: true,
|
||||
matchColumns: ['role_id', 'id_original', 'import_source_id'],
|
||||
jsonArray: importEntities.role_databases,
|
||||
deleteRestrictionColumns: ['import_source_id'],
|
||||
},
|
||||
{
|
||||
name: 'role_tables',
|
||||
findExisting: true,
|
||||
createNew: true,
|
||||
updateExisting: true,
|
||||
deleteMissing: true,
|
||||
matchColumns: ['role_id', 'id_original', 'import_source_id'],
|
||||
jsonArray: importEntities.role_tables,
|
||||
deleteRestrictionColumns: ['import_source_id'],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
extractConnectionsFromEnv,
|
||||
extractImportEntitiesFromEnv,
|
||||
createStorageFromEnvReplicatorItems,
|
||||
};
|
||||
@@ -53,7 +53,7 @@ const getChartExport = (title, config, imageFile, plugins) => {
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
Exported from <a href='https://dbgate.io/' target='_blank'>DbGate</a>, powered by <a href='https://www.chartjs.org/' target='_blank'>Chart.js</a>
|
||||
Exported from <a href='https://www.dbgate.io/' target='_blank'>DbGate</a>, powered by <a href='https://www.chartjs.org/' target='_blank'>Chart.js</a>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ const getMapExport = (geoJson) => {
|
||||
leaflet
|
||||
.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '<a href="https://dbgate.io" title="Exported from DbGate">DbGate</a> | © OpenStreetMap',
|
||||
attribution: '<a href="https://www.dbgate.io" title="Exported from DbGate">DbGate</a> | © OpenStreetMap',
|
||||
})
|
||||
.addTo(map);
|
||||
|
||||
|
||||
@@ -14,9 +14,10 @@ class QueryStreamTableWriter {
|
||||
this.currentChangeIndex = 1;
|
||||
this.initializedFile = false;
|
||||
this.sesid = sesid;
|
||||
this.started = new Date().getTime();
|
||||
}
|
||||
|
||||
initializeFromQuery(structure, resultIndex, chartDefinition, autoDetectCharts = false) {
|
||||
initializeFromQuery(structure, resultIndex, chartDefinition, autoDetectCharts = false, options = {}) {
|
||||
this.jslid = crypto.randomUUID();
|
||||
this.currentFile = path.join(jsldir(), `${this.jslid}.jsonl`);
|
||||
fs.writeFileSync(
|
||||
@@ -24,6 +25,7 @@ class QueryStreamTableWriter {
|
||||
JSON.stringify({
|
||||
...structure,
|
||||
__isStreamHeader: true,
|
||||
...options
|
||||
}) + '\n'
|
||||
);
|
||||
this.currentStream = fs.createWriteStream(this.currentFile, { flags: 'a' });
|
||||
@@ -118,6 +120,13 @@ class QueryStreamTableWriter {
|
||||
this.chartProcessor = null;
|
||||
}
|
||||
}
|
||||
process.send({
|
||||
msgtype: 'endrecordset',
|
||||
jslid: this.jslid,
|
||||
rowCount: this.currentRowCount,
|
||||
sesid: this.sesid,
|
||||
durationMs: new Date().getTime() - this.started,
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
@@ -148,6 +157,7 @@ class StreamHandler {
|
||||
// this.error = this.error.bind(this);
|
||||
this.done = this.done.bind(this);
|
||||
this.info = this.info.bind(this);
|
||||
this.changedCurrentDatabase = this.changedCurrentDatabase.bind(this);
|
||||
|
||||
// use this for cancelling - not implemented
|
||||
// this.stream = null;
|
||||
@@ -166,7 +176,11 @@ class StreamHandler {
|
||||
}
|
||||
}
|
||||
|
||||
recordset(columns) {
|
||||
changedCurrentDatabase(database) {
|
||||
process.send({ msgtype: 'changedCurrentDatabase', database, sesid: this.sesid });
|
||||
}
|
||||
|
||||
recordset(columns, options) {
|
||||
if (this.rowsLimitOverflow) {
|
||||
return;
|
||||
}
|
||||
@@ -176,7 +190,8 @@ class StreamHandler {
|
||||
Array.isArray(columns) ? { columns } : columns,
|
||||
this.queryStreamInfoHolder.resultIndex,
|
||||
this.frontMatter?.[`chart-${this.queryStreamInfoHolder.resultIndex + 1}`],
|
||||
this.autoDetectCharts
|
||||
this.autoDetectCharts,
|
||||
options
|
||||
);
|
||||
this.queryStreamInfoHolder.resultIndex += 1;
|
||||
this.rowCounter = 0;
|
||||
|
||||
@@ -3,18 +3,6 @@ const os = require('os');
|
||||
const crypto = require('crypto');
|
||||
const platformInfo = require('./platformInfo');
|
||||
|
||||
async function getPublicIpInfo() {
|
||||
try {
|
||||
const resp = await axios.default.get('https://ipinfo.io/json');
|
||||
if (!resp.data?.ip) {
|
||||
return { ip: 'unknown-ip' };
|
||||
}
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
return { ip: 'unknown-ip' };
|
||||
}
|
||||
}
|
||||
|
||||
function getMacAddress() {
|
||||
try {
|
||||
const interfaces = os.networkInterfaces();
|
||||
@@ -32,6 +20,7 @@ function getMacAddress() {
|
||||
}
|
||||
|
||||
async function getHardwareFingerprint() {
|
||||
const { getPublicIpInfo } = require('./cloudIntf');
|
||||
const publicIpInfo = await getPublicIpInfo();
|
||||
const macAddress = getMacAddress();
|
||||
const platform = os.platform();
|
||||
@@ -42,8 +31,6 @@ async function getHardwareFingerprint() {
|
||||
return {
|
||||
publicIp: publicIpInfo.ip,
|
||||
country: publicIpInfo.country,
|
||||
region: publicIpInfo.region,
|
||||
city: publicIpInfo.city,
|
||||
macAddress,
|
||||
platform,
|
||||
release,
|
||||
@@ -68,9 +55,7 @@ async function getPublicHardwareFingerprint() {
|
||||
hash,
|
||||
payload: {
|
||||
platform: fingerprint.platform,
|
||||
city: fingerprint.city,
|
||||
country: fingerprint.country,
|
||||
region: fingerprint.region,
|
||||
isDocker: platformInfo.isDocker,
|
||||
isAwsUbuntuLayout: platformInfo.isAwsUbuntuLayout,
|
||||
isAzureUbuntuLayout: platformInfo.isAzureUbuntuLayout,
|
||||
@@ -87,5 +72,4 @@ module.exports = {
|
||||
getHardwareFingerprint,
|
||||
getHardwareFingerprintHash,
|
||||
getPublicHardwareFingerprint,
|
||||
getPublicIpInfo,
|
||||
};
|
||||
|
||||
@@ -1,96 +1,350 @@
|
||||
const { compilePermissions, testPermission } = require('dbgate-tools');
|
||||
const { compilePermissions, testPermission, getPermissionsCacheKey } = require('dbgate-tools');
|
||||
const _ = require('lodash');
|
||||
const { getAuthProviderFromReq } = require('../auth/authProvider');
|
||||
|
||||
const cachedPermissions = {};
|
||||
|
||||
function hasPermission(tested, req) {
|
||||
async function loadPermissionsFromRequest(req) {
|
||||
const authProvider = getAuthProviderFromReq(req);
|
||||
if (!req) {
|
||||
// request object not available, allow all
|
||||
return null;
|
||||
}
|
||||
|
||||
const loadedPermissions = await authProvider.getCurrentPermissions(req);
|
||||
return loadedPermissions;
|
||||
}
|
||||
|
||||
function hasPermission(tested, loadedPermissions) {
|
||||
if (!loadedPermissions) {
|
||||
// not available, allow all
|
||||
return true;
|
||||
}
|
||||
|
||||
const permissions = getAuthProviderFromReq(req).getCurrentPermissions(req);
|
||||
|
||||
if (!cachedPermissions[permissions]) {
|
||||
cachedPermissions[permissions] = compilePermissions(permissions);
|
||||
const permissionsKey = getPermissionsCacheKey(loadedPermissions);
|
||||
if (!cachedPermissions[permissionsKey]) {
|
||||
cachedPermissions[permissionsKey] = compilePermissions(loadedPermissions);
|
||||
}
|
||||
|
||||
return testPermission(tested, cachedPermissions[permissions]);
|
||||
|
||||
// const { user } = (req && req.auth) || {};
|
||||
// const { login } = (process.env.OAUTH_PERMISSIONS && req && req.user) || {};
|
||||
// const key = user || login || '';
|
||||
// const logins = getLogins();
|
||||
|
||||
// if (!userPermissions[key]) {
|
||||
// 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, cachedPermissions[permissionsKey]);
|
||||
}
|
||||
|
||||
// let loginsCache = null;
|
||||
// let loginsLoaded = false;
|
||||
|
||||
// function getLogins() {
|
||||
// if (loginsLoaded) {
|
||||
// return loginsCache;
|
||||
// }
|
||||
|
||||
// const res = [];
|
||||
// if (process.env.LOGIN && process.env.PASSWORD) {
|
||||
// res.push({
|
||||
// login: process.env.LOGIN,
|
||||
// password: process.env.PASSWORD,
|
||||
// permissions: process.env.PERMISSIONS,
|
||||
// });
|
||||
// }
|
||||
// if (process.env.LOGINS) {
|
||||
// const logins = _.compact(process.env.LOGINS.split(',').map(x => x.trim()));
|
||||
// for (const login of logins) {
|
||||
// const password = process.env[`LOGIN_PASSWORD_${login}`];
|
||||
// const permissions = process.env[`LOGIN_PERMISSIONS_${login}`];
|
||||
// if (password) {
|
||||
// res.push({
|
||||
// login,
|
||||
// password,
|
||||
// permissions,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// } else if (process.env.OAUTH_PERMISSIONS) {
|
||||
// const login_permission_keys = Object.keys(process.env).filter(key => _.startsWith(key, 'LOGIN_PERMISSIONS_'));
|
||||
// for (const permissions_key of login_permission_keys) {
|
||||
// const login = permissions_key.replace('LOGIN_PERMISSIONS_', '');
|
||||
// const permissions = process.env[permissions_key];
|
||||
// userPermissions[login] = compilePermissions(permissions);
|
||||
// }
|
||||
// }
|
||||
|
||||
// loginsCache = res.length > 0 ? res : null;
|
||||
// loginsLoaded = true;
|
||||
// return loginsCache;
|
||||
// }
|
||||
|
||||
function connectionHasPermission(connection, req) {
|
||||
function connectionHasPermission(connection, loadedPermissions) {
|
||||
if (!connection) {
|
||||
return true;
|
||||
}
|
||||
if (_.isString(connection)) {
|
||||
return hasPermission(`connections/${connection}`, req);
|
||||
return hasPermission(`connections/${connection}`, loadedPermissions);
|
||||
} else {
|
||||
return hasPermission(`connections/${connection._id}`, req);
|
||||
return hasPermission(`connections/${connection._id}`, loadedPermissions);
|
||||
}
|
||||
}
|
||||
|
||||
function testConnectionPermission(connection, req) {
|
||||
if (!connectionHasPermission(connection, req)) {
|
||||
throw new Error('Connection permission not granted');
|
||||
async function testConnectionPermission(connection, req, loadedPermissions) {
|
||||
if (!loadedPermissions) {
|
||||
loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
}
|
||||
if (process.env.STORAGE_DATABASE) {
|
||||
if (hasPermission(`all-connections`, loadedPermissions)) {
|
||||
return;
|
||||
}
|
||||
const conid = _.isString(connection) ? connection : connection?._id;
|
||||
if (hasPermission('internal-storage', loadedPermissions) && conid == '__storage') {
|
||||
return;
|
||||
}
|
||||
const authProvider = getAuthProviderFromReq(req);
|
||||
if (!req) {
|
||||
return;
|
||||
}
|
||||
if (!(await authProvider.checkCurrentConnectionPermission(req, conid))) {
|
||||
throw new Error('DBGM-00263 Connection permission not granted');
|
||||
}
|
||||
} else {
|
||||
if (!connectionHasPermission(connection, loadedPermissions)) {
|
||||
throw new Error('DBGM-00264 Connection permission not granted');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDatabasePermissionsFromRequest(req) {
|
||||
const authProvider = getAuthProviderFromReq(req);
|
||||
if (!req) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const databasePermissions = await authProvider.getCurrentDatabasePermissions(req);
|
||||
return databasePermissions;
|
||||
}
|
||||
|
||||
async function loadTablePermissionsFromRequest(req) {
|
||||
const authProvider = getAuthProviderFromReq(req);
|
||||
if (!req) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tablePermissions = await authProvider.getCurrentTablePermissions(req);
|
||||
return tablePermissions;
|
||||
}
|
||||
|
||||
async function loadFilePermissionsFromRequest(req) {
|
||||
const authProvider = getAuthProviderFromReq(req);
|
||||
if (!req) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filePermissions = await authProvider.getCurrentFilePermissions(req);
|
||||
return filePermissions;
|
||||
}
|
||||
|
||||
function matchDatabasePermissionRow(conid, database, permissionRow) {
|
||||
if (permissionRow.connection_id) {
|
||||
if (conid != permissionRow.connection_id) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (permissionRow.database_names_list) {
|
||||
const items = permissionRow.database_names_list.split('\n');
|
||||
if (!items.find(item => item.trim()?.toLowerCase() === database?.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (permissionRow.database_names_regex) {
|
||||
const regex = new RegExp(permissionRow.database_names_regex, 'i');
|
||||
if (!regex.test(database)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function matchTablePermissionRow(objectTypeField, schemaName, pureName, permissionRow) {
|
||||
if (permissionRow.table_names_list) {
|
||||
const items = permissionRow.table_names_list.split('\n');
|
||||
if (!items.find(item => item.trim()?.toLowerCase() === pureName?.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (permissionRow.table_names_regex) {
|
||||
const regex = new RegExp(permissionRow.table_names_regex, 'i');
|
||||
if (!regex.test(pureName)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (permissionRow.schema_names_list) {
|
||||
const items = permissionRow.schema_names_list.split('\n');
|
||||
if (!items.find(item => item.trim()?.toLowerCase() === schemaName?.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (permissionRow.schema_names_regex) {
|
||||
const regex = new RegExp(permissionRow.schema_names_regex, 'i');
|
||||
if (!regex.test(schemaName)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function matchFilePermissionRow(folder, file, permissionRow) {
|
||||
if (permissionRow.folder_name) {
|
||||
if (folder != permissionRow.folder_name) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (permissionRow.file_names_list) {
|
||||
const items = permissionRow.file_names_list.split('\n');
|
||||
if (!items.find(item => item.trim()?.toLowerCase() === file?.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (permissionRow.file_names_regex) {
|
||||
const regex = new RegExp(permissionRow.file_names_regex, 'i');
|
||||
if (!regex.test(file)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const DATABASE_ROLE_ID_NAMES = {
|
||||
'-1': 'view',
|
||||
'-2': 'read_content',
|
||||
'-3': 'write_data',
|
||||
'-4': 'run_script',
|
||||
'-5': 'deny',
|
||||
};
|
||||
|
||||
const FILE_ROLE_ID_NAMES = {
|
||||
'-1': 'allow',
|
||||
'-2': 'deny',
|
||||
};
|
||||
|
||||
function getDatabaseRoleLevelIndex(roleName) {
|
||||
if (!roleName) {
|
||||
return 6;
|
||||
}
|
||||
if (roleName == 'run_script') {
|
||||
return 5;
|
||||
}
|
||||
if (roleName == 'write_data') {
|
||||
return 4;
|
||||
}
|
||||
if (roleName == 'read_content') {
|
||||
return 3;
|
||||
}
|
||||
if (roleName == 'view') {
|
||||
return 2;
|
||||
}
|
||||
if (roleName == 'deny') {
|
||||
return 1;
|
||||
}
|
||||
return 6;
|
||||
}
|
||||
|
||||
function getTablePermissionRoleLevelIndex(roleName) {
|
||||
if (!roleName) {
|
||||
return 6;
|
||||
}
|
||||
if (roleName == 'run_script') {
|
||||
return 5;
|
||||
}
|
||||
if (roleName == 'create_update_delete') {
|
||||
return 4;
|
||||
}
|
||||
if (roleName == 'update_only') {
|
||||
return 3;
|
||||
}
|
||||
if (roleName == 'read') {
|
||||
return 2;
|
||||
}
|
||||
if (roleName == 'deny') {
|
||||
return 1;
|
||||
}
|
||||
return 6;
|
||||
}
|
||||
|
||||
function getDatabasePermissionRole(conid, database, loadedDatabasePermissions) {
|
||||
let res = 'deny';
|
||||
for (const permissionRow of loadedDatabasePermissions) {
|
||||
if (!matchDatabasePermissionRow(conid, database, permissionRow)) {
|
||||
continue;
|
||||
}
|
||||
res = DATABASE_ROLE_ID_NAMES[permissionRow.database_permission_role_id];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function getFilePermissionRole(folder, file, loadedFilePermissions) {
|
||||
let res = 'deny';
|
||||
for (const permissionRow of loadedFilePermissions) {
|
||||
if (!matchFilePermissionRow(folder, file, permissionRow)) {
|
||||
continue;
|
||||
}
|
||||
res = FILE_ROLE_ID_NAMES[permissionRow.file_permission_role_id];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
const TABLE_ROLE_ID_NAMES = {
|
||||
'-1': 'read',
|
||||
'-2': 'update_only',
|
||||
'-3': 'create_update_delete',
|
||||
'-4': 'run_script',
|
||||
'-5': 'deny',
|
||||
};
|
||||
|
||||
const TABLE_SCOPE_ID_NAMES = {
|
||||
'-1': 'all_objects',
|
||||
'-2': 'tables',
|
||||
'-3': 'views',
|
||||
'-4': 'tables_views_collections',
|
||||
'-5': 'procedures',
|
||||
'-6': 'functions',
|
||||
'-7': 'triggers',
|
||||
'-8': 'sql_objects',
|
||||
'-9': 'collections',
|
||||
};
|
||||
|
||||
function getTablePermissionRole(
|
||||
conid,
|
||||
database,
|
||||
objectTypeField,
|
||||
schemaName,
|
||||
pureName,
|
||||
loadedTablePermissions,
|
||||
databasePermissionRole
|
||||
) {
|
||||
let res =
|
||||
databasePermissionRole == 'read_content'
|
||||
? 'read'
|
||||
: databasePermissionRole == 'write_data'
|
||||
? 'create_update_delete'
|
||||
: databasePermissionRole == 'run_script'
|
||||
? 'run_script'
|
||||
: 'deny';
|
||||
for (const permissionRow of loadedTablePermissions) {
|
||||
if (!matchDatabasePermissionRow(conid, database, permissionRow)) {
|
||||
continue;
|
||||
}
|
||||
if (!matchTablePermissionRow(objectTypeField, schemaName, pureName, permissionRow)) {
|
||||
continue;
|
||||
}
|
||||
const scope = TABLE_SCOPE_ID_NAMES[permissionRow.table_permission_scope_id];
|
||||
switch (scope) {
|
||||
case 'tables':
|
||||
if (objectTypeField != 'tables') continue;
|
||||
break;
|
||||
case 'views':
|
||||
if (objectTypeField != 'views') continue;
|
||||
break;
|
||||
case 'tables_views_collections':
|
||||
if (objectTypeField != 'tables' && objectTypeField != 'views' && objectTypeField != 'collections') continue;
|
||||
break;
|
||||
case 'procedures':
|
||||
if (objectTypeField != 'procedures') continue;
|
||||
break;
|
||||
case 'functions':
|
||||
if (objectTypeField != 'functions') continue;
|
||||
break;
|
||||
case 'triggers':
|
||||
if (objectTypeField != 'triggers') continue;
|
||||
break;
|
||||
case 'sql_objects':
|
||||
if (objectTypeField != 'procedures' && objectTypeField != 'functions' && objectTypeField != 'triggers')
|
||||
continue;
|
||||
break;
|
||||
case 'collections':
|
||||
if (objectTypeField != 'collections') continue;
|
||||
break;
|
||||
}
|
||||
res = TABLE_ROLE_ID_NAMES[permissionRow.table_permission_role_id];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async function testStandardPermission(permission, req, loadedPermissions) {
|
||||
if (!loadedPermissions) {
|
||||
loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
}
|
||||
if (!hasPermission(permission, loadedPermissions)) {
|
||||
throw new Error(`DBGM-00265 Permission ${permission} not granted`);
|
||||
}
|
||||
}
|
||||
|
||||
async function testDatabaseRolePermission(conid, database, requiredRole, req) {
|
||||
if (!process.env.STORAGE_DATABASE) {
|
||||
return;
|
||||
}
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (hasPermission(`all-databases`, loadedPermissions)) {
|
||||
return;
|
||||
}
|
||||
const databasePermissions = await loadDatabasePermissionsFromRequest(req);
|
||||
const role = getDatabasePermissionRole(conid, database, databasePermissions);
|
||||
const requiredIndex = getDatabaseRoleLevelIndex(requiredRole);
|
||||
const roleIndex = getDatabaseRoleLevelIndex(role);
|
||||
if (roleIndex < requiredIndex) {
|
||||
throw new Error(`DBGM-00266 Permission ${requiredRole} not granted`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,4 +352,14 @@ module.exports = {
|
||||
hasPermission,
|
||||
connectionHasPermission,
|
||||
testConnectionPermission,
|
||||
loadPermissionsFromRequest,
|
||||
loadDatabasePermissionsFromRequest,
|
||||
loadTablePermissionsFromRequest,
|
||||
loadFilePermissionsFromRequest,
|
||||
getDatabasePermissionRole,
|
||||
getTablePermissionRole,
|
||||
getFilePermissionRole,
|
||||
testStandardPermission,
|
||||
testDatabaseRolePermission,
|
||||
getTablePermissionRoleLevelIndex,
|
||||
};
|
||||
|
||||
@@ -2,4 +2,5 @@ module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
moduleFileExtensions: ['js'],
|
||||
reporters: ['default', 'github-actions'],
|
||||
};
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
"name": "dbgate-datalib",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "jest",
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface DataReplicatorItem {
|
||||
deleteMissing: boolean;
|
||||
deleteRestrictionColumns: string[];
|
||||
matchColumns: string[];
|
||||
skipUpdateColumns?: string[];
|
||||
}
|
||||
|
||||
export interface DataReplicatorOptions {
|
||||
@@ -151,7 +152,12 @@ class ReplicatorItemHolder {
|
||||
chunk,
|
||||
this.table.columns.map(x => x.columnName)
|
||||
),
|
||||
[this.autoColumn, ...this.backReferences.map(x => x.columnName), ...this.references.map(x => x.columnName)]
|
||||
[
|
||||
this.autoColumn,
|
||||
...this.backReferences.map(x => x.columnName),
|
||||
...this.references.map(x => x.columnName),
|
||||
...(this.item.skipUpdateColumns || []),
|
||||
]
|
||||
);
|
||||
|
||||
return res;
|
||||
|
||||
@@ -31,6 +31,8 @@ export interface GridConfig extends GridConfigColumns {
|
||||
formFilterColumns: string[];
|
||||
multiColumnFilter?: string;
|
||||
searchInColumns?: string;
|
||||
disabledFilterColumns: string[];
|
||||
disabledMultiColumnFilter?: boolean;
|
||||
}
|
||||
|
||||
export interface GridCache {
|
||||
@@ -48,6 +50,7 @@ export function createGridConfig(): GridConfig {
|
||||
focusedColumns: null,
|
||||
grouping: {},
|
||||
formFilterColumns: [],
|
||||
disabledFilterColumns: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
FilterBehaviour,
|
||||
} from 'dbgate-types';
|
||||
import { parseFilter } from 'dbgate-filterparser';
|
||||
import { filterName } from 'dbgate-tools';
|
||||
import { filterName, shortenIdentifier } from 'dbgate-tools';
|
||||
import { ChangeSetFieldDefinition, ChangeSetRowDefinition } from './ChangeSet';
|
||||
import { Expression, Select, treeToSql, dumpSqlSelect, Condition, CompoudCondition } from 'dbgate-sqltree';
|
||||
import { isTypeLogical, standardFilterBehaviours, detectSqlFilterBehaviour, stringFilterBehaviour } from 'dbgate-tools';
|
||||
@@ -24,6 +24,7 @@ export interface DisplayColumn {
|
||||
columnName: string;
|
||||
headerText: string;
|
||||
uniqueName: string;
|
||||
uniqueNameShorten?: string;
|
||||
uniquePath: string[];
|
||||
notNull?: boolean;
|
||||
autoIncrement?: boolean;
|
||||
@@ -232,6 +233,7 @@ export abstract class GridDisplay {
|
||||
if (!filter) continue;
|
||||
const column = displayedColumnInfo[uniqueName];
|
||||
if (!column) continue;
|
||||
if (this.isFilterDisabled(uniqueName)) continue;
|
||||
try {
|
||||
const condition = parseFilter(
|
||||
filter,
|
||||
@@ -258,7 +260,7 @@ export abstract class GridDisplay {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.baseTableOrView && this.config.multiColumnFilter) {
|
||||
if (this.baseTableOrView && this.config.multiColumnFilter && !this.isMultiColumnFilterDisabled()) {
|
||||
const orCondition: CompoudCondition = {
|
||||
conditionType: 'or',
|
||||
conditions: [],
|
||||
@@ -415,6 +417,7 @@ export abstract class GridDisplay {
|
||||
[uniqueName]: value,
|
||||
},
|
||||
formViewRecordNumber: 0,
|
||||
disabledFilterColumns: cfg.disabledFilterColumns.filter(x => x != uniqueName),
|
||||
}));
|
||||
this.reload();
|
||||
}
|
||||
@@ -424,6 +427,7 @@ export abstract class GridDisplay {
|
||||
...cfg,
|
||||
multiColumnFilter: value,
|
||||
formViewRecordNumber: 0,
|
||||
disabledMultiColumnFilter: false,
|
||||
}));
|
||||
this.reload();
|
||||
}
|
||||
@@ -447,6 +451,7 @@ export abstract class GridDisplay {
|
||||
...cfg,
|
||||
filters: _.omit(cfg.filters, [uniqueName]),
|
||||
formFilterColumns: (cfg.formFilterColumns || []).filter(x => x != uniqueName),
|
||||
disabledFilterColumns: (cfg.disabledFilterColumns).filter(x => x != uniqueName),
|
||||
}));
|
||||
this.reload();
|
||||
}
|
||||
@@ -462,6 +467,37 @@ export abstract class GridDisplay {
|
||||
this.reload();
|
||||
}
|
||||
|
||||
toggleFilterEnabled(uniqueName) {
|
||||
if (this.isFilterDisabled(uniqueName)) {
|
||||
this.setConfig(cfg => ({
|
||||
...cfg,
|
||||
disabledFilterColumns: cfg.disabledFilterColumns.filter(x => x != uniqueName),
|
||||
}));
|
||||
} else {
|
||||
this.setConfig(cfg => ({
|
||||
...cfg,
|
||||
disabledFilterColumns: [...cfg.disabledFilterColumns, uniqueName],
|
||||
}));
|
||||
}
|
||||
this.reload();
|
||||
}
|
||||
|
||||
isFilterDisabled(uniqueName: string) {
|
||||
return this.config.disabledFilterColumns.includes(uniqueName);
|
||||
}
|
||||
|
||||
toggleMultiColumnFilterEnabled() {
|
||||
this.setConfig(cfg => ({
|
||||
...cfg,
|
||||
disabledMultiColumnFilter: !cfg.disabledMultiColumnFilter,
|
||||
}));
|
||||
this.reload();
|
||||
}
|
||||
|
||||
isMultiColumnFilterDisabled() {
|
||||
return this.config.disabledMultiColumnFilter;
|
||||
}
|
||||
|
||||
setSort(uniqueName, order) {
|
||||
this.setConfig(cfg => ({
|
||||
...cfg,
|
||||
@@ -606,7 +642,9 @@ export abstract class GridDisplay {
|
||||
}
|
||||
return {
|
||||
exprType: 'column',
|
||||
...(!this.dialect.omitTableAliases && { alias: alias || col.columnName }),
|
||||
...(!this.dialect.omitTableAliases && {
|
||||
alias: alias ?? col.columnName,
|
||||
}),
|
||||
source,
|
||||
...col,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import { filterName, isTableColumnUnique } from 'dbgate-tools';
|
||||
import { filterName, isTableColumnUnique, shortenIdentifier } from 'dbgate-tools';
|
||||
import { GridDisplay, ChangeCacheFunc, DisplayColumn, DisplayedColumnInfo, ChangeConfigFunc } from './GridDisplay';
|
||||
import type {
|
||||
TableInfo,
|
||||
@@ -39,7 +39,8 @@ export class TableGridDisplay extends GridDisplay {
|
||||
public getDictionaryDescription: DictionaryDescriptionFunc = null,
|
||||
isReadOnly = false,
|
||||
public isRawMode = false,
|
||||
public currentSettings = null
|
||||
public currentSettings = null,
|
||||
public areReferencesAllowed = true
|
||||
) {
|
||||
super(config, setConfig, cache, setCache, driver, dbinfo, serverVersion, currentSettings);
|
||||
|
||||
@@ -93,7 +94,7 @@ export class TableGridDisplay extends GridDisplay {
|
||||
);
|
||||
}
|
||||
|
||||
getDisplayColumns(table: TableInfo, parentPath: string[]) {
|
||||
getDisplayColumns(table: TableInfo, parentPath: string[]): DisplayColumn[] {
|
||||
return (
|
||||
table?.columns
|
||||
?.map(col => this.getDisplayColumn(table, col, parentPath))
|
||||
@@ -101,11 +102,12 @@ export class TableGridDisplay extends GridDisplay {
|
||||
...col,
|
||||
isChecked: this.isColumnChecked(col),
|
||||
hintColumnNames:
|
||||
this.getFkDictionaryDescription(col.isForeignKeyUnique ? col.foreignKey : null)?.columns?.map(
|
||||
columnName => `hint_${col.uniqueName}_${columnName}`
|
||||
this.getFkDictionaryDescription(col.isForeignKeyUnique ? col.foreignKey : null)?.columns?.map(columnName =>
|
||||
shortenIdentifier(`hint_${col.uniqueName}_${columnName}`, this.driver?.dialect?.maxIdentifierLength)
|
||||
) || null,
|
||||
hintColumnDelimiter: this.getFkDictionaryDescription(col.isForeignKeyUnique ? col.foreignKey : null)
|
||||
?.delimiter,
|
||||
uniqueNameShorten: shortenIdentifier(col.uniqueName, this.driver?.dialect?.maxIdentifierLength),
|
||||
isExpandable: !!col.foreignKey,
|
||||
})) || []
|
||||
);
|
||||
@@ -116,7 +118,7 @@ export class TableGridDisplay extends GridDisplay {
|
||||
if (this.isExpandedColumn(column.uniqueName)) {
|
||||
const table = this.getFkTarget(column);
|
||||
if (table) {
|
||||
const childAlias = `${column.uniqueName}_ref`;
|
||||
const childAlias = shortenIdentifier(`${column.uniqueName}_ref`, this.driver?.dialect?.maxIdentifierLength);
|
||||
const subcolumns = this.getDisplayColumns(table, column.uniquePath);
|
||||
|
||||
this.addReferenceToSelect(select, parentAlias, column);
|
||||
@@ -129,7 +131,7 @@ export class TableGridDisplay extends GridDisplay {
|
||||
}
|
||||
|
||||
addReferenceToSelect(select: Select, parentAlias: string, column: DisplayColumn) {
|
||||
const childAlias = `${column.uniqueName}_ref`;
|
||||
const childAlias = shortenIdentifier(`${column.uniqueName}_ref`, this.driver?.dialect?.maxIdentifierLength);
|
||||
if ((select.from.relations || []).find(x => x.alias == childAlias)) return;
|
||||
const table = this.getFkTarget(column);
|
||||
if (table && table.primaryKey) {
|
||||
@@ -191,15 +193,24 @@ export class TableGridDisplay extends GridDisplay {
|
||||
const hintDescription = this.getDictionaryDescription(table);
|
||||
if (hintDescription) {
|
||||
const parentUniqueName = column.uniquePath.slice(0, -1).join('.');
|
||||
this.addReferenceToSelect(select, parentUniqueName ? `${parentUniqueName}_ref` : 'basetbl', column);
|
||||
const childAlias = `${column.uniqueName}_ref`;
|
||||
this.addReferenceToSelect(
|
||||
select,
|
||||
parentUniqueName
|
||||
? shortenIdentifier(`${parentUniqueName}_ref`, this.driver?.dialect?.maxIdentifierLength)
|
||||
: 'basetbl',
|
||||
column
|
||||
);
|
||||
const childAlias = shortenIdentifier(`${column.uniqueName}_ref`, this.driver?.dialect?.maxIdentifierLength);
|
||||
select.columns.push(
|
||||
...hintDescription.columns.map(
|
||||
columnName =>
|
||||
({
|
||||
exprType: 'column',
|
||||
columnName,
|
||||
alias: `hint_${column.uniqueName}_${columnName}`,
|
||||
alias: shortenIdentifier(
|
||||
`hint_${column.uniqueName}_${columnName}`,
|
||||
this.driver?.dialect?.maxIdentifierLength
|
||||
),
|
||||
source: { alias: childAlias },
|
||||
} as ColumnRefExpression)
|
||||
)
|
||||
@@ -230,7 +241,7 @@ export class TableGridDisplay extends GridDisplay {
|
||||
}
|
||||
|
||||
getFkTarget(column: DisplayColumn) {
|
||||
const { uniqueName, foreignKey, isForeignKeyUnique } = column;
|
||||
const { foreignKey, isForeignKeyUnique } = column;
|
||||
if (!isForeignKeyUnique) return null;
|
||||
const pureName = foreignKey.refTableName;
|
||||
const schemaName = foreignKey.refSchemaName;
|
||||
@@ -238,6 +249,7 @@ export class TableGridDisplay extends GridDisplay {
|
||||
}
|
||||
|
||||
processReferences(select: Select, displayedColumnInfo: DisplayedColumnInfo, options) {
|
||||
if (!this.areReferencesAllowed) return;
|
||||
this.addJoinsFromExpandedColumns(select, this.columns, 'basetbl', displayedColumnInfo);
|
||||
if (!options.isExport && this.displayOptions.showHintColumns) {
|
||||
this.addHintsToSelect(select);
|
||||
@@ -298,7 +310,12 @@ export class TableGridDisplay extends GridDisplay {
|
||||
for (const column of columns) {
|
||||
if (this.addAllExpandedColumnsToSelected || this.config.addedColumns.includes(column.uniqueName)) {
|
||||
select.columns.push(
|
||||
this.createColumnExpression(column, { name: column, alias: parentAlias }, column.uniqueName, 'view')
|
||||
this.createColumnExpression(
|
||||
column,
|
||||
{ name: column, alias: parentAlias },
|
||||
column.uniqueNameShorten ?? column.uniqueName,
|
||||
'view'
|
||||
)
|
||||
);
|
||||
displayedColumnInfo[column.uniqueName] = {
|
||||
...column,
|
||||
|
||||
@@ -4,6 +4,7 @@ export type ChartXTransformFunction =
|
||||
| 'date:minute'
|
||||
| 'date:hour'
|
||||
| 'date:day'
|
||||
| 'date:week'
|
||||
| 'date:month'
|
||||
| 'date:year';
|
||||
export type ChartYAggregateFunction = 'sum' | 'first' | 'last' | 'min' | 'max' | 'count' | 'avg';
|
||||
@@ -70,6 +71,7 @@ export interface ChartDateParsed {
|
||||
minute?: number;
|
||||
second?: number;
|
||||
fraction?: string;
|
||||
week?: number;
|
||||
}
|
||||
|
||||
export interface ChartAvailableColumn {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
ChartYFieldDefinition,
|
||||
ProcessedChart,
|
||||
} from './chartDefinitions';
|
||||
import { addMinutes, addHours, addDays, addMonths, addYears } from 'date-fns';
|
||||
import { addMinutes, addHours, addDays, addMonths, addWeeks, addYears, getWeek } from 'date-fns';
|
||||
|
||||
export function getChartDebugPrint(chart: ProcessedChart) {
|
||||
let res = '';
|
||||
@@ -29,6 +29,7 @@ export function tryParseChartDate(dateInput: any): ChartDateParsed | null {
|
||||
return {
|
||||
year: dateInput.getFullYear(),
|
||||
month: dateInput.getMonth() + 1,
|
||||
week: getWeek(dateInput),
|
||||
day: dateInput.getDate(),
|
||||
hour: dateInput.getHours(),
|
||||
minute: dateInput.getMinutes(),
|
||||
@@ -42,15 +43,21 @@ export function tryParseChartDate(dateInput: any): ChartDateParsed | null {
|
||||
/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(Z|[+-]\d{2}:\d{2})?)?$/
|
||||
);
|
||||
const monthMatch = dateInput.match(/^(\d{4})-(\d{2})$/);
|
||||
const weekMatch = dateInput.match(/^(\d{4})\@(\d{2})$/);
|
||||
// const yearMatch = dateInput.match(/^(\d{4})$/);
|
||||
|
||||
if (dateMatch) {
|
||||
const [_notUsed, year, month, day, hour, minute, second, fraction] = dateMatch;
|
||||
const [_notUsed, yearStr, monthStr, dayStr, hour, minute, second, fraction] = dateMatch;
|
||||
|
||||
const year = parseInt(yearStr, 10);
|
||||
const month = parseInt(monthStr, 10);
|
||||
const day = parseInt(dayStr, 10);
|
||||
|
||||
return {
|
||||
year: parseInt(year, 10),
|
||||
month: parseInt(month, 10),
|
||||
day: parseInt(day, 10),
|
||||
year,
|
||||
month,
|
||||
week: getWeek(new Date(year, month - 1, day)),
|
||||
day,
|
||||
hour: parseInt(hour, 10) || 0,
|
||||
minute: parseInt(minute, 10) || 0,
|
||||
second: parseInt(second, 10) || 0,
|
||||
@@ -71,6 +78,19 @@ export function tryParseChartDate(dateInput: any): ChartDateParsed | null {
|
||||
};
|
||||
}
|
||||
|
||||
if (weekMatch) {
|
||||
const [_notUsed, year, week] = weekMatch;
|
||||
return {
|
||||
year: parseInt(year, 10),
|
||||
week: parseInt(week, 10),
|
||||
day: 1,
|
||||
hour: 0,
|
||||
minute: 0,
|
||||
second: 0,
|
||||
fraction: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// if (yearMatch) {
|
||||
// const [_notUsed, year] = yearMatch;
|
||||
// return {
|
||||
@@ -97,6 +117,8 @@ export function stringifyChartDate(value: ChartDateParsed, transform: ChartXTran
|
||||
return `${value.year}`;
|
||||
case 'date:month':
|
||||
return `${value.year}-${pad2Digits(value.month)}`;
|
||||
case 'date:week':
|
||||
return `${value.year}@${pad2Digits(getWeek(new Date(value.year, (value.month ?? 1) - 1, value.day ?? 1)))}`;
|
||||
case 'date:day':
|
||||
return `${value.year}-${pad2Digits(value.month)}-${pad2Digits(value.day)}`;
|
||||
case 'date:hour':
|
||||
@@ -126,6 +148,9 @@ export function incrementChartDate(value: ChartDateParsed, transform: ChartXTran
|
||||
case 'date:month':
|
||||
newDateRepresentation = addMonths(dateRepresentation, 1);
|
||||
break;
|
||||
case 'date:week':
|
||||
newDateRepresentation = addWeeks(dateRepresentation, 1);
|
||||
break;
|
||||
case 'date:day':
|
||||
newDateRepresentation = addDays(dateRepresentation, 1);
|
||||
break;
|
||||
@@ -144,6 +169,11 @@ export function incrementChartDate(value: ChartDateParsed, transform: ChartXTran
|
||||
year: newDateRepresentation.getFullYear(),
|
||||
month: newDateRepresentation.getMonth() + 1,
|
||||
};
|
||||
case 'date:week':
|
||||
return {
|
||||
year: newDateRepresentation.getFullYear(),
|
||||
week: getWeek(newDateRepresentation),
|
||||
};
|
||||
case 'date:day':
|
||||
return {
|
||||
year: newDateRepresentation.getFullYear(),
|
||||
@@ -175,6 +205,8 @@ export function runTransformFunction(value: string, transformFunction: ChartXTra
|
||||
return dateParsed ? `${dateParsed.year}` : null;
|
||||
case 'date:month':
|
||||
return dateParsed ? `${dateParsed.year}-${pad2Digits(dateParsed.month)}` : null;
|
||||
case 'date:week':
|
||||
return dateParsed ? `${dateParsed.year}@${pad2Digits(dateParsed.week)}` : null;
|
||||
case 'date:day':
|
||||
return dateParsed ? `${dateParsed.year}-${pad2Digits(dateParsed.month)}-${pad2Digits(dateParsed.day)}` : null;
|
||||
case 'date:hour':
|
||||
@@ -211,6 +243,14 @@ export function computeChartBucketKey(
|
||||
month: dateParsed.month,
|
||||
},
|
||||
];
|
||||
case 'date:week':
|
||||
return [
|
||||
dateParsed ? `${dateParsed.year}@${pad2Digits(dateParsed.week)}` : null,
|
||||
{
|
||||
year: dateParsed.year,
|
||||
week: dateParsed.week,
|
||||
},
|
||||
];
|
||||
case 'date:day':
|
||||
return [
|
||||
dateParsed ? `${dateParsed.year}-${pad2Digits(dateParsed.month)}-${pad2Digits(dateParsed.day)}` : null,
|
||||
@@ -265,6 +305,8 @@ export function computeDateBucketDistance(
|
||||
return end.year - begin.year;
|
||||
case 'date:month':
|
||||
return (end.year - begin.year) * 12 + (end.month - begin.month);
|
||||
case 'date:week':
|
||||
return (end.year - begin.year) * 52 + (end.week - begin.week);
|
||||
case 'date:day':
|
||||
return (
|
||||
(end.year - begin.year) * 365 +
|
||||
@@ -302,6 +344,8 @@ export function compareChartDatesParsed(
|
||||
return a.year - b.year;
|
||||
case 'date:month':
|
||||
return a.year === b.year ? a.month - b.month : a.year - b.year;
|
||||
case 'date:week':
|
||||
return a.year === b.year ? a.week - b.week : a.year - b.year;
|
||||
case 'date:day':
|
||||
return a.year === b.year && a.month === b.month
|
||||
? a.day - b.day
|
||||
@@ -356,6 +400,8 @@ function getParentDateBucketKey(
|
||||
return null; // no parent for year
|
||||
case 'date:month':
|
||||
return bucketKey.slice(0, 4);
|
||||
case 'date:week':
|
||||
return bucketKey.slice(0, 4);
|
||||
case 'date:day':
|
||||
return bucketKey.slice(0, 7);
|
||||
case 'date:hour':
|
||||
@@ -371,6 +417,8 @@ function getParentDateBucketTransform(transform: ChartXTransformFunction): Chart
|
||||
return null; // no parent for year
|
||||
case 'date:month':
|
||||
return 'date:year';
|
||||
case 'date:week':
|
||||
return 'date:year';
|
||||
case 'date:day':
|
||||
return 'date:month';
|
||||
case 'date:hour':
|
||||
@@ -388,6 +436,8 @@ function getParentKeyParsed(date: ChartDateParsed, transform: ChartXTransformFun
|
||||
return null; // no parent for year
|
||||
case 'date:month':
|
||||
return { year: date.year };
|
||||
case 'date:week':
|
||||
return { year: date.week };
|
||||
case 'date:day':
|
||||
return { year: date.year, month: date.month };
|
||||
case 'date:hour':
|
||||
|
||||
@@ -35,6 +35,12 @@ program
|
||||
.option('-u, --user <user>', 'user name')
|
||||
.option('-p, --password <password>', 'password')
|
||||
.option('-d, --database <database>', 'database name')
|
||||
.option('--url <url>', 'database url')
|
||||
.option('--file <file>', 'database file')
|
||||
.option('--socket-path <socketPath>', 'socket path')
|
||||
.option('--service-name <serviceName>', 'service name (for Oracle)')
|
||||
.option('--auth-type <authType>', 'authentication type')
|
||||
.option('--use-ssl', 'use SSL connection')
|
||||
.option('--auto-index-foreign-keys', 'automatically adds indexes to all foreign keys')
|
||||
.option(
|
||||
'--load-data-condition <condition>',
|
||||
@@ -48,7 +54,7 @@ program
|
||||
.command('deploy <modelFolder>')
|
||||
.description('Deploys model to database')
|
||||
.action(modelFolder => {
|
||||
const { engine, server, user, password, database, transaction } = program.opts();
|
||||
const { engine, server, user, password, database, url, file, transaction } = program.opts();
|
||||
// const hooks = [];
|
||||
// if (program.autoIndexForeignKeys) hooks.push(dbmodel.hooks.autoIndexForeignKeys);
|
||||
|
||||
@@ -60,6 +66,13 @@ program
|
||||
user,
|
||||
password,
|
||||
database,
|
||||
databaseUrl: url,
|
||||
useDatabaseUrl: !!url,
|
||||
databaseFile: file,
|
||||
socketPath: program.socketPath,
|
||||
serviceName: program.serviceName,
|
||||
authType: program.authType,
|
||||
useSsl: program.useSsl,
|
||||
},
|
||||
modelFolder,
|
||||
useTransaction: transaction,
|
||||
|
||||
@@ -2,4 +2,5 @@ module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
moduleFileExtensions: ['js'],
|
||||
reporters: ['default', 'github-actions'],
|
||||
};
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
"name": "dbgate-filterparser",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "tsc --watch",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { arrayToHexString, evalFilterBehaviour, isTypeDateTime } from 'dbgate-tools';
|
||||
import { arrayToHexString, base64ToHex, evalFilterBehaviour, isTypeDateTime } from 'dbgate-tools';
|
||||
import { format, toDate } from 'date-fns';
|
||||
import _isString from 'lodash/isString';
|
||||
import _cloneDeepWith from 'lodash/cloneDeepWith';
|
||||
@@ -21,10 +21,13 @@ export function getFilterValueExpression(value, dataType?) {
|
||||
if (value === false) return 'FALSE';
|
||||
if (value.$oid) return `ObjectId("${value.$oid}")`;
|
||||
if (value.$bigint) return value.$bigint;
|
||||
if (value.$decimal) return value.$decimal;
|
||||
if (value.type == 'Buffer' && Array.isArray(value.data)) {
|
||||
return '0x' + arrayToHexString(value.data);
|
||||
}
|
||||
|
||||
if (value?.$binary?.base64) {
|
||||
return base64ToHex(value.$binary.base64);
|
||||
}
|
||||
return `="${value}"`;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import P from 'parsimmon';
|
||||
import moment from 'moment';
|
||||
import { Condition } from 'dbgate-sqltree';
|
||||
import { interpretEscapes, token, word, whitespace } from './common';
|
||||
import { hexStringToArray, parseNumberSafe } from 'dbgate-tools';
|
||||
import { hexToBase64, parseNumberSafe } from 'dbgate-tools';
|
||||
import { FilterBehaviour, TransformType } from 'dbgate-types';
|
||||
|
||||
const binaryCondition =
|
||||
@@ -385,10 +385,7 @@ const createParser = (filterBehaviour: FilterBehaviour) => {
|
||||
|
||||
hexstring: () =>
|
||||
token(P.regexp(/0x(([0-9a-fA-F][0-9a-fA-F])+)/, 1))
|
||||
.map(x => ({
|
||||
type: 'Buffer',
|
||||
data: hexStringToArray(x),
|
||||
}))
|
||||
.map(x => ({ $binary: { base64: hexToBase64(x) } }))
|
||||
.desc('hex string'),
|
||||
|
||||
noQuotedString: () => P.regexp(/[^\s^,^'^"]+/).desc('string unquoted'),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { SqlDumper } from 'dbgate-types';
|
||||
import { Command, Select, Update, Delete, Insert } from './types';
|
||||
import { dumpSqlExpression } from './dumpSqlExpression';
|
||||
import { dumpSqlFromDefinition, dumpSqlSourceRef } from './dumpSqlSource';
|
||||
import { dumpSqlFromDefinition, dumpSqlSourceDef, dumpSqlSourceRef } from './dumpSqlSource';
|
||||
import { dumpSqlCondition } from './dumpSqlCondition';
|
||||
|
||||
export function dumpSqlSelect(dmp: SqlDumper, cmd: Select) {
|
||||
@@ -115,7 +115,10 @@ export function dumpSqlInsert(dmp: SqlDumper, cmd: Insert) {
|
||||
cmd.fields.map(x => x.targetColumn)
|
||||
);
|
||||
dmp.putCollection(',', cmd.fields, x => dumpSqlExpression(dmp, x));
|
||||
if (dmp.dialect.requireFromDual) {
|
||||
if (cmd.whereNotExistsSource) {
|
||||
dmp.put(' ^from ');
|
||||
dumpSqlSourceDef(dmp, cmd.whereNotExistsSource);
|
||||
} else if (dmp.dialect.requireFromDual) {
|
||||
dmp.put(' ^from ^dual ');
|
||||
}
|
||||
dmp.put(' ^where ^not ^exists (^select * ^from %f ^where ', cmd.targetTable);
|
||||
|
||||
@@ -2,6 +2,7 @@ import _ from 'lodash';
|
||||
import type { SqlDumper } from 'dbgate-types';
|
||||
import { Expression, ColumnRefExpression } from './types';
|
||||
import { dumpSqlSourceRef } from './dumpSqlSource';
|
||||
import { dumpSqlSelect } from './dumpSqlCommand';
|
||||
|
||||
export function dumpSqlExpression(dmp: SqlDumper, expr: Expression) {
|
||||
switch (expr.exprType) {
|
||||
@@ -67,5 +68,11 @@ export function dumpSqlExpression(dmp: SqlDumper, expr: Expression) {
|
||||
});
|
||||
dmp.put(')');
|
||||
break;
|
||||
|
||||
case 'select':
|
||||
dmp.put('(');
|
||||
dumpSqlSelect(dmp, expr.select);
|
||||
dmp.put(')');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ function isLike(value, test) {
|
||||
function extractRawValue(value) {
|
||||
if (value?.$bigint) return value.$bigint;
|
||||
if (value?.$oid) return value.$oid;
|
||||
if (value?.$decimal) return value.$decimal;
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ export interface Insert {
|
||||
fields: UpdateField[];
|
||||
targetTable: NamedObjectInfo;
|
||||
insertWhereNotExistsCondition?: Condition;
|
||||
whereNotExistsSource?: Source;
|
||||
}
|
||||
|
||||
export interface AllowIdentityInsert {
|
||||
@@ -226,6 +227,11 @@ export interface RowNumberExpression {
|
||||
orderBy: OrderByExpression[];
|
||||
}
|
||||
|
||||
export interface SelectExpression {
|
||||
exprType: 'select';
|
||||
select: Select;
|
||||
}
|
||||
|
||||
export type Expression =
|
||||
| ColumnRefExpression
|
||||
| ValueExpression
|
||||
@@ -235,7 +241,8 @@ export type Expression =
|
||||
| CallExpression
|
||||
| MethodCallExpression
|
||||
| TranformExpression
|
||||
| RowNumberExpression;
|
||||
| RowNumberExpression
|
||||
| SelectExpression;
|
||||
export type OrderByExpression = Expression & { direction: 'ASC' | 'DESC' };
|
||||
|
||||
export type ResultField = Expression & { alias?: string };
|
||||
|
||||
@@ -2,4 +2,5 @@ module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
moduleFileExtensions: ['js'],
|
||||
reporters: ['default', 'github-actions'],
|
||||
};
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"typescript": "^4.4.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"dbgate-query-splitter": "^4.11.5",
|
||||
"blueimp-md5": "^2.19.0",
|
||||
"dbgate-query-splitter": "^4.11.9",
|
||||
"dbgate-sqltree": "^6.0.0-alpha.1",
|
||||
"debug": "^4.3.4",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
|
||||
@@ -49,6 +49,8 @@ export class DatabaseAnalyser<TClient = any> {
|
||||
singleObjectId: string = null;
|
||||
dialect: SqlDialect;
|
||||
logger: Logger;
|
||||
startedTm = Date.now();
|
||||
analyseIdentifier = Math.random().toString().substring(2);
|
||||
|
||||
constructor(public dbhan: DatabaseHandle<TClient>, public driver: EngineDriver, version) {
|
||||
this.dialect = (driver?.dialectByVersion && driver?.dialectByVersion(version)) || driver?.dialect;
|
||||
@@ -78,14 +80,24 @@ export class DatabaseAnalyser<TClient = any> {
|
||||
}
|
||||
|
||||
getLogDbInfo() {
|
||||
return this.driver.getLogDbInfo(this.dbhan);
|
||||
return {
|
||||
...this.driver.getLogDbInfo(this.dbhan),
|
||||
analyserTime: Date.now() - this.startedTm,
|
||||
analyseIdentifier: this.analyseIdentifier,
|
||||
};
|
||||
}
|
||||
|
||||
async fullAnalysis() {
|
||||
logger.debug(this.getLogDbInfo(), 'DBGM-00126 Performing full analysis');
|
||||
const res = this.addEngineField(await this._runAnalysis());
|
||||
try {
|
||||
const res = this.addEngineField(await this._runAnalysis());
|
||||
logger.debug(this.getLogDbInfo(), 'DBGM-00271 Full analysis finished successfully');
|
||||
return res;
|
||||
} catch (err) {
|
||||
logger.error(extractErrorLogData(err, this.getLogDbInfo()), 'DBGM-00272 Error during full analysis');
|
||||
throw err;
|
||||
}
|
||||
// console.log('FULL ANALYSIS', res);
|
||||
return res;
|
||||
}
|
||||
|
||||
async singleObjectAnalysis(name, typeField) {
|
||||
@@ -106,31 +118,40 @@ export class DatabaseAnalyser<TClient = any> {
|
||||
logger.info(this.getLogDbInfo(), 'DBGM-00127 Performing incremental analysis');
|
||||
this.structure = structure;
|
||||
|
||||
const modifications = await this.getModifications();
|
||||
if (modifications == null) {
|
||||
// modifications not implemented, perform full analysis
|
||||
this.structure = null;
|
||||
return this.addEngineField(await this._runAnalysis());
|
||||
}
|
||||
const structureModifications = modifications.filter(x => x.action != 'setTableRowCounts');
|
||||
const setTableRowCounts = modifications.find(x => x.action == 'setTableRowCounts');
|
||||
|
||||
let structureWithRowCounts = null;
|
||||
if (setTableRowCounts) {
|
||||
const newStructure = mergeTableRowCounts(structure, setTableRowCounts.rowCounts);
|
||||
if (areDifferentRowCounts(structure, newStructure)) {
|
||||
structureWithRowCounts = newStructure;
|
||||
try {
|
||||
const modifications = await this.getModifications();
|
||||
if (modifications == null) {
|
||||
// modifications not implemented, perform full analysis
|
||||
this.structure = null;
|
||||
return this.addEngineField(await this._runAnalysis());
|
||||
}
|
||||
}
|
||||
const structureModifications = modifications.filter(x => x.action != 'setTableRowCounts');
|
||||
const setTableRowCounts = modifications.find(x => x.action == 'setTableRowCounts');
|
||||
|
||||
if (structureModifications.length == 0) {
|
||||
return structureWithRowCounts ? this.addEngineField(structureWithRowCounts) : null;
|
||||
}
|
||||
let structureWithRowCounts = null;
|
||||
if (setTableRowCounts) {
|
||||
const newStructure = mergeTableRowCounts(structure, setTableRowCounts.rowCounts);
|
||||
if (areDifferentRowCounts(structure, newStructure)) {
|
||||
structureWithRowCounts = newStructure;
|
||||
}
|
||||
}
|
||||
|
||||
this.modifications = structureModifications;
|
||||
if (structureWithRowCounts) this.structure = structureWithRowCounts;
|
||||
logger.info({ ...this.getLogDbInfo(), modifications: this.modifications }, 'DBGM-00128 DB modifications detected');
|
||||
return this.addEngineField(this.mergeAnalyseResult(await this._runAnalysis()));
|
||||
if (structureModifications.length == 0) {
|
||||
logger.debug(this.getLogDbInfo(), 'DBGM-00267 No changes in database structure detected');
|
||||
return structureWithRowCounts ? this.addEngineField(structureWithRowCounts) : null;
|
||||
}
|
||||
|
||||
this.modifications = structureModifications;
|
||||
if (structureWithRowCounts) this.structure = structureWithRowCounts;
|
||||
logger.info(
|
||||
{ ...this.getLogDbInfo(), modifications: this.modifications },
|
||||
'DBGM-00128 DB modifications detected'
|
||||
);
|
||||
return this.addEngineField(this.mergeAnalyseResult(await this._runAnalysis()));
|
||||
} catch (err) {
|
||||
logger.error(extractErrorLogData(err, this.getLogDbInfo()), 'DBGM-00273 Error during incremental analysis');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
mergeAnalyseResult(newlyAnalysed) {
|
||||
@@ -143,6 +164,11 @@ export class DatabaseAnalyser<TClient = any> {
|
||||
|
||||
const res = {};
|
||||
for (const field of STRUCTURE_FIELDS) {
|
||||
const isAll = this.modifications.some(x => x.action == 'all' && x.objectTypeField == field);
|
||||
if (isAll) {
|
||||
res[field] = newlyAnalysed[field] || [];
|
||||
continue;
|
||||
}
|
||||
const removedIds = this.modifications
|
||||
.filter(x => x.action == 'remove' && x.objectTypeField == field)
|
||||
.map(x => x.objectId);
|
||||
|
||||
@@ -26,6 +26,7 @@ import _isDate from 'lodash/isDate';
|
||||
import _isArray from 'lodash/isArray';
|
||||
import _isPlainObject from 'lodash/isPlainObject';
|
||||
import _keys from 'lodash/keys';
|
||||
import _cloneDeep from 'lodash/cloneDeep';
|
||||
import uuidv1 from 'uuid/v1';
|
||||
|
||||
export class SqlDumper implements AlterProcessor {
|
||||
@@ -78,7 +79,16 @@ export class SqlDumper implements AlterProcessor {
|
||||
else if (_isNumber(value)) this.putRaw(value.toString());
|
||||
else if (_isDate(value)) this.putStringValue(new Date(value).toISOString());
|
||||
else if (value?.type == 'Buffer' && _isArray(value?.data)) this.putByteArrayValue(value?.data);
|
||||
else if (value?.$binary?.base64) {
|
||||
const binary = atob(value.$binary.base64);
|
||||
const bytes = new Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) {
|
||||
bytes[i] = binary.charCodeAt(i);
|
||||
}
|
||||
this.putByteArrayValue(bytes);
|
||||
}
|
||||
else if (value?.$bigint) this.putRaw(value?.$bigint);
|
||||
else if (value?.$decimal) this.putRaw(value?.$decimal);
|
||||
else if (_isPlainObject(value) || _isArray(value)) this.putStringValue(JSON.stringify(value));
|
||||
else this.put('^null');
|
||||
}
|
||||
@@ -658,6 +668,68 @@ export class SqlDumper implements AlterProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
sanitizeTableConstraints(table: TableInfo): TableInfo {
|
||||
// Create a deep copy of the table
|
||||
const sanitized = _cloneDeep(table);
|
||||
|
||||
// Get the set of existing column names
|
||||
const existingColumns = new Set(sanitized.columns.map(col => col.columnName));
|
||||
|
||||
// Filter primary key columns to only include existing columns
|
||||
if (sanitized.primaryKey) {
|
||||
const validPkColumns = sanitized.primaryKey.columns.filter(col => existingColumns.has(col.columnName));
|
||||
if (validPkColumns.length === 0) {
|
||||
// If no valid columns remain, remove the primary key entirely
|
||||
sanitized.primaryKey = null;
|
||||
} else if (validPkColumns.length < sanitized.primaryKey.columns.length) {
|
||||
// Update primary key with only valid columns
|
||||
sanitized.primaryKey = {
|
||||
...sanitized.primaryKey,
|
||||
columns: validPkColumns
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Filter sorting key columns to only include existing columns
|
||||
if (sanitized.sortingKey) {
|
||||
const validSkColumns = sanitized.sortingKey.columns.filter(col => existingColumns.has(col.columnName));
|
||||
if (validSkColumns.length === 0) {
|
||||
sanitized.sortingKey = null;
|
||||
} else if (validSkColumns.length < sanitized.sortingKey.columns.length) {
|
||||
sanitized.sortingKey = {
|
||||
...sanitized.sortingKey,
|
||||
columns: validSkColumns
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Filter foreign keys to only include those with all columns present
|
||||
if (sanitized.foreignKeys) {
|
||||
sanitized.foreignKeys = sanitized.foreignKeys.filter(fk =>
|
||||
fk.columns.every(col => existingColumns.has(col.columnName))
|
||||
);
|
||||
}
|
||||
|
||||
// Filter indexes to only include those with all columns present
|
||||
if (sanitized.indexes) {
|
||||
sanitized.indexes = sanitized.indexes.filter(idx =>
|
||||
idx.columns.every(col => existingColumns.has(col.columnName))
|
||||
);
|
||||
}
|
||||
|
||||
// Filter unique constraints to only include those with all columns present
|
||||
if (sanitized.uniques) {
|
||||
sanitized.uniques = sanitized.uniques.filter(uq =>
|
||||
uq.columns.every(col => existingColumns.has(col.columnName))
|
||||
);
|
||||
}
|
||||
|
||||
// Filter dependencies (references from other tables) - these should remain as-is
|
||||
// since they don't affect the CREATE TABLE statement for this table
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
recreateTable(oldTable: TableInfo, newTable: TableInfo) {
|
||||
if (!oldTable.pairingId || !newTable.pairingId || oldTable.pairingId != newTable.pairingId) {
|
||||
throw new Error('Recreate is not possible: oldTable.paringId != newTable.paringId');
|
||||
@@ -672,48 +744,51 @@ export class SqlDumper implements AlterProcessor {
|
||||
}))
|
||||
.filter(x => x.newcol);
|
||||
|
||||
// Create a sanitized version of newTable with constraints that only reference existing columns
|
||||
const sanitizedNewTable = this.sanitizeTableConstraints(newTable);
|
||||
|
||||
if (this.driver.supportsTransactions) {
|
||||
this.dropConstraints(oldTable, true);
|
||||
this.renameTable(oldTable, tmpTable);
|
||||
|
||||
this.createTable(newTable);
|
||||
this.createTable(sanitizedNewTable);
|
||||
|
||||
const autoinc = newTable.columns.find(x => x.autoIncrement);
|
||||
const autoinc = sanitizedNewTable.columns.find(x => x.autoIncrement);
|
||||
if (autoinc) {
|
||||
this.allowIdentityInsert(newTable, true);
|
||||
this.allowIdentityInsert(sanitizedNewTable, true);
|
||||
}
|
||||
|
||||
this.putCmd(
|
||||
'^insert ^into %f (%,i) select %,i ^from %f',
|
||||
newTable,
|
||||
sanitizedNewTable,
|
||||
columnPairs.map(x => x.newcol.columnName),
|
||||
columnPairs.map(x => x.oldcol.columnName),
|
||||
{ ...oldTable, pureName: tmpTable }
|
||||
);
|
||||
|
||||
if (autoinc) {
|
||||
this.allowIdentityInsert(newTable, false);
|
||||
this.allowIdentityInsert(sanitizedNewTable, false);
|
||||
}
|
||||
|
||||
if (this.dialect.dropForeignKey) {
|
||||
newTable.dependencies.forEach(cnt => this.createConstraint(cnt));
|
||||
sanitizedNewTable.dependencies.forEach(cnt => this.createConstraint(cnt));
|
||||
}
|
||||
|
||||
this.dropTable({ ...oldTable, pureName: tmpTable });
|
||||
} else {
|
||||
// we have to preserve old table as long as possible
|
||||
this.createTable({ ...newTable, pureName: tmpTable });
|
||||
this.createTable({ ...sanitizedNewTable, pureName: tmpTable });
|
||||
|
||||
this.putCmd(
|
||||
'^insert ^into %f (%,i) select %,s ^from %f',
|
||||
{ ...newTable, pureName: tmpTable },
|
||||
{ ...sanitizedNewTable, pureName: tmpTable },
|
||||
columnPairs.map(x => x.newcol.columnName),
|
||||
columnPairs.map(x => x.oldcol.columnName),
|
||||
oldTable
|
||||
);
|
||||
|
||||
this.dropTable(oldTable);
|
||||
this.renameTable({ ...newTable, pureName: tmpTable }, newTable.pureName);
|
||||
this.renameTable({ ...sanitizedNewTable, pureName: tmpTable }, newTable.pureName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -91,8 +91,8 @@ interface AlterOperation_RenameConstraint {
|
||||
}
|
||||
interface AlterOperation_RecreateTable {
|
||||
operationType: 'recreateTable';
|
||||
table: TableInfo;
|
||||
operations: AlterOperation[];
|
||||
oldTable: TableInfo;
|
||||
newTable: TableInfo;
|
||||
}
|
||||
interface AlterOperation_FillPreloadedRows {
|
||||
operationType: 'fillPreloadedRows';
|
||||
@@ -249,11 +249,11 @@ export class AlterPlan {
|
||||
});
|
||||
}
|
||||
|
||||
recreateTable(table: TableInfo, operations: AlterOperation[]) {
|
||||
recreateTable(oldTable: TableInfo, newTable: TableInfo) {
|
||||
this.operations.push({
|
||||
operationType: 'recreateTable',
|
||||
table,
|
||||
operations,
|
||||
oldTable,
|
||||
newTable,
|
||||
});
|
||||
this.recreates.tables += 1;
|
||||
}
|
||||
@@ -337,7 +337,13 @@ export class AlterPlan {
|
||||
return opRes;
|
||||
}),
|
||||
op,
|
||||
];
|
||||
].filter(op => {
|
||||
// filter duplicated drops
|
||||
const existingDrop = this.operations.find(
|
||||
o => o.operationType == 'dropConstraint' && o.oldObject === op['oldObject']
|
||||
);
|
||||
return existingDrop == null;
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -498,53 +504,121 @@ export class AlterPlan {
|
||||
return [];
|
||||
}
|
||||
|
||||
const table = this.wholeNewDb.tables.find(
|
||||
const oldTable = this.wholeOldDb.tables.find(
|
||||
x => x.pureName == op[objectField].pureName && x.schemaName == op[objectField].schemaName
|
||||
);
|
||||
const newTable = this.wholeNewDb.tables.find(
|
||||
x => x.pureName == op[objectField].pureName && x.schemaName == op[objectField].schemaName
|
||||
);
|
||||
this.recreates.tables += 1;
|
||||
return [
|
||||
{
|
||||
operationType: 'recreateTable',
|
||||
table,
|
||||
operations: [op],
|
||||
oldTable,
|
||||
newTable,
|
||||
// operations: [op],
|
||||
},
|
||||
];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
_groupTableRecreations(): AlterOperation[] {
|
||||
const res = [];
|
||||
const recreates = {};
|
||||
_removeRecreatedTableAlters(): AlterOperation[] {
|
||||
const res: AlterOperation[] = [];
|
||||
const recreates = new Set<string>();
|
||||
for (const op of this.operations) {
|
||||
if (op.operationType == 'recreateTable' && op.table) {
|
||||
const existingRecreate = recreates[`${op.table.schemaName}||${op.table.pureName}`];
|
||||
if (existingRecreate) {
|
||||
existingRecreate.operations.push(...op.operations);
|
||||
} else {
|
||||
const recreate = {
|
||||
...op,
|
||||
operations: [...op.operations],
|
||||
};
|
||||
res.push(recreate);
|
||||
recreates[`${op.table.schemaName}||${op.table.pureName}`] = recreate;
|
||||
}
|
||||
} else {
|
||||
// @ts-ignore
|
||||
const oldObject: TableInfo = op.oldObject || op.object;
|
||||
if (oldObject) {
|
||||
const recreated = recreates[`${oldObject.schemaName}||${oldObject.pureName}`];
|
||||
if (recreated) {
|
||||
recreated.operations.push(op);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
res.push(op);
|
||||
if (op.operationType == 'recreateTable' && op.oldTable && op.newTable) {
|
||||
const key = `${op.oldTable.schemaName}||${op.oldTable.pureName}`;
|
||||
recreates.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (const op of this.operations) {
|
||||
switch (op.operationType) {
|
||||
case 'createColumn':
|
||||
case 'createConstraint':
|
||||
{
|
||||
const key = `${op.newObject.schemaName}||${op.newObject.pureName}`;
|
||||
if (recreates.has(key)) {
|
||||
// skip create inside recreated table
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'dropColumn':
|
||||
case 'dropConstraint':
|
||||
case 'changeColumn':
|
||||
{
|
||||
const key = `${op.oldObject.schemaName}||${op.oldObject.pureName}`;
|
||||
if (recreates.has(key)) {
|
||||
// skip drop/change inside recreated table
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'renameColumn':
|
||||
{
|
||||
const key = `${op.object.schemaName}||${op.object.pureName}`;
|
||||
if (recreates.has(key)) {
|
||||
// skip rename inside recreated table
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
res.push(op);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
_groupTableRecreations(): AlterOperation[] {
|
||||
const res = [];
|
||||
const recreates = new Set<string>();
|
||||
for (const op of this.operations) {
|
||||
if (op.operationType == 'recreateTable' && op.oldTable && op.newTable) {
|
||||
const key = `${op.oldTable.schemaName}||${op.oldTable.pureName}`;
|
||||
if (recreates.has(key)) {
|
||||
// prevent duplicate recreates
|
||||
continue;
|
||||
}
|
||||
recreates.add(key);
|
||||
}
|
||||
|
||||
res.push(op);
|
||||
}
|
||||
return res;
|
||||
|
||||
// const res = [];
|
||||
// const recreates = {};
|
||||
// for (const op of this.operations) {
|
||||
// if (op.operationType == 'recreateTable' && op.table) {
|
||||
// const existingRecreate = recreates[`${op.table.schemaName}||${op.table.pureName}`];
|
||||
// if (existingRecreate) {
|
||||
// existingRecreate.operations.push(...op.operations);
|
||||
// } else {
|
||||
// const recreate = {
|
||||
// ...op,
|
||||
// operations: [...op.operations],
|
||||
// };
|
||||
// res.push(recreate);
|
||||
// recreates[`${op.table.schemaName}||${op.table.pureName}`] = recreate;
|
||||
// }
|
||||
// } else {
|
||||
// // @ts-ignore
|
||||
// const oldObject: TableInfo = op.oldObject || op.object;
|
||||
// if (oldObject) {
|
||||
// const recreated = recreates[`${oldObject.schemaName}||${oldObject.pureName}`];
|
||||
// if (recreated) {
|
||||
// recreated.operations.push(op);
|
||||
// continue;
|
||||
// }
|
||||
// }
|
||||
// res.push(op);
|
||||
// }
|
||||
// }
|
||||
// return res;
|
||||
}
|
||||
|
||||
_moveForeignKeysToLast(): AlterOperation[] {
|
||||
if (!this.dialect.createForeignKey) {
|
||||
return this.operations;
|
||||
@@ -611,6 +685,8 @@ export class AlterPlan {
|
||||
|
||||
// console.log('*****************OPERATIONS3', this.operations);
|
||||
|
||||
this.operations = this._removeRecreatedTableAlters();
|
||||
|
||||
this.operations = this._moveForeignKeysToLast();
|
||||
|
||||
// console.log('*****************OPERATIONS4', this.operations);
|
||||
@@ -673,16 +749,16 @@ export function runAlterOperation(op: AlterOperation, processor: AlterProcessor)
|
||||
break;
|
||||
case 'recreateTable':
|
||||
{
|
||||
const oldTable = generateTablePairingId(op.table);
|
||||
const newTable = _.cloneDeep(oldTable);
|
||||
const newDb = DatabaseAnalyser.createEmptyStructure();
|
||||
newDb.tables.push(newTable);
|
||||
// console.log('////////////////////////////newTable1', newTable);
|
||||
op.operations.forEach(child => runAlterOperation(child, new DatabaseInfoAlterProcessor(newDb)));
|
||||
// console.log('////////////////////////////op.operations', op.operations);
|
||||
// console.log('////////////////////////////op.table', op.table);
|
||||
// console.log('////////////////////////////newTable2', newTable);
|
||||
processor.recreateTable(oldTable, newTable);
|
||||
// const oldTable = generateTablePairingId(op.table);
|
||||
// const newTable = _.cloneDeep(oldTable);
|
||||
// const newDb = DatabaseAnalyser.createEmptyStructure();
|
||||
// newDb.tables.push(newTable);
|
||||
// // console.log('////////////////////////////newTable1', newTable);
|
||||
// op.operations.forEach(child => runAlterOperation(child, new DatabaseInfoAlterProcessor(newDb)));
|
||||
// // console.log('////////////////////////////op.operations', op.operations);
|
||||
// // console.log('////////////////////////////op.table', op.table);
|
||||
// // console.log('////////////////////////////newTable2', newTable);
|
||||
processor.recreateTable(op.oldTable, op.newTable);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -60,4 +60,4 @@ export function chooseTopTables(tables: TableInfo[], count: number, tableFilter:
|
||||
|
||||
export const DIAGRAM_ZOOMS = [0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.8, 1, 1.25, 1.5, 1.75, 2];
|
||||
|
||||
export const DIAGRAM_DEFAULT_WATERMARK = 'Powered by [dbgate.io](https://dbgate.io)';
|
||||
export const DIAGRAM_DEFAULT_WATERMARK = 'Powered by [dbgate.io](https://www.dbgate.io)';
|
||||
|
||||
@@ -47,6 +47,7 @@ export const mongoFilterBehaviour: FilterBehaviour = {
|
||||
allowStringToken: true,
|
||||
allowNumberDualTesting: true,
|
||||
allowObjectIdTesting: true,
|
||||
allowHexString: true,
|
||||
};
|
||||
|
||||
export const evalFilterBehaviour: FilterBehaviour = {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import _cloneDeep from 'lodash/cloneDeep';
|
||||
import _uniq from 'lodash/uniq';
|
||||
import _isString from 'lodash/isString';
|
||||
import type {
|
||||
ColumnInfo,
|
||||
@@ -75,9 +76,27 @@ export function findForeignKeyForColumn(table: TableInfo, column: ColumnInfo | s
|
||||
return (table.foreignKeys || []).find(fk => fk.columns.find(col => col.columnName == column.columnName));
|
||||
}
|
||||
|
||||
export function getConflictingColumnNames(columns: ColumnInfo[]): Set<string> {
|
||||
const conflictingNames = new Set(
|
||||
_uniq(columns.map(x => x.columnName).filter((item, index, arr) => arr.indexOf(item) !== index))
|
||||
);
|
||||
return conflictingNames;
|
||||
}
|
||||
|
||||
export function makeUniqueColumnNames(res: ColumnInfo[]) {
|
||||
const usedNames = new Set();
|
||||
const conflictingNames = getConflictingColumnNames(res);
|
||||
for (let i = 0; i < res.length; i++) {
|
||||
if (
|
||||
conflictingNames.has(res[i].columnName) &&
|
||||
res[i].pureName &&
|
||||
!usedNames.has(`${res[i].pureName}_${res[i].columnName}`)
|
||||
) {
|
||||
res[i].columnName = `${res[i].pureName}_${res[i].columnName}`;
|
||||
usedNames.add(res[i].columnName);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (usedNames.has(res[i].columnName)) {
|
||||
let suffix = 2;
|
||||
while (usedNames.has(`${res[i].columnName}${suffix}`)) suffix++;
|
||||
@@ -111,3 +130,20 @@ export function fillConstraintNames(table: TableInfo, dialect: SqlDialect) {
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export const DATA_FOLDER_NAMES = [
|
||||
{ name: 'sql', label: 'SQL scripts' },
|
||||
{ name: 'shell', label: 'Shell scripts' },
|
||||
{ name: 'markdown', label: 'Markdown files' },
|
||||
{ name: 'charts', label: 'Charts' },
|
||||
{ name: 'query', label: 'Query designs' },
|
||||
{ name: 'sqlite', label: 'SQLite files' },
|
||||
{ name: 'duckdb', label: 'DuckDB files' },
|
||||
{ name: 'diagrams', label: 'Diagrams' },
|
||||
{ name: 'perspectives', label: 'Perspectives' },
|
||||
{ name: 'impexp', label: 'Import/Export jobs' },
|
||||
{ name: 'modtrans', label: 'Model transforms' },
|
||||
{ name: 'datadeploy', label: 'Data deploy jobs' },
|
||||
{ name: 'dbcompare', label: 'Database compare jobs' },
|
||||
{ name: 'apps', label: 'Applications' },
|
||||
];
|
||||
|
||||
@@ -9,6 +9,7 @@ import _isEmpty from 'lodash/isEmpty';
|
||||
import _omitBy from 'lodash/omitBy';
|
||||
import { DataEditorTypesBehaviour } from 'dbgate-types';
|
||||
import isPlainObject from 'lodash/isPlainObject';
|
||||
import md5 from 'blueimp-md5';
|
||||
|
||||
export const MAX_GRID_TEXT_LENGTH = 1000; // maximum length of text in grid cell, longer text is truncated
|
||||
|
||||
@@ -42,6 +43,20 @@ export function hexStringToArray(inputString) {
|
||||
return res;
|
||||
}
|
||||
|
||||
export function base64ToHex(base64String) {
|
||||
const binaryString = atob(base64String);
|
||||
const hexString = Array.from(binaryString, c => c.charCodeAt(0).toString(16).padStart(2, '0')).join('');
|
||||
return '0x' + hexString.toUpperCase();
|
||||
}
|
||||
|
||||
export function hexToBase64(hexString) {
|
||||
const binaryString = hexString
|
||||
.match(/.{1,2}/g)
|
||||
.map(byte => String.fromCharCode(parseInt(byte, 16)))
|
||||
.join('');
|
||||
return btoa(binaryString);
|
||||
}
|
||||
|
||||
export function parseCellValue(value, editorTypes?: DataEditorTypesBehaviour) {
|
||||
if (!_isString(value)) return value;
|
||||
|
||||
@@ -53,8 +68,9 @@ export function parseCellValue(value, editorTypes?: DataEditorTypesBehaviour) {
|
||||
const mHex = value.match(/^0x([0-9a-fA-F][0-9a-fA-F])+$/);
|
||||
if (mHex) {
|
||||
return {
|
||||
type: 'Buffer',
|
||||
data: hexStringToArray(value.substring(2)),
|
||||
$binary: {
|
||||
base64: hexToBase64(value.substring(2)),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -185,6 +201,26 @@ function stringifyJsonToGrid(value): ReturnType<typeof stringifyCellValue> {
|
||||
return { value: '(JSON)', gridStyle: 'nullCellStyle' };
|
||||
}
|
||||
|
||||
function formatNumberCustomSeparator(value, thousandsSeparator) {
|
||||
const [intPart, decPart] = value.split('.');
|
||||
const intPartWithSeparator = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, thousandsSeparator);
|
||||
return decPart ? `${intPartWithSeparator}.${decPart}` : intPartWithSeparator;
|
||||
}
|
||||
|
||||
function formatCellNumber(value, gridFormattingOptions?: { thousandsSeparator?: string }) {
|
||||
const separator = gridFormattingOptions?.thousandsSeparator;
|
||||
if (_isNumber(value)) {
|
||||
if (separator === 'none' || (value < 1000 && value > -1000)) return value.toString();
|
||||
if (separator === 'system') return value.toLocaleString();
|
||||
}
|
||||
// fallback for system locale
|
||||
if (separator === 'space' || separator === 'system') return formatNumberCustomSeparator(value.toString(), ' ');
|
||||
if (separator === 'narrowspace') return formatNumberCustomSeparator(value.toString(), '\u202F');
|
||||
if (separator === 'comma') return formatNumberCustomSeparator(value.toString(), ',');
|
||||
if (separator === 'dot') return formatNumberCustomSeparator(value.toString(), '.');
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
export function stringifyCellValue(
|
||||
value,
|
||||
intent:
|
||||
@@ -195,7 +231,7 @@ export function stringifyCellValue(
|
||||
| 'exportIntent'
|
||||
| 'clipboardIntent',
|
||||
editorTypes?: DataEditorTypesBehaviour,
|
||||
gridFormattingOptions?: { useThousandsSeparator?: boolean },
|
||||
gridFormattingOptions?: { thousandsSeparator?: string },
|
||||
jsonParsedValue?: any
|
||||
): {
|
||||
value: string;
|
||||
@@ -229,11 +265,26 @@ export function stringifyCellValue(
|
||||
if (value === true) return { value: 'true', gridStyle: 'valueCellStyle' };
|
||||
if (value === false) return { value: 'false', gridStyle: 'valueCellStyle' };
|
||||
|
||||
if (editorTypes?.parseHexAsBuffer) {
|
||||
if (value?.type == 'Buffer' && _isArray(value.data)) {
|
||||
return { value: '0x' + arrayToHexString(value.data), gridStyle: 'valueCellStyle' };
|
||||
}
|
||||
if (value?.$binary?.base64) {
|
||||
return {
|
||||
value: base64ToHex(value.$binary.base64),
|
||||
gridStyle: 'valueCellStyle',
|
||||
};
|
||||
}
|
||||
|
||||
if (value?.$decimal) {
|
||||
return {
|
||||
value: formatCellNumber(value.$decimal, gridFormattingOptions),
|
||||
gridStyle: 'valueCellStyle',
|
||||
};
|
||||
}
|
||||
|
||||
if (editorTypes?.parseHexAsBuffer) {
|
||||
// if (value?.type == 'Buffer' && _isArray(value.data)) {
|
||||
// return { value: '0x' + arrayToHexString(value.data), gridStyle: 'valueCellStyle' };
|
||||
// }
|
||||
}
|
||||
|
||||
if (editorTypes?.parseObjectIdAsDollar) {
|
||||
if (value?.$oid) {
|
||||
switch (intent) {
|
||||
@@ -247,13 +298,13 @@ export function stringifyCellValue(
|
||||
}
|
||||
if (value?.$bigint) {
|
||||
return {
|
||||
value: value.$bigint,
|
||||
value: formatCellNumber(value.$bigint, gridFormattingOptions),
|
||||
gridStyle: 'valueCellStyle',
|
||||
};
|
||||
}
|
||||
if (typeof value === 'bigint') {
|
||||
return {
|
||||
value: value.toString(),
|
||||
value: formatCellNumber(value.toString(), gridFormattingOptions),
|
||||
gridStyle: 'valueCellStyle',
|
||||
};
|
||||
}
|
||||
@@ -328,13 +379,8 @@ export function stringifyCellValue(
|
||||
if (_isNumber(value)) {
|
||||
switch (intent) {
|
||||
case 'gridCellIntent':
|
||||
return {
|
||||
value:
|
||||
gridFormattingOptions?.useThousandsSeparator && (value >= 10000 || value <= -10000)
|
||||
? value.toLocaleString()
|
||||
: value.toString(),
|
||||
gridStyle: 'valueCellStyle',
|
||||
};
|
||||
const separator = gridFormattingOptions?.thousandsSeparator;
|
||||
return { value: formatCellNumber(value, gridFormattingOptions), gridStyle: 'valueCellStyle' };
|
||||
default:
|
||||
return { value: value.toString() };
|
||||
}
|
||||
@@ -386,6 +432,9 @@ export function safeJsonParse(json, defaultValue?, logError = false) {
|
||||
if (_isArray(json) || _isPlainObject(json)) {
|
||||
return json;
|
||||
}
|
||||
if (!json) {
|
||||
return defaultValue;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(json);
|
||||
} catch (err) {
|
||||
@@ -423,6 +472,9 @@ export function shouldOpenMultilineDialog(value) {
|
||||
if (value?.$bigint) {
|
||||
return false;
|
||||
}
|
||||
if (value?.$decimal) {
|
||||
return false;
|
||||
}
|
||||
if (_isPlainObject(value) || _isArray(value)) {
|
||||
return true;
|
||||
}
|
||||
@@ -478,6 +530,9 @@ export function getAsImageSrc(obj) {
|
||||
if (obj?.type == 'Buffer' && _isArray(obj?.data)) {
|
||||
return `data:image/png;base64, ${arrayBufferToBase64(obj?.data)}`;
|
||||
}
|
||||
if (obj?.$binary?.base64) {
|
||||
return `data:image/png;base64, ${obj.$binary.base64}`;
|
||||
}
|
||||
|
||||
if (_isString(obj) && (obj.startsWith('http://') || obj.startsWith('https://'))) {
|
||||
return obj;
|
||||
@@ -670,6 +725,9 @@ export function deserializeJsTypesFromJsonParse(obj) {
|
||||
if (value?.$bigint) {
|
||||
return BigInt(value.$bigint);
|
||||
}
|
||||
if (value?.$decimal) {
|
||||
return value.$decimal;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -684,6 +742,9 @@ export function deserializeJsTypesReviver(key, value) {
|
||||
if (value?.$bigint) {
|
||||
return BigInt(value.$bigint);
|
||||
}
|
||||
if (value?.$decimal) {
|
||||
return value.$decimal;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -734,3 +795,12 @@ export function setSqlFrontMatter(text: string, data: { [key: string]: any }, ya
|
||||
const frontMatterContent = `-- >>>\n${yamlContentMapped}\n-- <<<\n`;
|
||||
return frontMatterContent + (textClean || '');
|
||||
}
|
||||
|
||||
export function shortenIdentifier(s: string, maxLength?: number) {
|
||||
if (!maxLength || maxLength < 10) return s;
|
||||
if (s.length <= maxLength) return s;
|
||||
const hash = md5(s).substring(0, 8);
|
||||
const partLength = Math.floor((maxLength - 9) / 2);
|
||||
const restLength = maxLength - 10 - partLength;
|
||||
return s.substring(0, partLength) + '_' + hash + '_' + s.substring(s.length - restLength);
|
||||
}
|
||||
|
||||
@@ -57,6 +57,12 @@ export function compilePermissions(permissions: string[] | string): CompiledPerm
|
||||
return res;
|
||||
}
|
||||
|
||||
export function getPermissionsCacheKey(permissions: string[] | string) {
|
||||
if (!permissions) return null;
|
||||
if (_isString(permissions)) return permissions;
|
||||
return permissions.join('|');
|
||||
}
|
||||
|
||||
export function testPermission(tested: string, permissions: CompiledPermissions) {
|
||||
let allow = true;
|
||||
|
||||
@@ -103,9 +109,25 @@ export function getPredefinedPermissions(predefinedRoleName: string) {
|
||||
case 'superadmin':
|
||||
return ['*', '~widgets/*', 'widgets/admin', 'widgets/database', '~all-connections'];
|
||||
case 'logged-user':
|
||||
return ['*', '~widgets/admin', '~admin/*', '~internal-storage', '~all-connections'];
|
||||
return [
|
||||
'*',
|
||||
'~widgets/admin',
|
||||
'~admin/*',
|
||||
'~internal-storage',
|
||||
'~all-connections',
|
||||
'~run-shell-script',
|
||||
'~all-team-files/*',
|
||||
];
|
||||
case 'anonymous-user':
|
||||
return ['*', '~widgets/admin', '~admin/*', '~internal-storage', '~all-connections'];
|
||||
return [
|
||||
'*',
|
||||
'~widgets/admin',
|
||||
'~admin/*',
|
||||
'~internal-storage',
|
||||
'~all-connections',
|
||||
'~run-shell-script',
|
||||
'~all-team-files/*',
|
||||
];
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
50
packages/types/appdefs.d.ts
vendored
50
packages/types/appdefs.d.ts
vendored
@@ -1,12 +1,12 @@
|
||||
interface ApplicationCommand {
|
||||
name: string;
|
||||
sql: string;
|
||||
}
|
||||
// interface ApplicationCommand {
|
||||
// name: string;
|
||||
// sql: string;
|
||||
// }
|
||||
|
||||
interface ApplicationQuery {
|
||||
name: string;
|
||||
sql: string;
|
||||
}
|
||||
// interface ApplicationQuery {
|
||||
// name: string;
|
||||
// sql: string;
|
||||
// }
|
||||
|
||||
interface VirtualReferenceDefinition {
|
||||
pureName: string;
|
||||
@@ -27,11 +27,31 @@ interface DictionaryDescriptionDefinition {
|
||||
delimiter: string;
|
||||
}
|
||||
|
||||
export interface ApplicationDefinition {
|
||||
name: string;
|
||||
|
||||
queries: ApplicationQuery[];
|
||||
commands: ApplicationCommand[];
|
||||
virtualReferences: VirtualReferenceDefinition[];
|
||||
dictionaryDescriptions: DictionaryDescriptionDefinition[];
|
||||
interface ApplicationUsageRule {
|
||||
conditionGroup?: string;
|
||||
serverHostsRegex?: string;
|
||||
serverHostsList?: string[];
|
||||
databaseNamesRegex?: string;
|
||||
databaseNamesList?: string[];
|
||||
tableNamesRegex?: string;
|
||||
tableNamesList?: string[];
|
||||
columnNamesRegex?: string;
|
||||
columnNamesList?: string[];
|
||||
}
|
||||
|
||||
export interface ApplicationDefinition {
|
||||
appid: string;
|
||||
applicationName: string;
|
||||
applicationIcon?: string;
|
||||
applicationColor?: string;
|
||||
usageRules?: ApplicationUsageRule[];
|
||||
files?: {
|
||||
[key: string]: {
|
||||
label: string;
|
||||
sql: string;
|
||||
type: 'query' | 'command';
|
||||
};
|
||||
};
|
||||
virtualReferences?: VirtualReferenceDefinition[];
|
||||
dictionaryDescriptions?: DictionaryDescriptionDefinition[];
|
||||
}
|
||||
|
||||
29
packages/types/dbinfo.d.ts
vendored
29
packages/types/dbinfo.d.ts
vendored
@@ -22,7 +22,7 @@ export interface ColumnsConstraintInfo extends ConstraintInfo {
|
||||
columns: ColumnReference[];
|
||||
}
|
||||
|
||||
export interface PrimaryKeyInfo extends ColumnsConstraintInfo {}
|
||||
export interface PrimaryKeyInfo extends ColumnsConstraintInfo { }
|
||||
|
||||
export interface ForeignKeyInfo extends ColumnsConstraintInfo {
|
||||
refSchemaName?: string;
|
||||
@@ -39,7 +39,7 @@ export interface IndexInfo extends ColumnsConstraintInfo {
|
||||
filterDefinition?: string;
|
||||
}
|
||||
|
||||
export interface UniqueInfo extends ColumnsConstraintInfo {}
|
||||
export interface UniqueInfo extends ColumnsConstraintInfo { }
|
||||
|
||||
export interface CheckInfo extends ConstraintInfo {
|
||||
definition: string;
|
||||
@@ -77,6 +77,7 @@ export interface DatabaseObjectInfo extends NamedObjectInfo {
|
||||
hashCode?: string;
|
||||
objectTypeField?: string;
|
||||
objectComment?: string;
|
||||
tablePermissionRole?: 'read' | 'update_only' | 'create_update_delete' | 'deny';
|
||||
}
|
||||
|
||||
export interface SqlObjectInfo extends DatabaseObjectInfo {
|
||||
@@ -134,7 +135,7 @@ export interface CallableObjectInfo extends SqlObjectInfo {
|
||||
parameters?: ParameterInfo[];
|
||||
}
|
||||
|
||||
export interface ProcedureInfo extends CallableObjectInfo {}
|
||||
export interface ProcedureInfo extends CallableObjectInfo { }
|
||||
|
||||
export interface FunctionInfo extends CallableObjectInfo {
|
||||
returnType?: string;
|
||||
@@ -145,17 +146,17 @@ export interface TriggerInfo extends SqlObjectInfo {
|
||||
functionName?: string;
|
||||
tableName?: string;
|
||||
triggerTiming?:
|
||||
| 'BEFORE'
|
||||
| 'AFTER'
|
||||
| 'INSTEAD OF'
|
||||
| 'BEFORE EACH ROW'
|
||||
| 'INSTEAD OF'
|
||||
| 'AFTER EACH ROW'
|
||||
| 'AFTER STATEMENT'
|
||||
| 'BEFORE STATEMENT'
|
||||
| 'AFTER EVENT'
|
||||
| 'BEFORE EVENT'
|
||||
| null;
|
||||
| 'BEFORE'
|
||||
| 'AFTER'
|
||||
| 'INSTEAD OF'
|
||||
| 'BEFORE EACH ROW'
|
||||
| 'INSTEAD OF'
|
||||
| 'AFTER EACH ROW'
|
||||
| 'AFTER STATEMENT'
|
||||
| 'BEFORE STATEMENT'
|
||||
| 'AFTER EVENT'
|
||||
| 'BEFORE EVENT'
|
||||
| null;
|
||||
triggerLevel?: 'ROW' | 'STATEMENT';
|
||||
eventType?: 'INSERT' | 'UPDATE' | 'DELETE' | 'TRUNCATE';
|
||||
}
|
||||
|
||||
1
packages/types/dialect.d.ts
vendored
1
packages/types/dialect.d.ts
vendored
@@ -22,6 +22,7 @@ export interface SqlDialect {
|
||||
requireStandaloneSelectForScopeIdentity?: boolean;
|
||||
allowMultipleValuesInsert?: boolean;
|
||||
useServerDatabaseFile?: boolean;
|
||||
maxIdentifierLength?: number;
|
||||
|
||||
dropColumnDependencies?: string[];
|
||||
changeColumnDependencies?: string[];
|
||||
|
||||
1
packages/types/dumper.d.ts
vendored
1
packages/types/dumper.d.ts
vendored
@@ -16,6 +16,7 @@ export interface SqlDumper extends AlterProcessor {
|
||||
transform(type: TransformType, dumpExpr: () => void);
|
||||
createDatabase(name: string);
|
||||
dropDatabase(name: string);
|
||||
comment(value: string);
|
||||
|
||||
callableTemplate(func: CallableObjectInfo);
|
||||
|
||||
|
||||
147
packages/types/engines.d.ts
vendored
147
packages/types/engines.d.ts
vendored
@@ -21,6 +21,7 @@ export interface StreamOptions {
|
||||
error?: (error) => void;
|
||||
done?: (result) => void;
|
||||
info?: (info) => void;
|
||||
changedCurrentDatabase?: (database: string) => void;
|
||||
}
|
||||
|
||||
export type CollectionOperationInfo =
|
||||
@@ -99,19 +100,46 @@ export interface SupportedDbKeyType {
|
||||
showItemList?: boolean;
|
||||
}
|
||||
|
||||
export type DatabaseProcess = {
|
||||
processId: number;
|
||||
connectionId: number;
|
||||
client: string;
|
||||
operation?: string;
|
||||
namespace?: string;
|
||||
command?: any;
|
||||
runningTime: number;
|
||||
state?: any;
|
||||
waitingFor?: boolean;
|
||||
locks?: any;
|
||||
progress?: any;
|
||||
};
|
||||
|
||||
export type DatabaseVariable = {
|
||||
variable: string;
|
||||
value: any;
|
||||
};
|
||||
|
||||
export interface SqlBackupDumper {
|
||||
run();
|
||||
}
|
||||
|
||||
export interface SummaryColumn {
|
||||
fieldName: string;
|
||||
header: string;
|
||||
dataType: 'string' | 'number' | 'bytes';
|
||||
export interface ServerSummaryDatabases {
|
||||
rows: any[];
|
||||
columns: SummaryDatabaseColumn[];
|
||||
}
|
||||
export interface ServerSummaryDatabase {}
|
||||
|
||||
export type SummaryDatabaseColumn = {
|
||||
header: string;
|
||||
fieldName: string;
|
||||
type: 'data' | 'fileSize';
|
||||
filterable?: boolean;
|
||||
sortable?: boolean;
|
||||
};
|
||||
|
||||
export interface ServerSummary {
|
||||
columns: SummaryColumn[];
|
||||
databases: ServerSummaryDatabase[];
|
||||
processes: DatabaseProcess[];
|
||||
variables: DatabaseVariable[];
|
||||
databases: ServerSummaryDatabases;
|
||||
}
|
||||
|
||||
export type CollectionAggregateFunction = 'count' | 'sum' | 'avg' | 'min' | 'max';
|
||||
@@ -161,12 +189,12 @@ export interface FilterBehaviourProvider {
|
||||
getFilterBehaviour(dataType: string, standardFilterBehaviours: { [id: string]: FilterBehaviour }): FilterBehaviour;
|
||||
}
|
||||
|
||||
export interface DatabaseHandle<TClient = any> {
|
||||
export interface DatabaseHandle<TClient = any, TDataBase = any> {
|
||||
client: TClient;
|
||||
database?: string;
|
||||
conid?: string;
|
||||
feedback?: (message: any) => void;
|
||||
getDatabase?: () => any;
|
||||
getDatabase?: () => TDataBase;
|
||||
connectionType?: string;
|
||||
treeKeySeparator?: string;
|
||||
}
|
||||
@@ -196,7 +224,7 @@ export interface RestoreDatabaseSettings extends BackupRestoreSettingsBase {
|
||||
inputFile: string;
|
||||
}
|
||||
|
||||
export interface EngineDriver<TClient = any> extends FilterBehaviourProvider {
|
||||
export interface EngineDriver<TClient = any, TDataBase = any> extends FilterBehaviourProvider {
|
||||
engine: string;
|
||||
title: string;
|
||||
defaultPort?: number;
|
||||
@@ -210,6 +238,7 @@ export interface EngineDriver<TClient = any> extends FilterBehaviourProvider {
|
||||
supportsDatabaseRestore?: boolean;
|
||||
supportsServerSummary?: boolean;
|
||||
supportsDatabaseProfiler?: boolean;
|
||||
supportsIncrementalAnalysis?: boolean;
|
||||
requiresDefaultSortCriteria?: boolean;
|
||||
profilerFormatterFunction?: string;
|
||||
profilerTimestampFunction?: string;
|
||||
@@ -242,61 +271,88 @@ export interface EngineDriver<TClient = any> extends FilterBehaviourProvider {
|
||||
defaultSocketPath?: string;
|
||||
authTypeLabel?: string;
|
||||
importExportArgs?: any[];
|
||||
connect({ server, port, user, password, database, connectionDefinition }): Promise<DatabaseHandle<TClient>>;
|
||||
close(dbhan: DatabaseHandle<TClient>): Promise<any>;
|
||||
query(dbhan: DatabaseHandle<TClient>, sql: string, options?: QueryOptions): Promise<QueryResult>;
|
||||
stream(dbhan: DatabaseHandle<TClient>, sql: string, options: StreamOptions);
|
||||
readQuery(dbhan: DatabaseHandle<TClient>, sql: string, structure?: TableInfo): Promise<StreamResult>;
|
||||
readJsonQuery(dbhan: DatabaseHandle<TClient>, query: any, structure?: TableInfo): Promise<StreamResult>;
|
||||
connect({
|
||||
server,
|
||||
port,
|
||||
user,
|
||||
password,
|
||||
database,
|
||||
connectionDefinition,
|
||||
}): Promise<DatabaseHandle<TClient, TDataBase>>;
|
||||
close(dbhan: DatabaseHandle<TClient, TDataBase>): Promise<any>;
|
||||
query(dbhan: DatabaseHandle<TClient, TDataBase>, sql: string, options?: QueryOptions): Promise<QueryResult>;
|
||||
stream(dbhan: DatabaseHandle<TClient, TDataBase>, sql: string, options: StreamOptions);
|
||||
readQuery(dbhan: DatabaseHandle<TClient, TDataBase>, sql: string, structure?: TableInfo): Promise<StreamResult>;
|
||||
readJsonQuery(dbhan: DatabaseHandle<TClient, TDataBase>, query: any, structure?: TableInfo): Promise<StreamResult>;
|
||||
// eg. PostgreSQL COPY FROM stdin
|
||||
writeQueryFromStream(dbhan: DatabaseHandle<TClient>, sql: string): Promise<StreamResult>;
|
||||
writeTable(dbhan: DatabaseHandle<TClient>, name: NamedObjectInfo, options: WriteTableOptions): Promise<StreamResult>;
|
||||
writeQueryFromStream(dbhan: DatabaseHandle<TClient, TDataBase>, sql: string): Promise<StreamResult>;
|
||||
writeTable(
|
||||
dbhan: DatabaseHandle<TClient, TDataBase>,
|
||||
name: NamedObjectInfo,
|
||||
options: WriteTableOptions
|
||||
): Promise<StreamResult>;
|
||||
analyseSingleObject(
|
||||
dbhan: DatabaseHandle<TClient>,
|
||||
dbhan: DatabaseHandle<TClient, TDataBase>,
|
||||
name: NamedObjectInfo,
|
||||
objectTypeField: keyof DatabaseInfo
|
||||
): Promise<TableInfo | ViewInfo | ProcedureInfo | FunctionInfo | TriggerInfo>;
|
||||
analyseSingleTable(dbhan: DatabaseHandle<TClient>, name: NamedObjectInfo): Promise<TableInfo>;
|
||||
getVersion(dbhan: DatabaseHandle<TClient>): Promise<{ version: string; versionText?: string }>;
|
||||
listDatabases(dbhan: DatabaseHandle<TClient>): Promise<
|
||||
analyseSingleTable(dbhan: DatabaseHandle<TClient, TDataBase>, name: NamedObjectInfo): Promise<TableInfo>;
|
||||
getVersion(dbhan: DatabaseHandle<TClient, TDataBase>): Promise<{ version: string; versionText?: string }>;
|
||||
listDatabases(dbhan: DatabaseHandle<TClient, TDataBase>): Promise<
|
||||
{
|
||||
name: string;
|
||||
sizeOnDisk?: number;
|
||||
empty?: boolean;
|
||||
}[]
|
||||
>;
|
||||
loadKeys(dbhan: DatabaseHandle<TClient>, root: string, filter?: string): Promise;
|
||||
scanKeys(dbhan: DatabaseHandle<TClient>, root: string, pattern: string, cursor: string, count: number): Promise;
|
||||
exportKeys(dbhan: DatabaseHandle<TClient>, options: {}): Promise;
|
||||
loadKeyInfo(dbhan: DatabaseHandle<TClient>, key): Promise;
|
||||
loadKeyTableRange(dbhan: DatabaseHandle<TClient>, key, cursor, count): Promise;
|
||||
loadKeys(dbhan: DatabaseHandle<TClient, TDataBase>, root: string, filter?: string): Promise;
|
||||
scanKeys(
|
||||
dbhan: DatabaseHandle<TClient, TDataBase>,
|
||||
root: string,
|
||||
pattern: string,
|
||||
cursor: string,
|
||||
count: number
|
||||
): Promise;
|
||||
exportKeys(dbhan: DatabaseHandle<TClient, TDataBase>, options: {}): Promise;
|
||||
loadKeyInfo(dbhan: DatabaseHandle<TClient, TDataBase>, key): Promise;
|
||||
loadKeyTableRange(dbhan: DatabaseHandle<TClient, TDataBase>, key, cursor, count): Promise;
|
||||
loadFieldValues(
|
||||
dbhan: DatabaseHandle<TClient>,
|
||||
dbhan: DatabaseHandle<TClient, TDataBase>,
|
||||
name: NamedObjectInfo,
|
||||
field: string,
|
||||
search: string,
|
||||
dataType: string
|
||||
): Promise;
|
||||
analyseFull(dbhan: DatabaseHandle<TClient>, serverVersion): Promise<DatabaseInfo>;
|
||||
analyseIncremental(dbhan: DatabaseHandle<TClient>, structure: DatabaseInfo, serverVersion): Promise<DatabaseInfo>;
|
||||
analyseFull(dbhan: DatabaseHandle<TClient, TDataBase>, serverVersion): Promise<DatabaseInfo>;
|
||||
analyseIncremental(
|
||||
dbhan: DatabaseHandle<TClient, TDataBase>,
|
||||
structure: DatabaseInfo,
|
||||
serverVersion
|
||||
): Promise<DatabaseInfo>;
|
||||
dialect: SqlDialect;
|
||||
dialectByVersion(version): SqlDialect;
|
||||
createDumper(options = null): SqlDumper;
|
||||
createBackupDumper(dbhan: DatabaseHandle<TClient>, options): Promise<SqlBackupDumper>;
|
||||
createBackupDumper(dbhan: DatabaseHandle<TClient, TDataBase>, options): Promise<SqlBackupDumper>;
|
||||
getAuthTypes(): EngineAuthType[];
|
||||
readCollection(dbhan: DatabaseHandle<TClient>, options: ReadCollectionOptions): Promise<any>;
|
||||
updateCollection(dbhan: DatabaseHandle<TClient>, changeSet: any): Promise<any>;
|
||||
readCollection(dbhan: DatabaseHandle<TClient, TDataBase>, options: ReadCollectionOptions): Promise<any>;
|
||||
updateCollection(dbhan: DatabaseHandle<TClient, TDataBase>, changeSet: any): Promise<any>;
|
||||
getCollectionUpdateScript(changeSet: any, collectionInfo: CollectionInfo): string;
|
||||
createDatabase(dbhan: DatabaseHandle<TClient>, name: string): Promise;
|
||||
dropDatabase(dbhan: DatabaseHandle<TClient>, name: string): Promise;
|
||||
createDatabase(dbhan: DatabaseHandle<TClient, TDataBase>, name: string): Promise;
|
||||
dropDatabase(dbhan: DatabaseHandle<TClient, TDataBase>, name: string): Promise;
|
||||
getQuerySplitterOptions(usage: 'stream' | 'script' | 'editor' | 'import'): any;
|
||||
script(dbhan: DatabaseHandle<TClient>, sql: string, options?: RunScriptOptions): Promise;
|
||||
operation(dbhan: DatabaseHandle<TClient>, operation: CollectionOperationInfo, options?: RunScriptOptions): Promise;
|
||||
script(dbhan: DatabaseHandle<TClient, TDataBase>, sql: string, options?: RunScriptOptions): Promise;
|
||||
operation(
|
||||
dbhan: DatabaseHandle<TClient, TDataBase>,
|
||||
operation: CollectionOperationInfo,
|
||||
options?: RunScriptOptions
|
||||
): Promise;
|
||||
getNewObjectTemplates(): NewObjectTemplate[];
|
||||
// direct call of dbhan.client method, only some methods could be supported, on only some drivers
|
||||
callMethod(dbhan: DatabaseHandle<TClient>, method, args);
|
||||
serverSummary(dbhan: DatabaseHandle<TClient>): Promise<ServerSummary>;
|
||||
summaryCommand(dbhan: DatabaseHandle<TClient>, command, row): Promise<void>;
|
||||
startProfiler(dbhan: DatabaseHandle<TClient>, options): Promise<any>;
|
||||
stopProfiler(dbhan: DatabaseHandle<TClient>, profiler): Promise<void>;
|
||||
callMethod(dbhan: DatabaseHandle<TClient, TDataBase>, method, args);
|
||||
serverSummary(dbhan: DatabaseHandle<TClient, TDataBase>): Promise<ServerSummary>;
|
||||
summaryCommand(dbhan: DatabaseHandle<TClient, TDataBase>, command, row): Promise<void>;
|
||||
startProfiler(dbhan: DatabaseHandle<TClient, TDataBase>, options): Promise<any>;
|
||||
stopProfiler(dbhan: DatabaseHandle<TClient, TDataBase>, profiler): Promise<void>;
|
||||
getRedirectAuthUrl(connection, options): Promise<{ url: string; sid: string }>;
|
||||
getAuthTokenFromCode(connection, options): Promise<string>;
|
||||
getAccessTokenFromAuth(connection, req): Promise<string | null>;
|
||||
@@ -313,7 +369,10 @@ export interface EngineDriver<TClient = any> extends FilterBehaviourProvider {
|
||||
adaptTableInfo(table: TableInfo): TableInfo;
|
||||
// simple data type adapter
|
||||
adaptDataType(dataType: string): string;
|
||||
listSchemas(dbhan: DatabaseHandle<TClient>): Promise<SchemaInfo[] | null>;
|
||||
listSchemas(dbhan: DatabaseHandle<TClient, TDataBase>): Promise<SchemaInfo[] | null>;
|
||||
listProcesses(dbhan: DatabaseHandle<TClient, TDataBase>): Promise<DatabaseProcess[] | null>;
|
||||
listVariables(dbhan: DatabaseHandle<TClient, TDataBase>): Promise<DatabaseVariable[] | null>;
|
||||
killProcess(dbhan: DatabaseHandle<TClient, TDataBase>, pid: number): Promise<any>;
|
||||
backupDatabaseCommand(
|
||||
connection: any,
|
||||
settings: BackupDatabaseSettings,
|
||||
@@ -337,7 +396,7 @@ export interface EngineDriver<TClient = any> extends FilterBehaviourProvider {
|
||||
analyserClass?: any;
|
||||
dumperClass?: any;
|
||||
singleConnectionOnly?: boolean;
|
||||
getLogDbInfo(dbhan: DatabaseHandle<TClient>): {
|
||||
getLogDbInfo(dbhan: DatabaseHandle<TClient, TDataBase>): {
|
||||
database?: string;
|
||||
engine: string;
|
||||
conid?: string;
|
||||
|
||||
2
packages/types/test-engines.d.ts
vendored
2
packages/types/test-engines.d.ts
vendored
@@ -96,4 +96,6 @@ export type TestEngineInfo = {
|
||||
}>;
|
||||
|
||||
objects?: Array<TestObjectInfo>;
|
||||
|
||||
binaryDataType?: string;
|
||||
};
|
||||
|
||||
@@ -26,12 +26,23 @@
|
||||
|
||||
<script lang="javascript">
|
||||
window.dbgate_page = '{{page}}';
|
||||
</script>
|
||||
|
||||
if (localStorage.getItem('currentThemeType') == 'dark') {
|
||||
document.documentElement.style.setProperty('--theme-background', '#111');
|
||||
document.documentElement.style.setProperty('--theme-foreground', '#e3e3e3');
|
||||
} else {
|
||||
document.documentElement.style.setProperty('--theme-background', '#fff');
|
||||
document.documentElement.style.setProperty('--theme-foreground', '#262626');
|
||||
}
|
||||
</script>
|
||||
|
||||
<script defer src="build/bundle.js"></script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
background-color: var(--theme-background);
|
||||
}
|
||||
|
||||
.lds-ellipsis {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
@@ -44,7 +55,7 @@
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
background: #000;
|
||||
background: var(--theme-foreground);
|
||||
animation-timing-function: cubic-bezier(0, 1, 1, 0);
|
||||
}
|
||||
.lds-ellipsis div:nth-child(1) {
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"build:index": "node build-index.js",
|
||||
"prepublishOnly": "yarn build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"files": [
|
||||
"public"
|
||||
],
|
||||
@@ -28,7 +32,7 @@
|
||||
"chartjs-plugin-datalabels": "^2.2.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"dbgate-datalib": "^6.0.0-alpha.1",
|
||||
"dbgate-query-splitter": "^4.11.5",
|
||||
"dbgate-query-splitter": "^4.11.9",
|
||||
"dbgate-sqltree": "^6.0.0-alpha.1",
|
||||
"dbgate-tools": "^6.0.0-alpha.1",
|
||||
"dbgate-types": "^6.0.0-alpha.1",
|
||||
@@ -60,6 +64,9 @@
|
||||
"uuid": "^3.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@langchain/core": "^0.3.72",
|
||||
"@langchain/langgraph": "^0.4.9",
|
||||
"@langchain/openai": "^0.6.9",
|
||||
"@messageformat/core": "^3.4.0",
|
||||
"chartjs-plugin-zoom": "^1.2.0",
|
||||
"date-fns": "^4.1.0",
|
||||
@@ -71,6 +78,7 @@
|
||||
"leaflet": "^1.8.0",
|
||||
"openai": "^5.10.1",
|
||||
"wellknown": "^0.5.0",
|
||||
"xml-formatter": "^3.6.4"
|
||||
"xml-formatter": "^3.6.4",
|
||||
"zod": "^4.1.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,3 +30,20 @@
|
||||
.color-icon-inv-red {
|
||||
color: var(--theme-icon-inv-red);
|
||||
}
|
||||
|
||||
.premium-background-gradient {
|
||||
background: linear-gradient(135deg, #1686c8, #8a25b1);
|
||||
}
|
||||
|
||||
.premium-gradient {
|
||||
background: linear-gradient(135deg, #1686c8, #8a25b1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.web-color-primary {
|
||||
background: #1686c8;
|
||||
}
|
||||
|
||||
.web-color-secondary {
|
||||
background: #8a25b1;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ export default [
|
||||
{
|
||||
input: 'src/query/QueryParserWorker.js',
|
||||
output: {
|
||||
sourcemap: true,
|
||||
sourcemap: !production,
|
||||
format: 'iife',
|
||||
file: 'public/build/query-parser-worker.js',
|
||||
},
|
||||
@@ -56,7 +56,7 @@ export default [
|
||||
{
|
||||
input: 'src/main.ts',
|
||||
output: {
|
||||
sourcemap: true,
|
||||
sourcemap: !production,
|
||||
format: 'iife',
|
||||
name: 'app',
|
||||
file: 'public/build/bundle.js',
|
||||
|
||||
@@ -20,14 +20,14 @@
|
||||
installNewVolatileConnectionListener,
|
||||
refreshPublicCloudFiles,
|
||||
} from './utility/api';
|
||||
import { getConfig, getSettings, getUsedApps } from './utility/metadataLoaders';
|
||||
import { getAllApps, getConfig, getSettings } from './utility/metadataLoaders';
|
||||
import AppTitleProvider from './utility/AppTitleProvider.svelte';
|
||||
import getElectron from './utility/getElectron';
|
||||
import AppStartInfo from './widgets/AppStartInfo.svelte';
|
||||
import SettingsListener from './utility/SettingsListener.svelte';
|
||||
import { handleAuthOnStartup } from './clientAuth';
|
||||
import { initializeAppUpdates } from './utility/appUpdate';
|
||||
import { _t } from './translations';
|
||||
import { _t, getCurrentTranslations, saveSelectedLanguageToCache } from './translations';
|
||||
import { installCloudListeners } from './utility/cloudListeners';
|
||||
|
||||
export let isAdminPage = false;
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
const connections = await apiCall('connections/list');
|
||||
const settings = await getSettings();
|
||||
const apps = await getUsedApps();
|
||||
const apps = await getAllApps();
|
||||
const loadedApiValue = !!(settings && connections && config && apps);
|
||||
|
||||
if (loadedApiValue) {
|
||||
@@ -61,6 +61,13 @@
|
||||
initializeAppUpdates();
|
||||
installCloudListeners();
|
||||
refreshPublicCloudFiles();
|
||||
saveSelectedLanguageToCache(config.preferrendLanguage);
|
||||
|
||||
const electron = getElectron();
|
||||
if (electron) {
|
||||
electron.send('translation-data', JSON.stringify(getCurrentTranslations()));
|
||||
global.TRANSLATION_DATA = getCurrentTranslations();
|
||||
}
|
||||
}
|
||||
|
||||
loadedApi = loadedApiValue;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import ErrorInfo from './elements/ErrorInfo.svelte';
|
||||
import { isOneOfPage } from './utility/pageDefs';
|
||||
import { openWebLink } from './utility/simpleTools';
|
||||
import FontIcon from './icons/FontIcon.svelte';
|
||||
|
||||
const config = useConfig();
|
||||
const values = writable({ amoid: null, databaseServer: null });
|
||||
@@ -22,17 +23,15 @@
|
||||
$: trialDaysLeft = $config?.trialDaysLeft;
|
||||
|
||||
let errorMessage = '';
|
||||
let expiredMessageSet = false;
|
||||
|
||||
$: if (isExpired && !expiredMessageSet) {
|
||||
errorMessage = 'Your license is expired';
|
||||
expiredMessageSet = true;
|
||||
}
|
||||
let isInsertingLicense = false;
|
||||
|
||||
$: trialButtonAvailable = !isExpired && trialDaysLeft == null;
|
||||
|
||||
// $: console.log('CONFIG', $config);
|
||||
|
||||
$: {
|
||||
if ($config?.isLicenseValid) {
|
||||
if ($config?.isLicenseValid && trialDaysLeft == null) {
|
||||
internalRedirectTo(isOneOfPage('admin-license') ? '/admin.html' : '/index.html');
|
||||
}
|
||||
}
|
||||
@@ -41,83 +40,124 @@
|
||||
<FormProviderCore {values}>
|
||||
<SpecialPageLayout>
|
||||
{#if getElectron() || ($config?.storageDatabase && hasPermission('admin/license'))}
|
||||
<div class="heading">License</div>
|
||||
<FormTextAreaField label="Enter your license key" name="licenseKey" rows={5} />
|
||||
<div class="heading">Thank you for using DbGate!</div>
|
||||
|
||||
<div class="submit">
|
||||
<FormSubmit
|
||||
value="Save license"
|
||||
on:click={async e => {
|
||||
sessionStorage.setItem('continueTrialConfirmed', '1');
|
||||
const { licenseKey } = e.detail;
|
||||
const resp = await apiCall('config/save-license-key', { licenseKey, tryToRenew: true });
|
||||
if (resp?.status == 'ok') {
|
||||
internalRedirectTo(isOneOfPage('admin-license') ? '/admin.html' : '/index.html');
|
||||
} else {
|
||||
errorMessage = resp?.errorMessage || 'Error saving license key';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{#if isExpired}
|
||||
<div class="infotext"><FontIcon icon="img warn" /> Your license has expired. Please insert new license.</div>
|
||||
{:else if trialDaysLeft > 0}
|
||||
<div class="infotext">
|
||||
<FontIcon icon="img warn" /> Your trial period will expire in {trialDaysLeft} day{trialDaysLeft != 1
|
||||
? 's'
|
||||
: ''}.
|
||||
</div>
|
||||
{:else}
|
||||
<div class="infotext">
|
||||
<FontIcon icon="img info" /> Proceed by selecting a licensing option or providing your license key.
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !isExpired && trialDaysLeft == null}
|
||||
{#if isInsertingLicense}
|
||||
<FormTextAreaField label="Enter your license key" name="licenseKey" rows={5} />
|
||||
|
||||
<div class="submit">
|
||||
<div class="flex flex1">
|
||||
<div class="col-6 flex">
|
||||
<FormSubmit
|
||||
value="Save license"
|
||||
on:click={async e => {
|
||||
sessionStorage.setItem('continueTrialConfirmed', '1');
|
||||
const { licenseKey } = e.detail;
|
||||
const resp = await apiCall('config/save-license-key', { licenseKey, tryToRenew: true });
|
||||
if (resp?.status == 'ok') {
|
||||
internalRedirectTo(isOneOfPage('admin-license') ? '/admin.html' : '/index.html');
|
||||
} else {
|
||||
errorMessage = resp?.errorMessage || 'Error saving license key';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="col-6 flex">
|
||||
<FormStyledButton
|
||||
value="Cancel"
|
||||
on:click={() => {
|
||||
isInsertingLicense = false;
|
||||
errorMessage = '';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !isInsertingLicense}
|
||||
<div class="submit">
|
||||
<FormStyledButton
|
||||
value="Start 30-day trial"
|
||||
on:click={async e => {
|
||||
errorMessage = '';
|
||||
const license = await apiCall('config/start-trial');
|
||||
if (license?.status == 'ok') {
|
||||
value="Insert license key"
|
||||
on:click={() => {
|
||||
isInsertingLicense = true;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if trialButtonAvailable}
|
||||
<div class="submit">
|
||||
<FormStyledButton
|
||||
value="Start 30-day trial"
|
||||
on:click={async e => {
|
||||
errorMessage = '';
|
||||
const license = await apiCall('config/start-trial');
|
||||
if (license?.status == 'ok') {
|
||||
sessionStorage.setItem('continueTrialConfirmed', '1');
|
||||
internalRedirectTo(isOneOfPage('admin-license') ? '/admin.html' : '/index.html');
|
||||
} else {
|
||||
errorMessage = license?.errorMessage || 'Error starting trial';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if trialDaysLeft > 0}
|
||||
<div class="submit">
|
||||
<FormStyledButton
|
||||
value={`Continue trial (${trialDaysLeft} days left)`}
|
||||
on:click={async e => {
|
||||
sessionStorage.setItem('continueTrialConfirmed', '1');
|
||||
internalRedirectTo(isOneOfPage('admin-license') ? '/admin.html' : '/index.html');
|
||||
} else {
|
||||
errorMessage = license?.errorMessage || 'Error starting trial';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if trialDaysLeft > 0}
|
||||
<div class="submit">
|
||||
<FormStyledButton
|
||||
value={`Continue trial (${trialDaysLeft} days left)`}
|
||||
value="Purchase DbGate Premium"
|
||||
on:click={async e => {
|
||||
sessionStorage.setItem('continueTrialConfirmed', '1');
|
||||
internalRedirectTo(isOneOfPage('admin-license') ? '/admin.html' : '/index.html');
|
||||
// openWebLink(
|
||||
// `https://auth.dbgate.eu/create-checkout-session-simple?source=trial-${isExpired ? 'expired' : (trialDaysLeft ?? 'no')}`
|
||||
// );
|
||||
|
||||
// openWebLink(
|
||||
// `https://auth-proxy.dbgate.udolni.net/redirect-to-purchase?product=${getElectron() ? 'premium' : 'teram-premium'}&source=trial-${isExpired ? 'expired' : (trialDaysLeft ?? 'no')}`
|
||||
// );
|
||||
|
||||
openWebLink(
|
||||
`https://auth.dbgate.eu/redirect-to-purchase?product=${getElectron() ? 'premium' : 'team-premium'}&source=trial-${isExpired ? 'expired' : (trialDaysLeft ?? 'no')}`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="submit">
|
||||
<FormStyledButton
|
||||
value="Purchase DbGate Premium"
|
||||
on:click={async e => {
|
||||
// openWebLink(
|
||||
// `https://auth.dbgate.eu/create-checkout-session-simple?source=trial-${isExpired ? 'expired' : (trialDaysLeft ?? 'no')}`
|
||||
// );
|
||||
|
||||
// openWebLink(
|
||||
// `https://auth-proxy.dbgate.udolni.net/redirect-to-purchase?product=${getElectron() ? 'premium' : 'teram-premium'}&source=trial-${isExpired ? 'expired' : (trialDaysLeft ?? 'no')}`
|
||||
// );
|
||||
|
||||
openWebLink(
|
||||
`https://auth.dbgate.eu/redirect-to-purchase?product=${getElectron() ? 'premium' : 'team-premium'}&source=trial-${isExpired ? 'expired' : (trialDaysLeft ?? 'no')}`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if getElectron()}
|
||||
<div class="submit">
|
||||
<FormStyledButton
|
||||
value="Exit"
|
||||
on:click={e => {
|
||||
getElectron().send('quit-app');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{#if getElectron()}
|
||||
<div class="submit">
|
||||
<FormStyledButton
|
||||
value="Exit"
|
||||
on:click={e => {
|
||||
getElectron().send('quit-app');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if errorMessage}
|
||||
@@ -125,8 +165,8 @@
|
||||
{/if}
|
||||
|
||||
<div class="purchase-info">
|
||||
For more info about DbGate licensing, you could visit <Link href="https://dbgate.eu/">dbgate.eu</Link> web or contact
|
||||
us at <Link href="mailto:sales@dbgate.eu">sales@dbgate.eu</Link>
|
||||
For more info about DbGate licensing, you could visit <Link href="https://dbgate.io/">dbgate.io</Link> web or contact
|
||||
us at <Link href="mailto:sales@dbgate.io">sales@dbgate.io</Link>
|
||||
</div>
|
||||
{:else}
|
||||
<ErrorInfo message="License for DbGate is not valid. Please contact administrator." />
|
||||
@@ -141,6 +181,10 @@
|
||||
font-size: xx-large;
|
||||
}
|
||||
|
||||
.infotext {
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
.submit {
|
||||
margin: var(--dim-large-form-margin);
|
||||
display: flex;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="heading">Configuration error</div>
|
||||
{#if $config?.checkedLicense?.status == 'error'}
|
||||
<ErrorInfo
|
||||
message={`Invalid license. Please contact sales@dbgate.eu for more details. ${$config?.checkedLicense?.error}`}
|
||||
message={`Invalid license. Please contact sales@dbgate.io for more details. ${$config?.checkedLicense?.error || ''}`}
|
||||
/>
|
||||
{:else if $config?.configurationError}
|
||||
<ErrorInfo message={$config?.configurationError} />
|
||||
|
||||
39
packages/web/src/admin/FolderPermissionChooser.svelte
Normal file
39
packages/web/src/admin/FolderPermissionChooser.svelte
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang='ts'>
|
||||
import PermissionCheckBox from './PermissionCheckBox.svelte';
|
||||
import { getFormContext } from '../forms/FormProviderCore.svelte';
|
||||
|
||||
const { values } = getFormContext();
|
||||
|
||||
export let onSetPermission;
|
||||
export let label;
|
||||
export let folder;
|
||||
export let disabled = false;
|
||||
</script>
|
||||
|
||||
<PermissionCheckBox
|
||||
{label}
|
||||
permission={`files/${folder}/*`}
|
||||
permissions={$values.permissions}
|
||||
basePermissions={$values.basePermissions}
|
||||
{onSetPermission}
|
||||
{disabled}
|
||||
/>
|
||||
|
||||
<div class="ml-4">
|
||||
<PermissionCheckBox
|
||||
label="Read"
|
||||
permission={`files/${folder}/read`}
|
||||
permissions={$values.permissions}
|
||||
basePermissions={$values.basePermissions}
|
||||
{onSetPermission}
|
||||
{disabled}
|
||||
/>
|
||||
<PermissionCheckBox
|
||||
label="Write"
|
||||
permission={`files/${folder}/write`}
|
||||
permissions={$values.permissions}
|
||||
basePermissions={$values.basePermissions}
|
||||
{onSetPermission}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
1
packages/web/src/ai/QueryAiAssistant.svelte
Normal file
1
packages/web/src/ai/QueryAiAssistant.svelte
Normal file
@@ -0,0 +1 @@
|
||||
This component is only for Premium edition
|
||||
@@ -53,14 +53,15 @@
|
||||
import InputTextModal from '../modals/InputTextModal.svelte';
|
||||
import ConfirmModal from '../modals/ConfirmModal.svelte';
|
||||
import { apiCall } from '../utility/api';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let data;
|
||||
|
||||
const handleRename = () => {
|
||||
showModal(InputTextModal, {
|
||||
value: data.fileName,
|
||||
label: 'New file name',
|
||||
header: 'Rename file',
|
||||
label: _t('appFile.newFileName', { defaultMessage: 'New file name' }),
|
||||
header: _t('appFile.renameFile', { defaultMessage: 'Rename file' }),
|
||||
onConfirm: newFile => {
|
||||
apiCall('apps/rename-file', {
|
||||
file: data.fileName,
|
||||
@@ -74,7 +75,7 @@
|
||||
|
||||
const handleDelete = () => {
|
||||
showModal(ConfirmModal, {
|
||||
message: `Really delete file ${data.fileName}?`,
|
||||
message: _t('appFile.deleteFileConfirm', { defaultMessage: 'Really delete file {fileName}?', values: { fileName: data.fileName } }),
|
||||
onConfirm: () => {
|
||||
apiCall('apps/delete-file', {
|
||||
file: data.fileName,
|
||||
@@ -101,10 +102,10 @@
|
||||
|
||||
function createMenu() {
|
||||
return [
|
||||
{ text: 'Delete', onClick: handleDelete },
|
||||
{ text: 'Rename', onClick: handleRename },
|
||||
data.fileType.endsWith('.sql') && { text: 'Open SQL', onClick: handleOpenSqlFile },
|
||||
data.fileType.endsWith('.json') && { text: 'Open JSON', onClick: handleOpenJsonFile },
|
||||
{ text: _t('common.delete', { defaultMessage: 'Delete' }), onClick: handleDelete },
|
||||
{ text: _t('common.rename', { defaultMessage: 'Rename' }), onClick: handleRename },
|
||||
data.fileType.endsWith('.sql') && { text: _t('common.openSql', { defaultMessage: 'Open SQL' }), onClick: handleOpenSqlFile },
|
||||
data.fileType.endsWith('.json') && { text: _t('common.openJson', { defaultMessage: 'Open JSON' }), onClick: handleOpenJsonFile },
|
||||
|
||||
// data.fileType.endsWith('.yaml') && { text: 'Open YAML', onClick: handleOpenYamlFile },
|
||||
];
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import InputTextModal from '../modals/InputTextModal.svelte';
|
||||
import { apiCall } from '../utility/api';
|
||||
import { useConnectionList } from '../utility/metadataLoaders';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -34,8 +35,8 @@
|
||||
|
||||
showModal(InputTextModal, {
|
||||
value: name,
|
||||
label: 'New application name',
|
||||
header: 'Rename application',
|
||||
label: _t('appFolder.newApplicationName', { defaultMessage: 'New application name' }),
|
||||
header: _t('appFolder.renameApplication', { defaultMessage: 'Rename application' }),
|
||||
onConfirm: async newFolder => {
|
||||
await apiCall('apps/rename-folder', {
|
||||
folder: data.name,
|
||||
@@ -60,16 +61,16 @@
|
||||
|
||||
function createMenu() {
|
||||
return [
|
||||
{ text: 'Delete', onClick: handleDelete },
|
||||
{ text: 'Rename', onClick: handleRename },
|
||||
{ text: _t('common.delete', { defaultMessage: 'Delete' }), onClick: handleDelete },
|
||||
{ text: _t('common.rename', { defaultMessage: 'Rename' }), onClick: handleRename },
|
||||
|
||||
$currentDatabase && [
|
||||
!isOnCurrentDb($currentDatabase, $connections) && {
|
||||
text: 'Enable on current database',
|
||||
text: _t('appFolder.enableOnCurrentDatabase', { defaultMessage: 'Enable on current database' }),
|
||||
onClick: () => setOnCurrentDb(true),
|
||||
},
|
||||
isOnCurrentDb($currentDatabase, $connections) && {
|
||||
text: 'Disable on current database',
|
||||
text: _t('appFolder.disableOnCurrentDatabase', { defaultMessage: 'Disable on current database' }),
|
||||
onClick: () => setOnCurrentDb(false),
|
||||
},
|
||||
],
|
||||
@@ -90,7 +91,7 @@
|
||||
title={data.name}
|
||||
icon={'img app'}
|
||||
statusIcon={isOnCurrentDb($currentDatabase, $connections) ? 'icon check' : null}
|
||||
statusTitle={`Application ${data.name} is used for database ${$currentDatabase?.name}`}
|
||||
statusTitle={_t('appFolder.applicationUsedForDatabase', { defaultMessage: 'Application {application} is used for database {database}', values: { application: data.name, database: $currentDatabase?.name } })}
|
||||
isBold={data.name == $currentApplication}
|
||||
on:click={() => ($currentApplication = data.name)}
|
||||
menu={createMenu}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
export let filter = null;
|
||||
export let disableHover = false;
|
||||
export let divProps = {};
|
||||
export let additionalIcons = null;
|
||||
|
||||
$: isChecked =
|
||||
checkedObjectsStore && $checkedObjectsStore.find(x => module?.extractKey(data) == module?.extractKey(x));
|
||||
@@ -160,6 +161,11 @@
|
||||
/>
|
||||
</span>
|
||||
{/if}
|
||||
{#if additionalIcons}
|
||||
{#each additionalIcons as ic}
|
||||
<FontIcon icon={ic.icon} title={ic.title} colorClass={ic.colorClass} />
|
||||
{/each}
|
||||
{/if}
|
||||
{#if extInfo}
|
||||
<span class="ext-info">
|
||||
<TokenizedFilteredText text={extInfo} {filter} />
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div on:drop={handleDrop}>
|
||||
<div on:drop={handleDrop} data-testid={`app-object-group-items-${_.kebabCase(group)}`}>
|
||||
{#each items as item}
|
||||
<AppObjectListItem
|
||||
isHidden={!item.isMatched}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import Link from '../elements/Link.svelte';
|
||||
import { focusedConnectionOrDatabase } from '../stores';
|
||||
import { tick } from 'svelte';
|
||||
import { _tval } from '../translations';
|
||||
|
||||
export let list;
|
||||
export let module;
|
||||
@@ -38,8 +39,19 @@
|
||||
|
||||
$: matcher = module.createMatcher && module.createMatcher(filter, passProps?.searchSettings);
|
||||
|
||||
$: listTranslated = (list || []).map(data => ({
|
||||
...data,
|
||||
group: data?.group && _tval(data.group),
|
||||
title: data?.title && _tval(data.title),
|
||||
description: data?.description && _tval(data.description),
|
||||
args: (data?.args || []).map(x => ({
|
||||
...x,
|
||||
label: x?.label && _tval(x.label),
|
||||
})),
|
||||
}));
|
||||
|
||||
$: dataLabeled = _.compact(
|
||||
(list || []).map(data => {
|
||||
(listTranslated || []).map(data => {
|
||||
const matchResult = matcher ? matcher(data) : true;
|
||||
|
||||
let isMatched = true;
|
||||
@@ -102,7 +114,8 @@
|
||||
|
||||
$: groups = groupFunc ? extendGroups(_.groupBy(dataLabeled, 'group'), emptyGroupNames) : null;
|
||||
|
||||
$: listLimited = isExpandedBySearch && !expandLimited ? filtered.slice(0, filter.trim().length < 3 ? 1 : 3) : list;
|
||||
$: listLimited =
|
||||
isExpandedBySearch && !expandLimited ? filtered.slice(0, filter.trim().length < 3 ? 1 : 3) : listTranslated;
|
||||
$: isListLimited = isExpandedBySearch && listLimited.length < filtered.length;
|
||||
$: listMissingItems = isListLimited ? filtered.slice(listLimited.length) : [];
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
import { apiCall } from '../utility/api';
|
||||
import { openImportExportTab } from '../utility/importExportTools';
|
||||
import { isProApp } from '../utility/proTools';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let data;
|
||||
$: isZipped = data.folderName?.endsWith('.zip');
|
||||
@@ -89,8 +90,8 @@
|
||||
const handleRename = () => {
|
||||
showModal(InputTextModal, {
|
||||
value: data.fileName,
|
||||
label: 'New file name',
|
||||
header: 'Rename file',
|
||||
label: _t('archiveFile.newFileName', { defaultMessage: 'New file name' }),
|
||||
header: _t('archiveFile.renameFile', { defaultMessage: 'Rename file' }),
|
||||
onConfirm: newFile => {
|
||||
apiCall('archive/rename-file', {
|
||||
file: data.fileName,
|
||||
@@ -104,7 +105,7 @@
|
||||
|
||||
const handleDelete = () => {
|
||||
showModal(ConfirmModal, {
|
||||
message: `Really delete file ${data.fileName}?`,
|
||||
message: _t('archiveFile.deleteFileConfirm', { defaultMessage: 'Really delete file {fileName}?', values: { fileName: data.fileName } }),
|
||||
onConfirm: () => {
|
||||
apiCall('archive/delete-file', {
|
||||
file: data.fileName,
|
||||
@@ -147,10 +148,10 @@
|
||||
}
|
||||
|
||||
return [
|
||||
data.fileType == 'jsonl' && { text: 'Open', onClick: handleOpenArchive },
|
||||
data.fileType == 'jsonl' && { text: 'Open in text editor', onClick: handleOpenJsonLinesText },
|
||||
!isZipped && { text: 'Delete', onClick: handleDelete },
|
||||
!isZipped && { text: 'Rename', onClick: handleRename },
|
||||
data.fileType == 'jsonl' && { text: _t('common.open', { defaultMessage: 'Open' }), onClick: handleOpenArchive },
|
||||
data.fileType == 'jsonl' && { text: _t('common.openInTextEditor', { defaultMessage: 'Open in text editor' }), onClick: handleOpenJsonLinesText },
|
||||
!isZipped && { text: _t('common.delete', { defaultMessage: 'Delete' }), onClick: handleDelete },
|
||||
!isZipped && { text: _t('common.rename', { defaultMessage: 'Rename' }), onClick: handleRename },
|
||||
data.fileType == 'jsonl' &&
|
||||
createQuickExportMenu(
|
||||
fmt => async () => {
|
||||
@@ -185,19 +186,19 @@
|
||||
},
|
||||
}
|
||||
),
|
||||
data.fileType.endsWith('.sql') && { text: 'Open SQL', onClick: handleOpenSqlFile },
|
||||
data.fileType.endsWith('.yaml') && { text: 'Open YAML', onClick: handleOpenYamlFile },
|
||||
data.fileType.endsWith('.sql') && { text: _t('common.openSql', { defaultMessage: 'Open SQL' }), onClick: handleOpenSqlFile },
|
||||
data.fileType.endsWith('.yaml') && { text: _t('common.openYaml', { defaultMessage: 'Open YAML' }), onClick: handleOpenYamlFile },
|
||||
!isZipped &&
|
||||
isProApp() &&
|
||||
data.fileType == 'jsonl' && {
|
||||
text: 'Open in profiler',
|
||||
text: _t('common.openInProfiler', { defaultMessage: 'Open in profiler' }),
|
||||
submenu: getExtensions()
|
||||
.drivers.filter(eng => eng.profilerFormatterFunction)
|
||||
.map(eng => ({
|
||||
text: eng.title,
|
||||
onClick: () => {
|
||||
openNewTab({
|
||||
title: 'Profiler',
|
||||
title: _t('common.profiler', { defaultMessage: 'Profiler' }),
|
||||
icon: 'img profiler',
|
||||
tabComponent: 'ProfilerTab',
|
||||
props: {
|
||||
|
||||
@@ -21,14 +21,15 @@
|
||||
import { isProApp } from '../utility/proTools';
|
||||
import { extractShellConnection } from '../impexp/createImpExpScript';
|
||||
import { saveFileToDisk } from '../utility/exportFileTools';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let data;
|
||||
|
||||
const handleDelete = () => {
|
||||
showModal(ConfirmModal, {
|
||||
message: data.name.endsWith('.link')
|
||||
? `Really delete link to folder ${data.name}? Folder content remains untouched.`
|
||||
: `Really delete folder ${data.name}?`,
|
||||
? _t('archiveFolder.deleteLinkConfirm', { defaultMessage: 'Really delete link to folder {folderName}? Folder content remains untouched.', values: { folderName: data.name } })
|
||||
: _t('archiveFolder.deleteFolderConfirm', { defaultMessage: 'Really delete folder {folderName}?', values: { folderName: data.name } }),
|
||||
onConfirm: () => {
|
||||
apiCall('archive/delete-folder', { folder: data.name });
|
||||
},
|
||||
@@ -42,8 +43,8 @@
|
||||
|
||||
showModal(InputTextModal, {
|
||||
value: name,
|
||||
label: 'New folder name',
|
||||
header: 'Rename folder',
|
||||
label: _t('archiveFolder.newFolderName', { defaultMessage: 'New folder name' }),
|
||||
header: _t('archiveFolder.renameFolder', { defaultMessage: 'Rename folder' }),
|
||||
onConfirm: async newFolder => {
|
||||
await apiCall('archive/rename-folder', {
|
||||
folder: data.name,
|
||||
@@ -95,7 +96,7 @@ await dbgateApi.deployDb(${JSON.stringify(
|
||||
const handleCompareWithCurrentDb = () => {
|
||||
openNewTab(
|
||||
{
|
||||
title: 'Compare',
|
||||
title: _t('common.compare', { defaultMessage: 'Compare' }),
|
||||
icon: 'img compare',
|
||||
tabComponent: 'CompareModelTab',
|
||||
props: {
|
||||
@@ -153,7 +154,7 @@ await dbgateApi.deployDb(${JSON.stringify(
|
||||
});
|
||||
},
|
||||
{
|
||||
formatLabel: 'ZIP files',
|
||||
formatLabel: _t('common.zipFiles', { defaultMessage: 'ZIP files' }),
|
||||
formatExtension: 'zip',
|
||||
defaultFileName: data.name?.endsWith('.zip') ? data.name : data.name + '.zip',
|
||||
}
|
||||
@@ -162,28 +163,28 @@ await dbgateApi.deployDb(${JSON.stringify(
|
||||
|
||||
function createMenu() {
|
||||
return [
|
||||
data.name != 'default' && { text: 'Delete', onClick: handleDelete },
|
||||
data.name != 'default' && { text: 'Rename', onClick: handleRename },
|
||||
isProApp() && { text: 'Data deployer', onClick: handleOpenDataDeployTab },
|
||||
data.name != 'default' && { text: _t('common.delete', { defaultMessage: 'Delete' }), onClick: handleDelete },
|
||||
data.name != 'default' && { text: _t('common.rename', { defaultMessage: 'Rename' }), onClick: handleRename },
|
||||
isProApp() && { text: _t('common.dataDeployer', { defaultMessage: 'Data deployer' }), onClick: handleOpenDataDeployTab },
|
||||
$currentDatabase && [
|
||||
{ text: 'Generate deploy DB SQL', onClick: handleGenerateDeploySql },
|
||||
{ text: 'Shell: Deploy DB', onClick: handleGenerateDeployScript },
|
||||
{ text: _t('archiveFolder.generateDeployDbSql', { defaultMessage: 'Generate deploy DB SQL' }), onClick: handleGenerateDeploySql },
|
||||
hasPermission(`run-shell-script`) && { text: _t('archiveFolder.shellDeployDb', { defaultMessage: 'Shell: Deploy DB' }), onClick: handleGenerateDeployScript },
|
||||
],
|
||||
data.name != 'default' &&
|
||||
isProApp() &&
|
||||
data.name.endsWith('.zip') && { text: 'Unpack ZIP', onClick: () => handleZipUnzip('archive/unzip') },
|
||||
data.name.endsWith('.zip') && { text: _t('archiveFolder.unpackZip', { defaultMessage: 'Unpack ZIP' }), onClick: () => handleZipUnzip('archive/unzip') },
|
||||
data.name != 'default' &&
|
||||
isProApp() &&
|
||||
!data.name.endsWith('.zip') && { text: 'Pack (create ZIP)', onClick: () => handleZipUnzip('archive/zip') },
|
||||
!data.name.endsWith('.zip') && { text: _t('archiveFolder.packZip', { defaultMessage: 'Pack (create ZIP)' }), onClick: () => handleZipUnzip('archive/zip') },
|
||||
|
||||
isProApp() && { text: 'Download ZIP', onClick: handleDownloadZip },
|
||||
isProApp() && { text: _t('archiveFolder.downloadZip', { defaultMessage: 'Download ZIP' }), onClick: handleDownloadZip },
|
||||
|
||||
data.name != 'default' &&
|
||||
hasPermission('dbops/model/compare') &&
|
||||
isProApp() &&
|
||||
_.get($currentDatabase, 'connection._id') && {
|
||||
onClick: handleCompareWithCurrentDb,
|
||||
text: `Compare with ${_.get($currentDatabase, 'name')}`,
|
||||
text: _t('archiveFolder.compareWithCurrentDb', { defaultMessage: 'Compare with {name}', values: { name: _.get($currentDatabase, 'name') } }),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -122,6 +122,7 @@
|
||||
getOpenedTabs,
|
||||
openedConnections,
|
||||
openedSingleDatabaseConnections,
|
||||
pinnedDatabases,
|
||||
} from '../stores';
|
||||
import { filterName, filterNameCompoud } from 'dbgate-tools';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
@@ -130,7 +131,7 @@
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import { getDatabaseMenuItems } from './DatabaseAppObject.svelte';
|
||||
import getElectron from '../utility/getElectron';
|
||||
import { getDatabaseList, useUsedApps } from '../utility/metadataLoaders';
|
||||
import { getDatabaseList, useAllApps } from '../utility/metadataLoaders';
|
||||
import { getLocalStorage } from '../utility/storageCache';
|
||||
import { apiCall, removeVolatileMapping } from '../utility/api';
|
||||
import { closeMultipleTabs } from '../tabpanel/TabsPanel.svelte';
|
||||
@@ -152,6 +153,8 @@
|
||||
let engineStatusIcon = null;
|
||||
let engineStatusTitle = null;
|
||||
|
||||
$: isPinned = data.singleDatabase && !!$pinnedDatabases.find(x => x?.connection?._id == data?._id);
|
||||
|
||||
const electron = getElectron();
|
||||
|
||||
const handleConnect = (disableExpand = false) => {
|
||||
@@ -276,7 +279,7 @@
|
||||
showModal(InputTextModal, {
|
||||
header: _t('connection.createDatabase', { defaultMessage: 'Create database' }),
|
||||
value: 'newdb',
|
||||
label: _t('connection.databaseName', { defaultMessage: 'Database name' }),
|
||||
label: _t('connection.database', { defaultMessage: 'Database name' }),
|
||||
onConfirm: name =>
|
||||
apiCall('server-connections/create-database', {
|
||||
conid: data._id,
|
||||
@@ -382,7 +385,8 @@
|
||||
$extensions,
|
||||
$currentDatabase,
|
||||
$apps,
|
||||
$openedSingleDatabaseConnections
|
||||
$openedSingleDatabaseConnections,
|
||||
data.databasePermissionRole
|
||||
),
|
||||
],
|
||||
|
||||
@@ -426,7 +430,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: apps = useUsedApps();
|
||||
$: apps = useAllApps();
|
||||
</script>
|
||||
|
||||
<AppObjectCore
|
||||
@@ -454,6 +458,19 @@
|
||||
.find(x => x.isNewQuery)
|
||||
.onClick();
|
||||
}}
|
||||
onPin={!isPinned && data.singleDatabase
|
||||
? () =>
|
||||
pinnedDatabases.update(list => [
|
||||
...list,
|
||||
{
|
||||
name: data.defaultDatabase,
|
||||
connection: data,
|
||||
},
|
||||
])
|
||||
: null}
|
||||
onUnpin={isPinned && data.singleDatabase
|
||||
? () => pinnedDatabases.update(list => list.filter(x => x?.connection?._id != data?._id))
|
||||
: null}
|
||||
isChoosed={data._id == $focusedConnectionOrDatabase?.conid &&
|
||||
(data.singleDatabase
|
||||
? $focusedConnectionOrDatabase?.database == data.defaultDatabase
|
||||
|
||||
@@ -46,7 +46,8 @@
|
||||
$extensions,
|
||||
$currentDatabase,
|
||||
$apps,
|
||||
$openedSingleDatabaseConnections
|
||||
$openedSingleDatabaseConnections,
|
||||
databasePermissionRole
|
||||
) {
|
||||
const apps = filterAppsForDatabase(connection, name, $apps);
|
||||
const handleNewQuery = () => {
|
||||
@@ -404,19 +405,36 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreateNewApp = () => {
|
||||
showModal(InputTextModal, {
|
||||
header: _t('database.newApplication', { defaultMessage: 'New application' }),
|
||||
label: _t('database.applicationName', { defaultMessage: 'Application name' }),
|
||||
value: _.startCase(name),
|
||||
onConfirm: async appName => {
|
||||
const newAppId = await apiCall('apps/create-app-from-db', {
|
||||
appName,
|
||||
server: connection?.server,
|
||||
database: name,
|
||||
});
|
||||
openApplicationEditor(newAppId);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const driver = findEngineDriver(connection, getExtensions());
|
||||
|
||||
const commands = _.flatten((apps || []).map(x => x.commands || []));
|
||||
const commands = _.flatten((apps || []).map(x => Object.values(x.files || {}).filter(x => x.type == 'command')));
|
||||
|
||||
const isSqlOrDoc =
|
||||
driver?.databaseEngineTypes?.includes('sql') || driver?.databaseEngineTypes?.includes('document');
|
||||
|
||||
return [
|
||||
hasPermission(`dbops/query`) && {
|
||||
onClick: handleNewQuery,
|
||||
text: _t('database.newQuery', { defaultMessage: 'New query' }),
|
||||
isNewQuery: true,
|
||||
},
|
||||
hasPermission(`dbops/query`) &&
|
||||
isAllowedDatabaseRunScript(databasePermissionRole) && {
|
||||
onClick: handleNewQuery,
|
||||
text: _t('database.newQuery', { defaultMessage: 'New query' }),
|
||||
isNewQuery: true,
|
||||
},
|
||||
hasPermission(`dbops/model/edit`) &&
|
||||
!connection.isReadOnly &&
|
||||
driver?.databaseEngineTypes?.includes('sql') && {
|
||||
@@ -428,8 +446,7 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
driver?.databaseEngineTypes?.includes('document') && {
|
||||
onClick: handleNewCollection,
|
||||
text: _t('database.newCollection', {
|
||||
defaultMessage: 'New {collectionLabel}',
|
||||
values: { collectionLabel: driver?.collectionSingularLabel ?? 'collection/container' },
|
||||
defaultMessage: 'New collection/container'
|
||||
}),
|
||||
},
|
||||
hasPermission(`dbops/query`) &&
|
||||
@@ -545,12 +562,13 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
{ divider: true },
|
||||
|
||||
driver?.databaseEngineTypes?.includes('sql') &&
|
||||
hasPermission(`run-shell-script`) &&
|
||||
hasPermission(`dbops/dropdb`) && {
|
||||
onClick: handleGenerateDropAllObjectsScript,
|
||||
text: _t('database.shellDropAllObjects', { defaultMessage: 'Shell: Drop all objects' }),
|
||||
},
|
||||
|
||||
{
|
||||
hasPermission(`run-shell-script`) && {
|
||||
onClick: handleGenerateRunScript,
|
||||
text: _t('database.shellRunScript', { defaultMessage: 'Shell: Run script' }),
|
||||
},
|
||||
@@ -561,11 +579,26 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
text: _t('database.dataDeployer', { defaultMessage: 'Data deployer' }),
|
||||
},
|
||||
|
||||
isProApp() &&
|
||||
hasPermission(`files/apps/write`) && {
|
||||
onClick: handleCreateNewApp,
|
||||
text: _t('database.createNewApplication', { defaultMessage: 'Create new application' }),
|
||||
},
|
||||
|
||||
isProApp() &&
|
||||
apps?.length > 0 && {
|
||||
text: _t('database.editApplications', { defaultMessage: 'Edit application' }),
|
||||
submenu: apps.map((app: any) => ({
|
||||
text: app.applicationName,
|
||||
onClick: () => openApplicationEditor(app.appid),
|
||||
})),
|
||||
},
|
||||
|
||||
{ divider: true },
|
||||
|
||||
commands.length > 0 && [
|
||||
commands.map((cmd: any) => ({
|
||||
text: cmd.name,
|
||||
text: cmd.label,
|
||||
onClick: () => {
|
||||
showModal(ConfirmSqlModal, {
|
||||
sql: cmd.sql,
|
||||
@@ -615,17 +648,17 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
getConnectionLabel,
|
||||
} from 'dbgate-tools';
|
||||
import InputTextModal from '../modals/InputTextModal.svelte';
|
||||
import { getDatabaseInfo, useUsedApps } from '../utility/metadataLoaders';
|
||||
import { getDatabaseInfo, useAllApps, useDatabaseInfoPeek } from '../utility/metadataLoaders';
|
||||
import { openJsonDocument } from '../tabs/JsonTab.svelte';
|
||||
import { apiCall } from '../utility/api';
|
||||
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
|
||||
import ConfirmSqlModal, { runOperationOnDatabase, saveScriptToDatabase } from '../modals/ConfirmSqlModal.svelte';
|
||||
import { filterAppsForDatabase } from '../utility/appTools';
|
||||
import { filterAppsForDatabase, openApplicationEditor } from '../utility/appTools';
|
||||
import newQuery from '../query/newQuery';
|
||||
import ConfirmModal from '../modals/ConfirmModal.svelte';
|
||||
import { closeMultipleTabs } from '../tabpanel/TabsPanel.svelte';
|
||||
import NewCollectionModal from '../modals/NewCollectionModal.svelte';
|
||||
import hasPermission from '../utility/hasPermission';
|
||||
import hasPermission, { isAllowedDatabaseRunScript } from '../utility/hasPermission';
|
||||
import { openImportExportTab } from '../utility/importExportTools';
|
||||
import newTable from '../tableeditor/newTable';
|
||||
import { loadSchemaList, switchCurrentDatabase } from '../utility/common';
|
||||
@@ -636,6 +669,7 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
import { getNumberIcon } from '../icons/FontIcon.svelte';
|
||||
import { getDatabaseClickActionSetting } from '../settings/settingsTools';
|
||||
import { _t } from '../translations';
|
||||
import { tick } from 'svelte';
|
||||
|
||||
export let data;
|
||||
export let passProps;
|
||||
@@ -647,13 +681,19 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
$extensions,
|
||||
$currentDatabase,
|
||||
$apps,
|
||||
$openedSingleDatabaseConnections
|
||||
$openedSingleDatabaseConnections,
|
||||
data.databasePermissionRole
|
||||
);
|
||||
}
|
||||
|
||||
$: isPinned = !!$pinnedDatabases.find(x => x?.name == data.name && x?.connection?._id == data.connection?._id);
|
||||
$: apps = useUsedApps();
|
||||
$: apps = useAllApps();
|
||||
$: isLoadingSchemas = $loadingSchemaLists[`${data?.connection?._id}::${data?.name}`];
|
||||
$: dbInfo = useDatabaseInfoPeek({ conid: data?.connection?._id, database: data?.name });
|
||||
|
||||
$: appsForDb = filterAppsForDatabase(data?.connection, data?.name, $apps, $dbInfo);
|
||||
|
||||
// $: console.log('AppsForDB:', data?.name, appsForDb);
|
||||
</script>
|
||||
|
||||
<AppObjectCore
|
||||
@@ -676,6 +716,13 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
switchCurrentDatabase(data);
|
||||
}
|
||||
}}
|
||||
additionalIcons={appsForDb?.length > 0
|
||||
? appsForDb.map(ic => ({
|
||||
icon: ic.applicationIcon || 'img app',
|
||||
title: ic.applicationName,
|
||||
colorClass: ic.applicationColor ? `color-icon-${ic.applicationColor}` : undefined,
|
||||
}))
|
||||
: null}
|
||||
on:mousedown={() => {
|
||||
$focusedConnectionOrDatabase = { conid: data.connection?._id, database: data.name, connection: data.connection };
|
||||
}}
|
||||
@@ -697,6 +744,9 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
).length
|
||||
)
|
||||
: ''}
|
||||
statusIconBefore={data.databasePermissionRole == 'read_content' || data.databasePermissionRole == 'view'
|
||||
? 'icon lock'
|
||||
: null}
|
||||
menu={createMenu}
|
||||
showPinnedInsteadOfUnpin={passProps?.showPinnedInsteadOfUnpin}
|
||||
onPin={isPinned ? null : () => pinnedDatabases.update(list => [...list, data])}
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
<script lang="ts" context="module">
|
||||
import { copyTextToClipboard } from '../utility/clipboard';
|
||||
import { _t, _tval, DefferedTranslationResult } from '../translations';
|
||||
import sqlFormatter from 'sql-formatter';
|
||||
|
||||
export const extractKey = ({ schemaName, pureName }) => (schemaName ? `${schemaName}.${pureName}` : pureName);
|
||||
export const createMatcher =
|
||||
(filter, cfg = DEFAULT_OBJECT_SEARCH_SETTINGS) =>
|
||||
({ schemaName, pureName, objectComment, tableEngine, columns, objectTypeField, tableName, createSql }) => {
|
||||
({
|
||||
schemaName,
|
||||
pureName,
|
||||
objectComment,
|
||||
tableEngine,
|
||||
columns,
|
||||
objectTypeField,
|
||||
tableName,
|
||||
createSql,
|
||||
tableRowCount,
|
||||
}) => {
|
||||
const mainArgs = [];
|
||||
const childArgs = [];
|
||||
if (cfg.schemaName) mainArgs.push(schemaName);
|
||||
@@ -12,6 +24,7 @@
|
||||
if (objectTypeField == 'tables') {
|
||||
if (cfg.tableComment) mainArgs.push(objectComment);
|
||||
if (cfg.tableEngine) mainArgs.push(tableEngine);
|
||||
if (cfg.tablesWithRows && !tableRowCount) return 'none';
|
||||
|
||||
for (const column of columns || []) {
|
||||
if (cfg.columnName) childArgs.push(column.columnName);
|
||||
@@ -45,26 +58,26 @@
|
||||
schedulerEvents: 'icon scheduler-event',
|
||||
};
|
||||
|
||||
const defaultTabs = {
|
||||
tables: 'TableDataTab',
|
||||
collections: 'CollectionDataTab',
|
||||
views: 'ViewDataTab',
|
||||
matviews: 'ViewDataTab',
|
||||
queries: 'QueryDataTab',
|
||||
procedures: 'SqlObjectTab',
|
||||
functions: 'SqlObjectTab',
|
||||
triggers: 'SqlObjectTab',
|
||||
};
|
||||
// const defaultTabs = {
|
||||
// tables: 'TableDataTab',
|
||||
// collections: 'CollectionDataTab',
|
||||
// views: 'ViewDataTab',
|
||||
// matviews: 'ViewDataTab',
|
||||
// queries: 'QueryDataTab',
|
||||
// procedures: 'SqlObjectTab',
|
||||
// functions: 'SqlObjectTab',
|
||||
// triggers: 'SqlObjectTab',
|
||||
// };
|
||||
|
||||
function createScriptTemplatesSubmenu(objectTypeField) {
|
||||
return {
|
||||
label: 'SQL template',
|
||||
label: _t('dbObject.sqlTemplate', { defaultMessage: 'SQL template' }),
|
||||
submenu: getSupportedScriptTemplates(objectTypeField),
|
||||
};
|
||||
}
|
||||
|
||||
interface DbObjMenuItem {
|
||||
label?: string;
|
||||
label?: string | DefferedTranslationResult;
|
||||
tab?: string;
|
||||
forceNewTab?: boolean;
|
||||
initialData?: any;
|
||||
@@ -76,7 +89,8 @@
|
||||
isRename?: boolean;
|
||||
isTruncate?: boolean;
|
||||
isCopyTableName?: boolean;
|
||||
isDuplicateTable?: boolean;
|
||||
isTableBackup?: boolean;
|
||||
isTableRestore?: boolean;
|
||||
isDiagram?: boolean;
|
||||
functionName?: string;
|
||||
isExport?: boolean;
|
||||
@@ -94,6 +108,8 @@
|
||||
}
|
||||
|
||||
function createMenusCore(objectTypeField, driver, data): DbObjMenuItem[] {
|
||||
const backupMatch = data.objectTypeField === 'tables' ? data.pureName.match(TABLE_BACKUP_REGEX) : null;
|
||||
|
||||
switch (objectTypeField) {
|
||||
case 'tables':
|
||||
return [
|
||||
@@ -102,19 +118,19 @@
|
||||
divider: true,
|
||||
},
|
||||
isProApp() && {
|
||||
label: 'Design query',
|
||||
label: _t('dbObject.designQuery', { defaultMessage: 'Design query' }),
|
||||
isQueryDesigner: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
isProApp() && {
|
||||
label: 'Design perspective query',
|
||||
label: _t('dbObject.designPerspectiveQuery', { defaultMessage: 'Design perspective query' }),
|
||||
tab: 'PerspectiveTab',
|
||||
forceNewTab: true,
|
||||
icon: 'img perspective',
|
||||
},
|
||||
createScriptTemplatesSubmenu('tables'),
|
||||
{
|
||||
label: 'SQL generator',
|
||||
label: _t('dbObject.sqlGenerator', { defaultMessage: 'SQL generator' }),
|
||||
submenu: [
|
||||
{
|
||||
label: 'CREATE TABLE',
|
||||
@@ -143,45 +159,52 @@
|
||||
divider: true,
|
||||
},
|
||||
hasPermission('dbops/model/edit') && {
|
||||
label: 'Drop table',
|
||||
label: _t('dbObject.dropTable', { defaultMessage: 'Drop table' }),
|
||||
isDrop: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
hasPermission('dbops/table/rename') &&
|
||||
!driver?.dialect.disableRenameTable && {
|
||||
label: 'Rename table',
|
||||
label: _t('dbObject.renameTable', { defaultMessage: 'Rename table' }),
|
||||
isRename: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
hasPermission('dbops/table/truncate') && {
|
||||
label: 'Truncate table',
|
||||
label: _t('dbObject.truncateTable', { defaultMessage: 'Truncate table' }),
|
||||
isTruncate: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
{
|
||||
label: 'Copy table name',
|
||||
label: _t('dbObject.copyTableName', { defaultMessage: 'Copy table name' }),
|
||||
isCopyTableName: true,
|
||||
requiresWriteAccess: false,
|
||||
},
|
||||
hasPermission('dbops/table/backup') && {
|
||||
label: 'Create table backup',
|
||||
isDuplicateTable: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
hasPermission('dbops/table/backup') &&
|
||||
!backupMatch && {
|
||||
label: _t('dbObject.createTableBackup', { defaultMessage: 'Create table backup' }),
|
||||
isTableBackup: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
hasPermission('dbops/table/restore') &&
|
||||
backupMatch && {
|
||||
label: _t('dbObject.createRestoreScript', { defaultMessage: 'Create restore script' }),
|
||||
isTableRestore: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
hasPermission('dbops/model/view') && {
|
||||
label: 'Show diagram',
|
||||
label: _t('dbObject.showDiagram', { defaultMessage: 'Show diagram' }),
|
||||
isDiagram: true,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
hasPermission('dbops/export') && {
|
||||
label: 'Export',
|
||||
label: _t('common.export', { defaultMessage: 'Export' }),
|
||||
functionName: 'tableReader',
|
||||
isExport: true,
|
||||
},
|
||||
hasPermission('dbops/import') && {
|
||||
label: 'Import',
|
||||
label: _t('common.import', { defaultMessage: 'Import' }),
|
||||
isImport: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
@@ -193,18 +216,18 @@
|
||||
divider: true,
|
||||
},
|
||||
isProApp() && {
|
||||
label: 'Design query',
|
||||
label: _t('dbObject.designQuery', { defaultMessage: 'Design query' }),
|
||||
isQueryDesigner: true,
|
||||
},
|
||||
isProApp() && {
|
||||
label: 'Design perspective query',
|
||||
label: _t('dbObject.designPerspectiveQuery', { defaultMessage: 'Design perspective query' }),
|
||||
tab: 'PerspectiveTab',
|
||||
forceNewTab: true,
|
||||
icon: 'img perspective',
|
||||
},
|
||||
createScriptTemplatesSubmenu('views'),
|
||||
{
|
||||
label: 'SQL generator',
|
||||
label: _t('dbObject.sqlGenerator', { defaultMessage: 'SQL generator' }),
|
||||
submenu: [
|
||||
{
|
||||
label: 'CREATE VIEW',
|
||||
@@ -224,12 +247,12 @@
|
||||
divider: true,
|
||||
},
|
||||
hasPermission('dbops/model/edit') && {
|
||||
label: 'Drop view',
|
||||
label: _t('dbObject.dropView', { defaultMessage: 'Drop view' }),
|
||||
isDrop: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
hasPermission('dbops/model/edit') && {
|
||||
label: 'Rename view',
|
||||
label: _t('dbObject.renameView', { defaultMessage: 'Rename view' }),
|
||||
isRename: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
@@ -237,7 +260,7 @@
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: 'Export',
|
||||
label: _t('common.export', { defaultMessage: 'Export' }),
|
||||
isExport: true,
|
||||
functionName: 'tableReader',
|
||||
},
|
||||
@@ -249,12 +272,12 @@
|
||||
divider: true,
|
||||
},
|
||||
hasPermission('dbops/model/edit') && {
|
||||
label: 'Drop view',
|
||||
label: _t('dbObject.dropView', { defaultMessage: 'Drop view' }),
|
||||
isDrop: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
hasPermission('dbops/model/edit') && {
|
||||
label: 'Rename view',
|
||||
label: _t('dbObject.renameView', { defaultMessage: 'Rename view' }),
|
||||
isRename: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
@@ -262,12 +285,12 @@
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: 'Query designer',
|
||||
label: _t('dbObject.queryDesigner', { defaultMessage: 'Query designer' }),
|
||||
isQueryDesigner: true,
|
||||
},
|
||||
createScriptTemplatesSubmenu('matviews'),
|
||||
{
|
||||
label: 'SQL generator',
|
||||
label: _t('dbObject.sqlGenerator', { defaultMessage: 'SQL generator' }),
|
||||
submenu: [
|
||||
{
|
||||
label: 'CREATE MATERIALIZED VIEW',
|
||||
@@ -287,7 +310,7 @@
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: 'Export',
|
||||
label: _t('common.export', { defaultMessage: 'Export' }),
|
||||
isExport: true,
|
||||
functionName: 'tableReader',
|
||||
},
|
||||
@@ -295,7 +318,7 @@
|
||||
case 'queries':
|
||||
return [
|
||||
{
|
||||
label: 'Open data',
|
||||
label: _t('dbObject.openData', { defaultMessage: 'Open data' }),
|
||||
tab: 'QueryDataTab',
|
||||
forceNewTab: true,
|
||||
},
|
||||
@@ -307,18 +330,18 @@
|
||||
divider: true,
|
||||
},
|
||||
hasPermission('dbops/model/edit') && {
|
||||
label: 'Drop procedure',
|
||||
label: _t('dbObject.dropProcedure', { defaultMessage: 'Drop procedure' }),
|
||||
isDrop: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
hasPermission('dbops/model/edit') && {
|
||||
label: 'Rename procedure',
|
||||
label: _t('dbObject.renameProcedure', { defaultMessage: 'Rename procedure' }),
|
||||
isRename: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
createScriptTemplatesSubmenu('procedures'),
|
||||
{
|
||||
label: 'SQL generator',
|
||||
label: _t('dbObject.sqlGenerator', { defaultMessage: 'SQL generator' }),
|
||||
submenu: [
|
||||
{
|
||||
label: 'CREATE PROCEDURE',
|
||||
@@ -341,7 +364,7 @@
|
||||
return [
|
||||
...defaultDatabaseObjectAppObjectActions['triggers'],
|
||||
hasPermission('dbops/model/edit') && {
|
||||
label: 'Drop trigger',
|
||||
label: _t('dbObject.dropTrigger', { defaultMessage: 'Drop trigger' }),
|
||||
isDrop: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
@@ -349,7 +372,7 @@
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: 'SQL generator',
|
||||
label: _t('dbObject.sqlGenerator', { defaultMessage: 'SQL generator' }),
|
||||
submenu: [
|
||||
{
|
||||
label: 'CREATE TRIGGER',
|
||||
@@ -373,28 +396,28 @@
|
||||
divider: true,
|
||||
},
|
||||
isProApp() && {
|
||||
label: 'Design perspective query',
|
||||
label: _t('dbObject.designPerspectiveQuery', { defaultMessage: 'Design perspective query' }),
|
||||
tab: 'PerspectiveTab',
|
||||
forceNewTab: true,
|
||||
icon: 'img perspective',
|
||||
},
|
||||
hasPermission('dbops/export') && {
|
||||
label: 'Export',
|
||||
label: _t('common.export', { defaultMessage: 'Export' }),
|
||||
isExport: true,
|
||||
functionName: 'tableReader',
|
||||
},
|
||||
hasPermission('dbops/model/edit') && {
|
||||
label: `Drop ${driver?.collectionSingularLabel ?? 'collection/container'}`,
|
||||
label: _t('dbObject.dropCollection', { defaultMessage: 'Drop collection/container' }),
|
||||
isDropCollection: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
hasPermission('dbops/table/rename') && {
|
||||
label: `Rename ${driver?.collectionSingularLabel ?? 'collection/container'}`,
|
||||
label: _t('dbObject.renameCollection', { defaultMessage: 'Rename collection/container' }),
|
||||
isRenameCollection: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
hasPermission('dbops/table/backup') && {
|
||||
label: `Create ${driver?.collectionSingularLabel ?? 'collection/container'} backup`,
|
||||
label: _t('dbObject.createCollectionBackup', { defaultMessage: 'Create collection/container backup' }),
|
||||
isDuplicateCollection: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
@@ -407,7 +430,7 @@
|
||||
const menu: DbObjMenuItem[] = [
|
||||
...defaultDatabaseObjectAppObjectActions['schedulerEvents'],
|
||||
hasPermission('dbops/model/edit') && {
|
||||
label: 'Drop event',
|
||||
label: _t('dbObject.dropEvent', { defaultMessage: 'Drop event' }),
|
||||
isDrop: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
@@ -415,12 +438,12 @@
|
||||
|
||||
if (data?.status === 'ENABLED') {
|
||||
menu.push({
|
||||
label: 'Disable',
|
||||
label: _t('dbObject.disable', { defaultMessage: 'Disable' }),
|
||||
isDisableEvent: true,
|
||||
});
|
||||
} else {
|
||||
menu.push({
|
||||
label: 'Enable',
|
||||
label: _t('dbObject.enable', { defaultMessage: 'Enable' }),
|
||||
isEnableEvent: true,
|
||||
});
|
||||
}
|
||||
@@ -430,7 +453,7 @@
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: 'SQL generator',
|
||||
label: _t('dbObject.sqlGenerator', { defaultMessage: 'SQL generator' }),
|
||||
submenu: [
|
||||
{
|
||||
label: 'CREATE SCHEDULER EVENT',
|
||||
@@ -463,7 +486,7 @@
|
||||
if (menu.isQueryDesigner) {
|
||||
openNewTab(
|
||||
{
|
||||
title: 'Query #',
|
||||
title: _t('dbObject.query', { defaultMessage: 'Query #' }),
|
||||
icon: 'img query-design',
|
||||
tabComponent: 'QueryDesignTab',
|
||||
focused: true,
|
||||
@@ -488,7 +511,7 @@
|
||||
} else if (menu.isDiagram) {
|
||||
openNewTab(
|
||||
{
|
||||
title: 'Diagram #',
|
||||
title: _t('dbObject.diagram', { defaultMessage: 'Diagram #' }),
|
||||
icon: 'img diagram',
|
||||
tabComponent: 'DiagramTab',
|
||||
props: {
|
||||
@@ -578,7 +601,10 @@
|
||||
});
|
||||
} else if (menu.isDropCollection) {
|
||||
showModal(ConfirmModal, {
|
||||
message: `Really drop collection ${data.pureName}?`,
|
||||
message: _t('dbObject.confirmDropCollection', {
|
||||
defaultMessage: 'Really drop collection {name}?',
|
||||
values: { name: data.pureName },
|
||||
}),
|
||||
onConfirm: async () => {
|
||||
const dbid = _.pick(data, ['conid', 'database']);
|
||||
runOperationOnDatabase(dbid, {
|
||||
@@ -592,8 +618,8 @@
|
||||
} else if (menu.isRenameCollection) {
|
||||
const driver = await getDriver();
|
||||
showModal(InputTextModal, {
|
||||
label: `New ${driver?.collectionSingularLabel ?? 'collection/container'} name`,
|
||||
header: `Rename ${driver?.collectionSingularLabel ?? 'collection/container'}`,
|
||||
label: _t('dbObject.newCollectionName', { defaultMessage: 'New collection/container name' }),
|
||||
header: _t('dbObject.renameCollection', { defaultMessage: 'Rename collection/container' }),
|
||||
value: data.pureName,
|
||||
onConfirm: async newName => {
|
||||
const dbid = _.pick(data, ['conid', 'database']);
|
||||
@@ -609,7 +635,10 @@
|
||||
const driver = await getDriver();
|
||||
|
||||
showModal(ConfirmModal, {
|
||||
message: `Really create ${driver?.collectionSingularLabel ?? 'collection/container'} copy named ${newName}?`,
|
||||
message: _t('dbObject.confirmCloneCollection', {
|
||||
defaultMessage: 'Really create collection/container copy named {name}?',
|
||||
values: { name: newName },
|
||||
}),
|
||||
onConfirm: async () => {
|
||||
const dbid = _.pick(data, ['conid', 'database']);
|
||||
runOperationOnDatabase(dbid, {
|
||||
@@ -619,7 +648,7 @@
|
||||
});
|
||||
},
|
||||
});
|
||||
} else if (menu.isDuplicateTable) {
|
||||
} else if (menu.isTableBackup) {
|
||||
const driver = await getDriver();
|
||||
const dmp = driver.createDumper();
|
||||
const newTable = _.cloneDeep(data);
|
||||
@@ -653,6 +682,25 @@
|
||||
},
|
||||
engine: driver.engine,
|
||||
});
|
||||
} else if (menu.isTableRestore) {
|
||||
const backupMatch = data.objectTypeField === 'tables' ? data.pureName.match(TABLE_BACKUP_REGEX) : null;
|
||||
|
||||
const driver = await getDriver();
|
||||
const dmp = driver.createDumper();
|
||||
const db = await getDatabaseInfo(data);
|
||||
if (db) {
|
||||
const originalTable = db?.tables?.find(x => x.pureName == backupMatch[1] && x.schemaName == data.schemaName);
|
||||
if (originalTable) {
|
||||
createTableRestoreScript(data, originalTable, dmp);
|
||||
newQuery({
|
||||
title: _t('dbObject.restoreScript', {
|
||||
defaultMessage: 'Restore {name} #',
|
||||
values: { name: backupMatch[1] },
|
||||
}),
|
||||
initialData: sqlFormatter.format(dmp.s),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (menu.isImport) {
|
||||
const { conid, database } = data;
|
||||
openImportExportTab({
|
||||
@@ -703,15 +751,30 @@
|
||||
}
|
||||
|
||||
function createMenus(objectTypeField, driver, data): ReturnType<typeof createMenusCore> {
|
||||
return createMenusCore(objectTypeField, driver, data).filter(x => {
|
||||
if (x.scriptTemplate) {
|
||||
return hasPermission(`dbops/sql-template/${x.scriptTemplate}`);
|
||||
const coreMenus = createMenusCore(objectTypeField, driver, data);
|
||||
|
||||
const filteredSumenus = coreMenus.map(item => {
|
||||
if (!item) return item;
|
||||
if (!item.submenu) {
|
||||
return { ...item, label: _tval(item.label) };
|
||||
}
|
||||
if (x.sqlGeneratorProps) {
|
||||
return hasPermission(`dbops/sql-generator`);
|
||||
}
|
||||
return true;
|
||||
return {
|
||||
...item,
|
||||
submenu: item.submenu.filter(x => {
|
||||
if (x.scriptTemplate) {
|
||||
return hasPermission(`dbops/sql-template/${x.scriptTemplate}`);
|
||||
}
|
||||
if (x.sqlGeneratorProps) {
|
||||
return hasPermission(`dbops/sql-generator`);
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const filteredNoEmptySubmenus = _.compact(filteredSumenus).filter(x => !x.submenu || x.submenu.length > 0);
|
||||
|
||||
return filteredNoEmptySubmenus;
|
||||
}
|
||||
|
||||
function getObjectTitle(connection, schemaName, pureName) {
|
||||
@@ -727,7 +790,7 @@
|
||||
export async function openDatabaseObjectDetail(
|
||||
tabComponent,
|
||||
scriptTemplate,
|
||||
{ schemaName, pureName, conid, database, objectTypeField, defaultActionId, isRawMode },
|
||||
{ schemaName, pureName, conid, database, objectTypeField, defaultActionId, isRawMode, sql },
|
||||
forceNewTab?,
|
||||
initialData?,
|
||||
icon?,
|
||||
@@ -743,7 +806,9 @@
|
||||
openNewTab(
|
||||
{
|
||||
// title: getObjectTitle(connection, schemaName, pureName),
|
||||
title: tabComponent ? getObjectTitle(connection, schemaName, pureName) : 'Query #',
|
||||
title: tabComponent
|
||||
? getObjectTitle(connection, schemaName, pureName)
|
||||
: _t('dbObject.query', { defaultMessage: 'Query #' }),
|
||||
focused: tabComponent == null,
|
||||
tooltip,
|
||||
icon:
|
||||
@@ -762,6 +827,7 @@
|
||||
initialArgs: scriptTemplate ? { scriptTemplate } : null,
|
||||
defaultActionId,
|
||||
isRawMode,
|
||||
sql,
|
||||
},
|
||||
},
|
||||
initialData,
|
||||
@@ -783,7 +849,7 @@
|
||||
data,
|
||||
{ forceNewTab = false, tabPreviewMode = false, focusTab = false } = {}
|
||||
) {
|
||||
const { schemaName, pureName, conid, database, objectTypeField } = data;
|
||||
const { schemaName, pureName, conid, database, objectTypeField, sql } = data;
|
||||
const driver = findEngineDriver(data, getExtensions());
|
||||
|
||||
const activeTab = getActiveTab();
|
||||
@@ -829,6 +895,7 @@
|
||||
objectTypeField,
|
||||
defaultActionId: prefferedAction.defaultActionId,
|
||||
isRawMode: prefferedAction?.isRawMode ?? false,
|
||||
sql,
|
||||
},
|
||||
forceNewTab,
|
||||
prefferedAction?.initialData,
|
||||
@@ -970,6 +1037,8 @@
|
||||
|
||||
return handleDatabaseObjectClick(data, { forceNewTab, tabPreviewMode, focusTab });
|
||||
}
|
||||
|
||||
export const TABLE_BACKUP_REGEX = /^_(.*)_(\d\d\d\d)-(\d\d)-(\d\d)-(\d\d)-(\d\d)-(\d\d)$/;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -987,7 +1056,7 @@
|
||||
} from '../stores';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import { extractDbNameFromComposite, filterNameCompoud, getConnectionLabel } from 'dbgate-tools';
|
||||
import { getConnectionInfo } from '../utility/metadataLoaders';
|
||||
import { getConnectionInfo, getDatabaseInfo } from '../utility/metadataLoaders';
|
||||
import fullDisplayName from '../utility/fullDisplayName';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import { findEngineDriver } from 'dbgate-tools';
|
||||
@@ -1008,6 +1077,9 @@
|
||||
import { getSupportedScriptTemplates } from '../utility/applyScriptTemplate';
|
||||
import { getBoolSettingsValue, getOpenDetailOnArrowsSettings } from '../settings/settingsTools';
|
||||
import { isProApp } from '../utility/proTools';
|
||||
import formatFileSize from '../utility/formatFileSize';
|
||||
import { createTableRestoreScript } from '../utility/tableRestoreScript';
|
||||
import newQuery from '../query/newQuery';
|
||||
|
||||
export let data;
|
||||
export let passProps;
|
||||
@@ -1036,6 +1108,9 @@
|
||||
if (data.tableRowCount != null) {
|
||||
res.push(`${formatRowCount(data.tableRowCount)} rows`);
|
||||
}
|
||||
if (data.sizeBytes) {
|
||||
res.push(formatFileSize(data.sizeBytes));
|
||||
}
|
||||
if (data.tableEngine) {
|
||||
res.push(data.tableEngine);
|
||||
}
|
||||
@@ -1044,14 +1119,21 @@
|
||||
}
|
||||
|
||||
$: isPinned = !!$pinnedTables.find(x => testEqual(data, x));
|
||||
|
||||
$: backupParsed = data.objectTypeField === 'tables' ? data.pureName.match(TABLE_BACKUP_REGEX) : null;
|
||||
$: backupTitle =
|
||||
backupParsed != null
|
||||
? `${backupParsed[1]} (${backupParsed[2]}-${backupParsed[3]}-${backupParsed[4]} ${backupParsed[5]}:${backupParsed[6]}:${backupParsed[7]})`
|
||||
: null;
|
||||
</script>
|
||||
|
||||
<AppObjectCore
|
||||
{...$$restProps}
|
||||
module={$$props.module}
|
||||
{data}
|
||||
title={data.schemaName && !passProps?.hideSchemaName ? `${data.schemaName}.${data.pureName}` : data.pureName}
|
||||
icon={databaseObjectIcons[data.objectTypeField]}
|
||||
title={backupTitle ??
|
||||
(data.schemaName && !passProps?.hideSchemaName ? `${data.schemaName}.${data.pureName}` : data.pureName)}
|
||||
icon={backupParsed ? 'img table-backup' : databaseObjectIcons[data.objectTypeField]}
|
||||
menu={createMenu}
|
||||
showPinnedInsteadOfUnpin={passProps?.showPinnedInsteadOfUnpin}
|
||||
onPin={passProps?.ingorePin ? null : isPinned ? null : () => pinnedTables.update(list => [...list, data])}
|
||||
@@ -1062,6 +1144,7 @@
|
||||
: null}
|
||||
extInfo={getExtInfo(data)}
|
||||
isChoosed={matchDatabaseObjectAppObject($selectedDatabaseObjectAppObject, data)}
|
||||
statusIconBefore={data.tablePermissionRole == 'read' ? 'icon lock' : null}
|
||||
on:click={() => handleObjectClick(data, 'leftClick')}
|
||||
on:middleclick={() => handleObjectClick(data, 'middleClick')}
|
||||
on:dblclick={() => handleObjectClick(data, 'dblClick')}
|
||||
|
||||
@@ -142,6 +142,18 @@
|
||||
label: 'Model transform file',
|
||||
};
|
||||
|
||||
const apps: FileTypeHandler = isProApp()
|
||||
? {
|
||||
icon: 'img app',
|
||||
format: 'json',
|
||||
tabComponent: 'AppEditorTab',
|
||||
folder: 'apps',
|
||||
currentConnection: false,
|
||||
extension: 'json',
|
||||
label: 'Application file',
|
||||
}
|
||||
: undefined;
|
||||
|
||||
export const SAVED_FILE_HANDLERS = {
|
||||
sql,
|
||||
shell,
|
||||
@@ -154,6 +166,7 @@
|
||||
modtrans,
|
||||
datadeploy,
|
||||
dbcompare,
|
||||
apps,
|
||||
};
|
||||
|
||||
export const extractKey = data => data.file;
|
||||
@@ -179,6 +192,8 @@
|
||||
import { isProApp } from '../utility/proTools';
|
||||
import { saveFileToDisk } from '../utility/exportFileTools';
|
||||
import { getConnectionInfo } from '../utility/metadataLoaders';
|
||||
import { showSnackbarError } from '../utility/snackbar';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -200,20 +215,30 @@
|
||||
|
||||
function createMenu() {
|
||||
return [
|
||||
handler?.tabComponent && { text: 'Open', onClick: openTab },
|
||||
hasPermission(`files/${data.folder}/write`) && { text: 'Rename', onClick: handleRename },
|
||||
hasPermission(`files/${data.folder}/write`) && { text: 'Create copy', onClick: handleCopy },
|
||||
hasPermission(`files/${data.folder}/write`) && { text: 'Delete', onClick: handleDelete },
|
||||
folder == 'markdown' && { text: 'Show page', onClick: showMarkdownPage },
|
||||
{ text: 'Download', onClick: handleDownload },
|
||||
handler?.tabComponent && { text: _t('common.open', { defaultMessage: 'Open' }), onClick: openTab },
|
||||
|
||||
!data.teamFileId && hasPermission(`files/${data.folder}/write`) && { text: _t('common.rename', { defaultMessage: 'Rename' }), onClick: handleRename },
|
||||
!data.teamFileId && hasPermission(`files/${data.folder}/write`) && { text: _t('common.createCopy', { defaultMessage: 'Create copy' }), onClick: handleCopy },
|
||||
!data.teamFileId && hasPermission(`files/${data.folder}/write`) && { text: _t('common.delete', { defaultMessage: 'Delete' }), onClick: handleDelete },
|
||||
data.teamFileId && data.allowWrite && { text: _t('common.rename', { defaultMessage: 'Rename' }), onClick: handleRename },
|
||||
data.teamFileId &&
|
||||
data.allowRead &&
|
||||
hasPermission('all-team-files/create') && { text: _t('common.createCopy', { defaultMessage: 'Create copy' }), onClick: handleCopy },
|
||||
data.teamFileId && data.allowWrite && { text: _t('common.delete', { defaultMessage: 'Delete' }), onClick: handleDelete },
|
||||
|
||||
folder == 'markdown' && { text: _t('common.showPage', { defaultMessage: 'Show page' }), onClick: showMarkdownPage },
|
||||
!data.teamFileId && { text: _t('common.download', { defaultMessage: 'Download' }), onClick: handleDownload },
|
||||
data.teamFileId && data.allowRead && { text: _t('common.download', { defaultMessage: 'Download' }), onClick: handleDownload },
|
||||
];
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
showModal(ConfirmModal, {
|
||||
message: `Really delete file ${data.file}?`,
|
||||
message: _t('common.reallyDeleteFile', { defaultMessage: 'Really delete file {file}?', values: { file: data.file } }),
|
||||
onConfirm: () => {
|
||||
if (data.folid && data.cntid) {
|
||||
if (data.teamFileId) {
|
||||
apiCall('team-files/delete', { teamFileId: data.teamFileId });
|
||||
} else if (data.folid && data.cntid) {
|
||||
apiCall('cloud/delete-content', {
|
||||
folid: data.folid,
|
||||
cntid: data.cntid,
|
||||
@@ -228,10 +253,12 @@
|
||||
const handleRename = () => {
|
||||
showModal(InputTextModal, {
|
||||
value: data.file,
|
||||
label: 'New file name',
|
||||
header: 'Rename file',
|
||||
label: _t('common.newFileName', { defaultMessage: 'New file name' }),
|
||||
header: _t('common.renameFile', { defaultMessage: 'Rename file' }),
|
||||
onConfirm: newFile => {
|
||||
if (data.folid && data.cntid) {
|
||||
if (data.teamFileId) {
|
||||
apiCall('team-files/update', { teamFileId: data.teamFileId, name: newFile });
|
||||
} else if (data.folid && data.cntid) {
|
||||
apiCall('cloud/rename-content', {
|
||||
folid: data.folid,
|
||||
cntid: data.cntid,
|
||||
@@ -247,10 +274,12 @@
|
||||
const handleCopy = () => {
|
||||
showModal(InputTextModal, {
|
||||
value: data.file,
|
||||
label: 'New file name',
|
||||
header: 'Copy file',
|
||||
label: _t('savedFile.newFileName', { defaultMessage: 'New file name' }),
|
||||
header: _t('savedFile.copyFile', { defaultMessage: 'Copy file' }),
|
||||
onConfirm: newFile => {
|
||||
if (data.folid && data.cntid) {
|
||||
if (data.teamFileId) {
|
||||
apiCall('team-files/copy', { teamFileId: data.teamFileId, newName: newFile });
|
||||
} else if (data.folid && data.cntid) {
|
||||
apiCall('cloud/copy-file', {
|
||||
folid: data.folid,
|
||||
cntid: data.cntid,
|
||||
@@ -266,7 +295,12 @@
|
||||
const handleDownload = () => {
|
||||
saveFileToDisk(
|
||||
async filePath => {
|
||||
if (data.folid && data.cntid) {
|
||||
if (data.teamFileId) {
|
||||
await apiCall('team-files/export-file', {
|
||||
teamFileId: data.teamFileId,
|
||||
filePath,
|
||||
});
|
||||
} else if (data.folid && data.cntid) {
|
||||
await apiCall('cloud/export-file', {
|
||||
folid: data.folid,
|
||||
cntid: data.cntid,
|
||||
@@ -286,7 +320,23 @@
|
||||
|
||||
async function openTab() {
|
||||
let dataContent;
|
||||
if (data.folid && data.cntid) {
|
||||
if (data.teamFileId) {
|
||||
if (data?.metadata?.autoExecute) {
|
||||
if (!data.allowUse) {
|
||||
showSnackbarError(_t('savedFile.noPermissionUseTeamFile', { defaultMessage: 'You do not have permission to use this team file' }));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!data.allowRead) {
|
||||
showSnackbarError(_t('savedFile.noPermissionReadTeamFile', { defaultMessage: 'You do not have permission to read this team file' }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
const resp = await apiCall('team-files/get-content', {
|
||||
teamFileId: data.teamFileId,
|
||||
});
|
||||
dataContent = resp.content;
|
||||
} else if (data.folid && data.cntid) {
|
||||
const resp = await apiCall('cloud/get-content', {
|
||||
folid: data.folid,
|
||||
cntid: data.cntid,
|
||||
@@ -311,6 +361,11 @@
|
||||
tooltip = `${getConnectionLabel(connection)}\n${database}`;
|
||||
}
|
||||
|
||||
if (data?.metadata?.connectionId) {
|
||||
connProps.conid = data.metadata.connectionId;
|
||||
connProps.database = data.metadata.databaseName;
|
||||
}
|
||||
|
||||
openNewTab(
|
||||
{
|
||||
title: data.file,
|
||||
@@ -323,6 +378,8 @@
|
||||
savedFormat: handler.format,
|
||||
savedCloudFolderId: data.folid,
|
||||
savedCloudContentId: data.cntid,
|
||||
savedTeamFileId: data.teamFileId,
|
||||
hideEditor: data.teamFileId && data?.metadata?.autoExecute && !data.allowRead,
|
||||
...connProps,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { __t } from '../translations';
|
||||
export function matchDatabaseObjectAppObject(obj1, obj2) {
|
||||
return (
|
||||
obj1?.objectTypeField == obj2?.objectTypeField &&
|
||||
@@ -11,12 +12,12 @@ export function matchDatabaseObjectAppObject(obj1, obj2) {
|
||||
function getTableLikeActions(dataTab) {
|
||||
return [
|
||||
{
|
||||
label: 'Open data',
|
||||
label: __t('dbObject.openData', { defaultMessage: 'Open data' }),
|
||||
tab: dataTab,
|
||||
defaultActionId: 'openTable',
|
||||
},
|
||||
{
|
||||
label: 'Open raw data',
|
||||
label: __t('dbObject.openRawData', { defaultMessage: 'Open raw data' }),
|
||||
tab: dataTab,
|
||||
defaultActionId: 'openRawTable',
|
||||
isRawMode: true,
|
||||
@@ -33,13 +34,13 @@ function getTableLikeActions(dataTab) {
|
||||
// defaultActionId: 'openForm',
|
||||
// },
|
||||
{
|
||||
label: 'Open structure',
|
||||
label: __t('dbObject.openStructure', { defaultMessage: 'Open structure' }),
|
||||
tab: 'TableStructureTab',
|
||||
icon: 'img table-structure',
|
||||
defaultActionId: 'openStructure',
|
||||
},
|
||||
{
|
||||
label: 'Show SQL',
|
||||
label: __t('dbObject.showSql', { defaultMessage: 'Show SQL' }),
|
||||
tab: 'SqlObjectTab',
|
||||
defaultActionId: 'showSql',
|
||||
icon: 'img sql-file',
|
||||
@@ -53,7 +54,7 @@ export const defaultDatabaseObjectAppObjectActions = {
|
||||
matviews: getTableLikeActions('ViewDataTab'),
|
||||
procedures: [
|
||||
{
|
||||
label: 'Show SQL',
|
||||
label: __t('dbObject.showSql', { defaultMessage: 'Show SQL' }),
|
||||
tab: 'SqlObjectTab',
|
||||
defaultActionId: 'showSql',
|
||||
icon: 'img sql-file',
|
||||
@@ -61,7 +62,7 @@ export const defaultDatabaseObjectAppObjectActions = {
|
||||
],
|
||||
functions: [
|
||||
{
|
||||
label: 'Show SQL',
|
||||
label: __t('dbObject.showSql', { defaultMessage: 'Show SQL' }),
|
||||
tab: 'SqlObjectTab',
|
||||
defaultActionId: 'showSql',
|
||||
icon: 'img sql-file',
|
||||
@@ -69,7 +70,7 @@ export const defaultDatabaseObjectAppObjectActions = {
|
||||
],
|
||||
triggers: [
|
||||
{
|
||||
label: 'Show SQL',
|
||||
label: __t('dbObject.showSql', { defaultMessage: 'Show SQL' }),
|
||||
tab: 'SqlObjectTab',
|
||||
defaultActionId: 'showSql',
|
||||
icon: 'img sql-file',
|
||||
@@ -77,12 +78,12 @@ export const defaultDatabaseObjectAppObjectActions = {
|
||||
],
|
||||
collections: [
|
||||
{
|
||||
label: 'Open data',
|
||||
label: __t('dbObject.openData', { defaultMessage: 'Open data' }),
|
||||
tab: 'CollectionDataTab',
|
||||
defaultActionId: 'openTable',
|
||||
},
|
||||
{
|
||||
label: 'Open JSON',
|
||||
label: __t('dbObject.openJson', { defaultMessage: 'Open JSON' }),
|
||||
tab: 'CollectionDataTab',
|
||||
defaultActionId: 'openJson',
|
||||
initialData: {
|
||||
@@ -94,10 +95,18 @@ export const defaultDatabaseObjectAppObjectActions = {
|
||||
],
|
||||
schedulerEvents: [
|
||||
{
|
||||
label: 'Show SQL',
|
||||
label: __t('dbObject.showSql', { defaultMessage: 'Show SQL' }),
|
||||
tab: 'SqlObjectTab',
|
||||
defaultActionId: 'showSql',
|
||||
icon: 'img sql-file',
|
||||
},
|
||||
],
|
||||
queries: [
|
||||
{
|
||||
label: __t('dbObject.showQuery', { defaultMessage: 'Show query' }),
|
||||
tab: 'QueryDataTab',
|
||||
defaultActionId: 'showAppQuery',
|
||||
icon: 'img app-query',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
46
packages/web/src/buttons/CtaButton.svelte
Normal file
46
packages/web/src/buttons/CtaButton.svelte
Normal file
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
export let disabled = false;
|
||||
export let title = null;
|
||||
|
||||
let domButton;
|
||||
|
||||
export function getBoundingClientRect() {
|
||||
return domButton.getBoundingClientRect();
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="cta-button"
|
||||
{title}
|
||||
{disabled}
|
||||
on:click
|
||||
bind:this={domButton}
|
||||
data-testid={$$props['data-testid']}
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.cta-button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: var(--theme-font-link);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.cta-button:hover:not(:disabled) {
|
||||
color: var(--theme-font-hover);
|
||||
}
|
||||
|
||||
.cta-button:disabled {
|
||||
color: var(--theme-font-3);
|
||||
cursor: not-allowed;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
@@ -9,6 +9,7 @@
|
||||
export let title = null;
|
||||
export let skipWidth = false;
|
||||
export let outline = false;
|
||||
export let colorClass = '';
|
||||
|
||||
function handleClick() {
|
||||
if (!disabled) dispatch('click');
|
||||
@@ -31,6 +32,8 @@
|
||||
bind:this={domButton}
|
||||
class:skipWidth
|
||||
class:outline
|
||||
class={colorClass}
|
||||
class:setBackgroundColor={!colorClass}
|
||||
/>
|
||||
|
||||
<style>
|
||||
@@ -38,19 +41,26 @@
|
||||
border: 1px solid var(--theme-bg-button-inv-2);
|
||||
padding: 5px;
|
||||
margin: 2px;
|
||||
background-color: var(--theme-bg-button-inv);
|
||||
color: var(--theme-font-inv-1);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.setBackgroundColor {
|
||||
background-color: var(--theme-bg-button-inv);
|
||||
}
|
||||
|
||||
input:not(.skipWidth) {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
input:hover:not(.disabled):not(.outline) {
|
||||
input:not(.setBackgroundColor) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input.setBackgroundColor:hover:not(.disabled):not(.outline) {
|
||||
background-color: var(--theme-bg-button-inv-2);
|
||||
}
|
||||
input:active:not(.disabled):not(.outline) {
|
||||
input.setBackgroundColor:active:not(.disabled):not(.outline) {
|
||||
background-color: var(--theme-bg-button-inv-3);
|
||||
}
|
||||
input.disabled {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
export let square = false;
|
||||
export let narrow = false;
|
||||
export let title = null;
|
||||
export let inlineBlock=false;
|
||||
|
||||
let domButton;
|
||||
|
||||
@@ -17,6 +18,7 @@
|
||||
class:disabled
|
||||
class:square
|
||||
class:narrow
|
||||
class:inlineBlock
|
||||
on:click
|
||||
bind:this={domButton}
|
||||
data-testid={$$props['data-testid']}
|
||||
@@ -71,4 +73,8 @@
|
||||
.square {
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
.inlineBlock {
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import getElectron from '../utility/getElectron';
|
||||
import InlineButtonLabel from '../buttons/InlineButtonLabel.svelte';
|
||||
import resolveApi, { resolveApiHeaders } from '../utility/resolveApi';
|
||||
import { _t } from '../translations';
|
||||
|
||||
import uuidv1 from 'uuid/v1';
|
||||
|
||||
@@ -49,11 +50,11 @@
|
||||
</script>
|
||||
|
||||
{#if electron}
|
||||
<InlineButton on:click={handleOpenElectronFile} title="Open file" data-testid={$$props['data-testid']}>
|
||||
<InlineButton on:click={handleOpenElectronFile} title={_t('files.openFile', { defaultMessage: "Open file" })} data-testid={$$props['data-testid']}>
|
||||
<FontIcon {icon} />
|
||||
</InlineButton>
|
||||
{:else}
|
||||
<InlineButtonLabel on:click={() => {}} title="Upload file" data-testid={$$props['data-testid']} htmlFor={inputId}>
|
||||
<InlineButtonLabel on:click={() => {}} title={_t('files.uploadFile', { defaultMessage: "Upload file" })} data-testid={$$props['data-testid']} htmlFor={inputId}>
|
||||
<FontIcon {icon} />
|
||||
</InlineButtonLabel>
|
||||
{/if}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import { isProApp } from '../utility/proTools';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let icon;
|
||||
export let title;
|
||||
@@ -21,7 +22,7 @@
|
||||
data-testid={$$props['data-testid']}
|
||||
title={disabled
|
||||
? isProFeature && !isProApp()
|
||||
? 'This feature is available only in DbGate Premium'
|
||||
? _t('common.featurePremium', { defaultMessage: 'This feature is available only in DbGate Premium' })
|
||||
: disabledMessage
|
||||
: undefined}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script context="module">
|
||||
function getCommandTitle(command) {
|
||||
let res = command.text;
|
||||
let res = _tval(command.text);
|
||||
if (command.keyText || command.keyTextFromGroup) {
|
||||
res += ` (${formatKeyText(command.keyText || command.keyTextFromGroup)})`;
|
||||
}
|
||||
@@ -12,6 +12,8 @@
|
||||
import { commandsCustomized } from '../stores';
|
||||
import { formatKeyText } from '../utility/common';
|
||||
import ToolStripButton from './ToolStripButton.svelte';
|
||||
import _ from 'lodash';
|
||||
import { _tval } from '../translations';
|
||||
|
||||
export let command;
|
||||
export let component = ToolStripButton;
|
||||
@@ -32,6 +34,6 @@
|
||||
{iconAfter}
|
||||
{...$$restProps}
|
||||
>
|
||||
{buttonLabel || cmd.toolbarName || cmd.name}
|
||||
{(_tval(buttonLabel) || _tval(cmd?.toolbarName) || _tval(cmd?.name))}
|
||||
</svelte:component>
|
||||
{/if}
|
||||
|
||||
@@ -4,15 +4,17 @@
|
||||
|
||||
const thisInstance = get_current_component();
|
||||
|
||||
export const activator = createActivator('ToolStripContainer', true);
|
||||
|
||||
$: isComponentActive = $isComponentActiveStore('ToolStripContainer', thisInstance);
|
||||
export let showAlways = false;
|
||||
export const activator = showAlways ? null : createActivator('ToolStripContainer', true);
|
||||
|
||||
export function activate() {
|
||||
activator?.activate();
|
||||
}
|
||||
|
||||
export let scrollContent = false;
|
||||
export let hideToolStrip = false;
|
||||
|
||||
$: isComponentActive = showAlways || ($isComponentActiveStore('ToolStripContainer', thisInstance) && !hideToolStrip);
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:component this={component} {title} {icon} on:click={handleClick}>
|
||||
<svelte:component this={component} {title} {icon} on:click={handleClick} {...$$restProps}>
|
||||
{label}
|
||||
<FontIcon icon="icon chevron-down" />
|
||||
</svelte:component>
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
import hasPermission from '../utility/hasPermission';
|
||||
import ToolStripCommandButton from './ToolStripCommandButton.svelte';
|
||||
import ToolStripDropDownButton from './ToolStripDropDownButton.svelte';
|
||||
|
||||
import _ from 'lodash';
|
||||
import { _tval } from '../translations';
|
||||
export let quickExportHandlerRef = null;
|
||||
export let command = 'sqlDataGrid.export';
|
||||
export let label = 'Export';
|
||||
@@ -39,7 +40,7 @@
|
||||
|
||||
{#if hasPermission('dbops/export')}
|
||||
{#if quickExportHandlerRef}
|
||||
<ToolStripDropDownButton menu={getExportMenu} {label} icon="icon export" />
|
||||
<ToolStripDropDownButton menu={getExportMenu} label={_tval(label)} icon="icon export" />
|
||||
{:else}
|
||||
<ToolStripCommandButton {command} />
|
||||
{/if}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import FormStyledButtonLikeLabel from '../buttons/FormStyledButtonLikeLabel.svelte';
|
||||
import uploadFiles from '../utility/uploadFiles';
|
||||
import { _t } from '../translations';
|
||||
|
||||
const handleChange = e => {
|
||||
const files = [...e.target.files];
|
||||
@@ -9,6 +10,6 @@
|
||||
</script>
|
||||
|
||||
<div class="m-1">
|
||||
<FormStyledButtonLikeLabel htmlFor="uploadFileButton">Upload file</FormStyledButtonLikeLabel>
|
||||
<FormStyledButtonLikeLabel htmlFor="uploadFileButton">{_t('files.uploadFile', { defaultMessage: "Upload file" })}</FormStyledButtonLikeLabel>
|
||||
<input type="file" id="uploadFileButton" hidden on:change={handleChange} />
|
||||
</div>
|
||||
|
||||
398
packages/web/src/celldata/FormCellView.svelte
Normal file
398
packages/web/src/celldata/FormCellView.svelte
Normal file
@@ -0,0 +1,398 @@
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import { tick } from 'svelte';
|
||||
import CellValue from '../datagrid/CellValue.svelte';
|
||||
import { isJsonLikeLongString, safeJsonParse, parseCellValue, stringifyCellValue, filterName } from 'dbgate-tools';
|
||||
import keycodes from '../utility/keycodes';
|
||||
import createRef from '../utility/createRef';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import EditCellDataModal from '../modals/EditCellDataModal.svelte';
|
||||
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
|
||||
import SearchInput from '../elements/SearchInput.svelte';
|
||||
import CloseSearchButton from '../buttons/CloseSearchButton.svelte';
|
||||
import { _t } from '../translations';
|
||||
import ColumnLabel from '../elements/ColumnLabel.svelte';
|
||||
import CheckboxField from '../forms/CheckboxField.svelte';
|
||||
import { getLocalStorage, setLocalStorage } from '../utility/storageCache';
|
||||
import JSONTree from '../jsontree/JSONTree.svelte';
|
||||
import Link from '../elements/Link.svelte';
|
||||
|
||||
export let selection;
|
||||
|
||||
$: firstSelection = selection?.[0];
|
||||
$: rowData = firstSelection?.rowData;
|
||||
$: editable = firstSelection?.editable;
|
||||
$: editorTypes = firstSelection?.editorTypes;
|
||||
$: displayColumns = firstSelection?.displayColumns || [];
|
||||
$: realColumnUniqueNames = firstSelection?.realColumnUniqueNames || [];
|
||||
$: grider = firstSelection?.grider;
|
||||
|
||||
$: uniqueRows = _.uniqBy(selection || [], 'row');
|
||||
$: isMultipleRows = uniqueRows.length > 1;
|
||||
|
||||
function areValuesEqual(val1, val2) {
|
||||
if (val1 === val2) return true;
|
||||
if (val1 == null && val2 == null) return true;
|
||||
if (val1 == null || val2 == null) return false;
|
||||
return _.isEqual(val1, val2);
|
||||
}
|
||||
|
||||
function getFieldValue(colName) {
|
||||
if (!isMultipleRows) return { value: rowData?.[colName], hasMultipleValues: false };
|
||||
|
||||
const values = uniqueRows.map(sel => sel.rowData?.[colName]);
|
||||
const firstValue = values[0];
|
||||
const allSame = values.every(v => areValuesEqual(v, firstValue));
|
||||
|
||||
return allSame ? { value: firstValue, hasMultipleValues: false } : { value: null, hasMultipleValues: true };
|
||||
}
|
||||
|
||||
let filter = '';
|
||||
let notNull = getLocalStorage('dataGridCellDataFormNotNull') === 'true';
|
||||
|
||||
$: orderedFields = realColumnUniqueNames
|
||||
.map(colName => {
|
||||
const col = displayColumns.find(c => c.uniqueName === colName);
|
||||
if (!col) return null;
|
||||
const { value, hasMultipleValues } = getFieldValue(colName);
|
||||
return {
|
||||
...col,
|
||||
value,
|
||||
hasMultipleValues,
|
||||
// columnName: col.columnName || colName,
|
||||
// uniqueName: colName,
|
||||
// value,
|
||||
// hasMultipleValues,
|
||||
// col,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
$: filteredFields = orderedFields
|
||||
.filter(field => filterName(filter, field.columnName))
|
||||
.filter(field => {
|
||||
if (notNull) {
|
||||
return field.value != null || field.hasMultipleValues;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
let editingColumn = null;
|
||||
let editValue = '';
|
||||
let domEditor = null;
|
||||
const isChangedRef = createRef(false);
|
||||
|
||||
function isJsonValue(value) {
|
||||
if (
|
||||
_.isPlainObject(value) &&
|
||||
!(value?.type == 'Buffer' && _.isArray(value.data)) &&
|
||||
!value.$oid &&
|
||||
!value.$bigint &&
|
||||
!value.$decimal
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (_.isArray(value)) return true;
|
||||
if (typeof value !== 'string') return false;
|
||||
if (!isJsonLikeLongString(value)) return false;
|
||||
const parsed = safeJsonParse(value);
|
||||
return parsed !== null && (_.isPlainObject(parsed) || _.isArray(parsed));
|
||||
}
|
||||
|
||||
function getJsonObject(value) {
|
||||
if (_.isPlainObject(value) || _.isArray(value)) return value;
|
||||
if (typeof value === 'string') return safeJsonParse(value);
|
||||
return null;
|
||||
}
|
||||
|
||||
function handleClick(field) {
|
||||
if (!editable || !grider) return;
|
||||
if (isJsonValue(field.value)) return;
|
||||
// if (isJsonValue(field.value) && !field.hasMultipleValues) {
|
||||
// openEditModal(field);
|
||||
// return;
|
||||
// }
|
||||
startEditing(field);
|
||||
}
|
||||
|
||||
function handleDoubleClick(field) {
|
||||
if (!editable || !grider) return;
|
||||
if (isJsonValue(field.value) && !field.hasMultipleValues) {
|
||||
openEditModal(field);
|
||||
return;
|
||||
}
|
||||
startEditing(field);
|
||||
}
|
||||
|
||||
function startEditing(field) {
|
||||
if (!editable || !grider) return;
|
||||
editingColumn = field.uniqueName;
|
||||
editValue = field.hasMultipleValues ? '' : stringifyCellValue(field.value, 'inlineEditorIntent', editorTypes).value;
|
||||
isChangedRef.set(false);
|
||||
tick().then(() => {
|
||||
if (!domEditor) return;
|
||||
domEditor.focus();
|
||||
if (!field.hasMultipleValues) domEditor.select();
|
||||
});
|
||||
}
|
||||
|
||||
function handleKeyDown(event, field) {
|
||||
switch (event.keyCode) {
|
||||
case keycodes.escape:
|
||||
isChangedRef.set(false);
|
||||
editingColumn = null;
|
||||
break;
|
||||
case keycodes.enter:
|
||||
if (isChangedRef.get()) {
|
||||
saveValue(field);
|
||||
}
|
||||
editingColumn = null;
|
||||
event.preventDefault();
|
||||
break;
|
||||
case keycodes.tab:
|
||||
case keycodes.upArrow:
|
||||
case keycodes.downArrow:
|
||||
const reverse = event.keyCode === keycodes.upArrow || (event.keyCode === keycodes.tab && event.shiftKey);
|
||||
event.preventDefault();
|
||||
moveToNextField(field, reverse);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function moveToNextField(field, reverse) {
|
||||
const currentIndex = filteredFields.findIndex(f => f.uniqueName === field.uniqueName);
|
||||
const nextIndex = reverse ? currentIndex - 1 : currentIndex + 1;
|
||||
const nextField = filteredFields[nextIndex];
|
||||
if (!nextField) return;
|
||||
|
||||
if (isChangedRef.get()) {
|
||||
saveValue(field);
|
||||
}
|
||||
editingColumn = null;
|
||||
if (nextIndex < 0 || nextIndex >= filteredFields.length) return;
|
||||
|
||||
tick().then(() => {
|
||||
startEditing(nextField);
|
||||
// if (isJsonValue(nextField.value)) {
|
||||
// openEditModal(nextField);
|
||||
// } else {
|
||||
// startEditing(nextField);
|
||||
// }
|
||||
});
|
||||
}
|
||||
|
||||
function handleSearchKeyDown(e) {
|
||||
if (e.keyCode === keycodes.backspace && (e.metaKey || e.ctrlKey)) {
|
||||
filter = '';
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
function handleBlur(field) {
|
||||
if (isChangedRef.get()) {
|
||||
saveValue(field);
|
||||
}
|
||||
editingColumn = null;
|
||||
}
|
||||
|
||||
function setCellValue(fieldName, value) {
|
||||
if (!grider) return;
|
||||
|
||||
if (selection.length > 0) {
|
||||
const uniqueRowIndices = _.uniq(selection.map(x => x.row));
|
||||
grider.beginUpdate();
|
||||
for (const row of uniqueRowIndices) {
|
||||
grider.setCellValue(row, fieldName, value);
|
||||
}
|
||||
grider.endUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
function saveValue(field) {
|
||||
if (!grider) return;
|
||||
const parsedValue = parseCellValue(editValue, editorTypes);
|
||||
setCellValue(field.uniqueName, parsedValue);
|
||||
isChangedRef.set(false);
|
||||
}
|
||||
|
||||
function openEditModal(field) {
|
||||
if (!grider) return;
|
||||
showModal(EditCellDataModal, {
|
||||
value: field.value,
|
||||
dataEditorTypesBehaviour: editorTypes,
|
||||
onSave: value => setCellValue(field.uniqueName, value),
|
||||
});
|
||||
}
|
||||
|
||||
function getJsonParsedValue(value) {
|
||||
if (editorTypes?.explicitDataType) return null;
|
||||
if (!isJsonLikeLongString(value)) return null;
|
||||
return safeJsonParse(value);
|
||||
}
|
||||
|
||||
function handleEdit(field) {
|
||||
editingColumn = null;
|
||||
openEditModal(field);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="outer">
|
||||
<div class="content">
|
||||
{#if rowData}
|
||||
<div class="search-wrapper" on:keydown={handleSearchKeyDown}>
|
||||
<SearchBoxWrapper noMargin>
|
||||
<SearchInput
|
||||
placeholder={_t('tableCell.filterColumns', { defaultMessage: 'Filter columns' })}
|
||||
bind:value={filter}
|
||||
/>
|
||||
<CloseSearchButton bind:filter />
|
||||
</SearchBoxWrapper>
|
||||
<CheckboxField
|
||||
defaultChecked={notNull}
|
||||
on:change={e => {
|
||||
// @ts-ignore
|
||||
notNull = e.target.checked;
|
||||
setLocalStorage('dataGridCellDataFormNotNull', notNull ? 'true' : 'false');
|
||||
}}
|
||||
/>
|
||||
{_t('tableCell.hideNullValues', { defaultMessage: 'Hide NULL values' })}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="inner">
|
||||
{#if !rowData}
|
||||
<div class="no-data">{_t('tableCell.noDataSelected', { defaultMessage: 'No data selected' })}</div>
|
||||
{:else}
|
||||
{#each filteredFields as field (field.uniqueName)}
|
||||
<div class="field">
|
||||
<div class="field-name">
|
||||
<ColumnLabel {...field} showDataType /><Link onClick={() => handleEdit(field)}
|
||||
>{_t('tableCell.edit', { defaultMessage: 'Edit' })}
|
||||
</Link>
|
||||
</div>
|
||||
<div class="field-value" class:editable on:click={() => handleClick(field)}>
|
||||
{#if editingColumn === field.uniqueName}
|
||||
<div class="editor-wrapper">
|
||||
<input
|
||||
type="text"
|
||||
bind:this={domEditor}
|
||||
bind:value={editValue}
|
||||
on:input={() => isChangedRef.set(true)}
|
||||
on:keydown={e => handleKeyDown(e, field)}
|
||||
on:blur={() => handleBlur(field)}
|
||||
class="inline-editor"
|
||||
/>
|
||||
</div>
|
||||
{:else if field.hasMultipleValues}
|
||||
<span class="multiple-values"
|
||||
>({_t('tableCell.multipleValues', { defaultMessage: 'Multiple values' })})</span
|
||||
>
|
||||
{:else if isJsonValue(field.value)}
|
||||
<JSONTree value={getJsonParsedValue(field.value)} />
|
||||
{:else}
|
||||
<CellValue
|
||||
{rowData}
|
||||
value={field.value}
|
||||
jsonParsedValue={getJsonParsedValue(field.value)}
|
||||
{editorTypes}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.outer {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search-wrapper {
|
||||
padding: 4px 4px 0 4px;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--theme-border);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.inner {
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
color: var(--theme-font-3);
|
||||
font-style: italic;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid var(--theme-border);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.field-name {
|
||||
background: var(--theme-bg-1);
|
||||
padding: 4px 8px;
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
color: var(--theme-font-2);
|
||||
border-bottom: 1px solid var(--theme-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.field-value {
|
||||
padding: 6px 8px;
|
||||
background: var(--theme-bg-0);
|
||||
min-height: 20px;
|
||||
word-break: break-all;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.field-value.editable {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.editor-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.inline-editor {
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: var(--theme-bg-0);
|
||||
color: var(--theme-font-1);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.inline-editor:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.multiple-values {
|
||||
color: var(--theme-font-3);
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
@@ -10,6 +10,9 @@
|
||||
if (value?.type == 'Buffer' && _.isArray(value?.data)) {
|
||||
return 'data:image/png;base64, ' + btoa(String.fromCharCode.apply(null, value?.data));
|
||||
}
|
||||
if (value?.$binary?.base64) {
|
||||
return 'data:image/png;base64, ' + value.$binary.base64;
|
||||
}
|
||||
return null;
|
||||
} catch (err) {
|
||||
console.log('Error showing picture', err);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user