Compare commits
148 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8489c171f3 | |||
| 592865b16e | |||
| 012d3ec2e1 | |||
| d84adcca5d | |||
| b1ae7d53b9 | |||
| 9a5287725b | |||
| 5ccd724166 | |||
| 5e4c286427 | |||
| 70413b954b | |||
| 07b2a3e923 | |||
| 94a91d5fed | |||
| 576fc2062c | |||
| 37a8783751 | |||
| f42d78b2fb | |||
| 792fa75ccd | |||
| cbd3f1bae9 | |||
| cd92231769 | |||
| ecad1ae01b | |||
| dc576e6ced | |||
| 6cca81f8f1 | |||
| a9f1f19696 | |||
| 390ddac75b | |||
| e2e7c6f06b | |||
| 3a3d0683d5 | |||
| d5534dcf07 | |||
| b0a86f9f4a | |||
| b833a30148 | |||
| d9c1bbaa39 | |||
| 4b74dbbd68 | |||
| 9bcc61551c | |||
| ed71ef312d | |||
| 4fa043b7e5 | |||
| 83725dd349 | |||
| 4e25b71b06 | |||
| 607ae7c872 | |||
| 66ade5823f | |||
| ebfa0a1939 | |||
| 909591404f | |||
| 7a5f2a70ad | |||
| d41b254058 | |||
| 435d06ffb9 | |||
| f4a4eb7f9e | |||
| 9910bbead3 | |||
| cb619a0fe0 | |||
| b0d61f974c | |||
| 8c051ff5f7 | |||
| f713a4b183 | |||
| 6c7e263f0e | |||
| ec3bfb4fae | |||
| 712ec8e6ee | |||
| 4da0b25f44 | |||
| 9b60b7a003 | |||
| 8ed73195c5 | |||
| c69fcd5eff | |||
| 310774db3b | |||
| 1dd166b563 | |||
| 0497f541cb | |||
| 42333a97b8 | |||
| 494c3c8e4a | |||
| 69a87bc076 | |||
| bf4eb19ef5 | |||
| 225518df3e | |||
| 0028240552 | |||
| 44be1bdd11 | |||
| e0703b1bae | |||
| a240681d6d | |||
| f5906587db | |||
| dc0001a8cd | |||
| f19835203f | |||
| 2a2debbb88 | |||
| 23cb3a4b12 | |||
| 13d4d34453 | |||
| 2adca64159 | |||
| 18519b5519 | |||
| 4ddea55d23 | |||
| 5858061349 | |||
| d86a5c0cb4 | |||
| c712005e33 | |||
| 7e28e2257e | |||
| d0c7d591c8 | |||
| 17b73a58c8 | |||
| d765591e8c | |||
| be0aeeb2c8 | |||
| 23b345c898 | |||
| 1d85a17533 | |||
| 7a3c46b691 | |||
| d647d30258 | |||
| 8b511a0532 | |||
| ccb52e9b58 | |||
| f60e1190c8 | |||
| da5dd7ac62 | |||
| 08abec7c3e | |||
| b3839def32 | |||
| efe15bf0bb | |||
| f9e167fc7b | |||
| b35e8fcdf4 | |||
| 4bdd988682 | |||
| 94f21472be | |||
| dd33d96ef6 | |||
| 7604889b72 | |||
| 1382461bdc | |||
| 833f029ab5 | |||
| 04d39f6646 | |||
| 4de8a5b038 | |||
| 1dfdeed018 | |||
| 4892e46795 | |||
| 5aff68d313 | |||
| cdd4382266 | |||
| bbd00ac94d | |||
| dba3183c94 | |||
| a2906cca9d | |||
| 140291696b | |||
| 975643fb24 | |||
| bf9a933fb1 | |||
| 643b792069 | |||
| b4d0ccbd8c | |||
| c9bf949d02 | |||
| 074390ac11 | |||
| 45e54475d0 | |||
| f157fc77d4 | |||
| dac1110404 | |||
| da00e1c228 | |||
| 9ed1cdf4b7 | |||
| 18b7792370 | |||
| 53b6b71a29 | |||
| b2204e1d77 | |||
| e7ac7558ca | |||
| c5a7f458ba | |||
| 8ce5e68c0d | |||
| e9256fe20e | |||
| 5913788035 | |||
| 6c9c4be311 | |||
| 1454ddacb8 | |||
| 2b26779ea8 | |||
| 7781ad69cf | |||
| 1a7f06342f | |||
| 2f820d8dac | |||
| 1535dfd407 | |||
| 3fe7d652b2 | |||
| 7fc8b2901b | |||
| a56f59ceba | |||
| 2ac1072357 | |||
| 24c26a6d87 | |||
| 83693e9f2c | |||
| 59efdd735c | |||
| 41afd177ef | |||
| 0137b191b9 | |||
| 054b90c90d |
@@ -51,8 +51,10 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }} # token for electron publish
|
||||
|
||||
WIN_CSC_LINK: ${{ secrets.WINCERT_CERTIFICATE }}
|
||||
WIN_CSC_KEY_PASSWORD: ${{ secrets.WINCERT_PASSWORD }}
|
||||
WIN_CSC_LINK: ${{ secrets.WINCERT_2025 }}
|
||||
WIN_CSC_KEY_PASSWORD: ${{ secrets.WINCERT_2025_PASSWORD }}
|
||||
# WIN_CSC_LINK: ${{ secrets.WINCERT_CERTIFICATE }}
|
||||
# WIN_CSC_KEY_PASSWORD: ${{ secrets.WINCERT_PASSWORD }}
|
||||
|
||||
CSC_LINK: ${{ secrets.APPLECERT_CERTIFICATE }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.APPLECERT_PASSWORD }}
|
||||
|
||||
@@ -57,8 +57,10 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }} # token for electron publish
|
||||
|
||||
WIN_CSC_LINK: ${{ secrets.WINCERT_CERTIFICATE }}
|
||||
WIN_CSC_KEY_PASSWORD: ${{ secrets.WINCERT_PASSWORD }}
|
||||
WIN_CSC_LINK: ${{ secrets.WINCERT_2025 }}
|
||||
WIN_CSC_KEY_PASSWORD: ${{ secrets.WINCERT_2025_PASSWORD }}
|
||||
# WIN_CSC_LINK: ${{ secrets.WINCERT_CERTIFICATE }}
|
||||
# WIN_CSC_KEY_PASSWORD: ${{ secrets.WINCERT_PASSWORD }}
|
||||
|
||||
CSC_LINK: ${{ secrets.APPLECERT_CERTIFICATE }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.APPLECERT_PASSWORD }}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
name: Docker image BETA
|
||||
|
||||
# on: [push]
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+'
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+-docker.[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@v2
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.x
|
||||
- name: yarn install
|
||||
run: |
|
||||
# yarn --version
|
||||
# yarn config set network-timeout 300000
|
||||
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
|
||||
- name: Build alpine docker image
|
||||
run: |
|
||||
docker build ./docker -t dbgate -f docker/Dockerfile-alpine
|
||||
- name: Push alpine docker image
|
||||
run: |
|
||||
docker tag dbgate dbgate/dbgate:beta-alpine
|
||||
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
|
||||
docker push dbgate/dbgate:beta-alpine
|
||||
@@ -1,17 +1,11 @@
|
||||
name: Docker image
|
||||
|
||||
# on: [push]
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
# - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||
|
||||
# on:
|
||||
# push:
|
||||
# branches:
|
||||
# - production
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+'
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+-docker.[0-9]+'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -30,12 +24,43 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
dbgate/dbgate
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
type=raw,value=beta,enable=${{ contains(github.ref_name, '-docker.') || contains(github.ref_name, '-beta.') }}
|
||||
|
||||
type=match,pattern=\d+.\d+.\d+,enable=${{ !contains(github.ref_name, '-docker.') && !contains(github.ref_name, '-beta.') }}
|
||||
type=raw,value=latest,enable=${{ !contains(github.ref_name, '-docker.') && !contains(github.ref_name, '-beta.') }}
|
||||
|
||||
- name: Docker alpine meta
|
||||
id: alpmeta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
dbgate/dbgate
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
type=raw,value=beta-alpine,enable=${{ contains(github.ref_name, '-docker.') || contains(github.ref_name, '-beta.') }}
|
||||
|
||||
type=match,pattern=\d+.\d+.\d+,suffix=-alpine,enable=${{ !contains(github.ref_name, '-docker.') && !contains(github.ref_name, '-beta.') }}
|
||||
type=raw,value=alpine,enable=${{ !contains(github.ref_name, '-docker.') && !contains(github.ref_name, '-beta.') }}
|
||||
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.x
|
||||
- name: yarn install
|
||||
run: |
|
||||
# yarn --version
|
||||
# yarn config set network-timeout 300000
|
||||
yarn install
|
||||
- name: setCurrentVersion
|
||||
run: |
|
||||
@@ -43,19 +68,28 @@ jobs:
|
||||
- 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
|
||||
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
|
||||
docker push dbgate/dbgate
|
||||
- name: Build alpine docker image
|
||||
run: |
|
||||
docker build ./docker -t dbgate -f docker/Dockerfile-alpine
|
||||
- name: Push alpine docker image
|
||||
run: |
|
||||
docker tag dbgate dbgate/dbgate:alpine
|
||||
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
|
||||
docker push dbgate/dbgate:alpine
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
push: true
|
||||
context: ./docker
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
|
||||
- name: Build and push alpine
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
push: true
|
||||
context: ./docker
|
||||
file: ./docker/Dockerfile-alpine
|
||||
tags: ${{ steps.alpmeta.outputs.tags }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
16.14.2
|
||||
@@ -8,6 +8,45 @@ Builds:
|
||||
- linux - application for linux
|
||||
- win - application for Windows
|
||||
|
||||
### 5.1.5
|
||||
- ADDED: Support perspectives for MongoDB - MongoDB query designer
|
||||
- ADDED: Show JSON content directly in the overview #395
|
||||
- CHANGED: OSX Command H shortcut for hiding window #390
|
||||
- ADDED: Uppercase Autocomplete Suggestions #389
|
||||
- FIXED: Record view left/right arrows cause start record number to be treated as string #388
|
||||
- FIXED: MongoDb ObjectId behaviour not consistent in nested objects #387
|
||||
- FIXED: demo.dbgate.org - beta version crash 5.1.5-beta.3 #386
|
||||
- ADDED: connect via socket - configurable via environment variables #358
|
||||
|
||||
### 5.1.4
|
||||
- ADDED: Drop database commands #384
|
||||
- ADDED: Customizable Redis key separator #379
|
||||
- ADDED: ARM support for docker images
|
||||
- ADDED: Version tags for docker images
|
||||
- ADDED: Better SQL command splitting and highlighting
|
||||
- ADDED: Unsaved marker for SQL files
|
||||
|
||||
### 5.1.3
|
||||
- ADDED: Editing multiline cell values #378 #371 #359
|
||||
- ADDED: Truncate table #333
|
||||
- ADDED: Perspectives - show row count
|
||||
- ADDED: Query - error markers in gutter area
|
||||
- ADDED: Query - ability to execute query elements from gutter
|
||||
- FIXED: Correct error line numbers returned from queries
|
||||
|
||||
### 5.1.2
|
||||
- FIXED: MongoDb any export function does not work. #373
|
||||
- ADDED: Query Designer short order more flexibility #372
|
||||
- ADDED: Form View move between records #370
|
||||
- ADDED: Custom SQL conditions in query designer and table filtering #369
|
||||
- ADDED: Query Designer filter eq to X or IS NULL #368
|
||||
- FIXED: Query designer, open a saved query lost sort order #363
|
||||
- ADDED: Query designer reorder columns #362
|
||||
- ADDED: connect via socket #358
|
||||
- FIXED: Show affected rows after UPDATE/DELETE/INSERT #361
|
||||
- ADDED: Perspective cell formatters - JSON, image
|
||||
- ADDED: Perspectives - cells without joined data are gray
|
||||
|
||||
### 5.1.1
|
||||
- ADDED: Perspective designer
|
||||
- FIXED: NULL,NOT NULL filter datatime columns #356
|
||||
|
||||
@@ -80,7 +80,8 @@ Any contributions are welcome. If you want to contribute without coding, conside
|
||||
* Tell your friends about DbGate or share on social networks - when more people will use DbGate, it will grow to be better
|
||||
* Write review on [Slant.co](https://www.slant.co/improve/options/41086/~dbgate-review) or [G2](https://www.g2.com/products/dbgate/reviews)
|
||||
* Create issue, if you find problem in app, or you have idea to new feature. If issue already exists, you could leave comment on it, to prioritise most wanted issues.
|
||||
* Become a backer on [Open collective](https://opencollective.com/dbgate)
|
||||
* Create some tutorial video on [youtube](https://www.youtube.com/playlist?list=PLCo7KjCVXhr0RfUSjM9wJMsp_ShL1q61A)
|
||||
* Become a backer on [GitHub sponsors](https://github.com/sponsors/dbgate) or [Open collective](https://opencollective.com/dbgate)
|
||||
* Where a small coding is acceptable for you, you could [create plugin](https://dbgate.org/docs/plugin-development.html). Plugins for new themes can be created actually without JS coding.
|
||||
|
||||
Thank you!
|
||||
|
||||
@@ -69,6 +69,7 @@ module.exports = ({ editMenu }) => [
|
||||
{ command: 'app.toggleDevTools', hideDisabled: true },
|
||||
{ command: 'app.toggleFullScreen', hideDisabled: true },
|
||||
{ command: 'app.minimize', hideDisabled: true },
|
||||
{ command: 'toggle.sidebar' },
|
||||
{ divider: true },
|
||||
{ command: 'theme.changeTheme', hideDisabled: true },
|
||||
{ command: 'settings.show' },
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 17.804 17.804" style="enable-background:new 0 0 17.804 17.804;" xml:space="preserve">
|
||||
<g>
|
||||
<g id="c98_play">
|
||||
<path fill='#ccc' d="M2.067,0.043C2.21-0.028,2.372-0.008,2.493,0.085l13.312,8.503c0.094,0.078,0.154,0.191,0.154,0.313
|
||||
c0,0.12-0.061,0.237-0.154,0.314L2.492,17.717c-0.07,0.057-0.162,0.087-0.25,0.087l-0.176-0.04
|
||||
c-0.136-0.065-0.222-0.207-0.222-0.361V0.402C1.844,0.25,1.93,0.107,2.067,0.043z"/>
|
||||
</g>
|
||||
<g id="Capa_1_78_">
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 733 B |
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 17.804 17.804" style="enable-background:new 0 0 17.804 17.804;" xml:space="preserve">
|
||||
<g>
|
||||
<g id="c98_play">
|
||||
<path fill='#444' d="M2.067,0.043C2.21-0.028,2.372-0.008,2.493,0.085l13.312,8.503c0.094,0.078,0.154,0.191,0.154,0.313
|
||||
c0,0.12-0.061,0.237-0.154,0.314L2.492,17.717c-0.07,0.057-0.162,0.087-0.25,0.087l-0.176-0.04
|
||||
c-0.136-0.065-0.222-0.207-0.222-0.361V0.402C1.844,0.25,1.93,0.107,2.067,0.043z"/>
|
||||
</g>
|
||||
<g id="Capa_1_78_">
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 733 B |
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "5.1.2",
|
||||
"version": "5.1.6-beta.7",
|
||||
"name": "dbgate-all",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
@@ -16,6 +16,7 @@
|
||||
"start:app:debug:ssh": "cd app && cross-env DEBUG=ssh yarn start",
|
||||
"start:api:portal": "yarn workspace dbgate-api start:portal",
|
||||
"start:api:singledb": "yarn workspace dbgate-api start:singledb",
|
||||
"start:api:auth": "yarn workspace dbgate-api start:auth",
|
||||
"start:web": "yarn workspace dbgate-web dev",
|
||||
"start:sqltree": "yarn workspace dbgate-sqltree start",
|
||||
"start:tools": "yarn workspace dbgate-tools start",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
.env
|
||||
@@ -17,6 +17,7 @@
|
||||
"dbgate"
|
||||
],
|
||||
"dependencies": {
|
||||
"activedirectory2": "^2.1.0",
|
||||
"async-lock": "^1.2.4",
|
||||
"axios": "^0.21.1",
|
||||
"body-parser": "^1.19.0",
|
||||
@@ -25,7 +26,7 @@
|
||||
"compare-versions": "^3.6.0",
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^6.0.3",
|
||||
"dbgate-query-splitter": "^4.9.0",
|
||||
"dbgate-query-splitter": "^4.9.2",
|
||||
"dbgate-sqltree": "^5.0.0-alpha.1",
|
||||
"dbgate-tools": "^5.0.0-alpha.1",
|
||||
"debug": "^4.3.4",
|
||||
@@ -42,6 +43,7 @@
|
||||
"is-electron": "^2.2.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"line-reader": "^0.4.0",
|
||||
"lodash": "^4.17.21",
|
||||
"ncp": "^2.0.0",
|
||||
@@ -57,6 +59,7 @@
|
||||
"start": "env-cmd node src/index.js --listen-api",
|
||||
"start:portal": "env-cmd -f env/portal/.env node src/index.js --listen-api",
|
||||
"start:singledb": "env-cmd -f env/singledb/.env node src/index.js --listen-api",
|
||||
"start:auth": "env-cmd -f env/auth/.env node src/index.js --listen-api",
|
||||
"start:filedb": "env-cmd node src/index.js /home/jena/test/chinook/Chinook.db --listen-api",
|
||||
"start:singleconn": "env-cmd node src/index.js --server localhost --user root --port 3307 --engine mysql@dbgate-plugin-mysql --password test --listen-api",
|
||||
"ts": "tsc",
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
const axios = require('axios');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const getExpressPath = require('../utility/getExpressPath');
|
||||
const uuidv1 = require('uuid/v1');
|
||||
const { getLogins } = require('../utility/hasPermission');
|
||||
const AD = require('activedirectory2').promiseWrapper;
|
||||
|
||||
const tokenSecret = uuidv1();
|
||||
|
||||
function shouldAuthorizeApi() {
|
||||
const logins = getLogins();
|
||||
return !!process.env.OAUTH_AUTH || !!process.env.AD_URL || (!!logins && !process.env.BASIC_AUTH);
|
||||
}
|
||||
|
||||
function getTokenLifetime() {
|
||||
return process.env.TOKEN_LIFETIME || '1d';
|
||||
}
|
||||
|
||||
function unauthorizedResponse(req, res, text) {
|
||||
// if (req.path == getExpressPath('/config/get-settings')) {
|
||||
// return res.json({});
|
||||
// }
|
||||
// if (req.path == getExpressPath('/connections/list')) {
|
||||
// return res.json([]);
|
||||
// }
|
||||
return res.sendStatus(401).send(text);
|
||||
}
|
||||
|
||||
function authMiddleware(req, res, next) {
|
||||
const SKIP_AUTH_PATHS = ['/config/get', '/auth/oauth-token', '/auth/login', '/stream'];
|
||||
|
||||
if (!shouldAuthorizeApi()) {
|
||||
return next();
|
||||
}
|
||||
let skipAuth = !!SKIP_AUTH_PATHS.find(x => req.path == getExpressPath(x));
|
||||
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader) {
|
||||
if (skipAuth) {
|
||||
return next();
|
||||
}
|
||||
return unauthorizedResponse(req, res, 'missing authorization header');
|
||||
}
|
||||
const token = authHeader.split(' ')[1];
|
||||
try {
|
||||
const decoded = jwt.verify(token, tokenSecret);
|
||||
req.user = decoded;
|
||||
return next();
|
||||
} catch (err) {
|
||||
if (skipAuth) {
|
||||
return next();
|
||||
}
|
||||
|
||||
console.log('Sending invalid token error', err.message);
|
||||
|
||||
return unauthorizedResponse(req, res, 'invalid token');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
oauthToken_meta: true,
|
||||
async oauthToken(params) {
|
||||
const { redirectUri, code } = params;
|
||||
|
||||
const resp = await axios.default.post(
|
||||
`${process.env.OAUTH_TOKEN}`,
|
||||
`grant_type=authorization_code&code=${encodeURIComponent(code)}&redirect_uri=${encodeURIComponent(
|
||||
redirectUri
|
||||
)}&client_id=${process.env.OAUTH_CLIENT_ID}&client_secret=${process.env.OAUTH_CLIENT_SECRET}`
|
||||
);
|
||||
|
||||
const { access_token, refresh_token } = resp.data;
|
||||
|
||||
const payload = jwt.decode(access_token);
|
||||
|
||||
console.log('User payload returned from OAUTH:', payload);
|
||||
|
||||
const login = process.env.OAUTH_LOGIN_FIELD ? payload[process.env.OAUTH_LOGIN_FIELD] : 'oauth';
|
||||
|
||||
if (
|
||||
process.env.OAUTH_ALLOWED_LOGINS &&
|
||||
!process.env.OAUTH_ALLOWED_LOGINS.split(',').find(x => x.toLowerCase().trim() == login.toLowerCase().trim())
|
||||
) {
|
||||
return { error: `Username ${login} not allowed to log in` };
|
||||
}
|
||||
if (access_token) {
|
||||
return {
|
||||
accessToken: jwt.sign({ login }, tokenSecret, { expiresIn: getTokenLifetime() }),
|
||||
};
|
||||
}
|
||||
|
||||
return { error: 'Token not found' };
|
||||
},
|
||||
login_meta: true,
|
||||
async login(params) {
|
||||
const { login, password } = params;
|
||||
|
||||
if (process.env.AD_URL) {
|
||||
const adConfig = {
|
||||
url: process.env.AD_URL,
|
||||
baseDN: process.env.AD_BASEDN,
|
||||
username: process.env.AD_USERNAME,
|
||||
password: process.env.AD_PASSOWRD,
|
||||
};
|
||||
const ad = new AD(adConfig);
|
||||
try {
|
||||
const res = await ad.authenticate(login, password);
|
||||
if (!res) {
|
||||
return { error: 'Login failed' };
|
||||
}
|
||||
if (
|
||||
process.env.AD_ALLOWED_LOGINS &&
|
||||
!process.env.AD_ALLOWED_LOGINS.split(',').find(x => x.toLowerCase().trim() == login.toLowerCase().trim())
|
||||
) {
|
||||
return { error: `Username ${login} not allowed to log in` };
|
||||
}
|
||||
return {
|
||||
accessToken: jwt.sign({ login }, tokenSecret, { expiresIn: getTokenLifetime() }),
|
||||
};
|
||||
} catch (err) {
|
||||
console.log('Failed active directory authentization', err.message);
|
||||
return {
|
||||
error: err.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const logins = getLogins();
|
||||
if (!logins) {
|
||||
return { error: 'Logins not configured' };
|
||||
}
|
||||
if (logins.find(x => x.login == login)?.password == password) {
|
||||
return {
|
||||
accessToken: jwt.sign({ login }, tokenSecret, { expiresIn: getTokenLifetime() }),
|
||||
};
|
||||
}
|
||||
return { error: 'Invalid credentials' };
|
||||
},
|
||||
|
||||
authMiddleware,
|
||||
shouldAuthorizeApi,
|
||||
};
|
||||
@@ -28,7 +28,7 @@ module.exports = {
|
||||
get_meta: true,
|
||||
async get(_params, req) {
|
||||
const logins = getLogins();
|
||||
const login = logins ? logins.find(x => x.login == (req.auth && req.auth.user)) : null;
|
||||
const login = req.user ? req.user.login : logins ? logins.find(x => x.login == (req.auth && req.auth.user)) : null;
|
||||
const permissions = login ? login.permissions : process.env.PERMISSIONS;
|
||||
|
||||
return {
|
||||
@@ -40,6 +40,9 @@ module.exports = {
|
||||
isDocker: platformInfo.isDocker,
|
||||
permissions,
|
||||
login,
|
||||
oauth: process.env.OAUTH_AUTH,
|
||||
oauthLogout: process.env.OAUTH_LOGOUT,
|
||||
isLoginForm: !!process.env.AD_URL || (!!logins && !process.env.BASIC_AUTH),
|
||||
...currentVersion,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -53,6 +53,8 @@ function getPortalCollections() {
|
||||
databaseUrl: process.env[`URL_${id}`],
|
||||
useDatabaseUrl: !!process.env[`URL_${id}`],
|
||||
databaseFile: process.env[`FILE_${id}`],
|
||||
socketPath: process.env[`SOCKET_PATH_${id}`],
|
||||
authType: process.env[`AUTH_TYPE_${id}`] || (process.env[`SOCKET_PATH_${id}`] ? 'socket' : undefined),
|
||||
defaultDatabase:
|
||||
process.env[`DATABASE_${id}`] ||
|
||||
(process.env[`FILE_${id}`] ? getDatabaseFileLabel(process.env[`FILE_${id}`]) : null),
|
||||
@@ -60,6 +62,7 @@ function getPortalCollections() {
|
||||
displayName: process.env[`LABEL_${id}`],
|
||||
isReadOnly: process.env[`READONLY_${id}`],
|
||||
databases: process.env[`DBCONFIG_${id}`] ? safeJsonParse(process.env[`DBCONFIG_${id}`]) : null,
|
||||
parent: process.env[`PARENT_${id}`] || undefined,
|
||||
|
||||
// SSH tunnel
|
||||
useSshTunnel: process.env[`USE_SSH_${id}`],
|
||||
|
||||
@@ -152,4 +152,13 @@ module.exports = {
|
||||
opened.subprocess.send({ msgtype: 'createDatabase', name });
|
||||
return { status: 'ok' };
|
||||
},
|
||||
|
||||
dropDatabase_meta: true,
|
||||
async dropDatabase({ conid, name }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid);
|
||||
if (opened.connection.isReadOnly) return false;
|
||||
opened.subprocess.send({ msgtype: 'dropDatabase', name });
|
||||
return { status: 'ok' };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -103,6 +103,12 @@ module.exports = {
|
||||
if (handleProcessCommunication(message, subprocess)) return;
|
||||
this[`handle_${msgtype}`](sesid, message);
|
||||
});
|
||||
subprocess.on('exit', () => {
|
||||
this.opened = this.opened.filter(x => x.sesid != sesid);
|
||||
this.dispatchMessage(sesid, 'Query session closed');
|
||||
socket.emit(`session-closed-${sesid}`);
|
||||
});
|
||||
|
||||
subprocess.send({ msgtype: 'connect', ...connection, database });
|
||||
return _.pick(newOpened, ['conid', 'database', 'sesid']);
|
||||
},
|
||||
@@ -165,6 +171,17 @@ module.exports = {
|
||||
return { state: 'ok' };
|
||||
},
|
||||
|
||||
ping_meta: true,
|
||||
async ping({ sesid }) {
|
||||
const session = this.opened.find(x => x.sesid == sesid);
|
||||
if (!session) {
|
||||
throw new Error('Invalid session');
|
||||
}
|
||||
session.subprocess.send({ msgtype: 'ping' });
|
||||
|
||||
return { state: 'ok' };
|
||||
},
|
||||
|
||||
// runCommand_meta: true,
|
||||
// async runCommand({ conid, database, sql }) {
|
||||
// console.log(`Running SQL command , conid=${conid}, database=${database}, sql=${sql}`);
|
||||
|
||||
@@ -20,6 +20,7 @@ const jsldata = require('./controllers/jsldata');
|
||||
const config = require('./controllers/config');
|
||||
const archive = require('./controllers/archive');
|
||||
const apps = require('./controllers/apps');
|
||||
const auth = require('./controllers/auth');
|
||||
const uploads = require('./controllers/uploads');
|
||||
const plugins = require('./controllers/plugins');
|
||||
const files = require('./controllers/files');
|
||||
@@ -41,7 +42,7 @@ function start() {
|
||||
const server = http.createServer(app);
|
||||
|
||||
const logins = getLogins();
|
||||
if (logins) {
|
||||
if (logins && process.env.BASIC_AUTH) {
|
||||
app.use(
|
||||
basicAuth({
|
||||
users: _.fromPairs(logins.map(x => [x.login, x.password])),
|
||||
@@ -53,6 +54,10 @@ function start() {
|
||||
|
||||
app.use(cors());
|
||||
|
||||
if (auth.shouldAuthorizeApi()) {
|
||||
app.use(auth.authMiddleware);
|
||||
}
|
||||
|
||||
app.get(getExpressPath('/stream'), async function (req, res) {
|
||||
res.set({
|
||||
'Cache-Control': 'no-cache',
|
||||
@@ -157,6 +162,7 @@ function useAllControllers(app, electron) {
|
||||
useController(app, electron, '/scheduler', scheduler);
|
||||
useController(app, electron, '/query-history', queryHistory);
|
||||
useController(app, electron, '/apps', apps);
|
||||
useController(app, electron, '/auth', auth);
|
||||
}
|
||||
|
||||
function setElectronSender(electronSender) {
|
||||
|
||||
@@ -177,7 +177,7 @@ async function handleQueryData({ msgid, sql }, skipReadonlyCheck = false) {
|
||||
const res = await driver.query(systemConnection, sql);
|
||||
process.send({ msgtype: 'response', msgid, ...res });
|
||||
} catch (err) {
|
||||
process.send({ msgtype: 'response', msgid, errorMessage: err.message });
|
||||
process.send({ msgtype: 'response', msgid, errorMessage: err.message || 'Error executing SQL script' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,11 +335,11 @@ function start() {
|
||||
|
||||
setInterval(() => {
|
||||
const time = new Date().getTime();
|
||||
if (time - lastPing > 120 * 1000) {
|
||||
if (time - lastPing > 40 * 1000) {
|
||||
console.log('Database connection not alive, exiting');
|
||||
process.exit(0);
|
||||
}
|
||||
}, 60 * 1000);
|
||||
}, 10 * 1000);
|
||||
|
||||
process.on('message', async message => {
|
||||
if (handleProcessCommunication(message)) return;
|
||||
|
||||
@@ -2,7 +2,6 @@ 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');
|
||||
const connectUtility = require('../utility/connectUtility');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
|
||||
@@ -81,14 +80,16 @@ function handlePing() {
|
||||
lastPing = new Date().getTime();
|
||||
}
|
||||
|
||||
async function handleCreateDatabase({ name }) {
|
||||
async function handleDatabaseOp(op, { name }) {
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
systemConnection = await connectUtility(driver, storedConnection, 'app');
|
||||
console.log(`RUNNING SCRIPT: CREATE DATABASE ${driver.dialect.quoteIdentifier(name)}`);
|
||||
if (driver.createDatabase) {
|
||||
await driver.createDatabase(systemConnection, name);
|
||||
if (driver[op]) {
|
||||
await driver[op](systemConnection, name);
|
||||
} else {
|
||||
await driver.query(systemConnection, `CREATE DATABASE ${driver.dialect.quoteIdentifier(name)}`);
|
||||
const dmp = driver.createDumper();
|
||||
dmp[op](name);
|
||||
console.log(`RUNNING SCRIPT: ${dmp.s}`);
|
||||
await driver.query(systemConnection, dmp.s);
|
||||
}
|
||||
await handleRefresh();
|
||||
}
|
||||
@@ -96,7 +97,8 @@ async function handleCreateDatabase({ name }) {
|
||||
const messageHandlers = {
|
||||
connect: handleConnect,
|
||||
ping: handlePing,
|
||||
createDatabase: handleCreateDatabase,
|
||||
createDatabase: props => handleDatabaseOp('createDatabase', props),
|
||||
dropDatabase: props => handleDatabaseOp('dropDatabase', props),
|
||||
};
|
||||
|
||||
async function handleMessage({ msgtype, ...other }) {
|
||||
@@ -109,11 +111,11 @@ function start() {
|
||||
|
||||
setInterval(() => {
|
||||
const time = new Date().getTime();
|
||||
if (time - lastPing > 120 * 1000) {
|
||||
if (time - lastPing > 40 * 1000) {
|
||||
console.log('Server connection not alive, exiting');
|
||||
process.exit(0);
|
||||
}
|
||||
}, 60 * 1000);
|
||||
}, 10 * 1000);
|
||||
|
||||
process.on('message', async message => {
|
||||
if (handleProcessCommunication(message)) return;
|
||||
|
||||
@@ -15,6 +15,7 @@ let systemConnection;
|
||||
let storedConnection;
|
||||
let afterConnectCallbacks = [];
|
||||
// let currentHandlers = [];
|
||||
let lastPing = null;
|
||||
|
||||
class TableWriter {
|
||||
constructor() {
|
||||
@@ -101,8 +102,9 @@ class TableWriter {
|
||||
}
|
||||
|
||||
class StreamHandler {
|
||||
constructor(resultIndexHolder, resolve) {
|
||||
constructor(resultIndexHolder, resolve, startLine) {
|
||||
this.recordset = this.recordset.bind(this);
|
||||
this.startLine = startLine;
|
||||
this.row = this.row.bind(this);
|
||||
// this.error = this.error.bind(this);
|
||||
this.done = this.done.bind(this);
|
||||
@@ -155,14 +157,21 @@ class StreamHandler {
|
||||
this.resolve();
|
||||
}
|
||||
info(info) {
|
||||
if (info && info.line != null) {
|
||||
info = {
|
||||
...info,
|
||||
line: this.startLine + info.line,
|
||||
};
|
||||
}
|
||||
process.send({ msgtype: 'info', info });
|
||||
}
|
||||
}
|
||||
|
||||
function handleStream(driver, resultIndexHolder, sql) {
|
||||
function handleStream(driver, resultIndexHolder, sqlItem) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const handler = new StreamHandler(resultIndexHolder, resolve);
|
||||
driver.stream(systemConnection, sql, handler);
|
||||
const start = sqlItem.trimStart || sqlItem.start;
|
||||
const handler = new StreamHandler(resultIndexHolder, resolve, start && start.line);
|
||||
driver.stream(systemConnection, sqlItem.text, handler);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -221,7 +230,10 @@ async function handleExecuteQuery({ sql }) {
|
||||
const resultIndexHolder = {
|
||||
value: 0,
|
||||
};
|
||||
for (const sqlItem of splitQuery(sql, driver.getQuerySplitterOptions('stream'))) {
|
||||
for (const sqlItem of splitQuery(sql, {
|
||||
...driver.getQuerySplitterOptions('stream'),
|
||||
returnRichInfo: true,
|
||||
})) {
|
||||
await handleStream(driver, resultIndexHolder, sqlItem);
|
||||
// const handler = new StreamHandler(resultIndex);
|
||||
// const stream = await driver.stream(systemConnection, sqlItem, handler);
|
||||
@@ -260,10 +272,15 @@ async function handleExecuteReader({ jslid, sql, fileName }) {
|
||||
});
|
||||
}
|
||||
|
||||
function handlePing() {
|
||||
lastPing = new Date().getTime();
|
||||
}
|
||||
|
||||
const messageHandlers = {
|
||||
connect: handleConnect,
|
||||
executeQuery: handleExecuteQuery,
|
||||
executeReader: handleExecuteReader,
|
||||
ping: handlePing,
|
||||
// cancel: handleCancel,
|
||||
};
|
||||
|
||||
@@ -274,6 +291,17 @@ async function handleMessage({ msgtype, ...other }) {
|
||||
|
||||
function start() {
|
||||
childProcessChecker();
|
||||
|
||||
lastPing = new Date().getTime();
|
||||
|
||||
setInterval(() => {
|
||||
const time = new Date().getTime();
|
||||
if (time - lastPing > 25 * 1000) {
|
||||
console.log('Session not alive, exiting');
|
||||
process.exit(0);
|
||||
}
|
||||
}, 10 * 1000);
|
||||
|
||||
process.on('message', async message => {
|
||||
if (handleProcessCommunication(message)) return;
|
||||
try {
|
||||
|
||||
@@ -47,7 +47,7 @@ async function importDatabase({ connection = undefined, systemConnection = undef
|
||||
const downloadedFile = await download(inputFile);
|
||||
|
||||
const fileStream = fs.createReadStream(downloadedFile, 'utf-8');
|
||||
const splittedStream = splitQueryStream(fileStream, driver.getQuerySplitterOptions());
|
||||
const splittedStream = splitQueryStream(fileStream, driver.getQuerySplitterOptions('script'));
|
||||
const importStream = new ImportStream(pool, driver);
|
||||
// @ts-ignore
|
||||
splittedStream.pipe(importStream);
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
AllowIdentityInsert,
|
||||
Expression,
|
||||
} from 'dbgate-sqltree';
|
||||
import { NamedObjectInfo, DatabaseInfo } from 'dbgate-types';
|
||||
import type { NamedObjectInfo, DatabaseInfo } from 'dbgate-types';
|
||||
|
||||
export interface ChangeSetItem {
|
||||
pureName: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import { GridDisplay, ChangeCacheFunc, ChangeConfigFunc, DisplayColumn } from './GridDisplay';
|
||||
import { EngineDriver, ViewInfo, ColumnInfo, CollectionInfo } from 'dbgate-types';
|
||||
import type { EngineDriver, ViewInfo, ColumnInfo, CollectionInfo } from 'dbgate-types';
|
||||
import { GridConfig, GridCache } from './GridConfig';
|
||||
|
||||
function getObjectKeys(obj) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import { GridConfig, GridCache, GridConfigColumns, createGridCache, GroupFunc } from './GridConfig';
|
||||
import { TableInfo, EngineDriver, DatabaseInfo, SqlDialect } from 'dbgate-types';
|
||||
import type { TableInfo, EngineDriver, DatabaseInfo, SqlDialect } from 'dbgate-types';
|
||||
import { getFilterValueExpression } from 'dbgate-filterparser';
|
||||
import { ChangeCacheFunc, ChangeConfigFunc, DisplayColumn } from './GridDisplay';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import { EngineDriver, ViewInfo, ColumnInfo } from 'dbgate-types';
|
||||
import type { EngineDriver, ViewInfo, ColumnInfo } from 'dbgate-types';
|
||||
import { GridDisplay, ChangeCacheFunc, ChangeConfigFunc } from './GridDisplay';
|
||||
import { GridConfig, GridCache } from './GridConfig';
|
||||
import { FreeTableModel } from './FreeTableModel';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TableInfo } from 'dbgate-types';
|
||||
import type { TableInfo } from 'dbgate-types';
|
||||
|
||||
export interface FreeTableModel {
|
||||
structure: TableInfo;
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { DisplayColumn } from './GridDisplay';
|
||||
import { TableInfo } from 'dbgate-types';
|
||||
|
||||
export interface GridConfigColumns {
|
||||
hiddenColumns: string[];
|
||||
expandedColumns: string[];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import { GridConfig, GridCache, GridConfigColumns, createGridCache, GroupFunc, createGridConfig } from './GridConfig';
|
||||
import {
|
||||
import type {
|
||||
ForeignKeyInfo,
|
||||
TableInfo,
|
||||
ColumnInfo,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import { GridDisplay, ChangeCacheFunc, ChangeConfigFunc } from './GridDisplay';
|
||||
import { QueryResultColumn } from 'dbgate-types';
|
||||
import { GridConfig, GridCache } from './GridConfig';
|
||||
import { analyseCollectionDisplayColumns } from './CollectionGridDisplay';
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { RangeDefinition } from 'dbgate-types';
|
||||
import { PerspectiveDataLoadProps } from './PerspectiveDataProvider';
|
||||
import _pick from 'lodash/pick';
|
||||
import _zip from 'lodash/zip';
|
||||
import _difference from 'lodash/difference';
|
||||
import debug from 'debug';
|
||||
import stableStringify from 'json-stable-stringify';
|
||||
import { PerspectiveDataPattern } from './PerspectiveDataPattern';
|
||||
|
||||
const dbg = debug('dbgate:PerspectiveCache');
|
||||
|
||||
@@ -34,9 +34,11 @@ export class PerspectiveCacheTable {
|
||||
pureName: string;
|
||||
bindingColumns?: string[];
|
||||
dataColumns: string[];
|
||||
allColumns?: boolean;
|
||||
loadedAll: boolean;
|
||||
loadedRows: any[] = [];
|
||||
bindingGroups: { [bindingKey: string]: PerspectiveBindingGroup } = {};
|
||||
allRowCount: number = null;
|
||||
|
||||
get loadedCount() {
|
||||
return this.loadedRows.length;
|
||||
@@ -85,14 +87,23 @@ export class PerspectiveCache {
|
||||
constructor() {}
|
||||
|
||||
tables: { [tableKey: string]: PerspectiveCacheTable } = {};
|
||||
dataPatterns: PerspectiveDataPattern[] = [];
|
||||
|
||||
getTableCache(props: PerspectiveDataLoadProps) {
|
||||
const tableKey = stableStringify(
|
||||
_pick(props, ['schemaName', 'pureName', 'bindingColumns', 'databaseConfig', 'orderBy', 'condition'])
|
||||
_pick(props, [
|
||||
'schemaName',
|
||||
'pureName',
|
||||
'bindingColumns',
|
||||
'databaseConfig',
|
||||
'orderBy',
|
||||
'sqlCondition',
|
||||
'mongoCondition',
|
||||
])
|
||||
);
|
||||
let res = this.tables[tableKey];
|
||||
|
||||
if (res && _difference(props.dataColumns, res.dataColumns).length > 0) {
|
||||
if (res && _difference(props.dataColumns, res.dataColumns).length > 0 && !res.allColumns) {
|
||||
dbg('Delete cache because incomplete columns', props.pureName, res.dataColumns);
|
||||
|
||||
// we have incomplete cache
|
||||
@@ -112,5 +123,6 @@ export class PerspectiveCache {
|
||||
|
||||
clear() {
|
||||
this.tables = {};
|
||||
this.dataPatterns = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DatabaseInfo, ForeignKeyInfo, NamedObjectInfo, TableInfo } from 'dbgate-types';
|
||||
import type { DatabaseInfo, ForeignKeyInfo, NamedObjectInfo, TableInfo } from 'dbgate-types';
|
||||
import uuidv1 from 'uuid/v1';
|
||||
|
||||
// export interface PerspectiveConfigColumns {
|
||||
@@ -7,6 +7,13 @@ import uuidv1 from 'uuid/v1';
|
||||
// uncheckedColumns: string[];
|
||||
// }
|
||||
|
||||
export type PerspectiveDatabaseEngineType = 'sqldb' | 'docdb';
|
||||
|
||||
export interface PerspectiveDatabaseConfig {
|
||||
conid: string;
|
||||
database: string;
|
||||
}
|
||||
|
||||
export interface PerspectiveCustomJoinConfig {
|
||||
refNodeDesignerId: string;
|
||||
referenceDesignerId: string;
|
||||
|
||||
@@ -1,19 +1,40 @@
|
||||
import { Condition, Expression, Select } from 'dbgate-sqltree';
|
||||
import { PerspectiveDataLoadProps } from './PerspectiveDataProvider';
|
||||
import debug from 'debug';
|
||||
import _zipObject from 'lodash/zipObject';
|
||||
import _mapValues from 'lodash/mapValues';
|
||||
import _isArray from 'lodash/isArray';
|
||||
import { safeJsonParse } from 'dbgate-tools';
|
||||
|
||||
function normalizeLoadedRow(row) {
|
||||
return _mapValues(row, v => safeJsonParse(v) || v);
|
||||
}
|
||||
|
||||
function normalizeResult(result) {
|
||||
if (_isArray(result)) {
|
||||
return result.map(normalizeLoadedRow);
|
||||
}
|
||||
if (result.errorMessage) {
|
||||
return result;
|
||||
}
|
||||
return {
|
||||
...result,
|
||||
errorMessage: 'Unspecified error',
|
||||
};
|
||||
}
|
||||
|
||||
const dbg = debug('dbgate:PerspectiveDataLoader');
|
||||
|
||||
export class PerspectiveDataLoader {
|
||||
constructor(public apiCall) {}
|
||||
|
||||
buildCondition(props: PerspectiveDataLoadProps): Condition {
|
||||
const { schemaName, pureName, bindingColumns, bindingValues, dataColumns, orderBy, condition } = props;
|
||||
buildSqlCondition(props: PerspectiveDataLoadProps): Condition {
|
||||
const { schemaName, pureName, bindingColumns, bindingValues, dataColumns, orderBy, sqlCondition } = props;
|
||||
|
||||
const conditions = [];
|
||||
|
||||
if (condition) {
|
||||
conditions.push(condition);
|
||||
if (sqlCondition) {
|
||||
conditions.push(sqlCondition);
|
||||
}
|
||||
|
||||
if (bindingColumns?.length == 1) {
|
||||
@@ -38,8 +59,26 @@ export class PerspectiveDataLoader {
|
||||
: null;
|
||||
}
|
||||
|
||||
async loadGrouping(props: PerspectiveDataLoadProps) {
|
||||
const { schemaName, pureName, bindingColumns, bindingValues, dataColumns } = props;
|
||||
buildMongoCondition(props: PerspectiveDataLoadProps): {} {
|
||||
const { schemaName, pureName, bindingColumns, bindingValues, dataColumns, orderBy, mongoCondition } = props;
|
||||
|
||||
const conditions = [];
|
||||
|
||||
if (mongoCondition) {
|
||||
conditions.push(mongoCondition);
|
||||
}
|
||||
|
||||
if (bindingColumns?.length == 1) {
|
||||
conditions.push({
|
||||
[bindingColumns[0]]: { $in: bindingValues.map(x => x[0]) },
|
||||
});
|
||||
}
|
||||
|
||||
return conditions.length == 1 ? conditions[0] : conditions.length > 0 ? { $and: conditions } : null;
|
||||
}
|
||||
|
||||
async loadGroupingSqlDb(props: PerspectiveDataLoadProps) {
|
||||
const { schemaName, pureName, bindingColumns } = props;
|
||||
|
||||
const bindingColumnExpressions = bindingColumns.map(
|
||||
columnName =>
|
||||
@@ -71,13 +110,13 @@ export class PerspectiveDataLoader {
|
||||
},
|
||||
...bindingColumnExpressions,
|
||||
],
|
||||
where: this.buildCondition(props),
|
||||
where: this.buildSqlCondition(props),
|
||||
};
|
||||
|
||||
select.groupBy = bindingColumnExpressions;
|
||||
|
||||
if (dbg?.enabled) {
|
||||
dbg(`LOAD COUNTS, table=${props.pureName}, columns=${props.dataColumns?.join(',')}`);
|
||||
dbg(`LOAD COUNTS, table=${props.pureName}, columns=${bindingColumns?.join(',')}`);
|
||||
}
|
||||
|
||||
const response = await this.apiCall('database-connections/sql-select', {
|
||||
@@ -93,8 +132,63 @@ export class PerspectiveDataLoader {
|
||||
}));
|
||||
}
|
||||
|
||||
async loadData(props: PerspectiveDataLoadProps) {
|
||||
const { schemaName, pureName, bindingColumns, bindingValues, dataColumns, orderBy, condition } = props;
|
||||
async loadGroupingDocDb(props: PerspectiveDataLoadProps) {
|
||||
const { schemaName, pureName, bindingColumns } = props;
|
||||
|
||||
const aggregate = [
|
||||
{ $match: this.buildMongoCondition(props) },
|
||||
{
|
||||
$group: {
|
||||
_id: _zipObject(
|
||||
bindingColumns,
|
||||
bindingColumns.map(col => '$' + col)
|
||||
),
|
||||
count: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (dbg?.enabled) {
|
||||
dbg(`LOAD COUNTS, table=${props.pureName}, columns=${bindingColumns?.join(',')}`);
|
||||
}
|
||||
|
||||
const response = await this.apiCall('database-connections/collection-data', {
|
||||
conid: props.databaseConfig.conid,
|
||||
database: props.databaseConfig.database,
|
||||
options: {
|
||||
pureName,
|
||||
aggregate,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.errorMessage) return response;
|
||||
return response.rows.map(row => ({
|
||||
...row._id,
|
||||
_perspective_group_size_: parseInt(row.count),
|
||||
}));
|
||||
}
|
||||
|
||||
async loadGrouping(props: PerspectiveDataLoadProps) {
|
||||
const { engineType } = props;
|
||||
switch (engineType) {
|
||||
case 'sqldb':
|
||||
return this.loadGroupingSqlDb(props);
|
||||
case 'docdb':
|
||||
return this.loadGroupingDocDb(props);
|
||||
}
|
||||
}
|
||||
|
||||
async loadDataSqlDb(props: PerspectiveDataLoadProps) {
|
||||
const {
|
||||
schemaName,
|
||||
pureName,
|
||||
bindingColumns,
|
||||
bindingValues,
|
||||
dataColumns,
|
||||
orderBy,
|
||||
sqlCondition: condition,
|
||||
engineType,
|
||||
} = props;
|
||||
|
||||
if (dataColumns?.length == 0) {
|
||||
return [];
|
||||
@@ -113,16 +207,19 @@ export class PerspectiveDataLoader {
|
||||
},
|
||||
})),
|
||||
selectAll: !dataColumns,
|
||||
orderBy: orderBy?.map(({ columnName, order }) => ({
|
||||
exprType: 'column',
|
||||
columnName,
|
||||
direction: order,
|
||||
source: {
|
||||
name: { schemaName, pureName },
|
||||
},
|
||||
})),
|
||||
orderBy:
|
||||
orderBy?.length > 0
|
||||
? orderBy?.map(({ columnName, order }) => ({
|
||||
exprType: 'column',
|
||||
columnName,
|
||||
direction: order,
|
||||
source: {
|
||||
name: { schemaName, pureName },
|
||||
},
|
||||
}))
|
||||
: null,
|
||||
range: props.range,
|
||||
where: this.buildCondition(props),
|
||||
where: this.buildSqlCondition(props),
|
||||
};
|
||||
|
||||
if (dbg?.enabled) {
|
||||
@@ -142,4 +239,135 @@ export class PerspectiveDataLoader {
|
||||
if (response.errorMessage) return response;
|
||||
return response.rows;
|
||||
}
|
||||
|
||||
getDocDbLoadOptions(props: PerspectiveDataLoadProps, useSort: boolean) {
|
||||
const { pureName } = props;
|
||||
const res: any = {
|
||||
pureName,
|
||||
condition: this.buildMongoCondition(props),
|
||||
skip: props.range?.offset,
|
||||
limit: props.range?.limit,
|
||||
};
|
||||
if (useSort && props.orderBy?.length > 0) {
|
||||
res.sort = _zipObject(
|
||||
props.orderBy.map(col => col.columnName),
|
||||
props.orderBy.map(col => (col.order == 'DESC' ? -1 : 1))
|
||||
);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
async loadDataDocDb(props: PerspectiveDataLoadProps) {
|
||||
const {
|
||||
schemaName,
|
||||
pureName,
|
||||
bindingColumns,
|
||||
bindingValues,
|
||||
dataColumns,
|
||||
orderBy,
|
||||
sqlCondition: condition,
|
||||
engineType,
|
||||
} = props;
|
||||
|
||||
if (dbg?.enabled) {
|
||||
dbg(
|
||||
`LOAD DATA, collection=${props.pureName}, columns=${props.dataColumns?.join(',')}, range=${
|
||||
props.range?.offset
|
||||
},${props.range?.limit}`
|
||||
);
|
||||
}
|
||||
|
||||
const options = this.getDocDbLoadOptions(props, true);
|
||||
|
||||
const response = await this.apiCall('database-connections/collection-data', {
|
||||
conid: props.databaseConfig.conid,
|
||||
database: props.databaseConfig.database,
|
||||
options,
|
||||
});
|
||||
|
||||
if (response.errorMessage) return response;
|
||||
return response.rows;
|
||||
}
|
||||
|
||||
async loadData(props: PerspectiveDataLoadProps) {
|
||||
const { engineType } = props;
|
||||
switch (engineType) {
|
||||
case 'sqldb':
|
||||
return normalizeResult(await this.loadDataSqlDb(props));
|
||||
case 'docdb':
|
||||
return normalizeResult(await this.loadDataDocDb(props));
|
||||
}
|
||||
}
|
||||
|
||||
async loadRowCountSqlDb(props: PerspectiveDataLoadProps) {
|
||||
const {
|
||||
schemaName,
|
||||
pureName,
|
||||
bindingColumns,
|
||||
bindingValues,
|
||||
dataColumns,
|
||||
orderBy,
|
||||
sqlCondition: condition,
|
||||
} = props;
|
||||
|
||||
const select: Select = {
|
||||
commandType: 'select',
|
||||
from: {
|
||||
name: { schemaName, pureName },
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
exprType: 'raw',
|
||||
sql: 'COUNT(*)',
|
||||
alias: 'count',
|
||||
},
|
||||
],
|
||||
where: this.buildSqlCondition(props),
|
||||
};
|
||||
|
||||
const response = await this.apiCall('database-connections/sql-select', {
|
||||
conid: props.databaseConfig.conid,
|
||||
database: props.databaseConfig.database,
|
||||
select,
|
||||
});
|
||||
|
||||
if (response.errorMessage) return response;
|
||||
return response.rows[0];
|
||||
}
|
||||
|
||||
async loadRowCountDocDb(props: PerspectiveDataLoadProps) {
|
||||
const {
|
||||
schemaName,
|
||||
pureName,
|
||||
bindingColumns,
|
||||
bindingValues,
|
||||
dataColumns,
|
||||
orderBy,
|
||||
sqlCondition: condition,
|
||||
} = props;
|
||||
|
||||
const options = {
|
||||
...this.getDocDbLoadOptions(props, false),
|
||||
countDocuments: true,
|
||||
};
|
||||
|
||||
const response = await this.apiCall('database-connections/collection-data', {
|
||||
conid: props.databaseConfig.conid,
|
||||
database: props.databaseConfig.database,
|
||||
options,
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async loadRowCount(props: PerspectiveDataLoadProps) {
|
||||
const { engineType } = props;
|
||||
switch (engineType) {
|
||||
case 'sqldb':
|
||||
return this.loadRowCountSqlDb(props);
|
||||
case 'docdb':
|
||||
return this.loadRowCountDocDb(props);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { PerspectiveDataLoader } from './PerspectiveDataLoader';
|
||||
import { PerspectiveDataLoadProps } from './PerspectiveDataProvider';
|
||||
import _isString from 'lodash/isString';
|
||||
import _isPlainObject from 'lodash/isPlainObject';
|
||||
import _isNumber from 'lodash/isNumber';
|
||||
import _isBoolean from 'lodash/isBoolean';
|
||||
import _isArray from 'lodash/isArray';
|
||||
import { safeJsonParse } from 'dbgate-tools';
|
||||
|
||||
export type PerspectiveDataPatternColumnType = 'null' | 'oid' | 'string' | 'number' | 'boolean' | 'json';
|
||||
|
||||
export interface PerspectiveDataPatternColumn {
|
||||
name: string;
|
||||
types: PerspectiveDataPatternColumnType[];
|
||||
columns: PerspectiveDataPatternColumn[];
|
||||
}
|
||||
|
||||
export interface PerspectiveDataPattern {
|
||||
conid: string;
|
||||
database: string;
|
||||
schemaName?: string;
|
||||
pureName: string;
|
||||
columns: PerspectiveDataPatternColumn[];
|
||||
}
|
||||
|
||||
export type PerspectiveDataPatternDict = { [designerId: string]: PerspectiveDataPattern };
|
||||
|
||||
function detectValueType(value): PerspectiveDataPatternColumnType {
|
||||
if (_isString(value)) return 'string';
|
||||
if (_isNumber(value)) return 'number';
|
||||
if (_isBoolean(value)) return 'boolean';
|
||||
if (value?.$oid) return 'oid';
|
||||
if (_isPlainObject(value) || _isArray(value)) return 'json';
|
||||
if (value == null) return 'null';
|
||||
}
|
||||
|
||||
function addObjectToColumns(columns: PerspectiveDataPatternColumn[], row) {
|
||||
if (_isPlainObject(row)) {
|
||||
for (const key of Object.keys(row)) {
|
||||
let column: PerspectiveDataPatternColumn = columns.find(x => x.name == key);
|
||||
if (!column) {
|
||||
column = {
|
||||
name: key,
|
||||
types: [],
|
||||
columns: [],
|
||||
};
|
||||
columns.push(column);
|
||||
}
|
||||
const value = row[key];
|
||||
const type = detectValueType(value);
|
||||
if (!column.types.includes(type)) {
|
||||
column.types.push(type);
|
||||
}
|
||||
if (_isPlainObject(value)) {
|
||||
addObjectToColumns(column.columns, value);
|
||||
}
|
||||
if (_isArray(value)) {
|
||||
for (const item of value) {
|
||||
addObjectToColumns(column.columns, item);
|
||||
}
|
||||
}
|
||||
if (_isString(value)) {
|
||||
const json = safeJsonParse(value);
|
||||
if (json && (_isPlainObject(json) || _isArray(json))) {
|
||||
if (!column.types.includes('json')) {
|
||||
column.types.push('json');
|
||||
}
|
||||
if (_isPlainObject(json)) {
|
||||
addObjectToColumns(column.columns, json);
|
||||
}
|
||||
if (_isArray(json)) {
|
||||
for (const item of json) {
|
||||
addObjectToColumns(column.columns, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function analyseDataPattern(
|
||||
patternBase: Omit<PerspectiveDataPattern, 'columns'>,
|
||||
rows: any[]
|
||||
): PerspectiveDataPattern {
|
||||
const res: PerspectiveDataPattern = {
|
||||
...patternBase,
|
||||
columns: [],
|
||||
};
|
||||
// console.log('ROWS', rows);
|
||||
for (const row of rows) {
|
||||
addObjectToColumns(res.columns, row);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
@@ -1,24 +1,21 @@
|
||||
import debug from 'debug';
|
||||
import { Condition } from 'dbgate-sqltree';
|
||||
import { RangeDefinition } from 'dbgate-types';
|
||||
import { format } from 'path';
|
||||
import type { RangeDefinition } from 'dbgate-types';
|
||||
import { PerspectiveBindingGroup, PerspectiveCache } from './PerspectiveCache';
|
||||
import { PerspectiveDataLoader } from './PerspectiveDataLoader';
|
||||
import { PerspectiveDataPatternDict } from './PerspectiveDataPattern';
|
||||
import { PerspectiveDatabaseConfig, PerspectiveDatabaseEngineType } from './PerspectiveConfig';
|
||||
|
||||
export const PERSPECTIVE_PAGE_SIZE = 100;
|
||||
|
||||
const dbg = debug('dbgate:PerspectiveDataProvider');
|
||||
|
||||
export interface PerspectiveDatabaseConfig {
|
||||
conid: string;
|
||||
database: string;
|
||||
}
|
||||
|
||||
export interface PerspectiveDataLoadProps {
|
||||
databaseConfig: PerspectiveDatabaseConfig;
|
||||
schemaName: string;
|
||||
schemaName?: string;
|
||||
pureName: string;
|
||||
dataColumns: string[];
|
||||
dataColumns?: string[];
|
||||
allColumns?: boolean;
|
||||
orderBy: {
|
||||
columnName: string;
|
||||
order: 'ASC' | 'DESC';
|
||||
@@ -27,11 +24,17 @@ export interface PerspectiveDataLoadProps {
|
||||
bindingValues?: any[][];
|
||||
range?: RangeDefinition;
|
||||
topCount?: number;
|
||||
condition?: Condition;
|
||||
sqlCondition?: Condition;
|
||||
mongoCondition?: any;
|
||||
engineType: PerspectiveDatabaseEngineType;
|
||||
}
|
||||
|
||||
export class PerspectiveDataProvider {
|
||||
constructor(public cache: PerspectiveCache, public loader: PerspectiveDataLoader) {}
|
||||
constructor(
|
||||
public cache: PerspectiveCache,
|
||||
public loader: PerspectiveDataLoader,
|
||||
public dataPatterns: PerspectiveDataPatternDict
|
||||
) {}
|
||||
async loadData(props: PerspectiveDataLoadProps): Promise<{ rows: any[]; incomplete: boolean }> {
|
||||
dbg('load data', props);
|
||||
// console.log('LOAD DATA', props);
|
||||
@@ -182,6 +185,7 @@ export class PerspectiveDataProvider {
|
||||
|
||||
// load missing rows
|
||||
tableCache.dataColumns = props.dataColumns;
|
||||
tableCache.allColumns = props.allColumns;
|
||||
|
||||
const nextRows = await this.loader.loadData({
|
||||
...props,
|
||||
@@ -203,4 +207,23 @@ export class PerspectiveDataProvider {
|
||||
|
||||
return tableCache.getRowsResult(props);
|
||||
}
|
||||
|
||||
async loadRowCount(props: PerspectiveDataLoadProps): Promise<number> {
|
||||
const tableCache = this.cache.getTableCache(props);
|
||||
|
||||
if (tableCache.allRowCount != null) {
|
||||
return tableCache.allRowCount;
|
||||
}
|
||||
|
||||
const result = await this.loader.loadRowCount({
|
||||
...props,
|
||||
});
|
||||
|
||||
if (result.errorMessage) {
|
||||
throw new Error(result.errorMessage);
|
||||
}
|
||||
|
||||
tableCache.allRowCount = parseInt(result.count);
|
||||
return tableCache.allRowCount;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import _max from 'lodash/max';
|
||||
import _range from 'lodash/max';
|
||||
import _fill from 'lodash/fill';
|
||||
import _findIndex from 'lodash/findIndex';
|
||||
import _isPlainObject from 'lodash/isPlainObject';
|
||||
import _isArray from 'lodash/isArray';
|
||||
import debug from 'debug';
|
||||
|
||||
const dbg = debug('dbgate:PerspectiveDisplay');
|
||||
@@ -126,14 +128,14 @@ export class PerspectiveDisplay {
|
||||
|
||||
fillColumns(children: PerspectiveTreeNode[], parentNodes: PerspectiveTreeNode[]) {
|
||||
for (const child of children) {
|
||||
if (child.isCheckedColumn || child.isCheckedNode) {
|
||||
if (child.generatesHiearchicGridColumn || child.generatesDataGridColumn) {
|
||||
this.processColumn(child, parentNodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processColumn(node: PerspectiveTreeNode, parentNodes: PerspectiveTreeNode[]) {
|
||||
if (node.isCheckedColumn) {
|
||||
if (node.generatesDataGridColumn) {
|
||||
const column = new PerspectiveDisplayColumn(this);
|
||||
column.title = node.columnTitle;
|
||||
column.dataField = node.dataField;
|
||||
@@ -145,7 +147,7 @@ export class PerspectiveDisplay {
|
||||
this.columns.push(column);
|
||||
}
|
||||
|
||||
if (node.isExpandable && node.isCheckedNode) {
|
||||
if (node.generatesHiearchicGridColumn) {
|
||||
const countBefore = this.columns.length;
|
||||
this.fillColumns(node.childNodes, [...parentNodes, node]);
|
||||
|
||||
@@ -167,13 +169,30 @@ export class PerspectiveDisplay {
|
||||
// return _findIndex(this.columns, x => x.dataNode.designerId == node.designerId);
|
||||
// }
|
||||
|
||||
extractArray(value) {
|
||||
if (_isArray(value)) return value;
|
||||
if (_isPlainObject(value)) return [value];
|
||||
return [];
|
||||
}
|
||||
|
||||
collectRows(sourceRows: any[], nodes: PerspectiveTreeNode[]): CollectedPerspectiveDisplayRow[] {
|
||||
// console.log('********** COLLECT ROWS', sourceRows);
|
||||
const columnNodes = nodes.filter(x => x.isCheckedColumn);
|
||||
const treeNodes = nodes.filter(x => x.isCheckedNode);
|
||||
const columnNodes = nodes.filter(x => x.generatesDataGridColumn);
|
||||
const treeNodes = nodes.filter(x => x.generatesHiearchicGridColumn);
|
||||
|
||||
// console.log('columnNodes', columnNodes);
|
||||
// console.log('treeNodes', treeNodes);
|
||||
// console.log(
|
||||
// 'columnNodes',
|
||||
// columnNodes.map(x => x.title)
|
||||
// );
|
||||
// console.log(
|
||||
// 'treeNodes',
|
||||
// treeNodes.map(x => x.title)
|
||||
// );
|
||||
|
||||
// console.log(
|
||||
// 'nodes',
|
||||
// nodes.map(x => x.title)
|
||||
// );
|
||||
|
||||
const columnIndexes = columnNodes.map(node => this.findColumnIndexFromNode(node));
|
||||
|
||||
@@ -181,13 +200,14 @@ export class PerspectiveDisplay {
|
||||
for (const sourceRow of sourceRows) {
|
||||
// console.log('PROCESS SOURCE', sourceRow);
|
||||
// row.startIndex = startIndex;
|
||||
const rowData = columnNodes.map(node => sourceRow[node.codeName]);
|
||||
const rowData = columnNodes.map(node => sourceRow[node.columnName]);
|
||||
const subRowCollections = [];
|
||||
|
||||
for (const node of treeNodes) {
|
||||
// console.log('sourceRow[node.fieldName]', node.fieldName, sourceRow[node.fieldName]);
|
||||
if (sourceRow[node.fieldName]) {
|
||||
const subrows = {
|
||||
rows: this.collectRows(sourceRow[node.fieldName], node.childNodes),
|
||||
rows: this.collectRows(this.extractArray(sourceRow[node.fieldName]), node.childNodes),
|
||||
};
|
||||
subRowCollections.push(subrows);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
import type {
|
||||
CollectionInfo,
|
||||
ColumnInfo,
|
||||
DatabaseInfo,
|
||||
ForeignKeyInfo,
|
||||
@@ -7,13 +8,15 @@ import {
|
||||
TableInfo,
|
||||
ViewInfo,
|
||||
} from 'dbgate-types';
|
||||
import { equalFullName } from 'dbgate-tools';
|
||||
import { equalFullName, isCollectionInfo, isTableInfo, isViewInfo } from 'dbgate-tools';
|
||||
import {
|
||||
ChangePerspectiveConfigFunc,
|
||||
createPerspectiveNodeConfig,
|
||||
MultipleDatabaseInfo,
|
||||
PerspectiveConfig,
|
||||
PerspectiveCustomJoinConfig,
|
||||
PerspectiveDatabaseConfig,
|
||||
PerspectiveDatabaseEngineType,
|
||||
PerspectiveFilterColumnInfo,
|
||||
PerspectiveNodeConfig,
|
||||
PerspectiveReferenceConfig,
|
||||
@@ -27,17 +30,14 @@ import _uniqBy from 'lodash/uniqBy';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
import _cloneDeepWith from 'lodash/cloneDeepWith';
|
||||
import _findIndex from 'lodash/findIndex';
|
||||
import {
|
||||
PerspectiveDatabaseConfig,
|
||||
PerspectiveDataLoadProps,
|
||||
PerspectiveDataProvider,
|
||||
} from './PerspectiveDataProvider';
|
||||
import { PerspectiveDataLoadProps, PerspectiveDataProvider } from './PerspectiveDataProvider';
|
||||
import stableStringify from 'json-stable-stringify';
|
||||
import { getFilterType, parseFilter } from 'dbgate-filterparser';
|
||||
import { FilterType } from 'dbgate-filterparser/lib/types';
|
||||
import { Condition, Expression, Select } from 'dbgate-sqltree';
|
||||
// import { getPerspectiveDefaultColumns } from './getPerspectiveDefaultColumns';
|
||||
import uuidv1 from 'uuid/v1';
|
||||
import { PerspectiveDataPatternColumn } from './PerspectiveDataPattern';
|
||||
|
||||
export interface PerspectiveDataLoadPropsWithNode {
|
||||
props: PerspectiveDataLoadProps;
|
||||
@@ -79,7 +79,7 @@ export abstract class PerspectiveTreeNode {
|
||||
this.parentNodeConfig = parentNode?.nodeConfig;
|
||||
}
|
||||
readonly nodeConfig: PerspectiveNodeConfig;
|
||||
readonly parentNodeConfig: PerspectiveNodeConfig;
|
||||
parentNodeConfig: PerspectiveNodeConfig;
|
||||
// defaultChecked: boolean;
|
||||
abstract get title();
|
||||
abstract get codeName();
|
||||
@@ -108,6 +108,18 @@ export abstract class PerspectiveTreeNode {
|
||||
get namedObject(): NamedObjectInfo {
|
||||
return null;
|
||||
}
|
||||
get tableNodeOrParent(): PerspectiveTableNode {
|
||||
if (this instanceof PerspectiveTableNode) {
|
||||
return this;
|
||||
}
|
||||
if (this.parentNode == null) {
|
||||
return null;
|
||||
}
|
||||
return this.parentNode.tableNodeOrParent;
|
||||
}
|
||||
get engineType(): PerspectiveDatabaseEngineType {
|
||||
return null;
|
||||
}
|
||||
abstract getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps;
|
||||
get isRoot() {
|
||||
return this.parentNode == null;
|
||||
@@ -119,6 +131,12 @@ export abstract class PerspectiveTreeNode {
|
||||
get isSortable() {
|
||||
return false;
|
||||
}
|
||||
get generatesHiearchicGridColumn() {
|
||||
return this.isExpandable && this.isCheckedNode;
|
||||
}
|
||||
get generatesDataGridColumn() {
|
||||
return this.isCheckedColumn;
|
||||
}
|
||||
matchChildRow(parentRow: any, childRow: any): boolean {
|
||||
return true;
|
||||
}
|
||||
@@ -271,14 +289,15 @@ export abstract class PerspectiveTreeNode {
|
||||
[field]: isIncluded ? [...(n[field] || []), this.codeName] : (n[field] || []).filter(x => x != this.codeName),
|
||||
});
|
||||
|
||||
const [cfgChanged, nodeCfg] = this.parentNode?.ensureNodeConfig(cfg);
|
||||
const [cfgChanged, nodeCfg] = this.parentNode?.tableNodeOrParent?.ensureNodeConfig(cfg);
|
||||
|
||||
return {
|
||||
const res = {
|
||||
...cfgChanged,
|
||||
nodes: cfgChanged.nodes.map(n =>
|
||||
n.designerId == (this.parentNode?.designerId || nodeCfg?.designerId) ? changedFields(n) : n
|
||||
n.designerId == (this.parentNode?.tableNodeOrParent?.designerId || nodeCfg?.designerId) ? changedFields(n) : n
|
||||
),
|
||||
};
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -292,11 +311,15 @@ export abstract class PerspectiveTreeNode {
|
||||
...this.childNodes.map(x => x.childDataColumn),
|
||||
..._flatten(this.childNodes.filter(x => x.isExpandable && x.isChecked).map(x => x.getChildMatchColumns())),
|
||||
...this.getParentMatchColumns(),
|
||||
...this.childNodes
|
||||
.filter(x => x instanceof PerspectivePatternColumnNode)
|
||||
.filter(x => this.nodeConfig?.checkedColumns?.find(y => y.startsWith(x.codeName + '::')))
|
||||
.map(x => x.columnName),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
getChildrenCondition(source = null): Condition {
|
||||
getChildrenSqlCondition(source = null): Condition {
|
||||
const conditions = _compact([
|
||||
...this.childNodes.map(x => x.parseFilterCondition(source)),
|
||||
...this.buildParentFilterConditions(),
|
||||
@@ -313,7 +336,18 @@ export abstract class PerspectiveTreeNode {
|
||||
};
|
||||
}
|
||||
|
||||
getOrderBy(table: TableInfo | ViewInfo): PerspectiveDataLoadProps['orderBy'] {
|
||||
getChildrenMongoCondition(source = null): {} {
|
||||
const conditions = _compact([...this.childNodes.map(x => x.parseFilterCondition(source))]);
|
||||
if (conditions.length == 0) {
|
||||
return null;
|
||||
}
|
||||
if (conditions.length == 1) {
|
||||
return conditions[0];
|
||||
}
|
||||
return { $and: conditions };
|
||||
}
|
||||
|
||||
getOrderBy(table: TableInfo | ViewInfo | CollectionInfo): PerspectiveDataLoadProps['orderBy'] {
|
||||
const res = _compact(
|
||||
this.childNodes.map(node => {
|
||||
const sort = this.nodeConfig?.sort?.find(x => x.columnName == node.columnName);
|
||||
@@ -325,11 +359,15 @@ export abstract class PerspectiveTreeNode {
|
||||
}
|
||||
})
|
||||
);
|
||||
return res.length > 0
|
||||
? res
|
||||
: (table as TableInfo)?.primaryKey?.columns.map(x => ({ columnName: x.columnName, order: 'ASC' })) || [
|
||||
{ columnName: table?.columns[0].columnName, order: 'ASC' },
|
||||
];
|
||||
if (res.length > 0) return res;
|
||||
const pkColumns = (table as TableInfo)?.primaryKey?.columns.map(x => ({
|
||||
columnName: x.columnName,
|
||||
order: 'ASC' as 'ASC',
|
||||
}));
|
||||
if (pkColumns) return pkColumns;
|
||||
const columns = (table as TableInfo | ViewInfo)?.columns;
|
||||
if (columns) return [{ columnName: columns[0].columnName, order: 'ASC' }];
|
||||
return [{ columnName: '_id', order: 'ASC' }];
|
||||
}
|
||||
|
||||
getBaseTables() {
|
||||
@@ -390,7 +428,9 @@ export abstract class PerspectiveTreeNode {
|
||||
return (
|
||||
(this.parentNode?.isRoot || this.parentNode?.supportsParentFilter) &&
|
||||
this.parentNode?.databaseConfig?.conid == this.databaseConfig?.conid &&
|
||||
this.parentNode?.databaseConfig?.database == this.databaseConfig?.database
|
||||
this.parentNode?.databaseConfig?.database == this.databaseConfig?.database &&
|
||||
this.engineType == 'sqldb' &&
|
||||
this.parentNode?.engineType == 'sqldb'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -438,7 +478,7 @@ export abstract class PerspectiveTreeNode {
|
||||
conditionType: 'and',
|
||||
conditions: _compact([
|
||||
...lastNode.getParentJoinCondition(lastAlias, this.namedObject.pureName),
|
||||
leafNode.getChildrenCondition({ alias: 'pert_0' }),
|
||||
leafNode.getChildrenSqlCondition({ alias: 'pert_0' }),
|
||||
]),
|
||||
};
|
||||
|
||||
@@ -496,6 +536,10 @@ export class PerspectiveTableColumnNode extends PerspectiveTreeNode {
|
||||
);
|
||||
}
|
||||
|
||||
get engineType() {
|
||||
return this.parentNode.engineType;
|
||||
}
|
||||
|
||||
matchChildRow(parentRow: any, childRow: any): boolean {
|
||||
if (!this.foreignKey) return false;
|
||||
return parentRow[this.foreignKey.columns[0].columnName] == childRow[this.foreignKey.columns[0].refColumnName];
|
||||
@@ -552,7 +596,8 @@ export class PerspectiveTableColumnNode extends PerspectiveTreeNode {
|
||||
dataColumns: this.getDataLoadColumns(),
|
||||
databaseConfig: this.databaseConfig,
|
||||
orderBy: this.getOrderBy(this.refTable),
|
||||
condition: this.getChildrenCondition(),
|
||||
sqlCondition: this.getChildrenSqlCondition(),
|
||||
engineType: 'sqldb',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -573,6 +618,7 @@ export class PerspectiveTableColumnNode extends PerspectiveTreeNode {
|
||||
|
||||
get fieldName() {
|
||||
return this.codeName + 'Ref';
|
||||
// return this.codeName ;
|
||||
}
|
||||
|
||||
get title() {
|
||||
@@ -670,6 +716,7 @@ export class PerspectiveTableColumnNode extends PerspectiveTreeNode {
|
||||
pureName: this.foreignKey.refTableName,
|
||||
conid: this.databaseConfig.conid,
|
||||
database: this.databaseConfig.database,
|
||||
objectTypeField: this.table.objectTypeField,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@@ -693,9 +740,216 @@ export class PerspectiveTableColumnNode extends PerspectiveTreeNode {
|
||||
}
|
||||
}
|
||||
|
||||
export class PerspectivePatternColumnNode extends PerspectiveTreeNode {
|
||||
foreignKey: ForeignKeyInfo;
|
||||
refTable: TableInfo;
|
||||
|
||||
constructor(
|
||||
public table: TableInfo | ViewInfo | CollectionInfo,
|
||||
public column: PerspectiveDataPatternColumn,
|
||||
public tableColumn: ColumnInfo,
|
||||
dbs: MultipleDatabaseInfo,
|
||||
config: PerspectiveConfig,
|
||||
setConfig: ChangePerspectiveConfigFunc,
|
||||
dataProvider: PerspectiveDataProvider,
|
||||
databaseConfig: PerspectiveDatabaseConfig,
|
||||
parentNode: PerspectiveTreeNode,
|
||||
designerId: string
|
||||
) {
|
||||
super(dbs, config, setConfig, parentNode, dataProvider, databaseConfig, designerId);
|
||||
this.parentNodeConfig = this.tableNodeOrParent?.nodeConfig;
|
||||
}
|
||||
|
||||
get isChildColumn() {
|
||||
return this.parentNode instanceof PerspectivePatternColumnNode;
|
||||
}
|
||||
|
||||
// matchChildRow(parentRow: any, childRow: any): boolean {
|
||||
// if (!this.foreignKey) return false;
|
||||
// return parentRow[this.foreignKey.columns[0].columnName] == childRow[this.foreignKey.columns[0].refColumnName];
|
||||
// }
|
||||
|
||||
// getChildMatchColumns() {
|
||||
// if (!this.foreignKey) return [];
|
||||
// return [this.foreignKey.columns[0].columnName];
|
||||
// }
|
||||
|
||||
// getParentMatchColumns() {
|
||||
// if (!this.foreignKey) return [];
|
||||
// return [this.foreignKey.columns[0].refColumnName];
|
||||
// }
|
||||
|
||||
// getParentJoinCondition(alias: string, parentAlias: string): Condition[] {
|
||||
// if (!this.foreignKey) return [];
|
||||
// return this.foreignKey.columns.map(column => {
|
||||
// const res: Condition = {
|
||||
// conditionType: 'binary',
|
||||
// operator: '=',
|
||||
// left: {
|
||||
// exprType: 'column',
|
||||
// columnName: column.columnName,
|
||||
// source: { alias: parentAlias },
|
||||
// },
|
||||
// right: {
|
||||
// exprType: 'column',
|
||||
// columnName: column.refColumnName,
|
||||
// source: { alias },
|
||||
// },
|
||||
// };
|
||||
// return res;
|
||||
// });
|
||||
// }
|
||||
|
||||
// createReferenceConfigColumns(): PerspectiveReferenceConfig['columns'] {
|
||||
// return this.foreignKey?.columns?.map(col => ({
|
||||
// source: col.columnName,
|
||||
// target: col.refColumnName,
|
||||
// }));
|
||||
// }
|
||||
|
||||
getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps {
|
||||
return null;
|
||||
}
|
||||
|
||||
get generatesHiearchicGridColumn() {
|
||||
// console.log('generatesHiearchicGridColumn', this.parentTableNode?.nodeConfig?.checkedColumns, this.codeName + '::');
|
||||
return !!this.tableNodeOrParent?.nodeConfig?.checkedColumns?.find(x => x.startsWith(this.codeName + '::'));
|
||||
}
|
||||
|
||||
// get generatesHiearchicGridColumn() {
|
||||
// // return this.config &&;
|
||||
// }
|
||||
|
||||
get icon() {
|
||||
if (this.column.types.includes('json')) {
|
||||
return 'img json';
|
||||
}
|
||||
return 'img column';
|
||||
}
|
||||
|
||||
get codeName() {
|
||||
if (this.parentNode instanceof PerspectivePatternColumnNode) {
|
||||
return `${this.parentNode.codeName}::${this.column.name}`;
|
||||
}
|
||||
return this.column.name;
|
||||
}
|
||||
|
||||
get columnName() {
|
||||
return this.column.name;
|
||||
}
|
||||
|
||||
get fieldName() {
|
||||
return this.column.name;
|
||||
}
|
||||
|
||||
get title() {
|
||||
return this.column.name;
|
||||
}
|
||||
|
||||
get isExpandable() {
|
||||
return this.column.columns.length > 0;
|
||||
}
|
||||
|
||||
get isSortable() {
|
||||
return !this.isChildColumn;
|
||||
}
|
||||
|
||||
get filterType(): FilterType {
|
||||
if (this.tableColumn) return getFilterType(this.tableColumn.dataType);
|
||||
return 'mongo';
|
||||
}
|
||||
|
||||
generateChildNodes(): PerspectiveTreeNode[] {
|
||||
return this.column.columns.map(
|
||||
column =>
|
||||
new PerspectivePatternColumnNode(
|
||||
this.table,
|
||||
column,
|
||||
this.tableColumn,
|
||||
this.dbs,
|
||||
this.config,
|
||||
this.setConfig,
|
||||
this.dataProvider,
|
||||
this.databaseConfig,
|
||||
this,
|
||||
null
|
||||
)
|
||||
);
|
||||
return [];
|
||||
// if (!this.foreignKey) return [];
|
||||
// const tbl = this?.db?.tables?.find(
|
||||
// x => x.pureName == this.foreignKey?.refTableName && x.schemaName == this.foreignKey?.refSchemaName
|
||||
// );
|
||||
|
||||
// return getTableChildPerspectiveNodes(
|
||||
// tbl,
|
||||
// this.dbs,
|
||||
// this.config,
|
||||
// this.setConfig,
|
||||
// this.dataProvider,
|
||||
// this.databaseConfig,
|
||||
// this
|
||||
// );
|
||||
}
|
||||
|
||||
get filterInfo(): PerspectiveFilterColumnInfo {
|
||||
if (this.isChildColumn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
columnName: this.columnName,
|
||||
filterType: this.filterType,
|
||||
pureName: this.table.pureName,
|
||||
schemaName: this.table.schemaName,
|
||||
foreignKey: this.foreignKey,
|
||||
};
|
||||
}
|
||||
|
||||
parseFilterCondition(source = null): {} {
|
||||
const filter = this.getFilter();
|
||||
if (!filter) return null;
|
||||
const condition = parseFilter(filter, 'mongo');
|
||||
if (!condition) return null;
|
||||
return _cloneDeepWith(condition, expr => {
|
||||
if (expr.__placeholder__) {
|
||||
return {
|
||||
[this.columnName]: expr.__placeholder__,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// get headerTableAttributes() {
|
||||
// if (this.foreignKey) {
|
||||
// return {
|
||||
// schemaName: this.foreignKey.refSchemaName,
|
||||
// pureName: this.foreignKey.refTableName,
|
||||
// conid: this.databaseConfig.conid,
|
||||
// database: this.databaseConfig.database,
|
||||
// };
|
||||
// }
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// get tableCode() {
|
||||
// return `${this.collection.schemaName}|${this.table.pureName}`;
|
||||
// }
|
||||
|
||||
// get namedObject(): NamedObjectInfo {
|
||||
// if (this.foreignKey) {
|
||||
// return {
|
||||
// schemaName: this.foreignKey.refSchemaName,
|
||||
// pureName: this.foreignKey.refTableName,
|
||||
// };
|
||||
// }
|
||||
// return null;
|
||||
// }
|
||||
}
|
||||
|
||||
export class PerspectiveTableNode extends PerspectiveTreeNode {
|
||||
constructor(
|
||||
public table: TableInfo | ViewInfo,
|
||||
public table: TableInfo | ViewInfo | CollectionInfo,
|
||||
dbs: MultipleDatabaseInfo,
|
||||
config: PerspectiveConfig,
|
||||
setConfig: ChangePerspectiveConfigFunc,
|
||||
@@ -707,14 +961,22 @@ export class PerspectiveTableNode extends PerspectiveTreeNode {
|
||||
super(dbs, config, setConfig, parentNode, dataProvider, databaseConfig, designerId);
|
||||
}
|
||||
|
||||
get engineType(): PerspectiveDatabaseEngineType {
|
||||
return isCollectionInfo(this.table) ? 'docdb' : 'sqldb';
|
||||
}
|
||||
|
||||
getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps {
|
||||
const isMongo = isCollectionInfo(this.table);
|
||||
return {
|
||||
schemaName: this.table.schemaName,
|
||||
pureName: this.table.pureName,
|
||||
dataColumns: this.getDataLoadColumns(),
|
||||
allColumns: isMongo,
|
||||
databaseConfig: this.databaseConfig,
|
||||
orderBy: this.getOrderBy(this.table),
|
||||
condition: this.getChildrenCondition(),
|
||||
sqlCondition: isMongo ? null : this.getChildrenSqlCondition(),
|
||||
mongoCondition: isMongo ? this.getChildrenMongoCondition() : null,
|
||||
engineType: isMongo ? 'docdb' : 'sqldb',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -756,6 +1018,7 @@ export class PerspectiveTableNode extends PerspectiveTreeNode {
|
||||
pureName: this.table.pureName,
|
||||
conid: this.databaseConfig.conid,
|
||||
database: this.databaseConfig.database,
|
||||
objectTypeField: this.table.objectTypeField,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -770,64 +1033,6 @@ export class PerspectiveTableNode extends PerspectiveTreeNode {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// export class PerspectiveViewNode extends PerspectiveTreeNode {
|
||||
// constructor(
|
||||
// public view: ViewInfo,
|
||||
// dbs: MultipleDatabaseInfo,
|
||||
// config: PerspectiveConfig,
|
||||
// setConfig: ChangePerspectiveConfigFunc,
|
||||
// public dataProvider: PerspectiveDataProvider,
|
||||
// databaseConfig: PerspectiveDatabaseConfig,
|
||||
// parentNode: PerspectiveTreeNode
|
||||
// ) {
|
||||
// super(dbs, config, setConfig, parentNode, dataProvider, databaseConfig);
|
||||
// }
|
||||
|
||||
// getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps {
|
||||
// return {
|
||||
// schemaName: this.view.schemaName,
|
||||
// pureName: this.view.pureName,
|
||||
// dataColumns: this.getDataLoadColumns(),
|
||||
// databaseConfig: this.databaseConfig,
|
||||
// orderBy: this.getOrderBy(this.view),
|
||||
// condition: this.getChildrenCondition(),
|
||||
// };
|
||||
// }
|
||||
|
||||
// get codeName() {
|
||||
// return this.view.schemaName ? `${this.view.schemaName}:${this.view.pureName}` : this.view.pureName;
|
||||
// }
|
||||
|
||||
// get title() {
|
||||
// return this.view.pureName;
|
||||
// }
|
||||
|
||||
// get isExpandable() {
|
||||
// return true;
|
||||
// }
|
||||
|
||||
// get childNodes(): PerspectiveTreeNode[] {
|
||||
// return getTableChildPerspectiveNodes(
|
||||
// this.view,
|
||||
// this.dbs,
|
||||
// this.config,
|
||||
// this.setConfig,
|
||||
// this.dataProvider,
|
||||
// this.databaseConfig,
|
||||
// this
|
||||
// );
|
||||
// }
|
||||
|
||||
// get icon() {
|
||||
// return 'img table';
|
||||
// }
|
||||
|
||||
// getBaseTableFromThis() {
|
||||
// return this.view;
|
||||
// }
|
||||
// }
|
||||
|
||||
export class PerspectiveTableReferenceNode extends PerspectiveTableNode {
|
||||
constructor(
|
||||
public foreignKey: ForeignKeyInfo,
|
||||
@@ -872,7 +1077,8 @@ export class PerspectiveTableReferenceNode extends PerspectiveTableNode {
|
||||
dataColumns: this.getDataLoadColumns(),
|
||||
databaseConfig: this.databaseConfig,
|
||||
orderBy: this.getOrderBy(this.table),
|
||||
condition: this.getChildrenCondition(),
|
||||
sqlCondition: this.getChildrenSqlCondition(),
|
||||
engineType: 'sqldb',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -934,7 +1140,7 @@ export class PerspectiveTableReferenceNode extends PerspectiveTableNode {
|
||||
export class PerspectiveCustomJoinTreeNode extends PerspectiveTableNode {
|
||||
constructor(
|
||||
public customJoin: PerspectiveCustomJoinConfig,
|
||||
table: TableInfo | ViewInfo,
|
||||
table: TableInfo | ViewInfo | CollectionInfo,
|
||||
dbs: MultipleDatabaseInfo,
|
||||
config: PerspectiveConfig,
|
||||
setConfig: ChangePerspectiveConfigFunc,
|
||||
@@ -966,6 +1172,8 @@ export class PerspectiveCustomJoinTreeNode extends PerspectiveTableNode {
|
||||
getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps {
|
||||
// console.log('CUSTOM JOIN', this.customJoin);
|
||||
// console.log('this.getDataLoadColumns()', this.getDataLoadColumns());
|
||||
const isMongo = isCollectionInfo(this.table);
|
||||
|
||||
return {
|
||||
schemaName: this.table.schemaName,
|
||||
pureName: this.table.pureName,
|
||||
@@ -975,9 +1183,12 @@ export class PerspectiveCustomJoinTreeNode extends PerspectiveTableNode {
|
||||
stableStringify
|
||||
),
|
||||
dataColumns: this.getDataLoadColumns(),
|
||||
allColumns: isMongo,
|
||||
databaseConfig: this.databaseConfig,
|
||||
orderBy: this.getOrderBy(this.table),
|
||||
condition: this.getChildrenCondition(),
|
||||
sqlCondition: isMongo ? null : this.getChildrenSqlCondition(),
|
||||
mongoCondition: isMongo ? this.getChildrenMongoCondition() : null,
|
||||
engineType: isMongo ? 'docdb' : 'sqldb',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1082,7 +1293,7 @@ function findDesignerIdForNode<T extends PerspectiveTreeNode>(
|
||||
}
|
||||
|
||||
export function getTableChildPerspectiveNodes(
|
||||
table: TableInfo | ViewInfo,
|
||||
table: TableInfo | ViewInfo | CollectionInfo,
|
||||
dbs: MultipleDatabaseInfo,
|
||||
config: PerspectiveConfig,
|
||||
setConfig: ChangePerspectiveConfigFunc,
|
||||
@@ -1093,25 +1304,59 @@ export function getTableChildPerspectiveNodes(
|
||||
if (!table) return [];
|
||||
const db = parentNode.db;
|
||||
|
||||
const columnNodes = table.columns.map(col =>
|
||||
findDesignerIdForNode(
|
||||
config,
|
||||
parentNode,
|
||||
designerId =>
|
||||
new PerspectiveTableColumnNode(
|
||||
col,
|
||||
table,
|
||||
dbs,
|
||||
config,
|
||||
setConfig,
|
||||
dataProvider,
|
||||
databaseConfig,
|
||||
parentNode,
|
||||
designerId
|
||||
)
|
||||
)
|
||||
);
|
||||
const pattern = dataProvider?.dataPatterns?.[parentNode.designerId];
|
||||
|
||||
const tableOrView = isTableInfo(table) || isViewInfo(table) ? table : null;
|
||||
|
||||
const columnNodes =
|
||||
tableOrView?.columns?.map(col =>
|
||||
findDesignerIdForNode(config, parentNode, designerId =>
|
||||
pattern?.columns?.find(x => x.name == col.columnName)?.types.includes('json')
|
||||
? new PerspectivePatternColumnNode(
|
||||
table,
|
||||
pattern?.columns?.find(x => x.name == col.columnName),
|
||||
col,
|
||||
dbs,
|
||||
config,
|
||||
setConfig,
|
||||
dataProvider,
|
||||
databaseConfig,
|
||||
parentNode,
|
||||
designerId
|
||||
)
|
||||
: new PerspectiveTableColumnNode(
|
||||
col,
|
||||
tableOrView,
|
||||
dbs,
|
||||
config,
|
||||
setConfig,
|
||||
dataProvider,
|
||||
databaseConfig,
|
||||
parentNode,
|
||||
designerId
|
||||
)
|
||||
)
|
||||
) ||
|
||||
pattern?.columns?.map(col =>
|
||||
findDesignerIdForNode(
|
||||
config,
|
||||
parentNode,
|
||||
designerId =>
|
||||
new PerspectivePatternColumnNode(
|
||||
table,
|
||||
col,
|
||||
null,
|
||||
dbs,
|
||||
config,
|
||||
setConfig,
|
||||
dataProvider,
|
||||
databaseConfig,
|
||||
parentNode,
|
||||
designerId
|
||||
)
|
||||
)
|
||||
) ||
|
||||
[];
|
||||
// if (!columnNodes.find(x => x.isChecked)) {
|
||||
// const circularColumns = columnNodes.filter(x => x.isCircular).map(x => x.columnName);
|
||||
// const defaultColumns = getPerspectiveDefaultColumns(table, db, circularColumns);
|
||||
@@ -1173,6 +1418,7 @@ export function getTableChildPerspectiveNodes(
|
||||
const db = dbs?.[newConfig.conid]?.[newConfig.database];
|
||||
const table = db?.tables?.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName);
|
||||
const view = db?.views?.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName);
|
||||
const collection = db?.collections?.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName);
|
||||
|
||||
const join: PerspectiveCustomJoinConfig = {
|
||||
refNodeDesignerId: node.designerId,
|
||||
@@ -1189,11 +1435,11 @@ export function getTableChildPerspectiveNodes(
|
||||
: ref.columns.map(col => ({ baseColumnName: col.target, refColumnName: col.source })),
|
||||
};
|
||||
|
||||
if (table || view) {
|
||||
if (table || view || collection) {
|
||||
customs.push(
|
||||
new PerspectiveCustomJoinTreeNode(
|
||||
join,
|
||||
table || view,
|
||||
table || view || collection,
|
||||
dbs,
|
||||
config,
|
||||
setConfig,
|
||||
@@ -1210,34 +1456,5 @@ export function getTableChildPerspectiveNodes(
|
||||
|
||||
res.push(..._sortBy(customs, 'title'));
|
||||
|
||||
// const customs = [];
|
||||
// for (const join of config.customJoins || []) {
|
||||
// if (join.baseUniqueName == parentColumn.uniqueName) {
|
||||
// const newConfig = { ...databaseConfig };
|
||||
// if (join.conid) newConfig.conid = join.conid;
|
||||
// if (join.database) newConfig.database = join.database;
|
||||
// const db = dbs?.[newConfig.conid]?.[newConfig.database];
|
||||
// const table = db?.tables?.find(x => x.pureName == join.refTableName && x.schemaName == join.refSchemaName);
|
||||
// const view = db?.views?.find(x => x.pureName == join.refTableName && x.schemaName == join.refSchemaName);
|
||||
|
||||
// if (table || view) {
|
||||
// customs.push(
|
||||
// new PerspectiveCustomJoinTreeNode(
|
||||
// join,
|
||||
// table || view,
|
||||
// dbs,
|
||||
// config,
|
||||
// setConfig,
|
||||
// dataProvider,
|
||||
// newConfig,
|
||||
// parentColumn,
|
||||
// null
|
||||
// )
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// res.push(..._sortBy(customs, 'title'));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FormViewDisplay } from './FormViewDisplay';
|
||||
import _ from 'lodash';
|
||||
import { ChangeCacheFunc, DisplayColumn, ChangeConfigFunc } from './GridDisplay';
|
||||
import { EngineDriver, NamedObjectInfo, DatabaseInfo } from 'dbgate-types';
|
||||
import type { EngineDriver, NamedObjectInfo, DatabaseInfo } from 'dbgate-types';
|
||||
import { GridConfig, GridCache } from './GridConfig';
|
||||
import { mergeConditions, Condition, OrderByExpression } from 'dbgate-sqltree';
|
||||
import { TableGridDisplay } from './TableGridDisplay';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import { filterName, isTableColumnUnique } from 'dbgate-tools';
|
||||
import { GridDisplay, ChangeCacheFunc, DisplayColumn, DisplayedColumnInfo, ChangeConfigFunc } from './GridDisplay';
|
||||
import {
|
||||
import type {
|
||||
TableInfo,
|
||||
EngineDriver,
|
||||
ViewInfo,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import { GridDisplay, ChangeCacheFunc, ChangeConfigFunc } from './GridDisplay';
|
||||
import { EngineDriver, ViewInfo, ColumnInfo } from 'dbgate-types';
|
||||
import type { EngineDriver, ViewInfo, ColumnInfo } from 'dbgate-types';
|
||||
import { GridConfig, GridCache } from './GridConfig';
|
||||
|
||||
export class ViewGridDisplay extends GridDisplay {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import { Command, Insert, Update, Delete, UpdateField, Condition, AllowIdentityInsert } from 'dbgate-sqltree';
|
||||
import { NamedObjectInfo, DatabaseInfo, ForeignKeyInfo, TableInfo } from 'dbgate-types';
|
||||
import type { NamedObjectInfo, DatabaseInfo, ForeignKeyInfo, TableInfo } from 'dbgate-types';
|
||||
import { ChangeSet, ChangeSetItem, extractChangeSetCondition } from './ChangeSet';
|
||||
|
||||
export interface ChangeSetDeleteCascade {
|
||||
|
||||
@@ -19,3 +19,5 @@ export * from './PerspectiveDataProvider';
|
||||
export * from './PerspectiveCache';
|
||||
export * from './PerspectiveConfig';
|
||||
export * from './processPerspectiveDefaultColunns';
|
||||
export * from './PerspectiveDataPattern';
|
||||
export * from './PerspectiveDataLoader';
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import { findForeignKeyForColumn } from 'dbgate-tools';
|
||||
import { DatabaseInfo, TableInfo, ViewInfo } from 'dbgate-types';
|
||||
import type { DatabaseInfo, TableInfo, ViewInfo } from 'dbgate-types';
|
||||
import { createPerspectiveNodeConfig, MultipleDatabaseInfo, PerspectiveConfig } from './PerspectiveConfig';
|
||||
import { PerspectiveDataPattern, PerspectiveDataPatternDict } from './PerspectiveDataPattern';
|
||||
import { PerspectiveTableNode } from './PerspectiveTreeNode';
|
||||
|
||||
const namePredicates = [
|
||||
x => x.toLowerCase() == 'name',
|
||||
x => x.toLowerCase() == 'title',
|
||||
x => x.toLowerCase().includes('name'),
|
||||
x => x.toLowerCase().includes('title'),
|
||||
x => x.toLowerCase().includes('subject'),
|
||||
];
|
||||
|
||||
function getPerspectiveDefaultColumns(
|
||||
table: TableInfo | ViewInfo,
|
||||
db: DatabaseInfo,
|
||||
@@ -10,13 +19,7 @@ function getPerspectiveDefaultColumns(
|
||||
): [string[], string[]] {
|
||||
const columns = table.columns.map(x => x.columnName);
|
||||
const predicates = [
|
||||
x => x.toLowerCase() == 'name',
|
||||
x => x.toLowerCase() == 'title',
|
||||
x => x.toLowerCase().includes('name'),
|
||||
x => x.toLowerCase().includes('title'),
|
||||
x => x.toLowerCase().includes('subject'),
|
||||
// x => x.toLowerCase().includes('text'),
|
||||
// x => x.toLowerCase().includes('desc'),
|
||||
...namePredicates,
|
||||
x =>
|
||||
table.columns
|
||||
.find(y => y.columnName == x)
|
||||
@@ -44,9 +47,20 @@ function getPerspectiveDefaultColumns(
|
||||
return [[columns[0]], null];
|
||||
}
|
||||
|
||||
function getPerspectiveDefaultCollectionColumns(pattern: PerspectiveDataPattern): string[] {
|
||||
const columns = pattern.columns.map(x => x.name);
|
||||
const predicates = [...namePredicates, x => pattern.columns.find(y => y.name == x)?.types?.includes('string')];
|
||||
|
||||
for (const predicate of predicates) {
|
||||
const col = columns.find(predicate);
|
||||
if (col) return [col];
|
||||
}
|
||||
}
|
||||
|
||||
export function perspectiveNodesHaveStructure(
|
||||
config: PerspectiveConfig,
|
||||
dbInfos: MultipleDatabaseInfo,
|
||||
dataPatterns: PerspectiveDataPatternDict,
|
||||
conid: string,
|
||||
database: string
|
||||
) {
|
||||
@@ -56,8 +70,10 @@ export function perspectiveNodesHaveStructure(
|
||||
|
||||
const table = db.tables.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName);
|
||||
const view = db.views.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName);
|
||||
const collection = db.collections.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName);
|
||||
|
||||
if (!table && !view) return false;
|
||||
if (!table && !view && !collection) return false;
|
||||
if (collection && !dataPatterns?.[node.designerId]) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -66,18 +82,20 @@ export function perspectiveNodesHaveStructure(
|
||||
export function shouldProcessPerspectiveDefaultColunns(
|
||||
config: PerspectiveConfig,
|
||||
dbInfos: MultipleDatabaseInfo,
|
||||
dataPatterns: PerspectiveDataPatternDict,
|
||||
conid: string,
|
||||
database: string
|
||||
) {
|
||||
const nodesNotProcessed = config.nodes.filter(x => !x.defaultColumnsProcessed);
|
||||
if (nodesNotProcessed.length == 0) return false;
|
||||
|
||||
return perspectiveNodesHaveStructure(config, dbInfos, conid, database);
|
||||
return perspectiveNodesHaveStructure(config, dbInfos, dataPatterns, conid, database);
|
||||
}
|
||||
|
||||
function processPerspectiveDefaultColunnsStep(
|
||||
config: PerspectiveConfig,
|
||||
dbInfos: MultipleDatabaseInfo,
|
||||
dataPatterns: PerspectiveDataPatternDict,
|
||||
conid: string,
|
||||
database: string
|
||||
) {
|
||||
@@ -107,6 +125,7 @@ function processPerspectiveDefaultColunnsStep(
|
||||
|
||||
const table = db.tables.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName);
|
||||
const view = db.views.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName);
|
||||
const collection = db.collections.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName);
|
||||
|
||||
if (table || view) {
|
||||
const treeNode = root.findNodeByDesignerId(node.designerId);
|
||||
@@ -181,6 +200,22 @@ function processPerspectiveDefaultColunnsStep(
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (collection) {
|
||||
const defaultColumns = getPerspectiveDefaultCollectionColumns(dataPatterns?.[node.designerId]);
|
||||
return {
|
||||
...config,
|
||||
nodes: config.nodes.map(n =>
|
||||
n.designerId == node.designerId
|
||||
? {
|
||||
...n,
|
||||
defaultColumnsProcessed: true,
|
||||
checkedColumns: defaultColumns,
|
||||
}
|
||||
: n
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -199,11 +234,12 @@ function markAllProcessed(config: PerspectiveConfig): PerspectiveConfig {
|
||||
export function processPerspectiveDefaultColunns(
|
||||
config: PerspectiveConfig,
|
||||
dbInfos: MultipleDatabaseInfo,
|
||||
dataPatterns: PerspectiveDataPatternDict,
|
||||
conid: string,
|
||||
database: string
|
||||
): PerspectiveConfig {
|
||||
while (config.nodes.filter(x => !x.defaultColumnsProcessed).length > 0) {
|
||||
const newConfig = processPerspectiveDefaultColunnsStep(config, dbInfos, conid, database);
|
||||
const newConfig = processPerspectiveDefaultColunnsStep(config, dbInfos, dataPatterns, conid, database);
|
||||
if (!newConfig) {
|
||||
return markAllProcessed(config);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { TableInfo } from 'dbgate-types';
|
||||
import { PerspectiveDisplay } from '../PerspectiveDisplay';
|
||||
import { PerspectiveTableNode } from '../PerspectiveTreeNode';
|
||||
import { chinookDbInfo } from './chinookDbInfo';
|
||||
@@ -13,6 +12,7 @@ test('test flat view', () => {
|
||||
const configColumns = processPerspectiveDefaultColunns(
|
||||
createPerspectiveConfig({ pureName: 'Artist' }),
|
||||
{ conid: { db: chinookDbInfo } },
|
||||
null,
|
||||
'conid',
|
||||
'db'
|
||||
);
|
||||
@@ -47,7 +47,7 @@ test('test one level nesting', () => {
|
||||
columns: [{ source: 'ArtistId', target: 'ArtistId' }],
|
||||
});
|
||||
|
||||
const configColumns = processPerspectiveDefaultColunns(config, { conid: { db: chinookDbInfo } }, 'conid', 'db');
|
||||
const configColumns = processPerspectiveDefaultColunns(config, { conid: { db: chinookDbInfo } }, null, 'conid', 'db');
|
||||
|
||||
// const config = createPerspectiveConfig({ pureName: 'Artist' });
|
||||
// config.nodes[0].checkedColumns = ['Album'];
|
||||
@@ -107,7 +107,7 @@ test('test two level nesting', () => {
|
||||
designerId: '2',
|
||||
columns: [{ source: 'AlbumId', target: 'AlbumId' }],
|
||||
});
|
||||
const configColumns = processPerspectiveDefaultColunns(config, { conid: { db: chinookDbInfo } }, 'conid', 'db');
|
||||
const configColumns = processPerspectiveDefaultColunns(config, { conid: { db: chinookDbInfo } }, null, 'conid', 'db');
|
||||
|
||||
const root = new PerspectiveTableNode(
|
||||
artistTable,
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { PerspectiveDisplay } from '../PerspectiveDisplay';
|
||||
import { PerspectiveTableNode } from '../PerspectiveTreeNode';
|
||||
import { createPerspectiveConfig, PerspectiveNodeConfig } from '../PerspectiveConfig';
|
||||
import { processPerspectiveDefaultColunns } from '../processPerspectiveDefaultColunns';
|
||||
import { DatabaseAnalyser } from 'dbgate-tools';
|
||||
import { analyseDataPattern } from '../PerspectiveDataPattern';
|
||||
import { PerspectiveDataProvider } from '../PerspectiveDataProvider';
|
||||
|
||||
const accountData = [
|
||||
{
|
||||
name: 'jan',
|
||||
email: 'jan@foo.co',
|
||||
follows: [{ name: 'lucie' }, { name: 'petr' }],
|
||||
nested: { email: 'jan@nest.cz' },
|
||||
},
|
||||
{
|
||||
name: 'romeo',
|
||||
email: 'romeo@foo.co',
|
||||
follows: [{ name: 'julie' }, { name: 'wiliam' }],
|
||||
nested: { email: 'romeo@nest.cz' },
|
||||
},
|
||||
];
|
||||
|
||||
function createDisplay(cfgFunc?: (cfg: PerspectiveNodeConfig) => void) {
|
||||
const collectionInfo = {
|
||||
objectTypeField: 'collections',
|
||||
pureName: 'Account',
|
||||
};
|
||||
const dbInfo = {
|
||||
...DatabaseAnalyser.createEmptyStructure(),
|
||||
collections: [collectionInfo],
|
||||
};
|
||||
const config = createPerspectiveConfig({ pureName: 'Account' });
|
||||
const dataPatterns = {
|
||||
[config.rootDesignerId]: analyseDataPattern(
|
||||
{
|
||||
conid: 'conid',
|
||||
database: 'db',
|
||||
pureName: 'Account',
|
||||
},
|
||||
accountData
|
||||
),
|
||||
};
|
||||
const configColumns = processPerspectiveDefaultColunns(
|
||||
config,
|
||||
{ conid: { db: dbInfo } },
|
||||
dataPatterns,
|
||||
'conid',
|
||||
'db'
|
||||
);
|
||||
if (cfgFunc) {
|
||||
cfgFunc(configColumns.nodes[0]);
|
||||
}
|
||||
const root = new PerspectiveTableNode(
|
||||
collectionInfo,
|
||||
{ conid: { db: dbInfo } },
|
||||
configColumns,
|
||||
null,
|
||||
new PerspectiveDataProvider(null, null, dataPatterns),
|
||||
{ conid: 'conid', database: 'db' },
|
||||
null,
|
||||
configColumns.rootDesignerId
|
||||
);
|
||||
|
||||
const display = new PerspectiveDisplay(root, accountData);
|
||||
|
||||
return display;
|
||||
}
|
||||
|
||||
test('test nosql display', () => {
|
||||
const display = createDisplay();
|
||||
|
||||
expect(display.rows.length).toEqual(2);
|
||||
expect(display.rows[0].rowData).toEqual(['jan']);
|
||||
expect(display.rows[1].rowData).toEqual(['romeo']);
|
||||
});
|
||||
|
||||
test('test nosql nested array display', () => {
|
||||
const display = createDisplay(cfg => {
|
||||
cfg.checkedColumns = ['name', 'follows::name'];
|
||||
});
|
||||
|
||||
expect(display.rows.length).toEqual(4);
|
||||
expect(display.rows[0].rowData).toEqual(['jan', 'lucie']);
|
||||
expect(display.rows[1].rowData).toEqual([undefined, 'petr']);
|
||||
expect(display.rows[2].rowData).toEqual(['romeo', 'julie']);
|
||||
expect(display.rows[3].rowData).toEqual([undefined, 'wiliam']);
|
||||
});
|
||||
|
||||
test('test nosql nested object', () => {
|
||||
const display = createDisplay(cfg => {
|
||||
cfg.checkedColumns = ['name', 'nested::email'];
|
||||
});
|
||||
|
||||
expect(display.rows.length).toEqual(2);
|
||||
expect(display.rows[0].rowData).toEqual(['jan', 'jan@nest.cz']);
|
||||
expect(display.rows[1].rowData).toEqual(['romeo', 'romeo@nest.cz']);
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DatabaseInfo } from 'dbgate-types';
|
||||
import type { DatabaseInfo } from 'dbgate-types';
|
||||
|
||||
export const chinookDbInfo: DatabaseInfo = {
|
||||
tables: [
|
||||
|
||||
@@ -2,7 +2,7 @@ import P from 'parsimmon';
|
||||
import moment from 'moment';
|
||||
import { FilterType } from './types';
|
||||
import { Condition } from 'dbgate-sqltree';
|
||||
import { TransformType } from 'dbgate-types';
|
||||
import type { TransformType } from 'dbgate-types';
|
||||
import { interpretEscapes, token, word, whitespace } from './common';
|
||||
|
||||
const compoudCondition = conditionType => conditions => {
|
||||
|
||||
@@ -2,7 +2,6 @@ import P from 'parsimmon';
|
||||
import moment from 'moment';
|
||||
import { FilterType } from './types';
|
||||
import { Condition } from 'dbgate-sqltree';
|
||||
import { TransformType } from 'dbgate-types';
|
||||
import { interpretEscapes, token, word, whitespace } from './common';
|
||||
import { mongoParser } from './mongoParser';
|
||||
import { datetimeParser } from './datetimeParser';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SqlDumper } from 'dbgate-types';
|
||||
import type { SqlDumper } from 'dbgate-types';
|
||||
import { Command, Select, Update, Delete, Insert } from './types';
|
||||
import { dumpSqlExpression } from './dumpSqlExpression';
|
||||
import { dumpSqlFromDefinition, dumpSqlSourceRef } from './dumpSqlSource';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SqlDumper } from 'dbgate-types';
|
||||
import type { SqlDumper } from 'dbgate-types';
|
||||
import { Condition, BinaryCondition } from './types';
|
||||
import { dumpSqlExpression } from './dumpSqlExpression';
|
||||
import { dumpSqlSelect } from './dumpSqlCommand';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import { SqlDumper } from 'dbgate-types';
|
||||
import type { SqlDumper } from 'dbgate-types';
|
||||
import { Expression, ColumnRefExpression } from './types';
|
||||
import { dumpSqlSourceRef } from './dumpSqlSource';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Source, FromDefinition, Relation } from './types';
|
||||
import { SqlDumper } from 'dbgate-types';
|
||||
import type { SqlDumper } from 'dbgate-types';
|
||||
import { dumpSqlSelect } from './dumpSqlCommand';
|
||||
import { dumpSqlCondition } from './dumpSqlCondition';
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { SqlDumper } from 'dbgate-types';
|
||||
import _ from 'lodash';
|
||||
import { Condition, BinaryCondition } from './types';
|
||||
import { dumpSqlExpression } from './dumpSqlExpression';
|
||||
import { link } from 'fs';
|
||||
import { evaluateExpression } from './evaluateExpression';
|
||||
import { cond } from 'lodash';
|
||||
|
||||
function isEmpty(value) {
|
||||
if (value == null) return true;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NamedObjectInfo, RangeDefinition, TransformType } from 'dbgate-types';
|
||||
import type { NamedObjectInfo, RangeDefinition, TransformType } from 'dbgate-types';
|
||||
|
||||
// export interface Command {
|
||||
// }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EngineDriver, SqlDumper } from 'dbgate-types';
|
||||
import type { EngineDriver, SqlDumper } from 'dbgate-types';
|
||||
import { Command, Condition } from './types';
|
||||
import { dumpSqlCommand } from './dumpSqlCommand';
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"typescript": "^4.4.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"dbgate-query-splitter": "^4.9.0",
|
||||
"dbgate-query-splitter": "^4.9.2",
|
||||
"dbgate-sqltree": "^5.0.0-alpha.1",
|
||||
"debug": "^4.3.4",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
import type {
|
||||
ColumnInfo,
|
||||
ConstraintInfo,
|
||||
EngineDriver,
|
||||
@@ -32,6 +32,12 @@ export class SqlDumper implements AlterProcessor {
|
||||
dialect: SqlDialect;
|
||||
indentLevel = 0;
|
||||
|
||||
static keywordsCase = 'upperCase';
|
||||
static convertKeywordCase(keyword: any): string {
|
||||
if (this.keywordsCase == 'lowerCase') return keyword?.toString()?.toLowerCase();
|
||||
return keyword?.toString()?.toUpperCase();
|
||||
}
|
||||
|
||||
constructor(driver: EngineDriver) {
|
||||
this.driver = driver;
|
||||
this.dialect = driver.dialect;
|
||||
@@ -60,10 +66,10 @@ export class SqlDumper implements AlterProcessor {
|
||||
this.putRaw("'");
|
||||
}
|
||||
putByteArrayValue(value) {
|
||||
this.putRaw('NULL');
|
||||
this.put('^null');
|
||||
}
|
||||
putValue(value) {
|
||||
if (value === null) this.putRaw('NULL');
|
||||
if (value === null) this.put('^null');
|
||||
else if (value === true) this.putRaw('1');
|
||||
else if (value === false) this.putRaw('0');
|
||||
else if (_isString(value)) this.putStringValue(value);
|
||||
@@ -71,7 +77,7 @@ export class SqlDumper implements AlterProcessor {
|
||||
else if (_isDate(value)) this.putStringValue(new Date(value).toISOString());
|
||||
else if (value?.type == 'Buffer' && _isArray(value?.data)) this.putByteArrayValue(value?.data);
|
||||
else if (_isPlainObject(value) || _isArray(value)) this.putStringValue(JSON.stringify(value));
|
||||
else this.putRaw('NULL');
|
||||
else this.put('^null');
|
||||
}
|
||||
putCmd(format, ...args) {
|
||||
this.put(format, ...args);
|
||||
@@ -92,7 +98,7 @@ export class SqlDumper implements AlterProcessor {
|
||||
case 'k':
|
||||
{
|
||||
if (value) {
|
||||
this.putRaw(value.toUpperCase());
|
||||
this.putRaw(SqlDumper.convertKeywordCase(value));
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -128,7 +134,7 @@ export class SqlDumper implements AlterProcessor {
|
||||
switch (c) {
|
||||
case '^':
|
||||
while (i < length && format[i].match(/[a-z0-9_]/i)) {
|
||||
this.putRaw(format[i].toUpperCase());
|
||||
this.putRaw(SqlDumper.convertKeywordCase(format[i]));
|
||||
i++;
|
||||
}
|
||||
break;
|
||||
@@ -181,6 +187,14 @@ export class SqlDumper implements AlterProcessor {
|
||||
this.put(' ^auto_increment');
|
||||
}
|
||||
|
||||
createDatabase(name: string) {
|
||||
this.putCmd('^create ^database %i', name);
|
||||
}
|
||||
|
||||
dropDatabase(name: string) {
|
||||
this.putCmd('^drop ^database %i', name);
|
||||
}
|
||||
|
||||
specialColumnOptions(column) {}
|
||||
|
||||
columnDefinition(column: ColumnInfo, { includeDefault = true, includeNullable = true, includeCollate = true } = {}) {
|
||||
@@ -527,7 +541,7 @@ export class SqlDumper implements AlterProcessor {
|
||||
}
|
||||
|
||||
truncateTable(name: NamedObjectInfo) {
|
||||
this.putCmd('^delete ^from %f', name);
|
||||
this.putCmd('^truncate ^table %f', name);
|
||||
}
|
||||
|
||||
dropConstraints(table: TableInfo, dropReferences = false) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {
|
||||
import type {
|
||||
DatabaseInfo,
|
||||
EngineDriver,
|
||||
FunctionInfo,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DbDiffOptions, testEqualColumns, testEqualTables, testEqualSqlObjects } from './diffTools';
|
||||
import { DatabaseInfo, EngineDriver, SqlObjectInfo, TableInfo } from 'dbgate-types';
|
||||
import type { DatabaseInfo, EngineDriver, SqlObjectInfo, TableInfo } from 'dbgate-types';
|
||||
import _ from 'lodash';
|
||||
|
||||
export function computeDiffRowsCore(sourceList, targetList, testEqual) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { EngineDriver } from 'dbgate-types';
|
||||
import _intersection from 'lodash/intersection';
|
||||
import { prepareTableForImport } from './tableTransforms';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {
|
||||
import type {
|
||||
ColumnInfo,
|
||||
ConstraintInfo,
|
||||
DatabaseInfo,
|
||||
|
||||
@@ -16,6 +16,7 @@ const dialect = {
|
||||
isSparse: false,
|
||||
isPersisted: false,
|
||||
},
|
||||
defaultSchemaName: null,
|
||||
};
|
||||
|
||||
export const driverBase = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import _cloneDeep from 'lodash/cloneDeep';
|
||||
import _isString from 'lodash/isString';
|
||||
import {
|
||||
import type {
|
||||
ColumnInfo,
|
||||
ColumnReference,
|
||||
DatabaseInfo,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EngineDriver, ExtensionsDirectory } from 'dbgate-types';
|
||||
import type { EngineDriver, ExtensionsDirectory } from 'dbgate-types';
|
||||
import _camelCase from 'lodash/camelCase';
|
||||
import _isString from 'lodash/isString';
|
||||
import _isPlainObject from 'lodash/isPlainObject';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import { DatabaseInfo, EngineDriver } from 'dbgate-types';
|
||||
import type { DatabaseInfo, EngineDriver } from 'dbgate-types';
|
||||
|
||||
export async function enrichWithPreloadedRows(
|
||||
dbModel: DatabaseInfo,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import uuidv1 from 'uuid/v1';
|
||||
import _omit from 'lodash/omit';
|
||||
import {
|
||||
import type {
|
||||
ColumnInfo,
|
||||
ConstraintInfo,
|
||||
ForeignKeyInfo,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DatabaseInfo, TableInfo, ApplicationDefinition } from 'dbgate-types';
|
||||
import type { DatabaseInfo, TableInfo, ApplicationDefinition, ViewInfo, CollectionInfo } from 'dbgate-types';
|
||||
import _flatten from 'lodash/flatten';
|
||||
|
||||
export function addTableDependencies(db: DatabaseInfo): DatabaseInfo {
|
||||
@@ -118,3 +118,15 @@ export function isTableColumnUnique(table: TableInfo, column: string) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isTableInfo(obj: { objectTypeField?: string }): obj is TableInfo {
|
||||
return obj.objectTypeField == 'tables';
|
||||
}
|
||||
|
||||
export function isViewInfo(obj: { objectTypeField?: string }): obj is ViewInfo {
|
||||
return obj.objectTypeField == 'views';
|
||||
}
|
||||
|
||||
export function isCollectionInfo(obj: { objectTypeField?: string }): obj is CollectionInfo {
|
||||
return obj.objectTypeField == 'collections';
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TableInfo } from 'dbgate-types';
|
||||
import type { TableInfo } from 'dbgate-types';
|
||||
import _cloneDeep from 'lodash/cloneDeep';
|
||||
import _fromPairs from 'lodash/fromPairs';
|
||||
import _get from 'lodash/get';
|
||||
|
||||
Vendored
+1
@@ -9,6 +9,7 @@ export interface SqlDialect {
|
||||
fallbackDataType?: string;
|
||||
explicitDropConstraint?: boolean;
|
||||
anonymousPrimaryKey?: boolean;
|
||||
defaultSchemaName?: string;
|
||||
enableConstraintsPerTable?: boolean;
|
||||
|
||||
dropColumnDependencies?: string[];
|
||||
|
||||
Vendored
+3
@@ -14,9 +14,12 @@ export interface SqlDumper extends AlterProcessor {
|
||||
putValue(value: string | number | Date);
|
||||
putCollection<T>(delimiter: string, collection: T[], lambda: (item: T) => void);
|
||||
transform(type: TransformType, dumpExpr: () => void);
|
||||
createDatabase(name: string);
|
||||
dropDatabase(name: string);
|
||||
|
||||
endCommand();
|
||||
allowIdentityInsert(table: NamedObjectInfo, allow: boolean);
|
||||
truncateTable(table: NamedObjectInfo);
|
||||
beginTransaction();
|
||||
commitTransaction();
|
||||
}
|
||||
|
||||
Vendored
+3
-4
@@ -89,9 +89,7 @@ export interface EngineDriver {
|
||||
): Promise<TableInfo | ViewInfo | ProcedureInfo | FunctionInfo | TriggerInfo>;
|
||||
analyseSingleTable(pool: any, name: NamedObjectInfo): Promise<TableInfo>;
|
||||
getVersion(pool: any): Promise<{ version: string }>;
|
||||
listDatabases(
|
||||
pool: any
|
||||
): Promise<
|
||||
listDatabases(pool: any): Promise<
|
||||
{
|
||||
name: string;
|
||||
}[]
|
||||
@@ -112,7 +110,8 @@ export interface EngineDriver {
|
||||
updateCollection(pool: any, changeSet: any): Promise<any>;
|
||||
getCollectionUpdateScript(changeSet: any): string;
|
||||
createDatabase(pool: any, name: string): Promise;
|
||||
getQuerySplitterOptions(usage: 'stream' | 'script'): any;
|
||||
dropDatabase(pool: any, name: string): Promise;
|
||||
getQuerySplitterOptions(usage: 'stream' | 'script' | 'editor'): any;
|
||||
script(pool: any, sql: string): Promise;
|
||||
getNewObjectTemplates(): NewObjectTemplate[];
|
||||
// direct call of pool method, only some methods could be supported, on only some drivers
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"chartjs-adapter-moment": "^1.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"dbgate-datalib": "^5.0.0-alpha.1",
|
||||
"dbgate-query-splitter": "^4.9.0",
|
||||
"dbgate-query-splitter": "^4.9.2",
|
||||
"dbgate-sqltree": "^5.0.0-alpha.1",
|
||||
"dbgate-tools": "^5.0.0-alpha.1",
|
||||
"dbgate-types": "^5.0.0-alpha.1",
|
||||
|
||||
@@ -167,3 +167,33 @@ textarea {
|
||||
color: var(--theme-font-1);
|
||||
border: 1px solid var(--theme-border);
|
||||
}
|
||||
|
||||
.ace_gutter-cell.ace-gutter-sql-run {
|
||||
background-repeat: no-repeat;
|
||||
background-position: 2px center;
|
||||
|
||||
/* content: '▶';
|
||||
margin-right: 3px; */
|
||||
|
||||
/* border-radius: 20px 0px 0px 20px; */
|
||||
/* Change the color of the breakpoint if you want
|
||||
box-shadow: 0px 0px 1px 1px #248c46 inset; */
|
||||
}
|
||||
|
||||
.theme-type-light .ace_gutter-cell.ace-gutter-sql-run {
|
||||
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTguMS4xLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKCSB2aWV3Qm94PSIwIDAgMTcuODA0IDE3LjgwNCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTcuODA0IDE3LjgwNDsiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8Zz4KCTxnIGlkPSJjOThfcGxheSI+CgkJPHBhdGggZmlsbD0nIzQ0NCcgZD0iTTIuMDY3LDAuMDQzQzIuMjEtMC4wMjgsMi4zNzItMC4wMDgsMi40OTMsMC4wODVsMTMuMzEyLDguNTAzYzAuMDk0LDAuMDc4LDAuMTU0LDAuMTkxLDAuMTU0LDAuMzEzCgkJCWMwLDAuMTItMC4wNjEsMC4yMzctMC4xNTQsMC4zMTRMMi40OTIsMTcuNzE3Yy0wLjA3LDAuMDU3LTAuMTYyLDAuMDg3LTAuMjUsMC4wODdsLTAuMTc2LTAuMDQKCQkJYy0wLjEzNi0wLjA2NS0wLjIyMi0wLjIwNy0wLjIyMi0wLjM2MVYwLjQwMkMxLjg0NCwwLjI1LDEuOTMsMC4xMDcsMi4wNjcsMC4wNDN6Ii8+Cgk8L2c+Cgk8ZyBpZD0iQ2FwYV8xXzc4XyI+Cgk8L2c+CjwvZz4KPC9zdmc+Cg==);
|
||||
}
|
||||
|
||||
.theme-type-dark .ace_gutter-cell.ace-gutter-sql-run {
|
||||
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTguMS4xLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKCSB2aWV3Qm94PSIwIDAgMTcuODA0IDE3LjgwNCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTcuODA0IDE3LjgwNDsiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8Zz4KCTxnIGlkPSJjOThfcGxheSI+CgkJPHBhdGggZmlsbD0nI2RkZCcgZD0iTTIuMDY3LDAuMDQzQzIuMjEtMC4wMjgsMi4zNzItMC4wMDgsMi40OTMsMC4wODVsMTMuMzEyLDguNTAzYzAuMDk0LDAuMDc4LDAuMTU0LDAuMTkxLDAuMTU0LDAuMzEzCgkJCWMwLDAuMTItMC4wNjEsMC4yMzctMC4xNTQsMC4zMTRMMi40OTIsMTcuNzE3Yy0wLjA3LDAuMDU3LTAuMTYyLDAuMDg3LTAuMjUsMC4wODdsLTAuMTc2LTAuMDQKCQkJYy0wLjEzNi0wLjA2NS0wLjIyMi0wLjIwNy0wLjIyMi0wLjM2MVYwLjQwMkMxLjg0NCwwLjI1LDEuOTMsMC4xMDcsMi4wNjcsMC4wNDN6Ii8+Cgk8L2c+Cgk8ZyBpZD0iQ2FwYV8xXzc4XyI+Cgk8L2c+CjwvZz4KPC9zdmc+Cg==);
|
||||
}
|
||||
|
||||
.ace_gutter-cell.ace-gutter-sql-run:hover {
|
||||
background-color: var(--theme-bg-2);
|
||||
}
|
||||
|
||||
.ace_gutter-cell.ace-gutter-current-part {
|
||||
/* background-color: var(--theme-bg-2); */
|
||||
font-weight: bold;
|
||||
color: var(--theme-font-hover);
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export default [
|
||||
resolve({
|
||||
browser: true,
|
||||
}),
|
||||
|
||||
|
||||
// If we're building for production (npm run build
|
||||
// instead of npm run dev), minify
|
||||
production && terser(),
|
||||
@@ -88,6 +88,20 @@ export default [
|
||||
// enable run-time checks when not in production
|
||||
dev: !production,
|
||||
},
|
||||
onwarn: (warning, handler) => {
|
||||
const ignoreWarnings = [
|
||||
'a11y-click-events-have-key-events',
|
||||
'a11y-missing-attribute',
|
||||
'a11y-invalid-attribute',
|
||||
'a11y-no-noninteractive-tabindex',
|
||||
'a11y-label-has-associated-control',
|
||||
'vite-plugin-svelte-css-no-scopable-elements',
|
||||
'unused-export-let',
|
||||
];
|
||||
if (ignoreWarnings.includes(warning.code)) return;
|
||||
// console.log('***************************', warning.code);
|
||||
handler(warning);
|
||||
},
|
||||
}),
|
||||
// we'll extract any component CSS out into
|
||||
// a separate file - better for performance
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
import AppTitleProvider from './utility/AppTitleProvider.svelte';
|
||||
import getElectron from './utility/getElectron';
|
||||
import AppStartInfo from './widgets/AppStartInfo.svelte';
|
||||
import SettingsListener from './utility/SettingsListener.svelte';
|
||||
import { handleAuthOnStartup, handleOauthCallback } from './clientAuth';
|
||||
|
||||
let loadedApi = false;
|
||||
let loadedPlugins = false;
|
||||
@@ -32,9 +34,11 @@
|
||||
try {
|
||||
// console.log('************** LOADING API');
|
||||
|
||||
const config = await getConfig();
|
||||
await handleAuthOnStartup(config);
|
||||
|
||||
const connections = await apiCall('connections/list');
|
||||
const settings = await getSettings();
|
||||
const config = await getConfig();
|
||||
const apps = await getUsedApps();
|
||||
loadedApi = settings && connections && config && apps;
|
||||
|
||||
@@ -79,6 +83,7 @@
|
||||
<AppTitleProvider />
|
||||
{#if loadedPlugins}
|
||||
<OpenTabsOnStartup />
|
||||
<SettingsListener />
|
||||
<Screen />
|
||||
{:else}
|
||||
<AppStartInfo
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { internalRedirectTo } from './clientAuth';
|
||||
import FormButton from './forms/FormButton.svelte';
|
||||
import FormPasswordField from './forms/FormPasswordField.svelte';
|
||||
import FormProvider from './forms/FormProvider.svelte';
|
||||
import FormSubmit from './forms/FormSubmit.svelte';
|
||||
import FormTextField from './forms/FormTextField.svelte';
|
||||
import { apiCall, enableApi } from './utility/api';
|
||||
|
||||
onMount(() => {
|
||||
const removed = document.getElementById('starting_dbgate_zero');
|
||||
if (removed) removed.remove();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="root theme-light theme-type-light">
|
||||
<div class="text">DbGate</div>
|
||||
<div class="wrap">
|
||||
<div class="logo">
|
||||
<img class="img" src="logo192.png" />
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="heading">Log In</div>
|
||||
<FormProvider>
|
||||
<FormTextField label="Username" name="login" autocomplete="username" />
|
||||
<FormPasswordField label="Password" name="password" autocomplete="current-password" />
|
||||
|
||||
<div class="submit">
|
||||
<FormSubmit
|
||||
value="Log In"
|
||||
on:click={async e => {
|
||||
enableApi();
|
||||
const resp = await apiCall('auth/login', e.detail);
|
||||
if (resp.error) {
|
||||
internalRedirectTo(`/?page=not-logged&error=${encodeURIComponent(resp.error)}`);
|
||||
return;
|
||||
}
|
||||
const { accessToken } = resp;
|
||||
if (accessToken) {
|
||||
localStorage.setItem('accessToken', accessToken);
|
||||
internalRedirectTo('/');
|
||||
return;
|
||||
}
|
||||
internalRedirectTo(`/?page=not-logged`);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</FormProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.logo {
|
||||
display: flex;
|
||||
margin-bottom: 1rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.img {
|
||||
width: 80px;
|
||||
}
|
||||
.text {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
left: 1rem;
|
||||
font-size: 30pt;
|
||||
font-family: monospace;
|
||||
color: var(--theme-bg-2);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.submit {
|
||||
margin: var(--dim-large-form-margin);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.submit :global(input) {
|
||||
flex: 1;
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
.root {
|
||||
color: var(--theme-font-1);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background-color: var(--theme-bg-1);
|
||||
align-items: baseline;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.box {
|
||||
width: 600px;
|
||||
max-width: 80vw;
|
||||
/* max-width: 600px;
|
||||
width: 40vw; */
|
||||
border: 1px solid var(--theme-border);
|
||||
border-radius: 4px;
|
||||
background-color: var(--theme-bg-0);
|
||||
}
|
||||
|
||||
.wrap {
|
||||
margin-top: 20vh;
|
||||
}
|
||||
|
||||
.heading {
|
||||
text-align: center;
|
||||
margin: 1em;
|
||||
font-size: xx-large;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import FormStyledButton from './buttons/FormStyledButton.svelte';
|
||||
import { doLogout, redirectToLogin } from './clientAuth';
|
||||
|
||||
onMount(() => {
|
||||
const removed = document.getElementById('starting_dbgate_zero');
|
||||
if (removed) removed.remove();
|
||||
});
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const error = params.get('error');
|
||||
|
||||
function handleLogin() {
|
||||
redirectToLogin(undefined, true);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="root theme-light theme-type-light">
|
||||
<div class="title">Sorry, you are not authorized to run DbGate</div>
|
||||
{#if error}
|
||||
<div class="error">{error}</div>
|
||||
{/if}
|
||||
|
||||
<div class="button">
|
||||
<FormStyledButton value="Log In" on:click={handleLogin} />
|
||||
<FormStyledButton value="Log Out" on:click={doLogout} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
color: var(--theme-font-1);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: x-large;
|
||||
margin-top: 20vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin-top: 1em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 1em;
|
||||
}
|
||||
</style>
|
||||
@@ -8,6 +8,7 @@
|
||||
leftPanelWidth,
|
||||
openedSnackbars,
|
||||
selectedWidget,
|
||||
visibleWidgetSideBar,
|
||||
visibleCommandPalette,
|
||||
visibleTitleBar,
|
||||
visibleToolbar,
|
||||
@@ -29,7 +30,7 @@
|
||||
|
||||
$: currentThemeType = $currentThemeDefinition?.themeType == 'dark' ? 'theme-type-dark' : 'theme-type-light';
|
||||
|
||||
$: themeStyle = `<style id="themePlugin">${$currentThemeDefinition?.themeCss}</style>`;
|
||||
$: themeStyle = `<st` + `yle id="themePlugin">${$currentThemeDefinition?.themeCss}</st` + `yle>`;
|
||||
|
||||
const isElectron = !!getElectron();
|
||||
</script>
|
||||
@@ -63,7 +64,7 @@
|
||||
<div class="statusbar">
|
||||
<StatusBar />
|
||||
</div>
|
||||
{#if $selectedWidget}
|
||||
{#if $selectedWidget && $visibleWidgetSideBar}
|
||||
<div class="leftpanel">
|
||||
<WidgetContainer />
|
||||
</div>
|
||||
@@ -74,7 +75,7 @@
|
||||
<div class="content">
|
||||
<TabRegister />
|
||||
</div>
|
||||
{#if $selectedWidget}
|
||||
{#if $selectedWidget && $visibleWidgetSideBar}
|
||||
<div
|
||||
class="horizontal-split-handle splitter"
|
||||
use:splitterDrag={'clientX'}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
tabComponent,
|
||||
tooltip,
|
||||
props: {
|
||||
savedFile:fileName + '.' + fileType,
|
||||
savedFile: fileName + '.' + fileType,
|
||||
savedFolder: 'app:' + folderName,
|
||||
savedFormat: 'text',
|
||||
appFolder: folderName,
|
||||
@@ -28,7 +28,10 @@
|
||||
}
|
||||
|
||||
export const extractKey = data => data.fileName;
|
||||
export const createMatcher = ({ fileName }) => filter => filterName(filter, fileName);
|
||||
export const createMatcher =
|
||||
({ fileName }) =>
|
||||
filter =>
|
||||
filterName(filter, fileName);
|
||||
const APP_ICONS = {
|
||||
'config.json': 'img json',
|
||||
'command.sql': 'img app-command',
|
||||
@@ -50,7 +53,6 @@
|
||||
import InputTextModal from '../modals/InputTextModal.svelte';
|
||||
import ConfirmModal from '../modals/ConfirmModal.svelte';
|
||||
import { apiCall } from '../utility/api';
|
||||
import { currentDatabase, currentDatabase } from '../stores';
|
||||
|
||||
export let data;
|
||||
|
||||
|
||||
@@ -10,10 +10,12 @@
|
||||
export let group;
|
||||
export let groupFunc;
|
||||
export let items;
|
||||
export let groupIconFunc = plusExpandIcon;
|
||||
export let module;
|
||||
export let checkedObjectsStore = null;
|
||||
export let disableContextMenu = false;
|
||||
export let passProps;
|
||||
export let onDropOnGroup = undefined;
|
||||
|
||||
let isExpanded = true;
|
||||
|
||||
@@ -33,11 +35,19 @@
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
function handleDrop(e) {
|
||||
var data = e.dataTransfer.getData('app_object_drag_data');
|
||||
if (data && onDropOnGroup) {
|
||||
e.stopPropagation();
|
||||
onDropOnGroup(data, group);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="group" on:click={() => (isExpanded = !isExpanded)}>
|
||||
<div class="group" on:click={() => (isExpanded = !isExpanded)} on:drop={handleDrop}>
|
||||
<span class="expand-icon">
|
||||
<FontIcon icon={plusExpandIcon(isExpanded)} />
|
||||
<FontIcon icon={groupIconFunc(isExpanded)} />
|
||||
</span>
|
||||
|
||||
{group}
|
||||
@@ -53,18 +63,20 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each items as item}
|
||||
<AppObjectListItem
|
||||
isHidden={!item.isMatched}
|
||||
{...$$restProps}
|
||||
{module}
|
||||
data={item.data}
|
||||
{checkedObjectsStore}
|
||||
on:objectClick
|
||||
{disableContextMenu}
|
||||
{passProps}
|
||||
/>
|
||||
{/each}
|
||||
<div on:drop={handleDrop}>
|
||||
{#each items as item}
|
||||
<AppObjectListItem
|
||||
isHidden={!item.isMatched}
|
||||
{...$$restProps}
|
||||
{module}
|
||||
data={item.data}
|
||||
{checkedObjectsStore}
|
||||
on:objectClick
|
||||
{disableContextMenu}
|
||||
{passProps}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script>
|
||||
import _ from 'lodash';
|
||||
import _, { sortBy } from 'lodash';
|
||||
import { asyncFilter } from '../utility/common';
|
||||
import AppObjectGroup from './AppObjectGroup.svelte';
|
||||
import { plusExpandIcon } from '../icons/expandIcons';
|
||||
|
||||
import AppObjectListItem from './AppObjectListItem.svelte';
|
||||
|
||||
@@ -17,8 +18,12 @@
|
||||
export let passProps;
|
||||
export let getIsExpanded = null;
|
||||
export let setIsExpanded = null;
|
||||
export let sortGroups = false;
|
||||
|
||||
export let groupIconFunc = plusExpandIcon;
|
||||
export let groupFunc = undefined;
|
||||
export let onDropOnGroup = undefined;
|
||||
export let emptyGroupNames = [];
|
||||
|
||||
$: filtered = !groupFunc
|
||||
? list.filter(data => {
|
||||
@@ -61,16 +66,28 @@
|
||||
)
|
||||
: null;
|
||||
|
||||
$: groups = groupFunc ? _.groupBy(listGrouped, 'group') : null;
|
||||
function extendGroups(base, emptyList) {
|
||||
const res = {
|
||||
...base,
|
||||
};
|
||||
for (const item of emptyList) {
|
||||
if (res[item]) continue;
|
||||
res[item] = [];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
$: groups = groupFunc ? extendGroups(_.groupBy(listGrouped, 'group'), emptyGroupNames) : null;
|
||||
</script>
|
||||
|
||||
{#if groupFunc}
|
||||
{#each _.keys(groups) as group}
|
||||
{#each sortGroups ? _.sortBy(_.keys(groups)) : _.keys(groups) as group}
|
||||
<AppObjectGroup
|
||||
{group}
|
||||
{module}
|
||||
items={groups[group]}
|
||||
{expandIconFunc}
|
||||
{groupIconFunc}
|
||||
{isExpandable}
|
||||
{subItemsComponent}
|
||||
{checkedObjectsStore}
|
||||
@@ -80,6 +97,7 @@
|
||||
{passProps}
|
||||
{getIsExpanded}
|
||||
{setIsExpanded}
|
||||
{onDropOnGroup}
|
||||
/>
|
||||
{/each}
|
||||
{:else}
|
||||
|
||||
@@ -77,6 +77,17 @@
|
||||
);
|
||||
};
|
||||
|
||||
const handleDropDatabase = () => {
|
||||
showModal(ConfirmModal, {
|
||||
message: `Really drop database ${name}? All opened sessions with this database will be forcefully closed.`,
|
||||
onConfirm: () =>
|
||||
apiCall('server-connections/drop-database', {
|
||||
conid: connection._id,
|
||||
name,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const handleNewCollection = () => {
|
||||
showModal(InputTextModal, {
|
||||
value: '',
|
||||
@@ -169,6 +180,7 @@
|
||||
});
|
||||
currentArchive.set(resp.archiveFolder);
|
||||
selectedWidget.set('archive');
|
||||
visibleWidgetSideBar.set(true);
|
||||
showSnackbarSuccess(`Saved to archive ${resp.archiveFolder}`);
|
||||
};
|
||||
|
||||
@@ -218,6 +230,30 @@
|
||||
});
|
||||
};
|
||||
|
||||
const handleQueryDesigner = () => {
|
||||
openNewTab({
|
||||
title: 'Query #',
|
||||
icon: 'img query-design',
|
||||
tabComponent: 'QueryDesignTab',
|
||||
props: {
|
||||
conid: connection._id,
|
||||
database: name,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleNewPerspective = () => {
|
||||
openNewTab({
|
||||
title: 'Perspective #',
|
||||
icon: 'img perspective',
|
||||
tabComponent: 'PerspectiveTab',
|
||||
props: {
|
||||
conid: connection._id,
|
||||
database: name,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
async function handleConfirmSql(sql) {
|
||||
saveScriptToDatabase({ conid: connection._id, database: name }, sql, false);
|
||||
}
|
||||
@@ -233,13 +269,21 @@
|
||||
{ onClick: handleNewQuery, text: 'New query', isNewQuery: true },
|
||||
driver?.databaseEngineTypes?.includes('sql') && { onClick: handleNewTable, text: 'New table' },
|
||||
driver?.databaseEngineTypes?.includes('document') && { onClick: handleNewCollection, text: 'New collection' },
|
||||
driver?.databaseEngineTypes?.includes('sql') && { onClick: handleQueryDesigner, text: 'Design query' },
|
||||
driver?.databaseEngineTypes?.includes('sql') && {
|
||||
onClick: handleNewPerspective,
|
||||
text: 'Design perspective query',
|
||||
},
|
||||
{ divider: true },
|
||||
isSqlOrDoc && !connection.isReadOnly && { onClick: handleImport, text: 'Import wizard' },
|
||||
isSqlOrDoc && { onClick: handleExport, text: 'Export wizard' },
|
||||
driver?.databaseEngineTypes?.includes('sql') && { onClick: handleSqlRestore, text: 'Restore/import SQL dump' },
|
||||
driver?.supportsDatabaseDump && { onClick: handleSqlDump, text: 'Backup/export SQL dump' },
|
||||
isSqlOrDoc &&
|
||||
!connection.isReadOnly &&
|
||||
!connection.singleDatabase && { onClick: handleDropDatabase, text: 'Drop database' },
|
||||
{ divider: true },
|
||||
isSqlOrDoc && { onClick: handleShowDiagram, text: 'Show diagram' },
|
||||
driver?.databaseEngineTypes?.includes('sql') && { onClick: handleShowDiagram, text: 'Show diagram' },
|
||||
isSqlOrDoc && { onClick: handleSqlGenerator, text: 'SQL Generator' },
|
||||
isSqlOrDoc && { onClick: handleOpenJsonModel, text: 'Open model as JSON' },
|
||||
isSqlOrDoc && { onClick: handleExportModel, text: 'Export DB model - experimental' },
|
||||
@@ -295,6 +339,7 @@
|
||||
openedSingleDatabaseConnections,
|
||||
pinnedDatabases,
|
||||
selectedWidget,
|
||||
visibleWidgetSideBar,
|
||||
} from '../stores';
|
||||
import getElectron from '../utility/getElectron';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
<script lang="ts" context="module">
|
||||
export const extractKey = ({ schemaName, pureName }) => (schemaName ? `${schemaName}.${pureName}` : pureName);
|
||||
export const createMatcher = ({ schemaName, pureName, columns }) => filter =>
|
||||
filterName(filter, pureName, schemaName, ...(columns?.map(({ columnName }) => ({ childName: columnName })) || []));
|
||||
export const createTitle = ({ pureName }) => pureName;
|
||||
export const createMatcher =
|
||||
({ schemaName, pureName, columns }) =>
|
||||
filter =>
|
||||
filterName(
|
||||
filter,
|
||||
pureName,
|
||||
schemaName,
|
||||
...(columns?.map(({ columnName }) => ({ childName: columnName })) || [])
|
||||
);
|
||||
export const createTitle = ({ schemaName, pureName }) => (schemaName ? `${schemaName}.${pureName}` : pureName);
|
||||
|
||||
export const databaseObjectIcons = {
|
||||
tables: 'img table',
|
||||
@@ -45,7 +52,12 @@
|
||||
icon: 'img table-structure',
|
||||
},
|
||||
{
|
||||
label: 'Open perspective',
|
||||
label: 'Design query',
|
||||
isQueryDesigner: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
{
|
||||
label: 'Design perspective query',
|
||||
tab: 'PerspectiveTab',
|
||||
forceNewTab: true,
|
||||
icon: 'img perspective',
|
||||
@@ -64,13 +76,13 @@
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
{
|
||||
label: 'Create table backup',
|
||||
isDuplicateTable: true,
|
||||
label: 'Truncate table',
|
||||
isTruncate: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
{
|
||||
label: 'Query designer',
|
||||
isQueryDesigner: true,
|
||||
label: 'Create table backup',
|
||||
isDuplicateTable: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
{
|
||||
@@ -143,7 +155,11 @@
|
||||
icon: 'img view-structure',
|
||||
},
|
||||
{
|
||||
label: 'Open perspective',
|
||||
label: 'Design query',
|
||||
isQueryDesigner: true,
|
||||
},
|
||||
{
|
||||
label: 'Design perspective query',
|
||||
tab: 'PerspectiveTab',
|
||||
forceNewTab: true,
|
||||
icon: 'img perspective',
|
||||
@@ -152,10 +168,6 @@
|
||||
label: 'Drop view',
|
||||
isDrop: true,
|
||||
},
|
||||
{
|
||||
label: 'Query designer',
|
||||
isQueryDesigner: true,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
@@ -333,6 +345,12 @@
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Design perspective query',
|
||||
tab: 'PerspectiveTab',
|
||||
forceNewTab: true,
|
||||
icon: 'img perspective',
|
||||
},
|
||||
{
|
||||
label: 'Export',
|
||||
isExport: true,
|
||||
@@ -473,6 +491,21 @@
|
||||
x => x.schemaName == data.schemaName && x.pureName == data.pureName
|
||||
);
|
||||
});
|
||||
} else if (menu.isTruncate) {
|
||||
const { conid, database } = data;
|
||||
const driver = await getDriver();
|
||||
const dmp = driver.createDumper();
|
||||
dmp.truncateTable(data);
|
||||
|
||||
const sql = dmp.s;
|
||||
|
||||
showModal(ConfirmSqlModal, {
|
||||
sql,
|
||||
onConfirm: async () => {
|
||||
saveScriptToDatabase({ conid, database }, sql);
|
||||
},
|
||||
engine: driver.engine,
|
||||
});
|
||||
} else if (menu.isRename) {
|
||||
const { conid, database } = data;
|
||||
renameDatabaseObjectDialog(conid, database, data.pureName, (db, newName) => {
|
||||
@@ -559,6 +592,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
function getObjectTitle(connection, schemaName, pureName) {
|
||||
const driver = findEngineDriver(connection, getExtensions());
|
||||
|
||||
const defaultSchema = driver?.dialect?.defaultSchemaName;
|
||||
if (schemaName && defaultSchema && schemaName != defaultSchema) {
|
||||
return `${schemaName}.${pureName}`;
|
||||
}
|
||||
return pureName;
|
||||
}
|
||||
|
||||
export async function openDatabaseObjectDetail(
|
||||
tabComponent,
|
||||
scriptTemplate,
|
||||
@@ -576,7 +619,7 @@
|
||||
|
||||
openNewTab(
|
||||
{
|
||||
title: scriptTemplate ? 'Query #' : pureName,
|
||||
title: scriptTemplate ? 'Query #' : getObjectTitle(connection, schemaName, pureName),
|
||||
tooltip,
|
||||
icon: icon || (scriptTemplate ? 'img sql-file' : databaseObjectIcons[objectTypeField]),
|
||||
tabComponent: scriptTemplate ? 'QueryTab' : tabComponent,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
export let type = 'button';
|
||||
export let disabled = false;
|
||||
export let value;
|
||||
export let title = null;
|
||||
|
||||
function handleClick() {
|
||||
if (!disabled) dispatch('click');
|
||||
@@ -18,7 +19,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<input {type} {value} class:disabled {...$$restProps} on:click={handleClick} bind:this={domButton} />
|
||||
<input {type} {value} {title} class:disabled {...$$restProps} on:click={handleClick} bind:this={domButton} />
|
||||
|
||||
<style>
|
||||
input {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { dumpSqlSelect, Select } from 'dbgate-sqltree';
|
||||
import { EngineDriver } from 'dbgate-types';
|
||||
import type { Select } from 'dbgate-sqltree';
|
||||
import type { EngineDriver } from 'dbgate-types';
|
||||
import _ from 'lodash';
|
||||
import { apiCall } from '../utility/api';
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { apiCall, enableApi } from './utility/api';
|
||||
import { getConfig } from './utility/metadataLoaders';
|
||||
|
||||
export function isOauthCallback() {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const sentCode = params.get('code');
|
||||
const sentState = params.get('state');
|
||||
|
||||
return (
|
||||
sentCode && sentState && sentState.startsWith('dbg-oauth:') && sentState == sessionStorage.getItem('oauthState')
|
||||
);
|
||||
}
|
||||
|
||||
export function handleOauthCallback() {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const sentCode = params.get('code');
|
||||
|
||||
if (isOauthCallback()) {
|
||||
sessionStorage.removeItem('oauthState');
|
||||
apiCall('auth/oauth-token', {
|
||||
code: sentCode,
|
||||
redirectUri: location.origin + location.pathname,
|
||||
}).then(authResp => {
|
||||
const { accessToken, error, errorMessage } = authResp;
|
||||
|
||||
if (accessToken) {
|
||||
console.log('Settings access token from OAUTH');
|
||||
localStorage.setItem('accessToken', accessToken);
|
||||
internalRedirectTo('/');
|
||||
} else {
|
||||
console.log('Error when processing OAUTH callback', error || errorMessage);
|
||||
internalRedirectTo(`/?page=not-logged&error=${error || errorMessage}`);
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function handleAuthOnStartup(config) {
|
||||
if (config.oauth) {
|
||||
console.log('OAUTH callback URL:', location.origin + location.pathname);
|
||||
}
|
||||
if (config.oauth || config.isLoginForm) {
|
||||
if (localStorage.getItem('accessToken')) {
|
||||
return;
|
||||
}
|
||||
|
||||
redirectToLogin(config);
|
||||
}
|
||||
}
|
||||
|
||||
export async function redirectToLogin(config = null, force = false) {
|
||||
if (!config) {
|
||||
enableApi();
|
||||
config = await getConfig();
|
||||
}
|
||||
|
||||
if (config.isLoginForm) {
|
||||
if (!force) {
|
||||
const params = new URLSearchParams(location.search);
|
||||
if (params.get('page') == 'login' || params.get('page') == 'not-logged') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
internalRedirectTo('/?page=login');
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.oauth) {
|
||||
const state = `dbg-oauth:${Math.random().toString().substr(2)}`;
|
||||
sessionStorage.setItem('oauthState', state);
|
||||
console.log('Redirecting to OAUTH provider');
|
||||
location.replace(
|
||||
`${config.oauth}?client_id=dbgate&response_type=code&redirect_uri=${encodeURIComponent(
|
||||
location.origin + location.pathname
|
||||
)}&state=${encodeURIComponent(state)}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export function internalRedirectTo(path) {
|
||||
const index = location.pathname.lastIndexOf('/');
|
||||
const newPath = index >= 0 ? location.pathname.substring(0, index) + path : path;
|
||||
location.replace(newPath);
|
||||
}
|
||||
|
||||
export async function doLogout() {
|
||||
enableApi();
|
||||
const config = await getConfig();
|
||||
if (config.oauth) {
|
||||
localStorage.removeItem('accessToken');
|
||||
if (config.oauthLogout) {
|
||||
window.location.href = config.oauthLogout;
|
||||
} else {
|
||||
internalRedirectTo('/?page=not-logged');
|
||||
}
|
||||
} else if (config.isLoginForm) {
|
||||
localStorage.removeItem('accessToken');
|
||||
internalRedirectTo('/?page=not-logged');
|
||||
} else {
|
||||
window.location.href = 'config/logout';
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { tick } from 'svelte';
|
||||
import { commands } from '../stores';
|
||||
import { GlobalCommand } from './registerCommand';
|
||||
import type { GlobalCommand } from './registerCommand';
|
||||
|
||||
let isInvalidated = false;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getCommands, visibleCommandPalette } from '../stores';
|
||||
import { GlobalCommand } from './registerCommand';
|
||||
import type { GlobalCommand } from './registerCommand';
|
||||
|
||||
export default function runCommand(id) {
|
||||
const commandsValue = getCommands();
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import { currentDatabase, currentTheme, extensions, getExtensions, getVisibleToolbar, visibleToolbar } from '../stores';
|
||||
import {
|
||||
currentDatabase,
|
||||
currentTheme,
|
||||
emptyConnectionGroupNames,
|
||||
extensions,
|
||||
getExtensions,
|
||||
getVisibleToolbar,
|
||||
visibleToolbar,
|
||||
visibleWidgetSideBar,
|
||||
} from '../stores';
|
||||
import registerCommand from './registerCommand';
|
||||
import { get } from 'svelte/store';
|
||||
import AboutModal from '../modals/AboutModal.svelte';
|
||||
@@ -27,6 +36,7 @@ import runCommand from './runCommand';
|
||||
import { openWebLink } from '../utility/exportFileTools';
|
||||
import { getSettings } from '../utility/metadataLoaders';
|
||||
import { isMac } from '../utility/common';
|
||||
import { doLogout, internalRedirectTo } from '../clientAuth';
|
||||
|
||||
// function themeCommand(theme: ThemeDefinition) {
|
||||
// return {
|
||||
@@ -75,6 +85,15 @@ registerCommand({
|
||||
onClick: () => showModal(AboutModal),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'toggle.sidebar',
|
||||
category: 'Sidebar',
|
||||
name: 'Show',
|
||||
toolbarName: 'Toggle sidebar',
|
||||
keyText: 'CtrlOrCommand+B',
|
||||
onClick: () => visibleWidgetSideBar.update(x => !x),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'new.connection',
|
||||
toolbar: true,
|
||||
@@ -93,6 +112,31 @@ registerCommand({
|
||||
},
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'new.connection.folder',
|
||||
toolbar: true,
|
||||
icon: 'icon add-folder',
|
||||
toolbarName: 'Add connection folder',
|
||||
category: 'New',
|
||||
toolbarOrder: 1,
|
||||
name: 'Connection',
|
||||
testEnabled: () => !getCurrentConfig()?.runAsPortal,
|
||||
onClick: () => {
|
||||
showModal(InputTextModal, {
|
||||
value: '',
|
||||
label: 'New connection folder name',
|
||||
header: 'Create connection folder',
|
||||
onConfirm: async folder => {
|
||||
emptyConnectionGroupNames.update(names => {
|
||||
if (!folder) return names;
|
||||
if (names.includes(folder)) return names;
|
||||
return [...names, folder];
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'new.query',
|
||||
category: 'New',
|
||||
@@ -127,6 +171,9 @@ registerCommand({
|
||||
name: 'Query design',
|
||||
menuName: 'New query design',
|
||||
onClick: () => newQueryDesign(),
|
||||
testEnabled: () =>
|
||||
getCurrentDatabase() &&
|
||||
findEngineDriver(getCurrentDatabase()?.connection, getExtensions())?.databaseEngineTypes?.includes('sql'),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
@@ -144,6 +191,9 @@ registerCommand({
|
||||
icon: 'img diagram',
|
||||
name: 'ER Diagram',
|
||||
menuName: 'New ER diagram',
|
||||
testEnabled: () =>
|
||||
getCurrentDatabase() &&
|
||||
findEngineDriver(getCurrentDatabase()?.connection, getExtensions())?.databaseEngineTypes?.includes('sql'),
|
||||
onClick: () => newDiagram(),
|
||||
});
|
||||
|
||||
@@ -499,9 +549,7 @@ registerCommand({
|
||||
category: 'App',
|
||||
name: 'Logout',
|
||||
testEnabled: () => getCurrentConfig()?.login != null,
|
||||
onClick: () => {
|
||||
window.location.href = 'config/logout';
|
||||
},
|
||||
onClick: doLogout,
|
||||
});
|
||||
|
||||
export function registerFileCommands({
|
||||
@@ -599,7 +647,7 @@ export function registerFileCommands({
|
||||
registerCommand({
|
||||
id: idPrefix + '.replace',
|
||||
category,
|
||||
keyText: 'CtrlOrCommand+H',
|
||||
keyText: isMac() ? 'Alt+Command+F' : 'CtrlOrCommand+H',
|
||||
name: 'Replace',
|
||||
testEnabled: () => getCurrentEditor() != null,
|
||||
onClick: () => getCurrentEditor().replace(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ChangeSet, MacroDefinition, MacroSelectedCell } from 'dbgate-datalib';
|
||||
|
||||
import {
|
||||
ChangeSet,
|
||||
changeSetContainsChanges,
|
||||
changeSetInsertNewRow,
|
||||
createChangeSet,
|
||||
@@ -7,8 +8,6 @@ import {
|
||||
findExistingChangeSetItem,
|
||||
getChangeSetInsertedRows,
|
||||
GridDisplay,
|
||||
MacroDefinition,
|
||||
MacroSelectedCell,
|
||||
revertChangeSetRowChanges,
|
||||
setChangeSetValue,
|
||||
setChangeSetRowData,
|
||||
@@ -16,7 +15,8 @@ import {
|
||||
runMacroOnValue,
|
||||
changeSetInsertDocuments,
|
||||
} from 'dbgate-datalib';
|
||||
import Grider, { GriderRowStatus } from './Grider';
|
||||
import Grider from './Grider';
|
||||
import type { GriderRowStatus } from './Grider';
|
||||
|
||||
function getRowFromItem(row, matchedChangeSetItem) {
|
||||
return matchedChangeSetItem.document
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
import { openJsonDocument } from '../tabs/JsonTab.svelte';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import CellValue from './CellValue.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import EditCellDataModal from '../modals/EditCellDataModal.svelte';
|
||||
|
||||
export let rowIndex;
|
||||
export let col;
|
||||
@@ -29,6 +31,7 @@
|
||||
export let isAutoFillMarker = false;
|
||||
export let isCurrentCell = false;
|
||||
export let onDictionaryLookup = null;
|
||||
export let onSetValue;
|
||||
|
||||
$: value = col.isStructured ? _.get(rowData || {}, col.uniquePath) : (rowData || {})[col.uniqueName];
|
||||
|
||||
@@ -48,15 +51,6 @@
|
||||
|
||||
$: isJson = _.isPlainObject(value) && !(value?.type == 'Buffer' && _.isArray(value.data)) && !value.$oid;
|
||||
$: jsonParsedValue = isJsonLikeLongString(value) ? safeJsonParse(value) : null;
|
||||
|
||||
function shouldShowTextModalButton(col) {
|
||||
const m = col?.dataType?.match(/.*char.*\(([^\)]+)\)/);
|
||||
if (m && m[1]) {
|
||||
return parseInt(m[1]) >= 30 || m[1]?.toUpperCase() == 'MAX';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<td
|
||||
@@ -123,10 +117,6 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- {#if shouldShowTextModalButton(col)}
|
||||
<ShowFormButton icon="icon edit" on:click={() => openJsonDocument(value, undefined, true)} />
|
||||
{/if} -->
|
||||
|
||||
{#if isAutoFillMarker}
|
||||
<div class="autoFillMarker autofillHandleMarker" />
|
||||
{/if}
|
||||
|
||||
@@ -200,7 +200,7 @@
|
||||
id: 'dataGrid.hideColumn',
|
||||
category: 'Data grid',
|
||||
name: 'Hide column',
|
||||
keyText: 'CtrlOrCommand+H',
|
||||
keyText: isMac() ? 'Alt+Command+F' : 'CtrlOrCommand+H',
|
||||
testEnabled: () => getCurrentDataGrid() != null,
|
||||
onClick: () => getCurrentDataGrid().hideColumn(),
|
||||
});
|
||||
@@ -247,6 +247,14 @@
|
||||
onClick: () => getCurrentDataGrid().addJsonDocument(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.editCellValue',
|
||||
category: 'Data grid',
|
||||
name: 'Edit cell value',
|
||||
testEnabled: () => getCurrentDataGrid()?.editCellValueEnabled(),
|
||||
onClick: () => getCurrentDataGrid().editCellValue(),
|
||||
});
|
||||
|
||||
function getSelectedCellsInfo(selectedCells, grider, realColumnUniqueNames, selectedRowData) {
|
||||
if (selectedCells.length > 1 && selectedCells.every(x => _.isNumber(x[0]) && _.isNumber(x[1]))) {
|
||||
let sum = _.sumBy(selectedCells, cell => {
|
||||
@@ -327,6 +335,7 @@
|
||||
import { isCtrlOrCommandKey, isMac } from '../utility/common';
|
||||
import { selectionCouldBeShownOnMap } from '../elements/MapView.svelte';
|
||||
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
|
||||
import EditCellDataModal, { shouldOpenMultilineDialog } from '../modals/EditCellDataModal.svelte';
|
||||
|
||||
export let onLoadNextData = undefined;
|
||||
export let grider = undefined;
|
||||
@@ -706,6 +715,22 @@
|
||||
editJsonRowDocument(grider, rowIndex);
|
||||
}
|
||||
|
||||
export function editCellValueEnabled() {
|
||||
return grider.editable && selectedCells.length == 1;
|
||||
}
|
||||
|
||||
export function editCellValue() {
|
||||
if (!currentCell) return false;
|
||||
const rowData = grider.getRowData(currentCell[0]);
|
||||
if (!rowData) return null;
|
||||
const cellData = rowData[realColumnUniqueNames[currentCell[1]]];
|
||||
|
||||
showModal(EditCellDataModal, {
|
||||
value: cellData?.toString() || '',
|
||||
onSave: value => grider.setCellValue(currentCell[0], realColumnUniqueNames[currentCell[1]], value),
|
||||
});
|
||||
}
|
||||
|
||||
export function addJsonDocumentEnabled() {
|
||||
return grider.editable;
|
||||
}
|
||||
@@ -1058,7 +1083,9 @@
|
||||
dragStartCell = cell;
|
||||
|
||||
if (isRegularCell(cell) && !_.isEqual(cell, $inplaceEditorState.cell) && _.isEqual(cell, oldCurrentCell)) {
|
||||
dispatchInsplaceEditor({ type: 'show', cell, selectAll: true });
|
||||
if (!showMultilineCellEditorConditional(cell)) {
|
||||
dispatchInsplaceEditor({ type: 'show', cell, selectAll: true });
|
||||
}
|
||||
} else if (!_.isEqual(cell, $inplaceEditorState.cell)) {
|
||||
dispatchInsplaceEditor({ type: 'close' });
|
||||
}
|
||||
@@ -1068,6 +1095,26 @@
|
||||
if (display.focusedColumns) display.focusColumns(null);
|
||||
}
|
||||
|
||||
function handleBlur() {
|
||||
shiftDragStartCell = null;
|
||||
dragStartCell = null;
|
||||
}
|
||||
|
||||
function showMultilineCellEditorConditional(cell) {
|
||||
if (!cell) return false;
|
||||
const rowData = grider.getRowData(cell[0]);
|
||||
if (!rowData) return null;
|
||||
const cellData = rowData[realColumnUniqueNames[cell[1]]];
|
||||
if (shouldOpenMultilineDialog(cellData)) {
|
||||
showModal(EditCellDataModal, {
|
||||
value: cellData,
|
||||
onSave: value => grider.setCellValue(cell[0], realColumnUniqueNames[cell[1]], value),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleGridMouseMove(event) {
|
||||
if (autofillDragStartCell) {
|
||||
const cell = cellFromEvent(event);
|
||||
@@ -1197,7 +1244,9 @@
|
||||
|
||||
if (event.keyCode == keycodes.f2 || event.keyCode == keycodes.enter) {
|
||||
// @ts-ignore
|
||||
dispatchInsplaceEditor({ type: 'show', cell: currentCell, selectAll: true });
|
||||
if (!showMultilineCellEditorConditional(currentCell)) {
|
||||
dispatchInsplaceEditor({ type: 'show', cell: currentCell, selectAll: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (event.shiftKey) {
|
||||
@@ -1516,6 +1565,8 @@
|
||||
{ command: 'dataGrid.clearFilter' },
|
||||
{ command: 'dataGrid.undo', hideDisabled: true },
|
||||
{ command: 'dataGrid.redo', hideDisabled: true },
|
||||
{ divider: true },
|
||||
{ command: 'dataGrid.editCellValue', hideDisabled: true },
|
||||
{ command: 'dataGrid.newJson', hideDisabled: true },
|
||||
{ command: 'dataGrid.editJsonDocument', hideDisabled: true },
|
||||
{ command: 'dataGrid.viewJsonDocument', hideDisabled: true },
|
||||
@@ -1609,6 +1660,7 @@
|
||||
}}
|
||||
on:paste={handlePaste}
|
||||
on:copy={copyToClipboard}
|
||||
on:blur={handleBlur}
|
||||
/>
|
||||
<table
|
||||
class="table"
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<RowHeaderCell {rowIndex} onShowForm={onSetFormView ? () => onSetFormView(rowData, null) : null} />
|
||||
{#each visibleRealColumns as col (col.uniqueName)}
|
||||
{#if inplaceEditorState.cell && rowIndex == inplaceEditorState.cell[0] && col.colIndex == inplaceEditorState.cell[1]}
|
||||
<td>
|
||||
<td class='editor'>
|
||||
<InplaceEditor
|
||||
width={col.width}
|
||||
{inplaceEditorState}
|
||||
@@ -89,6 +89,7 @@
|
||||
autofillMarkerCell[0] == rowIndex &&
|
||||
grider.editable}
|
||||
onDictionaryLookup={() => handleLookup(col)}
|
||||
onSetValue={value => grider.setCellValue(rowIndex, col.uniqueName, value)}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
@@ -98,6 +99,11 @@
|
||||
tr {
|
||||
background-color: var(--theme-bg-0);
|
||||
}
|
||||
|
||||
td.editor {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
tr:nth-child(6n + 3) {
|
||||
background-color: var(--theme-bg-1);
|
||||
}
|
||||
|
||||
@@ -9,11 +9,14 @@
|
||||
|
||||
<script lang="ts">
|
||||
import keycodes from '../utility/keycodes';
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, tick } from 'svelte';
|
||||
import createRef from '../utility/createRef';
|
||||
import _ from 'lodash';
|
||||
import { arrayToHexString, parseCellValue, stringifyCellValue } from 'dbgate-tools';
|
||||
import { isCtrlOrCommandKey } from '../utility/common';
|
||||
import ShowFormButton from '../formview/ShowFormButton.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import EditCellDataModal from '../modals/EditCellDataModal.svelte';
|
||||
|
||||
export let inplaceEditorState;
|
||||
export let dispatchInsplaceEditor;
|
||||
@@ -23,12 +26,15 @@
|
||||
export let fillParent = false;
|
||||
|
||||
let domEditor;
|
||||
let showEditorButton = true;
|
||||
|
||||
const widthCopy = width;
|
||||
|
||||
const isChangedRef = createRef(!!inplaceEditorState.text);
|
||||
|
||||
function handleKeyDown(event) {
|
||||
showEditorButton = false;
|
||||
|
||||
switch (event.keyCode) {
|
||||
case keycodes.escape:
|
||||
isChangedRef.set(false);
|
||||
@@ -81,18 +87,39 @@
|
||||
domEditor.select();
|
||||
}
|
||||
});
|
||||
|
||||
$: realWidth = widthCopy ? widthCopy - (showEditorButton ? 16 : 0) : undefined;
|
||||
</script>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
on:change={() => isChangedRef.set(true)}
|
||||
on:change={() => {
|
||||
isChangedRef.set(true);
|
||||
showEditorButton = false;
|
||||
}}
|
||||
on:keydown={handleKeyDown}
|
||||
on:blur={handleBlur}
|
||||
bind:this={domEditor}
|
||||
style={widthCopy ? `width:${widthCopy}px;min-width:${widthCopy}px;max-width:${widthCopy}px` : undefined}
|
||||
style={widthCopy ? `width:${realWidth}px;min-width:${realWidth}px;max-width:${realWidth}px` : undefined}
|
||||
class:fillParent
|
||||
class:showEditorButton
|
||||
/>
|
||||
|
||||
{#if showEditorButton}
|
||||
<ShowFormButton
|
||||
icon="icon edit"
|
||||
on:click={() => {
|
||||
isChangedRef.set(false);
|
||||
dispatchInsplaceEditor({ type: 'close' });
|
||||
|
||||
showModal(EditCellDataModal, {
|
||||
value: stringifyCellValue(cellValue),
|
||||
onSave: onSetValue,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
input {
|
||||
border: 0px solid;
|
||||
@@ -109,4 +136,8 @@
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
input.showEditorButton {
|
||||
margin-right: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
let changeIndex = 0;
|
||||
let rowCountLoaded = null;
|
||||
|
||||
const throttleLoadNext = _.throttle(() => domGrid.resetLoadedAll(), 500);
|
||||
const throttleLoadNext = _.throttle(() => domGrid?.resetLoadedAll(), 500);
|
||||
|
||||
const handleJslDataStats = stats => {
|
||||
if (stats.changeIndex < changeIndex) return;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Grider, { GriderRowStatus } from './Grider';
|
||||
import Grider from './Grider';
|
||||
|
||||
export default class RowsArrayGrider extends Grider {
|
||||
constructor(private rows: any[]) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import _ from 'lodash';
|
||||
import { SeriesSizes } from './SeriesSizes';
|
||||
import { CellAddress } from './selection';
|
||||
import { GridDisplay } from 'dbgate-datalib';
|
||||
import Grider from './Grider';
|
||||
import type { CellAddress } from './selection';
|
||||
import type { GridDisplay } from 'dbgate-datalib';
|
||||
import type Grider from './Grider';
|
||||
import { isJsonLikeLongString, safeJsonParse } from 'dbgate-tools';
|
||||
|
||||
export function countColumnSizes(grider: Grider, columns, containerWidth, display: GridDisplay) {
|
||||
|
||||
@@ -61,6 +61,9 @@
|
||||
}
|
||||
|
||||
$: sortOrderProps = settings?.getSortOrderProps ? settings?.getSortOrderProps(designerId, column.columnName) : null;
|
||||
$: iconOverride = settings?.getColumnIconOverride
|
||||
? settings?.getColumnIconOverride(designerId, column.columnName)
|
||||
: null;
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -144,7 +147,7 @@
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<ColumnLabel {...column} {foreignKey} forceIcon />
|
||||
<ColumnLabel {...column} {foreignKey} forceIcon {iconOverride} />
|
||||
{#if designerColumn?.filter}
|
||||
<FontIcon icon="img filter" />
|
||||
{/if}
|
||||
|
||||
@@ -479,7 +479,7 @@
|
||||
const rect = e.target.getBoundingClientRect();
|
||||
var json = JSON.parse(data);
|
||||
const { objectTypeField } = json;
|
||||
if (objectTypeField != 'tables' && objectTypeField != 'views') return;
|
||||
if (objectTypeField != 'tables' && objectTypeField != 'views' && objectTypeField != 'collections') return;
|
||||
json.designerId = `${json.pureName}-${uuidv1()}`;
|
||||
json.left = e.clientX - rect.left;
|
||||
json.top = e.clientY - rect.top;
|
||||
@@ -941,6 +941,7 @@
|
||||
.empty {
|
||||
margin: 50px;
|
||||
font-size: 20px;
|
||||
position: absolute;
|
||||
}
|
||||
.canvas {
|
||||
position: relative;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user