dbgate-api
Version:
Allows run DbGate data-manipulation scripts.
1,010 lines (892 loc) • 35.1 kB
JavaScript
const connections = require('./connections');
const runners = require('./runners');
const archive = require('./archive');
const socket = require('../utility/socket');
const { fork } = require('child_process');
const {
DatabaseAnalyser,
computeDbDiffRows,
getCreateObjectScript,
getAlterDatabaseScript,
generateDbPairingId,
matchPairedObjects,
extendDatabaseInfo,
modelCompareDbDiffOptions,
getLogger,
extractErrorLogData,
filterStructureBySchema,
} = require('dbgate-tools');
const { html, parse } = require('diff2html');
const { handleProcessCommunication } = require('../utility/processComm');
const config = require('./config');
const fs = require('fs-extra');
const exportDbModel = require('../utility/exportDbModel');
const { archivedir, resolveArchiveFolder, uploadsdir } = require('../utility/directories');
const path = require('path');
const importDbModel = require('../utility/importDbModel');
const requireEngineDriver = require('../utility/requireEngineDriver');
const generateDeploySql = require('../shell/generateDeploySql');
const { createTwoFilesPatch } = require('diff');
const diff2htmlPage = require('../utility/diff2htmlPage');
const processArgs = require('../utility/processArgs');
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');
const loadModelTransform = require('../utility/loadModelTransform');
const exportDbModelSql = require('../utility/exportDbModelSql');
const axios = require('axios');
const { callTextToSqlApi, callCompleteOnCursorApi, callRefactorSqlQueryApi } = require('../utility/authProxy');
const { decryptConnection } = require('../utility/crypting');
const { getSshTunnel } = require('../utility/sshTunnel');
const sessions = require('./sessions');
const jsldata = require('./jsldata');
const { sendToAuditLog } = require('../utility/auditlog');
const logger = getLogger('databaseConnections');
module.exports = {
/** @type {import('dbgate-types').OpenedDatabaseConnection[]} */
opened: [],
closed: {},
requests: {},
async _init() {
connections._closeAll = conid => this.closeAll(conid);
},
handle_structure(conid, database, { structure }) {
const existing = this.opened.find(x => x.conid == conid && x.database == database);
if (!existing) return;
existing.structure = structure;
socket.emitChanged('database-structure-changed', { conid, database });
},
handle_structureTime(conid, database, { analysedTime }) {
const existing = this.opened.find(x => x.conid == conid && x.database == database);
if (!existing) return;
existing.analysedTime = analysedTime;
socket.emitChanged(`database-status-changed`, { conid, database });
},
handle_version(conid, database, { version }) {
const existing = this.opened.find(x => x.conid == conid && x.database == database);
if (!existing) return;
existing.serverVersion = version;
socket.emitChanged(`database-server-version-changed`, { conid, database });
},
handle_error(conid, database, props) {
const { error } = props;
logger.error(`DBGM-00102 Error in database connection ${conid}, database ${database}: ${error}`);
if (props?.msgid) {
const [resolve, reject] = this.requests[props?.msgid];
reject(error);
delete this.requests[props?.msgid];
}
},
handle_response(conid, database, { msgid, ...response }) {
const [resolve, reject, additionalData] = this.requests[msgid];
resolve(response);
if (additionalData?.auditLogger) {
additionalData?.auditLogger(response);
}
delete this.requests[msgid];
},
handle_status(conid, database, { status }) {
// console.log('HANDLE SET STATUS', status);
const existing = this.opened.find(x => x.conid == conid && x.database == database);
if (!existing) return;
if (existing.status && status && existing.status.counter > status.counter) return;
existing.status = status;
socket.emitChanged(`database-status-changed`, { conid, database });
},
handle_ping() { },
// session event handlers
handle_info(conid, database, props) {
const { sesid, info } = props;
sessions.dispatchMessage(sesid, info);
},
handle_done(conid, database, props) {
const { sesid } = props;
socket.emit(`session-done-${sesid}`);
sessions.dispatchMessage(sesid, 'Query execution finished');
},
handle_recordset(conid, database, props) {
const { jslid, resultIndex } = props;
socket.emit(`session-recordset-${props.sesid}`, { jslid, resultIndex });
},
handle_stats(conid, database, stats) {
jsldata.notifyChangedStats(stats);
},
handle_initializeFile(conid, database, props) {
const { jslid } = props;
socket.emit(`session-initialize-file-${jslid}`);
},
// eval event handler
handle_runnerDone(conid, database, props) {
const { runid } = props;
socket.emit(`runner-done-${runid}`);
},
handle_progress(conid, database, progressData) {
const { progressName } = progressData;
const { name, runid } = progressName;
socket.emit(`runner-progress-${runid}`, { ...progressData, progressName: name });
},
handle_copyStreamError(conid, database, { copyStreamError }) {
const { progressName } = copyStreamError;
const { runid } = progressName;
logger.error(`DBGM-00103 Error in database connection ${conid}, database ${database}: ${copyStreamError}`);
socket.emit(`runner-done-${runid}`);
},
async ensureOpened(conid, database) {
const existing = this.opened.find(x => x.conid == conid && x.database == database);
if (existing) return existing;
const connection = await connections.getCore({ conid });
if (!connection) {
throw new Error(`databaseConnections: Connection with conid="${conid}" not found`);
}
if (connection.passwordMode == 'askPassword' || connection.passwordMode == 'askUser') {
throw new MissingCredentialsError({ conid, passwordMode: connection.passwordMode });
}
if (connection.useRedirectDbLogin) {
throw new MissingCredentialsError({ conid, redirectToDbLogin: true });
}
const subprocess = fork(
global['API_PACKAGE'] || process.argv[1],
[
'--is-forked-api',
'--start-process',
'databaseConnectionProcess',
...processArgs.getPassArgs(),
// ...process.argv.slice(3),
],
{
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
}
);
pipeForkLogs(subprocess);
const lastClosed = this.closed[`${conid}/${database}`];
const newOpened = {
conid,
database,
subprocess,
structure: lastClosed ? lastClosed.structure : DatabaseAnalyser.createEmptyStructure(),
serverVersion: lastClosed ? lastClosed.serverVersion : null,
connection,
status: { name: 'pending' },
};
this.opened.push(newOpened);
subprocess.on('message', message => {
// @ts-ignore
const { msgtype } = message;
if (handleProcessCommunication(message, subprocess)) return;
if (newOpened.disconnected) return;
const funcName = `handle_${msgtype}`;
if (!this[funcName]) {
logger.error(`DBGM-00104 Unknown message type ${msgtype} from subprocess databaseConnectionProcess`);
return;
}
this[funcName](conid, database, message);
});
subprocess.on('exit', () => {
if (newOpened.disconnected) return;
this.close(conid, database, false);
});
subprocess.on('error', err => {
logger.error(extractErrorLogData(err), 'DBGM-00114 Error in database connection subprocess');
if (newOpened.disconnected) return;
this.close(conid, database, false);
});
subprocess.send({
msgtype: 'connect',
connection: { ...connection, database },
structure: lastClosed ? lastClosed.structure : null,
globalSettings: await config.getSettings(),
});
return newOpened;
},
/** @param {import('dbgate-types').OpenedDatabaseConnection} conn */
sendRequest(conn, message, additionalData = {}) {
const msgid = crypto.randomUUID();
const promise = new Promise((resolve, reject) => {
this.requests[msgid] = [resolve, reject, additionalData];
try {
conn.subprocess.send({ msgid, ...message });
} catch (err) {
logger.error(extractErrorLogData(err), 'DBGM-00115 Error sending request do process');
this.close(conn.conid, conn.database);
}
});
return promise;
},
queryData_meta: true,
async queryData({ conid, database, sql }, 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') {
// return opened.status;
// }
const res = await this.sendRequest(opened, { msgtype: 'queryData', sql });
return res;
},
sqlSelect_meta: true,
async sqlSelect({ conid, database, select, auditLogSessionGroup }, req) {
await testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid, database);
const res = await this.sendRequest(
opened,
{ msgtype: 'sqlSelect', select },
{
auditLogger:
auditLogSessionGroup && select?.from?.name?.pureName
? response => {
sendToAuditLog(req, {
category: 'dbop',
component: 'DatabaseConnectionsController',
event: 'sql.select',
action: 'select',
severity: 'info',
conid,
database,
schemaName: select?.from?.name?.schemaName,
pureName: select?.from?.name?.pureName,
sumint1: response?.rows?.length,
sessionParam: `${conid}::${database}::${select?.from?.name?.schemaName || '0'}::${select?.from?.name?.pureName
}`,
sessionGroup: auditLogSessionGroup,
message: `Loaded table data from ${select?.from?.name?.pureName}`,
});
}
: null,
}
);
return res;
},
runScript_meta: true,
async runScript({ conid, database, sql, useTransaction, logMessage }, 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, {
category: 'dbop',
component: 'DatabaseConnectionsController',
event: 'sql.runscript',
action: 'runscript',
severity: 'info',
conid,
database,
detail: sql,
message: logMessage || `Running SQL script`,
});
const res = await this.sendRequest(opened, { msgtype: 'runScript', sql, useTransaction });
return res;
},
runOperation_meta: true,
async runOperation({ conid, database, operation, useTransaction }, req) {
await testConnectionPermission(conid, req);
logger.info({ conid, database, operation }, 'DBGM-00009 Processing operation');
sendToAuditLog(req, {
category: 'dbop',
component: 'DatabaseConnectionsController',
event: 'sql.runoperation',
action: operation.type,
severity: 'info',
conid,
database,
detail: operation,
message: `Running DB operation: ${operation.type}`,
});
const opened = await this.ensureOpened(conid, database);
const res = await this.sendRequest(opened, { msgtype: 'runOperation', operation, useTransaction });
return res;
},
collectionData_meta: true,
async collectionData({ conid, database, options, auditLogSessionGroup }, req) {
await testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid, database);
const res = await this.sendRequest(
opened,
{ msgtype: 'collectionData', options },
{
auditLogger:
auditLogSessionGroup && options?.pureName
? response => {
sendToAuditLog(req, {
category: 'dbop',
component: 'DatabaseConnectionsController',
event: 'nosql.collectionData',
action: 'select',
severity: 'info',
conid,
database,
pureName: options?.pureName,
sumint1: response?.result?.rows?.length,
sessionParam: `${conid}::${database}::${options?.pureName}`,
sessionGroup: auditLogSessionGroup,
message: `Loaded collection data ${options?.pureName}`,
});
}
: null,
}
);
return res.result || null;
},
async loadDataCore(msgtype, { conid, database, ...args }, req) {
await testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid, database);
const res = await this.sendRequest(opened, { msgtype, ...args });
if (res.errorMessage) {
console.error(res.errorMessage);
return {
errorMessage: res.errorMessage,
};
}
return res.result || null;
},
schemaList_meta: true,
async schemaList({ conid, database }, req) {
await testConnectionPermission(conid, req);
return this.loadDataCore('schemaList', { conid, database });
},
dispatchDatabaseChangedEvent_meta: true,
dispatchDatabaseChangedEvent({ event, conid, database }) {
socket.emitChanged(event, { conid, database });
return null;
},
loadKeys_meta: true,
async loadKeys({ conid, database, root, filter, limit }, 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) {
await testConnectionPermission(conid, req);
return this.loadDataCore('scanKeys', { conid, database, root, pattern, cursor, count });
},
exportKeys_meta: true,
async exportKeys({ conid, database, options }, req) {
await testConnectionPermission(conid, req);
return this.loadDataCore('exportKeys', { conid, database, options });
},
loadKeyInfo_meta: true,
async loadKeyInfo({ conid, database, key }, req) {
await testConnectionPermission(conid, req);
return this.loadDataCore('loadKeyInfo', { conid, database, key });
},
loadKeyTableRange_meta: true,
async loadKeyTableRange({ conid, database, key, cursor, count }, 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) {
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) {
await testConnectionPermission(conid, req);
return this.loadDataCore('callMethod', { conid, database, method, args });
// const opened = await this.ensureOpened(conid, database);
// const res = await this.sendRequest(opened, { msgtype: 'callMethod', method, args });
// if (res.errorMessage) {
// console.error(res.errorMessage);
// }
// return res.result || null;
},
updateCollection_meta: true,
async updateCollection({ conid, database, changeSet }, req) {
await testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid, database);
const res = await this.sendRequest(opened, { msgtype: 'updateCollection', changeSet });
if (res.errorMessage) {
return {
errorMessage: res.errorMessage,
};
}
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) {
return {
name: 'error',
message: 'No connection',
};
}
await testConnectionPermission(conid, req);
const existing = this.opened.find(x => x.conid == conid && x.database == database);
if (existing) {
return {
...existing.status,
analysedTime: existing.analysedTime,
};
}
const lastClosed = this.closed[`${conid}/${database}`];
if (lastClosed) {
return {
...lastClosed.status,
analysedTime: lastClosed.analysedTime,
};
}
return {
name: 'error',
message: 'Not connected',
};
},
ping_meta: true,
async ping({ conid, database }, req) {
await testConnectionPermission(conid, req);
let existing = this.opened.find(x => x.conid == conid && x.database == database);
if (existing) {
try {
existing.subprocess.send({ msgtype: 'ping' });
} catch (err) {
logger.error(extractErrorLogData(err), 'DBGM-00116 Error pinging DB connection');
this.close(conid, database);
return {
status: 'error',
message: 'Ping failed',
};
}
} else {
// @ts-ignore
existing = await this.ensureOpened(conid, database);
}
return {
status: 'ok',
connectionStatus: existing ? existing.status : null,
};
},
refresh_meta: true,
async refresh({ conid, database, keepOpen }, req) {
await testConnectionPermission(conid, req);
if (!keepOpen) this.close(conid, database);
await this.ensureOpened(conid, database);
return { status: 'ok' };
},
syncModel_meta: true,
async syncModel({ conid, database, isFullRefresh }, req) {
if (conid == '__model') {
socket.emitChanged('database-structure-changed', { conid, database });
return { status: 'ok' };
}
await testConnectionPermission(conid, req);
const conn = await this.ensureOpened(conid, database);
conn.subprocess.send({ msgtype: 'syncModel', isFullRefresh });
return { status: 'ok' };
},
close(conid, database, kill = true) {
const existing = this.opened.find(x => x.conid == conid && x.database == database);
if (existing) {
existing.disconnected = true;
if (kill) {
try {
existing.subprocess.kill();
} catch (err) {
logger.error(extractErrorLogData(err), 'DBGM-00117 Error killing subprocess');
}
}
this.opened = this.opened.filter(x => x.conid != conid || x.database != database);
this.closed[`${conid}/${database}`] = {
status: {
...existing.status,
name: 'error',
},
structure: existing.structure,
};
socket.emitChanged(`database-status-changed`, { conid, database });
}
},
closeAll(conid, kill = true) {
for (const existing of this.opened.filter(x => x.conid == conid)) {
this.close(conid, existing.database, kill);
}
},
disconnect_meta: true,
async disconnect({ conid, database }, req) {
await testConnectionPermission(conid, req);
await this.close(conid, database, true);
return { status: 'ok' };
},
structure_meta: true,
async structure({ conid, database, modelTransFile = null }, req) {
if (!conid || !database) {
return {};
}
const loadedPermissions = await loadPermissionsFromRequest(req);
await testConnectionPermission(conid, req, loadedPermissions);
if (conid == '__model') {
const model = await importDbModel(database);
const trans = await loadModelTransform(modelTransFile);
return trans ? trans(model) : model;
}
const opened = await this.ensureOpened(conid, database);
sendToAuditLog(req, {
category: 'dbop',
component: 'DatabaseConnectionsController',
action: 'structure',
event: 'dbStructure.get',
severity: 'info',
conid,
database,
sessionParam: `${conid}::${database}`,
sessionGroup: 'getStructure',
message: `Loaded database structure for ${database}`,
});
if (!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;
// return {
// name: 'error',
// message: 'Not connected',
// };
},
serverVersion_meta: true,
async serverVersion({ conid, database }, req) {
if (!conid) {
return null;
}
await testConnectionPermission(conid, req);
if (!conid) return null;
const opened = await this.ensureOpened(conid, database);
return opened.serverVersion || null;
},
sqlPreview_meta: true,
async sqlPreview({ conid, database, objects, options }, req) {
await testConnectionPermission(conid, req);
// wait for structure
await this.structure({ conid, database });
const opened = await this.ensureOpened(conid, database);
const res = await this.sendRequest(opened, { msgtype: 'sqlPreview', objects, options });
return res;
},
exportModel_meta: true,
async exportModel({ conid, database, outputFolder, schema }, req) {
await testConnectionPermission(conid, req);
const realFolder = outputFolder.startsWith('archive:')
? resolveArchiveFolder(outputFolder.substring('archive:'.length))
: outputFolder;
const model = await this.structure({ conid, database });
const filteredModel = schema ? filterStructureBySchema(model, schema) : model;
await exportDbModel(extendDatabaseInfo(filteredModel), realFolder);
if (outputFolder.startsWith('archive:')) {
socket.emitChanged(`archive-files-changed`, { folder: outputFolder.substring('archive:'.length) });
}
return { status: 'ok' };
},
exportModelSql_meta: true,
async exportModelSql({ conid, database, outputFolder, outputFile, schema }, req) {
await testConnectionPermission(conid, req);
const connection = await connections.getCore({ conid });
const driver = requireEngineDriver(connection);
const model = await this.structure({ conid, database });
const filteredModel = schema ? filterStructureBySchema(model, schema) : model;
await exportDbModelSql(extendDatabaseInfo(filteredModel), driver, outputFolder, outputFile);
return { status: 'ok' };
},
generateDeploySql_meta: true,
async generateDeploySql({ conid, database, archiveFolder }, req) {
await testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid, database);
const res = await this.sendRequest(opened, {
msgtype: 'generateDeploySql',
modelFolder: resolveArchiveFolder(archiveFolder),
});
return res;
// const connection = await connections.get({ conid });
// return generateDeploySql({
// connection,
// analysedStructure: await this.structure({ conid, database }),
// modelFolder: resolveArchiveFolder(archiveFolder),
// });
// const deployedModel = generateDbPairingId(await importDbModel(path.join(archivedir(), archiveFolder)));
// const currentModel = generateDbPairingId(await this.structure({ conid, database }));
// const currentModelPaired = matchPairedObjects(deployedModel, currentModel);
// const connection = await connections.get({ conid });
// const driver = requireEngineDriver(connection);
// const { sql } = getAlterDatabaseScript(currentModelPaired, deployedModel, {}, deployedModel, driver);
// return {
// deployedModel,
// currentModel,
// currentModelPaired,
// sql,
// };
// return sql;
},
// runCommand_meta: true,
// async runCommand({ conid, database, sql }) {
// console.log(`Running SQL command , conid=${conid}, database=${database}, sql=${sql}`);
// const opened = await this.ensureOpened(conid, database);
// const res = await this.sendRequest(opened, { msgtype: 'queryData', sql });
// return res;
// },
async getUnifiedDiff({ sourceConid, sourceDatabase, targetConid, targetDatabase }) {
const dbDiffOptions = sourceConid == '__model' ? modelCompareDbDiffOptions : {};
const sourceDb = generateDbPairingId(
extendDatabaseInfo(await this.structure({ conid: sourceConid, database: sourceDatabase }))
);
const targetDb = generateDbPairingId(
extendDatabaseInfo(await this.structure({ conid: targetConid, database: targetDatabase }))
);
// const sourceConnection = await connections.getCore({conid:sourceConid})
const connection = await connections.getCore({ conid: targetConid });
const driver = requireEngineDriver(connection);
const targetDbPaired = matchPairedObjects(sourceDb, targetDb, dbDiffOptions);
const diffRows = computeDbDiffRows(sourceDb, targetDbPaired, dbDiffOptions, driver);
// console.log('sourceDb', sourceDb);
// console.log('targetDb', targetDb);
// console.log('sourceConid, sourceDatabase', sourceConid, sourceDatabase);
let res = '';
for (const row of diffRows) {
// console.log('PAIR', row.source && row.source.pureName, row.target && row.target.pureName);
const unifiedDiff = createTwoFilesPatch(
(row.target && row.target.pureName) || '',
(row.source && row.source.pureName) || '',
getCreateObjectScript(row.target, driver),
getCreateObjectScript(row.source, driver),
'',
''
);
res += unifiedDiff;
}
return res;
},
generateDbDiffReport_meta: true,
async generateDbDiffReport({ filePath, sourceConid, sourceDatabase, targetConid, targetDatabase }) {
const unifiedDiff = await this.getUnifiedDiff({ sourceConid, sourceDatabase, targetConid, targetDatabase });
const diffJson = parse(unifiedDiff);
// $: diffHtml = html(diffJson, { outputFormat: 'side-by-side', drawFileList: false });
const diffHtml = html(diffJson, { outputFormat: 'side-by-side' });
await fs.writeFile(filePath, diff2htmlPage(diffHtml));
return true;
},
textToSql_meta: true,
async textToSql({ conid, database, text, dialect }) {
const existing = this.opened.find(x => x.conid == conid && x.database == database);
const { structure } = existing || {};
if (!structure) return { errorMessage: 'No database structure' };
const res = await callTextToSqlApi(text, structure, dialect);
if (!res?.sql) {
return { errorMessage: 'No SQL generated' };
}
return res;
},
completeOnCursor_meta: true,
async completeOnCursor({ conid, database, text, dialect, line }) {
const existing = this.opened.find(x => x.conid == conid && x.database == database);
const { structure } = existing || {};
if (!structure) return { errorMessage: 'No database structure' };
const res = await callCompleteOnCursorApi(text, structure, dialect, line);
if (!res?.variants) {
return { errorMessage: 'No SQL generated' };
}
return res;
},
refactorSqlQuery_meta: true,
async refactorSqlQuery({ conid, database, query, task, dialect }) {
const existing = this.opened.find(x => x.conid == conid && x.database == database);
const { structure } = existing || {};
if (!structure) return { errorMessage: 'No database structure' };
const res = await callRefactorSqlQueryApi(query, task, structure, dialect);
if (!res?.sql) {
return { errorMessage: 'No SQL generated' };
}
return res;
},
async getNativeOpCommandArgs(
command,
{ conid, database, outputFile, inputFile, options, selectedTables, skippedTables, argsFormat }
) {
const sourceConnection = await connections.getCore({ conid });
const connection = {
...decryptConnection(sourceConnection),
};
const driver = requireEngineDriver(connection);
if (!connection.port && driver.defaultPort) {
connection.port = driver.defaultPort.toString();
}
if (connection.useSshTunnel) {
const tunnel = await getSshTunnel(connection);
if (tunnel.state == 'error') {
throw new Error(tunnel.message);
}
connection.server = tunnel.localHost;
connection.port = tunnel.localPort;
}
const settingsValue = await config.getSettings();
const externalTools = {};
for (const pair of Object.entries(settingsValue || {})) {
const [name, value] = pair;
if (name.startsWith('externalTools.')) {
externalTools[name.substring('externalTools.'.length)] = value;
}
}
return {
...(command == 'backup'
? driver.backupDatabaseCommand(
connection,
{ outputFile, database, options, selectedTables, skippedTables, argsFormat },
// @ts-ignore
externalTools
)
: driver.restoreDatabaseCommand(
connection,
{ inputFile, database, options, argsFormat },
// @ts-ignore
externalTools
)),
transformMessage: driver.transformNativeCommandMessage
? message => driver.transformNativeCommandMessage(message, command)
: null,
};
},
commandArgsToCommandLine(commandArgs) {
const { command, args, stdinFilePath } = commandArgs;
let res = `${command} ${args.join(' ')}`;
if (stdinFilePath) {
res += ` < ${stdinFilePath}`;
}
return res;
},
nativeBackup_meta: true,
async nativeBackup({ conid, database, outputFile, runid, options, selectedTables, skippedTables }) {
const commandArgs = await this.getNativeOpCommandArgs('backup', {
conid,
database,
inputFile: undefined,
outputFile,
options,
selectedTables,
skippedTables,
argsFormat: 'spawn',
});
return runners.nativeRunCore(runid, {
...commandArgs,
onFinished: () => {
socket.emitChanged(`files-changed`, { folder: 'sql' });
socket.emitChanged(`all-files-changed`);
},
});
},
nativeBackupCommand_meta: true,
async nativeBackupCommand({ conid, database, outputFile, options, selectedTables, skippedTables }) {
const commandArgs = await this.getNativeOpCommandArgs('backup', {
conid,
database,
outputFile,
inputFile: undefined,
options,
selectedTables,
skippedTables,
argsFormat: 'shell',
});
return {
...commandArgs,
transformMessage: null,
commandLine: this.commandArgsToCommandLine(commandArgs),
};
},
nativeRestore_meta: true,
async nativeRestore({ conid, database, inputFile, runid }) {
const commandArgs = await this.getNativeOpCommandArgs('restore', {
conid,
database,
inputFile,
outputFile: undefined,
options: undefined,
argsFormat: 'spawn',
});
return runners.nativeRunCore(runid, {
...commandArgs,
onFinished: () => {
this.syncModel({ conid, database, isFullRefresh: true });
},
});
},
nativeRestoreCommand_meta: true,
async nativeRestoreCommand({ conid, database, inputFile }) {
const commandArgs = await this.getNativeOpCommandArgs('restore', {
conid,
database,
inputFile,
outputFile: undefined,
options: undefined,
argsFormat: 'shell',
});
return {
...commandArgs,
transformMessage: null,
commandLine: this.commandArgsToCommandLine(commandArgs),
};
},
executeSessionQuery_meta: true,
async executeSessionQuery({ sesid, conid, database, sql }, req) {
await testConnectionPermission(conid, req);
logger.info({ sesid, sql }, 'DBGM-00010 Processing query');
sessions.dispatchMessage(sesid, 'Query execution started');
const opened = await this.ensureOpened(conid, database);
opened.subprocess.send({ msgtype: 'executeSessionQuery', sql, sesid });
return { state: 'ok' };
},
evalJsonScript_meta: true,
async evalJsonScript({ conid, database, script, runid }, req) {
await testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid, database);
opened.subprocess.send({ msgtype: 'evalJsonScript', script, runid });
return { state: 'ok' };
},
};