diff --git a/README.md b/README.md index a7cbc0ec..abfa60e9 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,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 diff --git a/package.json b/package.json index b1337b49..e8a6aa91 100644 --- a/package.json +++ b/package.json @@ -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,9 +47,9 @@ } }, "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", "monaco-editor": "^0.20.0", @@ -67,7 +69,7 @@ "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", diff --git a/src/main/ipc-handlers/connection.js b/src/main/ipc-handlers/connection.js index f3768342..9b40d86f 100644 --- a/src/main/ipc-handlers/connection.js +++ b/src/main/ipc-handlers/connection.js @@ -1,12 +1,12 @@ import { ipcMain } from 'electron'; -import { AntaresConnector } from '../libs/AntaresConnector'; +import { ClientsFactory } from '../libs/ClientsFactory'; import InformationSchema from '../models/InformationSchema'; import Generic from '../models/Generic'; 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, @@ -33,7 +33,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, diff --git a/src/main/libs/AntaresConnector.js b/src/main/libs/AntaresConnector.js deleted file mode 100644 index fc73f718..00000000 --- a/src/main/libs/AntaresConnector.js +++ /dev/null @@ -1,341 +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 - * @param {boolean} [nest] - * @returns {Promise} - * @memberof AntaresConnector - */ - 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; - - 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: 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 }); - break; - } - case 'mssql': { - const results = await this._connection.request().query(query); - resultsArr.push({ rows: results.recordsets[0] });// TODO: fields - break; - } - default: - break; - } - } - - return resultsArr.length === 1 ? resultsArr[0] : resultsArr; - } - - /** - * @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..d8fbcc74 --- /dev/null +++ b/src/main/libs/clients/MySQLClient.js @@ -0,0 +1,129 @@ +'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 {*} schema + * @memberof MySQLClient + */ + use (schema) { + const sql = `USE \`${schema}\``; + return this.raw(sql); + } + + /** + * @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/renderer/components/WorkspaceQueryTable.vue b/src/renderer/components/WorkspaceQueryTable.vue index 7d746305..08dcd6ee 100644 --- a/src/renderer/components/WorkspaceQueryTable.vue +++ b/src/renderer/components/WorkspaceQueryTable.vue @@ -29,6 +29,7 @@ v-for="(field, index) in fields" :key="index" class="th c-hand" + :title="field.comment ? field.comment : false" >
@@ -38,7 +39,7 @@ :class="`key-${field.key}`" :title="keyName(field.key)" /> - {{ field.alias || field.name }} + {{ field.alias || field.name }}