Compare commits
141 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9545eaf7f | ||
|
|
216ef7736b | ||
|
|
ae7697f655 | ||
|
|
23225cf86b | ||
|
|
63ad36f758 | ||
|
|
80e1563877 | ||
|
|
3f5c7aecd7 | ||
|
|
abd2492889 | ||
|
|
872468899d | ||
|
|
7a008e5a9d | ||
|
|
23940aa324 | ||
|
|
1888de8728 | ||
|
|
615397f332 | ||
|
|
e251459512 | ||
|
|
a9c8cee08a | ||
|
|
1638095c98 | ||
|
|
62cedd23b7 | ||
|
|
3d882f47a7 | ||
|
|
88ddc28208 | ||
|
|
800666f813 | ||
|
|
0b8add848a | ||
|
|
cd7edcb443 | ||
|
|
e483fd9e99 | ||
|
|
9664e6f981 | ||
|
|
d1429dd2a1 | ||
|
|
e739aed80d | ||
|
|
28e19402f3 | ||
|
|
45a065f391 | ||
|
|
67e8eb32f7 | ||
|
|
5622e3af77 | ||
|
|
7d34458553 | ||
|
|
8b747796e7 | ||
|
|
4802c36b54 | ||
|
|
988e4345d4 | ||
|
|
e02305879e | ||
|
|
8baad56315 | ||
|
|
14bbc7b057 | ||
|
|
7b6ca27b66 | ||
|
|
38aae142ea | ||
|
|
bd6c116cc0 | ||
|
|
4522c37bfa | ||
|
|
7d789d5712 | ||
|
|
c4c2274488 | ||
|
|
a8b71d452b | ||
|
|
c7d69b0fb5 | ||
|
|
47ea474555 | ||
|
|
e647ab471e | ||
|
|
fd6524867e | ||
|
|
c24cc1dc72 | ||
|
|
e3d1e4f53e | ||
|
|
7b32424143 | ||
|
|
519767fd49 | ||
|
|
505ab2e075 | ||
|
|
00d0c27502 | ||
|
|
d171d7d785 | ||
|
|
09593e0b22 | ||
|
|
771ca6ad83 | ||
|
|
83014d3a5b | ||
|
|
caa2d22dbd | ||
|
|
3c089a5b81 | ||
|
|
d1bf2dbc4b | ||
|
|
a8a9afc936 | ||
|
|
d0cbd5d0a4 | ||
|
|
67e1913683 | ||
|
|
8ff706a17f | ||
|
|
08692dc63f | ||
|
|
41d85d4117 | ||
|
|
f343d414ef | ||
|
|
6cda7b2508 | ||
|
|
9085d49d21 | ||
|
|
bb11d7e62b | ||
|
|
7524b30f50 | ||
|
|
c30724c5da | ||
|
|
1e4c108f6f | ||
|
|
72033e5830 | ||
|
|
1d24fd9942 | ||
|
|
e104feef14 | ||
|
|
ccdce6ef43 | ||
|
|
fccd550d4b | ||
|
|
3a4a10985b | ||
|
|
8ee96bd4a0 | ||
|
|
269046daa5 | ||
|
|
4738113ce3 | ||
|
|
f7a2931253 | ||
|
|
d832057076 | ||
|
|
00fdf14b6e | ||
|
|
ab26f4624a | ||
|
|
8caf5d622e | ||
|
|
0cf8fc79c2 | ||
|
|
65aa6067e1 | ||
|
|
9a2d56bfe4 | ||
|
|
a1b8e7b641 | ||
|
|
137bb7b002 | ||
|
|
e83946f35e | ||
|
|
64af838f40 | ||
|
|
0d31cc4204 | ||
|
|
73a1fce919 | ||
|
|
e8d5bdbfaf | ||
|
|
2e37af1ee4 | ||
|
|
55564ef82a | ||
|
|
2461b48244 | ||
|
|
b05f91f4cb | ||
|
|
238b6d94d1 | ||
|
|
8ee2db1bec | ||
|
|
484aa932d3 | ||
|
|
29aa59771c | ||
|
|
cef6b8520e | ||
|
|
49f8fb71e4 | ||
|
|
375a441abf | ||
|
|
0848008302 | ||
|
|
cacd6ae849 | ||
|
|
e97388e14b | ||
|
|
67b57ab756 | ||
|
|
bcf183abe2 | ||
|
|
f92df5c326 | ||
|
|
28bbf9a01e | ||
|
|
08d6f83a48 | ||
|
|
90af165afd | ||
|
|
8a4ee3e01e | ||
|
|
977818253d | ||
|
|
ec5db6d562 | ||
|
|
2d4098ff6a | ||
|
|
321d95f522 | ||
|
|
53480210d4 | ||
|
|
0b1a4ee33f | ||
|
|
477099e508 | ||
|
|
516d007c22 | ||
|
|
ab4febf938 | ||
|
|
361875d7fc | ||
|
|
c0c1f9d786 | ||
|
|
1d264ab559 | ||
|
|
553329688a | ||
|
|
585731a1b3 | ||
|
|
a6207f01af | ||
|
|
76e51343d0 | ||
|
|
6c246c9eaa | ||
|
|
479cec4209 | ||
|
|
6b85870523 | ||
|
|
a98380a941 | ||
|
|
89a3798d56 | ||
|
|
bf202719eb |
13
.github/workflows/build-app-beta.yaml
vendored
13
.github/workflows/build-app-beta.yaml
vendored
@@ -11,8 +11,8 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-18.04, windows-2016]
|
||||
# os: [macOS-10.14, windows-2016, ubuntu-18.04]
|
||||
# os: [ubuntu-18.04, windows-2016]
|
||||
os: [macOS-10.14, windows-2016, ubuntu-18.04]
|
||||
|
||||
steps:
|
||||
- name: Context
|
||||
@@ -63,11 +63,18 @@ jobs:
|
||||
run: |
|
||||
mkdir artifacts
|
||||
|
||||
cp app/dist/*.deb artifacts/dbgate-beta.deb || true
|
||||
cp app/dist/*.AppImage artifacts/dbgate-beta.AppImage || true
|
||||
cp app/dist/*.exe artifacts/dbgate-beta.exe || true
|
||||
cp app/dist/*windows*.zip artifacts/dbgate-windows-beta.zip || true
|
||||
cp app/dist/*.dmg artifacts/dbgate-beta.dmg || true
|
||||
|
||||
mv app/dist/*.exe artifacts/ || true
|
||||
mv app/dist/*.zip artifacts/ || true
|
||||
mv app/dist/*.AppImage artifacts/ || true
|
||||
mv app/dist/*.deb artifacts/ || true
|
||||
mv app/dist/*.snap artifacts/ || true
|
||||
# mv app/dist/*.dmg artifacts/ || true
|
||||
mv app/dist/*.dmg artifacts/ || true
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v1
|
||||
|
||||
2
.github/workflows/build-app.yaml
vendored
2
.github/workflows/build-app.yaml
vendored
@@ -74,9 +74,11 @@ jobs:
|
||||
cp app/dist/*.deb artifacts/dbgate-latest.deb || true
|
||||
cp app/dist/*.AppImage artifacts/dbgate-latest.AppImage || true
|
||||
cp app/dist/*.exe artifacts/dbgate-latest.exe || true
|
||||
cp app/dist/*windows*.zip artifacts/dbgate-windows-latest.zip || true
|
||||
cp app/dist/*.dmg artifacts/dbgate-latest.dmg || true
|
||||
|
||||
mv app/dist/*.exe artifacts/ || true
|
||||
mv app/dist/*.zip artifacts/ || true
|
||||
mv app/dist/*.AppImage artifacts/ || true
|
||||
mv app/dist/*.deb artifacts/ || true
|
||||
mv app/dist/*.dmg artifacts/ || true
|
||||
|
||||
47
.github/workflows/build-docker-beta.yaml
vendored
Normal file
47
.github/workflows/build-docker-beta.yaml
vendored
Normal 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
|
||||
29
CHANGELOG.md
29
CHANGELOG.md
@@ -1,5 +1,34 @@
|
||||
# ChangeLog
|
||||
|
||||
### 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
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[](https://www.npmjs.com/package/dbgate)
|
||||

|
||||
[](https://snapcraft.io/dbgate)
|
||||
[](https://snapcraft.io/dbgate)
|
||||
[](https://github.com/prettier/prettier)
|
||||
[](https://paypal.me/JanProchazkaCz/30eur)
|
||||
|
||||
# DbGate - database administration tool
|
||||
|
||||
@@ -26,7 +26,7 @@ Supported databases:
|
||||
* Master/detail views
|
||||
* Query designer
|
||||
* Form view for comfortable work with tables with many columns
|
||||
* JSON view on MognoDB collections
|
||||
* JSON view on MongoDB collections
|
||||
* Explore tables, views, procedures, functions, MongoDB collections
|
||||
* SQL editor, execute SQL script, SQL code formatter, SQL code completion, SQL join wizard
|
||||
* Mongo JavaScript editor, execute Mongo script (with NodeJs syntax)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"name": "dbgate",
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.1",
|
||||
"private": true,
|
||||
"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"
|
||||
@@ -21,7 +22,13 @@
|
||||
"artifactName": "dbgate-mac-${version}.${ext}",
|
||||
"publish": [
|
||||
"github"
|
||||
]
|
||||
],
|
||||
"target": {
|
||||
"target": "default",
|
||||
"arch": [
|
||||
"x64"
|
||||
]
|
||||
}
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
@@ -48,7 +55,8 @@
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
"nsis"
|
||||
"nsis",
|
||||
"zip"
|
||||
],
|
||||
"artifactName": "dbgate-windows-${version}.${ext}",
|
||||
"icon": "icon.ico",
|
||||
@@ -83,4 +91,4 @@
|
||||
"optionalDependencies": {
|
||||
"msnodesqlv8": "^2.0.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,14 +205,16 @@ function createWindow() {
|
||||
} else {
|
||||
const apiProcess = fork(path.join(__dirname, '../packages/api/dist/bundle.js'), [
|
||||
'--dynport',
|
||||
'--is-electron-bundle',
|
||||
'--native-modules',
|
||||
path.join(__dirname, 'nativeModules'),
|
||||
// '../../../src/nativeModules'
|
||||
]);
|
||||
apiProcess.on('message', msg => {
|
||||
if (msg.msgtype == 'listening') {
|
||||
const { port } = msg;
|
||||
const { port, authorization } = msg;
|
||||
global['port'] = port;
|
||||
global['authorization'] = authorization;
|
||||
loadMainWindow();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -2,10 +2,10 @@ const fs = require('fs');
|
||||
|
||||
let fillContent = '';
|
||||
|
||||
// if (!process.argv.includes('--electron')) {
|
||||
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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "4.1.1",
|
||||
"version": "4.2.0-beta.3",
|
||||
"name": "dbgate-all",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
@@ -31,17 +31,16 @@
|
||||
"setCurrentVersion": "node setCurrentVersion",
|
||||
"generatePadFile": "node generatePadFile",
|
||||
"fillNativeModules": "node fillNativeModules",
|
||||
"fillNativeModulesElectron": "node fillNativeModules --eletron",
|
||||
"fillNativeModulesElectron": "node fillNativeModules --electron",
|
||||
"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",
|
||||
"prepare": "yarn build:lib",
|
||||
"start": "concurrently --kill-others-on-fail \"yarn start:api\" \"yarn start:web\"",
|
||||
"lib": "concurrently --kill-others-on-fail \"yarn start:sqltree\" \"yarn start:filterparser\" \"yarn start:datalib\" \"yarn start:tools\" \"yarn build:plugins:frontend:watch\"",
|
||||
"ts:api": "yarn workspace dbgate-api ts",
|
||||
"ts:web": "yarn workspace dbgate-web ts",
|
||||
"ts": "yarn ts:api && yarn ts:web",
|
||||
"postinstall": "patch-package && yarn fillNativeModules && yarn build:plugins:frontend"
|
||||
"postinstall": "yarn build:lib && patch-package && yarn fillNativeModules && yarn build:plugins:frontend"
|
||||
},
|
||||
"dependencies": {
|
||||
"concurrently": "^5.1.0",
|
||||
|
||||
@@ -1,15 +1 @@
|
||||
CONNECTIONS=mysql,postgres
|
||||
|
||||
LABEL_mysql=MySql localhost
|
||||
SERVER_mysql=localhost
|
||||
USER_mysql=root
|
||||
PASSWORD_mysql=test
|
||||
PORT_mysql=3307
|
||||
ENGINE_mysql=mysql@dbgate-plugin-mysql
|
||||
|
||||
LABEL_postgres=Postgres localhost
|
||||
SERVER_postgres=localhost
|
||||
USER_postgres=postgres
|
||||
PASSWORD_postgres=test
|
||||
PORT_postgres=5433
|
||||
ENGINE_postgres=postgres@dbgate-plugin-postgres
|
||||
DEVMODE=1
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
DEVMODE=1
|
||||
|
||||
CONNECTIONS=mysql
|
||||
|
||||
LABEL_mysql=MySql
|
||||
17
packages/api/.env-portal
Normal file
17
packages/api/.env-portal
Normal file
@@ -0,0 +1,17 @@
|
||||
DEVMODE=1
|
||||
|
||||
CONNECTIONS=mysql,postgres
|
||||
|
||||
LABEL_mysql=MySql localhost
|
||||
SERVER_mysql=localhost
|
||||
USER_mysql=root
|
||||
PASSWORD_mysql=test
|
||||
PORT_mysql=3307
|
||||
ENGINE_mysql=mysql@dbgate-plugin-mysql
|
||||
|
||||
LABEL_postgres=Postgres localhost
|
||||
SERVER_postgres=localhost
|
||||
USER_postgres=postgres
|
||||
PASSWORD_postgres=test
|
||||
PORT_postgres=5433
|
||||
ENGINE_postgres=postgres@dbgate-plugin-postgres
|
||||
@@ -1,13 +1,12 @@
|
||||
{
|
||||
"name": "dbgate-api",
|
||||
"main": "src/index.js",
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.1",
|
||||
"homepage": "https://dbgate.org/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
|
||||
"author": "Jan Prochazka",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
@@ -20,20 +19,21 @@
|
||||
"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",
|
||||
"compare-versions": "^3.6.0",
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^6.0.3",
|
||||
"dbgate-sqltree": "^4.0.0",
|
||||
"dbgate-tools": "^4.0.0",
|
||||
"dbgate-sqltree": "^4.1.1",
|
||||
"dbgate-tools": "^4.1.1",
|
||||
"eslint": "^6.8.0",
|
||||
"express": "^4.17.1",
|
||||
"express-basic-auth": "^1.2.0",
|
||||
"express-fileupload": "^1.2.0",
|
||||
"find-free-port": "^2.0.0",
|
||||
"fs-extra": "^9.1.0",
|
||||
"get-port": "^5.1.1",
|
||||
"http": "^0.0.0",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
"line-reader": "^0.4.0",
|
||||
@@ -49,16 +49,16 @@
|
||||
"uuid": "^3.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node src/index.js",
|
||||
"start:portal": "env-cmd nodemon src/index.js",
|
||||
"start:covid": "env-cmd -f .covid-env nodemon src/index.js",
|
||||
"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",
|
||||
"ts": "tsc",
|
||||
"build": "webpack"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^9.0.11",
|
||||
"@types/lodash": "^4.14.149",
|
||||
"dbgate-types": "^4.0.0",
|
||||
"dbgate-types": "^4.1.1",
|
||||
"env-cmd": "^10.1.0",
|
||||
"node-loader": "^1.0.2",
|
||||
"nodemon": "^2.0.2",
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { datadir } = require('../utility/directories');
|
||||
const hasPermission = require('../utility/hasPermission');
|
||||
const socket = require('../utility/socket');
|
||||
const _ = require('lodash');
|
||||
|
||||
const currentVersion = require('../currentVersion');
|
||||
const platformInfo = require('../utility/platformInfo');
|
||||
|
||||
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;
|
||||
@@ -37,5 +54,26 @@ module.exports = {
|
||||
async platformInfo() {
|
||||
return platformInfo;
|
||||
},
|
||||
|
||||
|
||||
getSettings_meta: 'get',
|
||||
async getSettings() {
|
||||
return this.settingsValue;
|
||||
},
|
||||
|
||||
updateSettings_meta: 'post',
|
||||
async updateSettings(values) {
|
||||
if (!hasPermission(`settings/change`)) return false;
|
||||
try {
|
||||
const updated = {
|
||||
...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) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -46,7 +46,7 @@ module.exports = {
|
||||
raw: true,
|
||||
},
|
||||
test(req, res) {
|
||||
const subprocess = fork(process.argv[1], ['connectProcess', ...process.argv.slice(3)]);
|
||||
const subprocess = fork(process.argv[1], ['--start-process', 'connectProcess', ...process.argv.slice(3)]);
|
||||
subprocess.on('message', resp => {
|
||||
if (handleProcessCommunication(resp, subprocess)) return;
|
||||
// @ts-ignore
|
||||
|
||||
@@ -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,13 @@ module.exports = {
|
||||
existing.structure = structure;
|
||||
socket.emitChanged(`database-structure-changed-${conid}-${database}`);
|
||||
},
|
||||
handle_version(conid, database, { version }) {
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
if (!existing) return;
|
||||
existing.serverVersion = version;
|
||||
socket.emitChanged(`database-server-version-changed-${conid}-${database}`);
|
||||
},
|
||||
|
||||
handle_error(conid, database, props) {
|
||||
const { error } = props;
|
||||
console.log(`Error in database connection ${conid}, database ${database}: ${error}`);
|
||||
@@ -40,13 +48,18 @@ module.exports = {
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
if (existing) return existing;
|
||||
const connection = await connections.get({ conid });
|
||||
const subprocess = fork(process.argv[1], ['databaseConnectionProcess', ...process.argv.slice(3)]);
|
||||
const subprocess = fork(process.argv[1], [
|
||||
'--start-process',
|
||||
'databaseConnectionProcess',
|
||||
...process.argv.slice(3),
|
||||
]);
|
||||
const lastClosed = this.closed[`${conid}/${database}`];
|
||||
const newOpened = {
|
||||
conid,
|
||||
database,
|
||||
subprocess,
|
||||
structure: lastClosed ? lastClosed.structure : DatabaseAnalyser.createEmptyStructure(),
|
||||
serverVersion: lastClosed ? lastClosed.serverVersion : null,
|
||||
connection,
|
||||
status: { name: 'pending' },
|
||||
};
|
||||
@@ -67,6 +80,7 @@ module.exports = {
|
||||
msgtype: 'connect',
|
||||
connection: { ...connection, database },
|
||||
structure: lastClosed ? lastClosed.structure : null,
|
||||
globalSettings: config.settingsValue,
|
||||
});
|
||||
return newOpened;
|
||||
},
|
||||
@@ -127,7 +141,7 @@ module.exports = {
|
||||
} else {
|
||||
existing = await this.ensureOpened(conid, database);
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
connectionStatus: existing ? existing.status : null,
|
||||
@@ -135,8 +149,8 @@ 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' };
|
||||
@@ -159,6 +173,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);
|
||||
@@ -171,6 +191,12 @@ module.exports = {
|
||||
// };
|
||||
},
|
||||
|
||||
serverVersion_meta: 'get',
|
||||
async serverVersion({ conid, database }) {
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
return opened.serverVersion;
|
||||
},
|
||||
|
||||
sqlPreview_meta: 'post',
|
||||
async sqlPreview({ conid, database, objects, options }) {
|
||||
// wait for structure
|
||||
|
||||
@@ -64,19 +64,23 @@ module.exports = {
|
||||
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 {
|
||||
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;
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ const uuidv1 = require('uuid/v1');
|
||||
const byline = require('byline');
|
||||
const socket = require('../utility/socket');
|
||||
const { fork } = require('child_process');
|
||||
const { rundir, uploadsdir, pluginsdir } = require('../utility/directories');
|
||||
const { rundir, uploadsdir, pluginsdir, getPluginBackendPath, packagedPluginList } = require('../utility/directories');
|
||||
const { extractShellApiPlugins, extractShellApiFunctionName } = require('dbgate-tools');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
|
||||
@@ -92,7 +92,7 @@ module.exports = {
|
||||
const scriptFile = path.join(uploadsdir(), runid + '.js');
|
||||
fs.writeFileSync(`${scriptFile}`, scriptText);
|
||||
fs.mkdirSync(directory);
|
||||
const pluginNames = fs.readdirSync(pluginsdir());
|
||||
const pluginNames = _.union(fs.readdirSync(pluginsdir()), packagedPluginList);
|
||||
console.log(`RUNNING SCRIPT ${scriptFile}`);
|
||||
// const subprocess = fork(scriptFile, ['--checkParent', '--max-old-space-size=8192'], {
|
||||
const subprocess = fork(scriptFile, ['--checkParent', ...process.argv.slice(3)], {
|
||||
@@ -101,7 +101,7 @@ module.exports = {
|
||||
env: {
|
||||
...process.env,
|
||||
DBGATE_API: global['dbgateApiModulePath'] || process.argv[1],
|
||||
..._.fromPairs(pluginNames.map(name => [`PLUGIN_${_.camelCase(name)}`, path.join(pluginsdir(), name)])),
|
||||
..._.fromPairs(pluginNames.map(name => [`PLUGIN_${_.camelCase(name)}`, getPluginBackendPath(name)])),
|
||||
},
|
||||
});
|
||||
const pipeDispatcher = severity => data =>
|
||||
|
||||
@@ -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: [],
|
||||
@@ -17,6 +18,12 @@ module.exports = {
|
||||
existing.databases = databases;
|
||||
socket.emitChanged(`database-list-changed-${conid}`);
|
||||
},
|
||||
handle_version(conid, { version }) {
|
||||
const existing = this.opened.find(x => x.conid == conid);
|
||||
if (!existing) return;
|
||||
existing.version = version;
|
||||
socket.emitChanged(`server-version-changed-${conid}`);
|
||||
},
|
||||
handle_status(conid, { status }) {
|
||||
const existing = this.opened.find(x => x.conid == conid);
|
||||
if (!existing) return;
|
||||
@@ -30,7 +37,11 @@ module.exports = {
|
||||
const existing = this.opened.find(x => x.conid == conid);
|
||||
if (existing) return existing;
|
||||
const connection = await connections.get({ conid });
|
||||
const subprocess = fork(process.argv[1], ['serverConnectionProcess', ...process.argv.slice(3)]);
|
||||
const subprocess = fork(process.argv[1], [
|
||||
'--start-process',
|
||||
'serverConnectionProcess',
|
||||
...process.argv.slice(3),
|
||||
]);
|
||||
const newOpened = {
|
||||
conid,
|
||||
subprocess,
|
||||
@@ -55,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;
|
||||
@@ -75,12 +86,24 @@ 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);
|
||||
return opened.databases;
|
||||
},
|
||||
|
||||
version_meta: 'get',
|
||||
async version({ conid }) {
|
||||
const opened = await this.ensureOpened(conid);
|
||||
return opened.version;
|
||||
},
|
||||
|
||||
serverStatus_meta: 'get',
|
||||
async serverStatus() {
|
||||
return {
|
||||
@@ -106,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' };
|
||||
|
||||
@@ -65,7 +65,7 @@ module.exports = {
|
||||
async create({ conid, database }) {
|
||||
const sesid = uuidv1();
|
||||
const connection = await connections.get({ conid });
|
||||
const subprocess = fork(process.argv[1], ['sessionProcess', ...process.argv.slice(3)]);
|
||||
const subprocess = fork(process.argv[1], ['--start-process', 'sessionProcess', ...process.argv.slice(3)]);
|
||||
const newOpened = {
|
||||
conid,
|
||||
database,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
module.exports = {
|
||||
version: '4.0.0',
|
||||
buildTime: '2021-04-01T10:48:22.253Z'
|
||||
version: '4.1.1',
|
||||
buildTime: '2021-04-17T07:22:49.702Z'
|
||||
};
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
const shell = require('./shell');
|
||||
const processArgs = require('./utility/processArgs');
|
||||
|
||||
const argument = process.argv[2];
|
||||
if (argument && argument.endsWith('Process')) {
|
||||
if (processArgs.startProcess) {
|
||||
const proc = require('./proc');
|
||||
|
||||
const module = proc[argument];
|
||||
const module = proc[processArgs.startProcess];
|
||||
module.start();
|
||||
} else if (!module['parent'] && !process.argv.includes('--checkParent')) {
|
||||
} else if (!module['parent'] && !processArgs.checkParent) {
|
||||
const main = require('./main');
|
||||
|
||||
main.start(argument);
|
||||
main.start();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -6,9 +6,10 @@ const http = require('http');
|
||||
const cors = require('cors');
|
||||
const io = require('socket.io');
|
||||
const fs = require('fs');
|
||||
const findFreePort = require('find-free-port');
|
||||
const getPort = require('get-port');
|
||||
const childProcessChecker = require('./utility/childProcessChecker');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const useController = require('./utility/useController');
|
||||
const socket = require('./utility/socket');
|
||||
@@ -28,8 +29,14 @@ const files = require('./controllers/files');
|
||||
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');
|
||||
|
||||
function start(argument = null) {
|
||||
let authorization = null;
|
||||
let checkLocalhostOrigin = null;
|
||||
|
||||
function start() {
|
||||
// console.log('process.argv', process.argv);
|
||||
|
||||
const app = express();
|
||||
@@ -49,6 +56,29 @@ function start(argument = null) {
|
||||
);
|
||||
}
|
||||
|
||||
app.use(function (req, res, next) {
|
||||
if (authorization && !timingSafeCheckToken(req.headers.authorization, authorization)) {
|
||||
return res.status(403).json({ error: 'Not authorized!' });
|
||||
}
|
||||
if (checkLocalhostOrigin) {
|
||||
if (
|
||||
req.headers.origin &&
|
||||
req.headers.origin != checkLocalhostOrigin &&
|
||||
req.headers.origin != `http://${checkLocalhostOrigin}`
|
||||
) {
|
||||
console.log('API origin check FAILED');
|
||||
console.log('HEADERS', { ...req.headers, authorization: '***' });
|
||||
return res.status(403).json({ error: 'Not authorized!' });
|
||||
}
|
||||
if (!req.headers.origin && req.headers.host != checkLocalhostOrigin) {
|
||||
console.log('API host check FAILED');
|
||||
console.log('HEADERS', { ...req.headers, authorization: '***' });
|
||||
return res.status(403).json({ error: 'Not authorized!' });
|
||||
}
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(cors());
|
||||
app.use(bodyParser.json({ limit: '50mb' }));
|
||||
|
||||
@@ -79,29 +109,32 @@ function start(argument = null) {
|
||||
|
||||
app.use('/runners/data', express.static(rundir()));
|
||||
|
||||
if (fs.existsSync('/home/dbgate-docker/public')) {
|
||||
if (platformInfo.isDocker) {
|
||||
// server static files inside docker container
|
||||
app.use(express.static('/home/dbgate-docker/public'));
|
||||
} else {
|
||||
if (argument != 'startNodeWeb') {
|
||||
if (!platformInfo.isNpmDist) {
|
||||
app.get('/', (req, res) => {
|
||||
res.send('DbGate API');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (argument == '--dynport') {
|
||||
if (processArgs.dynport) {
|
||||
childProcessChecker();
|
||||
|
||||
findFreePort(53911, function (err, port) {
|
||||
authorization = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
getPort().then(port => {
|
||||
checkLocalhostOrigin = `localhost:${port}`;
|
||||
server.listen(port, () => {
|
||||
console.log(`DbGate API listening on port ${port}`);
|
||||
process.send({ msgtype: 'listening', port });
|
||||
process.send({ msgtype: 'listening', port, authorization });
|
||||
});
|
||||
});
|
||||
} else if (argument == 'startNodeWeb') {
|
||||
} else if (platformInfo.isNpmDist) {
|
||||
app.use(express.static(path.join(__dirname, '../../dbgate-web/public')));
|
||||
findFreePort(5000, function (err, port) {
|
||||
getPort({ port: 5000 }).then(port => {
|
||||
server.listen(port, () => {
|
||||
console.log(`DbGate API listening on port ${port}`);
|
||||
});
|
||||
|
||||
@@ -2,6 +2,25 @@ const childProcessChecker = require('../utility/childProcessChecker');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const connectUtility = require('../utility/connectUtility');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
const _ = require('lodash');
|
||||
|
||||
function pickSafeConnectionInfo(connection) {
|
||||
return _.mapValues(connection, (v, k) => {
|
||||
if (k == 'engine' || k == 'port' || k == 'authType' || k == 'sshMode' || k == 'passwordMode') return v;
|
||||
if (v === null || v === true || v === false) return v;
|
||||
if (v) return '***';
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
const formatErrorDetail = (e, connection) => `${e.stack}
|
||||
|
||||
Error JSON: ${JSON.stringify(e, undefined, 2)}
|
||||
|
||||
Connection: ${JSON.stringify(pickSafeConnectionInfo(connection), undefined, 2)}
|
||||
|
||||
Platform: ${process.platform}
|
||||
`;
|
||||
|
||||
function start() {
|
||||
childProcessChecker();
|
||||
@@ -14,7 +33,11 @@ function start() {
|
||||
process.send({ msgtype: 'connected', ...res });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.send({ msgtype: 'error', error: e.message });
|
||||
process.send({
|
||||
msgtype: 'error',
|
||||
error: e.message,
|
||||
detail: formatErrorDetail(e, connection),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
@@ -29,6 +30,7 @@ async function checkedAsyncCall(promise) {
|
||||
|
||||
async function handleFullRefresh() {
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
setStatusName('loadStructure');
|
||||
analysedStructure = await checkedAsyncCall(driver.analyseFull(systemConnection));
|
||||
process.send({ msgtype: 'structure', structure: analysedStructure });
|
||||
setStatusName('ok');
|
||||
@@ -36,6 +38,7 @@ async function handleFullRefresh() {
|
||||
|
||||
async function handleIncrementalRefresh() {
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
setStatusName('checkStructure');
|
||||
const newStructure = await checkedAsyncCall(driver.analyseIncremental(systemConnection, analysedStructure));
|
||||
if (newStructure != null) {
|
||||
analysedStructure = newStructure;
|
||||
@@ -56,20 +59,34 @@ function setStatusName(name) {
|
||||
setStatus({ name });
|
||||
}
|
||||
|
||||
async function handleConnect({ connection, structure }) {
|
||||
async function readVersion() {
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
const version = await driver.getVersion(systemConnection);
|
||||
process.send({ msgtype: 'version', version });
|
||||
}
|
||||
|
||||
async function handleConnect({ connection, structure, globalSettings }) {
|
||||
storedConnection = connection;
|
||||
lastPing = new Date().getTime();
|
||||
|
||||
if (!structure) setStatusName('pending');
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
systemConnection = await checkedAsyncCall(connectUtility(driver, storedConnection));
|
||||
readVersion();
|
||||
if (structure) {
|
||||
analysedStructure = structure;
|
||||
handleIncrementalRefresh();
|
||||
} else {
|
||||
handleFullRefresh();
|
||||
}
|
||||
setInterval(handleIncrementalRefresh, 30 * 1000);
|
||||
|
||||
if (extractBoolSettingsValue(globalSettings, 'connection.autoRefresh', true)) {
|
||||
setInterval(
|
||||
handleIncrementalRefresh,
|
||||
extractIntSettingsValue(globalSettings, 'connection.autoRefreshInterval', 30, 3, 3600) * 1000
|
||||
);
|
||||
}
|
||||
|
||||
for (const [resolve] of afterConnectCallbacks) {
|
||||
resolve();
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
@@ -31,6 +32,12 @@ async function handleRefresh() {
|
||||
}
|
||||
}
|
||||
|
||||
async function readVersion() {
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
const version = await driver.getVersion(systemConnection);
|
||||
process.send({ msgtype: 'version', version });
|
||||
}
|
||||
|
||||
function setStatus(status) {
|
||||
const statusString = stableStringify(status);
|
||||
if (lastStatus != statusString) {
|
||||
@@ -45,14 +52,18 @@ function setStatusName(name) {
|
||||
|
||||
async function handleConnect(connection) {
|
||||
storedConnection = connection;
|
||||
const { globalSettings } = storedConnection;
|
||||
setStatusName('pending');
|
||||
lastPing = new Date().getTime();
|
||||
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
try {
|
||||
systemConnection = await connectUtility(driver, storedConnection);
|
||||
readVersion();
|
||||
handleRefresh();
|
||||
setInterval(handleRefresh, 30 * 1000);
|
||||
if (extractBoolSettingsValue(globalSettings, 'connection.autoRefresh', true)) {
|
||||
setInterval(handleRefresh, extractIntSettingsValue(globalSettings, 'connection.autoRefreshInterval', 30, 5, 3600) * 1000);
|
||||
}
|
||||
} catch (err) {
|
||||
setStatus({
|
||||
name: 'error',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { pluginsdir, packagedPluginsDir } = require('../utility/directories');
|
||||
const { pluginsdir, packagedPluginsDir, getPluginBackendPath } = require('../utility/directories');
|
||||
const nativeModules = require('../nativeModules');
|
||||
const _isRunOnSource = require('../utility/_isRunOnSource');
|
||||
const platformInfo = require('../utility/platformInfo');
|
||||
|
||||
const loadedPlugins = {};
|
||||
|
||||
@@ -10,26 +10,13 @@ const dbgateEnv = {
|
||||
dbgateApi: null,
|
||||
nativeModules,
|
||||
};
|
||||
|
||||
function getModulePath(packageName) {
|
||||
const packagedModulePath = _isRunOnSource()
|
||||
? path.join(packagedPluginsDir(), packageName, 'src', 'backend', 'index.js')
|
||||
: path.join(packagedPluginsDir(), packageName, 'dist', 'backend.js');
|
||||
|
||||
if (fs.existsSync(packagedModulePath)) {
|
||||
return packagedModulePath;
|
||||
}
|
||||
|
||||
return path.join(pluginsdir(), packageName, 'dist', 'backend.js');
|
||||
}
|
||||
|
||||
function requirePlugin(packageName, requiredPlugin = null) {
|
||||
if (!packageName) throw new Error('Missing packageName in plugin');
|
||||
if (loadedPlugins[packageName]) return loadedPlugins[packageName];
|
||||
|
||||
if (requiredPlugin == null) {
|
||||
let module;
|
||||
const modulePath = getModulePath(packageName);
|
||||
const modulePath = getPluginBackendPath(packageName);
|
||||
console.log(`Loading module ${packageName} from ${modulePath}`);
|
||||
try {
|
||||
// @ts-ignore
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const childProcessChecker = require('../utility/childProcessChecker');
|
||||
const processArgs = require('../utility/processArgs');
|
||||
|
||||
async function runScript(func) {
|
||||
if (process.argv.includes('--checkParent')) {
|
||||
if (processArgs.checkParent) {
|
||||
childProcessChecker();
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -29,7 +29,7 @@ class DatastoreProxy {
|
||||
|
||||
async ensureSubprocess() {
|
||||
if (!this.subprocess) {
|
||||
this.subprocess = fork(process.argv[1], ['jslDatastoreProcess', ...process.argv.slice(3)]);
|
||||
this.subprocess = fork(process.argv[1], ['--start-process', 'jslDatastoreProcess', ...process.argv.slice(3)]);
|
||||
|
||||
this.subprocess.on('message', message => {
|
||||
// @ts-ignore
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
function _isRunOnSource() {
|
||||
return __filename.endsWith('_isRunOnSource.js');
|
||||
}
|
||||
|
||||
module.exports = _isRunOnSource;
|
||||
@@ -7,6 +7,7 @@ const { getSshTunnelProxy } = require('./sshTunnelProxy');
|
||||
|
||||
async function connectUtility(driver, storedConnection) {
|
||||
const connection = {
|
||||
database: storedConnection.defaultDatabase,
|
||||
...decryptConnection(storedConnection),
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ const os = require('os');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const cleanDirectory = require('./cleanDirectory');
|
||||
const _isRunOnSource = require('./_isRunOnSource');
|
||||
const platformInfo = require('./platformInfo');
|
||||
|
||||
const createDirectories = {};
|
||||
@@ -42,17 +41,34 @@ const archivedir = dirFunc('archive');
|
||||
const filesdir = dirFunc('files');
|
||||
|
||||
function packagedPluginsDir() {
|
||||
if (_isRunOnSource()) {
|
||||
if (platformInfo.isDevMode) {
|
||||
return path.resolve(__dirname, '../../../../plugins');
|
||||
}
|
||||
if (platformInfo.isDocker) {
|
||||
return path.resolve(__dirname, 'plugins');
|
||||
return '/home/dbgate-docker/plugins';
|
||||
}
|
||||
if (process.argv[2] == 'startNodeWeb') {
|
||||
if (platformInfo.isNpmDist) {
|
||||
// node_modules
|
||||
return path.resolve(__dirname, '../../..');
|
||||
return global['dbgateApiPackagedPluginsPath'];
|
||||
}
|
||||
return path.resolve(__dirname, '../../plugins');
|
||||
if (platformInfo.isElectronBundle) {
|
||||
return path.resolve(__dirname, '../../plugins');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const packagedPluginList =
|
||||
packagedPluginsDir() != null ? fs.readdirSync(packagedPluginsDir()).filter(x => x.startsWith('dbgate-plugin-')) : [];
|
||||
|
||||
function getPluginBackendPath(packageName) {
|
||||
if (packagedPluginList.includes(packageName)) {
|
||||
if (platformInfo.isDevMode) {
|
||||
return path.join(packagedPluginsDir(), packageName, 'src', 'backend', 'index.js');
|
||||
}
|
||||
return path.join(packagedPluginsDir(), packageName, 'dist', 'backend.js');
|
||||
}
|
||||
|
||||
return path.join(pluginsdir(), packageName, 'dist', 'backend.js');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@@ -65,4 +81,6 @@ module.exports = {
|
||||
pluginsdir,
|
||||
filesdir,
|
||||
packagedPluginsDir,
|
||||
packagedPluginList,
|
||||
getPluginBackendPath,
|
||||
};
|
||||
|
||||
@@ -1,26 +1,42 @@
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const processArgs = require('./processArgs');
|
||||
|
||||
const p = process;
|
||||
const platform = p.env.OS_OVERRIDE ? p.env.OS_OVERRIDE : p.platform;
|
||||
const platform = process.env.OS_OVERRIDE ? process.env.OS_OVERRIDE : process.platform;
|
||||
const isWindows = platform === 'win32';
|
||||
const isMac = platform === 'darwin';
|
||||
const isLinux = platform === 'linux';
|
||||
const isDocker = fs.existsSync('/home/dbgate-docker/build');
|
||||
const isDocker = fs.existsSync('/home/dbgate-docker/public');
|
||||
const isDevMode = process.env.DEVMODE == '1';
|
||||
const isNpmDist = !!global['dbgateApiModulePath'];
|
||||
|
||||
// function moduleAvailable(name) {
|
||||
// try {
|
||||
// require.resolve(name);
|
||||
// return true;
|
||||
// } catch (e) {
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
|
||||
const isElectronBundle = processArgs.isElectronBundle;
|
||||
|
||||
const platformInfo = {
|
||||
isWindows,
|
||||
isMac,
|
||||
isLinux,
|
||||
isDocker,
|
||||
isSnap: p.env.ELECTRON_SNAP == 'true',
|
||||
isPortable: isWindows && p.env.PORTABLE_EXECUTABLE_DIR,
|
||||
isAppImage: p.env.DESKTOPINTEGRATION === 'AppImageLauncher',
|
||||
sshAuthSock: p.env.SSH_AUTH_SOCK,
|
||||
isElectronBundle,
|
||||
isDevMode,
|
||||
isNpmDist,
|
||||
isSnap: process.env.ELECTRON_SNAP == 'true',
|
||||
isPortable: isWindows && process.env.PORTABLE_EXECUTABLE_DIR,
|
||||
isAppImage: process.env.DESKTOPINTEGRATION === 'AppImageLauncher',
|
||||
sshAuthSock: process.env.SSH_AUTH_SOCK,
|
||||
environment: process.env.NODE_ENV,
|
||||
platform,
|
||||
runningInWebpack: !!p.env.WEBPACK_DEV_SERVER_URL,
|
||||
runningInWebpack: !!process.env.WEBPACK_DEV_SERVER_URL,
|
||||
defaultKeyFile: path.join(os.homedir(), '.ssh/id_rsa'),
|
||||
};
|
||||
|
||||
|
||||
21
packages/api/src/utility/processArgs.js
Normal file
21
packages/api/src/utility/processArgs.js
Normal file
@@ -0,0 +1,21 @@
|
||||
function getNamedArg(name) {
|
||||
const argIndex = process.argv.indexOf(name);
|
||||
if (argIndex > 0) {
|
||||
return process.argv[argIndex + 1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const checkParent = process.argv.includes('--checkParent');
|
||||
const dynport = process.argv.includes('--dynport');
|
||||
const nativeModules = getNamedArg('--native-modules');
|
||||
const startProcess = getNamedArg('--start-process');
|
||||
const isElectronBundle = process.argv.includes('--is-electron-bundle');
|
||||
|
||||
module.exports = {
|
||||
checkParent,
|
||||
nativeModules,
|
||||
startProcess,
|
||||
dynport,
|
||||
isElectronBundle,
|
||||
};
|
||||
@@ -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 = {
|
||||
|
||||
9
packages/api/src/utility/timingSafeCheckToken.js
Normal file
9
packages/api/src/utility/timingSafeCheckToken.js
Normal file
@@ -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;
|
||||
@@ -1,10 +1,9 @@
|
||||
{
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.1",
|
||||
"name": "dbgate-datalib",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
"scripts": {
|
||||
"prepare": "yarn build",
|
||||
"build": "tsc",
|
||||
"start": "tsc --watch"
|
||||
},
|
||||
@@ -12,11 +11,11 @@
|
||||
"lib"
|
||||
],
|
||||
"dependencies": {
|
||||
"dbgate-sqltree": "^4.0.0",
|
||||
"dbgate-filterparser": "^4.0.0"
|
||||
"dbgate-sqltree": "^4.1.1",
|
||||
"dbgate-filterparser": "^4.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dbgate-types": "^4.0.0",
|
||||
"dbgate-types": "^4.1.1",
|
||||
"@types/node": "^13.7.0",
|
||||
"typescript": "^3.7.5"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import _ from 'lodash';
|
||||
import { GridConfig, GridCache, GridConfigColumns, createGridCache, GroupFunc } from './GridConfig';
|
||||
import { ForeignKeyInfo, TableInfo, ColumnInfo, EngineDriver, NamedObjectInfo, DatabaseInfo } from 'dbgate-types';
|
||||
import {
|
||||
ForeignKeyInfo,
|
||||
TableInfo,
|
||||
ColumnInfo,
|
||||
EngineDriver,
|
||||
NamedObjectInfo,
|
||||
DatabaseInfo,
|
||||
SqlDialect,
|
||||
} from 'dbgate-types';
|
||||
import { parseFilter, getFilterType, getFilterValueExpression } from 'dbgate-filterparser';
|
||||
import { filterName } from './filterName';
|
||||
import { ChangeSetFieldDefinition, ChangeSetRowDefinition } from './ChangeSet';
|
||||
@@ -12,6 +20,7 @@ export class FormViewDisplay {
|
||||
isLoadedCorrectly = true;
|
||||
columns: DisplayColumn[];
|
||||
public baseTable: TableInfo;
|
||||
dialect: SqlDialect;
|
||||
|
||||
constructor(
|
||||
public config: GridConfig,
|
||||
@@ -19,8 +28,11 @@ export class FormViewDisplay {
|
||||
public cache: GridCache,
|
||||
protected setCache: ChangeCacheFunc,
|
||||
public driver?: EngineDriver,
|
||||
public dbinfo: DatabaseInfo = null
|
||||
) {}
|
||||
public dbinfo: DatabaseInfo = null,
|
||||
public serverVersion = null
|
||||
) {
|
||||
this.dialect = (driver?.dialectByVersion && driver?.dialectByVersion(serverVersion)) || driver?.dialect;
|
||||
}
|
||||
|
||||
addFilterColumn(column) {
|
||||
if (!column) return;
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
NamedObjectInfo,
|
||||
DatabaseInfo,
|
||||
CollectionInfo,
|
||||
SqlDialect,
|
||||
} from 'dbgate-types';
|
||||
import { parseFilter, getFilterType } from 'dbgate-filterparser';
|
||||
import { filterName } from './filterName';
|
||||
@@ -60,8 +61,12 @@ export abstract class GridDisplay {
|
||||
public cache: GridCache,
|
||||
protected setCache: ChangeCacheFunc,
|
||||
public driver?: EngineDriver,
|
||||
public dbinfo: DatabaseInfo = null
|
||||
) {}
|
||||
public dbinfo: DatabaseInfo = null,
|
||||
public serverVersion = null
|
||||
) {
|
||||
this.dialect = (driver?.dialectByVersion && driver?.dialectByVersion(serverVersion)) || driver?.dialect;
|
||||
}
|
||||
dialect: SqlDialect;
|
||||
columns: DisplayColumn[];
|
||||
baseTable?: TableInfo;
|
||||
baseCollection?: CollectionInfo;
|
||||
@@ -460,12 +465,75 @@ export abstract class GridDisplay {
|
||||
return select;
|
||||
}
|
||||
|
||||
getRowNumberOverSelect(select: Select, offset: number, count: number): Select {
|
||||
const innerSelect: Select = {
|
||||
commandType: 'select',
|
||||
from: select.from,
|
||||
where: select.where,
|
||||
columns: [
|
||||
...select.columns,
|
||||
{
|
||||
alias: '_rowNumber',
|
||||
exprType: 'rowNumber',
|
||||
orderBy: select.orderBy
|
||||
? select.orderBy.map(x =>
|
||||
x.exprType != 'column'
|
||||
? x
|
||||
: x.source
|
||||
? x
|
||||
: {
|
||||
...x,
|
||||
source: { alias: 'basetbl' },
|
||||
}
|
||||
)
|
||||
: [
|
||||
{
|
||||
...select.columns[0],
|
||||
direction: 'ASC',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const res: Select = {
|
||||
commandType: 'select',
|
||||
selectAll: true,
|
||||
from: {
|
||||
subQuery: innerSelect,
|
||||
alias: '_RowNumberResult',
|
||||
},
|
||||
where: {
|
||||
conditionType: 'between',
|
||||
expr: {
|
||||
exprType: 'column',
|
||||
columnName: '_RowNumber',
|
||||
source: {
|
||||
alias: '_RowNumberResult',
|
||||
},
|
||||
},
|
||||
left: {
|
||||
exprType: 'value',
|
||||
value: offset + 1,
|
||||
},
|
||||
right: {
|
||||
exprType: 'value',
|
||||
value: offset + count,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
getPageQuery(offset: number, count: number) {
|
||||
if (!this.driver) return null;
|
||||
const select = this.createSelect();
|
||||
let select = this.createSelect();
|
||||
if (!select) return null;
|
||||
if (this.driver.dialect.rangeSelect) select.range = { offset: offset, limit: count };
|
||||
else if (this.driver.dialect.limitSelect) select.topRecords = count;
|
||||
if (this.dialect.rangeSelect) select.range = { offset: offset, limit: count };
|
||||
else if (this.dialect.rowNumberOverPaging && offset > 0)
|
||||
select = this.getRowNumberOverSelect(select, offset, count);
|
||||
else if (this.dialect.limitSelect) select.topRecords = count;
|
||||
const sql = treeToSql(this.driver, select, dumpSqlSelect);
|
||||
return sql;
|
||||
}
|
||||
|
||||
@@ -28,10 +28,22 @@ export class TableFormViewDisplay extends FormViewDisplay {
|
||||
setConfig: ChangeConfigFunc,
|
||||
cache: GridCache,
|
||||
setCache: ChangeCacheFunc,
|
||||
dbinfo: DatabaseInfo
|
||||
dbinfo: DatabaseInfo,
|
||||
displayOptions,
|
||||
serverVersion
|
||||
) {
|
||||
super(config, setConfig, cache, setCache, driver, dbinfo);
|
||||
this.gridDisplay = new TableGridDisplay(tableName, driver, config, setConfig, cache, setCache, dbinfo);
|
||||
super(config, setConfig, cache, setCache, driver, dbinfo, serverVersion);
|
||||
this.gridDisplay = new TableGridDisplay(
|
||||
tableName,
|
||||
driver,
|
||||
config,
|
||||
setConfig,
|
||||
cache,
|
||||
setCache,
|
||||
dbinfo,
|
||||
displayOptions,
|
||||
serverVersion
|
||||
);
|
||||
this.gridDisplay.addAllExpandedColumnsToSelected = true;
|
||||
|
||||
this.isLoadedCorrectly = this.gridDisplay.isLoadedCorrectly && !!this.driver;
|
||||
|
||||
@@ -17,9 +17,11 @@ export class TableGridDisplay extends GridDisplay {
|
||||
setConfig: ChangeConfigFunc,
|
||||
cache: GridCache,
|
||||
setCache: ChangeCacheFunc,
|
||||
dbinfo: DatabaseInfo
|
||||
dbinfo: DatabaseInfo,
|
||||
public displayOptions: any,
|
||||
serverVersion
|
||||
) {
|
||||
super(config, setConfig, cache, setCache, driver, dbinfo);
|
||||
super(config, setConfig, cache, setCache, driver, dbinfo, serverVersion);
|
||||
|
||||
this.table = this.findTable(tableName);
|
||||
if (!this.table) {
|
||||
@@ -168,7 +170,7 @@ export class TableGridDisplay extends GridDisplay {
|
||||
|
||||
processReferences(select: Select, displayedColumnInfo: DisplayedColumnInfo, options) {
|
||||
this.addJoinsFromExpandedColumns(select, this.columns, 'basetbl', displayedColumnInfo);
|
||||
if (!options.isExport) {
|
||||
if (!options.isExport && this.displayOptions.showHintColumns) {
|
||||
this.addHintsToSelect(select);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,10 @@ export class ViewGridDisplay extends GridDisplay {
|
||||
config: GridConfig,
|
||||
setConfig: ChangeConfigFunc,
|
||||
cache: GridCache,
|
||||
setCache: ChangeCacheFunc
|
||||
setCache: ChangeCacheFunc,
|
||||
serverVersion
|
||||
) {
|
||||
super(config, setConfig, cache, setCache, driver);
|
||||
super(config, setConfig, cache, setCache, driver, serverVersion);
|
||||
this.columns = this.getDisplayColumns(view);
|
||||
this.filterable = true;
|
||||
this.sortable = true;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
[](https://github.com/prettier/prettier)
|
||||
[](https://paypal.me/JanProchazkaCz/30eur)
|
||||
[](https://www.npmjs.com/package/dbgate)
|
||||
|
||||
# DbGate - database administration tool
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const path = require('path');
|
||||
|
||||
global.dbgateApiModulePath = path.dirname(path.dirname(require.resolve('dbgate-api')));
|
||||
global.dbgateApiPackagedPluginsPath = path.dirname(global.dbgateApiModulePath);
|
||||
|
||||
const dbgateApi = require('dbgate-api');
|
||||
|
||||
global.dbgateApiModulePath = require.resolve('dbgate-api');
|
||||
|
||||
dbgateApi.getMainModule().start('startNodeWeb');
|
||||
dbgateApi.getMainModule().start();
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
{
|
||||
"name": "dbgate",
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.1",
|
||||
"homepage": "https://dbgate.org/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
|
||||
"description": "Opensource database administration tool - web interface",
|
||||
"author": "Jan Prochazka",
|
||||
"license": "MIT",
|
||||
@@ -19,7 +18,13 @@
|
||||
"web"
|
||||
],
|
||||
"dependencies": {
|
||||
"dbgate-api": "^4.0.0",
|
||||
"dbgate-web": "^4.0.0"
|
||||
"dbgate-api": "^4.1.1",
|
||||
"dbgate-web": "^4.1.1",
|
||||
"dbgate-plugin-csv": "^4.1.1",
|
||||
"dbgate-plugin-excel": "^4.1.1",
|
||||
"dbgate-plugin-mongo": "^4.1.1",
|
||||
"dbgate-plugin-mysql": "^4.1.1",
|
||||
"dbgate-plugin-mssql": "^4.1.1",
|
||||
"dbgate-plugin-postgres": "^4.1.1"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
{
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.1",
|
||||
"name": "dbgate-filterparser",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
"scripts": {
|
||||
"prepare": "yarn build",
|
||||
"build": "tsc",
|
||||
"start": "tsc --watch",
|
||||
"test": "jest"
|
||||
@@ -13,7 +12,7 @@
|
||||
"lib"
|
||||
],
|
||||
"devDependencies": {
|
||||
"dbgate-types": "^4.0.0",
|
||||
"dbgate-types": "^4.1.1",
|
||||
"@types/jest": "^25.1.4",
|
||||
"@types/node": "^13.7.0",
|
||||
"jest": "^24.9.0",
|
||||
@@ -22,7 +21,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/parsimmon": "^1.10.1",
|
||||
"dbgate-tools": "^4.0.0",
|
||||
"dbgate-tools": "^4.1.1",
|
||||
"lodash": "^4.17.15",
|
||||
"moment": "^2.24.0",
|
||||
"parsimmon": "^1.13.0"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.1",
|
||||
"name": "dbgate-sqltree",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
@@ -8,7 +8,6 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
|
||||
"author": "Jan Prochazka",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
@@ -20,7 +19,6 @@
|
||||
"dbgate"
|
||||
],
|
||||
"scripts": {
|
||||
"prepare": "yarn build",
|
||||
"build": "tsc",
|
||||
"start": "tsc --watch"
|
||||
},
|
||||
@@ -29,7 +27,7 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/node": "^13.7.0",
|
||||
"dbgate-types": "^4.0.0",
|
||||
"dbgate-types": "^4.1.1",
|
||||
"typescript": "^3.7.5"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -62,5 +62,12 @@ export function dumpSqlCondition(dmp: SqlDumper, condition: Condition) {
|
||||
dumpSqlSelect(dmp, condition.subQuery);
|
||||
dmp.put(')');
|
||||
break;
|
||||
case 'between':
|
||||
dumpSqlExpression(dmp, condition.expr);
|
||||
dmp.put(' ^between ');
|
||||
dumpSqlExpression(dmp, condition.left);
|
||||
dmp.put(' ^and ');
|
||||
dumpSqlExpression(dmp, condition.right);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,5 +38,14 @@ export function dumpSqlExpression(dmp: SqlDumper, expr: Expression) {
|
||||
case 'transform':
|
||||
dmp.transform(expr.transform, () => dumpSqlExpression(dmp, expr.expr));
|
||||
break;
|
||||
|
||||
case 'rowNumber':
|
||||
dmp.put(" ^row_number() ^over (^order ^by ");
|
||||
dmp.putCollection(', ', expr.orderBy, x => {
|
||||
dumpSqlExpression(dmp, x);
|
||||
dmp.put(' %k', x.direction);
|
||||
});
|
||||
dmp.put(")");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +92,13 @@ export interface NotExistsCondition {
|
||||
subQuery: Select;
|
||||
}
|
||||
|
||||
export interface BetweenCondition {
|
||||
conditionType: 'between';
|
||||
expr: Expression;
|
||||
left: Expression;
|
||||
right: Expression;
|
||||
}
|
||||
|
||||
export type Condition =
|
||||
| BinaryCondition
|
||||
| NotCondition
|
||||
@@ -99,7 +106,8 @@ export type Condition =
|
||||
| CompoudCondition
|
||||
| LikeCondition
|
||||
| ExistsCondition
|
||||
| NotExistsCondition;
|
||||
| NotExistsCondition
|
||||
| BetweenCondition;
|
||||
|
||||
export interface Source {
|
||||
name?: NamedObjectInfo;
|
||||
@@ -153,13 +161,19 @@ export interface TranformExpression {
|
||||
transform: TransformType;
|
||||
}
|
||||
|
||||
export interface RowNumberExpression {
|
||||
exprType: 'rowNumber';
|
||||
orderBy: OrderByExpression[];
|
||||
}
|
||||
|
||||
export type Expression =
|
||||
| ColumnRefExpression
|
||||
| ValueExpression
|
||||
| PlaceholderExpression
|
||||
| RawExpression
|
||||
| CallExpression
|
||||
| TranformExpression;
|
||||
| TranformExpression
|
||||
| RowNumberExpression;
|
||||
export type OrderByExpression = Expression & { direction: 'ASC' | 'DESC' };
|
||||
|
||||
export type ResultField = Expression & { alias?: string };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.1",
|
||||
"name": "dbgate-tools",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
@@ -8,7 +8,6 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
|
||||
"author": "Jan Prochazka",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
@@ -16,7 +15,6 @@
|
||||
"dbgate"
|
||||
],
|
||||
"scripts": {
|
||||
"prepare": "yarn build",
|
||||
"build": "tsc",
|
||||
"start": "tsc --watch",
|
||||
"test": "jest",
|
||||
@@ -27,7 +25,7 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/node": "^13.7.0",
|
||||
"dbgate-types": "^4.0.0",
|
||||
"dbgate-types": "^4.1.1",
|
||||
"jest": "^24.9.0",
|
||||
"ts-jest": "^25.2.1",
|
||||
"typescript": "^3.7.5"
|
||||
@@ -35,4 +33,4 @@
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.15"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,17 @@ 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);
|
||||
// }
|
||||
|
||||
@@ -24,7 +24,10 @@ export const driverBase = {
|
||||
const analyser = new this.analyserClass(pool, this);
|
||||
analyser.singleObjectFilter = { ...name, typeField };
|
||||
const res = await analyser.fullAnalysis();
|
||||
return res.tables[0];
|
||||
if (res[typeField].length == 1) return res[typeField][0];
|
||||
const obj = res[typeField].find(x => x.pureName == name.pureName && x.schemaName == name.schemaName);
|
||||
// console.log('FIND', name, obj);
|
||||
return obj;
|
||||
},
|
||||
analyseSingleTable(pool, name) {
|
||||
return this.analyseSingleObject(pool, name, 'tables');
|
||||
|
||||
@@ -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';
|
||||
|
||||
20
packages/tools/src/settingsExtractors.ts
Normal file
20
packages/tools/src/settingsExtractors.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
1
packages/types/dialect.d.ts
vendored
1
packages/types/dialect.d.ts
vendored
@@ -1,6 +1,7 @@
|
||||
export interface SqlDialect {
|
||||
rangeSelect?: boolean;
|
||||
limitSelect?: boolean;
|
||||
rowNumberOverPaging?: boolean;
|
||||
stringEscapeChar: string;
|
||||
offsetFetchRangeSyntax?: boolean;
|
||||
quoteIdentifier(s: string): string;
|
||||
|
||||
2
packages/types/engines.d.ts
vendored
2
packages/types/engines.d.ts
vendored
@@ -38,6 +38,7 @@ export interface EngineDriver {
|
||||
title: string;
|
||||
defaultPort?: number;
|
||||
supportsDatabaseUrl?: boolean;
|
||||
isFileDatabase?: boolean;
|
||||
databaseUrlPlaceholder?: string;
|
||||
connect({ server, port, user, password, database }): any;
|
||||
query(pool: any, sql: string): Promise<QueryResult>;
|
||||
@@ -61,6 +62,7 @@ export interface EngineDriver {
|
||||
analyseFull(pool: any): Promise<DatabaseInfo>;
|
||||
analyseIncremental(pool: any, structure: DatabaseInfo): Promise<DatabaseInfo>;
|
||||
dialect: SqlDialect;
|
||||
dialectByVersion(version): SqlDialect;
|
||||
createDumper(): SqlDumper;
|
||||
getAuthTypes(): EngineAuthType[];
|
||||
readCollection(pool: any, options: ReadCollectionOptions): Promise<any>;
|
||||
|
||||
1
packages/types/index.d.ts
vendored
1
packages/types/index.d.ts
vendored
@@ -4,6 +4,7 @@ export interface OpenedDatabaseConnection {
|
||||
conid: string;
|
||||
database: string;
|
||||
structure: DatabaseInfo;
|
||||
serverVersion?: any;
|
||||
subprocess: ChildProcess;
|
||||
disconnected?: boolean;
|
||||
status?: {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
{
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.1",
|
||||
"name": "dbgate-types",
|
||||
"homepage": "https://dbgate.org/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
|
||||
"author": "Jan Prochazka",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dbgate-web",
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.1",
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"dev": "cross-env API_URL=http://localhost:3000 rollup -c -w",
|
||||
@@ -22,10 +22,10 @@
|
||||
"ace-builds": "^1.4.8",
|
||||
"chart.js": "^2.9.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"dbgate-datalib": "^4.0.0",
|
||||
"dbgate-sqltree": "^4.0.0",
|
||||
"dbgate-tools": "^4.0.0",
|
||||
"dbgate-types": "^4.0.0",
|
||||
"dbgate-datalib": "^4.1.1",
|
||||
"dbgate-sqltree": "^4.1.1",
|
||||
"dbgate-tools": "^4.1.1",
|
||||
"dbgate-types": "^4.1.1",
|
||||
"file-selector": "^0.2.4",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
"localforage": "^1.9.0",
|
||||
@@ -50,4 +50,4 @@
|
||||
"typescript": "^3.9.3",
|
||||
"uuid": "^3.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
<script lang="ts">
|
||||
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 ErrorHandler from './utility/ErrorHandler.svelte';
|
||||
import { useSettings } from './utility/metadataLoaders';
|
||||
import OpenTabsOnStartup from './utility/OpenTabsOnStartup.svelte';
|
||||
|
||||
const settings = useSettings();
|
||||
</script>
|
||||
|
||||
<DataGridRowHeightMeter />
|
||||
@@ -13,4 +17,9 @@
|
||||
<PluginsProvider />
|
||||
<CommandListener />
|
||||
<OpenTabsOnStartup />
|
||||
<Screen />
|
||||
|
||||
{#if $settings}
|
||||
<Screen />
|
||||
{:else}
|
||||
<LoadingInfo message="Loading settings..." 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,68 +1,4 @@
|
||||
<script context="module">
|
||||
const getContextMenu = (data, $openedConnections) => () => {
|
||||
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]));
|
||||
};
|
||||
const handleEdit = () => {
|
||||
showModal(ConnectionModal, { connection: data });
|
||||
};
|
||||
const handleDelete = () => {
|
||||
showModal(ConfirmModal, {
|
||||
message: `Really delete connection ${data.displayName || data.server}?`,
|
||||
onConfirm: () => axiosInstance.post('connections/delete', data),
|
||||
});
|
||||
};
|
||||
const handleCreateDatabase = () => {
|
||||
showModal(InputTextModal, {
|
||||
header: 'Create database',
|
||||
value: 'newdb',
|
||||
label: 'Database name',
|
||||
onConfirm: name =>
|
||||
axiosInstance.post('server-connections/create-database', {
|
||||
conid: data._id,
|
||||
name,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
return [
|
||||
config.runAsPortal == false && [
|
||||
{
|
||||
text: 'Edit',
|
||||
onClick: handleEdit,
|
||||
},
|
||||
{
|
||||
text: 'Delete',
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
!$openedConnections.includes(data._id) && {
|
||||
text: 'Connect',
|
||||
onClick: handleConnect,
|
||||
},
|
||||
$openedConnections.includes(data._id) &&
|
||||
data.status && {
|
||||
text: 'Refresh',
|
||||
onClick: handleRefresh,
|
||||
},
|
||||
$openedConnections.includes(data._id) && {
|
||||
text: 'Disconnect',
|
||||
onClick: handleDisconnect,
|
||||
},
|
||||
$openedConnections.includes(data._id) && {
|
||||
text: 'Create database',
|
||||
onClick: handleCreateDatabase,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const extractKey = data => data._id;
|
||||
export const createMatcher = ({ displayName, server }) => filter => filterName(filter, displayName, server);
|
||||
</script>
|
||||
@@ -77,6 +13,10 @@
|
||||
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;
|
||||
|
||||
@@ -86,6 +26,125 @@
|
||||
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));
|
||||
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 ${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',
|
||||
value: 'newdb',
|
||||
label: 'Database name',
|
||||
onConfirm: name =>
|
||||
axiosInstance.post('server-connections/create-database', {
|
||||
conid: data._id,
|
||||
name,
|
||||
}),
|
||||
});
|
||||
};
|
||||
const handleNewQuery = () => {
|
||||
const tooltip = `${getConnectionLabel(data)}`;
|
||||
openNewTab({
|
||||
title: 'Query #',
|
||||
icon: 'img sql-file',
|
||||
tooltip,
|
||||
tabComponent: 'QueryTab',
|
||||
props: {
|
||||
conid: data._id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return [
|
||||
config.runAsPortal == false && [
|
||||
{
|
||||
text: 'Edit',
|
||||
onClick: handleEdit,
|
||||
},
|
||||
{
|
||||
text: 'Delete',
|
||||
onClick: handleDelete,
|
||||
},
|
||||
{
|
||||
text: 'Duplicate',
|
||||
onClick: handleDuplicate,
|
||||
},
|
||||
],
|
||||
!data.singleDatabase && [
|
||||
!$openedConnections.includes(data._id) && {
|
||||
text: 'Connect',
|
||||
onClick: handleConnect,
|
||||
},
|
||||
{ onClick: handleNewQuery, text: 'New query' },
|
||||
$openedConnections.includes(data._id) &&
|
||||
data.status && {
|
||||
text: 'Refresh',
|
||||
onClick: handleRefresh,
|
||||
},
|
||||
$openedConnections.includes(data._id) && {
|
||||
text: 'Disconnect',
|
||||
onClick: handleDisconnect,
|
||||
},
|
||||
$openedConnections.includes(data._id) && {
|
||||
text: 'Create database',
|
||||
onClick: handleCreateDatabase,
|
||||
},
|
||||
],
|
||||
data.singleDatabase && [
|
||||
{ divider: true },
|
||||
getDatabaseMenuItems(data, data.defaultDatabase, $extensions, $currentDatabase),
|
||||
],
|
||||
];
|
||||
};
|
||||
|
||||
$: {
|
||||
if ($extensions.drivers.find(x => x.engine == data.engine)) {
|
||||
const match = (data.engine || '').match(/^([^@]*)@/);
|
||||
@@ -114,34 +173,21 @@
|
||||
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.displayName || data.server}
|
||||
icon="img server"
|
||||
isBold={_.get($currentDatabase, 'connection._id') == data._id}
|
||||
title={getConnectionLabel(data)}
|
||||
icon={data.singleDatabase ? 'img database' : 'img server'}
|
||||
isBold={data.singleDatabase
|
||||
? _.get($currentDatabase, 'connection._id') == data._id && _.get($currentDatabase, 'name') == data.defaultDatabase
|
||||
: _.get($currentDatabase, 'connection._id') == data._id}
|
||||
statusIcon={statusIcon || engineStatusIcon}
|
||||
statusTitle={statusTitle || engineStatusTitle}
|
||||
{extInfo}
|
||||
menu={getContextMenu(data, $openedConnections)}
|
||||
on:click={() => ($openedConnections = _.uniq([...$openedConnections, data._id]))}
|
||||
menu={getContextMenu}
|
||||
on:click={handleConnect}
|
||||
on:click
|
||||
on:expand
|
||||
/>
|
||||
|
||||
@@ -1,67 +1,90 @@
|
||||
<script lang="ts" context="module">
|
||||
export const extractKey = props => props.name;
|
||||
</script>
|
||||
const electron = getElectron();
|
||||
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import ImportExportModal from '../modals/ImportExportModal.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import { getDefaultFileFormat } from '../plugins/fileformats';
|
||||
import { currentDatabase, extensions } from '../stores';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import AppObjectCore from './AppObjectCore.svelte';
|
||||
export let data;
|
||||
export function getDatabaseMenuItems(connection, name, $extensions, $currentDatabase) {
|
||||
const handleNewQuery = () => {
|
||||
const tooltip = `${getConnectionLabel(connection)}\n${name}`;
|
||||
openNewTab({
|
||||
title: 'Query #',
|
||||
icon: 'img sql-file',
|
||||
tooltip,
|
||||
tabComponent: 'QueryTab',
|
||||
props: {
|
||||
conid: connection._id,
|
||||
database: name,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleNewQuery = () => {
|
||||
const { connection, name } = data;
|
||||
const tooltip = `${connection.displayName || connection.server}\n${name}`;
|
||||
openNewTab({
|
||||
title: 'Query #',
|
||||
icon: 'img sql-file',
|
||||
tooltip,
|
||||
tabComponent: 'QueryTab',
|
||||
props: {
|
||||
const handleImport = () => {
|
||||
showModal(ImportExportModal, {
|
||||
initialValues: {
|
||||
sourceStorageType: getDefaultFileFormat($extensions).storageType,
|
||||
targetStorageType: 'database',
|
||||
targetConnectionId: connection._id,
|
||||
targetDatabaseName: name,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
showModal(ImportExportModal, {
|
||||
initialValues: {
|
||||
targetStorageType: getDefaultFileFormat($extensions).storageType,
|
||||
sourceStorageType: 'database',
|
||||
sourceConnectionId: connection._id,
|
||||
sourceDatabaseName: name,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSqlGenerator = () => {
|
||||
showModal(SqlGeneratorModal, {
|
||||
conid: connection._id,
|
||||
database: name,
|
||||
},
|
||||
});
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleImport = () => {
|
||||
const { connection, name } = data;
|
||||
const handleDisconnect = () => {
|
||||
if (electron) {
|
||||
axiosInstance.post('database-connections/disconnect', { conid: connection._id, database: name });
|
||||
}
|
||||
currentDatabase.set(null);
|
||||
};
|
||||
|
||||
showModal(ImportExportModal, {
|
||||
initialValues: {
|
||||
sourceStorageType: getDefaultFileFormat($extensions).storageType,
|
||||
targetStorageType: 'database',
|
||||
targetConnectionId: connection._id,
|
||||
targetDatabaseName: name,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
const { connection, name } = data;
|
||||
|
||||
showModal(ImportExportModal, {
|
||||
initialValues: {
|
||||
targetStorageType: getDefaultFileFormat($extensions).storageType,
|
||||
sourceStorageType: 'database',
|
||||
sourceConnectionId: connection._id,
|
||||
sourceDatabaseName: name,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
function createMenu() {
|
||||
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, $currentDatabase);
|
||||
}
|
||||
</script>
|
||||
|
||||
<AppObjectCore
|
||||
{...$$restProps}
|
||||
{data}
|
||||
|
||||
@@ -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,6 +1,19 @@
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
|
||||
export let selection;
|
||||
export let wrap;
|
||||
</script>
|
||||
|
||||
<textarea class="flex1" {wrap} readonly value={selection.map(cell => cell.value).join('\n')} />
|
||||
<textarea
|
||||
class="flex1"
|
||||
{wrap}
|
||||
readonly
|
||||
value={selection
|
||||
.map(cell => {
|
||||
const { value } = cell;
|
||||
if (_.isPlainObject(value) || _.isArray(value)) return JSON.stringify(value, undefined, 2);
|
||||
return cell.value;
|
||||
})
|
||||
.join('\n')}
|
||||
/>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { derived, get } from 'svelte/store';
|
||||
import { ThemeDefinition } from 'dbgate-types';
|
||||
import ConnectionModal from '../modals/ConnectionModal.svelte';
|
||||
import AboutModal from '../modals/AboutModal.svelte';
|
||||
import SettingsModal from '../settings/SettingsModal.svelte';
|
||||
import ImportExportModal from '../modals/ImportExportModal.svelte';
|
||||
import SqlGeneratorModal from '../modals/SqlGeneratorModal.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
@@ -15,6 +16,7 @@ import { openElectronFile } from '../utility/openElectronFile';
|
||||
import { getDefaultFileFormat } from '../plugins/fileformats';
|
||||
import { getCurrentConfig, getCurrentDatabase } from '../stores';
|
||||
import './recentDatabaseSwitch';
|
||||
import hasPermission from '../utility/hasPermission';
|
||||
|
||||
const electron = getElectron();
|
||||
|
||||
@@ -44,7 +46,7 @@ registerCommand({
|
||||
id: 'toolbar.show',
|
||||
category: 'Toolbar',
|
||||
name: 'Show',
|
||||
onClick: () => visibleToolbar.set(1),
|
||||
onClick: () => visibleToolbar.set(true),
|
||||
testEnabled: () => !getVisibleToolbar(),
|
||||
});
|
||||
|
||||
@@ -52,7 +54,7 @@ registerCommand({
|
||||
id: 'toolbar.hide',
|
||||
category: 'Toolbar',
|
||||
name: 'Hide',
|
||||
onClick: () => visibleToolbar.set(0),
|
||||
onClick: () => visibleToolbar.set(false),
|
||||
testEnabled: () => getVisibleToolbar(),
|
||||
});
|
||||
|
||||
@@ -203,19 +205,29 @@ registerCommand({
|
||||
}),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'settings.commands',
|
||||
category: 'Settings',
|
||||
name: 'Keyboard shortcuts',
|
||||
onClick: () => {
|
||||
openNewTab({
|
||||
title: 'Keyboard Shortcuts',
|
||||
icon: 'icon keyboard',
|
||||
tabComponent: 'CommandListTab',
|
||||
props: {},
|
||||
});
|
||||
},
|
||||
});
|
||||
if (hasPermission('settings/change')) {
|
||||
registerCommand({
|
||||
id: 'settings.commands',
|
||||
category: 'Settings',
|
||||
name: 'Keyboard shortcuts',
|
||||
onClick: () => {
|
||||
openNewTab({
|
||||
title: 'Keyboard Shortcuts',
|
||||
icon: 'icon keyboard',
|
||||
tabComponent: 'CommandListTab',
|
||||
props: {},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'settings.show',
|
||||
category: 'Settings',
|
||||
name: 'Change',
|
||||
toolbarName: 'Settings',
|
||||
onClick: () => showModal(SettingsModal),
|
||||
});
|
||||
}
|
||||
|
||||
export function registerFileCommands({
|
||||
idPrefix,
|
||||
@@ -229,6 +241,7 @@ export function registerFileCommands({
|
||||
toggleComment = false,
|
||||
findReplace = false,
|
||||
undoRedo = false,
|
||||
executeAdditionalCondition = null,
|
||||
}) {
|
||||
if (save) {
|
||||
registerCommand({
|
||||
@@ -239,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),
|
||||
});
|
||||
@@ -259,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({
|
||||
@@ -269,6 +287,7 @@ export function registerFileCommands({
|
||||
name: 'Kill',
|
||||
icon: 'icon close',
|
||||
toolbar: true,
|
||||
isRelatedToTab: true,
|
||||
testEnabled: () => getCurrentEditor()?.canKill && getCurrentEditor().canKill(),
|
||||
onClick: () => getCurrentEditor().kill(),
|
||||
});
|
||||
|
||||
26
packages/web/src/datagrid/CollapseButton.svelte
Normal file
26
packages/web/src/datagrid/CollapseButton.svelte
Normal file
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
|
||||
export let collapsed;
|
||||
</script>
|
||||
|
||||
<div on:click|stopPropagation class='collapseButtonMarker'>
|
||||
<FontIcon icon={collapsed ? 'icon triple-right' : 'icon triple-left'} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 4px;
|
||||
color: var(--theme-font-3);
|
||||
background-color: var(--theme-bg-1);
|
||||
border: 1px solid var(--theme-bg-1);
|
||||
}
|
||||
|
||||
div:hover {
|
||||
color: var(--theme-font-hover);
|
||||
border: var(--theme-border);
|
||||
top: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -226,6 +226,7 @@
|
||||
on:paste={handlePaste}
|
||||
class:isError
|
||||
class:isOk
|
||||
placeholder='Filter'
|
||||
/>
|
||||
<DropDownButton icon="icon filter" menu={createMenu} />
|
||||
{#if showResizeSplitter}
|
||||
|
||||
@@ -28,6 +28,15 @@
|
||||
onClick: () => getCurrentEditor().switchToView('table'),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.toggleLeftPanel',
|
||||
category: 'Data grid',
|
||||
name: 'Toggle left panel',
|
||||
keyText: 'Ctrl+L',
|
||||
testEnabled: () => getCurrentEditor() != null,
|
||||
onClick: () => getCurrentEditor().toggleLeftPanel(),
|
||||
});
|
||||
|
||||
function extractMacroValuesForMacro(macroValues, macro) {
|
||||
// return {};
|
||||
if (!macro) return {};
|
||||
@@ -57,6 +66,9 @@
|
||||
import _ from 'lodash';
|
||||
import registerCommand from '../commands/registerCommand';
|
||||
import { registerMenu } from '../utility/contextMenu';
|
||||
import { useSettings } from '../utility/metadataLoaders';
|
||||
import { getCurrentSettings } from '../stores';
|
||||
import { getBoolSettingsValue } from '../settings/settingsTools';
|
||||
|
||||
export let config;
|
||||
export let setConfig;
|
||||
@@ -88,6 +100,7 @@
|
||||
setContext('macroValues', macroValues);
|
||||
|
||||
let managerSize;
|
||||
const collapsedLeftColumnStore = writable(getBoolSettingsValue('dataGrid.hideLeftColumn', false));
|
||||
|
||||
$: isFormView = !!(formDisplay && formDisplay.config && formDisplay.config.isFormView);
|
||||
$: isJsonView = !!config?.isJsonView;
|
||||
@@ -120,22 +133,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
export function toggleLeftPanel() {
|
||||
collapsedLeftColumnStore.update(x => !x);
|
||||
}
|
||||
|
||||
registerMenu(
|
||||
{ command: 'dataGrid.switchToForm', tag: 'switch', hideDisabled: true },
|
||||
{ command: 'dataGrid.switchToTable', tag: 'switch', hideDisabled: true },
|
||||
{ command: 'dataGrid.switchToJson', tag: 'switch', hideDisabled: true }
|
||||
{ command: 'dataGrid.switchToJson', tag: 'switch', hideDisabled: true },
|
||||
{ command: 'dataGrid.toggleLeftPanel', tag: 'switch' }
|
||||
);
|
||||
</script>
|
||||
|
||||
<HorizontalSplitter initialValue="300px" bind:size={managerSize}>
|
||||
<HorizontalSplitter initialValue="300px" bind:size={managerSize} hideFirst={$collapsedLeftColumnStore}>
|
||||
<div class="left" slot="1">
|
||||
<WidgetColumnBar>
|
||||
<WidgetColumnBarItem
|
||||
title="Columns"
|
||||
name="columns"
|
||||
height={showReferences ? '40%' : '60%'}
|
||||
skip={freeTableColumn || isFormView}
|
||||
>
|
||||
<WidgetColumnBarItem title="Columns" name="columns" height="45%" skip={freeTableColumn || isFormView}>
|
||||
<ColumnManager {...$$props} {managerSize} {isJsonView} />
|
||||
</WidgetColumnBarItem>
|
||||
|
||||
@@ -161,7 +174,7 @@
|
||||
<ReferenceManager {...$$props} {managerSize} />
|
||||
</WidgetColumnBarItem>
|
||||
|
||||
<WidgetColumnBarItem title="Macros" name="macros" skip={!showMacros} collapsed={isDetailView}>
|
||||
<WidgetColumnBarItem title="Macros" name="macros" skip={!showMacros} collapsed>
|
||||
<MacroManager {...$$props} {managerSize} />
|
||||
</WidgetColumnBarItem>
|
||||
</WidgetColumnBar>
|
||||
@@ -177,6 +190,7 @@
|
||||
<svelte:component
|
||||
this={gridCoreComponent}
|
||||
{...$$props}
|
||||
{collapsedLeftColumnStore}
|
||||
formViewAvailable={!!formViewComponent && !!formDisplay}
|
||||
macroValues={extractMacroValuesForMacro($macroValues, $selectedMacro)}
|
||||
macroPreview={$selectedMacro}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
export let col;
|
||||
export let rowData;
|
||||
export let colIndex = undefined;
|
||||
export let hintFieldsAllowed = undefined;
|
||||
export let allowHintField = false;
|
||||
|
||||
export let isSelected = false;
|
||||
export let isFrameSelected = false;
|
||||
@@ -100,14 +100,14 @@
|
||||
{:else if value.type == 'Buffer' && _.isArray(value.data)}
|
||||
<span class="null">({value.data.length} bytes)</span>
|
||||
{:else if _.isPlainObject(value)}
|
||||
<span class="null">(JSON)</span>
|
||||
<span class="null" title={JSON.stringify(value, undefined, 2)}>(JSON)</span>
|
||||
{:else if _.isArray(value)}
|
||||
<span class="null">[{value.length} items]</span>
|
||||
{:else}
|
||||
{value.toString()}
|
||||
{/if}
|
||||
|
||||
{#if hintFieldsAllowed && hintFieldsAllowed.includes(col.uniqueName) && rowData && rowData[col.hintColumnName]}
|
||||
{#if allowHintField && rowData && rowData[col.hintColumnName]}
|
||||
<span class="hint">{rowData[col.hintColumnName]}</span>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
@@ -161,6 +164,13 @@
|
||||
if (allRowCount == null) return 'Loading row count...';
|
||||
return `Rows: ${allRowCount.toLocaleString()}`;
|
||||
}
|
||||
|
||||
function getCopiedValue(value) {
|
||||
if (value === null) return '(NULL)';
|
||||
if (value === undefined) return '(NoField)';
|
||||
if (_.isPlainObject(value) || _.isArray(value)) return JSON.stringify(value);
|
||||
return value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -202,6 +212,7 @@
|
||||
import FormStyledButton from '../elements/FormStyledButton.svelte';
|
||||
import { editJsonRowDocument } from '../jsonview/CollectionJsonRow.svelte';
|
||||
import createActivator, { getActiveComponent } from '../utility/createActivator';
|
||||
import CollapseButton from './CollapseButton.svelte';
|
||||
|
||||
export let onLoadNextData = undefined;
|
||||
export let grider = undefined;
|
||||
@@ -223,6 +234,7 @@
|
||||
export let changeSetStore;
|
||||
export let isDynamicStructure = false;
|
||||
export let selectedCellsPublished = () => [];
|
||||
export let collapsedLeftColumnStore;
|
||||
// export let generalAllowSave = false;
|
||||
|
||||
export const activator = createActivator('DataGridCore', false);
|
||||
@@ -327,7 +339,7 @@
|
||||
if (!rowData) return '';
|
||||
const line = colIndexes
|
||||
.map(col => realColumnUniqueNames[col])
|
||||
.map(col => (rowData[col] == null ? '(NULL)' : rowData[col]))
|
||||
.map(col => getCopiedValue(rowData[col]))
|
||||
.join('\t');
|
||||
return line;
|
||||
});
|
||||
@@ -521,6 +533,7 @@
|
||||
rowData,
|
||||
column,
|
||||
value: rowData && rowData[column],
|
||||
engine: display?.driver,
|
||||
};
|
||||
})
|
||||
.filter(x => x.column);
|
||||
@@ -565,6 +578,7 @@
|
||||
function handleGridMouseDown(event) {
|
||||
if (event.target.closest('.buttonLike')) return;
|
||||
if (event.target.closest('.resizeHandleControl')) return;
|
||||
if (event.target.closest('.collapseButtonMarker')) return;
|
||||
if (event.target.closest('input')) return;
|
||||
|
||||
// event.target.closest('table').focus();
|
||||
@@ -991,7 +1005,12 @@
|
||||
data-row="header"
|
||||
data-col="header"
|
||||
style={`width:${headerColWidth}px; min-width:${headerColWidth}px; max-width:${headerColWidth}px`}
|
||||
/>
|
||||
>
|
||||
<CollapseButton
|
||||
collapsed={$collapsedLeftColumnStore}
|
||||
on:click={() => collapsedLeftColumnStore.update(x => !x)}
|
||||
/>
|
||||
</td>
|
||||
{#each visibleRealColumns as col (col.uniqueName)}
|
||||
<td
|
||||
class="header-cell"
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
{rowIndex}
|
||||
{rowData}
|
||||
{col}
|
||||
{hintFieldsAllowed}
|
||||
allowHintField={hintFieldsAllowed?.includes(col.uniqueName)}
|
||||
isSelected={frameSelection ? false : cellIsSelected(rowIndex, col.colIndex, selectedCells)}
|
||||
isFrameSelected={frameSelection ? cellIsSelected(rowIndex, col.colIndex, selectedCells) : false}
|
||||
isAutofillSelected={cellIsSelected(rowIndex, col.colIndex, autofillSelectedCells)}
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
<script lang="ts" context="module">
|
||||
function getEditedValue(value) {
|
||||
if (_.isPlainObject(value) || _.isArray(value)) return JSON.stringify(value);
|
||||
return value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import keycodes from '../utility/keycodes';
|
||||
import { onMount } from 'svelte';
|
||||
import createRef from '../utility/createRef';
|
||||
import _ from 'lodash';
|
||||
|
||||
export let inplaceEditorState;
|
||||
export let dispatchInsplaceEditor;
|
||||
@@ -54,7 +62,7 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
domEditor.value = inplaceEditorState.text || cellValue;
|
||||
domEditor.value = inplaceEditorState.text || getEditedValue(cellValue);
|
||||
domEditor.focus();
|
||||
if (inplaceEditorState.selectAll) {
|
||||
domEditor.select();
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { getIntSettingsValue } from '../settings/settingsTools';
|
||||
|
||||
import createRef from '../utility/createRef';
|
||||
import { useSettings } from '../utility/metadataLoaders';
|
||||
|
||||
import DataGridCore from './DataGridCore.svelte';
|
||||
|
||||
@@ -10,6 +13,7 @@
|
||||
export let display;
|
||||
export let masterLoadedTime = undefined;
|
||||
export let selectedCellsPublished;
|
||||
export let rowCountLoaded = null;
|
||||
|
||||
// export let griderFactory;
|
||||
|
||||
@@ -45,7 +49,11 @@
|
||||
loadedTimeRef.set(loadStart);
|
||||
// console.log('LOAD NEXT ROWS', loadedRows);
|
||||
|
||||
const nextRows = await loadDataPage($$props, loadedRows.length, 100);
|
||||
const nextRows = await loadDataPage(
|
||||
$$props,
|
||||
loadedRows.length,
|
||||
getIntSettingsValue('dataGrid.pageSize', 100, 5, 1000)
|
||||
);
|
||||
if (loadedTimeRef.get() !== loadStart) {
|
||||
// new load was dispatched
|
||||
return;
|
||||
@@ -119,7 +127,7 @@
|
||||
{errorMessage}
|
||||
{grider}
|
||||
{isLoading}
|
||||
{allRowCount}
|
||||
allRowCount={rowCountLoaded || allRowCount}
|
||||
{isLoadedAll}
|
||||
{loadedTime}
|
||||
bind:selectedCellsPublished
|
||||
|
||||
@@ -14,12 +14,18 @@
|
||||
import { extensions } from '../stores';
|
||||
import stableStringify from 'json-stable-stringify';
|
||||
|
||||
import { useConnectionInfo, useDatabaseInfo } from '../utility/metadataLoaders';
|
||||
import {
|
||||
useConnectionInfo,
|
||||
useDatabaseInfo,
|
||||
useDatabaseServerVersion,
|
||||
useServerVersion,
|
||||
} from '../utility/metadataLoaders';
|
||||
|
||||
import DataGrid from './DataGrid.svelte';
|
||||
import ReferenceHeader from './ReferenceHeader.svelte';
|
||||
import SqlDataGridCore from './SqlDataGridCore.svelte';
|
||||
import SqlFormView from '../formview/SqlFormView.svelte';
|
||||
import { getBoolSettingsValue } from '../settings/settingsTools';
|
||||
|
||||
export let conid;
|
||||
export let database;
|
||||
@@ -37,6 +43,9 @@
|
||||
|
||||
$: connection = useConnectionInfo({ conid });
|
||||
$: dbinfo = useDatabaseInfo({ conid, database });
|
||||
$: serverVersion = useDatabaseServerVersion({ conid, database });
|
||||
|
||||
// $: console.log('serverVersion', $serverVersion);
|
||||
|
||||
let myLoadedTime = 0;
|
||||
|
||||
@@ -44,29 +53,35 @@
|
||||
|
||||
// $: console.log('display', display);
|
||||
|
||||
$: display = connection
|
||||
? new TableGridDisplay(
|
||||
{ schemaName, pureName },
|
||||
findEngineDriver($connection, $extensions),
|
||||
config,
|
||||
setConfig,
|
||||
cache,
|
||||
setCache,
|
||||
$dbinfo
|
||||
)
|
||||
: null;
|
||||
$: display =
|
||||
connection && $serverVersion
|
||||
? new TableGridDisplay(
|
||||
{ schemaName, pureName },
|
||||
findEngineDriver($connection, $extensions),
|
||||
config,
|
||||
setConfig,
|
||||
cache,
|
||||
setCache,
|
||||
$dbinfo,
|
||||
{ showHintColumns: getBoolSettingsValue('dataGrid.showHintColumns', true) },
|
||||
$serverVersion
|
||||
)
|
||||
: null;
|
||||
|
||||
$: formDisplay = connection
|
||||
? new TableFormViewDisplay(
|
||||
{ schemaName, pureName },
|
||||
findEngineDriver($connection, $extensions),
|
||||
config,
|
||||
setConfig,
|
||||
cache,
|
||||
setCache,
|
||||
$dbinfo
|
||||
)
|
||||
: null;
|
||||
$: formDisplay =
|
||||
connection && $serverVersion
|
||||
? new TableFormViewDisplay(
|
||||
{ schemaName, pureName },
|
||||
findEngineDriver($connection, $extensions),
|
||||
config,
|
||||
setConfig,
|
||||
cache,
|
||||
setCache,
|
||||
$dbinfo,
|
||||
{ showHintColumns: getBoolSettingsValue('dataGrid.showHintColumns', true) },
|
||||
$serverVersion
|
||||
)
|
||||
: null;
|
||||
|
||||
const setChildConfig = (value, reference = undefined) => {
|
||||
if (_.isFunction(value)) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
export let isSplitter = true;
|
||||
export let initialValue = undefined;
|
||||
export let hideFirst = false;
|
||||
|
||||
export let size = 0;
|
||||
let clientWidth;
|
||||
@@ -24,11 +25,15 @@
|
||||
</script>
|
||||
|
||||
<div class="container" bind:clientWidth>
|
||||
<div class="child1" style={isSplitter ? `width:${size}px; min-width:${size}px; max-width:${size}px}` : `flex:1`}>
|
||||
<slot name="1" />
|
||||
</div>
|
||||
{#if !hideFirst}
|
||||
<div class="child1" style={isSplitter ? `width:${size}px; min-width:${size}px; max-width:${size}px}` : `flex:1`}>
|
||||
<slot name="1" />
|
||||
</div>
|
||||
{/if}
|
||||
{#if isSplitter}
|
||||
<div class="horizontal-split-handle" use:splitterDrag={'clientX'} on:resizeSplitter={e => (size += e.detail)} />
|
||||
{#if !hideFirst}
|
||||
<div class="horizontal-split-handle" use:splitterDrag={'clientX'} on:resizeSplitter={e => (size += e.detail)} />
|
||||
{/if}
|
||||
<div class="child2">
|
||||
<slot name="2" />
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script lang="ts" context="module">
|
||||
<script context="module">
|
||||
function getTableDisplayName(column, tables) {
|
||||
const table = (tables || []).find(x => x.designerId == column.designerId);
|
||||
if (table) return table.alias || table.pureName;
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
<script>
|
||||
import { map } from 'lodash';
|
||||
import DataFilterControl from '../datagrid/DataFilterControl.svelte';
|
||||
import { findDesignerFilterType } from '../designer/designerTools';
|
||||
@@ -41,96 +41,105 @@
|
||||
$: hasGroupedColumn = !!(columns || []).find(x => x.isGrouped);
|
||||
</script>
|
||||
|
||||
<TableControl
|
||||
rows={columns || []}
|
||||
columns={[
|
||||
{ fieldName: 'columnName', header: 'Column/Expression' },
|
||||
{ fieldName: 'tableDisplayName', header: 'Table', formatter: row => getTableDisplayName(row, tables) },
|
||||
{ fieldName: 'isOutput', header: 'Output', slot: 0 },
|
||||
{ fieldName: 'alias', header: 'Alias', slot: 1 },
|
||||
{ fieldName: 'isGrouped', header: 'Group by', slot: 2 },
|
||||
{ fieldName: 'aggregate', header: 'Aggregate', slot: 3 },
|
||||
{ fieldName: 'sortOrder', header: 'Sort order', slot: 4 },
|
||||
{ fieldName: 'filter', header: 'Filter', slot: 5 },
|
||||
hasGroupedColumn && { fieldName: 'groupFilter', header: 'Group filter', slot: 6 },
|
||||
{ fieldName: 'actions', header: '', slot: 7 },
|
||||
]}
|
||||
>
|
||||
<svelte:fragment slot="0" let:row>
|
||||
<CheckboxField
|
||||
checked={row.isOutput}
|
||||
onChange={e => {
|
||||
if (e.target.checked) changeColumn({ ...row, isOutput: true });
|
||||
else changeColumn({ ...row, isOutput: false });
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="1" let:row>
|
||||
<TextField
|
||||
value={row.alias}
|
||||
on:input={e => {
|
||||
changeColumn({ ...row, alias: e.target.value });
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="2" let:row>
|
||||
<CheckboxField
|
||||
checked={row.isGrouped}
|
||||
on:change={e => {
|
||||
if (e.target.checked) changeColumn({ ...row, isGrouped: true });
|
||||
else changeColumn({ ...row, isGrouped: false });
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="3" let:row>
|
||||
{#if !row.isGrouped}
|
||||
<div class="wrapper">
|
||||
<TableControl
|
||||
rows={columns || []}
|
||||
columns={[
|
||||
{ fieldName: 'columnName', header: 'Column/Expression' },
|
||||
{ fieldName: 'tableDisplayName', header: 'Table', formatter: row => getTableDisplayName(row, tables) },
|
||||
{ fieldName: 'isOutput', header: 'Output', slot: 0 },
|
||||
{ fieldName: 'alias', header: 'Alias', slot: 1 },
|
||||
{ fieldName: 'isGrouped', header: 'Group by', slot: 2 },
|
||||
{ fieldName: 'aggregate', header: 'Aggregate', slot: 3 },
|
||||
{ fieldName: 'sortOrder', header: 'Sort order', slot: 4 },
|
||||
{ fieldName: 'filter', header: 'Filter', slot: 5 },
|
||||
hasGroupedColumn && { fieldName: 'groupFilter', header: 'Group filter', slot: 6 },
|
||||
{ fieldName: 'actions', header: '', slot: 7 },
|
||||
]}
|
||||
>
|
||||
<svelte:fragment slot="0" let:row>
|
||||
<CheckboxField
|
||||
checked={row.isOutput}
|
||||
onChange={e => {
|
||||
if (e.target.checked) changeColumn({ ...row, isOutput: true });
|
||||
else changeColumn({ ...row, isOutput: false });
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="1" let:row>
|
||||
<TextField
|
||||
value={row.alias}
|
||||
on:input={e => {
|
||||
changeColumn({ ...row, alias: e.target.value });
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="2" let:row>
|
||||
<CheckboxField
|
||||
checked={row.isGrouped}
|
||||
on:change={e => {
|
||||
if (e.target.checked) changeColumn({ ...row, isGrouped: true });
|
||||
else changeColumn({ ...row, isGrouped: false });
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="3" let:row>
|
||||
{#if !row.isGrouped}
|
||||
<SelectField
|
||||
isNative
|
||||
value={row.aggregate}
|
||||
on:change={e => {
|
||||
changeColumn({ ...row, aggregate: e.detail });
|
||||
}}
|
||||
options={['---', 'MIN', 'MAX', 'COUNT', 'COUNT DISTINCT', 'SUM', 'AVG'].map(x => ({ label: x, value: x }))}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="4" let:row>
|
||||
<SelectField
|
||||
isNative
|
||||
value={row.aggregate}
|
||||
value={row.sortOrder}
|
||||
on:change={e => {
|
||||
changeColumn({ ...row, aggregate: e.detail });
|
||||
changeColumn({ ...row, sortOrder: parseInt(e.detail) });
|
||||
}}
|
||||
options={['---', 'MIN', 'MAX', 'COUNT', 'COUNT DISTINCT', 'SUM', 'AVG'].map(x => ({ label: x, value: x }))}
|
||||
options={[
|
||||
{ label: '---', value: '0' },
|
||||
{ label: '1st, ascending', value: '1' },
|
||||
{ label: '1st, descending', value: '-1' },
|
||||
{ label: '2nd, ascending', value: '2' },
|
||||
{ label: '2nd, descending', value: '-2' },
|
||||
{ label: '3rd, ascending', value: '3' },
|
||||
{ label: '3rd, descending', value: '-3' },
|
||||
]}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="4" let:row>
|
||||
<SelectField
|
||||
isNative
|
||||
value={row.sortOrder}
|
||||
on:change={e => {
|
||||
changeColumn({ ...row, sortOrder: parseInt(e.detail) });
|
||||
}}
|
||||
options={[
|
||||
{ label: '---', value: '0' },
|
||||
{ label: '1st, ascending', value: '1' },
|
||||
{ label: '1st, descending', value: '-1' },
|
||||
{ label: '2nd, ascending', value: '2' },
|
||||
{ label: '2nd, descending', value: '-2' },
|
||||
{ label: '3rd, ascending', value: '3' },
|
||||
{ label: '3rd, descending', value: '-3' },
|
||||
]}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="5" let:row>
|
||||
<DataFilterControl
|
||||
filterType={findDesignerFilterType(row, value)}
|
||||
filter={row.filter}
|
||||
setFilter={filter => {
|
||||
changeColumn({ ...row, filter });
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="6" let:row>
|
||||
<DataFilterControl
|
||||
filterType={findDesignerFilterType(row, value)}
|
||||
filter={row.groupFilter}
|
||||
setFilter={groupFilter => {
|
||||
changeColumn({ ...row, groupFilter });
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="7" let:row>
|
||||
<InlineButton on:click={() => removeColumn(row)}>Remove</InlineButton>
|
||||
</svelte:fragment>
|
||||
</TableControl>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="5" let:row>
|
||||
<DataFilterControl
|
||||
filterType={findDesignerFilterType(row, value)}
|
||||
filter={row.filter}
|
||||
setFilter={filter => {
|
||||
changeColumn({ ...row, filter });
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="6" let:row>
|
||||
<DataFilterControl
|
||||
filterType={findDesignerFilterType(row, value)}
|
||||
filter={row.groupFilter}
|
||||
setFilter={groupFilter => {
|
||||
changeColumn({ ...row, groupFilter });
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="7" let:row>
|
||||
<InlineButton on:click={() => removeColumn(row)}>Remove</InlineButton>
|
||||
</svelte:fragment>
|
||||
</TableControl>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
}
|
||||
</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'),
|
||||
@@ -477,6 +484,7 @@
|
||||
colIndex={chunkIndex * 2 + 1}
|
||||
isSelected={currentCell[0] == rowIndex && currentCell[1] == chunkIndex * 2 + 1}
|
||||
isModifiedCell={rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)}
|
||||
allowHintField={!(rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName))}
|
||||
bind:domCell={domCells[`${rowIndex},${chunkIndex * 2 + 1}`]}
|
||||
onSetFormView={handleSetFormView}
|
||||
hideContent={!rowData ||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
'icon sql-generator': 'mdi mdi-cog-transfer',
|
||||
'icon keyboard': 'mdi mdi-keyboard-settings',
|
||||
'icon settings': 'mdi mdi-cog',
|
||||
'icon version': 'mdi mdi-ticket-confirmation',
|
||||
|
||||
'icon database': 'mdi mdi-database',
|
||||
'icon server': 'mdi mdi-server',
|
||||
@@ -48,6 +49,8 @@
|
||||
'icon arrow-begin': 'mdi mdi-arrow-collapse-left',
|
||||
'icon arrow-end': 'mdi mdi-arrow-collapse-right',
|
||||
'icon arrow-right': 'mdi mdi-arrow-right',
|
||||
'icon triple-left': 'mdi mdi-chevron-triple-left',
|
||||
'icon triple-right': 'mdi mdi-chevron-triple-right',
|
||||
'icon format-code': 'mdi mdi-code-tags-check',
|
||||
'icon show-wizard': 'mdi mdi-comment-edit',
|
||||
'icon disconnected': 'mdi mdi-lan-disconnect',
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
import FormSubmit from '../forms/FormSubmit.svelte';
|
||||
import FormTextField from '../forms/FormTextField.svelte';
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import { customKeyboardShortcuts } from '../stores';
|
||||
import { commandsSettings } from '../stores';
|
||||
import axiosInstance from '../utility/axiosInstance';
|
||||
import KeyboardModal from './KeyboardModal.svelte';
|
||||
import ModalBase from './ModalBase.svelte';
|
||||
import { closeCurrentModal, showModal } from './modalTools';
|
||||
@@ -41,13 +42,15 @@
|
||||
value="OK"
|
||||
on:click={e => {
|
||||
closeCurrentModal();
|
||||
customKeyboardShortcuts.update(list => ({
|
||||
...list,
|
||||
[command.id]: {
|
||||
keyText: e.detail.keyText,
|
||||
customKeyboardShortcut: true,
|
||||
axiosInstance.post('config/update-settings', {
|
||||
commands: {
|
||||
...$commandsSettings,
|
||||
[command.id]: {
|
||||
keyText: e.detail.keyText,
|
||||
customKeyboardShortcut: true,
|
||||
},
|
||||
},
|
||||
}));
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<FormStyledButton
|
||||
@@ -55,7 +58,9 @@
|
||||
value="Reset"
|
||||
on:click={() => {
|
||||
closeCurrentModal();
|
||||
customKeyboardShortcuts.update(list => _.omit(list, [command.id]));
|
||||
axiosInstance.post('config/update-settings', {
|
||||
commands: _.omit($commandsSettings, [command.id]),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<FormStyledButton type="button" value="Close" on:click={closeCurrentModal} />
|
||||
|
||||
@@ -12,14 +12,26 @@
|
||||
import FormFieldTemplateLarge from '../forms/FormFieldTemplateLarge.svelte';
|
||||
|
||||
import ModalBase from './ModalBase.svelte';
|
||||
import { closeCurrentModal, closeModal } from './modalTools';
|
||||
import { closeCurrentModal, closeModal, showModal } from './modalTools';
|
||||
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) {
|
||||
@@ -39,15 +51,22 @@
|
||||
}
|
||||
|
||||
async function handleSubmit(e) {
|
||||
axiosInstance.post('connections/save', e.detail);
|
||||
const connection = driver?.isFileDatabase
|
||||
? {
|
||||
..._.omit(e.detail, ['server', 'port', 'defaultDatabase']),
|
||||
singleDatabase: true,
|
||||
defaultDatabase: getDatabaseFileLabel(e.detail.databaseFile),
|
||||
}
|
||||
: {
|
||||
..._.omit(e.detail, ['databaseFile']),
|
||||
singleDatabase: e.detail.defaultDatabase ? e.detail.singleDatabase : false,
|
||||
};
|
||||
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>
|
||||
|
||||
@@ -58,11 +77,11 @@
|
||||
label: 'Main',
|
||||
component: ConnectionModalDriverFields,
|
||||
},
|
||||
{
|
||||
!driver?.isFileDatabase && {
|
||||
label: 'SSH Tunnel',
|
||||
component: ConnectionModalSshTunnelFields,
|
||||
},
|
||||
{
|
||||
!driver?.isFileDatabase && {
|
||||
label: 'SSL',
|
||||
component: ConnectionModalSslFields,
|
||||
},
|
||||
@@ -89,6 +108,16 @@
|
||||
<div class="error-result">
|
||||
Connect failed: <FontIcon icon="img error" />
|
||||
{sqlConnectResult.error}
|
||||
<Link
|
||||
onClick={() =>
|
||||
showModal(ErrorMessageModal, {
|
||||
message: sqlConnectResult.detail,
|
||||
showAsCode: true,
|
||||
title: 'Database connection error',
|
||||
})}
|
||||
>
|
||||
Show detail
|
||||
</Link>
|
||||
</div>
|
||||
{/if}
|
||||
{#if isTesting}
|
||||
@@ -99,7 +128,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</ModalBase>
|
||||
</FormProvider>
|
||||
</FormProviderCore>
|
||||
|
||||
<style>
|
||||
.buttons {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<script lang="ts">
|
||||
import FormCheckboxField from '../forms/FormCheckboxField.svelte';
|
||||
import FormElectronFileSelector from '../forms/FormElectronFileSelector.svelte';
|
||||
|
||||
import FormPasswordField from '../forms/FormPasswordField.svelte';
|
||||
|
||||
import { getFormContext } from '../forms/FormProviderCore.svelte';
|
||||
@@ -7,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;
|
||||
@@ -17,6 +23,7 @@
|
||||
$: currentAuthType = $authTypes && $authTypes.find(x => x.name == authType);
|
||||
$: disabledFields = (currentAuthType ? currentAuthType.disabledFields : null) || [];
|
||||
$: driver = $extensions.drivers.find(x => x.engine == engine);
|
||||
$: defaultDatabase = $values.defaultDatabase;
|
||||
</script>
|
||||
|
||||
<FormSelectField
|
||||
@@ -24,91 +31,99 @@
|
||||
name="engine"
|
||||
options={[
|
||||
{ label: '(select driver)', value: '' },
|
||||
...$extensions.drivers.map(driver => ({
|
||||
value: driver.engine,
|
||||
label: driver.title,
|
||||
})),
|
||||
...$extensions.drivers
|
||||
.filter(driver => !driver.isFileDatabase || electron)
|
||||
.map(driver => ({
|
||||
value: driver.engine,
|
||||
label: driver.title,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
|
||||
{#if driver?.supportsDatabaseUrl}
|
||||
<div class="radio">
|
||||
<FormRadioGroupField
|
||||
name="useDatabaseUrl"
|
||||
options={[
|
||||
{ label: 'Fill database connection details', value: '', default: true },
|
||||
{ label: 'Use database URL', value: '1' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if driver?.supportsDatabaseUrl && useDatabaseUrl}
|
||||
<FormTextField
|
||||
label="Database URL"
|
||||
name="databaseUrl"
|
||||
placeholder={driver?.databaseUrlPlaceholder}
|
||||
/>
|
||||
{#if driver?.isFileDatabase}
|
||||
<FormElectronFileSelector label="Database file" name="databaseFile" disabled={!electron} />
|
||||
{:else}
|
||||
{#if $authTypes}
|
||||
<FormSelectField
|
||||
label="Authentication"
|
||||
name="authType"
|
||||
options={$authTypes.map(auth => ({
|
||||
value: auth.name,
|
||||
label: auth.title,
|
||||
}))}
|
||||
/>
|
||||
{#if driver?.supportsDatabaseUrl}
|
||||
<div class="radio">
|
||||
<FormRadioGroupField
|
||||
name="useDatabaseUrl"
|
||||
options={[
|
||||
{ label: 'Fill database connection details', value: '', default: true },
|
||||
{ label: 'Use database URL', value: '1' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-9 mr-1">
|
||||
<FormTextField
|
||||
label="Server"
|
||||
name="server"
|
||||
disabled={disabledFields.includes('server')}
|
||||
templateProps={{ noMargin: true }}
|
||||
{#if driver?.supportsDatabaseUrl && useDatabaseUrl}
|
||||
<FormTextField label="Database URL" name="databaseUrl" placeholder={driver?.databaseUrlPlaceholder} />
|
||||
{:else}
|
||||
{#if $authTypes}
|
||||
<FormSelectField
|
||||
label="Authentication"
|
||||
name="authType"
|
||||
options={$authTypes.map(auth => ({
|
||||
value: auth.name,
|
||||
label: auth.title,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<div class="col-3 mr-1">
|
||||
<FormTextField
|
||||
label="Port"
|
||||
name="port"
|
||||
disabled={disabledFields.includes('port')}
|
||||
templateProps={{ noMargin: true }}
|
||||
placeholder={driver && driver.defaultPort}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6 mr-1">
|
||||
<FormTextField
|
||||
label="User"
|
||||
name="user"
|
||||
disabled={disabledFields.includes('user')}
|
||||
templateProps={{ noMargin: true }}
|
||||
/>
|
||||
<div class="row">
|
||||
<div class="col-9 mr-1">
|
||||
<FormTextField
|
||||
label="Server"
|
||||
name="server"
|
||||
disabled={disabledFields.includes('server')}
|
||||
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>
|
||||
</div>
|
||||
<div class="col-6 mr-1">
|
||||
<FormPasswordField
|
||||
label="Password"
|
||||
name="password"
|
||||
disabled={disabledFields.includes('password')}
|
||||
templateProps={{ noMargin: true }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !disabledFields.includes('password')}
|
||||
<FormSelectField
|
||||
label="Password mode"
|
||||
name="passwordMode"
|
||||
options={[
|
||||
{ value: 'saveEncrypted', label: 'Save and encrypt' },
|
||||
{ value: 'saveRaw', label: 'Save raw (UNSAFE!!)' },
|
||||
]}
|
||||
/>
|
||||
<div class="row">
|
||||
<div class="col-6 mr-1">
|
||||
<FormTextField
|
||||
label="User"
|
||||
name="user"
|
||||
disabled={disabledFields.includes('user')}
|
||||
templateProps={{ noMargin: true }}
|
||||
/>
|
||||
</div>
|
||||
<div class="col-6 mr-1">
|
||||
<FormPasswordField
|
||||
label="Password"
|
||||
name="password"
|
||||
disabled={disabledFields.includes('password')}
|
||||
templateProps={{ noMargin: true }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !disabledFields.includes('password')}
|
||||
<FormSelectField
|
||||
label="Password mode"
|
||||
name="passwordMode"
|
||||
options={[
|
||||
{ value: 'saveEncrypted', label: 'Save and encrypt' },
|
||||
{ value: 'saveRaw', label: 'Save raw (UNSAFE!!)' },
|
||||
]}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<FormTextField label="Default database" name="defaultDatabase" />
|
||||
|
||||
{#if defaultDatabase}
|
||||
<FormCheckboxField label={`Use only database ${defaultDatabase}`} name="singleDatabase" />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -16,9 +16,17 @@
|
||||
|
||||
if (offset.left + width > window.innerWidth) {
|
||||
newLeft = offset.left - width;
|
||||
|
||||
if (newLeft < 0) newLeft = 0;
|
||||
}
|
||||
|
||||
if (offset.top + height > window.innerHeight) {
|
||||
newTop = offset.top - height;
|
||||
|
||||
if (newTop < 0) newTop = 0;
|
||||
if (newTop + height > window.innerHeight) {
|
||||
element.style.height = `${window.innerHeight - newTop}px`;
|
||||
}
|
||||
}
|
||||
|
||||
if (newLeft != null) element.style.left = `${newLeft}px`;
|
||||
@@ -30,9 +38,12 @@
|
||||
const command = commands[item.command];
|
||||
if (command) {
|
||||
return {
|
||||
text: command.name,
|
||||
text: command.menuName || command.toolbarName || command.name,
|
||||
keyText: command.keyText || command.keyTextFromGroup,
|
||||
onClick: command.onClick,
|
||||
onClick: () => {
|
||||
if (command.getSubCommands) visibleCommandPalette.set(command);
|
||||
else if (command.onClick) command.onClick();
|
||||
},
|
||||
disabled: !command.enabled,
|
||||
hideDisabled: item.hideDisabled,
|
||||
};
|
||||
@@ -48,7 +59,7 @@
|
||||
import clickOutside from '../utility/clickOutside';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { commands, commandsCustomized } from '../stores';
|
||||
import { commandsCustomized, visibleCommandPalette } from '../stores';
|
||||
import { extractMenuItems } from '../utility/contextMenu';
|
||||
|
||||
export let items;
|
||||
@@ -112,6 +123,7 @@
|
||||
z-index: 1050;
|
||||
cursor: default;
|
||||
white-space: nowrap;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.keyText {
|
||||
|
||||
@@ -9,18 +9,25 @@
|
||||
|
||||
export let title = 'Error';
|
||||
export let message;
|
||||
export let showAsCode = false;
|
||||
</script>
|
||||
|
||||
<FormProvider>
|
||||
<ModalBase {...$$restProps}>
|
||||
<div slot="header">{title}</div>
|
||||
|
||||
<div class="wrapper">
|
||||
<div class="icon">
|
||||
<FontIcon icon="img error" />
|
||||
{#if showAsCode}
|
||||
<pre>{message}</pre>
|
||||
{:else}
|
||||
<div class="wrapper">
|
||||
<div class="icon">
|
||||
<FontIcon icon="img error" />
|
||||
</div>
|
||||
<div>
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
{message}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div slot="footer">
|
||||
<FormSubmit value="Close" on:click={closeCurrentModal} />
|
||||
@@ -38,4 +45,9 @@
|
||||
margin-right: 10px;
|
||||
font-size: 20pt;
|
||||
}
|
||||
|
||||
pre {
|
||||
max-height: calc(100vh - 300px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -6,15 +6,16 @@ export function extractPluginIcon(packageManifest) {
|
||||
const homepage = (links && links.homepage) || packageManifest.homepage;
|
||||
const tested = repository || homepage || packageManifest.homepage;
|
||||
|
||||
if (tested == 'https://dbgate.org' || tested == 'https://github.com/dbgate/dbgate') {
|
||||
// monorepo plugin
|
||||
return `https://github.com/dbgate/dbgate/raw/master/plugins/${packageManifest.name}/icon.svg`;
|
||||
}
|
||||
|
||||
if (tested) {
|
||||
const match = tested.match(/https:\/\/github.com\/([^/]*)\/([^/]*)/);
|
||||
if (match) {
|
||||
return `https://raw.githubusercontent.com/${match[1]}/${match[2]}/master/icon.svg`;
|
||||
}
|
||||
|
||||
if (tested == 'https://dbgate.org') {
|
||||
return `https://github.com/dbgate/dbgate/raw/master/plugins/${packageManifest.name}/icon.svg`;
|
||||
}
|
||||
}
|
||||
return 'unknown.svg';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user