Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 688086e00f | |||
| 09498f2ac3 | |||
| 7dd9e9a9b1 | |||
| 19d135e435 | |||
| d453e52ff3 | |||
| 29a77cc053 | |||
| 11fd2d1d8a | |||
| b5fe8508b1 | |||
| 25881e80db | |||
| e43fa96e34 | |||
| 0200c7c78b | |||
| 42e573a3ae | |||
| 395b0a91b0 | |||
| 62c529cf50 | |||
| 00a169725e | |||
| bcf0bfd5ef | |||
| 52ed8874e3 | |||
| 319e08f5f3 | |||
| c4491050cd | |||
| f0ea35d576 | |||
| 70d53e8abe | |||
| 8f28ce3659 | |||
| 050b46813f | |||
| 4bae23ecfa | |||
| 6eb16ad750 | |||
| 482a823f4f | |||
| 9d933d669a | |||
| e44a95d723 | |||
| cae882c8d6 | |||
| 026726a6ed | |||
| 70d06deeb0 | |||
| 6dfe9b798b | |||
| 73c14eba6d | |||
| 7c91dda170 | |||
| 40ebedaef0 | |||
| 614f852f71 | |||
| a8e3a6cfec | |||
| be053acf3c | |||
| 91741655b7 | |||
| c57a67da09 | |||
| 2376cb30db | |||
| 3a08462018 | |||
| c95677bd83 | |||
| 8bffa4a7dd | |||
| 6d7cc7d441 | |||
| acc49273c1 | |||
| 640b53e45f | |||
| 7857771056 | |||
| b8513b3ecd | |||
| 0a56e3b782 | |||
| 87e75c6ba1 | |||
| cf5afb43eb | |||
| 2eb1c04fcf | |||
| 4a4c4b41c0 | |||
| 032eaf9eb0 | |||
| 06a028a093 | |||
| 21ceaecec6 | |||
| c5605d63ca | |||
| f9545eaf7f | |||
| 216ef7736b |
@@ -35,6 +35,9 @@ jobs:
|
||||
- name: fillNativeModulesElectron
|
||||
run: |
|
||||
yarn fillNativeModulesElectron
|
||||
- name: fillPackagedPlugins
|
||||
run: |
|
||||
yarn fillPackagedPlugins
|
||||
- name: Install Snapcraft
|
||||
if: matrix.os == 'ubuntu-18.04'
|
||||
uses: samuelmeuli/action-snapcraft@v1
|
||||
|
||||
@@ -39,6 +39,9 @@ jobs:
|
||||
- name: fillNativeModulesElectron
|
||||
run: |
|
||||
yarn fillNativeModulesElectron
|
||||
- name: fillPackagedPlugins
|
||||
run: |
|
||||
yarn fillPackagedPlugins
|
||||
- name: Install Snapcraft
|
||||
if: matrix.os == 'ubuntu-18.04'
|
||||
uses: samuelmeuli/action-snapcraft@v1
|
||||
|
||||
@@ -119,3 +119,8 @@ jobs:
|
||||
working-directory: plugins/dbgate-plugin-postgres
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
- name: Publish dbgate-plugin-sqlite
|
||||
working-directory: plugins/dbgate-plugin-sqlite
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
@@ -30,4 +30,5 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
app/src/nativeModulesContent.js
|
||||
packages/api/src/nativeModulesContent.js
|
||||
packages/api/src/packagedPluginsContent.js
|
||||
.VSCodeCounter
|
||||
@@ -1,5 +1,26 @@
|
||||
# ChangeLog
|
||||
|
||||
### 4.2.2
|
||||
- CHANGED: Further startup optimalization (approx. 2 times quicker start of electron app)
|
||||
|
||||
### 4.2.1
|
||||
- FIXED: Fixed+optimalized app startup (esp. on Windows)
|
||||
|
||||
### 4.2.0
|
||||
- ADDED: Support of SQLite database
|
||||
- ADDED: Support of Amazon Redshift database
|
||||
- ADDED: Support of CockcroachDB
|
||||
- CHANGED: DB Model is not auto-refreshed by default, refresh could be invoked from statusbar
|
||||
- FIXED: Fixed race conditions on startup
|
||||
- FIXED: Fixed broken style in data grid under strange circumstances
|
||||
- ADDED: Configure connections with commandline arguments #108
|
||||
- CHANGED: Optimalized algorithm of incremental DB model updates
|
||||
- CHANGED: Loading queries from PostgreSQL doesn't need cursors, using streamed query instead
|
||||
- ADDED: Disconnect command
|
||||
- ADDED: Query executed on server has tab marker (formerly it had only "No DB" marker)
|
||||
- ADDED: Horizontal scroll using shift+mouse wheel #113
|
||||
- ADDED: Cosmetic improvements of MariaDB support
|
||||
|
||||
### 4.1.11
|
||||
- FIX: Fixed crash of API process when using SSH tunnel connection (race condition)
|
||||
|
||||
|
||||
@@ -17,11 +17,14 @@ Supported databases:
|
||||
* PostgreSQL
|
||||
* SQL Server
|
||||
* MongoDB
|
||||
* SQLite
|
||||
* Amazon Redshift
|
||||
* CockroachDB
|
||||
* MariaDB
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
* Connect to Microsoft SQL Server, Postgre SQL, MySQL, MongoDB
|
||||
* Table data editing, with SQL change script preview
|
||||
* Master/detail views
|
||||
* Query designer
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@
|
||||
"author": "Jan Prochazka <jenasoft.database@gmail.com>",
|
||||
"description": "Opensource database administration tool",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^7.3.1",
|
||||
"better-sqlite3-with-prebuilds": "^7.1.8",
|
||||
"electron-log": "^4.3.1",
|
||||
"electron-store": "^5.1.1",
|
||||
"electron-updater": "^4.3.5"
|
||||
@@ -39,7 +39,7 @@
|
||||
"icon": "icon.png",
|
||||
"artifactName": "dbgate-linux-${version}.${ext}",
|
||||
"category": "Development",
|
||||
"synopsis": "Database administration tool for MS SQL, MySQL and PostgreSQL",
|
||||
"synopsis": "Database manager for SQL Server, MySQL, PostgreSQL, MongoDB and SQLite",
|
||||
"publish": [
|
||||
"github"
|
||||
]
|
||||
|
||||
+1
-25
@@ -19,7 +19,6 @@ const store = new Store();
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
let mainWindow;
|
||||
let splashWindow;
|
||||
let mainMenu;
|
||||
|
||||
log.transports.file.level = 'debug';
|
||||
@@ -29,14 +28,6 @@ autoUpdater.logger = log;
|
||||
|
||||
let commands = {};
|
||||
|
||||
function hideSplash() {
|
||||
if (splashWindow) {
|
||||
splashWindow.destroy();
|
||||
splashWindow = null;
|
||||
}
|
||||
mainWindow.show();
|
||||
}
|
||||
|
||||
function commandItem(id) {
|
||||
const command = commands[id];
|
||||
return {
|
||||
@@ -156,7 +147,6 @@ function createWindow() {
|
||||
title: 'DbGate',
|
||||
...bounds,
|
||||
icon: os.platform() == 'win32' ? 'icon.ico' : path.resolve(__dirname, '../icon.png'),
|
||||
show: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true,
|
||||
@@ -175,7 +165,7 @@ function createWindow() {
|
||||
slashes: true,
|
||||
});
|
||||
mainWindow.webContents.on('did-finish-load', function () {
|
||||
hideSplash();
|
||||
// hideSplash();
|
||||
});
|
||||
mainWindow.on('close', () => {
|
||||
store.set('winBounds', mainWindow.getBounds());
|
||||
@@ -186,20 +176,6 @@ function createWindow() {
|
||||
}
|
||||
}
|
||||
|
||||
splashWindow = new BrowserWindow({
|
||||
width: 300,
|
||||
height: 120,
|
||||
transparent: true,
|
||||
frame: false,
|
||||
});
|
||||
splashWindow.loadURL(
|
||||
url.format({
|
||||
pathname: path.join(__dirname, '../packages/web/build/splash.html'),
|
||||
protocol: 'file:',
|
||||
slashes: true,
|
||||
})
|
||||
);
|
||||
|
||||
if (process.env.ELECTRON_START_URL) {
|
||||
loadMainWindow();
|
||||
} else {
|
||||
|
||||
+10
-4
@@ -60,6 +60,11 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/integer@latest":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/integer/-/integer-4.0.0.tgz#3b778715df72d2cf8ba73bad27bd9d830907f944"
|
||||
integrity sha512-2U1i6bIRiqizl6O+ETkp2HhUZIxg7g+burUabh9tzGd0qcszfNaFRaY9bGNlQKgEU7DCsH5qMajRDW5QamWQbw==
|
||||
|
||||
"@types/node@*":
|
||||
version "13.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.9.1.tgz#96f606f8cd67fb018847d9b61e93997dabdefc72"
|
||||
@@ -232,11 +237,12 @@ base64-js@^1.3.1:
|
||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
|
||||
better-sqlite3@^7.3.1:
|
||||
version "7.3.1"
|
||||
resolved "https://registry.yarnpkg.com/better-sqlite3/-/better-sqlite3-7.3.1.tgz#2dcccfa4c34c544073d12fbb172281a13925fbb8"
|
||||
integrity sha512-Io0eWFWEtHsA7KS7Ehm45AGwi5SHeCD1hIHd+b1nj26Tf7rFTBqMltuVDimNMNMJ6f+Oy29RT7XWinv3yWKvIQ==
|
||||
better-sqlite3-with-prebuilds@^7.1.8:
|
||||
version "7.1.8"
|
||||
resolved "https://registry.yarnpkg.com/better-sqlite3-with-prebuilds/-/better-sqlite3-with-prebuilds-7.1.8.tgz#3090c478fe9b60e74ce053a76807b189784f62d7"
|
||||
integrity sha512-trwg1qhN91cPYEB8D2K0KVHIsMsiAnxKx6/syfQ7rLrtD+zOS3fqJq4VGszMF+OuYAZJNAR4oLsikys3YW/6aA==
|
||||
dependencies:
|
||||
"@types/integer" latest
|
||||
bindings "^1.5.0"
|
||||
prebuild-install "^6.0.1"
|
||||
tar "^6.1.0"
|
||||
|
||||
@@ -5,7 +5,7 @@ let fillContent = '';
|
||||
if (process.platform == 'win32') {
|
||||
fillContent += `content.msnodesqlv8 = () => require('msnodesqlv8');`;
|
||||
}
|
||||
fillContent += `content['better-sqlite3'] = () => require('better-sqlite3');`;
|
||||
fillContent += `content['better-sqlite3-with-prebuilds'] = () => require('better-sqlite3-with-prebuilds');`;
|
||||
|
||||
const getContent = (empty) => `
|
||||
// this file is generated automatically by script fillNativeModules.js, do not edit it manually
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function load() {
|
||||
const plugins = {};
|
||||
|
||||
for (const packageName of fs.readdirSync('plugins')) {
|
||||
if (!packageName.startsWith('dbgate-plugin-')) continue;
|
||||
const dir = path.join('plugins', packageName);
|
||||
const frontend = fs.readFileSync(path.join(dir, 'dist', 'frontend.js'), 'utf-8');
|
||||
const readme = fs.readFileSync(path.join(dir, 'README.md'), 'utf-8');
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8'));
|
||||
plugins[packageName] = {
|
||||
manifest,
|
||||
frontend,
|
||||
readme,
|
||||
};
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
||||
fs.writeFileSync('packages/api/src/packagedPluginsContent.js', `module.exports = () => (${JSON.stringify(load())});`);
|
||||
+4
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "4.2.0-beta.4",
|
||||
"version": "4.2.2",
|
||||
"name": "dbgate-all",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
@@ -32,6 +32,8 @@
|
||||
"generatePadFile": "node generatePadFile",
|
||||
"fillNativeModules": "node fillNativeModules",
|
||||
"fillNativeModulesElectron": "node fillNativeModules --electron",
|
||||
"fillPackagedPlugins": "node fillPackagedPlugins",
|
||||
"resetPackagedPlugins": "node resetPackagedPlugins",
|
||||
"prettier": "prettier --write packages/api/src && prettier --write packages/datalib/src && prettier --write packages/filterparser/src && prettier --write packages/sqltree/src && prettier --write packages/tools/src && prettier --write packages/types && prettier --write packages/web/src && prettier --write app/src",
|
||||
"copy:docker:build": "copyfiles packages/api/dist/* docker -f && copyfiles packages/web/public/* docker -u 2 && copyfiles \"packages/web/public/**/*\" docker -u 2 && copyfiles \"plugins/dist/**/*\" docker/plugins -u 2",
|
||||
"prepare:docker": "yarn plugins:copydist && yarn build:web:docker && yarn build:api && yarn copy:docker:build",
|
||||
@@ -40,7 +42,7 @@
|
||||
"ts:api": "yarn workspace dbgate-api ts",
|
||||
"ts:web": "yarn workspace dbgate-web ts",
|
||||
"ts": "yarn ts:api && yarn ts:web",
|
||||
"postinstall": "yarn build:lib && patch-package && yarn fillNativeModules && yarn build:plugins:frontend"
|
||||
"postinstall": "yarn resetPackagedPlugins && yarn build:lib && patch-package && yarn fillNativeModules && yarn build:plugins:frontend"
|
||||
},
|
||||
"dependencies": {
|
||||
"concurrently": "^5.1.0",
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
DEVMODE=1
|
||||
|
||||
CONNECTIONS=mysql
|
||||
|
||||
LABEL_mysql=MySql
|
||||
SERVER_mysql=dbgate.org
|
||||
USER_mysql=reader
|
||||
PASSWORD_mysql=CovidReader2020
|
||||
PORT_mysql=3326
|
||||
ENGINE_mysql=mysql@dbgate-plugin-mysql
|
||||
SINGLE_CONNECTION=mysql
|
||||
SINGLE_DATABASE=covid
|
||||
|
||||
PERMISSIONS=files/charts/read
|
||||
@@ -0,0 +1,15 @@
|
||||
DEVMODE=1
|
||||
|
||||
CONNECTIONS=mysql
|
||||
|
||||
LABEL_mysql=MySql localhost
|
||||
SERVER_mysql=localhost
|
||||
USER_mysql=root
|
||||
PASSWORD_mysql=test
|
||||
PORT_mysql=3307
|
||||
ENGINE_mysql=mysql@dbgate-plugin-mysql
|
||||
|
||||
SINGLE_CONNECTION=mysql
|
||||
SINGLE_DATABASE=Chinook
|
||||
|
||||
PERMISSIONS=files/charts/read
|
||||
@@ -19,7 +19,7 @@
|
||||
"dependencies": {
|
||||
"async-lock": "^1.2.4",
|
||||
"axios": "^0.19.0",
|
||||
"better-sqlite3": "^7.3.1",
|
||||
"better-sqlite3-with-prebuilds": "^7.1.8",
|
||||
"body-parser": "^1.19.0",
|
||||
"bufferutil": "^4.0.1",
|
||||
"byline": "^5.0.0",
|
||||
@@ -51,7 +51,9 @@
|
||||
"scripts": {
|
||||
"start": "env-cmd node src/index.js",
|
||||
"start:portal": "env-cmd -f .env-portal node src/index.js",
|
||||
"start:covid": "env-cmd -f .env-covid node src/index.js",
|
||||
"start:singledb": "env-cmd -f .env-singledb node src/index.js",
|
||||
"start:filedb": "env-cmd node src/index.js /home/jena/test/chinook/Chinook.db",
|
||||
"start:singleconn": "env-cmd node src/index.js --server localhost --user root --port 3307 --engine mysql@dbgate-plugin-mysql --password test",
|
||||
"ts": "tsc",
|
||||
"build": "webpack"
|
||||
},
|
||||
@@ -69,4 +71,4 @@
|
||||
"optionalDependencies": {
|
||||
"msnodesqlv8": "^2.0.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ const _ = require('lodash');
|
||||
|
||||
const currentVersion = require('../currentVersion');
|
||||
const platformInfo = require('../utility/platformInfo');
|
||||
const connections = require('../controllers/connections');
|
||||
|
||||
module.exports = {
|
||||
settingsValue: {},
|
||||
@@ -21,30 +22,11 @@ module.exports = {
|
||||
|
||||
get_meta: 'get',
|
||||
async get() {
|
||||
// const toolbarButtons = process.env.TOOLBAR;
|
||||
// const toolbar = toolbarButtons
|
||||
// ? toolbarButtons.split(',').map((name) => ({
|
||||
// name,
|
||||
// icon: process.env[`ICON_${name}`],
|
||||
// title: process.env[`TITLE_${name}`],
|
||||
// page: process.env[`PAGE_${name}`],
|
||||
// }))
|
||||
// : null;
|
||||
// const startupPages = process.env.STARTUP_PAGES ? process.env.STARTUP_PAGES.split(',') : [];
|
||||
const permissions = process.env.PERMISSIONS ? process.env.PERMISSIONS.split(',') : null;
|
||||
const singleDatabase =
|
||||
process.env.SINGLE_CONNECTION && process.env.SINGLE_DATABASE
|
||||
? {
|
||||
conid: process.env.SINGLE_CONNECTION,
|
||||
database: process.env.SINGLE_DATABASE,
|
||||
}
|
||||
: null;
|
||||
|
||||
return {
|
||||
runAsPortal: !!process.env.CONNECTIONS,
|
||||
// toolbar,
|
||||
// startupPages,
|
||||
singleDatabase,
|
||||
runAsPortal: !!connections.portalConnections,
|
||||
singleDatabase: connections.singleDatabase,
|
||||
permissions,
|
||||
...currentVersion,
|
||||
};
|
||||
|
||||
@@ -8,6 +8,32 @@ const socket = require('../utility/socket');
|
||||
const { encryptConnection } = require('../utility/crypting');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
|
||||
function getNamedArgs() {
|
||||
const res = {};
|
||||
for (let i = 0; i < process.argv.length; i++) {
|
||||
const name = process.argv[i];
|
||||
if (name.startsWith('--')) {
|
||||
let value = process.argv[i + 1];
|
||||
if (value && value.startsWith('--')) value = null;
|
||||
res[name.substring(2)] = value == null ? true : value;
|
||||
i++;
|
||||
} else {
|
||||
if (name.endsWith('.db') || name.endsWith('.sqlite') || name.endsWith('.sqlite3')) {
|
||||
res.databaseFile = name;
|
||||
res.engine = 'sqlite@dbgate-plugin-sqlite';
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function getDatabaseFileLabel(databaseFile) {
|
||||
if (!databaseFile) return databaseFile;
|
||||
const m = databaseFile.match(/[\/]([^\/]+)$/);
|
||||
if (m) return m[1];
|
||||
return databaseFile;
|
||||
}
|
||||
|
||||
function getPortalCollections() {
|
||||
if (process.env.CONNECTIONS) {
|
||||
return _.compact(process.env.CONNECTIONS.split(',')).map(id => ({
|
||||
@@ -17,16 +43,79 @@ function getPortalCollections() {
|
||||
user: process.env[`USER_${id}`],
|
||||
password: process.env[`PASSWORD_${id}`],
|
||||
port: process.env[`PORT_${id}`],
|
||||
databaseUrl: process.env[`URL_${id}`],
|
||||
databaseFile: process.env[`FILE_${id}`],
|
||||
defaultDatabase: process.env[`DATABASE_${id}`],
|
||||
singleDatabase: !!process.env[`DATABASE_${id}`],
|
||||
displayName: process.env[`LABEL_${id}`],
|
||||
}));
|
||||
}
|
||||
|
||||
const args = getNamedArgs();
|
||||
if (args.databaseFile) {
|
||||
return [
|
||||
{
|
||||
_id: 'argv',
|
||||
databaseFile: args.databaseFile,
|
||||
singleDatabase: true,
|
||||
defaultDatabase: getDatabaseFileLabel(args.databaseFile),
|
||||
engine: args.engine,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (args.databaseUrl) {
|
||||
return [
|
||||
{
|
||||
_id: 'argv',
|
||||
useDatabaseUrl: true,
|
||||
...args,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (args.server) {
|
||||
return [
|
||||
{
|
||||
_id: 'argv',
|
||||
...args,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
const portalConnections = getPortalCollections();
|
||||
|
||||
function getSingleDatabase() {
|
||||
if (process.env.SINGLE_CONNECTION && process.env.SINGLE_DATABASE) {
|
||||
// @ts-ignore
|
||||
const connection = portalConnections.find(x => x._id == process.env.SINGLE_CONNECTION);
|
||||
return {
|
||||
connection,
|
||||
name: process.env.SINGLE_DATABASE,
|
||||
};
|
||||
}
|
||||
// @ts-ignore
|
||||
const arg0 = (portalConnections || []).find(x => x._id == 'argv');
|
||||
if (arg0) {
|
||||
// @ts-ignore
|
||||
if (arg0.singleDatabase) {
|
||||
return {
|
||||
connection: arg0,
|
||||
// @ts-ignore
|
||||
name: arg0.defaultDatabase,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const singleDatabase = getSingleDatabase();
|
||||
|
||||
module.exports = {
|
||||
datastore: null,
|
||||
opened: [],
|
||||
singleDatabase,
|
||||
portalConnections,
|
||||
|
||||
async _init() {
|
||||
const dir = datadir();
|
||||
|
||||
@@ -18,6 +18,12 @@ module.exports = {
|
||||
existing.structure = structure;
|
||||
socket.emitChanged(`database-structure-changed-${conid}-${database}`);
|
||||
},
|
||||
handle_structureTime(conid, database, { analysedTime }) {
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
if (!existing) return;
|
||||
existing.analysedTime = analysedTime;
|
||||
socket.emitChanged(`database-status-changed-${conid}-${database}`);
|
||||
},
|
||||
handle_version(conid, database, { version }) {
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
if (!existing) return;
|
||||
@@ -123,9 +129,19 @@ module.exports = {
|
||||
status_meta: 'get',
|
||||
async status({ conid, database }) {
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
if (existing) return existing.status;
|
||||
if (existing) {
|
||||
return {
|
||||
...existing.status,
|
||||
analysedTime: existing.analysedTime,
|
||||
};
|
||||
}
|
||||
const lastClosed = this.closed[`${conid}/${database}`];
|
||||
if (lastClosed) return lastClosed.status;
|
||||
if (lastClosed) {
|
||||
return {
|
||||
...lastClosed.status,
|
||||
analysedTime: lastClosed.analysedTime,
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: 'error',
|
||||
message: 'Not connected',
|
||||
@@ -156,6 +172,13 @@ module.exports = {
|
||||
return { status: 'ok' };
|
||||
},
|
||||
|
||||
syncModel_meta: 'post',
|
||||
async syncModel({ conid, database }) {
|
||||
const conn = await this.ensureOpened(conid, database);
|
||||
conn.subprocess.send({ msgtype: 'syncModel' });
|
||||
return { status: 'ok' };
|
||||
},
|
||||
|
||||
close(conid, database, kill = true) {
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
if (existing) {
|
||||
|
||||
@@ -9,10 +9,17 @@ const requirePlugin = require('../shell/requirePlugin');
|
||||
const downloadPackage = require('../utility/downloadPackage');
|
||||
const hasPermission = require('../utility/hasPermission');
|
||||
const _ = require('lodash');
|
||||
const packagedPluginsContent = require('../packagedPluginsContent');
|
||||
|
||||
module.exports = {
|
||||
script_meta: 'get',
|
||||
async script({ packageName }) {
|
||||
const packagedContent = packagedPluginsContent();
|
||||
|
||||
if (packagedContent && packagedContent[packageName]) {
|
||||
return packagedContent[packageName].frontend;
|
||||
}
|
||||
|
||||
const file1 = path.join(packagedPluginsDir(), packageName, 'dist', 'frontend.js');
|
||||
const file2 = path.join(pluginsdir(), packageName, 'dist', 'frontend.js');
|
||||
// @ts-ignore
|
||||
@@ -58,26 +65,37 @@ module.exports = {
|
||||
|
||||
installed_meta: 'get',
|
||||
async installed() {
|
||||
const files1 = await fs.readdir(packagedPluginsDir());
|
||||
const packagedContent = packagedPluginsContent();
|
||||
|
||||
const files1 = packagedContent ? _.keys(packagedContent) : await fs.readdir(packagedPluginsDir());
|
||||
const files2 = await fs.readdir(pluginsdir());
|
||||
|
||||
const res = [];
|
||||
for (const packageName of _.union(files1, files2)) {
|
||||
if (!/^dbgate-plugin-.*$/.test(packageName)) continue;
|
||||
try {
|
||||
const isPackaged = files1.includes(packageName);
|
||||
const manifest = await fs
|
||||
.readFile(path.join(isPackaged ? packagedPluginsDir() : pluginsdir(), packageName, 'package.json'), {
|
||||
encoding: 'utf-8',
|
||||
})
|
||||
.then(x => JSON.parse(x));
|
||||
const readmeFile = path.join(isPackaged ? packagedPluginsDir() : pluginsdir(), packageName, 'README.md');
|
||||
// @ts-ignore
|
||||
if (await fs.exists(readmeFile)) {
|
||||
manifest.readme = await fs.readFile(readmeFile, { encoding: 'utf-8' });
|
||||
if (packagedContent && packagedContent[packageName]) {
|
||||
const manifest = {
|
||||
...packagedContent[packageName].manifest,
|
||||
};
|
||||
manifest.isPackaged = true;
|
||||
manifest.readme = packagedContent[packageName].readme;
|
||||
res.push(manifest);
|
||||
} else {
|
||||
const isPackaged = files1.includes(packageName);
|
||||
const manifest = await fs
|
||||
.readFile(path.join(isPackaged ? packagedPluginsDir() : pluginsdir(), packageName, 'package.json'), {
|
||||
encoding: 'utf-8',
|
||||
})
|
||||
.then(x => JSON.parse(x));
|
||||
const readmeFile = path.join(isPackaged ? packagedPluginsDir() : pluginsdir(), packageName, 'README.md');
|
||||
// @ts-ignore
|
||||
if (await fs.exists(readmeFile)) {
|
||||
manifest.readme = await fs.readFile(readmeFile, { encoding: 'utf-8' });
|
||||
}
|
||||
manifest.isPackaged = isPackaged;
|
||||
res.push(manifest);
|
||||
}
|
||||
manifest.isPackaged = isPackaged;
|
||||
res.push(manifest);
|
||||
} catch (err) {
|
||||
console.log(`Skipped plugin ${packageName}, error:`, err.message);
|
||||
}
|
||||
@@ -135,8 +153,9 @@ module.exports = {
|
||||
async authTypes({ engine }) {
|
||||
const packageName = extractPackageName(engine);
|
||||
const content = requirePlugin(packageName);
|
||||
if (!content.driver || content.driver.engine != engine || !content.driver.getAuthTypes) return null;
|
||||
return content.driver.getAuthTypes() || null;
|
||||
const driver = content.drivers.find(x => x.engine == engine);
|
||||
if (!driver || !driver.getAuthTypes) return null;
|
||||
return driver.getAuthTypes() || null;
|
||||
},
|
||||
|
||||
// async _init() {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
const shell = require('./shell');
|
||||
const processArgs = require('./utility/processArgs');
|
||||
const dbgateTools = require('dbgate-tools');
|
||||
|
||||
global['DBGATE_TOOLS'] = dbgateTools;
|
||||
|
||||
if (processArgs.startProcess) {
|
||||
const proc = require('./proc');
|
||||
|
||||
@@ -12,6 +12,7 @@ let afterConnectCallbacks = [];
|
||||
let analysedStructure = null;
|
||||
let lastPing = null;
|
||||
let lastStatus = null;
|
||||
let analysedTime = 0;
|
||||
|
||||
async function checkedAsyncCall(promise) {
|
||||
try {
|
||||
@@ -28,23 +29,42 @@ async function checkedAsyncCall(promise) {
|
||||
}
|
||||
}
|
||||
|
||||
let loadingModel = false;
|
||||
|
||||
async function handleFullRefresh() {
|
||||
loadingModel = true;
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
setStatusName('loadStructure');
|
||||
analysedStructure = await checkedAsyncCall(driver.analyseFull(systemConnection));
|
||||
analysedTime = new Date().getTime();
|
||||
process.send({ msgtype: 'structure', structure: analysedStructure });
|
||||
process.send({ msgtype: 'structureTime', analysedTime });
|
||||
setStatusName('ok');
|
||||
loadingModel = false;
|
||||
}
|
||||
|
||||
async function handleIncrementalRefresh() {
|
||||
async function handleIncrementalRefresh(forceSend) {
|
||||
loadingModel = true;
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
setStatusName('checkStructure');
|
||||
const newStructure = await checkedAsyncCall(driver.analyseIncremental(systemConnection, analysedStructure));
|
||||
analysedTime = new Date().getTime();
|
||||
if (newStructure != null) {
|
||||
analysedStructure = newStructure;
|
||||
}
|
||||
|
||||
if (forceSend || newStructure != null) {
|
||||
process.send({ msgtype: 'structure', structure: analysedStructure });
|
||||
}
|
||||
|
||||
process.send({ msgtype: 'structureTime', analysedTime });
|
||||
setStatusName('ok');
|
||||
loadingModel = false;
|
||||
}
|
||||
|
||||
function handleSyncModel() {
|
||||
if (loadingModel) return;
|
||||
handleIncrementalRefresh();
|
||||
}
|
||||
|
||||
function setStatus(status) {
|
||||
@@ -75,12 +95,12 @@ async function handleConnect({ connection, structure, globalSettings }) {
|
||||
readVersion();
|
||||
if (structure) {
|
||||
analysedStructure = structure;
|
||||
handleIncrementalRefresh();
|
||||
handleIncrementalRefresh(true);
|
||||
} else {
|
||||
handleFullRefresh();
|
||||
}
|
||||
|
||||
if (extractBoolSettingsValue(globalSettings, 'connection.autoRefresh', true)) {
|
||||
if (extractBoolSettingsValue(globalSettings, 'connection.autoRefresh', false)) {
|
||||
setInterval(
|
||||
handleIncrementalRefresh,
|
||||
extractIntSettingsValue(globalSettings, 'connection.autoRefreshInterval', 30, 3, 3600) * 1000
|
||||
@@ -172,6 +192,7 @@ const messageHandlers = {
|
||||
collectionData: handleCollectionData,
|
||||
sqlPreview: handleSqlPreview,
|
||||
ping: handlePing,
|
||||
syncModel: handleSyncModel,
|
||||
// runCommand: handleRunCommand,
|
||||
};
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ async function handleConnect(connection) {
|
||||
systemConnection = await connectUtility(driver, storedConnection);
|
||||
readVersion();
|
||||
handleRefresh();
|
||||
if (extractBoolSettingsValue(globalSettings, 'connection.autoRefresh', true)) {
|
||||
if (extractBoolSettingsValue(globalSettings, 'connection.autoRefresh', false)) {
|
||||
setInterval(handleRefresh, extractIntSettingsValue(globalSettings, 'connection.autoRefreshInterval', 30, 5, 3600) * 1000);
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -15,7 +15,7 @@ function requireEngineDriver(connection) {
|
||||
if (engine.includes('@')) {
|
||||
const [shortName, packageName] = engine.split('@');
|
||||
const plugin = requirePlugin(packageName);
|
||||
return plugin.driver;
|
||||
return plugin.drivers.find(x => x.engine == engine);
|
||||
}
|
||||
throw new Error(`Could not found engine driver ${engine}`);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ import { DatabaseInfo, DatabaseModification, EngineDriver } from 'dbgate-types';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
import _groupBy from 'lodash/groupBy';
|
||||
import _pick from 'lodash/pick';
|
||||
import _compact from 'lodash/compact';
|
||||
|
||||
const fp_pick = arg => array => _pick(array, arg);
|
||||
export class DatabaseAnalyser {
|
||||
structure: DatabaseInfo;
|
||||
modifications: DatabaseModification[];
|
||||
singleObjectFilter: any;
|
||||
singleObjectId: string = null;
|
||||
|
||||
constructor(public pool, public driver: EngineDriver) {}
|
||||
|
||||
@@ -15,15 +17,30 @@ export class DatabaseAnalyser {
|
||||
return DatabaseAnalyser.createEmptyStructure();
|
||||
}
|
||||
|
||||
/** @returns {Promise<import('dbgate-types').DatabaseModification[]>} */
|
||||
async getModifications() {
|
||||
if (this.structure == null) throw new Error('DatabaseAnalyse.getModifications - structure must be filled');
|
||||
|
||||
async _getFastSnapshot(): Promise<DatabaseInfo> {
|
||||
return null;
|
||||
}
|
||||
|
||||
async _computeSingleObjectId() {}
|
||||
|
||||
async fullAnalysis() {
|
||||
return this._runAnalysis();
|
||||
const res = await this._runAnalysis();
|
||||
// console.log('FULL ANALYSIS', res);
|
||||
return res;
|
||||
}
|
||||
|
||||
async singleObjectAnalysis(name, typeField) {
|
||||
// console.log('Analysing SINGLE OBJECT', name, typeField);
|
||||
this.singleObjectFilter = { ...name, typeField };
|
||||
await this._computeSingleObjectId();
|
||||
const res = await this._runAnalysis();
|
||||
// console.log('SINGLE OBJECT RES', res);
|
||||
const obj =
|
||||
res[typeField]?.length == 1
|
||||
? res[typeField][0]
|
||||
: res[typeField]?.find(x => x.pureName == name.pureName && x.schemaName == name.schemaName);
|
||||
// console.log('SINGLE OBJECT', obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
async incrementalAnalysis(structure) {
|
||||
@@ -37,7 +54,7 @@ export class DatabaseAnalyser {
|
||||
}
|
||||
if (this.modifications.length == 0) return null;
|
||||
console.log('DB modifications detected:', this.modifications);
|
||||
return this._runAnalysis();
|
||||
return this.mergeAnalyseResult(await this._runAnalysis());
|
||||
}
|
||||
|
||||
mergeAnalyseResult(newlyAnalysed) {
|
||||
@@ -57,7 +74,7 @@ export class DatabaseAnalyser {
|
||||
const addedChangedIds = newArray.map(x => x.objectId);
|
||||
const removeAllIds = [...removedIds, ...addedChangedIds];
|
||||
res[field] = _sortBy(
|
||||
[...this.structure[field].filter(x => !removeAllIds.includes(x.objectId)), ...newArray],
|
||||
[...(this.structure[field] || []).filter(x => !removeAllIds.includes(x.objectId)), ...newArray],
|
||||
x => x.pureName
|
||||
);
|
||||
}
|
||||
@@ -86,6 +103,96 @@ export class DatabaseAnalyser {
|
||||
// return this.structure.tables.find((x) => x.objectId == id);
|
||||
// }
|
||||
|
||||
createQuery(template, typeFields) {
|
||||
// let res = template;
|
||||
if (this.singleObjectFilter) {
|
||||
const { typeField } = this.singleObjectFilter;
|
||||
if (!this.singleObjectId) return null;
|
||||
if (!typeFields || !typeFields.includes(typeField)) return null;
|
||||
return template.replace(/=OBJECT_ID_CONDITION/g, ` = '${this.singleObjectId}'`);
|
||||
}
|
||||
if (!this.modifications || !typeFields || this.modifications.length == 0) {
|
||||
return template.replace(/=OBJECT_ID_CONDITION/g, ' is not null');
|
||||
}
|
||||
if (this.modifications.some(x => typeFields.includes(x.objectTypeField) && x.action == 'all')) {
|
||||
// do not filter objects
|
||||
return template.replace(/=OBJECT_ID_CONDITION/g, ' is not null');
|
||||
}
|
||||
const filterIds = this.modifications
|
||||
.filter(x => typeFields.includes(x.objectTypeField) && (x.action == 'add' || x.action == 'change'))
|
||||
.map(x => x.objectId);
|
||||
if (filterIds.length == 0) {
|
||||
return template.replace(/=OBJECT_ID_CONDITION/g, " = '0'");
|
||||
}
|
||||
return template.replace(/=OBJECT_ID_CONDITION/g, ` in (${filterIds.map(x => `'${x}'`).join(',')})`);
|
||||
}
|
||||
|
||||
getDeletedObjectsForField(snapshot, objectTypeField) {
|
||||
const items = snapshot[objectTypeField];
|
||||
if (!items) return [];
|
||||
if (!this.structure[objectTypeField]) return [];
|
||||
return this.structure[objectTypeField]
|
||||
.filter(x => !items.find(y => x.objectId == y.objectId))
|
||||
.map(x => ({
|
||||
oldName: _pick(x, ['schemaName', 'pureName']),
|
||||
objectId: x.objectId,
|
||||
action: 'remove',
|
||||
objectTypeField,
|
||||
}));
|
||||
}
|
||||
|
||||
getDeletedObjects(snapshot) {
|
||||
return [
|
||||
...this.getDeletedObjectsForField(snapshot, 'tables'),
|
||||
...this.getDeletedObjectsForField(snapshot, 'collections'),
|
||||
...this.getDeletedObjectsForField(snapshot, 'views'),
|
||||
...this.getDeletedObjectsForField(snapshot, 'procedures'),
|
||||
...this.getDeletedObjectsForField(snapshot, 'functions'),
|
||||
...this.getDeletedObjectsForField(snapshot, 'triggers'),
|
||||
];
|
||||
}
|
||||
|
||||
async getModifications() {
|
||||
const snapshot = await this._getFastSnapshot();
|
||||
if (!snapshot) return null;
|
||||
|
||||
// console.log('STRUCTURE', this.structure);
|
||||
// console.log('SNAPSHOT', snapshot);
|
||||
|
||||
const res = [];
|
||||
for (const field in snapshot) {
|
||||
const items = snapshot[field];
|
||||
if (items === null) {
|
||||
res.push({ objectTypeField: field, action: 'all' });
|
||||
continue;
|
||||
}
|
||||
for (const item of items) {
|
||||
const { objectId, schemaName, pureName, contentHash } = item;
|
||||
const obj = this.structure[field].find(x => x.objectId == objectId);
|
||||
|
||||
if (obj && contentHash && obj.contentHash == contentHash) continue;
|
||||
|
||||
const action = obj
|
||||
? {
|
||||
newName: { schemaName, pureName },
|
||||
oldName: _pick(obj, ['schemaName', 'pureName']),
|
||||
action: 'change',
|
||||
objectTypeField: field,
|
||||
objectId,
|
||||
}
|
||||
: {
|
||||
newName: { schemaName, pureName },
|
||||
action: 'add',
|
||||
objectTypeField: field,
|
||||
objectId,
|
||||
};
|
||||
res.push(action);
|
||||
}
|
||||
|
||||
return [..._compact(res), ...this.getDeletedObjects(snapshot)];
|
||||
}
|
||||
}
|
||||
|
||||
static createEmptyStructure(): DatabaseInfo {
|
||||
return {
|
||||
tables: [],
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
TriggerInfo,
|
||||
ViewInfo,
|
||||
} from 'dbgate-types';
|
||||
import _ from 'lodash';
|
||||
import _flatten from 'lodash/flatten';
|
||||
import _uniqBy from 'lodash/uniqBy'
|
||||
import { SqlDumper } from './SqlDumper';
|
||||
import { extendDatabaseInfo } from './structureTools';
|
||||
|
||||
@@ -122,9 +123,9 @@ export class SqlGenerator {
|
||||
|
||||
createForeignKeys() {
|
||||
const fks = [];
|
||||
if (this.options.createForeignKeys) fks.push(..._.flatten(this.tables.map(x => x.foreignKeys || [])));
|
||||
if (this.options.createReferences) fks.push(..._.flatten(this.tables.map(x => x.dependencies || [])));
|
||||
for (const fk of _.uniqBy(fks, 'constraintName')) {
|
||||
if (this.options.createForeignKeys) fks.push(..._flatten(this.tables.map(x => x.foreignKeys || [])));
|
||||
if (this.options.createReferences) fks.push(..._flatten(this.tables.map(x => x.dependencies || [])));
|
||||
for (const fk of _uniqBy(fks, 'constraintName')) {
|
||||
this.dmp.createForeignKey(fk);
|
||||
if (this.checkDumper()) return;
|
||||
}
|
||||
@@ -152,7 +153,7 @@ export class SqlGenerator {
|
||||
}
|
||||
}
|
||||
if (this.options.createIndexes) {
|
||||
for (const index of _.flatten(this.tables.map(x => x.indexes || []))) {
|
||||
for (const index of _flatten(this.tables.map(x => x.indexes || []))) {
|
||||
this.dmp.createIndex(index);
|
||||
}
|
||||
}
|
||||
@@ -204,7 +205,7 @@ export class SqlGenerator {
|
||||
|
||||
dropTables() {
|
||||
if (this.options.dropReferences) {
|
||||
for (const fk of _.flatten(this.tables.map(x => x.dependencies || []))) {
|
||||
for (const fk of _flatten(this.tables.map(x => x.dependencies || []))) {
|
||||
this.dmp.dropForeignKey(fk);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,12 +22,7 @@ export const driverBase = {
|
||||
},
|
||||
async analyseSingleObject(pool, name, typeField = 'tables') {
|
||||
const analyser = new this.analyserClass(pool, this);
|
||||
analyser.singleObjectFilter = { ...name, typeField };
|
||||
const res = await analyser.fullAnalysis();
|
||||
if (res[typeField].length == 1) return res[typeField][0];
|
||||
const obj = res[typeField].find(x => x.pureName == name.pureName && x.schemaName == name.schemaName);
|
||||
// console.log('FIND', name, obj);
|
||||
return obj;
|
||||
return analyser.singleObjectAnalysis(name, typeField);
|
||||
},
|
||||
analyseSingleTable(pool, name) {
|
||||
return this.analyseSingleObject(pool, name, 'tables');
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import _ from 'lodash';
|
||||
import _isNaN from 'lodash/isNaN';
|
||||
import _isNumber from 'lodash/isNumber';
|
||||
|
||||
export function extractIntSettingsValue(settings, name, defaultValue, min = null, max = null) {
|
||||
const parsed = parseInt(settings[name]);
|
||||
if (_.isNaN(parsed)) {
|
||||
if (_isNaN(parsed)) {
|
||||
return defaultValue;
|
||||
}
|
||||
if (_.isNumber(parsed)) {
|
||||
if (_isNumber(parsed)) {
|
||||
if (min != null && parsed < min) return min;
|
||||
if (max != null && parsed > max) return max;
|
||||
return parsed;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { DatabaseInfo } from 'dbgate-types';
|
||||
import _ from 'lodash';
|
||||
import _flatten from 'lodash/flatten';
|
||||
|
||||
export function addTableDependencies(db: DatabaseInfo): DatabaseInfo {
|
||||
const allForeignKeys = _.flatten(db.tables.map(x => x.foreignKeys || []));
|
||||
const allForeignKeys = _flatten(db.tables.map(x => x.foreignKeys || []));
|
||||
return {
|
||||
...db,
|
||||
tables: db.tables.map(table => ({
|
||||
|
||||
Vendored
+5
-2
@@ -38,7 +38,10 @@ export interface EngineDriver {
|
||||
title: string;
|
||||
defaultPort?: number;
|
||||
supportsDatabaseUrl?: boolean;
|
||||
isFileDatabase?: boolean;
|
||||
isElectronOnly?: boolean;
|
||||
showConnectionField?: (field: string, values: any) => boolean;
|
||||
showConnectionTab?: (tab: 'ssl' | 'sshTunnel', values: any) => boolean;
|
||||
beforeConnectionSave?: (values: any) => any;
|
||||
databaseUrlPlaceholder?: string;
|
||||
connect({ server, port, user, password, database }): any;
|
||||
query(pool: any, sql: string): Promise<QueryResult>;
|
||||
@@ -78,6 +81,6 @@ export interface DatabaseModification {
|
||||
oldName?: NamedObjectInfo;
|
||||
newName?: NamedObjectInfo;
|
||||
objectId?: string;
|
||||
action: 'add' | 'remove' | 'change';
|
||||
action: 'add' | 'remove' | 'change' | 'all';
|
||||
objectTypeField: keyof DatabaseInfo;
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -4,6 +4,7 @@ export interface OpenedDatabaseConnection {
|
||||
conid: string;
|
||||
database: string;
|
||||
structure: DatabaseInfo;
|
||||
analysedTime?: number;
|
||||
serverVersion?: any;
|
||||
subprocess: ChildProcess;
|
||||
disconnected?: boolean;
|
||||
|
||||
@@ -21,9 +21,25 @@
|
||||
<link rel='stylesheet' href='build/fonts/materialdesignicons.css'>
|
||||
|
||||
<script defer src='build/bundle.js'></script>
|
||||
|
||||
<style>
|
||||
#starting_dbgate_zero {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id='starting_dbgate_zero'>
|
||||
Loading DbGate App ...
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,25 +1,68 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import CommandListener from './commands/CommandListener.svelte';
|
||||
import DataGridRowHeightMeter from './datagrid/DataGridRowHeightMeter.svelte';
|
||||
import LoadingInfo from './elements/LoadingInfo.svelte';
|
||||
|
||||
import PluginsProvider from './plugins/PluginsProvider.svelte';
|
||||
import Screen from './Screen.svelte';
|
||||
import { loadingPluginStore } from './stores';
|
||||
import { setAppLoaded } from './utility/appLoadManager';
|
||||
import axiosInstance from './utility/axiosInstance';
|
||||
import ErrorHandler from './utility/ErrorHandler.svelte';
|
||||
import { useSettings } from './utility/metadataLoaders';
|
||||
import OpenTabsOnStartup from './utility/OpenTabsOnStartup.svelte';
|
||||
|
||||
const settings = useSettings();
|
||||
let loadedApi = false;
|
||||
|
||||
async function loadApi() {
|
||||
try {
|
||||
const settings = await axiosInstance.get('config/get-settings');
|
||||
const connections = await axiosInstance.get('connections/list');
|
||||
const config = await axiosInstance.get('config/get');
|
||||
loadedApi = settings?.data && connections?.data && config?.data;
|
||||
if (!loadedApi) {
|
||||
console.log('API not initialized correctly, trying again in 1s');
|
||||
setTimeout(loadApi, 1000);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Error calling API, trying again in 1s');
|
||||
setTimeout(loadApi, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(loadApi);
|
||||
|
||||
onMount(() => {
|
||||
const removed = document.getElementById('starting_dbgate_zero');
|
||||
if (removed) removed.remove();
|
||||
});
|
||||
|
||||
$: {
|
||||
if (loadedApi && $loadingPluginStore?.loaded) {
|
||||
setAppLoaded();
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<DataGridRowHeightMeter />
|
||||
<ErrorHandler />
|
||||
<PluginsProvider />
|
||||
<CommandListener />
|
||||
<OpenTabsOnStartup />
|
||||
|
||||
{#if $settings}
|
||||
<Screen />
|
||||
{#if loadedApi}
|
||||
<PluginsProvider />
|
||||
{#if $loadingPluginStore?.loaded}
|
||||
<OpenTabsOnStartup />
|
||||
<Screen />
|
||||
{:else}
|
||||
<LoadingInfo
|
||||
message={$loadingPluginStore.loadingPackageName
|
||||
? `Loading plugin ${$loadingPluginStore.loadingPackageName} ...`
|
||||
: 'Preparing plugins ...'}
|
||||
wrapper
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
<LoadingInfo message="Loading settings..." wrapper />
|
||||
<LoadingInfo message="Starting DbGate ..." wrapper />
|
||||
{/if}
|
||||
|
||||
@@ -171,6 +171,7 @@
|
||||
if (_.isPlainObject(value) || _.isArray(value)) return JSON.stringify(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -581,6 +582,7 @@
|
||||
if (event.target.closest('.collapseButtonMarker')) return;
|
||||
if (event.target.closest('input')) return;
|
||||
|
||||
shiftDragStartCell = null;
|
||||
// event.target.closest('table').focus();
|
||||
event.preventDefault();
|
||||
if (domFocusField) domFocusField.focus();
|
||||
@@ -663,23 +665,42 @@
|
||||
}
|
||||
|
||||
function handleGridWheel(event) {
|
||||
let newFirstVisibleRowScrollIndex = firstVisibleRowScrollIndex;
|
||||
if (event.deltaY > 0) {
|
||||
newFirstVisibleRowScrollIndex += wheelRowCount;
|
||||
}
|
||||
if (event.deltaY < 0) {
|
||||
newFirstVisibleRowScrollIndex -= wheelRowCount;
|
||||
}
|
||||
let rowCount = grider.rowCount;
|
||||
if (newFirstVisibleRowScrollIndex + visibleRowCountLowerBound > rowCount) {
|
||||
newFirstVisibleRowScrollIndex = rowCount - visibleRowCountLowerBound + 1;
|
||||
}
|
||||
if (newFirstVisibleRowScrollIndex < 0) {
|
||||
newFirstVisibleRowScrollIndex = 0;
|
||||
}
|
||||
firstVisibleRowScrollIndex = newFirstVisibleRowScrollIndex;
|
||||
if (event.shiftKey) {
|
||||
let newFirstVisibleColumnScrollIndex = firstVisibleColumnScrollIndex;
|
||||
if (event.deltaY > 0) {
|
||||
newFirstVisibleColumnScrollIndex++;
|
||||
}
|
||||
if (event.deltaY < 0) {
|
||||
newFirstVisibleColumnScrollIndex--;
|
||||
}
|
||||
if (newFirstVisibleColumnScrollIndex > maxScrollColumn) {
|
||||
newFirstVisibleColumnScrollIndex = maxScrollColumn;
|
||||
}
|
||||
if (newFirstVisibleColumnScrollIndex < 0) {
|
||||
newFirstVisibleColumnScrollIndex = 0;
|
||||
}
|
||||
firstVisibleColumnScrollIndex = newFirstVisibleColumnScrollIndex;
|
||||
|
||||
domVerticalScroll.scroll(newFirstVisibleRowScrollIndex);
|
||||
domHorizontalScroll.scroll(newFirstVisibleColumnScrollIndex);
|
||||
} else {
|
||||
let newFirstVisibleRowScrollIndex = firstVisibleRowScrollIndex;
|
||||
if (event.deltaY > 0) {
|
||||
newFirstVisibleRowScrollIndex += wheelRowCount;
|
||||
}
|
||||
if (event.deltaY < 0) {
|
||||
newFirstVisibleRowScrollIndex -= wheelRowCount;
|
||||
}
|
||||
let rowCount = grider.rowCount;
|
||||
if (newFirstVisibleRowScrollIndex + visibleRowCountLowerBound > rowCount) {
|
||||
newFirstVisibleRowScrollIndex = rowCount - visibleRowCountLowerBound + 1;
|
||||
}
|
||||
if (newFirstVisibleRowScrollIndex < 0) {
|
||||
newFirstVisibleRowScrollIndex = 0;
|
||||
}
|
||||
firstVisibleRowScrollIndex = newFirstVisibleRowScrollIndex;
|
||||
|
||||
domVerticalScroll.scroll(newFirstVisibleRowScrollIndex);
|
||||
}
|
||||
}
|
||||
|
||||
function getSelectedRowIndexes() {
|
||||
@@ -735,7 +756,7 @@
|
||||
|
||||
handleCursorMove(event);
|
||||
|
||||
if (event.shiftKey) {
|
||||
if (event.shiftKey && event.keyCode != keycodes.shift) {
|
||||
selectedCells = getCellRange(shiftDragStartCell || currentCell, currentCell);
|
||||
}
|
||||
}
|
||||
@@ -958,6 +979,7 @@
|
||||
);
|
||||
|
||||
const menu = getContextMenu();
|
||||
|
||||
</script>
|
||||
|
||||
{#if !display || (!isDynamicStructure && (!columns || columns.length == 0))}
|
||||
@@ -1164,4 +1186,5 @@
|
||||
right: 40px;
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
export let collection;
|
||||
export let columns;
|
||||
export let showIfEmpty = false;
|
||||
|
||||
</script>
|
||||
|
||||
{#if collection?.length > 0 || showIfEmpty}
|
||||
@@ -55,24 +56,24 @@
|
||||
</TableControl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: var(--theme-bg-1);
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin: 20px;
|
||||
}
|
||||
</style>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: var(--theme-bg-1);
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
'icon home': 'mdi mdi-home',
|
||||
'icon query-design': 'mdi mdi-vector-polyline-edit',
|
||||
'icon form': 'mdi mdi-form-select',
|
||||
'icon history': 'mdi mdi-history',
|
||||
|
||||
'icon edit': 'mdi mdi-pencil',
|
||||
'icon delete': 'mdi mdi-delete',
|
||||
|
||||
@@ -51,19 +51,29 @@
|
||||
}
|
||||
|
||||
async function handleSubmit(e) {
|
||||
const connection = driver?.isFileDatabase
|
||||
? {
|
||||
..._.omit(e.detail, ['server', 'port', 'defaultDatabase']),
|
||||
singleDatabase: true,
|
||||
defaultDatabase: getDatabaseFileLabel(e.detail.databaseFile),
|
||||
}
|
||||
: {
|
||||
..._.omit(e.detail, ['databaseFile']),
|
||||
singleDatabase: e.detail.defaultDatabase ? e.detail.singleDatabase : false,
|
||||
};
|
||||
const allProps = [
|
||||
'databaseFile',
|
||||
'useDatabaseUrl',
|
||||
'databaseUrl',
|
||||
'authType',
|
||||
'server',
|
||||
'port',
|
||||
'user',
|
||||
'password',
|
||||
'defaultDatabase',
|
||||
'singleDatabase',
|
||||
];
|
||||
const visibleProps = allProps.filter(x => !driver?.showConnectionField || driver.showConnectionField(x, $values));
|
||||
const omitProps = _.difference(allProps, visibleProps);
|
||||
if (!$values.defaultDatabase) omitProps.push('singleDatabase');
|
||||
|
||||
let connection = _.omit(e.detail, omitProps);
|
||||
if (driver?.beforeConnectionSave) connection = driver?.beforeConnectionSave(connection);
|
||||
|
||||
axiosInstance.post('connections/save', connection);
|
||||
closeCurrentModal();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<FormProviderCore template={FormFieldTemplateLarge} {values}>
|
||||
@@ -77,11 +87,11 @@
|
||||
label: 'Main',
|
||||
component: ConnectionModalDriverFields,
|
||||
},
|
||||
!driver?.isFileDatabase && {
|
||||
(!driver?.showConnectionTab || driver?.showConnectionTab('sshTunnel', $values)) && {
|
||||
label: 'SSH Tunnel',
|
||||
component: ConnectionModalSshTunnelFields,
|
||||
},
|
||||
!driver?.isFileDatabase && {
|
||||
(!driver?.showConnectionTab || driver?.showConnectionTab('ssl', $values)) && {
|
||||
label: 'SSL',
|
||||
component: ConnectionModalSslFields,
|
||||
},
|
||||
@@ -146,4 +156,5 @@
|
||||
.error-result {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -24,15 +24,17 @@
|
||||
$: disabledFields = (currentAuthType ? currentAuthType.disabledFields : null) || [];
|
||||
$: driver = $extensions.drivers.find(x => x.engine == engine);
|
||||
$: defaultDatabase = $values.defaultDatabase;
|
||||
|
||||
</script>
|
||||
|
||||
<FormSelectField
|
||||
label="Database engine"
|
||||
name="engine"
|
||||
isNative
|
||||
options={[
|
||||
{ label: '(select driver)', value: '' },
|
||||
...$extensions.drivers
|
||||
.filter(driver => !driver.isFileDatabase || electron)
|
||||
.filter(driver => !driver.isElectronOnly || electron)
|
||||
.map(driver => ({
|
||||
value: driver.engine,
|
||||
label: driver.title,
|
||||
@@ -40,44 +42,48 @@
|
||||
]}
|
||||
/>
|
||||
|
||||
{#if driver?.isFileDatabase}
|
||||
{#if !driver?.showConnectionField || driver.showConnectionField('databaseFile', $values)}
|
||||
<FormElectronFileSelector label="Database file" name="databaseFile" disabled={!electron} />
|
||||
{:else}
|
||||
{#if driver?.supportsDatabaseUrl}
|
||||
<div class="radio">
|
||||
<FormRadioGroupField
|
||||
name="useDatabaseUrl"
|
||||
options={[
|
||||
{ label: 'Fill database connection details', value: '', default: true },
|
||||
{ label: 'Use database URL', value: '1' },
|
||||
]}
|
||||
{/if}
|
||||
|
||||
{#if !driver?.showConnectionField || driver.showConnectionField('useDatabaseUrl', $values)}
|
||||
<div class="radio">
|
||||
<FormRadioGroupField
|
||||
name="useDatabaseUrl"
|
||||
options={[
|
||||
{ label: 'Fill database connection details', value: '', default: true },
|
||||
{ label: 'Use database URL', value: '1' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !driver?.showConnectionField || driver.showConnectionField('databaseUrl', $values)}
|
||||
<FormTextField label="Database URL" name="databaseUrl" placeholder={driver?.databaseUrlPlaceholder} />
|
||||
{/if}
|
||||
|
||||
{#if $authTypes && (!driver?.showConnectionField || driver.showConnectionField('authType', $values))}
|
||||
<FormSelectField
|
||||
label="Authentication"
|
||||
name="authType"
|
||||
options={$authTypes.map(auth => ({
|
||||
value: auth.name,
|
||||
label: auth.title,
|
||||
}))}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if !driver?.showConnectionField || driver.showConnectionField('server', $values)}
|
||||
<div class="row">
|
||||
<div class="col-9 mr-1">
|
||||
<FormTextField
|
||||
label="Server"
|
||||
name="server"
|
||||
disabled={disabledFields.includes('server')}
|
||||
templateProps={{ noMargin: true }}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if driver?.supportsDatabaseUrl && useDatabaseUrl}
|
||||
<FormTextField label="Database URL" name="databaseUrl" placeholder={driver?.databaseUrlPlaceholder} />
|
||||
{:else}
|
||||
{#if $authTypes}
|
||||
<FormSelectField
|
||||
label="Authentication"
|
||||
name="authType"
|
||||
options={$authTypes.map(auth => ({
|
||||
value: auth.name,
|
||||
label: auth.title,
|
||||
}))}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-9 mr-1">
|
||||
<FormTextField
|
||||
label="Server"
|
||||
name="server"
|
||||
disabled={disabledFields.includes('server')}
|
||||
templateProps={{ noMargin: true }}
|
||||
/>
|
||||
</div>
|
||||
{#if !driver?.showConnectionField || driver.showConnectionField('port', $values)}
|
||||
<div class="col-3 mr-1">
|
||||
<FormTextField
|
||||
label="Port"
|
||||
@@ -87,17 +93,21 @@
|
||||
placeholder={driver && driver.defaultPort}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6 mr-1">
|
||||
<FormTextField
|
||||
label="User"
|
||||
name="user"
|
||||
disabled={disabledFields.includes('user')}
|
||||
templateProps={{ noMargin: true }}
|
||||
/>
|
||||
</div>
|
||||
{#if !driver?.showConnectionField || driver.showConnectionField('user', $values)}
|
||||
<div class="row">
|
||||
<div class="col-6 mr-1">
|
||||
<FormTextField
|
||||
label="User"
|
||||
name="user"
|
||||
disabled={disabledFields.includes('user')}
|
||||
templateProps={{ noMargin: true }}
|
||||
/>
|
||||
</div>
|
||||
{#if !driver?.showConnectionField || driver.showConnectionField('password', $values)}
|
||||
<div class="col-6 mr-1">
|
||||
<FormPasswordField
|
||||
label="Password"
|
||||
@@ -106,25 +116,28 @@
|
||||
templateProps={{ noMargin: true }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !disabledFields.includes('password')}
|
||||
<FormSelectField
|
||||
label="Password mode"
|
||||
name="passwordMode"
|
||||
options={[
|
||||
{ value: 'saveEncrypted', label: 'Save and encrypt' },
|
||||
{ value: 'saveRaw', label: 'Save raw (UNSAFE!!)' },
|
||||
]}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !disabledFields.includes('password') && (!driver?.showConnectionField || driver.showConnectionField('password', $values))}
|
||||
<FormSelectField
|
||||
label="Password mode"
|
||||
isNative
|
||||
name="passwordMode"
|
||||
options={[
|
||||
{ value: 'saveEncrypted', label: 'Save and encrypt' },
|
||||
{ value: 'saveRaw', label: 'Save raw (UNSAFE!!)' },
|
||||
]}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if !driver?.showConnectionField || driver.showConnectionField('defaultDatabase', $values)}
|
||||
<FormTextField label="Default database" name="defaultDatabase" />
|
||||
{/if}
|
||||
|
||||
{#if defaultDatabase}
|
||||
<FormCheckboxField label={`Use only database ${defaultDatabase}`} name="singleDatabase" />
|
||||
{/if}
|
||||
{#if defaultDatabase && (!driver?.showConnectionField || driver.showConnectionField('singleDatabase', $values))}
|
||||
<FormCheckboxField label={`Use only database ${defaultDatabase}`} name="singleDatabase" />
|
||||
{/if}
|
||||
|
||||
<FormTextField label="Display name" name="displayName" />
|
||||
@@ -141,4 +154,5 @@
|
||||
.radio :global(label) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
<FormSelectField
|
||||
label="SSH Authentication"
|
||||
name="sshMode"
|
||||
isNative
|
||||
disabled={!useSshTunnel}
|
||||
options={[
|
||||
{ value: 'userPassword', label: 'Username & password' },
|
||||
|
||||
@@ -4,10 +4,16 @@
|
||||
};
|
||||
|
||||
async function loadPlugins(pluginsDict, installedPlugins) {
|
||||
window['DBGATE_TOOLS'] = dbgateTools;
|
||||
|
||||
const newPlugins = {};
|
||||
for (const installed of installedPlugins || []) {
|
||||
if (!_.keys(pluginsDict).includes(installed.name)) {
|
||||
console.log('Loading module', installed.name);
|
||||
loadingPluginStore.set({
|
||||
loaded: false,
|
||||
loadingPackageName: installed.name,
|
||||
});
|
||||
const resp = await axiosInstance.request({
|
||||
method: 'get',
|
||||
url: 'plugins/script',
|
||||
@@ -22,13 +28,19 @@
|
||||
newPlugins[installed.name] = moduleContent;
|
||||
}
|
||||
}
|
||||
if (installedPlugins) {
|
||||
loadingPluginStore.set({
|
||||
loaded: true,
|
||||
loadingPackageName: null,
|
||||
});
|
||||
}
|
||||
return newPlugins;
|
||||
}
|
||||
|
||||
function buildDrivers(plugins) {
|
||||
const res = [];
|
||||
for (const { content } of plugins) {
|
||||
if (content.driver) res.push(content.driver);
|
||||
// if (content.driver) res.push(content.driver);
|
||||
if (content.drivers) res.push(...content.drivers);
|
||||
}
|
||||
return res;
|
||||
@@ -43,15 +55,17 @@
|
||||
};
|
||||
return extensions;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import { extensions } from '../stores';
|
||||
import { extensions, loadingPluginStore } from '../stores';
|
||||
import axiosInstance from '../utility/axiosInstance';
|
||||
import { useInstalledPlugins } from '../utility/metadataLoaders';
|
||||
import { buildFileFormats } from './fileformats';
|
||||
import { buildThemes } from './themes';
|
||||
import dbgateTools from 'dbgate-tools';
|
||||
|
||||
let pluginsDict = {};
|
||||
const installedPlugins = useInstalledPlugins();
|
||||
@@ -73,4 +87,5 @@
|
||||
.filter(x => x.content);
|
||||
|
||||
$: $extensions = buildExtensions(plugins);
|
||||
|
||||
</script>
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
--theme-bg-selected-point: #1765ad; /* blue-5 */
|
||||
|
||||
--theme-bg-statusbar-inv: blue;
|
||||
--theme-bg-statusbar-inv-hover: #4040FF;
|
||||
--theme-bg-modalheader: rgb(43, 60, 61);
|
||||
|
||||
--theme-bg-button-inv: #004488;
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
|
||||
|
||||
--theme-bg-statusbar-inv: blue;
|
||||
--theme-bg-statusbar-inv-hover: #4040FF;
|
||||
--theme-bg-modalheader: #eff;
|
||||
|
||||
--theme-bg-button-inv: #337ab7;
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
<FormCheckboxField
|
||||
name="connection.autoRefresh"
|
||||
label="Automatic refresh of database model on background"
|
||||
defaultValue={true}
|
||||
defaultValue={false}
|
||||
/>
|
||||
<FormTextField
|
||||
name="connection.autoRefreshInterval"
|
||||
|
||||
@@ -59,6 +59,10 @@ export const nullStore = readable(null, () => {});
|
||||
export const currentArchive = writable('default');
|
||||
export const isFileDragActive = writable(false);
|
||||
export const selectedCellsCallback = writable(null);
|
||||
export const loadingPluginStore = writable({
|
||||
loaded: false,
|
||||
loadingPackageName: null,
|
||||
});
|
||||
|
||||
export const currentThemeDefinition = derived([currentTheme, extensions], ([$currentTheme, $extensions]) =>
|
||||
$extensions.themes.find(x => x.className == $currentTheme)
|
||||
@@ -120,6 +124,9 @@ let currentConfigValue = null;
|
||||
currentConfigStore.subscribe(value => {
|
||||
currentConfigValue = value;
|
||||
invalidateCommands();
|
||||
if (value.singleDatabase) {
|
||||
currentDatabase.set(value.singleDatabase);
|
||||
}
|
||||
});
|
||||
export const getCurrentConfig = () => currentConfigValue;
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
let appIsLoaded = false;
|
||||
let onLoad = [];
|
||||
|
||||
export function setAppLoaded() {
|
||||
appIsLoaded = true;
|
||||
for (const func of onLoad) {
|
||||
func();
|
||||
}
|
||||
onLoad = [];
|
||||
}
|
||||
|
||||
export function getAppLoaded() {
|
||||
return appIsLoaded;
|
||||
}
|
||||
|
||||
export function callWhenAppLoaded(callback) {
|
||||
if (appIsLoaded) {
|
||||
callback();
|
||||
} else {
|
||||
onLoad.push(callback);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import _ from 'lodash';
|
||||
import { currentDatabase, openedTabs } from '../stores';
|
||||
import { callWhenAppLoaded } from './appLoadManager';
|
||||
import { getConnectionInfo } from './metadataLoaders';
|
||||
|
||||
let lastCurrentTab = null;
|
||||
|
||||
openedTabs.subscribe(async value => {
|
||||
openedTabs.subscribe(value => {
|
||||
const newCurrentTab = (value || []).find(x => x.selected);
|
||||
if (newCurrentTab == lastCurrentTab) return;
|
||||
|
||||
@@ -15,11 +16,14 @@ openedTabs.subscribe(async value => {
|
||||
database &&
|
||||
(conid != _.get(lastCurrentTab, 'props.conid') || database != _.get(lastCurrentTab, 'props.database'))
|
||||
) {
|
||||
const connection = await getConnectionInfo({ conid });
|
||||
currentDatabase.set({
|
||||
connection,
|
||||
name: database,
|
||||
});
|
||||
const doWork = async () => {
|
||||
const connection = await getConnectionInfo({ conid });
|
||||
currentDatabase.set({
|
||||
connection,
|
||||
name: database,
|
||||
});
|
||||
};
|
||||
callWhenAppLoaded(doWork);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,5 +21,9 @@ export default function getConnectionLabel(connection, { allowExplicitDatabase =
|
||||
if (connection.server) {
|
||||
return connection.server;
|
||||
}
|
||||
if (connection.singleDatabase && connection.defaultDatabase) {
|
||||
return `${connection.defaultDatabase}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { findEngineDriver } from 'dbgate-tools';
|
||||
import { currentDatabase, extensions } from '../stores';
|
||||
import { useConnectionInfo } from '../utility/metadataLoaders';
|
||||
import { useConfig, useConnectionInfo } from '../utility/metadataLoaders';
|
||||
|
||||
import ConnectionList from './ConnectionList.svelte';
|
||||
import SqlObjectListWrapper from './SqlObjectListWrapper.svelte';
|
||||
@@ -12,12 +12,16 @@
|
||||
$: conid = $currentDatabase?.connection?._id;
|
||||
$: connection = useConnectionInfo({ conid });
|
||||
$: driver = findEngineDriver($connection, $extensions);
|
||||
$: config = useConfig();
|
||||
|
||||
</script>
|
||||
|
||||
<WidgetColumnBar>
|
||||
<WidgetColumnBarItem title="Connections" name="connections" height="50%">
|
||||
<ConnectionList />
|
||||
</WidgetColumnBarItem>
|
||||
{#if !$config?.singleDatabase}
|
||||
<WidgetColumnBarItem title="Connections" name="connections" height="50%">
|
||||
<ConnectionList />
|
||||
</WidgetColumnBarItem>
|
||||
{/if}
|
||||
<WidgetColumnBarItem title={driver?.dialect?.nosql ? 'Collections' : 'Tables, views, functions'} name="dbObjects">
|
||||
<SqlObjectListWrapper />
|
||||
</WidgetColumnBarItem>
|
||||
|
||||
@@ -7,16 +7,19 @@
|
||||
[tabid]: info,
|
||||
}));
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { writable } from 'svelte/store';
|
||||
import moment from 'moment';
|
||||
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
|
||||
import { activeTabId, currentDatabase } from '../stores';
|
||||
import getConnectionLabel from '../utility/getConnectionLabel';
|
||||
import { useDatabaseServerVersion, useDatabaseStatus } from '../utility/metadataLoaders';
|
||||
import axiosInstance from '../utility/axiosInstance';
|
||||
|
||||
$: databaseName = $currentDatabase && $currentDatabase.name;
|
||||
$: connection = $currentDatabase && $currentDatabase.connection;
|
||||
@@ -25,6 +28,19 @@
|
||||
|
||||
$: contextItems = $statusBarTabInfo[$activeTabId] as any[];
|
||||
$: connectionLabel = getConnectionLabel(connection, { allowExplicitDatabase: false });
|
||||
|
||||
let timerValue = 1;
|
||||
|
||||
setInterval(() => {
|
||||
timerValue++;
|
||||
}, 10000);
|
||||
|
||||
async function handleSyncModel() {
|
||||
if (connection && databaseName) {
|
||||
await axiosInstance.post('database-connections/sync-model', { conid: connection._id, database: databaseName });
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="main">
|
||||
@@ -75,6 +91,18 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $status?.analysedTime}
|
||||
<div
|
||||
class="item flex clickable"
|
||||
title={`Last ${databaseName} model refresh: ${moment($status?.analysedTime).format('HH:mm:ss')}\nClick for refresh DB model`}
|
||||
on:click={handleSyncModel}
|
||||
>
|
||||
<FontIcon icon="icon history" />
|
||||
<div class="version ml-1">
|
||||
{moment($status?.analysedTime).fromNow() + (timerValue ? '' : '')}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="container">
|
||||
{#each contextItems || [] as item}
|
||||
@@ -94,6 +122,7 @@
|
||||
color: var(--theme-font-inv-1);
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
cursor: default;
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
@@ -108,4 +137,12 @@
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.clickable:hover {
|
||||
background-color: var(--theme-bg-statusbar-inv-hover);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -15,6 +15,12 @@ var config = {
|
||||
library: 'plugin',
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'global.DBGATE_TOOLS': 'window.DBGATE_TOOLS',
|
||||
}),
|
||||
],
|
||||
|
||||
// uncomment for disable minimalization
|
||||
// optimization: {
|
||||
// minimize: false,
|
||||
|
||||
@@ -15,6 +15,12 @@ var config = {
|
||||
library: 'plugin',
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'global.DBGATE_TOOLS': 'window.DBGATE_TOOLS',
|
||||
}),
|
||||
],
|
||||
|
||||
// uncomment for disable minimalization
|
||||
// optimization: {
|
||||
// minimize: false,
|
||||
|
||||
@@ -2,5 +2,5 @@ const driver = require('./driver');
|
||||
|
||||
module.exports = {
|
||||
packageName: 'dbgate-plugin-mongo',
|
||||
driver,
|
||||
drivers: [driver],
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const { SqlDumper } = require('dbgate-tools');
|
||||
const { SqlDumper } = global.DBGATE_TOOLS;
|
||||
|
||||
class Dumper extends SqlDumper {
|
||||
}
|
||||
class Dumper extends SqlDumper {}
|
||||
|
||||
module.exports = Dumper;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { driverBase } = require('dbgate-tools');
|
||||
const { driverBase } = global.DBGATE_TOOLS;
|
||||
const Dumper = require('./Dumper');
|
||||
|
||||
const mongoIdRegex = /^[0-9a-f]{24}$/;
|
||||
@@ -34,6 +34,14 @@ const driver = {
|
||||
supportsDatabaseUrl: true,
|
||||
databaseUrlPlaceholder: 'e.g. mongodb://username:password@mongodb.mydomain.net/dbname',
|
||||
|
||||
showConnectionField: (field, values) => {
|
||||
if (field == 'useDatabaseUrl') return true;
|
||||
if (values.useDatabaseUrl) {
|
||||
return ['databaseUrl', 'defaultDatabase', 'singleDatabase'].includes(field);
|
||||
}
|
||||
return ['server', 'port', 'user', 'password', 'defaultDatabase', 'singleDatabase'].includes(field);
|
||||
},
|
||||
|
||||
getCollectionUpdateScript(changeSet) {
|
||||
let res = '';
|
||||
for (const insert of changeSet.inserts) {
|
||||
|
||||
@@ -2,5 +2,5 @@ import driver from './driver';
|
||||
|
||||
export default {
|
||||
packageName: 'dbgate-plugin-mongo',
|
||||
driver,
|
||||
drivers: [driver],
|
||||
};
|
||||
|
||||
@@ -15,6 +15,12 @@ var config = {
|
||||
library: 'plugin',
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'global.DBGATE_TOOLS': 'window.DBGATE_TOOLS',
|
||||
}),
|
||||
],
|
||||
|
||||
// uncomment for disable minimalization
|
||||
// optimization: {
|
||||
// minimize: false,
|
||||
|
||||
@@ -48,43 +48,20 @@ function getColumnInfo({
|
||||
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;
|
||||
return super.createQuery(sql[resFileName], typeFields);
|
||||
}
|
||||
|
||||
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 _computeSingleObjectId() {
|
||||
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']));
|
||||
@@ -97,10 +74,10 @@ class MsSqlAnalyser extends DatabaseAnalyser {
|
||||
this.pool,
|
||||
this.createQuery('loadSqlCode', ['views', 'procedures', 'functions', 'triggers'])
|
||||
);
|
||||
const getCreateSql = (row) =>
|
||||
const getCreateSql = row =>
|
||||
sqlCodeRows.rows
|
||||
.filter((x) => x.pureName == row.pureName && x.schemaName == row.schemaName)
|
||||
.map((x) => x.codeText)
|
||||
.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(
|
||||
@@ -109,99 +86,63 @@ class MsSqlAnalyser extends DatabaseAnalyser {
|
||||
);
|
||||
const viewColumnRows = await this.driver.query(this.pool, this.createQuery('viewColumns', ['views']));
|
||||
|
||||
const tables = tablesRows.rows.map((row) => ({
|
||||
const tables = tablesRows.rows.map(row => ({
|
||||
...row,
|
||||
columns: columnsRows.rows.filter((col) => col.objectId == row.objectId).map(getColumnInfo),
|
||||
contentHash: row.modifyDate.toISOString(),
|
||||
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) => ({
|
||||
const views = viewsRows.rows.map(row => ({
|
||||
...row,
|
||||
contentHash: row.modifyDate.toISOString(),
|
||||
createSql: getCreateSql(row),
|
||||
columns: viewColumnRows.rows.filter((col) => col.objectId == row.objectId).map(getColumnInfo),
|
||||
columns: viewColumnRows.rows.filter(col => col.objectId == row.objectId).map(getColumnInfo),
|
||||
}));
|
||||
|
||||
const procedures = programmableRows.rows
|
||||
.filter((x) => x.sqlObjectType.trim() == 'P')
|
||||
.map((row) => ({
|
||||
.filter(x => x.sqlObjectType.trim() == 'P')
|
||||
.map(row => ({
|
||||
...row,
|
||||
contentHash: row.modifyDate.toISOString(),
|
||||
createSql: getCreateSql(row),
|
||||
}));
|
||||
|
||||
const functions = programmableRows.rows
|
||||
.filter((x) => ['FN', 'IF', 'TF'].includes(x.sqlObjectType.trim()))
|
||||
.map((row) => ({
|
||||
.filter(x => ['FN', 'IF', 'TF'].includes(x.sqlObjectType.trim()))
|
||||
.map(row => ({
|
||||
...row,
|
||||
contentHash: row.modifyDate.toISOString(),
|
||||
createSql: getCreateSql(row),
|
||||
}));
|
||||
|
||||
return this.mergeAnalyseResult({
|
||||
return {
|
||||
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() {
|
||||
async _getFastSnapshot() {
|
||||
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 res = DatabaseAnalyser.createEmptyStructure();
|
||||
for (const item of modificationsQueryData.rows) {
|
||||
const { type, objectId, modifyDate, schemaName, pureName } = item;
|
||||
const field = objectTypeToField(type);
|
||||
if (!this.structure[field]) return null;
|
||||
// @ts-ignore
|
||||
const obj = this.structure[field].find((x) => x.objectId == objectId);
|
||||
if (!field || !res[field]) continue;
|
||||
|
||||
// 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))];
|
||||
res[field].push({
|
||||
objectId,
|
||||
contentHash: modifyDate.toISOString(),
|
||||
schemaName,
|
||||
pureName,
|
||||
});
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ const driver = require('./driver');
|
||||
|
||||
module.exports = {
|
||||
packageName: 'dbgate-plugin-mssql',
|
||||
driver,
|
||||
drivers: [driver],
|
||||
initialize(dbgateEnv) {
|
||||
driver.initialize(dbgateEnv);
|
||||
},
|
||||
|
||||
@@ -15,6 +15,6 @@ 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]
|
||||
where o.type = 'U' and o.object_id =OBJECT_ID_CONDITION
|
||||
order by c.column_id
|
||||
`;
|
||||
|
||||
@@ -36,5 +36,5 @@ 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]
|
||||
where o.object_id =OBJECT_ID_CONDITION
|
||||
`;
|
||||
|
||||
@@ -3,6 +3,6 @@ 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])
|
||||
where (s.object_id =OBJECT_ID_CONDITION)
|
||||
order by u.name, s.name, c.colid
|
||||
`;
|
||||
|
||||
@@ -10,5 +10,5 @@ where
|
||||
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]
|
||||
and o.object_id =OBJECT_ID_CONDITION
|
||||
`;
|
||||
|
||||
@@ -2,5 +2,5 @@ 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]
|
||||
where o.type in ('P', 'IF', 'FN', 'TF') and o.object_id =OBJECT_ID_CONDITION
|
||||
`;
|
||||
|
||||
@@ -4,5 +4,5 @@ select
|
||||
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]
|
||||
where o.object_id =OBJECT_ID_CONDITION
|
||||
`;
|
||||
|
||||
@@ -13,6 +13,6 @@ select
|
||||
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]
|
||||
WHERE o.type in ('V') and o.object_id =OBJECT_ID_CONDITION
|
||||
order by col.ORDINAL_POSITION
|
||||
`;
|
||||
|
||||
@@ -6,5 +6,5 @@ SELECT
|
||||
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]
|
||||
WHERE type in ('V') and o.object_id =OBJECT_ID_CONDITION
|
||||
`;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { SqlDumper } = require('dbgate-tools');
|
||||
const { SqlDumper } = global.DBGATE_TOOLS;
|
||||
|
||||
class MsSqlDumper extends SqlDumper {
|
||||
autoIncrement() {
|
||||
@@ -67,12 +67,12 @@ class MsSqlDumper extends SqlDumper {
|
||||
|
||||
dropDefault(col) {
|
||||
if (col.defaultConstraint) {
|
||||
this.putCmd("^alter ^table %f ^drop ^constraint %i", col, 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`
|
||||
return col.defaultConstraint || `DF${col.schemaName || 'dbo'}_${col.pureName}_col.columnName`;
|
||||
}
|
||||
|
||||
createDefault(col) {
|
||||
@@ -80,7 +80,7 @@ class MsSqlDumper extends SqlDumper {
|
||||
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);
|
||||
this.putCmd('^alter ^table %f ^add ^constraint %i ^default %s for %i', col, defname, defsql, col.columnName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,8 +89,14 @@ class MsSqlDumper extends SqlDumper {
|
||||
}
|
||||
|
||||
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);
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,5 +115,4 @@ MsSqlDumper.prototype.changeTriggerSchema = MsSqlDumper.prototype.changeObjectSc
|
||||
MsSqlDumper.prototype.renameTable = MsSqlDumper.prototype.renameObject;
|
||||
MsSqlDumper.prototype.changeTableSchema = MsSqlDumper.prototype.changeObjectSchema;
|
||||
|
||||
|
||||
module.exports = MsSqlDumper;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { driverBase } = require('dbgate-tools');
|
||||
const { driverBase } = global.DBGATE_TOOLS;
|
||||
const MsSqlDumper = require('./MsSqlDumper');
|
||||
|
||||
/** @type {import('dbgate-types').SqlDialect} */
|
||||
@@ -32,6 +32,9 @@ const driver = {
|
||||
}
|
||||
return dialect;
|
||||
},
|
||||
showConnectionField: (field, values) =>
|
||||
['authType', 'server', 'port', 'user', 'password', 'defaultDatabase', 'singleDatabase'].includes(field),
|
||||
|
||||
engine: 'mssql@dbgate-plugin-mssql',
|
||||
title: 'Microsoft SQL Server',
|
||||
defaultPort: 1433,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import driver from './driver';
|
||||
|
||||
export default {
|
||||
driver,
|
||||
drivers: [driver],
|
||||
};
|
||||
|
||||
@@ -15,6 +15,12 @@ var config = {
|
||||
library: 'plugin',
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'global.DBGATE_TOOLS': 'window.DBGATE_TOOLS',
|
||||
}),
|
||||
],
|
||||
|
||||
// optimization: {
|
||||
// minimize: false,
|
||||
// },
|
||||
|
||||
@@ -4,7 +4,6 @@ const sql = require('./sql');
|
||||
|
||||
const { DatabaseAnalyser } = require('dbgate-tools');
|
||||
const { isTypeString, isTypeNumeric } = require('dbgate-tools');
|
||||
const { rangeStep } = require('lodash/fp');
|
||||
|
||||
function getColumnInfo({
|
||||
isNullable,
|
||||
@@ -29,12 +28,6 @@ function getColumnInfo({
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -42,33 +35,19 @@ class Analyser extends DatabaseAnalyser {
|
||||
|
||||
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;
|
||||
return super.createQuery(res, typeFields);
|
||||
}
|
||||
|
||||
getRequestedViewNames(allViewNames) {
|
||||
return this.getRequestedObjectPureNames('views', allViewNames);
|
||||
}
|
||||
|
||||
async _computeSingleObjectId() {
|
||||
const { pureName } = this.singleObjectFilter;
|
||||
this.singleObjectId = pureName;
|
||||
}
|
||||
|
||||
async getViewTexts(allViewNames) {
|
||||
const res = {};
|
||||
for (const viewName of this.getRequestedViewNames(allViewNames)) {
|
||||
@@ -96,10 +75,11 @@ class Analyser extends DatabaseAnalyser {
|
||||
|
||||
const viewTexts = await this.getViewTexts(views.rows.map(x => x.pureName));
|
||||
|
||||
return this.mergeAnalyseResult({
|
||||
return {
|
||||
tables: tables.rows.map(table => ({
|
||||
...table,
|
||||
objectId: table.pureName,
|
||||
contentHash: table.modifyDate.toISOString(),
|
||||
columns: columns.rows.filter(col => col.pureName == table.pureName).map(getColumnInfo),
|
||||
primaryKey: DatabaseAnalyser.extractPrimaryKeys(table, pkColumns.rows),
|
||||
foreignKeys: DatabaseAnalyser.extractForeignKeys(table, fkColumns.rows),
|
||||
@@ -107,6 +87,7 @@ class Analyser extends DatabaseAnalyser {
|
||||
views: views.rows.map(view => ({
|
||||
...view,
|
||||
objectId: view.pureName,
|
||||
contentHash: view.modifyDate.toISOString(),
|
||||
columns: columns.rows.filter(col => col.pureName == view.pureName).map(getColumnInfo),
|
||||
createSql: viewTexts[view.pureName],
|
||||
requiresFormat: true,
|
||||
@@ -114,36 +95,23 @@ class Analyser extends DatabaseAnalyser {
|
||||
procedures: programmables.rows
|
||||
.filter(x => x.objectType == 'PROCEDURE')
|
||||
.map(fp.omit(['objectType']))
|
||||
.map(x => ({ ...x, objectId: x.pureName })),
|
||||
.map(x => ({
|
||||
...x,
|
||||
objectId: x.pureName,
|
||||
contentHash: x.modifyDate.toISOString(),
|
||||
})),
|
||||
functions: programmables.rows
|
||||
.filter(x => x.objectType == 'FUNCTION')
|
||||
.map(fp.omit(['objectType']))
|
||||
.map(x => ({ ...x, objectId: x.pureName })),
|
||||
});
|
||||
.map(x => ({
|
||||
...x,
|
||||
objectId: x.pureName,
|
||||
contentHash: x.modifyDate.toISOString(),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
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() {
|
||||
async _getFastSnapshot() {
|
||||
const tableModificationsQueryData = await this.driver.query(this.pool, this.createQuery('tableModifications'));
|
||||
const procedureModificationsQueryData = await this.driver.query(
|
||||
this.pool,
|
||||
@@ -154,66 +122,32 @@ class Analyser extends DatabaseAnalyser {
|
||||
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,
|
||||
return {
|
||||
tables: tableModificationsQueryData.rows
|
||||
.filter(x => x.objectType == 'BASE TABLE')
|
||||
.map(x => ({
|
||||
...x,
|
||||
objectId: x.pureName,
|
||||
contentHash: x.modifyDate.toISOString(),
|
||||
})),
|
||||
views: tableModificationsQueryData.rows
|
||||
.filter(x => x.objectType == 'VIEW')
|
||||
.map(x => ({
|
||||
...x,
|
||||
objectId: x.pureName,
|
||||
contentHash: x.modifyDate.toISOString(),
|
||||
})),
|
||||
procedures: procedureModificationsQueryData.rows.map(x => ({
|
||||
contentHash: x.Modified,
|
||||
objectId: x.Name,
|
||||
pureName: x.Name,
|
||||
})),
|
||||
...functionModificationsQueryData.rows.map(x => ({
|
||||
objectTypeField: 'functions',
|
||||
modifyDate: x.Modified,
|
||||
functions: functionModificationsQueryData.rows.map(x => ({
|
||||
contentHash: x.Modified,
|
||||
objectId: x.Name,
|
||||
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)])];
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+14
-4
@@ -1,6 +1,6 @@
|
||||
const _ = require('lodash');
|
||||
const stream = require('stream');
|
||||
const driverBase = require('../frontend/driver');
|
||||
const driverBases = require('../frontend/drivers');
|
||||
const Analyser = require('./Analyser');
|
||||
const mysql2 = require('mysql2');
|
||||
const { createBulkInsertStreamBase, makeUniqueColumnNames } = require('dbgate-tools');
|
||||
@@ -90,7 +90,7 @@ async function runStreamItem(connection, sql, options) {
|
||||
}
|
||||
|
||||
/** @type {import('dbgate-types').EngineDriver} */
|
||||
const driver = {
|
||||
const drivers = driverBases.map(driverBase => ({
|
||||
...driverBase,
|
||||
analyserClass: Analyser,
|
||||
|
||||
@@ -171,6 +171,16 @@ const driver = {
|
||||
async getVersion(connection) {
|
||||
const { rows } = await this.query(connection, "show variables like 'version'");
|
||||
const version = rows[0].Value;
|
||||
if (version) {
|
||||
const m = version.match(/(.*)-MariaDB-/);
|
||||
if (m) {
|
||||
return {
|
||||
version,
|
||||
versionText: `MariaDB ${m[1]}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
version,
|
||||
versionText: `MySQL ${version}`,
|
||||
@@ -184,6 +194,6 @@ const driver = {
|
||||
// @ts-ignore
|
||||
return createBulkInsertStreamBase(this, stream, pool, name, options);
|
||||
},
|
||||
};
|
||||
}));
|
||||
|
||||
module.exports = driver;
|
||||
module.exports = drivers;
|
||||
@@ -1,6 +1,6 @@
|
||||
const driver = require('./driver');
|
||||
const drivers = require('./drivers');
|
||||
|
||||
module.exports = {
|
||||
packageName: 'dbgate-plugin-mysql',
|
||||
driver,
|
||||
drivers,
|
||||
};
|
||||
|
||||
@@ -10,6 +10,6 @@ select
|
||||
COLUMN_DEFAULT as defaultValue,
|
||||
EXTRA as extra
|
||||
from INFORMATION_SCHEMA.COLUMNS
|
||||
where TABLE_SCHEMA = '#DATABASE#' and TABLE_NAME =[OBJECT_NAME_CONDITION]
|
||||
where TABLE_SCHEMA = '#DATABASE#' and TABLE_NAME =OBJECT_ID_CONDITION
|
||||
order by ORDINAL_POSITION
|
||||
`;
|
||||
|
||||
@@ -12,6 +12,6 @@ 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]
|
||||
where REFERENTIAL_CONSTRAINTS.CONSTRAINT_SCHEMA = '#DATABASE#' and REFERENTIAL_CONSTRAINTS.TABLE_NAME =OBJECT_ID_CONDITION
|
||||
order by KEY_COLUMN_USAGE.ORDINAL_POSITION
|
||||
`;
|
||||
|
||||
@@ -7,6 +7,6 @@ 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'
|
||||
where TABLE_CONSTRAINTS.CONSTRAINT_SCHEMA = '#DATABASE#' and TABLE_CONSTRAINTS.TABLE_NAME =OBJECT_ID_CONDITION AND TABLE_CONSTRAINTS.CONSTRAINT_TYPE = 'PRIMARY KEY'
|
||||
order by KEY_COLUMN_USAGE.ORDINAL_POSITION
|
||||
`;
|
||||
|
||||
@@ -5,5 +5,5 @@ select
|
||||
COALESCE(LAST_ALTERED, CREATED) as modifyDate,
|
||||
ROUTINE_DEFINITION as createSql
|
||||
from information_schema.routines
|
||||
where ROUTINE_SCHEMA = '#DATABASE#' and ROUTINE_NAME =[OBJECT_NAME_CONDITION]
|
||||
where ROUTINE_SCHEMA = '#DATABASE#' and ROUTINE_NAME =OBJECT_ID_CONDITION
|
||||
`;
|
||||
|
||||
@@ -3,5 +3,5 @@ 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];
|
||||
where TABLE_SCHEMA = '#DATABASE#' and TABLE_TYPE='BASE TABLE' and TABLE_NAME =OBJECT_ID_CONDITION;
|
||||
`;
|
||||
|
||||
@@ -3,5 +3,5 @@ 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';
|
||||
where TABLE_SCHEMA = '#DATABASE#' and TABLE_NAME =OBJECT_ID_CONDITION and TABLE_TYPE = 'VIEW';
|
||||
`;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { SqlDumper } = require('dbgate-tools');
|
||||
const { SqlDumper } = global.DBGATE_TOOLS;
|
||||
|
||||
class Dumper extends SqlDumper {
|
||||
/** @param type {import('dbgate-types').TransformType} */
|
||||
|
||||
+19
-6
@@ -1,4 +1,4 @@
|
||||
const { driverBase } = require('dbgate-tools');
|
||||
const { driverBase } = global.DBGATE_TOOLS;
|
||||
const Dumper = require('./Dumper');
|
||||
|
||||
/** @type {import('dbgate-types').SqlDialect} */
|
||||
@@ -14,14 +14,27 @@ const dialect = {
|
||||
},
|
||||
};
|
||||
|
||||
/** @type {import('dbgate-types').EngineDriver} */
|
||||
const driver = {
|
||||
const mysqlDriverBase = {
|
||||
...driverBase,
|
||||
showConnectionField: (field, values) =>
|
||||
['server', 'port', 'user', 'password', 'defaultDatabase', 'singleDatabase'].includes(field),
|
||||
dumperClass: Dumper,
|
||||
dialect,
|
||||
engine: 'mysql@dbgate-plugin-mysql',
|
||||
title: 'MySQL / MariaDB',
|
||||
defaultPort: 3306,
|
||||
};
|
||||
|
||||
module.exports = driver;
|
||||
/** @type {import('dbgate-types').EngineDriver} */
|
||||
const mysqlDriver = {
|
||||
...mysqlDriverBase,
|
||||
engine: 'mysql@dbgate-plugin-mysql',
|
||||
title: 'MySQL',
|
||||
};
|
||||
|
||||
/** @type {import('dbgate-types').EngineDriver} */
|
||||
const mariaDriver = {
|
||||
...mysqlDriverBase,
|
||||
engine: 'mariadb@dbgate-plugin-mysql',
|
||||
title: 'MariaDB',
|
||||
};
|
||||
|
||||
module.exports = [mysqlDriver, mariaDriver];
|
||||
@@ -1,6 +1,6 @@
|
||||
import driver from './driver';
|
||||
import drivers from './drivers';
|
||||
|
||||
export default {
|
||||
packageName: 'dbgate-plugin-mysql',
|
||||
driver,
|
||||
drivers,
|
||||
};
|
||||
|
||||
@@ -15,6 +15,12 @@ var config = {
|
||||
library: 'plugin',
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'global.DBGATE_TOOLS': 'window.DBGATE_TOOLS',
|
||||
}),
|
||||
],
|
||||
|
||||
// uncomment for disable minimalization
|
||||
// optimization: {
|
||||
// minimize: false,
|
||||
|
||||
@@ -34,9 +34,8 @@
|
||||
"dbgate-tools": "^4.1.1",
|
||||
"lodash": "^4.17.15",
|
||||
"pg": "^7.17.0",
|
||||
"pg-query-stream": "^3.1.1",
|
||||
"sql-query-identifier": "^2.1.0",
|
||||
"webpack": "^4.42.0",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"sql-query-identifier": "^2.1.0"
|
||||
"webpack-cli": "^3.3.11"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,26 +12,24 @@ function normalizeTypeName(dataType) {
|
||||
}
|
||||
|
||||
function getColumnInfo({
|
||||
isNullable,
|
||||
isIdentity,
|
||||
columnName,
|
||||
dataType,
|
||||
charMaxLength,
|
||||
numericPrecision,
|
||||
numericScale,
|
||||
defaultValue,
|
||||
is_nullable,
|
||||
column_name,
|
||||
data_type,
|
||||
char_max_length,
|
||||
numeric_precision,
|
||||
numeric_ccale,
|
||||
default_value,
|
||||
}) {
|
||||
const normDataType = normalizeTypeName(dataType);
|
||||
const normDataType = normalizeTypeName(data_type);
|
||||
let fullDataType = normDataType;
|
||||
if (charMaxLength && isTypeString(normDataType)) fullDataType = `${normDataType}(${charMaxLength})`;
|
||||
if (numericPrecision && numericScale && isTypeNumeric(normDataType))
|
||||
fullDataType = `${normDataType}(${numericPrecision},${numericScale})`;
|
||||
if (char_max_length && isTypeString(normDataType)) fullDataType = `${normDataType}(${char_max_length})`;
|
||||
if (numeric_precision && numeric_ccale && isTypeNumeric(normDataType))
|
||||
fullDataType = `${normDataType}(${numeric_precision},${numeric_ccale})`;
|
||||
return {
|
||||
columnName,
|
||||
columnName: column_name,
|
||||
dataType: fullDataType,
|
||||
notNull: !isNullable || isNullable == 'NO' || isNullable == 'no',
|
||||
autoIncrement: !!isIdentity,
|
||||
defaultValue,
|
||||
notNull: !is_nullable || is_nullable == 'NO' || is_nullable == 'no',
|
||||
defaultValue: default_value,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -41,144 +39,132 @@ class Analyser extends DatabaseAnalyser {
|
||||
}
|
||||
|
||||
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;
|
||||
return super.createQuery(sql[resFileName], typeFields);
|
||||
}
|
||||
|
||||
async _computeSingleObjectId() {
|
||||
const { typeField, schemaName, pureName } = this.singleObjectFilter;
|
||||
this.singleObjectId = `${typeField}:${schemaName || 'public'}.${pureName}`;
|
||||
}
|
||||
|
||||
async _runAnalysis() {
|
||||
const tables = await this.driver.query(this.pool, this.createQuery('tableModifications', ['tables']));
|
||||
const tables = await this.driver.query(
|
||||
this.pool,
|
||||
this.createQuery(this.driver.dialect.stringAgg ? 'tableModifications' : 'tableList', ['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),
|
||||
})),
|
||||
return {
|
||||
tables: tables.rows.map(table => {
|
||||
const newTable = {
|
||||
pureName: table.pure_name,
|
||||
schemaName: table.schema_name,
|
||||
objectId: `tables:${table.schema_name}.${table.pure_name}`,
|
||||
contentHash: table.hash_code_columns ? `${table.hash_code_columns}-${table.hash_code_constraints}` : null,
|
||||
};
|
||||
return {
|
||||
...newTable,
|
||||
columns: columns.rows
|
||||
.filter(col => col.pure_name == table.pure_name && col.schema_name == table.schema_name)
|
||||
.map(getColumnInfo),
|
||||
primaryKey: DatabaseAnalyser.extractPrimaryKeys(
|
||||
newTable,
|
||||
pkColumns.rows.map(x => ({
|
||||
pureName: x.pure_name,
|
||||
schemaName: x.schema_name,
|
||||
constraintSchema: x.constraint_schema,
|
||||
constraintName: x.constraint_name,
|
||||
columnName: x.column_name,
|
||||
}))
|
||||
),
|
||||
foreignKeys: DatabaseAnalyser.extractForeignKeys(
|
||||
newTable,
|
||||
fkColumns.rows.map(x => ({
|
||||
pureName: x.pure_name,
|
||||
schemaName: x.schema_name,
|
||||
constraintSchema: x.constraint_schema,
|
||||
constraintName: x.constraint_name,
|
||||
columnName: x.column_name,
|
||||
refColumnName: x.ref_column_name,
|
||||
updateAction: x.update_action,
|
||||
deleteAction: x.delete_action,
|
||||
refTableName: x.ref_table_name,
|
||||
refSchemaName: x.ref_schema_name,
|
||||
}))
|
||||
),
|
||||
};
|
||||
}),
|
||||
views: views.rows.map(view => ({
|
||||
...view,
|
||||
objectId: `views:${view.schemaName}.${view.pureName}`,
|
||||
objectId: `views:${view.schema_name}.${view.pure_name}`,
|
||||
pureName: view.pure_name,
|
||||
schemaName: view.schema_name,
|
||||
contentHash: view.hash_code,
|
||||
columns: columns.rows
|
||||
.filter(col => col.pureName == view.pureName && col.schemaName == view.schemaName)
|
||||
.filter(col => col.pure_name == view.pure_name && col.schema_name == view.schema_name)
|
||||
.map(getColumnInfo),
|
||||
})),
|
||||
procedures: routines.rows
|
||||
.filter(x => x.objectType == 'PROCEDURE')
|
||||
.map(proc => ({
|
||||
objectId: `procedures:${proc.schemaName}.${proc.pureName}`,
|
||||
...proc,
|
||||
objectId: `procedures:${proc.schema_name}.${proc.pure_name}`,
|
||||
pureName: proc.pure_name,
|
||||
schemaName: proc.schema_name,
|
||||
contentHash: proc.hash_code,
|
||||
})),
|
||||
functions: routines.rows
|
||||
.filter(x => x.objectType == 'FUNCTION')
|
||||
.map(func => ({
|
||||
objectId: `functions:${func.schemaName}.${func.pureName}`,
|
||||
...func,
|
||||
objectId: `functions:${func.schema_name}.${func.pure_name}`,
|
||||
pureName: func.pure_name,
|
||||
schemaName: func.schema_name,
|
||||
contentHash: func.hash_code,
|
||||
})),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
async getModifications() {
|
||||
const tableModificationsQueryData = await this.driver.query(this.pool, this.createQuery('tableModifications'));
|
||||
async _getFastSnapshot() {
|
||||
const tableModificationsQueryData = this.driver.dialect.stringAgg
|
||||
? await this.driver.query(this.pool, this.createQuery('tableModifications'))
|
||||
: null;
|
||||
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
|
||||
return {
|
||||
tables: tableModificationsQueryData
|
||||
? tableModificationsQueryData.rows.map(x => ({
|
||||
objectId: `tables:${x.schema_name}.${x.pure_name}`,
|
||||
pureName: x.pure_name,
|
||||
schemaName: x.schema_name,
|
||||
contentHash: `${x.hash_code_columns}-${x.hash_code_constraints}`,
|
||||
}))
|
||||
: null,
|
||||
views: viewModificationsQueryData.rows.map(x => ({
|
||||
objectId: `views:${x.schema_name}.${x.pure_name}`,
|
||||
pureName: x.pure_name,
|
||||
schemaName: x.schema_name,
|
||||
contentHash: x.hash_code,
|
||||
})),
|
||||
procedures: routineModificationsQueryData.rows
|
||||
.filter(x => x.objectType == 'PROCEDURE')
|
||||
.map(x => ({ ...x, objectTypeField: 'procedures' })),
|
||||
...routineModificationsQueryData.rows
|
||||
.map(x => ({
|
||||
objectId: `procedures:${x.schema_name}.${x.pure_name}`,
|
||||
pureName: x.pure_name,
|
||||
schemaName: x.schema_name,
|
||||
contentHash: x.hash_code,
|
||||
})),
|
||||
functions: 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'),
|
||||
];
|
||||
.map(x => ({
|
||||
objectId: `functions:${x.schema_name}.${x.pure_name}`,
|
||||
pureName: x.pure_name,
|
||||
schemaName: x.schema_name,
|
||||
contentHash: x.hash_code,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+101
-93
@@ -2,10 +2,9 @@ const _ = require('lodash');
|
||||
const stream = require('stream');
|
||||
const { identify } = require('sql-query-identifier');
|
||||
|
||||
const driverBase = require('../frontend/driver');
|
||||
const driverBases = require('../frontend/drivers');
|
||||
const Analyser = require('./Analyser');
|
||||
const pg = require('pg');
|
||||
const pgQueryStream = require('pg-query-stream');
|
||||
const { createBulkInsertStreamBase, makeUniqueColumnNames } = require('dbgate-tools');
|
||||
|
||||
function extractPostgresColumns(result) {
|
||||
@@ -26,29 +25,14 @@ function zipDataRow(rowArray, columns) {
|
||||
|
||||
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',
|
||||
// });
|
||||
// };
|
||||
const query = new pg.Query({
|
||||
text: sql,
|
||||
rowMode: 'array',
|
||||
});
|
||||
|
||||
let wasHeader = false;
|
||||
|
||||
const handleEnd = result => {
|
||||
// console.log('RESULT', result);
|
||||
resolve();
|
||||
};
|
||||
|
||||
let columns = null;
|
||||
const handleReadable = () => {
|
||||
query.on('row', row => {
|
||||
if (!wasHeader) {
|
||||
columns = extractPostgresColumns(query._result);
|
||||
if (columns && columns.length > 0) {
|
||||
@@ -57,21 +41,22 @@ async function runStreamItem(client, sql, options) {
|
||||
wasHeader = true;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
const row = stream.read();
|
||||
if (!row) break;
|
||||
options.row(zipDataRow(row, columns));
|
||||
});
|
||||
|
||||
options.row(zipDataRow(row, columns));
|
||||
query.on('end', () => {
|
||||
if (!wasHeader) {
|
||||
columns = extractPostgresColumns(query._result);
|
||||
if (columns && columns.length > 0) {
|
||||
options.recordset(columns);
|
||||
}
|
||||
wasHeader = true;
|
||||
}
|
||||
};
|
||||
|
||||
// const handleFields = (columns) => {
|
||||
// // console.log('FIELDS', columns[0].name);
|
||||
// options.recordset(columns);
|
||||
// // options.recordset(extractColumns(columns));
|
||||
// };
|
||||
resolve();
|
||||
});
|
||||
|
||||
const handleError = error => {
|
||||
query.on('error', error => {
|
||||
console.log('ERROR', error);
|
||||
const { message, lineNumber, procName } = error;
|
||||
options.info({
|
||||
@@ -82,30 +67,49 @@ async function runStreamItem(client, sql, options) {
|
||||
severity: 'error',
|
||||
});
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
|
||||
stream.on('error', handleError);
|
||||
stream.on('readable', handleReadable);
|
||||
// stream.on('result', handleRow)
|
||||
// stream.on('data', handleRow)
|
||||
stream.on('end', handleEnd);
|
||||
client.query(query);
|
||||
});
|
||||
}
|
||||
|
||||
/** @type {import('dbgate-types').EngineDriver} */
|
||||
const driver = {
|
||||
const drivers = driverBases.map(driverBase => ({
|
||||
...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,
|
||||
});
|
||||
async connect({ engine, server, port, user, password, database, databaseUrl, ssl }) {
|
||||
let options = null;
|
||||
|
||||
if (engine == 'redshift@dbgate-plugin-postgres') {
|
||||
let url = databaseUrl;
|
||||
if (url && url.startsWith('jdbc:redshift://')) {
|
||||
url = url.substring('jdbc:redshift://'.length);
|
||||
}
|
||||
if (user && password) {
|
||||
url = `postgres://${user}:${password}@${url}`;
|
||||
} else if (user) {
|
||||
url = `postgres://${user}@${url}`;
|
||||
} else {
|
||||
url = `postgres://${url}`;
|
||||
}
|
||||
|
||||
options = {
|
||||
connectionString: url,
|
||||
};
|
||||
} else {
|
||||
options = {
|
||||
// connectionString: 'postgres://root@localhost:26257/postgres?sslmode=disabke'
|
||||
host: server,
|
||||
port,
|
||||
user,
|
||||
password,
|
||||
database: database || 'postgres',
|
||||
ssl,
|
||||
};
|
||||
}
|
||||
|
||||
const client = new pg.Client(options);
|
||||
await client.connect();
|
||||
return client;
|
||||
},
|
||||
@@ -121,7 +125,13 @@ const driver = {
|
||||
return { rows: res.rows.map(row => zipDataRow(row, columns)), columns };
|
||||
},
|
||||
async stream(client, sql, options) {
|
||||
const sqlSplitted = identify(sql, { dialect: 'psql', strict: false });
|
||||
let sqlSplitted;
|
||||
try {
|
||||
sqlSplitted = identify(sql, { dialect: 'psql', strict: false });
|
||||
} catch (e) {
|
||||
// workaround
|
||||
sqlSplitted = [{ text: sql }];
|
||||
}
|
||||
|
||||
for (const sqlItem of sqlSplitted) {
|
||||
await runStreamItem(client, sqlItem.text, options);
|
||||
@@ -130,50 +140,45 @@ const driver = {
|
||||
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];
|
||||
|
||||
const isCockroach = version.toLowerCase().includes('cockroachdb');
|
||||
const isRedshift = version.toLowerCase().includes('redshift');
|
||||
const isPostgres = !isCockroach && !isRedshift;
|
||||
|
||||
const m = version.match(/([\d\.]+)/);
|
||||
let versionText = null;
|
||||
if (m) {
|
||||
if (isCockroach) versionText = `CockroachDB ${m[1]}`;
|
||||
if (isRedshift) versionText = `Redshift ${m[1]}`;
|
||||
if (isPostgres) versionText = `PostgreSQL ${m[1]}`;
|
||||
}
|
||||
|
||||
return {
|
||||
version,
|
||||
versionText: (version || '').replace(/\s*\(.*$/, ''),
|
||||
versionText,
|
||||
isPostgres,
|
||||
isCockroach,
|
||||
isRedshift,
|
||||
};
|
||||
},
|
||||
// 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);
|
||||
const query = new pg.Query({
|
||||
text: sql,
|
||||
rowMode: 'array',
|
||||
});
|
||||
|
||||
let wasHeader = false;
|
||||
let columns = null;
|
||||
|
||||
const pass = new stream.PassThrough({
|
||||
objectMode: true,
|
||||
highWaterMark: 100,
|
||||
});
|
||||
|
||||
const handleEnd = result => {
|
||||
pass.end();
|
||||
};
|
||||
|
||||
let columns = null;
|
||||
const handleReadable = () => {
|
||||
query.on('row', row => {
|
||||
if (!wasHeader) {
|
||||
columns = extractPostgresColumns(query._result);
|
||||
pass.write({
|
||||
@@ -183,28 +188,31 @@ const driver = {
|
||||
wasHeader = true;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
const row = queryStream.read();
|
||||
if (!row) break;
|
||||
pass.write(zipDataRow(row, columns));
|
||||
});
|
||||
|
||||
pass.write(zipDataRow(row, columns));
|
||||
query.on('end', () => {
|
||||
if (!wasHeader) {
|
||||
columns = extractPostgresColumns(query._result);
|
||||
pass.write({
|
||||
__isStreamHeader: true,
|
||||
...(structure || { columns }),
|
||||
});
|
||||
wasHeader = true;
|
||||
}
|
||||
};
|
||||
|
||||
const handleError = error => {
|
||||
pass.end();
|
||||
});
|
||||
|
||||
query.on('error', error => {
|
||||
console.error(error);
|
||||
pass.end();
|
||||
};
|
||||
});
|
||||
|
||||
queryStream.on('error', handleError);
|
||||
queryStream.on('readable', handleReadable);
|
||||
queryStream.on('end', handleEnd);
|
||||
client.query(query);
|
||||
|
||||
return pass;
|
||||
},
|
||||
// createDumper() {
|
||||
// return new PostgreDumper(this);
|
||||
// },
|
||||
async writeTable(pool, name, options) {
|
||||
// @ts-ignore
|
||||
return createBulkInsertStreamBase(this, stream, pool, name, options);
|
||||
@@ -213,6 +221,6 @@ const driver = {
|
||||
const { rows } = await this.query(client, 'SELECT datname AS name FROM pg_database WHERE datistemplate = false');
|
||||
return rows;
|
||||
},
|
||||
};
|
||||
}));
|
||||
|
||||
module.exports = driver;
|
||||
module.exports = drivers;
|
||||
@@ -1,6 +1,6 @@
|
||||
const driver = require('./driver');
|
||||
const drivers = require('./drivers');
|
||||
|
||||
module.exports = {
|
||||
packageName: 'dbgate-plugin-postgres',
|
||||
driver,
|
||||
drivers,
|
||||
};
|
||||
|
||||
@@ -1,19 +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"
|
||||
table_schema as "schema_name",
|
||||
table_name as "pure_name",
|
||||
column_name as "column_name",
|
||||
is_nullable as "is_nullable",
|
||||
data_type as "data_type",
|
||||
character_maximum_length as "char_max_length",
|
||||
numeric_precision as "numeric_precision",
|
||||
numeric_scale as "numeric_scale",
|
||||
column_default as "default_value"
|
||||
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
|
||||
and ('tables:' || table_schema || '.' || table_name) =OBJECT_ID_CONDITION
|
||||
order by ordinal_position
|
||||
`;
|
||||
@@ -1,15 +1,15 @@
|
||||
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"
|
||||
fk.constraint_name as "constraint_name",
|
||||
fk.constraint_schema as "constraint_schema",
|
||||
base.table_name as "pure_name",
|
||||
base.table_schema as "schema_name",
|
||||
fk.update_rule as "update_action",
|
||||
fk.delete_rule as "delete_action",
|
||||
ref.table_name as "ref_table_name",
|
||||
ref.table_schema as "ref_schema_name",
|
||||
basecol.column_name as "column_name",
|
||||
refcol.column_name as "ref_column_name"
|
||||
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
|
||||
@@ -19,6 +19,6 @@ 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
|
||||
and ('tables:' || base.table_schema || '.' || base.table_name) =OBJECT_ID_CONDITION
|
||||
order by basecol.ordinal_position
|
||||
`;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const columns = require('./columns');
|
||||
const tableModifications = require('./tableModifications');
|
||||
const tableList = require('./tableList');
|
||||
const viewModifications = require('./viewModifications');
|
||||
const primaryKeys = require('./primaryKeys');
|
||||
const foreignKeys = require('./foreignKeys');
|
||||
@@ -10,6 +11,7 @@ const routineModifications = require('./routineModifications');
|
||||
module.exports = {
|
||||
columns,
|
||||
tableModifications,
|
||||
tableList,
|
||||
viewModifications,
|
||||
primaryKeys,
|
||||
foreignKeys,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
module.exports = `
|
||||
select
|
||||
table_constraints.constraint_schema as "constraintSchema",
|
||||
table_constraints.constraint_name as "constraintName",
|
||||
table_constraints.table_schema as "schemaName",
|
||||
table_constraints.table_name as "pureName",
|
||||
key_column_usage.column_name as "columnName"
|
||||
table_constraints.constraint_schema as "constraint_schema",
|
||||
table_constraints.constraint_name as "constraint_name",
|
||||
table_constraints.table_schema as "schema_name",
|
||||
table_constraints.table_name as "pure_name",
|
||||
key_column_usage.column_name as "column_name"
|
||||
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
|
||||
where
|
||||
@@ -12,6 +12,6 @@ where
|
||||
and table_constraints.table_schema <> 'pg_catalog'
|
||||
and table_constraints.table_schema !~ '^pg_toast'
|
||||
and table_constraints.constraint_type = 'PRIMARY KEY'
|
||||
and 'tables:' || table_constraints.table_schema || '.' || table_constraints.table_name =OBJECT_ID_CONDITION
|
||||
and ('tables:' || table_constraints.table_schema || '.' || table_constraints.table_name) =OBJECT_ID_CONDITION
|
||||
order by key_column_usage.ordinal_position
|
||||
`;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
module.exports = `
|
||||
select
|
||||
routine_name as "pureName",
|
||||
routine_schema as "schemaName",
|
||||
md5(routine_definition) as "hashCode",
|
||||
routine_type as "objectType"
|
||||
routine_name as "pure_name",
|
||||
routine_schema as "schema_name",
|
||||
md5(routine_definition) as "hash_code",
|
||||
routine_type as "object_type"
|
||||
from
|
||||
information_schema.routines where routine_schema != 'information_schema' and routine_schema != 'pg_catalog'
|
||||
and routine_type in ('PROCEDURE', 'FUNCTION')
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
module.exports = `
|
||||
select infoTables.table_schema as "schema_name", infoTables.table_name as "pure_name"
|
||||
from information_schema.tables infoTables
|
||||
where infoTables.table_type not like '%VIEW%'
|
||||
and ('tables:' || infoTables.table_schema || '.' || infoTables.table_name) =OBJECT_ID_CONDITION
|
||||
and infoTables.table_schema <> 'pg_catalog'
|
||||
and infoTables.table_schema <> 'information_schema'
|
||||
and infoTables.table_schema <> 'pg_internal'
|
||||
and infoTables.table_schema !~ '^pg_toast'
|
||||
`;
|
||||
@@ -1,52 +1,28 @@
|
||||
module.exports = `
|
||||
with pkey as
|
||||
(
|
||||
select cc.conrelid, format(E'create constraint %I primary key(%s);\\n', cc.conname,
|
||||
string_agg(a.attname, ', '
|
||||
order by array_position(cc.conkey, a.attnum))) pkey
|
||||
from pg_catalog.pg_constraint cc
|
||||
join pg_catalog.pg_class c on c.oid = cc.conrelid
|
||||
join pg_catalog.pg_attribute a on a.attrelid = cc.conrelid
|
||||
and a.attnum = any(cc.conkey)
|
||||
where cc.contype = 'p'
|
||||
group by cc.conrelid, cc.conname
|
||||
)
|
||||
|
||||
|
||||
SELECT oid as "objectId", nspname as "schemaName", relname as "pureName",
|
||||
md5('CREATE TABLE ' || nspname || '.' || relname || E'\\n(\\n' ||
|
||||
array_to_string(
|
||||
array_agg(
|
||||
' ' || column_name || ' ' || type || ' '|| not_null
|
||||
select infoTables.table_schema as "schema_name", infoTables.table_name as "pure_name",
|
||||
(
|
||||
select md5(string_agg(
|
||||
infoColumns.column_name || '|' || infoColumns.data_type || '|' || infoColumns.is_nullable::varchar(255) || '|' || coalesce(infoColumns.character_maximum_length, -1)::varchar(255)
|
||||
|| '|' || coalesce(infoColumns.numeric_precision, -1)::varchar(255) ,
|
||||
',' order by infoColumns.ordinal_position
|
||||
)) as "hash_code_columns"
|
||||
from information_schema.columns infoColumns
|
||||
where infoColumns.table_schema = infoTables.table_schema and infoColumns.table_name = infoTables.table_name
|
||||
),
|
||||
(
|
||||
select md5(string_agg(
|
||||
infoConstraints.constraint_name || '|' || infoConstraints.constraint_type ,
|
||||
',' order by infoConstraints.constraint_name
|
||||
)) as "hash_code_constraints"
|
||||
from information_schema.table_constraints infoConstraints
|
||||
where infoConstraints.table_schema = infoTables.table_schema and infoConstraints.table_name = infoTables.table_name
|
||||
)
|
||||
, E',\\n'
|
||||
) || E'\\n);\\n' || coalesce((select pkey from pkey where pkey.conrelid = oid),'NO_PK')) as "hashCode"
|
||||
from
|
||||
(
|
||||
SELECT
|
||||
c.relname, a.attname AS column_name, c.oid,
|
||||
n.nspname,
|
||||
pg_catalog.format_type(a.atttypid, a.atttypmod) as type,
|
||||
case
|
||||
when a.attnotnull
|
||||
then 'NOT NULL'
|
||||
else 'NULL'
|
||||
END as not_null
|
||||
FROM pg_class c,
|
||||
pg_namespace n,
|
||||
pg_attribute a,
|
||||
pg_type t
|
||||
|
||||
WHERE c.relkind = 'r'
|
||||
AND a.attnum > 0
|
||||
AND a.attrelid = c.oid
|
||||
AND a.atttypid = t.oid
|
||||
AND n.oid = c.relnamespace
|
||||
AND n.nspname <> 'pg_catalog'
|
||||
AND n.nspname <> 'information_schema'
|
||||
AND n.nspname !~ '^pg_toast'
|
||||
ORDER BY a.attnum
|
||||
) as tabledefinition
|
||||
where ('tables:' || nspname || '.' || relname) =OBJECT_ID_CONDITION
|
||||
group by relname, nspname, oid
|
||||
|
||||
from information_schema.tables infoTables
|
||||
where infoTables.table_type not like '%VIEW%'
|
||||
and ('tables:' || infoTables.table_schema || '.' || infoTables.table_name) =OBJECT_ID_CONDITION
|
||||
and infoTables.table_schema <> 'pg_catalog'
|
||||
and infoTables.table_schema <> 'information_schema'
|
||||
and infoTables.table_schema <> 'pg_internal'
|
||||
and infoTables.table_schema !~ '^pg_toast'
|
||||
`;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
module.exports = `
|
||||
select
|
||||
table_name as "pureName",
|
||||
table_schema as "schemaName",
|
||||
md5(view_definition) as "hashCode"
|
||||
table_name as "pure_name",
|
||||
table_schema as "schema_name",
|
||||
md5(view_definition) as "hash_code"
|
||||
from
|
||||
information_schema.views where table_schema != 'information_schema' and table_schema != 'pg_catalog'
|
||||
`;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
module.exports = `
|
||||
select
|
||||
table_name as "pureName",
|
||||
table_schema as "schemaName",
|
||||
view_definition as "createSql",
|
||||
md5(view_definition) as "hashCode"
|
||||
table_name as "pure_name",
|
||||
table_schema as "schema_name",
|
||||
view_definition as "create_sql",
|
||||
md5(view_definition) as "hash_code"
|
||||
from
|
||||
information_schema.views
|
||||
where table_schema != 'information_schema' and table_schema != 'pg_catalog'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { SqlDumper } = require('dbgate-tools');
|
||||
const { SqlDumper } = global.DBGATE_TOOLS;
|
||||
|
||||
class Dumper extends SqlDumper {
|
||||
/** @param type {import('dbgate-types').TransformType} */
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
const { driverBase } = require('dbgate-tools');
|
||||
const Dumper = require('./Dumper');
|
||||
|
||||
/** @type {import('dbgate-types').SqlDialect} */
|
||||
const dialect = {
|
||||
rangeSelect: true,
|
||||
// stringEscapeChar: '\\',
|
||||
stringEscapeChar: "'",
|
||||
fallbackDataType: 'varchar',
|
||||
anonymousPrimaryKey: true,
|
||||
enableConstraintsPerTable: true,
|
||||
quoteIdentifier(s) {
|
||||
return '"' + s + '"';
|
||||
},
|
||||
};
|
||||
|
||||
/** @type {import('dbgate-types').EngineDriver} */
|
||||
const driver = {
|
||||
...driverBase,
|
||||
dumperClass: Dumper,
|
||||
dialect,
|
||||
engine: 'postgres@dbgate-plugin-postgres',
|
||||
title: 'Postgre SQL',
|
||||
defaultPort: 5432,
|
||||
};
|
||||
|
||||
module.exports = driver;
|
||||
@@ -0,0 +1,71 @@
|
||||
const { driverBase } = global.DBGATE_TOOLS;
|
||||
const Dumper = require('./Dumper');
|
||||
|
||||
/** @type {import('dbgate-types').SqlDialect} */
|
||||
const dialect = {
|
||||
rangeSelect: true,
|
||||
// stringEscapeChar: '\\',
|
||||
stringEscapeChar: "'",
|
||||
fallbackDataType: 'varchar',
|
||||
anonymousPrimaryKey: true,
|
||||
enableConstraintsPerTable: true,
|
||||
quoteIdentifier(s) {
|
||||
return '"' + s + '"';
|
||||
},
|
||||
stringAgg: true,
|
||||
};
|
||||
|
||||
const postgresDriverBase = {
|
||||
...driverBase,
|
||||
dumperClass: Dumper,
|
||||
dialect,
|
||||
showConnectionField: (field, values) =>
|
||||
['server', 'port', 'user', 'password', 'defaultDatabase', 'singleDatabase'].includes(field),
|
||||
};
|
||||
|
||||
/** @type {import('dbgate-types').EngineDriver} */
|
||||
const postgresDriver = {
|
||||
...postgresDriverBase,
|
||||
engine: 'postgres@dbgate-plugin-postgres',
|
||||
title: 'Postgre SQL',
|
||||
defaultPort: 5432,
|
||||
};
|
||||
|
||||
/** @type {import('dbgate-types').EngineDriver} */
|
||||
const cockroachDriver = {
|
||||
...postgresDriverBase,
|
||||
engine: 'cockroach@dbgate-plugin-postgres',
|
||||
title: 'CockroachDB',
|
||||
defaultPort: 26257,
|
||||
};
|
||||
|
||||
/** @type {import('dbgate-types').EngineDriver} */
|
||||
const redshiftDriver = {
|
||||
...postgresDriverBase,
|
||||
dialect: {
|
||||
...dialect,
|
||||
stringAgg: false,
|
||||
},
|
||||
engine: 'redshift@dbgate-plugin-postgres',
|
||||
title: 'Amazon Redshift',
|
||||
defaultPort: 5439,
|
||||
databaseUrlPlaceholder: 'e.g. redshift-cluster-1.xxxx.redshift.amazonaws.com:5439/dev',
|
||||
showConnectionField: (field, values) => ['databaseUrl', 'user', 'password'].includes(field),
|
||||
beforeConnectionSave: connection => {
|
||||
const { databaseUrl } = connection;
|
||||
if (databaseUrl) {
|
||||
const m = databaseUrl.match(/\/([^/]+)$/);
|
||||
if (m) {
|
||||
return {
|
||||
...connection,
|
||||
singleDatabase: true,
|
||||
defaultDatabase: m[1],
|
||||
// displayName: connection.displayName || `${m[1]} on Amazon Redshift`,
|
||||
};
|
||||
}
|
||||
}
|
||||
return connection;
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = [postgresDriver, cockroachDriver, redshiftDriver];
|
||||
@@ -1,6 +1,6 @@
|
||||
import driver from './driver';
|
||||
import drivers from './drivers';
|
||||
|
||||
export default {
|
||||
packageName: 'dbgate-plugin-postgres',
|
||||
driver,
|
||||
drivers,
|
||||
};
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
var webpack = require("webpack");
|
||||
var path = require("path");
|
||||
var webpack = require('webpack');
|
||||
var path = require('path');
|
||||
|
||||
var config = {
|
||||
context: __dirname + "/src/frontend",
|
||||
context: __dirname + '/src/frontend',
|
||||
|
||||
entry: {
|
||||
app: "./index.js",
|
||||
app: './index.js',
|
||||
},
|
||||
target: "web",
|
||||
target: 'web',
|
||||
output: {
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
filename: "frontend.js",
|
||||
libraryTarget: "var",
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'frontend.js',
|
||||
libraryTarget: 'var',
|
||||
library: 'plugin',
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'global.DBGATE_TOOLS': 'window.DBGATE_TOOLS',
|
||||
}),
|
||||
],
|
||||
|
||||
// uncomment for disable minimalization
|
||||
// optimization: {
|
||||
// minimize: false,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user