diff --git a/packages/api/src/controllers/jsldata.js b/packages/api/src/controllers/jsldata.js index 280e57f5..609a673c 100644 --- a/packages/api/src/controllers/jsldata.js +++ b/packages/api/src/controllers/jsldata.js @@ -111,18 +111,22 @@ module.exports = { getInfo_meta: true, async getInfo({ jslid }) { const file = getJslFileName(jslid); - const firstLine = await readFirstLine(file); - if (firstLine) { - const parsed = JSON.parse(firstLine); - if (parsed.__isStreamHeader) { - return parsed; + try { + const firstLine = await readFirstLine(file); + if (firstLine) { + const parsed = JSON.parse(firstLine); + if (parsed.__isStreamHeader) { + return parsed; + } + return { + __isStreamHeader: true, + __isDynamicStructure: true, + }; } - return { - __isStreamHeader: true, - __isDynamicStructure: true, - }; + return null; + } catch (err) { + return null; } - return null; }, getRows_meta: true, diff --git a/packages/api/src/controllers/sessions.js b/packages/api/src/controllers/sessions.js index b651c37c..f8e995fd 100644 --- a/packages/api/src/controllers/sessions.js +++ b/packages/api/src/controllers/sessions.js @@ -4,8 +4,10 @@ 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'); module.exports = { /** @type {import('dbgate-types').OpenedSession[]} */ @@ -46,9 +48,15 @@ module.exports = { this.dispatchMessage(sesid, info); }, - handle_done(sesid) { + handle_done(sesid, props) { socket.emit(`session-done-${sesid}`); - this.dispatchMessage(sesid, 'Query execution finished'); + if (!props.skipFinishedMessage) { + this.dispatchMessage(sesid, 'Query execution finished'); + } + const session = this.opened.find(x => x.sesid == sesid); + if (session.killOnDone) { + this.kill({ sesid }); + } }, handle_recordset(sesid, props) { @@ -60,6 +68,11 @@ module.exports = { jsldata.notifyChangedStats(stats); }, + handle_initializeFile(sesid, props) { + const { jslid } = props; + socket.emit(`session-initialize-file-${jslid}`); + }, + handle_ping() {}, create_meta: true, @@ -105,6 +118,19 @@ module.exports = { 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 = uuidv1(); + const fileName = queryName && appFolder ? path.join(appdir(), appFolder, `${queryName}.query.sql`) : null; + + session.subprocess.send({ msgtype: 'executeReader', sql, fileName, jslid }); + + return { jslid }; + }, + // cancel_meta: true, // async cancel({ sesid }) { // const session = this.opened.find((x) => x.sesid == sesid); diff --git a/packages/api/src/proc/sessionProcess.js b/packages/api/src/proc/sessionProcess.js index 6e8355de..c63b8cdb 100644 --- a/packages/api/src/proc/sessionProcess.js +++ b/packages/api/src/proc/sessionProcess.js @@ -17,11 +17,15 @@ let afterConnectCallbacks = []; // let currentHandlers = []; class TableWriter { - constructor(structure, resultIndex) { - this.jslid = uuidv1(); - this.currentFile = path.join(jsldir(), `${this.jslid}.jsonl`); + constructor() { this.currentRowCount = 0; this.currentChangeIndex = 1; + this.initializedFile = false; + } + + initializeFromQuery(structure, resultIndex) { + this.jslid = uuidv1(); + this.currentFile = path.join(jsldir(), `${this.jslid}.jsonl`); fs.writeFileSync( this.currentFile, JSON.stringify({ @@ -32,13 +36,21 @@ class TableWriter { this.currentStream = fs.createWriteStream(this.currentFile, { flags: 'a' }); this.writeCurrentStats(false, false); this.resultIndex = resultIndex; + this.initializedFile = true; process.send({ msgtype: 'recordset', jslid: this.jslid, resultIndex }); } + initializeFromReader(jslid) { + this.jslid = jslid; + this.currentFile = path.join(jsldir(), `${this.jslid}.jsonl`); + this.writeCurrentStats(false, false); + } + row(row) { // console.log('ACCEPT ROW', row); this.currentStream.write(JSON.stringify(row) + '\n'); this.currentRowCount += 1; + if (!this.plannedStats) { this.plannedStats = true; process.nextTick(() => { @@ -49,6 +61,21 @@ class TableWriter { } } + rowFromReader(row) { + if (!this.initializedFile) { + process.send({ msgtype: 'initializeFile', jslid: this.jslid }); + this.initializedFile = true; + + fs.writeFileSync(this.currentFile, JSON.stringify(row) + '\n'); + this.currentStream = fs.createWriteStream(this.currentFile, { flags: 'a' }); + this.writeCurrentStats(false, false); + this.initializedFile = true; + return; + } + + this.row(row); + } + writeCurrentStats(isFinished = false, emitEvent = false) { const stats = { rowCount: this.currentRowCount, @@ -63,10 +90,11 @@ class TableWriter { } } - close() { + close(afterClose) { if (this.currentStream) { this.currentStream.end(() => { this.writeCurrentStats(true, true); + if (afterClose) afterClose(); }); } } @@ -98,7 +126,11 @@ class StreamHandler { recordset(columns) { this.closeCurrentWriter(); - this.currentWriter = new TableWriter(Array.isArray(columns) ? { columns } : columns, this.resultIndexHolder.value); + this.currentWriter = new TableWriter(); + this.currentWriter.initializeFromQuery( + Array.isArray(columns) ? { columns } : columns, + this.resultIndexHolder.value + ); this.resultIndexHolder.value += 1; // this.writeCurrentStats(); @@ -110,7 +142,6 @@ class StreamHandler { // }, 500); } row(row) { - // console.log('ACCEPT ROW', row); if (this.currentWriter) this.currentWriter.row(row); else if (row.message) process.send({ msgtype: 'info', info: { message: row.message } }); // this.onRow(this.jslid); @@ -135,20 +166,21 @@ function handleStream(driver, resultIndexHolder, sql) { }); } -function ensureExecuteCustomScript(driver) { +function allowExecuteCustomScript(driver) { if (driver.readOnlySessions) { - return; + return true; } if (storedConnection.isReadOnly) { - throw new Error('Connection is read only'); + return false; + // throw new Error('Connection is read only'); } + return true; } async function handleConnect(connection) { storedConnection = connection; const driver = requireEngineDriver(storedConnection); - ensureExecuteCustomScript(driver); systemConnection = await connectUtility(driver, storedConnection); for (const [resolve] of afterConnectCallbacks) { resolve(); @@ -173,6 +205,19 @@ async function handleExecuteQuery({ sql }) { await waitConnected(); const driver = requireEngineDriver(storedConnection); + if (!allowExecuteCustomScript(driver)) { + process.send({ + msgtype: 'info', + info: { + message: 'Connection without read-only sessions is read only', + severity: 'error', + }, + }); + process.send({ msgtype: 'done', skipFinishedMessage: true }); + return; + //process.send({ msgtype: 'error', error: e.message }); + } + const resultIndexHolder = { value: 0, }; @@ -186,9 +231,39 @@ async function handleExecuteQuery({ sql }) { process.send({ msgtype: 'done' }); } +async function handleExecuteReader({ jslid, sql, fileName }) { + await waitConnected(); + + const driver = requireEngineDriver(storedConnection); + + if (fileName) { + sql = fs.readFileSync(fileName, 'utf-8'); + } else { + if (!allowExecuteCustomScript(driver)) { + process.send({ msgtype: 'done' }); + return; + } + } + + const writer = new TableWriter(); + writer.initializeFromReader(jslid); + + const reader = await driver.readQuery(systemConnection, sql); + + reader.on('data', data => { + writer.rowFromReader(data); + }); + reader.on('end', () => { + writer.close(() => { + process.send({ msgtype: 'done' }); + }); + }); +} + const messageHandlers = { connect: handleConnect, executeQuery: handleExecuteQuery, + executeReader: handleExecuteReader, // cancel: handleCancel, }; diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index 48dd47e3..6b0174db 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -19,6 +19,7 @@ export interface OpenedSession { sesid: string; conid: string; database: string; + killOnDone?: boolean; subprocess: ChildProcess; } diff --git a/packages/web/src/appobj/DatabaseObjectAppObject.svelte b/packages/web/src/appobj/DatabaseObjectAppObject.svelte index c7bb2c01..3237c18c 100644 --- a/packages/web/src/appobj/DatabaseObjectAppObject.svelte +++ b/packages/web/src/appobj/DatabaseObjectAppObject.svelte @@ -10,6 +10,7 @@ matviews: 'img view', procedures: 'img procedure', functions: 'img function', + queries: 'img query-data', }; const defaultTabs = { @@ -17,6 +18,7 @@ collections: 'CollectionDataTab', views: 'ViewDataTab', matviews: 'ViewDataTab', + queries: 'QueryDataTab', }; const menus = { @@ -231,6 +233,13 @@ }, }, ], + queries: [ + { + label: 'Open data', + tab: 'QueryDataTab', + forceNewTab: true, + }, + ], procedures: [ { label: 'Drop procedure', diff --git a/packages/web/src/datagrid/JslDataGrid.svelte b/packages/web/src/datagrid/JslDataGrid.svelte index 651c720a..9cb7db2a 100644 --- a/packages/web/src/datagrid/JslDataGrid.svelte +++ b/packages/web/src/datagrid/JslDataGrid.svelte @@ -1,22 +1,42 @@ + + + + + {#if jslid} + + {:else} + + {/if} + + + + + diff --git a/packages/web/src/tabs/QueryTab.svelte b/packages/web/src/tabs/QueryTab.svelte index dc11c89e..1fe3af03 100644 --- a/packages/web/src/tabs/QueryTab.svelte +++ b/packages/web/src/tabs/QueryTab.svelte @@ -140,7 +140,7 @@ } export function hasConnection() { - return !!conid && (!$connection.isReadOnly || driver.readOnlySessions); + return !!conid && (!$connection?.isReadOnly || driver?.readOnlySessions); } async function executeCore(sql) { diff --git a/packages/web/src/tabs/index.js b/packages/web/src/tabs/index.js index 18d6fafc..eabdc6a5 100644 --- a/packages/web/src/tabs/index.js +++ b/packages/web/src/tabs/index.js @@ -22,6 +22,7 @@ import * as JsonTab from './JsonTab.svelte'; import * as ChangelogTab from './ChangelogTab.svelte'; import * as DiagramTab from './DiagramTab.svelte'; import * as DbKeyDetailTab from './DbKeyDetailTab.svelte'; +import * as QueryDataTab from './QueryDataTab.svelte'; export default { TableDataTab, @@ -48,4 +49,5 @@ export default { ChangelogTab, DiagramTab, DbKeyDetailTab, + QueryDataTab, }; diff --git a/packages/web/src/widgets/AppFilesList.svelte b/packages/web/src/widgets/AppFilesList.svelte index 3c50951a..2747ba49 100644 --- a/packages/web/src/widgets/AppFilesList.svelte +++ b/packages/web/src/widgets/AppFilesList.svelte @@ -79,12 +79,16 @@ text: 'New SQL command', onClick: () => handleNewSqlFile('command.sql', 'Create new SQL command', COMMAND_TEMPLATE), }, + { + text: 'New SQL query', + onClick: () => handleNewSqlFile('query.sql', 'Create new SQL query', QUERY_TEMPLATE), + }, { text: 'New virtual references file', onClick: () => handleNewConfigFile('virtual-references.config.json', []), }, { - text: 'New disctionary descriptions file', + text: 'New dictionary descriptions file', onClick: () => handleNewConfigFile('dictionary-descriptions.config.json', []), }, diff --git a/packages/web/src/widgets/SqlObjectList.svelte b/packages/web/src/widgets/SqlObjectList.svelte index 74769630..a4d9c60a 100644 --- a/packages/web/src/widgets/SqlObjectList.svelte +++ b/packages/web/src/widgets/SqlObjectList.svelte @@ -16,7 +16,7 @@ import InlineButton from '../buttons/InlineButton.svelte'; import SearchInput from '../elements/SearchInput.svelte'; import WidgetsInnerContainer from './WidgetsInnerContainer.svelte'; - import { useConnectionInfo, useDatabaseInfo, useDatabaseStatus } from '../utility/metadataLoaders'; + import { useConnectionInfo, useDatabaseInfo, useDatabaseStatus, useUsedApps } from '../utility/metadataLoaders'; import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte'; import AppObjectList from '../appobj/AppObjectList.svelte'; import _ from 'lodash'; @@ -30,10 +30,11 @@ import FontIcon from '../icons/FontIcon.svelte'; import CloseSearchButton from '../buttons/CloseSearchButton.svelte'; import { findEngineDriver } from 'dbgate-tools'; - import { extensions } from '../stores'; + import { currentDatabase, extensions } from '../stores'; import newQuery from '../query/newQuery'; import runCommand from '../commands/runCommand'; import { apiCall } from '../utility/api'; + import { filterAppsForDatabase } from '../utility/appTools'; export let conid; export let database; @@ -46,16 +47,28 @@ $: connection = useConnectionInfo({ conid }); $: driver = findEngineDriver($connection, $extensions); + $: apps = useUsedApps(); + + $: dbApps = filterAppsForDatabase($currentDatabase.connection, $currentDatabase.name, $apps || []); + // $: console.log('OBJECTS', $objects); - $: objectList = _.flatten( - ['tables', 'collections', 'views', 'matviews', 'procedures', 'functions'].map(objectTypeField => + $: objectList = _.flatten([ + ...['tables', 'collections', 'views', 'matviews', 'procedures', 'functions'].map(objectTypeField => _.sortBy( (($objects || {})[objectTypeField] || []).map(obj => ({ ...obj, objectTypeField })), ['schemaName', 'pureName'] ) - ) - ); + ), + ...dbApps.map(app => + app.queries.map(query => ({ + objectTypeField: 'queries', + pureName: query.name, + schemaName: app.name, + sql: query.sql + })) + ), + ]); // let generateIndex = 0; // setInterval(() => (generateIndex += 1), 2000); @@ -69,7 +82,7 @@ const res = []; if (driver?.databaseEngineTypes?.includes('document')) { res.push({ command: 'new.collection' }); - } + } if (driver?.databaseEngineTypes?.includes('sql')) { res.push({ command: 'new.table' }); }