Compare commits
378 Commits
tableedito
...
mongoform
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f43ffd9af4 | ||
|
|
573e404612 | ||
|
|
91c88bd92d | ||
|
|
475f82a35e | ||
|
|
0548bae7af | ||
|
|
0413f4b4d9 | ||
|
|
9e9991c675 | ||
|
|
21502bda65 | ||
|
|
69e1c6c625 | ||
|
|
fcedeb2316 | ||
|
|
a23ff752a3 | ||
|
|
138b0414f2 | ||
|
|
87988d5c3a | ||
|
|
41cf7009b3 | ||
|
|
18860c823d | ||
|
|
55cc51d24a | ||
|
|
2a49eaab12 | ||
|
|
394c6028c9 | ||
|
|
d4bd6e03c9 | ||
|
|
1a94222262 | ||
|
|
94b41fecbc | ||
|
|
9ed6932c1e | ||
|
|
e748591c10 | ||
|
|
fca6b87cb9 | ||
|
|
c23ecfff47 | ||
|
|
8738665dcf | ||
|
|
a05fc90579 | ||
|
|
71f7a705c4 | ||
|
|
9cb2d397ad | ||
|
|
484e7c27a2 | ||
|
|
acdba0c52c | ||
|
|
943544958a | ||
|
|
e78420b1b0 | ||
|
|
69d639b9ea | ||
|
|
a540220c05 | ||
|
|
b1ddcbfbfc | ||
|
|
87aaa281e4 | ||
|
|
704733d80d | ||
|
|
a50458494e | ||
|
|
2a980a7892 | ||
|
|
a3762c6caa | ||
|
|
d6ba822338 | ||
|
|
f146d70e2b | ||
|
|
d62177d996 | ||
|
|
a1993214e2 | ||
|
|
70411b764b | ||
|
|
cd0fb0fdf2 | ||
|
|
9713c9b88e | ||
|
|
d5118909d1 | ||
|
|
bb41236a5f | ||
|
|
9d84c0f213 | ||
|
|
d45fbcb8c8 | ||
|
|
4762597741 | ||
|
|
9c27c224ec | ||
|
|
2268d6126b | ||
|
|
bbc50ea3fb | ||
|
|
11985004b5 | ||
|
|
4f58d2ff80 | ||
|
|
987fe6095a | ||
|
|
d8fcbc8c17 | ||
|
|
833610c88b | ||
|
|
218478c128 | ||
|
|
3f8ff91e2c | ||
|
|
11436c065c | ||
|
|
f1c70f6f82 | ||
|
|
c66ff5820c | ||
|
|
4c87ad50b1 | ||
|
|
6c5f5a7cfb | ||
|
|
9876a76836 | ||
|
|
952bdc4baa | ||
|
|
2afa7a5f58 | ||
|
|
bc9e8a2ea6 | ||
|
|
e2dcfe9940 | ||
|
|
638b04877d | ||
|
|
2a9c67d2f6 | ||
|
|
2c9d424fc8 | ||
|
|
4e76f10175 | ||
|
|
5f2afc037e | ||
|
|
50e61cdce1 | ||
|
|
586f2fed21 | ||
|
|
020b4163d7 | ||
|
|
6b0e1e322a | ||
|
|
0d0bd29812 | ||
|
|
4e4447de8a | ||
|
|
15c9e93e8a | ||
|
|
5531705433 | ||
|
|
dfadf0653d | ||
|
|
742b68453a | ||
|
|
155406827e | ||
|
|
a97e6dbcab | ||
|
|
d4989c75ca | ||
|
|
b7b9dde5ae | ||
|
|
34f2fb2a0a | ||
|
|
965a967450 | ||
|
|
40872699c6 | ||
|
|
ec90a8b952 | ||
|
|
90c4b44fdb | ||
|
|
df5062c9a5 | ||
|
|
437155e4c5 | ||
|
|
5ebee161ae | ||
|
|
10c77ad153 | ||
|
|
5e59926556 | ||
|
|
4b1b61328a | ||
|
|
4c6d9f0660 | ||
|
|
a5a7447cec | ||
|
|
dd373f9db9 | ||
|
|
bb0f5e4404 | ||
|
|
c77bc820d4 | ||
|
|
a1ab47a6f9 | ||
|
|
efc07280a6 | ||
|
|
dcb4c5071a | ||
|
|
7b625c6073 | ||
|
|
489c9a905c | ||
|
|
75c578de47 | ||
|
|
f7c4bbc708 | ||
|
|
9c1227273c | ||
|
|
47a045fc24 | ||
|
|
9e9df60d37 | ||
|
|
93b7a9a674 | ||
|
|
b96576ba6f | ||
|
|
0524b4c5b6 | ||
|
|
b7663e2e06 | ||
|
|
24f4e1d898 | ||
|
|
9089f78593 | ||
|
|
8f90dad303 | ||
|
|
c823e18699 | ||
|
|
73bfac2bfb | ||
|
|
08b5bce03c | ||
|
|
ddf8a5806c | ||
|
|
eceab2dde9 | ||
|
|
9c7df42948 | ||
|
|
321d5d71de | ||
|
|
d4a35fb414 | ||
|
|
21feb3a042 | ||
|
|
9adb4b41c6 | ||
|
|
3b3e81e3f7 | ||
|
|
dfa8ca6797 | ||
|
|
0af207d330 | ||
|
|
49337a4112 | ||
|
|
7cd26c4fe4 | ||
|
|
159ba72129 | ||
|
|
cfb772c717 | ||
|
|
500c1c76ba | ||
|
|
834eeabd3f | ||
|
|
8770034bf5 | ||
|
|
423f876d68 | ||
|
|
af54c958ba | ||
|
|
4e16119835 | ||
|
|
d6f9db48aa | ||
|
|
2063005d5c | ||
|
|
c2c54856ff | ||
|
|
cedb740fb0 | ||
|
|
913f89e970 | ||
|
|
7d6bf90a0a | ||
|
|
8a4275fb09 | ||
|
|
d5ebea3764 | ||
|
|
a93aff1bb7 | ||
|
|
c193955fbe | ||
|
|
5f97f7d922 | ||
|
|
54d17a67d4 | ||
|
|
0c94d7fcac | ||
|
|
fca23f65de | ||
|
|
a35d5525a3 | ||
|
|
904d35d26a | ||
|
|
929c08e094 | ||
|
|
97a27381f2 | ||
|
|
f4fe5b9b53 | ||
|
|
00d5b25baa | ||
|
|
c6e95dbb6a | ||
|
|
da6bd9b475 | ||
|
|
2eebe44f87 | ||
|
|
3dd99a44cb | ||
|
|
ec2acebdc9 | ||
|
|
e49c0169da | ||
|
|
49f22e1a3b | ||
|
|
423644e9d9 | ||
|
|
b64b6be68a | ||
|
|
d3c4c18b62 | ||
|
|
fe5826bc8e | ||
|
|
7709e24ccd | ||
|
|
b91cf18aee | ||
|
|
ae9c4b4f98 | ||
|
|
3d25a51cf9 | ||
|
|
18e7171038 | ||
|
|
8cf014efa4 | ||
|
|
78d71602bf | ||
|
|
dcfd6ee1dc | ||
|
|
eb4ecb4cf8 | ||
|
|
bc54564d64 | ||
|
|
1c7052810a | ||
|
|
4bfba2ec02 | ||
|
|
c2dc4d76ba | ||
|
|
ce44e271ae | ||
|
|
ef5bfb5a89 | ||
|
|
7acea0f4ac | ||
|
|
593e61abb9 | ||
|
|
a0aa508e8d | ||
|
|
ca517f9c73 | ||
|
|
ad0e02de5d | ||
|
|
ea05ae0079 | ||
|
|
689eb7baa2 | ||
|
|
a4387155e7 | ||
|
|
565a60e638 | ||
|
|
5dba5a6dfd | ||
|
|
c497c1ceca | ||
|
|
5929a01010 | ||
|
|
182b0e0a75 | ||
|
|
2cc74b594e | ||
|
|
e9430988f4 | ||
|
|
f30bd0a894 | ||
|
|
1c0a8cad56 | ||
|
|
8a4fd302d2 | ||
|
|
db7f8d6a74 | ||
|
|
4f1eb4003a | ||
|
|
3efaac7d1f | ||
|
|
d9387bef1f | ||
|
|
a101f21483 | ||
|
|
8a0d10e50d | ||
|
|
fe1fc7923f | ||
|
|
f0802dc471 | ||
|
|
30ade5867c | ||
|
|
ea54673497 | ||
|
|
2ffd729465 | ||
|
|
ef910f43a6 | ||
|
|
fc333167ac | ||
|
|
390447c948 | ||
|
|
1bb5f4974d | ||
|
|
48e3cf1be5 | ||
|
|
1e540b3fe9 | ||
|
|
60c1090d6c | ||
|
|
71bea87a7a | ||
|
|
a03261bfd4 | ||
|
|
704a04e9bb | ||
|
|
28c1421294 | ||
|
|
7a5bcc62c8 | ||
|
|
321eedefea | ||
|
|
daf9e9d18b | ||
|
|
dd7db5904c | ||
|
|
6bddf3aa83 | ||
|
|
9743569ca7 | ||
|
|
40ed020c0a | ||
|
|
63ac08cc27 | ||
|
|
e16b0ef61f | ||
|
|
14f9a40851 | ||
|
|
ba6abd1e64 | ||
|
|
4ffc5842bb | ||
|
|
4cb304a41c | ||
|
|
abdbe4122f | ||
|
|
b2e37e88ea | ||
|
|
6a28ceaa51 | ||
|
|
c15f859eae | ||
|
|
f717dfa3b5 | ||
|
|
ab7510c8e8 | ||
|
|
3601ac21b4 | ||
|
|
65bd5a60ef | ||
|
|
6a9bd8248c | ||
|
|
98ff6db701 | ||
|
|
7b67576131 | ||
|
|
d0d6d86bb9 | ||
|
|
1f057fb0a9 | ||
|
|
75a429b74f | ||
|
|
d34524c3d0 | ||
|
|
bdfa66d37d | ||
|
|
1d5d87e26a | ||
|
|
9f3aadc17d | ||
|
|
58f213d042 | ||
|
|
89dbf38962 | ||
|
|
670e3d127e | ||
|
|
72181e70a1 | ||
|
|
c3ac836fa9 | ||
|
|
b0deba4bae | ||
|
|
d08fc85459 | ||
|
|
417ec9fcd2 | ||
|
|
f9d4a9a3a0 | ||
|
|
eab870c237 | ||
|
|
521199ee1a | ||
|
|
0d1a6e96f3 | ||
|
|
1076fb8391 | ||
|
|
114dc0b543 | ||
|
|
728ca72cc1 | ||
|
|
e243ecd96a | ||
|
|
2defdc3f28 | ||
|
|
7aeef55a58 | ||
|
|
5e8967da52 | ||
|
|
679145a394 | ||
|
|
9b012c187a | ||
|
|
9ce1fdd59e | ||
|
|
c0d0a00615 | ||
|
|
c67c08bd69 | ||
|
|
6e846797b9 | ||
|
|
a3ad98d2a9 | ||
|
|
c7dbf333c7 | ||
|
|
19392e9406 | ||
|
|
818de9b111 | ||
|
|
0292a37b16 | ||
|
|
8b9031b0c2 | ||
|
|
d88591032e | ||
|
|
2c6a59638b | ||
|
|
8312415430 | ||
|
|
fdb14d687b | ||
|
|
a20b351938 | ||
|
|
f8016d26ec | ||
|
|
ad186f5efb | ||
|
|
8e26918975 | ||
|
|
e0303aa77e | ||
|
|
6a02c4ebaa | ||
|
|
2c73ab6bc1 | ||
|
|
b60714f30c | ||
|
|
f3163617e0 | ||
|
|
834be32676 | ||
|
|
0f6637188b | ||
|
|
ef0921ecf5 | ||
|
|
1ffa613e09 | ||
|
|
c0c8cd88e3 | ||
|
|
2666717c3a | ||
|
|
777abbc097 | ||
|
|
e4db985ef9 | ||
|
|
6afaa6f856 | ||
|
|
1325851bcf | ||
|
|
fb7da60127 | ||
|
|
ecde9cb6bd | ||
|
|
6b06ed5baf | ||
|
|
2aa965cf3b | ||
|
|
fc11fe1e8d | ||
|
|
e33e14bd5f | ||
|
|
0c7fc0b7b6 | ||
|
|
5904d45c44 | ||
|
|
5d997fc1c9 | ||
|
|
4693564ffa | ||
|
|
3377929b30 | ||
|
|
a35f6f2629 | ||
|
|
cb264ac6cc | ||
|
|
9236e1a6c2 | ||
|
|
df359aea58 | ||
|
|
fdf60b5267 | ||
|
|
bd3c18d883 | ||
|
|
18bf6e5979 | ||
|
|
edaf9676e4 | ||
|
|
bd524d345a | ||
|
|
0a39a6829c | ||
|
|
5c7a011efb | ||
|
|
4e350e99c4 | ||
|
|
a714f7ae54 | ||
|
|
a17b76c570 | ||
|
|
54d476a972 | ||
|
|
255c3e5ef4 | ||
|
|
059eabf2fa | ||
|
|
79fdde73ae | ||
|
|
84e475192e | ||
|
|
3907b1ae8b | ||
|
|
dcfefc78a2 | ||
|
|
d3039a9248 | ||
|
|
31dd80b79a | ||
|
|
8d6d1d979e | ||
|
|
fe1c5f5801 | ||
|
|
df976a84d2 | ||
|
|
420e94600e | ||
|
|
9940bd5177 | ||
|
|
45d99a4126 | ||
|
|
c2b7c775c0 | ||
|
|
51ba9d3b5a | ||
|
|
8396e726ec | ||
|
|
cb67b57faf | ||
|
|
99381536d7 | ||
|
|
a9cb9f1874 | ||
|
|
420a58380a | ||
|
|
75ca3cbb11 | ||
|
|
a5c1966a94 | ||
|
|
ca4ff95316 | ||
|
|
a3294950a4 | ||
|
|
29355a6d3e | ||
|
|
add0ba09c3 | ||
|
|
005ae87309 | ||
|
|
5f372a1d0f | ||
|
|
ecce75960a | ||
|
|
72cc510c64 | ||
|
|
7e39b8c2a0 | ||
|
|
ed4ef4d999 |
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve DbGate
|
||||
title: 'BUG: Say something here'
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Version Information (please complete the following information):**
|
||||
- OS: [e.g. Windows/Mac/Ubuntu]
|
||||
- App Version [see Help -> About]
|
||||
- Install source [e.g. installer/SNAP/Docker/NPM]
|
||||
- Type - Web/Application
|
||||
- Database engine: [e.g. MySQL/PostgreSQL/SQL Server]
|
||||
|
||||
**Additional context**
|
||||
Anything else you think might be helpful
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for DbGate
|
||||
title: 'FEAT: '
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
18
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
18
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
name: Question
|
||||
about: Ask a question about how to do something
|
||||
title: 'QUESTION: Summary of your question'
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Details:**
|
||||
Details about your question
|
||||
|
||||
**Version Information [might be relevant to your issue]**
|
||||
Operating System:
|
||||
App Version [help -> about]:
|
||||
|
||||
**Screenshot [if appropriate]**:
|
||||
A screenshot of the app if that helps
|
||||
91
.github/workflows/build-npm.yaml
vendored
Normal file
91
.github/workflows/build-npm.yaml
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
name: NPM packages
|
||||
|
||||
# on: [push]
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
# - 'v[0-9]+.[0-9]+.[0-9]+-alpha.[0-9]+'
|
||||
|
||||
# on:
|
||||
# push:
|
||||
# branches:
|
||||
# - production
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-18.04]
|
||||
# os: [macOS-10.14, windows-2016, ubuntu-18.04]
|
||||
|
||||
steps:
|
||||
- name: Context
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Use Node.js 10.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10.x
|
||||
|
||||
- name: Configure NPM token
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
npm config set '//registry.npmjs.org/:_authToken' "${NPM_TOKEN}"
|
||||
|
||||
- name: yarn install
|
||||
run: |
|
||||
yarn install
|
||||
|
||||
- name: setCurrentVersion
|
||||
run: |
|
||||
yarn setCurrentVersion
|
||||
|
||||
- name: Publish types
|
||||
working-directory: packages/types
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
- name: Publish tools
|
||||
working-directory: packages/tools
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
- name: Publish sqltree
|
||||
working-directory: packages/sqltree
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
- name: Publish api
|
||||
working-directory: packages/api
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
- name: Publish datalib
|
||||
working-directory: packages/datalib
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
- name: Publish filterparser
|
||||
working-directory: packages/filterparser
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
- name: Publish web
|
||||
working-directory: packages/web
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
- name: Publish dbgate
|
||||
working-directory: packages/dbgate
|
||||
run: |
|
||||
npm publish
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -12,6 +12,10 @@ node_modules
|
||||
build
|
||||
dist
|
||||
|
||||
app/packages/web/public
|
||||
docker/public
|
||||
docker/bundle.js
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
@@ -24,3 +28,4 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
app/src/nativeModulesContent.js
|
||||
packages/api/src/nativeModulesContent.js
|
||||
.VSCodeCounter
|
||||
28
CHANGELOG.md
Normal file
28
CHANGELOG.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# ChangeLog
|
||||
|
||||
### 4.0.3
|
||||
- FIX: fixes for FireFox (mainly incorrent handle of bind:clientHeight, replaces with resizeobserver)
|
||||
### 4.0.2
|
||||
- FIX: fixed docker and NPM build
|
||||
### 4.0.0
|
||||
- CHANGED: Excahnged React with Svelte. Changed theme colors. Huge speed and memory optimalization
|
||||
- ADDED: SQL Generator (CREATE, INSERT, DROP)
|
||||
- ADDED: Command palette (F1). Introduced commands, extended some context menus
|
||||
- ADDED: New keyboard shortcuts
|
||||
- ADDED: Switch to recent database feature
|
||||
- ADDED: Macros from free table editor are available also in table data editor
|
||||
- CHANGED: Cell data preview is now in left widgets panel
|
||||
- CHANGED: Toolbar refactor
|
||||
- FIX: Solved reconnecting expired connection
|
||||
|
||||
### 3.9.6
|
||||
- ADDED: Connect using SSH Tunnel
|
||||
- ADDED: Connect using SSL
|
||||
- ADDED: Database connection dialog redesigned
|
||||
- ADDED: #63 Ctrl+Enter runs query
|
||||
- ADDED: Published dbgate NPM package
|
||||
- ADDED: SQL editor context menu
|
||||
- FIX: #62 - import, export executed from SNAP installs didn't work
|
||||
|
||||
### 3.9.5
|
||||
- Start point of changelog
|
||||
76
README.md
76
README.md
@@ -1,37 +1,46 @@
|
||||
[](https://www.npmjs.com/package/dbgate)
|
||||
[](https://snapcraft.io/dbgate)
|
||||
[](https://snapcraft.io/dbgate)
|
||||
[](https://github.com/prettier/prettier)
|
||||
[](https://paypal.me/JanProchazkaCz/30eur)
|
||||
[](https://www.npmjs.com/package/dbgate-api)
|
||||
|
||||
# DbGate - database administration tool
|
||||
|
||||
DbGate is fast and efficient database administration tool. It is focused to work with data (filtering, editing, master/detail views etc.)
|
||||
DbGate is fast and easy to use database manager. Works with MySQL, PostgreSQL and SQL Server.
|
||||
|
||||
**Try it online** - https://demo.dbgate.org - online demo application
|
||||
* Try it online - [demo.dbgate.org](https://demo.dbgate.org) - online demo application
|
||||
* Download application for Windows, Linux or Mac from [dbgate.org](https://dbgate.org/download/)
|
||||
* Run web version as [NPM package](https://www.npmjs.com/package/dbgate) or as [docker image](https://hub.docker.com/r/dbgate/dbgate)
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
* Support for Microsoft SQL Server, Postgre SQL, MySQL
|
||||
* Table data browsing - filtering, sorting, related columns using foreign keys
|
||||
* Connect to Microsoft SQL Server, Postgre SQL, MySQL
|
||||
* Table data editing, with SQL change script preview
|
||||
* Master/detail views
|
||||
* Query designer
|
||||
* Form view for comfortable work with tables with many columns
|
||||
* Charts
|
||||
* Browsing objects - tables, views, procedures, functions
|
||||
* Table data editing, with SQL change script preview
|
||||
* Explore tables, views, procedures, functions
|
||||
* SQL editor, execute SQL script, SQL code formatter, SQL code completion, SQL join wizard
|
||||
* Runs as application for Windows, Linux and Mac. Or in Docker container on server and in web Browser on client.
|
||||
* Import, export from/to CSV, Excel, JSON
|
||||
* Free table editor - quick table data editing (cleanup data after import/before export, prototype tables etc.)
|
||||
* Archives - backup your data in JSON files on local filesystem (or on DbGate server, when using web application)
|
||||
* Light and dark theme
|
||||
* Charts
|
||||
* For detailed info, how to run DbGate in docker container, visit [docker hub](https://hub.docker.com/r/dbgate/dbgate)
|
||||
* Extensible plugin architecture
|
||||
|
||||

|
||||
## Why is DbGate different
|
||||
There are many database managers now, so why DbGate?
|
||||
* Works everywhere - Windows, Linux, Mac, Web browser (+mobile web is planned), without compromises in features
|
||||
* Based on standalone NPM packages, scripts can be run without DbGate (example - [CSV export](https://www.npmjs.com/package/dbgate-plugin-csv) )
|
||||
* Many data browsing functions based using foreign keys - master/detail, expand columns, expandable form view (on screenshot above)
|
||||
|
||||
## Design goals
|
||||
* Application simplicity - DbGate takes the best and only the best from old [DbGate](http://www.jenasoft.com/dbgate), [DatAdmin](http://www.jenasoft.com/datadmin) and [DbMouse](http://www.jenasoft.com/dbmouse) .
|
||||
* Minimal dependencies
|
||||
* Frontend - React, styled-components, socket.io
|
||||
* Frontend - Svelte, socket.io
|
||||
* Backend - NodeJs, ExpressJs, socket.io, database connection drivers
|
||||
* JavaScript + TypeScript
|
||||
* App - electron
|
||||
@@ -41,36 +50,12 @@ DbGate is fast and efficient database administration tool. It is focused to work
|
||||
## Plugins
|
||||
Plugins are standard NPM packages published on [npmjs.com](https://www.npmjs.com).
|
||||
See all [existing DbGate plugins](https://www.npmjs.com/search?q=keywords:dbgateplugin).
|
||||
Visit [dbgate generator homepage](https://github.com/dbshell/generator-dbgate) to see, how to create your own plugin.
|
||||
Visit [dbgate generator homepage](https://github.com/dbgate/generator-dbgate) to see, how to create your own plugin.
|
||||
|
||||
Currently following extensions can be implemented using plugins:
|
||||
- File format parsers/writers
|
||||
- Database engine connectors
|
||||
|
||||
## How Can I Contribute?
|
||||
You're welcome to contribute to this project! Below are some ideas, how to contribute:
|
||||
|
||||
* Create plugins for new import/export formats or database engines
|
||||
* Bug fixing
|
||||
* Test Mac edition
|
||||
* Create unit tests
|
||||
* Whatever else
|
||||
|
||||
Any help is appreciated!
|
||||
|
||||
Feel free to report issues and open merge requests.
|
||||
|
||||
## Roadmap
|
||||
|
||||
| Feature | Complexity | Schedule |
|
||||
|---|---|---|
|
||||
| Table designer (structure editor) | big | february 2021 |
|
||||
| Support for SQLite | big | 2021 |
|
||||
| Filtering, sorting in free table editor | small | ??? |
|
||||
| Query designer | medium | december 2020 - done |
|
||||
| Using tedious driver instead of mssql | small | january 2021 - done |
|
||||
| Filter SQL result sets | small | november 2020 - done |
|
||||
|
||||
## How to run development environment
|
||||
|
||||
```sh
|
||||
@@ -85,7 +70,7 @@ yarn lib
|
||||
|
||||
Open http://localhost:5000 in your browser
|
||||
|
||||
You could run electron app, using this server:
|
||||
You could run electron app (requires running localhost:5000):
|
||||
```sh
|
||||
cd app
|
||||
yarn
|
||||
@@ -93,7 +78,7 @@ yarn start
|
||||
```
|
||||
|
||||
## How to run built electron app locally
|
||||
This mode is very similar to production run of electron app. Electron app forks process with API on dynamically allocated port, works with compiled javascript files.
|
||||
This mode is very similar to production run of electron app. Electron app forks process with API on dynamically allocated port, works with compiled javascript files (doesn't use localhost:5000)
|
||||
|
||||
```sh
|
||||
cd app
|
||||
@@ -109,13 +94,12 @@ yarn start:app:local
|
||||
## Packages
|
||||
Some dbgate packages can be used also without DbGate. You can find them on [NPM repository](https://www.npmjs.com/search?q=keywords:dbgate)
|
||||
|
||||
* [api](https://github.com/dbshell/dbgate/tree/master/packages/api) - backend, Javascript, ExpressJS [](https://www.npmjs.com/package/dbgate-api)
|
||||
* [datalib](https://github.com/dbshell/dbgate/tree/master/packages/datalib) - TypeScript library for utility classes
|
||||
* [app](https://github.com/dbshell/dbgate/tree/master/app) - application (JavaScript)
|
||||
structure, creating specific queries (JavaScript) [](https://www.npmjs.com/package/dbgate-engines)
|
||||
* [filterparser](https://github.com/dbshell/dbgate/tree/master/packages/filterparser) - TypeScript library for parsing data filter expressions using parsimmon
|
||||
* [sqltree](https://github.com/dbshell/dbgate/tree/master/packages/sqltree) - JSON representation of SQL query, functions converting to SQL (TypeScript) [](https://www.npmjs.com/package/dbgate-sqltree)
|
||||
* [types](https://github.com/dbshell/dbgate/tree/master/packages/types) - common TypeScript definitions [](https://www.npmjs.com/package/dbgate-types)
|
||||
* [web](https://github.com/dbshell/dbgate/tree/master/packages/web) - frontend in React (JavaScript)
|
||||
* [tools](https://github.com/dbshell/dbgate/tree/master/packages/tools) - various tools [](https://www.npmjs.com/package/dbgate-tools)
|
||||
* [api](https://github.com/dbgate/dbgate/tree/master/packages/api) - backend, Javascript, ExpressJS [](https://www.npmjs.com/package/dbgate-api)
|
||||
* [datalib](https://github.com/dbgate/dbgate/tree/master/packages/datalib) - TypeScript library for utility classes [](https://www.npmjs.com/package/dbgate-datalib)
|
||||
* [app](https://github.com/dbgate/dbgate/tree/master/app) - application (JavaScript) structure, creating specific queries (JavaScript)
|
||||
* [filterparser](https://github.com/dbgate/dbgate/tree/master/packages/filterparser) - TypeScript library for parsing data filter expressions using parsimmon [](https://www.npmjs.com/package/dbgate-filterparser)
|
||||
* [sqltree](https://github.com/dbgate/dbgate/tree/master/packages/sqltree) - JSON representation of SQL query, functions converting to SQL (TypeScript) [](https://www.npmjs.com/package/dbgate-sqltree)
|
||||
* [types](https://github.com/dbgate/dbgate/tree/master/packages/types) - common TypeScript definitions [](https://www.npmjs.com/package/dbgate-types)
|
||||
* [web](https://github.com/dbgate/dbgate/tree/master/packages/web) - frontend in Svelte (JavaScript) [](https://www.npmjs.com/package/dbgate-web)
|
||||
* [tools](https://github.com/dbgate/dbgate/tree/master/packages/tools) - various tools [](https://www.npmjs.com/package/dbgate-tools)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dbgate",
|
||||
"version": "3.9.1-beta.1",
|
||||
"version": "4.0.0",
|
||||
"private": true,
|
||||
"author": "Jan Prochazka <jenasoft.database@gmail.com>",
|
||||
"description": "Opensource database administration tool",
|
||||
@@ -11,7 +11,7 @@
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbshell/dbgate.git"
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"build": {
|
||||
"appId": "org.dbgate",
|
||||
@@ -37,6 +37,15 @@
|
||||
"github"
|
||||
]
|
||||
},
|
||||
"snap": {
|
||||
"publish": [
|
||||
"github",
|
||||
"snapStore"
|
||||
],
|
||||
"environment": {
|
||||
"ELECTRON_SNAP": "true"
|
||||
}
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
"nsis"
|
||||
@@ -59,19 +68,19 @@
|
||||
"start": "cross-env ELECTRON_START_URL=http://localhost:5000 electron .",
|
||||
"start:local": "cross-env electron .",
|
||||
"dist": "electron-builder",
|
||||
"build": "cd ../packages/api && yarn build && cd ../web && yarn build:app && cd ../../app && yarn dist",
|
||||
"build:local": "cd ../packages/api && yarn build && cd ../web && yarn build:app && cd ../../app && yarn predist",
|
||||
"build": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && yarn dist",
|
||||
"build:local": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && yarn predist",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"predist": "copyfiles ../packages/api/dist/* packages && copyfiles \"../packages/web/build/*\" packages && copyfiles \"../packages/web/build/**/*\" packages"
|
||||
"predist": "copyfiles ../packages/api/dist/* packages && copyfiles \"../packages/web/public/*\" packages && copyfiles \"../packages/web/public/**/*\" packages"
|
||||
},
|
||||
"main": "src/electron.js",
|
||||
"devDependencies": {
|
||||
"copyfiles": "^2.2.0",
|
||||
"cross-env": "^6.0.3",
|
||||
"electron": "11.1.1",
|
||||
"electron": "11.2.3",
|
||||
"electron-builder": "22.9.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"msnodesqlv8": "^2.0.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
const electron = require('electron');
|
||||
const os = require('os');
|
||||
const { Menu } = require('electron');
|
||||
const { Menu, ipcMain } = require('electron');
|
||||
const { fork } = require('child_process');
|
||||
const { autoUpdater } = require('electron-updater');
|
||||
const Store = require('electron-store');
|
||||
@@ -20,12 +20,15 @@ const store = new Store();
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
let mainWindow;
|
||||
let splashWindow;
|
||||
let mainMenu;
|
||||
|
||||
log.transports.file.level = 'debug';
|
||||
autoUpdater.logger = log;
|
||||
// TODO - create settings for this
|
||||
// appUpdater.channel = 'beta';
|
||||
|
||||
let commands = {};
|
||||
|
||||
function hideSplash() {
|
||||
if (splashWindow) {
|
||||
splashWindow.destroy();
|
||||
@@ -34,29 +37,48 @@ function hideSplash() {
|
||||
mainWindow.show();
|
||||
}
|
||||
|
||||
function commandItem(id) {
|
||||
const command = commands[id];
|
||||
return {
|
||||
id,
|
||||
label: command ? command.menuName || command.toolbarName || command.name : id,
|
||||
accelerator: command ? command.keyText : undefined,
|
||||
enabled: command ? command.enabled : false,
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_runCommand('${id}')`);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildMenu() {
|
||||
const template = [
|
||||
{
|
||||
label: 'File',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Connect to database',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_createNewConnection()`);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'New query',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_newQuery()`);
|
||||
},
|
||||
},
|
||||
commandItem('new.connection'),
|
||||
commandItem('file.open'),
|
||||
commandItem('group.save'),
|
||||
commandItem('group.saveAs'),
|
||||
{ type: 'separator' },
|
||||
{ role: 'close' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Edit',
|
||||
submenu: [{ role: 'copy' }, { role: 'paste' }],
|
||||
label: 'Window',
|
||||
submenu: [commandItem('new.query'), { type: 'separator' }, commandItem('tabs.closeAll'), { role: 'minimize' }],
|
||||
},
|
||||
|
||||
// {
|
||||
// label: 'Edit',
|
||||
// submenu: [
|
||||
// { role: 'undo' },
|
||||
// { role: 'redo' },
|
||||
// { type: 'separator' },
|
||||
// { role: 'cut' },
|
||||
// { role: 'copy' },
|
||||
// { role: 'paste' },
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
label: 'View',
|
||||
submenu: [
|
||||
@@ -69,20 +91,7 @@ function buildMenu() {
|
||||
{ role: 'zoomout' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'togglefullscreen' },
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'window',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Close all tabs',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript('dbgate_closeAll()');
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ role: 'minimize' },
|
||||
{ role: 'close' },
|
||||
commandItem('theme.changeTheme'),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -97,7 +106,7 @@ function buildMenu() {
|
||||
{
|
||||
label: 'DbGate on GitHub',
|
||||
click() {
|
||||
require('electron').shell.openExternal('https://github.com/dbshell/dbgate');
|
||||
require('electron').shell.openExternal('https://github.com/dbgate/dbgate');
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -107,11 +116,12 @@ function buildMenu() {
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'About',
|
||||
label: 'Report problem or feature request',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_showAbout()`);
|
||||
require('electron').shell.openExternal('https://github.com/dbgate/dbgate/issues/new');
|
||||
},
|
||||
},
|
||||
commandItem('about.show'),
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -119,6 +129,24 @@ function buildMenu() {
|
||||
return Menu.buildFromTemplate(template);
|
||||
}
|
||||
|
||||
ipcMain.on('update-commands', async (event, arg) => {
|
||||
commands = JSON.parse(arg);
|
||||
for (const key of Object.keys(commands)) {
|
||||
const menu = mainMenu.getMenuItemById(key);
|
||||
if (!menu) continue;
|
||||
const command = commands[key];
|
||||
|
||||
// rebuild menu
|
||||
if (menu.label != command.text || menu.accelerator != command.keyText) {
|
||||
mainMenu = buildMenu();
|
||||
mainWindow.setMenu(mainMenu);
|
||||
return;
|
||||
}
|
||||
|
||||
menu.enabled = command.enabled;
|
||||
}
|
||||
});
|
||||
|
||||
function createWindow() {
|
||||
const bounds = store.get('winBounds');
|
||||
|
||||
@@ -135,13 +163,14 @@ function createWindow() {
|
||||
},
|
||||
});
|
||||
|
||||
mainWindow.setMenu(buildMenu());
|
||||
mainMenu = buildMenu();
|
||||
mainWindow.setMenu(mainMenu);
|
||||
|
||||
function loadMainWindow() {
|
||||
const startUrl =
|
||||
process.env.ELECTRON_START_URL ||
|
||||
url.format({
|
||||
pathname: path.join(__dirname, '../packages/web/build/index.html'),
|
||||
pathname: path.join(__dirname, '../packages/web/public/index.html'),
|
||||
protocol: 'file:',
|
||||
slashes: true,
|
||||
});
|
||||
|
||||
@@ -717,10 +717,10 @@ electron-updater@^4.3.5:
|
||||
lodash.isequal "^4.5.0"
|
||||
semver "^7.3.2"
|
||||
|
||||
electron@11.1.1:
|
||||
version "11.1.1"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-11.1.1.tgz#188f036f8282798398dca9513e9bb3b10213e3aa"
|
||||
integrity sha512-tlbex3xosJgfileN6BAQRotevPRXB/wQIq48QeQ08tUJJrXwE72c8smsM/hbHx5eDgnbfJ2G3a60PmRjHU2NhA==
|
||||
electron@11.2.3:
|
||||
version "11.2.3"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-11.2.3.tgz#8ad1d9858436cfca0e2e5ea7fea326794ae58ebb"
|
||||
integrity sha512-6yxOc42nDAptHKNlUG/vcOh2GI9x2fqp2nQbZO0/3sz2CrwsJkwR3i3oMN9XhVJaqI7GK1vSCJz0verOkWlXcQ==
|
||||
dependencies:
|
||||
"@electron/get" "^1.0.1"
|
||||
"@types/node" "^12.0.12"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "4.0.3-beta.1",
|
||||
"name": "dbgate-all",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
@@ -8,7 +9,7 @@
|
||||
"start:api": "yarn workspace dbgate-api start",
|
||||
"start:api:portal": "yarn workspace dbgate-api start:portal",
|
||||
"start:api:covid": "yarn workspace dbgate-api start:covid",
|
||||
"start:web": "yarn workspace dbgate-web start",
|
||||
"start:web": "yarn workspace dbgate-web dev",
|
||||
"start:sqltree": "yarn workspace dbgate-sqltree start",
|
||||
"start:tools": "yarn workspace dbgate-tools start",
|
||||
"start:datalib": "yarn workspace dbgate-datalib start",
|
||||
@@ -20,7 +21,7 @@
|
||||
"build:lib": "yarn build:tools && yarn build:sqltree && yarn build:filterparser && yarn build:datalib",
|
||||
"build:app": "cd app && yarn install && yarn build",
|
||||
"build:api": "yarn workspace dbgate-api build",
|
||||
"build:web:docker": "yarn workspace dbgate-web build:docker",
|
||||
"build:web:docker": "yarn workspace dbgate-web build",
|
||||
"build:app:local": "cd app && yarn build:local",
|
||||
"start:app:local": "cd app && yarn start:local",
|
||||
"setCurrentVersion": "node setCurrentVersion",
|
||||
@@ -28,7 +29,7 @@
|
||||
"fillNativeModules": "node fillNativeModules",
|
||||
"fillNativeModulesElectron": "node fillNativeModules --eletron",
|
||||
"prettier": "prettier --write packages/api/src && prettier --write packages/datalib/src && prettier --write packages/filterparser/src && prettier --write packages/sqltree/src && prettier --write packages/tools/src && prettier --write packages/types && prettier --write packages/web/src && prettier --write app/src",
|
||||
"copy:docker:build": "copyfiles packages/api/dist/* docker -f && copyfiles packages/web/build/* docker -u 2 && copyfiles \"packages/web/build/**/*\" docker -u 2",
|
||||
"copy:docker:build": "copyfiles packages/api/dist/* docker -f && copyfiles packages/web/public/* docker -u 2 && copyfiles \"packages/web/public/**/*\" docker -u 2",
|
||||
"prepare:docker": "yarn build:web:docker && yarn build:api && yarn copy:docker:build",
|
||||
"prepare": "yarn build:lib",
|
||||
"start": "concurrently --kill-others-on-fail \"yarn start:api\" \"yarn start:web\"",
|
||||
|
||||
@@ -5,19 +5,11 @@ SERVER_mysql=localhost
|
||||
USER_mysql=root
|
||||
PASSWORD_mysql=test
|
||||
PORT_mysql=3307
|
||||
ENGINE_mysql=mysql
|
||||
ENGINE_mysql=mysql@dbgate-plugin-mysql
|
||||
|
||||
LABEL_postgres=Postgres localhost
|
||||
SERVER_postgres=localhost
|
||||
USER_postgres=postgres
|
||||
PASSWORD_postgres=test
|
||||
PORT_postgres=5433
|
||||
ENGINE_postgres=postgres
|
||||
|
||||
TOOLBAR=home
|
||||
ICON_home=mdi mdi-home
|
||||
TITLE_home=Home
|
||||
PAGE_home=home.html
|
||||
STARTUP_PAGES=home
|
||||
|
||||
PAGES_DIRECTORY=/home/jena/jenasoft/dbgate-web/pages
|
||||
ENGINE_postgres=postgres@dbgate-plugin-postgres
|
||||
|
||||
@@ -32,7 +32,7 @@ dbgateApi.runScript(run);
|
||||
|
||||
```
|
||||
|
||||
Silly example, runs without any dependencies. Copy [fakeObjectReader](https://github.com/dbshell/dbgate/blob/master/packages/api/src/shell/fakeObjectReader.js) to [consoleObjectWriter](https://github.com/dbshell/dbgate/blob/master/packages/api/src/shell/consoleObjectWriter.js) .
|
||||
Silly example, runs without any dependencies. Copy [fakeObjectReader](https://github.com/dbgate/dbgate/blob/master/packages/api/src/shell/fakeObjectReader.js) to [consoleObjectWriter](https://github.com/dbgate/dbgate/blob/master/packages/api/src/shell/consoleObjectWriter.js) .
|
||||
|
||||
```javascript
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "dbgate-api",
|
||||
"main": "src/index.js",
|
||||
"version": "1.0.7",
|
||||
"version": "4.0.0",
|
||||
"homepage": "https://dbgate.org/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbshell/dbgate.git"
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
|
||||
"author": "Jan Prochazka",
|
||||
"license": "GPL",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"sql",
|
||||
"json",
|
||||
@@ -26,8 +26,8 @@
|
||||
"compare-versions": "^3.6.0",
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^6.0.3",
|
||||
"dbgate-sqltree": "^1.0.0",
|
||||
"dbgate-tools": "^1.0.0",
|
||||
"dbgate-sqltree": "^4.0.0",
|
||||
"dbgate-tools": "^4.0.0",
|
||||
"eslint": "^6.8.0",
|
||||
"express": "^4.17.1",
|
||||
"express-basic-auth": "^1.2.0",
|
||||
@@ -35,17 +35,21 @@
|
||||
"find-free-port": "^2.0.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"http": "^0.0.0",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
"line-reader": "^0.4.0",
|
||||
"lodash": "^4.17.15",
|
||||
"ncp": "^2.0.0",
|
||||
"nedb-promises": "^4.0.1",
|
||||
"node-cron": "^2.0.3",
|
||||
"node-ssh-forward": "^0.7.2",
|
||||
"portfinder": "^1.0.28",
|
||||
"simple-encryptor": "^4.0.0",
|
||||
"socket.io": "^2.3.0",
|
||||
"tar": "^6.0.5",
|
||||
"uuid": "^3.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "nodemon src/index.js",
|
||||
"start": "node src/index.js",
|
||||
"start:portal": "env-cmd nodemon src/index.js",
|
||||
"start:covid": "env-cmd -f .covid-env nodemon src/index.js",
|
||||
"ts": "tsc",
|
||||
@@ -53,7 +57,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.14.149",
|
||||
"dbgate-types": "^1.0.0",
|
||||
"dbgate-types": "^4.0.0",
|
||||
"env-cmd": "^10.1.0",
|
||||
"node-loader": "^1.0.2",
|
||||
"nodemon": "^2.0.2",
|
||||
@@ -64,4 +68,4 @@
|
||||
"optionalDependencies": {
|
||||
"msnodesqlv8": "^2.0.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
const currentVersion = require('../currentVersion');
|
||||
const platformInfo = require('../utility/platformInfo');
|
||||
|
||||
module.exports = {
|
||||
get_meta: 'get',
|
||||
@@ -31,4 +32,10 @@ module.exports = {
|
||||
...currentVersion,
|
||||
};
|
||||
},
|
||||
|
||||
platformInfo_meta: 'get',
|
||||
async platformInfo() {
|
||||
return platformInfo;
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ const nedb = require('nedb-promises');
|
||||
const { datadir } = require('../utility/directories');
|
||||
const socket = require('../utility/socket');
|
||||
const { encryptConnection } = require('../utility/crypting');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
|
||||
function getPortalCollections() {
|
||||
if (process.env.CONNECTIONS) {
|
||||
@@ -47,6 +48,7 @@ module.exports = {
|
||||
test(req, res) {
|
||||
const subprocess = fork(process.argv[1], ['connectProcess', ...process.argv.slice(3)]);
|
||||
subprocess.on('message', resp => {
|
||||
if (handleProcessCommunication(resp, subprocess)) return;
|
||||
// @ts-ignore
|
||||
const { msgtype } = resp;
|
||||
if (msgtype == 'connected' || msgtype == 'error') {
|
||||
|
||||
@@ -3,6 +3,7 @@ const connections = require('./connections');
|
||||
const socket = require('../utility/socket');
|
||||
const { fork } = require('child_process');
|
||||
const { DatabaseAnalyser } = require('dbgate-tools');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
|
||||
module.exports = {
|
||||
/** @type {import('dbgate-types').OpenedDatabaseConnection[]} */
|
||||
@@ -50,8 +51,10 @@ module.exports = {
|
||||
status: { name: 'pending' },
|
||||
};
|
||||
this.opened.push(newOpened);
|
||||
// @ts-ignore
|
||||
subprocess.on('message', ({ msgtype, ...message }) => {
|
||||
subprocess.on('message', message => {
|
||||
// @ts-ignore
|
||||
const { msgtype } = message;
|
||||
if (handleProcessCommunication(message, subprocess)) return;
|
||||
if (newOpened.disconnected) return;
|
||||
this[`handle_${msgtype}`](conid, database, message);
|
||||
});
|
||||
@@ -89,6 +92,13 @@ module.exports = {
|
||||
return res;
|
||||
},
|
||||
|
||||
collectionData_meta: 'post',
|
||||
async collectionData({ conid, database, options }) {
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(opened, { msgtype: 'collectionData', options });
|
||||
return res.result;
|
||||
},
|
||||
|
||||
status_meta: 'get',
|
||||
async status({ conid, database }) {
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
@@ -103,10 +113,14 @@ module.exports = {
|
||||
|
||||
ping_meta: 'post',
|
||||
async ping({ conid, database }) {
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
let existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
|
||||
if (existing) {
|
||||
existing.subprocess.send({ msgtype: 'ping' });
|
||||
} else {
|
||||
existing = await this.ensureOpened(conid, database);
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
connectionStatus: existing ? existing.status : null,
|
||||
@@ -150,6 +164,16 @@ module.exports = {
|
||||
// };
|
||||
},
|
||||
|
||||
sqlPreview_meta: 'post',
|
||||
async sqlPreview({ conid, database, objects, options }) {
|
||||
// wait for structure
|
||||
await this.structure({ conid, database });
|
||||
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(opened, { msgtype: 'sqlPreview', objects, options });
|
||||
return res;
|
||||
},
|
||||
|
||||
// runCommand_meta: 'post',
|
||||
// async runCommand({ conid, database, sql }) {
|
||||
// console.log(`Running SQL command , conid=${conid}, database=${database}, sql=${sql}`);
|
||||
|
||||
@@ -78,6 +78,11 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
|
||||
saveAs_meta: 'post',
|
||||
async saveAs({ filePath, data, format }) {
|
||||
await fs.writeFile(filePath, serialize(format, data));
|
||||
},
|
||||
|
||||
favorites_meta: 'get',
|
||||
async favorites() {
|
||||
if (!hasPermission(`files/favorites/read`)) return [];
|
||||
|
||||
@@ -121,7 +121,13 @@ module.exports = {
|
||||
getStats_meta: 'get',
|
||||
getStats({ jslid }) {
|
||||
const file = `${getJslFileName(jslid)}.stats`;
|
||||
if (fs.existsSync(file)) return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||
if (fs.existsSync(file)) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
},
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ module.exports = {
|
||||
listObjects_meta: 'get',
|
||||
async listObjects({ conid, database }) {
|
||||
const opened = await databaseConnections.ensureOpened(conid, database);
|
||||
const types = ['tables', 'views', 'procedures', 'functions', 'triggers'];
|
||||
const types = ['tables', 'collections', 'views', 'procedures', 'functions', 'triggers'];
|
||||
return types.reduce(
|
||||
(res, type) => ({
|
||||
...res,
|
||||
|
||||
@@ -28,9 +28,9 @@ const hasPermission = require('../utility/hasPermission');
|
||||
// }
|
||||
|
||||
const preinstallPluginMinimalVersions = {
|
||||
'dbgate-plugin-mssql': '1.0.10',
|
||||
'dbgate-plugin-mysql': '1.0.3',
|
||||
'dbgate-plugin-postgres': '1.0.2',
|
||||
'dbgate-plugin-mssql': '1.2.1',
|
||||
'dbgate-plugin-mysql': '1.2.1',
|
||||
'dbgate-plugin-postgres': '1.2.1',
|
||||
'dbgate-plugin-csv': '1.0.8',
|
||||
'dbgate-plugin-excel': '1.0.6',
|
||||
};
|
||||
@@ -149,7 +149,7 @@ module.exports = {
|
||||
return content.commands[command](args);
|
||||
},
|
||||
|
||||
authTypes_meta: 'post',
|
||||
authTypes_meta: 'get',
|
||||
async authTypes({ engine }) {
|
||||
const packageName = extractPackageName(engine);
|
||||
const content = requirePlugin(packageName);
|
||||
|
||||
@@ -7,6 +7,7 @@ const socket = require('../utility/socket');
|
||||
const { fork } = require('child_process');
|
||||
const { rundir, uploadsdir, pluginsdir } = require('../utility/directories');
|
||||
const { extractShellApiPlugins, extractShellApiFunctionName } = require('dbgate-tools');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
|
||||
function extractPlugins(script) {
|
||||
const requireRegex = /\s*\/\/\s*@require\s+([^\s]+)\s*\n/g;
|
||||
@@ -23,6 +24,7 @@ const requirePluginsTemplate = plugins =>
|
||||
|
||||
const scriptTemplate = script => `
|
||||
const dbgateApi = require(process.env.DBGATE_API);
|
||||
dbgateApi.initializeApiEnvironment();
|
||||
${requirePluginsTemplate(extractPlugins(script))}
|
||||
require=null;
|
||||
async function run() {
|
||||
@@ -35,6 +37,7 @@ dbgateApi.runScript(run);
|
||||
|
||||
const loaderScriptTemplate = (functionName, props, runid) => `
|
||||
const dbgateApi = require(process.env.DBGATE_API);
|
||||
dbgateApi.initializeApiEnvironment();
|
||||
${requirePluginsTemplate(extractShellApiPlugins(functionName, props))}
|
||||
require=null;
|
||||
async function run() {
|
||||
@@ -96,7 +99,8 @@ module.exports = {
|
||||
cwd: directory,
|
||||
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
||||
env: {
|
||||
DBGATE_API: process.argv[1],
|
||||
...process.env,
|
||||
DBGATE_API: global['dbgateApiModulePath'] || process.argv[1],
|
||||
..._.fromPairs(pluginNames.map(name => [`PLUGIN_${_.camelCase(name)}`, path.join(pluginsdir(), name)])),
|
||||
},
|
||||
});
|
||||
@@ -123,8 +127,10 @@ module.exports = {
|
||||
subprocess,
|
||||
};
|
||||
this.opened.push(newOpened);
|
||||
// @ts-ignore
|
||||
subprocess.on('message', ({ msgtype, ...message }) => {
|
||||
subprocess.on('message', message => {
|
||||
// @ts-ignore
|
||||
const { msgtype } = message;
|
||||
if (handleProcessCommunication(message, subprocess)) return;
|
||||
this[`handle_${msgtype}`](runid, message);
|
||||
});
|
||||
return newOpened;
|
||||
|
||||
@@ -3,11 +3,13 @@ const socket = require('../utility/socket');
|
||||
const { fork } = require('child_process');
|
||||
const _ = require('lodash');
|
||||
const AsyncLock = require('async-lock');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
const lock = new AsyncLock();
|
||||
|
||||
module.exports = {
|
||||
opened: [],
|
||||
closed: {},
|
||||
lastPinged: {},
|
||||
|
||||
handle_databases(conid, { databases }) {
|
||||
const existing = this.opened.find(x => x.conid == conid);
|
||||
@@ -42,8 +44,10 @@ module.exports = {
|
||||
this.opened.push(newOpened);
|
||||
delete this.closed[conid];
|
||||
socket.emitChanged(`server-status-changed`);
|
||||
// @ts-ignore
|
||||
subprocess.on('message', ({ msgtype, ...message }) => {
|
||||
subprocess.on('message', message => {
|
||||
// @ts-ignore
|
||||
const { msgtype } = message;
|
||||
if (handleProcessCommunication(message, subprocess)) return;
|
||||
if (newOpened.disconnected) return;
|
||||
this[`handle_${msgtype}`](conid, message);
|
||||
});
|
||||
@@ -88,7 +92,12 @@ module.exports = {
|
||||
ping_meta: 'post',
|
||||
async ping({ connections }) {
|
||||
await Promise.all(
|
||||
connections.map(async conid => {
|
||||
_.uniq(connections).map(async conid => {
|
||||
const last = this.lastPinged[conid];
|
||||
if (last && new Date().getTime() - last < 30 * 1000) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
this.lastPinged[conid] = new Date().getTime();
|
||||
const opened = await this.ensureOpened(conid);
|
||||
opened.subprocess.send({ msgtype: 'ping' });
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ const connections = require('./connections');
|
||||
const socket = require('../utility/socket');
|
||||
const { fork } = require('child_process');
|
||||
const jsldata = require('./jsldata');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
|
||||
module.exports = {
|
||||
/** @type {import('dbgate-types').OpenedSession[]} */
|
||||
@@ -73,8 +74,10 @@ module.exports = {
|
||||
sesid,
|
||||
};
|
||||
this.opened.push(newOpened);
|
||||
// @ts-ignore
|
||||
subprocess.on('message', ({ msgtype, ...message }) => {
|
||||
subprocess.on('message', message => {
|
||||
// @ts-ignore
|
||||
const { msgtype } = message;
|
||||
if (handleProcessCommunication(message, subprocess)) return;
|
||||
this[`handle_${msgtype}`](sesid, message);
|
||||
});
|
||||
subprocess.send({ msgtype: 'connect', ...connection, database });
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
version: '3.8.6',
|
||||
buildTime: '2020-12-10T11:14:01.053Z',
|
||||
|
||||
module.exports = {
|
||||
version: '4.0.0',
|
||||
buildTime: '2021-04-01T10:48:22.253Z'
|
||||
};
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
const shell = require('./shell');
|
||||
// require('socket.io-client');
|
||||
|
||||
// "socket.io-client": "^2.3.0",
|
||||
// "utf-8-validate": "^5.0.2",
|
||||
// "uuid": "^3.4.0",
|
||||
// "uws": "10.148.1"
|
||||
|
||||
const argument = process.argv[2];
|
||||
if (argument && argument.endsWith('Process')) {
|
||||
@@ -18,4 +12,7 @@ if (argument && argument.endsWith('Process')) {
|
||||
main.start(argument);
|
||||
}
|
||||
|
||||
module.exports = shell;
|
||||
module.exports = {
|
||||
...shell,
|
||||
getMainModule: () => require('./main'),
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ const io = require('socket.io');
|
||||
const fs = require('fs');
|
||||
const findFreePort = require('find-free-port');
|
||||
const childProcessChecker = require('./utility/childProcessChecker');
|
||||
const path = require('path');
|
||||
|
||||
const useController = require('./utility/useController');
|
||||
const socket = require('./utility/socket');
|
||||
@@ -78,13 +79,15 @@ function start(argument = null) {
|
||||
|
||||
app.use('/runners/data', express.static(rundir()));
|
||||
|
||||
if (fs.existsSync('/home/dbgate-docker/build')) {
|
||||
if (fs.existsSync('/home/dbgate-docker/public')) {
|
||||
// server static files inside docker container
|
||||
app.use(express.static('/home/dbgate-docker/build'));
|
||||
app.use(express.static('/home/dbgate-docker/public'));
|
||||
} else {
|
||||
app.get('/', (req, res) => {
|
||||
res.send('DbGate API');
|
||||
});
|
||||
if (argument != 'startNodeWeb') {
|
||||
app.get('/', (req, res) => {
|
||||
res.send('DbGate API');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (argument == '--dynport') {
|
||||
@@ -96,6 +99,13 @@ function start(argument = null) {
|
||||
process.send({ msgtype: 'listening', port });
|
||||
});
|
||||
});
|
||||
} else if (argument == 'startNodeWeb') {
|
||||
app.use(express.static(path.join(__dirname, '../../dbgate-web/public')));
|
||||
findFreePort(5000, function (err, port) {
|
||||
server.listen(port, () => {
|
||||
console.log(`DbGate API listening on port ${port}`);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
server.listen(3000);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
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');
|
||||
|
||||
function start() {
|
||||
childProcessChecker();
|
||||
process.on('message', async connection => {
|
||||
if (handleProcessCommunication(connection)) return;
|
||||
try {
|
||||
const driver = requireEngineDriver(connection);
|
||||
const conn = await driver.connect(decryptConnection(connection));
|
||||
const conn = await connectUtility(driver, connection);
|
||||
const res = await driver.getVersion(conn);
|
||||
process.send({ msgtype: 'connected', ...res });
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
const stableStringify = require('json-stable-stringify');
|
||||
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');
|
||||
const { SqlGenerator } = require('dbgate-tools');
|
||||
|
||||
let systemConnection;
|
||||
let storedConnection;
|
||||
@@ -60,7 +62,7 @@ async function handleConnect({ connection, structure }) {
|
||||
|
||||
if (!structure) setStatusName('pending');
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
systemConnection = await checkedAsyncCall(driver.connect(decryptConnection(storedConnection)));
|
||||
systemConnection = await checkedAsyncCall(connectUtility(driver, storedConnection));
|
||||
if (structure) {
|
||||
analysedStructure = structure;
|
||||
handleIncrementalRefresh();
|
||||
@@ -92,6 +94,38 @@ async function handleQueryData({ msgid, sql }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCollectionData({ msgid, options }) {
|
||||
await waitConnected();
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
try {
|
||||
const result = await driver.readCollection(systemConnection, options);
|
||||
process.send({ msgtype: 'response', msgid, result });
|
||||
} catch (err) {
|
||||
process.send({ msgtype: 'response', msgid, errorMessage: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSqlPreview({ msgid, objects, options }) {
|
||||
await waitConnected();
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
|
||||
try {
|
||||
const dmp = driver.createDumper();
|
||||
const generator = new SqlGenerator(analysedStructure, options, objects, dmp, driver, systemConnection);
|
||||
|
||||
await generator.dump();
|
||||
process.send({ msgtype: 'response', msgid, sql: dmp.s, isTruncated: generator.isTruncated });
|
||||
if (generator.isUnhandledException) {
|
||||
setTimeout(() => {
|
||||
console.log('Exiting because of unhandled exception');
|
||||
process.exit(0);
|
||||
}, 500);
|
||||
}
|
||||
} catch (err) {
|
||||
process.send({ msgtype: 'response', msgid, isError: true, errorMessage: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
// async function handleRunCommand({ msgid, sql }) {
|
||||
// await waitConnected();
|
||||
// const driver = engines(storedConnection);
|
||||
@@ -106,6 +140,8 @@ function handlePing() {
|
||||
const messageHandlers = {
|
||||
connect: handleConnect,
|
||||
queryData: handleQueryData,
|
||||
collectionData: handleCollectionData,
|
||||
sqlPreview: handleSqlPreview,
|
||||
ping: handlePing,
|
||||
// runCommand: handleRunCommand,
|
||||
};
|
||||
@@ -127,6 +163,7 @@ function start() {
|
||||
}, 60 * 1000);
|
||||
|
||||
process.on('message', async message => {
|
||||
if (handleProcessCommunication(message)) return;
|
||||
try {
|
||||
await handleMessage(message);
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const childProcessChecker = require('../utility/childProcessChecker');
|
||||
const JsonLinesDatastore = require('../utility/JsonLinesDatastore');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
|
||||
let lastPing = null;
|
||||
let datastore = new JsonLinesDatastore();
|
||||
@@ -47,6 +48,7 @@ function start() {
|
||||
}, 60 * 1000);
|
||||
|
||||
process.on('message', async message => {
|
||||
if (handleProcessCommunication(message)) return;
|
||||
try {
|
||||
await handleMessage(message);
|
||||
} catch (e) {
|
||||
|
||||
@@ -2,6 +2,8 @@ const stableStringify = require('json-stable-stringify');
|
||||
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');
|
||||
|
||||
let systemConnection;
|
||||
let storedConnection;
|
||||
@@ -48,7 +50,7 @@ async function handleConnect(connection) {
|
||||
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
try {
|
||||
systemConnection = await driver.connect(decryptConnection(storedConnection));
|
||||
systemConnection = await connectUtility(driver, storedConnection);
|
||||
handleRefresh();
|
||||
setInterval(handleRefresh, 30 * 1000);
|
||||
} catch (err) {
|
||||
@@ -67,7 +69,7 @@ function handlePing() {
|
||||
|
||||
async function handleCreateDatabase({ name }) {
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
systemConnection = await driver.connect(decryptConnection(storedConnection));
|
||||
systemConnection = await connectUtility(driver, storedConnection);
|
||||
console.log(`RUNNING SCRIPT: CREATE DATABASE ${driver.dialect.quoteIdentifier(name)}`);
|
||||
await driver.query(systemConnection, `CREATE DATABASE ${driver.dialect.quoteIdentifier(name)}`);
|
||||
await handleRefresh();
|
||||
@@ -96,6 +98,7 @@ function start() {
|
||||
}, 60 * 1000);
|
||||
|
||||
process.on('message', async message => {
|
||||
if (handleProcessCommunication(message)) return;
|
||||
try {
|
||||
await handleMessage(message);
|
||||
} catch (err) {
|
||||
|
||||
@@ -8,6 +8,8 @@ const goSplit = require('../utility/goSplit');
|
||||
const { jsldir } = require('../utility/directories');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const { decryptConnection } = require('../utility/crypting');
|
||||
const connectUtility = require('../utility/connectUtility');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
|
||||
let systemConnection;
|
||||
let storedConnection;
|
||||
@@ -131,7 +133,7 @@ async function handleConnect(connection) {
|
||||
storedConnection = connection;
|
||||
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
systemConnection = await driver.connect(decryptConnection(storedConnection));
|
||||
systemConnection = await connectUtility(driver, storedConnection);
|
||||
for (const [resolve] of afterConnectCallbacks) {
|
||||
resolve();
|
||||
}
|
||||
@@ -182,6 +184,7 @@ async function handleMessage({ msgtype, ...other }) {
|
||||
function start() {
|
||||
childProcessChecker();
|
||||
process.on('message', async message => {
|
||||
if (handleProcessCommunication(message)) return;
|
||||
try {
|
||||
await handleMessage(message);
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
const goSplit = require('../utility/goSplit');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const { decryptConnection } = require('../utility/crypting');
|
||||
const connectUtility = require('../utility/connectUtility');
|
||||
|
||||
async function executeQuery({ connection, sql }) {
|
||||
console.log(`Execute query ${sql}`);
|
||||
|
||||
const driver = requireEngineDriver(connection);
|
||||
const pool = await driver.connect(decryptConnection(connection));
|
||||
const pool = await connectUtility(driver, connection);
|
||||
console.log(`Connected.`);
|
||||
|
||||
for (const sqlItem of goSplit(sql)) {
|
||||
|
||||
@@ -17,6 +17,7 @@ const requirePlugin = require('./requirePlugin');
|
||||
const download = require('./download');
|
||||
const executeQuery = require('./executeQuery');
|
||||
const loadFile = require('./loadFile');
|
||||
const initializeApiEnvironment = require('./initializeApiEnvironment');
|
||||
|
||||
const dbgateApi = {
|
||||
queryReader,
|
||||
@@ -37,6 +38,7 @@ const dbgateApi = {
|
||||
registerPlugins,
|
||||
executeQuery,
|
||||
loadFile,
|
||||
initializeApiEnvironment,
|
||||
};
|
||||
|
||||
requirePlugin.initializeDbgateApi(dbgateApi);
|
||||
|
||||
9
packages/api/src/shell/initializeApiEnvironment.js
Normal file
9
packages/api/src/shell/initializeApiEnvironment.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
|
||||
async function initializeApiEnvironment() {
|
||||
process.on('message', async message => {
|
||||
handleProcessCommunication(message);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = initializeApiEnvironment;
|
||||
@@ -1,11 +1,12 @@
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const { decryptConnection } = require('../utility/crypting');
|
||||
const connectUtility = require('../utility/connectUtility');
|
||||
|
||||
async function queryReader({ connection, sql }) {
|
||||
console.log(`Reading query ${sql}`);
|
||||
|
||||
const driver = requireEngineDriver(connection);
|
||||
const pool = await driver.connect(decryptConnection(connection));
|
||||
const pool = await connectUtility(driver, connection);
|
||||
console.log(`Connected.`);
|
||||
return await driver.readQuery(pool, sql);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
const { quoteFullName, fullNameToString } = require('dbgate-tools');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const { decryptConnection } = require('../utility/crypting');
|
||||
const connectUtility = require('../utility/connectUtility');
|
||||
|
||||
async function tableReader({ connection, pureName, schemaName }) {
|
||||
const driver = requireEngineDriver(connection);
|
||||
const pool = await driver.connect(decryptConnection(connection));
|
||||
const pool = await connectUtility(driver, connection);
|
||||
console.log(`Connected.`);
|
||||
|
||||
const fullName = { pureName, schemaName };
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
const { fullNameToString } = require('dbgate-tools');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const { decryptConnection } = require('../utility/crypting');
|
||||
const connectUtility = require('../utility/connectUtility');
|
||||
|
||||
async function tableWriter({ connection, schemaName, pureName, ...options }) {
|
||||
console.log(`Writing table ${fullNameToString({ schemaName, pureName })}`);
|
||||
|
||||
const driver = requireEngineDriver(connection);
|
||||
const pool = await driver.connect(decryptConnection(connection));
|
||||
const pool = await connectUtility(driver, connection);
|
||||
console.log(`Connected.`);
|
||||
return await driver.writeTable(pool, { schemaName, pureName }, options);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const { fork } = require('child_process');
|
||||
const uuidv1 = require('uuid/v1');
|
||||
const { handleProcessCommunication } = require('./processComm');
|
||||
|
||||
class DatastoreProxy {
|
||||
constructor(file) {
|
||||
@@ -30,8 +31,11 @@ class DatastoreProxy {
|
||||
if (!this.subprocess) {
|
||||
this.subprocess = fork(process.argv[1], ['jslDatastoreProcess', ...process.argv.slice(3)]);
|
||||
|
||||
// @ts-ignore
|
||||
this.subprocess.on('message', ({ msgtype, ...message }) => {
|
||||
this.subprocess.on('message', message => {
|
||||
// @ts-ignore
|
||||
const { msgtype } = message;
|
||||
if (handleProcessCommunication(message, this.subprocess)) return;
|
||||
|
||||
// if (this.disconnected) return;
|
||||
this[`handle_${msgtype}`](message);
|
||||
});
|
||||
|
||||
59
packages/api/src/utility/connectUtility.js
Normal file
59
packages/api/src/utility/connectUtility.js
Normal file
@@ -0,0 +1,59 @@
|
||||
const { SSHConnection } = require('node-ssh-forward');
|
||||
const portfinder = require('portfinder');
|
||||
const fs = require('fs-extra');
|
||||
const { decryptConnection } = require('./crypting');
|
||||
const { getSshTunnel } = require('./sshTunnel');
|
||||
const { getSshTunnelProxy } = require('./sshTunnelProxy');
|
||||
|
||||
async function connectUtility(driver, storedConnection) {
|
||||
const connection = {
|
||||
...decryptConnection(storedConnection),
|
||||
};
|
||||
|
||||
if (!connection.port && driver.defaultPort) connection.port = driver.defaultPort.toString();
|
||||
|
||||
if (connection.useSshTunnel) {
|
||||
const tunnel = await getSshTunnelProxy(connection);
|
||||
if (tunnel.state == 'error') {
|
||||
throw new Error(tunnel.message);
|
||||
}
|
||||
|
||||
connection.server = '127.0.0.1';
|
||||
connection.port = tunnel.localPort;
|
||||
}
|
||||
|
||||
// SSL functionality - copied from https://github.com/beekeeper-studio/beekeeper-studio
|
||||
if (connection.useSsl) {
|
||||
connection.ssl = {};
|
||||
|
||||
if (connection.sslCaFile) {
|
||||
connection.ssl.ca = await fs.readFile(connection.sslCaFile);
|
||||
}
|
||||
|
||||
if (connection.sslCertFile) {
|
||||
connection.ssl.cert = await fs.readFile(connection.sslCertFile);
|
||||
}
|
||||
|
||||
if (connection.sslKeyFile) {
|
||||
connection.ssl.key = await fs.readFile(connection.sslKeyFile);
|
||||
}
|
||||
|
||||
if (!connection.ssl.key && !connection.ssl.ca && !connection.ssl.cert) {
|
||||
// TODO: provide this as an option in settings
|
||||
// or per-connection as 'reject self-signed certs'
|
||||
// How it works:
|
||||
// if false, cert can be self-signed
|
||||
// if true, has to be from a public CA
|
||||
// Heroku certs are self-signed.
|
||||
// if you provide ca/cert/key files, it overrides this
|
||||
connection.ssl.rejectUnauthorized = false;
|
||||
} else {
|
||||
connection.ssl.rejectUnauthorized = connection.sslRejectUnauthorized;
|
||||
}
|
||||
}
|
||||
|
||||
const conn = await driver.connect(connection);
|
||||
return conn;
|
||||
}
|
||||
|
||||
module.exports = connectUtility;
|
||||
@@ -42,28 +42,42 @@ function getEncryptor() {
|
||||
return _encryptor;
|
||||
}
|
||||
|
||||
function encryptConnection(connection) {
|
||||
function encryptPasswordField(connection, field) {
|
||||
if (
|
||||
connection &&
|
||||
connection.password &&
|
||||
!connection.password.startsWith('crypt:') &&
|
||||
connection[field] &&
|
||||
!connection[field].startsWith('crypt:') &&
|
||||
connection.passwordMode != 'saveRaw'
|
||||
) {
|
||||
return {
|
||||
...connection,
|
||||
password: 'crypt:' + getEncryptor().encrypt(connection.password),
|
||||
[field]: 'crypt:' + getEncryptor().encrypt(connection[field]),
|
||||
};
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
function decryptPasswordField(connection, field) {
|
||||
if (connection && connection[field] && connection[field].startsWith('crypt:')) {
|
||||
return {
|
||||
...connection,
|
||||
[field]: getEncryptor().decrypt(connection[field].substring('crypt:'.length)),
|
||||
};
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
function encryptConnection(connection) {
|
||||
connection = encryptPasswordField(connection, 'password');
|
||||
connection = encryptPasswordField(connection, 'sshPassword');
|
||||
connection = encryptPasswordField(connection, 'sshKeyFilePassword');
|
||||
return connection;
|
||||
}
|
||||
|
||||
function decryptConnection(connection) {
|
||||
if (connection && connection.password && connection.password.startsWith('crypt:')) {
|
||||
return {
|
||||
...connection,
|
||||
password: getEncryptor().decrypt(connection.password.substring('crypt:'.length)),
|
||||
};
|
||||
}
|
||||
connection = decryptPasswordField(connection, 'password');
|
||||
connection = decryptPasswordField(connection, 'sshPassword');
|
||||
connection = decryptPasswordField(connection, 'sshKeyFilePassword');
|
||||
return connection;
|
||||
}
|
||||
|
||||
|
||||
27
packages/api/src/utility/platformInfo.js
Normal file
27
packages/api/src/utility/platformInfo.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const p = process;
|
||||
const platform = p.env.OS_OVERRIDE ? p.env.OS_OVERRIDE : p.platform;
|
||||
const isWindows = platform === 'win32';
|
||||
const isMac = platform === 'darwin';
|
||||
const isLinux = platform === 'linux';
|
||||
const isDocker = fs.existsSync('/home/dbgate-docker/build');
|
||||
|
||||
const platformInfo = {
|
||||
isWindows,
|
||||
isMac,
|
||||
isLinux,
|
||||
isDocker,
|
||||
isSnap: p.env.ELECTRON_SNAP == 'true',
|
||||
isPortable: isWindows && p.env.PORTABLE_EXECUTABLE_DIR,
|
||||
isAppImage: p.env.DESKTOPINTEGRATION === 'AppImageLauncher',
|
||||
sshAuthSock: p.env.SSH_AUTH_SOCK,
|
||||
environment: process.env.NODE_ENV,
|
||||
platform,
|
||||
runningInWebpack: !!p.env.WEBPACK_DEV_SERVER_URL,
|
||||
defaultKeyFile: path.join(os.homedir(), '.ssh/id_rsa'),
|
||||
};
|
||||
|
||||
module.exports = platformInfo;
|
||||
18
packages/api/src/utility/processComm.js
Normal file
18
packages/api/src/utility/processComm.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const { handleGetSshTunnelRequest, handleGetSshTunnelResponse } = require('./sshTunnelProxy');
|
||||
|
||||
function handleProcessCommunication(message, subprocess) {
|
||||
const { msgtype } = message;
|
||||
if (msgtype == 'getsshtunnel-request') {
|
||||
handleGetSshTunnelRequest(message, subprocess);
|
||||
return true;
|
||||
}
|
||||
if (msgtype == 'getsshtunnel-response') {
|
||||
handleGetSshTunnelResponse(message, subprocess);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
handleProcessCommunication,
|
||||
};
|
||||
82
packages/api/src/utility/sshTunnel.js
Normal file
82
packages/api/src/utility/sshTunnel.js
Normal file
@@ -0,0 +1,82 @@
|
||||
const { SSHConnection } = require('node-ssh-forward');
|
||||
const fs = require('fs-extra');
|
||||
const portfinder = require('portfinder');
|
||||
const stableStringify = require('json-stable-stringify');
|
||||
const _ = require('lodash');
|
||||
const platformInfo = require('./platformInfo');
|
||||
|
||||
const sshConnectionCache = {};
|
||||
const sshTunnelCache = {};
|
||||
|
||||
const CONNECTION_FIELDS = [
|
||||
'sshHost',
|
||||
'sshPort',
|
||||
'sshLogin',
|
||||
'sshPassword',
|
||||
'sshMode',
|
||||
'sshKeyFile',
|
||||
'sshBastionHost',
|
||||
'sshKeyFilePassword',
|
||||
];
|
||||
const TUNNEL_FIELDS = [...CONNECTION_FIELDS, 'server', 'port'];
|
||||
|
||||
async function getSshConnection(connection) {
|
||||
const connectionCacheKey = stableStringify(_.pick(connection, CONNECTION_FIELDS));
|
||||
if (sshConnectionCache[connectionCacheKey]) return sshConnectionCache[connectionCacheKey];
|
||||
|
||||
const sshConfig = {
|
||||
endHost: connection.sshHost || '',
|
||||
endPort: connection.sshPort || 22,
|
||||
bastionHost: connection.sshBastionHost || '',
|
||||
agentForward: connection.sshMode == 'agent',
|
||||
passphrase: connection.sshMode == 'keyFile' ? connection.sshKeyFilePassword : undefined,
|
||||
username: connection.sshLogin,
|
||||
password: connection.sshMode == 'userPassword' ? connection.sshPassword : undefined,
|
||||
agentSocket: connection.sshMode == 'agent' ? platformInfo.sshAuthSock : undefined,
|
||||
privateKey:
|
||||
connection.sshMode == 'keyFile' && connection.sshKeyFile ? await fs.readFile(connection.sshKeyFile) : undefined,
|
||||
skipAutoPrivateKey: true,
|
||||
noReadline: true,
|
||||
};
|
||||
|
||||
const sshConn = new SSHConnection(sshConfig);
|
||||
sshConnectionCache[connectionCacheKey] = sshConn;
|
||||
return sshConn;
|
||||
}
|
||||
|
||||
async function getSshTunnel(connection) {
|
||||
const sshConn = await getSshConnection(connection);
|
||||
const tunnelCacheKey = stableStringify(_.pick(connection, TUNNEL_FIELDS));
|
||||
if (sshTunnelCache[tunnelCacheKey]) return sshTunnelCache[tunnelCacheKey];
|
||||
|
||||
const localPort = await portfinder.getPortPromise({ port: 10000, stopPort: 60000 });
|
||||
// workaround for `getPortPromise` not releasing the port quickly enough
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const tunnelConfig = {
|
||||
fromPort: localPort,
|
||||
toPort: connection.port,
|
||||
toHost: connection.server,
|
||||
};
|
||||
try {
|
||||
const tunnel = await sshConn.forward(tunnelConfig);
|
||||
console.log(
|
||||
`Created SSH tunnel to ${connection.sshHost}-${connection.server}:${connection.port}, using local port ${localPort}`
|
||||
);
|
||||
|
||||
sshTunnelCache[tunnelCacheKey] = {
|
||||
state: 'ok',
|
||||
localPort,
|
||||
};
|
||||
return sshTunnelCache[tunnelCacheKey];
|
||||
} catch (err) {
|
||||
// error is not cached
|
||||
return {
|
||||
state: 'error',
|
||||
message: err.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getSshTunnel,
|
||||
};
|
||||
30
packages/api/src/utility/sshTunnelProxy.js
Normal file
30
packages/api/src/utility/sshTunnelProxy.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const uuidv1 = require('uuid/v1');
|
||||
const { getSshTunnel } = require('./sshTunnel');
|
||||
|
||||
const dispatchedMessages = {};
|
||||
|
||||
async function handleGetSshTunnelRequest({ msgid, connection }, subprocess) {
|
||||
const response = await getSshTunnel(connection);
|
||||
subprocess.send({ msgtype: 'getsshtunnel-response', msgid, response });
|
||||
}
|
||||
|
||||
function handleGetSshTunnelResponse({ msgid, response }, subprocess) {
|
||||
const { resolve } = dispatchedMessages[msgid];
|
||||
delete dispatchedMessages[msgid];
|
||||
resolve(response);
|
||||
}
|
||||
|
||||
async function getSshTunnelProxy(connection) {
|
||||
if (!process.send) return getSshTunnel(connection);
|
||||
const msgid = uuidv1();
|
||||
process.send({ msgtype: 'getsshtunnel-request', msgid, connection });
|
||||
return new Promise((resolve, reject) => {
|
||||
dispatchedMessages[msgid] = { resolve, reject };
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
handleGetSshTunnelRequest,
|
||||
handleGetSshTunnelResponse,
|
||||
getSshTunnelProxy,
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"version": "4.0.0",
|
||||
"name": "dbgate-datalib",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
@@ -12,11 +12,11 @@
|
||||
"lib"
|
||||
],
|
||||
"dependencies": {
|
||||
"dbgate-sqltree": "^1.0.0",
|
||||
"dbgate-filterparser": "^1.0.0"
|
||||
"dbgate-sqltree": "^4.0.0",
|
||||
"dbgate-filterparser": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dbgate-types": "^1.0.0",
|
||||
"dbgate-types": "^4.0.0",
|
||||
"@types/node": "^13.7.0",
|
||||
"typescript": "^3.7.5"
|
||||
}
|
||||
|
||||
@@ -347,5 +347,6 @@ export function changeSetInsertNewRow(changeSet: ChangeSet, name?: NamedObjectIn
|
||||
}
|
||||
|
||||
export function changeSetContainsChanges(changeSet: ChangeSet) {
|
||||
if (!changeSet) return false;
|
||||
return changeSet.deletes.length > 0 || changeSet.updates.length > 0 || changeSet.inserts.length > 0;
|
||||
}
|
||||
|
||||
127
packages/datalib/src/CollectionFormViewDisplay.ts
Normal file
127
packages/datalib/src/CollectionFormViewDisplay.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { FormViewDisplay } from './FormViewDisplay';
|
||||
import _ from 'lodash';
|
||||
import { GridDisplay, ChangeCacheFunc, DisplayColumn, DisplayedColumnInfo, ChangeConfigFunc } from './GridDisplay';
|
||||
import { TableInfo, EngineDriver, ViewInfo, ColumnInfo, NamedObjectInfo, DatabaseInfo } from 'dbgate-types';
|
||||
import { GridConfig, GridCache, createGridCache } from './GridConfig';
|
||||
import {
|
||||
Expression,
|
||||
Select,
|
||||
treeToSql,
|
||||
dumpSqlSelect,
|
||||
mergeConditions,
|
||||
Condition,
|
||||
OrderByExpression,
|
||||
} from 'dbgate-sqltree';
|
||||
import { filterName } from './filterName';
|
||||
import { TableGridDisplay } from './TableGridDisplay';
|
||||
import stableStringify from 'json-stable-stringify';
|
||||
import { ChangeSetFieldDefinition, ChangeSetRowDefinition } from './ChangeSet';
|
||||
import { CollectionGridDisplay } from './CollectionGridDisplay';
|
||||
|
||||
export class CollectionFormViewDisplay extends FormViewDisplay {
|
||||
// use utility functions from GridDisplay and publish result in FromViewDisplay interface
|
||||
private gridDisplay: CollectionGridDisplay;
|
||||
|
||||
constructor(
|
||||
public collectionName: NamedObjectInfo,
|
||||
driver: EngineDriver,
|
||||
config: GridConfig,
|
||||
setConfig: ChangeConfigFunc,
|
||||
cache: GridCache,
|
||||
setCache: ChangeCacheFunc,
|
||||
loadedRow: any
|
||||
) {
|
||||
super(config, setConfig, cache, setCache, driver);
|
||||
this.gridDisplay = new CollectionGridDisplay(collectionName, driver, config, setConfig, cache, setCache, [loadedRow]);
|
||||
|
||||
this.isLoadedCorrectly = this.gridDisplay.isLoadedCorrectly && !!this.driver;
|
||||
this.columns = [];
|
||||
this.addDisplayColumns(this.gridDisplay.columns);
|
||||
}
|
||||
|
||||
addDisplayColumns(columns: DisplayColumn[]) {
|
||||
for (const col of columns) {
|
||||
this.columns.push(col);
|
||||
}
|
||||
}
|
||||
|
||||
navigate(row) {
|
||||
const formViewKey = this.extractKey(row);
|
||||
this.setConfig(cfg => ({
|
||||
...cfg,
|
||||
formViewKey,
|
||||
}));
|
||||
}
|
||||
|
||||
isLoadedCurrentRow(row) {
|
||||
if (!row) return false;
|
||||
const formViewKey = this.extractKey(row);
|
||||
return stableStringify(formViewKey) == stableStringify(this.config.formViewKey);
|
||||
}
|
||||
|
||||
navigateRowQuery(commmand: 'begin' | 'previous' | 'next' | 'end') {
|
||||
if (!this.driver) return null;
|
||||
const select = this.gridDisplay.createSelect();
|
||||
if (!select) return null;
|
||||
const { primaryKey } = this.gridDisplay.baseTable;
|
||||
|
||||
function getOrderBy(direction): OrderByExpression[] {
|
||||
return primaryKey.columns.map(({ columnName }) => ({
|
||||
exprType: 'column',
|
||||
columnName,
|
||||
direction,
|
||||
}));
|
||||
}
|
||||
|
||||
select.topRecords = 1;
|
||||
switch (commmand) {
|
||||
case 'begin':
|
||||
select.orderBy = getOrderBy('ASC');
|
||||
break;
|
||||
case 'end':
|
||||
select.orderBy = getOrderBy('DESC');
|
||||
break;
|
||||
case 'previous':
|
||||
select.orderBy = getOrderBy('DESC');
|
||||
select.where = mergeConditions(select.where, this.getPrimaryKeyOperatorCondition('<'));
|
||||
break;
|
||||
case 'next':
|
||||
select.orderBy = getOrderBy('ASC');
|
||||
select.where = mergeConditions(select.where, this.getPrimaryKeyOperatorCondition('>'));
|
||||
break;
|
||||
}
|
||||
|
||||
const sql = treeToSql(this.driver, select, dumpSqlSelect);
|
||||
return sql;
|
||||
}
|
||||
|
||||
getChangeSetRow(row): ChangeSetRowDefinition {
|
||||
if (!this.baseTable) return null;
|
||||
return {
|
||||
pureName: this.baseTable.pureName,
|
||||
schemaName: this.baseTable.schemaName,
|
||||
condition: this.extractKey(row),
|
||||
};
|
||||
}
|
||||
|
||||
getChangeSetField(row, uniqueName): ChangeSetFieldDefinition {
|
||||
const col = this.columns.find(x => x.uniqueName == uniqueName);
|
||||
if (!col) return null;
|
||||
if (!this.baseTable) return null;
|
||||
if (this.baseTable.pureName != col.pureName || this.baseTable.schemaName != col.schemaName) return null;
|
||||
return {
|
||||
...this.getChangeSetRow(row),
|
||||
uniqueName: uniqueName,
|
||||
columnName: col.columnName,
|
||||
};
|
||||
}
|
||||
|
||||
toggleExpandedColumn(uniqueName: string) {
|
||||
this.gridDisplay.toggleExpandedColumn(uniqueName);
|
||||
this.gridDisplay.reload();
|
||||
}
|
||||
|
||||
isExpandedColumn(uniqueName: string) {
|
||||
return this.gridDisplay.isExpandedColumn(uniqueName);
|
||||
}
|
||||
}
|
||||
100
packages/datalib/src/CollectionGridDisplay.ts
Normal file
100
packages/datalib/src/CollectionGridDisplay.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import _ from 'lodash';
|
||||
import { GridDisplay, ChangeCacheFunc, ChangeConfigFunc, DisplayColumn } from './GridDisplay';
|
||||
import { EngineDriver, ViewInfo, ColumnInfo, CollectionInfo } from 'dbgate-types';
|
||||
import { GridConfig, GridCache } from './GridConfig';
|
||||
|
||||
function getObjectKeys(obj) {
|
||||
if (_.isArray(obj)) {
|
||||
return Object.keys(obj)
|
||||
.slice(0, 10)
|
||||
.map(x => parseInt(x));
|
||||
}
|
||||
if (_.isPlainObject(obj)) {
|
||||
return Object.keys(obj);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function createHeaderText(path) {
|
||||
let res = `${path[0]}`;
|
||||
for (let i = 1; i < path.length; i++) {
|
||||
const name = path[i];
|
||||
if (_.isNumber(name)) res += `[${name}]`;
|
||||
else res += `.${name}`;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export class CollectionGridDisplay extends GridDisplay {
|
||||
constructor(
|
||||
public collection: CollectionInfo,
|
||||
driver: EngineDriver,
|
||||
config: GridConfig,
|
||||
setConfig: ChangeConfigFunc,
|
||||
cache: GridCache,
|
||||
setCache: ChangeCacheFunc,
|
||||
loadedRows
|
||||
) {
|
||||
super(config, setConfig, cache, setCache, driver);
|
||||
this.columns = this.getDisplayColumns(loadedRows || []);
|
||||
this.filterable = true;
|
||||
this.sortable = true;
|
||||
this.editable = false;
|
||||
this.supportsReload = true;
|
||||
this.isDynamicStructure = true;
|
||||
}
|
||||
|
||||
getDisplayColumns(rows) {
|
||||
const res = [];
|
||||
for (const row of rows) {
|
||||
this.getColumnsForObject([], row, res);
|
||||
}
|
||||
return (
|
||||
res.map(col => ({
|
||||
...col,
|
||||
isChecked: this.isColumnChecked(col),
|
||||
})) || []
|
||||
);
|
||||
}
|
||||
|
||||
getColumnsForObject(basePath, obj, res: any[]) {
|
||||
for (const name of getObjectKeys(obj)) {
|
||||
const uniqueName = [...basePath, name].join('.');
|
||||
let column = res.find(x => x.uniqueName == uniqueName);
|
||||
if (!column) {
|
||||
column = this.getDisplayColumn(basePath, name);
|
||||
if (basePath.length > 0) {
|
||||
const lastIndex1 = _.findLastIndex(res, x => x.parentHeaderText.startsWith(column.parentHeaderText));
|
||||
const lastIndex2 = _.findLastIndex(res, x => x.headerText == column.parentHeaderText);
|
||||
// console.log(uniqueName, lastIndex1, lastIndex2);
|
||||
if (lastIndex1 >= 0) res.splice(lastIndex1 + 1, 0, column);
|
||||
else if (lastIndex2 >= 0) res.splice(lastIndex2 + 1, 0, column);
|
||||
else res.push(column);
|
||||
} else {
|
||||
res.push(column);
|
||||
}
|
||||
}
|
||||
if (_.isPlainObject(obj[name]) || _.isArray(obj[name])) {
|
||||
column.isExpandable = true;
|
||||
}
|
||||
|
||||
if (this.isExpandedColumn(column.uniqueName)) {
|
||||
this.getColumnsForObject([...basePath, name], obj[name], res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getDisplayColumn(basePath, columnName) {
|
||||
const uniquePath = [...basePath, columnName];
|
||||
const uniqueName = uniquePath.join('.');
|
||||
return {
|
||||
columnName,
|
||||
headerText: createHeaderText(uniquePath),
|
||||
uniqueName,
|
||||
uniquePath,
|
||||
isStructured: true,
|
||||
parentHeaderText: createHeaderText(basePath),
|
||||
filterType: 'mongo',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,12 @@ export interface DisplayColumn {
|
||||
autoIncrement?: boolean;
|
||||
isPrimaryKey?: boolean;
|
||||
foreignKey?: ForeignKeyInfo;
|
||||
isExpandable?: boolean;
|
||||
isChecked?: boolean;
|
||||
hintColumnName?: string;
|
||||
dataType?: string;
|
||||
filterType?: boolean;
|
||||
isStructured?: boolean;
|
||||
}
|
||||
|
||||
export interface DisplayedColumnEx extends DisplayColumn {
|
||||
@@ -59,6 +62,7 @@ export abstract class GridDisplay {
|
||||
editable = false;
|
||||
isLoadedCorrectly = true;
|
||||
supportsReload = false;
|
||||
isDynamicStructure = false;
|
||||
|
||||
setColumnVisibility(uniquePath: string[], isVisible: boolean) {
|
||||
const uniqueName = uniquePath.join('.');
|
||||
@@ -66,7 +70,7 @@ export abstract class GridDisplay {
|
||||
this.includeInColumnSet('hiddenColumns', uniqueName, !isVisible);
|
||||
} else {
|
||||
this.includeInColumnSet('addedColumns', uniqueName, isVisible);
|
||||
this.reload();
|
||||
if (!this.isDynamicStructure) this.reload();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,4 +19,6 @@ export interface MacroDefinition {
|
||||
export interface MacroSelectedCell {
|
||||
column: string;
|
||||
row: number;
|
||||
rowData: any;
|
||||
value: any;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import stableStringify from 'json-stable-stringify';
|
||||
import { ChangeSetFieldDefinition, ChangeSetRowDefinition } from './ChangeSet';
|
||||
|
||||
export class TableFormViewDisplay extends FormViewDisplay {
|
||||
// use utility functions from GridDisplay and publish result in FromViewDisplat interface
|
||||
// use utility functions from GridDisplay and publish result in FromViewDisplay interface
|
||||
private gridDisplay: TableGridDisplay;
|
||||
|
||||
constructor(
|
||||
|
||||
@@ -59,6 +59,7 @@ export class TableGridDisplay extends GridDisplay {
|
||||
...col,
|
||||
isChecked: this.isColumnChecked(col),
|
||||
hintColumnName: col.foreignKey ? `hint_${col.uniqueName}` : null,
|
||||
isExpandable: !!col.foreignKey,
|
||||
})) || []
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,3 +11,4 @@ export * from './MacroDefinition';
|
||||
export * from './runMacro';
|
||||
export * from './FormViewDisplay';
|
||||
export * from './TableFormViewDisplay';
|
||||
export * from './CollectionGridDisplay';
|
||||
|
||||
@@ -4,6 +4,8 @@ import uuidv1 from 'uuid/v1';
|
||||
import uuidv4 from 'uuid/v4';
|
||||
import moment from 'moment';
|
||||
import { MacroDefinition, MacroSelectedCell } from './MacroDefinition';
|
||||
import { ChangeSet, setChangeSetValue } from './ChangeSet';
|
||||
import { GridDisplay } from './GridDisplay';
|
||||
|
||||
const getMacroFunction = {
|
||||
transformValue: code => `
|
||||
@@ -183,3 +185,55 @@ export function runMacro(
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export function compileMacroFunction(macro: MacroDefinition, errors = []) {
|
||||
if (!macro) return null;
|
||||
let func;
|
||||
try {
|
||||
func = eval(getMacroFunction[macro.type](macro.code));
|
||||
return func;
|
||||
} catch (err) {
|
||||
errors.push(`Error compiling macro ${macro.name}: ${err.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function runMacroOnValue(compiledFunc, macroArgs, value, rowIndex, row, column, errors = []) {
|
||||
if (!compiledFunc) return value;
|
||||
try {
|
||||
const res = compiledFunc(value, macroArgs, modules, rowIndex, row, column);
|
||||
return res;
|
||||
} catch (err) {
|
||||
errors.push(`Error processing column ${column} on row ${rowIndex}: ${err.message}`);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export function runMacroOnChangeSet(
|
||||
macro: MacroDefinition,
|
||||
macroArgs: {},
|
||||
selectedCells: MacroSelectedCell[],
|
||||
changeSet: ChangeSet,
|
||||
display: GridDisplay
|
||||
): ChangeSet {
|
||||
const errors = [];
|
||||
const compiledMacroFunc = compileMacroFunction(macro, errors);
|
||||
if (!compiledMacroFunc) return null;
|
||||
|
||||
let res = changeSet;
|
||||
for (const cell of selectedCells) {
|
||||
const definition = display.getChangeSetField(cell.rowData, cell.column, undefined);
|
||||
const macroResult = runMacroOnValue(
|
||||
compiledMacroFunc,
|
||||
macroArgs,
|
||||
cell.value,
|
||||
cell.row,
|
||||
cell.rowData,
|
||||
cell.column,
|
||||
errors
|
||||
);
|
||||
res = setChangeSetValue(res, definition, macroResult);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
28
packages/dbgate/README.md
Normal file
28
packages/dbgate/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
[](https://github.com/prettier/prettier)
|
||||
[](https://paypal.me/JanProchazkaCz/30eur)
|
||||
[](https://www.npmjs.com/package/dbgate)
|
||||
|
||||
# DbGate - database administration tool
|
||||
DbGate is fast and easy to use database administration tool for MySQL, PostgreSQL, SQL Server.
|
||||
|
||||
## Install using npm
|
||||
```sh
|
||||
npm install -g dbgate
|
||||
```
|
||||
|
||||
After installing, you can run dbgate with command:
|
||||
```sh
|
||||
dbgate
|
||||
```
|
||||
|
||||
Then open http://localhost:5000 in your browser
|
||||
|
||||
## Download electron app
|
||||
You can also download binary packages from https://dbgate.org . Or run from source code, as described on [github](https://github.com/dbgate/dbgate)
|
||||
|
||||
## Other dbgate packages
|
||||
You can use some functionality of dbgate from your JavaScript code. See [dbgate-api](https://npmjs.com/dbgate-api) package.
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||
7
packages/dbgate/bin/dbgate.js
Executable file
7
packages/dbgate/bin/dbgate.js
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const dbgateApi = require('dbgate-api');
|
||||
|
||||
global.dbgateApiModulePath = require.resolve('dbgate-api');
|
||||
|
||||
dbgateApi.getMainModule().start('startNodeWeb');
|
||||
25
packages/dbgate/package.json
Normal file
25
packages/dbgate/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "dbgate",
|
||||
"version": "4.0.0",
|
||||
"homepage": "https://dbgate.org/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
|
||||
"description": "Opensource database administration tool - web interface",
|
||||
"author": "Jan Prochazka",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"dbgate": "./bin/dbgate.js"
|
||||
},
|
||||
"keywords": [
|
||||
"sql",
|
||||
"dbgate",
|
||||
"web"
|
||||
],
|
||||
"dependencies": {
|
||||
"dbgate-api": "^4.0.0",
|
||||
"dbgate-web": "^4.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"version": "4.0.0",
|
||||
"name": "dbgate-filterparser",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
@@ -13,7 +13,7 @@
|
||||
"lib"
|
||||
],
|
||||
"devDependencies": {
|
||||
"dbgate-types": "^1.0.0",
|
||||
"dbgate-types": "^4.0.0",
|
||||
"@types/jest": "^25.1.4",
|
||||
"@types/node": "^13.7.0",
|
||||
"jest": "^24.9.0",
|
||||
@@ -22,9 +22,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/parsimmon": "^1.10.1",
|
||||
"dbgate-tools": "^1.0.0",
|
||||
"dbgate-tools": "^4.0.0",
|
||||
"lodash": "^4.17.15",
|
||||
"moment": "^2.24.0",
|
||||
"parsimmon": "^1.13.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
32
packages/filterparser/src/common.ts
Normal file
32
packages/filterparser/src/common.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import P from 'parsimmon';
|
||||
|
||||
export const whitespace = P.regexp(/\s*/m);
|
||||
|
||||
export function token(parser) {
|
||||
return parser.skip(whitespace);
|
||||
}
|
||||
|
||||
export function word(str) {
|
||||
return P.string(str).thru(token);
|
||||
}
|
||||
|
||||
export function interpretEscapes(str) {
|
||||
let escapes = {
|
||||
b: '\b',
|
||||
f: '\f',
|
||||
n: '\n',
|
||||
r: '\r',
|
||||
t: '\t',
|
||||
};
|
||||
return str.replace(/\\(u[0-9a-fA-F]{4}|[^u])/, (_, escape) => {
|
||||
let type = escape.charAt(0);
|
||||
let hex = escape.slice(1);
|
||||
if (type === 'u') {
|
||||
return String.fromCharCode(parseInt(hex, 16));
|
||||
}
|
||||
if (escapes.hasOwnProperty(type)) {
|
||||
return escapes[type];
|
||||
}
|
||||
return type;
|
||||
});
|
||||
}
|
||||
121
packages/filterparser/src/mongoParser.ts
Normal file
121
packages/filterparser/src/mongoParser.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import P from 'parsimmon';
|
||||
import { interpretEscapes, token, word, whitespace } from './common';
|
||||
|
||||
const operatorCondition = operator => value => ({
|
||||
__placeholder__: {
|
||||
[operator]: value,
|
||||
},
|
||||
});
|
||||
|
||||
const regexCondition = regexString => value => ({
|
||||
__placeholder__: {
|
||||
$regex: regexString.replace('#VALUE#', value),
|
||||
$options: 'i',
|
||||
},
|
||||
});
|
||||
|
||||
const numberTestCondition = () => value => ({
|
||||
$or: [
|
||||
{
|
||||
__placeholder__: {
|
||||
$regex: `.*${value}.*`,
|
||||
$options: 'i',
|
||||
},
|
||||
},
|
||||
{
|
||||
__placeholder__: value,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const testCondition = (operator, value) => () => ({
|
||||
__placeholder__: {
|
||||
[operator]: value,
|
||||
},
|
||||
});
|
||||
|
||||
const compoudCondition = conditionType => conditions => {
|
||||
if (conditions.length == 1) return conditions[0];
|
||||
return {
|
||||
[conditionType]: conditions,
|
||||
};
|
||||
};
|
||||
|
||||
const negateCondition = condition => ({
|
||||
__placeholder__: {
|
||||
$not: condition.__placeholder__,
|
||||
},
|
||||
});
|
||||
|
||||
const createParser = () => {
|
||||
const langDef = {
|
||||
string1: () =>
|
||||
token(P.regexp(/"((?:\\.|.)*?)"/, 1))
|
||||
.map(interpretEscapes)
|
||||
.desc('string quoted'),
|
||||
|
||||
string2: () =>
|
||||
token(P.regexp(/'((?:\\.|.)*?)'/, 1))
|
||||
.map(interpretEscapes)
|
||||
.desc('string quoted'),
|
||||
|
||||
number: () =>
|
||||
token(P.regexp(/-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?/))
|
||||
.map(Number)
|
||||
.desc('number'),
|
||||
|
||||
noQuotedString: () => P.regexp(/[^\s^,^'^"]+/).desc('string unquoted'),
|
||||
|
||||
value: r => P.alt(r.string1, r.string2, r.number, r.noQuotedString),
|
||||
valueTestNum: r => r.number.map(numberTestCondition()),
|
||||
valueTest: r => r.value.map(regexCondition('.*#VALUE#.*')),
|
||||
|
||||
comma: () => word(','),
|
||||
not: () => word('NOT'),
|
||||
notExists: r => r.not.then(r.exists).map(testCondition('$exists', false)),
|
||||
exists: () => word('EXISTS').map(testCondition('$exists', true)),
|
||||
true: () => word('TRUE').map(testCondition('$eq', true)),
|
||||
false: () => word('FALSE').map(testCondition('$eq', false)),
|
||||
|
||||
eq: r => word('=').then(r.value).map(operatorCondition('$eq')),
|
||||
ne: r => word('!=').then(r.value).map(operatorCondition('$ne')),
|
||||
lt: r => word('<').then(r.value).map(operatorCondition('$lt')),
|
||||
gt: r => word('>').then(r.value).map(operatorCondition('$gt')),
|
||||
le: r => word('<=').then(r.value).map(operatorCondition('$lte')),
|
||||
ge: r => word('>=').then(r.value).map(operatorCondition('$gte')),
|
||||
startsWith: r => word('^').then(r.value).map(regexCondition('#VALUE#.*')),
|
||||
endsWith: r => word('$').then(r.value).map(regexCondition('.*#VALUE#')),
|
||||
contains: r => word('+').then(r.value).map(regexCondition('.*#VALUE#.*')),
|
||||
startsWithNot: r => word('!^').then(r.value).map(regexCondition('#VALUE#.*')).map(negateCondition),
|
||||
endsWithNot: r => word('!$').then(r.value).map(regexCondition('.*#VALUE#')).map(negateCondition),
|
||||
containsNot: r => word('~').then(r.value).map(regexCondition('.*#VALUE#.*')).map(negateCondition),
|
||||
|
||||
element: r =>
|
||||
P.alt(
|
||||
r.exists,
|
||||
r.notExists,
|
||||
r.true,
|
||||
r.false,
|
||||
r.eq,
|
||||
r.ne,
|
||||
r.lt,
|
||||
r.gt,
|
||||
r.le,
|
||||
r.ge,
|
||||
r.startsWith,
|
||||
r.endsWith,
|
||||
r.contains,
|
||||
r.startsWithNot,
|
||||
r.endsWithNot,
|
||||
r.containsNot,
|
||||
r.valueTestNum,
|
||||
r.valueTest
|
||||
).trim(whitespace),
|
||||
factor: r => r.element.sepBy(whitespace).map(compoudCondition('$and')),
|
||||
list: r => r.factor.sepBy(r.comma).map(compoudCondition('$or')),
|
||||
};
|
||||
|
||||
return P.createLanguage(langDef);
|
||||
};
|
||||
|
||||
export const mongoParser = createParser();
|
||||
@@ -3,37 +3,8 @@ import moment from 'moment';
|
||||
import { FilterType } from './types';
|
||||
import { Condition } from 'dbgate-sqltree';
|
||||
import { TransformType } from 'dbgate-types';
|
||||
|
||||
const whitespace = P.regexp(/\s*/m);
|
||||
|
||||
function token(parser) {
|
||||
return parser.skip(whitespace);
|
||||
}
|
||||
|
||||
function word(str) {
|
||||
return P.string(str).thru(token);
|
||||
}
|
||||
|
||||
function interpretEscapes(str) {
|
||||
let escapes = {
|
||||
b: '\b',
|
||||
f: '\f',
|
||||
n: '\n',
|
||||
r: '\r',
|
||||
t: '\t',
|
||||
};
|
||||
return str.replace(/\\(u[0-9a-fA-F]{4}|[^u])/, (_, escape) => {
|
||||
let type = escape.charAt(0);
|
||||
let hex = escape.slice(1);
|
||||
if (type === 'u') {
|
||||
return String.fromCharCode(parseInt(hex, 16));
|
||||
}
|
||||
if (escapes.hasOwnProperty(type)) {
|
||||
return escapes[type];
|
||||
}
|
||||
return type;
|
||||
});
|
||||
}
|
||||
import { interpretEscapes, token, word, whitespace } from './common';
|
||||
import { mongoParser } from './mongoParser';
|
||||
|
||||
const binaryCondition = operator => value => ({
|
||||
conditionType: 'binary',
|
||||
@@ -239,7 +210,8 @@ const createParser = (filterType: FilterType) => {
|
||||
yearMonthNum: () => P.regexp(/\d\d\d\d-\d\d?/).map(yearMonthCondition()),
|
||||
yearMonthDayNum: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?/).map(yearMonthDayCondition()),
|
||||
yearMonthDayMinute: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?\s+\d\d?:\d\d?/).map(yearMonthDayMinuteCondition()),
|
||||
yearMonthDaySecond: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?(\s+|T)\d\d?:\d\d?:\d\d?/).map(yearMonthDaySecondCondition()),
|
||||
yearMonthDaySecond: () =>
|
||||
P.regexp(/\d\d\d\d-\d\d?-\d\d?(\s+|T)\d\d?:\d\d?:\d\d?/).map(yearMonthDaySecondCondition()),
|
||||
|
||||
value: r => P.alt(...allowedValues.map(x => r[x])),
|
||||
valueTestEq: r => r.value.map(binaryCondition('=')),
|
||||
@@ -348,9 +320,11 @@ const parsers = {
|
||||
string: createParser('string'),
|
||||
datetime: createParser('datetime'),
|
||||
logical: createParser('logical'),
|
||||
mongo: mongoParser,
|
||||
};
|
||||
|
||||
export function parseFilter(value: string, filterType: FilterType): Condition {
|
||||
// console.log('PARSING', value, 'WITH', filterType);
|
||||
const ast = parsers[filterType].list.tryParse(value);
|
||||
// console.log('AST', ast);
|
||||
return ast;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// import types from 'dbgate-types';
|
||||
|
||||
export type FilterType = 'number' | 'string' | 'datetime' | 'logical';
|
||||
export type FilterType = 'number' | 'string' | 'datetime' | 'logical' | 'mongo';
|
||||
|
||||
@@ -37,7 +37,7 @@ console.log("Generated query:", sql);
|
||||
|
||||
```
|
||||
|
||||
See [TypeScript definitions](https://github.com/dbshell/dbgate/blob/master/packages/sqltree/src/types.ts) for complete list of available SQL command options.
|
||||
See [TypeScript definitions](https://github.com/dbgate/dbgate/blob/master/packages/sqltree/src/types.ts) for complete list of available SQL command options.
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"version": "1.0.4",
|
||||
"version": "4.0.0",
|
||||
"name": "dbgate-sqltree",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
"homepage": "https://dbgate.org/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbshell/dbgate.git"
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
|
||||
"author": "Jan Prochazka",
|
||||
"license": "GPL",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"sql",
|
||||
"mssql",
|
||||
@@ -29,10 +29,10 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/node": "^13.7.0",
|
||||
"dbgate-types": "^1.0.0",
|
||||
"dbgate-types": "^4.0.0",
|
||||
"typescript": "^3.7.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.15"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"version": "1.0.7",
|
||||
"version": "4.0.3-rc.1",
|
||||
"name": "dbgate-tools",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
"homepage": "https://dbgate.org/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbshell/dbgate.git"
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
|
||||
"author": "Jan Prochazka",
|
||||
"license": "GPL",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"sql",
|
||||
"dbgate"
|
||||
@@ -27,7 +27,7 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/node": "^13.7.0",
|
||||
"dbgate-types": "^1.0.0",
|
||||
"dbgate-types": "^4.0.0",
|
||||
"jest": "^24.9.0",
|
||||
"ts-jest": "^25.2.1",
|
||||
"typescript": "^3.7.5"
|
||||
|
||||
@@ -40,7 +40,7 @@ export class DatabaseAnalyser {
|
||||
return this._runAnalysis();
|
||||
}
|
||||
|
||||
mergeAnalyseResult(newlyAnalysed, extractObjectId) {
|
||||
mergeAnalyseResult(newlyAnalysed) {
|
||||
if (this.structure == null) {
|
||||
return {
|
||||
...DatabaseAnalyser.createEmptyStructure(),
|
||||
@@ -49,15 +49,15 @@ export class DatabaseAnalyser {
|
||||
}
|
||||
|
||||
const res = {};
|
||||
for (const field of ['tables', 'views', 'functions', 'procedures', 'triggers']) {
|
||||
for (const field of ['tables', 'collections', 'views', 'functions', 'procedures', 'triggers']) {
|
||||
const removedIds = this.modifications
|
||||
.filter(x => x.action == 'remove' && x.objectTypeField == field)
|
||||
.map(x => extractObjectId(x));
|
||||
.map(x => x.objectId);
|
||||
const newArray = newlyAnalysed[field] || [];
|
||||
const addedChangedIds = newArray.map(x => extractObjectId(x));
|
||||
const addedChangedIds = newArray.map(x => x.objectId);
|
||||
const removeAllIds = [...removedIds, ...addedChangedIds];
|
||||
res[field] = _sortBy(
|
||||
[...this.structure[field].filter(x => !removeAllIds.includes(extractObjectId(x))), ...newArray],
|
||||
[...this.structure[field].filter(x => !removeAllIds.includes(x.objectId)), ...newArray],
|
||||
x => x.pureName
|
||||
);
|
||||
}
|
||||
@@ -78,6 +78,7 @@ export class DatabaseAnalyser {
|
||||
static createEmptyStructure(): DatabaseInfo {
|
||||
return {
|
||||
tables: [],
|
||||
collections: [],
|
||||
views: [],
|
||||
functions: [],
|
||||
procedures: [],
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import {
|
||||
ColumnInfo,
|
||||
ConstraintInfo,
|
||||
EngineDriver,
|
||||
ForeignKeyInfo,
|
||||
FunctionInfo,
|
||||
NamedObjectInfo,
|
||||
PrimaryKeyInfo,
|
||||
ProcedureInfo,
|
||||
SqlDialect,
|
||||
TableInfo,
|
||||
TransformType,
|
||||
TriggerInfo,
|
||||
ViewInfo,
|
||||
IndexInfo,
|
||||
UniqueInfo,
|
||||
CheckInfo,
|
||||
} from 'dbgate-types';
|
||||
import _isString from 'lodash/isString';
|
||||
import _isNumber from 'lodash/isNumber';
|
||||
@@ -46,11 +55,12 @@ export class SqlDumper {
|
||||
}
|
||||
putValue(value) {
|
||||
if (value === null) this.putRaw('NULL');
|
||||
if (value === true) this.putRaw('1');
|
||||
if (value === false) this.putRaw('0');
|
||||
else if (value === true) this.putRaw('1');
|
||||
else if (value === false) this.putRaw('0');
|
||||
else if (_isString(value)) this.putStringValue(value);
|
||||
else if (_isNumber(value)) this.putRaw(value.toString());
|
||||
else if (_isDate(value)) this.putStringValue(new Date(value).toISOString());
|
||||
else this.putRaw('NULL');
|
||||
}
|
||||
putCmd(format, ...args) {
|
||||
this.put(format, ...args);
|
||||
@@ -260,4 +270,189 @@ export class SqlDumper {
|
||||
}
|
||||
|
||||
allowIdentityInsert(table: NamedObjectInfo, allow: boolean) {}
|
||||
enableConstraints(table: NamedObjectInfo, enabled: boolean) {}
|
||||
|
||||
comment(value: string) {
|
||||
if (!value) return;
|
||||
for (const line of value.split('\n')) {
|
||||
this.put(' -- %s', line.trimRight());
|
||||
}
|
||||
}
|
||||
|
||||
createView(obj: ViewInfo) {
|
||||
this.putRaw(obj.createSql);
|
||||
this.endCommand();
|
||||
}
|
||||
dropView(obj: ViewInfo, { testIfExists = false }) {
|
||||
this.putCmd('^drop ^view %f', obj);
|
||||
}
|
||||
alterView(obj: ViewInfo) {
|
||||
this.putRaw(obj.createSql.replace(/create\s+view/i, 'ALTER VIEW'));
|
||||
this.endCommand();
|
||||
}
|
||||
changeViewSchema(obj: ViewInfo, newSchema: string) {}
|
||||
renameView(obj: ViewInfo, newSchema: string) {}
|
||||
|
||||
createProcedure(obj: ProcedureInfo) {
|
||||
this.putRaw(obj.createSql);
|
||||
this.endCommand();
|
||||
}
|
||||
dropProcedure(obj: ProcedureInfo, { testIfExists = false }) {
|
||||
this.putCmd('^drop ^procedure %f', obj);
|
||||
}
|
||||
alterProcedure(obj: ProcedureInfo) {
|
||||
this.putRaw(obj.createSql.replace(/create\s+procedure/i, 'ALTER PROCEDURE'));
|
||||
this.endCommand();
|
||||
}
|
||||
changeProcedureSchema(obj: ProcedureInfo, newSchema: string) {}
|
||||
renameProcedure(obj: ProcedureInfo, newSchema: string) {}
|
||||
|
||||
createFunction(obj: FunctionInfo) {
|
||||
this.putRaw(obj.createSql);
|
||||
this.endCommand();
|
||||
}
|
||||
dropFunction(obj: FunctionInfo, { testIfExists = false }) {
|
||||
this.putCmd('^drop ^function %f', obj);
|
||||
}
|
||||
alterFunction(obj: FunctionInfo) {
|
||||
this.putRaw(obj.createSql.replace(/create\s+function/i, 'ALTER FUNCTION'));
|
||||
this.endCommand();
|
||||
}
|
||||
changeFunctionSchema(obj: FunctionInfo, newSchema: string) {}
|
||||
renameFunction(obj: FunctionInfo, newSchema: string) {}
|
||||
|
||||
createTrigger(obj: TriggerInfo) {
|
||||
this.putRaw(obj.createSql);
|
||||
this.endCommand();
|
||||
}
|
||||
dropTrigger(obj: TriggerInfo, { testIfExists = false }) {
|
||||
this.putCmd('^drop ^trigger %f', obj);
|
||||
}
|
||||
alterTrigger(obj: TriggerInfo) {
|
||||
this.putRaw(obj.createSql.replace(/create\s+trigger/i, 'ALTER TRIGGER'));
|
||||
this.endCommand();
|
||||
}
|
||||
changeTriggerSchema(obj: TriggerInfo, newSchema: string) {}
|
||||
renameTrigger(obj: TriggerInfo, newSchema: string) {}
|
||||
|
||||
dropConstraint(cnt: ConstraintInfo) {
|
||||
this.putCmd('^alter ^table %f ^drop ^constraint %i', cnt, cnt.constraintName);
|
||||
}
|
||||
dropForeignKey(fk: ForeignKeyInfo) {
|
||||
if (this.dialect.explicitDropConstraint) {
|
||||
this.putCmd('^alter ^table %f ^drop ^foreign ^key %i', fk, fk.constraintName);
|
||||
} else {
|
||||
this.dropConstraint(fk);
|
||||
}
|
||||
}
|
||||
createForeignKey(fk: ForeignKeyInfo) {
|
||||
this.put('^alter ^table %f ^add ', fk);
|
||||
this.createForeignKeyFore(fk);
|
||||
this.endCommand();
|
||||
}
|
||||
dropPrimaryKey(pk: PrimaryKeyInfo) {
|
||||
if (this.dialect.explicitDropConstraint) {
|
||||
this.putCmd('^alter ^table %f ^drop ^primary ^key', pk);
|
||||
} else {
|
||||
this.dropConstraint(pk);
|
||||
}
|
||||
}
|
||||
createPrimaryKey(pk: PrimaryKeyInfo) {
|
||||
this.putCmd(
|
||||
'^alter ^table %f ^add ^constraint %i ^primary ^key (%,i)',
|
||||
pk,
|
||||
pk.constraintName,
|
||||
pk.columns.map(x => x.columnName)
|
||||
);
|
||||
}
|
||||
|
||||
dropIndex(ix: IndexInfo) {}
|
||||
createIndex(ix: IndexInfo) {}
|
||||
|
||||
dropUnique(uq: UniqueInfo) {
|
||||
this.dropConstraint(uq);
|
||||
}
|
||||
createUniqueCore(uq: UniqueInfo) {
|
||||
this.put(
|
||||
'^constraint %i ^unique (%,i)',
|
||||
uq.constraintName,
|
||||
uq.columns.map(x => x.columnName)
|
||||
);
|
||||
}
|
||||
|
||||
createUnique(uq: UniqueInfo) {
|
||||
this.put('^alter ^table %f ^add ', uq);
|
||||
this.createUniqueCore(uq);
|
||||
this.endCommand();
|
||||
}
|
||||
|
||||
dropCheck(ch: CheckInfo) {
|
||||
this.dropConstraint(ch);
|
||||
}
|
||||
|
||||
createCheckCore(ch: CheckInfo) {
|
||||
this.put('^constraint %i ^check (%s)', ch.constraintName, ch.definition);
|
||||
}
|
||||
|
||||
createCheck(ch: CheckInfo) {
|
||||
this.put('^alter ^table %f ^add ', ch);
|
||||
this.createCheckCore(ch);
|
||||
this.endCommand();
|
||||
}
|
||||
|
||||
renameConstraint(constraint: ConstraintInfo, newName: string) {}
|
||||
|
||||
createColumn(table: TableInfo, column: ColumnInfo, constraints: ConstraintInfo[]) {
|
||||
this.put('^alter ^table %f ^add %i ', table, column.columnName);
|
||||
this.columnDefinition(column);
|
||||
this.inlineConstraints(constraints);
|
||||
this.endCommand();
|
||||
}
|
||||
|
||||
inlineConstraints(constrains: ConstraintInfo[]) {
|
||||
if (constrains == null) return;
|
||||
for (const cnt of constrains) {
|
||||
if (cnt.constraintType == 'primaryKey') {
|
||||
if (cnt.constraintName != null && !this.dialect.anonymousPrimaryKey) {
|
||||
this.put(' ^constraint %i', cnt.constraintName);
|
||||
}
|
||||
this.put(' ^primary ^key ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dropColumn(column: ColumnInfo) {
|
||||
this.putCmd('^alter ^table %f ^drop ^column %i', column, column.columnName);
|
||||
}
|
||||
|
||||
renameColumn(column: ColumnInfo, newName: string) {}
|
||||
|
||||
changeColumn(oldcol: ColumnInfo, newcol: ColumnInfo, constraints: ConstraintInfo[]) {}
|
||||
|
||||
dropTable(obj: TableInfo, { testIfExists = false }) {
|
||||
this.putCmd('^drop ^table %f', obj);
|
||||
}
|
||||
|
||||
changeTableSchema(obj: TableInfo, schema: string) {}
|
||||
|
||||
renameTable(obj: TableInfo, newname: string) {}
|
||||
|
||||
beginTransaction() {
|
||||
this.putCmd('^begin ^transaction');
|
||||
}
|
||||
|
||||
commitTransaction() {
|
||||
this.putCmd('^commit');
|
||||
}
|
||||
|
||||
alterProlog() {}
|
||||
alterEpilog() {}
|
||||
|
||||
selectTableIntoNewTable(sourceName: NamedObjectInfo, targetName: NamedObjectInfo) {
|
||||
this.putCmd('^select * ^into %f ^from %f', targetName, sourceName);
|
||||
}
|
||||
|
||||
truncateTable(name: NamedObjectInfo) {
|
||||
this.putCmd('^delete ^from %f', name);
|
||||
}
|
||||
}
|
||||
|
||||
292
packages/tools/src/SqlGenerator.ts
Normal file
292
packages/tools/src/SqlGenerator.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
import {
|
||||
DatabaseInfo,
|
||||
EngineDriver,
|
||||
FunctionInfo,
|
||||
ProcedureInfo,
|
||||
TableInfo,
|
||||
TriggerInfo,
|
||||
ViewInfo,
|
||||
} from 'dbgate-types';
|
||||
import _ from 'lodash';
|
||||
import { SqlDumper } from './SqlDumper';
|
||||
import { extendDatabaseInfo } from './structureTools';
|
||||
|
||||
interface SqlGeneratorOptions {
|
||||
dropTables: boolean;
|
||||
checkIfTableExists: boolean;
|
||||
dropReferences: boolean;
|
||||
createTables: boolean;
|
||||
createReferences: boolean;
|
||||
createForeignKeys: boolean;
|
||||
createIndexes: boolean;
|
||||
insert: boolean;
|
||||
skipAutoincrementColumn: boolean;
|
||||
disableConstraints: boolean;
|
||||
omitNulls: boolean;
|
||||
truncate: boolean;
|
||||
|
||||
dropViews: boolean;
|
||||
checkIfViewExists: boolean;
|
||||
createViews: boolean;
|
||||
|
||||
dropProcedures: boolean;
|
||||
checkIfProcedureExists: boolean;
|
||||
createProcedures: boolean;
|
||||
|
||||
dropFunctions: boolean;
|
||||
checkIfFunctionExists: boolean;
|
||||
createFunctions: boolean;
|
||||
|
||||
dropTriggers: boolean;
|
||||
checkIfTriggerExists: boolean;
|
||||
createTriggers: boolean;
|
||||
}
|
||||
|
||||
interface SqlGeneratorObject {
|
||||
schemaName: string;
|
||||
pureName: string;
|
||||
objectTypeField: 'tables' | 'views' | 'procedures' | 'functions';
|
||||
}
|
||||
|
||||
export class SqlGenerator {
|
||||
private tables: TableInfo[];
|
||||
private views: ViewInfo[];
|
||||
private procedures: ProcedureInfo[];
|
||||
private functions: FunctionInfo[];
|
||||
private triggers: TriggerInfo[];
|
||||
public dbinfo: DatabaseInfo;
|
||||
public isTruncated = false;
|
||||
public isUnhandledException = false;
|
||||
|
||||
constructor(
|
||||
dbinfo: DatabaseInfo,
|
||||
public options: SqlGeneratorOptions,
|
||||
public objects: SqlGeneratorObject[],
|
||||
public dmp: SqlDumper,
|
||||
public driver: EngineDriver,
|
||||
public pool
|
||||
) {
|
||||
this.dbinfo = extendDatabaseInfo(dbinfo);
|
||||
this.tables = this.extract('tables');
|
||||
this.views = this.extract('views');
|
||||
this.procedures = this.extract('procedures');
|
||||
this.functions = this.extract('functions');
|
||||
this.triggers = this.extract('triggers');
|
||||
}
|
||||
|
||||
private handleException = error => {
|
||||
console.log('Unhandled error', error);
|
||||
this.isUnhandledException = true;
|
||||
};
|
||||
|
||||
async dump() {
|
||||
try {
|
||||
process.on('uncaughtException', this.handleException);
|
||||
|
||||
this.dropObjects(this.procedures, 'Procedure');
|
||||
if (this.checkDumper()) return;
|
||||
this.dropObjects(this.functions, 'Function');
|
||||
if (this.checkDumper()) return;
|
||||
this.dropObjects(this.views, 'View');
|
||||
if (this.checkDumper()) return;
|
||||
this.dropObjects(this.triggers, 'Trigger');
|
||||
if (this.checkDumper()) return;
|
||||
|
||||
this.dropTables();
|
||||
if (this.checkDumper()) return;
|
||||
|
||||
this.createTables();
|
||||
if (this.checkDumper()) return;
|
||||
|
||||
this.truncateTables();
|
||||
if (this.checkDumper()) return;
|
||||
|
||||
await this.insertData();
|
||||
if (this.checkDumper()) return;
|
||||
|
||||
this.createForeignKeys();
|
||||
if (this.checkDumper()) return;
|
||||
|
||||
this.createObjects(this.procedures, 'Procedure');
|
||||
if (this.checkDumper()) return;
|
||||
this.createObjects(this.functions, 'Function');
|
||||
if (this.checkDumper()) return;
|
||||
this.createObjects(this.views, 'View');
|
||||
if (this.checkDumper()) return;
|
||||
this.createObjects(this.triggers, 'Trigger');
|
||||
if (this.checkDumper()) return;
|
||||
} finally {
|
||||
process.off('uncaughtException', this.handleException);
|
||||
}
|
||||
}
|
||||
|
||||
createForeignKeys() {
|
||||
const fks = [];
|
||||
if (this.options.createForeignKeys) fks.push(..._.flatten(this.tables.map(x => x.foreignKeys || [])));
|
||||
if (this.options.createReferences) fks.push(..._.flatten(this.tables.map(x => x.dependencies || [])));
|
||||
for (const fk of _.uniqBy(fks, 'constraintName')) {
|
||||
this.dmp.createForeignKey(fk);
|
||||
if (this.checkDumper()) return;
|
||||
}
|
||||
}
|
||||
|
||||
truncateTables() {
|
||||
if (this.options.truncate) {
|
||||
for (const table of this.tables) {
|
||||
this.dmp.truncateTable(table);
|
||||
if (this.checkDumper()) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createTables() {
|
||||
if (this.options.createTables) {
|
||||
for (const table of this.tables) {
|
||||
this.dmp.createTable({
|
||||
...table,
|
||||
foreignKeys: [],
|
||||
dependencies: [],
|
||||
indexes: [],
|
||||
});
|
||||
if (this.checkDumper()) return;
|
||||
}
|
||||
}
|
||||
if (this.options.createIndexes) {
|
||||
for (const index of _.flatten(this.tables.map(x => x.indexes || []))) {
|
||||
this.dmp.createIndex(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async insertData() {
|
||||
if (!this.options.insert) return;
|
||||
|
||||
this.enableConstraints(false);
|
||||
|
||||
for (const table of this.tables) {
|
||||
await this.insertTableData(table);
|
||||
if (this.checkDumper()) return;
|
||||
}
|
||||
|
||||
this.enableConstraints(true);
|
||||
}
|
||||
|
||||
checkDumper() {
|
||||
if (this.dmp.s.length > 4000000) {
|
||||
if (!this.isTruncated) {
|
||||
this.dmp.putRaw('\n');
|
||||
this.dmp.comment(' *************** SQL is truncated ******************');
|
||||
this.dmp.putRaw('\n');
|
||||
}
|
||||
this.isTruncated = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
dropObjects(list, name) {
|
||||
if (this.options[`drop${name}s`]) {
|
||||
for (const item of list) {
|
||||
this.dmp[`drop${name}`](item, { testIfExists: this.options[`checkIf${name}Exists`] });
|
||||
if (this.checkDumper()) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createObjects(list, name) {
|
||||
if (this.options[`create${name}s`]) {
|
||||
for (const item of list) {
|
||||
this.dmp[`create${name}`](item);
|
||||
if (this.checkDumper()) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dropTables() {
|
||||
if (this.options.dropReferences) {
|
||||
for (const fk of _.flatten(this.tables.map(x => x.dependencies || []))) {
|
||||
this.dmp.dropForeignKey(fk);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.dropTables) {
|
||||
for (const table of this.tables) {
|
||||
this.dmp.dropTable(table, { testIfExists: this.options.checkIfTableExists });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async insertTableData(table: TableInfo) {
|
||||
const dmpLocal = this.driver.createDumper();
|
||||
dmpLocal.put('^select * ^from %f', table);
|
||||
|
||||
const autoinc = table.columns.find(x => x.autoIncrement);
|
||||
if (autoinc && !this.options.skipAutoincrementColumn) {
|
||||
this.dmp.allowIdentityInsert(table, true);
|
||||
}
|
||||
|
||||
const readable = await this.driver.readQuery(this.pool, dmpLocal.s, table);
|
||||
await this.processReadable(table, readable);
|
||||
|
||||
if (autoinc && !this.options.skipAutoincrementColumn) {
|
||||
this.dmp.allowIdentityInsert(table, false);
|
||||
}
|
||||
}
|
||||
|
||||
processReadable(table: TableInfo, readable) {
|
||||
const columnsFiltered = this.options.skipAutoincrementColumn
|
||||
? table.columns.filter(x => !x.autoIncrement)
|
||||
: table.columns;
|
||||
const columnNames = columnsFiltered.map(x => x.columnName);
|
||||
let isClosed = false;
|
||||
let isHeaderRead = false;
|
||||
|
||||
return new Promise(resolve => {
|
||||
readable.on('data', chunk => {
|
||||
if (isClosed) return;
|
||||
if (!isHeaderRead) {
|
||||
isHeaderRead = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.checkDumper()) {
|
||||
isClosed = true;
|
||||
resolve(undefined);
|
||||
readable.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
const columnNamesCopy = this.options.omitNulls ? columnNames.filter(col => chunk[col] != null) : columnNames;
|
||||
this.dmp.put(
|
||||
'^insert ^into %f (%,i) ^values (%,v);&n',
|
||||
table,
|
||||
columnNamesCopy,
|
||||
columnNamesCopy.map(col => chunk[col])
|
||||
);
|
||||
});
|
||||
readable.on('end', () => {
|
||||
resolve(undefined);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
extract(objectTypeField) {
|
||||
return this.dbinfo[objectTypeField].filter(x =>
|
||||
this.objects.find(
|
||||
y => x.pureName == y.pureName && x.schemaName == y.schemaName && y.objectTypeField == objectTypeField
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
enableConstraints(enabled) {
|
||||
if (this.options.disableConstraints) {
|
||||
if (this.driver.dialect.enableConstraintsPerTable) {
|
||||
for (const table of this.tables) {
|
||||
this.dmp.enableConstraints(table, enabled);
|
||||
}
|
||||
} else {
|
||||
this.dmp.enableConstraints(null, enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,7 @@ export function createBulkInsertStreamBase(driver, stream, pool, name, options):
|
||||
}
|
||||
dmp.putRaw(';');
|
||||
// require('fs').writeFileSync('/home/jena/test.sql', dmp.s);
|
||||
// console.log(dmp.s);
|
||||
await driver.query(pool, dmp.s);
|
||||
};
|
||||
|
||||
|
||||
@@ -8,3 +8,5 @@ export * from './driverBase';
|
||||
export * from './SqlDumper';
|
||||
export * from './testPermission';
|
||||
export * from './splitPostgresQuery';
|
||||
export * from './SqlGenerator';
|
||||
export * from './structureTools';
|
||||
|
||||
@@ -38,14 +38,26 @@ export function findObjectLike(
|
||||
if (!dbinfo) return null;
|
||||
if (schemaName) {
|
||||
// @ts-ignore
|
||||
return dbinfo[objectTypeField].find(
|
||||
return dbinfo[objectTypeField]?.find(
|
||||
x => equalStringLike(x.pureName, pureName) && equalStringLike(x.schemaName, schemaName)
|
||||
);
|
||||
}
|
||||
// @ts-ignore
|
||||
return dbinfo[objectTypeField].find(x => equalStringLike(x.pureName, pureName));
|
||||
return dbinfo[objectTypeField]?.find(x => equalStringLike(x.pureName, pureName));
|
||||
}
|
||||
|
||||
export function findForeignKeyForColumn(table: TableInfo, column: ColumnInfo) {
|
||||
return (table.foreignKeys || []).find(fk => fk.columns.find(col => col.columnName == column.columnName));
|
||||
}
|
||||
|
||||
export function makeUniqueColumnNames(res: ColumnInfo[]) {
|
||||
const usedNames = new Set();
|
||||
for (let i = 0; i < res.length; i++) {
|
||||
if (usedNames.has(res[i].columnName)) {
|
||||
let suffix = 2;
|
||||
while (usedNames.has(`${res[i].columnName}${suffix}`)) suffix++;
|
||||
res[i].columnName = `${res[i].columnName}${suffix}`;
|
||||
}
|
||||
usedNames.add(res[i].columnName);
|
||||
}
|
||||
}
|
||||
|
||||
84
packages/tools/src/structureTools.ts
Normal file
84
packages/tools/src/structureTools.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { DatabaseInfo } from 'dbgate-types';
|
||||
import _ from 'lodash';
|
||||
|
||||
export function addTableDependencies(db: DatabaseInfo): DatabaseInfo {
|
||||
const allForeignKeys = _.flatten(db.tables.map(x => x.foreignKeys || []));
|
||||
return {
|
||||
...db,
|
||||
tables: db.tables.map(table => ({
|
||||
...table,
|
||||
dependencies: allForeignKeys.filter(x => x.refSchemaName == table.schemaName && x.refTableName == table.pureName),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function fillTableExtendedInfo(db: DatabaseInfo): DatabaseInfo {
|
||||
return {
|
||||
...db,
|
||||
tables: (db.tables || []).map(obj => ({
|
||||
...obj,
|
||||
objectTypeField: 'tables',
|
||||
columns: (obj.columns || []).map(column => ({
|
||||
pureName: obj.pureName,
|
||||
schemaName: obj.schemaName,
|
||||
...column,
|
||||
})),
|
||||
primaryKey: obj.primaryKey
|
||||
? {
|
||||
...obj.primaryKey,
|
||||
pureName: obj.pureName,
|
||||
schemaName: obj.schemaName,
|
||||
constraintType: 'primaryKey',
|
||||
}
|
||||
: undefined,
|
||||
foreignKeys: (obj.foreignKeys || []).map(cnt => ({
|
||||
...cnt,
|
||||
pureName: obj.pureName,
|
||||
schemaName: obj.schemaName,
|
||||
constraintType: 'foreignKey',
|
||||
})),
|
||||
indexes: (obj.indexes || []).map(cnt => ({
|
||||
...cnt,
|
||||
pureName: obj.pureName,
|
||||
schemaName: obj.schemaName,
|
||||
constraintType: 'index',
|
||||
})),
|
||||
checks: (obj.checks || []).map(cnt => ({
|
||||
...cnt,
|
||||
pureName: obj.pureName,
|
||||
schemaName: obj.schemaName,
|
||||
constraintType: 'check',
|
||||
})),
|
||||
uniques: (obj.uniques || []).map(cnt => ({
|
||||
...cnt,
|
||||
pureName: obj.pureName,
|
||||
schemaName: obj.schemaName,
|
||||
constraintType: 'unique',
|
||||
})),
|
||||
})),
|
||||
collections: (db.collections || []).map(obj => ({
|
||||
...obj,
|
||||
objectTypeField: 'collections',
|
||||
})),
|
||||
views: (db.views || []).map(obj => ({
|
||||
...obj,
|
||||
objectTypeField: 'views',
|
||||
})),
|
||||
procedures: (db.procedures || []).map(obj => ({
|
||||
...obj,
|
||||
objectTypeField: 'procedures',
|
||||
})),
|
||||
functions: (db.functions || []).map(obj => ({
|
||||
...obj,
|
||||
objectTypeField: 'functions',
|
||||
})),
|
||||
triggers: (db.triggers || []).map(obj => ({
|
||||
...obj,
|
||||
objectTypeField: 'triggers',
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function extendDatabaseInfo(db: DatabaseInfo): DatabaseInfo {
|
||||
return fillTableExtendedInfo(addTableDependencies(db));
|
||||
}
|
||||
21
packages/types/dbinfo.d.ts
vendored
21
packages/types/dbinfo.d.ts
vendored
@@ -10,7 +10,7 @@ export interface ColumnReference {
|
||||
|
||||
export interface ConstraintInfo extends NamedObjectInfo {
|
||||
constraintName: string;
|
||||
constraintType: string;
|
||||
constraintType: 'primaryKey' | 'foreignKey' | 'index' | 'check' | 'unique';
|
||||
}
|
||||
|
||||
export interface ColumnsConstraintInfo extends ConstraintInfo {
|
||||
@@ -26,7 +26,18 @@ export interface ForeignKeyInfo extends ColumnsConstraintInfo {
|
||||
deleteAction: string;
|
||||
}
|
||||
|
||||
export interface ColumnInfo {
|
||||
export interface IndexInfo extends ColumnsConstraintInfo {
|
||||
isUnique: boolean;
|
||||
indexType: 'normal' | 'clustered' | 'xml' | 'spatial' | 'fulltext';
|
||||
}
|
||||
|
||||
export interface UniqueInfo extends ColumnsConstraintInfo {}
|
||||
|
||||
export interface CheckInfo extends ConstraintInfo {
|
||||
definition: string;
|
||||
}
|
||||
|
||||
export interface ColumnInfo extends NamedObjectInfo {
|
||||
columnName: string;
|
||||
notNull: boolean;
|
||||
autoIncrement: boolean;
|
||||
@@ -58,8 +69,13 @@ export interface TableInfo extends DatabaseObjectInfo {
|
||||
primaryKey?: PrimaryKeyInfo;
|
||||
foreignKeys: ForeignKeyInfo[];
|
||||
dependencies?: ForeignKeyInfo[];
|
||||
indexes?: IndexInfo[];
|
||||
uniques?: UniqueInfo[];
|
||||
checks?: CheckInfo[];
|
||||
}
|
||||
|
||||
export interface CollectionInfo extends DatabaseObjectInfo {}
|
||||
|
||||
export interface ViewInfo extends SqlObjectInfo {
|
||||
columns: ColumnInfo[];
|
||||
}
|
||||
@@ -77,6 +93,7 @@ export interface SchemaInfo {
|
||||
|
||||
export interface DatabaseInfoObjects {
|
||||
tables: TableInfo[];
|
||||
collections: CollectionInfo[];
|
||||
views: ViewInfo[];
|
||||
procedures: ProcedureInfo[];
|
||||
functions: FunctionInfo[];
|
||||
|
||||
4
packages/types/dialect.d.ts
vendored
4
packages/types/dialect.d.ts
vendored
@@ -5,4 +5,8 @@ export interface SqlDialect {
|
||||
offsetFetchRangeSyntax?: boolean;
|
||||
quoteIdentifier(s: string): string;
|
||||
fallbackDataType?: string;
|
||||
explicitDropConstraint?: boolean;
|
||||
anonymousPrimaryKey?: boolean;
|
||||
enableConstraintsPerTable?: boolean;
|
||||
nosql?: boolean; // mongo
|
||||
}
|
||||
|
||||
10
packages/types/engines.d.ts
vendored
10
packages/types/engines.d.ts
vendored
@@ -24,9 +24,18 @@ export interface EngineAuthType {
|
||||
disabledFields: string[];
|
||||
}
|
||||
|
||||
export interface ReadCollectionOptions {
|
||||
pureName: string;
|
||||
schemaName?: string;
|
||||
|
||||
countDocuments?: boolean;
|
||||
skip?: number;
|
||||
limit?: number;
|
||||
}
|
||||
export interface EngineDriver {
|
||||
engine: string;
|
||||
title: string;
|
||||
defaultPort?: number;
|
||||
connect({ server, port, user, password, database }): any;
|
||||
query(pool: any, sql: string): Promise<QueryResult>;
|
||||
stream(pool: any, sql: string, options: StreamOptions);
|
||||
@@ -51,6 +60,7 @@ export interface EngineDriver {
|
||||
dialect: SqlDialect;
|
||||
createDumper(): SqlDumper;
|
||||
getAuthTypes(): EngineAuthType[];
|
||||
readCollection(pool: any, options: ReadCollectionOptions): Promise<any>;
|
||||
|
||||
analyserClass?: any;
|
||||
dumperClass?: any;
|
||||
|
||||
7
packages/types/extensions.d.ts
vendored
7
packages/types/extensions.d.ts
vendored
@@ -21,6 +21,12 @@ export interface FileFormatDefinition {
|
||||
getOutputParams?: (sourceName, values) => any;
|
||||
}
|
||||
|
||||
export interface ThemeDefinition {
|
||||
className: string;
|
||||
themeName: string;
|
||||
themeType: 'light' | 'dark';
|
||||
}
|
||||
|
||||
export interface PluginDefinition {
|
||||
packageName: string;
|
||||
manifest: any;
|
||||
@@ -31,4 +37,5 @@ export interface ExtensionsDirectory {
|
||||
plugins: PluginDefinition[];
|
||||
fileFormats: FileFormatDefinition[];
|
||||
drivers: EngineDriver[];
|
||||
themes: ThemeDefinition[];
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"version": "1.0.2",
|
||||
"version": "4.0.0",
|
||||
"name": "dbgate-types",
|
||||
"homepage": "https://dbgate.org/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbshell/dbgate.git"
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
|
||||
"author": "Jan Prochazka",
|
||||
"license": "GPL",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"dbgate"
|
||||
],
|
||||
"types": "index.d.ts",
|
||||
"main": "",
|
||||
"typeScriptVersion": "2.8"
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
module.exports = {
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"globals": {
|
||||
"Atomics": "readonly",
|
||||
"SharedArrayBuffer": "readonly"
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": 2018,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"react/prop-types": "off",
|
||||
"no-unused-vars": "warn"
|
||||
}
|
||||
};
|
||||
105
packages/web/README.md
Normal file
105
packages/web/README.md
Normal file
@@ -0,0 +1,105 @@
|
||||
*Looking for a shareable component template? Go here --> [sveltejs/component-template](https://github.com/sveltejs/component-template)*
|
||||
|
||||
---
|
||||
|
||||
# svelte app
|
||||
|
||||
This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template.
|
||||
|
||||
To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit):
|
||||
|
||||
```bash
|
||||
npx degit sveltejs/template svelte-app
|
||||
cd svelte-app
|
||||
```
|
||||
|
||||
*Note that you will need to have [Node.js](https://nodejs.org) installed.*
|
||||
|
||||
|
||||
## Get started
|
||||
|
||||
Install the dependencies...
|
||||
|
||||
```bash
|
||||
cd svelte-app
|
||||
npm install
|
||||
```
|
||||
|
||||
...then start [Rollup](https://rollupjs.org):
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes.
|
||||
|
||||
By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`.
|
||||
|
||||
If you're using [Visual Studio Code](https://code.visualstudio.com/) we recommend installing the official extension [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). If you are using other editors you may need to install a plugin in order to get syntax highlighting and intellisense.
|
||||
|
||||
## Building and running in production mode
|
||||
|
||||
To create an optimised version of the app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com).
|
||||
|
||||
|
||||
## Single-page app mode
|
||||
|
||||
By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere.
|
||||
|
||||
If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json:
|
||||
|
||||
```js
|
||||
"start": "sirv public --single"
|
||||
```
|
||||
|
||||
## Using TypeScript
|
||||
|
||||
This template comes with a script to set up a TypeScript development environment, you can run it immediately after cloning the template with:
|
||||
|
||||
```bash
|
||||
node scripts/setupTypeScript.js
|
||||
```
|
||||
|
||||
Or remove the script via:
|
||||
|
||||
```bash
|
||||
rm scripts/setupTypeScript.js
|
||||
```
|
||||
|
||||
## Deploying to the web
|
||||
|
||||
### With [Vercel](https://vercel.com)
|
||||
|
||||
Install `vercel` if you haven't already:
|
||||
|
||||
```bash
|
||||
npm install -g vercel
|
||||
```
|
||||
|
||||
Then, from within your project folder:
|
||||
|
||||
```bash
|
||||
cd public
|
||||
vercel deploy --name my-project
|
||||
```
|
||||
|
||||
### With [surge](https://surge.sh/)
|
||||
|
||||
Install `surge` if you haven't already:
|
||||
|
||||
```bash
|
||||
npm install -g surge
|
||||
```
|
||||
|
||||
Then, from within your project folder:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
surge public my-project.surge.sh
|
||||
```
|
||||
@@ -1,67 +1,54 @@
|
||||
{
|
||||
"name": "dbgate-web",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"version": "4.0.0",
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"dev": "cross-env API_URL=http://localhost:3000 rollup -c -w",
|
||||
"start": "sirv public",
|
||||
"validate": "svelte-check",
|
||||
"prepublishOnly": "yarn build"
|
||||
},
|
||||
"files": [
|
||||
"public"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@ant-design/colors": "^5.0.0",
|
||||
"@mdi/font": "^5.8.55",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@rollup/plugin-commonjs": "^17.0.0",
|
||||
"@rollup/plugin-node-resolve": "^11.0.0",
|
||||
"@rollup/plugin-replace": "^2.4.1",
|
||||
"@rollup/plugin-typescript": "^6.0.0",
|
||||
"@tsconfig/svelte": "^1.0.0",
|
||||
"ace-builds": "^1.4.8",
|
||||
"axios": "^0.19.0",
|
||||
"chart.js": "^2.9.4",
|
||||
"compare-versions": "^3.6.0",
|
||||
"cross-env": "^6.0.3",
|
||||
"dbgate-datalib": "^1.0.0",
|
||||
"dbgate-sqltree": "^1.0.0",
|
||||
"dbgate-tools": "^1.0.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-plugin-react": "^7.17.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"dbgate-datalib": "^4.0.0",
|
||||
"dbgate-sqltree": "^4.0.0",
|
||||
"dbgate-tools": "^4.0.0",
|
||||
"dbgate-types": "^4.0.0",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
"localforage": "^1.9.0",
|
||||
"markdown-to-jsx": "^7.1.0",
|
||||
"lodash": "^4.17.15",
|
||||
"randomcolor": "^0.6.2",
|
||||
"react": "^16.12.0",
|
||||
"react-ace": "^8.0.0",
|
||||
"react-chartjs-2": "^2.11.1",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-dropzone": "^11.2.3",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-json-view": "^1.19.1",
|
||||
"react-modal": "^3.11.1",
|
||||
"react-scripts": "3.3.0",
|
||||
"react-select": "^3.1.0",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"rollup": "^2.3.4",
|
||||
"rollup-plugin-copy": "^3.3.0",
|
||||
"rollup-plugin-css-only": "^3.1.0",
|
||||
"rollup-plugin-livereload": "^2.0.0",
|
||||
"rollup-plugin-svelte": "^7.0.0",
|
||||
"rollup-plugin-terser": "^7.0.0",
|
||||
"sirv-cli": "^1.0.0",
|
||||
"socket.io-client": "^2.3.0",
|
||||
"sql-formatter": "^2.3.3",
|
||||
"styled-components": "^4.4.1",
|
||||
"uuid": "^3.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env BROWSER=none PORT=5000 react-scripts start",
|
||||
"build:docker": "cross-env CI=false REACT_APP_API_URL=ORIGIN react-scripts build",
|
||||
"build:app": "cross-env PUBLIC_URL=. CI=false react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"ts": "tsc"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^16.9.17",
|
||||
"@types/styled-components": "^4.4.2",
|
||||
"dbgate-types": "^1.0.0",
|
||||
"typescript": "^3.7.4"
|
||||
"svelte": "^3.35.0",
|
||||
"svelte-check": "^1.0.0",
|
||||
"svelte-preprocess": "^4.0.0",
|
||||
"tslib": "^2.0.0",
|
||||
"typescript": "^3.9.3",
|
||||
"uuid": "^3.4.0",
|
||||
"@mdi/font": "^5.9.55",
|
||||
"file-selector": "^0.2.4",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"svelte-json-tree": "^0.1.0",
|
||||
"svelte-markdown": "^0.1.4",
|
||||
"svelte-select": "^3.17.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
419
packages/web/public/bulma.css
vendored
Normal file
419
packages/web/public/bulma.css
vendored
Normal file
@@ -0,0 +1,419 @@
|
||||
.m-0 {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.mt-0 {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.mr-0 {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.mb-0 {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.ml-0 {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.mx-0 {
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.my-0 {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.m-1 {
|
||||
margin: 0.25rem !important;
|
||||
}
|
||||
|
||||
.mt-1 {
|
||||
margin-top: 0.25rem !important;
|
||||
}
|
||||
|
||||
.mr-1 {
|
||||
margin-right: 0.25rem !important;
|
||||
}
|
||||
|
||||
.mb-1 {
|
||||
margin-bottom: 0.25rem !important;
|
||||
}
|
||||
|
||||
.ml-1 {
|
||||
margin-left: 0.25rem !important;
|
||||
}
|
||||
|
||||
.mx-1 {
|
||||
margin-left: 0.25rem !important;
|
||||
margin-right: 0.25rem !important;
|
||||
}
|
||||
|
||||
.my-1 {
|
||||
margin-top: 0.25rem !important;
|
||||
margin-bottom: 0.25rem !important;
|
||||
}
|
||||
|
||||
.m-2 {
|
||||
margin: 0.5rem !important;
|
||||
}
|
||||
|
||||
.mt-2 {
|
||||
margin-top: 0.5rem !important;
|
||||
}
|
||||
|
||||
.mr-2 {
|
||||
margin-right: 0.5rem !important;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
.ml-2 {
|
||||
margin-left: 0.5rem !important;
|
||||
}
|
||||
|
||||
.mx-2 {
|
||||
margin-left: 0.5rem !important;
|
||||
margin-right: 0.5rem !important;
|
||||
}
|
||||
|
||||
.my-2 {
|
||||
margin-top: 0.5rem !important;
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
.m-3 {
|
||||
margin: 0.75rem !important;
|
||||
}
|
||||
|
||||
.mt-3 {
|
||||
margin-top: 0.75rem !important;
|
||||
}
|
||||
|
||||
.mr-3 {
|
||||
margin-right: 0.75rem !important;
|
||||
}
|
||||
|
||||
.mb-3 {
|
||||
margin-bottom: 0.75rem !important;
|
||||
}
|
||||
|
||||
.ml-3 {
|
||||
margin-left: 0.75rem !important;
|
||||
}
|
||||
|
||||
.mx-3 {
|
||||
margin-left: 0.75rem !important;
|
||||
margin-right: 0.75rem !important;
|
||||
}
|
||||
|
||||
.my-3 {
|
||||
margin-top: 0.75rem !important;
|
||||
margin-bottom: 0.75rem !important;
|
||||
}
|
||||
|
||||
.m-4 {
|
||||
margin: 1rem !important;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 1rem !important;
|
||||
}
|
||||
|
||||
.mr-4 {
|
||||
margin-right: 1rem !important;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem !important;
|
||||
}
|
||||
|
||||
.ml-4 {
|
||||
margin-left: 1rem !important;
|
||||
}
|
||||
|
||||
.mx-4 {
|
||||
margin-left: 1rem !important;
|
||||
margin-right: 1rem !important;
|
||||
}
|
||||
|
||||
.my-4 {
|
||||
margin-top: 1rem !important;
|
||||
margin-bottom: 1rem !important;
|
||||
}
|
||||
|
||||
.m-5 {
|
||||
margin: 1.5rem !important;
|
||||
}
|
||||
|
||||
.mt-5 {
|
||||
margin-top: 1.5rem !important;
|
||||
}
|
||||
|
||||
.mr-5 {
|
||||
margin-right: 1.5rem !important;
|
||||
}
|
||||
|
||||
.mb-5 {
|
||||
margin-bottom: 1.5rem !important;
|
||||
}
|
||||
|
||||
.ml-5 {
|
||||
margin-left: 1.5rem !important;
|
||||
}
|
||||
|
||||
.mx-5 {
|
||||
margin-left: 1.5rem !important;
|
||||
margin-right: 1.5rem !important;
|
||||
}
|
||||
|
||||
.my-5 {
|
||||
margin-top: 1.5rem !important;
|
||||
margin-bottom: 1.5rem !important;
|
||||
}
|
||||
|
||||
.m-6 {
|
||||
margin: 3rem !important;
|
||||
}
|
||||
|
||||
.mt-6 {
|
||||
margin-top: 3rem !important;
|
||||
}
|
||||
|
||||
.mr-6 {
|
||||
margin-right: 3rem !important;
|
||||
}
|
||||
|
||||
.mb-6 {
|
||||
margin-bottom: 3rem !important;
|
||||
}
|
||||
|
||||
.ml-6 {
|
||||
margin-left: 3rem !important;
|
||||
}
|
||||
|
||||
.mx-6 {
|
||||
margin-left: 3rem !important;
|
||||
margin-right: 3rem !important;
|
||||
}
|
||||
|
||||
.my-6 {
|
||||
margin-top: 3rem !important;
|
||||
margin-bottom: 3rem !important;
|
||||
}
|
||||
|
||||
.p-0 {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.pt-0 {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.pr-0 {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.pb-0 {
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.pl-0 {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
.px-0 {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.py-0 {
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.p-1 {
|
||||
padding: 0.25rem !important;
|
||||
}
|
||||
|
||||
.pt-1 {
|
||||
padding-top: 0.25rem !important;
|
||||
}
|
||||
|
||||
.pr-1 {
|
||||
padding-right: 0.25rem !important;
|
||||
}
|
||||
|
||||
.pb-1 {
|
||||
padding-bottom: 0.25rem !important;
|
||||
}
|
||||
|
||||
.pl-1 {
|
||||
padding-left: 0.25rem !important;
|
||||
}
|
||||
|
||||
.px-1 {
|
||||
padding-left: 0.25rem !important;
|
||||
padding-right: 0.25rem !important;
|
||||
}
|
||||
|
||||
.py-1 {
|
||||
padding-top: 0.25rem !important;
|
||||
padding-bottom: 0.25rem !important;
|
||||
}
|
||||
|
||||
.p-2 {
|
||||
padding: 0.5rem !important;
|
||||
}
|
||||
|
||||
.pt-2 {
|
||||
padding-top: 0.5rem !important;
|
||||
}
|
||||
|
||||
.pr-2 {
|
||||
padding-right: 0.5rem !important;
|
||||
}
|
||||
|
||||
.pb-2 {
|
||||
padding-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
.pl-2 {
|
||||
padding-left: 0.5rem !important;
|
||||
}
|
||||
|
||||
.px-2 {
|
||||
padding-left: 0.5rem !important;
|
||||
padding-right: 0.5rem !important;
|
||||
}
|
||||
|
||||
.py-2 {
|
||||
padding-top: 0.5rem !important;
|
||||
padding-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
.p-3 {
|
||||
padding: 0.75rem !important;
|
||||
}
|
||||
|
||||
.pt-3 {
|
||||
padding-top: 0.75rem !important;
|
||||
}
|
||||
|
||||
.pr-3 {
|
||||
padding-right: 0.75rem !important;
|
||||
}
|
||||
|
||||
.pb-3 {
|
||||
padding-bottom: 0.75rem !important;
|
||||
}
|
||||
|
||||
.pl-3 {
|
||||
padding-left: 0.75rem !important;
|
||||
}
|
||||
|
||||
.px-3 {
|
||||
padding-left: 0.75rem !important;
|
||||
padding-right: 0.75rem !important;
|
||||
}
|
||||
|
||||
.py-3 {
|
||||
padding-top: 0.75rem !important;
|
||||
padding-bottom: 0.75rem !important;
|
||||
}
|
||||
|
||||
.p-4 {
|
||||
padding: 1rem !important;
|
||||
}
|
||||
|
||||
.pt-4 {
|
||||
padding-top: 1rem !important;
|
||||
}
|
||||
|
||||
.pr-4 {
|
||||
padding-right: 1rem !important;
|
||||
}
|
||||
|
||||
.pb-4 {
|
||||
padding-bottom: 1rem !important;
|
||||
}
|
||||
|
||||
.pl-4 {
|
||||
padding-left: 1rem !important;
|
||||
}
|
||||
|
||||
.px-4 {
|
||||
padding-left: 1rem !important;
|
||||
padding-right: 1rem !important;
|
||||
}
|
||||
|
||||
.py-4 {
|
||||
padding-top: 1rem !important;
|
||||
padding-bottom: 1rem !important;
|
||||
}
|
||||
|
||||
.p-5 {
|
||||
padding: 1.5rem !important;
|
||||
}
|
||||
|
||||
.pt-5 {
|
||||
padding-top: 1.5rem !important;
|
||||
}
|
||||
|
||||
.pr-5 {
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
|
||||
.pb-5 {
|
||||
padding-bottom: 1.5rem !important;
|
||||
}
|
||||
|
||||
.pl-5 {
|
||||
padding-left: 1.5rem !important;
|
||||
}
|
||||
|
||||
.px-5 {
|
||||
padding-left: 1.5rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
|
||||
.py-5 {
|
||||
padding-top: 1.5rem !important;
|
||||
padding-bottom: 1.5rem !important;
|
||||
}
|
||||
|
||||
.p-6 {
|
||||
padding: 3rem !important;
|
||||
}
|
||||
|
||||
.pt-6 {
|
||||
padding-top: 3rem !important;
|
||||
}
|
||||
|
||||
.pr-6 {
|
||||
padding-right: 3rem !important;
|
||||
}
|
||||
|
||||
.pb-6 {
|
||||
padding-bottom: 3rem !important;
|
||||
}
|
||||
|
||||
.pl-6 {
|
||||
padding-left: 3rem !important;
|
||||
}
|
||||
|
||||
.px-6 {
|
||||
padding-left: 3rem !important;
|
||||
padding-right: 3rem !important;
|
||||
}
|
||||
|
||||
.py-6 {
|
||||
padding-top: 3rem !important;
|
||||
padding-bottom: 3rem !important;
|
||||
}
|
||||
22
packages/web/public/dimensions.css
Normal file
22
packages/web/public/dimensions.css
Normal file
@@ -0,0 +1,22 @@
|
||||
:root {
|
||||
--dim-widget-icon-size: 60px;
|
||||
--dim-statusbar-height: 20px;
|
||||
--dim-left-panel-width: 300px;
|
||||
--dim-tabs-panel-height: 53px;
|
||||
--dim-tabs-height: 33px;
|
||||
--dim-splitter-thickness: 3px;
|
||||
|
||||
--dim-visible-left-panel: 1; /* set from JS */
|
||||
--dim-content-left: calc(
|
||||
var(--dim-widget-icon-size) + var(--dim-visible-left-panel) *
|
||||
(var(--dim-left-panel-width) + var(--dim-splitter-thickness))
|
||||
);
|
||||
|
||||
--dim-visible-toolbar: 1; /* set from JS */
|
||||
|
||||
--dim-toolbar-height: 30px;
|
||||
--dim-header-top: calc(var(--dim-toolbar-height) * var(--dim-visible-toolbar));
|
||||
--dim-content-top: calc(var(--dim-header-top) + var(--dim-tabs-panel-height));
|
||||
|
||||
--dim-large-form-margin: 20px;
|
||||
}
|
||||
149
packages/web/public/global.css
Normal file
149
packages/web/public/global.css
Normal file
@@ -0,0 +1,149 @@
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe WPC, Segoe UI, HelveticaNeue-Light, Ubuntu, Droid Sans,
|
||||
sans-serif;
|
||||
font-size: 14px;
|
||||
/* font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
*/
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.horizontal-split-handle {
|
||||
background-color: var(--theme-border);
|
||||
width: var(--dim-splitter-thickness);
|
||||
cursor: col-resize;
|
||||
}
|
||||
.horizontal-split-handle:hover {
|
||||
background-color: var(--theme-bg-2);
|
||||
}
|
||||
|
||||
.vertical-split-handle {
|
||||
background-color: var(--theme-border);
|
||||
height: var(--dim-splitter-thickness);
|
||||
cursor: row-resize;
|
||||
}
|
||||
.vertical-split-handle:hover {
|
||||
background-color: var(--theme-bg-2);
|
||||
}
|
||||
|
||||
.icon-invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
.space-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.flexcol {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.flex1 {
|
||||
flex: 1;
|
||||
}
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.col-9 {
|
||||
flex-basis: 75%;
|
||||
max-width: 75%;
|
||||
}
|
||||
.col-8 {
|
||||
flex-basis: 66.6667%;
|
||||
max-width: 66.6667%;
|
||||
}
|
||||
.col-6 {
|
||||
flex-basis: 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
.col-4 {
|
||||
flex-basis: 33.3333%;
|
||||
max-width: 33.3333%;
|
||||
}
|
||||
.col-3 {
|
||||
flex-basis: 25%;
|
||||
max-width: 25%;
|
||||
}
|
||||
|
||||
.largeFormMarker input[type='text'] {
|
||||
width: 100%;
|
||||
padding: 10px 10px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.largeFormMarker input[type='password'] {
|
||||
width: 100%;
|
||||
padding: 10px 10px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.largeFormMarker select {
|
||||
width: 100%;
|
||||
padding: 10px 10px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
body *::-webkit-scrollbar {
|
||||
height: 0.8em;
|
||||
width: 0.8em;
|
||||
}
|
||||
body *::-webkit-scrollbar-track {
|
||||
border-radius: 1px;
|
||||
background-color: var(--theme-bg-1);
|
||||
}
|
||||
body *::-webkit-scrollbar-corner {
|
||||
border-radius: 1px;
|
||||
background-color: var(--theme-bg-2);
|
||||
}
|
||||
|
||||
body *::-webkit-scrollbar-thumb {
|
||||
border-radius: 1px;
|
||||
background-color: var(--theme-bg-3);
|
||||
}
|
||||
|
||||
body *::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--theme-bg-4);
|
||||
}
|
||||
|
||||
input {
|
||||
background-color: var(--theme-bg-0);
|
||||
color: var(--theme-font-1);
|
||||
border: 1px solid var(--theme-border);
|
||||
}
|
||||
|
||||
input[disabled] {
|
||||
background-color: var(--theme-bg-1);
|
||||
}
|
||||
|
||||
select {
|
||||
background-color: var(--theme-bg-0);
|
||||
color: var(--theme-font-1);
|
||||
border: 1px solid var(--theme-border);
|
||||
}
|
||||
|
||||
select[disabled] {
|
||||
background-color: var(--theme-bg-1);
|
||||
}
|
||||
|
||||
textarea {
|
||||
background-color: var(--theme-bg-0);
|
||||
color: var(--theme-font-1);
|
||||
border: 1px solid var(--theme-border);
|
||||
}
|
||||
32
packages/web/public/icon-colors.css
Normal file
32
packages/web/public/icon-colors.css
Normal file
@@ -0,0 +1,32 @@
|
||||
.color-icon-blue {
|
||||
color: var(--theme-icon-blue);
|
||||
}
|
||||
|
||||
.color-icon-green {
|
||||
color: var(--theme-icon-green);
|
||||
}
|
||||
|
||||
.color-icon-red {
|
||||
color: var(--theme-icon-red);
|
||||
}
|
||||
|
||||
.color-icon-gold {
|
||||
color: var(--theme-icon-gold);
|
||||
}
|
||||
|
||||
.color-icon-yellow {
|
||||
color: var(--theme-icon-yellow);
|
||||
}
|
||||
|
||||
.color-icon-magenta {
|
||||
color: var(--theme-icon-magenta);
|
||||
}
|
||||
|
||||
|
||||
.color-icon-inv-green {
|
||||
color: var(--theme-icon-inv-green);
|
||||
}
|
||||
|
||||
.color-icon-inv-red {
|
||||
color: var(--theme-icon-inv-red);
|
||||
}
|
||||
@@ -2,43 +2,28 @@
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description"
|
||||
content="DbGate - web based opensource database administration tool for MS SQL, MySQL, Postgre SQL" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
<meta charset='utf-8'>
|
||||
<meta name='viewport' content='width=device-width,initial-scale=1'>
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>DbGate</title>
|
||||
<title>DbGate</title>
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description"
|
||||
content="DbGate - web based opensource database administration tool for MS SQL, MySQL, Postgre SQL" />
|
||||
|
||||
<link rel='icon' type='image/png' href='favicon.ico'>
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
|
||||
<link rel='stylesheet' href='global.css'>
|
||||
<link rel='stylesheet' href='dimensions.css'>
|
||||
<link rel='stylesheet' href='bulma.css'>
|
||||
<link rel='stylesheet' href='icon-colors.css'>
|
||||
<link rel='stylesheet' href='build/bundle.css'>
|
||||
<link rel='stylesheet' href='build/fonts/materialdesignicons.css'>
|
||||
|
||||
<script defer src='build/bundle.js'></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root">Loading DbGate...</div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,2 +0,0 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
@@ -1,16 +0,0 @@
|
||||
body {
|
||||
background: #666;
|
||||
}
|
||||
|
||||
div {
|
||||
color: white;
|
||||
font-size: 25pt;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
text-align: center;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
margin-top: 40px;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<link rel="stylesheet" href="splash.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>Starting DbGate...</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
102
packages/web/rollup.config.js
Normal file
102
packages/web/rollup.config.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import svelte from 'rollup-plugin-svelte';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import livereload from 'rollup-plugin-livereload';
|
||||
import copy from 'rollup-plugin-copy';
|
||||
import { terser } from 'rollup-plugin-terser';
|
||||
import sveltePreprocess from 'svelte-preprocess';
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import replace from '@rollup/plugin-replace';
|
||||
import css from 'rollup-plugin-css-only';
|
||||
|
||||
const production = !process.env.ROLLUP_WATCH;
|
||||
|
||||
function serve() {
|
||||
let server;
|
||||
|
||||
function toExit() {
|
||||
if (server) server.kill(0);
|
||||
}
|
||||
|
||||
return {
|
||||
writeBundle() {
|
||||
if (server) return;
|
||||
server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
|
||||
stdio: ['ignore', 'inherit', 'inherit'],
|
||||
shell: true,
|
||||
});
|
||||
|
||||
process.on('SIGTERM', toExit);
|
||||
process.on('exit', toExit);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
input: 'src/main.ts',
|
||||
output: {
|
||||
sourcemap: true,
|
||||
format: 'iife',
|
||||
name: 'app',
|
||||
file: 'public/build/bundle.js',
|
||||
},
|
||||
plugins: [
|
||||
copy({
|
||||
targets: [
|
||||
{
|
||||
src: '../../node_modules/@mdi/font/css/materialdesignicons.css',
|
||||
dest: 'public/build/fonts/',
|
||||
},
|
||||
{
|
||||
src: '../../node_modules/@mdi/font/fonts/*',
|
||||
dest: 'public/build/fonts/',
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
replace({
|
||||
'process.env.API_URL': JSON.stringify(process.env.API_URL),
|
||||
}),
|
||||
|
||||
svelte({
|
||||
preprocess: sveltePreprocess({ sourceMap: !production }),
|
||||
compilerOptions: {
|
||||
// enable run-time checks when not in production
|
||||
dev: !production,
|
||||
},
|
||||
}),
|
||||
// we'll extract any component CSS out into
|
||||
// a separate file - better for performance
|
||||
css({ output: 'bundle.css' }),
|
||||
|
||||
// If you have external dependencies installed from
|
||||
// npm, you'll most likely need these plugins. In
|
||||
// some cases you'll need additional configuration -
|
||||
// consult the documentation for details:
|
||||
// https://github.com/rollup/plugins/tree/master/packages/commonjs
|
||||
resolve({
|
||||
browser: true,
|
||||
dedupe: ['svelte'],
|
||||
}),
|
||||
commonjs(),
|
||||
typescript({
|
||||
sourceMap: !production,
|
||||
inlineSources: !production,
|
||||
}),
|
||||
|
||||
// In dev mode, call `npm run start` once
|
||||
// the bundle has been generated
|
||||
!production && serve(),
|
||||
|
||||
// Watch the `public` directory and refresh the
|
||||
// browser on changes when not in production
|
||||
!production && livereload('public'),
|
||||
|
||||
// If we're building for production (npm run build
|
||||
// instead of npm run dev), minify
|
||||
production && terser(),
|
||||
],
|
||||
watch: {
|
||||
clearScreen: false,
|
||||
},
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import React from 'react';
|
||||
import './index.css';
|
||||
import Screen from './Screen';
|
||||
import {
|
||||
CurrentWidgetProvider,
|
||||
CurrentDatabaseProvider,
|
||||
OpenedTabsProvider,
|
||||
OpenedConnectionsProvider,
|
||||
LeftPanelWidthProvider,
|
||||
CurrentArchiveProvider,
|
||||
CurrentThemeProvider,
|
||||
} from './utility/globalState';
|
||||
import { SocketProvider } from './utility/SocketProvider';
|
||||
import ConnectionsPinger from './utility/ConnectionsPinger';
|
||||
import { ModalLayerProvider } from './modals/showModal';
|
||||
import UploadsProvider from './utility/UploadsProvider';
|
||||
import ThemeHelmet from './themes/ThemeHelmet';
|
||||
import PluginsProvider from './plugins/PluginsProvider';
|
||||
import { ExtensionsProvider } from './utility/useExtensions';
|
||||
import { MenuLayerProvider } from './modals/showMenu';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<CurrentWidgetProvider>
|
||||
<CurrentDatabaseProvider>
|
||||
<SocketProvider>
|
||||
<OpenedTabsProvider>
|
||||
<OpenedConnectionsProvider>
|
||||
<LeftPanelWidthProvider>
|
||||
<ConnectionsPinger>
|
||||
<PluginsProvider>
|
||||
<ExtensionsProvider>
|
||||
<CurrentArchiveProvider>
|
||||
<CurrentThemeProvider>
|
||||
<UploadsProvider>
|
||||
<ModalLayerProvider>
|
||||
<MenuLayerProvider>
|
||||
<ThemeHelmet />
|
||||
<Screen />
|
||||
</MenuLayerProvider>
|
||||
</ModalLayerProvider>
|
||||
</UploadsProvider>
|
||||
</CurrentThemeProvider>
|
||||
</CurrentArchiveProvider>
|
||||
</ExtensionsProvider>
|
||||
</PluginsProvider>
|
||||
</ConnectionsPinger>
|
||||
</LeftPanelWidthProvider>
|
||||
</OpenedConnectionsProvider>
|
||||
</OpenedTabsProvider>
|
||||
</SocketProvider>
|
||||
</CurrentDatabaseProvider>
|
||||
</CurrentWidgetProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
16
packages/web/src/App.svelte
Normal file
16
packages/web/src/App.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import CommandListener from './commands/CommandListener.svelte';
|
||||
import DataGridRowHeightMeter from './datagrid/DataGridRowHeightMeter.svelte';
|
||||
|
||||
import PluginsProvider from './plugins/PluginsProvider.svelte';
|
||||
import Screen from './Screen.svelte';
|
||||
import ErrorHandler from './utility/ErrorHandler.svelte';
|
||||
import OpenTabsOnStartup from './utility/OpenTabsOnStartup.svelte';
|
||||
</script>
|
||||
|
||||
<DataGridRowHeightMeter />
|
||||
<ErrorHandler />
|
||||
<PluginsProvider />
|
||||
<CommandListener />
|
||||
<OpenTabsOnStartup />
|
||||
<Screen />
|
||||
@@ -1,11 +0,0 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
const { getByText } = render(<App />);
|
||||
const linkElement = getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
@@ -1,64 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { FontIcon } from './icons';
|
||||
import useTheme from './theme/useTheme';
|
||||
import useExtensions from './utility/useExtensions';
|
||||
|
||||
const TargetStyled = styled.div`
|
||||
position: fixed;
|
||||
display: flex;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: ${props => props.theme.main_background_blue[3]};
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
z-index: 1000;
|
||||
`;
|
||||
|
||||
const InfoBox = styled.div``;
|
||||
|
||||
const IconWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
font-size: 50px;
|
||||
margin-bottom: 20px;
|
||||
`;
|
||||
|
||||
const InfoWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-top: 10px;
|
||||
`;
|
||||
|
||||
const TitleWrapper = styled.div`
|
||||
font-size: 30px;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
`;
|
||||
|
||||
export default function DragAndDropFileTarget({ isDragActive, inputProps }) {
|
||||
const theme = useTheme();
|
||||
const { fileFormats } = useExtensions();
|
||||
return (
|
||||
!!isDragActive && (
|
||||
<TargetStyled theme={theme}>
|
||||
<InfoBox>
|
||||
<IconWrapper>
|
||||
<FontIcon icon="icon cloud-upload" />
|
||||
</IconWrapper>
|
||||
<TitleWrapper>Drop the files to upload to DbGate</TitleWrapper>
|
||||
<InfoWrapper>
|
||||
Supported file types:{' '}
|
||||
{fileFormats
|
||||
.filter(x => x.readerFunc)
|
||||
.map(x => x.name)
|
||||
.join(', ')}
|
||||
</InfoWrapper>
|
||||
</InfoBox>
|
||||
<input {...inputProps} />
|
||||
</TargetStyled>
|
||||
)
|
||||
);
|
||||
}
|
||||
55
packages/web/src/DragAndDropFileTarget.svelte
Normal file
55
packages/web/src/DragAndDropFileTarget.svelte
Normal file
@@ -0,0 +1,55 @@
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import FontIcon from './icons/FontIcon.svelte';
|
||||
|
||||
import { extensions } from './stores';
|
||||
|
||||
import getElectron from './utility/getElectron';
|
||||
|
||||
const electron = getElectron();
|
||||
$: fileTypeNames = _.compact([
|
||||
...$extensions.fileFormats.filter(x => x.readerFunc).map(x => x.name),
|
||||
electron ? 'SQL' : null,
|
||||
]);
|
||||
</script>
|
||||
|
||||
<div class="target">
|
||||
<div>
|
||||
<div class="icon">
|
||||
<FontIcon icon="icon cloud-upload" />
|
||||
</div>
|
||||
<div class="title">Drop the files to upload to DbGate</div>
|
||||
<div class="info">Supported file types: {fileTypeNames.join(', ')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.target {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--theme-bg-selected);
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
z-index: 1000;
|
||||
}
|
||||
.icon {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
font-size: 50px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.info {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.title {
|
||||
font-size: 30px;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
</style>
|
||||
@@ -1,144 +0,0 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import React from 'react';
|
||||
import dimensions from './theme/dimensions';
|
||||
import styled from 'styled-components';
|
||||
import TabsPanel from './TabsPanel';
|
||||
import TabContent from './TabContent';
|
||||
import WidgetIconPanel from './widgets/WidgetIconPanel';
|
||||
import { useCurrentWidget, useLeftPanelWidth, useSetLeftPanelWidth } from './utility/globalState';
|
||||
import WidgetContainer from './widgets/WidgetContainer';
|
||||
import ToolBar from './widgets/Toolbar';
|
||||
import StatusBar from './widgets/StatusBar';
|
||||
import { useSplitterDrag, HorizontalSplitHandle } from './widgets/Splitter';
|
||||
import { ModalLayer } from './modals/showModal';
|
||||
import DragAndDropFileTarget from './DragAndDropFileTarget';
|
||||
import { useUploadsZone } from './utility/UploadsProvider';
|
||||
import useTheme from './theme/useTheme';
|
||||
import { MenuLayer } from './modals/showMenu';
|
||||
import ErrorBoundary from './utility/ErrorBoundary';
|
||||
|
||||
const BodyDiv = styled.div`
|
||||
position: fixed;
|
||||
top: ${dimensions.tabsPanel.height + dimensions.toolBar.height}px;
|
||||
left: ${props => props.contentLeft}px;
|
||||
bottom: ${dimensions.statusBar.height}px;
|
||||
right: 0;
|
||||
background-color: ${props => props.theme.content_background};
|
||||
`;
|
||||
|
||||
const ToolBarDiv = styled.div`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: ${props => props.theme.toolbar_background};
|
||||
height: ${dimensions.toolBar.height}px;
|
||||
`;
|
||||
|
||||
const IconBar = styled.div`
|
||||
position: fixed;
|
||||
top: ${dimensions.toolBar.height}px;
|
||||
left: 0;
|
||||
bottom: ${dimensions.statusBar.height}px;
|
||||
width: ${dimensions.widgetMenu.iconSize}px;
|
||||
background-color: ${props => props.theme.widget_background};
|
||||
`;
|
||||
|
||||
const LeftPanel = styled.div`
|
||||
position: fixed;
|
||||
top: ${dimensions.toolBar.height}px;
|
||||
left: ${dimensions.widgetMenu.iconSize}px;
|
||||
bottom: ${dimensions.statusBar.height}px;
|
||||
background-color: ${props => props.theme.left_background};
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const TabsPanelContainer = styled.div`
|
||||
display: flex;
|
||||
position: fixed;
|
||||
top: ${dimensions.toolBar.height}px;
|
||||
left: ${props => props.contentLeft}px;
|
||||
height: ${dimensions.tabsPanel.height}px;
|
||||
right: 0;
|
||||
background-color: ${props => props.theme.tabs_background2};
|
||||
border-top: 1px solid ${props => props.theme.border};
|
||||
|
||||
overflow-x: auto;
|
||||
|
||||
::-webkit-scrollbar {
|
||||
height: 7px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StausBarContainer = styled.div`
|
||||
position: fixed;
|
||||
height: ${dimensions.statusBar.height}px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: ${props => props.theme.statusbar_background};
|
||||
`;
|
||||
|
||||
const ScreenHorizontalSplitHandle = styled(HorizontalSplitHandle)`
|
||||
position: absolute;
|
||||
top: ${dimensions.toolBar.height}px;
|
||||
bottom: ${dimensions.statusBar.height}px;
|
||||
`;
|
||||
|
||||
// const StyledRoot = styled.div`
|
||||
// // color: ${(props) => props.theme.fontColor};
|
||||
// `;
|
||||
|
||||
export default function Screen() {
|
||||
const theme = useTheme();
|
||||
const currentWidget = useCurrentWidget();
|
||||
const leftPanelWidth = useLeftPanelWidth();
|
||||
const setLeftPanelWidth = useSetLeftPanelWidth();
|
||||
const contentLeft = currentWidget
|
||||
? dimensions.widgetMenu.iconSize + leftPanelWidth + dimensions.splitter.thickness
|
||||
: dimensions.widgetMenu.iconSize;
|
||||
const toolbarPortalRef = React.useRef();
|
||||
const onSplitDown = useSplitterDrag('clientX', diff => setLeftPanelWidth(v => v + diff));
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useUploadsZone();
|
||||
|
||||
return (
|
||||
<div {...getRootProps()}>
|
||||
<ToolBarDiv theme={theme}>
|
||||
<ToolBar toolbarPortalRef={toolbarPortalRef} />
|
||||
</ToolBarDiv>
|
||||
<IconBar theme={theme}>
|
||||
<WidgetIconPanel />
|
||||
</IconBar>
|
||||
{!!currentWidget && (
|
||||
<LeftPanel theme={theme}>
|
||||
<ErrorBoundary>
|
||||
<WidgetContainer />
|
||||
</ErrorBoundary>
|
||||
</LeftPanel>
|
||||
)}
|
||||
{!!currentWidget && (
|
||||
<ScreenHorizontalSplitHandle
|
||||
onMouseDown={onSplitDown}
|
||||
theme={theme}
|
||||
style={{ left: leftPanelWidth + dimensions.widgetMenu.iconSize }}
|
||||
/>
|
||||
)}
|
||||
<TabsPanelContainer contentLeft={contentLeft} theme={theme}>
|
||||
<TabsPanel></TabsPanel>
|
||||
</TabsPanelContainer>
|
||||
<BodyDiv contentLeft={contentLeft} theme={theme}>
|
||||
<TabContent toolbarPortalRef={toolbarPortalRef} />
|
||||
</BodyDiv>
|
||||
<StausBarContainer theme={theme}>
|
||||
<StatusBar />
|
||||
</StausBarContainer>
|
||||
<ModalLayer />
|
||||
<MenuLayer />
|
||||
|
||||
<DragAndDropFileTarget inputProps={getInputProps()} isDragActive={isDragActive} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
141
packages/web/src/Screen.svelte
Normal file
141
packages/web/src/Screen.svelte
Normal file
@@ -0,0 +1,141 @@
|
||||
<script>
|
||||
import WidgetContainer from './widgets/WidgetContainer.svelte';
|
||||
import WidgetIconPanel from './widgets/WidgetIconPanel.svelte';
|
||||
import {
|
||||
currentTheme,
|
||||
currentThemeDefinition,
|
||||
isFileDragActive,
|
||||
leftPanelWidth,
|
||||
selectedWidget,
|
||||
visibleCommandPalette,
|
||||
visibleToolbar,
|
||||
} from './stores';
|
||||
import TabsPanel from './widgets/TabsPanel.svelte';
|
||||
import TabRegister from './TabRegister.svelte';
|
||||
import CommandPalette from './commands/CommandPalette.svelte';
|
||||
import Toolbar from './widgets/Toolbar.svelte';
|
||||
import splitterDrag from './utility/splitterDrag';
|
||||
import CurrentDropDownMenu from './modals/CurrentDropDownMenu.svelte';
|
||||
import StatusBar from './widgets/StatusBar.svelte';
|
||||
import ModalLayer from './modals/ModalLayer.svelte';
|
||||
import DragAndDropFileTarget from './DragAndDropFileTarget.svelte';
|
||||
import dragDropFileTarget from './utility/dragDropFileTarget';
|
||||
|
||||
$: currentThemeType = $currentThemeDefinition?.themeType == 'dark' ? 'theme-type-dark' : 'theme-type-light';
|
||||
</script>
|
||||
|
||||
<div class={`${$currentTheme} ${currentThemeType} root`} use:dragDropFileTarget>
|
||||
<div class="iconbar">
|
||||
<WidgetIconPanel />
|
||||
</div>
|
||||
<div class="statusbar">
|
||||
<StatusBar />
|
||||
</div>
|
||||
{#if $selectedWidget}
|
||||
<div class="leftpanel">
|
||||
<WidgetContainer />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="tabs">
|
||||
<TabsPanel />
|
||||
</div>
|
||||
<div class="content">
|
||||
<TabRegister />
|
||||
</div>
|
||||
{#if $selectedWidget}
|
||||
<div
|
||||
class="horizontal-split-handle splitter"
|
||||
use:splitterDrag={'clientX'}
|
||||
on:resizeSplitter={e => leftPanelWidth.update(x => x + e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
{#if $visibleCommandPalette}
|
||||
<div class="commads">
|
||||
<CommandPalette />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $visibleToolbar}
|
||||
<div class="toolbar">
|
||||
<Toolbar />
|
||||
</div>
|
||||
{/if}
|
||||
<CurrentDropDownMenu />
|
||||
<ModalLayer />
|
||||
{#if $isFileDragActive}
|
||||
<DragAndDropFileTarget />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
color: var(--theme-font-1);
|
||||
}
|
||||
.iconbar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: var(--dim-header-top);
|
||||
bottom: var(--dim-statusbar-height);
|
||||
width: var(--dim-widget-icon-size);
|
||||
background: var(--theme-bg-inv-1);
|
||||
}
|
||||
.statusbar {
|
||||
position: fixed;
|
||||
background: var(--theme-bg-statusbar-inv);
|
||||
height: var(--dim-statusbar-height);
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.leftpanel {
|
||||
position: fixed;
|
||||
top: var(--dim-header-top);
|
||||
left: var(--dim-widget-icon-size);
|
||||
bottom: var(--dim-statusbar-height);
|
||||
width: var(--dim-left-panel-width);
|
||||
background-color: var(--theme-bg-1);
|
||||
display: flex;
|
||||
}
|
||||
.tabs {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
top: var(--dim-header-top);
|
||||
left: var(--dim-content-left);
|
||||
height: var(--dim-tabs-panel-height);
|
||||
right: 0;
|
||||
background-color: var(--theme-bg-2);
|
||||
border-top: 1px solid var(--theme-border);
|
||||
|
||||
overflow-x: auto;
|
||||
}
|
||||
.tabs::-webkit-scrollbar {
|
||||
height: 7px;
|
||||
}
|
||||
.content {
|
||||
position: fixed;
|
||||
top: var(--dim-content-top);
|
||||
left: var(--dim-content-left);
|
||||
bottom: var(--dim-statusbar-height);
|
||||
right: 0;
|
||||
background-color: var(--theme-bg-1);
|
||||
}
|
||||
.commads {
|
||||
position: fixed;
|
||||
top: var(--dim-header-top);
|
||||
left: var(--dim-widget-icon-size);
|
||||
}
|
||||
.toolbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
height: var(--dim-toolbar-height);
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--theme-bg-1);
|
||||
}
|
||||
|
||||
.splitter {
|
||||
position: absolute;
|
||||
top: var(--dim-header-top);
|
||||
bottom: var(--dim-statusbar-height);
|
||||
left: calc(var(--dim-widget-icon-size) + var(--dim-left-panel-width));
|
||||
}
|
||||
</style>
|
||||
@@ -1,91 +0,0 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import styled from 'styled-components';
|
||||
import tabs from './tabs';
|
||||
import { useOpenedTabs } from './utility/globalState';
|
||||
import ErrorBoundary from './utility/ErrorBoundary';
|
||||
|
||||
const TabContainerStyled = styled.div`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
visibility: ${props =>
|
||||
// @ts-ignore
|
||||
props.tabVisible ? 'visible' : 'hidden'};
|
||||
`;
|
||||
|
||||
function TabContainer({ TabComponent, ...props }) {
|
||||
const { tabVisible, tabid, toolbarPortalRef } = props;
|
||||
return (
|
||||
// @ts-ignore
|
||||
<TabContainerStyled tabVisible={tabVisible}>
|
||||
<ErrorBoundary>
|
||||
<TabComponent {...props} tabid={tabid} tabVisible={tabVisible} toolbarPortalRef={toolbarPortalRef} />
|
||||
</ErrorBoundary>
|
||||
</TabContainerStyled>
|
||||
);
|
||||
}
|
||||
|
||||
const TabContainerMemo = React.memo(TabContainer);
|
||||
|
||||
function createTabComponent(selectedTab) {
|
||||
const TabComponent = tabs[selectedTab.tabComponent];
|
||||
if (TabComponent) {
|
||||
return {
|
||||
TabComponent,
|
||||
props: selectedTab.props,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function TabContent({ toolbarPortalRef }) {
|
||||
const files = useOpenedTabs();
|
||||
|
||||
const [mountedTabs, setMountedTabs] = React.useState({});
|
||||
|
||||
const selectedTab = files.find(x => x.selected && x.closedTime == null);
|
||||
|
||||
React.useEffect(() => {
|
||||
// cleanup closed tabs
|
||||
|
||||
if (
|
||||
_.difference(
|
||||
_.keys(mountedTabs),
|
||||
_.map(
|
||||
files.filter(x => x.closedTime == null),
|
||||
'tabid'
|
||||
)
|
||||
).length > 0
|
||||
) {
|
||||
setMountedTabs(_.pickBy(mountedTabs, (v, k) => files.find(x => x.tabid == k && x.closedTime == null)));
|
||||
}
|
||||
|
||||
if (selectedTab) {
|
||||
const { tabid } = selectedTab;
|
||||
if (tabid && !mountedTabs[tabid])
|
||||
setMountedTabs({
|
||||
...mountedTabs,
|
||||
[tabid]: createTabComponent(selectedTab),
|
||||
});
|
||||
}
|
||||
}, [mountedTabs, files]);
|
||||
|
||||
return _.keys(mountedTabs).map(tabid => {
|
||||
const { TabComponent, props } = mountedTabs[tabid];
|
||||
const tabVisible = tabid == (selectedTab && selectedTab.tabid);
|
||||
return (
|
||||
<TabContainerMemo
|
||||
key={tabid}
|
||||
{...props}
|
||||
tabid={tabid}
|
||||
tabVisible={tabVisible}
|
||||
toolbarPortalRef={toolbarPortalRef}
|
||||
TabComponent={TabComponent}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
35
packages/web/src/TabContent.svelte
Normal file
35
packages/web/src/TabContent.svelte
Normal file
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { setContext } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export let tabid;
|
||||
export let tabVisible;
|
||||
export let tabComponent;
|
||||
|
||||
const tabVisibleStore = writable(tabVisible);
|
||||
setContext('tabid', tabid);
|
||||
setContext('tabVisible', tabVisibleStore);
|
||||
|
||||
$: tabVisibleStore.set(tabVisible);
|
||||
</script>
|
||||
|
||||
<div class:tabVisible>
|
||||
<svelte:component this={tabComponent} {...$$restProps} {tabid} {tabVisible} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
}
|
||||
.tabVisible {
|
||||
visibility: visible;
|
||||
}
|
||||
:not(.tabVisible) {
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user