1
1
mirror of https://github.com/Fabio286/antares.git synced 2025-02-17 04:00:48 +01:00

Merge pull request #129 from toriphes/feat/db-import-export

feat(MySQL): db import export
This commit is contained in:
Fabio Di Stasio 2022-03-12 14:17:48 +01:00 committed by GitHub
commit dd070d008d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 2217 additions and 95 deletions

3
.gitignore vendored
View File

@ -5,4 +5,5 @@ node_modules
thumbs.db
NOTES.md
*.txt
package-lock.json
package-lock.json
*.heapsnapshot

14
.vscode/launch.json vendored
View File

@ -2,8 +2,8 @@
"version": "0.2.0",
"configurations": [
{
"cwd": "${workspaceFolder}",
"name": "Electron: Main",
"cwd": "${workspaceFolder}",
"port": 9222,
"protocol": "inspector",
"request": "attach",
@ -18,7 +18,17 @@
"sourceMaps": true,
"type": "chrome",
"webRoot": "${workspaceFolder}"
}
},
{
"name": "Electron: Worker",
"cwd": "${workspaceFolder}",
"port": 9224,
"protocol": "inspector",
"request": "attach",
"sourceMaps": true,
"type": "node",
"timeout": 1000000
},
],
"compounds": [
{

View File

@ -8,13 +8,14 @@
"scripts": {
"debug": "npm run rebuild:electron && npm run debug-runner",
"debug-runner": "node scripts/devRunner.js --remote-debug",
"compile": "npm run compile:main && npm run compile:renderer",
"compile": "npm run compile:main && npm run compile:workers && npm run compile:renderer",
"compile:main": "webpack --mode=production --config webpack.main.config.js",
"compile:workers": "webpack --mode=production --config webpack.workers.config.js",
"compile:renderer": "webpack --mode=production --config webpack.renderer.config.js",
"build": "cross-env NODE_ENV=production npm run compile",
"build:local": "npm run build && electron-builder",
"build:appx": "npm run build:local -- --win appx",
"rebuild:electron": "npm run postinstall",
"rebuild:electron": "rimraf ./dist && npm run postinstall",
"release": "standard-version",
"release:pre": "npm run release -- --prerelease alpha",
"postinstall": "electron-builder install-app-deps",
@ -136,7 +137,6 @@
"all-contributors-cli": "^6.20.0",
"babel-loader": "^8.2.3",
"chalk": "^4.1.2",
"clean-webpack-plugin": "^4.0.0",
"cross-env": "^7.0.2",
"css-loader": "^6.5.0",
"electron": "^17.0.1",
@ -154,6 +154,7 @@
"node-loader": "^2.0.0",
"playwright": "^1.18.1",
"progress-webpack-plugin": "^1.0.12",
"rimraf": "^3.0.2",
"sass": "^1.42.1",
"sass-loader": "^12.3.0",
"standard-version": "^9.3.1",

View File

@ -12,7 +12,7 @@ const { spawn } = require('child_process');
const mainConfig = require('../webpack.main.config');
const rendererConfig = require('../webpack.renderer.config');
// const workersConfig = require('../webpack.workers.config');
const workersConfig = require('../webpack.workers.config');
let electronProcess = null;
let manualRestart = null;
@ -64,7 +64,7 @@ async function restartElectron () {
}
function startMain () {
const webpackSetup = webpack(mainConfig);
const webpackSetup = webpack([mainConfig, workersConfig]);
webpackSetup.compilers.forEach((compiler) => {
const { name } = compiler;

View File

@ -134,7 +134,7 @@ export default class {
{ name: 'phoneNumberFormat', group: 'phone', types: ['string'] },
{ name: 'phoneFormats', group: 'phone', types: ['string'] },
{ name: 'number', group: 'random', types: ['string', 'number'], params: ['min', 'max'] },
{ name: 'number', group: 'datatype', types: ['string', 'number'], params: ['min', 'max'] },
{ name: 'float', group: 'random', types: ['string', 'float'], params: ['min', 'max'] },
{ name: 'arrayElement', group: 'random', types: ['string'] },
{ name: 'arrayElements', group: 'random', types: ['string'] },

View File

@ -38,6 +38,8 @@ module.exports = {
databaseEdit: false,
schemaEdit: false,
schemaDrop: false,
schemaExport: false,
schemaImport: false,
tableSettings: false,
tableOptions: false,
tableArray: false,

View File

@ -34,6 +34,8 @@ module.exports = {
schedulerAdd: true,
schemaEdit: true,
schemaDrop: true,
schemaExport: true,
schemaImport: true,
tableSettings: true,
viewSettings: true,
triggerSettings: true,

View File

@ -32,6 +32,8 @@ module.exports = {
functionAdd: true,
schemaDrop: true,
databaseEdit: false,
schemaExport: false,
schemaImport: false,
tableSettings: true,
viewSettings: true,
triggerSettings: true,

View File

@ -41,6 +41,7 @@ export const NUMBER = [
export const FLOAT = [
'FLOAT',
'DECIMAL',
'DOUBLE',
'REAL',
'DOUBLE PRECISION',

View File

@ -33,7 +33,7 @@ const lookup = {
*/
export default function hexToBinary (hex) {
let binary = '';
for (let i = 0, len = hex.length; i < len; i++)
for (let i = 0; i < hex.length; i++)
binary += lookup[hex[i]];
return binary;

View File

@ -23,7 +23,7 @@ export function mimeFromHex (hex) {
case '425A68':
return { ext: 'bz2', mime: 'application/x-bzip2' };
default:
switch (hex) { // 4 bites
switch (hex) { // 4 bytes
case '89504E47':
return { ext: 'png', mime: 'image/png' };
case '47494638':

View File

@ -0,0 +1,93 @@
import { Transform } from 'stream';
export default class SqlParser extends Transform {
constructor (opts) {
opts = {
delimiter: ';',
encoding: 'utf8',
writableObjectMode: true,
readableObjectMode: true,
...opts
};
super(opts);
this._buffer = '';
this._lastChar = '';
this._last9Chars = '';
this.encoding = opts.encoding;
this.delimiter = opts.delimiter;
this.isEscape = false;
this.currentQuote = null;
this.isDelimiter = false;
}
_transform (chunk, encoding, next) {
for (const char of chunk.toString(this.encoding)) {
this.checkEscape();
this._buffer += char;
this._lastChar = char;
this._last9Chars += char.trim().toLocaleLowerCase();
if (this._last9Chars.length > 9)
this._last9Chars = this._last9Chars.slice(-9);
this.checkNewDelimiter(char);
this.checkQuote(char);
const query = this.getQuery();
if (query)
this.push(query);
}
next();
}
checkEscape () {
if (this._buffer.length > 0) {
this.isEscape = this._lastChar === '\\'
? !this.isEscape
: false;
}
}
checkNewDelimiter (char) {
if (this.currentQuote === null && this._last9Chars === 'delimiter') {
this.isDelimiter = true;
this._buffer = '';
}
else {
const isNewLine = char === '\n' || char === '\r';
if (isNewLine && this.isDelimiter) {
this.isDelimiter = false;
this.delimiter = this._buffer.trim();
this._buffer = '';
}
}
}
checkQuote (char) {
const isQuote = !this.isEscape && (char === '\'' || char === '"');
if (isQuote && this.currentQuote === char)
this.currentQuote = null;
else if (isQuote && this.currentQuote === null)
this.currentQuote = char;
}
getQuery () {
if (this.isDelimiter)
return false;
let query = false;
let demiliterFound = false;
if (this.currentQuote === null && this._buffer.length >= this.delimiter.length)
demiliterFound = this._last9Chars.slice(-this.delimiter.length) === this.delimiter;
if (demiliterFound) {
const parsedStr = this._buffer.trim();
query = parsedStr.slice(0, parsedStr.length - this.delimiter.length);
this._buffer = '';
}
return query;
}
}

View File

@ -1,4 +1,4 @@
import { app, ipcMain } from 'electron';
import { app, ipcMain, dialog } from 'electron';
export default () => {
ipcMain.on('close-app', () => {
@ -9,4 +9,12 @@ export default () => {
const key = false;
event.returnValue = key;
});
ipcMain.handle('showOpenDialog', (event, options) => {
return dialog.showOpenDialog(options);
});
ipcMain.handle('get-download-dir-path', () => {
return app.getPath('downloads');
});
};

View File

@ -40,7 +40,7 @@ export default connections => {
}
try {
const connection = await ClientsFactory.getConnection({
const connection = await ClientsFactory.getClient({
client: conn.client,
params
});
@ -100,7 +100,7 @@ export default connections => {
}
try {
const connection = ClientsFactory.getConnection({
const connection = ClientsFactory.getClient({
client: conn.client,
params,
poolSize: 5

View File

@ -1,7 +1,15 @@
import fs from 'fs';
import path from 'path';
import { fork } from 'child_process';
import { ipcMain, dialog } from 'electron';
import { ipcMain } from 'electron';
// @TODO: need some factories
const isDevelopment = process.env.NODE_ENV !== 'production';
export default connections => {
let exporter = null;
let importer = null;
ipcMain.handle('create-schema', async (event, params) => {
try {
await connections[params.uid].createSchema(params);
@ -37,9 +45,16 @@ export default connections => {
ipcMain.handle('get-schema-collation', async (event, params) => {
try {
const collation = await connections[params.uid].getDatabaseCollation(params);
const collation = await connections[params.uid].getDatabaseCollation(
params
);
return { status: 'success', response: collation.rows.length ? collation.rows[0].DEFAULT_COLLATION_NAME : '' };
return {
status: 'success',
response: collation.rows.length
? collation.rows[0].DEFAULT_COLLATION_NAME
: ''
};
}
catch (err) {
return { status: 'error', response: err.toString() };
@ -48,7 +63,9 @@ export default connections => {
ipcMain.handle('get-structure', async (event, params) => {
try {
const structure = await connections[params.uid].getStructure(params.schemas);
const structure = await connections[params.uid].getStructure(
params.schemas
);
return { status: 'success', response: structure };
}
@ -155,6 +172,169 @@ export default connections => {
}
});
ipcMain.handle('export', (event, { uid, type, tables, ...rest }) => {
if (exporter !== null) return;
return new Promise((resolve, reject) => {
(async () => {
if (fs.existsSync(rest.outputFile)) { // If file exists ask for replace
const result = await dialog.showMessageBox({
type: 'warning',
message: `File ${rest.outputFile} already exists. Do you want to replace it?`,
detail: 'A file with the same name already exists in the target folder. Replacing it will overwrite its current contents.',
buttons: ['Cancel', 'Replace'],
defaultId: 0,
cancelId: 0
});
if (result.response !== 1) {
resolve({
type: 'error',
response: 'Operation aborted'
});
return;
}
}
// Init exporter process
exporter = fork(isDevelopment ? './dist/exporter.js' : path.resolve(__dirname, './exporter.js'), [], {
execArgv: isDevelopment ? ['--inspect=9224'] : undefined
});
exporter.send({
type: 'init',
client: {
name: type,
config: await connections[uid].getDbConfig()
},
tables,
options: rest
});
// Exporter message listener
exporter.on('message', ({ type, payload }) => {
switch (type) {
case 'export-progress':
event.sender.send('export-progress', payload);
break;
case 'end':
setTimeout(() => { // Ensures that writing process has finished
exporter.kill();
exporter = null;
}, 2000);
resolve({ status: 'success', response: payload });
break;
case 'cancel':
exporter.kill();
exporter = null;
resolve({ status: 'error', response: 'Operation cancelled' });
break;
case 'error':
exporter.kill();
exporter = null;
resolve({ status: 'error', response: payload });
break;
}
});
exporter.on('exit', code => {
exporter = null;
resolve({ status: 'error', response: `Operation ended with code: ${code}` });
});
})();
});
});
ipcMain.handle('abort-export', async event => {
let willAbort = false;
if (exporter) {
const result = await dialog.showMessageBox({
type: 'warning',
message: 'Are you sure you want to abort the export',
buttons: ['Cancel', 'Abort'],
defaultId: 0,
cancelId: 0
});
if (result.response === 1) {
willAbort = true;
exporter.send({ type: 'cancel' });
}
}
return { status: 'success', response: { willAbort } };
});
ipcMain.handle('import-sql', async (event, options) => {
if (importer !== null) return;
return new Promise((resolve, reject) => {
(async () => {
const dbConfig = await connections[options.uid].getDbConfig();
// Init importer process
importer = fork(isDevelopment ? './dist/importer.js' : path.resolve(__dirname, './importer.js'), [], {
execArgv: isDevelopment ? ['--inspect=9224'] : undefined
});
importer.send({
type: 'init',
dbConfig,
options
});
// Importer message listener
importer.on('message', ({ type, payload }) => {
switch (type) {
case 'import-progress':
event.sender.send('import-progress', payload);
break;
case 'query-error':
event.sender.send('query-error', payload);
break;
case 'end':
setTimeout(() => { // Ensures that writing process has finished
importer?.kill();
importer = null;
}, 2000);
resolve({ status: 'success', response: payload });
break;
case 'cancel':
importer.kill();
importer = null;
resolve({ status: 'error', response: 'Operation cancelled' });
break;
case 'error':
importer.kill();
importer = null;
resolve({ status: 'error', response: payload });
break;
}
});
})();
});
});
ipcMain.handle('abort-import-sql', async event => {
let willAbort = false;
if (importer) {
const result = await dialog.showMessageBox({
type: 'warning',
message: 'Are you sure you want to abort the import',
buttons: ['Cancel', 'Abort'],
defaultId: 0,
cancelId: 0
});
if (result.response === 1) {
willAbort = true;
importer.send({ type: 'cancel' });
}
}
return { status: 'success', response: { willAbort } };
});
ipcMain.handle('kill-tab-query', async (event, { uid, tabUid }) => {
if (!tabUid) return;

View File

@ -149,7 +149,7 @@ export default (connections) => {
}
}
}
else if ([...BIT].includes(params.type)) {
else if (BIT.includes(params.type)) {
escapedParam = `b'${sqlEscaper(params.content)}'`;
reload = true;
}

View File

@ -1,4 +1,10 @@
'use strict';
const queryLogger = sql => {
// Remove comments, newlines and multiple spaces
const escapedSql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '').replace(/\s\s+/g, ' ');
console.log(escapedSql);
};
/**
* As Simple As Possible Query Builder Core
*
@ -17,7 +23,7 @@ export class AntaresCore {
this._poolSize = args.poolSize || false;
this._connection = null;
this._ssh = null;
this._logger = args.logger || console.log;
this._logger = args.logger || queryLogger;
this._queryDefaults = {
schema: '',

View File

@ -2,13 +2,6 @@
import { MySQLClient } from './clients/MySQLClient';
import { PostgreSQLClient } from './clients/PostgreSQLClient';
import { SQLiteClient } from './clients/SQLiteClient';
const queryLogger = sql => {
// Remove comments, newlines and multiple spaces
const escapedSql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '').replace(/\s\s+/g, ' ');
console.log(escapedSql);
};
export class ClientsFactory {
/**
* Returns a database connection based on received args.
@ -29,9 +22,7 @@ export class ClientsFactory {
* @returns Database Connection
* @memberof ClientsFactory
*/
static getConnection (args) {
args.logger = queryLogger;
static getClient (args) {
switch (args.client) {
case 'mysql':
case 'maria':

View File

@ -101,6 +101,26 @@ export class MySQLClient extends AntaresCore {
.filter(_type => _type.name === type.toUpperCase())[0];
}
_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;
}
}
}
/**
*
* @returns dbConfig
@ -498,6 +518,7 @@ export class MySQLClient extends AntaresCore {
charset: field.CHARACTER_SET_NAME,
collation: field.COLLATION_NAME,
autoIncrement: field.EXTRA.includes('auto_increment'),
generated: field.EXTRA.toLowerCase().includes('generated'),
onUpdate: field.EXTRA.toLowerCase().includes('on update')
? field.EXTRA.substr(field.EXTRA.indexOf('on update') + 9, field.EXTRA.length).trim()
: '',
@ -1602,7 +1623,7 @@ export class MySQLClient extends AntaresCore {
let insertRaw = '';
if (this._query.insert.length) {
const fieldsList = Object.keys(this._query.insert[0]);
const fieldsList = Object.keys(this._query.insert[0]).map(col => '`' + col + '`');
const rowsList = this._query.insert.map(el => `(${Object.values(el).join(', ')})`);
insertRaw = `(${fieldsList.join(', ')}) VALUES ${rowsList.join(', ')} `;

View File

@ -0,0 +1,77 @@
import fs from 'fs';
import path from 'path';
import EventEmitter from 'events';
export class BaseExporter extends EventEmitter {
constructor (tables, options) {
super();
this._tables = tables;
this._options = options;
this._isCancelled = false;
this._outputStream = fs.createWriteStream(this._options.outputFile, {
flags: 'w'
});
this._state = {};
this._outputStream.once('error', err => {
this._isCancelled = true;
this.emit('error', err);
});
}
async run () {
try {
this.emit('start', this);
await this.dump();
}
catch (err) {
this.emit('error', err);
throw err;
}
finally {
this._outputStream.end();
this.emit('end');
}
}
get isCancelled () {
return this._isCancelled;
}
get outputFile () {
return this._options.outputFile;
}
outputFileExists () {
return fs.existsSync(this._options.outputFile);
}
cancel () {
this._isCancelled = true;
this.emit('cancel');
this.emitUpdate({ op: 'cancelling' });
}
emitUpdate (state) {
this.emit('progress', { ...this._state, ...state });
}
writeString (data) {
if (this._isCancelled) return;
try {
fs.accessSync(this._options.outputFile);
}
catch (err) {
this._isCancelled = true;
const fileName = path.basename(this._options.outputFile);
this.emit('error', `The file ${fileName} is not accessible`);
}
this._outputStream.write(data);
}
dump () {
throw new Error('Exporter must implement the "dump" method');
}
}

View File

@ -0,0 +1,412 @@
import { SqlExporter } from './SqlExporter';
import { BLOB, BIT, DATE, DATETIME, FLOAT, SPATIAL, IS_MULTI_SPATIAL, NUMBER } from 'common/fieldTypes';
import hexToBinary from 'common/libs/hexToBinary';
import { getArrayDepth } from 'common/libs/getArrayDepth';
import moment from 'moment';
import { lineString, point, polygon } from '@turf/helpers';
export default class MysqlExporter extends SqlExporter {
async getSqlHeader () {
let dump = await super.getSqlHeader();
dump += `
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
SET NAMES utf8mb4;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;`;
return dump;
}
async getFooter () {
const footer = await super.getFooter();
return `/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
${footer}
`;
}
async getCreateTable (tableName) {
const { rows } = await this._client.raw(
`SHOW CREATE TABLE \`${this.schemaName}\`.\`${tableName}\``
);
if (rows.length !== 1) return '';
const col = 'Create View' in rows[0] ? 'Create View' : 'Create Table';
return rows[0][col] + ';';
}
getDropTable (tableName) {
return `DROP TABLE IF EXISTS \`${tableName}\`;`;
}
async * getTableInsert (tableName) {
let rowCount = 0;
let sqlStr = '';
const countResults = await this._client.raw(`SELECT COUNT(1) as count FROM \`${this.schemaName}\`.\`${tableName}\``);
if (countResults.rows.length === 1) rowCount = countResults.rows[0].count;
if (rowCount > 0) {
let queryLength = 0;
let rowsWritten = 0;
let rowIndex = 0;
const { sqlInsertDivider, sqlInsertAfter } = this._options;
const columns = await this._client.getTableColumns({
table: tableName,
schema: this.schemaName
});
const notGeneratedColumns = columns.filter(col => !col.generated);
const columnNames = notGeneratedColumns.map(col => '`' + col.name + '`');
const insertStmt = `INSERT INTO \`${tableName}\` (${columnNames.join(
', '
)}) VALUES`;
sqlStr += `LOCK TABLES \`${tableName}\` WRITE;\n`;
sqlStr += `/*!40000 ALTER TABLE \`${tableName}\` DISABLE KEYS */;`;
sqlStr += '\n\n';
yield sqlStr;
yield insertStmt;
const stream = await this._queryStream(
`SELECT ${columnNames.join(', ')} FROM \`${this.schemaName}\`.\`${tableName}\``
);
for await (const row of stream) {
if (this.isCancelled) {
stream.destroy();
yield null;
return;
}
let sqlInsertString = '';
if (
(sqlInsertDivider === 'bytes' && queryLength >= sqlInsertAfter * 1024) ||
(sqlInsertDivider === 'rows' && rowsWritten === sqlInsertAfter)
) {
sqlInsertString += `;\n${insertStmt}\n\t(`;
queryLength = 0;
rowsWritten = 0;
}
else if (parseInt(rowIndex) === 0) sqlInsertString += '\n\t(';
else sqlInsertString += ',\n\t(';
for (const i in notGeneratedColumns) {
const column = notGeneratedColumns[i];
const val = row[column.name];
if (val === null) sqlInsertString += 'NULL';
else if (DATE.includes(column.type)) {
sqlInsertString += moment(val).isValid()
? this.escapeAndQuote(moment(val).format('YYYY-MM-DD'))
: val;
}
else if (DATETIME.includes(column.type)) {
let datePrecision = '';
for (let i = 0; i < column.precision; i++)
datePrecision += i === 0 ? '.S' : 'S';
sqlInsertString += moment(val).isValid()
? this.escapeAndQuote(moment(val).format(`YYYY-MM-DD HH:mm:ss${datePrecision}`))
: this.escapeAndQuote(val);
}
else if (BIT.includes(column.type))
sqlInsertString += `b'${hexToBinary(Buffer.from(val).toString('hex'))}'`;
else if (BLOB.includes(column.type))
sqlInsertString += `X'${val.toString('hex').toUpperCase()}'`;
else if (NUMBER.includes(column.type))
sqlInsertString += val;
else if (FLOAT.includes(column.type))
sqlInsertString += parseFloat(val);
else if (SPATIAL.includes(column.type)) {
let geoJson;
if (IS_MULTI_SPATIAL.includes(column.type)) {
const features = [];
for (const element of val)
features.push(this.getMarkers(element));
geoJson = {
type: 'FeatureCollection',
features
};
}
else
geoJson = this._getGeoJSON(val);
sqlInsertString += `ST_GeomFromGeoJSON('${JSON.stringify(geoJson)}')`;
}
else if (val === '') sqlInsertString += '\'\'';
else {
sqlInsertString += typeof val === 'string'
? this.escapeAndQuote(val)
: typeof val === 'object'
? this.escapeAndQuote(JSON.stringify(val))
: val;
}
if (parseInt(i) !== notGeneratedColumns.length - 1)
sqlInsertString += ', ';
}
sqlInsertString += ')';
queryLength += sqlInsertString.length;
rowsWritten++;
rowIndex++;
yield sqlInsertString;
}
sqlStr = ';\n\n';
sqlStr += `/*!40000 ALTER TABLE \`${tableName}\` ENABLE KEYS */;\n`;
sqlStr += 'UNLOCK TABLES;';
yield sqlStr;
}
}
async getViews () {
const { rows: views } = await this._client.raw(
`SHOW TABLE STATUS FROM \`${this.schemaName}\` WHERE Comment = 'VIEW'`
);
let sqlString = '';
for (const view of views) {
sqlString += `DROP VIEW IF EXISTS \`${view.Name}\`;\n`;
const viewSyntax = await this.getCreateTable(view.Name);
sqlString += viewSyntax.replaceAll('`' + this.schemaName + '`.', '');
sqlString += '\n';
}
return sqlString;
}
async getTriggers () {
const { rows: triggers } = await this._client.raw(
`SHOW TRIGGERS FROM \`${this.schemaName}\``
);
const generatedTables = this._tables
.filter(t => t.includeStructure)
.map(t => t.table);
let sqlString = '';
for (const trigger of triggers) {
const {
Trigger: name,
Timing: timing,
Event: event,
Table: table,
Statement: statement,
sql_mode: sqlMode
} = trigger;
if (!generatedTables.includes(table)) continue;
const definer = this.getEscapedDefiner(trigger.Definer);
sqlString += '/*!50003 SET @OLD_SQL_MODE=@@SQL_MODE*/;;\n';
sqlString += `/*!50003 SET SQL_MODE="${sqlMode}" */;\n`;
sqlString += 'DELIMITER ;;\n';
sqlString += '/*!50003 CREATE*/ ';
sqlString += `/*!50017 DEFINER=${definer}*/ `;
sqlString += `/*!50003 TRIGGER \`${name}\` ${timing} ${event} ON \`${table}\` FOR EACH ROW ${statement}*/;;\n`;
sqlString += 'DELIMITER ;\n';
sqlString += '/*!50003 SET SQL_MODE=@OLD_SQL_MODE */;\n\n';
}
return sqlString;
}
async getSchedulers () {
const { rows: schedulers } = await this._client.raw(
`SELECT *, EVENT_SCHEMA AS \`Db\`, EVENT_NAME AS \`Name\` FROM information_schema.\`EVENTS\` WHERE EVENT_SCHEMA = '${this.schemaName}'`
);
let sqlString = '';
for (const scheduler of schedulers) {
const {
EVENT_NAME: name,
SQL_MODE: sqlMode,
EVENT_TYPE: type,
INTERVAL_VALUE: intervalValue,
INTERVAL_FIELD: intervalField,
STARTS: starts,
ENDS: ends,
EXECUTE_AT: at,
ON_COMPLETION: onCompletion,
STATUS: status,
EVENT_DEFINITION: definition
} = scheduler;
const definer = this.getEscapedDefiner(scheduler.DEFINER);
const comment = this.escapeAndQuote(scheduler.EVENT_COMMENT);
sqlString += `/*!50106 DROP EVENT IF EXISTS \`${name}\` */;\n`;
sqlString += '/*!50003 SET @OLD_SQL_MODE=@@SQL_MODE*/;;\n';
sqlString += `/*!50003 SET SQL_MODE='${sqlMode}' */;\n`;
sqlString += 'DELIMITER ;;\n';
sqlString += '/*!50106 CREATE*/ ';
sqlString += `/*!50117 DEFINER=${definer}*/ `;
sqlString += `/*!50106 EVENT \`${name}\` ON SCHEDULE `;
if (type === 'RECURRING') {
sqlString += `EVERY ${intervalValue} ${intervalField} STARTS '${starts}' `;
if (ends) sqlString += `ENDS '${ends}' `;
}
else sqlString += `AT '${at}' `;
sqlString += `ON COMPLETION ${onCompletion} ${
status === 'disabled' ? 'DISABLE' : 'ENABLE'
} COMMENT ${comment || '\'\''} DO ${definition}*/;;\n`;
sqlString += 'DELIMITER ;\n';
sqlString += '/*!50003 SET SQL_MODE=@OLD_SQL_MODE*/;;\n';
}
return sqlString;
}
async getFunctions () {
const { rows: functions } = await this._client.raw(
`SHOW FUNCTION STATUS WHERE \`Db\` = '${this.schemaName}';`
);
let sqlString = '';
for (const func of functions) {
const definer = this.getEscapedDefiner(func.Definer);
sqlString += await this.getRoutineSyntax(
func.Name,
func.Type,
definer
);
}
return sqlString;
}
async getRoutines () {
const { rows: routines } = await this._client.raw(
`SHOW PROCEDURE STATUS WHERE \`Db\` = '${this.schemaName}';`
);
let sqlString = '';
for (const routine of routines) {
const definer = this.getEscapedDefiner(routine.Definer);
sqlString += await this.getRoutineSyntax(
routine.Name,
routine.Type,
definer
);
}
return sqlString;
}
async getRoutineSyntax (name, type, definer) {
const { rows: routines } = await this._client.raw(
`SHOW CREATE ${type} \`${this.schemaName}\`.\`${name}\``
);
if (routines.length === 0) return '';
const routine = routines[0];
const fieldName = `Create ${type === 'PROCEDURE' ? 'Procedure' : 'Function'}`;
const sqlMode = routine.sql_mode;
const createProcedure = routine[fieldName];
let sqlString = '';
if (createProcedure) { // If procedure body not empty
const startOffset = createProcedure.indexOf(type);
const procedureBody = createProcedure.substring(startOffset);
sqlString += `/*!50003 DROP ${type} IF EXISTS ${name}*/;;\n`;
sqlString += '/*!50003 SET @OLD_SQL_MODE=@@SQL_MODE*/;;\n';
sqlString += `/*!50003 SET SQL_MODE="${sqlMode}"*/;;\n`;
sqlString += 'DELIMITER ;;\n';
sqlString += `/*!50003 CREATE*/ /*!50020 DEFINER=${definer}*/ /*!50003 ${procedureBody}*/;;\n`;
sqlString += 'DELIMITER ;\n';
sqlString += '/*!50003 SET SQL_MODE=@OLD_SQL_MODE*/;\n';
}
return sqlString;
}
async _queryStream (sql) {
if (process.env.NODE_ENV === 'development') console.log('EXPORTER:', sql);
const isPool = typeof this._client._connection.getConnection === 'function';
const connection = isPool ? await this._client._connection.getConnection() : this._client._connection;
const stream = connection.connection.query(sql).stream();
const dispose = () => connection.destroy();
stream.on('end', dispose);
stream.on('error', dispose);
stream.on('close', dispose);
return stream;
}
getEscapedDefiner (definer) {
return definer
.split('@')
.map(part => '`' + part + '`')
.join('@');
}
escapeAndQuote (val) {
// eslint-disable-next-line no-control-regex
const CHARS_TO_ESCAPE = /[\0\b\t\n\r\x1a"'\\]/g;
const CHARS_ESCAPE_MAP = {
'\0': '\\0',
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\r': '\\r',
'\x1a': '\\Z',
'"': '\\"',
'\'': '\\\'',
'\\': '\\\\'
};
let chunkIndex = CHARS_TO_ESCAPE.lastIndex = 0;
let escapedVal = '';
let match;
while ((match = CHARS_TO_ESCAPE.exec(val))) {
escapedVal += val.slice(chunkIndex, match.index) + CHARS_ESCAPE_MAP[match[0]];
chunkIndex = CHARS_TO_ESCAPE.lastIndex;
}
if (chunkIndex === 0)
return `'${val}'`;
if (chunkIndex < val.length)
return `'${escapedVal + val.slice(chunkIndex)}'`;
return `'${escapedVal}'`;
}
_getGeoJSON (val) {
if (Array.isArray(val)) {
if (getArrayDepth(val) === 1)
return lineString(val.reduce((acc, curr) => [...acc, [curr.x, curr.y]], []));
else
return polygon(val.map(arr => arr.reduce((acc, curr) => [...acc, [curr.x, curr.y]], [])));
}
else
return point([val.x, val.y]);
}
}

View File

@ -0,0 +1,162 @@
import moment from 'moment';
import { BaseExporter } from '../BaseExporter';
export class SqlExporter extends BaseExporter {
constructor (client, tables, options) {
super(tables, options);
this._client = client;
this._commentChar = '#';
}
get schemaName () {
return this._options.schema;
}
get host () {
return this._client._params.host;
}
async getServerVersion () {
const version = await this._client.getVersion();
return `${version.name} ${version.number}`;
}
async dump () {
const { includes } = this._options;
const extraItems = Object.keys(includes).filter(key => includes[key]);
const totalTableToProcess = this._tables.filter(
t => t.includeStructure || t.includeContent || t.includeDropStatement
).length;
const processingItemCount = totalTableToProcess + extraItems.length;
const exportState = {
totalItems: processingItemCount,
currentItemIndex: 0,
currentItem: '',
op: ''
};
const header = await this.getSqlHeader();
this.writeString(header);
this.writeString('\n\n\n');
for (const item of this._tables) {
// user abort operation
if (this.isCancelled) return;
// skip item if not set to output any detail for them
if (
!item.includeStructure &&
!item.includeContent &&
!item.includeDropStatement
)
continue;
exportState.currentItemIndex++;
exportState.currentItem = item.table;
exportState.op = 'FETCH';
this.emitUpdate(exportState);
const tableHeader = this.buildComment(
`Dump of table ${item.table}\n------------------------------------------------------------`
);
this.writeString(tableHeader);
this.writeString('\n\n');
if (item.includeDropStatement) {
const dropTableSyntax = this.getDropTable(item.table);
this.writeString(dropTableSyntax);
this.writeString('\n\n');
}
if (item.includeStructure) {
const createTableSyntax = await this.getCreateTable(item.table);
this.writeString(createTableSyntax);
this.writeString('\n\n');
}
if (item.includeContent) {
exportState.op = 'WRITE';
this.emitUpdate(exportState);
for await (const sqlStr of this.getTableInsert(item.table)) {
if (this.isCancelled) return;
this.writeString(sqlStr);
}
this.writeString('\n\n');
}
this.writeString('\n\n');
}
for (const item of extraItems) {
const processingMethod = `get${item.charAt(0).toUpperCase() + item.slice(1)}`;
exportState.currentItemIndex++;
exportState.currentItem = item;
exportState.op = 'PROCESSING';
this.emitUpdate(exportState);
if (this[processingMethod]) {
const data = await this[processingMethod]();
if (data !== '') {
const header =
this.buildComment(
`Dump of ${item}\n------------------------------------------------------------`
) + '\n\n';
this.writeString(header);
this.writeString(data);
this.writeString('\n\n');
}
}
}
const footer = await this.getFooter();
this.writeString(footer);
}
buildComment (text) {
return text
.split('\n')
.map(txt => `${this._commentChar} ${txt}`)
.join('\n');
}
async getSqlHeader () {
const serverVersion = await this.getServerVersion();
const header = `************************************************************
Antares - SQL Client
Version ${process.env.PACKAGE_VERSION}
https://antares-sql.app/
https://github.com/Fabio286/antares
Host: ${this.host} (${serverVersion})
Database: ${this.schemaName}
Generation time: ${moment().format()}
************************************************************`;
return this.buildComment(header);
}
async getFooter () {
return this.buildComment(`Dump completed on ${moment().format()}`);
}
getCreateTable (tableName) {
throw new Error(
'Sql Exporter must implement the "getCreateTable" method'
);
}
getDropTable (tableName) {
throw new Error('Sql Exporter must implement the "getDropTable" method');
}
getTableInsert (tableName) {
throw new Error(
'Sql Exporter must implement the "getTableInsert" method'
);
}
}

View File

@ -0,0 +1,53 @@
import fs from 'fs';
import EventEmitter from 'events';
export class BaseImporter extends EventEmitter {
constructor (options) {
super();
this._options = options;
this._isCancelled = false;
this._fileHandler = fs.createReadStream(this._options.file, {
flags: 'r',
highWaterMark: 4 * 1024
});
this._state = {};
this._fileHandler.once('error', err => {
this._isCancelled = true;
this.emit('error', err);
});
}
async run () {
try {
this.emit('start', this);
await this.import();
}
catch (err) {
this.emit('error', err);
throw err;
}
finally {
this._fileHandler.close();
this.emit('end');
}
}
get isCancelled () {
return this._isCancelled;
}
cancel () {
this._isCancelled = true;
this.emit('cancel');
this.emitUpdate({ op: 'cancelling' });
}
emitUpdate (state) {
this.emit('progress', { ...this._state, ...state });
}
import () {
throw new Error('Exporter must implement the "import" method');
}
}

View File

@ -0,0 +1,85 @@
import fs from 'fs/promises';
import SqlParser from '../../../../common/libs/sqlParser';
import { BaseImporter } from '../BaseImporter';
export default class MysqlImporter extends BaseImporter {
constructor (client, options) {
super(options);
this._client = client;
}
async import () {
try {
const { size: totalFileSize } = await fs.stat(this._options.file);
const parser = new SqlParser();
let readPosition = 0;
let queryCount = 0;
this.emitUpdate({
fileSize: totalFileSize,
readPosition: 0,
percentage: 0,
queryCount: 0
});
// 1. detect file encoding
// 2. set fh encoding
// 3. detect sql mode
// 4. restore sql mode in case of exception
return new Promise((resolve, reject) => {
this._fileHandler.pipe(parser);
parser.on('error', reject);
parser.on('close', async () => {
console.log('TOTAL QUERIES', queryCount);
console.log('import end');
resolve();
});
parser.on('data', async (query) => {
queryCount++;
parser.pause();
try {
await this._client.query(query);
}
catch (error) {
this.emit('query-error', {
sql: query,
message: error.sqlMessage,
sqlSnippet: error.sql,
time: new Date().getTime()
});
}
this.emitUpdate({
queryCount,
readPosition,
percentage: readPosition / totalFileSize * 100
});
this._fileHandler.pipe(parser);
parser.resume();
});
parser.on('pause', () => {
this._fileHandler.unpipe(parser);
this._fileHandler.readableFlowing = false;
});
this._fileHandler.on('data', (chunk) => {
readPosition += chunk.length;
});
this._fileHandler.on('error', (err) => {
console.log(err);
reject(err);
});
});
}
catch (err) {
console.log(err);
}
}
}

View File

@ -0,0 +1,60 @@
import { ClientsFactory } from '../libs/ClientsFactory';
import MysqlExporter from '../libs/exporters/sql/MysqlExporter.js';
import fs from 'fs';
let exporter;
process.on('message', async ({ type, client, tables, options }) => {
if (type === 'init') {
const connection = await ClientsFactory.getClient({
client: client.name,
params: client.config,
poolSize: 5
});
await connection.connect();
switch (client.name) {
case 'mysql':
case 'maria':
exporter = new MysqlExporter(connection, tables, options);
break;
default:
process.send({
type: 'error',
payload: `"${client.name}" exporter not aviable`
});
return;
}
exporter.once('error', err => {
console.error(err);
process.send({
type: 'error',
payload: err.toString()
});
});
exporter.once('end', () => {
process.send({
type: 'end',
payload: { cancelled: exporter.isCancelled }
});
connection.destroy();
});
exporter.once('cancel', () => {
fs.unlinkSync(exporter.outputFile);
process.send({ type: 'cancel' });
});
exporter.on('progress', state => {
process.send({
type: 'export-progress',
payload: state
});
});
exporter.run();
}
else if (type === 'cancel')
exporter.cancel();
});

View File

@ -0,0 +1,68 @@
import { ClientsFactory } from '../libs/ClientsFactory';
import MysqlImporter from '../libs/importers/sql/MysqlImporter';
let importer;
process.on('message', async ({ type, dbConfig, options }) => {
if (type === 'init') {
const connection = await ClientsFactory.getClient({
client: options.type,
params: {
...dbConfig,
schema: options.schema
},
poolSize: 1
});
const pool = await connection.getConnectionPool();
switch (options.type) {
case 'mysql':
case 'maria':
importer = new MysqlImporter(pool, options);
break;
default:
process.send({
type: 'error',
payload: `"${options.type}" importer not aviable`
});
return;
}
importer.once('error', err => {
console.error(err);
process.send({
type: 'error',
payload: err.toString()
});
});
importer.once('end', () => {
process.send({
type: 'end',
payload: { cancelled: importer.isCancelled }
});
});
importer.once('cancel', () => {
process.send({ type: 'cancel' });
});
importer.on('progress', state => {
process.send({
type: 'import-progress',
payload: state
});
});
importer.on('query-error', state => {
process.send({
type: 'query-error',
payload: state
});
});
importer.run();
}
else if (type === 'cancel')
importer.cancel();
});

View File

@ -110,5 +110,4 @@ export default {
.modal.modal-sm .modal-container {
padding: 0;
}
</style>

View File

@ -0,0 +1,491 @@
<template>
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-database-arrow-down mr-1" />
<span class="cut-text">{{ $t('message.exportSchema') }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="modal-body pb-0">
<div class="container">
<div class="columns">
<div class="col-3">
<label class="form-label">{{ $t('message.directoryPath') }}</label>
</div>
<div class="col-9">
<fieldset class="input-group">
<input
v-model="basePath"
class="form-input"
type="text"
required
readonly
:placeholder="$t('message.schemaName')"
>
<button
type="button"
class="btn btn-primary input-group-btn"
@click.prevent="openPathDialog"
>
{{ $t('word.change') }}
</button>
</fieldset>
</div>
</div>
</div>
<div class="columns export-options">
<div class="column col-8 left">
<div class="columns mb-2">
<div class="column col-auto d-flex text-italic ">
<i class="mdi mdi-file-document-outline mr-2" />
{{ filename }}
</div>
<div class="column col-auto col-ml-auto ">
<button
class="btn btn-dark btn-sm"
:title="$t('word.refresh')"
@click="refresh"
>
<i class="mdi mdi-database-refresh" />
</button>
<button
class="btn btn-dark btn-sm"
:title="$t('message.uncheckAllTables')"
:disabled="isRefreshing"
@click="uncheckAllTables"
>
<i class="mdi mdi-file-tree-outline" />
</button>
<button
class="btn btn-dark btn-sm"
:title="$t('message.checkAllTables')"
:disabled="isRefreshing"
@click="checkAllTables"
>
<i class="mdi mdi-file-tree" />
</button>
</div>
</div>
<div class="workspace-query-results">
<div ref="table" class="table table-hover">
<div class="thead">
<div class="tr text-center">
<div class="th no-border" style="width: 50%;" />
<div class="th no-border">
<label
class="form-checkbox m-0 px-2 form-inline"
@click.prevent="toggleAllTablesOption('includeStructure')"
>
<input
type="checkbox"
:indeterminate.prop="includeStructureStatus === 2"
:checked.prop="!!includeStructureStatus"
>
<i class="form-icon" />
</label>
</div>
<div class="th no-border">
<label
class="form-checkbox m-0 px-2 form-inline"
@click.prevent="toggleAllTablesOption('includeContent')"
>
<input
type="checkbox"
:indeterminate.prop="includeContentStatus === 2"
:checked.prop="!!includeContentStatus"
>
<i class="form-icon" />
</label>
</div>
<div class="th no-border">
<label
class="form-checkbox m-0 px-2 form-inline"
@click.prevent="toggleAllTablesOption('includeDropStatement')"
>
<input
type="checkbox"
:indeterminate.prop="includeDropStatementStatus === 2"
:checked.prop="!!includeDropStatementStatus"
>
<i class="form-icon" />
</label>
</div>
</div>
<div class="tr">
<div class="th" style="width: 50%;">
<div class="table-column-title">
<span>{{ $t('word.table') }}</span>
</div>
</div>
<div class="th text-center">
<div class="table-column-title">
<span>{{ $t('word.structure') }}</span>
</div>
</div>
<div class="th text-center">
<div class="table-column-title">
<span>{{ $t('word.content') }}</span>
</div>
</div>
<div class="th text-center">
<div class="table-column-title">
<span>{{ $t('word.drop') }}</span>
</div>
</div>
</div>
</div>
<div class="tbody">
<div
v-for="item in tables"
:key="item.name"
class="tr"
>
<div class="td">
{{ item.table }}
</div>
<div class="td text-center">
<label class="form-checkbox m-0 px-2 form-inline">
<input
v-model="item.includeStructure"
type="checkbox"
><i class="form-icon" />
</label>
</div>
<div class="td text-center">
<label class="form-checkbox m-0 px-2 form-inline">
<input
v-model="item.includeContent"
type="checkbox"
><i class="form-icon" />
</label>
</div>
<div class="td text-center">
<label class="form-checkbox m-0 px-2 form-inline">
<input
v-model="item.includeDropStatement"
type="checkbox"
><i class="form-icon" />
</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="column col-4">
<h4>
{{ $t('word.options') }}
</h4>
<span>{{ $t('word.includes') }}:</span>
<label
v-for="(_, key) in options.includes"
:key="key"
class="form-checkbox"
>
<input v-model="options.includes[key]" type="checkbox"><i class="form-icon" /> {{ $t(`word.${key}`) }}
</label>
<div class="mt-4 mb-2">
{{ $t('message.newInserStmtEvery') }}:
</div>
<div class="columns">
<div class="column col-6">
<input
v-model.number="options.sqlInsertAfter"
type="number"
class="form-input"
value="250"
>
</div>
<div class="column col-6">
<select v-model="options.sqlInsertDivider" class="form-select">
<option value="bytes">
KiB
</option>
<option value="rows">
{{ $tc('word.row', 2) }}
</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer columns">
<div class="column col modal-progress-wrapper text-left">
<div v-if="progressPercentage > 0" class="export-progress">
<span class="progress-status">
{{ progressPercentage }}% - {{ progressStatus }}
</span>
<progress
class="progress d-block"
:value="progressPercentage"
max="100"
/>
</div>
</div>
<div class="column col-auto px-0">
<button class="btn btn-link" @click.stop="closeModal">
{{ $t('word.close') }}
</button>
<button
class="btn btn-primary mr-2"
:class="{'loading': isExporting}"
:disabled="isExporting || isRefreshing"
autofocus
@click.prevent="startExport"
>
{{ $t('word.export') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { ipcRenderer } from 'electron';
import { mapActions, mapGetters } from 'vuex';
import moment from 'moment';
import customizations from 'common/customizations';
import Application from '@/ipc-api/Application';
import Schema from '@/ipc-api/Schema';
export default {
name: 'ModalExportSchema',
props: {
selectedSchema: String
},
data () {
return {
isExporting: false,
isRefreshing: false,
progressPercentage: 0,
progressStatus: '',
tables: [],
options: {
includes: {},
sqlInsertAfter: 250,
sqlInsertDivider: 'bytes'
},
basePath: ''
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace',
getDatabaseVariable: 'workspaces/getDatabaseVariable'
}),
currentWorkspace () {
return this.getWorkspace(this.selectedWorkspace);
},
schemaItems () {
const db = this.currentWorkspace.structure.find(db => db.name === this.selectedSchema);
if (db)
return db.tables.filter(table => table.type === 'table');
return [];
},
filename () {
const date = moment().format('YYYY-MM-DD');
return `${this.selectedSchema}_${date}.sql`;
},
dumpFilePath () {
return `${this.basePath}/${this.filename}`;
},
includeStructureStatus () {
if (this.tables.every(item => item.includeStructure)) return 1;
else if (this.tables.some(item => item.includeStructure)) return 2;
else return 0;
},
includeContentStatus () {
if (this.tables.every(item => item.includeContent)) return 1;
else if (this.tables.some(item => item.includeContent)) return 2;
else return 0;
},
includeDropStatementStatus () {
if (this.tables.every(item => item.includeDropStatement)) return 1;
else if (this.tables.some(item => item.includeDropStatement)) return 2;
else return 0;
}
},
async created () {
if (!this.schemaItems.length) await this.refresh();
window.addEventListener('keydown', this.onKey);
this.basePath = await Application.getDownloadPathDirectory();
this.tables = this.schemaItems.map(item => ({
table: item.name,
includeStructure: true,
includeContent: true,
includeDropStatement: true
}));
const structure = ['views', 'triggers', 'routines', 'functions', 'schedulers', 'triggerFunctions'];
structure.forEach(feat => {
const val = customizations[this.currentWorkspace.client][feat];
if (val)
this.$set(this.options.includes, feat, true);
});
ipcRenderer.on('export-progress', this.updateProgress);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
ipcRenderer.off('export-progress', this.updateProgress);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
refreshSchema: 'workspaces/refreshSchema'
}),
async startExport () {
this.isExporting = true;
const { uid, client } = this.currentWorkspace;
const params = {
uid,
type: client,
schema: this.selectedSchema,
outputFile: this.dumpFilePath,
tables: [...this.tables],
...this.options
};
try {
const { status, response } = await Schema.export(params);
if (status === 'success')
this.progressStatus = response.cancelled ? this.$t('word.aborted') : this.$t('word.completed');
else {
this.progressStatus = response;
this.addNotification({ status: 'error', message: response });
}
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isExporting = false;
},
updateProgress (event, state) {
this.progressPercentage = Number((state.currentItemIndex / state.totalItems * 100).toFixed(1));
switch (state.op) {
case 'PROCESSING':
this.progressStatus = this.$t('message.processingTableExport', { table: state.currentItem });
break;
case 'FETCH':
this.progressStatus = this.$t('message.fechingTableExport', { table: state.currentItem });
break;
case 'WRITE':
this.progressStatus = this.$t('message.writingTableExport', { table: state.currentItem });
break;
}
},
async closeModal () {
let willClose = true;
if (this.isExporting) {
willClose = false;
const { response } = await Schema.abortExport();
willClose = response.willAbort;
}
if (willClose)
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
},
checkAllTables () {
this.tables = this.tables.map(item => ({ ...item, includeStructure: true, includeContent: true, includeDropStatement: true }));
},
uncheckAllTables () {
this.tables = this.tables.map(item => ({ ...item, includeStructure: false, includeContent: false, includeDropStatement: false }));
},
toggleAllTablesOption (option) {
const options = ['includeStructure', 'includeContent', 'includeDropStatement'];
if (!options.includes(option)) return;
if (this[`${option}Status`] !== 1)
this.tables = this.tables.map(item => ({ ...item, [option]: true }));
else
this.tables = this.tables.map(item => ({ ...item, [option]: false }));
},
async refresh () {
this.isRefreshing = true;
await this.refreshSchema({ uid: this.currentWorkspace.uid, schema: this.selectedSchema });
this.isRefreshing = false;
},
async openPathDialog () {
const result = await Application.showOpenDialog({ properties: ['openDirectory'] });
if (result && !result.canceled)
this.basePath = result.filePaths[0];
}
}
};
</script>
<style lang="scss" scoped>
.export-options {
flex: 1;
overflow: hidden;
.left {
display: flex;
flex-direction: column;
flex: 1;
}
}
.workspace-query-results {
flex: 1 0 1px;
.table {
width: 100% !important;
}
.form-checkbox {
min-height: 0.8rem;
padding: 0;
.form-icon {
top: 0.1rem;
}
}
}
.modal {
.modal-container {
max-width: 800px;
}
.modal-body {
max-height: 60vh;
display: flex;
flex-direction: column;
}
.modal-footer {
display: flex;
}
}
.progress-status {
font-style: italic;
font-size: 80%;
}
</style>

View File

@ -0,0 +1,181 @@
<template>
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-database-arrow-up mr-1" />
<span class="cut-text">{{ $t('message.importSchema') }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="modal-body pb-0">
{{ sqlFile }}
<div v-if="queryErrors.length > 0" class="mt-2">
<label>{{ $tc('message.importQueryErrors', queryErrors.length) }}</label>
<textarea
v-model="formattedQueryErrors"
class="form-input"
rows="5"
readonly
/>
</div>
</div>
<div class="modal-footer columns">
<div class="column col modal-progress-wrapper text-left">
<div class="import-progress">
<span class="progress-status">
{{ progressPercentage }}% - {{ progressStatus }} - {{ $tc('message.executedQueries', queryCount) }}
</span>
<progress
class="progress d-block"
:value="progressPercentage"
max="100"
/>
</div>
</div>
<div class="column col-auto px-0">
<button class="btn btn-link" @click.stop="closeModal">
{{ completed ? $t('word.close') : $t('word.cancel') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { ipcRenderer } from 'electron';
import { mapActions, mapGetters } from 'vuex';
import moment from 'moment';
import Schema from '@/ipc-api/Schema';
export default {
name: 'ModalImportSchema',
props: {
selectedSchema: String
},
data () {
return {
sqlFile: '',
isImporting: false,
progressPercentage: 0,
queryCount: 0,
completed: false,
progressStatus: 'Reading',
queryErrors: []
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
currentWorkspace () {
return this.getWorkspace(this.selectedWorkspace);
},
formattedQueryErrors () {
return this.queryErrors.map(err =>
`Time: ${moment(err.time).format('HH:mm:ss.S')} (${err.time})\nError: ${err.message}`
).join('\n\n');
}
},
async created () {
window.addEventListener('keydown', this.onKey);
ipcRenderer.on('import-progress', this.updateProgress);
ipcRenderer.on('query-error', this.handleQueryError);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
ipcRenderer.off('import-progress', this.updateProgress);
ipcRenderer.off('query-error', this.handleQueryError);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
refreshSchema: 'workspaces/refreshSchema'
}),
async startImport (sqlFile) {
this.isImporting = true;
this.sqlFile = sqlFile;
const { uid, client } = this.currentWorkspace;
const params = {
uid,
type: client,
schema: this.selectedSchema,
file: sqlFile
};
try {
this.completed = false;
const { status, response } = await Schema.import(params);
if (status === 'success')
this.progressStatus = response.cancelled ? this.$t('word.aborted') : this.$t('word.completed');
else {
this.progressStatus = response;
this.addNotification({ status: 'error', message: response });
}
this.refreshSchema({ uid, schema: this.selectedSchema });
this.completed = true;
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isImporting = false;
},
updateProgress (event, state) {
this.progressPercentage = Number(state.percentage).toFixed(1);
this.queryCount = Number(state.queryCount);
},
handleQueryError (event, err) {
this.queryErrors.push(err);
},
async closeModal () {
let willClose = true;
if (this.isImporting) {
willClose = false;
const { response } = await Schema.abortImport();
willClose = response.willAbort;
}
if (willClose)
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
}
}
};
</script>
<style lang="scss" scoped>
.modal {
.modal-container {
max-width: 800px;
}
.modal-body {
max-height: 60vh;
display: flex;
flex-direction: column;
}
.modal-footer {
display: flex;
}
}
.progress-status {
font-style: italic;
font-size: 80%;
}
</style>

View File

@ -58,6 +58,20 @@
</div>
</div>
</div>
<div
v-if="workspace.customizations.schemaExport"
class="context-element"
@click="showExportSchemaModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-database-arrow-down text-light pr-1" /> {{ $t('word.export') }}</span>
</div>
<div
v-if="workspace.customizations.schemaImport"
class="context-element"
@click="initImport"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-database-arrow-up text-light pr-1" /> {{ $t('word.import') }}</span>
</div>
<div
v-if="workspace.customizations.schemaEdit"
class="context-element"
@ -95,6 +109,17 @@
:selected-schema="selectedSchema"
@close="hideEditModal"
/>
<ModalExportSchema
v-if="isExportSchemaModal"
:selected-schema="selectedSchema"
@close="hideExportSchemaModal"
/>
<ModalImportSchema
v-if="isImportSchemaModal"
ref="importModalRef"
:selected-schema="selectedSchema"
@close="hideImportSchemaModal"
/>
</BaseContextMenu>
</template>
@ -103,14 +128,19 @@ import { mapGetters, mapActions } from 'vuex';
import BaseContextMenu from '@/components/BaseContextMenu';
import ConfirmModal from '@/components/BaseConfirmModal';
import ModalEditSchema from '@/components/ModalEditSchema';
import ModalExportSchema from '@/components/ModalExportSchema';
import ModalImportSchema from '@/components/ModalImportSchema';
import Schema from '@/ipc-api/Schema';
import Application from '@/ipc-api/Application';
export default {
name: 'WorkspaceExploreBarSchemaContext',
components: {
BaseContextMenu,
ConfirmModal,
ModalEditSchema
ModalEditSchema,
ModalExportSchema,
ModalImportSchema
},
props: {
contextEvent: MouseEvent,
@ -119,7 +149,9 @@ export default {
data () {
return {
isDeleteModal: false,
isEditModal: false
isEditModal: false,
isExportSchemaModal: false,
isImportSchemaModal: false
};
},
computed: {
@ -170,6 +202,30 @@ export default {
this.isEditModal = false;
this.closeContext();
},
showExportSchemaModal () {
this.isExportSchemaModal = true;
},
hideExportSchemaModal () {
this.isExportSchemaModal = false;
this.closeContext();
},
showImportSchemaModal () {
this.isImportSchemaModal = true;
},
hideImportSchemaModal () {
this.isImportSchemaModal = false;
this.closeContext();
},
async initImport () {
const result = await Application.showOpenDialog({ properties: ['openFile'], filters: [{ name: 'SQL', extensions: ['sql'] }] });
if (result && !result.canceled) {
const file = result.filePaths[0];
this.showImportSchemaModal();
this.$nextTick(() => {
this.$refs.importModalRef.startImport(file);
});
}
},
closeContext () {
this.$emit('close-context');
},

View File

@ -80,6 +80,7 @@ module.exports = {
deterministic: 'Deterministic',
context: 'Context',
export: 'Export',
import: 'Import',
returns: 'Returns',
timing: 'Timing',
state: 'State',
@ -122,6 +123,16 @@ module.exports = {
select: 'Select',
passphrase: 'Passphrase',
filter: 'Filter',
change: 'Change',
views: 'Views',
triggers: 'Triggers',
routines: 'Routines',
functions: 'Functions',
schedulers: 'Schedulers',
includes: 'Includes',
drop: 'Drop',
completed: 'Completed',
aborted: 'Aborted',
disabled: 'Disabled',
enable: 'Enable',
disable: 'Disable',
@ -254,6 +265,15 @@ module.exports = {
searchForQueries: 'Search for queries',
killProcess: 'Kill process',
closeTab: 'Close tab',
exportSchema: 'Export schema',
importSchema: 'Import schema',
directoryPath: 'Directory path',
newInserStmtEvery: 'New INSERT statement every',
processingTableExport: 'Processing {table}',
fechingTableExport: 'Fetching {table} data',
writingTableExport: 'Writing {table} data',
checkAllTables: 'Check all tables',
uncheckAllTables: 'Uncheck all tables',
goToDownloadPage: 'Go to download page',
readOnlyMode: 'Read-only mode',
killQuery: 'Kill query',
@ -261,7 +281,9 @@ module.exports = {
commitMode: 'Commit mode',
autoCommit: 'Auto commit',
manualCommit: 'Manual commit',
actionSuccessful: '{action} successful'
actionSuccessful: '{action} successful',
importQueryErrors: 'Warning: {n} error has accurrend | Warning: {n} errors occurred',
executedQueries: '{n} query executed | {n} queries executed'
},
faker: {
address: 'Address',

View File

@ -80,6 +80,7 @@ module.exports = {
deterministic: 'Deterministico',
context: 'Contesto',
export: 'Esporta',
import: 'Importa',
returns: 'Ritorna',
timing: 'Temporizzazione',
state: 'Stato',
@ -122,6 +123,16 @@ module.exports = {
select: 'Seleziona',
passphrase: 'Passphrase',
filter: 'Filtra',
change: 'Cambia',
views: 'Viste',
triggers: 'Trigger',
routines: 'Routine',
functions: 'Function',
schedulers: 'Scheduler',
includes: 'Includi',
drop: 'Drop',
completed: 'Completato',
aborted: 'Annullato',
disabled: 'Disabilitato',
enable: 'Abilita',
disable: 'Disabilita'
@ -237,6 +248,15 @@ module.exports = {
noOpenTabs: 'Non ci sono tab aperte, naviga nella barra sinistra o:',
noSchema: 'Nessuno schema',
restorePreviourSession: 'Ripristina sessione precedente',
exportSchema: 'Esporta schema',
importSchema: 'Importa schema',
directoryPath: 'Percorso directory',
newInserStmtEvery: 'Nuova istruzione INSERT ogni',
processingTableExport: 'Processo {table}',
fechingTableExport: 'Ricavo i dati {table}',
writingTableExport: 'Scrittura dati {table}',
checkAllTables: 'Seleziona tutte le tabelle',
uncheckAllTables: 'Deseleziona tutte le tabelle',
runQuery: 'Esegui query',
thereAreNoTableFields: 'Non ci sono campi della tabella',
newTable: 'Nuova tabella',
@ -251,7 +271,9 @@ module.exports = {
killProcess: 'Uccidi processo',
closeTab: 'Chiudi tab',
goToDownloadPage: 'Vai alla pagina di download',
readOnlyMode: 'Modalità sola lettura'
readOnlyMode: 'Modalità sola lettura',
importQueryErrors: 'Attenzione: si è verificato un errore | Attenzione si sono verificati {n} errori',
executedQueries: '{n} query eseguite | {n} query eseguite'
},
faker: {
address: 'Indirizzo',

View File

@ -5,4 +5,12 @@ export default class {
static getKey (params) {
return ipcRenderer.sendSync('get-key', params);
}
static showOpenDialog (options) {
return ipcRenderer.invoke('showOpenDialog', options);
}
static getDownloadPathDirectory () {
return ipcRenderer.invoke('get-download-dir-path');
}
}

View File

@ -69,4 +69,20 @@ export default class {
static rawQuery (params) {
return ipcRenderer.invoke('raw-query', params);
}
static export (params) {
return ipcRenderer.invoke('export', params);
}
static abortExport () {
return ipcRenderer.invoke('abort-export');
}
static import (params) {
return ipcRenderer.invoke('import-sql', params);
}
static abortImport () {
return ipcRenderer.invoke('abort-import-sql');
}
}

View File

@ -165,6 +165,15 @@ option:checked {
}
}
}
.modal-overlay{
background: rgba( 255, 255, 255, 0.1 );
box-shadow: 0 8px 32px 0 rgba( 31, 38, 135, 0.37 );
backdrop-filter: blur( 4px );
-webkit-backdrop-filter: blur( 4px );
border-radius: 10px;
border: 1px solid rgba( 255, 255, 255, 0.18 );
}
}
.tab {

View File

@ -1,70 +1,72 @@
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const webpack = require('webpack');
const ProgressPlugin = require('progress-webpack-plugin');
const { dependencies, devDependencies } = require('./package.json');
const { dependencies, devDependencies, version } = require('./package.json');
const externals = Object.keys(dependencies).concat(Object.keys(devDependencies));
const isDevMode = process.env.NODE_ENV === 'development';
const whiteListedModules = [];
module.exports = [
{ // Main
name: 'main',
mode: process.env.NODE_ENV,
devtool: isDevMode ? 'eval-source-map' : false,
entry: {
main: path.join(__dirname, './src/main/main.js')
module.exports = { // Main
name: 'main',
mode: process.env.NODE_ENV,
devtool: isDevMode ? 'eval-source-map' : false,
entry: {
main: path.join(__dirname, './src/main/main.js')
},
target: 'electron-main',
output: {
libraryTarget: 'commonjs2',
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
node: {
global: true,
__dirname: isDevMode,
__filename: isDevMode
},
externals: externals.filter((d) => !whiteListedModules.includes(d)),
resolve: {
extensions: ['.js', '.json'],
alias: {
src: path.join(__dirname, 'src/'),
common: path.resolve(__dirname, 'src/common')
},
target: 'electron-main',
output: {
libraryTarget: 'commonjs2',
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
node: {
global: true,
__dirname: isDevMode,
__filename: isDevMode
},
externals: externals.filter((d) => !whiteListedModules.includes(d)),
resolve: {
extensions: ['.js', '.json'],
alias: {
src: path.join(__dirname, 'src/'),
common: path.resolve(__dirname, 'src/common')
},
fallback: {
'pg-native': false,
'cpu-features': false,
cardinal: false
}
},
plugins: [
new ProgressPlugin(true),
new CleanWebpackPlugin({ root: path.join(__dirname, 'dist') })
],
module: {
rules: [
{
test: /\.node$/,
loader: 'node-loader',
options: {
name: '[path][name].[ext]'
}
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
},
{
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'file-loader'
}]
}
]
fallback: {
'pg-native': false,
'cpu-features': false,
cardinal: false
}
},
plugins: [
new ProgressPlugin(true),
new webpack.DefinePlugin({
'process.env': {
PACKAGE_VERSION: `"${version}"`
}
})
],
module: {
rules: [
{
test: /\.node$/,
loader: 'node-loader',
options: {
name: '[path][name].[ext]'
}
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
},
{
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'file-loader'
}]
}
]
}
];
};

81
webpack.workers.config.js Normal file
View File

@ -0,0 +1,81 @@
const path = require('path');
const webpack = require('webpack');
const ProgressPlugin = require('progress-webpack-plugin');
const { dependencies, devDependencies, version } = require('./package.json');
const externals = Object.keys(dependencies).concat(Object.keys(devDependencies));
const isDevMode = process.env.NODE_ENV === 'development';
const whiteListedModules = [];
const config = {
name: 'workers',
mode: process.env.NODE_ENV,
devtool: isDevMode ? 'eval-source-map' : false,
entry: {
exporter: path.join(__dirname, './src/main/workers/exporter.js'),
importer: path.join(__dirname, './src/main/workers/importer.js')
},
target: 'node',
output: {
libraryTarget: 'commonjs2',
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
node: {
global: true,
__dirname: isDevMode,
__filename: isDevMode
},
externals: externals.filter((d) => !whiteListedModules.includes(d)),
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.node$/,
use: 'node-loader'
}
]
},
resolve: {
extensions: ['.js', '.json'],
alias: {
src: path.join(__dirname, 'src/'),
common: path.resolve(__dirname, 'src/common')
},
fallback: {
'pg-native': false,
'cpu-features': false,
cardinal: false
}
},
plugins: [
new ProgressPlugin(true),
new webpack.DefinePlugin({
'process.env': {
PACKAGE_VERSION: `"${version}"`
}
})
]
};
/**
* Adjust rendererConfig for production settings
*/
if (isDevMode) {
// any dev only config
config.plugins.push(new webpack.HotModuleReplacementPlugin());
}
else {
config.plugins.push(
new webpack.LoaderOptionsPlugin({
minimize: true
})
);
}
module.exports = config;