dbgate-api
Version:
Allows run DbGate data-manipulation scripts.
296 lines (261 loc) • 8.82 kB
JavaScript
const crypto = require('crypto');
const _ = require('lodash');
const connections = require('./connections');
const socket = require('../utility/socket');
const { fork } = require('child_process');
const jsldata = require('./jsldata');
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 pipeForkLogs = require('../utility/pipeForkLogs');
const config = require('./config');
const { sendToAuditLog } = require('../utility/auditlog');
const { testStandardPermission, testDatabaseRolePermission } = require('../utility/hasPermission');
const logger = getLogger('sessions');
module.exports = {
/** @type {import('dbgate-types').OpenedSession[]} */
opened: [],
// handle_error(sesid, props) {
// const { error } = props;
// console.log(`Error in database session ${sesid}`, error);
// this.dispatchMessage(sesid, {
// severity: 'error',
// message: error && error.toString(),
// });
// },
// handle_row(sesid, props) {
// const { row } = props;
// socket.emit('sessionRow', row);
// },
dispatchMessage(sesid, message) {
if (_.isString(message)) {
socket.emit(`session-info-${sesid}`, {
message,
time: new Date(),
severity: 'info',
});
}
if (_.isPlainObject(message)) {
socket.emit(`session-info-${sesid}`, {
time: new Date(),
severity: 'info',
...message,
});
}
},
handle_info(sesid, props) {
const { info } = props;
this.dispatchMessage(sesid, info);
},
handle_done(sesid, props) {
socket.emit(`session-done-${sesid}`);
if (!props.skipFinishedMessage) {
if (props.controlCommand) {
this.dispatchMessage(sesid, `${_.startCase(props.controlCommand)} finished`);
} else {
this.dispatchMessage(sesid, 'Query execution finished');
}
}
const session = this.opened.find(x => x.sesid == sesid);
if (session.loadingReader_jslid) {
socket.emit(`session-jslid-done-${session.loadingReader_jslid}`);
}
if (props.autoCommit) {
this.executeControlCommand({ sesid, command: 'commitTransaction' });
}
if (session.killOnDone) {
this.kill({ sesid });
}
},
handle_recordset(sesid, props) {
const { jslid, resultIndex } = props;
socket.emit(`session-recordset-${sesid}`, { jslid, resultIndex });
},
handle_stats(sesid, stats) {
jsldata.notifyChangedStats(stats);
},
handle_charts(sesid, props) {
const { jslid, charts, resultIndex } = props;
socket.emit(`session-charts-${sesid}`, { jslid, resultIndex, charts });
},
handle_initializeFile(sesid, props) {
const { jslid } = props;
socket.emit(`session-initialize-file-${jslid}`);
},
handle_ping() { },
create_meta: true,
async create({ conid, database }) {
const sesid = crypto.randomUUID();
const connection = await connections.getCore({ conid });
const subprocess = fork(
global['API_PACKAGE'] || process.argv[1],
[
'--is-forked-api',
'--start-process',
'sessionProcess',
...processArgs.getPassArgs(),
// ...process.argv.slice(3),
],
{
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
}
);
pipeForkLogs(subprocess);
const newOpened = {
conid,
database,
subprocess,
connection,
sesid,
};
this.opened.push(newOpened);
subprocess.on('message', message => {
// @ts-ignore
const { msgtype } = message;
if (handleProcessCommunication(message, subprocess)) return;
this[`handle_${msgtype}`](sesid, message);
});
subprocess.on('exit', () => {
this.opened = this.opened.filter(x => x.sesid != sesid);
this.dispatchMessage(sesid, 'Query session closed');
socket.emit(`session-closed-${sesid}`);
});
subprocess.on('error', () => {
this.opened = this.opened.filter(x => x.sesid != sesid);
});
subprocess.send({
msgtype: 'connect',
...connection,
database,
globalSettings: await config.getSettings(),
});
return _.pick(newOpened, ['conid', 'database', 'sesid']);
},
executeQuery_meta: true,
async executeQuery({ sesid, sql, autoCommit, autoDetectCharts, limitRows, frontMatter }, req) {
await testStandardPermission('dbops/query', req);
const session = this.opened.find(x => x.sesid == sesid);
if (!session) {
throw new Error('Invalid session');
}
await testDatabaseRolePermission(session.conid, session.database, 'run_script', req);
sendToAuditLog(req, {
category: 'dbop',
component: 'SessionController',
action: 'executeQuery',
event: 'query.execute',
severity: 'info',
detail: sql,
conid: session.conid,
database: session.database,
message: 'Executing query',
});
logger.info({ sesid, sql }, 'DBGM-00019 Processing query');
this.dispatchMessage(sesid, 'Query execution started');
session.subprocess.send({
msgtype: 'executeQuery',
sql,
autoCommit,
autoDetectCharts: autoDetectCharts || !!frontMatter?.['selected-chart'],
limitRows,
frontMatter,
});
return { state: 'ok' };
},
executeControlCommand_meta: true,
async executeControlCommand({ sesid, command }) {
const session = this.opened.find(x => x.sesid == sesid);
if (!session) {
throw new Error('Invalid session');
}
logger.info({ sesid, command }, 'DBGM-00020 Processing control command');
this.dispatchMessage(sesid, `${_.startCase(command)} started`);
session.subprocess.send({ msgtype: 'executeControlCommand', command });
return { state: 'ok' };
},
executeReader_meta: true,
async executeReader({ conid, database, sql, queryName, appFolder }) {
const { sesid } = await this.create({ conid, database });
const session = this.opened.find(x => x.sesid == sesid);
session.killOnDone = true;
const jslid = crypto.randomUUID();
session.loadingReader_jslid = jslid;
const fileName = queryName && appFolder ? path.join(appdir(), appFolder, `${queryName}.query.sql`) : null;
session.subprocess.send({ msgtype: 'executeReader', sql, fileName, jslid });
return { jslid };
},
stopLoadingReader_meta: true,
async stopLoadingReader({ jslid }) {
const session = this.opened.find(x => x.loadingReader_jslid == jslid);
if (session) {
this.kill({ sesid: session.sesid });
}
return true;
},
startProfiler_meta: true,
async startProfiler({ sesid }) {
const jslid = crypto.randomUUID();
const session = this.opened.find(x => x.sesid == sesid);
if (!session) {
throw new Error('Invalid session');
}
logger.info({ sesid }, 'DBGM-00021 Starting profiler');
session.loadingReader_jslid = jslid;
session.subprocess.send({ msgtype: 'startProfiler', jslid });
return { state: 'ok', jslid };
},
stopProfiler_meta: true,
async stopProfiler({ sesid }) {
const session = this.opened.find(x => x.sesid == sesid);
if (!session) {
throw new Error('Invalid session');
}
session.subprocess.send({ msgtype: 'stopProfiler' });
return { state: 'ok' };
},
// cancel_meta: true,
// async cancel({ sesid }) {
// const session = this.opened.find((x) => x.sesid == sesid);
// if (!session) {
// throw new Error('Invalid session');
// }
// session.subprocess.send({ msgtype: 'cancel' });
// return { state: 'ok' };
// },
kill_meta: true,
async kill({ sesid }) {
const session = this.opened.find(x => x.sesid == sesid);
if (!session) {
throw new Error('Invalid session');
}
session.subprocess.kill();
this.dispatchMessage(sesid, 'Connection closed');
return { state: 'ok' };
},
ping_meta: true,
async ping({ sesid }) {
const session = this.opened.find(x => x.sesid == sesid);
if (!session) {
throw new Error('Invalid session');
}
try {
session.subprocess.send({ msgtype: 'ping' });
} catch (err) {
logger.error(extractErrorLogData(err), 'DBGM-00145 Error pinging session');
return {
status: 'error',
message: 'Ping failed',
};
}
return { state: 'ok' };
},
// 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;
// },
};