Compare commits
230 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 688086e00f | |||
| 09498f2ac3 | |||
| 7dd9e9a9b1 | |||
| 19d135e435 | |||
| d453e52ff3 | |||
| 29a77cc053 | |||
| 11fd2d1d8a | |||
| b5fe8508b1 | |||
| 25881e80db | |||
| e43fa96e34 | |||
| 0200c7c78b | |||
| 42e573a3ae | |||
| 395b0a91b0 | |||
| 62c529cf50 | |||
| 00a169725e | |||
| bcf0bfd5ef | |||
| 52ed8874e3 | |||
| 319e08f5f3 | |||
| c4491050cd | |||
| f0ea35d576 | |||
| 70d53e8abe | |||
| 8f28ce3659 | |||
| 050b46813f | |||
| 4bae23ecfa | |||
| 6eb16ad750 | |||
| 482a823f4f | |||
| 9d933d669a | |||
| e44a95d723 | |||
| cae882c8d6 | |||
| 026726a6ed | |||
| 70d06deeb0 | |||
| 6dfe9b798b | |||
| 73c14eba6d | |||
| 7c91dda170 | |||
| 40ebedaef0 | |||
| 614f852f71 | |||
| a8e3a6cfec | |||
| be053acf3c | |||
| 91741655b7 | |||
| c57a67da09 | |||
| 2376cb30db | |||
| 3a08462018 | |||
| c95677bd83 | |||
| 8bffa4a7dd | |||
| 6d7cc7d441 | |||
| acc49273c1 | |||
| 640b53e45f | |||
| 7857771056 | |||
| b8513b3ecd | |||
| 0a56e3b782 | |||
| 87e75c6ba1 | |||
| cf5afb43eb | |||
| 2eb1c04fcf | |||
| 4a4c4b41c0 | |||
| 032eaf9eb0 | |||
| 06a028a093 | |||
| 21ceaecec6 | |||
| c5605d63ca | |||
| f9545eaf7f | |||
| 216ef7736b | |||
| ae7697f655 | |||
| 23225cf86b | |||
| 63ad36f758 | |||
| 80e1563877 | |||
| 3f5c7aecd7 | |||
| abd2492889 | |||
| 872468899d | |||
| 7a008e5a9d | |||
| 23940aa324 | |||
| 1888de8728 | |||
| 615397f332 | |||
| e251459512 | |||
| a9c8cee08a | |||
| 1638095c98 | |||
| 62cedd23b7 | |||
| 3d882f47a7 | |||
| 88ddc28208 | |||
| 800666f813 | |||
| 0b8add848a | |||
| cd7edcb443 | |||
| e483fd9e99 | |||
| 9664e6f981 | |||
| d1429dd2a1 | |||
| e739aed80d | |||
| 28e19402f3 | |||
| 45a065f391 | |||
| 67e8eb32f7 | |||
| 5622e3af77 | |||
| 7d34458553 | |||
| 8b747796e7 | |||
| 4802c36b54 | |||
| 988e4345d4 | |||
| e02305879e | |||
| 8baad56315 | |||
| 14bbc7b057 | |||
| 7b6ca27b66 | |||
| 38aae142ea | |||
| bd6c116cc0 | |||
| 4522c37bfa | |||
| 7d789d5712 | |||
| c4c2274488 | |||
| a8b71d452b | |||
| c7d69b0fb5 | |||
| 47ea474555 | |||
| e647ab471e | |||
| fd6524867e | |||
| c24cc1dc72 | |||
| e3d1e4f53e | |||
| 7b32424143 | |||
| 519767fd49 | |||
| 505ab2e075 | |||
| 00d0c27502 | |||
| d171d7d785 | |||
| 09593e0b22 | |||
| 771ca6ad83 | |||
| 83014d3a5b | |||
| caa2d22dbd | |||
| 3c089a5b81 | |||
| d1bf2dbc4b | |||
| a8a9afc936 | |||
| d0cbd5d0a4 | |||
| 67e1913683 | |||
| 8ff706a17f | |||
| 08692dc63f | |||
| 41d85d4117 | |||
| f343d414ef | |||
| 6cda7b2508 | |||
| 9085d49d21 | |||
| bb11d7e62b | |||
| 7524b30f50 | |||
| c30724c5da | |||
| 1e4c108f6f | |||
| 72033e5830 | |||
| 1d24fd9942 | |||
| e104feef14 | |||
| ccdce6ef43 | |||
| fccd550d4b | |||
| 3a4a10985b | |||
| 8ee96bd4a0 | |||
| 269046daa5 | |||
| 4738113ce3 | |||
| f7a2931253 | |||
| d832057076 | |||
| 00fdf14b6e | |||
| ab26f4624a | |||
| 8caf5d622e | |||
| 0cf8fc79c2 | |||
| 65aa6067e1 | |||
| 9a2d56bfe4 | |||
| a1b8e7b641 | |||
| 137bb7b002 | |||
| e83946f35e | |||
| 64af838f40 | |||
| 0d31cc4204 | |||
| 73a1fce919 | |||
| e8d5bdbfaf | |||
| 2e37af1ee4 | |||
| 55564ef82a | |||
| 2461b48244 | |||
| b05f91f4cb | |||
| 238b6d94d1 | |||
| 8ee2db1bec | |||
| 484aa932d3 | |||
| 29aa59771c | |||
| cef6b8520e | |||
| 49f8fb71e4 | |||
| 375a441abf | |||
| 0848008302 | |||
| cacd6ae849 | |||
| e97388e14b | |||
| 67b57ab756 | |||
| bcf183abe2 | |||
| f92df5c326 | |||
| 28bbf9a01e | |||
| 08d6f83a48 | |||
| 90af165afd | |||
| 8a4ee3e01e | |||
| 977818253d | |||
| ec5db6d562 | |||
| 2d4098ff6a | |||
| 321d95f522 | |||
| 53480210d4 | |||
| 0b1a4ee33f | |||
| 477099e508 | |||
| 516d007c22 | |||
| ab4febf938 | |||
| 361875d7fc | |||
| c0c1f9d786 | |||
| 1d264ab559 | |||
| 553329688a | |||
| 585731a1b3 | |||
| a6207f01af | |||
| 76e51343d0 | |||
| 6c246c9eaa | |||
| 479cec4209 | |||
| 6b85870523 | |||
| a98380a941 | |||
| 89a3798d56 | |||
| bf202719eb | |||
| 9d9b970fd5 | |||
| 56fcfd84e5 | |||
| e7d575dc8e | |||
| b61c454a3a | |||
| a2af86c705 | |||
| 2594be70fc | |||
| 6ff63e40f0 | |||
| a33f09c185 | |||
| 09f14a0717 | |||
| 9139ff0f44 | |||
| 1cc955f997 | |||
| 4dfaf1346e | |||
| 419ab985c1 | |||
| c5ec22d504 | |||
| 5dd03484ea | |||
| 4d5cc119f2 | |||
| 446e7c139f | |||
| 5a6641bc6e | |||
| 43c00e88bb | |||
| 297df772a1 | |||
| 449bbde645 | |||
| b222b916ec | |||
| d3d695ed81 | |||
| c1778bea26 | |||
| 1d401e302a | |||
| 12fd5f6943 | |||
| 817286d326 | |||
| cc2c55b20f | |||
| 90169a7624 | |||
| 88b4c9daff | |||
| e8b43820b9 |
@@ -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
|
||||
@@ -35,6 +35,9 @@ jobs:
|
||||
- name: fillNativeModulesElectron
|
||||
run: |
|
||||
yarn fillNativeModulesElectron
|
||||
- name: fillPackagedPlugins
|
||||
run: |
|
||||
yarn fillPackagedPlugins
|
||||
- name: Install Snapcraft
|
||||
if: matrix.os == 'ubuntu-18.04'
|
||||
uses: samuelmeuli/action-snapcraft@v1
|
||||
@@ -63,11 +66,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
|
||||
|
||||
@@ -39,6 +39,9 @@ jobs:
|
||||
- name: fillNativeModulesElectron
|
||||
run: |
|
||||
yarn fillNativeModulesElectron
|
||||
- name: fillPackagedPlugins
|
||||
run: |
|
||||
yarn fillPackagedPlugins
|
||||
- name: Install Snapcraft
|
||||
if: matrix.os == 'ubuntu-18.04'
|
||||
uses: samuelmeuli/action-snapcraft@v1
|
||||
@@ -74,9 +77,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
|
||||
|
||||
@@ -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
|
||||
@@ -89,3 +89,38 @@ 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
|
||||
|
||||
- name: Publish dbgate-plugin-sqlite
|
||||
working-directory: plugins/dbgate-plugin-sqlite
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
@@ -13,8 +13,10 @@ build
|
||||
dist
|
||||
|
||||
app/packages/web/public
|
||||
app/packages/plugins
|
||||
docker/public
|
||||
docker/bundle.js
|
||||
docker/plugins
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
@@ -28,4 +30,5 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
app/src/nativeModulesContent.js
|
||||
packages/api/src/nativeModulesContent.js
|
||||
packages/api/src/packagedPluginsContent.js
|
||||
.VSCodeCounter
|
||||
@@ -1,5 +1,63 @@
|
||||
# ChangeLog
|
||||
|
||||
### 4.2.2
|
||||
- CHANGED: Further startup optimalization (approx. 2 times quicker start of electron app)
|
||||
|
||||
### 4.2.1
|
||||
- FIXED: Fixed+optimalized app startup (esp. on Windows)
|
||||
|
||||
### 4.2.0
|
||||
- ADDED: Support of SQLite database
|
||||
- ADDED: Support of Amazon Redshift database
|
||||
- ADDED: Support of CockcroachDB
|
||||
- CHANGED: DB Model is not auto-refreshed by default, refresh could be invoked from statusbar
|
||||
- FIXED: Fixed race conditions on startup
|
||||
- FIXED: Fixed broken style in data grid under strange circumstances
|
||||
- ADDED: Configure connections with commandline arguments #108
|
||||
- CHANGED: Optimalized algorithm of incremental DB model updates
|
||||
- CHANGED: Loading queries from PostgreSQL doesn't need cursors, using streamed query instead
|
||||
- ADDED: Disconnect command
|
||||
- ADDED: Query executed on server has tab marker (formerly it had only "No DB" marker)
|
||||
- ADDED: Horizontal scroll using shift+mouse wheel #113
|
||||
- ADDED: Cosmetic improvements of MariaDB support
|
||||
|
||||
### 4.1.11
|
||||
- FIX: Fixed crash of API process when using SSH tunnel connection (race condition)
|
||||
|
||||
### 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
|
||||
|
||||
@@ -1,27 +1,38 @@
|
||||
[](https://www.npmjs.com/package/dbgate)
|
||||

|
||||
[](https://snapcraft.io/dbgate)
|
||||
[](https://snapcraft.io/dbgate)
|
||||
[](https://github.com/prettier/prettier)
|
||||
[](https://paypal.me/JanProchazkaCz/30eur)
|
||||
|
||||
# DbGate - database administration tool
|
||||
|
||||
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
|
||||
* SQLite
|
||||
* Amazon Redshift
|
||||
* CockroachDB
|
||||
* MariaDB
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
* Connect to Microsoft SQL Server, Postgre SQL, MySQL
|
||||
* 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 +67,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 +76,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 +91,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
|
||||
|
||||
+14
-6
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"name": "dbgate",
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.1",
|
||||
"private": true,
|
||||
"author": "Jan Prochazka <jenasoft.database@gmail.com>",
|
||||
"description": "Opensource database administration tool",
|
||||
"dependencies": {
|
||||
"better-sqlite3-with-prebuilds": "^7.1.8",
|
||||
"electron-log": "^4.3.1",
|
||||
"electron-store": "^5.1.1",
|
||||
"electron-updater": "^4.3.5"
|
||||
@@ -21,7 +22,13 @@
|
||||
"artifactName": "dbgate-mac-${version}.${ext}",
|
||||
"publish": [
|
||||
"github"
|
||||
]
|
||||
],
|
||||
"target": {
|
||||
"target": "default",
|
||||
"arch": [
|
||||
"x64"
|
||||
]
|
||||
}
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
@@ -32,7 +39,7 @@
|
||||
"icon": "icon.png",
|
||||
"artifactName": "dbgate-linux-${version}.${ext}",
|
||||
"category": "Development",
|
||||
"synopsis": "Database administration tool for MS SQL, MySQL and PostgreSQL",
|
||||
"synopsis": "Database manager for SQL Server, MySQL, PostgreSQL, MongoDB and SQLite",
|
||||
"publish": [
|
||||
"github"
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-26
@@ -19,7 +19,6 @@ const store = new Store();
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
let mainWindow;
|
||||
let splashWindow;
|
||||
let mainMenu;
|
||||
|
||||
log.transports.file.level = 'debug';
|
||||
@@ -29,14 +28,6 @@ autoUpdater.logger = log;
|
||||
|
||||
let commands = {};
|
||||
|
||||
function hideSplash() {
|
||||
if (splashWindow) {
|
||||
splashWindow.destroy();
|
||||
splashWindow = null;
|
||||
}
|
||||
mainWindow.show();
|
||||
}
|
||||
|
||||
function commandItem(id) {
|
||||
const command = commands[id];
|
||||
return {
|
||||
@@ -156,7 +147,6 @@ function createWindow() {
|
||||
title: 'DbGate',
|
||||
...bounds,
|
||||
icon: os.platform() == 'win32' ? 'icon.ico' : path.resolve(__dirname, '../icon.png'),
|
||||
show: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true,
|
||||
@@ -175,7 +165,7 @@ function createWindow() {
|
||||
slashes: true,
|
||||
});
|
||||
mainWindow.webContents.on('did-finish-load', function () {
|
||||
hideSplash();
|
||||
// hideSplash();
|
||||
});
|
||||
mainWindow.on('close', () => {
|
||||
store.set('winBounds', mainWindow.getBounds());
|
||||
@@ -186,33 +176,21 @@ function createWindow() {
|
||||
}
|
||||
}
|
||||
|
||||
splashWindow = new BrowserWindow({
|
||||
width: 300,
|
||||
height: 120,
|
||||
transparent: true,
|
||||
frame: false,
|
||||
});
|
||||
splashWindow.loadURL(
|
||||
url.format({
|
||||
pathname: path.join(__dirname, '../packages/web/build/splash.html'),
|
||||
protocol: 'file:',
|
||||
slashes: true,
|
||||
})
|
||||
);
|
||||
|
||||
if (process.env.ELECTRON_START_URL) {
|
||||
loadMainWindow();
|
||||
} else {
|
||||
const apiProcess = fork(path.join(__dirname, '../packages/api/dist/bundle.js'), [
|
||||
'--dynport',
|
||||
'--is-electron-bundle',
|
||||
'--native-modules',
|
||||
path.join(__dirname, 'nativeModules'),
|
||||
// '../../../src/nativeModules'
|
||||
]);
|
||||
apiProcess.on('message', msg => {
|
||||
if (msg.msgtype == 'listening') {
|
||||
const { port } = msg;
|
||||
const { port, authorization } = msg;
|
||||
global['port'] = port;
|
||||
global['authorization'] = authorization;
|
||||
loadMainWindow();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -60,6 +60,11 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/integer@latest":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/integer/-/integer-4.0.0.tgz#3b778715df72d2cf8ba73bad27bd9d830907f944"
|
||||
integrity sha512-2U1i6bIRiqizl6O+ETkp2HhUZIxg7g+burUabh9tzGd0qcszfNaFRaY9bGNlQKgEU7DCsH5qMajRDW5QamWQbw==
|
||||
|
||||
"@types/node@*":
|
||||
version "13.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.9.1.tgz#96f606f8cd67fb018847d9b61e93997dabdefc72"
|
||||
@@ -232,6 +237,23 @@ base64-js@^1.3.1:
|
||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
|
||||
better-sqlite3-with-prebuilds@^7.1.8:
|
||||
version "7.1.8"
|
||||
resolved "https://registry.yarnpkg.com/better-sqlite3-with-prebuilds/-/better-sqlite3-with-prebuilds-7.1.8.tgz#3090c478fe9b60e74ce053a76807b189784f62d7"
|
||||
integrity sha512-trwg1qhN91cPYEB8D2K0KVHIsMsiAnxKx6/syfQ7rLrtD+zOS3fqJq4VGszMF+OuYAZJNAR4oLsikys3YW/6aA==
|
||||
dependencies:
|
||||
"@types/integer" latest
|
||||
bindings "^1.5.0"
|
||||
prebuild-install "^6.0.1"
|
||||
tar "^6.1.0"
|
||||
|
||||
bindings@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
|
||||
integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
|
||||
dependencies:
|
||||
file-uri-to-path "1.0.0"
|
||||
|
||||
bl@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489"
|
||||
@@ -369,6 +391,11 @@ chownr@^1.1.1:
|
||||
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
|
||||
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
|
||||
|
||||
chownr@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
|
||||
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
|
||||
|
||||
chromium-pickle-js@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz#04a106672c18b085ab774d983dfa3ea138f22205"
|
||||
@@ -815,6 +842,11 @@ fd-slicer@~1.0.1:
|
||||
dependencies:
|
||||
pend "~1.2.0"
|
||||
|
||||
file-uri-to-path@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
|
||||
integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
|
||||
|
||||
filelist@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.1.tgz#f10d1a3ae86c1694808e8f20906f43d4c9132dbb"
|
||||
@@ -853,6 +885,13 @@ fs-extra@^9.0.1:
|
||||
jsonfile "^6.0.1"
|
||||
universalify "^1.0.0"
|
||||
|
||||
fs-minipass@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
|
||||
integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
|
||||
dependencies:
|
||||
minipass "^3.0.0"
|
||||
|
||||
fs.realpath@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
@@ -1295,6 +1334,21 @@ minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5:
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||
|
||||
minipass@^3.0.0:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd"
|
||||
integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==
|
||||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
|
||||
minizlib@^2.1.1:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
|
||||
integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
|
||||
dependencies:
|
||||
minipass "^3.0.0"
|
||||
yallist "^4.0.0"
|
||||
|
||||
mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
|
||||
version "0.5.3"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
|
||||
@@ -1307,6 +1361,11 @@ mkdirp@0.5.1, mkdirp@^0.5.1:
|
||||
dependencies:
|
||||
minimist "0.0.8"
|
||||
|
||||
mkdirp@^1.0.3:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
||||
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
||||
|
||||
ms@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
@@ -1335,6 +1394,13 @@ napi-build-utils@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806"
|
||||
integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==
|
||||
|
||||
node-abi@^2.21.0:
|
||||
version "2.26.0"
|
||||
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.26.0.tgz#355d5d4bc603e856f74197adbf3f5117a396ba40"
|
||||
integrity sha512-ag/Vos/mXXpWLLAYWsAoQdgS+gW7IwvgMLOgqopm/DbzAjazLltzgzpVMsFlgmo9TzG5hGXeaBZx2AI731RIsQ==
|
||||
dependencies:
|
||||
semver "^5.4.1"
|
||||
|
||||
node-abi@^2.7.0:
|
||||
version "2.19.3"
|
||||
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.19.3.tgz#252f5dcab12dad1b5503b2d27eddd4733930282d"
|
||||
@@ -1509,6 +1575,26 @@ prebuild-install@^6.0.0:
|
||||
tunnel-agent "^0.6.0"
|
||||
which-pm-runs "^1.0.0"
|
||||
|
||||
prebuild-install@^6.0.1:
|
||||
version "6.1.2"
|
||||
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-6.1.2.tgz#6ce5fc5978feba5d3cbffedca0682b136a0b5bff"
|
||||
integrity sha512-PzYWIKZeP+967WuKYXlTOhYBgGOvTRSfaKI89XnfJ0ansRAH7hDU45X+K+FZeI1Wb/7p/NnuctPH3g0IqKUuSQ==
|
||||
dependencies:
|
||||
detect-libc "^1.0.3"
|
||||
expand-template "^2.0.3"
|
||||
github-from-package "0.0.0"
|
||||
minimist "^1.2.3"
|
||||
mkdirp-classic "^0.5.3"
|
||||
napi-build-utils "^1.0.1"
|
||||
node-abi "^2.21.0"
|
||||
noop-logger "^0.1.1"
|
||||
npmlog "^4.0.1"
|
||||
pump "^3.0.0"
|
||||
rc "^1.2.7"
|
||||
simple-get "^3.0.3"
|
||||
tar-fs "^2.0.0"
|
||||
tunnel-agent "^0.6.0"
|
||||
|
||||
prepend-http@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
|
||||
@@ -1944,6 +2030,18 @@ tar-stream@^2.1.4:
|
||||
inherits "^2.0.3"
|
||||
readable-stream "^3.1.1"
|
||||
|
||||
tar@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83"
|
||||
integrity sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==
|
||||
dependencies:
|
||||
chownr "^2.0.0"
|
||||
fs-minipass "^2.0.0"
|
||||
minipass "^3.0.0"
|
||||
minizlib "^2.1.1"
|
||||
mkdirp "^1.0.3"
|
||||
yallist "^4.0.0"
|
||||
|
||||
temp-file@^3.3.7:
|
||||
version "3.3.7"
|
||||
resolved "https://registry.yarnpkg.com/temp-file/-/temp-file-3.3.7.tgz#686885d635f872748e384e871855958470aeb18a"
|
||||
|
||||
@@ -2,10 +2,10 @@ const fs = require('fs');
|
||||
|
||||
let fillContent = '';
|
||||
|
||||
// if (!process.argv.includes('--electron')) {
|
||||
if (process.platform == 'win32') {
|
||||
fillContent += `content.msnodesqlv8 = () => require('msnodesqlv8');`;
|
||||
}
|
||||
fillContent += `content['better-sqlite3-with-prebuilds'] = () => require('better-sqlite3-with-prebuilds');`;
|
||||
|
||||
const getContent = (empty) => `
|
||||
// this file is generated automatically by script fillNativeModules.js, do not edit it manually
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function load() {
|
||||
const plugins = {};
|
||||
|
||||
for (const packageName of fs.readdirSync('plugins')) {
|
||||
if (!packageName.startsWith('dbgate-plugin-')) continue;
|
||||
const dir = path.join('plugins', packageName);
|
||||
const frontend = fs.readFileSync(path.join(dir, 'dist', 'frontend.js'), 'utf-8');
|
||||
const readme = fs.readFileSync(path.join(dir, 'README.md'), 'utf-8');
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8'));
|
||||
plugins[packageName] = {
|
||||
manifest,
|
||||
frontend,
|
||||
readme,
|
||||
};
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
||||
fs.writeFileSync('packages/api/src/packagedPluginsContent.js', `module.exports = () => (${JSON.stringify(load())});`);
|
||||
+17
-11
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "4.0.3",
|
||||
"version": "4.2.2",
|
||||
"name": "dbgate-all",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
"packages/*",
|
||||
"plugins/*"
|
||||
],
|
||||
"scripts": {
|
||||
"start:api": "yarn workspace dbgate-api start",
|
||||
@@ -19,25 +20,29 @@
|
||||
"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",
|
||||
"fillPackagedPlugins": "node fillPackagedPlugins",
|
||||
"resetPackagedPlugins": "node resetPackagedPlugins",
|
||||
"prettier": "prettier --write packages/api/src && prettier --write packages/datalib/src && prettier --write packages/filterparser/src && prettier --write packages/sqltree/src && prettier --write packages/tools/src && prettier --write packages/types && prettier --write packages/web/src && prettier --write app/src",
|
||||
"copy:docker:build": "copyfiles packages/api/dist/* docker -f && copyfiles packages/web/public/* docker -u 2 && copyfiles \"packages/web/public/**/*\" docker -u 2",
|
||||
"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 resetPackagedPlugins && yarn build:lib && patch-package && yarn fillNativeModules && yarn build:plugins:frontend"
|
||||
},
|
||||
"dependencies": {
|
||||
"concurrently": "^5.1.0",
|
||||
@@ -46,6 +51,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"copyfiles": "^2.2.0",
|
||||
"prettier": "^2.2.1"
|
||||
"prettier": "^2.2.1",
|
||||
"workspaces-run": "^1.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
CONNECTIONS=mysql
|
||||
|
||||
LABEL_mysql=MySql
|
||||
SERVER_mysql=dbgate.org
|
||||
USER_mysql=reader
|
||||
PASSWORD_mysql=CovidReader2020
|
||||
PORT_mysql=3326
|
||||
ENGINE_mysql=mysql@dbgate-plugin-mysql
|
||||
SINGLE_CONNECTION=mysql
|
||||
SINGLE_DATABASE=covid
|
||||
|
||||
PERMISSIONS=files/charts/read
|
||||
+1
-15
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,15 @@
|
||||
DEVMODE=1
|
||||
|
||||
CONNECTIONS=mysql
|
||||
|
||||
LABEL_mysql=MySql localhost
|
||||
SERVER_mysql=localhost
|
||||
USER_mysql=root
|
||||
PASSWORD_mysql=test
|
||||
PORT_mysql=3307
|
||||
ENGINE_mysql=mysql@dbgate-plugin-mysql
|
||||
|
||||
SINGLE_CONNECTION=mysql
|
||||
SINGLE_DATABASE=Chinook
|
||||
|
||||
PERMISSIONS=files/charts/read
|
||||
+13
-10
@@ -1,13 +1,12 @@
|
||||
{
|
||||
"name": "dbgate-api",
|
||||
"main": "src/index.js",
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.1",
|
||||
"homepage": "https://dbgate.org/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
|
||||
"author": "Jan Prochazka",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
@@ -20,20 +19,21 @@
|
||||
"dependencies": {
|
||||
"async-lock": "^1.2.4",
|
||||
"axios": "^0.19.0",
|
||||
"better-sqlite3-with-prebuilds": "^7.1.8",
|
||||
"body-parser": "^1.19.0",
|
||||
"bufferutil": "^4.0.1",
|
||||
"byline": "^5.0.0",
|
||||
"compare-versions": "^3.6.0",
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^6.0.3",
|
||||
"dbgate-sqltree": "^4.0.0",
|
||||
"dbgate-tools": "^4.0.0",
|
||||
"dbgate-sqltree": "^4.1.1",
|
||||
"dbgate-tools": "^4.1.1",
|
||||
"eslint": "^6.8.0",
|
||||
"express": "^4.17.1",
|
||||
"express-basic-auth": "^1.2.0",
|
||||
"express-fileupload": "^1.2.0",
|
||||
"find-free-port": "^2.0.0",
|
||||
"fs-extra": "^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,18 @@
|
||||
"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:singledb": "env-cmd -f .env-singledb node src/index.js",
|
||||
"start:filedb": "env-cmd node src/index.js /home/jena/test/chinook/Chinook.db",
|
||||
"start:singleconn": "env-cmd node src/index.js --server localhost --user root --port 3307 --engine mysql@dbgate-plugin-mysql --password test",
|
||||
"ts": "tsc",
|
||||
"build": "webpack"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^9.0.11",
|
||||
"@types/lodash": "^4.14.149",
|
||||
"dbgate-types": "^4.0.0",
|
||||
"dbgate-types": "^4.1.1",
|
||||
"env-cmd": "^10.1.0",
|
||||
"node-loader": "^1.0.2",
|
||||
"nodemon": "^2.0.2",
|
||||
|
||||
@@ -1,33 +1,32 @@
|
||||
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');
|
||||
const connections = require('../controllers/connections');
|
||||
|
||||
module.exports = {
|
||||
settingsValue: {},
|
||||
|
||||
async _init() {
|
||||
try {
|
||||
this.settingsValue = JSON.parse(await fs.readFile(path.join(datadir(), 'settings.json'), { encoding: 'utf-8' }));
|
||||
} catch (err) {
|
||||
this.settingsValue = {};
|
||||
}
|
||||
},
|
||||
|
||||
get_meta: 'get',
|
||||
async get() {
|
||||
// const toolbarButtons = process.env.TOOLBAR;
|
||||
// const toolbar = toolbarButtons
|
||||
// ? toolbarButtons.split(',').map((name) => ({
|
||||
// name,
|
||||
// icon: process.env[`ICON_${name}`],
|
||||
// title: process.env[`TITLE_${name}`],
|
||||
// page: process.env[`PAGE_${name}`],
|
||||
// }))
|
||||
// : null;
|
||||
// const startupPages = process.env.STARTUP_PAGES ? process.env.STARTUP_PAGES.split(',') : [];
|
||||
const permissions = process.env.PERMISSIONS ? process.env.PERMISSIONS.split(',') : null;
|
||||
const singleDatabase =
|
||||
process.env.SINGLE_CONNECTION && process.env.SINGLE_DATABASE
|
||||
? {
|
||||
conid: process.env.SINGLE_CONNECTION,
|
||||
database: process.env.SINGLE_DATABASE,
|
||||
}
|
||||
: null;
|
||||
|
||||
return {
|
||||
runAsPortal: !!process.env.CONNECTIONS,
|
||||
// toolbar,
|
||||
// startupPages,
|
||||
singleDatabase,
|
||||
runAsPortal: !!connections.portalConnections,
|
||||
singleDatabase: connections.singleDatabase,
|
||||
permissions,
|
||||
...currentVersion,
|
||||
};
|
||||
@@ -37,5 +36,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;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,6 +8,32 @@ const socket = require('../utility/socket');
|
||||
const { encryptConnection } = require('../utility/crypting');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
|
||||
function getNamedArgs() {
|
||||
const res = {};
|
||||
for (let i = 0; i < process.argv.length; i++) {
|
||||
const name = process.argv[i];
|
||||
if (name.startsWith('--')) {
|
||||
let value = process.argv[i + 1];
|
||||
if (value && value.startsWith('--')) value = null;
|
||||
res[name.substring(2)] = value == null ? true : value;
|
||||
i++;
|
||||
} else {
|
||||
if (name.endsWith('.db') || name.endsWith('.sqlite') || name.endsWith('.sqlite3')) {
|
||||
res.databaseFile = name;
|
||||
res.engine = 'sqlite@dbgate-plugin-sqlite';
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function getDatabaseFileLabel(databaseFile) {
|
||||
if (!databaseFile) return databaseFile;
|
||||
const m = databaseFile.match(/[\/]([^\/]+)$/);
|
||||
if (m) return m[1];
|
||||
return databaseFile;
|
||||
}
|
||||
|
||||
function getPortalCollections() {
|
||||
if (process.env.CONNECTIONS) {
|
||||
return _.compact(process.env.CONNECTIONS.split(',')).map(id => ({
|
||||
@@ -17,16 +43,79 @@ function getPortalCollections() {
|
||||
user: process.env[`USER_${id}`],
|
||||
password: process.env[`PASSWORD_${id}`],
|
||||
port: process.env[`PORT_${id}`],
|
||||
databaseUrl: process.env[`URL_${id}`],
|
||||
databaseFile: process.env[`FILE_${id}`],
|
||||
defaultDatabase: process.env[`DATABASE_${id}`],
|
||||
singleDatabase: !!process.env[`DATABASE_${id}`],
|
||||
displayName: process.env[`LABEL_${id}`],
|
||||
}));
|
||||
}
|
||||
|
||||
const args = getNamedArgs();
|
||||
if (args.databaseFile) {
|
||||
return [
|
||||
{
|
||||
_id: 'argv',
|
||||
databaseFile: args.databaseFile,
|
||||
singleDatabase: true,
|
||||
defaultDatabase: getDatabaseFileLabel(args.databaseFile),
|
||||
engine: args.engine,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (args.databaseUrl) {
|
||||
return [
|
||||
{
|
||||
_id: 'argv',
|
||||
useDatabaseUrl: true,
|
||||
...args,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (args.server) {
|
||||
return [
|
||||
{
|
||||
_id: 'argv',
|
||||
...args,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
const portalConnections = getPortalCollections();
|
||||
|
||||
function getSingleDatabase() {
|
||||
if (process.env.SINGLE_CONNECTION && process.env.SINGLE_DATABASE) {
|
||||
// @ts-ignore
|
||||
const connection = portalConnections.find(x => x._id == process.env.SINGLE_CONNECTION);
|
||||
return {
|
||||
connection,
|
||||
name: process.env.SINGLE_DATABASE,
|
||||
};
|
||||
}
|
||||
// @ts-ignore
|
||||
const arg0 = (portalConnections || []).find(x => x._id == 'argv');
|
||||
if (arg0) {
|
||||
// @ts-ignore
|
||||
if (arg0.singleDatabase) {
|
||||
return {
|
||||
connection: arg0,
|
||||
// @ts-ignore
|
||||
name: arg0.defaultDatabase,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const singleDatabase = getSingleDatabase();
|
||||
|
||||
module.exports = {
|
||||
datastore: null,
|
||||
opened: [],
|
||||
singleDatabase,
|
||||
portalConnections,
|
||||
|
||||
async _init() {
|
||||
const dir = datadir();
|
||||
@@ -46,7 +135,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,19 @@ module.exports = {
|
||||
existing.structure = structure;
|
||||
socket.emitChanged(`database-structure-changed-${conid}-${database}`);
|
||||
},
|
||||
handle_structureTime(conid, database, { analysedTime }) {
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
if (!existing) return;
|
||||
existing.analysedTime = analysedTime;
|
||||
socket.emitChanged(`database-status-changed-${conid}-${database}`);
|
||||
},
|
||||
handle_version(conid, database, { version }) {
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
if (!existing) return;
|
||||
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 +54,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 +86,7 @@ module.exports = {
|
||||
msgtype: 'connect',
|
||||
connection: { ...connection, database },
|
||||
structure: lastClosed ? lastClosed.structure : null,
|
||||
globalSettings: config.settingsValue,
|
||||
});
|
||||
return newOpened;
|
||||
},
|
||||
@@ -109,9 +129,19 @@ module.exports = {
|
||||
status_meta: 'get',
|
||||
async status({ conid, database }) {
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
if (existing) return existing.status;
|
||||
if (existing) {
|
||||
return {
|
||||
...existing.status,
|
||||
analysedTime: existing.analysedTime,
|
||||
};
|
||||
}
|
||||
const lastClosed = this.closed[`${conid}/${database}`];
|
||||
if (lastClosed) return lastClosed.status;
|
||||
if (lastClosed) {
|
||||
return {
|
||||
...lastClosed.status,
|
||||
analysedTime: lastClosed.analysedTime,
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: 'error',
|
||||
message: 'Not connected',
|
||||
@@ -127,7 +157,7 @@ module.exports = {
|
||||
} else {
|
||||
existing = await this.ensureOpened(conid, database);
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
connectionStatus: existing ? existing.status : null,
|
||||
@@ -135,13 +165,20 @@ module.exports = {
|
||||
},
|
||||
|
||||
refresh_meta: 'post',
|
||||
async refresh({ conid, database }) {
|
||||
this.close(conid, database);
|
||||
async refresh({ conid, database, keepOpen }) {
|
||||
if (!keepOpen) this.close(conid, database);
|
||||
|
||||
await this.ensureOpened(conid, database);
|
||||
return { status: 'ok' };
|
||||
},
|
||||
|
||||
syncModel_meta: 'post',
|
||||
async syncModel({ conid, database }) {
|
||||
const conn = await this.ensureOpened(conid, database);
|
||||
conn.subprocess.send({ msgtype: 'syncModel' });
|
||||
return { status: 'ok' };
|
||||
},
|
||||
|
||||
close(conid, database, kill = true) {
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
if (existing) {
|
||||
@@ -159,6 +196,12 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
|
||||
disconnect_meta: 'post',
|
||||
async disconnect({ conid, database }) {
|
||||
await this.close(conid, database, true);
|
||||
return { status: 'ok' };
|
||||
},
|
||||
|
||||
structure_meta: 'get',
|
||||
async structure({ conid, database }) {
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
@@ -171,6 +214,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
|
||||
|
||||
@@ -2,43 +2,28 @@ 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');
|
||||
const packagedPluginsContent = require('../packagedPluginsContent');
|
||||
|
||||
module.exports = {
|
||||
script_meta: 'get',
|
||||
async script({ packageName }) {
|
||||
const file = path.join(pluginsdir(), packageName, 'dist', 'frontend.js');
|
||||
const packagedContent = packagedPluginsContent();
|
||||
|
||||
if (packagedContent && packagedContent[packageName]) {
|
||||
return packagedContent[packageName].frontend;
|
||||
}
|
||||
|
||||
const file1 = path.join(packagedPluginsDir(), packageName, 'dist', 'frontend.js');
|
||||
const file2 = path.join(pluginsdir(), packageName, 'dist', 'frontend.js');
|
||||
// @ts-ignore
|
||||
const file = (await fs.exists(file1)) ? file1 : file2;
|
||||
const data = await fs.readFile(file, {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
@@ -62,10 +47,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 +61,63 @@ 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 packagedContent = packagedPluginsContent();
|
||||
|
||||
const files1 = packagedContent ? _.keys(packagedContent) : 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 {
|
||||
if (packagedContent && packagedContent[packageName]) {
|
||||
const manifest = {
|
||||
...packagedContent[packageName].manifest,
|
||||
};
|
||||
manifest.isPackaged = true;
|
||||
manifest.readme = packagedContent[packageName].readme;
|
||||
res.push(manifest);
|
||||
} else {
|
||||
const isPackaged = files1.includes(packageName);
|
||||
const manifest = await fs
|
||||
.readFile(path.join(isPackaged ? packagedPluginsDir() : pluginsdir(), packageName, 'package.json'), {
|
||||
encoding: 'utf-8',
|
||||
})
|
||||
.then(x => JSON.parse(x));
|
||||
const readmeFile = path.join(isPackaged ? packagedPluginsDir() : pluginsdir(), packageName, 'README.md');
|
||||
// @ts-ignore
|
||||
if (await fs.exists(readmeFile)) {
|
||||
manifest.readme = await fs.readFile(readmeFile, { encoding: 'utf-8' });
|
||||
}
|
||||
manifest.isPackaged = isPackaged;
|
||||
res.push(manifest);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`Skipped plugin ${packageName}, error:`, err.message);
|
||||
}
|
||||
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 +126,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 +134,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);
|
||||
@@ -153,50 +153,51 @@ module.exports = {
|
||||
async authTypes({ engine }) {
|
||||
const packageName = extractPackageName(engine);
|
||||
const content = requirePlugin(packageName);
|
||||
if (!content.driver || content.driver.engine != engine || !content.driver.getAuthTypes) return null;
|
||||
return content.driver.getAuthTypes() || null;
|
||||
const driver = content.drivers.find(x => x.engine == engine);
|
||||
if (!driver || !driver.getAuthTypes) return null;
|
||||
return driver.getAuthTypes() || null;
|
||||
},
|
||||
|
||||
async _init() {
|
||||
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);
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ const uuidv1 = require('uuid/v1');
|
||||
const byline = require('byline');
|
||||
const socket = require('../utility/socket');
|
||||
const { fork } = require('child_process');
|
||||
const { rundir, uploadsdir, pluginsdir } = require('../utility/directories');
|
||||
const { rundir, uploadsdir, pluginsdir, getPluginBackendPath, packagedPluginList } = require('../utility/directories');
|
||||
const { extractShellApiPlugins, extractShellApiFunctionName } = require('dbgate-tools');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
|
||||
@@ -92,7 +92,7 @@ module.exports = {
|
||||
const scriptFile = path.join(uploadsdir(), runid + '.js');
|
||||
fs.writeFileSync(`${scriptFile}`, scriptText);
|
||||
fs.mkdirSync(directory);
|
||||
const pluginNames = fs.readdirSync(pluginsdir());
|
||||
const pluginNames = _.union(fs.readdirSync(pluginsdir()), packagedPluginList);
|
||||
console.log(`RUNNING SCRIPT ${scriptFile}`);
|
||||
// const subprocess = fork(scriptFile, ['--checkParent', '--max-old-space-size=8192'], {
|
||||
const subprocess = fork(scriptFile, ['--checkParent', ...process.argv.slice(3)], {
|
||||
@@ -101,7 +101,7 @@ module.exports = {
|
||||
env: {
|
||||
...process.env,
|
||||
DBGATE_API: global['dbgateApiModulePath'] || process.argv[1],
|
||||
..._.fromPairs(pluginNames.map(name => [`PLUGIN_${_.camelCase(name)}`, path.join(pluginsdir(), name)])),
|
||||
..._.fromPairs(pluginNames.map(name => [`PLUGIN_${_.camelCase(name)}`, getPluginBackendPath(name)])),
|
||||
},
|
||||
});
|
||||
const pipeDispatcher = severity => data =>
|
||||
|
||||
@@ -5,6 +5,7 @@ const _ = require('lodash');
|
||||
const AsyncLock = require('async-lock');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
const lock = new AsyncLock();
|
||||
const config = require('./config');
|
||||
|
||||
module.exports = {
|
||||
opened: [],
|
||||
@@ -17,6 +18,12 @@ module.exports = {
|
||||
existing.databases = databases;
|
||||
socket.emitChanged(`database-list-changed-${conid}`);
|
||||
},
|
||||
handle_version(conid, { version }) {
|
||||
const existing = this.opened.find(x => x.conid == conid);
|
||||
if (!existing) return;
|
||||
existing.version = version;
|
||||
socket.emitChanged(`server-version-changed-${conid}`);
|
||||
},
|
||||
handle_status(conid, { status }) {
|
||||
const existing = this.opened.find(x => x.conid == conid);
|
||||
if (!existing) return;
|
||||
@@ -30,7 +37,11 @@ module.exports = {
|
||||
const existing = this.opened.find(x => x.conid == conid);
|
||||
if (existing) return existing;
|
||||
const connection = await connections.get({ conid });
|
||||
const subprocess = fork(process.argv[1], ['serverConnectionProcess', ...process.argv.slice(3)]);
|
||||
const subprocess = fork(process.argv[1], [
|
||||
'--start-process',
|
||||
'serverConnectionProcess',
|
||||
...process.argv.slice(3),
|
||||
]);
|
||||
const newOpened = {
|
||||
conid,
|
||||
subprocess,
|
||||
@@ -55,7 +66,7 @@ module.exports = {
|
||||
if (newOpened.disconnected) return;
|
||||
this.close(conid, false);
|
||||
});
|
||||
subprocess.send({ msgtype: 'connect', ...connection });
|
||||
subprocess.send({ msgtype: 'connect', ...connection, globalSettings: config.settingsValue });
|
||||
return newOpened;
|
||||
});
|
||||
return res;
|
||||
@@ -75,12 +86,24 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
|
||||
disconnect_meta: 'post',
|
||||
async disconnect({ conid }) {
|
||||
await this.close(conid, true);
|
||||
return { status: 'ok' };
|
||||
},
|
||||
|
||||
listDatabases_meta: 'get',
|
||||
async listDatabases({ conid }) {
|
||||
const opened = await this.ensureOpened(conid);
|
||||
return opened.databases;
|
||||
},
|
||||
|
||||
version_meta: 'get',
|
||||
async version({ conid }) {
|
||||
const opened = await this.ensureOpened(conid);
|
||||
return opened.version;
|
||||
},
|
||||
|
||||
serverStatus_meta: 'get',
|
||||
async serverStatus() {
|
||||
return {
|
||||
@@ -106,8 +129,8 @@ module.exports = {
|
||||
},
|
||||
|
||||
refresh_meta: 'post',
|
||||
async refresh({ conid }) {
|
||||
this.close(conid);
|
||||
async refresh({ conid, keepOpen }) {
|
||||
if (!keepOpen) this.close(conid);
|
||||
|
||||
await this.ensureOpened(conid);
|
||||
return { status: 'ok' };
|
||||
|
||||
@@ -65,7 +65,7 @@ module.exports = {
|
||||
async create({ conid, database }) {
|
||||
const sesid = uuidv1();
|
||||
const connection = await connections.get({ conid });
|
||||
const subprocess = fork(process.argv[1], ['sessionProcess', ...process.argv.slice(3)]);
|
||||
const subprocess = fork(process.argv[1], ['--start-process', 'sessionProcess', ...process.argv.slice(3)]);
|
||||
const newOpened = {
|
||||
conid,
|
||||
database,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
module.exports = {
|
||||
version: '4.0.0',
|
||||
buildTime: '2021-04-01T10:48:22.253Z'
|
||||
version: '4.1.1',
|
||||
buildTime: '2021-04-17T07:22:49.702Z'
|
||||
};
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
const shell = require('./shell');
|
||||
const processArgs = require('./utility/processArgs');
|
||||
const dbgateTools = require('dbgate-tools');
|
||||
|
||||
const argument = process.argv[2];
|
||||
if (argument && argument.endsWith('Process')) {
|
||||
global['DBGATE_TOOLS'] = dbgateTools;
|
||||
|
||||
if (processArgs.startProcess) {
|
||||
const proc = require('./proc');
|
||||
|
||||
const module = proc[argument];
|
||||
const module = proc[processArgs.startProcess];
|
||||
module.start();
|
||||
} else if (!module['parent'] && !process.argv.includes('--checkParent')) {
|
||||
} else if (!module['parent'] && !processArgs.checkParent) {
|
||||
const main = require('./main');
|
||||
|
||||
main.start(argument);
|
||||
main.start();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -6,9 +6,10 @@ const http = require('http');
|
||||
const cors = require('cors');
|
||||
const io = require('socket.io');
|
||||
const fs = require('fs');
|
||||
const findFreePort = require('find-free-port');
|
||||
const getPort = require('get-port');
|
||||
const childProcessChecker = require('./utility/childProcessChecker');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const useController = require('./utility/useController');
|
||||
const socket = require('./utility/socket');
|
||||
@@ -28,8 +29,14 @@ const files = require('./controllers/files');
|
||||
const scheduler = require('./controllers/scheduler');
|
||||
|
||||
const { rundir } = require('./utility/directories');
|
||||
const platformInfo = require('./utility/platformInfo');
|
||||
const processArgs = require('./utility/processArgs');
|
||||
const timingSafeCheckToken = require('./utility/timingSafeCheckToken');
|
||||
|
||||
function start(argument = null) {
|
||||
let authorization = null;
|
||||
let checkLocalhostOrigin = null;
|
||||
|
||||
function start() {
|
||||
// console.log('process.argv', process.argv);
|
||||
|
||||
const app = express();
|
||||
@@ -49,6 +56,29 @@ function start(argument = null) {
|
||||
);
|
||||
}
|
||||
|
||||
app.use(function (req, res, next) {
|
||||
if (authorization && !timingSafeCheckToken(req.headers.authorization, authorization)) {
|
||||
return res.status(403).json({ error: 'Not authorized!' });
|
||||
}
|
||||
if (checkLocalhostOrigin) {
|
||||
if (
|
||||
req.headers.origin &&
|
||||
req.headers.origin != checkLocalhostOrigin &&
|
||||
req.headers.origin != `http://${checkLocalhostOrigin}`
|
||||
) {
|
||||
console.log('API origin check FAILED');
|
||||
console.log('HEADERS', { ...req.headers, authorization: '***' });
|
||||
return res.status(403).json({ error: 'Not authorized!' });
|
||||
}
|
||||
if (!req.headers.origin && req.headers.host != checkLocalhostOrigin) {
|
||||
console.log('API host check FAILED');
|
||||
console.log('HEADERS', { ...req.headers, authorization: '***' });
|
||||
return res.status(403).json({ error: 'Not authorized!' });
|
||||
}
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(cors());
|
||||
app.use(bodyParser.json({ limit: '50mb' }));
|
||||
|
||||
@@ -79,29 +109,32 @@ function start(argument = null) {
|
||||
|
||||
app.use('/runners/data', express.static(rundir()));
|
||||
|
||||
if (fs.existsSync('/home/dbgate-docker/public')) {
|
||||
if (platformInfo.isDocker) {
|
||||
// server static files inside docker container
|
||||
app.use(express.static('/home/dbgate-docker/public'));
|
||||
} else {
|
||||
if (argument != 'startNodeWeb') {
|
||||
if (!platformInfo.isNpmDist) {
|
||||
app.get('/', (req, res) => {
|
||||
res.send('DbGate API');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (argument == '--dynport') {
|
||||
if (processArgs.dynport) {
|
||||
childProcessChecker();
|
||||
|
||||
findFreePort(53911, function (err, port) {
|
||||
authorization = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
getPort().then(port => {
|
||||
checkLocalhostOrigin = `localhost:${port}`;
|
||||
server.listen(port, () => {
|
||||
console.log(`DbGate API listening on port ${port}`);
|
||||
process.send({ msgtype: 'listening', port });
|
||||
process.send({ msgtype: 'listening', port, authorization });
|
||||
});
|
||||
});
|
||||
} else if (argument == 'startNodeWeb') {
|
||||
} else if (platformInfo.isNpmDist) {
|
||||
app.use(express.static(path.join(__dirname, '../../dbgate-web/public')));
|
||||
findFreePort(5000, function (err, port) {
|
||||
getPort({ port: 5000 }).then(port => {
|
||||
server.listen(port, () => {
|
||||
console.log(`DbGate API listening on port ${port}`);
|
||||
});
|
||||
|
||||
@@ -2,6 +2,25 @@ const childProcessChecker = require('../utility/childProcessChecker');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const connectUtility = require('../utility/connectUtility');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
const _ = require('lodash');
|
||||
|
||||
function pickSafeConnectionInfo(connection) {
|
||||
return _.mapValues(connection, (v, k) => {
|
||||
if (k == 'engine' || k == 'port' || k == 'authType' || k == 'sshMode' || k == 'passwordMode') return v;
|
||||
if (v === null || v === true || v === false) return v;
|
||||
if (v) return '***';
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
const formatErrorDetail = (e, connection) => `${e.stack}
|
||||
|
||||
Error JSON: ${JSON.stringify(e, undefined, 2)}
|
||||
|
||||
Connection: ${JSON.stringify(pickSafeConnectionInfo(connection), undefined, 2)}
|
||||
|
||||
Platform: ${process.platform}
|
||||
`;
|
||||
|
||||
function start() {
|
||||
childProcessChecker();
|
||||
@@ -14,7 +33,11 @@ function start() {
|
||||
process.send({ msgtype: 'connected', ...res });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.send({ msgtype: 'error', error: e.message });
|
||||
process.send({
|
||||
msgtype: 'error',
|
||||
error: e.message,
|
||||
detail: formatErrorDetail(e, connection),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const stableStringify = require('json-stable-stringify');
|
||||
const childProcessChecker = require('../utility/childProcessChecker');
|
||||
const { extractBoolSettingsValue, extractIntSettingsValue } = require('dbgate-tools');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const connectUtility = require('../utility/connectUtility');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
@@ -11,6 +12,7 @@ let afterConnectCallbacks = [];
|
||||
let analysedStructure = null;
|
||||
let lastPing = null;
|
||||
let lastStatus = null;
|
||||
let analysedTime = 0;
|
||||
|
||||
async function checkedAsyncCall(promise) {
|
||||
try {
|
||||
@@ -27,21 +29,42 @@ async function checkedAsyncCall(promise) {
|
||||
}
|
||||
}
|
||||
|
||||
let loadingModel = false;
|
||||
|
||||
async function handleFullRefresh() {
|
||||
loadingModel = true;
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
setStatusName('loadStructure');
|
||||
analysedStructure = await checkedAsyncCall(driver.analyseFull(systemConnection));
|
||||
analysedTime = new Date().getTime();
|
||||
process.send({ msgtype: 'structure', structure: analysedStructure });
|
||||
process.send({ msgtype: 'structureTime', analysedTime });
|
||||
setStatusName('ok');
|
||||
loadingModel = false;
|
||||
}
|
||||
|
||||
async function handleIncrementalRefresh() {
|
||||
async function handleIncrementalRefresh(forceSend) {
|
||||
loadingModel = true;
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
setStatusName('checkStructure');
|
||||
const newStructure = await checkedAsyncCall(driver.analyseIncremental(systemConnection, analysedStructure));
|
||||
analysedTime = new Date().getTime();
|
||||
if (newStructure != null) {
|
||||
analysedStructure = newStructure;
|
||||
}
|
||||
|
||||
if (forceSend || newStructure != null) {
|
||||
process.send({ msgtype: 'structure', structure: analysedStructure });
|
||||
}
|
||||
|
||||
process.send({ msgtype: 'structureTime', analysedTime });
|
||||
setStatusName('ok');
|
||||
loadingModel = false;
|
||||
}
|
||||
|
||||
function handleSyncModel() {
|
||||
if (loadingModel) return;
|
||||
handleIncrementalRefresh();
|
||||
}
|
||||
|
||||
function setStatus(status) {
|
||||
@@ -56,20 +79,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();
|
||||
handleIncrementalRefresh(true);
|
||||
} else {
|
||||
handleFullRefresh();
|
||||
}
|
||||
setInterval(handleIncrementalRefresh, 30 * 1000);
|
||||
|
||||
if (extractBoolSettingsValue(globalSettings, 'connection.autoRefresh', false)) {
|
||||
setInterval(
|
||||
handleIncrementalRefresh,
|
||||
extractIntSettingsValue(globalSettings, 'connection.autoRefreshInterval', 30, 3, 3600) * 1000
|
||||
);
|
||||
}
|
||||
|
||||
for (const [resolve] of afterConnectCallbacks) {
|
||||
resolve();
|
||||
}
|
||||
@@ -155,6 +192,7 @@ const messageHandlers = {
|
||||
collectionData: handleCollectionData,
|
||||
sqlPreview: handleSqlPreview,
|
||||
ping: handlePing,
|
||||
syncModel: handleSyncModel,
|
||||
// runCommand: handleRunCommand,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const stableStringify = require('json-stable-stringify');
|
||||
const { extractBoolSettingsValue, extractIntSettingsValue } = require('dbgate-tools');
|
||||
const childProcessChecker = require('../utility/childProcessChecker');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const { decryptConnection } = require('../utility/crypting');
|
||||
@@ -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', false)) {
|
||||
setInterval(handleRefresh, extractIntSettingsValue(globalSettings, 'connection.autoRefreshInterval', 30, 5, 3600) * 1000);
|
||||
}
|
||||
} catch (err) {
|
||||
setStatus({
|
||||
name: 'error',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const childProcessChecker = require('../utility/childProcessChecker');
|
||||
const processArgs = require('../utility/processArgs');
|
||||
|
||||
async function runScript(func) {
|
||||
if (process.argv.includes('--checkParent')) {
|
||||
if (processArgs.checkParent) {
|
||||
childProcessChecker();
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -29,7 +29,7 @@ class DatastoreProxy {
|
||||
|
||||
async ensureSubprocess() {
|
||||
if (!this.subprocess) {
|
||||
this.subprocess = fork(process.argv[1], ['jslDatastoreProcess', ...process.argv.slice(3)]);
|
||||
this.subprocess = fork(process.argv[1], ['--start-process', 'jslDatastoreProcess', ...process.argv.slice(3)]);
|
||||
|
||||
this.subprocess.on('message', message => {
|
||||
// @ts-ignore
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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'),
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -15,7 +15,7 @@ function requireEngineDriver(connection) {
|
||||
if (engine.includes('@')) {
|
||||
const [shortName, packageName] = engine.split('@');
|
||||
const plugin = requirePlugin(packageName);
|
||||
return plugin.driver;
|
||||
return plugin.drivers.find(x => x.engine == engine);
|
||||
}
|
||||
throw new Error(`Could not found engine driver ${engine}`);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -1,10 +1,9 @@
|
||||
{
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.1",
|
||||
"name": "dbgate-datalib",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
"scripts": {
|
||||
"prepare": "yarn build",
|
||||
"build": "tsc",
|
||||
"start": "tsc --watch"
|
||||
},
|
||||
@@ -12,11 +11,11 @@
|
||||
"lib"
|
||||
],
|
||||
"dependencies": {
|
||||
"dbgate-sqltree": "^4.0.0",
|
||||
"dbgate-filterparser": "^4.0.0"
|
||||
"dbgate-sqltree": "^4.1.1",
|
||||
"dbgate-filterparser": "^4.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dbgate-types": "^4.0.0",
|
||||
"dbgate-types": "^4.1.1",
|
||||
"@types/node": "^13.7.0",
|
||||
"typescript": "^3.7.5"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import _ from 'lodash';
|
||||
import { GridConfig, GridCache, GridConfigColumns, createGridCache, GroupFunc } from './GridConfig';
|
||||
import { ForeignKeyInfo, TableInfo, ColumnInfo, EngineDriver, NamedObjectInfo, DatabaseInfo } from 'dbgate-types';
|
||||
import {
|
||||
ForeignKeyInfo,
|
||||
TableInfo,
|
||||
ColumnInfo,
|
||||
EngineDriver,
|
||||
NamedObjectInfo,
|
||||
DatabaseInfo,
|
||||
SqlDialect,
|
||||
} from 'dbgate-types';
|
||||
import { parseFilter, getFilterType, getFilterValueExpression } from 'dbgate-filterparser';
|
||||
import { filterName } from './filterName';
|
||||
import { ChangeSetFieldDefinition, ChangeSetRowDefinition } from './ChangeSet';
|
||||
@@ -12,6 +20,7 @@ export class FormViewDisplay {
|
||||
isLoadedCorrectly = true;
|
||||
columns: DisplayColumn[];
|
||||
public baseTable: TableInfo;
|
||||
dialect: SqlDialect;
|
||||
|
||||
constructor(
|
||||
public config: GridConfig,
|
||||
@@ -19,8 +28,11 @@ export class FormViewDisplay {
|
||||
public cache: GridCache,
|
||||
protected setCache: ChangeCacheFunc,
|
||||
public driver?: EngineDriver,
|
||||
public dbinfo: DatabaseInfo = null
|
||||
) {}
|
||||
public dbinfo: DatabaseInfo = null,
|
||||
public serverVersion = null
|
||||
) {
|
||||
this.dialect = (driver?.dialectByVersion && driver?.dialectByVersion(serverVersion)) || driver?.dialect;
|
||||
}
|
||||
|
||||
addFilterColumn(column) {
|
||||
if (!column) return;
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
NamedObjectInfo,
|
||||
DatabaseInfo,
|
||||
CollectionInfo,
|
||||
SqlDialect,
|
||||
} from 'dbgate-types';
|
||||
import { parseFilter, getFilterType } from 'dbgate-filterparser';
|
||||
import { filterName } from './filterName';
|
||||
@@ -60,8 +61,12 @@ export abstract class GridDisplay {
|
||||
public cache: GridCache,
|
||||
protected setCache: ChangeCacheFunc,
|
||||
public driver?: EngineDriver,
|
||||
public dbinfo: DatabaseInfo = null
|
||||
) {}
|
||||
public dbinfo: DatabaseInfo = null,
|
||||
public serverVersion = null
|
||||
) {
|
||||
this.dialect = (driver?.dialectByVersion && driver?.dialectByVersion(serverVersion)) || driver?.dialect;
|
||||
}
|
||||
dialect: SqlDialect;
|
||||
columns: DisplayColumn[];
|
||||
baseTable?: TableInfo;
|
||||
baseCollection?: CollectionInfo;
|
||||
@@ -460,12 +465,75 @@ export abstract class GridDisplay {
|
||||
return select;
|
||||
}
|
||||
|
||||
getRowNumberOverSelect(select: Select, offset: number, count: number): Select {
|
||||
const innerSelect: Select = {
|
||||
commandType: 'select',
|
||||
from: select.from,
|
||||
where: select.where,
|
||||
columns: [
|
||||
...select.columns,
|
||||
{
|
||||
alias: '_rowNumber',
|
||||
exprType: 'rowNumber',
|
||||
orderBy: select.orderBy
|
||||
? select.orderBy.map(x =>
|
||||
x.exprType != 'column'
|
||||
? x
|
||||
: x.source
|
||||
? x
|
||||
: {
|
||||
...x,
|
||||
source: { alias: 'basetbl' },
|
||||
}
|
||||
)
|
||||
: [
|
||||
{
|
||||
...select.columns[0],
|
||||
direction: 'ASC',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const res: Select = {
|
||||
commandType: 'select',
|
||||
selectAll: true,
|
||||
from: {
|
||||
subQuery: innerSelect,
|
||||
alias: '_RowNumberResult',
|
||||
},
|
||||
where: {
|
||||
conditionType: 'between',
|
||||
expr: {
|
||||
exprType: 'column',
|
||||
columnName: '_RowNumber',
|
||||
source: {
|
||||
alias: '_RowNumberResult',
|
||||
},
|
||||
},
|
||||
left: {
|
||||
exprType: 'value',
|
||||
value: offset + 1,
|
||||
},
|
||||
right: {
|
||||
exprType: 'value',
|
||||
value: offset + count,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
getPageQuery(offset: number, count: number) {
|
||||
if (!this.driver) return null;
|
||||
const select = this.createSelect();
|
||||
let select = this.createSelect();
|
||||
if (!select) return null;
|
||||
if (this.driver.dialect.rangeSelect) select.range = { offset: offset, limit: count };
|
||||
else if (this.driver.dialect.limitSelect) select.topRecords = count;
|
||||
if (this.dialect.rangeSelect) select.range = { offset: offset, limit: count };
|
||||
else if (this.dialect.rowNumberOverPaging && offset > 0)
|
||||
select = this.getRowNumberOverSelect(select, offset, count);
|
||||
else if (this.dialect.limitSelect) select.topRecords = count;
|
||||
const sql = treeToSql(this.driver, select, dumpSqlSelect);
|
||||
return sql;
|
||||
}
|
||||
|
||||
@@ -28,10 +28,22 @@ export class TableFormViewDisplay extends FormViewDisplay {
|
||||
setConfig: ChangeConfigFunc,
|
||||
cache: GridCache,
|
||||
setCache: ChangeCacheFunc,
|
||||
dbinfo: DatabaseInfo
|
||||
dbinfo: DatabaseInfo,
|
||||
displayOptions,
|
||||
serverVersion
|
||||
) {
|
||||
super(config, setConfig, cache, setCache, driver, dbinfo);
|
||||
this.gridDisplay = new TableGridDisplay(tableName, driver, config, setConfig, cache, setCache, dbinfo);
|
||||
super(config, setConfig, cache, setCache, driver, dbinfo, serverVersion);
|
||||
this.gridDisplay = new TableGridDisplay(
|
||||
tableName,
|
||||
driver,
|
||||
config,
|
||||
setConfig,
|
||||
cache,
|
||||
setCache,
|
||||
dbinfo,
|
||||
displayOptions,
|
||||
serverVersion
|
||||
);
|
||||
this.gridDisplay.addAllExpandedColumnsToSelected = true;
|
||||
|
||||
this.isLoadedCorrectly = this.gridDisplay.isLoadedCorrectly && !!this.driver;
|
||||
|
||||
@@ -17,9 +17,11 @@ export class TableGridDisplay extends GridDisplay {
|
||||
setConfig: ChangeConfigFunc,
|
||||
cache: GridCache,
|
||||
setCache: ChangeCacheFunc,
|
||||
dbinfo: DatabaseInfo
|
||||
dbinfo: DatabaseInfo,
|
||||
public displayOptions: any,
|
||||
serverVersion
|
||||
) {
|
||||
super(config, setConfig, cache, setCache, driver, dbinfo);
|
||||
super(config, setConfig, cache, setCache, driver, dbinfo, serverVersion);
|
||||
|
||||
this.table = this.findTable(tableName);
|
||||
if (!this.table) {
|
||||
@@ -168,7 +170,7 @@ export class TableGridDisplay extends GridDisplay {
|
||||
|
||||
processReferences(select: Select, displayedColumnInfo: DisplayedColumnInfo, options) {
|
||||
this.addJoinsFromExpandedColumns(select, this.columns, 'basetbl', displayedColumnInfo);
|
||||
if (!options.isExport) {
|
||||
if (!options.isExport && this.displayOptions.showHintColumns) {
|
||||
this.addHintsToSelect(select);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,10 @@ export class ViewGridDisplay extends GridDisplay {
|
||||
config: GridConfig,
|
||||
setConfig: ChangeConfigFunc,
|
||||
cache: GridCache,
|
||||
setCache: ChangeCacheFunc
|
||||
setCache: ChangeCacheFunc,
|
||||
serverVersion
|
||||
) {
|
||||
super(config, setConfig, cache, setCache, driver);
|
||||
super(config, setConfig, cache, setCache, driver, serverVersion);
|
||||
this.columns = this.getDisplayColumns(view);
|
||||
this.filterable = true;
|
||||
this.sortable = true;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
[](https://github.com/prettier/prettier)
|
||||
[](https://paypal.me/JanProchazkaCz/30eur)
|
||||
[](https://www.npmjs.com/package/dbgate)
|
||||
|
||||
# DbGate - database administration tool
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const path = require('path');
|
||||
|
||||
global.dbgateApiModulePath = path.dirname(path.dirname(require.resolve('dbgate-api')));
|
||||
global.dbgateApiPackagedPluginsPath = path.dirname(global.dbgateApiModulePath);
|
||||
|
||||
const dbgateApi = require('dbgate-api');
|
||||
|
||||
global.dbgateApiModulePath = require.resolve('dbgate-api');
|
||||
|
||||
dbgateApi.getMainModule().start('startNodeWeb');
|
||||
dbgateApi.getMainModule().start();
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
{
|
||||
"name": "dbgate",
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.1",
|
||||
"homepage": "https://dbgate.org/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
|
||||
"description": "Opensource database administration tool - web interface",
|
||||
"author": "Jan Prochazka",
|
||||
"license": "MIT",
|
||||
@@ -19,7 +18,13 @@
|
||||
"web"
|
||||
],
|
||||
"dependencies": {
|
||||
"dbgate-api": "^4.0.0",
|
||||
"dbgate-web": "^4.0.0"
|
||||
"dbgate-api": "^4.1.1",
|
||||
"dbgate-web": "^4.1.1",
|
||||
"dbgate-plugin-csv": "^4.1.1",
|
||||
"dbgate-plugin-excel": "^4.1.1",
|
||||
"dbgate-plugin-mongo": "^4.1.1",
|
||||
"dbgate-plugin-mysql": "^4.1.1",
|
||||
"dbgate-plugin-mssql": "^4.1.1",
|
||||
"dbgate-plugin-postgres": "^4.1.1"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
{
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.1",
|
||||
"name": "dbgate-filterparser",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
"scripts": {
|
||||
"prepare": "yarn build",
|
||||
"build": "tsc",
|
||||
"start": "tsc --watch",
|
||||
"test": "jest"
|
||||
@@ -13,7 +12,7 @@
|
||||
"lib"
|
||||
],
|
||||
"devDependencies": {
|
||||
"dbgate-types": "^4.0.0",
|
||||
"dbgate-types": "^4.1.1",
|
||||
"@types/jest": "^25.1.4",
|
||||
"@types/node": "^13.7.0",
|
||||
"jest": "^24.9.0",
|
||||
@@ -22,7 +21,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/parsimmon": "^1.10.1",
|
||||
"dbgate-tools": "^4.0.0",
|
||||
"dbgate-tools": "^4.1.1",
|
||||
"lodash": "^4.17.15",
|
||||
"moment": "^2.24.0",
|
||||
"parsimmon": "^1.13.0"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.1",
|
||||
"name": "dbgate-sqltree",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
@@ -8,7 +8,6 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
|
||||
"author": "Jan Prochazka",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
@@ -20,7 +19,6 @@
|
||||
"dbgate"
|
||||
],
|
||||
"scripts": {
|
||||
"prepare": "yarn build",
|
||||
"build": "tsc",
|
||||
"start": "tsc --watch"
|
||||
},
|
||||
@@ -29,7 +27,7 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/node": "^13.7.0",
|
||||
"dbgate-types": "^4.0.0",
|
||||
"dbgate-types": "^4.1.1",
|
||||
"typescript": "^3.7.5"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -62,5 +62,12 @@ export function dumpSqlCondition(dmp: SqlDumper, condition: Condition) {
|
||||
dumpSqlSelect(dmp, condition.subQuery);
|
||||
dmp.put(')');
|
||||
break;
|
||||
case 'between':
|
||||
dumpSqlExpression(dmp, condition.expr);
|
||||
dmp.put(' ^between ');
|
||||
dumpSqlExpression(dmp, condition.left);
|
||||
dmp.put(' ^and ');
|
||||
dumpSqlExpression(dmp, condition.right);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,5 +38,14 @@ export function dumpSqlExpression(dmp: SqlDumper, expr: Expression) {
|
||||
case 'transform':
|
||||
dmp.transform(expr.transform, () => dumpSqlExpression(dmp, expr.expr));
|
||||
break;
|
||||
|
||||
case 'rowNumber':
|
||||
dmp.put(" ^row_number() ^over (^order ^by ");
|
||||
dmp.putCollection(', ', expr.orderBy, x => {
|
||||
dumpSqlExpression(dmp, x);
|
||||
dmp.put(' %k', x.direction);
|
||||
});
|
||||
dmp.put(")");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +92,13 @@ export interface NotExistsCondition {
|
||||
subQuery: Select;
|
||||
}
|
||||
|
||||
export interface BetweenCondition {
|
||||
conditionType: 'between';
|
||||
expr: Expression;
|
||||
left: Expression;
|
||||
right: Expression;
|
||||
}
|
||||
|
||||
export type Condition =
|
||||
| BinaryCondition
|
||||
| NotCondition
|
||||
@@ -99,7 +106,8 @@ export type Condition =
|
||||
| CompoudCondition
|
||||
| LikeCondition
|
||||
| ExistsCondition
|
||||
| NotExistsCondition;
|
||||
| NotExistsCondition
|
||||
| BetweenCondition;
|
||||
|
||||
export interface Source {
|
||||
name?: NamedObjectInfo;
|
||||
@@ -153,13 +161,19 @@ export interface TranformExpression {
|
||||
transform: TransformType;
|
||||
}
|
||||
|
||||
export interface RowNumberExpression {
|
||||
exprType: 'rowNumber';
|
||||
orderBy: OrderByExpression[];
|
||||
}
|
||||
|
||||
export type Expression =
|
||||
| ColumnRefExpression
|
||||
| ValueExpression
|
||||
| PlaceholderExpression
|
||||
| RawExpression
|
||||
| CallExpression
|
||||
| TranformExpression;
|
||||
| TranformExpression
|
||||
| RowNumberExpression;
|
||||
export type OrderByExpression = Expression & { direction: 'ASC' | 'DESC' };
|
||||
|
||||
export type ResultField = Expression & { alias?: string };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.1",
|
||||
"name": "dbgate-tools",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
@@ -8,7 +8,6 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
|
||||
"author": "Jan Prochazka",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
@@ -16,7 +15,6 @@
|
||||
"dbgate"
|
||||
],
|
||||
"scripts": {
|
||||
"prepare": "yarn build",
|
||||
"build": "tsc",
|
||||
"start": "tsc --watch",
|
||||
"test": "jest",
|
||||
@@ -27,7 +25,7 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/node": "^13.7.0",
|
||||
"dbgate-types": "^4.0.0",
|
||||
"dbgate-types": "^4.1.1",
|
||||
"jest": "^24.9.0",
|
||||
"ts-jest": "^25.2.1",
|
||||
"typescript": "^3.7.5"
|
||||
@@ -35,4 +33,4 @@
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.15"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,14 @@ import { DatabaseInfo, DatabaseModification, EngineDriver } from 'dbgate-types';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
import _groupBy from 'lodash/groupBy';
|
||||
import _pick from 'lodash/pick';
|
||||
import _compact from 'lodash/compact';
|
||||
|
||||
const fp_pick = arg => array => _pick(array, arg);
|
||||
export class DatabaseAnalyser {
|
||||
structure: DatabaseInfo;
|
||||
modifications: DatabaseModification[];
|
||||
singleObjectFilter: any;
|
||||
singleObjectId: string = null;
|
||||
|
||||
constructor(public pool, public driver: EngineDriver) {}
|
||||
|
||||
@@ -15,15 +17,30 @@ export class DatabaseAnalyser {
|
||||
return DatabaseAnalyser.createEmptyStructure();
|
||||
}
|
||||
|
||||
/** @returns {Promise<import('dbgate-types').DatabaseModification[]>} */
|
||||
async getModifications() {
|
||||
if (this.structure == null) throw new Error('DatabaseAnalyse.getModifications - structure must be filled');
|
||||
|
||||
async _getFastSnapshot(): Promise<DatabaseInfo> {
|
||||
return null;
|
||||
}
|
||||
|
||||
async _computeSingleObjectId() {}
|
||||
|
||||
async fullAnalysis() {
|
||||
return this._runAnalysis();
|
||||
const res = await this._runAnalysis();
|
||||
// console.log('FULL ANALYSIS', res);
|
||||
return res;
|
||||
}
|
||||
|
||||
async singleObjectAnalysis(name, typeField) {
|
||||
// console.log('Analysing SINGLE OBJECT', name, typeField);
|
||||
this.singleObjectFilter = { ...name, typeField };
|
||||
await this._computeSingleObjectId();
|
||||
const res = await this._runAnalysis();
|
||||
// console.log('SINGLE OBJECT RES', res);
|
||||
const obj =
|
||||
res[typeField]?.length == 1
|
||||
? res[typeField][0]
|
||||
: res[typeField]?.find(x => x.pureName == name.pureName && x.schemaName == name.schemaName);
|
||||
// console.log('SINGLE OBJECT', obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
async incrementalAnalysis(structure) {
|
||||
@@ -37,7 +54,7 @@ export class DatabaseAnalyser {
|
||||
}
|
||||
if (this.modifications.length == 0) return null;
|
||||
console.log('DB modifications detected:', this.modifications);
|
||||
return this._runAnalysis();
|
||||
return this.mergeAnalyseResult(await this._runAnalysis());
|
||||
}
|
||||
|
||||
mergeAnalyseResult(newlyAnalysed) {
|
||||
@@ -57,7 +74,7 @@ export class DatabaseAnalyser {
|
||||
const addedChangedIds = newArray.map(x => x.objectId);
|
||||
const removeAllIds = [...removedIds, ...addedChangedIds];
|
||||
res[field] = _sortBy(
|
||||
[...this.structure[field].filter(x => !removeAllIds.includes(x.objectId)), ...newArray],
|
||||
[...(this.structure[field] || []).filter(x => !removeAllIds.includes(x.objectId)), ...newArray],
|
||||
x => x.pureName
|
||||
);
|
||||
}
|
||||
@@ -71,10 +88,111 @@ export class DatabaseAnalyser {
|
||||
// }
|
||||
}
|
||||
|
||||
getRequestedObjectPureNames(objectTypeField, allPureNames) {
|
||||
if (this.singleObjectFilter) {
|
||||
const { typeField, pureName } = this.singleObjectFilter;
|
||||
if (typeField == objectTypeField) return [pureName];
|
||||
}
|
||||
if (this.modifications) {
|
||||
return this.modifications.filter(x => x.objectTypeField == objectTypeField).map(x => x.newName.pureName);
|
||||
}
|
||||
return allPureNames;
|
||||
}
|
||||
|
||||
// findObjectById(id) {
|
||||
// return this.structure.tables.find((x) => x.objectId == id);
|
||||
// }
|
||||
|
||||
createQuery(template, typeFields) {
|
||||
// let res = template;
|
||||
if (this.singleObjectFilter) {
|
||||
const { typeField } = this.singleObjectFilter;
|
||||
if (!this.singleObjectId) return null;
|
||||
if (!typeFields || !typeFields.includes(typeField)) return null;
|
||||
return template.replace(/=OBJECT_ID_CONDITION/g, ` = '${this.singleObjectId}'`);
|
||||
}
|
||||
if (!this.modifications || !typeFields || this.modifications.length == 0) {
|
||||
return template.replace(/=OBJECT_ID_CONDITION/g, ' is not null');
|
||||
}
|
||||
if (this.modifications.some(x => typeFields.includes(x.objectTypeField) && x.action == 'all')) {
|
||||
// do not filter objects
|
||||
return template.replace(/=OBJECT_ID_CONDITION/g, ' is not null');
|
||||
}
|
||||
const filterIds = this.modifications
|
||||
.filter(x => typeFields.includes(x.objectTypeField) && (x.action == 'add' || x.action == 'change'))
|
||||
.map(x => x.objectId);
|
||||
if (filterIds.length == 0) {
|
||||
return template.replace(/=OBJECT_ID_CONDITION/g, " = '0'");
|
||||
}
|
||||
return template.replace(/=OBJECT_ID_CONDITION/g, ` in (${filterIds.map(x => `'${x}'`).join(',')})`);
|
||||
}
|
||||
|
||||
getDeletedObjectsForField(snapshot, objectTypeField) {
|
||||
const items = snapshot[objectTypeField];
|
||||
if (!items) return [];
|
||||
if (!this.structure[objectTypeField]) return [];
|
||||
return this.structure[objectTypeField]
|
||||
.filter(x => !items.find(y => x.objectId == y.objectId))
|
||||
.map(x => ({
|
||||
oldName: _pick(x, ['schemaName', 'pureName']),
|
||||
objectId: x.objectId,
|
||||
action: 'remove',
|
||||
objectTypeField,
|
||||
}));
|
||||
}
|
||||
|
||||
getDeletedObjects(snapshot) {
|
||||
return [
|
||||
...this.getDeletedObjectsForField(snapshot, 'tables'),
|
||||
...this.getDeletedObjectsForField(snapshot, 'collections'),
|
||||
...this.getDeletedObjectsForField(snapshot, 'views'),
|
||||
...this.getDeletedObjectsForField(snapshot, 'procedures'),
|
||||
...this.getDeletedObjectsForField(snapshot, 'functions'),
|
||||
...this.getDeletedObjectsForField(snapshot, 'triggers'),
|
||||
];
|
||||
}
|
||||
|
||||
async getModifications() {
|
||||
const snapshot = await this._getFastSnapshot();
|
||||
if (!snapshot) return null;
|
||||
|
||||
// console.log('STRUCTURE', this.structure);
|
||||
// console.log('SNAPSHOT', snapshot);
|
||||
|
||||
const res = [];
|
||||
for (const field in snapshot) {
|
||||
const items = snapshot[field];
|
||||
if (items === null) {
|
||||
res.push({ objectTypeField: field, action: 'all' });
|
||||
continue;
|
||||
}
|
||||
for (const item of items) {
|
||||
const { objectId, schemaName, pureName, contentHash } = item;
|
||||
const obj = this.structure[field].find(x => x.objectId == objectId);
|
||||
|
||||
if (obj && contentHash && obj.contentHash == contentHash) continue;
|
||||
|
||||
const action = obj
|
||||
? {
|
||||
newName: { schemaName, pureName },
|
||||
oldName: _pick(obj, ['schemaName', 'pureName']),
|
||||
action: 'change',
|
||||
objectTypeField: field,
|
||||
objectId,
|
||||
}
|
||||
: {
|
||||
newName: { schemaName, pureName },
|
||||
action: 'add',
|
||||
objectTypeField: field,
|
||||
objectId,
|
||||
};
|
||||
res.push(action);
|
||||
}
|
||||
|
||||
return [..._compact(res), ...this.getDeletedObjects(snapshot)];
|
||||
}
|
||||
}
|
||||
|
||||
static createEmptyStructure(): DatabaseInfo {
|
||||
return {
|
||||
tables: [],
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
TriggerInfo,
|
||||
ViewInfo,
|
||||
} from 'dbgate-types';
|
||||
import _ from 'lodash';
|
||||
import _flatten from 'lodash/flatten';
|
||||
import _uniqBy from 'lodash/uniqBy'
|
||||
import { SqlDumper } from './SqlDumper';
|
||||
import { extendDatabaseInfo } from './structureTools';
|
||||
|
||||
@@ -122,9 +123,9 @@ export class SqlGenerator {
|
||||
|
||||
createForeignKeys() {
|
||||
const fks = [];
|
||||
if (this.options.createForeignKeys) fks.push(..._.flatten(this.tables.map(x => x.foreignKeys || [])));
|
||||
if (this.options.createReferences) fks.push(..._.flatten(this.tables.map(x => x.dependencies || [])));
|
||||
for (const fk of _.uniqBy(fks, 'constraintName')) {
|
||||
if (this.options.createForeignKeys) fks.push(..._flatten(this.tables.map(x => x.foreignKeys || [])));
|
||||
if (this.options.createReferences) fks.push(..._flatten(this.tables.map(x => x.dependencies || [])));
|
||||
for (const fk of _uniqBy(fks, 'constraintName')) {
|
||||
this.dmp.createForeignKey(fk);
|
||||
if (this.checkDumper()) return;
|
||||
}
|
||||
@@ -152,7 +153,7 @@ export class SqlGenerator {
|
||||
}
|
||||
}
|
||||
if (this.options.createIndexes) {
|
||||
for (const index of _.flatten(this.tables.map(x => x.indexes || []))) {
|
||||
for (const index of _flatten(this.tables.map(x => x.indexes || []))) {
|
||||
this.dmp.createIndex(index);
|
||||
}
|
||||
}
|
||||
@@ -204,7 +205,7 @@ export class SqlGenerator {
|
||||
|
||||
dropTables() {
|
||||
if (this.options.dropReferences) {
|
||||
for (const fk of _.flatten(this.tables.map(x => x.dependencies || []))) {
|
||||
for (const fk of _flatten(this.tables.map(x => x.dependencies || []))) {
|
||||
this.dmp.dropForeignKey(fk);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,7 @@ export const driverBase = {
|
||||
},
|
||||
async analyseSingleObject(pool, name, typeField = 'tables') {
|
||||
const analyser = new this.analyserClass(pool, this);
|
||||
analyser.singleObjectFilter = { ...name, typeField };
|
||||
const res = await analyser.fullAnalysis();
|
||||
return res.tables[0];
|
||||
return analyser.singleObjectAnalysis(name, typeField);
|
||||
},
|
||||
analyseSingleTable(pool, name) {
|
||||
return this.analyseSingleObject(pool, name, 'tables');
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import _isNaN from 'lodash/isNaN';
|
||||
import _isNumber from 'lodash/isNumber';
|
||||
|
||||
export function extractIntSettingsValue(settings, name, defaultValue, min = null, max = null) {
|
||||
const parsed = parseInt(settings[name]);
|
||||
if (_isNaN(parsed)) {
|
||||
return defaultValue;
|
||||
}
|
||||
if (_isNumber(parsed)) {
|
||||
if (min != null && parsed < min) return min;
|
||||
if (max != null && parsed > max) return max;
|
||||
return parsed;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
export function extractBoolSettingsValue(settings, name, defaultValue) {
|
||||
const res = settings[name];
|
||||
if (res == null) return defaultValue;
|
||||
return !!res;
|
||||
}
|
||||
@@ -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,8 +1,8 @@
|
||||
import { DatabaseInfo } from 'dbgate-types';
|
||||
import _ from 'lodash';
|
||||
import _flatten from 'lodash/flatten';
|
||||
|
||||
export function addTableDependencies(db: DatabaseInfo): DatabaseInfo {
|
||||
const allForeignKeys = _.flatten(db.tables.map(x => x.foreignKeys || []));
|
||||
const allForeignKeys = _flatten(db.tables.map(x => x.foreignKeys || []));
|
||||
return {
|
||||
...db,
|
||||
tables: db.tables.map(table => ({
|
||||
|
||||
Vendored
+1
@@ -1,6 +1,7 @@
|
||||
export interface SqlDialect {
|
||||
rangeSelect?: boolean;
|
||||
limitSelect?: boolean;
|
||||
rowNumberOverPaging?: boolean;
|
||||
stringEscapeChar: string;
|
||||
offsetFetchRangeSyntax?: boolean;
|
||||
quoteIdentifier(s: string): string;
|
||||
|
||||
Vendored
+8
-1
@@ -37,6 +37,12 @@ export interface EngineDriver {
|
||||
engine: string;
|
||||
title: string;
|
||||
defaultPort?: number;
|
||||
supportsDatabaseUrl?: boolean;
|
||||
isElectronOnly?: boolean;
|
||||
showConnectionField?: (field: string, values: any) => boolean;
|
||||
showConnectionTab?: (tab: 'ssl' | 'sshTunnel', values: any) => boolean;
|
||||
beforeConnectionSave?: (values: any) => any;
|
||||
databaseUrlPlaceholder?: string;
|
||||
connect({ server, port, user, password, database }): any;
|
||||
query(pool: any, sql: string): Promise<QueryResult>;
|
||||
stream(pool: any, sql: string, options: StreamOptions);
|
||||
@@ -59,6 +65,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>;
|
||||
@@ -74,6 +81,6 @@ export interface DatabaseModification {
|
||||
oldName?: NamedObjectInfo;
|
||||
newName?: NamedObjectInfo;
|
||||
objectId?: string;
|
||||
action: 'add' | 'remove' | 'change';
|
||||
action: 'add' | 'remove' | 'change' | 'all';
|
||||
objectTypeField: keyof DatabaseInfo;
|
||||
}
|
||||
|
||||
Vendored
+2
@@ -4,6 +4,8 @@ export interface OpenedDatabaseConnection {
|
||||
conid: string;
|
||||
database: string;
|
||||
structure: DatabaseInfo;
|
||||
analysedTime?: number;
|
||||
serverVersion?: any;
|
||||
subprocess: ChildProcess;
|
||||
disconnected?: boolean;
|
||||
status?: {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
{
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.1",
|
||||
"name": "dbgate-types",
|
||||
"homepage": "https://dbgate.org/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
|
||||
"author": "Jan Prochazka",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dbgate-web",
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.1",
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"dev": "cross-env API_URL=http://localhost:3000 rollup -c -w",
|
||||
@@ -22,10 +22,10 @@
|
||||
"ace-builds": "^1.4.8",
|
||||
"chart.js": "^2.9.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"dbgate-datalib": "^4.0.0",
|
||||
"dbgate-sqltree": "^4.0.0",
|
||||
"dbgate-tools": "^4.0.0",
|
||||
"dbgate-types": "^4.0.0",
|
||||
"dbgate-datalib": "^4.1.1",
|
||||
"dbgate-sqltree": "^4.1.1",
|
||||
"dbgate-tools": "^4.1.1",
|
||||
"dbgate-types": "^4.1.1",
|
||||
"file-selector": "^0.2.4",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
"localforage": "^1.9.0",
|
||||
@@ -50,4 +50,4 @@
|
||||
"typescript": "^3.9.3",
|
||||
"uuid": "^3.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,9 +21,25 @@
|
||||
<link rel='stylesheet' href='build/fonts/materialdesignicons.css'>
|
||||
|
||||
<script defer src='build/bundle.js'></script>
|
||||
|
||||
<style>
|
||||
#starting_dbgate_zero {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id='starting_dbgate_zero'>
|
||||
Loading DbGate App ...
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,16 +1,68 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import CommandListener from './commands/CommandListener.svelte';
|
||||
import DataGridRowHeightMeter from './datagrid/DataGridRowHeightMeter.svelte';
|
||||
import LoadingInfo from './elements/LoadingInfo.svelte';
|
||||
|
||||
import PluginsProvider from './plugins/PluginsProvider.svelte';
|
||||
import Screen from './Screen.svelte';
|
||||
import { loadingPluginStore } from './stores';
|
||||
import { setAppLoaded } from './utility/appLoadManager';
|
||||
import axiosInstance from './utility/axiosInstance';
|
||||
import ErrorHandler from './utility/ErrorHandler.svelte';
|
||||
import OpenTabsOnStartup from './utility/OpenTabsOnStartup.svelte';
|
||||
|
||||
let loadedApi = false;
|
||||
|
||||
async function loadApi() {
|
||||
try {
|
||||
const settings = await axiosInstance.get('config/get-settings');
|
||||
const connections = await axiosInstance.get('connections/list');
|
||||
const config = await axiosInstance.get('config/get');
|
||||
loadedApi = settings?.data && connections?.data && config?.data;
|
||||
if (!loadedApi) {
|
||||
console.log('API not initialized correctly, trying again in 1s');
|
||||
setTimeout(loadApi, 1000);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Error calling API, trying again in 1s');
|
||||
setTimeout(loadApi, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(loadApi);
|
||||
|
||||
onMount(() => {
|
||||
const removed = document.getElementById('starting_dbgate_zero');
|
||||
if (removed) removed.remove();
|
||||
});
|
||||
|
||||
$: {
|
||||
if (loadedApi && $loadingPluginStore?.loaded) {
|
||||
setAppLoaded();
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<DataGridRowHeightMeter />
|
||||
<ErrorHandler />
|
||||
<PluginsProvider />
|
||||
<CommandListener />
|
||||
<OpenTabsOnStartup />
|
||||
<Screen />
|
||||
|
||||
{#if loadedApi}
|
||||
<PluginsProvider />
|
||||
{#if $loadingPluginStore?.loaded}
|
||||
<OpenTabsOnStartup />
|
||||
<Screen />
|
||||
{:else}
|
||||
<LoadingInfo
|
||||
message={$loadingPluginStore.loadingPackageName
|
||||
? `Loading plugin ${$loadingPluginStore.loadingPackageName} ...`
|
||||
: 'Preparing plugins ...'}
|
||||
wrapper
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
<LoadingInfo message="Starting DbGate ..." wrapper />
|
||||
{/if}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
$: fileTypeNames = _.compact([
|
||||
...$extensions.fileFormats.filter(x => x.readerFunc).map(x => x.name),
|
||||
electron ? 'SQL' : null,
|
||||
electron ? 'SQLite database' : null,
|
||||
]);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -28,6 +28,15 @@
|
||||
onClick: () => getCurrentEditor().switchToView('table'),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.toggleLeftPanel',
|
||||
category: 'Data grid',
|
||||
name: 'Toggle left panel',
|
||||
keyText: 'Ctrl+L',
|
||||
testEnabled: () => getCurrentEditor() != null,
|
||||
onClick: () => getCurrentEditor().toggleLeftPanel(),
|
||||
});
|
||||
|
||||
function extractMacroValuesForMacro(macroValues, macro) {
|
||||
// return {};
|
||||
if (!macro) return {};
|
||||
@@ -57,6 +66,9 @@
|
||||
import _ from 'lodash';
|
||||
import registerCommand from '../commands/registerCommand';
|
||||
import { registerMenu } from '../utility/contextMenu';
|
||||
import { useSettings } from '../utility/metadataLoaders';
|
||||
import { getCurrentSettings } from '../stores';
|
||||
import { getBoolSettingsValue } from '../settings/settingsTools';
|
||||
|
||||
export let config;
|
||||
export let setConfig;
|
||||
@@ -88,6 +100,7 @@
|
||||
setContext('macroValues', macroValues);
|
||||
|
||||
let managerSize;
|
||||
const collapsedLeftColumnStore = writable(getBoolSettingsValue('dataGrid.hideLeftColumn', false));
|
||||
|
||||
$: isFormView = !!(formDisplay && formDisplay.config && formDisplay.config.isFormView);
|
||||
$: isJsonView = !!config?.isJsonView;
|
||||
@@ -120,22 +133,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
export function toggleLeftPanel() {
|
||||
collapsedLeftColumnStore.update(x => !x);
|
||||
}
|
||||
|
||||
registerMenu(
|
||||
{ command: 'dataGrid.switchToForm', tag: 'switch', hideDisabled: true },
|
||||
{ command: 'dataGrid.switchToTable', tag: 'switch', hideDisabled: true },
|
||||
{ command: 'dataGrid.switchToJson', tag: 'switch', hideDisabled: true }
|
||||
{ command: 'dataGrid.switchToJson', tag: 'switch', hideDisabled: true },
|
||||
{ command: 'dataGrid.toggleLeftPanel', tag: 'switch' }
|
||||
);
|
||||
</script>
|
||||
|
||||
<HorizontalSplitter initialValue="300px" bind:size={managerSize}>
|
||||
<HorizontalSplitter initialValue="300px" bind:size={managerSize} hideFirst={$collapsedLeftColumnStore}>
|
||||
<div class="left" slot="1">
|
||||
<WidgetColumnBar>
|
||||
<WidgetColumnBarItem
|
||||
title="Columns"
|
||||
name="columns"
|
||||
height={showReferences ? '40%' : '60%'}
|
||||
skip={freeTableColumn || isFormView}
|
||||
>
|
||||
<WidgetColumnBarItem title="Columns" name="columns" height="45%" skip={freeTableColumn || isFormView}>
|
||||
<ColumnManager {...$$props} {managerSize} {isJsonView} />
|
||||
</WidgetColumnBarItem>
|
||||
|
||||
@@ -161,7 +174,7 @@
|
||||
<ReferenceManager {...$$props} {managerSize} />
|
||||
</WidgetColumnBarItem>
|
||||
|
||||
<WidgetColumnBarItem title="Macros" name="macros" skip={!showMacros} collapsed={isDetailView}>
|
||||
<WidgetColumnBarItem title="Macros" name="macros" skip={!showMacros} collapsed>
|
||||
<MacroManager {...$$props} {managerSize} />
|
||||
</WidgetColumnBarItem>
|
||||
</WidgetColumnBar>
|
||||
@@ -177,6 +190,7 @@
|
||||
<svelte:component
|
||||
this={gridCoreComponent}
|
||||
{...$$props}
|
||||
{collapsedLeftColumnStore}
|
||||
formViewAvailable={!!formViewComponent && !!formDisplay}
|
||||
macroValues={extractMacroValuesForMacro($macroValues, $selectedMacro)}
|
||||
macroPreview={$selectedMacro}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
export let col;
|
||||
export let rowData;
|
||||
export let colIndex = undefined;
|
||||
export let hintFieldsAllowed = undefined;
|
||||
export let allowHintField = false;
|
||||
|
||||
export let isSelected = false;
|
||||
export let isFrameSelected = false;
|
||||
@@ -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>
|
||||
|
||||
@@ -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,14 @@
|
||||
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 +213,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 +235,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 +340,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 +534,7 @@
|
||||
rowData,
|
||||
column,
|
||||
value: rowData && rowData[column],
|
||||
engine: display?.driver,
|
||||
};
|
||||
})
|
||||
.filter(x => x.column);
|
||||
@@ -565,8 +579,10 @@
|
||||
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;
|
||||
|
||||
shiftDragStartCell = null;
|
||||
// event.target.closest('table').focus();
|
||||
event.preventDefault();
|
||||
if (domFocusField) domFocusField.focus();
|
||||
@@ -649,23 +665,42 @@
|
||||
}
|
||||
|
||||
function handleGridWheel(event) {
|
||||
let newFirstVisibleRowScrollIndex = firstVisibleRowScrollIndex;
|
||||
if (event.deltaY > 0) {
|
||||
newFirstVisibleRowScrollIndex += wheelRowCount;
|
||||
}
|
||||
if (event.deltaY < 0) {
|
||||
newFirstVisibleRowScrollIndex -= wheelRowCount;
|
||||
}
|
||||
let rowCount = grider.rowCount;
|
||||
if (newFirstVisibleRowScrollIndex + visibleRowCountLowerBound > rowCount) {
|
||||
newFirstVisibleRowScrollIndex = rowCount - visibleRowCountLowerBound + 1;
|
||||
}
|
||||
if (newFirstVisibleRowScrollIndex < 0) {
|
||||
newFirstVisibleRowScrollIndex = 0;
|
||||
}
|
||||
firstVisibleRowScrollIndex = newFirstVisibleRowScrollIndex;
|
||||
if (event.shiftKey) {
|
||||
let newFirstVisibleColumnScrollIndex = firstVisibleColumnScrollIndex;
|
||||
if (event.deltaY > 0) {
|
||||
newFirstVisibleColumnScrollIndex++;
|
||||
}
|
||||
if (event.deltaY < 0) {
|
||||
newFirstVisibleColumnScrollIndex--;
|
||||
}
|
||||
if (newFirstVisibleColumnScrollIndex > maxScrollColumn) {
|
||||
newFirstVisibleColumnScrollIndex = maxScrollColumn;
|
||||
}
|
||||
if (newFirstVisibleColumnScrollIndex < 0) {
|
||||
newFirstVisibleColumnScrollIndex = 0;
|
||||
}
|
||||
firstVisibleColumnScrollIndex = newFirstVisibleColumnScrollIndex;
|
||||
|
||||
domVerticalScroll.scroll(newFirstVisibleRowScrollIndex);
|
||||
domHorizontalScroll.scroll(newFirstVisibleColumnScrollIndex);
|
||||
} else {
|
||||
let newFirstVisibleRowScrollIndex = firstVisibleRowScrollIndex;
|
||||
if (event.deltaY > 0) {
|
||||
newFirstVisibleRowScrollIndex += wheelRowCount;
|
||||
}
|
||||
if (event.deltaY < 0) {
|
||||
newFirstVisibleRowScrollIndex -= wheelRowCount;
|
||||
}
|
||||
let rowCount = grider.rowCount;
|
||||
if (newFirstVisibleRowScrollIndex + visibleRowCountLowerBound > rowCount) {
|
||||
newFirstVisibleRowScrollIndex = rowCount - visibleRowCountLowerBound + 1;
|
||||
}
|
||||
if (newFirstVisibleRowScrollIndex < 0) {
|
||||
newFirstVisibleRowScrollIndex = 0;
|
||||
}
|
||||
firstVisibleRowScrollIndex = newFirstVisibleRowScrollIndex;
|
||||
|
||||
domVerticalScroll.scroll(newFirstVisibleRowScrollIndex);
|
||||
}
|
||||
}
|
||||
|
||||
function getSelectedRowIndexes() {
|
||||
@@ -721,7 +756,7 @@
|
||||
|
||||
handleCursorMove(event);
|
||||
|
||||
if (event.shiftKey) {
|
||||
if (event.shiftKey && event.keyCode != keycodes.shift) {
|
||||
selectedCells = getCellRange(shiftDragStartCell || currentCell, currentCell);
|
||||
}
|
||||
}
|
||||
@@ -944,6 +979,7 @@
|
||||
);
|
||||
|
||||
const menu = getContextMenu();
|
||||
|
||||
</script>
|
||||
|
||||
{#if !display || (!isDynamicStructure && (!columns || columns.length == 0))}
|
||||
@@ -991,7 +1027,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"
|
||||
@@ -1145,4 +1186,5 @@
|
||||
right: 40px;
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -14,12 +14,18 @@
|
||||
import { extensions } from '../stores';
|
||||
import stableStringify from 'json-stable-stringify';
|
||||
|
||||
import { useConnectionInfo, useDatabaseInfo } from '../utility/metadataLoaders';
|
||||
import {
|
||||
useConnectionInfo,
|
||||
useDatabaseInfo,
|
||||
useDatabaseServerVersion,
|
||||
useServerVersion,
|
||||
} from '../utility/metadataLoaders';
|
||||
|
||||
import DataGrid from './DataGrid.svelte';
|
||||
import ReferenceHeader from './ReferenceHeader.svelte';
|
||||
import SqlDataGridCore from './SqlDataGridCore.svelte';
|
||||
import SqlFormView from '../formview/SqlFormView.svelte';
|
||||
import { getBoolSettingsValue } from '../settings/settingsTools';
|
||||
|
||||
export let conid;
|
||||
export let database;
|
||||
@@ -37,6 +43,9 @@
|
||||
|
||||
$: connection = useConnectionInfo({ conid });
|
||||
$: dbinfo = useDatabaseInfo({ conid, database });
|
||||
$: serverVersion = useDatabaseServerVersion({ conid, database });
|
||||
|
||||
// $: console.log('serverVersion', $serverVersion);
|
||||
|
||||
let myLoadedTime = 0;
|
||||
|
||||
@@ -44,29 +53,35 @@
|
||||
|
||||
// $: console.log('display', display);
|
||||
|
||||
$: display = connection
|
||||
? new TableGridDisplay(
|
||||
{ schemaName, pureName },
|
||||
findEngineDriver($connection, $extensions),
|
||||
config,
|
||||
setConfig,
|
||||
cache,
|
||||
setCache,
|
||||
$dbinfo
|
||||
)
|
||||
: null;
|
||||
$: display =
|
||||
connection && $serverVersion
|
||||
? new TableGridDisplay(
|
||||
{ schemaName, pureName },
|
||||
findEngineDriver($connection, $extensions),
|
||||
config,
|
||||
setConfig,
|
||||
cache,
|
||||
setCache,
|
||||
$dbinfo,
|
||||
{ showHintColumns: getBoolSettingsValue('dataGrid.showHintColumns', true) },
|
||||
$serverVersion
|
||||
)
|
||||
: null;
|
||||
|
||||
$: formDisplay = connection
|
||||
? new TableFormViewDisplay(
|
||||
{ schemaName, pureName },
|
||||
findEngineDriver($connection, $extensions),
|
||||
config,
|
||||
setConfig,
|
||||
cache,
|
||||
setCache,
|
||||
$dbinfo
|
||||
)
|
||||
: null;
|
||||
$: formDisplay =
|
||||
connection && $serverVersion
|
||||
? new TableFormViewDisplay(
|
||||
{ schemaName, pureName },
|
||||
findEngineDriver($connection, $extensions),
|
||||
config,
|
||||
setConfig,
|
||||
cache,
|
||||
setCache,
|
||||
$dbinfo,
|
||||
{ showHintColumns: getBoolSettingsValue('dataGrid.showHintColumns', true) },
|
||||
$serverVersion
|
||||
)
|
||||
: null;
|
||||
|
||||
const setChildConfig = (value, reference = undefined) => {
|
||||
if (_.isFunction(value)) {
|
||||
|
||||
@@ -54,6 +54,9 @@ export function countColumnSizes(grider: Grider, columns, containerWidth, displa
|
||||
context.font = '14px Helvetica';
|
||||
for (let rowIndex = 0; rowIndex < Math.min(grider.rowCount, 20); rowIndex += 1) {
|
||||
const row = grider.getRowData(rowIndex);
|
||||
if (!row) {
|
||||
continue;
|
||||
}
|
||||
for (let colIndex = 0; colIndex < columns.length; colIndex++) {
|
||||
const uqName = columns[colIndex].uniqueName;
|
||||
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
|
||||
<span class="nowrap">
|
||||
<FontIcon icon={getConstraintIcon(constraintType)} />
|
||||
{constraintName}
|
||||
{constraintName || '(without name)'}
|
||||
</span>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
export let isSplitter = true;
|
||||
export let initialValue = undefined;
|
||||
export let hideFirst = false;
|
||||
|
||||
export let size = 0;
|
||||
let clientWidth;
|
||||
@@ -24,11 +25,15 @@
|
||||
</script>
|
||||
|
||||
<div class="container" bind:clientWidth>
|
||||
<div class="child1" style={isSplitter ? `width:${size}px; min-width:${size}px; max-width:${size}px}` : `flex:1`}>
|
||||
<slot name="1" />
|
||||
</div>
|
||||
{#if !hideFirst}
|
||||
<div class="child1" style={isSplitter ? `width:${size}px; min-width:${size}px; max-width:${size}px}` : `flex:1`}>
|
||||
<slot name="1" />
|
||||
</div>
|
||||
{/if}
|
||||
{#if isSplitter}
|
||||
<div class="horizontal-split-handle" use:splitterDrag={'clientX'} on:resizeSplitter={e => (size += e.detail)} />
|
||||
{#if !hideFirst}
|
||||
<div class="horizontal-split-handle" use:splitterDrag={'clientX'} on:resizeSplitter={e => (size += e.detail)} />
|
||||
{/if}
|
||||
<div class="child2">
|
||||
<slot name="2" />
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
export let collection;
|
||||
export let columns;
|
||||
export let showIfEmpty = false;
|
||||
|
||||
</script>
|
||||
|
||||
{#if collection?.length > 0 || showIfEmpty}
|
||||
@@ -55,24 +56,24 @@
|
||||
</TableControl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: var(--theme-bg-1);
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin: 20px;
|
||||
}
|
||||
</style>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: var(--theme-bg-1);
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user