Compare commits

..

173 Commits

Author SHA1 Message Date
Jan Prochazka 354b5c2fa3 v4.2.0-beta.4 2021-05-13 09:34:30 +02:00
Jan Prochazka f28e0374f8 upgrade better-sqlite 2021-05-13 09:34:07 +02:00
Jan Prochazka 99d97e5dda Revert "try to fix sqlite problem"
This reverts commit 23225cf86b.
2021-05-13 09:32:17 +02:00
Jan Prochazka ae7697f655 v4.2.0-beta.3 2021-05-13 09:08:27 +02:00
Jan Prochazka 23225cf86b try to fix sqlite problem 2021-05-13 08:41:45 +02:00
Jan Prochazka 63ad36f758 v4.2.0-beta.2 2021-05-06 19:18:47 +02:00
Jan Prochazka 80e1563877 missing dependency 2021-05-06 19:18:34 +02:00
Jan Prochazka 3f5c7aecd7 v4.2.0-beta.1 2021-05-06 18:36:36 +02:00
Jan Prochazka abd2492889 Merge branch 'master' into sqlite 2021-05-06 18:36:15 +02:00
Jan Prochazka 872468899d electron - open sqlite database with drag & drop or in open file menu 2021-05-06 18:33:50 +02:00
Jan Prochazka 7a008e5a9d sqlite bulk insert 2021-05-06 15:57:50 +02:00
Jan Prochazka 23940aa324 sqlite version 2021-05-06 15:27:25 +02:00
Jan Prochazka 1888de8728 sqlite stream reader 2021-05-06 15:23:45 +02:00
Jan Prochazka 615397f332 sqlite FK analyser, query runs in transaction 2021-05-06 14:11:51 +02:00
Jan Prochazka e251459512 sqlite sync query 2021-05-06 13:32:37 +02:00
Jan Prochazka a9c8cee08a sqlite stream 2021-05-06 12:32:54 +02:00
Jan Prochazka 1638095c98 database file label 2021-05-06 11:17:30 +02:00
Jan Prochazka 62cedd23b7 extracted getConnectionLabel functionality 2021-05-06 11:08:03 +02:00
Jan Prochazka 3d882f47a7 connection modal fix 2021-05-06 10:50:11 +02:00
Jan Prochazka 88ddc28208 scripts related to server 2021-05-06 10:34:24 +02:00
Jan Prochazka 800666f813 expand button fix 2021-05-06 09:48:07 +02:00
Jan Prochazka 0b8add848a execute command disabled, when query has not connection 2021-05-06 09:43:32 +02:00
Jan Prochazka cd7edcb443 disconnect command (hard disconnect in electron, soft disconnect in webapp) 2021-05-06 09:34:05 +02:00
Jan Prochazka e483fd9e99 changelog 2021-05-05 20:07:04 +02:00
Jan Prochazka 9664e6f981 v4.1.12 2021-05-05 20:05:35 +02:00
Jan Prochazka d1429dd2a1 readme 2021-05-05 20:05:09 +02:00
Jan Prochazka e739aed80d sqlite table analyser 2021-05-05 20:04:49 +02:00
Jan Prochazka 28e19402f3 Merge branch 'master' into sqlite 2021-05-03 21:09:41 +02:00
Jan Prochazka 45a065f391 v4.1.12-beta.2 2021-05-03 21:08:58 +02:00
Jan Prochazka 67e8eb32f7 svelte select fix 2021-05-03 21:08:45 +02:00
Jan Prochazka 5622e3af77 v4.1.12-beta.1 2021-05-03 20:41:19 +02:00
Jan Prochazka 7d34458553 fixed race condition when using SSH tunnel #110 2021-05-03 20:39:41 +02:00
Jan Prochazka 8b747796e7 Merge branch 'master' into sqlite 2021-05-03 18:43:34 +02:00
Jan Prochazka 4802c36b54 changelog 2021-05-03 18:42:04 +02:00
Jan Prochazka 988e4345d4 v4.1.11 2021-05-03 18:36:38 +02:00
Jan Prochazka e02305879e v4.1.11-beta.2 2021-04-30 20:42:34 +02:00
Jan Prochazka 8baad56315 toolbar shows tab related commands aligned to right 2021-04-30 20:35:43 +02:00
Jan Prochazka 14bbc7b057 duplicate tab popup menu 2021-04-30 18:46:44 +02:00
Jan Prochazka 7b6ca27b66 add to favorites moved from toolbar into tab context menu 2021-04-30 18:03:34 +02:00
Jan Prochazka 38aae142ea loading structure status fix 2021-04-30 17:30:18 +02:00
Jan Prochazka bd6c116cc0 timg safe compare token fixes #91 2021-04-30 17:21:35 +02:00
Jan Prochazka 4522c37bfa docker beta build 2021-04-29 20:47:35 +02:00
Jan Prochazka 7d789d5712 #109 all tables button in export fixed + added All collections button for nosql 2021-04-29 20:44:46 +02:00
Jan Prochazka c4c2274488 v4.1.11-beta.1 2021-04-29 14:06:34 +02:00
Jan Prochazka a8b71d452b ssh tunnel keyfile auth fix #106 2021-04-29 14:05:32 +02:00
Jan Prochazka c7d69b0fb5 duplicate connection command 2021-04-29 13:25:12 +02:00
Jan Prochazka 47ea474555 settings optimalization 2021-04-29 11:28:32 +02:00
Jan Prochazka e647ab471e ability to disable background model updates 2021-04-29 11:17:17 +02:00
Jan Prochazka fd6524867e check & load db model in statusbar 2021-04-29 10:40:53 +02:00
Jan Prochazka c24cc1dc72 patched svelte crash #105 2021-04-29 10:03:13 +02:00
Jan Prochazka e3d1e4f53e fixed analysing postgre functions #105 2021-04-29 09:32:59 +02:00
Jan Prochazka 7b32424143 fix 2021-04-29 09:31:41 +02:00
Jan Prochazka 519767fd49 fixed postgres split query 2021-04-29 08:55:38 +02:00
Jan Prochazka 505ab2e075 editor theme to be added 2021-04-29 08:28:00 +02:00
Jan Prochazka 00d0c27502 handle plugin load error 2021-04-29 07:38:44 +02:00
Jan Prochazka d171d7d785 changelog 2021-04-26 18:56:42 +02:00
Jan Prochazka 09593e0b22 changelog 2021-04-26 18:33:57 +02:00
Jan Prochazka 771ca6ad83 v4.1.10 2021-04-26 17:51:26 +02:00
Jan Prochazka 83014d3a5b v4.1.10-beta.6 2021-04-25 21:53:48 +02:00
Jan Prochazka caa2d22dbd sqlite WIP 2021-04-25 21:53:27 +02:00
Jan Prochazka 3c089a5b81 connection modal supports file database 2021-04-25 20:38:41 +02:00
Jan Prochazka d1bf2dbc4b sqlite plugin scaffold 2021-04-25 18:49:53 +02:00
Jan Prochazka a8a9afc936 better display of server version 2021-04-25 12:28:18 +02:00
Jan Prochazka d0cbd5d0a4 server version in statusbar 2021-04-25 12:08:47 +02:00
Jan Prochazka 67e1913683 select page by row_number for MS SQL 2008 #93 2021-04-25 11:48:23 +02:00
Jan Prochazka 8ff706a17f get server version 2021-04-25 10:25:16 +02:00
Jan Prochazka 08692dc63f error detail for connection errors 2021-04-25 09:00:11 +02:00
Jan Prochazka 41d85d4117 build 2021-04-24 13:23:39 +02:00
Jan Prochazka f343d414ef v4.1.10-beta.5 2021-04-24 13:12:45 +02:00
Jan Prochazka 6cda7b2508 build 2021-04-24 13:12:00 +02:00
Jan Prochazka 9085d49d21 v4.1.10-beta.4 2021-04-24 12:32:14 +02:00
Jan Prochazka bb11d7e62b removed arm64 build temporarily #98 2021-04-24 12:31:58 +02:00
Jan Prochazka 7524b30f50 #90 handle native json field in datagrid 2021-04-24 12:24:58 +02:00
Jan Prochazka c30724c5da #94 fixed dropdown menu placement in small window 2021-04-24 11:53:13 +02:00
Jan Prochazka 1e4c108f6f #97 2021-04-24 09:44:58 +02:00
Jan Prochazka 72033e5830 copy windows zip file to release #84 2021-04-24 09:12:30 +02:00
Jan Prochazka 1d24fd9942 fix 2021-04-24 09:04:23 +02:00
Jan Prochazka e104feef14 single database support 2021-04-24 09:01:30 +02:00
Jan Prochazka ccdce6ef43 allow to specify default database #96 #92 2021-04-24 08:21:18 +02:00
Jan Prochazka fccd550d4b electron check origin and host headers #91 2021-04-24 07:52:36 +02:00
Jan Prochazka 3a4a10985b v4.1.10-beta.3 2021-04-23 21:04:22 +02:00
Jan Prochazka 8ee96bd4a0 fix 2021-04-23 21:04:06 +02:00
Jan Prochazka 269046daa5 fix 2021-04-23 21:01:58 +02:00
Jan Prochazka 4738113ce3 v4.1.10-beta.2 2021-04-23 20:58:50 +02:00
Jan Prochazka f7a2931253 build beta also for mac #98 2021-04-23 20:58:36 +02:00
Jan Prochazka d832057076 v4.1.10-beta.1 2021-04-23 20:52:02 +02:00
Jan Prochazka 00fdf14b6e zip target for windows #84 2021-04-23 20:51:44 +02:00
Jan Prochazka ab26f4624a Merge branch 'master' of https://github.com/dbgate/dbgate 2021-04-23 20:50:03 +02:00
Jan Prochazka 8caf5d622e using random free port for electron app #91 #86 2021-04-23 20:49:52 +02:00
Jan Prochazka 0cf8fc79c2 Merge pull request #99 from LinusU/patch-1
add arm64 arch target for macOS
2021-04-23 20:40:14 +02:00
Jan Prochazka 65aa6067e1 Merge branch 'master' of https://github.com/dbgate/dbgate 2021-04-23 20:39:19 +02:00
Jan Prochazka 9a2d56bfe4 #91 authorization header in electron app 2021-04-23 20:39:08 +02:00
Jan Prochazka a1b8e7b641 Merge pull request #101 from knixeur/fix/allow_structure_on_view_error
fix: catch getViewTexts errors otherwise no structure can be seen
2021-04-23 17:17:04 +02:00
Guillermo Bonvehí 137bb7b002 fix: catch getViewTexts errors otherwise no structure can be seen
if somehow SHOW CREATE VIEW fails (invalid refs, permissions) it throw an
error and no structure is shown at all.
2021-04-23 12:07:38 -03:00
Linus Unnebäck e83946f35e add arm64 arch target for macOS 2021-04-23 10:25:18 +02:00
Jan Prochazka 64af838f40 Merge pull request #88 from jfunez/patch-1
fix typo in Readme file
2021-04-22 20:58:24 +02:00
Juan Funez 0d31cc4204 fix typo in Readme file 2021-04-22 17:06:07 +02:00
Jan Prochazka 73a1fce919 v4.1.9 2021-04-21 22:14:37 +02:00
Jan Prochazka e8d5bdbfaf v4.1.9-beta.1 2021-04-21 09:22:24 +02:00
Jan Prochazka 2e37af1ee4 fixes #83 2021-04-21 09:21:38 +02:00
Jan Prochazka 55564ef82a v4.1.8 2021-04-19 17:59:17 +02:00
Jan Prochazka 2461b48244 removed paypal links 2021-04-19 17:55:16 +02:00
Jan Prochazka b05f91f4cb v4.1.8-beta.1 2021-04-18 20:41:28 +02:00
Jan Prochazka 238b6d94d1 sql generator ctx menu on database 2021-04-18 20:40:13 +02:00
Jan Prochazka 8ee2db1bec run query on server 2021-04-18 20:25:08 +02:00
Jan Prochazka 484aa932d3 autodetect jsonrow cell data view for nosql databases 2021-04-18 20:20:31 +02:00
Jan Prochazka 29aa59771c fix 2021-04-18 20:14:32 +02:00
Jan Prochazka cef6b8520e fixed showing FK hint in form view 2021-04-18 11:49:23 +02:00
Jan Prochazka 49f8fb71e4 show toolbar settings 2021-04-18 11:11:06 +02:00
Jan Prochazka 375a441abf typo 2021-04-18 10:56:56 +02:00
Jan Prochazka 0848008302 option not to show FK hints 2021-04-18 10:55:18 +02:00
Jan Prochazka cacd6ae849 simplify settings 2021-04-18 10:40:33 +02:00
Jan Prochazka e97388e14b settings modal 2021-04-18 10:26:59 +02:00
Jan Prochazka 67b57ab756 keyboard settings saved to server 2021-04-18 09:08:01 +02:00
Jan Prochazka bcf183abe2 filter placeholder 2021-04-17 21:05:44 +02:00
Jan Prochazka f92df5c326 toggle left panel command + menu 2021-04-17 21:04:22 +02:00
Jan Prochazka 28bbf9a01e collapsiple grid left column 2021-04-17 21:00:37 +02:00
Jan Prochazka 08d6f83a48 qury designer fix 2021-04-17 20:41:42 +02:00
Jan Prochazka 90af165afd hide macros by default 2021-04-17 20:35:30 +02:00
Jan Prochazka 8a4ee3e01e hide results tab when no result 2021-04-17 20:32:52 +02:00
Jan Prochazka 977818253d v4.1.7 2021-04-17 18:25:02 +02:00
Jan Prochazka ec5db6d562 removed postinstall step from libraries 2021-04-17 18:24:50 +02:00
Jan Prochazka 2d4098ff6a npm dist fix 2021-04-17 18:20:00 +02:00
Jan Prochazka 321d95f522 v4.1.6 2021-04-17 17:59:12 +02:00
Jan Prochazka 53480210d4 plugins - use compiled version by default, zero dependencies 2021-04-17 17:57:31 +02:00
Jan Prochazka 0b1a4ee33f v4.1.5 2021-04-17 17:42:42 +02:00
Jan Prochazka 477099e508 v4.1.5-beta.2 2021-04-17 17:31:54 +02:00
Jan Prochazka 516d007c22 fix 2021-04-17 17:31:32 +02:00
Jan Prochazka ab4febf938 v4.1.5-beta.1 2021-04-17 16:53:35 +02:00
Jan Prochazka 361875d7fc fix 2021-04-17 16:52:56 +02:00
Jan Prochazka c0c1f9d786 command line params refactor 2021-04-17 16:38:10 +02:00
Jan Prochazka 1d264ab559 v4.1.4 2021-04-17 11:22:00 +02:00
Jan Prochazka 553329688a fix 2021-04-17 11:18:40 +02:00
Jan Prochazka 585731a1b3 v4.1.4-beta.1 2021-04-17 11:01:52 +02:00
Jan Prochazka a6207f01af fix 2021-04-17 11:01:42 +02:00
Jan Prochazka 76e51343d0 platform info refactor 2021-04-17 10:42:29 +02:00
Jan Prochazka 6c246c9eaa v4.1.3 2021-04-17 10:10:03 +02:00
Jan Prochazka 479cec4209 prepare replaced with postinstall 2021-04-17 09:57:12 +02:00
Jan Prochazka 6b85870523 v4.1.2 2021-04-17 09:26:15 +02:00
Jan Prochazka a98380a941 plugin version 2021-04-17 09:24:46 +02:00
Jan Prochazka 89a3798d56 npm dist plugins 2021-04-17 09:21:22 +02:00
Jan Prochazka bf202719eb docker fix 2021-04-17 09:13:15 +02:00
Jan Prochazka 9d9b970fd5 v4.1.1 2021-04-17 08:37:25 +02:00
Jan Prochazka 56fcfd84e5 v4.1.1-beta.3 2021-04-15 20:38:48 +02:00
Jan Prochazka e7d575dc8e plugins package fixes 2021-04-15 20:37:23 +02:00
Jan Prochazka b61c454a3a fix 2021-04-15 20:36:45 +02:00
Jan Prochazka a2af86c705 readme 2021-04-15 20:16:52 +02:00
Jan Prochazka 2594be70fc v4.1.1-beta.2 2021-04-15 20:04:52 +02:00
Jan Prochazka 6ff63e40f0 fixed plugin probleM (fs-extra.pathExists doesn't work correctly with paths in ASAR used in electron) 2021-04-15 20:04:29 +02:00
Jan Prochazka a33f09c185 v4.1.1-beta.1 2021-04-15 18:00:23 +02:00
Jan Prochazka 09f14a0717 changelog 2021-04-15 17:59:32 +02:00
Jan Prochazka 9139ff0f44 plugins 2021-04-15 17:56:35 +02:00
Jan Prochazka 1cc955f997 plugins dir 2021-04-15 17:41:35 +02:00
Jan Prochazka 4dfaf1346e icons 2021-04-15 17:22:53 +02:00
Jan Prochazka 419ab985c1 copydist command 2021-04-15 12:42:34 +02:00
Jan Prochazka c5ec22d504 watch mode for plugins 2021-04-15 11:00:42 +02:00
Jan Prochazka 5dd03484ea packaged plugins 2021-04-15 10:52:02 +02:00
Jan Prochazka 4d5cc119f2 added plugins 2021-04-13 16:17:53 +02:00
Jan Prochazka 446e7c139f readme 2021-04-12 21:19:25 +02:00
Jan Prochazka 5a6641bc6e v4.1.0 2021-04-12 18:40:36 +02:00
Jan Prochazka 43c00e88bb yarn.lock 2021-04-12 18:39:23 +02:00
Jan Prochazka 297df772a1 v4.1.0-beta.2 2021-04-11 20:25:14 +02:00
Jan Prochazka 449bbde645 plugin versions 2021-04-11 20:24:20 +02:00
Jan Prochazka b222b916ec scroll in view active tab 2021-04-11 11:05:07 +02:00
Jan Prochazka d3d695ed81 #64 2021-04-11 10:43:21 +02:00
Jan Prochazka c1778bea26 autofill - forgotten from react 2021-04-11 09:47:39 +02:00
Jan Prochazka 1d401e302a #63 - keyboard modal, settings icon 2021-04-11 09:04:50 +02:00
Jan Prochazka 12fd5f6943 custom error shortcuts 2021-04-10 20:51:33 +02:00
Jan Prochazka 817286d326 export mongo grid, generate query 2021-04-10 17:21:29 +02:00
Jan Prochazka cc2c55b20f generate script menu for collections 2021-04-10 17:03:00 +02:00
Jan Prochazka 90169a7624 sql template => script template 2021-04-10 16:58:16 +02:00
Jan Prochazka 88b4c9daff custom DB URI support (used by Mongo) 2021-04-10 10:47:36 +02:00
Jan Prochazka e8b43820b9 v4.1.0-beta.1 2021-04-08 20:14:14 +02:00
279 changed files with 9350 additions and 1208 deletions
+10 -3
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,47 @@
name: Docker image
# on: [push]
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+'
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-18.04]
steps:
- name: Context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Use Node.js 10.x
uses: actions/setup-node@v1
with:
node-version: 10.x
- name: yarn install
run: |
yarn install
- name: setCurrentVersion
run: |
yarn setCurrentVersion
- name: Prepare docker image
run: |
yarn run prepare:docker
- name: Build docker image
run: |
docker build ./docker -t dbgate
- name: Push docker image
run: |
docker tag dbgate dbgate/dbgate:beta
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker push dbgate/dbgate:beta
+30
View File
@@ -89,3 +89,33 @@ jobs:
working-directory: packages/dbgate
run: |
npm publish
- name: Publish dbgate-plugin-csv
working-directory: plugins/dbgate-plugin-csv
run: |
npm publish
- name: Publish dbgate-plugin-excel
working-directory: plugins/dbgate-plugin-excel
run: |
npm publish
- name: Publish dbgate-plugin-mssql
working-directory: plugins/dbgate-plugin-mssql
run: |
npm publish
- name: Publish dbgate-plugin-mysql
working-directory: plugins/dbgate-plugin-mysql
run: |
npm publish
- name: Publish dbgate-plugin-mongo
working-directory: plugins/dbgate-plugin-mongo
run: |
npm publish
- name: Publish dbgate-plugin-postgres
working-directory: plugins/dbgate-plugin-postgres
run: |
npm publish
+2
View File
@@ -13,8 +13,10 @@ build
dist
app/packages/web/public
app/packages/plugins
docker/public
docker/bundle.js
docker/plugins
# misc
.DS_Store
+37
View File
@@ -1,5 +1,42 @@
# 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
- ADDED: MongoDB support
- ADDED: Configurable keyboard shortcuts
- ADDED: JSON row cell data view
- FIX: Fixed some problems from migration to Svelte
### 4.0.3
- FIX: fixes for FireFox (mainly incorrent handle of bind:clientHeight, replaces with resizeobserver)
### 4.0.2
+16 -6
View File
@@ -1,27 +1,35 @@
[![NPM version](https://img.shields.io/npm/v/dbgate.svg)](https://www.npmjs.com/package/dbgate)
![GitHub All Releases](https://img.shields.io/github/downloads/dbgate/dbgate/total)
[![dbgate](https://snapcraft.io/dbgate/badge.svg)](https://snapcraft.io/dbgate)
[![dbgate](https://snapcraft.io/dbgate/trending.svg?name=0)](https://snapcraft.io/dbgate)
[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
[![Donate](https://img.shields.io/badge/donate-paypal-blue.svg)](https://paypal.me/JanProchazkaCz/30eur)
# DbGate - database administration tool
DbGate is fast and easy to use database manager. Works with MySQL, PostgreSQL and SQL Server.
DbGate modern, fast and easy to use database manager
* Try it online - [demo.dbgate.org](https://demo.dbgate.org) - online demo application
* Download application for Windows, Linux or Mac from [dbgate.org](https://dbgate.org/download/)
* Run web version as [NPM package](https://www.npmjs.com/package/dbgate) or as [docker image](https://hub.docker.com/r/dbgate/dbgate)
Supported databases:
* MySQL
* PostgreSQL
* SQL Server
* MongoDB
![Screenshot](https://raw.githubusercontent.com/dbgate/dbgate/master/screenshot.png)
## Features
* Connect to Microsoft SQL Server, Postgre SQL, MySQL
* Connect to Microsoft SQL Server, Postgre SQL, MySQL, MongoDB
* Table data editing, with SQL change script preview
* Master/detail views
* Query designer
* Form view for comfortable work with tables with many columns
* Explore tables, views, procedures, functions
* 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)
* Runs as application for Windows, Linux and Mac. Or in Docker container on server and in web Browser on client.
* Import, export from/to CSV, Excel, JSON
* Free table editor - quick table data editing (cleanup data after import/before export, prototype tables etc.)
@@ -56,6 +64,8 @@ Currently following extensions can be implemented using plugins:
- File format parsers/writers
- Database engine connectors
Basic set of plugins is part of DbGate git repository and is installed with app. Additional plugins pust be downloaded from NPM (this task is handled by DbGate)
## How to run development environment
```sh
@@ -63,7 +73,7 @@ yarn
yarn start
```
If you want to make modifications in TypeScript packages, run TypeScript compiler in watch mode in seconds terminal:
If you want to make modifications in libraries or plugins, run library compiler in watch mode in the second terminal:
```sh
yarn lib
```
@@ -78,7 +88,7 @@ yarn start
```
## How to run built electron app locally
This mode is very similar to production run of electron app. Electron app forks process with API on dynamically allocated port, works with compiled javascript files (doesn't use localhost:5000)
This mode is very similar to production run of electron app. Electron app forks process with API on dynamically allocated port, works with compiled javascript files and uses compiled version of plugins (doesn't use localhost:5000)
```sh
cd app
+13 -5
View File
@@ -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": "^7.3.1",
"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",
@@ -71,7 +79,7 @@
"build": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && yarn dist",
"build:local": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && yarn predist",
"postinstall": "electron-builder install-app-deps",
"predist": "copyfiles ../packages/api/dist/* packages && copyfiles \"../packages/web/public/*\" packages && copyfiles \"../packages/web/public/**/*\" packages"
"predist": "copyfiles ../packages/api/dist/* packages && copyfiles \"../packages/web/public/*\" packages && copyfiles \"../packages/web/public/**/*\" packages && copyfiles --up 3 \"../plugins/dist/**/*\" packages/plugins"
},
"main": "src/electron.js",
"devDependencies": {
@@ -83,4 +91,4 @@
"optionalDependencies": {
"msnodesqlv8": "^2.0.10"
}
}
}
+3 -1
View File
@@ -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();
}
});
+92
View File
@@ -232,6 +232,22 @@ base64-js@^1.3.1:
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
better-sqlite3@^7.3.1:
version "7.3.1"
resolved "https://registry.yarnpkg.com/better-sqlite3/-/better-sqlite3-7.3.1.tgz#2dcccfa4c34c544073d12fbb172281a13925fbb8"
integrity sha512-Io0eWFWEtHsA7KS7Ehm45AGwi5SHeCD1hIHd+b1nj26Tf7rFTBqMltuVDimNMNMJ6f+Oy29RT7XWinv3yWKvIQ==
dependencies:
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 +385,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 +836,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 +879,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 +1328,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 +1355,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 +1388,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 +1569,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 +2024,18 @@ tar-stream@^2.1.4:
inherits "^2.0.3"
readable-stream "^3.1.1"
tar@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83"
integrity sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==
dependencies:
chownr "^2.0.0"
fs-minipass "^2.0.0"
minipass "^3.0.0"
minizlib "^2.1.1"
mkdirp "^1.0.3"
yallist "^4.0.0"
temp-file@^3.3.7:
version "3.3.7"
resolved "https://registry.yarnpkg.com/temp-file/-/temp-file-3.3.7.tgz#686885d635f872748e384e871855958470aeb18a"
+1 -1
View File
@@ -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'] = () => require('better-sqlite3');`;
const getContent = (empty) => `
// this file is generated automatically by script fillNativeModules.js, do not edit it manually
+15 -11
View File
@@ -1,9 +1,10 @@
{
"private": true,
"version": "4.0.3",
"version": "4.2.0-beta.4",
"name": "dbgate-all",
"workspaces": [
"packages/*"
"packages/*",
"plugins/*"
],
"scripts": {
"start:api": "yarn workspace dbgate-api start",
@@ -19,25 +20,27 @@
"build:filterparser": "yarn workspace dbgate-filterparser build",
"build:tools": "yarn workspace dbgate-tools build",
"build:lib": "yarn build:tools && yarn build:sqltree && yarn build:filterparser && yarn build:datalib",
"build:app": "cd app && yarn install && yarn build",
"build:app": "yarn plugins:copydist && cd app && yarn install && yarn build",
"build:api": "yarn workspace dbgate-api build",
"build:web:docker": "yarn workspace dbgate-web build",
"build:app:local": "cd app && yarn build:local",
"build:plugins:frontend": "workspaces-run --only=\"dbgate-plugin-*\" -- yarn build:frontend",
"build:plugins:frontend:watch": "workspaces-run --parallel --only=\"dbgate-plugin-*\" -- yarn build:frontend:watch",
"plugins:copydist": "workspaces-run --only=\"dbgate-plugin-*\" -- yarn copydist",
"build:app:local": "yarn plugins:copydist && cd app && yarn build:local",
"start:app:local": "cd app && yarn start:local",
"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",
"prepare:docker": "yarn build:web:docker && yarn build:api && yarn copy:docker:build",
"prepare": "yarn build:lib",
"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",
"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\"",
"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"
"postinstall": "yarn build:lib && patch-package && yarn fillNativeModules && yarn build:plugins:frontend"
},
"dependencies": {
"concurrently": "^5.1.0",
@@ -46,6 +49,7 @@
},
"devDependencies": {
"copyfiles": "^2.2.0",
"prettier": "^2.2.1"
"prettier": "^2.2.1",
"workspaces-run": "^1.0.1"
}
}
+1 -15
View File
@@ -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
View 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
+12 -11
View File
@@ -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": "^7.3.1",
"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": "^8.1.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,15 +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",
@@ -68,4 +69,4 @@
"optionalDependencies": {
"msnodesqlv8": "^2.0.10"
}
}
}
+39 -1
View File
@@ -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;
}
},
};
+1 -1
View File
@@ -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
+76 -94
View File
@@ -2,43 +2,21 @@ const fs = require('fs-extra');
const axios = require('axios');
const path = require('path');
const { extractPackageName } = require('dbgate-tools');
const { pluginsdir, datadir } = require('../utility/directories');
const { pluginsdir, packagedPluginsDir } = require('../utility/directories');
const socket = require('../utility/socket');
const compareVersions = require('compare-versions');
const requirePlugin = require('../shell/requirePlugin');
const downloadPackage = require('../utility/downloadPackage');
const hasPermission = require('../utility/hasPermission');
// async function loadPackageInfo(dir) {
// const readmeFile = path.join(dir, 'README.md');
// const packageFile = path.join(dir, 'package.json');
// if (!(await fs.exists(packageFile))) {
// return null;
// }
// let readme = null;
// let manifest = null;
// if (await fs.exists(readmeFile)) readme = await fs.readFile(readmeFile, { encoding: 'utf-8' });
// if (await fs.exists(packageFile)) manifest = JSON.parse(await fs.readFile(packageFile, { encoding: 'utf-8' }));
// return {
// readme,
// manifest,
// };
// }
const preinstallPluginMinimalVersions = {
'dbgate-plugin-mssql': '1.2.1',
'dbgate-plugin-mysql': '1.2.1',
'dbgate-plugin-postgres': '1.2.1',
'dbgate-plugin-csv': '1.0.8',
'dbgate-plugin-excel': '1.0.6',
};
const _ = require('lodash');
module.exports = {
script_meta: 'get',
async script({ packageName }) {
const file = path.join(pluginsdir(), packageName, 'dist', 'frontend.js');
const file1 = path.join(packagedPluginsDir(), packageName, 'dist', 'frontend.js');
const file2 = path.join(pluginsdir(), packageName, 'dist', 'frontend.js');
// @ts-ignore
const file = (await fs.exists(file1)) ? file1 : file2;
const data = await fs.readFile(file, {
encoding: 'utf-8',
});
@@ -62,10 +40,13 @@ module.exports = {
const { latest } = infoResp.data['dist-tags'];
const manifest = infoResp.data.versions[latest];
const { readme } = infoResp.data;
// @ts-ignore
const isPackaged = await fs.exists(path.join(packagedPluginsDir(), packageName));
return {
readme,
manifest,
isPackaged,
};
} catch (err) {
return {
@@ -73,52 +54,52 @@ module.exports = {
error: err.message,
};
}
// const dir = path.join(pluginstmpdir(), packageName);
// if (!(await fs.exists(dir))) {
// await downloadPackage(packageName, dir);
// }
// return await loadPackageInfo(dir);
// return await {
// ...loadPackageInfo(dir),
// installed: loadPackageInfo(path.join(pluginsdir(), packageName)),
// };
},
installed_meta: 'get',
async installed() {
const files = await fs.readdir(pluginsdir());
const files1 = await fs.readdir(packagedPluginsDir());
const files2 = await fs.readdir(pluginsdir());
const res = [];
for (const packageName of files) {
const manifest = await fs.readFile(path.join(pluginsdir(), packageName, 'package.json')).then(x => JSON.parse(x));
const readmeFile = path.join(pluginsdir(), packageName, 'README.md');
if (await fs.exists(readmeFile)) {
manifest.readme = await fs.readFile(readmeFile, { encoding: 'utf-8' });
for (const packageName of _.union(files1, files2)) {
if (!/^dbgate-plugin-.*$/.test(packageName)) continue;
try {
const isPackaged = files1.includes(packageName);
const manifest = await fs
.readFile(path.join(isPackaged ? packagedPluginsDir() : pluginsdir(), packageName, 'package.json'), {
encoding: 'utf-8',
})
.then(x => JSON.parse(x));
const readmeFile = path.join(isPackaged ? packagedPluginsDir() : pluginsdir(), packageName, 'README.md');
// @ts-ignore
if (await fs.exists(readmeFile)) {
manifest.readme = await fs.readFile(readmeFile, { encoding: 'utf-8' });
}
manifest.isPackaged = isPackaged;
res.push(manifest);
} catch (err) {
console.log(`Skipped plugin ${packageName}, error:`, err.message);
}
res.push(manifest);
}
return res;
// const res = await Promise.all(
// files.map((packageName) =>
// fs.readFile(path.join(pluginsdir(), packageName, 'package.json')).then((x) => JSON.parse(x))
// )
// );
},
async saveRemovePlugins() {
await fs.writeFile(path.join(datadir(), 'removed-plugins'), this.removedPlugins.join('\n'));
},
// async saveRemovePlugins() {
// await fs.writeFile(path.join(datadir(), 'removed-plugins'), this.removedPlugins.join('\n'));
// },
install_meta: 'post',
async install({ packageName }) {
if (!hasPermission(`plugins/install`)) return;
const dir = path.join(pluginsdir(), packageName);
// @ts-ignore
if (!(await fs.exists(dir))) {
await downloadPackage(packageName, dir);
}
socket.emitChanged(`installed-plugins-changed`);
this.removedPlugins = this.removedPlugins.filter(x => x != packageName);
await this.saveRemovePlugins();
// this.removedPlugins = this.removedPlugins.filter(x => x != packageName);
// await this.saveRemovePlugins();
},
uninstall_meta: 'post',
@@ -127,7 +108,7 @@ module.exports = {
const dir = path.join(pluginsdir(), packageName);
await fs.rmdir(dir, { recursive: true });
socket.emitChanged(`installed-plugins-changed`);
this.removedPlugins.push(packageName);
// this.removedPlugins.push(packageName);
await this.saveRemovePlugins();
},
@@ -135,6 +116,7 @@ module.exports = {
async upgrade({ packageName }) {
if (!hasPermission(`plugins/install`)) return;
const dir = path.join(pluginsdir(), packageName);
// @ts-ignore
if (await fs.exists(dir)) {
await fs.rmdir(dir, { recursive: true });
await downloadPackage(packageName, dir);
@@ -157,46 +139,46 @@ module.exports = {
return content.driver.getAuthTypes() || null;
},
async _init() {
const installed = await this.installed();
try {
this.removedPlugins = (await fs.readFile(path.join(datadir(), 'removed-plugins'), { encoding: 'utf-8' })).split(
'\n'
);
} catch (err) {
this.removedPlugins = [];
}
// async _init() {
// const installed = await this.installed();
// try {
// this.removedPlugins = (await fs.readFile(path.join(datadir(), 'removed-plugins'), { encoding: 'utf-8' })).split(
// '\n'
// );
// } catch (err) {
// this.removedPlugins = [];
// }
for (const packageName of Object.keys(preinstallPluginMinimalVersions)) {
const installedVersion = installed.find(x => x.name == packageName);
if (installedVersion) {
// plugin installed, test, whether upgrade
const requiredVersion = preinstallPluginMinimalVersions[packageName];
if (compareVersions(installedVersion.version, requiredVersion) < 0) {
console.log(
`Upgrading preinstalled plugin ${packageName}, found ${installedVersion.version}, required version ${requiredVersion}`
);
await this.upgrade({ packageName });
} else {
console.log(
`Plugin ${packageName} not upgraded, found ${installedVersion.version}, required version ${requiredVersion}`
);
}
// for (const packageName of Object.keys(preinstallPluginMinimalVersions)) {
// const installedVersion = installed.find(x => x.name == packageName);
// if (installedVersion) {
// // plugin installed, test, whether upgrade
// const requiredVersion = preinstallPluginMinimalVersions[packageName];
// if (compareVersions(installedVersion.version, requiredVersion) < 0) {
// console.log(
// `Upgrading preinstalled plugin ${packageName}, found ${installedVersion.version}, required version ${requiredVersion}`
// );
// await this.upgrade({ packageName });
// } else {
// console.log(
// `Plugin ${packageName} not upgraded, found ${installedVersion.version}, required version ${requiredVersion}`
// );
// }
continue;
}
// continue;
// }
if (this.removedPlugins.includes(packageName)) {
// plugin was remvoed, don't install again
continue;
}
// if (this.removedPlugins.includes(packageName)) {
// // plugin was remvoed, don't install again
// continue;
// }
try {
console.log('Preinstalling plugin', packageName);
await this.install({ packageName });
} catch (err) {
console.error('Error preinstalling plugin', packageName, err);
}
}
},
// try {
// console.log('Preinstalling plugin', packageName);
// await this.install({ packageName });
// } catch (err) {
// console.error('Error preinstalling plugin', packageName, err);
// }
// }
// },
};
+3 -3
View File
@@ -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' };
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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'
};
+5 -6
View File
@@ -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 = {
+42 -9
View File
@@ -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}`);
});
+24 -1
View File
@@ -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',
+4 -3
View File
@@ -1,6 +1,8 @@
const path = require('path');
const { pluginsdir } = require('../utility/directories');
const fs = require('fs');
const { pluginsdir, packagedPluginsDir, getPluginBackendPath } = require('../utility/directories');
const nativeModules = require('../nativeModules');
const platformInfo = require('../utility/platformInfo');
const loadedPlugins = {};
@@ -8,14 +10,13 @@ const dbgateEnv = {
dbgateApi: null,
nativeModules,
};
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 = path.join(pluginsdir(), packageName, 'dist', 'backend.js');
const modulePath = getPluginBackendPath(packageName);
console.log(`Loading module ${packageName} from ${modulePath}`);
try {
// @ts-ignore
+2 -1
View File
@@ -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 {
+1 -1
View File
@@ -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
@@ -7,6 +7,7 @@ const { getSshTunnelProxy } = require('./sshTunnelProxy');
async function connectUtility(driver, storedConnection) {
const connection = {
database: storedConnection.defaultDatabase,
...decryptConnection(storedConnection),
};
@@ -38,6 +39,10 @@ async function connectUtility(driver, storedConnection) {
connection.ssl.key = await fs.readFile(connection.sslKeyFile);
}
if (connection.sslCertFilePassword) {
connection.ssl.password = connection.sslCertFilePassword;
}
if (!connection.ssl.key && !connection.ssl.ca && !connection.ssl.cert) {
// TODO: provide this as an option in settings
// or per-connection as 'reject self-signed certs'
+35
View File
@@ -2,6 +2,7 @@ const os = require('os');
const path = require('path');
const fs = require('fs');
const cleanDirectory = require('./cleanDirectory');
const platformInfo = require('./platformInfo');
const createDirectories = {};
const ensureDirectory = (dir, clean) => {
@@ -39,6 +40,37 @@ const pluginsdir = dirFunc('plugins');
const archivedir = dirFunc('archive');
const filesdir = dirFunc('files');
function packagedPluginsDir() {
if (platformInfo.isDevMode) {
return path.resolve(__dirname, '../../../../plugins');
}
if (platformInfo.isDocker) {
return '/home/dbgate-docker/plugins';
}
if (platformInfo.isNpmDist) {
// node_modules
return global['dbgateApiPackagedPluginsPath'];
}
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 = {
datadir,
jsldir,
@@ -48,4 +80,7 @@ module.exports = {
ensureDirectory,
pluginsdir,
filesdir,
packagedPluginsDir,
packagedPluginList,
getPluginBackendPath,
};
+24 -8
View File
@@ -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
View 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,
};
+36 -27
View File
@@ -4,6 +4,8 @@ const portfinder = require('portfinder');
const stableStringify = require('json-stable-stringify');
const _ = require('lodash');
const platformInfo = require('./platformInfo');
const AsyncLock = require('async-lock');
const lock = new AsyncLock();
const sshConnectionCache = {};
const sshTunnelCache = {};
@@ -34,7 +36,7 @@ async function getSshConnection(connection) {
password: connection.sshMode == 'userPassword' ? connection.sshPassword : undefined,
agentSocket: connection.sshMode == 'agent' ? platformInfo.sshAuthSock : undefined,
privateKey:
connection.sshMode == 'keyFile' && connection.sshKeyFile ? await fs.readFile(connection.sshKeyFile) : undefined,
connection.sshMode == 'keyFile' && connection.sshKeyfile ? await fs.readFile(connection.sshKeyfile) : undefined,
skipAutoPrivateKey: true,
noReadline: true,
};
@@ -45,36 +47,43 @@ async function getSshConnection(connection) {
}
async function getSshTunnel(connection) {
const sshConn = await getSshConnection(connection);
const tunnelCacheKey = stableStringify(_.pick(connection, TUNNEL_FIELDS));
if (sshTunnelCache[tunnelCacheKey]) return sshTunnelCache[tunnelCacheKey];
const localPort = await portfinder.getPortPromise({ port: 10000, stopPort: 60000 });
// workaround for `getPortPromise` not releasing the port quickly enough
await new Promise(resolve => setTimeout(resolve, 500));
const tunnelConfig = {
fromPort: localPort,
toPort: connection.port,
toHost: connection.server,
};
try {
const tunnel = await sshConn.forward(tunnelConfig);
console.log(
`Created SSH tunnel to ${connection.sshHost}-${connection.server}:${connection.port}, using local port ${localPort}`
);
return await lock.acquire(tunnelCacheKey, async () => {
const sshConn = await getSshConnection(connection);
if (sshTunnelCache[tunnelCacheKey]) return sshTunnelCache[tunnelCacheKey];
sshTunnelCache[tunnelCacheKey] = {
state: 'ok',
localPort,
const localPort = await portfinder.getPortPromise({ port: 10000, stopPort: 60000 });
// workaround for `getPortPromise` not releasing the port quickly enough
await new Promise(resolve => setTimeout(resolve, 500));
const tunnelConfig = {
fromPort: localPort,
toPort: connection.port,
toHost: connection.server,
};
return sshTunnelCache[tunnelCacheKey];
} catch (err) {
// error is not cached
return {
state: 'error',
message: err.message,
};
}
try {
console.log(
`Creating SSH tunnel to ${connection.sshHost}-${connection.server}:${connection.port}, using local port ${localPort}`
);
const tunnel = await sshConn.forward(tunnelConfig);
console.log(
`Created SSH tunnel to ${connection.sshHost}-${connection.server}:${connection.port}, using local port ${localPort}`
);
sshTunnelCache[tunnelCacheKey] = {
state: 'ok',
localPort,
};
return sshTunnelCache[tunnelCacheKey];
} catch (err) {
// error is not cached
return {
state: 'error',
message: err.message,
};
}
});
}
module.exports = {
@@ -0,0 +1,9 @@
const crypto = require('crypto');
function timingSafeCheckToken(a, b) {
if (!a || !b) return false;
if (a.length != b.length) return false;
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
}
module.exports = timingSafeCheckToken;
+4 -5
View File
@@ -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"
}
+15 -3
View File
@@ -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;
+73 -5
View File
@@ -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;
}
+15 -3
View File
@@ -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;
+5 -3
View File
@@ -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);
}
}
+3 -2
View File
@@ -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
View File
@@ -1,5 +1,4 @@
[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
[![Donate](https://img.shields.io/badge/donate-paypal-blue.svg)](https://paypal.me/JanProchazkaCz/30eur)
[![NPM version](https://img.shields.io/npm/v/dbgate.svg)](https://www.npmjs.com/package/dbgate)
# DbGate - database administration tool
+6 -3
View File
@@ -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();
+9 -4
View File
@@ -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"
}
}
+3 -4
View File
@@ -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"
+2 -4
View File
@@ -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": {
+7
View File
@@ -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;
}
}
+16 -2
View File
@@ -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 };
+3 -5
View File
@@ -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"
}
}
}
+11
View File
@@ -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);
// }
+4 -1
View File
@@ -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');
+1 -1
View File
@@ -7,6 +7,6 @@ export * from './DatabaseAnalyser';
export * from './driverBase';
export * from './SqlDumper';
export * from './testPermission';
export * from './splitPostgresQuery';
export * from './SqlGenerator';
export * from './structureTools';
export * from './settingsExtractors';
+20
View 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;
}
-292
View File
@@ -1,292 +0,0 @@
const SINGLE_QUOTE = "'";
const DOUBLE_QUOTE = '"';
// const BACKTICK = '`';
const DOUBLE_DASH_COMMENT_START = '--';
const HASH_COMMENT_START = '#';
const C_STYLE_COMMENT_START = '/*';
const SEMICOLON = ';';
const LINE_FEED = '\n';
const DELIMITER_KEYWORD = 'DELIMITER';
export interface SplitOptions {
multipleStatements?: boolean;
retainComments?: boolean;
}
interface SqlStatement {
value: string;
supportMulti: boolean;
}
interface SplitExecutionContext extends Required<SplitOptions> {
unread: string;
currentDelimiter: string;
currentStatement: SqlStatement;
output: SqlStatement[];
}
interface FindExpResult {
expIndex: number;
exp: string | null;
nextIndex: number;
}
const regexEscapeSetRegex = /[-/\\^$*+?.()|[\]{}]/g;
const singleQuoteStringEndRegex = /(?<!\\)'/;
const doubleQuoteStringEndRegex = /(?<!\\)"/;
// const backtickQuoteEndRegex = /(?<!`)`(?!`)/;
const doubleDashCommentStartRegex = /--[ \f\n\r\t\v]/;
const cStyleCommentStartRegex = /\/\*/;
const cStyleCommentEndRegex = /(?<!\/)\*\//;
const newLineRegex = /(?:[\r\n]+|$)/;
const delimiterStartRegex = /(?:^|[\n\r]+)[ \f\t\v]*DELIMITER[ \t]+/i;
// Best effort only, unable to find a syntax specification on delimiter
const delimiterTokenRegex = /^(?:'(.+)'|"(.+)"|`(.+)`|([^\s]+))/;
const semicolonKeyTokenRegex = buildKeyTokenRegex(SEMICOLON);
const quoteEndRegexDict: Record<string, RegExp> = {
[SINGLE_QUOTE]: singleQuoteStringEndRegex,
[DOUBLE_QUOTE]: doubleQuoteStringEndRegex,
// [BACKTICK]: backtickQuoteEndRegex,
};
function escapeRegex(value: string): string {
return value.replace(regexEscapeSetRegex, '\\$&');
}
function buildKeyTokenRegex(delimiter: string): RegExp {
return new RegExp(
'(?:' +
[
escapeRegex(delimiter),
SINGLE_QUOTE,
DOUBLE_QUOTE,
// BACKTICK,
doubleDashCommentStartRegex.source,
HASH_COMMENT_START,
cStyleCommentStartRegex.source,
delimiterStartRegex.source,
].join('|') +
')',
'i'
);
}
function findExp(content: string, regex: RegExp): FindExpResult {
const match = content.match(regex);
let result: FindExpResult;
if (match?.index !== undefined) {
result = {
expIndex: match.index,
exp: match[0],
nextIndex: match.index + match[0].length,
};
} else {
result = {
expIndex: -1,
exp: null,
nextIndex: content.length,
};
}
return result;
}
function findKeyToken(content: string, currentDelimiter: string): FindExpResult {
let regex;
if (currentDelimiter === SEMICOLON) {
regex = semicolonKeyTokenRegex;
} else {
regex = buildKeyTokenRegex(currentDelimiter);
}
return findExp(content, regex);
}
function findEndQuote(content: string, quote: string): FindExpResult {
if (!(quote in quoteEndRegexDict)) {
throw new TypeError(`Incorrect quote ${quote} supplied`);
}
return findExp(content, quoteEndRegexDict[quote]);
}
function read(
context: SplitExecutionContext,
readToIndex: number,
nextUnreadIndex?: number,
checkSemicolon?: boolean
): void {
if (checkSemicolon === undefined) {
checkSemicolon = true;
}
const readContent = context.unread.slice(0, readToIndex);
if (checkSemicolon && readContent.includes(SEMICOLON)) {
context.currentStatement.supportMulti = false;
}
context.currentStatement.value += readContent;
if (nextUnreadIndex !== undefined && nextUnreadIndex > 0) {
context.unread = context.unread.slice(nextUnreadIndex);
} else {
context.unread = context.unread.slice(readToIndex);
}
}
function readTillNewLine(context: SplitExecutionContext, checkSemicolon?: boolean): void {
const findResult = findExp(context.unread, newLineRegex);
read(context, findResult.expIndex, findResult.expIndex, checkSemicolon);
}
function discard(context: SplitExecutionContext, nextUnreadIndex: number): void {
if (nextUnreadIndex > 0) {
context.unread = context.unread.slice(nextUnreadIndex);
}
}
function discardTillNewLine(context: SplitExecutionContext): void {
const findResult = findExp(context.unread, newLineRegex);
discard(context, findResult.expIndex);
}
function publishStatementInMultiMode(splitOutput: SqlStatement[], currentStatement: SqlStatement): void {
if (splitOutput.length === 0) {
splitOutput.push({
value: '',
supportMulti: true,
});
}
const lastSplitResult = splitOutput[splitOutput.length - 1];
if (currentStatement.supportMulti) {
if (lastSplitResult.supportMulti) {
if (lastSplitResult.value !== '' && !lastSplitResult.value.endsWith(LINE_FEED)) {
lastSplitResult.value += LINE_FEED;
}
lastSplitResult.value += currentStatement.value + SEMICOLON;
} else {
splitOutput.push({
value: currentStatement.value + SEMICOLON,
supportMulti: true,
});
}
} else {
splitOutput.push({
value: currentStatement.value,
supportMulti: false,
});
}
}
function publishStatement(context: SplitExecutionContext): void {
const trimmed = context.currentStatement.value.trim();
if (trimmed !== '') {
if (!context.multipleStatements) {
context.output.push({
value: trimmed,
supportMulti: context.currentStatement.supportMulti,
});
} else {
context.currentStatement.value = trimmed;
publishStatementInMultiMode(context.output, context.currentStatement);
}
}
context.currentStatement.value = '';
context.currentStatement.supportMulti = true;
}
function handleKeyTokenFindResult(context: SplitExecutionContext, findResult: FindExpResult): void {
switch (findResult.exp?.trim()) {
case context.currentDelimiter:
read(context, findResult.expIndex, findResult.nextIndex);
publishStatement(context);
break;
// case BACKTICK:
case SINGLE_QUOTE:
case DOUBLE_QUOTE: {
read(context, findResult.nextIndex);
const findQuoteResult = findEndQuote(context.unread, findResult.exp);
read(context, findQuoteResult.nextIndex, undefined, false);
break;
}
case DOUBLE_DASH_COMMENT_START: {
if (context.retainComments) {
read(context, findResult.nextIndex);
readTillNewLine(context, false);
} else {
read(context, findResult.expIndex, findResult.expIndex + DOUBLE_DASH_COMMENT_START.length);
discardTillNewLine(context);
}
break;
}
case HASH_COMMENT_START: {
if (context.retainComments) {
read(context, findResult.nextIndex);
readTillNewLine(context, false);
} else {
read(context, findResult.expIndex, findResult.nextIndex);
discardTillNewLine(context);
}
break;
}
case C_STYLE_COMMENT_START: {
if (['!', '+'].includes(context.unread[findResult.nextIndex]) || context.retainComments) {
// Should not be skipped, see https://dev.mysql.com/doc/refman/5.7/en/comments.html
read(context, findResult.nextIndex);
const findCommentResult = findExp(context.unread, cStyleCommentEndRegex);
read(context, findCommentResult.nextIndex);
} else {
read(context, findResult.expIndex, findResult.nextIndex);
const findCommentResult = findExp(context.unread, cStyleCommentEndRegex);
discard(context, findCommentResult.nextIndex);
}
break;
}
case DELIMITER_KEYWORD: {
read(context, findResult.expIndex, findResult.nextIndex);
// MySQL client will return `DELIMITER cannot contain a backslash character` if backslash is used
// Shall we reject backslash as well?
const matched = context.unread.match(delimiterTokenRegex);
if (matched?.index !== undefined) {
context.currentDelimiter = matched[0].trim();
discard(context, matched[0].length);
}
discardTillNewLine(context);
break;
}
case undefined:
case null:
read(context, findResult.nextIndex);
publishStatement(context);
break;
default:
// This should never happen
throw new Error(`Unknown token '${findResult.exp ?? '(null)'}'`);
}
}
export function splitPostgresQuery(sql: string, options?: SplitOptions): string[] {
options = options ?? {};
const context: SplitExecutionContext = {
multipleStatements: options.multipleStatements ?? false,
retainComments: options.retainComments ?? false,
unread: sql,
currentDelimiter: SEMICOLON,
currentStatement: {
value: '',
supportMulti: true,
},
output: [],
};
let findResult: FindExpResult = {
expIndex: -1,
exp: null,
nextIndex: 0,
};
let lastUnreadLength;
do {
lastUnreadLength = context.unread.length;
findResult = findKeyToken(context.unread, context.currentDelimiter);
handleKeyTokenFindResult(context, findResult);
// Prevent infinite loop by returning incorrect result
if (lastUnreadLength === context.unread.length) {
read(context, context.unread.length);
}
} while (context.unread !== '');
publishStatement(context);
return context.output.map(v => v.value);
}
+1
View File
@@ -1,6 +1,7 @@
export interface SqlDialect {
rangeSelect?: boolean;
limitSelect?: boolean;
rowNumberOverPaging?: boolean;
stringEscapeChar: string;
offsetFetchRangeSyntax?: boolean;
quoteIdentifier(s: string): string;
+4
View File
@@ -37,6 +37,9 @@ export interface EngineDriver {
engine: string;
title: string;
defaultPort?: number;
supportsDatabaseUrl?: boolean;
isFileDatabase?: boolean;
databaseUrlPlaceholder?: string;
connect({ server, port, user, password, database }): any;
query(pool: any, sql: string): Promise<QueryResult>;
stream(pool: any, sql: string, options: StreamOptions);
@@ -59,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
View File
@@ -4,6 +4,7 @@ export interface OpenedDatabaseConnection {
conid: string;
database: string;
structure: DatabaseInfo;
serverVersion?: any;
subprocess: ChildProcess;
disconnected?: boolean;
status?: {
+1 -2
View File
@@ -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": [
+6 -6
View File
@@ -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"
}
}
}
+10 -1
View File
@@ -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
View File
@@ -72,6 +72,7 @@
}
.iconbar {
position: fixed;
display: flex;
left: 0;
top: var(--dim-header-top);
bottom: var(--dim-statusbar-height);
@@ -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}
@@ -61,11 +61,11 @@
},
{
label: 'SQL: CREATE TABLE',
sqlTemplate: 'CREATE TABLE',
scriptTemplate: 'CREATE TABLE',
},
{
label: 'SQL: SELECT',
sqlTemplate: 'SELECT',
scriptTemplate: 'SELECT',
},
{
label: 'SQL Generator: CREATE TABLE',
@@ -123,15 +123,15 @@
},
{
label: 'SQL: CREATE VIEW',
sqlTemplate: 'CREATE OBJECT',
scriptTemplate: 'CREATE OBJECT',
},
{
label: 'SQL: CREATE TABLE',
sqlTemplate: 'CREATE TABLE',
scriptTemplate: 'CREATE TABLE',
},
{
label: 'SQL: SELECT',
sqlTemplate: 'SELECT',
scriptTemplate: 'SELECT',
},
{
label: 'SQL Generator: CREATE VIEW',
@@ -149,11 +149,11 @@
procedures: [
{
label: 'SQL: CREATE PROCEDURE',
sqlTemplate: 'CREATE OBJECT',
scriptTemplate: 'CREATE OBJECT',
},
{
label: 'SQL: EXECUTE',
sqlTemplate: 'EXECUTE PROCEDURE',
scriptTemplate: 'EXECUTE PROCEDURE',
},
{
label: 'SQL Generator: CREATE PROCEDURE',
@@ -171,7 +171,7 @@
functions: [
{
label: 'SQL: CREATE FUNCTION',
sqlTemplate: 'CREATE OBJECT',
scriptTemplate: 'CREATE OBJECT',
},
{
label: 'SQL Generator: CREATE FUNCTION',
@@ -206,35 +206,46 @@
label: 'Export',
isExport: true,
},
{
divider: true,
},
{
label: 'JS: dropCollection()',
scriptTemplate: 'dropCollection',
},
{
label: 'JS: find()',
scriptTemplate: 'findCollection',
},
],
};
export async function openDatabaseObjectDetail(
tabComponent,
sqlTemplate,
scriptTemplate,
{ schemaName, pureName, conid, database, objectTypeField },
forceNewTab,
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,
})}`;
openNewTab(
{
title: sqlTemplate ? 'Query #' : pureName,
title: scriptTemplate ? 'Query #' : pureName,
tooltip,
icon: sqlTemplate ? 'img sql-file' : icons[objectTypeField],
tabComponent: sqlTemplate ? 'QueryTab' : tabComponent,
icon: scriptTemplate ? 'img sql-file' : icons[objectTypeField],
tabComponent: scriptTemplate ? 'QueryTab' : tabComponent,
props: {
schemaName,
pureName,
conid,
database,
objectTypeField,
initialArgs: sqlTemplate ? { sqlTemplate } : null,
initialArgs: scriptTemplate ? { scriptTemplate } : null,
},
},
initialData,
@@ -256,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;
@@ -387,7 +399,7 @@
database: data.database,
});
} else {
openDatabaseObjectDetail(menu.tab, menu.sqlTemplate, data, menu.forceNewTab, menu.initialData);
openDatabaseObjectDetail(menu.tab, menu.scriptTemplate, data, menu.forceNewTab, menu.initialData);
}
},
};
@@ -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(
+14 -1
View File
@@ -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,5 @@
<script lang="ts" context="module">
import { commands, visibleCommandPalette } from '../stores';
import { commandsCustomized, visibleCommandPalette } from '../stores';
import { get } from 'svelte/store';
import { runGroupCommand } from './runCommand';
@@ -12,7 +12,7 @@
// console.log('keyText', keyText);
const commandsValue = get(commands);
const commandsValue = get(commandsCustomized);
const commandsFiltered: any = Object.values(commandsValue).filter(
(x: any) =>
x.keyText &&
@@ -19,7 +19,7 @@
import _ from 'lodash';
import { onMount } from 'svelte';
import { commands, getVisibleCommandPalette, visibleCommandPalette } from '../stores';
import { commands, commandsCustomized, getVisibleCommandPalette, visibleCommandPalette } from '../stores';
import clickOutside from '../utility/clickOutside';
import keycodes from '../utility/keycodes';
import registerCommand from './registerCommand';
@@ -40,7 +40,7 @@
});
$: sortedComands = _.sortBy(
Object.values($commands).filter(x => x.enabled),
Object.values($commandsCustomized).filter(x => x.enabled),
'text'
);
@@ -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) {
+36 -3
View File
@@ -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,6 +205,30 @@ registerCommand({
}),
});
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,
category,
@@ -215,6 +241,7 @@ export function registerFileCommands({
toggleComment = false,
findReplace = false,
undoRedo = false,
executeAdditionalCondition = null,
}) {
if (save) {
registerCommand({
@@ -225,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),
});
@@ -245,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({
@@ -255,6 +287,7 @@ export function registerFileCommands({
name: 'Kill',
icon: 'icon close',
toolbar: true,
isRelatedToTab: true,
testEnabled: () => getCurrentEditor()?.canKill && getCurrentEditor().canKill(),
onClick: () => getCurrentEditor().kill(),
});
@@ -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>
@@ -1,4 +1,23 @@
<script context="module" lang="ts">
const getCurrentEditor = () => getActiveComponent('CollectionDataGridCore');
registerCommand({
id: 'collectionDataGrid.openQuery',
category: 'Data grid',
name: 'Open query',
testEnabled: () => getCurrentEditor() != null,
onClick: () => getCurrentEditor().openQuery(),
});
registerCommand({
id: 'collectionDataGrid.export',
category: 'Data grid',
name: 'Export',
keyText: 'Ctrl+E',
testEnabled: () => getCurrentEditor() != null,
onClick: () => getCurrentEditor().exportGrid(),
});
function buildGridMongoCondition(props) {
const filters = props?.display?.config?.filters;
@@ -100,6 +119,7 @@
import { parseFilter } from 'dbgate-filterparser';
import { scriptToSql } from 'dbgate-sqltree';
import _ from 'lodash';
import registerCommand from '../commands/registerCommand';
import ErrorInfo from '../elements/ErrorInfo.svelte';
import ConfirmNoSqlModal from '../modals/ConfirmNoSqlModal.svelte';
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
@@ -107,6 +127,8 @@
import { showModal } from '../modals/modalTools';
import axiosInstance from '../utility/axiosInstance';
import { registerMenu } from '../utility/contextMenu';
import createActivator, { getActiveComponent } from '../utility/createActivator';
import openNewTab from '../utility/openNewTab';
import ChangeSetGrider from './ChangeSetGrider';
@@ -129,6 +151,8 @@
export let loadedRows = [];
export const activator = createActivator('CollectionDataGridCore', false);
// $: console.log('loadedRows BIND', loadedRows);
$: grider = new ChangeSetGrider(
loadedRows,
@@ -141,6 +165,44 @@
);
// $: console.log('GRIDER', grider);
// $: if (onChangeGrider) onChangeGrider(grider);
function getExportQuery() {
return `db.collection('${pureName}')
.find(${JSON.stringify(buildGridMongoCondition($$props) || {})})
.sort(${JSON.stringify(buildMongoSort($$props) || {})})`;
}
export function exportGrid() {
const initialValues: any = {};
initialValues.sourceStorageType = 'query';
initialValues.sourceConnectionId = conid;
initialValues.sourceDatabaseName = database;
initialValues.sourceSql = getExportQuery();
initialValues.sourceList = [pureName];
showModal(ImportExportModal, { initialValues });
}
export function openQuery() {
openNewTab(
{
title: 'Query #',
icon: 'img sql-file',
tabComponent: 'QueryTab',
props: {
conid,
database,
},
},
{
editor: getExportQuery(),
}
);
}
registerMenu(
{ command: 'collectionDataGrid.openQuery', tag: 'export' },
{ command: 'collectionDataGrid.export', tag: 'export' }
);
</script>
<LoadingDataGridCore
@@ -226,6 +226,7 @@
on:paste={handlePaste}
class:isError
class:isOk
placeholder='Filter'
/>
<DropDownButton icon="icon filter" menu={createMenu} />
{#if showResizeSplitter}
+23 -9
View File
@@ -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}
+19 -3
View File
@@ -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;
@@ -39,6 +39,7 @@
export let hideContent = false;
export let onSetFormView;
export let isDynamicStructure = false;
export let isAutoFillMarker = false;
$: value = col.isStructured ? _.get(rowData || {}, col.uniquePath) : (rowData || {})[col.uniqueName];
</script>
@@ -99,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}
@@ -114,6 +115,10 @@
<ShowFormButton on:click={() => onSetFormView(rowData, col)} />
{/if}
{/if}
{#if isAutoFillMarker}
<div class="autoFillMarker autofillHandleMarker" />
{/if}
</td>
<!-- {#if _.isArray(value.data)}
@@ -175,4 +180,15 @@
.value {
color: var(--theme-icon-green);
}
.autoFillMarker {
width: 8px;
height: 8px;
background: var(--theme-bg-selected-point);
position: absolute;
right: 0px;
bottom: 0px;
overflow: visible;
cursor: crosshair;
}
</style>
+21 -2
View File
@@ -7,6 +7,7 @@
name: 'Refresh',
keyText: 'F5',
toolbar: true,
isRelatedToTab: true,
icon: 'icon reload',
testEnabled: () => getCurrentDataGrid()?.getDisplay()?.supportsReload,
onClick: () => getCurrentDataGrid().refresh(),
@@ -63,6 +64,7 @@
group: 'undo',
icon: 'icon undo',
toolbar: true,
isRelatedToTab: true,
testEnabled: () => getCurrentDataGrid()?.getGrider()?.canUndo,
onClick: () => getCurrentDataGrid().undo(),
});
@@ -74,6 +76,7 @@
group: 'redo',
icon: 'icon redo',
toolbar: true,
isRelatedToTab: true,
testEnabled: () => getCurrentDataGrid()?.getGrider()?.canRedo,
onClick: () => getCurrentDataGrid().redo(),
});
@@ -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"
+5 -1
View File
@@ -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)}
@@ -64,6 +64,10 @@
(rowStatus.deletedFields && rowStatus.deletedFields.has(col.uniqueName))}
{onSetFormView}
{isDynamicStructure}
isAutoFillMarker={autofillMarkerCell &&
autofillMarkerCell[1] == col.colIndex &&
autofillMarkerCell[0] == rowIndex &&
grider.editable}
/>
{/if}
{/each}
@@ -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
+38 -23
View File
@@ -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)) {
+3
View File
@@ -54,6 +54,9 @@ export function countColumnSizes(grider: Grider, columns, containerWidth, displa
context.font = '14px Helvetica';
for (let rowIndex = 0; rowIndex < Math.min(grider.rowCount, 20); rowIndex += 1) {
const row = grider.getRowData(rowIndex);
if (!row) {
continue;
}
for (let colIndex = 0; colIndex < columns.length; colIndex++) {
const uqName = columns[colIndex].uniqueName;
@@ -13,5 +13,5 @@
<span class="nowrap">
<FontIcon icon={getConstraintIcon(constraintType)} />
{constraintName}
{constraintName || '(without name)'}
</span>
@@ -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>
@@ -1,5 +1,6 @@
<script lang="ts">
import _ from 'lodash';
import DropDownButton from './DropDownButton.svelte';
interface TabDef {
label: string;
@@ -10,6 +11,7 @@
export let tabs: TabDef[];
export let value = 0;
export let menu = null;
export let isInline = false;
export function setValue(index) {
@@ -29,6 +31,9 @@
</span>
</div>
{/each}
{#if menu}
<DropDownButton {menu} />
{/if}
</div>
<div class="content-container">
+17 -2
View File
@@ -6,24 +6,28 @@
getProps?: any;
formatter?: any;
slot?: number;
isHighlighted?: Function;
}
</script>
<script lang="ts">
import _ from 'lodash';
import { compact } from 'lodash';
import { onMount } from 'svelte';
import keycodes from '../utility/keycodes';
import { createEventDispatcher } from 'svelte';
export let columns: TableControlColumn[];
export let rows;
export let focusOnCreate = false;
export let selectable = false;
export let selectedIndex = 0;
export let clickable = false;
export let domTable;
const dispatch = createEventDispatcher();
$: columnList = _.compact(_.flatten(columns));
onMount(() => {
@@ -58,15 +62,19 @@
{#each rows as row, index}
<tr
class:selected={selectable && selectedIndex == index}
class:clickable
on:click={() => {
if (selectable) {
selectedIndex = index;
domTable.focus();
}
if (clickable) {
dispatch('clickrow', row);
}
}}
>
{#each columnList as col}
<td>
<td class:isHighlighted={col.isHighlighted && col.isHighlighted(row)}>
{#if col.component}
<svelte:component this={col.component} {...col.getProps(row)} />
{:else if col.formatter}
@@ -106,6 +114,9 @@
tbody tr.selected {
background: var(--theme-bg-selected);
}
tbody tr.clickable:hover {
background: var(--theme-bg-hover);
}
thead td {
border: 1px solid var(--theme-border);
background-color: var(--theme-bg-1);
@@ -115,4 +126,8 @@
border: 1px solid var(--theme-border);
padding: 5px;
}
td.isHighlighted {
background-color: var(--theme-bg-1);
}
</style>
@@ -0,0 +1,22 @@
<script lang="ts">
import { getFormContext } from './FormProviderCore.svelte';
import uuidv1 from 'uuid/v1';
export let options = [];
export let name;
const { values, setFieldValue } = getFormContext();
let group = $values[name] ?? options.find(x => x.default)?.value;
$: setFieldValue(name, group);
$: optionsWithId = options.map(x => ({ ...x, id: uuidv1() }));
</script>
{#each optionsWithId as option}
<div>
<input type="radio" bind:group value={option.value} id={option.id} />
<label for={option.id}>{option.label}</label>
</div>
{/each}
@@ -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>
+5
View File
@@ -17,6 +17,9 @@
'icon connection': 'mdi mdi-connection',
'icon cell-data': 'mdi mdi-details',
'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',
@@ -46,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 -17
View File
@@ -15,11 +15,7 @@
const { values, setFieldValue } = getFormContext();
$: dbinfo = useDatabaseInfo({ conid: $values[conidName], database: $values[databaseName] });
$: tablesOptions = [
...(($dbinfo && $dbinfo.tables) || []),
...(($dbinfo && $dbinfo.views) || []),
...(($dbinfo && $dbinfo.collections) || []),
]
$: tablesOptions = _.compact([...($dbinfo?.tables || []), ...($dbinfo?.views || []), ...($dbinfo?.collections || [])])
.filter(x => !$values[schemaName] || x.schemaName == $values[schemaName])
.map(x => ({
value: x.pureName,
@@ -31,18 +27,20 @@
<FormSelectField {...$$restProps} {name} options={tablesOptions} isMulti templateProps={{ noMargin: true }} />
<div>
<FormStyledButton
type="button"
value="All tables"
on:click={() =>
setFieldValue(name, _.uniq([...($values[name] || []), ...($dbinfo && $dbinfo.tables.map(x => x.pureName))]))}
/>
<FormStyledButton
type="button"
value="All views"
on:click={() =>
setFieldValue(name, _.uniq([...($values[name] || []), ...($dbinfo && $dbinfo.views.map(x => x.pureName))]))}
/>
{#each ['tables', 'views', 'collections'] as field}
{#if $dbinfo && $dbinfo[field]?.length > 0}
<FormStyledButton
type="button"
value={`All ${field}`}
on:click={() =>
setFieldValue(
name,
_.compact(_.uniq([...($values[name] || []), ...($dbinfo[field]?.map(x => x.pureName) || [])]))
)}
/>
{/if}
{/each}
<FormStyledButton type="button" value="Remove all" on:click={() => setFieldValue(name, [])} />
</div>
</div>
@@ -42,7 +42,7 @@
label: `${format.name} files(s)`,
directions: getFileFormatDirections(format),
})),
{ value: 'query', label: 'SQL Query', directions: ['source'] },
{ value: 'query', label: 'Query', directions: ['source'] },
{ value: 'archive', label: 'Archive', directions: ['source', 'target'] },
];

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