diff --git a/api/src/controllers/databaseConnections.js b/api/src/controllers/databaseConnections.js new file mode 100644 index 00000000..6f310efd --- /dev/null +++ b/api/src/controllers/databaseConnections.js @@ -0,0 +1,47 @@ +const fp = require('lodash/fp'); +const connections = require('./connections'); +const socket = require('../utility/socket'); +const { fork } = require('child_process'); +const DatabaseAnalyser = require('../engines/default/DatabaseAnalyser') + +module.exports = { + /** @type {import('../types').OpenedDatabaseConnection[]} */ + opened: [], + + handle_structure(id, database, { structure }) { + const existing = this.opened.find(x => x.id == id && x.database == database); + if (!existing) return; + existing.structure = structure; + socket.emit(`database-structure-changed-${id}-${database}`); + }, + handle_error(id, { error }) { + console.log(error); + }, + + async ensureOpened(id, database) { + const existing = this.opened.find(x => x.id == id && x.database == database); + if (existing) return existing; + const connection = await connections.get({ id }); + const subprocess = fork(`${__dirname}/../proc/databaseConnectionProcess.js`); + const newOpened = { + id, + database, + subprocess, + structure: DatabaseAnalyser.createEmptyStructure(), + connection, + }; + this.opened.push(newOpened); + // @ts-ignore + subprocess.on('message', ({ msgtype, ...message }) => { + this[`handle_${msgtype}`](id, database, message); + }); + subprocess.send({ msgtype: 'connect', ...connection }); + return newOpened; + }, + + listTables_meta: 'get', + async listTables({ id, database }) { + const opened = await this.ensureOpened(id, database); + return opened.structure.tables; // .map(fp.pick(['tableName', 'schemaName'])); + }, +}; diff --git a/api/src/engines/default/DatabaseAnalyser.js b/api/src/engines/default/DatabaseAnalyser.js index eab8d43a..cb4cad9d 100644 --- a/api/src/engines/default/DatabaseAnalyser.js +++ b/api/src/engines/default/DatabaseAnalyser.js @@ -1,14 +1,19 @@ - class DatabaseAnalyser { /** * - * @param {import('../default/types').EngineDriver} driver + * @param {import('../../types').EngineDriver} driver */ constructor(pool, driver) { this.pool = pool; this.driver = driver; + this.result = DatabaseAnalyser.createEmptyStructure(); } - runAnalysis() {} + async runAnalysis() {} } +/** @returns {import('../../types').DatabaseInfo} */ +DatabaseAnalyser.createEmptyStructure = () => ({ + tables: [], +}); + module.exports = DatabaseAnalyser; diff --git a/api/src/engines/default/types.ts b/api/src/engines/default/types.ts deleted file mode 100644 index 55adbe99..00000000 --- a/api/src/engines/default/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface EngineDriver { - connect({ server, port, user, password }); - query(pool, sql: string): []; - getVersion(pool): string; - listDatabases(pool): [{ name: string }]; - analyseFull(pool); - analyseIncremental(pool); -} diff --git a/api/src/engines/index.js b/api/src/engines/index.js index 8e6ffe17..a50b1990 100644 --- a/api/src/engines/index.js +++ b/api/src/engines/index.js @@ -1,4 +1,8 @@ -module.exports = connection => { + +/** @return {import('../types').EngineDriver} */ +function getDriver(connection) { const { engine } = connection; return require(`./${engine}`); -}; + +} +module.exports = getDriver; diff --git a/api/src/engines/mssql/MsSqlAnalyser.js b/api/src/engines/mssql/MsSqlAnalyser.js index 865cea27..bc09e01c 100644 --- a/api/src/engines/mssql/MsSqlAnalyser.js +++ b/api/src/engines/mssql/MsSqlAnalyser.js @@ -1,9 +1,9 @@ - const fs = require('fs-extra'); const path = require('path'); const DatabaseAnalayser = require('../default/DatabaseAnalyser'); +/** @returns {Promise} */ async function loadQuery(name) { return await fs.readFile(path.join(__dirname, name), 'utf-8'); } @@ -13,8 +13,27 @@ class MsSqlAnalyser extends DatabaseAnalayser { super(pool, driver); } + async createQuery( + resFileName, + tables = false, + views = false, + procedures = false, + functions = false, + triggers = false + ) { + let res = await loadQuery(resFileName); + res = res.replace('=[OBJECT_ID_CONDITION]', ' is not null'); + return res; + } async runAnalysis() { - const tables = this.driver.query(this.pool, await loadQuery('tables.sql')); + const tables = await this.driver.query(this.pool, await this.createQuery('tables.sql')); + // for (const table of tables) { + // table.name = { + // schema: table.schemaName, + // name: table.tableName, + // }; + // } + this.result.tables = tables; } } diff --git a/api/src/engines/mssql/index.js b/api/src/engines/mssql/index.js index 3d7260eb..38571f02 100644 --- a/api/src/engines/mssql/index.js +++ b/api/src/engines/mssql/index.js @@ -2,8 +2,8 @@ const mssql = require('mssql'); const MsSqlAnalyser = require('./MsSqlAnalyser'); module.exports = { - async connect({ server, port, user, password }) { - const pool = await mssql.connect({ server, port, user, password }); + async connect({ server, port, user, password, database }) { + const pool = await mssql.connect({ server, port, user, password, database }); return pool; }, async query(pool, sql) { @@ -19,9 +19,9 @@ module.exports = { return res; }, async analyseFull(pool) { - - }, - async analyseIncremental(pool) { - + const analyser = new MsSqlAnalyser(pool, this); + await analyser.runAnalysis(); + return analyser.result; }, + async analyseIncremental(pool) {}, }; diff --git a/api/src/engines/mssql/tables.sql b/api/src/engines/mssql/tables.sql index 85fe7842..b54cb573 100644 --- a/api/src/engines/mssql/tables.sql +++ b/api/src/engines/mssql/tables.sql @@ -1,6 +1,6 @@ select - o.name as tableName, s.name as schemaName, o.objectId, - o.createDate, o.modifyDate + o.name as tableName, s.name as schemaName, o.object_id, + o.create_date, o.modify_date from sys.tables o inner join sys.schemas s on o.schema_id = s.schema_id where o.object_id =[OBJECT_ID_CONDITION] diff --git a/api/src/engines/mysql/index.js b/api/src/engines/mysql/index.js index 029c38bc..d2dd6a4f 100644 --- a/api/src/engines/mysql/index.js +++ b/api/src/engines/mysql/index.js @@ -1,8 +1,8 @@ const mysql = require('mysql'); module.exports = { - async connect({ server, port, user, password }) { - const connection = mysql.createConnection({ host: server, port, user, password }); + async connect({ server, port, user, password, database }) { + const connection = mysql.createConnection({ host: server, port, user, password, database }); return connection; }, async query(connection, sql) { diff --git a/api/src/engines/postgres/index.js b/api/src/engines/postgres/index.js index 98ab8f2b..e179bce7 100644 --- a/api/src/engines/postgres/index.js +++ b/api/src/engines/postgres/index.js @@ -1,8 +1,8 @@ const { Client } = require('pg'); module.exports = { - async connect({ server, port, user, password }) { - const client = new Client({ host: server, port, user, password, database: 'postgres' }); + async connect({ server, port, user, password, database }) { + const client = new Client({ host: server, port, user, password, database: database || 'postgres' }); await client.connect(); return client; }, diff --git a/api/src/index.js b/api/src/index.js index fe5753ff..213dc523 100644 --- a/api/src/index.js +++ b/api/src/index.js @@ -7,6 +7,7 @@ const io = require('socket.io'); const useController = require('./utility/useController'); const connections = require('./controllers/connections'); const serverConnections = require('./controllers/serverConnections'); +const databaseConnections = require('./controllers/databaseConnections'); const socket = require('./utility/socket'); const app = express(); @@ -23,5 +24,6 @@ app.get('/', (req, res) => { useController(app, '/connections', connections); useController(app, '/server-connections', serverConnections); +useController(app, '/database-connections', databaseConnections); server.listen(3000); diff --git a/api/src/proc/databaseConnectionProcess.js b/api/src/proc/databaseConnectionProcess.js new file mode 100644 index 00000000..a2f93dee --- /dev/null +++ b/api/src/proc/databaseConnectionProcess.js @@ -0,0 +1,37 @@ +const engines = require('../engines'); + +let systemConnection; +let storedConnection; + +async function handleFullRefresh() { + const driver = engines(storedConnection); + const structure = await driver.analyseFull(systemConnection); + console.log('SENDING STRUCTURE', structure); + process.send({ msgtype: 'structure', structure }); +} + +async function handleConnect(connection, database) { + storedConnection = connection; + + const driver = engines(storedConnection); + systemConnection = await driver.connect({ ...storedConnection, database }); + handleFullRefresh(); + setInterval(handleFullRefresh, 30 * 1000); +} + +const messageHandlers = { + connect: handleConnect, +}; + +async function handleMessage({ msgtype, database, ...other }) { + const handler = messageHandlers[msgtype]; + await handler(other, database); +} + +process.on('message', async message => { + try { + await handleMessage(message); + } catch (e) { + process.send({ msgtype: 'error', error: e.message }); + } +}); diff --git a/api/src/types.ts b/api/src/types.ts new file mode 100644 index 00000000..01994593 --- /dev/null +++ b/api/src/types.ts @@ -0,0 +1,29 @@ +export interface EngineDriver { + connect({ server, port, user, password }); + query(pool, sql: string): Promise; + getVersion(pool): Promise; + listDatabases(pool): Promise<{ name: string }[]>; + analyseFull(pool): Promise; + analyseIncremental(pool): Promise; +} + +// export interface NameWithSchema { +// schema: string; +// name: string; +// } + +export interface TableInfo { + // name: NameWithSchema; + tableName: string; + schemaName: string; +} + +export interface DatabaseInfo { + tables: TableInfo[]; +} + +export interface OpenedDatabaseConnection { + id: string; + database: string; + structure: DatabaseInfo; +} \ No newline at end of file diff --git a/web/src/Screen.js b/web/src/Screen.js index 79fb6bbd..b6e66c91 100644 --- a/web/src/Screen.js +++ b/web/src/Screen.js @@ -1,3 +1,5 @@ +// @ts-nocheck + import React from 'react'; import theme from './theme'; import styled from 'styled-components'; @@ -9,10 +11,7 @@ import WidgetContainer from './widgets/WidgetContainer'; const BodyDiv = styled.div` position: fixed; top: ${theme.tabsPanel.height}px; - left: ${props => - theme.widgetMenu.iconSize + - // @ts-ignore - props.leftPanelWidth}px; + left: ${props => theme.widgetMenu.iconSize + props.leftPanelWidth}px; bottom: ${theme.statusBar.height}px; right: 0; background-color: ${theme.mainArea.background}; @@ -41,10 +40,7 @@ const TabsPanel = styled.div` display: flex; position: fixed; top: 0; - left: ${props => - theme.widgetMenu.iconSize + - // @ts-ignore - props.leftPanelWidth}px; + left: ${props => theme.widgetMenu.iconSize + props.leftPanelWidth}px; height: ${theme.tabsPanel.height}px; right: 0; background-color: ${theme.tabsPanel.background}; @@ -72,18 +68,10 @@ export default function Screen({ children = undefined }) { )} - + - - {children} - + {children} ); diff --git a/web/src/widgets/DatabaseWidget.js b/web/src/widgets/DatabaseWidget.js index 0e30335f..3cd94f8b 100644 --- a/web/src/widgets/DatabaseWidget.js +++ b/web/src/widgets/DatabaseWidget.js @@ -37,21 +37,56 @@ function SubDatabaseList({ data }) { return ; } -export default function DatabaseWidget() { - const db = useCurrentDatabase(); +function ConnectionList() { const modalState = useModalState(); const connections = useFetch({ url: 'connections/list', reloadTrigger: 'connection-list-changed', }); + return ( + <> + + + + + ); +} + +function SqlObjectList({ id, database }) { + const tables = + useFetch({ + url: `database-connections/list-tables?id=${id}&database=${database}`, + reloadTrigger: `database-structure-changed-${id}-${database}`, + }) || []; + return ( + <> + {tables.map(({ tableName, schemaName }) => ( +
{tableName}
+ ))} + + ); +} + +function SqlObjectListWrapper() { + const db = useCurrentDatabase(); + + if (!db) return
(Choose database)
; + const { name, connection } = db; + + return ; + // return
tables of {db && db.name}
+ // return
tables of {JSON.stringify(db)}
+} + +export default function DatabaseWidget() { return ( - - - + + + + - tables of {db && db.name} ); }