diff --git a/.eslintrc b/.eslintrc index ac7f368f..3ca19365 100644 --- a/.eslintrc +++ b/.eslintrc @@ -21,7 +21,7 @@ ], "linebreak-style": [ "error", - "windows" + "unix" ], "brace-style": [ "error", diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..dd84ea78 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..bbcbbe7d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..11e7a9fd --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,62 @@ +name: "CodeQL" + +on: + push: + branches: [master] + pull_request: + # The branches below must be a subset of the branches above + branches: [master] + schedule: + - cron: '0 15 * * 0' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + # Override automatic language detection by changing the below list + # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] + language: ['javascript'] + # Learn more... + # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d781886..b232ae87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,24 +2,55 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. -### [0.0.6](https://github.com/EStarium/antares/compare/v0.0.5...v0.0.6) (2020-09-03) +### [0.0.7](https://github.com/EStarium/antares/compare/v0.0.6...v0.0.7) (2020-10-03) ### Features -* aliases support ([264de9c](https://github.com/EStarium/antares/commit/264de9c5686fb3a2ef22d96171f45b915ba1b34b)) -* middle click to close tabs ([256ec76](https://github.com/EStarium/antares/commit/256ec765883fcf247355190827e943c76e95f13b)) -* monaco-editor as query editor ([196a3e0](https://github.com/EStarium/antares/commit/196a3e0185a3d68b7c4ade8dbf187d2b216cc00b)) -* sql suggestions in query editor ([8dc74ef](https://github.com/EStarium/antares/commit/8dc74ef2c335e8ae4a69f5d2651df65939139b1b)) -* support to multiple query tabs ([d7ed00f](https://github.com/EStarium/antares/commit/d7ed00f4a3613da9015c9fc48c4d8062d292e416)) -* tabs horizontal scroll with mouse wheel ([3a6ea76](https://github.com/EStarium/antares/commit/3a6ea76b93682ebd50908df7368c62c2c1e27958)) +* database creation ([3d0a83f](https://github.com/EStarium/antares/commit/3d0a83f2cf68c4dd412fd7679c39d63f081b7c19)) +* databases deletion ([4288a1f](https://github.com/EStarium/antares/commit/4288a1fd331f4a28de2e756f898d208a6a6599c4)) +* edit database collation ([54717e1](https://github.com/EStarium/antares/commit/54717e1f6a36ec0b3dd096d0e1e747512f6dda09)) +* field comment on mouse over a table field name ([2554444](https://github.com/EStarium/antares/commit/2554444322b59a6b1ab3ff05ccf8604bf6f8c8b8)) +* support to multiple queries in the same tab ([48f77ba](https://github.com/EStarium/antares/commit/48f77bae01efbff40bd0f5ce8c66e2619f44bf3a)) +* update italian translation ([89c3dc9](https://github.com/EStarium/antares/commit/89c3dc9fede63c77eb22b48df1a375ea44830306)) +* Update italian translation ([fe3d741](https://github.com/EStarium/antares/commit/fe3d7416013c44a4974471ab59b7c9a98afb7255)) + + +### Bug Fixes + +* cell update soft reload doesn't apply changes ([1b04b21](https://github.com/EStarium/antares/commit/1b04b216b21b697e47062a9366bc1b6a040a1a72)) +* empty databases not shown in explore bar ([3e737cb](https://github.com/EStarium/antares/commit/3e737cba62f795f225e944939c6bff04b27fa3d4)) +* glitch on table data tab ([10b426b](https://github.com/EStarium/antares/commit/10b426b90b6b9461cfffce3026c982463f6e0599)) +* lack of loading progressbar when an update is available ([86aec4f](https://github.com/EStarium/antares/commit/86aec4f5e41c059e88066a01f0d85155de99a5ee)) +* missing schema when queryng INFORMATION_SCHEMA ([530d1bd](https://github.com/EStarium/antares/commit/530d1bd43fa95de05f594b9b5cae2f4b397f96e0)) +* prevent multiple app instances ([12fbe8c](https://github.com/EStarium/antares/commit/12fbe8c1a03259648554f2a5c69b5abbedc18a48)) +* several fix on data and query tabs ([530907d](https://github.com/EStarium/antares/commit/530907d097ac4d995e1bfcb02e6c890fd6007e21)) +* unable to obtain fields informations for some queries ([43c7072](https://github.com/EStarium/antares/commit/43c7072c1c83a2455ae48a37be69b444b3eb6560)) +* unable to obtain keyUsage informations when adding new row ([023c6a6](https://github.com/EStarium/antares/commit/023c6a633a7f268b1a97b748ad08d2416cc30ffe)) +* value overridden when join tables with fields with same name ([78965d2](https://github.com/EStarium/antares/commit/78965d23e3efb7d8d6d110d79142966e57200757)) +* wrong field names when join tables ([ad0bad8](https://github.com/EStarium/antares/commit/ad0bad8486c3d67ec14ec1aed3d8aff6cce9df87)) +* wrong italian translation ([b29e07c](https://github.com/EStarium/antares/commit/b29e07c3b722aec7e78f3cef2e357a53cbcac474)) +* wrong schema fetching table fields and key usage ([8e71f42](https://github.com/EStarium/antares/commit/8e71f42a28060fdfeeb81502b0759d0d11f5bcfd)) +* wrong table and schema when more than one query in a tab ([4684b41](https://github.com/EStarium/antares/commit/4684b4114b9c9c253120292d7d164d7676011f86)) + +### [0.0.6](https://github.com/EStarium/antares/compare/v0.0.5...v0.0.6) (2020-09-03) + + +### Features +* Aliases support ([264de9c](https://github.com/EStarium/antares/commit/264de9c5686fb3a2ef22d96171f45b915ba1b34b)) +* Middle click to close tabs ([256ec76](https://github.com/EStarium/antares/commit/256ec765883fcf247355190827e943c76e95f13b)) +* Monaco-editor as query editor ([196a3e0](https://github.com/EStarium/antares/commit/196a3e0185a3d68b7c4ade8dbf187d2b216cc00b)) +* Sql suggestions in query editor ([8dc74ef](https://github.com/EStarium/antares/commit/8dc74ef2c335e8ae4a69f5d2651df65939139b1b)) +* Support to multiple query tabs ([d7ed00f](https://github.com/EStarium/antares/commit/d7ed00f4a3613da9015c9fc48c4d8062d292e416)) +* Tabs horizontal scroll with mouse wheel ([3a6ea76](https://github.com/EStarium/antares/commit/3a6ea76b93682ebd50908df7368c62c2c1e27958)) +* **Arabic translation** thanks to [Mohd-PH](https://github.com/Mohd-PH) ([#29](https://github.com/EStarium/antares/pull/29)) ### Bug Fixes -* error when launching queries without a result from query tabs ([a1a6f51](https://github.com/EStarium/antares/commit/a1a6f51f2fba5140f5e3bd9cd6557c8a13dfaa2c)) -* field name displayed instead of alias ([801a0de](https://github.com/EStarium/antares/commit/801a0de1865dea2a59ff057b7c2cc988cc9c87ed)) -* wrong table height calc in some cases ([fd6d517](https://github.com/EStarium/antares/commit/fd6d5177efb6161aab01f9e108eda60df6c7d8c4)) +* Error when launching queries without a result from query tabs ([a1a6f51](https://github.com/EStarium/antares/commit/a1a6f51f2fba5140f5e3bd9cd6557c8a13dfaa2c)) +* Field name displayed instead of alias ([801a0de](https://github.com/EStarium/antares/commit/801a0de1865dea2a59ff057b7c2cc988cc9c87ed)) +* Wrong table height calc in some cases ([fd6d517](https://github.com/EStarium/antares/commit/fd6d5177efb6161aab01f9e108eda60df6c7d8c4)) ### [0.0.5](https://github.com/EStarium/antares/compare/v0.0.4...v0.0.5) (2020-08-17) diff --git a/README.md b/README.md index aeb54252..7cf86a1c 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,15 @@ My target is to support as many databases as possible, and all major operating s **At the moment this application is an alpha, it lacks many features, and isn't ready as a main SQL client**. However i'm actively working on it, hoping to provide all essential features as soon as possible. -If you are curious to try this early state of Antares you can download and install the [latest release](https://github.com/EStarium/antares/releases), and stay tuned for updates. +🔗 If you are curious to try this early state of Antares you can download and install the [latest release](https://github.com/EStarium/antares/releases). +👁 To stay tuned for new releases watch this repo on **Release only** channel. +🌟 Don't forget to **leave a star** if you appreciate this project. ## Philosophy Why am I developing an SQL client when there are a lot of them on the market? The main goal is to develop a totally free, cross platform and open source alternative, empowered by JavaScript's ecosystem. -An application created with minimalism and semplicity in mind, with features in the righ places, not hundreds of tiny buttons or submenu. +An application created with minimalism and semplicity in mind, with features in the right places, not hundreds of tiny buttons or submenu. ## How to contribute @@ -28,8 +30,9 @@ An application created with minimalism and semplicity in mind, with features in This is a roadmap with major features will come in near future. - Improvements of query editor area. -- Multiple query tabs. +- Database management (add/edit/delete). - Tables management (add/edit/delete). +- Users management (add/edit/delete). - Stored procedures, views, schedulers and trigger support. - Database tools. - Context menu shortcuts. @@ -38,6 +41,7 @@ This is a roadmap with major features will come in near future. - Query logs console. - Fake data filler. - Import/export and migration. +- SSH tunnel. - Themes. ## Currently supported @@ -68,4 +72,5 @@ This is a roadmap with major features will come in near future. ## Translations [Giuseppe Gigliotti](https://github.com/ReverbOD) / [Italian Translation](https://github.com/EStarium/antares/pull/20) -[Mohd-PH](https://github.com/Mohd-PH) / [Arabic Translation](https://github.com/EStarium/antares/pull/29) +[Mohd-PH](https://github.com/Mohd-PH) / [Arabic Translation](https://github.com/EStarium/antares/pull/29) +[hongkfui](https://github.com/hongkfui) / [Spanish Translation](https://github.com/EStarium/antares/pull/32) diff --git a/build/icon.icns b/build/icon.icns new file mode 100644 index 00000000..52353eb5 Binary files /dev/null and b/build/icon.icns differ diff --git a/build/icon.ico b/build/icon.ico new file mode 100644 index 00000000..a1225f26 Binary files /dev/null and b/build/icon.ico differ diff --git a/build/icon.png b/build/icon.png deleted file mode 100644 index f5cda2bf..00000000 Binary files a/build/icon.png and /dev/null differ diff --git a/docs/screen-alpha.png b/docs/screen-alpha.png index 452226d2..df7100f4 100644 Binary files a/docs/screen-alpha.png and b/docs/screen-alpha.png differ diff --git a/package.json b/package.json index 25e9e2ea..9ad026c7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "antares", "productName": "Antares", - "version": "0.0.6", + "version": "0.0.7", "description": "A cross-platform easy to use SQL client.", "license": "MIT", "repository": "https://github.com/EStarium/antares.git", @@ -11,7 +11,9 @@ "build": "cross-env NODE_ENV=production npm run compile && electron-builder", "release": "standard-version", "release:pre": "npm run release -- --prerelease alpha", - "lint": "eslint ." + "test": "npm run lint", + "lint": "eslint . --ext .js,.vue && stylelint \"./src/**/*.{css,scss,sass,vue}\"", + "lint:fix": "eslint . --ext .js,.vue --fix && stylelint \"./src/**/*.{css,scss,sass,vue}\" --fix" }, "author": "Fabio Di Stasio ", "build": { @@ -45,47 +47,48 @@ } }, "dependencies": { - "@mdi/font": "^5.5.55", + "@mdi/font": "^5.6.55", "electron-log": "^4.2.4", - "electron-updater": "^4.3.4", + "electron-updater": "^4.3.5", "lodash": "^4.17.20", - "moment": "^2.27.0", + "moment": "^2.29.0", "monaco-editor": "^0.20.0", - "mssql": "^6.2.1", + "mssql": "^6.2.2", "mysql": "^2.18.1", - "pg": "^8.3.2", + "pg": "^8.3.3", "source-map-support": "^0.5.16", "spectre.css": "^0.5.9", "vue-click-outside": "^1.1.0", "vue-i18n": "^8.21.0", "vue-the-mask": "^0.11.1", - "vuedraggable": "^2.24.0", + "vuedraggable": "^2.24.1", "vuex": "^3.5.1", - "vuex-persist": "^2.2.0" + "vuex-persist": "^3.1.0" }, "devDependencies": { "babel-eslint": "^10.1.0", "cross-env": "^7.0.2", "electron": "^10.1.0", - "electron-builder": "^22.8.0", + "electron-builder": "^22.8.1", "electron-devtools-installer": "^3.1.1", "electron-webpack": "^2.8.2", "electron-webpack-vue": "^2.4.0", - "eslint": "^7.7.0", + "eslint": "^7.8.1", "eslint-config-standard": "^14.1.1", "eslint-plugin-import": "^2.22.0", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^4.2.1", "eslint-plugin-standard": "^4.0.1", "eslint-plugin-vue": "^6.2.2", - "monaco-editor-webpack-plugin": "^1.9.0", + "monaco-editor-webpack-plugin": "^1.9.1", "node-sass": "^4.14.1", - "sass-loader": "^10.0.1", + "sass-loader": "^10.0.2", "standard-version": "^9.0.0", - "stylelint": "^13.6.1", + "stylelint": "^13.7.0", "stylelint-config-standard": "^20.0.0", "stylelint-scss": "^3.18.0", - "vue": "^2.6.11", - "webpack": "^4.44.1" + "vue": "^2.6.12", + "vue-template-compiler": "^2.6.12", + "webpack": "^4.44.2" } } diff --git a/src/main/index.js b/src/main/index.js index 876e4f8d..9ccfd463 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -6,6 +6,8 @@ import { format as formatUrl } from 'url'; import ipcHandlers from './ipc-handlers'; const isDevelopment = process.env.NODE_ENV !== 'production'; +const gotTheLock = app.requestSingleInstanceLock(); + process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true'; // global reference to mainWindow (necessary to prevent window from being garbage collected) @@ -67,23 +69,27 @@ async function createMainWindow () { return window; }; -// Initialize ipcHandlers -ipcHandlers(); +if (!gotTheLock) + app.quit(); +else { + // Initialize ipcHandlers + ipcHandlers(); -// quit application when all windows are closed -app.on('window-all-closed', () => { - // on macOS it is common for applications to stay open until the user explicitly quits - if (process.platform !== 'darwin') - app.quit(); -}); + // quit application when all windows are closed + app.on('window-all-closed', () => { + // on macOS it is common for applications to stay open until the user explicitly quits + if (process.platform !== 'darwin') + app.quit(); + }); -app.on('activate', () => { - // on macOS it is common to re-create a window even after all windows have been closed - if (mainWindow === null) - mainWindow = createMainWindow(); -}); + app.on('activate', () => { + // on macOS it is common to re-create a window even after all windows have been closed + if (mainWindow === null) + mainWindow = createMainWindow(); + }); -// create main BrowserWindow when electron is ready -app.on('ready', () => { - mainWindow = createMainWindow(); -}); + // create main BrowserWindow when electron is ready + app.on('ready', () => { + mainWindow = createMainWindow(); + }); +} diff --git a/src/main/ipc-handlers/connection.js b/src/main/ipc-handlers/connection.js index f3768342..f029f7d0 100644 --- a/src/main/ipc-handlers/connection.js +++ b/src/main/ipc-handlers/connection.js @@ -1,12 +1,10 @@ import { ipcMain } from 'electron'; -import { AntaresConnector } from '../libs/AntaresConnector'; -import InformationSchema from '../models/InformationSchema'; -import Generic from '../models/Generic'; +import { ClientsFactory } from '../libs/ClientsFactory'; export default connections => { ipcMain.handle('test-connection', async (event, conn) => { - const Connection = new AntaresConnector({ + const connection = ClientsFactory.getConnection({ client: conn.client, params: { host: conn.host, @@ -16,10 +14,11 @@ export default connections => { } }); - await Connection.connect(); + await connection.connect(); try { - await InformationSchema.testConnection(Connection); + await connection.select('1+1').run(); + connection.destroy(); return { status: 'success' }; } @@ -33,7 +32,7 @@ export default connections => { }); ipcMain.handle('connect', async (event, conn) => { - const Connection = new AntaresConnector({ + const connection = ClientsFactory.getConnection({ client: conn.client, params: { host: conn.host, @@ -41,14 +40,16 @@ export default connections => { user: conn.user, password: conn.password }, - poolSize: 3 + poolSize: 1 }); try { - await Connection.connect(); + await connection.connect(); + + const structure = await connection.getStructure(); + + connections[conn.uid] = connection; - const { rows: structure } = await InformationSchema.getStructure(Connection); - connections[conn.uid] = Connection; return { status: 'success', response: structure }; } catch (err) { @@ -60,25 +61,4 @@ export default connections => { connections[uid].destroy(); delete connections[uid]; }); - - ipcMain.handle('refresh', async (event, uid) => { - try { - const { rows: structure } = await InformationSchema.getStructure(connections[uid]); - return { status: 'success', response: structure }; - } - catch (err) { - return { status: 'error', response: err.toString() }; - } - }); - - ipcMain.handle('raw-query', async (event, { uid, query, schema }) => { - if (!query) return; - try { - const result = await Generic.raw(connections[uid], query, schema); - return { status: 'success', response: result }; - } - catch (err) { - return { status: 'error', response: err.toString() }; - } - }); }; diff --git a/src/main/ipc-handlers/database.js b/src/main/ipc-handlers/database.js new file mode 100644 index 00000000..412fa18d --- /dev/null +++ b/src/main/ipc-handlers/database.js @@ -0,0 +1,110 @@ + +import { ipcMain } from 'electron'; + +export default connections => { + ipcMain.handle('create-database', async (event, params) => { + try { + const query = `CREATE DATABASE \`${params.name}\` COLLATE ${params.collation}`; + await connections[params.uid].raw(query); + + return { status: 'success' }; + } + catch (err) { + return { status: 'error', response: err.toString() }; + } + }); + + ipcMain.handle('update-database', async (event, params) => { + try { + const query = `ALTER DATABASE \`${params.name}\` COLLATE ${params.collation}`; + await connections[params.uid].raw(query); + + return { status: 'success' }; + } + catch (err) { + return { status: 'error', response: err.toString() }; + } + }); + + ipcMain.handle('delete-database', async (event, params) => { + try { + const query = `DROP DATABASE \`${params.database}\``; + await connections[params.uid].raw(query); + + return { status: 'success' }; + } + catch (err) { + return { status: 'error', response: err.toString() }; + } + }); + + ipcMain.handle('get-database-collation', async (event, params) => { + try { + const query = `SELECT \`DEFAULT_COLLATION_NAME\` FROM \`information_schema\`.\`SCHEMATA\` WHERE \`SCHEMA_NAME\`='${params.database}'`; + const collation = await connections[params.uid].raw(query); + + return { status: 'success', response: collation.rows.length ? collation.rows[0].DEFAULT_COLLATION_NAME : '' }; + } + catch (err) { + return { status: 'error', response: err.toString() }; + } + }); + + ipcMain.handle('get-structure', async (event, uid) => { + try { + const structure = await connections[uid].getStructure(); + + return { status: 'success', response: structure }; + } + catch (err) { + return { status: 'error', response: err.toString() }; + } + }); + + ipcMain.handle('get-collations', async (event, uid) => { + try { + const result = await connections[uid].getCollations(); + + return { status: 'success', response: result }; + } + catch (err) { + return { status: 'error', response: err.toString() }; + } + }); + + ipcMain.handle('get-variables', async (event, uid) => { + try { + const result = await connections[uid].getVariables(); + + return { status: 'success', response: result }; + } + catch (err) { + return { status: 'error', response: err.toString() }; + } + }); + + ipcMain.handle('use-schema', async (event, { uid, schema }) => { + if (!schema) return; + + try { + await connections[uid].use(schema); + return { status: 'success' }; + } + catch (err) { + return { status: 'error', response: err.toString() }; + } + }); + + ipcMain.handle('raw-query', async (event, { uid, query, schema }) => { + if (!query) return; + + try { + const result = await connections[uid].raw(query, true); + + return { status: 'success', response: result }; + } + catch (err) { + return { status: 'error', response: err.toString() }; + } + }); +}; diff --git a/src/main/ipc-handlers/index.js b/src/main/ipc-handlers/index.js index ebe5e0d7..d01e66ff 100644 --- a/src/main/ipc-handlers/index.js +++ b/src/main/ipc-handlers/index.js @@ -2,12 +2,14 @@ import connection from './connection'; import tables from './tables'; import updates from './updates'; import application from './application'; +import database from './database'; const connections = {}; export default () => { connection(connections); tables(connections); + database(connections); updates(); application(); }; diff --git a/src/main/ipc-handlers/tables.js b/src/main/ipc-handlers/tables.js index 7ac9d5f8..79cfa039 100644 --- a/src/main/ipc-handlers/tables.js +++ b/src/main/ipc-handlers/tables.js @@ -1,13 +1,39 @@ import { ipcMain } from 'electron'; -import InformationSchema from '../models/InformationSchema'; -import Tables from '../models/Tables'; +import { sqlEscaper } from 'common/libs/sqlEscaper'; +import { TEXT, LONG_TEXT, NUMBER, BLOB } from 'common/fieldTypes'; +import fs from 'fs'; // TODO: remap objects based on client export default (connections) => { ipcMain.handle('get-table-columns', async (event, { uid, schema, table }) => { try { - const result = await InformationSchema.getTableColumns(connections[uid], schema, table);// TODO: uniform column properties + const { rows } = await connections[uid] + .select('*') + .schema('information_schema') + .from('COLUMNS') + .where({ TABLE_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'` }) + .orderBy({ ORDINAL_POSITION: 'ASC' }) + .run(); + + const result = rows.map(field => { + return { + name: field.COLUMN_NAME, + key: field.COLUMN_KEY.toLowerCase(), + type: field.DATA_TYPE, + schema: field.TABLE_SCHEMA, + table: field.TABLE_NAME, + numPrecision: field.NUMERIC_PRECISION, + datePrecision: field.DATETIME_PRECISION, + charLength: field.CHARACTER_MAXIMUM_LENGTH, + isNullable: field.IS_NULLABLE, + default: field.COLUMN_DEFAULT, + charset: field.CHARACTER_SET_NAME, + collation: field.COLLATION_NAME, + autoIncrement: field.EXTRA.includes('auto_increment'), + comment: field.COLUMN_COMMENT + }; + }); return { status: 'success', response: result }; } catch (err) { @@ -17,7 +43,13 @@ export default (connections) => { ipcMain.handle('get-table-data', async (event, { uid, schema, table }) => { try { - const result = await Tables.getTableData(connections[uid], schema, table); + const result = await connections[uid] + .select('*') + .schema(schema) + .from(table) + .limit(1000) + .run(); + return { status: 'success', response: result }; } catch (err) { @@ -27,7 +59,27 @@ export default (connections) => { ipcMain.handle('get-key-usage', async (event, { uid, schema, table }) => { try { - const result = await InformationSchema.getKeyUsage(connections[uid], schema, table); + const { rows } = await connections[uid] + .select('*') + .schema('information_schema') + .from('KEY_COLUMN_USAGE') + .where({ TABLE_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'`, REFERENCED_TABLE_NAME: 'IS NOT NULL' }) + .run(); + + const result = rows.map(field => { + return { + schema: field.TABLE_SCHEMA, + table: field.TABLE_NAME, + column: field.COLUMN_NAME, + position: field.ORDINAL_POSITION, + constraintPosition: field.POSITION_IN_UNIQUE_CONSTRAINT, + constraintName: field.CONSTRAINT_NAME, + refSchema: field.REFERENCED_TABLE_SCHEMA, + refTable: field.REFERENCED_TABLE_NAME, + refColumn: field.REFERENCED_COLUMN_NAME + }; + }); + return { status: 'success', response: result }; } catch (err) { @@ -37,8 +89,34 @@ export default (connections) => { ipcMain.handle('update-table-cell', async (event, params) => { try { - const result = await Tables.updateTableCell(connections[params.uid], params); - return { status: 'success', response: result }; + let escapedParam; + let reload = false; + const id = typeof params.id === 'number' ? params.id : `"${params.id}"`; + + if (NUMBER.includes(params.type)) + escapedParam = params.content; + else if ([...TEXT, ...LONG_TEXT].includes(params.type)) + escapedParam = `"${sqlEscaper(params.content)}"`; + else if (BLOB.includes(params.type)) { + if (params.content) { + const fileBlob = fs.readFileSync(params.content); + escapedParam = `0x${fileBlob.toString('hex')}`; + reload = true; + } + else + escapedParam = '""'; + } + else + escapedParam = `"${sqlEscaper(params.content)}"`; + + await connections[params.uid] + .update({ [params.field]: `= ${escapedParam}` }) + .schema(params.schema) + .from(params.table) + .where({ [params.primary]: `= ${id}` }) + .run(); + + return { status: 'success', response: { reload } }; } catch (err) { return { status: 'error', response: err.toString() }; @@ -47,7 +125,12 @@ export default (connections) => { ipcMain.handle('delete-table-rows', async (event, params) => { try { - const result = await Tables.deleteTableRows(connections[params.uid], params); + const result = await connections[params.uid] + .schema(params.schema) + .delete(params.table) + .where({ [params.primary]: `IN (${params.rows.join(',')})` }) + .run(); + return { status: 'success', response: result }; } catch (err) { @@ -57,7 +140,39 @@ export default (connections) => { ipcMain.handle('insert-table-rows', async (event, params) => { try { - await Tables.insertTableRows(connections[params.uid], params); + const insertObj = {}; + for (const key in params.row) { + const type = params.fields[key]; + let escapedParam; + + if (params.row[key] === null) + escapedParam = 'NULL'; + else if (NUMBER.includes(type)) + escapedParam = params.row[key]; + else if ([...TEXT, ...LONG_TEXT].includes(type)) + escapedParam = `"${sqlEscaper(params.row[key])}"`; + else if (BLOB.includes(type)) { + if (params.row[key]) { + const fileBlob = fs.readFileSync(params.row[key]); + escapedParam = `0x${fileBlob.toString('hex')}`; + } + else + escapedParam = '""'; + } + else + escapedParam = `"${sqlEscaper(params.row[key])}"`; + + insertObj[key] = escapedParam; + } + + for (let i = 0; i < params.repeat; i++) { + await connections[params.uid] + .schema(params.schema) + .into(params.table) + .insert(insertObj) + .run(); + } + return { status: 'success' }; } catch (err) { @@ -67,7 +182,17 @@ export default (connections) => { ipcMain.handle('get-foreign-list', async (event, params) => { try { - const results = await Tables.getForeignList(connections[params.uid], params); + const query = connections[params.uid] + .select(`${params.column} AS foreignColumn`) + .schema(params.schema) + .from(params.table) + .orderBy('foreignColumn ASC'); + + if (params.description) + query.select(`LEFT(${params.description}, 20) AS foreignDescription`); + + const results = await query.run(); + return { status: 'success', response: results }; } catch (err) { diff --git a/src/main/libs/AntaresConnector.js b/src/main/libs/AntaresConnector.js deleted file mode 100644 index e1fdf26d..00000000 --- a/src/main/libs/AntaresConnector.js +++ /dev/null @@ -1,328 +0,0 @@ -'use strict'; -import mysql from 'mysql'; -import mssql from 'mssql'; -// import pg from 'pg'; TODO: PostgreSQL - -/** - * As Simple As Possible Query Builder - * - * @export - * @class AntaresConnector - */ -export class AntaresConnector { - /** - *Creates an instance of AntaresConnector. - * @param {Object} args connection params - * @memberof AntaresConnector - */ - constructor (args) { - this._client = args.client; - this._params = args.params; - this._poolSize = args.poolSize || false; - this._connection = null; - this._logger = args.logger || console.log; - - this._queryDefaults = { - schema: '', - select: [], - from: '', - where: [], - groupBy: [], - orderBy: [], - limit: [], - join: [], - update: [], - insert: {}, - delete: false - }; - this._query = Object.assign({}, this._queryDefaults); - } - - _reducer (acc, curr) { - const type = typeof curr; - - switch (type) { - case 'number': - case 'string': - return [...acc, curr]; - case 'object': - if (Array.isArray(curr)) - return [...acc, ...curr]; - else { - const clausoles = []; - for (const key in curr) - clausoles.push(`${key} ${curr[key]}`); - - return clausoles; - } - } - } - - /** - * Resets the query object after a query - * - * @memberof AntaresConnector - */ - _resetQuery () { - this._query = Object.assign({}, this._queryDefaults); - } - - /** - * @memberof AntaresConnector - */ - async connect () { - switch (this._client) { - case 'maria': - case 'mysql': - if (!this._poolSize) - this._connection = mysql.createConnection(this._params); - else - this._connection = mysql.createPool({ ...this._params, connectionLimit: this._poolSize }); - break; - case 'mssql': { - const mssqlParams = { - user: this._params.user, - password: this._params.password, - server: this._params.host - }; - this._connection = await mssql.connect(mssqlParams); - } - break; - default: - break; - } - } - - schema (schema) { - this._query.schema = schema; - return this; - } - - select (...args) { - this._query.select = [...this._query.select, ...args]; - return this; - } - - from (table) { - this._query.from = table; - return this; - } - - into (table) { - this._query.from = table; - return this; - } - - delete (table) { - this._query.delete = true; - this.from(table); - return this; - } - - where (...args) { - this._query.where = [...this._query.where, ...args]; - return this; - } - - groupBy (...args) { - this._query.groupBy = [...this._query.groupBy, ...args]; - return this; - } - - orderBy (...args) { - this._query.orderBy = [...this._query.orderBy, ...args]; - return this; - } - - limit (...args) { - this._query.limit = args; - return this; - } - - use (schema) { - let sql; - - switch (this._client) { - case 'maria': - case 'mysql': - sql = `USE \`${schema}\``; - break; - case 'mssql': - sql = `USE "${schema}"`; - break; - default: - break; - } - - return this.raw(sql); - } - - /** - * @param {String | Array} args field = value - * @returns - * @memberof AntaresConnector - */ - update (...args) { - this._query.update = [...this._query.update, ...args]; - return this; - } - - /** - * @param {Object} obj field: value - * @returns - * @memberof AntaresConnector - */ - insert (obj) { - this._query.insert = { ...this._query.insert, ...obj }; - return this; - } - - /** - * @returns {string} SQL string - * @memberof AntaresConnector - */ - getSQL () { - // SELECT - const selectArray = this._query.select.reduce(this._reducer, []); - let selectRaw = ''; - if (selectArray.length) { - switch (this._client) { - case 'maria': - case 'mysql': - selectRaw = selectArray.length ? `SELECT ${selectArray.join(', ')} ` : 'SELECT * '; - break; - case 'mssql': { - const topRaw = this._query.limit.length ? ` TOP (${this._query.limit[0]}) ` : ''; - selectRaw = selectArray.length ? `SELECT${topRaw} ${selectArray.join(', ')} ` : 'SELECT * '; - } - break; - default: - break; - } - } - - // FROM - let fromRaw = ''; - if (!this._query.update.length && !Object.keys(this._query.insert).length && !!this._query.from) - fromRaw = 'FROM'; - else if (Object.keys(this._query.insert).length) - fromRaw = 'INTO'; - - switch (this._client) { - case 'maria': - case 'mysql': - fromRaw += this._query.from ? ` ${this._query.schema ? `\`${this._query.schema}\`.` : ''}\`${this._query.from}\` ` : ''; - break; - case 'mssql': - fromRaw += this._query.from ? ` ${this._query.schema ? `${this._query.schema}.` : ''}${this._query.from} ` : ''; - break; - default: - break; - } - - const whereArray = this._query.where.reduce(this._reducer, []); - const whereRaw = whereArray.length ? `WHERE ${whereArray.join(' AND ')} ` : ''; - - const updateArray = this._query.update.reduce(this._reducer, []); - const updateRaw = updateArray.length ? `SET ${updateArray.join(', ')} ` : ''; - - let insertRaw = ''; - if (Object.keys(this._query.insert).length) { - const fieldsList = []; - const valueList = []; - const fields = this._query.insert; - - for (const key in fields) { - if (fields[key] === null) continue; - fieldsList.push(key); - valueList.push(fields[key]); - } - - insertRaw = `(${fieldsList.join(', ')}) VALUES (${valueList.join(', ')}) `; - } - - const groupByArray = this._query.groupBy.reduce(this._reducer, []); - const groupByRaw = groupByArray.length ? `GROUP BY ${groupByArray.join(', ')} ` : ''; - - const orderByArray = this._query.orderBy.reduce(this._reducer, []); - const orderByRaw = orderByArray.length ? `ORDER BY ${orderByArray.join(', ')} ` : ''; - - // LIMIT - let limitRaw; - switch (this._client) { - case 'maria': - case 'mysql': - limitRaw = this._query.limit.length ? `LIMIT ${this._query.limit.join(', ')} ` : ''; - break; - case 'mssql': - limitRaw = ''; - break; - default: - break; - } - - return `${selectRaw}${updateRaw ? 'UPDATE' : ''}${insertRaw ? 'INSERT ' : ''}${this._query.delete ? 'DELETE ' : ''}${fromRaw}${updateRaw}${whereRaw}${groupByRaw}${orderByRaw}${limitRaw}${insertRaw}`; - } - - /** - * @returns {Promise} - * @memberof AntaresConnector - */ - async run () { - const rawQuery = this.getSQL(); - this._resetQuery(); - return this.raw(rawQuery); - } - - /** - * @param {string} sql raw SQL query - * @returns {Promise} - * @memberof AntaresConnector - */ - async raw (sql) { - if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder - - switch (this._client) { // TODO: uniform fields with every client type, needed table name and fields array - case 'maria': - case 'mysql': { - const { rows, report, fields } = await new Promise((resolve, reject) => { - this._connection.query(sql, (err, response, fields) => { - if (err) - reject(err); - else { - resolve({ - rows: Array.isArray(response) ? response : false, - report: !Array.isArray(response) ? response : false, - fields - }); - } - }); - }); - return { rows, report, fields }; - } - case 'mssql': { - const results = await this._connection.request().query(sql); - return { rows: results.recordsets[0] };// TODO: fields - } - default: - break; - } - } - - /** - * @memberof AntaresConnector - */ - destroy () { - switch (this._client) { - case 'maria': - case 'mysql': - this._connection.end(); - break; - case 'mssql': - this._connection.close(); - break; - default: - break; - } - } -} diff --git a/src/main/libs/AntaresCore.js b/src/main/libs/AntaresCore.js new file mode 100644 index 00000000..c3c0d06f --- /dev/null +++ b/src/main/libs/AntaresCore.js @@ -0,0 +1,141 @@ +'use strict'; +/** + * As Simple As Possible Query Builder Core + * + * @class AntaresCore + */ +export class AntaresCore { + /** + * Creates an instance of AntaresCore. + * + * @param {Object} args connection params + * @memberof AntaresCore + */ + constructor (args) { + this._client = args.client; + this._params = args.params; + this._poolSize = args.poolSize || false; + this._connection = null; + this._logger = args.logger || console.log; + + this._queryDefaults = { + schema: '', + select: [], + from: '', + where: [], + groupBy: [], + orderBy: [], + limit: [], + join: [], + update: [], + insert: {}, + delete: false + }; + this._query = Object.assign({}, this._queryDefaults); + } + + _reducer (acc, curr) { + const type = typeof curr; + + switch (type) { + case 'number': + case 'string': + return [...acc, curr]; + case 'object': + if (Array.isArray(curr)) + return [...acc, ...curr]; + else { + const clausoles = []; + for (const key in curr) + clausoles.push(`${key} ${curr[key]}`); + + return clausoles; + } + } + } + + /** + * Resets the query object after a query + * + * @memberof AntaresCore + */ + _resetQuery () { + this._query = Object.assign({}, this._queryDefaults); + } + + schema (schema) { + this._query.schema = schema; + return this; + } + + select (...args) { + this._query.select = [...this._query.select, ...args]; + return this; + } + + from (table) { + this._query.from = table; + return this; + } + + into (table) { + this._query.from = table; + return this; + } + + delete (table) { + this._query.delete = true; + this.from(table); + return this; + } + + where (...args) { + this._query.where = [...this._query.where, ...args]; + return this; + } + + groupBy (...args) { + this._query.groupBy = [...this._query.groupBy, ...args]; + return this; + } + + orderBy (...args) { + this._query.orderBy = [...this._query.orderBy, ...args]; + return this; + } + + limit (...args) { + this._query.limit = args; + return this; + } + + /** + * @param {String | Array} args field = value + * @returns + * @memberof AntaresCore + */ + update (...args) { + this._query.update = [...this._query.update, ...args]; + return this; + } + + /** + * @param {Object} obj field: value + * @returns + * @memberof AntaresCore + */ + insert (obj) { + this._query.insert = { ...this._query.insert, ...obj }; + return this; + } + + /** + * @returns {Promise} + * @memberof AntaresCore + */ + async run () { + const rawQuery = this.getSQL(); + this._resetQuery(); + return this.raw(rawQuery); + } +} diff --git a/src/main/libs/ClientsFactory.js b/src/main/libs/ClientsFactory.js new file mode 100644 index 00000000..6b9293c0 --- /dev/null +++ b/src/main/libs/ClientsFactory.js @@ -0,0 +1,27 @@ +'use strict'; +import { MySQLClient } from './clients/MySQLClient'; + +export class ClientsFactory { + /** + * Returns a database connection based on received args. + * + * @param {Object} args + * @param {String} args.client + * @param {Object} args.params + * @param {String} args.params.host + * @param {Number} args.params.port + * @param {String} args.params.password + * @param {Number=} args.poolSize + * @returns Database Connection + * @memberof ClientsFactory + */ + static getConnection (args) { + switch (args.client) { + case 'mysql': + case 'maria': + return new MySQLClient(args); + default: + return new Error(`Unknown database client: ${args.client}`); + } + } +} diff --git a/src/main/libs/clients/MySQLClient.js b/src/main/libs/clients/MySQLClient.js new file mode 100644 index 00000000..1bee3e22 --- /dev/null +++ b/src/main/libs/clients/MySQLClient.js @@ -0,0 +1,210 @@ +'use strict'; +import mysql from 'mysql'; +import { AntaresCore } from '../AntaresCore'; + +export class MySQLClient extends AntaresCore { + /** + * @memberof MySQLClient + */ + async connect () { + if (!this._poolSize) + this._connection = mysql.createConnection(this._params); + else + this._connection = mysql.createPool({ ...this._params, connectionLimit: this._poolSize }); + } + + /** + * @memberof MySQLClient + */ + destroy () { + this._connection.end(); + } + + /** + * Executes an USE query + * + * @param {String} schema + * @memberof MySQLClient + */ + use (schema) { + return this.raw(`USE \`${schema}\``); + } + + /** + * @returns {Array.} databases scructure + * @memberof MySQLClient + */ + async getStructure () { + const { rows: databases } = await this.raw('SHOW DATABASES'); + // TODO: SHOW TABLE STATUS FROM `{DATABASE_NAME}`; + + const { rows: tables } = await this + .select('*') + .schema('information_schema') + .from('TABLES') + .orderBy({ TABLE_SCHEMA: 'ASC', TABLE_NAME: 'ASC' }) + .run(); + + const { rows: functions } = await this.raw('SHOW FUNCTION STATUS'); + const { rows: procedures } = await this.raw('SHOW PROCEDURE STATUS'); + const { rows: schedulers } = await this.raw('SELECT *, EVENT_SCHEMA AS `Db`, EVENT_NAME AS `Name` FROM information_schema.`EVENTS`'); + + const triggersArr = []; + for (const db of databases) { + let { rows: triggers } = await this.raw(`SHOW TRIGGERS FROM \`${db.Database}\``); + if (triggers.length) { + triggers = triggers.map(trigger => { + trigger.Db = db.Database; + return trigger; + }); + triggersArr.push(...triggers); + } + } + + return databases.map(db => { // TODO: remap all objects, + return { + name: db.Database, + tables: tables.filter(table => table.TABLE_SCHEMA === db.Database), + functions: functions.filter(func => func.Db === db.Database), + procedures: procedures.filter(procedure => procedure.Db === db.Database), + triggers: triggersArr.filter(trigger => trigger.Db === db.Database), + schedulers: schedulers.filter(scheduler => scheduler.Db === db.Database) + }; + }); + } + + /** + * SHOW COLLATION + * + * @returns {Array.} collations list + * @memberof MySQLClient + */ + async getCollations () { + const results = await this.raw('SHOW COLLATION'); + + return results.rows.map(row => { + return { + charset: row.Charset, + collation: row.Collation, + compiled: row.Compiled.includes('Yes'), + default: row.Default.includes('Yes'), + id: row.Id, + sortLen: row.Sortlen + }; + }); + } + + /** + * SHOW VARIABLES + * + * @returns {Array.} variables list + * @memberof MySQLClient + */ + async getVariables () { + const sql = 'SHOW VARIABLES'; + const results = await this.raw(sql); + + return results.rows.map(row => { + return { + name: row.Variable_name, + value: row.Value + }; + }); + } + + /** + * @returns {String} SQL string + * @memberof MySQLClient + */ + getSQL () { + // SELECT + const selectArray = this._query.select.reduce(this._reducer, []); + let selectRaw = ''; + + if (selectArray.length) + selectRaw = selectArray.length ? `SELECT ${selectArray.join(', ')} ` : 'SELECT * '; + + // FROM + let fromRaw = ''; + + if (!this._query.update.length && !Object.keys(this._query.insert).length && !!this._query.from) + fromRaw = 'FROM'; + else if (Object.keys(this._query.insert).length) + fromRaw = 'INTO'; + + fromRaw += this._query.from ? ` ${this._query.schema ? `\`${this._query.schema}\`.` : ''}\`${this._query.from}\` ` : ''; + + // WHERE + const whereArray = this._query.where.reduce(this._reducer, []); + const whereRaw = whereArray.length ? `WHERE ${whereArray.join(' AND ')} ` : ''; + + // UPDATE + const updateArray = this._query.update.reduce(this._reducer, []); + const updateRaw = updateArray.length ? `SET ${updateArray.join(', ')} ` : ''; + + // INSERT + let insertRaw = ''; + + if (Object.keys(this._query.insert).length) { + const fieldsList = []; + const valueList = []; + const fields = this._query.insert; + + for (const key in fields) { + if (fields[key] === null) continue; + fieldsList.push(key); + valueList.push(fields[key]); + } + + insertRaw = `(${fieldsList.join(', ')}) VALUES (${valueList.join(', ')}) `; + } + + // GROUP BY + const groupByArray = this._query.groupBy.reduce(this._reducer, []); + const groupByRaw = groupByArray.length ? `GROUP BY ${groupByArray.join(', ')} ` : ''; + + // ORDER BY + const orderByArray = this._query.orderBy.reduce(this._reducer, []); + const orderByRaw = orderByArray.length ? `ORDER BY ${orderByArray.join(', ')} ` : ''; + + // LIMIT + const limitRaw = this._query.limit.length ? `LIMIT ${this._query.limit.join(', ')} ` : ''; + + return `${selectRaw}${updateRaw ? 'UPDATE' : ''}${insertRaw ? 'INSERT ' : ''}${this._query.delete ? 'DELETE ' : ''}${fromRaw}${updateRaw}${whereRaw}${groupByRaw}${orderByRaw}${limitRaw}${insertRaw}`; + } + + /** + * @param {string} sql raw SQL query + * @param {boolean} [nest] + * @returns {Promise} + * @memberof MySQLClient + */ + async raw (sql, nest) { + const nestTables = nest ? '.' : false; + const resultsArr = []; + const queries = sql.split(';'); + + if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder + + for (const query of queries) { + if (!query) continue; + + const { rows, report, fields } = await new Promise((resolve, reject) => { + this._connection.query({ sql: query, nestTables }, (err, response, fields) => { + if (err) + reject(err); + else { + resolve({ + rows: Array.isArray(response) ? response : false, + report: !Array.isArray(response) ? response : false, + fields + }); + } + }); + }); + resultsArr.push({ rows, report, fields }); + } + + return resultsArr.length === 1 ? resultsArr[0] : resultsArr; + } +} diff --git a/src/main/models/Generic.js b/src/main/models/Generic.js deleted file mode 100644 index 55bf32d2..00000000 --- a/src/main/models/Generic.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; -export default class { - static async raw (connection, query, schema) { - if (schema) { - try { - await connection.use(schema); - } - catch (err) { - return err; - } - } - return connection.raw(query); - } -} diff --git a/src/main/models/InformationSchema.js b/src/main/models/InformationSchema.js deleted file mode 100644 index af25b345..00000000 --- a/src/main/models/InformationSchema.js +++ /dev/null @@ -1,64 +0,0 @@ -'use strict'; -export default class { - static testConnection (connection) { - return connection.select('1+1').run(); - } - - static getStructure (connection) { - return connection - .select('*') - .schema('information_schema') - .from('TABLES') - .orderBy({ TABLE_SCHEMA: 'ASC', TABLE_NAME: 'ASC' }) - .run(); - } - - static async getTableColumns (connection, schema, table) { - const { rows } = await connection - .select('*') - .schema('information_schema') - .from('COLUMNS') - .where({ TABLE_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'` }) - .orderBy({ ORDINAL_POSITION: 'ASC' }) - .run(); - - return rows.map(field => { - return { - name: field.COLUMN_NAME, - key: field.COLUMN_KEY.toLowerCase(), - type: field.DATA_TYPE, - numPrecision: field.NUMERIC_PRECISION, - datePrecision: field.DATETIME_PRECISION, - charLength: field.CHARACTER_MAXIMUM_LENGTH, - isNullable: field.IS_NULLABLE, - default: field.COLUMN_DEFAULT, - charset: field.CHARACTER_SET_NAME, - collation: field.COLLATION_NAME, - autoIncrement: field.EXTRA.includes('auto_increment') - }; - }); - } - - static async getKeyUsage (connection, schema, table) { - const { rows } = await connection - .select('*') - .schema('information_schema') - .from('KEY_COLUMN_USAGE') - .where({ TABLE_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'`, REFERENCED_TABLE_NAME: 'IS NOT NULL' }) - .run(); - - return rows.map(field => { - return { - schema: field.TABLE_SCHEMA, - table: field.TABLE_NAME, - column: field.COLUMN_NAME, - position: field.ORDINAL_POSITION, - constraintPosition: field.POSITION_IN_UNIQUE_CONSTRAINT, - constraintName: field.CONSTRAINT_NAME, - refSchema: field.REFERENCED_TABLE_SCHEMA, - refTable: field.REFERENCED_TABLE_NAME, - refColumn: field.REFERENCED_COLUMN_NAME - }; - }); - } -} diff --git a/src/main/models/Tables.js b/src/main/models/Tables.js deleted file mode 100644 index 046b2de2..00000000 --- a/src/main/models/Tables.js +++ /dev/null @@ -1,102 +0,0 @@ -'use strict'; -import { sqlEscaper } from 'common/libs/sqlEscaper'; -import { TEXT, LONG_TEXT, NUMBER, BLOB } from 'common/fieldTypes'; -import fs from 'fs'; - -export default class { - static async getTableData (connection, schema, table) { - return connection - .select('*') - .schema(schema) - .from(table) - .limit(1000) - .run(); - } - - static async updateTableCell (connection, params) { - let escapedParam; - let reload = false; - const id = typeof params.id === 'number' ? params.id : `"${params.id}"`; - - if (NUMBER.includes(params.type)) - escapedParam = params.content; - else if ([...TEXT, ...LONG_TEXT].includes(params.type)) - escapedParam = `"${sqlEscaper(params.content)}"`; - else if (BLOB.includes(params.type)) { - if (params.content) { - const fileBlob = fs.readFileSync(params.content); - escapedParam = `0x${fileBlob.toString('hex')}`; - reload = true; - } - else - escapedParam = '""'; - } - else - escapedParam = `"${sqlEscaper(params.content)}"`; - - await connection - .update({ [params.field]: `= ${escapedParam}` }) - .schema(params.schema) - .from(params.table) - .where({ [params.primary]: `= ${id}` }) - .run(); - - return { reload }; - } - - static async deleteTableRows (connection, params) { - return connection - .schema(params.schema) - .delete(params.table) - .where({ [params.primary]: `IN (${params.rows.join(',')})` }) - .run(); - } - - static async insertTableRows (connection, params) { - const insertObj = {}; - for (const key in params.row) { - const type = params.fields[key]; - let escapedParam; - - if (params.row[key] === null) - escapedParam = 'NULL'; - else if (NUMBER.includes(type)) - escapedParam = params.row[key]; - else if ([...TEXT, ...LONG_TEXT].includes(type)) - escapedParam = `"${sqlEscaper(params.row[key])}"`; - else if (BLOB.includes(type)) { - if (params.row[key]) { - const fileBlob = fs.readFileSync(params.row[key]); - escapedParam = `0x${fileBlob.toString('hex')}`; - } - else - escapedParam = '""'; - } - else - escapedParam = `"${sqlEscaper(params.row[key])}"`; - - insertObj[key] = escapedParam; - } - - for (let i = 0; i < params.repeat; i++) { - await connection - .schema(params.schema) - .into(params.table) - .insert(insertObj) - .run(); - } - } - - static async getForeignList (connection, params) { - const query = connection - .select(`${params.column} AS foreignColumn`) - .schema(params.schema) - .from(params.table) - .orderBy('foreignColumn ASC'); - - if (params.description) - query.select(`LEFT(${params.description}, 20) AS foreignDescription`); - - return query.run(); - } -} diff --git a/src/renderer/components/BaseConfirmModal.vue b/src/renderer/components/BaseConfirmModal.vue index 101b41a0..37cbaf7f 100644 --- a/src/renderer/components/BaseConfirmModal.vue +++ b/src/renderer/components/BaseConfirmModal.vue @@ -27,7 +27,7 @@ - - + + + + + + diff --git a/src/renderer/components/ModalNewTableRow.vue b/src/renderer/components/ModalNewTableRow.vue index fe9450a6..85f1967c 100644 --- a/src/renderer/components/ModalNewTableRow.vue +++ b/src/renderer/components/ModalNewTableRow.vue @@ -10,7 +10,7 @@ - @@ -44,12 +62,16 @@ import { mapGetters, mapActions } from 'vuex'; import _ from 'lodash'; import WorkspaceConnectPanel from '@/components/WorkspaceConnectPanel'; import WorkspaceExploreBarDatabase from '@/components/WorkspaceExploreBarDatabase'; +import DatabaseContext from '@/components/WorkspaceExploreBarDatabaseContext'; +import ModalNewDatabase from '@/components/ModalNewDatabase'; export default { name: 'WorkspaceExploreBar', components: { WorkspaceConnectPanel, - WorkspaceExploreBarDatabase + WorkspaceExploreBarDatabase, + DatabaseContext, + ModalNewDatabase }, props: { connection: Object, @@ -58,7 +80,14 @@ export default { data () { return { isRefreshing: false, - localWidth: null + isNewDBModal: false, + localWidth: null, + isDatabaseContext: false, + isTableContext: false, + databaseContextEvent: null, + tableContextEvent: null, + selectedDatabase: '', + selectedTable: '' }; }, computed: { @@ -117,6 +146,22 @@ export default { }, stopResize () { window.removeEventListener('mousemove', this.resize); + }, + showNewDBModal () { + this.isNewDBModal = true; + }, + hideNewDBModal () { + this.isNewDBModal = false; + }, + openDatabaseContext (payload) { + this.isTableContext = false; + this.selectedDatabase = payload.database; + this.databaseContextEvent = payload.event; + this.isDatabaseContext = true; + }, + closeDatabaseContext () { + this.isDatabaseContext = false; + this.selectedDatabase = ''; } } }; diff --git a/src/renderer/components/WorkspaceExploreBarDatabase.vue b/src/renderer/components/WorkspaceExploreBarDatabase.vue index b2e642ef..ee0c4600 100644 --- a/src/renderer/components/WorkspaceExploreBarDatabase.vue +++ b/src/renderer/components/WorkspaceExploreBarDatabase.vue @@ -4,6 +4,7 @@ class="accordion-header database-name pb-0" :class="{'text-bold': breadcrumbs.schema === database.name}" @click="changeBreadcrumbs({schema: database.name, table:null})" + @contextmenu.prevent="showDatabaseContext($event, database.name)" > @@ -18,6 +19,7 @@ class="menu-item" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.table === table.TABLE_NAME}" @click="changeBreadcrumbs({schema: database.name, table: table.TABLE_NAME})" + @contextmenu.prevent="showTableContext($event, table.TABLE_NAME)" > @@ -50,7 +52,13 @@ export default { methods: { ...mapActions({ changeBreadcrumbs: 'workspaces/changeBreadcrumbs' - }) + }), + showDatabaseContext (event, database) { + this.$emit('show-database-context', { event, database }); + }, + showTableContext (table) { + this.$emit('show-table-context', table); + } } }; diff --git a/src/renderer/components/WorkspaceExploreBarDatabaseContext.vue b/src/renderer/components/WorkspaceExploreBarDatabaseContext.vue new file mode 100644 index 00000000..7d141bcb --- /dev/null +++ b/src/renderer/components/WorkspaceExploreBarDatabaseContext.vue @@ -0,0 +1,112 @@ + + + diff --git a/src/renderer/components/WorkspaceQueryTab.vue b/src/renderer/components/WorkspaceQueryTab.vue index d215d43b..90bd30c8 100644 --- a/src/renderer/components/WorkspaceQueryTab.vue +++ b/src/renderer/components/WorkspaceQueryTab.vue @@ -15,11 +15,11 @@
-
- {{ $t('word.results') }}: {{ results.rows.length }} +
+ {{ $t('word.results') }}: {{ resultsCount }}
-
- {{ $t('message.affectedRows') }}: {{ results.report.affectedRows }} +
+ {{ $t('message.affectedRows') }}: {{ affectedCount }}
{{ $t('word.schema') }}: {{ workspace.breadcrumbs.schema }} @@ -34,6 +34,8 @@ ref="queryTable" :results="results" :tab-uid="tabUid" + :conn-uid="connection.uid" + mode="query" @update-field="updateField" @delete-selected="deleteSelected" /> @@ -42,7 +44,7 @@ - diff --git a/src/renderer/components/WorkspaceQueryTableContext.vue b/src/renderer/components/WorkspaceQueryTableContext.vue index b9d026f3..139756b5 100644 --- a/src/renderer/components/WorkspaceQueryTableContext.vue +++ b/src/renderer/components/WorkspaceQueryTableContext.vue @@ -4,7 +4,7 @@ @close-context="closeContext" >
- {{ $tc('message.deleteRows', selectedRows.length) }} + {{ $tc('message.deleteRows', selectedRows.length) }}
[] + }, keyUsage: Array }, data () { @@ -253,10 +256,12 @@ export default { return this.keyUsage.map(key => key.column); } }, - created () { - this.fields.forEach(field => { - this.isInlineEditor[field.name] = false; - }); + watch: { + fields () { + this.fields.forEach(field => { + this.isInlineEditor[field.name] = false; + }); + } }, methods: { getFieldType (cKey) { @@ -276,7 +281,13 @@ export default { return length; }, getFieldObj (cKey) { - return this.fields.filter(field => field.name === cKey || field.alias === cKey)[0]; + return this.fields.filter(field => + field.name === cKey || + field.alias === cKey || + `${field.table}.${field.name}` === cKey || + `${field.table}.${field.alias}` === cKey || + `${field.table.toLowerCase()}.${field.name}` === cKey || + `${field.table.toLowerCase()}.${field.alias}` === cKey)[0]; }, isNull (value) { return value === null ? ' is-null' : ''; diff --git a/src/renderer/components/WorkspaceTableTab.vue b/src/renderer/components/WorkspaceTableTab.vue index 4ea09a7b..8d25bd1e 100644 --- a/src/renderer/components/WorkspaceTableTab.vue +++ b/src/renderer/components/WorkspaceTableTab.vue @@ -21,8 +21,8 @@
-
- {{ $t('word.results') }}: {{ results.rows.length }} +
+ {{ $t('word.results') }}: {{ results[0].rows.length }}
{{ $t('word.schema') }}: {{ workspace.breadcrumbs.database }} @@ -33,9 +33,12 @@
@@ -71,7 +74,7 @@ export default { return { tabUid: 'data', isQuering: false, - results: {}, + results: [], fields: [], keyUsage: [], lastTable: null, @@ -115,12 +118,14 @@ export default { async getTableData () { if (!this.table) return; this.isQuering = true; - this.results = {}; + this.results = []; + const fieldsArr = []; + const keysArr = []; this.setTabFields({ cUid: this.connection.uid, tUid: this.tabUid, fields: [] }); const params = { uid: this.connection.uid, - schema: this.workspace.breadcrumbs.schema, + schema: this.schema, table: this.workspace.breadcrumbs.table }; @@ -128,7 +133,7 @@ export default { const { status, response } = await Tables.getTableColumns(params); if (status === 'success') { this.fields = response;// Needed to add new rows - this.setTabFields({ cUid: this.connection.uid, tUid: this.tabUid, fields: response }); + fieldsArr.push(response); } else this.addNotification({ status: 'error', message: response }); @@ -141,7 +146,7 @@ export default { const { status, response } = await Tables.getTableData(params); if (status === 'success') - this.results = response; + this.results = [response]; else this.addNotification({ status: 'error', message: response }); } @@ -151,9 +156,10 @@ export default { try { // Key usage (foreign keys) const { status, response } = await Tables.getKeyUsage(params); + if (status === 'success') { this.keyUsage = response;// Needed to add new rows - this.setTabKeyUsage({ cUid: this.connection.uid, tUid: this.tabUid, keyUsage: response }); + keysArr.push(response); } else this.addNotification({ status: 'error', message: response }); @@ -162,8 +168,13 @@ export default { this.addNotification({ status: 'error', message: err.stack }); } + this.setTabFields({ cUid: this.connection.uid, tUid: this.tabUid, fields: fieldsArr }); + this.setTabKeyUsage({ cUid: this.connection.uid, tUid: this.tabUid, keyUsage: keysArr }); this.isQuering = false; }, + getTable () { + return this.table; + }, reloadTable () { this.getTableData(); }, diff --git a/src/renderer/i18n/ar-SA.js b/src/renderer/i18n/ar-SA.js index 5279d5e7..50f36dba 100644 --- a/src/renderer/i18n/ar-SA.js +++ b/src/renderer/i18n/ar-SA.js @@ -50,7 +50,7 @@ module.exports = { testConnection: 'إختبر الإتصال', editConnection: 'عدل الإتصال', deleteConnection: 'إحذف الإتصال', - deleteConnectionCorfirm: 'هل أنت متأكد من حذف الإتصال؟', + deleteCorfirm: 'هل أنت متأكد من حذف الإتصال؟', connectionSuccessfullyMade: 'تم الإتصال بنجاح!', madeWithJS: 'بني بـ 💛 و جافاسكربت!', checkForUpdates: 'تأكد من التحديثات', diff --git a/src/renderer/i18n/en-US.js b/src/renderer/i18n/en-US.js index 0c376073..6f1753ea 100644 --- a/src/renderer/i18n/en-US.js +++ b/src/renderer/i18n/en-US.js @@ -38,7 +38,10 @@ module.exports = { add: 'Add', data: 'Data', properties: 'Properties', - insert: 'Insert' + insert: 'Insert', + connecting: 'Connecting', + name: 'Name', + collation: 'Collation' }, message: { appWelcome: 'Welcome to Antares SQL Client!', @@ -50,7 +53,7 @@ module.exports = { testConnection: 'Test connection', editConnection: 'Edit connection', deleteConnection: 'Delete connection', - deleteConnectionCorfirm: 'Do you confirm the cancellation of', + deleteCorfirm: 'Do you confirm the cancellation of', connectionSuccessfullyMade: 'Connection successfully made!', madeWithJS: 'Made with 💛 and JavaScript!', checkForUpdates: 'Check for updates', @@ -70,7 +73,12 @@ module.exports = { addNewRow: 'Add new row', numberOfInserts: 'Number of inserts', openNewTab: 'Open a new tab', - affectedRows: 'Affected rows' + affectedRows: 'Affected rows', + createNewDatabase: 'Create new Database', + databaseName: 'Database name', + serverDefault: 'Server default', + deleteDatabase: 'Delete database', + editDatabase: 'Edit database' }, // Date and Time short: { diff --git a/src/renderer/i18n/es-ES.js b/src/renderer/i18n/es-ES.js new file mode 100644 index 00000000..a222e2f6 --- /dev/null +++ b/src/renderer/i18n/es-ES.js @@ -0,0 +1,90 @@ +module.exports = { + word: { + edit: 'Editar', + save: 'Guardar', + close: 'Cerrar', + delete: 'Eliminar', + confirm: 'Confirmar', + cancel: 'Cancelar', + send: 'Enviar', + connectionName: 'Nombre de la conexión', + client: 'Cliente', + hostName: 'Servidor', + port: 'Puerto', + user: 'Usuario', + password: 'Contraseña', + credentials: 'Credenciales', + connect: 'Connectar', + connected: 'Conectado', + disconnect: 'Desconectar', + disconnected: 'Desconectado', + refresh: 'Refrescar', + settings: 'Configuración', + general: 'General', + themes: 'Temas', + update: 'Actualizar', + about: 'Sobre', + language: 'Idioma', + version: 'Versión', + donate: 'Donar', + run: 'Ejecutar', + schema: 'Esquema', + results: 'Resultados', + size: 'Tamaño', + seconds: 'Segundos', + type: 'Tipo', + mimeType: 'Mime-Type', + download: 'Descargar', + add: 'Añadir', + data: 'Datos', + properties: 'Propiedades', + insert: 'Insertar', + connecting: 'Conectando' + }, + message: { + appWelcome: 'Bienvenido a Antares Cliente SQL!', + appFirstStep: 'Primer paso: Crear una conexión a una Base de Datos.', + addConnection: 'Añadir conexión', + createConnection: 'Crear conexión', + createNewConnection: 'Crear nueva conexión', + askCredentials: 'Preguntar credenciales', + testConnection: 'Comprobar conexión', + editConnection: 'Editar conexión', + deleteConnection: 'Eliminar conexión', + deleteCorfirm: 'Confirmas la cancelación de', + connectionSuccessfullyMade: 'Conexión realizada correctamente!', + madeWithJS: 'Hecho con 💛 y JavaScript!', + checkForUpdates: 'Comprobar actualizaciones', + noUpdatesAvailable: 'No hay actualizaciones', + checkingForUpdate: 'Comprobando actualizaciones', + checkFailure: 'Error en la comprobación, por favor pruebe más tarde', + updateAvailable: 'Actualización disponible', + downloadingUpdate: 'Descargando actualización', + updateDownloaded: 'Descargada actualización', + restartToInstall: 'Reiniciar Antares para instalar', + unableEditFieldWithoutPrimary: 'No se puede editar una campo sin Llave Primaria en el registro', + editCell: 'Editar celda', + deleteRows: 'Eliminar fila | Eliminar {count} filas', + confirmToDeleteRows: '¿Quiere realmente eliminar una fila? | ¿Quiere realmente eliminar {count} filas?', + notificationsTimeout: 'Tiempo de espera', + uploadFile: 'Cargar fichero', + addNewRow: 'Añadir nueva fila', + numberOfInserts: 'Numero de inserciones', + openNewTab: 'Abrir nueva pestaña', + affectedRows: 'Filas afectadas' + }, + // Date and Time + short: { + year: 'numeric', + month: 'short', + day: 'numeric' + }, + long: { + year: 'numeric', + month: 'short', + day: 'numeric', + weekday: 'short', + hour: 'numeric', + minute: 'numeric' + } +}; diff --git a/src/renderer/i18n/index.js b/src/renderer/i18n/index.js index a78dddcf..9655e746 100644 --- a/src/renderer/i18n/index.js +++ b/src/renderer/i18n/index.js @@ -7,7 +7,8 @@ const i18n = new VueI18n({ messages: { 'en-US': require('./en-US'), 'it-IT': require('./it-IT'), - 'ar-SA': require('./ar-SA') + 'ar-SA': require('./ar-SA'), + 'es-ES': require('./es-ES') } }); export default i18n; diff --git a/src/renderer/i18n/it-IT.js b/src/renderer/i18n/it-IT.js index ec37e0c5..1ba017e9 100644 --- a/src/renderer/i18n/it-IT.js +++ b/src/renderer/i18n/it-IT.js @@ -29,7 +29,17 @@ module.exports = { donate: 'Dona', run: 'Esegui', schema: 'Schema', - results: 'Results' + results: 'Risultati', + size: 'Dimensioni', + seconds: 'Secondi', + type: 'Tipo', + mimeType: 'Mime-Type', + download: 'Scarica', + add: 'Aggiungi', + data: 'Dati', + properties: 'Proprietà', + insert: 'Inserisci', + connecting: 'Connessione in corso' }, message: { appWelcome: 'Benvenuto in Antares SQL Client!', @@ -41,7 +51,7 @@ module.exports = { testConnection: 'Testa connessione', editConnection: 'Modifica connessione', deleteConnection: 'Elimina connessione', - deleteConnectionCorfirm: 'Confermi l\'eliminazione di', + deleteCorfirm: 'Confermi l\'eliminazione di', connectionSuccessfullyMade: 'Connessione avvenuta con successo!', madeWithJS: 'Fatto con 💛 e JavaScript!', checkForUpdates: 'Cerca aggiornamenti', @@ -55,7 +65,13 @@ module.exports = { unableEditFieldWithoutPrimary: 'Impossibile modificare il campo senza una primary key nel resultset', editCell: 'Modifica cella', deleteRows: 'Elimina riga | Elimina {count} righe', - confirmToDeleteRows: 'Confermi di voler cancellare una riga? | Confermi di voler cancellare {count} righe?' + confirmToDeleteRows: 'Confermi di voler cancellare una riga? | Confermi di voler cancellare {count} righe?', + notificationsTimeout: 'Timeout Notifiche', + uploadFile: 'Carica file', + addNewRow: 'Aggiungi nuova riga', + numberOfInserts: 'Numero di insert', + openNewTab: 'Apri nuova scheda', + affectedRows: 'Righe interessate' }, // Date and Time short: { diff --git a/src/renderer/i18n/supported-locales.js b/src/renderer/i18n/supported-locales.js index 5e87bbb2..f2038f10 100644 --- a/src/renderer/i18n/supported-locales.js +++ b/src/renderer/i18n/supported-locales.js @@ -1,5 +1,6 @@ export default { 'en-US': 'English', 'it-IT': 'Italiano', - 'ar-SA': 'العربية' + 'ar-SA': 'العربية', + 'es-ES': 'Español' }; diff --git a/src/renderer/images/logo-full.png b/src/renderer/images/logo-full.png deleted file mode 100644 index deeaef41..00000000 Binary files a/src/renderer/images/logo-full.png and /dev/null differ diff --git a/src/renderer/images/logo.png b/src/renderer/images/logo.png index 9ce8a7fa..ab017b43 100644 Binary files a/src/renderer/images/logo.png and b/src/renderer/images/logo.png differ diff --git a/src/renderer/images/logo.svg b/src/renderer/images/logo.svg index da443bd3..226d31d1 100644 --- a/src/renderer/images/logo.svg +++ b/src/renderer/images/logo.svg @@ -1 +1,6 @@ - \ No newline at end of file + + + + + + diff --git a/src/renderer/ipc-api/Connection.js b/src/renderer/ipc-api/Connection.js index 9ff0561f..03d20938 100644 --- a/src/renderer/ipc-api/Connection.js +++ b/src/renderer/ipc-api/Connection.js @@ -17,12 +17,4 @@ export default class { static disconnect (uid) { return ipcRenderer.invoke('disconnect', uid); } - - static refresh (uid) { - return ipcRenderer.invoke('refresh', uid); - } - - static rawQuery (params) { - return ipcRenderer.invoke('raw-query', params); - } } diff --git a/src/renderer/ipc-api/Database.js b/src/renderer/ipc-api/Database.js new file mode 100644 index 00000000..6e1ac288 --- /dev/null +++ b/src/renderer/ipc-api/Database.js @@ -0,0 +1,40 @@ +'use strict'; +import { ipcRenderer } from 'electron'; + +export default class { + static createDatabase (params) { + return ipcRenderer.invoke('create-database', params); + } + + static updateDatabase (params) { + return ipcRenderer.invoke('update-database', params); + } + + static getDatabaseCollation (params) { + return ipcRenderer.invoke('get-database-collation', params); + } + + static deleteDatabase (params) { + return ipcRenderer.invoke('delete-database', params); + } + + static getStructure (uid) { + return ipcRenderer.invoke('get-structure', uid); + } + + static getCollations (uid) { + return ipcRenderer.invoke('get-collations', uid); + } + + static getVariables (uid) { + return ipcRenderer.invoke('get-variables', uid); + } + + static useSchema (params) { + return ipcRenderer.invoke('use-schema', params); + } + + static rawQuery (params) { + return ipcRenderer.invoke('raw-query', params); + } +} diff --git a/src/renderer/mixins/tableTabs.js b/src/renderer/mixins/tableTabs.js index 4e7b6f31..bea0538b 100644 --- a/src/renderer/mixins/tableTabs.js +++ b/src/renderer/mixins/tableTabs.js @@ -1,14 +1,17 @@ import Tables from '@/ipc-api/Tables'; export default { + computed: { + schema () { + return this.workspace.breadcrumbs.schema; + } + }, methods: { async updateField (payload) { this.isQuering = true; const params = { uid: this.connection.uid, - schema: this.workspace.breadcrumbs.schema, - table: this.table, ...payload }; @@ -34,18 +37,14 @@ export default { const params = { uid: this.connection.uid, - schema: this.workspace.breadcrumbs.schema, - table: this.workspace.breadcrumbs.table, ...payload }; try { const { status, response } = await Tables.deleteTableRows(params); - if (status === 'success') { - const { primary, rows } = params; - this.results = { ...this.results, rows: this.results.rows.filter(row => !rows.includes(row[primary])) }; - this.$refs.queryTable.refreshScroller();// Necessary to re-render virtual scroller - } + + if (status === 'success') + this.reloadTable(); else this.addNotification({ status: 'error', message: response }); } diff --git a/src/renderer/store/modules/application.store.js b/src/renderer/store/modules/application.store.js index 812af3b8..5b316e26 100644 --- a/src/renderer/store/modules/application.store.js +++ b/src/renderer/store/modules/application.store.js @@ -3,7 +3,7 @@ export default { namespaced: true, strict: true, state: { - app_name: 'Antares - Database Client', + app_name: 'Antares - SQL Client', app_version: process.env.PACKAGE_VERSION || 0, is_loading: false, is_new_modal: false, diff --git a/src/renderer/store/modules/settings.store.js b/src/renderer/store/modules/settings.store.js index 5b6d0b6a..1751f8c1 100644 --- a/src/renderer/store/modules/settings.store.js +++ b/src/renderer/store/modules/settings.store.js @@ -7,7 +7,7 @@ export default { state: { locale: 'en-US', explorebar_size: null, - notifications_timeout: 10 + notifications_timeout: 5 }, getters: { getLocale: state => state.locale, diff --git a/src/renderer/store/modules/workspaces.store.js b/src/renderer/store/modules/workspaces.store.js index 37d93802..66f9baea 100644 --- a/src/renderer/store/modules/workspaces.store.js +++ b/src/renderer/store/modules/workspaces.store.js @@ -1,19 +1,9 @@ 'use strict'; import Connection from '@/ipc-api/Connection'; +import Database from '@/ipc-api/Database'; import { uidGen } from 'common/libs/uidGen'; const tabIndex = []; - -function remapStructure (structure) { // TODO: move to main process and add fields (for autocomplete purpose) - const databases = structure.map(table => table.TABLE_SCHEMA) - .filter((value, index, self) => self.indexOf(value) === index); - - return databases.map(db => { - return { - name: db, - tables: structure.filter(table => table.TABLE_SCHEMA === db) - }; - }); -} +let lastSchema = ''; export default { namespaced: true, @@ -31,6 +21,9 @@ export default { getWorkspace: state => uid => { return state.workspaces.find(workspace => workspace.uid === uid); }, + getDatabaseVariable: state => (uid, name) => { + return state.workspaces.find(workspace => workspace.uid === uid).variables.find(variable => variable.name === name); + }, getWorkspaceTab: (state, getters) => tUid => { if (!getters.getSelected) return; const workspace = state.workspaces.find(workspace => workspace.uid === getters.getSelected); @@ -57,6 +50,12 @@ export default { REFRESH_STRUCTURE (state, { uid, structure }) { state.workspaces = state.workspaces.map(workspace => workspace.uid === uid ? { ...workspace, structure } : workspace); }, + REFRESH_COLLATIONS (state, { uid, collations }) { + state.workspaces = state.workspaces.map(workspace => workspace.uid === uid ? { ...workspace, collations } : workspace); + }, + REFRESH_VARIABLES (state, { uid, variables }) { + state.workspaces = state.workspaces.map(workspace => workspace.uid === uid ? { ...workspace, variables } : workspace); + }, ADD_WORKSPACE (state, workspace) { state.workspaces.push(workspace); }, @@ -136,7 +135,7 @@ export default { } }, actions: { - selectWorkspace ({ commit }, uid) { + selectWorkspace ({ commit, dispatch }, uid) { commit('SELECT_WORKSPACE', uid); }, async connectWorkspace ({ dispatch, commit }, connection) { @@ -144,8 +143,11 @@ export default { const { status, response } = await Connection.connect(connection); if (status === 'error') dispatch('notifications/addNotification', { status, message: response }, { root: true }); - else - commit('ADD_CONNECTED', { uid: connection.uid, structure: remapStructure(response) }); + else { + commit('ADD_CONNECTED', { uid: connection.uid, structure: response }); + dispatch('refreshCollations', connection.uid); + dispatch('refreshVariables', connection.uid); + } } catch (err) { dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true }); @@ -153,11 +155,35 @@ export default { }, async refreshStructure ({ dispatch, commit }, uid) { try { - const { status, response } = await Connection.refresh(uid); + const { status, response } = await Database.getStructure(uid); if (status === 'error') dispatch('notifications/addNotification', { status, message: response }, { root: true }); else - commit('REFRESH_STRUCTURE', { uid, structure: remapStructure(response) }); + commit('REFRESH_STRUCTURE', { uid, structure: response }); + } + catch (err) { + dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true }); + } + }, + async refreshCollations ({ dispatch, commit }, uid) { + try { + const { status, response } = await Database.getCollations(uid); + if (status === 'error') + dispatch('notifications/addNotification', { status, message: response }, { root: true }); + else + commit('REFRESH_COLLATIONS', { uid, collations: response }); + } + catch (err) { + dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true }); + } + }, + async refreshVariables ({ dispatch, commit }, uid) { + try { + const { status, response } = await Database.getVariables(uid); + if (status === 'error') + dispatch('notifications/addNotification', { status, message: response }, { root: true }); + else + commit('REFRESH_VARIABLES', { uid, variables: response }); } catch (err) { dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true }); @@ -186,6 +212,8 @@ export default { keyUsage: [] }], structure: {}, + variables: [], + collations: [], breadcrumbs: {} }; @@ -195,6 +223,11 @@ export default { dispatch('newTab', uid); }, changeBreadcrumbs ({ commit, getters }, payload) { + if (lastSchema !== payload.schema) { + Database.useSchema({ uid: getters.getSelected, schema: payload.schema }); + lastSchema = payload.schema; + } + commit('CHANGE_BREADCRUMBS', { uid: getters.getSelected, breadcrumbs: payload }); }, newTab ({ commit }, uid) { diff --git a/webpack.config.js b/webpack.config.js index 59c91017..80b9d899 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -2,6 +2,7 @@ const webpack = require('webpack'); const MonacoEditorPlugin = require('monaco-editor-webpack-plugin'); module.exports = { + stats: 'errors-warnings', plugins: [ new MonacoEditorPlugin({ languages: ['sql']