added plugins

This commit is contained in:
Jan Prochazka
2021-04-13 16:17:53 +02:00
parent 446e7c139f
commit 4d5cc119f2
115 changed files with 5519 additions and 24 deletions

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Jan Prochazka
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,60 @@
[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
[![Donate](https://img.shields.io/badge/donate-paypal-blue.svg)](https://paypal.me/JanProchazkaCz/30eur)
[![NPM version](https://img.shields.io/npm/v/dbgate-plugin-csv.svg)](https://www.npmjs.com/package/dbgate-plugin-csv)
# dbgate-plugin-csv
CSV import/export plugin for DbGate
## Usage without DbGate
Export from fake object reader into CSV file. Fake object file can be replaced with other reader/writer factory functions, as described in
[dbgate-api package](https://www.npmjs.com/package/dbgate-api)
```javascript
const dbgateApi = require('dbgate-api');
const dbgatePluginCsv = require("dbgate-plugin-csv");
dbgateApi.registerPlugins(dbgatePluginCsv);
async function run() {
const reader = await dbgateApi.fakeObjectReader();
const writer = await dbgatePluginCsv.shellApi.writer({ fileName: 'myfile1.csv', separator: ';' });
await dbgateApi.copyStream(reader, writer);
console.log('Finished job script');
}
dbgateApi.runScript(run);
```
## Factory functions
### shellApi.reader
Reads CSV file
```js
const dbgatePluginCsv = require("dbgate-plugin-csv");
const reader = await dbgatePluginCsv.shellApi.reader({
fileName: 'test.csv',
encoding: 'utf-8',
header: true,
delimiter: ',',
quoted: false,
limitRows: null
});
```
### shellApi.writer
Writes CSV file
```js
const dbgatePluginCsv = require("dbgate-plugin-csv");
const writer = await dbgatePluginCsv.shellApi.writer({
fileName: 'test.csv',
encoding: 'utf-8',
header: true,
delimiter: ',',
quoted: false
});
```

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 384 384" style="enable-background:new 0 0 384 384;" xml:space="preserve">
<polygon style="fill:#EFEEEE;" points="64,0 64,384 288,384 384,288 384,0 "/>
<polygon style="fill:#ABABAB;" points="288,288 288,384 384,288 "/>
<polygon style="fill:#DEDEDD;" points="192,384 288,384 288,288 "/>
<path style="fill:#448E47;" d="M0,96v112h256V96L0,96L0,96z"/>
<g>
<path style="fill:#FFFFFF;" d="M64.32,130.112c-1.184-2.288-3.344-3.424-6.48-3.424c-1.728,0-3.152,0.464-4.272,1.408
c-1.12,0.928-2,2.416-2.64,4.496s-1.088,4.8-1.344,8.176c-0.272,3.36-0.384,7.472-0.384,12.336c0,5.184,0.176,9.376,0.528,12.576
c0.336,3.2,0.896,5.664,1.632,7.44s1.664,2.96,2.784,3.552c1.12,0.608,2.416,0.928,3.888,0.928c1.216,0,2.352-0.208,3.408-0.624
s1.968-1.248,2.736-2.496c0.784-1.248,1.392-3.008,1.824-5.28c0.448-2.272,0.672-5.264,0.672-8.976H80.48
c0,3.696-0.288,7.232-0.864,10.56s-1.664,6.24-3.216,8.736c-1.584,2.48-3.776,4.432-6.624,5.84
c-2.848,1.408-6.544,2.128-11.088,2.128c-5.168,0-9.312-0.848-12.368-2.496c-3.072-1.664-5.424-4.064-7.056-7.2
s-2.688-6.88-3.168-11.232c-0.464-4.336-0.72-9.152-0.72-14.384c0-5.184,0.256-9.968,0.72-14.352
c0.48-4.368,1.552-8.144,3.168-11.28c1.648-3.12,3.984-5.584,7.056-7.344c3.056-1.744,7.2-2.64,12.368-2.64
c4.944,0,8.816,0.8,11.664,2.4c2.848,1.6,4.976,3.632,6.368,6.096s2.304,5.12,2.64,7.968c0.352,2.848,0.528,5.52,0.528,8.016H66.08
C66.08,136,65.488,132.368,64.32,130.112z"/>
<path style="fill:#FFFFFF;" d="M109.072,167.008c0,1.6,0.144,3.056,0.384,4.352c0.272,1.312,0.736,2.416,1.44,3.312
c0.704,0.912,1.664,1.616,2.848,2.128c1.168,0.496,2.672,0.768,4.448,0.768c2.128,0,4.016-0.688,5.712-2.064
c1.68-1.376,2.544-3.52,2.544-6.384c0-1.536-0.224-2.864-0.624-3.984c-0.416-1.12-1.104-2.128-2.064-3.008
c-0.976-0.912-2.24-1.712-3.792-2.448s-3.504-1.488-5.808-2.256c-3.056-1.024-5.712-2.16-7.968-3.376
c-2.24-1.2-4.112-2.624-5.616-4.272c-1.504-1.632-2.608-3.52-3.312-5.664c-0.704-2.16-1.056-4.624-1.056-7.456
c0-6.784,1.888-11.824,5.664-15.152c3.76-3.328,8.96-4.992,15.552-4.992c3.072,0,5.904,0.336,8.496,1.008s4.832,1.744,6.72,3.264
c1.888,1.504,3.36,3.424,4.416,5.744c1.04,2.336,1.584,5.136,1.584,8.4v1.92h-13.232c0-3.264-0.576-5.776-1.712-7.552
c-1.152-1.744-3.072-2.64-5.76-2.64c-1.536,0-2.816,0.24-3.84,0.672c-1.008,0.448-1.84,1.04-2.448,1.776s-1.04,1.616-1.264,2.576
c-0.24,0.96-0.336,1.952-0.336,2.976c0,2.128,0.448,3.888,1.344,5.328c0.896,1.456,2.816,2.784,5.76,3.984l10.656,4.608
c2.624,1.152,4.768,2.352,6.416,3.616c1.664,1.248,3.008,2.592,3.984,4.032c0.992,1.44,1.68,3.008,2.064,4.752
c0.384,1.712,0.576,3.648,0.576,5.744c0,7.232-2.096,12.496-6.288,15.792c-4.192,3.296-10.032,4.96-17.52,4.96
c-7.808,0-13.392-1.696-16.768-5.088c-3.36-3.392-5.024-8.256-5.024-14.592v-2.784h13.824L109.072,167.008L109.072,167.008z"/>
<path style="fill:#FFFFFF;" d="M177.344,168.544h0.304l10.176-50.688h14.32L186.4,186.4h-17.76l-15.728-68.544h14.784
L177.344,168.544z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,39 @@
{
"name": "dbgate-plugin-csv",
"main": "dist/backend.js",
"version": "1.0.9",
"homepage": "https://github.com/dbgate/dbgate-plugin-csv",
"description": "CSV import/export plugin for DbGate",
"repository": {
"type": "git",
"url": "https://github.com/dbgate/dbgate-plugin-csv.git"
},
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
"author": "Jan Prochazka",
"license": "GPL",
"keywords": [
"csv",
"import",
"export",
"dbgate",
"dbgateplugin"
],
"files": [
"dist"
],
"scripts": {
"build:frontend": "webpack --config webpack-frontend.config",
"build:backend": "webpack --config webpack-backend.config.js",
"build": "yarn build:frontend && yarn build:backend",
"plugin": "yarn build && yarn pack && dbgate-plugin dbgate-plugin-csv",
"plugout": "dbgate-plugout dbgate-plugin-csv",
"prepublishOnly": "yarn build"
},
"devDependencies": {
"csv": "^5.3.2",
"dbgate-plugin-tools": "^1.0.4",
"lodash": "^4.17.20",
"webpack": "^4.42.0",
"webpack-cli": "^3.3.11"
}
}

View File

@@ -0,0 +1,8 @@
module.exports = {
trailingComma: 'es5',
tabWidth: 2,
semi: true,
singleQuote: true,
arrowParen: 'avoid',
printWidth: 120,
};

View File

@@ -0,0 +1,13 @@
const reader = require('./reader');
const writer = require('./writer');
module.exports = {
packageName: 'dbgate-plugin-csv',
shellApi: {
reader,
writer,
},
initialize(dbgateEnv) {
reader.initialize(dbgateEnv);
},
};

View File

@@ -0,0 +1,67 @@
const zipObject = require('lodash/zipObject');
const csv = require('csv');
const fs = require('fs');
const stream = require('stream');
let dbgateApi;
class CsvPrepareStream extends stream.Transform {
constructor({ header }) {
super({ objectMode: true });
this.structure = null;
this.header = header;
}
_transform(chunk, encoding, done) {
if (this.structure) {
this.push(
zipObject(
this.structure.columns.map((x) => x.columnName),
chunk
)
);
done();
} else {
if (this.header) {
this.structure = {
__isStreamHeader: true,
columns: chunk.map((columnName) => ({ columnName })),
};
this.push(this.structure);
} else {
this.structure = {
__isStreamHeader: true,
columns: chunk.map((value, index) => ({ columnName: `col${index + 1}` })),
};
this.push(this.structure);
this.push(
zipObject(
this.structure.columns.map((x) => x.columnName),
chunk
)
);
}
done();
}
}
}
async function reader({ fileName, encoding = 'utf-8', header = true, delimiter, limitRows = undefined }) {
console.log(`Reading file ${fileName}`);
const csvStream = csv.parse({
// @ts-ignore
delimiter,
skip_lines_with_error: true,
to_line: limitRows ? limitRows + 1 : undefined,
});
const downloadedFile = await dbgateApi.download(fileName);
const fileStream = fs.createReadStream(downloadedFile, encoding);
const csvPrepare = new CsvPrepareStream({ header });
fileStream.pipe(csvStream);
csvStream.pipe(csvPrepare);
return csvPrepare;
}
reader.initialize = (dbgateEnv) => {
dbgateApi = dbgateEnv.dbgateApi;
};
module.exports = reader;

View File

@@ -0,0 +1,36 @@
const csv = require('csv');
const fs = require('fs');
const stream = require('stream');
class CsvPrepareStream extends stream.Transform {
constructor({ header }) {
super({ objectMode: true });
this.structure = null;
this.header = header;
}
_transform(chunk, encoding, done) {
if (this.structure) {
this.push(this.structure.columns.map((col) => chunk[col.columnName]));
done();
} else {
this.structure = chunk;
if (this.header) {
this.push(chunk.columns.map((x) => x.columnName));
}
done();
}
}
}
async function writer({ fileName, encoding = 'utf-8', header = true, delimiter, quoted }) {
console.log(`Writing file ${fileName}`);
const csvPrepare = new CsvPrepareStream({ header });
const csvStream = csv.stringify({ delimiter, quoted });
const fileStream = fs.createWriteStream(fileName, encoding);
csvPrepare.pipe(csvStream);
csvStream.pipe(fileStream);
csvPrepare['finisher'] = fileStream;
return csvPrepare;
}
module.exports = writer;

View File

@@ -0,0 +1,46 @@
const fileFormat = {
packageName: 'dbgate-plugin-csv',
// file format identifier
storageType: 'csv',
// file extension without leading dot
extension: 'csv',
// human readable file format name
name: 'CSV',
// function name from backend, which contains reader factory, postfixed by package name
readerFunc: 'reader@dbgate-plugin-csv',
// function name from backend, which contains writer factory, postfixed by package name
writerFunc: 'writer@dbgate-plugin-csv',
// optional list of format arguments, which can be edited from UI
args: [
{
type: 'select',
name: 'delimiter',
label: 'Delimiter',
options: [
{ name: 'Comma (,)', value: ',' },
{ name: 'Semicolon (;)', value: ';' },
{ name: 'Tab', value: '\t' },
{ name: 'Pipe (|)', value: '|' },
],
apiName: 'delimiter',
},
{
type: 'checkbox',
name: 'quoted',
label: 'Quoted',
apiName: 'quoted',
direction: 'target',
},
{
type: 'checkbox',
name: 'header',
label: 'Has header row',
apiName: 'header',
default: true,
},
],
};
export default {
fileFormats: [fileFormat],
};

View File

@@ -0,0 +1,23 @@
var webpack = require('webpack');
var path = require('path');
var config = {
context: __dirname + '/src/backend',
entry: {
app: './index.js',
},
target: 'node',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'backend.js',
libraryTarget: 'commonjs2',
},
// uncomment for disable minimalization
// optimization: {
// minimize: false,
// },
};
module.exports = config;

View File

@@ -0,0 +1,24 @@
var webpack = require("webpack");
var path = require("path");
var config = {
context: __dirname + "/src/frontend",
entry: {
app: "./index.js",
},
target: "web",
output: {
path: path.resolve(__dirname, "dist"),
filename: "frontend.js",
libraryTarget: "var",
library: 'plugin',
},
// uncomment for disable minimalization
// optimization: {
// minimize: false,
// },
};
module.exports = config;

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Jan Prochazka
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,52 @@
[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
[![Donate](https://img.shields.io/badge/donate-paypal-blue.svg)](https://paypal.me/JanProchazkaCz/30eur)
[![NPM version](https://img.shields.io/npm/v/dbgate-plugin-excel.svg)](https://www.npmjs.com/package/dbgate-plugin-excel)
# dbgate-plugin-excel
MS Excel import/export plugin for DbGate
## Usage without DbGate
Export from fake object reader into MS Excel file. Fake object file can be replaced with other reader/writer factory functions, as described in
[dbgate-api package](https://www.npmjs.com/package/dbgate-api)
```javascript
const dbgateApi = require('dbgate-api');
const dbgatePluginExcel = require("dbgate-plugin-excel");
dbgateApi.registerPlugins(dbgatePluginExcel);
async function run() {
const reader = await dbgateApi.fakeObjectReader();
const writer = await dbgatePluginExcel.shellApi.writer({ fileName: 'myfile1.xlsx', sheetName: 'Sheet 1' });
await dbgateApi.copyStream(reader, writer);
console.log('Finished job script');
}
dbgateApi.runScript(run);
```
## Factory functions
### shellApi.reader
Reads tabular data from one sheet in MS Excel file.
```js
const reader = await dbgatePluginExcel.shellApi.reader({
fileName: 'test.xlsx',
sheetName: 'Album',
limitRows: null
});
```
### shellApi.writer
Writes tabular data into MS excel file. There could be more writes into the some file in one script, if property sheetName is different.
```js
const reader = await dbgatePluginExcel.shellApi.writer({
fileName: 'test.xlsx',
sheetName: 'Album',
});
```

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<path style="fill:#ECEFF1;" d="M496,432.011H272c-8.832,0-16-7.168-16-16s0-311.168,0-320s7.168-16,16-16h224
c8.832,0,16,7.168,16,16v320C512,424.843,504.832,432.011,496,432.011z"/>
<g>
<path style="fill:#388E3C;" d="M336,176.011h-64c-8.832,0-16-7.168-16-16s7.168-16,16-16h64c8.832,0,16,7.168,16,16
S344.832,176.011,336,176.011z"/>
<path style="fill:#388E3C;" d="M336,240.011h-64c-8.832,0-16-7.168-16-16s7.168-16,16-16h64c8.832,0,16,7.168,16,16
S344.832,240.011,336,240.011z"/>
<path style="fill:#388E3C;" d="M336,304.011h-64c-8.832,0-16-7.168-16-16s7.168-16,16-16h64c8.832,0,16,7.168,16,16
S344.832,304.011,336,304.011z"/>
<path style="fill:#388E3C;" d="M336,368.011h-64c-8.832,0-16-7.168-16-16s7.168-16,16-16h64c8.832,0,16,7.168,16,16
S344.832,368.011,336,368.011z"/>
<path style="fill:#388E3C;" d="M432,176.011h-32c-8.832,0-16-7.168-16-16s7.168-16,16-16h32c8.832,0,16,7.168,16,16
S440.832,176.011,432,176.011z"/>
<path style="fill:#388E3C;" d="M432,240.011h-32c-8.832,0-16-7.168-16-16s7.168-16,16-16h32c8.832,0,16,7.168,16,16
S440.832,240.011,432,240.011z"/>
<path style="fill:#388E3C;" d="M432,304.011h-32c-8.832,0-16-7.168-16-16s7.168-16,16-16h32c8.832,0,16,7.168,16,16
S440.832,304.011,432,304.011z"/>
<path style="fill:#388E3C;" d="M432,368.011h-32c-8.832,0-16-7.168-16-16s7.168-16,16-16h32c8.832,0,16,7.168,16,16
S440.832,368.011,432,368.011z"/>
</g>
<path style="fill:#2E7D32;" d="M282.208,19.691c-3.648-3.04-8.544-4.352-13.152-3.392l-256,48C5.472,65.707,0,72.299,0,80.011v352
c0,7.68,5.472,14.304,13.056,15.712l256,48c0.96,0.192,1.952,0.288,2.944,0.288c3.712,0,7.328-1.28,10.208-3.68
c3.68-3.04,5.792-7.584,5.792-12.32v-448C288,27.243,285.888,22.731,282.208,19.691z"/>
<path style="fill:#FAFAFA;" d="M220.032,309.483l-50.592-57.824l51.168-65.792c5.44-6.976,4.16-17.024-2.784-22.464
c-6.944-5.44-16.992-4.16-22.464,2.784l-47.392,60.928l-39.936-45.632c-5.856-6.72-15.968-7.328-22.56-1.504
c-6.656,5.824-7.328,15.936-1.504,22.56l44,50.304L83.36,310.187c-5.44,6.976-4.16,17.024,2.784,22.464
c2.944,2.272,6.432,3.36,9.856,3.36c4.768,0,9.472-2.112,12.64-6.176l40.8-52.48l46.528,53.152
c3.168,3.648,7.584,5.504,12.032,5.504c3.744,0,7.488-1.312,10.528-3.968C225.184,326.219,225.856,316.107,220.032,309.483z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,39 @@
{
"name": "dbgate-plugin-excel",
"main": "dist/backend.js",
"version": "1.0.8",
"homepage": "https://github.com/dbgate/dbgate-plugin-excel",
"description": "MS Excel import/export plugin for DbGate",
"repository": {
"type": "git",
"url": "https://github.com/dbgate/dbgate-plugin-excel.git"
},
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
"author": "Jan Prochazka",
"license": "GPL",
"keywords": [
"excel",
"import",
"export",
"dbgate",
"dbgateplugin"
],
"files": [
"dist"
],
"scripts": {
"build:frontend": "webpack --config webpack-frontend.config",
"build:backend": "webpack --config webpack-backend.config.js",
"build": "yarn build:frontend && yarn build:backend",
"plugin": "yarn build && dbgate-plugin dbgate-plugin-excel",
"plugout": "dbgate-plugout dbgate-plugin-excel",
"prepublishOnly": "yarn build"
},
"devDependencies": {
"dbgate-plugin-tools": "^1.0.0",
"lodash": "^4.17.20",
"webpack": "^4.42.0",
"webpack-cli": "^3.3.11",
"xlsx": "^0.16.8"
}
}

View File

@@ -0,0 +1,8 @@
module.exports = {
trailingComma: 'es5',
tabWidth: 2,
semi: true,
singleQuote: true,
arrowParen: 'avoid',
printWidth: 120,
};

View File

@@ -0,0 +1,26 @@
const xlsx = require('xlsx');
const reader = require('./reader');
const writer = require('./writer');
let dbgateApi;
module.exports = {
packageName: 'dbgate-plugin-excel',
shellApi: {
reader,
writer,
},
commands: {
analyse: async ({ fileName }) => {
const downloadedFile = await dbgateApi.download(fileName);
const workbook = xlsx.readFile(downloadedFile, { bookSheets: true });
return workbook.SheetNames;
},
},
initialize(dbgateEnv) {
dbgateApi = dbgateEnv.dbgateApi;
writer.initialize(dbgateEnv);
reader.initialize(dbgateEnv);
},
};

View File

@@ -0,0 +1,67 @@
const xlsx = require('xlsx');
const stream = require('stream');
const _ = require('lodash');
const loadedWorkbooks = {};
let dbgateApi;
async function loadWorkbook(fileName) {
let workbook = loadedWorkbooks[fileName];
if (workbook) return workbook;
console.log(`Loading excel ${fileName}`);
const downloadedFile = await dbgateApi.download(fileName);
workbook = xlsx.readFile(downloadedFile);
loadedWorkbooks[fileName] = workbook;
return workbook;
}
async function waitForDrain(stream) {
return new Promise((resolve) => {
stream.once('drain', () => {
resolve();
});
});
}
async function reader({ fileName, sheetName, limitRows = undefined }) {
const pass = new stream.PassThrough({
objectMode: true,
highWaterMark: 100,
});
const workbook = await loadWorkbook(fileName);
const sheet = workbook.Sheets[sheetName];
const rows = xlsx.utils.sheet_to_json(sheet, {
header: 1,
blankrows: false,
});
const header = rows[0];
const structure = {
__isStreamHeader: true,
columns: _.range(header.length).map((index) => ({ columnName: header[index] })),
};
if (!pass.write(structure)) await waitForDrain(pass);
const sendAsync = async () => {
for (let rowIndex = 1; rowIndex < rows.length; rowIndex++) {
if (limitRows && rowIndex > limitRows) break;
const row = rows[rowIndex];
const rowData = _.fromPairs(structure.columns.map((col, index) => [col.columnName, row[index]]));
if (_.isEmpty(_.omitBy(rowData, (v) => v == null || v.toString().trim().length == 0))) continue;
if (!pass.write(rowData)) await waitForDrain(pass);
}
pass.end();
};
// don't wait for sending
sendAsync();
return pass;
}
reader.initialize = (dbgateEnv) => {
dbgateApi = dbgateEnv.dbgateApi;
};
module.exports = reader;

View File

@@ -0,0 +1,57 @@
const xlsx = require('xlsx');
const stream = require('stream');
const writingWorkbooks = {};
async function saveExcelFiles() {
for (const file in writingWorkbooks) {
xlsx.writeFile(writingWorkbooks[file], file);
}
}
function createWorkbook(fileName) {
let workbook = writingWorkbooks[fileName];
if (workbook) return workbook;
workbook = xlsx.utils.book_new();
writingWorkbooks[fileName] = workbook;
return workbook;
}
class ExcelSheetWriterStream extends stream.Writable {
constructor({ fileName, sheetName }) {
super({ objectMode: true });
this.rows = [];
this.structure = null;
this.fileName = fileName;
this.sheetName = sheetName;
}
_write(chunk, enc, next) {
if (this.structure) {
this.rows.push(this.structure.columns.map((col) => chunk[col.columnName]));
} else {
this.structure = chunk;
this.rows.push(chunk.columns.map((x) => x.columnName));
}
next();
}
_final(callback) {
const workbook = createWorkbook(this.fileName);
xlsx.utils.book_append_sheet(workbook, xlsx.utils.aoa_to_sheet(this.rows), this.sheetName || 'Sheet 1');
callback();
}
}
async function writer({ fileName, sheetName }) {
return new ExcelSheetWriterStream({
fileName,
sheetName,
});
}
writer.initialize = ({ dbgateApi }) => {
dbgateApi.finalizer.register(saveExcelFiles);
};
module.exports = writer;

View File

@@ -0,0 +1,68 @@
let axios;
function initialize(dbgateEnv) {
axios = dbgateEnv.axios;
}
const fileFormat = {
packageName: 'dbgate-plugin-excel',
// file format identifier
storageType: 'excel',
// file extension without leading dot
extension: 'xlsx',
// human readable file format name
name: 'MS Excel',
// function name from backend, which contains reader factory, postfixed by package name
readerFunc: 'reader@dbgate-plugin-excel',
// function name from backend, which contains writer factory, postfixed by package name
writerFunc: 'writer@dbgate-plugin-excel',
addFileToSourceList: async ({ fileName }, newSources, newValues) => {
const resp = await axios.post('plugins/command', {
command: 'analyse',
packageName: 'dbgate-plugin-excel',
args: {
fileName,
},
});
const sheetNames = resp.data;
for (const sheetName of sheetNames) {
newSources.push(sheetName);
newValues[`sourceFile_${sheetName}`] = {
fileName,
sheetName,
};
}
},
args: [
{
type: 'checkbox',
name: 'singleFile',
label: 'Create single file',
direction: 'target',
},
],
getDefaultOutputName: (sourceName, values) => {
if (values.target_excel_singleFile) {
return sourceName;
}
return null;
},
getOutputParams: (sourceName, values) => {
if (values.target_excel_singleFile) {
return {
sheetName: values[`targetName_${sourceName}`] || sourceName,
fileName: 'data.xlsx',
};
}
return null;
},
};
export default {
fileFormats: [fileFormat],
initialize,
};

View File

@@ -0,0 +1,23 @@
var webpack = require('webpack');
var path = require('path');
var config = {
context: __dirname + '/src/backend',
entry: {
app: './index.js',
},
target: 'node',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'backend.js',
libraryTarget: 'commonjs2',
},
// uncomment for disable minimalization
// optimization: {
// minimize: false,
// },
};
module.exports = config;

View File

@@ -0,0 +1,24 @@
var webpack = require("webpack");
var path = require("path");
var config = {
context: __dirname + "/src/frontend",
entry: {
app: "./index.js",
},
target: "web",
output: {
path: path.resolve(__dirname, "dist"),
filename: "frontend.js",
libraryTarget: "var",
library: 'plugin',
},
// uncomment for disable minimalization
// optimization: {
// minimize: false,
// },
};
module.exports = config;

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 DbGate
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,6 @@
[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
[![NPM version](https://img.shields.io/npm/v/dbgate-plugin-mongo.svg)](https://www.npmjs.com/package/dbgate-plugin-mongo)
# dbgate-plugin-mongo
Use DbGate for install of this plugin

View File

@@ -0,0 +1,6 @@
<svg height="2500" viewBox="8.738 -5.03622834 17.45992422 39.40619484" width="2500"
xmlns="http://www.w3.org/2000/svg">
<path d="m15.9.087.854 1.604c.192.296.4.558.645.802a22.406 22.406 0 0 1 2.004 2.266c1.447 1.9 2.423 4.01 3.12 6.292.418 1.394.645 2.824.662 4.27.07 4.323-1.412 8.035-4.4 11.12a12.7 12.7 0 0 1 -1.57 1.342c-.296 0-.436-.227-.558-.436a3.589 3.589 0 0 1 -.436-1.255c-.105-.523-.174-1.046-.14-1.586v-.244c-.024-.052-.285-24.052-.181-24.175z" fill="#599636"/>
<path d="m15.9.034c-.035-.07-.07-.017-.105.017.017.35-.105.662-.296.96-.21.296-.488.523-.767.767-1.55 1.342-2.77 2.963-3.747 4.776-1.3 2.44-1.97 5.055-2.16 7.808-.087.993.314 4.497.627 5.508.854 2.684 2.388 4.933 4.375 6.885.488.47 1.01.906 1.55 1.325.157 0 .174-.14.21-.244a4.78 4.78 0 0 0 .157-.68l.35-2.614z" fill="#6cac48"/>
<path d="m16.754 28.845c.035-.4.227-.732.436-1.063-.21-.087-.366-.26-.488-.453a3.235 3.235 0 0 1 -.26-.575c-.244-.732-.296-1.5-.366-2.248v-.453c-.087.07-.105.662-.105.75a17.37 17.37 0 0 1 -.314 2.353c-.052.314-.087.627-.28.906 0 .035 0 .07.017.122.314.924.4 1.865.453 2.824v.35c0 .418-.017.33.33.47.14.052.296.07.436.174.105 0 .122-.087.122-.157l-.052-.575v-1.604c-.017-.28.035-.558.07-.82z" fill="#c2bfbf"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,36 @@
{
"name": "dbgate-plugin-mongo",
"main": "dist/backend.js",
"version": "1.0.1",
"license": "MIT",
"author": "Jan Prochazka",
"homepage": "https://github.com/dbgate/dbgate-plugin-mongo",
"description": "MongoDB connect plugin for DbGate",
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
"keywords": [
"dbgate",
"dbgateplugin",
"mongo",
"mongodb"
],
"files": [
"dist"
],
"scripts": {
"build:frontend": "webpack --config webpack-frontend.config",
"build:backend": "webpack --config webpack-backend.config.js",
"build": "yarn build:frontend && yarn build:backend",
"plugin": "yarn build && yarn pack && dbgate-plugin dbgate-plugin-mongo",
"plugout": "dbgate-plugout dbgate-plugin-mongo",
"prepublishOnly": "yarn build"
},
"devDependencies": {
"byline": "^5.0.0",
"dbgate-plugin-tools": "^1.0.4",
"dbgate-tools": "^4.1.0-rc.1",
"is-promise": "^4.0.0",
"mongodb": "^3.6.5",
"webpack": "^4.42.0",
"webpack-cli": "^3.3.11"
}
}

View File

@@ -0,0 +1,8 @@
module.exports = {
trailingComma: 'es5',
tabWidth: 2,
semi: true,
singleQuote: true,
arrowParen: 'avoid',
printWidth: 120,
};

View File

@@ -0,0 +1,24 @@
const { DatabaseAnalyser } = require('dbgate-tools');
class Analyser extends DatabaseAnalyser {
constructor(pool, driver) {
super(pool, driver);
}
async _runAnalysis() {
const collections = await this.pool.__getDatabase().listCollections().toArray();
const res = this.mergeAnalyseResult(
{
collections: collections.map((x) => ({
pureName: x.name,
})),
},
(x) => x.pureName
);
// console.log('MERGED', res);
return res;
}
}
module.exports = Analyser;

View File

@@ -0,0 +1,58 @@
export function createBulkInsertStream(driver, stream, pool, name, options) {
const collectionName = name.pureName;
const db = pool.__getDatabase();
const writable = new stream.Writable({
objectMode: true,
});
writable.buffer = [];
writable.wasHeader = false;
writable.addRow = (row) => {
if (!writable.wasHeader) {
writable.wasHeader = true;
if (row.__isStreamHeader ||
// TODO remove isArray test
Array.isArray(row.columns)) return;
}
writable.buffer.push(row);
};
writable.checkStructure = async () => {
if (options.dropIfExists || options.truncate) {
console.log(`Dropping collection ${collectionName}`);
await db.collection(collectionName).drop();
}
if (options.truncate) {
console.log(`Truncating collection ${collectionName}`);
await db.collection(collectionName).deleteMany({});
}
};
writable.send = async () => {
const rows = writable.buffer;
writable.buffer = [];
await db.collection(collectionName).insertMany(rows);
};
writable.sendIfFull = async () => {
if (writable.buffer.length > 100) {
await writable.send();
}
};
writable._write = async (chunk, encoding, callback) => {
writable.addRow(chunk);
await writable.sendIfFull();
callback();
};
writable._final = async (callback) => {
await writable.send();
callback();
};
return writable;
}

View File

@@ -0,0 +1,260 @@
const _ = require('lodash');
const stream = require('stream');
const isPromise = require('is-promise');
const driverBase = require('../frontend/driver');
const Analyser = require('./Analyser');
const MongoClient = require('mongodb').MongoClient;
const ObjectId = require('mongodb').ObjectId;
const Cursor = require('mongodb').Cursor;
const { createBulkInsertStream } = require('./createBulkInsertStream');
function readCursor(cursor, options) {
return new Promise((resolve) => {
options.recordset({ __isDynamicStructure: true });
cursor.on('data', (data) => options.row(data));
cursor.on('end', () => resolve());
});
}
const mongoIdRegex = /^[0-9a-f]{24}$/;
function convertCondition(condition) {
if (condition && _.isString(condition._id) && condition._id.match(mongoIdRegex)) {
return {
_id: ObjectId(condition._id),
};
}
return condition;
}
function findArrayResult(resValue) {
if (!_.isPlainObject(resValue)) return null;
const arrays = _.values(resValue).filter((x) => _.isArray(x));
if (arrays.length == 1) return arrays[0];
return null;
}
async function getScriptableDb(pool) {
const db = pool.__getDatabase();
const collections = await db.listCollections().toArray();
for (const collection of collections) {
db[collection.name] = db.collection(collection.name);
}
return db;
}
/** @type {import('dbgate-types').EngineDriver} */
const driver = {
...driverBase,
analyserClass: Analyser,
async connect({ server, port, user, password, database, useDatabaseUrl, databaseUrl, ssl }) {
// let mongoUrl = databaseUrl;
// if (!useDatabaseUrl) {
// mongoUrl = user ? `mongodb://${user}:${password}@${server}:${port}` : `mongodb://${server}:${port}`;
// if (database) mongoUrl += '/' + database;
// }
const mongoUrl = useDatabaseUrl
? databaseUrl
: user
? `mongodb://${user}:${password}@${server}:${port}`
: `mongodb://${server}:${port}`;
const options = {};
if (ssl) {
options.tls = true;
options.tlsCAFile = ssl.ca;
options.tlsCertificateKeyFile = ssl.cert || ssl.key;
options.tlsCertificateKeyFilePassword = ssl.password;
options.tlsAllowInvalidCertificates = !ssl.rejectUnauthorized;
}
const pool = new MongoClient(mongoUrl, options);
await pool.connect();
// const pool = await MongoClient.connect(mongoUrl);
pool.__getDatabase = database ? () => pool.db(database) : () => pool.db();
pool.__databaseName = database;
return pool;
},
// @ts-ignore
async query(pool, sql) {
return {
rows: [],
columns: [],
};
},
async stream(pool, sql, options) {
let func;
try {
func = eval(`(db,ObjectId) => ${sql}`);
} catch (err) {
options.info({
message: 'Error compiling expression: ' + err.message,
time: new Date(),
severity: 'error',
});
options.done();
return;
}
const db = await getScriptableDb(pool);
let exprValue;
try {
exprValue = func(db, ObjectId);
} catch (err) {
options.info({
message: 'Error evaluating expression: ' + err.message,
time: new Date(),
severity: 'error',
});
options.done();
return;
}
if (exprValue instanceof Cursor) {
await readCursor(exprValue, options);
} else if (isPromise(exprValue)) {
try {
const resValue = await exprValue;
options.info({
message: 'Command succesfully executed',
time: new Date(),
severity: 'info',
});
options.info({
message: JSON.stringify(resValue),
time: new Date(),
severity: 'info',
});
const arrayRes = findArrayResult(resValue);
if (arrayRes) {
options.recordset({ __isDynamicStructure: true });
for (const row of arrayRes) {
options.row(row);
}
}
} catch (err) {
options.info({
message: 'Error when running command: ' + err.message,
time: new Date(),
severity: 'error',
});
}
}
options.done();
},
async readQuery(pool, sql, structure) {
try {
const json = JSON.parse(sql);
if (json && json.pureName) {
sql = `db.${json.pureName}.find()`;
}
} catch (err) {
// query is not JSON serialized collection name
}
// const pass = new stream.PassThrough({
// objectMode: true,
// highWaterMark: 100,
// });
func = eval(`(db,ObjectId) => ${sql}`);
const db = await getScriptableDb(pool);
exprValue = func(db, ObjectId);
// return directly stream without header row
return exprValue;
// pass.write(structure || { __isDynamicStructure: true });
// exprValue.on('data', (row) => pass.write(row));
// exprValue.on('end', () => pass.end());
// return pass;
},
async writeTable(pool, name, options) {
return createBulkInsertStream(this, stream, pool, name, options);
},
async getVersion(pool) {
const status = await pool.__getDatabase().admin().serverInfo();
return status;
},
async listDatabases(pool) {
const res = await pool.__getDatabase().admin().listDatabases();
return res.databases;
},
async readCollection(pool, options) {
try {
const collection = pool.__getDatabase().collection(options.pureName);
if (options.countDocuments) {
const count = await collection.countDocuments(options.condition || {});
return { count };
} else {
let cursor = await collection.find(options.condition || {});
if (options.sort) cursor = cursor.sort(options.sort);
if (options.skip) cursor = cursor.skip(options.skip);
if (options.limit) cursor = cursor.limit(options.limit);
const rows = await cursor.toArray();
return { rows };
}
} catch (err) {
return { errorMessage: err.message };
}
},
async updateCollection(pool, changeSet) {
const res = {
inserted: [],
updated: [],
deleted: [],
replaced: [],
};
try {
const db = pool.__getDatabase();
for (const insert of changeSet.inserts) {
const collection = db.collection(insert.pureName);
const document = {
...insert.document,
...insert.fields,
};
const resdoc = await collection.insert(document);
res.inserted.push(resdoc._id);
}
for (const update of changeSet.updates) {
const collection = db.collection(update.pureName);
if (update.document) {
const document = {
...update.document,
...update.fields,
};
const doc = await collection.findOne(convertCondition(update.condition));
if (doc) {
const resdoc = await collection.replaceOne(convertCondition(update.condition), {
...document,
_id: doc._id,
});
res.replaced.push(resdoc._id);
}
} else {
const resdoc = await collection.updateOne(convertCondition(update.condition), { $set: update.fields });
res.updated.push(resdoc._id);
}
}
for (const del of changeSet.deletes) {
const collection = db.collection(del.pureName);
const resdoc = await collection.deleteOne(convertCondition(del.condition));
res.deleted.push(resdoc._id);
}
return res;
} catch (err) {
return { errorMessage: err.message };
}
},
async createDatabase(pool, name) {
const db = pool.db(name);
await db.createCollection('collection1');
},
};
module.exports = driver;

View File

@@ -0,0 +1,6 @@
const driver = require('./driver');
module.exports = {
packageName: 'dbgate-plugin-mongo',
driver,
};

View File

@@ -0,0 +1,6 @@
const { SqlDumper } = require('dbgate-tools');
class Dumper extends SqlDumper {
}
module.exports = Dumper;

View File

@@ -0,0 +1,76 @@
const { driverBase } = require('dbgate-tools');
const Dumper = require('./Dumper');
const mongoIdRegex = /^[0-9a-f]{24}$/;
function getConditionPreview(condition) {
if (condition && _.isString(condition._id) && condition._id.match(mongoIdRegex)) {
return `{ _id: ObjectId('${condition._id}') }`;
}
return JSON.stringify(condition);
}
/** @type {import('dbgate-types').SqlDialect} */
const dialect = {
limitSelect: true,
rangeSelect: true,
offsetFetchRangeSyntax: true,
stringEscapeChar: "'",
fallbackDataType: 'nvarchar(max)',
nosql: true,
quoteIdentifier(s) {
return `[${s}]`;
},
};
/** @type {import('dbgate-types').EngineDriver} */
const driver = {
...driverBase,
dumperClass: Dumper,
dialect,
engine: 'mongo@dbgate-plugin-mongo',
title: 'MongoDB',
defaultPort: 27017,
supportsDatabaseUrl: true,
databaseUrlPlaceholder: 'e.g. mongodb://username:password@mongodb.mydomain.net/dbname',
getCollectionUpdateScript(changeSet) {
let res = '';
for (const insert of changeSet.inserts) {
res += `db.${insert.pureName}.insert(${JSON.stringify(
{
...insert.document,
...insert.fields,
},
undefined,
2
)});\n`;
}
for (const update of changeSet.updates) {
if (update.document) {
res += `db.${update.pureName}.replaceOne(${getConditionPreview(update.condition)}, ${JSON.stringify(
{
...update.document,
...update.fields,
},
undefined,
2
)});\n`;
} else {
res += `db.${update.pureName}.updateOne(${getConditionPreview(update.condition)}, ${JSON.stringify(
{
$set: update.fields,
},
undefined,
2
)});\n`;
}
}
for (const del of changeSet.deletes) {
res += `db.${del.pureName}.deleteOne(${getConditionPreview(del.condition)});\n`;
}
return res;
},
};
module.exports = driver;

View File

@@ -0,0 +1,6 @@
import driver from './driver';
export default {
packageName: 'dbgate-plugin-mongo',
driver,
};

View File

@@ -0,0 +1,23 @@
var webpack = require('webpack');
var path = require('path');
var config = {
context: __dirname + '/src/backend',
entry: {
app: './index.js',
},
target: 'node',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'backend.js',
libraryTarget: 'commonjs2',
},
// uncomment for disable minimalization
// optimization: {
// minimize: false,
// },
};
module.exports = config;

View File

@@ -0,0 +1,24 @@
var webpack = require("webpack");
var path = require("path");
var config = {
context: __dirname + "/src/frontend",
entry: {
app: "./index.js",
},
target: "web",
output: {
path: path.resolve(__dirname, "dist"),
filename: "frontend.js",
libraryTarget: "var",
library: 'plugin',
},
// uncomment for disable minimalization
// optimization: {
// minimize: false,
// },
};
module.exports = config;

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Jan Prochazka
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,7 @@
[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
[![Donate](https://img.shields.io/badge/donate-paypal-blue.svg)](https://paypal.me/JanProchazkaCz/30eur)
[![NPM version](https://img.shields.io/npm/v/dbgate-plugin-mssql.svg)](https://www.npmjs.com/package/dbgate-plugin-mssql)
# dbgate-plugin-mssql
MS SQL connector plugin for DbGate

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -0,0 +1,39 @@
{
"name": "dbgate-plugin-mssql",
"main": "dist/backend.js",
"version": "1.2.2",
"homepage": "https://github.com/dbgate/dbgate-plugin-mssql",
"description": "MS SQL connect plugin for DbGate",
"repository": {
"type": "git",
"url": "https://github.com/dbgate/dbgate-plugin-mssql.git"
},
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
"author": "Jan Prochazka",
"license": "GPL",
"keywords": [
"sql",
"mssql",
"dbgate",
"dbgateplugin"
],
"files": [
"dist"
],
"scripts": {
"build:frontend": "webpack --config webpack-frontend.config",
"build:backend": "webpack --config webpack-backend.config.js",
"build": "yarn build:frontend && yarn build:backend",
"prepublishOnly": "yarn build",
"plugin": "yarn build && yarn pack && dbgate-plugin dbgate-plugin-mssql",
"plugout": "dbgate-plugout dbgate-plugin-mssql"
},
"devDependencies": {
"async-lock": "^1.2.6",
"dbgate-plugin-tools": "^1.0.4",
"dbgate-tools": "^4.0.3-rc.1",
"tedious": "^9.2.3",
"webpack": "^4.42.0",
"webpack-cli": "^3.3.11"
}
}

View File

@@ -0,0 +1,9 @@
module.exports = {
trailingComma: 'es5',
tabWidth: 2,
semi: true,
singleQuote: true,
arrowParen: 'avoid',
arrowParens: 'avoid',
printWidth: 120,
};

View File

@@ -0,0 +1,208 @@
const fp = require('lodash/fp');
const _ = require('lodash');
const sql = require('./sql');
const { DatabaseAnalyser } = require('dbgate-tools');
const { isTypeString, isTypeNumeric } = require('dbgate-tools');
function objectTypeToField(type) {
switch (type.trim()) {
case 'U':
return 'tables';
case 'V':
return 'views';
case 'P':
return 'procedures';
case 'IF':
case 'FN':
case 'TF':
return 'functions';
case 'TR':
return 'triggers';
default:
return null;
}
}
function getColumnInfo({
isNullable,
isIdentity,
columnName,
dataType,
charMaxLength,
numericPrecision,
numericScale,
}) {
let fullDataType = dataType;
if (charMaxLength && isTypeString(dataType)) fullDataType = `${dataType}(${charMaxLength})`;
if (numericPrecision && numericScale && isTypeNumeric(dataType))
fullDataType = `${dataType}(${numericPrecision},${numericScale})`;
return {
columnName,
dataType: fullDataType,
notNull: !isNullable,
autoIncrement: !!isIdentity,
};
}
class MsSqlAnalyser extends DatabaseAnalyser {
constructor(pool, driver) {
super(pool, driver);
this.singleObjectId = null;
}
createQuery(resFileName, typeFields) {
let res = sql[resFileName];
if (this.singleObjectFilter) {
const { typeField } = this.singleObjectFilter;
if (!this.singleObjectId) return null;
if (!typeFields || !typeFields.includes(typeField)) return null;
return res.replace('=[OBJECT_ID_CONDITION]', ` = ${this.singleObjectId}`);
}
if (!this.modifications || !typeFields || this.modifications.length == 0) {
res = res.replace('=[OBJECT_ID_CONDITION]', ' is not null');
} else {
const filterIds = this.modifications
.filter((x) => typeFields.includes(x.objectTypeField) && (x.action == 'add' || x.action == 'change'))
.map((x) => x.objectId);
if (filterIds.length == 0) {
res = res.replace('=[OBJECT_ID_CONDITION]', ' = 0');
} else {
res = res.replace('=[OBJECT_ID_CONDITION]', ` in (${filterIds.join(',')})`);
}
}
return res;
}
async getSingleObjectId() {
if (this.singleObjectFilter) {
const { schemaName, pureName, typeField } = this.singleObjectFilter;
const fullName = schemaName ? `[${schemaName}].[${pureName}]` : pureName;
const resId = await this.driver.query(this.pool, `SELECT OBJECT_ID('${fullName}') AS id`);
this.singleObjectId = resId.rows[0].id;
}
}
async _runAnalysis() {
await this.getSingleObjectId();
const tablesRows = await this.driver.query(this.pool, this.createQuery('tables', ['tables']));
const columnsRows = await this.driver.query(this.pool, this.createQuery('columns', ['tables']));
const pkColumnsRows = await this.driver.query(this.pool, this.createQuery('primaryKeys', ['tables']));
const fkColumnsRows = await this.driver.query(this.pool, this.createQuery('foreignKeys', ['tables']));
const schemaRows = await this.driver.query(this.pool, this.createQuery('getSchemas'));
const schemas = schemaRows.rows;
const sqlCodeRows = await this.driver.query(
this.pool,
this.createQuery('loadSqlCode', ['views', 'procedures', 'functions', 'triggers'])
);
const getCreateSql = (row) =>
sqlCodeRows.rows
.filter((x) => x.pureName == row.pureName && x.schemaName == row.schemaName)
.map((x) => x.codeText)
.join('');
const viewsRows = await this.driver.query(this.pool, this.createQuery('views', ['views']));
const programmableRows = await this.driver.query(
this.pool,
this.createQuery('programmables', ['procedures', 'functions'])
);
const viewColumnRows = await this.driver.query(this.pool, this.createQuery('viewColumns', ['views']));
const tables = tablesRows.rows.map((row) => ({
...row,
columns: columnsRows.rows.filter((col) => col.objectId == row.objectId).map(getColumnInfo),
primaryKey: DatabaseAnalyser.extractPrimaryKeys(row, pkColumnsRows.rows),
foreignKeys: DatabaseAnalyser.extractForeignKeys(row, fkColumnsRows.rows),
}));
const views = viewsRows.rows.map((row) => ({
...row,
createSql: getCreateSql(row),
columns: viewColumnRows.rows.filter((col) => col.objectId == row.objectId).map(getColumnInfo),
}));
const procedures = programmableRows.rows
.filter((x) => x.sqlObjectType.trim() == 'P')
.map((row) => ({
...row,
createSql: getCreateSql(row),
}));
const functions = programmableRows.rows
.filter((x) => ['FN', 'IF', 'TF'].includes(x.sqlObjectType.trim()))
.map((row) => ({
...row,
createSql: getCreateSql(row),
}));
return this.mergeAnalyseResult({
tables,
views,
procedures,
functions,
schemas,
});
}
getDeletedObjectsForField(idArray, objectTypeField) {
return this.structure[objectTypeField]
.filter((x) => !idArray.includes(x.objectId))
.map((x) => ({
oldName: _.pick(x, ['schemaName', 'pureName']),
objectId: x.objectId,
action: 'remove',
objectTypeField,
}));
}
getDeletedObjects(idArray) {
return [
...this.getDeletedObjectsForField(idArray, 'tables'),
...this.getDeletedObjectsForField(idArray, 'views'),
...this.getDeletedObjectsForField(idArray, 'procedures'),
...this.getDeletedObjectsForField(idArray, 'functions'),
...this.getDeletedObjectsForField(idArray, 'triggers'),
];
}
async getModifications() {
const modificationsQueryData = await this.driver.query(this.pool, this.createQuery('modifications'));
// console.log('MOD - SRC', modifications);
// console.log(
// 'MODs',
// this.structure.tables.map((x) => x.modifyDate)
// );
const modifications = modificationsQueryData.rows.map((x) => {
const { type, objectId, modifyDate, schemaName, pureName } = x;
const field = objectTypeToField(type);
if (!this.structure[field]) return null;
// @ts-ignore
const obj = this.structure[field].find((x) => x.objectId == objectId);
// object not modified
if (obj && Math.abs(new Date(modifyDate).getTime() - new Date(obj.modifyDate).getTime()) < 1000) return null;
/** @type {import('dbgate-types').DatabaseModification} */
const action = obj
? {
newName: { schemaName, pureName },
oldName: _.pick(obj, ['schemaName', 'pureName']),
action: 'change',
objectTypeField: field,
objectId,
}
: {
newName: { schemaName, pureName },
action: 'add',
objectTypeField: field,
objectId,
};
return action;
});
return [..._.compact(modifications), ...this.getDeletedObjects(modificationsQueryData.rows.map((x) => x.objectId))];
}
}
module.exports = MsSqlAnalyser;

View File

@@ -0,0 +1,34 @@
const { createBulkInsertStreamBase } = require('dbgate-tools');
function runBulkInsertBatch(pool, tableName, writable, rows) {
return new Promise((resolve, reject) => {
const tableMgr = pool.tableMgr();
tableMgr.bind(tableName, bulkMgr => {
bulkMgr.insertRows(rows, err => {
if (err) reject(err);
resolve();
});
});
});
}
/**
*
* @param {import('dbgate-types').EngineDriver} driver
*/
function createNativeBulkInsertStream(driver, stream, pool, name, options) {
const writable = createBulkInsertStreamBase(driver, stream, pool, name, options);
const fullName = name.schemaName ? `[${name.schemaName}].[${name.pureName}]` : name.pureName;
writable.send = async () => {
const rows = writable.buffer;
writable.buffer = [];
await runBulkInsertBatch(pool, fullName, writable, rows);
};
return writable;
}
module.exports = createNativeBulkInsertStream;

View File

@@ -0,0 +1,70 @@
const { createBulkInsertStreamBase } = require('dbgate-tools');
const tedious = require('tedious');
const getConcreteType = require('./getConcreteType');
function runBulkInsertBatch(pool, tableName, writable, rows) {
return new Promise((resolve, reject) => {
var options = { keepNulls: true };
// instantiate - provide the table where you'll be inserting to, options and a callback
var bulkLoad = pool.newBulkLoad(tableName, options, (error, rowCount) => {
if (error) reject(error);
else resolve();
});
for (const column of writable.columnNames) {
const tcol = writable.templateColumns.find((x) => x.columnName == column);
bulkLoad.addColumn(
column,
tcol
? getConcreteType(tcol.driverNativeColumn.type, tcol.driverNativeColumn.dataLength)
: tedious.TYPES.NVarChar,
{
nullable: tcol ? !tcol.notNull : true,
length: tcol ? tcol.driverNativeColumn.dataLength : undefined,
precision: tcol ? tcol.driverNativeColumn.precision : undefined,
scale: tcol ? tcol.driverNativeColumn.scale : undefined,
}
);
}
for (const row of rows) {
bulkLoad.addRow(row);
}
pool.execBulkLoad(bulkLoad);
});
}
/**
*
* @param {import('dbgate-types').EngineDriver} driver
*/
function createTediousBulkInsertStream(driver, stream, pool, name, options) {
const writable = createBulkInsertStreamBase(driver, stream, pool, name, options);
const fullName = name.schemaName ? `[${name.schemaName}].[${name.pureName}]` : name.pureName;
writable.send = async () => {
if (!writable.templateColumns) {
const fullNameQuoted = name.schemaName
? `${driver.dialect.quoteIdentifier(name.schemaName)}.${driver.dialect.quoteIdentifier(name.pureName)}`
: driver.dialect.quoteIdentifier(name.pureName);
const respTemplate = await driver.query(pool, `SELECT * FROM ${fullNameQuoted} WHERE 1=0`, {
addDriverNativeColumn: true,
});
writable.templateColumns = respTemplate.columns;
}
const rows = writable.buffer;
writable.buffer = [];
await runBulkInsertBatch(pool, fullName, writable, rows);
};
return writable;
}
module.exports = createTediousBulkInsertStream;

View File

@@ -0,0 +1,98 @@
const _ = require('lodash');
const stream = require('stream');
const driverBase = require('../frontend/driver');
const MsSqlAnalyser = require('./MsSqlAnalyser');
const createTediousBulkInsertStream = require('./createTediousBulkInsertStream');
const createNativeBulkInsertStream = require('./createNativeBulkInsertStream');
const AsyncLock = require('async-lock');
const nativeDriver = require('./nativeDriver');
const lock = new AsyncLock();
const { tediousConnect, tediousQueryCore, tediousReadQuery, tediousStream } = require('./tediousDriver');
const { nativeConnect, nativeQueryCore, nativeReadQuery, nativeStream } = nativeDriver;
let msnodesqlv8;
const windowsAuthTypes = [
{
title: 'Windows',
name: 'sspi',
disabledFields: ['password', 'port', 'user'],
},
{
title: 'SQL Server',
name: 'sql',
disabledFields: ['port'],
},
{
title: 'Tedious driver',
name: 'tedious',
},
];
/** @type {import('dbgate-types').EngineDriver} */
const driver = {
...driverBase,
analyserClass: MsSqlAnalyser,
getAuthTypes() {
return msnodesqlv8 ? windowsAuthTypes : null;
},
async connect(conn) {
const { authType } = conn;
if (msnodesqlv8 && (authType == 'sspi' || authType == 'sql')) {
return nativeConnect(conn);
}
return tediousConnect(conn);
},
async queryCore(pool, sql, options) {
if (pool._connectionType == 'msnodesqlv8') {
return nativeQueryCore(pool, sql, options);
} else {
return tediousQueryCore(pool, sql, options);
}
},
async query(pool, sql, options) {
return lock.acquire('connection', async () => {
return this.queryCore(pool, sql, options);
});
},
async stream(pool, sql, options) {
if (pool._connectionType == 'msnodesqlv8') {
return nativeStream(pool, sql, options);
} else {
return tediousStream(pool, sql, options);
}
},
async readQuery(pool, sql, structure) {
if (pool._connectionType == 'msnodesqlv8') {
return nativeReadQuery(pool, sql, structure);
} else {
return tediousReadQuery(pool, sql, structure);
}
},
async writeTable(pool, name, options) {
if (pool._connectionType == 'msnodesqlv8') {
return createNativeBulkInsertStream(this, stream, pool, name, options);
} else {
return createTediousBulkInsertStream(this, stream, pool, name, options);
}
},
async getVersion(pool) {
const { version } = (await this.query(pool, 'SELECT @@VERSION AS version')).rows[0];
return { version };
},
async listDatabases(pool) {
const { rows } = await this.query(pool, 'SELECT name FROM sys.databases order by name');
return rows;
},
};
driver.initialize = dbgateEnv => {
if (dbgateEnv.nativeModules && dbgateEnv.nativeModules.msnodesqlv8) {
msnodesqlv8 = dbgateEnv.nativeModules.msnodesqlv8();
}
nativeDriver.initialize(dbgateEnv);
};
module.exports = driver;

View File

@@ -0,0 +1,41 @@
const tedious = require('tedious');
const { TYPES } = tedious;
const N_TYPES = {
BitN: 0x68,
DateTimeN: 0x6f,
DecimalN: 0x6a,
FloatN: 0x6d,
IntN: 0x26,
MoneyN: 0x6e,
NumericN: 0x6c,
};
function getConcreteType(type, length) {
switch (type.id) {
case N_TYPES.BitN:
return TYPES.Bit;
case N_TYPES.NumericN:
return TYPES.Numeric;
case N_TYPES.DecimalN:
return TYPES.Decimal;
case N_TYPES.IntN:
if (length === 8) return TYPES.BigInt;
if (length === 4) return TYPES.Int;
if (length === 2) return TYPES.SmallInt;
return TYPES.TinyInt;
case N_TYPES.FloatN:
if (length === 8) return TYPES.Float;
return TYPES.Real;
case N_TYPES.MoneyN:
if (length === 8) return TYPES.Money;
return TYPES.SmallMoney;
case N_TYPES.DateTimeN:
if (length === 8) return TYPES.DateTime;
return TYPES.SmallDateTime;
}
return type;
}
module.exports = getConcreteType;

View File

@@ -0,0 +1,9 @@
const driver = require('./driver');
module.exports = {
packageName: 'dbgate-plugin-mssql',
driver,
initialize(dbgateEnv) {
driver.initialize(dbgateEnv);
},
};

View File

@@ -0,0 +1,13 @@
function makeUniqueColumnNames(res) {
const usedNames = new Set();
for (let i = 0; i < res.length; i++) {
if (usedNames.has(res[i].columnName)) {
let suffix = 2;
while (usedNames.has(`${res[i].columnName}${suffix}`)) suffix++;
res[i].columnName = `${res[i].columnName}${suffix}`;
}
usedNames.add(res[i].columnName);
}
}
module.exports = makeUniqueColumnNames;

View File

@@ -0,0 +1,211 @@
const _ = require('lodash');
const stream = require('stream');
const makeUniqueColumnNames = require('./makeUniqueColumnNames');
let msnodesqlv8;
// async function nativeQueryCore(pool, sql, options) {
// if (sql == null) {
// return Promise.resolve({
// rows: [],
// columns: [],
// });
// }
// return new Promise((resolve, reject) => {
// pool.query(sql, (err, rows) => {
// if (err) reject(err);
// resolve({
// rows,
// });
// });
// });
// }
function extractNativeColumns(meta) {
const res = meta.map(col => {
const resCol = {
columnName: col.name,
dataType: col.sqlType.toLowerCase(),
notNull: !col.nullable,
};
if (resCol.dataType.endsWith(' identity')) {
resCol.dataType = resCol.dataType.replace(' identity', '');
resCol.autoIncrement = true;
}
if (col.size && resCol.dataType.includes('char')) {
resCol.dataType += `(${col.size})`;
}
return resCol;
});
makeUniqueColumnNames(res);
return res;
}
async function nativeConnect({ server, port, user, password, database, authType }) {
let connectionString = `server=${server}`;
if (port && !server.includes('\\')) connectionString += `,${port}`;
connectionString += ';Driver={SQL Server Native Client 11.0}';
if (authType == 'sspi') connectionString += ';Trusted_Connection=Yes';
else connectionString += `;UID=${user};PWD=${password}`;
if (database) connectionString += `;Database=${database}`;
return new Promise((resolve, reject) => {
msnodesqlv8.open(connectionString, (err, conn) => {
if (err) reject(err);
conn._connectionType = 'msnodesqlv8';
resolve(conn);
});
});
}
async function nativeQueryCore(pool, sql, options) {
if (sql == null) {
return Promise.resolve({
rows: [],
columns: [],
});
}
return new Promise((resolve, reject) => {
let columns = null;
let currentRow = null;
const q = pool.query(sql);
const rows = [];
q.on('meta', meta => {
columns = extractNativeColumns(meta);
});
q.on('column', (index, data) => {
currentRow[columns[index].columnName] = data;
});
q.on('row', index => {
if (currentRow) rows.push(currentRow);
currentRow = {};
});
q.on('error', err => {
reject(err);
});
q.on('done', () => {
if (currentRow) rows.push(currentRow);
resolve({
columns,
rows,
});
});
});
}
async function nativeReadQuery(pool, sql, structure) {
const pass = new stream.PassThrough({
objectMode: true,
highWaterMark: 100,
});
let columns = null;
let currentRow = null;
const q = pool.query(sql);
q.on('meta', meta => {
columns = extractNativeColumns(meta);
pass.write({
__isStreamHeader: true,
...(structure || { columns }),
});
});
q.on('column', (index, data) => {
currentRow[columns[index].columnName] = data;
});
q.on('row', index => {
if (currentRow) pass.write(currentRow);
currentRow = {};
});
q.on('error', err => {
console.error(err);
pass.end();
});
q.on('done', () => {
if (currentRow) pass.write(currentRow);
pass.end();
});
return pass;
}
async function nativeStream(pool, sql, options) {
const handleInfo = info => {
const { message, lineNumber, procName } = info;
options.info({
message,
line: lineNumber,
procedure: procName,
time: new Date(),
severity: 'info',
});
};
const handleError = error => {
const { message, lineNumber, procName } = error;
options.info({
message,
line: lineNumber,
procedure: procName,
time: new Date(),
severity: 'error',
});
};
let columns = null;
let currentRow = null;
const q = pool.query(sql);
q.on('meta', meta => {
if (currentRow) options.row(currentRow);
currentRow = null;
columns = extractNativeColumns(meta);
options.recordset(columns);
});
q.on('column', (index, data) => {
currentRow[columns[index].columnName] = data;
});
q.on('row', index => {
if (currentRow) options.row(currentRow);
currentRow = {};
});
q.on('error', err => {
handleError(err);
options.done();
});
q.on('info', info => {
handleInfo(info);
});
q.on('done', () => {
if (currentRow) options.row(currentRow);
options.done();
});
}
const initialize = dbgateEnv => {
if (dbgateEnv.nativeModules && dbgateEnv.nativeModules.msnodesqlv8) {
msnodesqlv8 = dbgateEnv.nativeModules.msnodesqlv8();
}
};
module.exports = {
nativeConnect,
nativeQueryCore,
nativeReadQuery,
nativeStream,
initialize,
};

View File

@@ -0,0 +1,20 @@
module.exports = `
select c.name as columnName, t.name as dataType, c.object_id as objectId, c.is_identity as isIdentity,
c.max_length as maxLength, c.precision, c.scale, c.is_nullable as isNullable,
col.CHARACTER_MAXIMUM_LENGTH as charMaxLength,
d.definition as defaultValue, d.name as defaultConstraint,
m.definition as computedExpression, m.is_persisted as isPersisted, c.column_id as columnId,
col.NUMERIC_PRECISION as numericPrecision,
col.NUMERIC_SCALE as numericScale,
-- TODO only if version >= 2008
c.is_sparse as isSparse
from sys.columns c
inner join sys.types t on c.system_type_id = t.system_type_id and c.user_type_id = t.user_type_id
inner join sys.objects o on c.object_id = o.object_id
INNER JOIN sys.schemas u ON u.schema_id=o.schema_id
INNER JOIN INFORMATION_SCHEMA.COLUMNS col ON col.TABLE_NAME = o.name AND col.TABLE_SCHEMA = u.name and col.COLUMN_NAME = c.name
left join sys.default_constraints d on c.default_object_id = d.object_id
left join sys.computed_columns m on m.object_id = c.object_id and m.column_id = c.column_id
where o.type = 'U' and o.object_id =[OBJECT_ID_CONDITION]
order by c.column_id
`;

View File

@@ -0,0 +1,40 @@
module.exports = `
SELECT
schemaName = FK.TABLE_SCHEMA,
pureName = FK.TABLE_NAME,
columnName = CU.COLUMN_NAME,
refSchemaName = ISNULL(IXS.name, PK.TABLE_SCHEMA),
refTableName = ISNULL(IXT.name, PK.TABLE_NAME),
refColumnName = IXCC.name,
constraintName = C.CONSTRAINT_NAME,
updateAction = rc.UPDATE_RULE,
deleteAction = rc.DELETE_RULE,
objectId = o.object_id
FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS C
INNER JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS FK ON C.CONSTRAINT_NAME = FK.CONSTRAINT_NAME
LEFT JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS PK ON C.UNIQUE_CONSTRAINT_NAME = PK.CONSTRAINT_NAME
LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE CU ON C.CONSTRAINT_NAME = CU.CONSTRAINT_NAME
--LEFT JOIN (
--SELECT i1.TABLE_NAME, i2.COLUMN_NAME
--FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS i1
--INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE i2 ON i1.CONSTRAINT_NAME = i2.CONSTRAINT_NAME
--WHERE i1.CONSTRAINT_TYPE = 'PRIMARY KEY'
--) PT ON PT.TABLE_NAME = PK.TABLE_NAME
INNER JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc ON FK.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
LEFT JOIN sys.indexes IX ON IX.name = C.UNIQUE_CONSTRAINT_NAME
LEFT JOIN sys.objects IXT ON IXT.object_id = IX.object_id
LEFT JOIN sys.index_columns IXC ON IX.index_id = IXC.index_id and IX.object_id = IXC.object_id
LEFT JOIN sys.columns IXCC ON IXCC.column_id = IXC.column_id AND IXCC.object_id = IXC.object_id
LEFT JOIN sys.schemas IXS ON IXT.schema_id = IXS.schema_id
inner join sys.objects o on FK.TABLE_NAME = o.name
inner join sys.schemas s on o.schema_id = s.schema_id and FK.TABLE_SCHEMA = s.name
where o.object_id =[OBJECT_ID_CONDITION]
`;

View File

@@ -0,0 +1 @@
module.exports = `select schema_id as objectId, name as schemaName from sys.schemas`;

View File

@@ -0,0 +1,23 @@
const columns = require('./columns');
const foreignKeys = require('./foreignKeys');
const primaryKeys = require('./primaryKeys');
const tables = require('./tables');
const modifications = require('./modifications');
const loadSqlCode = require('./loadSqlCode');
const views = require('./views');
const programmables = require('./programmables');
const viewColumns = require('./viewColumns');
const getSchemas = require('./getSchemas');
module.exports = {
columns,
tables,
foreignKeys,
primaryKeys,
modifications,
loadSqlCode,
views,
programmables,
viewColumns,
getSchemas,
};

View File

@@ -0,0 +1,8 @@
module.exports = `
select s.name as pureName, u.name as schemaName, c.text AS codeText
from sys.objects s
inner join sys.syscomments c on s.object_id = c.id
inner join sys.schemas u on u.schema_id = s.schema_id
where (s.object_id =[OBJECT_ID_CONDITION])
order by u.name, s.name, c.colid
`;

View File

@@ -0,0 +1,6 @@
module.exports = `
select o.object_id as objectId, o.modify_date as modifyDate, o.type, o.name as pureName, s.name as schemaName
from sys.objects o
inner join sys.schemas s on o.schema_id = s.schema_id
where o.type in ('U', 'V', 'P', 'IF', 'FN', 'TF') -- , 'TR' - triggers disabled
`;

View File

@@ -0,0 +1,14 @@
module.exports = `
select o.object_id, pureName = t.Table_Name, schemaName = t.Table_Schema, columnName = c.Column_Name, constraintName=t.constraint_name from
INFORMATION_SCHEMA.TABLE_CONSTRAINTS t,
sys.objects o,
sys.schemas s,
INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE c
where
c.Constraint_Name = t.Constraint_Name
and t.table_name = o.name
and o.schema_id = s.schema_id and t.Table_Schema = s.name
and c.Table_Name = t.Table_Name
and Constraint_Type = 'PRIMARY KEY'
and o.object_id =[OBJECT_ID_CONDITION]
`;

View File

@@ -0,0 +1,6 @@
module.exports = `
select o.name as pureName, s.name as schemaName, o.object_id as objectId, o.create_date as createDate, o.modify_date as modifyDate, o.type as sqlObjectType
from sys.objects o
inner join sys.schemas s on o.schema_id = s.schema_id
where o.type in ('P', 'IF', 'FN', 'TF') and o.object_id =[OBJECT_ID_CONDITION]
`;

View File

@@ -0,0 +1,8 @@
module.exports = `
select
o.name as pureName, s.name as schemaName, o.object_id as objectId,
o.create_date as createDate, o.modify_date as modifyDate
from sys.tables o
inner join sys.schemas s on o.schema_id = s.schema_id
where o.object_id =[OBJECT_ID_CONDITION]
`;

View File

@@ -0,0 +1,18 @@
module.exports = `
select
o.object_id AS objectId,
col.TABLE_SCHEMA as schemaName,
col.TABLE_NAME as pureName,
col.COLUMN_NAME as columnName,
col.IS_NULLABLE as isNullable,
col.DATA_TYPE as dataType,
col.CHARACTER_MAXIMUM_LENGTH as charMaxLength,
col.NUMERIC_PRECISION as precision,
col.NUMERIC_SCALE as scale,
col.COLUMN_DEFAULT
FROM sys.objects o
INNER JOIN sys.schemas u ON u.schema_id=o.schema_id
INNER JOIN INFORMATION_SCHEMA.COLUMNS col ON col.TABLE_NAME = o.name AND col.TABLE_SCHEMA = u.name
WHERE o.type in ('V') and o.object_id =[OBJECT_ID_CONDITION]
order by col.ORDINAL_POSITION
`;

View File

@@ -0,0 +1,10 @@
module.exports = `
SELECT
o.name as pureName,
u.name as schemaName,
o.object_id as objectId,
o.create_date as createDate,
o.modify_date as modifyDate
FROM sys.objects o INNER JOIN sys.schemas u ON u.schema_id=o.schema_id
WHERE type in ('V') and o.object_id =[OBJECT_ID_CONDITION]
`;

View File

@@ -0,0 +1,180 @@
const _ = require('lodash');
const stream = require('stream');
const tedious = require('tedious');
const makeUniqueColumnNames = require('./makeUniqueColumnNames');
function extractTediousColumns(columns, addDriverNativeColumn = false) {
const res = columns.map(col => {
const resCol = {
columnName: col.colName,
dataType: col.type.name.toLowerCase(),
driverNativeColumn: addDriverNativeColumn ? col : undefined,
notNull: !(col.flags & 0x01),
autoIncrement: !!(col.flags & 0x10),
};
if (col.dataLength) resCol.dataType += `(${col.dataLength})`;
return resCol;
});
makeUniqueColumnNames(res);
return res;
}
async function tediousConnect({ server, port, user, password, database, ssl }) {
return new Promise((resolve, reject) => {
const connection = new tedious.Connection({
server,
authentication: {
type: 'default',
options: {
userName: user,
password: password,
},
},
options: {
encrypt: !!ssl,
cryptoCredentialsDetails: ssl ? _.pick(ssl, ['ca', 'cert', 'key']) : undefined,
trustServerCertificate: ssl ? (!ssl.ca && !ssl.cert && !ssl.key ? true : ssl.rejectUnauthorized) : undefined,
enableArithAbort: true,
validateBulkLoadParameters: false,
requestTimeout: 1000 * 3600,
database,
port: port ? parseInt(port) : undefined,
},
});
connection.on('connect', function (err) {
if (err) {
reject(err);
}
connection._connectionType = 'tedious';
resolve(connection);
});
connection.connect();
});
}
async function tediousQueryCore(pool, sql, options) {
if (sql == null) {
return Promise.resolve({
rows: [],
columns: [],
});
}
const { addDriverNativeColumn } = options || {};
return new Promise((resolve, reject) => {
const result = {
rows: [],
columns: [],
};
const request = new tedious.Request(sql, (err, rowCount) => {
if (err) reject(err);
else resolve(result);
});
request.on('columnMetadata', function (columns) {
result.columns = extractTediousColumns(columns, addDriverNativeColumn);
});
request.on('row', function (columns) {
result.rows.push(
_.zipObject(
result.columns.map(x => x.columnName),
columns.map(x => x.value)
)
);
});
pool.execSql(request);
});
}
async function tediousReadQuery(pool, sql, structure) {
const pass = new stream.PassThrough({
objectMode: true,
highWaterMark: 100,
});
let currentColumns = [];
const request = new tedious.Request(sql, (err, rowCount) => {
if (err) console.error(err);
pass.end();
});
request.on('columnMetadata', function (columns) {
currentColumns = extractTediousColumns(columns);
pass.write({
__isStreamHeader: true,
...(structure || { columns: currentColumns }),
});
});
request.on('row', function (columns) {
const row = _.zipObject(
currentColumns.map(x => x.columnName),
columns.map(x => x.value)
);
pass.write(row);
});
pool.execSql(request);
return pass;
}
async function tediousStream(pool, sql, options) {
let currentColumns = [];
const handleInfo = info => {
const { message, lineNumber, procName } = info;
options.info({
message,
line: lineNumber,
procedure: procName,
time: new Date(),
severity: 'info',
});
};
const handleError = error => {
const { message, lineNumber, procName } = error;
options.info({
message,
line: lineNumber,
procedure: procName,
time: new Date(),
severity: 'error',
});
};
pool.on('infoMessage', handleInfo);
pool.on('errorMessage', handleError);
const request = new tedious.Request(sql, (err, rowCount) => {
// if (err) reject(err);
// else resolve(result);
options.done();
pool.off('infoMessage', handleInfo);
pool.off('errorMessage', handleError);
options.info({
message: `${rowCount} rows affected`,
time: new Date(),
severity: 'info',
});
});
request.on('columnMetadata', function (columns) {
currentColumns = extractTediousColumns(columns);
options.recordset(currentColumns);
});
request.on('row', function (columns) {
const row = _.zipObject(
currentColumns.map(x => x.columnName),
columns.map(x => x.value)
);
options.row(row);
});
pool.execSqlBatch(request);
}
module.exports = {
tediousConnect,
tediousQueryCore,
tediousReadQuery,
tediousStream,
};

View File

@@ -0,0 +1,113 @@
const { SqlDumper } = require('dbgate-tools');
class MsSqlDumper extends SqlDumper {
autoIncrement() {
this.put(' ^identity');
}
putStringValue(value) {
if (/[^\u0000-\u00ff]/.test(value)) {
this.putRaw('N');
}
super.putStringValue(value);
}
allowIdentityInsert(table, allow) {
this.putCmd('^set ^identity_insert %f %k', table, allow ? 'on' : 'off');
}
/** @param type {import('dbgate-types').TransformType} */
transform(type, dumpExpr) {
switch (type) {
case 'GROUP:YEAR':
case 'YEAR':
this.put('^datepart(^year, %c)', dumpExpr);
break;
case 'MONTH':
this.put('^datepart(^month, %c)', dumpExpr);
break;
case 'DAY':
this.put('^datepart(^day, %c)', dumpExpr);
break;
case 'GROUP:MONTH':
this.put(
"^convert(^varchar(100), ^datepart(^year, %c)) + '-' + right('0' + ^convert(^varchar(100), ^datepart(^month, %c)), 2)",
dumpExpr,
dumpExpr
);
break;
case 'GROUP:DAY':
this.put(
"^^convert(^varchar(100), ^datepart(^year, %c)) + '-' + ^right('0' + ^convert(^varchar(100), ^datepart(^month, %c)), 2)+'-' + ^right('0' + ^convert(^varchar(100), ^datepart(^day, %c)), 2)",
dumpExpr,
dumpExpr,
dumpExpr
);
break;
default:
dumpExpr();
break;
}
}
renameObject(obj, newname) {
this.putCmd("^execute sp_rename '%f', '%s', 'OBJECT'", obj, newname);
}
changeObjectSchema(obj, newschema) {
this.putCmd("^execute sp_changeobjectowner '%f', '%s'", obj, newschema);
}
dropTable(obj, options = {}) {
if (options.testIfExists) {
this.put("IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'%f') AND type in (N'U'))&n", obj);
}
super.dropTable(obj, options);
}
dropDefault(col) {
if (col.defaultConstraint) {
this.putCmd("^alter ^table %f ^drop ^constraint %i", col, col.defaultConstraint);
}
}
guessDefaultName(col) {
return col.defaultConstraint || `DF${col.schemaName || 'dbo'}_${col.pureName}_col.columnName`
}
createDefault(col) {
if (!col.defaultValue) return;
const defsql = col.defaultValue;
if (!defsql) {
const defname = this.guessDefaultName(col);
this.putCmd("^alter ^table %f ^add ^constraint %i ^default %s for %i", col, defname, defsql, col.columnName);
}
}
renameColumn(column, newcol) {
this.putCmd("^execute sp_rename '%f.%i', '%s', 'COLUMN'", column, column.columnName, newcol);
}
renameConstraint(cnt, newname) {
if (cnt.constraintType == 'index') this.putCmd("^execute sp_rename '%f.%i', '%s', 'INDEX'", cnt, cnt.constraintName, newname);
else this.putCmd("^execute sp_rename '%f', '%s', 'OBJECT'", { schemaName: cnt.schemaName, pureName: cnt.constraintName }, newname);
}
}
MsSqlDumper.prototype.renameView = MsSqlDumper.prototype.renameObject;
MsSqlDumper.prototype.changeViewSchema = MsSqlDumper.prototype.changeObjectSchema;
MsSqlDumper.prototype.renameProcedure = MsSqlDumper.prototype.renameObject;
MsSqlDumper.prototype.changeProcedureSchema = MsSqlDumper.prototype.changeObjectSchema;
MsSqlDumper.prototype.renameFunction = MsSqlDumper.prototype.renameObject;
MsSqlDumper.prototype.changeFunctionSchema = MsSqlDumper.prototype.changeObjectSchema;
MsSqlDumper.prototype.renameTrigger = MsSqlDumper.prototype.renameObject;
MsSqlDumper.prototype.changeTriggerSchema = MsSqlDumper.prototype.changeObjectSchema;
MsSqlDumper.prototype.renameTable = MsSqlDumper.prototype.renameObject;
MsSqlDumper.prototype.changeTableSchema = MsSqlDumper.prototype.changeObjectSchema;
module.exports = MsSqlDumper;

View File

@@ -0,0 +1,29 @@
const { driverBase } = require('dbgate-tools');
const MsSqlDumper = require('./MsSqlDumper');
/** @type {import('dbgate-types').SqlDialect} */
const dialect = {
limitSelect: true,
rangeSelect: true,
offsetFetchRangeSyntax: true,
stringEscapeChar: "'",
fallbackDataType: 'nvarchar(max)',
explicitDropConstraint: false,
enableConstraintsPerTable: true,
anonymousPrimaryKey: false,
quoteIdentifier(s) {
return `[${s}]`;
},
};
/** @type {import('dbgate-types').EngineDriver} */
const driver = {
...driverBase,
dumperClass: MsSqlDumper,
dialect,
engine: 'mssql@dbgate-plugin-mssql',
title: 'Microsoft SQL Server',
defaultPort: 1433,
};
module.exports = driver;

View File

@@ -0,0 +1,5 @@
import driver from './driver';
export default {
driver,
};

View File

@@ -0,0 +1,22 @@
var webpack = require('webpack');
var path = require('path');
var config = {
context: __dirname + '/src/backend',
entry: {
app: './index.js',
},
target: 'node',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'backend.js',
libraryTarget: 'commonjs2',
},
// optimization: {
// minimize: false,
// },
};
module.exports = config;

View File

@@ -0,0 +1,23 @@
var webpack = require("webpack");
var path = require("path");
var config = {
context: __dirname + "/src/frontend",
entry: {
app: "./index.js",
},
target: "web",
output: {
path: path.resolve(__dirname, "dist"),
filename: "frontend.js",
libraryTarget: "var",
library: 'plugin',
},
// optimization: {
// minimize: false,
// },
};
module.exports = config;

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Jan Prochazka
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,6 @@
[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
[![NPM version](https://img.shields.io/npm/v/dbgate-plugin-mysql.svg)](https://www.npmjs.com/package/dbgate-plugin-mysql)
# dbgate-plugin-mysql
Use DbGate for install of this plugin

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<path style="fill:#F2F2F2;" d="M512,256c0,36.937-7.826,72.056-21.911,103.769c-2.288,5.183-4.765,10.271-7.387,15.266
C439.85,456.474,354.408,512,256,512c-103.685,0-192.972-61.639-233.221-150.277c-7.022-15.464-12.549-31.744-16.394-48.661
C2.205,294.713,0,275.613,0,256C0,114.615,114.615,0,256,0c115.012,0,212.302,75.839,244.611,180.245
c1.024,3.323,1.985,6.687,2.884,10.073v0.01c1.672,6.332,3.114,12.737,4.305,19.247c0.554,3.03,1.055,6.071,1.494,9.132
c0.303,2.048,0.585,4.096,0.825,6.165c0.46,3.699,0.825,7.429,1.118,11.18C511.739,242.636,512,249.292,512,256z"/>
<g>
<path style="fill:#5181A2;" d="M367.961,77.614c0,0,7.42,7.632,8.691,13.355c0,0,5.3-4.028,1.696-10.387
C374.744,74.222,367.961,77.614,367.961,77.614z"/>
<path style="fill:#5181A2;" d="M503.495,190.318c-6.666-3.354-12.727-5.339-17.471-6.51c-6.416-1.588-11.609-6.311-13.855-12.528
c-2.884-7.973-8.255-20.888-18.139-40.458c-19.508-38.578-41.127-50.448-55.327-60.416c-9.32-6.541-21.567-7.868-28.881-8.014
c-3.688-0.073-7.241-1.421-10.073-3.793c-7.45-6.269-22.1-13.636-26.76-10.877c-5.726,3.396,0,11.243,8.046,21.201
c8.056,9.968,9.759,20.992,15.475,35.83c2.644,6.844,5.831,10.982,8.485,13.448c2.779,2.591,3.688,6.614,2.215,10.104
c-5.371,12.696-7.168,28.776-4.127,43.645c3.605,17.596,11.87,17.387,11.87,17.387c-0.846-23.531,11.452-33.708,11.452-33.708
c11.233,34.335,36.885,59.988,36.885,59.988c-20.773-7.419-36.247-41.336-36.247-41.336s-1.693,1.693-3.177,12.936
c-1.484,11.233-15.057,18.014-25.015-6.572c-9.968-24.597,2.967-55.547,2.967-55.547c-5.726-2.968-17.596-30.312-17.596-34.555
c0-4.232-4.671-13.563-10.386-19.289c-5.726-5.716-16.959-22.256-3.814-29.884c13.134-7.638,39.215,12.925,39.215,12.925
c41.127-1.484,83.519,48.546,90.3,66.351c6.781,17.805,25.861,53.426,25.861,53.426c5.413,1.641,10.501,3.762,15.214,6.175
C501.635,183.568,502.596,186.932,503.495,190.318z"/>
<path style="fill:#5181A2;" d="M511.237,236.053c-0.209-0.094-0.408-0.199-0.606-0.293c-13.145-6.363-18.442-19.717-18.442-19.717
c3.406-3.406,9.446-5.36,15.611-6.468c0.554,3.03,1.055,6.071,1.494,9.132c-2.299,0.449-3.751,0.721-3.751,0.721
c0,1.296,1.808,3.229,4.577,5.444C510.579,228.571,510.945,232.302,511.237,236.053z"/>
</g>
<g>
<path style="fill:#FFA600;" d="M133.371,374.659H113.34c0,0,0-45.777-6.677-100.153L75.18,374.659H58.964l-31.953-99.673
c0,0-3.72,29.288-4.232,86.737c-7.022-15.464-12.549-31.744-16.394-48.661c1.473-21.264,3.542-44.44,6.322-63.352h26.07
l29.727,92.526l31.483-92.526h23.845C123.831,249.71,133.371,331.264,133.371,374.659z"/>
<path style="fill:#FFA600;" d="M144.423,283.452l20.668-0.318c0,0,23.212,69.637,20.987,78.222c0,0,14.945-41.655,18.761-78.222
h20.35c0,0-10.812,73.771-43.88,112.564c-12.375,14.517-26.262,14.695-38.799,9.236v-11.143c0,0,17.177,6.042,26.081-6.678
c7.168-10.24,0.322-27.898-5.938-46.671C161.135,335.896,144.423,283.452,144.423,283.452z"/>
</g>
<g>
<path style="fill:#5181A2;" d="M237.908,365.491l5.405-10.812c0,0,37.204,16.534,46.742,0c9.539-16.534,0.318-25.438-18.442-35.296
c-18.76-9.858-38.475-22.258-29.89-49.287c8.586-27.028,54.056-25.12,67.093-12.083l-4.451,10.493c0,0-33.705-14.309-42.926,4.451
c-9.221,18.76,15.263,27.664,20.987,30.208c5.724,2.544,37.521,15.58,34.024,38.793s-17.807,37.521-50.558,34.659
C254.124,373.758,247.447,371.85,237.908,365.491z"/>
<path style="fill:#5181A2;" d="M405.452,367c14.054-11.295,20.083-31.389,20.083-54.293c0-35.328-10.167-63.969-50.333-63.969
c-34.586,0-50.333,28.641-50.333,63.969s11.515,63.969,50.333,63.969c5.622,0,10.721-0.7,15.318-2.017l29.905,17.544l7.314-14.315
L405.452,367z M375.202,361.357c-22.507,0-29.184-21.786-29.184-48.65c0-26.875,6.363-48.65,29.184-48.65
c23.28,0,29.174,21.776,29.174,48.65C404.375,339.571,398.482,361.357,375.202,361.357z"/>
<path style="fill:#5181A2;" d="M490.088,359.769c-2.288,5.183-4.765,10.271-7.387,15.266h-39.382V250.378h19.717v109.39H490.088z"
/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -0,0 +1,39 @@
{
"name": "dbgate-plugin-mysql",
"main": "dist/backend.js",
"version": "1.2.2",
"homepage": "https://github.com/dbgate/dbgate-plugin-mysql",
"description": "MySQL connect plugin for DbGate",
"repository": {
"type": "git",
"url": "https://github.com/dbgate/dbgate-plugin-mysql.git"
},
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
"author": "Jan Prochazka",
"license": "MIT",
"keywords": [
"sql",
"dbgate",
"dbgateplugin",
"mysql"
],
"files": [
"dist"
],
"scripts": {
"build:frontend": "webpack --config webpack-frontend.config",
"build:backend": "webpack --config webpack-backend.config.js",
"build": "yarn build:frontend && yarn build:backend",
"plugin": "yarn build && yarn pack && dbgate-plugin dbgate-plugin-mysql",
"plugout": "dbgate-plugout dbgate-plugin-mysql",
"prepublishOnly": "yarn build"
},
"devDependencies": {
"@verycrazydog/mysql-parser": "^1.2.0",
"dbgate-plugin-tools": "^1.0.4",
"dbgate-tools": "^4.0.3-rc.1",
"mysql2": "^2.2.5",
"webpack": "^4.42.0",
"webpack-cli": "^3.3.11"
}
}

View File

@@ -0,0 +1,9 @@
module.exports = {
trailingComma: 'es5',
tabWidth: 2,
semi: true,
singleQuote: true,
arrowParen: 'avoid',
arrowParens: 'avoid',
printWidth: 120,
};

View File

@@ -0,0 +1,222 @@
const fp = require('lodash/fp');
const _ = require('lodash');
const sql = require('./sql');
const { DatabaseAnalyser } = require('dbgate-tools');
const { isTypeString, isTypeNumeric } = require('dbgate-tools');
const { rangeStep } = require('lodash/fp');
function getColumnInfo({
isNullable,
extra,
columnName,
dataType,
charMaxLength,
numericPrecision,
numericScale,
defaultValue,
}) {
let fullDataType = dataType;
if (charMaxLength && isTypeString(dataType)) fullDataType = `${dataType}(${charMaxLength})`;
if (numericPrecision && numericScale && isTypeNumeric(dataType))
fullDataType = `${dataType}(${numericPrecision},${numericScale})`;
return {
notNull: !isNullable || isNullable == 'NO' || isNullable == 'no',
autoIncrement: extra && extra.toLowerCase().includes('auto_increment'),
columnName,
dataType: fullDataType,
defaultValue,
};
}
function objectTypeToField(type) {
if (type == 'VIEW') return 'views';
if (type == 'BASE TABLE') return 'tables';
return null;
}
class Analyser extends DatabaseAnalyser {
constructor(pool, driver) {
super(pool, driver);
}
createQuery(resFileName, typeFields) {
let res = sql[resFileName];
if (this.singleObjectFilter) {
const { typeField, pureName } = this.singleObjectFilter;
if (!typeFields || !typeFields.includes(typeField)) return null;
res = res.replace('=[OBJECT_NAME_CONDITION]', ` = '${pureName}'`).replace('#DATABASE#', this.pool._database_name);
return res;
}
if (!this.modifications || !typeFields || this.modifications.length == 0) {
res = res.replace('=[OBJECT_NAME_CONDITION]', ' is not null');
} else {
const filterNames = this.modifications
.filter(x => typeFields.includes(x.objectTypeField) && (x.action == 'add' || x.action == 'change'))
.map(x => x.newName && x.newName.pureName)
.filter(Boolean);
if (filterNames.length == 0) {
res = res.replace('=[OBJECT_NAME_CONDITION]', ' IS NULL');
} else {
res = res.replace('=[OBJECT_NAME_CONDITION]', ` in (${filterNames.map(x => `'${x}'`).join(',')})`);
}
}
res = res.replace('#DATABASE#', this.pool._database_name);
return res;
}
getRequestedViewNames(allViewNames) {
if (this.singleObjectFilter) {
const { typeField, pureName } = this.singleObjectFilter;
if (typeField == 'views') return [pureName];
}
if (this.modifications) {
return this.modifications.filter(x => x.objectTypeField == 'views').map(x => x.newName.pureName);
}
return allViewNames;
}
async getViewTexts(allViewNames) {
const res = {};
for (const viewName of this.getRequestedViewNames(allViewNames)) {
const resp = await this.driver.query(this.pool, `SHOW CREATE VIEW \`${viewName}\``);
res[viewName] = resp.rows[0]['Create View'];
}
return res;
}
async _runAnalysis() {
const tables = await this.driver.query(this.pool, this.createQuery('tables', ['tables']));
const columns = await this.driver.query(this.pool, this.createQuery('columns', ['tables', 'views']));
const pkColumns = await this.driver.query(this.pool, this.createQuery('primaryKeys', ['tables']));
const fkColumns = await this.driver.query(this.pool, this.createQuery('foreignKeys', ['tables']));
const views = await this.driver.query(this.pool, this.createQuery('views', ['views']));
const programmables = await this.driver.query(
this.pool,
this.createQuery('programmables', ['procedures', 'functions'])
);
const viewTexts = await this.getViewTexts(views.rows.map(x => x.pureName));
return this.mergeAnalyseResult({
tables: tables.rows.map(table => ({
...table,
objectId: table.pureName,
columns: columns.rows.filter(col => col.pureName == table.pureName).map(getColumnInfo),
primaryKey: DatabaseAnalyser.extractPrimaryKeys(table, pkColumns.rows),
foreignKeys: DatabaseAnalyser.extractForeignKeys(table, fkColumns.rows),
})),
views: views.rows.map(view => ({
...view,
objectId: view.pureName,
columns: columns.rows.filter(col => col.pureName == view.pureName).map(getColumnInfo),
createSql: viewTexts[view.pureName],
requiresFormat: true,
})),
procedures: programmables.rows
.filter(x => x.objectType == 'PROCEDURE')
.map(fp.omit(['objectType']))
.map(x => ({ ...x, objectId: x.pureName })),
functions: programmables.rows
.filter(x => x.objectType == 'FUNCTION')
.map(fp.omit(['objectType']))
.map(x => ({ ...x, objectId: x.pureName })),
});
}
getDeletedObjectsForField(nameArray, objectTypeField) {
return this.structure[objectTypeField]
.filter(x => !nameArray.includes(x.pureName))
.map(x => ({
oldName: _.pick(x, ['pureName']),
action: 'remove',
objectTypeField,
objectId: x.pureName,
}));
}
getDeletedObjects(nameArray) {
return [
...this.getDeletedObjectsForField(nameArray, 'tables'),
...this.getDeletedObjectsForField(nameArray, 'views'),
...this.getDeletedObjectsForField(nameArray, 'procedures'),
...this.getDeletedObjectsForField(nameArray, 'functions'),
...this.getDeletedObjectsForField(nameArray, 'triggers'),
];
}
async getModifications() {
const tableModificationsQueryData = await this.driver.query(this.pool, this.createQuery('tableModifications'));
const procedureModificationsQueryData = await this.driver.query(
this.pool,
this.createQuery('procedureModifications')
);
const functionModificationsQueryData = await this.driver.query(
this.pool,
this.createQuery('functionModifications')
);
const allModifications = _.compact([
...tableModificationsQueryData.rows.map(x => {
if (x.objectType == 'BASE TABLE') return { ...x, objectTypeField: 'tables' };
if (x.objectType == 'VIEW') return { ...x, objectTypeField: 'views' };
return null;
}),
...procedureModificationsQueryData.rows.map(x => ({
objectTypeField: 'procedures',
modifyDate: x.Modified,
pureName: x.Name,
})),
...functionModificationsQueryData.rows.map(x => ({
objectTypeField: 'functions',
modifyDate: x.Modified,
pureName: x.Name,
})),
]);
// console.log('allModifications', allModifications);
// console.log(
// 'DATES',
// this.structure.procedures.map((x) => x.modifyDate)
// );
// console.log('MOD - SRC', modifications);
// console.log(
// 'MODs',
// this.structure.tables.map((x) => x.modifyDate)
// );
const modifications = allModifications.map(x => {
const { objectType, modifyDate, pureName } = x;
const field = objectTypeToField(objectType);
if (!field || !this.structure[field]) return null;
// @ts-ignore
const obj = this.structure[field].find(x => x.pureName == pureName);
// object not modified
if (obj && Math.abs(new Date(modifyDate).getTime() - new Date(obj.modifyDate).getTime()) < 1000) return null;
// console.log('MODIFICATION OF ', field, pureName, modifyDate, obj.modifyDate);
/** @type {import('dbgate-types').DatabaseModification} */
const action = obj
? {
newName: { pureName },
oldName: _.pick(obj, ['pureName']),
action: 'change',
objectTypeField: field,
objectId: pureName,
}
: {
newName: { pureName },
action: 'add',
objectTypeField: field,
objectId: pureName,
};
return action;
});
return [..._.compact(modifications), ...this.getDeletedObjects([...allModifications.map(x => x.pureName)])];
}
}
module.exports = Analyser;

View File

@@ -0,0 +1,186 @@
const _ = require('lodash');
const stream = require('stream');
const driverBase = require('../frontend/driver');
const Analyser = require('./Analyser');
const mysql2 = require('mysql2');
const { createBulkInsertStreamBase, makeUniqueColumnNames } = require('dbgate-tools');
const mysqlSplitter = require('@verycrazydog/mysql-parser');
function extractColumns(fields) {
if (fields) {
const res = fields.map(col => ({
columnName: col.name,
}));
makeUniqueColumnNames(res);
return res;
}
return null;
}
function zipDataRow(rowArray, columns) {
return _.zipObject(
columns.map(x => x.columnName),
rowArray
);
}
async function runQueryItem(connection, sql) {
return new Promise((resolve, reject) => {
connection.query(sql, function (error, results, fields) {
if (error) reject(error);
const columns = extractColumns(fields);
resolve({ rows: results && columns && results.map && results.map(row => zipDataRow(row, columns)), columns });
});
});
}
async function runStreamItem(connection, sql, options) {
return new Promise((resolve, reject) => {
const query = connection.query(sql);
let columns = [];
// const handleInfo = (info) => {
// const { message, lineNumber, procName } = info;
// options.info({
// message,
// line: lineNumber,
// procedure: procName,
// time: new Date(),
// severity: 'info',
// });
// };
const handleEnd = () => {
resolve();
};
const handleRow = row => {
if (row && row.constructor && row.constructor.name == 'OkPacket') {
options.info({
message: `${row.affectedRows} rows affected`,
time: new Date(),
severity: 'info',
});
} else {
if (columns) {
options.row(zipDataRow(row, columns));
}
}
};
const handleFields = fields => {
columns = extractColumns(fields);
if (columns) options.recordset(columns);
};
const handleError = error => {
console.log('ERROR', error);
const { message, lineNumber, procName } = error;
options.info({
message,
line: lineNumber,
procedure: procName,
time: new Date(),
severity: 'error',
});
};
query.on('error', handleError).on('fields', handleFields).on('result', handleRow).on('end', handleEnd);
});
}
/** @type {import('dbgate-types').EngineDriver} */
const driver = {
...driverBase,
analyserClass: Analyser,
async connect({ server, port, user, password, database, ssl }) {
const connection = mysql2.createConnection({
host: server,
port,
user,
password,
database,
ssl,
rowsAsArray: true,
supportBigNumbers: true,
bigNumberStrings: true,
// TODO: test following options
// multipleStatements: true,
// dateStrings: true,
});
connection._database_name = database;
return connection;
},
async query(connection, sql) {
if (sql == null) {
return {
rows: [],
columns: [],
};
}
const sqlSplitted = mysqlSplitter.split(sql);
let res = {
rows: [],
columns: [],
};
for (const sqlItem of sqlSplitted) {
const resultItem = await runQueryItem(connection, sqlItem);
if (resultItem.rows && resultItem.columns && resultItem.columns.length > 0) {
res = resultItem;
}
}
return res;
},
async stream(connection, sql, options) {
const sqlSplitted = mysqlSplitter.split(sql);
for (const sqlItem of sqlSplitted) {
await runStreamItem(connection, sqlItem, options);
}
options.done();
},
async readQuery(connection, sql, structure) {
const query = connection.query(sql);
const pass = new stream.PassThrough({
objectMode: true,
highWaterMark: 100,
});
let columns = [];
query
.on('error', err => {
console.error(err);
pass.end();
})
.on('fields', fields => {
columns = extractColumns(fields);
pass.write({
__isStreamHeader: true,
...(structure || { columns }),
});
})
.on('result', row => pass.write(zipDataRow(row, columns)))
.on('end', () => pass.end());
return pass;
},
async getVersion(connection) {
const { rows } = await this.query(connection, "show variables like 'version'");
const version = rows[0].Value;
return { version };
},
async listDatabases(connection) {
const { rows } = await this.query(connection, 'show databases');
return rows.map(x => ({ name: x.Database }));
},
async writeTable(pool, name, options) {
// @ts-ignore
return createBulkInsertStreamBase(this, stream, pool, name, options);
},
};
module.exports = driver;

View File

@@ -0,0 +1,6 @@
const driver = require('./driver');
module.exports = {
packageName: 'dbgate-plugin-mysql',
driver,
};

View File

@@ -0,0 +1,15 @@
module.exports = `
select
TABLE_NAME as pureName,
COLUMN_NAME as columnName,
IS_NULLABLE as isNullable,
DATA_TYPE as dataType,
CHARACTER_MAXIMUM_LENGTH as charMaxLength,
NUMERIC_PRECISION as numericPrecision,
NUMERIC_SCALE as numericScale,
COLUMN_DEFAULT as defaultValue,
EXTRA as extra
from INFORMATION_SCHEMA.COLUMNS
where TABLE_SCHEMA = '#DATABASE#' and TABLE_NAME =[OBJECT_NAME_CONDITION]
order by ORDINAL_POSITION
`;

View File

@@ -0,0 +1,17 @@
module.exports = `
select
REFERENTIAL_CONSTRAINTS.CONSTRAINT_NAME as constraintName,
REFERENTIAL_CONSTRAINTS.TABLE_NAME as pureName,
REFERENTIAL_CONSTRAINTS.UPDATE_RULE as updateAction,
REFERENTIAL_CONSTRAINTS.DELETE_RULE as deleteAction,
REFERENTIAL_CONSTRAINTS.REFERENCED_TABLE_NAME as refTableName,
KEY_COLUMN_USAGE.COLUMN_NAME as columnName,
KEY_COLUMN_USAGE.REFERENCED_COLUMN_NAME as refColumnName
from INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS
inner join INFORMATION_SCHEMA.KEY_COLUMN_USAGE
on REFERENTIAL_CONSTRAINTS.TABLE_NAME = KEY_COLUMN_USAGE.TABLE_NAME
and REFERENTIAL_CONSTRAINTS.CONSTRAINT_NAME = KEY_COLUMN_USAGE.CONSTRAINT_NAME
and REFERENTIAL_CONSTRAINTS.CONSTRAINT_SCHEMA = KEY_COLUMN_USAGE.CONSTRAINT_SCHEMA
where REFERENTIAL_CONSTRAINTS.CONSTRAINT_SCHEMA = '#DATABASE#' and REFERENTIAL_CONSTRAINTS.TABLE_NAME =[OBJECT_NAME_CONDITION]
order by KEY_COLUMN_USAGE.ORDINAL_POSITION
`;

View File

@@ -0,0 +1,3 @@
module.exports = `
SHOW FUNCTION STATUS WHERE Db = '#DATABASE#'
`;

View File

@@ -0,0 +1,21 @@
const columns = require('./columns');
const tables = require('./tables');
const primaryKeys = require('./primaryKeys');
const foreignKeys = require('./foreignKeys');
const tableModifications = require('./tableModifications');
const views = require('./views');
const programmables = require('./programmables');
const procedureModifications = require('./procedureModifications');
const functionModifications = require('./functionModifications');
module.exports = {
columns,
tables,
primaryKeys,
foreignKeys,
tableModifications,
views,
programmables,
procedureModifications,
functionModifications,
};

View File

@@ -0,0 +1,12 @@
module.exports = `select
TABLE_CONSTRAINTS.CONSTRAINT_NAME as constraintName,
TABLE_CONSTRAINTS.TABLE_NAME as pureName,
KEY_COLUMN_USAGE.COLUMN_NAME as columnName
from INFORMATION_SCHEMA.TABLE_CONSTRAINTS
inner join INFORMATION_SCHEMA.KEY_COLUMN_USAGE
on TABLE_CONSTRAINTS.TABLE_NAME = KEY_COLUMN_USAGE.TABLE_NAME
and TABLE_CONSTRAINTS.CONSTRAINT_NAME = KEY_COLUMN_USAGE.CONSTRAINT_NAME
and TABLE_CONSTRAINTS.CONSTRAINT_SCHEMA = KEY_COLUMN_USAGE.CONSTRAINT_SCHEMA
where TABLE_CONSTRAINTS.CONSTRAINT_SCHEMA = '#DATABASE#' and TABLE_CONSTRAINTS.TABLE_NAME =[OBJECT_NAME_CONDITION] AND TABLE_CONSTRAINTS.CONSTRAINT_TYPE = 'PRIMARY KEY'
order by KEY_COLUMN_USAGE.ORDINAL_POSITION
`;

View File

@@ -0,0 +1,3 @@
module.exports = `
SHOW PROCEDURE STATUS WHERE Db = '#DATABASE#'
`;

View File

@@ -0,0 +1,9 @@
module.exports = `
select
ROUTINE_NAME as pureName,
ROUTINE_TYPE as objectType,
COALESCE(LAST_ALTERED, CREATED) as modifyDate,
ROUTINE_DEFINITION as createSql
from information_schema.routines
where ROUTINE_SCHEMA = '#DATABASE#' and ROUTINE_NAME =[OBJECT_NAME_CONDITION]
`;

View File

@@ -0,0 +1,8 @@
module.exports = `
select
TABLE_NAME as pureName,
TABLE_TYPE as objectType,
case when ENGINE='InnoDB' then CREATE_TIME else coalesce(UPDATE_TIME, CREATE_TIME) end as modifyDate
from information_schema.tables
where TABLE_SCHEMA = '#DATABASE#'
`;

View File

@@ -0,0 +1,7 @@
module.exports = `
select
TABLE_NAME as pureName,
case when ENGINE='InnoDB' then CREATE_TIME else coalesce(UPDATE_TIME, CREATE_TIME) end as modifyDate
from information_schema.tables
where TABLE_SCHEMA = '#DATABASE#' and TABLE_TYPE='BASE TABLE' and TABLE_NAME =[OBJECT_NAME_CONDITION];
`;

View File

@@ -0,0 +1,7 @@
module.exports = `
select
TABLE_NAME as pureName,
coalesce(UPDATE_TIME, CREATE_TIME) as modifyDate
from information_schema.tables
where TABLE_SCHEMA = '#DATABASE#' and TABLE_NAME =[OBJECT_NAME_CONDITION] and TABLE_TYPE = 'VIEW';
`;

View File

@@ -0,0 +1,68 @@
const { SqlDumper } = require('dbgate-tools');
class Dumper extends SqlDumper {
/** @param type {import('dbgate-types').TransformType} */
transform(type, dumpExpr) {
switch (type) {
case 'GROUP:YEAR':
case 'YEAR':
this.put('^year(%c)', dumpExpr);
break;
case 'MONTH':
this.put('^month(%c)', dumpExpr);
break;
case 'DAY':
this.put('^day(%c)', dumpExpr);
break;
case 'GROUP:MONTH':
this.put("^date_format(%c, '%s')", dumpExpr, '%Y-%m');
break;
case 'GROUP:DAY':
this.put("^date_format(%c, '%s')", dumpExpr, '%Y-%m-%d');
break;
default:
dumpExpr();
break;
}
}
renameTable(obj, newName) {
this.putCmd('^rename ^table %f ^to %i', obj, newName);
}
changeColumn(oldcol, newcol, constraints) {
this.put('^alter ^table %f ^change ^column %i %i ', oldcol, oldcol.columnName, newcol.columnName);
this.columnDefinition(newcol, true, true, true);
this.inlineConstraints(constraints);
this.endCommand();
}
renameColumn(column, newcol) {
this.changeColumn(
column,
{
...column,
columnName: newcol,
},
[]
);
}
enableConstraints(table, enabled) {
this.putCmd('^set FOREIGN_KEY_CHECKS = %s', enabled ? '1' : '0');
}
comment(value) {
this.put('/* %s */', value);
}
beginTransaction() {
this.putCmd('^start ^transaction');
}
selectTableIntoNewTable(sourceName, targetName) {
this.putCmd('^create ^table %f (^select * ^from %f)', targetName, sourceName);
}
}
module.exports = Dumper;

View File

@@ -0,0 +1,27 @@
const { driverBase } = require('dbgate-tools');
const Dumper = require('./Dumper');
/** @type {import('dbgate-types').SqlDialect} */
const dialect = {
rangeSelect: true,
stringEscapeChar: '\\',
fallbackDataType: 'longtext',
enableConstraintsPerTable: false,
anonymousPrimaryKey: true,
explicitDropConstraint: true,
quoteIdentifier(s) {
return '`' + s + '`';
},
};
/** @type {import('dbgate-types').EngineDriver} */
const driver = {
...driverBase,
dumperClass: Dumper,
dialect,
engine: 'mysql@dbgate-plugin-mysql',
title: 'MySQL / MariaDB',
defaultPort: 3306,
};
module.exports = driver;

View File

@@ -0,0 +1,6 @@
import driver from './driver';
export default {
packageName: 'dbgate-plugin-mysql',
driver,
};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,23 @@
var webpack = require('webpack');
var path = require('path');
var config = {
context: __dirname + '/src/backend',
entry: {
app: './index.js',
},
target: 'node',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'backend.js',
libraryTarget: 'commonjs2',
},
// uncomment for disable minimalization
// optimization: {
// minimize: false,
// },
};
module.exports = config;

View File

@@ -0,0 +1,24 @@
var webpack = require("webpack");
var path = require("path");
var config = {
context: __dirname + "/src/frontend",
entry: {
app: "./index.js",
},
target: "web",
output: {
path: path.resolve(__dirname, "dist"),
filename: "frontend.js",
libraryTarget: "var",
library: 'plugin',
},
// uncomment for disable minimalization
// optimization: {
// minimize: false,
// },
};
module.exports = config;

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Jan Prochazka
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,6 @@
[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
[![NPM version](https://img.shields.io/npm/v/dbgate-plugin-postgres.svg)](https://www.npmjs.com/package/dbgate-plugin-postgres)
# dbgate-plugin-postgres
Use DbGate for install of this plugin

View File

@@ -0,0 +1,22 @@
<?xml version="1.0"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="432.071pt" height="445.383pt" viewBox="0 0 432.071 445.383" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
<g id="orginal" style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
</g>
<g id="Layer_x0020_3" style="fill-rule:nonzero;clip-rule:nonzero;fill:none;stroke:#FFFFFF;stroke-width:12.4651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;">
<path style="fill:#000000;stroke:#000000;stroke-width:37.3953;stroke-linecap:butt;stroke-linejoin:miter;" d="M323.205,324.227c2.833-23.601,1.984-27.062,19.563-23.239l4.463,0.392c13.517,0.615,31.199-2.174,41.587-7c22.362-10.376,35.622-27.7,13.572-23.148c-50.297,10.376-53.755-6.655-53.755-6.655c53.111-78.803,75.313-178.836,56.149-203.322 C352.514-5.534,262.036,26.049,260.522,26.869l-0.482,0.089c-9.938-2.062-21.06-3.294-33.554-3.496c-22.761-0.374-40.032,5.967-53.133,15.904c0,0-161.408-66.498-153.899,83.628c1.597,31.936,45.777,241.655,98.47,178.31 c19.259-23.163,37.871-42.748,37.871-42.748c9.242,6.14,20.307,9.272,31.912,8.147l0.897-0.765c-0.281,2.876-0.157,5.689,0.359,9.019c-13.572,15.167-9.584,17.83-36.723,23.416c-27.457,5.659-11.326,15.734-0.797,18.367c12.768,3.193,42.305,7.716,62.268-20.224 l-0.795,3.188c5.325,4.26,4.965,30.619,5.72,49.452c0.756,18.834,2.017,36.409,5.856,46.771c3.839,10.36,8.369,37.05,44.036,29.406c29.809-6.388,52.6-15.582,54.677-101.107"/>
<path style="fill:#336791;stroke:none;" d="M402.395,271.23c-50.302,10.376-53.76-6.655-53.76-6.655c53.111-78.808,75.313-178.843,56.153-203.326c-52.27-66.785-142.752-35.2-144.262-34.38l-0.486,0.087c-9.938-2.063-21.06-3.292-33.56-3.496c-22.761-0.373-40.026,5.967-53.127,15.902 c0,0-161.411-66.495-153.904,83.63c1.597,31.938,45.776,241.657,98.471,178.312c19.26-23.163,37.869-42.748,37.869-42.748c9.243,6.14,20.308,9.272,31.908,8.147l0.901-0.765c-0.28,2.876-0.152,5.689,0.361,9.019c-13.575,15.167-9.586,17.83-36.723,23.416 c-27.459,5.659-11.328,15.734-0.796,18.367c12.768,3.193,42.307,7.716,62.266-20.224l-0.796,3.188c5.319,4.26,9.054,27.711,8.428,48.969c-0.626,21.259-1.044,35.854,3.147,47.254c4.191,11.4,8.368,37.05,44.042,29.406c29.809-6.388,45.256-22.942,47.405-50.555 c1.525-19.631,4.976-16.729,5.194-34.28l2.768-8.309c3.192-26.611,0.507-35.196,18.872-31.203l4.463,0.392c13.517,0.615,31.208-2.174,41.591-7c22.358-10.376,35.618-27.7,13.573-23.148z"/>
<path d="M215.866,286.484c-1.385,49.516,0.348,99.377,5.193,111.495c4.848,12.118,15.223,35.688,50.9,28.045c29.806-6.39,40.651-18.756,45.357-46.051c3.466-20.082,10.148-75.854,11.005-87.281"/>
<path d="M173.104,38.256c0,0-161.521-66.016-154.012,84.109c1.597,31.938,45.779,241.664,98.473,178.316c19.256-23.166,36.671-41.335,36.671-41.335"/>
<path d="M260.349,26.207c-5.591,1.753,89.848-34.889,144.087,34.417c19.159,24.484-3.043,124.519-56.153,203.329"/>
<path style="stroke-linejoin:bevel;" d="M348.282,263.953c0,0,3.461,17.036,53.764,6.653c22.04-4.552,8.776,12.774-13.577,23.155c-18.345,8.514-59.474,10.696-60.146-1.069c-1.729-30.355,21.647-21.133,19.96-28.739c-1.525-6.85-11.979-13.573-18.894-30.338 c-6.037-14.633-82.796-126.849,21.287-110.183c3.813-0.789-27.146-99.002-124.553-100.599c-97.385-1.597-94.19,119.762-94.19,119.762"/>
<path d="M188.604,274.334c-13.577,15.166-9.584,17.829-36.723,23.417c-27.459,5.66-11.326,15.733-0.797,18.365c12.768,3.195,42.307,7.718,62.266-20.229c6.078-8.509-0.036-22.086-8.385-25.547c-4.034-1.671-9.428-3.765-16.361,3.994z"/>
<path d="M187.715,274.069c-1.368-8.917,2.93-19.528,7.536-31.942c6.922-18.626,22.893-37.255,10.117-96.339c-9.523-44.029-73.396-9.163-73.436-3.193c-0.039,5.968,2.889,30.26-1.067,58.548c-5.162,36.913,23.488,68.132,56.479,64.938"/>
<path style="fill:#FFFFFF;stroke-width:4.155;stroke-linecap:butt;stroke-linejoin:miter;" d="M172.517,141.7c-0.288,2.039,3.733,7.48,8.976,8.207c5.234,0.73,9.714-3.522,9.998-5.559c0.284-2.039-3.732-4.285-8.977-5.015c-5.237-0.731-9.719,0.333-9.996,2.367z"/>
<path style="fill:#FFFFFF;stroke-width:2.0775;stroke-linecap:butt;stroke-linejoin:miter;" d="M331.941,137.543c0.284,2.039-3.732,7.48-8.976,8.207c-5.238,0.73-9.718-3.522-10.005-5.559c-0.277-2.039,3.74-4.285,8.979-5.015c5.239-0.73,9.718,0.333,10.002,2.368z"/>
<path d="M350.676,123.432c0.863,15.994-3.445,26.888-3.988,43.914c-0.804,24.748,11.799,53.074-7.191,81.435"/>
<path style="stroke-width:3;" d="M0,60.232"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -0,0 +1,39 @@
{
"name": "dbgate-plugin-postgres",
"main": "dist/backend.js",
"version": "1.2.2",
"license": "MIT",
"description": "PostgreSQL connector plugin for DbGate",
"homepage": "https://github.com/dbgate/dbgate-plugin-postgres",
"repository": {
"type": "git",
"url": "https://github.com/dbgate/dbgate-plugin-postgres.git"
},
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
"author": "Jan Prochazka",
"keywords": [
"dbgate",
"dbgateplugin",
"postgresql"
],
"files": [
"dist"
],
"scripts": {
"build:frontend": "webpack --config webpack-frontend.config",
"build:backend": "webpack --config webpack-backend.config.js",
"build": "yarn build:frontend && yarn build:backend",
"plugin": "yarn build && yarn pack && dbgate-plugin dbgate-plugin-postgres",
"plugout": "dbgate-plugout dbgate-plugin-postgres",
"prepublishOnly": "yarn build"
},
"devDependencies": {
"dbgate-plugin-tools": "^1.0.4",
"dbgate-tools": "^4.0.3-rc.1",
"lodash": "^4.17.15",
"pg": "^7.17.0",
"pg-query-stream": "^3.1.1",
"webpack": "^4.42.0",
"webpack-cli": "^3.3.11"
}
}

View File

@@ -0,0 +1,9 @@
module.exports = {
trailingComma: 'es5',
tabWidth: 2,
semi: true,
singleQuote: true,
arrowParen: 'avoid',
arrowParens: 'avoid',
printWidth: 120,
};

View File

@@ -0,0 +1,185 @@
const fp = require('lodash/fp');
const _ = require('lodash');
const sql = require('./sql');
const { DatabaseAnalyser } = require('dbgate-tools');
const { isTypeString, isTypeNumeric } = require('dbgate-tools');
function normalizeTypeName(dataType) {
if (dataType == 'character varying') return 'varchar';
if (dataType == 'timestamp without time zone') return 'timestamp';
return dataType;
}
function getColumnInfo({
isNullable,
isIdentity,
columnName,
dataType,
charMaxLength,
numericPrecision,
numericScale,
defaultValue,
}) {
const normDataType = normalizeTypeName(dataType);
let fullDataType = normDataType;
if (charMaxLength && isTypeString(normDataType)) fullDataType = `${normDataType}(${charMaxLength})`;
if (numericPrecision && numericScale && isTypeNumeric(normDataType))
fullDataType = `${normDataType}(${numericPrecision},${numericScale})`;
return {
columnName,
dataType: fullDataType,
notNull: !isNullable || isNullable == 'NO' || isNullable == 'no',
autoIncrement: !!isIdentity,
defaultValue,
};
}
class Analyser extends DatabaseAnalyser {
constructor(pool, driver) {
super(pool, driver);
}
createQuery(resFileName, typeFields) {
let res = sql[resFileName];
if (this.singleObjectFilter) {
const { typeField, schemaName, pureName } = this.singleObjectFilter;
if (!typeFields || !typeFields.includes(typeField)) return null;
res = res.replace(/=OBJECT_ID_CONDITION/g, ` = '${typeField}:${schemaName || 'public'}.${pureName}'`);
return res;
}
if (!this.modifications || !typeFields || this.modifications.length == 0) {
res = res.replace(/=OBJECT_ID_CONDITION/g, ' is not null');
} else {
const filterNames = this.modifications
.filter(x => typeFields.includes(x.objectTypeField) && (x.action == 'add' || x.action == 'change'))
.filter(x => x.newName)
.map(x => `${x.objectTypeField}:${x.newName.schemaName}.${x.newName.pureName}`);
if (filterNames.length == 0) {
res = res.replace(/=OBJECT_ID_CONDITION/g, ' IS NULL');
} else {
res = res.replace(/=OBJECT_ID_CONDITION/g, ` in (${filterNames.map(x => `'${x}'`).join(',')})`);
}
}
return res;
// let res = sql[resFileName];
// res = res.replace('=[OBJECT_ID_CONDITION]', ' is not null');
// return res;
}
async _runAnalysis() {
const tables = await this.driver.query(this.pool, this.createQuery('tableModifications', ['tables']));
const columns = await this.driver.query(this.pool, this.createQuery('columns', ['tables']));
const pkColumns = await this.driver.query(this.pool, this.createQuery('primaryKeys', ['tables']));
const fkColumns = await this.driver.query(this.pool, this.createQuery('foreignKeys', ['tables']));
const views = await this.driver.query(this.pool, this.createQuery('views', ['views']));
const routines = await this.driver.query(this.pool, this.createQuery('routines', ['procedures', 'functions']));
// console.log('PG fkColumns', fkColumns.rows);
return this.mergeAnalyseResult({
tables: tables.rows.map(table => ({
...table,
objectId: `tables:${table.schemaName}.${table.pureName}`,
columns: columns.rows
.filter(col => col.pureName == table.pureName && col.schemaName == table.schemaName)
.map(getColumnInfo),
primaryKey: DatabaseAnalyser.extractPrimaryKeys(table, pkColumns.rows),
foreignKeys: DatabaseAnalyser.extractForeignKeys(table, fkColumns.rows),
})),
views: views.rows.map(view => ({
...view,
objectId: `views:${view.schemaName}.${view.pureName}`,
columns: columns.rows
.filter(col => col.pureName == view.pureName && col.schemaName == view.schemaName)
.map(getColumnInfo),
})),
procedures: routines.rows
.filter(x => x.objectType == 'PROCEDURE')
.map(proc => ({
objectId: `procedures:${proc.schemaName}.${proc.pureName}`,
...proc,
})),
functions: routines.rows
.filter(x => x.objectType == 'FUNCTION')
.map(func => ({
objectId: `functions:${func.schemaName}.${func.pureName}`,
...func,
})),
});
}
async getModifications() {
const tableModificationsQueryData = await this.driver.query(this.pool, this.createQuery('tableModifications'));
const viewModificationsQueryData = await this.driver.query(this.pool, this.createQuery('viewModifications'));
const routineModificationsQueryData = await this.driver.query(this.pool, this.createQuery('routineModifications'));
const allModifications = _.compact([
...tableModificationsQueryData.rows.map(x => ({ ...x, objectTypeField: 'tables' })),
...viewModificationsQueryData.rows.map(x => ({ ...x, objectTypeField: 'views' })),
...routineModificationsQueryData.rows
.filter(x => x.objectType == 'PROCEDURE')
.map(x => ({ ...x, objectTypeField: 'procedures' })),
...routineModificationsQueryData.rows
.filter(x => x.objectType == 'FUNCTION')
.map(x => ({ ...x, objectTypeField: 'functions' })),
]);
const modifications = allModifications.map(x => {
const { objectTypeField, hashCode, pureName, schemaName } = x;
if (!objectTypeField || !this.structure[objectTypeField]) return null;
const obj = this.structure[objectTypeField].find(x => x.pureName == pureName && x.schemaName == schemaName);
// object not modified
if (obj && obj.hashCode == hashCode) return null;
// console.log('MODIFICATION OF ', objectTypeField, schemaName, pureName);
/** @type {import('dbgate-types').DatabaseModification} */
const action = obj
? {
newName: { schemaName, pureName },
oldName: _.pick(obj, ['schemaName', 'pureName']),
action: 'change',
objectTypeField,
objectId: `${objectTypeField}:${schemaName}.${pureName}`,
}
: {
newName: { schemaName, pureName },
action: 'add',
objectTypeField,
objectId: `${objectTypeField}:${schemaName}.${pureName}`,
};
return action;
});
return [
..._.compact(modifications),
...this.getDeletedObjects([...allModifications.map(x => `${x.schemaName}.${x.pureName}`)]),
];
}
getDeletedObjectsForField(nameArray, objectTypeField) {
return this.structure[objectTypeField]
.filter(x => !nameArray.includes(`${x.schemaName}.${x.pureName}`))
.map(x => ({
oldName: _.pick(x, ['schemaName', 'pureName']),
action: 'remove',
objectTypeField,
objectId: `${objectTypeField}:${x.schemaName}.${x.pureName}`,
}));
}
getDeletedObjects(nameArray) {
return [
...this.getDeletedObjectsForField(nameArray, 'tables'),
...this.getDeletedObjectsForField(nameArray, 'views'),
...this.getDeletedObjectsForField(nameArray, 'procedures'),
...this.getDeletedObjectsForField(nameArray, 'functions'),
...this.getDeletedObjectsForField(nameArray, 'triggers'),
];
}
}
module.exports = Analyser;

View File

@@ -0,0 +1,213 @@
const _ = require('lodash');
const stream = require('stream');
const driverBase = require('../frontend/driver');
const Analyser = require('./Analyser');
const pg = require('pg');
const pgQueryStream = require('pg-query-stream');
const { createBulkInsertStreamBase, splitPostgresQuery, makeUniqueColumnNames } = require('dbgate-tools');
function extractPostgresColumns(result) {
if (!result || !result.fields) return [];
const res = result.fields.map(fld => ({
columnName: fld.name,
}));
makeUniqueColumnNames(res);
return res;
}
function zipDataRow(rowArray, columns) {
return _.zipObject(
columns.map(x => x.columnName),
rowArray
);
}
async function runStreamItem(client, sql, options) {
return new Promise((resolve, reject) => {
const query = new pgQueryStream(sql, undefined, { rowMode: 'array' });
const stream = client.query(query);
// const handleInfo = (info) => {
// const { message, lineNumber, procName } = info;
// options.info({
// message,
// line: lineNumber,
// procedure: procName,
// time: new Date(),
// severity: 'info',
// });
// };
let wasHeader = false;
const handleEnd = result => {
// console.log('RESULT', result);
resolve();
};
let columns = null;
const handleReadable = () => {
if (!wasHeader) {
columns = extractPostgresColumns(query._result);
if (columns && columns.length > 0) {
options.recordset(columns);
}
wasHeader = true;
}
for (;;) {
const row = stream.read();
if (!row) break;
options.row(zipDataRow(row, columns));
}
};
// const handleFields = (columns) => {
// // console.log('FIELDS', columns[0].name);
// options.recordset(columns);
// // options.recordset(extractColumns(columns));
// };
const handleError = error => {
console.log('ERROR', error);
const { message, lineNumber, procName } = error;
options.info({
message,
line: lineNumber,
procedure: procName,
time: new Date(),
severity: 'error',
});
resolve();
};
stream.on('error', handleError);
stream.on('readable', handleReadable);
// stream.on('result', handleRow)
// stream.on('data', handleRow)
stream.on('end', handleEnd);
});
}
/** @type {import('dbgate-types').EngineDriver} */
const driver = {
...driverBase,
analyserClass: Analyser,
async connect({ server, port, user, password, database, ssl }) {
const client = new pg.Client({
host: server,
port,
user,
password,
database: database || 'postgres',
ssl,
});
await client.connect();
return client;
},
async query(client, sql) {
if (sql == null) {
return {
rows: [],
columns: [],
};
}
const res = await client.query({ text: sql, rowMode: 'array' });
const columns = extractPostgresColumns(res);
return { rows: res.rows.map(row => zipDataRow(row, columns)), columns };
},
async stream(client, sql, options) {
const sqlSplitted = splitPostgresQuery(sql);
for (const sqlItem of sqlSplitted) {
await runStreamItem(client, sqlItem, options);
}
options.done();
// return stream;
},
// async analyseSingleObject(pool, name, typeField = 'tables') {
// const analyser = new PostgreAnalyser(pool, this);
// analyser.singleObjectFilter = { ...name, typeField };
// const res = await analyser.fullAnalysis();
// return res.tables[0];
// },
// // @ts-ignore
// analyseSingleTable(pool, name) {
// return this.analyseSingleObject(pool, name, 'tables');
// },
async getVersion(client) {
const { rows } = await this.query(client, 'SELECT version()');
const { version } = rows[0];
return { version };
},
// async analyseFull(pool) {
// const analyser = new PostgreAnalyser(pool, this);
// return analyser.fullAnalysis();
// },
// async analyseIncremental(pool, structure) {
// const analyser = new PostgreAnalyser(pool, this);
// return analyser.incrementalAnalysis(structure);
// },
async readQuery(client, sql, structure) {
const query = new pgQueryStream(sql, undefined, { rowMode: 'array' });
const queryStream = client.query(query);
let wasHeader = false;
const pass = new stream.PassThrough({
objectMode: true,
highWaterMark: 100,
});
const handleEnd = result => {
pass.end();
};
let columns = null;
const handleReadable = () => {
if (!wasHeader) {
columns = extractPostgresColumns(query._result);
pass.write({
__isStreamHeader: true,
...(structure || { columns }),
});
wasHeader = true;
}
for (;;) {
const row = queryStream.read();
if (!row) break;
pass.write(zipDataRow(row, columns));
}
};
const handleError = error => {
console.error(error);
pass.end();
};
queryStream.on('error', handleError);
queryStream.on('readable', handleReadable);
queryStream.on('end', handleEnd);
return pass;
},
// createDumper() {
// return new PostgreDumper(this);
// },
async writeTable(pool, name, options) {
// @ts-ignore
return createBulkInsertStreamBase(this, stream, pool, name, options);
},
async listDatabases(client) {
const { rows } = await this.query(client, 'SELECT datname AS name FROM pg_database WHERE datistemplate = false');
return rows;
},
};
module.exports = driver;

View File

@@ -0,0 +1,6 @@
const driver = require('./driver');
module.exports = {
packageName: 'dbgate-plugin-postgres',
driver,
};

View File

@@ -0,0 +1,19 @@
module.exports = `
select
table_schema as "schemaName",
table_name as "pureName",
column_name as "columnName",
is_nullable as "isNullable",
data_type as "dataType",
character_maximum_length as "charMaxLength",
numeric_precision as "numericPrecision",
numeric_scale as "numericScale",
column_default as "defaultValue"
from information_schema.columns
where
table_schema <> 'information_schema'
and table_schema <> 'pg_catalog'
and table_schema !~ '^pg_toast'
and 'tables:' || table_schema || '.' || table_name =OBJECT_ID_CONDITION
order by ordinal_position
`;

View File

@@ -0,0 +1,24 @@
module.exports = `
select
fk.constraint_name as "constraintName",
fk.constraint_schema as "constraintSchema",
base.table_name as "pureName",
base.table_schema as "schemaName",
fk.update_rule as "updateAction",
fk.delete_rule as "deleteAction",
ref.table_name as "refTableName",
ref.table_schema as "refSchemaName",
basecol.column_name as "columnName",
refcol.column_name as "refColumnName"
from information_schema.referential_constraints fk
inner join information_schema.table_constraints base on fk.constraint_name = base.constraint_name and fk.constraint_schema = base.constraint_schema
inner join information_schema.table_constraints ref on fk.unique_constraint_name = ref.constraint_name and fk.unique_constraint_schema = ref.constraint_schema
inner join information_schema.key_column_usage basecol on base.table_name = basecol.table_name and base.constraint_name = basecol.constraint_name
inner join information_schema.key_column_usage refcol on ref.table_name = refcol.table_name and ref.constraint_name = refcol.constraint_name and basecol.ordinal_position = refcol.ordinal_position
where
base.table_schema <> 'information_schema'
and base.table_schema <> 'pg_catalog'
and base.table_schema !~ '^pg_toast'
and 'tables:' || base.table_schema || '.' || base.table_name =OBJECT_ID_CONDITION
order by basecol.ordinal_position
`;

Some files were not shown because too many files have changed in this diff Show More