Compare commits

...

115 Commits

Author SHA1 Message Date
Jan Prochazka 7dd9e9a9b1 v4.2.2 2021-05-20 10:48:02 +02:00
Jan Prochazka 19d135e435 sqlite plugin package.json 2021-05-20 10:47:04 +02:00
Jan Prochazka d453e52ff3 v4.2.2-beta.2 2021-05-20 10:30:58 +02:00
Jan Prochazka 29a77cc053 zero loading info 2021-05-20 10:30:35 +02:00
Jan Prochazka 11fd2d1d8a description 2021-05-20 10:23:12 +02:00
Jan Prochazka b5fe8508b1 [ackaged plugins for electron optimalization 2021-05-20 10:22:08 +02:00
Jan Prochazka 25881e80db v4.2.2-beta.1 2021-05-20 08:56:17 +02:00
Jan Prochazka e43fa96e34 one more optimalization of plugin size & load time 2021-05-20 08:55:50 +02:00
Jan Prochazka 0200c7c78b further optimalization of frontend plugins 2021-05-20 07:42:25 +02:00
Jan Prochazka 42e573a3ae v4.2.1 2021-05-19 21:57:40 +02:00
Jan Prochazka 395b0a91b0 changedlog 2021-05-19 21:57:28 +02:00
Jan Prochazka 62c529cf50 v4.2.1-beta.2 2021-05-19 20:27:32 +02:00
Jan Prochazka 00a169725e lodash optimalization 2021-05-19 20:18:33 +02:00
Jan Prochazka bcf0bfd5ef fix loading message 2021-05-19 20:18:19 +02:00
Jan Prochazka 52ed8874e3 v4.2.1-beta.1 2021-05-19 19:39:32 +02:00
Jan Prochazka 319e08f5f3 start app after load plugins 2021-05-19 19:39:12 +02:00
Jan Prochazka c4491050cd removed splash, plugins load info 2021-05-19 19:36:12 +02:00
Jan Prochazka f0ea35d576 fixes 2021-05-17 20:47:29 +02:00
Jan Prochazka 70d53e8abe v4.2.0 2021-05-17 18:59:55 +02:00
Jan Prochazka 8f28ce3659 v4.2.0-beta.10 2021-05-17 18:20:54 +02:00
Jan Prochazka 050b46813f start app fix 2021-05-17 18:20:31 +02:00
Jan Prochazka 4bae23ecfa v4.2.0-beta.9 2021-05-17 17:56:57 +02:00
Jan Prochazka 6eb16ad750 fixed shift processing in datagrid 2021-05-17 17:55:25 +02:00
Jan Prochazka 482a823f4f better UX in model refresh 2021-05-17 17:50:04 +02:00
Jan Prochazka 9d933d669a fixed race conditions when starting app 2021-05-17 17:42:53 +02:00
Jan Prochazka e44a95d723 v4.2.0-beta.8 2021-05-16 20:22:09 +02:00
Jan Prochazka cae882c8d6 styling fix 2021-05-16 20:21:36 +02:00
Jan Prochazka 026726a6ed v4.2.0-beta.7 2021-05-16 20:17:03 +02:00
Jan Prochazka 70d06deeb0 model age in statusbar, sync model is not automatic by default 2021-05-16 20:14:46 +02:00
Jan Prochazka 6dfe9b798b Commandline arguments #108 2021-05-16 19:21:52 +02:00
Jan Prochazka 73c14eba6d v4.2.0-beta.6 2021-05-16 18:25:46 +02:00
Jan Prochazka 7c91dda170 fix 2021-05-16 18:25:32 +02:00
Jan Prochazka 40ebedaef0 v4.2.0-beta.5 2021-05-16 14:30:46 +02:00
Jan Prochazka 614f852f71 v4.2.0-beta.4 2021-05-16 14:30:17 +02:00
Jan Prochazka a8e3a6cfec env variables configuration 2021-05-16 14:29:59 +02:00
Jan Prochazka be053acf3c horizontal scroll in datagrid #113 2021-05-16 14:21:36 +02:00
Jan Prochazka 91741655b7 fixes single object analyser 2021-05-16 14:11:35 +02:00
Jan Prochazka c57a67da09 postgre: using streamed query instead of cursor 2021-05-16 13:59:49 +02:00
Jan Prochazka 2376cb30db fix 2021-05-16 13:58:22 +02:00
Jan Prochazka 3a08462018 fix analyser for cockroach 2021-05-16 11:43:19 +02:00
Jan Prochazka c95677bd83 redshift url placeholder 2021-05-16 11:25:07 +02:00
Jan Prochazka 8bffa4a7dd more flexible connection dialog, improved UX when connecting to redshift 2021-05-16 11:22:48 +02:00
Jan Prochazka 6d7cc7d441 design 2021-05-16 09:40:41 +02:00
Jan Prochazka acc49273c1 postgre sql analyser - works also for redshift 2021-05-16 08:56:56 +02:00
Jan Prochazka 640b53e45f small refactor 2021-05-15 22:08:15 +02:00
Jan Prochazka 7857771056 postgre modification detection algorithm 2021-05-15 22:03:23 +02:00
Jan Prochazka b8513b3ecd code cleanup 2021-05-15 21:36:00 +02:00
Jan Prochazka 0a56e3b782 delete commented code 2021-05-15 21:30:48 +02:00
Jan Prochazka 87e75c6ba1 mysql analyser - new changes detection 2021-05-15 21:30:12 +02:00
Jan Prochazka cf5afb43eb improved modification detection algorithm - for mssql 2021-05-15 21:14:00 +02:00
Jan Prochazka 2eb1c04fcf DatabaseAnalyser.createQuery core moved to base class 2021-05-15 18:41:30 +02:00
Jan Prochazka 4a4c4b41c0 OBJECT_ID_CONDITION 2021-05-15 18:15:46 +02:00
Jan Prochazka 032eaf9eb0 single object analysis refactor 2021-05-15 18:13:20 +02:00
Jan Prochazka 06a028a093 code cleanup 2021-05-15 18:08:12 +02:00
Jan Prochazka 21ceaecec6 mariadb version parsing 2021-05-15 09:01:06 +02:00
Jan Prochazka c5605d63ca driver plugins supports more drivers. Added derived drivers for MariaDB, CockroachDB, Amazon Redshift 2021-05-15 08:49:58 +02:00
Jan Prochazka f9545eaf7f CockroachDB analysis #112 2021-05-14 16:44:48 +02:00
Jan Prochazka 216ef7736b #112 fix for CockroachDB 2021-05-13 12:05:56 +02:00
Jan Prochazka ae7697f655 v4.2.0-beta.3 2021-05-13 09:08:27 +02:00
Jan Prochazka 23225cf86b try to fix sqlite problem 2021-05-13 08:41:45 +02:00
Jan Prochazka 63ad36f758 v4.2.0-beta.2 2021-05-06 19:18:47 +02:00
Jan Prochazka 80e1563877 missing dependency 2021-05-06 19:18:34 +02:00
Jan Prochazka 3f5c7aecd7 v4.2.0-beta.1 2021-05-06 18:36:36 +02:00
Jan Prochazka abd2492889 Merge branch 'master' into sqlite 2021-05-06 18:36:15 +02:00
Jan Prochazka 872468899d electron - open sqlite database with drag & drop or in open file menu 2021-05-06 18:33:50 +02:00
Jan Prochazka 7a008e5a9d sqlite bulk insert 2021-05-06 15:57:50 +02:00
Jan Prochazka 23940aa324 sqlite version 2021-05-06 15:27:25 +02:00
Jan Prochazka 1888de8728 sqlite stream reader 2021-05-06 15:23:45 +02:00
Jan Prochazka 615397f332 sqlite FK analyser, query runs in transaction 2021-05-06 14:11:51 +02:00
Jan Prochazka e251459512 sqlite sync query 2021-05-06 13:32:37 +02:00
Jan Prochazka a9c8cee08a sqlite stream 2021-05-06 12:32:54 +02:00
Jan Prochazka 1638095c98 database file label 2021-05-06 11:17:30 +02:00
Jan Prochazka 62cedd23b7 extracted getConnectionLabel functionality 2021-05-06 11:08:03 +02:00
Jan Prochazka 3d882f47a7 connection modal fix 2021-05-06 10:50:11 +02:00
Jan Prochazka 88ddc28208 scripts related to server 2021-05-06 10:34:24 +02:00
Jan Prochazka 800666f813 expand button fix 2021-05-06 09:48:07 +02:00
Jan Prochazka 0b8add848a execute command disabled, when query has not connection 2021-05-06 09:43:32 +02:00
Jan Prochazka cd7edcb443 disconnect command (hard disconnect in electron, soft disconnect in webapp) 2021-05-06 09:34:05 +02:00
Jan Prochazka e483fd9e99 changelog 2021-05-05 20:07:04 +02:00
Jan Prochazka 9664e6f981 v4.1.12 2021-05-05 20:05:35 +02:00
Jan Prochazka d1429dd2a1 readme 2021-05-05 20:05:09 +02:00
Jan Prochazka e739aed80d sqlite table analyser 2021-05-05 20:04:49 +02:00
Jan Prochazka 28e19402f3 Merge branch 'master' into sqlite 2021-05-03 21:09:41 +02:00
Jan Prochazka 45a065f391 v4.1.12-beta.2 2021-05-03 21:08:58 +02:00
Jan Prochazka 67e8eb32f7 svelte select fix 2021-05-03 21:08:45 +02:00
Jan Prochazka 5622e3af77 v4.1.12-beta.1 2021-05-03 20:41:19 +02:00
Jan Prochazka 7d34458553 fixed race condition when using SSH tunnel #110 2021-05-03 20:39:41 +02:00
Jan Prochazka 8b747796e7 Merge branch 'master' into sqlite 2021-05-03 18:43:34 +02:00
Jan Prochazka 4802c36b54 changelog 2021-05-03 18:42:04 +02:00
Jan Prochazka 988e4345d4 v4.1.11 2021-05-03 18:36:38 +02:00
Jan Prochazka e02305879e v4.1.11-beta.2 2021-04-30 20:42:34 +02:00
Jan Prochazka 8baad56315 toolbar shows tab related commands aligned to right 2021-04-30 20:35:43 +02:00
Jan Prochazka 14bbc7b057 duplicate tab popup menu 2021-04-30 18:46:44 +02:00
Jan Prochazka 7b6ca27b66 add to favorites moved from toolbar into tab context menu 2021-04-30 18:03:34 +02:00
Jan Prochazka 38aae142ea loading structure status fix 2021-04-30 17:30:18 +02:00
Jan Prochazka bd6c116cc0 timg safe compare token fixes #91 2021-04-30 17:21:35 +02:00
Jan Prochazka 4522c37bfa docker beta build 2021-04-29 20:47:35 +02:00
Jan Prochazka 7d789d5712 #109 all tables button in export fixed + added All collections button for nosql 2021-04-29 20:44:46 +02:00
Jan Prochazka c4c2274488 v4.1.11-beta.1 2021-04-29 14:06:34 +02:00
Jan Prochazka a8b71d452b ssh tunnel keyfile auth fix #106 2021-04-29 14:05:32 +02:00
Jan Prochazka c7d69b0fb5 duplicate connection command 2021-04-29 13:25:12 +02:00
Jan Prochazka 47ea474555 settings optimalization 2021-04-29 11:28:32 +02:00
Jan Prochazka e647ab471e ability to disable background model updates 2021-04-29 11:17:17 +02:00
Jan Prochazka fd6524867e check & load db model in statusbar 2021-04-29 10:40:53 +02:00
Jan Prochazka c24cc1dc72 patched svelte crash #105 2021-04-29 10:03:13 +02:00
Jan Prochazka e3d1e4f53e fixed analysing postgre functions #105 2021-04-29 09:32:59 +02:00
Jan Prochazka 7b32424143 fix 2021-04-29 09:31:41 +02:00
Jan Prochazka 519767fd49 fixed postgres split query 2021-04-29 08:55:38 +02:00
Jan Prochazka 505ab2e075 editor theme to be added 2021-04-29 08:28:00 +02:00
Jan Prochazka 00d0c27502 handle plugin load error 2021-04-29 07:38:44 +02:00
Jan Prochazka d171d7d785 changelog 2021-04-26 18:56:42 +02:00
Jan Prochazka 09593e0b22 changelog 2021-04-26 18:33:57 +02:00
Jan Prochazka caa2d22dbd sqlite WIP 2021-04-25 21:53:27 +02:00
Jan Prochazka 3c089a5b81 connection modal supports file database 2021-04-25 20:38:41 +02:00
Jan Prochazka d1bf2dbc4b sqlite plugin scaffold 2021-04-25 18:49:53 +02:00
159 changed files with 2704 additions and 1462 deletions
+3
View File
@@ -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
+3
View File
@@ -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
+47
View File
@@ -0,0 +1,47 @@
name: Docker image
# on: [push]
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+'
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-18.04]
steps:
- name: Context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Use Node.js 10.x
uses: actions/setup-node@v1
with:
node-version: 10.x
- name: yarn install
run: |
yarn install
- name: setCurrentVersion
run: |
yarn setCurrentVersion
- name: Prepare docker image
run: |
yarn run prepare:docker
- name: Build docker image
run: |
docker build ./docker -t dbgate
- name: Push docker image
run: |
docker tag dbgate dbgate/dbgate:beta
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker push dbgate/dbgate:beta
+5
View File
@@ -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
+1
View File
@@ -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
+47
View File
@@ -1,5 +1,52 @@
# ChangeLog
### 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)
### 4.1.11
- FIX: fixed processing postgre query containing $$
- FIX: fixed postgre analysing procedures & functions
- FIX: patched svelte crash #105
- ADDED: ability to disbale background DB model updates
- ADDED: Duplicate connection
- ADDED: Duplicate tab
- FIX: SSH tunnel connection using keyfile auth #106
- FIX: All tables button fix in export #109
- CHANGED: Add to favorites moved from toolbar to tab context menu
- CHANGED: Toolbar design - current tab related commands are delimited
### 4.1.10
- ADDED: Default database option in connectin settings #96 #92
- FIX: Bundle size optimalization for Windows #97
- FIX: Popup menu placement on smaller displays #94
- ADDED: Browse table data with SQL Server 2008 #93
- FIX: Prevented malicious origins / DNS rebinding #91
- ADDED: Handle JSON fields in data editor (eg. jsonb field in Postgres) #90
- FIX: Fixed crash on Windows with Hyper-V #86
- ADDED: Show database server version in status bar
- ADDED: Show detailed info about error, when connect to database fails
- ADDED: Portable ZIP distribution for Windows #84
### 4.1.9
- FIX: Incorrect row count info in query result #83
### 4.1.1
- CHANGED: Default plugins are now part of installation
### 4.1.0
+5 -1
View File
@@ -1,4 +1,5 @@
[![NPM version](https://img.shields.io/npm/v/dbgate.svg)](https://www.npmjs.com/package/dbgate)
![GitHub All Releases](https://img.shields.io/github/downloads/dbgate/dbgate/total)
[![dbgate](https://snapcraft.io/dbgate/badge.svg)](https://snapcraft.io/dbgate)
[![dbgate](https://snapcraft.io/dbgate/trending.svg?name=0)](https://snapcraft.io/dbgate)
[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
@@ -16,11 +17,14 @@ Supported databases:
* PostgreSQL
* SQL Server
* MongoDB
* SQLite
* Amazon Redshift
* CockroachDB
* MariaDB
![Screenshot](https://raw.githubusercontent.com/dbgate/dbgate/master/screenshot.png)
## Features
* Connect to Microsoft SQL Server, Postgre SQL, MySQL, MongoDB
* Table data editing, with SQL change script preview
* Master/detail views
* Query designer
+2 -1
View File
@@ -5,6 +5,7 @@
"author": "Jan Prochazka <jenasoft.database@gmail.com>",
"description": "Opensource database administration tool",
"dependencies": {
"better-sqlite3-with-prebuilds": "^7.1.8",
"electron-log": "^4.3.1",
"electron-store": "^5.1.1",
"electron-updater": "^4.3.5"
@@ -38,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
View File
@@ -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 {
+98
View File
@@ -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,6 +237,23 @@ 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-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"
bindings@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
dependencies:
file-uri-to-path "1.0.0"
bl@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489"
@@ -369,6 +391,11 @@ chownr@^1.1.1:
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
chownr@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
chromium-pickle-js@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz#04a106672c18b085ab774d983dfa3ea138f22205"
@@ -815,6 +842,11 @@ fd-slicer@~1.0.1:
dependencies:
pend "~1.2.0"
file-uri-to-path@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
filelist@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.1.tgz#f10d1a3ae86c1694808e8f20906f43d4c9132dbb"
@@ -853,6 +885,13 @@ fs-extra@^9.0.1:
jsonfile "^6.0.1"
universalify "^1.0.0"
fs-minipass@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
dependencies:
minipass "^3.0.0"
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@@ -1295,6 +1334,21 @@ minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
minipass@^3.0.0:
version "3.1.3"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd"
integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==
dependencies:
yallist "^4.0.0"
minizlib@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
dependencies:
minipass "^3.0.0"
yallist "^4.0.0"
mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
@@ -1307,6 +1361,11 @@ mkdirp@0.5.1, mkdirp@^0.5.1:
dependencies:
minimist "0.0.8"
mkdirp@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -1335,6 +1394,13 @@ napi-build-utils@^1.0.1:
resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806"
integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==
node-abi@^2.21.0:
version "2.26.0"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.26.0.tgz#355d5d4bc603e856f74197adbf3f5117a396ba40"
integrity sha512-ag/Vos/mXXpWLLAYWsAoQdgS+gW7IwvgMLOgqopm/DbzAjazLltzgzpVMsFlgmo9TzG5hGXeaBZx2AI731RIsQ==
dependencies:
semver "^5.4.1"
node-abi@^2.7.0:
version "2.19.3"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.19.3.tgz#252f5dcab12dad1b5503b2d27eddd4733930282d"
@@ -1509,6 +1575,26 @@ prebuild-install@^6.0.0:
tunnel-agent "^0.6.0"
which-pm-runs "^1.0.0"
prebuild-install@^6.0.1:
version "6.1.2"
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-6.1.2.tgz#6ce5fc5978feba5d3cbffedca0682b136a0b5bff"
integrity sha512-PzYWIKZeP+967WuKYXlTOhYBgGOvTRSfaKI89XnfJ0ansRAH7hDU45X+K+FZeI1Wb/7p/NnuctPH3g0IqKUuSQ==
dependencies:
detect-libc "^1.0.3"
expand-template "^2.0.3"
github-from-package "0.0.0"
minimist "^1.2.3"
mkdirp-classic "^0.5.3"
napi-build-utils "^1.0.1"
node-abi "^2.21.0"
noop-logger "^0.1.1"
npmlog "^4.0.1"
pump "^3.0.0"
rc "^1.2.7"
simple-get "^3.0.3"
tar-fs "^2.0.0"
tunnel-agent "^0.6.0"
prepend-http@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
@@ -1944,6 +2030,18 @@ tar-stream@^2.1.4:
inherits "^2.0.3"
readable-stream "^3.1.1"
tar@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83"
integrity sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==
dependencies:
chownr "^2.0.0"
fs-minipass "^2.0.0"
minipass "^3.0.0"
minizlib "^2.1.1"
mkdirp "^1.0.3"
yallist "^4.0.0"
temp-file@^3.3.7:
version "3.3.7"
resolved "https://registry.yarnpkg.com/temp-file/-/temp-file-3.3.7.tgz#686885d635f872748e384e871855958470aeb18a"
+1
View File
@@ -5,6 +5,7 @@ let fillContent = '';
if (process.platform == 'win32') {
fillContent += `content.msnodesqlv8 = () => require('msnodesqlv8');`;
}
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
+23
View File
@@ -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
View File
@@ -1,6 +1,6 @@
{
"private": true,
"version": "4.1.10",
"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",
-14
View File
@@ -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
+15
View File
@@ -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
+5 -2
View File
@@ -19,6 +19,7 @@
"dependencies": {
"async-lock": "^1.2.4",
"axios": "^0.19.0",
"better-sqlite3-with-prebuilds": "^7.1.8",
"body-parser": "^1.19.0",
"bufferutil": "^4.0.1",
"byline": "^5.0.0",
@@ -50,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"
},
@@ -68,4 +71,4 @@
"optionalDependencies": {
"msnodesqlv8": "^2.0.10"
}
}
}
+16 -28
View File
@@ -7,34 +7,26 @@ const _ = require('lodash');
const currentVersion = require('../currentVersion');
const platformInfo = require('../utility/platformInfo');
const connections = require('../controllers/connections');
module.exports = {
settingsValue: {},
async _init() {
try {
this.settingsValue = JSON.parse(await fs.readFile(path.join(datadir(), 'settings.json'), { encoding: 'utf-8' }));
} catch (err) {
this.settingsValue = {};
}
},
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,
};
@@ -47,23 +39,19 @@ module.exports = {
getSettings_meta: 'get',
async getSettings() {
try {
return JSON.parse(await fs.readFile(path.join(datadir(), 'settings.json'), { encoding: 'utf-8' }));
} catch (err) {
return {};
}
return this.settingsValue;
},
updateSettings_meta: 'post',
async updateSettings(values) {
if (!hasPermission(`settings/change`)) return false;
const oldSettings = await this.getSettings();
try {
const updated = {
...oldSettings,
...this.settingsValue,
...values,
};
await fs.writeFile(path.join(datadir(), 'settings.json'), JSON.stringify(updated, undefined, 2));
this.settingsValue = updated;
socket.emitChanged(`settings-changed`);
return updated;
} catch (err) {
@@ -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();
@@ -4,6 +4,7 @@ const socket = require('../utility/socket');
const { fork } = require('child_process');
const { DatabaseAnalyser } = require('dbgate-tools');
const { handleProcessCommunication } = require('../utility/processComm');
const config = require('./config');
module.exports = {
/** @type {import('dbgate-types').OpenedDatabaseConnection[]} */
@@ -17,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;
@@ -79,6 +86,7 @@ module.exports = {
msgtype: 'connect',
connection: { ...connection, database },
structure: lastClosed ? lastClosed.structure : null,
globalSettings: config.settingsValue,
});
return newOpened;
},
@@ -121,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',
@@ -147,13 +165,20 @@ module.exports = {
},
refresh_meta: 'post',
async refresh({ conid, database }) {
this.close(conid, database);
async refresh({ conid, database, keepOpen }) {
if (!keepOpen) this.close(conid, database);
await this.ensureOpened(conid, database);
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) {
@@ -171,6 +196,12 @@ module.exports = {
}
},
disconnect_meta: 'post',
async disconnect({ conid, database }) {
await this.close(conid, database, true);
return { status: 'ok' };
},
structure_meta: 'get',
async structure({ conid, database }) {
const opened = await this.ensureOpened(conid, database);
+38 -15
View File
@@ -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,25 +65,40 @@ 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;
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' });
try {
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);
}
} catch (err) {
console.log(`Skipped plugin ${packageName}, error:`, err.message);
}
manifest.isPackaged = isPackaged;
res.push(manifest);
}
return res;
},
@@ -131,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() {
@@ -5,6 +5,7 @@ const _ = require('lodash');
const AsyncLock = require('async-lock');
const { handleProcessCommunication } = require('../utility/processComm');
const lock = new AsyncLock();
const config = require('./config');
module.exports = {
opened: [],
@@ -65,7 +66,7 @@ module.exports = {
if (newOpened.disconnected) return;
this.close(conid, false);
});
subprocess.send({ msgtype: 'connect', ...connection });
subprocess.send({ msgtype: 'connect', ...connection, globalSettings: config.settingsValue });
return newOpened;
});
return res;
@@ -85,6 +86,12 @@ module.exports = {
}
},
disconnect_meta: 'post',
async disconnect({ conid }) {
await this.close(conid, true);
return { status: 'ok' };
},
listDatabases_meta: 'get',
async listDatabases({ conid }) {
const opened = await this.ensureOpened(conid);
@@ -122,8 +129,8 @@ module.exports = {
},
refresh_meta: 'post',
async refresh({ conid }) {
this.close(conid);
async refresh({ conid, keepOpen }) {
if (!keepOpen) this.close(conid);
await this.ensureOpened(conid);
return { status: 'ok' };
+3
View File
@@ -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');
+2 -1
View File
@@ -31,6 +31,7 @@ const scheduler = require('./controllers/scheduler');
const { rundir } = require('./utility/directories');
const platformInfo = require('./utility/platformInfo');
const processArgs = require('./utility/processArgs');
const timingSafeCheckToken = require('./utility/timingSafeCheckToken');
let authorization = null;
let checkLocalhostOrigin = null;
@@ -56,7 +57,7 @@ function start() {
}
app.use(function (req, res, next) {
if (authorization && req.headers.authorization != authorization) {
if (authorization && !timingSafeCheckToken(req.headers.authorization, authorization)) {
return res.status(403).json({ error: 'Not authorized!' });
}
if (checkLocalhostOrigin) {
@@ -1,5 +1,6 @@
const stableStringify = require('json-stable-stringify');
const childProcessChecker = require('../utility/childProcessChecker');
const { extractBoolSettingsValue, extractIntSettingsValue } = require('dbgate-tools');
const requireEngineDriver = require('../utility/requireEngineDriver');
const connectUtility = require('../utility/connectUtility');
const { handleProcessCommunication } = require('../utility/processComm');
@@ -11,6 +12,7 @@ let afterConnectCallbacks = [];
let analysedStructure = null;
let lastPing = null;
let lastStatus = null;
let analysedTime = 0;
async function checkedAsyncCall(promise) {
try {
@@ -27,21 +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) {
@@ -62,7 +85,7 @@ async function readVersion() {
process.send({ msgtype: 'version', version });
}
async function handleConnect({ connection, structure }) {
async function handleConnect({ connection, structure, globalSettings }) {
storedConnection = connection;
lastPing = new Date().getTime();
@@ -72,11 +95,18 @@ async function handleConnect({ connection, structure }) {
readVersion();
if (structure) {
analysedStructure = structure;
handleIncrementalRefresh();
handleIncrementalRefresh(true);
} else {
handleFullRefresh();
}
setInterval(handleIncrementalRefresh, 30 * 1000);
if (extractBoolSettingsValue(globalSettings, 'connection.autoRefresh', false)) {
setInterval(
handleIncrementalRefresh,
extractIntSettingsValue(globalSettings, 'connection.autoRefreshInterval', 30, 3, 3600) * 1000
);
}
for (const [resolve] of afterConnectCallbacks) {
resolve();
}
@@ -162,6 +192,7 @@ const messageHandlers = {
collectionData: handleCollectionData,
sqlPreview: handleSqlPreview,
ping: handlePing,
syncModel: handleSyncModel,
// runCommand: handleRunCommand,
};
@@ -1,4 +1,5 @@
const stableStringify = require('json-stable-stringify');
const { extractBoolSettingsValue, extractIntSettingsValue } = require('dbgate-tools');
const childProcessChecker = require('../utility/childProcessChecker');
const requireEngineDriver = require('../utility/requireEngineDriver');
const { decryptConnection } = require('../utility/crypting');
@@ -51,6 +52,7 @@ function setStatusName(name) {
async function handleConnect(connection) {
storedConnection = connection;
const { globalSettings } = storedConnection;
setStatusName('pending');
lastPing = new Date().getTime();
@@ -59,7 +61,9 @@ async function handleConnect(connection) {
systemConnection = await connectUtility(driver, storedConnection);
readVersion();
handleRefresh();
setInterval(handleRefresh, 30 * 1000);
if (extractBoolSettingsValue(globalSettings, 'connection.autoRefresh', false)) {
setInterval(handleRefresh, extractIntSettingsValue(globalSettings, 'connection.autoRefreshInterval', 30, 5, 3600) * 1000);
}
} catch (err) {
setStatus({
name: 'error',
@@ -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}`);
}
+36 -27
View File
@@ -4,6 +4,8 @@ const portfinder = require('portfinder');
const stableStringify = require('json-stable-stringify');
const _ = require('lodash');
const platformInfo = require('./platformInfo');
const AsyncLock = require('async-lock');
const lock = new AsyncLock();
const sshConnectionCache = {};
const sshTunnelCache = {};
@@ -34,7 +36,7 @@ async function getSshConnection(connection) {
password: connection.sshMode == 'userPassword' ? connection.sshPassword : undefined,
agentSocket: connection.sshMode == 'agent' ? platformInfo.sshAuthSock : undefined,
privateKey:
connection.sshMode == 'keyFile' && connection.sshKeyFile ? await fs.readFile(connection.sshKeyFile) : undefined,
connection.sshMode == 'keyFile' && connection.sshKeyfile ? await fs.readFile(connection.sshKeyfile) : undefined,
skipAutoPrivateKey: true,
noReadline: true,
};
@@ -45,36 +47,43 @@ async function getSshConnection(connection) {
}
async function getSshTunnel(connection) {
const sshConn = await getSshConnection(connection);
const tunnelCacheKey = stableStringify(_.pick(connection, TUNNEL_FIELDS));
if (sshTunnelCache[tunnelCacheKey]) return sshTunnelCache[tunnelCacheKey];
const localPort = await portfinder.getPortPromise({ port: 10000, stopPort: 60000 });
// workaround for `getPortPromise` not releasing the port quickly enough
await new Promise(resolve => setTimeout(resolve, 500));
const tunnelConfig = {
fromPort: localPort,
toPort: connection.port,
toHost: connection.server,
};
try {
const tunnel = await sshConn.forward(tunnelConfig);
console.log(
`Created SSH tunnel to ${connection.sshHost}-${connection.server}:${connection.port}, using local port ${localPort}`
);
return await lock.acquire(tunnelCacheKey, async () => {
const sshConn = await getSshConnection(connection);
if (sshTunnelCache[tunnelCacheKey]) return sshTunnelCache[tunnelCacheKey];
sshTunnelCache[tunnelCacheKey] = {
state: 'ok',
localPort,
const localPort = await portfinder.getPortPromise({ port: 10000, stopPort: 60000 });
// workaround for `getPortPromise` not releasing the port quickly enough
await new Promise(resolve => setTimeout(resolve, 500));
const tunnelConfig = {
fromPort: localPort,
toPort: connection.port,
toHost: connection.server,
};
return sshTunnelCache[tunnelCacheKey];
} catch (err) {
// error is not cached
return {
state: 'error',
message: err.message,
};
}
try {
console.log(
`Creating SSH tunnel to ${connection.sshHost}-${connection.server}:${connection.port}, using local port ${localPort}`
);
const tunnel = await sshConn.forward(tunnelConfig);
console.log(
`Created SSH tunnel to ${connection.sshHost}-${connection.server}:${connection.port}, using local port ${localPort}`
);
sshTunnelCache[tunnelCacheKey] = {
state: 'ok',
localPort,
};
return sshTunnelCache[tunnelCacheKey];
} catch (err) {
// error is not cached
return {
state: 'error',
message: err.message,
};
}
});
}
module.exports = {
@@ -0,0 +1,9 @@
const crypto = require('crypto');
function timingSafeCheckToken(a, b) {
if (!a || !b) return false;
if (a.length != b.length) return false;
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
}
module.exports = timingSafeCheckToken;
+125 -7
View File
@@ -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
);
}
@@ -71,10 +88,111 @@ export class DatabaseAnalyser {
// }
}
getRequestedObjectPureNames(objectTypeField, allPureNames) {
if (this.singleObjectFilter) {
const { typeField, pureName } = this.singleObjectFilter;
if (typeField == objectTypeField) return [pureName];
}
if (this.modifications) {
return this.modifications.filter(x => x.objectTypeField == objectTypeField).map(x => x.newName.pureName);
}
return allPureNames;
}
// findObjectById(id) {
// 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 -6
View File
@@ -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);
}
}
+1 -3
View File
@@ -22,9 +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();
return res.tables[0];
return analyser.singleObjectAnalysis(name, typeField);
},
analyseSingleTable(pool, name) {
return this.analyseSingleObject(pool, name, 'tables');
+1 -1
View File
@@ -7,6 +7,6 @@ export * from './DatabaseAnalyser';
export * from './driverBase';
export * from './SqlDumper';
export * from './testPermission';
export * from './splitPostgresQuery';
export * from './SqlGenerator';
export * from './structureTools';
export * from './settingsExtractors';
+21
View File
@@ -0,0 +1,21 @@
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)) {
return defaultValue;
}
if (_isNumber(parsed)) {
if (min != null && parsed < min) return min;
if (max != null && parsed > max) return max;
return parsed;
}
return defaultValue;
}
export function extractBoolSettingsValue(settings, name, defaultValue) {
const res = settings[name];
if (res == null) return defaultValue;
return !!res;
}
-292
View File
@@ -1,292 +0,0 @@
const SINGLE_QUOTE = "'";
const DOUBLE_QUOTE = '"';
// const BACKTICK = '`';
const DOUBLE_DASH_COMMENT_START = '--';
const HASH_COMMENT_START = '#';
const C_STYLE_COMMENT_START = '/*';
const SEMICOLON = ';';
const LINE_FEED = '\n';
const DELIMITER_KEYWORD = 'DELIMITER';
export interface SplitOptions {
multipleStatements?: boolean;
retainComments?: boolean;
}
interface SqlStatement {
value: string;
supportMulti: boolean;
}
interface SplitExecutionContext extends Required<SplitOptions> {
unread: string;
currentDelimiter: string;
currentStatement: SqlStatement;
output: SqlStatement[];
}
interface FindExpResult {
expIndex: number;
exp: string | null;
nextIndex: number;
}
const regexEscapeSetRegex = /[-/\\^$*+?.()|[\]{}]/g;
const singleQuoteStringEndRegex = /(?<!\\)'/;
const doubleQuoteStringEndRegex = /(?<!\\)"/;
// const backtickQuoteEndRegex = /(?<!`)`(?!`)/;
const doubleDashCommentStartRegex = /--[ \f\n\r\t\v]/;
const cStyleCommentStartRegex = /\/\*/;
const cStyleCommentEndRegex = /(?<!\/)\*\//;
const newLineRegex = /(?:[\r\n]+|$)/;
const delimiterStartRegex = /(?:^|[\n\r]+)[ \f\t\v]*DELIMITER[ \t]+/i;
// Best effort only, unable to find a syntax specification on delimiter
const delimiterTokenRegex = /^(?:'(.+)'|"(.+)"|`(.+)`|([^\s]+))/;
const semicolonKeyTokenRegex = buildKeyTokenRegex(SEMICOLON);
const quoteEndRegexDict: Record<string, RegExp> = {
[SINGLE_QUOTE]: singleQuoteStringEndRegex,
[DOUBLE_QUOTE]: doubleQuoteStringEndRegex,
// [BACKTICK]: backtickQuoteEndRegex,
};
function escapeRegex(value: string): string {
return value.replace(regexEscapeSetRegex, '\\$&');
}
function buildKeyTokenRegex(delimiter: string): RegExp {
return new RegExp(
'(?:' +
[
escapeRegex(delimiter),
SINGLE_QUOTE,
DOUBLE_QUOTE,
// BACKTICK,
doubleDashCommentStartRegex.source,
HASH_COMMENT_START,
cStyleCommentStartRegex.source,
delimiterStartRegex.source,
].join('|') +
')',
'i'
);
}
function findExp(content: string, regex: RegExp): FindExpResult {
const match = content.match(regex);
let result: FindExpResult;
if (match?.index !== undefined) {
result = {
expIndex: match.index,
exp: match[0],
nextIndex: match.index + match[0].length,
};
} else {
result = {
expIndex: -1,
exp: null,
nextIndex: content.length,
};
}
return result;
}
function findKeyToken(content: string, currentDelimiter: string): FindExpResult {
let regex;
if (currentDelimiter === SEMICOLON) {
regex = semicolonKeyTokenRegex;
} else {
regex = buildKeyTokenRegex(currentDelimiter);
}
return findExp(content, regex);
}
function findEndQuote(content: string, quote: string): FindExpResult {
if (!(quote in quoteEndRegexDict)) {
throw new TypeError(`Incorrect quote ${quote} supplied`);
}
return findExp(content, quoteEndRegexDict[quote]);
}
function read(
context: SplitExecutionContext,
readToIndex: number,
nextUnreadIndex?: number,
checkSemicolon?: boolean
): void {
if (checkSemicolon === undefined) {
checkSemicolon = true;
}
const readContent = context.unread.slice(0, readToIndex);
if (checkSemicolon && readContent.includes(SEMICOLON)) {
context.currentStatement.supportMulti = false;
}
context.currentStatement.value += readContent;
if (nextUnreadIndex !== undefined && nextUnreadIndex > 0) {
context.unread = context.unread.slice(nextUnreadIndex);
} else {
context.unread = context.unread.slice(readToIndex);
}
}
function readTillNewLine(context: SplitExecutionContext, checkSemicolon?: boolean): void {
const findResult = findExp(context.unread, newLineRegex);
read(context, findResult.expIndex, findResult.expIndex, checkSemicolon);
}
function discard(context: SplitExecutionContext, nextUnreadIndex: number): void {
if (nextUnreadIndex > 0) {
context.unread = context.unread.slice(nextUnreadIndex);
}
}
function discardTillNewLine(context: SplitExecutionContext): void {
const findResult = findExp(context.unread, newLineRegex);
discard(context, findResult.expIndex);
}
function publishStatementInMultiMode(splitOutput: SqlStatement[], currentStatement: SqlStatement): void {
if (splitOutput.length === 0) {
splitOutput.push({
value: '',
supportMulti: true,
});
}
const lastSplitResult = splitOutput[splitOutput.length - 1];
if (currentStatement.supportMulti) {
if (lastSplitResult.supportMulti) {
if (lastSplitResult.value !== '' && !lastSplitResult.value.endsWith(LINE_FEED)) {
lastSplitResult.value += LINE_FEED;
}
lastSplitResult.value += currentStatement.value + SEMICOLON;
} else {
splitOutput.push({
value: currentStatement.value + SEMICOLON,
supportMulti: true,
});
}
} else {
splitOutput.push({
value: currentStatement.value,
supportMulti: false,
});
}
}
function publishStatement(context: SplitExecutionContext): void {
const trimmed = context.currentStatement.value.trim();
if (trimmed !== '') {
if (!context.multipleStatements) {
context.output.push({
value: trimmed,
supportMulti: context.currentStatement.supportMulti,
});
} else {
context.currentStatement.value = trimmed;
publishStatementInMultiMode(context.output, context.currentStatement);
}
}
context.currentStatement.value = '';
context.currentStatement.supportMulti = true;
}
function handleKeyTokenFindResult(context: SplitExecutionContext, findResult: FindExpResult): void {
switch (findResult.exp?.trim()) {
case context.currentDelimiter:
read(context, findResult.expIndex, findResult.nextIndex);
publishStatement(context);
break;
// case BACKTICK:
case SINGLE_QUOTE:
case DOUBLE_QUOTE: {
read(context, findResult.nextIndex);
const findQuoteResult = findEndQuote(context.unread, findResult.exp);
read(context, findQuoteResult.nextIndex, undefined, false);
break;
}
case DOUBLE_DASH_COMMENT_START: {
if (context.retainComments) {
read(context, findResult.nextIndex);
readTillNewLine(context, false);
} else {
read(context, findResult.expIndex, findResult.expIndex + DOUBLE_DASH_COMMENT_START.length);
discardTillNewLine(context);
}
break;
}
case HASH_COMMENT_START: {
if (context.retainComments) {
read(context, findResult.nextIndex);
readTillNewLine(context, false);
} else {
read(context, findResult.expIndex, findResult.nextIndex);
discardTillNewLine(context);
}
break;
}
case C_STYLE_COMMENT_START: {
if (['!', '+'].includes(context.unread[findResult.nextIndex]) || context.retainComments) {
// Should not be skipped, see https://dev.mysql.com/doc/refman/5.7/en/comments.html
read(context, findResult.nextIndex);
const findCommentResult = findExp(context.unread, cStyleCommentEndRegex);
read(context, findCommentResult.nextIndex);
} else {
read(context, findResult.expIndex, findResult.nextIndex);
const findCommentResult = findExp(context.unread, cStyleCommentEndRegex);
discard(context, findCommentResult.nextIndex);
}
break;
}
case DELIMITER_KEYWORD: {
read(context, findResult.expIndex, findResult.nextIndex);
// MySQL client will return `DELIMITER cannot contain a backslash character` if backslash is used
// Shall we reject backslash as well?
const matched = context.unread.match(delimiterTokenRegex);
if (matched?.index !== undefined) {
context.currentDelimiter = matched[0].trim();
discard(context, matched[0].length);
}
discardTillNewLine(context);
break;
}
case undefined:
case null:
read(context, findResult.nextIndex);
publishStatement(context);
break;
default:
// This should never happen
throw new Error(`Unknown token '${findResult.exp ?? '(null)'}'`);
}
}
export function splitPostgresQuery(sql: string, options?: SplitOptions): string[] {
options = options ?? {};
const context: SplitExecutionContext = {
multipleStatements: options.multipleStatements ?? false,
retainComments: options.retainComments ?? false,
unread: sql,
currentDelimiter: SEMICOLON,
currentStatement: {
value: '',
supportMulti: true,
},
output: [],
};
let findResult: FindExpResult = {
expIndex: -1,
exp: null,
nextIndex: 0,
};
let lastUnreadLength;
do {
lastUnreadLength = context.unread.length;
findResult = findKeyToken(context.unread, context.currentDelimiter);
handleKeyTokenFindResult(context, findResult);
// Prevent infinite loop by returning incorrect result
if (lastUnreadLength === context.unread.length) {
read(context, context.unread.length);
}
} while (context.unread !== '');
publishStatement(context);
return context.output.map(v => v.value);
}
+2 -2
View File
@@ -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 => ({
+5 -1
View File
@@ -38,6 +38,10 @@ export interface EngineDriver {
title: string;
defaultPort?: number;
supportsDatabaseUrl?: 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>;
@@ -77,6 +81,6 @@ export interface DatabaseModification {
oldName?: NamedObjectInfo;
newName?: NamedObjectInfo;
objectId?: string;
action: 'add' | 'remove' | 'change';
action: 'add' | 'remove' | 'change' | 'all';
objectTypeField: keyof DatabaseInfo;
}
+1
View File
@@ -4,6 +4,7 @@ export interface OpenedDatabaseConnection {
conid: string;
database: string;
structure: DatabaseInfo;
analysedTime?: number;
serverVersion?: any;
subprocess: ChildProcess;
disconnected?: boolean;
+16
View File
@@ -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>
+50 -7
View File
@@ -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}
@@ -10,6 +10,7 @@
$: fileTypeNames = _.compact([
...$extensions.fileFormats.filter(x => x.readerFunc).map(x => x.name),
electron ? 'SQL' : null,
electron ? 'SQLite database' : null,
]);
</script>
@@ -1,24 +1,83 @@
<script context="module">
const getContextMenu = (data, $openedConnections, $extensions) => () => {
export const extractKey = data => data._id;
export const createMatcher = ({ displayName, server }) => filter => filterName(filter, displayName, server);
</script>
<script lang="ts">
import _ from 'lodash';
import AppObjectCore from './AppObjectCore.svelte';
import { currentDatabase, extensions, getCurrentConfig, openedConnections } from '../stores';
import axiosInstance from '../utility/axiosInstance';
import { filterName } from 'dbgate-datalib';
import { showModal } from '../modals/modalTools';
import ConnectionModal from '../modals/ConnectionModal.svelte';
import ConfirmModal from '../modals/ConfirmModal.svelte';
import InputTextModal from '../modals/InputTextModal.svelte';
import openNewTab from '../utility/openNewTab';
import { getDatabaseMenuItems } from './DatabaseAppObject.svelte';
import getElectron from '../utility/getElectron';
import getConnectionLabel from '../utility/getConnectionLabel';
export let data;
let statusIcon = null;
let statusTitle = null;
let extInfo = null;
let engineStatusIcon = null;
let engineStatusTitle = null;
const electron = getElectron();
const handleConnect = () => {
if (data.singleDatabase) {
$currentDatabase = { connection: data, name: data.defaultDatabase };
axiosInstance.post('database-connections/refresh', {
conid: data._id,
database: data.defaultDatabase,
keepOpen: true,
});
} else {
$openedConnections = _.uniq([...$openedConnections, data._id]);
axiosInstance.post('server-connections/refresh', {
conid: data._id,
keepOpen: true,
});
}
};
const getContextMenu = () => {
const config = getCurrentConfig();
const handleRefresh = () => {
axiosInstance.post('server-connections/refresh', { conid: data._id });
};
const handleDisconnect = () => {
openedConnections.update(list => list.filter(x => x != data._id));
};
const handleConnect = () => {
openedConnections.update(list => _.uniq([...list, data._id]));
if (electron) {
axiosInstance.post('server-connections/disconnect', { conid: data._id });
}
if (_.get($currentDatabase, 'connection._id') == data._id) {
if (electron) {
axiosInstance.post('database-connections/disconnect', { conid: data._id, database: $currentDatabase.name });
}
currentDatabase.set(null);
}
};
const handleEdit = () => {
showModal(ConnectionModal, { connection: data });
};
const handleDelete = () => {
showModal(ConfirmModal, {
message: `Really delete connection ${data.displayName || data.server}?`,
message: `Really delete connection ${getConnectionLabel(data)}?`,
onConfirm: () => axiosInstance.post('connections/delete', data),
});
};
const handleDuplicate = () => {
axiosInstance.post('connections/save', {
...data,
_id: undefined,
displayName: `${getConnectionLabel(data)} - copy`,
});
};
const handleCreateDatabase = () => {
showModal(InputTextModal, {
header: 'Create database',
@@ -32,7 +91,7 @@
});
};
const handleNewQuery = () => {
const tooltip = `${data.displayName || data.server}`;
const tooltip = `${getConnectionLabel(data)}`;
openNewTab({
title: 'Query #',
icon: 'img sql-file',
@@ -54,6 +113,10 @@
text: 'Delete',
onClick: handleDelete,
},
{
text: 'Duplicate',
onClick: handleDuplicate,
},
],
!data.singleDatabase && [
!$openedConnections.includes(data._id) && {
@@ -75,35 +138,13 @@
onClick: handleCreateDatabase,
},
],
data.singleDatabase && [{ divider: true }, getDatabaseMenuItems(data, data.defaultDatabase, $extensions)],
data.singleDatabase && [
{ divider: true },
getDatabaseMenuItems(data, data.defaultDatabase, $extensions, $currentDatabase),
],
];
};
export const extractKey = data => data._id;
export const createMatcher = ({ displayName, server }) => filter => filterName(filter, displayName, server);
</script>
<script lang="ts">
import _ from 'lodash';
import AppObjectCore from './AppObjectCore.svelte';
import { currentDatabase, extensions, getCurrentConfig, openedConnections } from '../stores';
import axiosInstance from '../utility/axiosInstance';
import { filterName } from 'dbgate-datalib';
import { showModal } from '../modals/modalTools';
import ConnectionModal from '../modals/ConnectionModal.svelte';
import ConfirmModal from '../modals/ConfirmModal.svelte';
import InputTextModal from '../modals/InputTextModal.svelte';
import openNewTab from '../utility/openNewTab';
import { getDatabaseMenuItems } from './DatabaseAppObject.svelte';
export let data;
let statusIcon = null;
let statusTitle = null;
let extInfo = null;
let engineStatusIcon = null;
let engineStatusTitle = null;
$: {
if ($extensions.drivers.find(x => x.engine == data.engine)) {
const match = (data.engine || '').match(/^([^@]*)@/);
@@ -132,30 +173,12 @@
statusTitle = null;
}
}
// const handleEdit = () => {
// showModal(modalState => <ConnectionModal modalState={modalState} connection={data} />);
// };
// const handleDelete = () => {
// showModal(modalState => (
// <ConfirmModal
// modalState={modalState}
// message={`Really delete connection ${data.displayName || data.server}?`}
// onConfirm={() => axios.post('connections/delete', data)}
// />
// ));
// };
// const handleCreateDatabase = () => {
// showModal(modalState => <CreateDatabaseModal modalState={modalState} conid={data._id} />);
// };
</script>
<AppObjectCore
{...$$restProps}
{data}
title={data.singleDatabase
? data.displayName || `${data.defaultDatabase} on ${data.server}`
: data.displayName || data.server}
title={getConnectionLabel(data)}
icon={data.singleDatabase ? 'img database' : 'img server'}
isBold={data.singleDatabase
? _.get($currentDatabase, 'connection._id') == data._id && _.get($currentDatabase, 'name') == data.defaultDatabase
@@ -163,10 +186,8 @@
statusIcon={statusIcon || engineStatusIcon}
statusTitle={statusTitle || engineStatusTitle}
{extInfo}
menu={getContextMenu(data, $openedConnections, $extensions)}
on:click={() => {
if (data.singleDatabase) $currentDatabase = { connection: data, name: data.defaultDatabase };
else $openedConnections = _.uniq([...$openedConnections, data._id]);
}}
menu={getContextMenu}
on:click={handleConnect}
on:click
on:expand
/>
@@ -1,9 +1,10 @@
<script lang="ts" context="module">
export const extractKey = props => props.name;
const electron = getElectron();
export function getDatabaseMenuItems(connection, name, $extensions) {
export function getDatabaseMenuItems(connection, name, $extensions, $currentDatabase) {
const handleNewQuery = () => {
const tooltip = `${connection.displayName || connection.server}\n${name}`;
const tooltip = `${getConnectionLabel(connection)}\n${name}`;
openNewTab({
title: 'Query #',
icon: 'img sql-file',
@@ -45,28 +46,42 @@
});
};
const handleDisconnect = () => {
if (electron) {
axiosInstance.post('database-connections/disconnect', { conid: connection._id, database: name });
}
currentDatabase.set(null);
};
return [
{ onClick: handleNewQuery, text: 'New query' },
{ onClick: handleImport, text: 'Import' },
{ onClick: handleExport, text: 'Export' },
{ onClick: handleSqlGenerator, text: 'SQL Generator' },
_.get($currentDatabase, 'connection._id') == _.get(connection, '_id') &&
_.get($currentDatabase, 'name') == name && { onClick: handleDisconnect, text: 'Disconnect' },
];
}
</script>
<script lang="ts">
import getConnectionLabel from '../utility/getConnectionLabel';
import _ from 'lodash';
import ImportExportModal from '../modals/ImportExportModal.svelte';
import { showModal } from '../modals/modalTools';
import SqlGeneratorModal from '../modals/SqlGeneratorModal.svelte';
import { getDefaultFileFormat } from '../plugins/fileformats';
import { currentDatabase, extensions } from '../stores';
import axiosInstance from '../utility/axiosInstance';
import getElectron from '../utility/getElectron';
import openNewTab from '../utility/openNewTab';
import AppObjectCore from './AppObjectCore.svelte';
export let data;
function createMenu() {
return getDatabaseMenuItems(data.connection, data.name, $extensions);
return getDatabaseMenuItems(data.connection, data.name, $extensions, $currentDatabase);
}
</script>
@@ -228,7 +228,7 @@
initialData
) {
const connection = await getConnectionInfo({ conid });
const tooltip = `${connection.displayName || connection.server}\n${database}\n${fullDisplayName({
const tooltip = `${getConnectionLabel(connection)}\n${database}\n${fullDisplayName({
schemaName,
pureName,
})}`;
@@ -267,6 +267,7 @@
import { findEngineDriver } from 'dbgate-tools';
import uuidv1 from 'uuid/v1';
import SqlGeneratorModal from '../modals/SqlGeneratorModal.svelte';
import getConnectionLabel from '../utility/getConnectionLabel';
export let data;
@@ -67,6 +67,7 @@
import { currentDatabase } from '../stores';
import axiosInstance from '../utility/axiosInstance';
import getConnectionLabel from '../utility/getConnectionLabel';
import hasPermission from '../utility/hasPermission';
import openNewTab from '../utility/openNewTab';
@@ -130,7 +131,7 @@
const database = _.get($currentDatabase, 'name');
connProps.conid = connection._id;
connProps.database = database;
tooltip = `${connection.displayName || connection.server}\n${database}`;
tooltip = `${getConnectionLabel(connection)}\n${database}`;
}
openNewTab(
@@ -1,5 +1,6 @@
import _ from 'lodash';
import { recentDatabases, currentDatabase, getRecentDatabases } from '../stores';
import getConnectionLabel from '../utility/getConnectionLabel';
import registerCommand from './registerCommand';
currentDatabase.subscribe(value => {
@@ -15,7 +16,7 @@ currentDatabase.subscribe(value => {
function switchDatabaseCommand(db) {
return {
text: `${db.name} on ${db?.connection?.displayName || db?.connection?.server}`,
text: `${db.name} on ${getConnectionLabel(db?.connection, { allowExplicitDatabase: false })}`,
onClick: () => currentDatabase.set(db),
};
}
@@ -27,6 +27,7 @@ export interface GlobalCommand {
menuName?: string;
toolbarOrder?: number;
disableHandleKeyText?: string;
isRelatedToTab?: boolean,
}
export default function registerCommand(command: GlobalCommand) {
+8 -1
View File
@@ -241,6 +241,7 @@ export function registerFileCommands({
toggleComment = false,
findReplace = false,
undoRedo = false,
executeAdditionalCondition = null,
}) {
if (save) {
registerCommand({
@@ -251,6 +252,7 @@ export function registerFileCommands({
// keyText: 'Ctrl+S',
icon: 'icon save',
toolbar: true,
isRelatedToTab: true,
testEnabled: () => getCurrentEditor() != null,
onClick: () => saveTabFile(getCurrentEditor(), false, folder, format, fileExtension),
});
@@ -271,8 +273,12 @@ export function registerFileCommands({
name: 'Execute',
icon: 'icon run',
toolbar: true,
isRelatedToTab: true,
keyText: 'F5 | Ctrl+Enter',
testEnabled: () => getCurrentEditor() != null && !getCurrentEditor()?.isBusy(),
testEnabled: () =>
getCurrentEditor() != null &&
!getCurrentEditor()?.isBusy() &&
(executeAdditionalCondition == null || executeAdditionalCondition()),
onClick: () => getCurrentEditor().execute(),
});
registerCommand({
@@ -281,6 +287,7 @@ export function registerFileCommands({
name: 'Kill',
icon: 'icon close',
toolbar: true,
isRelatedToTab: true,
testEnabled: () => getCurrentEditor()?.canKill && getCurrentEditor().canKill(),
onClick: () => getCurrentEditor().kill(),
});
+43 -17
View File
@@ -7,6 +7,7 @@
name: 'Refresh',
keyText: 'F5',
toolbar: true,
isRelatedToTab: true,
icon: 'icon reload',
testEnabled: () => getCurrentDataGrid()?.getDisplay()?.supportsReload,
onClick: () => getCurrentDataGrid().refresh(),
@@ -63,6 +64,7 @@
group: 'undo',
icon: 'icon undo',
toolbar: true,
isRelatedToTab: true,
testEnabled: () => getCurrentDataGrid()?.getGrider()?.canUndo,
onClick: () => getCurrentDataGrid().undo(),
});
@@ -74,6 +76,7 @@
group: 'redo',
icon: 'icon redo',
toolbar: true,
isRelatedToTab: true,
testEnabled: () => getCurrentDataGrid()?.getGrider()?.canRedo,
onClick: () => getCurrentDataGrid().redo(),
});
@@ -168,6 +171,7 @@
if (_.isPlainObject(value) || _.isArray(value)) return JSON.stringify(value);
return value;
}
</script>
<script lang="ts">
@@ -578,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();
@@ -660,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() {
@@ -732,7 +756,7 @@
handleCursorMove(event);
if (event.shiftKey) {
if (event.shiftKey && event.keyCode != keycodes.shift) {
selectedCells = getCellRange(shiftDragStartCell || currentCell, currentCell);
}
}
@@ -955,6 +979,7 @@
);
const menu = getContextMenu();
</script>
{#if !display || (!isDynamicStructure && (!columns || columns.length == 0))}
@@ -1161,4 +1186,5 @@
right: 40px;
bottom: 20px;
}
</style>
+3
View File
@@ -54,6 +54,9 @@ export function countColumnSizes(grider: Grider, columns, containerWidth, displa
context.font = '14px Helvetica';
for (let rowIndex = 0; rowIndex < Math.min(grider.rowCount, 20); rowIndex += 1) {
const row = grider.getRowData(rowIndex);
if (!row) {
continue;
}
for (let colIndex = 0; colIndex < columns.length; colIndex++) {
const uqName = columns[colIndex].uniqueName;
@@ -13,5 +13,5 @@
<span class="nowrap">
<FontIcon icon={getConstraintIcon(constraintType)} />
{constraintName}
{constraintName || '(without name)'}
</span>
@@ -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>
@@ -2,10 +2,12 @@
import { getFormContext } from './FormProviderCore.svelte';
import SelectField from './SelectField.svelte';
import { createEventDispatcher } from 'svelte';
import _ from 'lodash';
const dispatch = createEventDispatcher();
export let name;
export let options;
export let isClearable = false;
const { values, setFieldValue } = getFormContext();
@@ -14,6 +16,7 @@
<SelectField
{...$$restProps}
value={$values[name]}
options={_.compact(options)}
on:change={e => {
setFieldValue(name, e.detail);
dispatch('change', e.detail);
@@ -18,6 +18,7 @@
name: 'Refresh',
keyText: 'F5',
toolbar: true,
isRelatedToTab: true,
icon: 'icon reload',
testEnabled: () => getCurrentDataForm() != null,
onClick: () => getCurrentDataForm().refresh(),
@@ -58,6 +59,7 @@
group: 'undo',
icon: 'icon undo',
toolbar: true,
isRelatedToTab: true,
testEnabled: () => getCurrentDataForm()?.getFormer()?.canUndo,
onClick: () => getCurrentDataForm().getFormer().undo(),
});
@@ -69,6 +71,7 @@
group: 'redo',
icon: 'icon redo',
toolbar: true,
isRelatedToTab: true,
testEnabled: () => getCurrentDataForm()?.getFormer()?.canRedo,
onClick: () => getCurrentDataForm().getFormer().redo(),
});
@@ -104,6 +107,7 @@
name: 'First',
keyText: 'Ctrl+Home',
toolbar: true,
isRelatedToTab: true,
icon: 'icon arrow-begin',
testEnabled: () => getCurrentDataForm() != null,
onClick: () => getCurrentDataForm().navigate('begin'),
@@ -115,6 +119,7 @@
name: 'Previous',
keyText: 'Ctrl+ArrowUp',
toolbar: true,
isRelatedToTab: true,
icon: 'icon arrow-left',
testEnabled: () => getCurrentDataForm() != null,
onClick: () => getCurrentDataForm().navigate('previous'),
@@ -126,6 +131,7 @@
name: 'Next',
keyText: 'Ctrl+ArrowDown',
toolbar: true,
isRelatedToTab: true,
icon: 'icon arrow-right',
testEnabled: () => getCurrentDataForm() != null,
onClick: () => getCurrentDataForm().navigate('next'),
@@ -137,6 +143,7 @@
name: 'Last',
keyText: 'Ctrl+End',
toolbar: true,
isRelatedToTab: true,
icon: 'icon arrow-end',
testEnabled: () => getCurrentDataForm() != null,
onClick: () => getCurrentDataForm().navigate('end'),
@@ -40,7 +40,7 @@
</script>
<ManagerInnerContainer width={managerSize}>
{#each structure.columns as column, index}
{#each structure.columns || [] as column, index}
{#if index == editingColumn}
<ColumnNameEditor
defaultValue={column.columnName}
@@ -77,6 +77,6 @@
dispatchChangeColumns($$props, cols => [...cols, { columnName }]);
}}
placeholder="New column"
existingNames={structure.columns.map(x => x.columnName)}
existingNames={(structure.columns || []).map(x => x.columnName)}
/>
</ManagerInnerContainer>
+1
View File
@@ -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',
@@ -1,13 +1,14 @@
<script lang="ts">
import _ from 'lodash';
import FormSelectField from '../forms/FormSelectField.svelte';
import getConnectionLabel from '../utility/getConnectionLabel';
import { useConnectionList } from '../utility/metadataLoaders';
$: connections = useConnectionList();
$: connectionOptions = _.sortBy(
($connections || []).map(conn => ({
value: conn._id,
label: conn.displayName || conn.server,
label: getConnectionLabel(conn),
})),
'label'
);
+15 -17
View File
@@ -15,11 +15,7 @@
const { values, setFieldValue } = getFormContext();
$: dbinfo = useDatabaseInfo({ conid: $values[conidName], database: $values[databaseName] });
$: tablesOptions = [
...(($dbinfo && $dbinfo.tables) || []),
...(($dbinfo && $dbinfo.views) || []),
...(($dbinfo && $dbinfo.collections) || []),
]
$: tablesOptions = _.compact([...($dbinfo?.tables || []), ...($dbinfo?.views || []), ...($dbinfo?.collections || [])])
.filter(x => !$values[schemaName] || x.schemaName == $values[schemaName])
.map(x => ({
value: x.pureName,
@@ -31,18 +27,20 @@
<FormSelectField {...$$restProps} {name} options={tablesOptions} isMulti templateProps={{ noMargin: true }} />
<div>
<FormStyledButton
type="button"
value="All tables"
on:click={() =>
setFieldValue(name, _.uniq([...($values[name] || []), ...($dbinfo && $dbinfo.tables.map(x => x.pureName))]))}
/>
<FormStyledButton
type="button"
value="All views"
on:click={() =>
setFieldValue(name, _.uniq([...($values[name] || []), ...($dbinfo && $dbinfo.views.map(x => x.pureName))]))}
/>
{#each ['tables', 'views', 'collections'] as field}
{#if $dbinfo && $dbinfo[field]?.length > 0}
<FormStyledButton
type="button"
value={`All ${field}`}
on:click={() =>
setFieldValue(
name,
_.compact(_.uniq([...($values[name] || []), ...($dbinfo[field]?.map(x => x.pureName) || [])]))
)}
/>
{/if}
{/each}
<FormStyledButton type="button" value="Remove all" on:click={() => setFieldValue(name, [])} />
</div>
</div>
+36 -11
View File
@@ -16,12 +16,22 @@
import createRef from '../utility/createRef';
import Link from '../elements/Link.svelte';
import ErrorMessageModal from './ErrorMessageModal.svelte';
import { writable } from 'svelte/store';
import FormProviderCore from '../forms/FormProviderCore.svelte';
import { extensions } from '../stores';
import _ from 'lodash';
import { getDatabaseFileLabel } from '../utility/getConnectionLabel';
export let connection;
let isTesting;
let sqlConnectResult;
const values = writable(connection || { server: 'localhost', engine: 'mssql@dbgate-plugin-mssql' });
$: engine = $values.engine;
$: driver = $extensions.drivers.find(x => x.engine == engine);
const testIdRef = createRef(0);
async function handleTest(e) {
@@ -41,18 +51,32 @@
}
async function handleSubmit(e) {
axiosInstance.post('connections/save', {
...e.detail,
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>
<FormProvider
template={FormFieldTemplateLarge}
initialValues={connection || { server: 'localhost', engine: 'mssql@dbgate-plugin-mssql' }}
>
<FormProviderCore template={FormFieldTemplateLarge} {values}>
<ModalBase {...$$restProps} noPadding>
<div slot="header">Add connection</div>
@@ -63,11 +87,11 @@
label: 'Main',
component: ConnectionModalDriverFields,
},
{
(!driver?.showConnectionTab || driver?.showConnectionTab('sshTunnel', $values)) && {
label: 'SSH Tunnel',
component: ConnectionModalSshTunnelFields,
},
{
(!driver?.showConnectionTab || driver?.showConnectionTab('ssl', $values)) && {
label: 'SSL',
component: ConnectionModalSslFields,
},
@@ -114,7 +138,7 @@
</div>
</div>
</ModalBase>
</FormProvider>
</FormProviderCore>
<style>
.buttons {
@@ -132,4 +156,5 @@
.error-result {
white-space: normal;
}
</style>
@@ -1,5 +1,6 @@
<script lang="ts">
import FormCheckboxField from '../forms/FormCheckboxField.svelte';
import FormElectronFileSelector from '../forms/FormElectronFileSelector.svelte';
import FormPasswordField from '../forms/FormPasswordField.svelte';
@@ -9,9 +10,12 @@
import FormTextField from '../forms/FormTextField.svelte';
import { extensions } from '../stores';
import getElectron from '../utility/getElectron';
import { useAuthTypes } from '../utility/metadataLoaders';
const { values } = getFormContext();
const electron = getElectron();
$: authType = $values.authType;
$: engine = $values.engine;
$: useDatabaseUrl = $values.useDatabaseUrl;
@@ -20,21 +24,29 @@
$: 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.map(driver => ({
value: driver.engine,
label: driver.title,
})),
...$extensions.drivers
.filter(driver => !driver.isElectronOnly || electron)
.map(driver => ({
value: driver.engine,
label: driver.title,
})),
]}
/>
{#if driver?.supportsDatabaseUrl}
{#if !driver?.showConnectionField || driver.showConnectionField('databaseFile', $values)}
<FormElectronFileSelector label="Database file" name="databaseFile" disabled={!electron} />
{/if}
{#if !driver?.showConnectionField || driver.showConnectionField('useDatabaseUrl', $values)}
<div class="radio">
<FormRadioGroupField
name="useDatabaseUrl"
@@ -46,20 +58,22 @@
</div>
{/if}
{#if driver?.supportsDatabaseUrl && useDatabaseUrl}
{#if !driver?.showConnectionField || driver.showConnectionField('databaseUrl', $values)}
<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}
{/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
@@ -69,17 +83,21 @@
templateProps={{ noMargin: true }}
/>
</div>
<div class="col-3 mr-1">
<FormTextField
label="Port"
name="port"
disabled={disabledFields.includes('port')}
templateProps={{ noMargin: true }}
placeholder={driver && driver.defaultPort}
/>
</div>
{#if !driver?.showConnectionField || driver.showConnectionField('port', $values)}
<div class="col-3 mr-1">
<FormTextField
label="Port"
name="port"
disabled={disabledFields.includes('port')}
templateProps={{ noMargin: true }}
placeholder={driver && driver.defaultPort}
/>
</div>
{/if}
</div>
{/if}
{#if !driver?.showConnectionField || driver.showConnectionField('user', $values)}
<div class="row">
<div class="col-6 mr-1">
<FormTextField
@@ -89,31 +107,36 @@
templateProps={{ noMargin: true }}
/>
</div>
<div class="col-6 mr-1">
<FormPasswordField
label="Password"
name="password"
disabled={disabledFields.includes('password')}
templateProps={{ noMargin: true }}
/>
</div>
{#if !driver?.showConnectionField || driver.showConnectionField('password', $values)}
<div class="col-6 mr-1">
<FormPasswordField
label="Password"
name="password"
disabled={disabledFields.includes('password')}
templateProps={{ noMargin: true }}
/>
</div>
{/if}
</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}
<FormTextField label="Default database" name="defaultDatabase" />
{#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 defaultDatabase}
{#if !driver?.showConnectionField || driver.showConnectionField('defaultDatabase', $values)}
<FormTextField label="Default database" name="defaultDatabase" />
{/if}
{#if defaultDatabase && (!driver?.showConnectionField || driver.showConnectionField('singleDatabase', $values))}
<FormCheckboxField label={`Use only database ${defaultDatabase}`} name="singleDatabase" />
{/if}
@@ -131,4 +154,5 @@
.radio :global(label) {
margin-right: 10px;
}
</style>
@@ -11,6 +11,7 @@
import getElectron from '../utility/getElectron';
import { usePlatformInfo } from '../utility/metadataLoaders';
import FontIcon from '../icons/FontIcon.svelte';
import { extensions } from '../stores';
const { values, setFieldValue } = getFormContext();
const electron = getElectron();
@@ -40,6 +41,7 @@
<FormSelectField
label="SSH Authentication"
name="sshMode"
isNative
disabled={!useSshTunnel}
options={[
{ value: 'userPassword', label: 'Username & password' },
+1 -1
View File
@@ -17,7 +17,7 @@
}
</script>
{#each plugins as packageManifest (packageManifest.name)}
{#each plugins || [] as packageManifest (packageManifest.name)}
<div class="wrapper" on:click={() => openPlugin(packageManifest)}>
<img class="icon" src={extractPluginIcon(packageManifest)} />
<div class="ml-2">
@@ -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;
+7 -2
View File
@@ -11,10 +11,15 @@
import 'ace-builds/src-noconflict/mode-json';
import 'ace-builds/src-noconflict/mode-javascript';
import 'ace-builds/src-noconflict/mode-markdown';
import 'ace-builds/src-noconflict/theme-github';
import 'ace-builds/src-noconflict/theme-twilight';
import 'ace-builds/src-noconflict/ext-searchbox';
import 'ace-builds/src-noconflict/ext-language_tools';
import 'ace-builds/src-noconflict/theme-github';
// import 'ace-builds/src-noconflict/theme-sqlserver';
import 'ace-builds/src-noconflict/theme-twilight';
// import 'ace-builds/src-noconflict/theme-monokai';
import { currentDropDownMenu, currentThemeDefinition } from '../stores';
import _ from 'lodash';
import { handleCommandKeyDown } from '../commands/CommandListener.svelte';
+2 -1
View File
@@ -1,6 +1,7 @@
import _ from 'lodash';
import { get } from 'svelte/store';
import { currentDatabase } from '../stores';
import getConnectionLabel from '../utility/getConnectionLabel';
import openNewTab from '../utility/openNewTab';
export default function newQuery({
@@ -14,7 +15,7 @@ export default function newQuery({
const connection = _.get($currentDatabase, 'connection') || {};
const database = _.get($currentDatabase, 'name');
const tooltip = `${connection.displayName || connection.server}\n${database}`;
const tooltip = `${getConnectionLabel(connection)}\n${database}`;
openNewTab(
{
+15
View File
@@ -19,6 +19,8 @@ function getParsedLocalStorage(key) {
return null;
}
const saveHandlersList = [];
export default function useEditorData({ tabid, reloadToken = 0, loadFromArgs = null, onInitialData = null }) {
const localStorageKey = `tabdata_editor_${tabid}`;
let changeCounter = 0;
@@ -90,6 +92,11 @@ export default function useEditorData({ tabid, reloadToken = 0, loadFromArgs = n
}));
};
const saveToStorageIfNeeded = async () => {
if (savedCounter == changeCounter) return; // all saved
await saveToStorage();
};
const saveToStorage = async () => {
if (value == null) return;
try {
@@ -128,11 +135,13 @@ export default function useEditorData({ tabid, reloadToken = 0, loadFromArgs = n
onMount(() => {
window.addEventListener('beforeunload', saveToStorageSync);
initialLoad();
saveHandlersList.push(saveToStorageIfNeeded);
});
onDestroy(() => {
saveToStorage();
window.removeEventListener('beforeunload', saveToStorageSync);
_.remove(saveHandlersList, x => x == saveToStorageIfNeeded);
});
return {
@@ -144,3 +153,9 @@ export default function useEditorData({ tabid, reloadToken = 0, loadFromArgs = n
initialLoad,
};
}
export async function saveAllPendingEditorData() {
for (const item of saveHandlersList) {
await item();
}
}
+26 -10
View File
@@ -7,6 +7,7 @@
import FormProvider from '../forms/FormProvider.svelte';
import FormSubmit from '../forms/FormSubmit.svelte';
import FormTextField from '../forms/FormTextField.svelte';
import FormValues from '../forms/FormValues.svelte';
import ModalBase from '../modals/ModalBase.svelte';
import { closeCurrentModal } from '../modals/modalTools';
@@ -32,17 +33,32 @@
<ModalBase {...$$restProps}>
<div slot="header">Settings</div>
<div class="heading">Appearance</div>
<FormCheckboxField name=":visibleToolbar" label="Show toolbar" defaultValue={true} />
<FormValues let:values>
<div class="heading">Appearance</div>
<FormCheckboxField name=":visibleToolbar" label="Show toolbar" defaultValue={true} />
<div class="heading">Data grid</div>
<FormCheckboxField name="dataGrid.hideLeftColumn" label="Hide left column by default" />
<FormTextField
name="dataGrid.pageSize"
label="Page size (number of rows for incremental loading, must be between 5 and 1000)"
defaultValue="100"
/>
<FormCheckboxField name="dataGrid.showHintColumns" label="Show foreign key hints" defaultValue={true} />
<div class="heading">Data grid</div>
<FormCheckboxField name="dataGrid.hideLeftColumn" label="Hide left column by default" />
<FormTextField
name="dataGrid.pageSize"
label="Page size (number of rows for incremental loading, must be between 5 and 1000)"
defaultValue="100"
/>
<FormCheckboxField name="dataGrid.showHintColumns" label="Show foreign key hints" defaultValue={true} />
<div class="heading">Connection</div>
<FormCheckboxField
name="connection.autoRefresh"
label="Automatic refresh of database model on background"
defaultValue={false}
/>
<FormTextField
name="connection.autoRefreshInterval"
label="Interval between automatic refreshes in seconds"
defaultValue="30"
disabled={values['connection.autoRefresh'] === false}
/>
</FormValues>
<div slot="footer">
<FormSubmit value="OK" on:click={handleOk} />
+7
View File
@@ -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;
@@ -11,6 +11,7 @@
name: 'Save',
// keyText: 'Ctrl+S',
toolbar: true,
isRelatedToTab: true,
icon: 'icon save',
testEnabled: () => getCurrentEditor()?.canSave(),
onClick: () => getCurrentEditor().save(),
@@ -8,6 +8,7 @@
name: 'Save',
// keyText: 'Ctrl+S',
toolbar: true,
isRelatedToTab: true,
icon: 'icon save',
testEnabled: () => getCurrentEditor() != null,
onClick: () => getCurrentEditor().save(),
@@ -19,6 +19,7 @@
name: 'Preview',
icon: 'icon run',
toolbar: true,
isRelatedToTab: true,
keyText: 'F5 | Ctrl+Enter',
testEnabled: () => getCurrentEditor() != null,
onClick: () => getCurrentEditor().preview(),
+5
View File
@@ -27,6 +27,7 @@
execute: true,
toggleComment: true,
findReplace: true,
executeAdditionalCondition: () => getCurrentEditor()?.hasConnection(),
});
</script>
@@ -121,6 +122,10 @@
return tabid;
}
export function hasConnection() {
return !!conid;
}
export async function execute() {
if (busy) return;
executeNumber++;
@@ -8,6 +8,7 @@
name: 'Save',
// keyText: 'Ctrl+S',
toolbar: true,
isRelatedToTab: true,
icon: 'icon save',
testEnabled: () => getCurrentEditor()?.canSave(),
onClick: () => getCurrentEditor().save(),
@@ -13,6 +13,8 @@
console.log('CRASH DETECTED!!!');
const lastDbGateCrashJson = localStorage.getItem('lastDbGateCrash');
const lastDbGateCrash = lastDbGateCrashJson ? JSON.parse(lastDbGateCrashJson) : null;
// let detail = e?.reason?.stack || '';
// if (detail) detail = '\n\n' + detail;
if (lastDbGateCrash && new Date().getTime() - lastDbGateCrash < 30 * 1000) {
if (
@@ -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);
}
}
@@ -0,0 +1,29 @@
export function getDatabaseFileLabel(databaseFile) {
if (!databaseFile) return databaseFile;
const m = databaseFile.match(/[\/]([^\/]+)$/);
if (m) return m[1];
return databaseFile;
}
export default function getConnectionLabel(connection, { allowExplicitDatabase = true } = {}) {
if (!connection) {
return null;
}
if (connection.displayName) {
return connection.displayName;
}
if (connection.singleDatabase && connection.server && allowExplicitDatabase && connection.defaultDatabase) {
return `${connection.defaultDatabase} on ${connection.server}`;
}
if (connection.databaseFile) {
return getDatabaseFileLabel(connection.databaseFile);
}
if (connection.server) {
return connection.server;
}
if (connection.singleDatabase && connection.defaultDatabase) {
return `${connection.defaultDatabase}`;
}
return '';
}
+26 -2
View File
@@ -3,19 +3,37 @@ import { get } from 'svelte/store';
import newQuery from '../query/newQuery';
import ImportExportModal from '../modals/ImportExportModal.svelte';
import getElectron from './getElectron';
import { extensions } from '../stores';
import { currentDatabase, extensions } from '../stores';
import { getUploadListener } from './uploadFiles';
import axiosInstance from '../utility/axiosInstance';
import { getDatabaseFileLabel } from './getConnectionLabel';
export function canOpenByElectron(file, extensions) {
if (!file) return false;
const nameLower = file.toLowerCase();
if (nameLower.endsWith('.sql')) return true;
if (nameLower.endsWith('.db') || nameLower.endsWith('.sqlite') || nameLower.endsWith('.sqlite3')) return true;
for (const format of extensions.fileFormats) {
if (nameLower.endsWith(`.${format.extension}`)) return true;
}
return false;
}
export async function openSqliteFile(filePath) {
const defaultDatabase = getDatabaseFileLabel(filePath);
const resp = await axiosInstance.post('connections/save', {
_id: undefined,
databaseFile: filePath,
engine: 'sqlite@dbgate-plugin-sqlite',
singleDatabase: true,
defaultDatabase,
});
currentDatabase.set({
connection: resp.data,
name: getDatabaseFileLabel(filePath),
});
}
export function openElectronFileCore(filePath, extensions) {
const nameLower = filePath.toLowerCase();
const path = window.require('path');
@@ -33,6 +51,11 @@ export function openElectronFileCore(filePath, extensions) {
savedFilePath: filePath,
savedFormat: 'text',
});
return;
}
if (nameLower.endsWith('.db') || nameLower.endsWith('.sqlite') || nameLower.endsWith('.sqlite')) {
openSqliteFile(filePath);
return;
}
for (const format of extensions.fileFormats) {
if (nameLower.endsWith(`.${format.extension}`)) {
@@ -72,8 +95,9 @@ export function openElectronFile() {
const ext = get(extensions);
const filePaths = electron.remote.dialog.showOpenDialogSync(electron.remote.getCurrentWindow(), {
filters: [
{ name: `All supported files`, extensions: ['sql', ...getFileFormatExtensions(ext)] },
{ name: `All supported files`, extensions: ['sql', 'sqlite', 'db', 'sqlite3', ...getFileFormatExtensions(ext)] },
{ name: `SQL files`, extensions: ['sql'] },
{ name: `SQLite database`, extensions: ['sqlite', 'db', 'sqlite3'] },
...getFileFormatFilters(ext),
],
});
+34 -1
View File
@@ -6,6 +6,7 @@ import tabs from '../tabs';
import { setSelectedTabFunc } from './common';
import localforage from 'localforage';
import stableStringify from 'json-stable-stringify';
import { saveAllPendingEditorData } from '../query/useEditorData';
function findFreeNumber(numbers: number[]) {
if (numbers.length == 0) return 1;
@@ -74,9 +75,9 @@ export default async function openNewTab(newTab, initialData = undefined, option
openedTabs.update(files => [
...(files || []).map(x => ({ ...x, selected: false })),
{
...newTab,
tabid,
selected: true,
...newTab,
},
]);
@@ -91,3 +92,35 @@ export default async function openNewTab(newTab, initialData = undefined, option
// },
// ]);
}
export async function duplicateTab(tab) {
await saveAllPendingEditorData();
let title = tab.title;
const mtitle = title.match(/^(.*#)[\d]+$/);
if (mtitle) title = mtitle[1];
const keyRegex = /^tabdata_([^_]+)_([^_]+)$/;
const initialData = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const m = key.match(keyRegex);
if (m && m[2] == tab.tabid) {
initialData[m[1]] = JSON.parse(localStorage.getItem(key));
}
}
for (const key of await localforage.keys()) {
const m = key.match(keyRegex);
if (m && m[2] == tab.tabid) {
initialData[m[1]] = await localforage.getItem(key);
}
}
openNewTab(
{
..._.omit(tab, ['tabid']),
title,
},
initialData,
{ forceNewTab: true }
);
}
@@ -12,6 +12,7 @@
import axiosInstance from '../utility/axiosInstance';
import ToolbarButton from './ToolbarButton.svelte';
import runCommand from '../commands/runCommand';
import getConnectionLabel from '../utility/getConnectionLabel';
const connections = useConnectionList();
const serverStatus = useServerStatus();
@@ -36,7 +37,7 @@
</SearchBoxWrapper>
<WidgetsInnerContainer>
<AppObjectList
list={_.sortBy(connectionsWithStatus, ({ displayName, server }) => (displayName || server || '').toUpperCase())}
list={_.sortBy(connectionsWithStatus, connection => (getConnectionLabel(connection) || '').toUpperCase())}
module={connectionAppObject}
subItemsComponent={SubDatabaseList}
expandOnClick
@@ -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>
@@ -42,7 +42,7 @@
<ErrorInfo message={$status.message} icon="img error" />
<InlineButton on:click={handleRefreshDatabase}>Refresh</InlineButton>
</WidgetsInnerContainer>
{:else if objectList.length == 0 && $status && $status.name != 'pending' && $objects}
{:else if objectList.length == 0 && $status && $status.name != 'pending' && $status.name != 'checkStructure' && $status.name != 'loadStructure' && $objects}
<WidgetsInnerContainer>
<ErrorInfo
message={`Database ${database} is empty or structure is not loaded, press Refresh button to reload structure`}
@@ -56,7 +56,7 @@
<InlineButton on:click={handleRefreshDatabase}>Refresh</InlineButton>
</SearchBoxWrapper>
<WidgetsInnerContainer>
{#if ($status && $status.name == 'pending' && $objects) || !$objects}
{#if ($status && ($status.name == 'pending' || $status.name == 'checkStructure' || $status.name == 'loadStructure') && $objects) || !$objects}
<LoadingInfo message="Loading database structure" />
{:else}
<AppObjectList
@@ -6,10 +6,11 @@
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
$: conid = _.get($currentDatabase, 'connection._id');
$: singleDatabase = _.get($currentDatabase, 'connection.singleDatabase');
$: database = _.get($currentDatabase, 'name');
</script>
{#if conid && database}
{#if conid && (database || singleDatabase)}
<SqlObjectList {conid} {database} />
{:else}
<WidgetsInnerContainer>
+45 -2
View File
@@ -7,15 +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;
@@ -23,6 +27,20 @@
$: serverVersion = useDatabaseServerVersion(connection ? { conid: connection._id, database: databaseName } : {});
$: 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">
@@ -33,10 +51,10 @@
{databaseName}
</div>
{/if}
{#if connection && (connection.displayName || connection.server)}
{#if connectionLabel}
<div class="item">
<FontIcon icon="icon server" />
{connection.displayName || connection.server}
{connectionLabel}
</div>
{/if}
{#if connection && connection.user}
@@ -49,6 +67,10 @@
<div class="item">
{#if $status.name == 'pending'}
<FontIcon icon="icon loading" /> Loading
{:else if $status.name == 'checkStructure'}
<FontIcon icon="icon loading" /> Checking model
{:else if $status.name == 'loadStructure'}
<FontIcon icon="icon loading" /> Loading model
{:else if $status.name == 'ok'}
<FontIcon icon="img ok-inv" /> Connected
{:else if $status.name == 'error'}
@@ -69,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}
@@ -88,6 +122,7 @@
color: var(--theme-font-inv-1);
align-items: stretch;
justify-content: space-between;
cursor: default;
}
.container {
display: flex;
@@ -102,4 +137,12 @@
text-overflow: ellipsis;
white-space: nowrap;
}
.clickable {
cursor: pointer;
}
.clickable:hover {
background-color: var(--theme-bg-statusbar-inv-hover);
}
</style>
+56 -24
View File
@@ -45,22 +45,34 @@
);
const closeOthers = closeTabFunc((x, active) => x.tabid != active.tabid);
function getTabDbName(tab) {
function getTabDbName(tab, connectionList) {
if (tab.props && tab.props.conid && tab.props.database) return tab.props.database;
if (tab.props && tab.props.conid) {
const connection = connectionList?.find(x => x._id == tab.props.conid);
if (connection) return getConnectionLabel(connection.displayName, { allowExplicitDatabase: false });
return '???';
}
if (tab.props && tab.props.archiveFolder) return tab.props.archiveFolder;
return '(no DB)';
}
function getTabDbKey(tab) {
if (tab.props && tab.props.conid && tab.props.database)
if (tab.props && tab.props.conid && tab.props.database) {
return `database://${tab.props.database}-${tab.props.conid}`;
if (tab.props && tab.props.archiveFolder) return `archive://${tab.props.archiveFolder}`;
}
if (tab.props && tab.props.conid) {
return `server://${tab.props.conid}`;
}
if (tab.props && tab.props.archiveFolder) {
return `archive://${tab.props.archiveFolder}`;
}
return '_no';
}
function getDbIcon(key) {
if (key.startsWith('database://')) return 'icon database';
if (key.startsWith('archive://')) return 'icon archive';
if (key.startsWith('server://')) return 'icon server';
return 'icon file';
}
@@ -87,9 +99,9 @@
registerCommand({
id: 'tabs.addToFavorites',
category: 'Tabs',
name: 'Favorites',
icon: 'icon favorite',
toolbar: true,
name: 'Add current tab to favorites',
// icon: 'icon favorite',
// toolbar: true,
testEnabled: () =>
getActiveTab()?.tabComponent &&
tabs[getActiveTab()?.tabComponent] &&
@@ -112,18 +124,24 @@
import tabs from '../tabs';
import { setSelectedTab } from '../utility/common';
import contextMenu from '../utility/contextMenu';
import { getConnectionInfo } from '../utility/metadataLoaders';
import getConnectionLabel from '../utility/getConnectionLabel';
import { getConnectionInfo, useConnectionList } from '../utility/metadataLoaders';
import { duplicateTab } from '../utility/openNewTab';
$: connectionList = useConnectionList();
$: currentDbKey =
$currentDatabase && $currentDatabase.name && $currentDatabase.connection
? `database://${$currentDatabase.name}-${$currentDatabase.connection._id}`
: $currentDatabase && $currentDatabase.connection
? `server://${$currentDatabase.connection._id}`
: '_no';
$: tabsWithDb = $openedTabs
.filter(x => !x.closedTime)
.map(tab => ({
...tab,
tabDbName: getTabDbName(tab),
tabDbName: getTabDbName(tab, $connectionList),
tabDbKey: getTabDbKey(tab),
}));
@@ -146,9 +164,10 @@
}
};
const getContextMenu = (tabid, props) => () => {
const getContextMenu = tab => () => {
const { tabid, props, tabComponent } = tab;
const { conid, database } = props || {};
const res = [
return [
{
text: 'Close',
onClick: () => closeTab(tabid),
@@ -161,20 +180,33 @@
text: 'Close others',
onClick: () => closeOthers(tabid),
},
{
text: 'Duplicate',
onClick: () => duplicateTab(tab),
},
tabComponent &&
tabs[tabComponent] &&
tabs[tabComponent].allowAddToFavorites &&
tabs[tabComponent].allowAddToFavorites(props) && [
{ divider: true },
{
text: 'Add to favorites',
onClick: () => showModal(FavoriteModal, { savingTab: tab }),
},
],
conid &&
database && [
{ divider: true },
{
text: `Close with same DB - ${database}`,
onClick: () => closeWithSameDb(tabid),
},
{
text: `Close with other DB than ${database}`,
onClick: () => closeWithOtherDb(tabid),
},
],
];
if (conid && database) {
res.push(
{
text: `Close with same DB - ${database}`,
onClick: () => closeWithSameDb(tabid),
},
{
text: `Close with other DB than ${database}`,
onClick: () => closeWithOtherDb(tabid),
}
);
}
return res;
};
const handleSetDb = async props => {
@@ -216,7 +248,7 @@
class:selected={tab.selected}
on:click={e => handleTabClick(e, tab.tabid)}
on:mouseup={e => handleMouseUp(e, tab.tabid)}
use:contextMenu={getContextMenu(tab.tabid, tab.props)}
use:contextMenu={getContextMenu(tab)}
>
<FontIcon icon={tab.busy ? 'icon loading' : tab.icon} />
<span class="file-name">
+60 -20
View File
@@ -10,7 +10,8 @@
import _ from 'lodash';
import { openFavorite } from '../appobj/FavoriteFileAppObject.svelte';
import runCommand from '../commands/runCommand';
import { commands, commandsCustomized } from '../stores';
import FontIcon from '../icons/FontIcon.svelte';
import { activeTab, commands, commandsCustomized } from '../stores';
import getElectron from '../utility/getElectron';
import { useFavorites } from '../utility/metadataLoaders';
import ToolbarButton from './ToolbarButton.svelte';
@@ -25,26 +26,48 @@
);
</script>
<div class="container">
{#if !electron}
<ToolbarButton externalImage="logo192.png" on:click={() => runCommand('about.show')} />
{/if}
{#each ($favorites || []).filter(x => x.showInToolbar) as item}
<ToolbarButton on:click={() => openFavorite(item)} icon={item.icon || 'icon favorite'}>
{item.title}
</ToolbarButton>
{/each}
<div class="root">
<div class="container">
{#if !electron}
<ToolbarButton externalImage="logo192.png" on:click={() => runCommand('about.show')} />
{/if}
{#each ($favorites || []).filter(x => x.showInToolbar) as item}
<ToolbarButton on:click={() => openFavorite(item)} icon={item.icon || 'icon favorite'}>
{item.title}
</ToolbarButton>
{/each}
{#each list as command}
<ToolbarButton
icon={command.icon}
on:click={command.onClick}
disabled={!command.enabled}
title={getCommandTitle(command)}
>
{command.toolbarName || command.name}
</ToolbarButton>
{/each}
{#each list.filter(x => !x.isRelatedToTab) as command}
<ToolbarButton
icon={command.icon}
on:click={command.onClick}
disabled={!command.enabled}
title={getCommandTitle(command)}
>
{command.toolbarName || command.name}
</ToolbarButton>
{/each}
</div>
<div class="container">
{#if $activeTab && list.filter(x => x.isRelatedToTab).length > 0}
<div class="activeTab">
<div class="activeTabInner">
<FontIcon icon={$activeTab.icon} />
{$activeTab.title}:
</div>
</div>
{/if}
{#each list.filter(x => x.isRelatedToTab) as command}
<ToolbarButton
icon={command.icon}
on:click={command.onClick}
disabled={!command.enabled}
title={getCommandTitle(command)}
>
{command.toolbarName || command.name}
</ToolbarButton>
{/each}
</div>
</div>
<style>
@@ -54,4 +77,21 @@
align-items: stretch;
height: var(--dim-toolbar-height);
}
.root {
display: flex;
align-items: stretch;
justify-content: space-between;
}
.activeTab {
background-color: var(--theme-bg-2);
white-space: nowrap;
display: flex;
padding-left: 15px;
padding-right: 15px;
}
.activeTabInner {
align-self: center;
}
</style>
+13
View File
@@ -0,0 +1,13 @@
diff --git a/node_modules/sql-query-identifier/lib/tokenizer.js b/node_modules/sql-query-identifier/lib/tokenizer.js
index f8980fe..bb03059 100644
--- a/node_modules/sql-query-identifier/lib/tokenizer.js
+++ b/node_modules/sql-query-identifier/lib/tokenizer.js
@@ -249,7 +249,7 @@ function skipWord(state, value) {
};
}
function isWhitespace(ch) {
- return ch === ' ' || ch === '\t' || ch === '\n';
+ return ch === ' ' || ch === '\t' || ch === '\n' || ch == '\r';
}
function isString(ch, dialect) {
const stringStart = dialect === 'mysql' ? ["'", '"'] : ["'"];
+13
View File
@@ -0,0 +1,13 @@
diff --git a/node_modules/svelte/internal/index.js b/node_modules/svelte/internal/index.js
index ee20a17..7b6fff8 100644
--- a/node_modules/svelte/internal/index.js
+++ b/node_modules/svelte/internal/index.js
@@ -200,7 +200,7 @@ function insert(target, node, anchor) {
target.insertBefore(node, anchor || null);
}
function detach(node) {
- node.parentNode.removeChild(node);
+ if (node.parentNode) node.parentNode.removeChild(node);
}
function destroy_each(iterations, detaching) {
for (let i = 0; i < iterations.length; i += 1) {
@@ -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,
@@ -8,14 +8,11 @@ class Analyser extends DatabaseAnalyser {
async _runAnalysis() {
const collections = await this.pool.__getDatabase().listCollections().toArray();
const res = this.mergeAnalyseResult(
{
collections: collections.map((x) => ({
pureName: x.name,
})),
},
(x) => x.pureName
);
const res = this.mergeAnalyseResult({
collections: collections.map((x) => ({
pureName: x.name,
})),
});
// console.log('MERGED', res);
return res;
}
@@ -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
`;

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