Compare commits
284 Commits
check-2025
...
v6.5.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cbe021ffc | ||
|
|
7b39d8025b | ||
|
|
47bd35b151 | ||
|
|
d7add54a3c | ||
|
|
d3c937569b | ||
|
|
94ca613201 | ||
|
|
30f2f635be | ||
|
|
57f4d31c21 | ||
|
|
90e4fd7ff5 | ||
|
|
17835832f2 | ||
|
|
949817f597 | ||
|
|
23065f2c4b | ||
|
|
b623b06cf0 | ||
|
|
55c86d8ec7 | ||
|
|
e955617aa1 | ||
|
|
6304610713 | ||
|
|
47d20928e0 | ||
|
|
c9a4d02e0d | ||
|
|
6513dfb42a | ||
|
|
3f0412453f | ||
|
|
dcba319071 | ||
|
|
d19851fc0c | ||
|
|
d6eb06cb72 | ||
|
|
473080d7ee | ||
|
|
c98a6adb09 | ||
|
|
2cd56d5041 | ||
|
|
982098672e | ||
|
|
445ecea3e6 | ||
|
|
db977dfba4 | ||
|
|
a3c12ab9f5 | ||
|
|
0f7e152650 | ||
|
|
b55c7ba9a1 | ||
|
|
8256c9f7ad | ||
|
|
59727d7b0b | ||
|
|
2dd2210a73 | ||
|
|
25aafdbebc | ||
|
|
cd5717169c | ||
|
|
a38ad5a11e | ||
|
|
66d9b56976 | ||
|
|
ac40bd1e17 | ||
|
|
16d2a9bf99 | ||
|
|
b7e6838d26 | ||
|
|
21d23b5baa | ||
|
|
69a2941d57 | ||
|
|
3cc2abf8b9 | ||
|
|
6f4173650a | ||
|
|
0fcb8bdc0a | ||
|
|
c0937cf412 | ||
|
|
d9ab3aab0f | ||
|
|
c8652de78b | ||
|
|
86dc4e2bd5 | ||
|
|
1b9c56a9b9 | ||
|
|
08ab504fac | ||
|
|
21c0842fae | ||
|
|
8d10feaa68 | ||
|
|
df2171f253 | ||
|
|
f5fcd94faf | ||
|
|
15c5dbef00 | ||
|
|
79df56c096 | ||
|
|
d3fffd9530 | ||
|
|
527c9c8e6e | ||
|
|
d285be45cb | ||
|
|
0dda9c73f6 | ||
|
|
d07bf270e7 | ||
|
|
eb24dd5d9e | ||
|
|
ce693c7cd5 | ||
|
|
3198890269 | ||
|
|
eacc93de43 | ||
|
|
9795740257 | ||
|
|
4548f5d8aa | ||
|
|
8dfd2fb519 | ||
|
|
83a40f83e1 | ||
|
|
5b2fcb3c6c | ||
|
|
bcd9adb66d | ||
|
|
5e2dc114ab | ||
|
|
1ced4531be | ||
|
|
05fe39c0ae | ||
|
|
3769b2b3ea | ||
|
|
f4d5480f6f | ||
|
|
ddf3c0810b | ||
|
|
6afd6d0aa0 | ||
|
|
59fe92eb04 | ||
|
|
0550f32434 | ||
|
|
b702cad549 | ||
|
|
aa5c4d3c5e | ||
|
|
6a99445d97 | ||
|
|
c9880ef47d | ||
|
|
c16452dfcb | ||
|
|
af802c02fc | ||
|
|
8028aafeff | ||
|
|
b7469062a1 | ||
|
|
33b707aa68 | ||
|
|
cd3a1bebff | ||
|
|
794dd5a797 | ||
|
|
a1465432e8 | ||
|
|
e1f8af0909 | ||
|
|
88918be329 | ||
|
|
a3fc1dbff0 | ||
|
|
626c9825cc | ||
|
|
c10a84fc79 | ||
|
|
f14e4fe197 | ||
|
|
6eb218db5e | ||
|
|
0e77e053b0 | ||
|
|
b9a4128a3d | ||
|
|
16f480e1f3 | ||
|
|
7c42511133 | ||
|
|
1b252a84c2 | ||
|
|
bf833cadff | ||
|
|
b6f872882a | ||
|
|
a18d6fb441 | ||
|
|
922e703e81 | ||
|
|
d7f5817b8b | ||
|
|
92a8a4bfa6 | ||
|
|
b480151fc3 | ||
|
|
37bdbc1bd5 | ||
|
|
8eb669139b | ||
|
|
b485e8cacc | ||
|
|
c4bab61c47 | ||
|
|
72be417ff1 | ||
|
|
9be483d7a6 | ||
|
|
910f2cee2c | ||
|
|
1e47ace527 | ||
|
|
912b06b145 | ||
|
|
87d878e287 | ||
|
|
be886d6bce | ||
|
|
0683deb47e | ||
|
|
114bb22e27 | ||
|
|
c327ebc3df | ||
|
|
92cbd1c69c | ||
|
|
7242515e48 | ||
|
|
401d1a0ac2 | ||
|
|
863e042a37 | ||
|
|
39e6c45ec6 | ||
|
|
0d364d18c7 | ||
|
|
61444ea390 | ||
|
|
106a935efb | ||
|
|
d175d8a853 | ||
|
|
ce6d19a77a | ||
|
|
0a29273924 | ||
|
|
5ede64de58 | ||
|
|
224c6ad798 | ||
|
|
57b3a0dbe7 | ||
|
|
f381f708e0 | ||
|
|
63bf149546 | ||
|
|
cb5e671259 | ||
|
|
3e38173c4e | ||
|
|
efacb643fc | ||
|
|
1bd153ea0b | ||
|
|
bac3dc5f4c | ||
|
|
959a853d77 | ||
|
|
90bbdd563b | ||
|
|
e3c6d05a0a | ||
|
|
930b3d4538 | ||
|
|
74b78141b4 | ||
|
|
aa1108cd5b | ||
|
|
f24b1a9db3 | ||
|
|
71b191e740 | ||
|
|
8f6341b903 | ||
|
|
161586db7e | ||
|
|
052262bef9 | ||
|
|
a5a7144707 | ||
|
|
d945e0426d | ||
|
|
926970c4eb | ||
|
|
cce36e0f28 | ||
|
|
48c6dc5be5 | ||
|
|
c641830825 | ||
|
|
eba16cc15d | ||
|
|
bd88b8411e | ||
|
|
fc121e8750 | ||
|
|
d4142fe56a | ||
|
|
f76a3e72bb | ||
|
|
2d400ae7eb | ||
|
|
edf1632cab | ||
|
|
a648f1ee67 | ||
|
|
d004e6e86c | ||
|
|
fa321d3e8d | ||
|
|
e1e53d323f | ||
|
|
ccb18ca302 | ||
|
|
e170f36bc6 | ||
|
|
4bd9cc51ee | ||
|
|
43ffbda1a4 | ||
|
|
8240485fd1 | ||
|
|
7f053c0567 | ||
|
|
d2922eb0b7 | ||
|
|
fec10d453f | ||
|
|
162040545d | ||
|
|
f14577f8bf | ||
|
|
e5720bd1be | ||
|
|
6d4959bac8 | ||
|
|
d668128a34 | ||
|
|
f2af38da4c | ||
|
|
4776d18fd7 | ||
|
|
cdd0be7b78 | ||
|
|
cd505abb22 | ||
|
|
28439c010f | ||
|
|
e85f43beb1 | ||
|
|
a06cbc0840 | ||
|
|
adef9728f8 | ||
|
|
ff1b688b6e | ||
|
|
3e7574a927 | ||
|
|
f852ea90ad | ||
|
|
d8f6247c32 | ||
|
|
9dc28393a5 | ||
|
|
c442c98ecf | ||
|
|
71e0109927 | ||
|
|
9c7dd5ed1c | ||
|
|
83620848f2 | ||
|
|
d548a5b4f3 | ||
|
|
b6e5307755 | ||
|
|
4c5dc5a145 | ||
|
|
69ed9172b8 | ||
|
|
68551ae176 | ||
|
|
c97d9d35ba | ||
|
|
e86cc97cdf | ||
|
|
9bff8608c1 | ||
|
|
a10fe6994a | ||
|
|
67e6a37b59 | ||
|
|
3075a56735 | ||
|
|
7e4a862cc3 | ||
|
|
ed2078ee3b | ||
|
|
f99c23a622 | ||
|
|
41e7317764 | ||
|
|
e0a78c2399 | ||
|
|
95ad39d2d4 | ||
|
|
b831f827b1 | ||
|
|
c5d8413d9c | ||
|
|
4648ea3424 | ||
|
|
e05bd6f231 | ||
|
|
caadee7901 | ||
|
|
18c524117d | ||
|
|
ad30fb8b04 | ||
|
|
a8077965a9 | ||
|
|
532ab85ebb | ||
|
|
546227eb37 | ||
|
|
7ec3b262d3 | ||
|
|
c435000d24 | ||
|
|
eaa60c281e | ||
|
|
cd7cf63144 | ||
|
|
6a704aa079 | ||
|
|
d6b5a1cec8 | ||
|
|
9a24ad31cc | ||
|
|
9331630b54 | ||
|
|
904e869d7f | ||
|
|
307fa4f5e6 | ||
|
|
131d16d3ea | ||
|
|
dbc54c45dd | ||
|
|
a96a84d509 | ||
|
|
3eb8863f67 | ||
|
|
8737ab077b | ||
|
|
50bb6a1d19 | ||
|
|
4181b75af7 | ||
|
|
620705c87a | ||
|
|
50b7b93529 | ||
|
|
6f18f6bd5c | ||
|
|
01d256eeee | ||
|
|
a1405412a8 | ||
|
|
58589b3a15 | ||
|
|
2983266fdf | ||
|
|
e33df8f12d | ||
|
|
0e0e8e9d18 | ||
|
|
37f8b54752 | ||
|
|
e9a086ad23 | ||
|
|
7c06a8ac41 | ||
|
|
70801d958e | ||
|
|
cf3f95c952 | ||
|
|
d708616a6a | ||
|
|
17711bc5c9 | ||
|
|
1e2474921b | ||
|
|
3f37b2b728 | ||
|
|
18b11df672 | ||
|
|
c34f2d4da7 | ||
|
|
d61792581a | ||
|
|
76d9a511b8 | ||
|
|
4248326697 | ||
|
|
a540b38151 | ||
|
|
8fb5ef0c1d | ||
|
|
2da4979e59 | ||
|
|
0146e4a1dd | ||
|
|
34bdb72ffd | ||
|
|
2ef7c63047 | ||
|
|
95f5417761 | ||
|
|
4922ec4499 | ||
|
|
20d947a199 | ||
|
|
871dc90ee4 |
34
.github/workflows/build-app-beta.yaml
vendored
34
.github/workflows/build-app-beta.yaml
vendored
@@ -5,10 +5,10 @@ name: Electron app BETA
|
||||
'on':
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+'
|
||||
- v[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+
|
||||
jobs:
|
||||
build:
|
||||
runs-on: '${{ matrix.os }}'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
echo "PYTHON=/opt/homebrew/bin/python3.11" >> $GITHUB_ENV
|
||||
- name: Context
|
||||
env:
|
||||
GITHUB_CONTEXT: '${{ toJson(github) }}'
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
yarn printSecrets
|
||||
env:
|
||||
GIST_UPLOAD_SECRET: '${{secrets.GIST_UPLOAD_SECRET}}'
|
||||
GIST_UPLOAD_SECRET: ${{secrets.GIST_UPLOAD_SECRET}}
|
||||
- name: fillPackagedPlugins
|
||||
run: |
|
||||
|
||||
@@ -71,16 +71,16 @@ jobs:
|
||||
|
||||
yarn run build:app
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GH_TOKEN }}'
|
||||
WIN_CSC_LINK: '${{ secrets.WINCERT_2025 }}'
|
||||
WIN_CSC_KEY_PASSWORD: '${{ secrets.WINCERT_2025_PASSWORD }}'
|
||||
CSC_LINK: '${{ secrets.APPLECERT_CERTIFICATE }}'
|
||||
CSC_KEY_PASSWORD: '${{ secrets.APPLECERT_PASSWORD }}'
|
||||
APPLE_ID: '${{ secrets.APPLE_ID }}'
|
||||
APPLE_TEAM_ID: '${{ secrets.APPLE_TEAM_ID }}'
|
||||
APPLE_ID_PASSWORD: '${{ secrets.APPLE_ID_PASSWORD }}'
|
||||
SNAPCRAFT_STORE_CREDENTIALS: '${{secrets.SNAPCRAFT_LOGIN}}'
|
||||
APPLE_APP_SPECIFIC_PASSWORD: '${{secrets.APPLE_APP_SPECIFIC_PASSWORD}}'
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
WIN_CSC_LINK: ${{ secrets.WINCERT_2025 }}
|
||||
WIN_CSC_KEY_PASSWORD: ${{ secrets.WINCERT_2025_PASSWORD }}
|
||||
CSC_LINK: ${{ secrets.APPLECERT_CERTIFICATE }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.APPLECERT_PASSWORD }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{secrets.SNAPCRAFT_LOGIN}}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{secrets.APPLE_APP_SPECIFIC_PASSWORD}}
|
||||
- name: Copy artifacts
|
||||
run: |
|
||||
mkdir artifacts
|
||||
@@ -111,16 +111,16 @@ jobs:
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: '${{ matrix.os }}'
|
||||
name: ${{ matrix.os }}
|
||||
path: artifacts
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: 'startsWith(github.ref, ''refs/tags/'')'
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: artifacts/**
|
||||
prerelease: true
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Print content of notarization-error.log
|
||||
if: failure() && matrix.os == 'macos-14'
|
||||
run: |
|
||||
|
||||
5
.github/workflows/build-app-check.yaml
vendored
5
.github/workflows/build-app-check.yaml
vendored
@@ -104,11 +104,6 @@ jobs:
|
||||
|
||||
mv app/dist/*.yml artifacts/ || true
|
||||
rm artifacts/builder-debug.yml
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.os }}
|
||||
path: artifacts
|
||||
- name: Print content of notarization-error.log
|
||||
if: failure() && matrix.os == 'macos-14'
|
||||
run: |
|
||||
|
||||
38
.github/workflows/build-app-pro-beta.yaml
vendored
38
.github/workflows/build-app-pro-beta.yaml
vendored
@@ -5,10 +5,10 @@ name: Electron app PREMIUM BETA
|
||||
'on':
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+-premium-beta.[0-9]+'
|
||||
- v[0-9]+.[0-9]+.[0-9]+-premium-beta.[0-9]+
|
||||
jobs:
|
||||
build:
|
||||
runs-on: '${{ matrix.os }}'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
echo "PYTHON=/opt/homebrew/bin/python3.11" >> $GITHUB_ENV
|
||||
- name: Context
|
||||
env:
|
||||
GITHUB_CONTEXT: '${{ toJson(github) }}'
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
@@ -37,9 +37,9 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: '${{ secrets.GH_TOKEN }}'
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: ecea1eef17c69c56b0633317e24a68c5220a4810
|
||||
ref: 458d8843318c2f65aae6524bbea30513d88f4bf6
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
|
||||
yarn printSecrets
|
||||
env:
|
||||
GIST_UPLOAD_SECRET: '${{secrets.GIST_UPLOAD_SECRET}}'
|
||||
GIST_UPLOAD_SECRET: ${{secrets.GIST_UPLOAD_SECRET}}
|
||||
- name: fillPackagedPlugins
|
||||
run: |
|
||||
cd ..
|
||||
@@ -102,16 +102,16 @@ jobs:
|
||||
|
||||
yarn run build:app
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GH_TOKEN }}'
|
||||
WIN_CSC_LINK: '${{ secrets.WINCERT_2025 }}'
|
||||
WIN_CSC_KEY_PASSWORD: '${{ secrets.WINCERT_2025_PASSWORD }}'
|
||||
CSC_LINK: '${{ secrets.APPLECERT_CERTIFICATE }}'
|
||||
CSC_KEY_PASSWORD: '${{ secrets.APPLECERT_PASSWORD }}'
|
||||
APPLE_ID: '${{ secrets.APPLE_ID }}'
|
||||
APPLE_TEAM_ID: '${{ secrets.APPLE_TEAM_ID }}'
|
||||
APPLE_ID_PASSWORD: '${{ secrets.APPLE_ID_PASSWORD }}'
|
||||
SNAPCRAFT_STORE_CREDENTIALS: '${{secrets.SNAPCRAFT_LOGIN}}'
|
||||
APPLE_APP_SPECIFIC_PASSWORD: '${{secrets.APPLE_APP_SPECIFIC_PASSWORD}}'
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
WIN_CSC_LINK: ${{ secrets.WINCERT_2025 }}
|
||||
WIN_CSC_KEY_PASSWORD: ${{ secrets.WINCERT_2025_PASSWORD }}
|
||||
CSC_LINK: ${{ secrets.APPLECERT_CERTIFICATE }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.APPLECERT_PASSWORD }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{secrets.SNAPCRAFT_LOGIN}}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{secrets.APPLE_APP_SPECIFIC_PASSWORD}}
|
||||
- name: Copy artifacts
|
||||
run: |
|
||||
mkdir artifacts
|
||||
@@ -142,16 +142,16 @@ jobs:
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: '${{ matrix.os }}'
|
||||
name: ${{ matrix.os }}
|
||||
path: artifacts
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: 'startsWith(github.ref, ''refs/tags/'')'
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: artifacts/**
|
||||
prerelease: true
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Print content of notarization-error.log
|
||||
if: failure() && matrix.os == 'macos-14'
|
||||
run: |
|
||||
|
||||
38
.github/workflows/build-app-pro.yaml
vendored
38
.github/workflows/build-app-pro.yaml
vendored
@@ -5,10 +5,10 @@ name: Electron app PREMIUM
|
||||
'on':
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
- v[0-9]+.[0-9]+.[0-9]+
|
||||
jobs:
|
||||
build:
|
||||
runs-on: '${{ matrix.os }}'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
echo "PYTHON=/opt/homebrew/bin/python3.11" >> $GITHUB_ENV
|
||||
- name: Context
|
||||
env:
|
||||
GITHUB_CONTEXT: '${{ toJson(github) }}'
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
@@ -37,9 +37,9 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: '${{ secrets.GH_TOKEN }}'
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: ecea1eef17c69c56b0633317e24a68c5220a4810
|
||||
ref: 458d8843318c2f65aae6524bbea30513d88f4bf6
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
|
||||
yarn printSecrets
|
||||
env:
|
||||
GIST_UPLOAD_SECRET: '${{secrets.GIST_UPLOAD_SECRET}}'
|
||||
GIST_UPLOAD_SECRET: ${{secrets.GIST_UPLOAD_SECRET}}
|
||||
- name: fillPackagedPlugins
|
||||
run: |
|
||||
cd ..
|
||||
@@ -102,16 +102,16 @@ jobs:
|
||||
|
||||
yarn run build:app
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GH_TOKEN }}'
|
||||
WIN_CSC_LINK: '${{ secrets.WINCERT_2025 }}'
|
||||
WIN_CSC_KEY_PASSWORD: '${{ secrets.WINCERT_2025_PASSWORD }}'
|
||||
CSC_LINK: '${{ secrets.APPLECERT_CERTIFICATE }}'
|
||||
CSC_KEY_PASSWORD: '${{ secrets.APPLECERT_PASSWORD }}'
|
||||
APPLE_ID: '${{ secrets.APPLE_ID }}'
|
||||
APPLE_TEAM_ID: '${{ secrets.APPLE_TEAM_ID }}'
|
||||
APPLE_ID_PASSWORD: '${{ secrets.APPLE_ID_PASSWORD }}'
|
||||
SNAPCRAFT_STORE_CREDENTIALS: '${{secrets.SNAPCRAFT_LOGIN}}'
|
||||
APPLE_APP_SPECIFIC_PASSWORD: '${{secrets.APPLE_APP_SPECIFIC_PASSWORD}}'
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
WIN_CSC_LINK: ${{ secrets.WINCERT_2025 }}
|
||||
WIN_CSC_KEY_PASSWORD: ${{ secrets.WINCERT_2025_PASSWORD }}
|
||||
CSC_LINK: ${{ secrets.APPLECERT_CERTIFICATE }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.APPLECERT_PASSWORD }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{secrets.SNAPCRAFT_LOGIN}}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{secrets.APPLE_APP_SPECIFIC_PASSWORD}}
|
||||
- name: Copy artifacts
|
||||
run: |
|
||||
mkdir artifacts
|
||||
@@ -142,16 +142,16 @@ jobs:
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: '${{ matrix.os }}'
|
||||
name: ${{ matrix.os }}
|
||||
path: artifacts
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: 'startsWith(github.ref, ''refs/tags/'')'
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: artifacts/**
|
||||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Print content of notarization-error.log
|
||||
if: failure() && matrix.os == 'macos-14'
|
||||
run: |
|
||||
|
||||
34
.github/workflows/build-app.yaml
vendored
34
.github/workflows/build-app.yaml
vendored
@@ -5,10 +5,10 @@ name: Electron app
|
||||
'on':
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
- v[0-9]+.[0-9]+.[0-9]+
|
||||
jobs:
|
||||
build:
|
||||
runs-on: '${{ matrix.os }}'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
echo "PYTHON=/opt/homebrew/bin/python3.11" >> $GITHUB_ENV
|
||||
- name: Context
|
||||
env:
|
||||
GITHUB_CONTEXT: '${{ toJson(github) }}'
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
|
||||
yarn printSecrets
|
||||
env:
|
||||
GIST_UPLOAD_SECRET: '${{secrets.GIST_UPLOAD_SECRET}}'
|
||||
GIST_UPLOAD_SECRET: ${{secrets.GIST_UPLOAD_SECRET}}
|
||||
- name: fillPackagedPlugins
|
||||
run: |
|
||||
|
||||
@@ -67,16 +67,16 @@ jobs:
|
||||
|
||||
yarn run build:app
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GH_TOKEN }}'
|
||||
WIN_CSC_LINK: '${{ secrets.WINCERT_2025 }}'
|
||||
WIN_CSC_KEY_PASSWORD: '${{ secrets.WINCERT_2025_PASSWORD }}'
|
||||
CSC_LINK: '${{ secrets.APPLECERT_CERTIFICATE }}'
|
||||
CSC_KEY_PASSWORD: '${{ secrets.APPLECERT_PASSWORD }}'
|
||||
APPLE_ID: '${{ secrets.APPLE_ID }}'
|
||||
APPLE_TEAM_ID: '${{ secrets.APPLE_TEAM_ID }}'
|
||||
APPLE_ID_PASSWORD: '${{ secrets.APPLE_ID_PASSWORD }}'
|
||||
SNAPCRAFT_STORE_CREDENTIALS: '${{secrets.SNAPCRAFT_LOGIN}}'
|
||||
APPLE_APP_SPECIFIC_PASSWORD: '${{secrets.APPLE_APP_SPECIFIC_PASSWORD}}'
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
WIN_CSC_LINK: ${{ secrets.WINCERT_2025 }}
|
||||
WIN_CSC_KEY_PASSWORD: ${{ secrets.WINCERT_2025_PASSWORD }}
|
||||
CSC_LINK: ${{ secrets.APPLECERT_CERTIFICATE }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.APPLECERT_PASSWORD }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{secrets.SNAPCRAFT_LOGIN}}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{secrets.APPLE_APP_SPECIFIC_PASSWORD}}
|
||||
- name: generatePadFile
|
||||
run: |
|
||||
yarn generatePadFile
|
||||
@@ -114,16 +114,16 @@ jobs:
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: '${{ matrix.os }}'
|
||||
name: ${{ matrix.os }}
|
||||
path: artifacts
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: 'startsWith(github.ref, ''refs/tags/'')'
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: artifacts/**
|
||||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Print content of notarization-error.log
|
||||
if: failure() && matrix.os == 'macos-14'
|
||||
run: |
|
||||
|
||||
46
.github/workflows/build-cloud-pro.yaml
vendored
46
.github/workflows/build-cloud-pro.yaml
vendored
@@ -5,11 +5,11 @@ name: Cloud images PREMIUM
|
||||
'on':
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+-packer-beta.[0-9]+'
|
||||
- v[0-9]+.[0-9]+.[0-9]+
|
||||
- v[0-9]+.[0-9]+.[0-9]+-packer-beta.[0-9]+
|
||||
jobs:
|
||||
build:
|
||||
runs-on: '${{ matrix.os }}'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
- name: Context
|
||||
env:
|
||||
GITHUB_CONTEXT: '${{ toJson(github) }}'
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
@@ -37,9 +37,9 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: '${{ secrets.GH_TOKEN }}'
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: ecea1eef17c69c56b0633317e24a68c5220a4810
|
||||
ref: 458d8843318c2f65aae6524bbea30513d88f4bf6
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
cd dbgate-merged
|
||||
yarn printSecrets
|
||||
env:
|
||||
GIST_UPLOAD_SECRET: '${{secrets.GIST_UPLOAD_SECRET}}'
|
||||
GIST_UPLOAD_SECRET: ${{secrets.GIST_UPLOAD_SECRET}}
|
||||
- name: Prepare packer build
|
||||
run: |
|
||||
cd ..
|
||||
@@ -87,16 +87,16 @@ jobs:
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: '${{ matrix.os }}'
|
||||
name: ${{ matrix.os }}
|
||||
path: artifacts
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: 'startsWith(github.ref, ''refs/tags/'')'
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: artifacts/**
|
||||
prerelease: true
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run `packer init` for Azure
|
||||
run: |
|
||||
cd ../dbgate-merged/packer
|
||||
@@ -110,33 +110,33 @@ jobs:
|
||||
cd ../dbgate-merged/packer
|
||||
packer init ./aws-ubuntu.pkr.hcl
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: '${{secrets.AWS_ACCESS_KEY_ID}}'
|
||||
AWS_SECRET_ACCESS_KEY: '${{secrets.AWS_SECRET_ACCESS_KEY}}'
|
||||
AWS_DEFAULT_REGION: '${{secrets.AWS_DEFAULT_REGION}}'
|
||||
AWS_ACCESS_KEY_ID: ${{secrets.AWS_ACCESS_KEY_ID}}
|
||||
AWS_SECRET_ACCESS_KEY: ${{secrets.AWS_SECRET_ACCESS_KEY}}
|
||||
AWS_DEFAULT_REGION: ${{secrets.AWS_DEFAULT_REGION}}
|
||||
- name: Run `packer build` for AWS
|
||||
run: |
|
||||
cd ../dbgate-merged/packer
|
||||
packer build ./aws-ubuntu.pkr.hcl
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: '${{secrets.AWS_ACCESS_KEY_ID}}'
|
||||
AWS_SECRET_ACCESS_KEY: '${{secrets.AWS_SECRET_ACCESS_KEY}}'
|
||||
AWS_DEFAULT_REGION: '${{secrets.AWS_DEFAULT_REGION}}'
|
||||
AWS_ACCESS_KEY_ID: ${{secrets.AWS_ACCESS_KEY_ID}}
|
||||
AWS_SECRET_ACCESS_KEY: ${{secrets.AWS_SECRET_ACCESS_KEY}}
|
||||
AWS_DEFAULT_REGION: ${{secrets.AWS_DEFAULT_REGION}}
|
||||
- name: Delete old Azure VMs
|
||||
run: |
|
||||
cd ../dbgate-merged/packer
|
||||
chmod +x delete-old-azure-images.sh
|
||||
./delete-old-azure-images.sh
|
||||
env:
|
||||
AZURE_CLIENT_ID: '${{secrets.AZURE_CLIENT_ID}}'
|
||||
AZURE_CLIENT_SECRET: '${{secrets.AZURE_CLIENT_SECRET}}'
|
||||
AZURE_TENANT_ID: '${{secrets.AZURE_TENANT_ID}}'
|
||||
AZURE_SUBSCRIPTION_ID: '${{secrets.AZURE_SUBSCRIPTION_ID}}'
|
||||
AZURE_CLIENT_ID: ${{secrets.AZURE_CLIENT_ID}}
|
||||
AZURE_CLIENT_SECRET: ${{secrets.AZURE_CLIENT_SECRET}}
|
||||
AZURE_TENANT_ID: ${{secrets.AZURE_TENANT_ID}}
|
||||
AZURE_SUBSCRIPTION_ID: ${{secrets.AZURE_SUBSCRIPTION_ID}}
|
||||
- name: Delete old AMIs (AWS)
|
||||
run: |
|
||||
cd ../dbgate-merged/packer
|
||||
chmod +x delete-old-amis.sh
|
||||
./delete-old-amis.sh
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: '${{secrets.AWS_ACCESS_KEY_ID}}'
|
||||
AWS_SECRET_ACCESS_KEY: '${{secrets.AWS_SECRET_ACCESS_KEY}}'
|
||||
AWS_DEFAULT_REGION: '${{secrets.AWS_DEFAULT_REGION}}'
|
||||
AWS_ACCESS_KEY_ID: ${{secrets.AWS_ACCESS_KEY_ID}}
|
||||
AWS_SECRET_ACCESS_KEY: ${{secrets.AWS_SECRET_ACCESS_KEY}}
|
||||
AWS_DEFAULT_REGION: ${{secrets.AWS_DEFAULT_REGION}}
|
||||
|
||||
22
.github/workflows/build-docker-pro.yaml
vendored
22
.github/workflows/build-docker-pro.yaml
vendored
@@ -5,11 +5,11 @@ name: Docker image PREMIUM
|
||||
'on':
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+-premium-beta.[0-9]+'
|
||||
- v[0-9]+.[0-9]+.[0-9]+
|
||||
- v[0-9]+.[0-9]+.[0-9]+-premium-beta.[0-9]+
|
||||
jobs:
|
||||
build:
|
||||
runs-on: '${{ matrix.os }}'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
- name: Context
|
||||
env:
|
||||
GITHUB_CONTEXT: '${{ toJson(github) }}'
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
@@ -42,9 +42,9 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: '${{ secrets.GH_TOKEN }}'
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: ecea1eef17c69c56b0633317e24a68c5220a4810
|
||||
ref: 458d8843318c2f65aae6524bbea30513d88f4bf6
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
|
||||
yarn printSecrets
|
||||
env:
|
||||
GIST_UPLOAD_SECRET: '${{secrets.GIST_UPLOAD_SECRET}}'
|
||||
GIST_UPLOAD_SECRET: ${{secrets.GIST_UPLOAD_SECRET}}
|
||||
- name: Prepare docker image
|
||||
run: |
|
||||
cd ..
|
||||
@@ -97,12 +97,12 @@ jobs:
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: '${{ secrets.DOCKER_USERNAME }}'
|
||||
password: '${{ secrets.DOCKER_PASSWORD }}'
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
push: true
|
||||
context: ../dbgate-merged/docker
|
||||
tags: '${{ steps.meta.outputs.tags }}'
|
||||
platforms: 'linux/amd64,linux/arm64'
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
22
.github/workflows/build-docker.yaml
vendored
22
.github/workflows/build-docker.yaml
vendored
@@ -5,11 +5,11 @@ name: Docker image Community
|
||||
'on':
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+'
|
||||
- v[0-9]+.[0-9]+.[0-9]+
|
||||
- v[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+
|
||||
jobs:
|
||||
build:
|
||||
runs-on: '${{ matrix.os }}'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
- name: Context
|
||||
env:
|
||||
GITHUB_CONTEXT: '${{ toJson(github) }}'
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
|
||||
yarn printSecrets
|
||||
env:
|
||||
GIST_UPLOAD_SECRET: '${{secrets.GIST_UPLOAD_SECRET}}'
|
||||
GIST_UPLOAD_SECRET: ${{secrets.GIST_UPLOAD_SECRET}}
|
||||
- name: Prepare docker image
|
||||
run: |
|
||||
|
||||
@@ -82,20 +82,20 @@ jobs:
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: '${{ secrets.DOCKER_USERNAME }}'
|
||||
password: '${{ secrets.DOCKER_PASSWORD }}'
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
push: true
|
||||
context: ./docker
|
||||
tags: '${{ steps.meta.outputs.tags }}'
|
||||
platforms: 'linux/amd64,linux/arm64,linux/arm/v7'
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
- name: Build and push alpine
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
push: true
|
||||
context: ./docker
|
||||
file: ./docker/Dockerfile-alpine
|
||||
tags: '${{ steps.alpmeta.outputs.tags }}'
|
||||
platforms: 'linux/amd64,linux/arm64,linux/arm/v7'
|
||||
tags: ${{ steps.alpmeta.outputs.tags }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
|
||||
16
.github/workflows/build-npm-pro.yaml
vendored
16
.github/workflows/build-npm-pro.yaml
vendored
@@ -5,11 +5,11 @@ name: NPM packages PREMIUM
|
||||
'on':
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+-alpha.[0-9]+'
|
||||
- v[0-9]+.[0-9]+.[0-9]+
|
||||
- v[0-9]+.[0-9]+.[0-9]+-alpha.[0-9]+
|
||||
jobs:
|
||||
build:
|
||||
runs-on: '${{ matrix.os }}'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
- name: Context
|
||||
env:
|
||||
GITHUB_CONTEXT: '${{ toJson(github) }}'
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
@@ -30,9 +30,9 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: '${{ secrets.GH_TOKEN }}'
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: ecea1eef17c69c56b0633317e24a68c5220a4810
|
||||
ref: 458d8843318c2f65aae6524bbea30513d88f4bf6
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
node adjustNpmPackageJsonPremium
|
||||
- name: Configure NPM token
|
||||
env:
|
||||
NPM_TOKEN: '${{ secrets.NPM_TOKEN }}'
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
cd ..
|
||||
cd dbgate-merged
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
cd dbgate-merged
|
||||
yarn printSecrets
|
||||
env:
|
||||
GIST_UPLOAD_SECRET: '${{secrets.GIST_UPLOAD_SECRET}}'
|
||||
GIST_UPLOAD_SECRET: ${{secrets.GIST_UPLOAD_SECRET}}
|
||||
- name: Publish dbgate-api-premium
|
||||
run: |
|
||||
cd ..
|
||||
|
||||
12
.github/workflows/build-npm.yaml
vendored
12
.github/workflows/build-npm.yaml
vendored
@@ -5,11 +5,11 @@ name: NPM packages
|
||||
'on':
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+-alpha.[0-9]+'
|
||||
- v[0-9]+.[0-9]+.[0-9]+
|
||||
- v[0-9]+.[0-9]+.[0-9]+-alpha.[0-9]+
|
||||
jobs:
|
||||
build:
|
||||
runs-on: '${{ matrix.os }}'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
- name: Context
|
||||
env:
|
||||
GITHUB_CONTEXT: '${{ toJson(github) }}'
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
node-version: 18.x
|
||||
- name: Configure NPM token
|
||||
env:
|
||||
NPM_TOKEN: '${{ secrets.NPM_TOKEN }}'
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
npm config set '//registry.npmjs.org/:_authToken' "${NPM_TOKEN}"
|
||||
- name: yarn install
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
run: |
|
||||
yarn printSecrets
|
||||
env:
|
||||
GIST_UPLOAD_SECRET: '${{secrets.GIST_UPLOAD_SECRET}}'
|
||||
GIST_UPLOAD_SECRET: ${{secrets.GIST_UPLOAD_SECRET}}
|
||||
- name: Publish types
|
||||
working-directory: packages/types
|
||||
run: |
|
||||
|
||||
4
.github/workflows/build-test-containers.yaml
vendored
4
.github/workflows/build-test-containers.yaml
vendored
@@ -30,8 +30,8 @@ jobs:
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: '${{ github.actor }}'
|
||||
password: '${{ secrets.GITHUB_TOKEN }}'
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Push mysql-ssh-login to GHCR
|
||||
run: |
|
||||
docker tag dbgate/mysql-ssh-login:latest ghcr.io/dbgate/mysql-ssh-login:latest
|
||||
|
||||
2
.github/workflows/diflow.yaml
vendored
2
.github/workflows/diflow.yaml
vendored
@@ -33,4 +33,4 @@ jobs:
|
||||
cd diflow
|
||||
node dist/diflow.js sync -r https://DIFLOW_GIT_SECRET@github.com/dbgate/dbgate-diflow-config.git -b master
|
||||
env:
|
||||
DIFLOW_GIT_SECRET: '${{ secrets.DIFLOW_GIT_SECRET }}'
|
||||
DIFLOW_GIT_SECRET: ${{ secrets.DIFLOW_GIT_SECRET }}
|
||||
|
||||
18
.github/workflows/e2e-pro.yaml
vendored
18
.github/workflows/e2e-pro.yaml
vendored
@@ -24,9 +24,9 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: '${{ secrets.GH_TOKEN }}'
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: ecea1eef17c69c56b0633317e24a68c5220a4810
|
||||
ref: 458d8843318c2f65aae6524bbea30513d88f4bf6
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
name: screenshots
|
||||
path: screenshots
|
||||
- name: Push E2E screenshots
|
||||
if: '${{ github.ref_name == ''master'' }}'
|
||||
if: ${{ github.ref_name == 'master' }}
|
||||
run: |
|
||||
git config --global user.email "info@dbgate.info"
|
||||
git config --global user.name "GitHub Actions"
|
||||
@@ -89,25 +89,25 @@ jobs:
|
||||
ports:
|
||||
- '16000:5432'
|
||||
mysql-cypress:
|
||||
image: 'mysql:8.0.18'
|
||||
image: mysql:8.0.18
|
||||
ports:
|
||||
- '16004:3306'
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: Pwd2020Db
|
||||
mysql-ssh-login:
|
||||
image: 'ghcr.io/dbgate/mysql-ssh-login:latest'
|
||||
image: ghcr.io/dbgate/mysql-ssh-login:latest
|
||||
ports:
|
||||
- '16012:22'
|
||||
mysql-ssh-keyfile:
|
||||
image: 'ghcr.io/dbgate/mysql-ssh-keyfile:latest'
|
||||
image: ghcr.io/dbgate/mysql-ssh-keyfile:latest
|
||||
ports:
|
||||
- '16008:22'
|
||||
dex:
|
||||
image: 'ghcr.io/dbgate/dex:latest'
|
||||
image: ghcr.io/dbgate/dex:latest
|
||||
ports:
|
||||
- '16009:5556'
|
||||
mongo:
|
||||
image: 'mongo:4.0.12'
|
||||
image: mongo:4.0.12
|
||||
env:
|
||||
MONGO_INITDB_ROOT_USERNAME: root
|
||||
MONGO_INITDB_ROOT_PASSWORD: Pwd2020Db
|
||||
@@ -126,7 +126,7 @@ jobs:
|
||||
SA_PASSWORD: Pwd2020Db
|
||||
MSSQL_PID: Express
|
||||
oracle:
|
||||
image: 'gvenzl/oracle-xe:21-slim'
|
||||
image: gvenzl/oracle-xe:21-slim
|
||||
env:
|
||||
ORACLE_PASSWORD: Pwd2020Db
|
||||
ports:
|
||||
|
||||
4
.github/workflows/process-templates.yaml
vendored
4
.github/workflows/process-templates.yaml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
token: '${{ secrets.WORKFLOW_CHANGE_ACCESS_TOKEN }}'
|
||||
token: ${{ secrets.WORKFLOW_CHANGE_ACCESS_TOKEN }}
|
||||
- name: git pull
|
||||
run: |
|
||||
git pull
|
||||
@@ -47,5 +47,5 @@ jobs:
|
||||
- name: Push changes
|
||||
uses: ad-m/github-push-action@v0.6.0
|
||||
with:
|
||||
github_token: '${{ secrets.WORKFLOW_CHANGE_ACCESS_TOKEN }}'
|
||||
github_token: ${{ secrets.WORKFLOW_CHANGE_ACCESS_TOKEN }}
|
||||
branch: master
|
||||
|
||||
18
.github/workflows/run-tests.yaml
vendored
18
.github/workflows/run-tests.yaml
vendored
@@ -45,19 +45,19 @@ jobs:
|
||||
- uses: tanmen/jest-reporter@v1
|
||||
if: always()
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
result-file: integration-tests/result.json
|
||||
action-name: Integration tests
|
||||
- uses: tanmen/jest-reporter@v1
|
||||
if: always()
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
result-file: packages/filterparser/result.json
|
||||
action-name: Filter parser test results
|
||||
- uses: tanmen/jest-reporter@v1
|
||||
if: always()
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
result-file: packages/datalib/result.json
|
||||
action-name: Datalib (perspectives) test results
|
||||
services:
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
ports:
|
||||
- '15000:5432'
|
||||
mysql-integr:
|
||||
image: 'mysql:8.0.18'
|
||||
image: mysql:8.0.18
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: Pwd2020Db
|
||||
ports:
|
||||
@@ -83,27 +83,27 @@ jobs:
|
||||
ports:
|
||||
- '15002:1433'
|
||||
clickhouse-integr:
|
||||
image: 'bitnami/clickhouse:24.8.4'
|
||||
image: bitnami/clickhouse:24.8.4
|
||||
env:
|
||||
CLICKHOUSE_ADMIN_PASSWORD: Pwd2020Db
|
||||
ports:
|
||||
- '15005:8123'
|
||||
oracle-integr:
|
||||
image: 'gvenzl/oracle-xe:21-slim'
|
||||
image: gvenzl/oracle-xe:21-slim
|
||||
env:
|
||||
ORACLE_PASSWORD: Pwd2020Db
|
||||
ports:
|
||||
- '15006:1521'
|
||||
cassandradb:
|
||||
image: 'cassandra:5.0.2'
|
||||
image: cassandra:5.0.2
|
||||
ports:
|
||||
- '15942:9042'
|
||||
libsql:
|
||||
image: 'ghcr.io/tursodatabase/libsql-server:latest'
|
||||
image: ghcr.io/tursodatabase/libsql-server:latest
|
||||
ports:
|
||||
- '8080:8080'
|
||||
firebird:
|
||||
image: 'firebirdsql/firebird:latest'
|
||||
image: firebirdsql/firebird:latest
|
||||
env:
|
||||
FIREBIRD_DATABASE: mydatabase.fdb
|
||||
FIREBIRD_USER: dbuser
|
||||
|
||||
55
CHANGELOG.md
55
CHANGELOG.md
@@ -8,6 +8,61 @@ Builds:
|
||||
- linux - application for linux
|
||||
- win - application for Windows
|
||||
|
||||
## 6.5.6
|
||||
- ADDED: New object window - quick access to most common functions
|
||||
- ADDED: Possibility to disable split query by empty line #1162
|
||||
- ADDED: Possibility to opt out authentication #1152
|
||||
- FIXED: Separate schema mode now works in Team Premium edition
|
||||
- FIXED: Handled situation, when user enters expired license, which is already prolonged
|
||||
- FIXED: Fixed some minor problems of charts
|
||||
|
||||
## 6.5.5
|
||||
- ADDED: Administer cloud folder window
|
||||
- CHANGED: Cloud menu redesign
|
||||
- ADDED: Audit log (for Team Premium edition)
|
||||
- ADDED: Added new timeline chart type (line chart with time axis)
|
||||
- ADDED: Chart grouping (more measure determined from data)
|
||||
- CHANGED: Improved chart autodetection - string X axis (with bar type), COUNT as measure, split different measures
|
||||
- ADDED: Added chart data type detection
|
||||
- FIXED: Fixed chart displaying problems
|
||||
- FIXED: Fixed exporting chart to HTML
|
||||
- CHANGED: Choose COUNT measure without selecting underlying ID field (use virtual __count)
|
||||
- FIXED: Problems with authentification administration, especially for Postgres storage
|
||||
- CHANGED: Anonymous autentification (in Team Premium) is now by default disabled
|
||||
|
||||
## 6.5.3
|
||||
- CHANGED: Improved DbGate Cloud sign-in workflow
|
||||
- FIXED: Some fixes and error handling in new charts engine
|
||||
- ADDED: Charts - ability to choose aggregate function
|
||||
- CHANGED: Improved About window
|
||||
|
||||
## 6.5.2
|
||||
- CHANGED: Autodetecting charts is disabled by default #1145
|
||||
- CHANGED: Improved chart displaying workflow
|
||||
- ADDED: Ability to close chart
|
||||
|
||||
## 6.5.1
|
||||
- FIXED: DbGate Cloud e-mail sign-in method for desktop clients
|
||||
|
||||
## 6.5.0
|
||||
- ADDED: DbGate cloud - online storage for connections, SQL scripts and other objects
|
||||
- ADDED: Public knowledge base - common SQL scripts for specific DB engines (table sizes, index stats etc.)
|
||||
- ADDED: Query results could be visualised in charts (Premium)
|
||||
- REMOVED: Chart from selection, active charts - replaced by query result charts
|
||||
- ADDED: FirebirdSQL support
|
||||
- ADDED: SQL front matter - properties of SQL script
|
||||
- ADDED: Auto-execute SQL script on open (saved in SQL front matter)
|
||||
- CHANGED: Smaller widget icon panel
|
||||
- CHANGED: Applications and Single-connection mode removed from widget icon panel
|
||||
- CHANGED: Temporarily disabled MongoDB profiler support
|
||||
- FIXED: Pie chart distorted if settings change #838
|
||||
- FIXED: SQL server generated insert statement should exclude computed and timestamp columns #1111
|
||||
- ADDED: Added option "Show all columns when searching" #1118
|
||||
- ADDED: Copy cells/rows (e.g. column names) from Structure view #1119
|
||||
- ADDED: Setting "Show table aliases in code completion" #1122
|
||||
- FIXED: Vulnerability - check file paths in web version
|
||||
- FIXED: Very slow render of tables with very log cells
|
||||
|
||||
## 6.4.2
|
||||
|
||||
- ADDED: Source label to docker container #1105
|
||||
|
||||
@@ -38,6 +38,7 @@ DbGate is licensed under GPL-3.0 license and is free to use for any purpose.
|
||||
* Apache Cassandra
|
||||
* libSQL/Turso (Premium)
|
||||
* DuckDB
|
||||
* Firebird
|
||||
|
||||
|
||||
<a href="https://raw.githubusercontent.com/dbgate/dbgate/master/img/screenshot1.png">
|
||||
@@ -88,10 +89,12 @@ DbGate is licensed under GPL-3.0 license and is free to use for any purpose.
|
||||
Any contributions are welcome. If you want to contribute without coding, consider following:
|
||||
|
||||
* Tell your friends about DbGate or share on social networks - when more people will use DbGate, it will grow to be better
|
||||
* Purchase a [DbGate Premium](https://dbgate.io/purchase/premium/) liocense
|
||||
* Write review on [Slant.co](https://www.slant.co/improve/options/41086/~dbgate-review) or [G2](https://www.g2.com/products/dbgate/reviews)
|
||||
* Create issue, if you find problem in app, or you have idea to new feature. If issue already exists, you could leave comment on it, to prioritise most wanted issues
|
||||
* Create some tutorial video on [youtube](https://www.youtube.com/playlist?list=PLCo7KjCVXhr0RfUSjM9wJMsp_ShL1q61A)
|
||||
* Become a backer on [GitHub sponsors](https://github.com/sponsors/dbgate) or [Open collective](https://opencollective.com/dbgate)
|
||||
* Add a SQL script to [Public Knowledge Base](https://github.com/dbgate/dbgate-knowledge-base)
|
||||
* Where a small coding is acceptable for you, you could [create plugin](https://docs.dbgate.io/plugin-development). Plugins for new themes can be created actually without JS coding
|
||||
|
||||
Thank you!
|
||||
|
||||
@@ -7,7 +7,9 @@ const path = require('path');
|
||||
|
||||
module.exports = defineConfig({
|
||||
e2e: {
|
||||
// baseUrl: 'http://localhost:3000',
|
||||
// trashAssetsBeforeRuns: false,
|
||||
chromeWebSecurity: false,
|
||||
|
||||
setupNodeEvents(on, config) {
|
||||
// implement node event listeners here
|
||||
@@ -40,6 +42,12 @@ module.exports = defineConfig({
|
||||
case 'multi-sql':
|
||||
serverProcess = exec('yarn start:multi-sql');
|
||||
break;
|
||||
case 'cloud':
|
||||
serverProcess = exec('yarn start:cloud');
|
||||
break;
|
||||
case 'charts':
|
||||
serverProcess = exec('yarn start:charts');
|
||||
break;
|
||||
}
|
||||
|
||||
await waitOn({ resources: ['http://localhost:3000'] });
|
||||
|
||||
@@ -191,7 +191,8 @@ describe('Data browser data', () => {
|
||||
it('Query editor - join wizard', () => {
|
||||
cy.contains('MySql-connection').click();
|
||||
cy.contains('MyChinook').click();
|
||||
cy.testid('TabsPanel_buttonNewQuery').click();
|
||||
cy.testid('TabsPanel_buttonNewObject').click();
|
||||
cy.testid('NewObjectModal_query').click();
|
||||
cy.wait(1000);
|
||||
cy.get('body').realType('select * from Invoice');
|
||||
cy.get('body').realPress('{enter}');
|
||||
@@ -382,7 +383,8 @@ describe('Data browser data', () => {
|
||||
it('Query editor - AI assistant', () => {
|
||||
cy.contains('MySql-connection').click();
|
||||
cy.contains('MyChinook').click();
|
||||
cy.testid('TabsPanel_buttonNewQuery').click();
|
||||
cy.testid('TabsPanel_buttonNewObject').click();
|
||||
cy.testid('NewObjectModal_query').click();
|
||||
cy.testid('QueryTab_switchAiAssistantButton').click();
|
||||
cy.testid('QueryAiAssistant_allowSendToAiServiceButton').click();
|
||||
cy.testid('ConfirmModal_okButton').click();
|
||||
|
||||
112
e2e-tests/cypress/e2e/charts.cy.js
Normal file
112
e2e-tests/cypress/e2e/charts.cy.js
Normal file
@@ -0,0 +1,112 @@
|
||||
Cypress.on('uncaught:exception', (err, runnable) => {
|
||||
// if the error message matches the one about WorkerGlobalScope importScripts
|
||||
if (err.message.includes("Failed to execute 'importScripts' on 'WorkerGlobalScope'")) {
|
||||
// return false to let Cypress know we intentionally want to ignore this error
|
||||
return false;
|
||||
}
|
||||
// otherwise let Cypress throw the error
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('http://localhost:3000');
|
||||
cy.viewport(1250, 900);
|
||||
});
|
||||
|
||||
describe('Charts', () => {
|
||||
it('Auto detect chart', () => {
|
||||
cy.contains('MySql-connection').click();
|
||||
cy.contains('charts_sample').click();
|
||||
cy.testid('WidgetIconPanel_file').click();
|
||||
cy.contains('chart1').click();
|
||||
cy.contains('department_name');
|
||||
// cy.testid('QueryTab_executeButton').click();
|
||||
// cy.testid('QueryTab_openChartButton').click();
|
||||
cy.testid('chart-canvas').should($c => expect($c[0].toDataURL()).to.match(/^data:image\/png;base64/));
|
||||
cy.themeshot('choose-detected-chart');
|
||||
});
|
||||
|
||||
it('Two line charts', () => {
|
||||
cy.contains('MySql-connection').click();
|
||||
cy.contains('MyChinook').click();
|
||||
cy.testid('TabsPanel_buttonNewObject').click();
|
||||
cy.testid('NewObjectModal_query').click();
|
||||
cy.wait(1000);
|
||||
cy.get('body').realType('SELECT InvoiceDate, Total from Invoice');
|
||||
cy.contains('Execute').click();
|
||||
cy.contains('Open chart').click();
|
||||
cy.testid('chart-canvas').should($c => expect($c[0].toDataURL()).to.match(/^data:image\/png;base64/));
|
||||
cy.themeshot('two-line-charts');
|
||||
});
|
||||
|
||||
it('Invoice naive autodetection', () => {
|
||||
cy.contains('MySql-connection').click();
|
||||
cy.contains('MyChinook').click();
|
||||
cy.testid('TabsPanel_buttonNewObject').click();
|
||||
cy.testid('NewObjectModal_query').click();
|
||||
cy.wait(1000);
|
||||
cy.get('body').realType('SELECT * from Invoice');
|
||||
cy.contains('Execute').click();
|
||||
cy.contains('Open chart').click();
|
||||
cy.testid('chart-canvas').should($c => expect($c[0].toDataURL()).to.match(/^data:image\/png;base64/));
|
||||
cy.themeshot('chart-naive-autodetection');
|
||||
});
|
||||
|
||||
it('Invoice by country - grouped chart', () => {
|
||||
cy.contains('MySql-connection').click();
|
||||
cy.contains('MyChinook').click();
|
||||
cy.testid('TabsPanel_buttonNewObject').click();
|
||||
cy.testid('NewObjectModal_query').click();
|
||||
cy.wait(1000);
|
||||
cy.get('body').realType(
|
||||
"SELECT InvoiceDate, Total, BillingCountry from Invoice where BillingCountry in ('USA', 'Canada', 'Brazil', 'France', 'Germany')"
|
||||
);
|
||||
cy.contains('Execute').click();
|
||||
cy.contains('Open chart').click();
|
||||
cy.testid('ChartSelector_chart_1').click();
|
||||
cy.testid('JslChart_customizeButton').click();
|
||||
|
||||
cy.testid('chart-canvas').should($c => expect($c[0].toDataURL()).to.match(/^data:image\/png;base64/));
|
||||
cy.themeshot('chart-grouped-autodetected');
|
||||
|
||||
cy.testid('ChartDefinitionEditor_chartTypeSelect').select('Bar');
|
||||
cy.testid('ChartDefinitionEditor_xAxisTransformSelect').select('Date (Year)');
|
||||
|
||||
cy.testid('chart-canvas').should($c => expect($c[0].toDataURL()).to.match(/^data:image\/png;base64/));
|
||||
cy.themeshot('chart-grouped-bars');
|
||||
});
|
||||
|
||||
it('Public Knowledge base - show chart', () => {
|
||||
cy.contains('MySql-connection').click();
|
||||
cy.contains('MyChinook').click();
|
||||
cy.testid('WidgetIconPanel_cloud-public').click();
|
||||
cy.testid('public-cloud-file-tag-mysql/folder-MySQL/tag-premium/top-tables-row-count.sql').click();
|
||||
cy.testid('chart-canvas').should($c => expect($c[0].toDataURL()).to.match(/^data:image\/png;base64/));
|
||||
cy.themeshot('public-knowledge-base-tables-sizes');
|
||||
});
|
||||
|
||||
it('Auto detect chart', () => {
|
||||
cy.contains('MySql-connection').click();
|
||||
cy.contains('MyChinook').click();
|
||||
cy.contains('Invoice').rightclick();
|
||||
cy.contains('SQL template').click();
|
||||
cy.contains('SELECT').click();
|
||||
cy.testid('QueryTab_detectChartButton').click();
|
||||
cy.testid('QueryTab_executeButton').click();
|
||||
cy.contains('Chart 1').click();
|
||||
cy.testid('ChartSelector_chart_0').click();
|
||||
cy.testid('JslChart_customizeButton').click();
|
||||
cy.testid('ChartDefinitionEditor_chartTypeSelect').select('Bar');
|
||||
cy.testid('ChartDefinitionEditor_chartTypeSelect').select('Line');
|
||||
cy.testid('chart-canvas').should($c => expect($c[0].toDataURL()).to.match(/^data:image\/png;base64/));
|
||||
cy.themeshot('query-result-chart');
|
||||
});
|
||||
|
||||
it('New object window', () => {
|
||||
cy.contains('MySql-connection').click();
|
||||
cy.contains('MyChinook').click();
|
||||
cy.contains('Invoice').click();
|
||||
cy.testid('WidgetIconPanel_addButton').click();
|
||||
cy.contains('Compare database');
|
||||
cy.themeshot('new-object-window');
|
||||
});
|
||||
});
|
||||
56
e2e-tests/cypress/e2e/cloud.cy.js
Normal file
56
e2e-tests/cypress/e2e/cloud.cy.js
Normal file
@@ -0,0 +1,56 @@
|
||||
Cypress.on('uncaught:exception', (err, runnable) => {
|
||||
// if the error message matches the one about WorkerGlobalScope importScripts
|
||||
if (err.message.includes("Failed to execute 'importScripts' on 'WorkerGlobalScope'")) {
|
||||
// return false to let Cypress know we intentionally want to ignore this error
|
||||
return false;
|
||||
}
|
||||
// otherwise let Cypress throw the error
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('http://localhost:3000');
|
||||
cy.viewport(1250, 900);
|
||||
});
|
||||
|
||||
describe('Cloud tests', () => {
|
||||
it('Private cloud', () => {
|
||||
cy.testid('WidgetIconPanel_cloudAccount');
|
||||
cy.window().then(win => {
|
||||
win.__loginToCloudTest('dbgate.test@gmail.com');
|
||||
});
|
||||
cy.contains('dbgate.test@gmail.com');
|
||||
|
||||
// cy.testid('WidgetIconPanel_cloudAccount').click();
|
||||
|
||||
// cy.origin('https://identity.dbgate.io', () => {
|
||||
// cy.contains('Sign in with GitHub').click();
|
||||
// });
|
||||
|
||||
// cy.origin('https://github.com', () => {
|
||||
// cy.get('#login_field').type('dbgatetest');
|
||||
// cy.get('#password').type('Pwd2020Db');
|
||||
// cy.get('input[type="submit"]').click();
|
||||
// });
|
||||
|
||||
// cy.wait(3000);
|
||||
|
||||
// cy.location('origin').then(origin => {
|
||||
// if (origin === 'https://github.com') {
|
||||
// // Still on github.com → an authorization step is waiting
|
||||
// cy.origin('https://github.com', () => {
|
||||
// // Try once, don't wait the full default timeout
|
||||
// cy.get('button[data-octo-click="oauth_application_authorization"]', { timeout: 500, log: false }).click(); // if the button exists it will be clicked
|
||||
// // if not, the short timeout elapses and we drop out
|
||||
// });
|
||||
// } else {
|
||||
// // Already back on localhost – nothing to authorize
|
||||
// cy.log('OAuth redirect skipped the Authorize screen');
|
||||
// }
|
||||
// });
|
||||
|
||||
cy.contains('Testing Connections').rightclick();
|
||||
cy.contains('Administrate access').click();
|
||||
cy.contains('User email');
|
||||
cy.themeshot('administer-shared-folder');
|
||||
});
|
||||
});
|
||||
@@ -59,7 +59,8 @@ describe('Transactions', () => {
|
||||
|
||||
cy.contains(connectionName).click();
|
||||
if (databaseName) cy.contains(databaseName).click();
|
||||
cy.testid('TabsPanel_buttonNewQuery').click();
|
||||
cy.testid('TabsPanel_buttonNewObject').click();
|
||||
cy.testid('NewObjectModal_query').click();
|
||||
cy.wait(1000);
|
||||
cy.get('body').type(
|
||||
formatQueryWithoutParams(driver, "INSERT INTO ~categories (~category_id, ~category_name) VALUES (5, 'test');")
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
Cypress.on('uncaught:exception', (err, runnable) => {
|
||||
// if the error message matches the one about WorkerGlobalScope importScripts
|
||||
if (err.message.includes("Failed to execute 'importScripts' on 'WorkerGlobalScope'")) {
|
||||
// return false to let Cypress know we intentionally want to ignore this error
|
||||
return false;
|
||||
}
|
||||
// otherwise let Cypress throw the error
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('http://localhost:3000');
|
||||
cy.viewport(1250, 900);
|
||||
@@ -80,4 +89,34 @@ describe('Team edition tests', () => {
|
||||
cy.testid('AdminMenuWidget_itemUsers').click();
|
||||
cy.contains('test@example.com');
|
||||
});
|
||||
|
||||
it('Audit logging', () => {
|
||||
cy.testid('LoginPage_linkAdmin').click();
|
||||
cy.testid('LoginPage_password').type('adminpwd');
|
||||
cy.testid('LoginPage_submitLogin').click();
|
||||
|
||||
cy.testid('AdminMenuWidget_itemAuditLog').click();
|
||||
cy.contains('Audit log is not enabled');
|
||||
cy.testid('AdminMenuWidget_itemSettings').click();
|
||||
cy.testid('AdminSettingsTab_auditLogCheckbox').click();
|
||||
cy.testid('AdminMenuWidget_itemAuditLog').click();
|
||||
cy.contains('No data for selected date');
|
||||
|
||||
cy.testid('AdminMenuWidget_itemConnections').click();
|
||||
cy.contains('Open table').click();
|
||||
cy.contains('displayName');
|
||||
cy.get('.toolstrip').contains('Export').click();
|
||||
cy.contains('CSV file').click();
|
||||
|
||||
cy.testid('AdminMenuWidget_itemUsers').click();
|
||||
cy.contains('Open table').click();
|
||||
cy.contains('login');
|
||||
cy.get('.toolstrip').contains('Export').click();
|
||||
cy.contains('XML file').click();
|
||||
|
||||
cy.testid('AdminMenuWidget_itemAuditLog').click();
|
||||
cy.testid('AdminAuditLogTab_refreshButton').click();
|
||||
cy.contains('Exporting query').click();
|
||||
cy.themeshot('auditlog');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,3 +42,11 @@ beforeEach(() => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Cypress.Screenshot.defaults({
|
||||
// onBeforeScreenshot() {
|
||||
// if (window.Chart) {
|
||||
// Object.values(window.Chart.instances).forEach(c => c.resize());
|
||||
// }
|
||||
// },
|
||||
// });
|
||||
|
||||
6
e2e-tests/data/charts-sample/departments.jsonl
Normal file
6
e2e-tests/data/charts-sample/departments.jsonl
Normal file
@@ -0,0 +1,6 @@
|
||||
{"__isStreamHeader":true,"pureName":"departments","schemaName":"dbo","objectId":1205579333,"createDate":"2025-06-12T10:30:34.083Z","modifyDate":"2025-06-12T10:30:34.120Z","contentHash":"2025-06-12T10:30:34.120Z","columns":[{"columnName":"id","dataType":"int","notNull":true,"autoIncrement":false,"defaultValue":null,"defaultConstraint":null,"computedExpression":null,"hasAutoValue":false},{"columnName":"name","dataType":"varchar(100)","notNull":true,"autoIncrement":false,"defaultValue":null,"defaultConstraint":null,"computedExpression":null,"hasAutoValue":false}],"primaryKey":{"constraintName":"PK__departme__3213E83FE8E7043D","schemaName":"dbo","pureName":"departments","constraintType":"primaryKey","columns":[{"columnName":"id"}]},"foreignKeys":[],"indexes":[],"uniques":[],"engine":"mssql@dbgate-plugin-mssql"}
|
||||
{"id":1,"name":"IT"}
|
||||
{"id":2,"name":"Marketing"}
|
||||
{"id":3,"name":"Finance"}
|
||||
{"id":4,"name":"Human Resources"}
|
||||
{"id":5,"name":"Research and Development"}
|
||||
12
e2e-tests/data/charts-sample/departments.table.yaml
Normal file
12
e2e-tests/data/charts-sample/departments.table.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
name: departments
|
||||
columns:
|
||||
- name: id
|
||||
type: int
|
||||
default: null
|
||||
notNull: true
|
||||
- name: name
|
||||
type: varchar(100)
|
||||
default: null
|
||||
notNull: true
|
||||
primaryKey:
|
||||
- id
|
||||
39
e2e-tests/data/charts-sample/employee_project.jsonl
Normal file
39
e2e-tests/data/charts-sample/employee_project.jsonl
Normal file
@@ -0,0 +1,39 @@
|
||||
{"__isStreamHeader":true,"pureName":"employee_project","schemaName":"dbo","objectId":1333579789,"createDate":"2025-06-12T10:30:34.133Z","modifyDate":"2025-06-12T10:30:34.133Z","contentHash":"2025-06-12T10:30:34.133Z","columns":[{"columnName":"employee_id","dataType":"int","notNull":true,"autoIncrement":false,"defaultValue":null,"defaultConstraint":null,"computedExpression":null,"hasAutoValue":false},{"columnName":"project_id","dataType":"int","notNull":true,"autoIncrement":false,"defaultValue":null,"defaultConstraint":null,"computedExpression":null,"hasAutoValue":false},{"columnName":"role","dataType":"varchar(50)","notNull":false,"autoIncrement":false,"defaultValue":null,"defaultConstraint":null,"computedExpression":null,"hasAutoValue":false}],"primaryKey":{"constraintName":"PK__employee__2EE9924949ED9668","schemaName":"dbo","pureName":"employee_project","constraintType":"primaryKey","columns":[{"columnName":"employee_id"},{"columnName":"project_id"}]},"foreignKeys":[{"constraintName":"FK__employee___emplo__5165187F","constraintType":"foreignKey","schemaName":"dbo","pureName":"employee_project","refSchemaName":"dbo","refTableName":"employees","updateAction":"NO ACTION","deleteAction":"NO ACTION","columns":[{"columnName":"employee_id","refColumnName":"id"}]},{"constraintName":"FK__employee___proje__52593CB8","constraintType":"foreignKey","schemaName":"dbo","pureName":"employee_project","refSchemaName":"dbo","refTableName":"projects","updateAction":"NO ACTION","deleteAction":"NO ACTION","columns":[{"columnName":"project_id","refColumnName":"id"}]}],"indexes":[],"uniques":[],"engine":"mssql@dbgate-plugin-mssql"}
|
||||
{"employee_id":1,"project_id":6,"role":"Manager"}
|
||||
{"employee_id":1,"project_id":8,"role":"Developer"}
|
||||
{"employee_id":2,"project_id":7,"role":"Tester"}
|
||||
{"employee_id":2,"project_id":8,"role":"Developer"}
|
||||
{"employee_id":3,"project_id":4,"role":"Analyst"}
|
||||
{"employee_id":3,"project_id":6,"role":"Developer"}
|
||||
{"employee_id":4,"project_id":2,"role":"Manager"}
|
||||
{"employee_id":4,"project_id":4,"role":"Analyst"}
|
||||
{"employee_id":4,"project_id":5,"role":"Analyst"}
|
||||
{"employee_id":5,"project_id":5,"role":"Tester"}
|
||||
{"employee_id":6,"project_id":1,"role":"Analyst"}
|
||||
{"employee_id":6,"project_id":6,"role":"Tester"}
|
||||
{"employee_id":6,"project_id":9,"role":"Manager"}
|
||||
{"employee_id":7,"project_id":8,"role":"Manager"}
|
||||
{"employee_id":8,"project_id":10,"role":"Analyst"}
|
||||
{"employee_id":9,"project_id":2,"role":"Analyst"}
|
||||
{"employee_id":9,"project_id":6,"role":"Analyst"}
|
||||
{"employee_id":9,"project_id":7,"role":"Developer"}
|
||||
{"employee_id":10,"project_id":2,"role":"Manager"}
|
||||
{"employee_id":10,"project_id":6,"role":"Analyst"}
|
||||
{"employee_id":11,"project_id":1,"role":"Tester"}
|
||||
{"employee_id":12,"project_id":4,"role":"Tester"}
|
||||
{"employee_id":13,"project_id":2,"role":"Developer"}
|
||||
{"employee_id":13,"project_id":3,"role":"Analyst"}
|
||||
{"employee_id":13,"project_id":7,"role":"Developer"}
|
||||
{"employee_id":14,"project_id":3,"role":"Developer"}
|
||||
{"employee_id":14,"project_id":9,"role":"Manager"}
|
||||
{"employee_id":15,"project_id":1,"role":"Developer"}
|
||||
{"employee_id":15,"project_id":5,"role":"Manager"}
|
||||
{"employee_id":16,"project_id":3,"role":"Tester"}
|
||||
{"employee_id":16,"project_id":5,"role":"Developer"}
|
||||
{"employee_id":17,"project_id":6,"role":"Analyst"}
|
||||
{"employee_id":18,"project_id":1,"role":"Tester"}
|
||||
{"employee_id":18,"project_id":5,"role":"Tester"}
|
||||
{"employee_id":18,"project_id":6,"role":"Manager"}
|
||||
{"employee_id":19,"project_id":6,"role":"Analyst"}
|
||||
{"employee_id":20,"project_id":2,"role":"Developer"}
|
||||
{"employee_id":20,"project_id":4,"role":"Developer"}
|
||||
18
e2e-tests/data/charts-sample/employee_project.table.yaml
Normal file
18
e2e-tests/data/charts-sample/employee_project.table.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
name: employee_project
|
||||
columns:
|
||||
- name: employee_id
|
||||
type: int
|
||||
default: null
|
||||
notNull: true
|
||||
references: employees
|
||||
- name: project_id
|
||||
type: int
|
||||
default: null
|
||||
notNull: true
|
||||
references: projects
|
||||
- name: role
|
||||
type: varchar(50)
|
||||
default: null
|
||||
primaryKey:
|
||||
- employee_id
|
||||
- project_id
|
||||
21
e2e-tests/data/charts-sample/employees.jsonl
Normal file
21
e2e-tests/data/charts-sample/employees.jsonl
Normal file
@@ -0,0 +1,21 @@
|
||||
{"__isStreamHeader":true,"pureName":"employees","schemaName":"dbo","objectId":1237579447,"createDate":"2025-06-12T10:30:34.113Z","modifyDate":"2025-06-12T12:35:22.140Z","contentHash":"2025-06-12T12:35:22.140Z","columns":[{"columnName":"id","dataType":"int","notNull":true,"autoIncrement":false,"defaultValue":null,"defaultConstraint":null,"computedExpression":null,"hasAutoValue":false},{"columnName":"name","dataType":"varchar(100)","notNull":true,"autoIncrement":false,"defaultValue":null,"defaultConstraint":null,"computedExpression":null,"hasAutoValue":false},{"columnName":"email","dataType":"varchar(100)","notNull":true,"autoIncrement":false,"defaultValue":null,"defaultConstraint":null,"computedExpression":null,"hasAutoValue":false},{"columnName":"hire_date","dataType":"date","notNull":true,"autoIncrement":false,"defaultValue":null,"defaultConstraint":null,"computedExpression":null,"hasAutoValue":false},{"columnName":"department_id","dataType":"int","notNull":false,"autoIncrement":false,"defaultValue":null,"defaultConstraint":null,"computedExpression":null,"hasAutoValue":false}],"primaryKey":{"constraintName":"PK__employee__3213E83FE576E55A","schemaName":"dbo","pureName":"employees","constraintType":"primaryKey","columns":[{"columnName":"id"}]},"foreignKeys":[{"constraintName":"FK__employees__depar__4CA06362","constraintType":"foreignKey","schemaName":"dbo","pureName":"employees","refSchemaName":"dbo","refTableName":"departments","updateAction":"NO ACTION","deleteAction":"NO ACTION","columns":[{"columnName":"department_id","refColumnName":"id"}]}],"indexes":[],"uniques":[{"constraintName":"UQ__employee__AB6E6164E18D883F","columns":[{"columnName":"email"}]}],"engine":"mssql@dbgate-plugin-mssql"}
|
||||
{"id":1,"name":"John Smith","email":"john.smith@example.com","hire_date":"2018-07-09T00:00:00.000Z","department_id":2}
|
||||
{"id":2,"name":"Jane Garcia","email":"jane.garcia@example.com","hire_date":"2019-10-13T00:00:00.000Z","department_id":5}
|
||||
{"id":3,"name":"Grace Smith","email":"grace.smith@example.com","hire_date":"2019-03-16T00:00:00.000Z","department_id":1}
|
||||
{"id":4,"name":"Charlie Williams","email":"charlie.williams@example.com","hire_date":"2020-10-18T00:00:00.000Z","department_id":2}
|
||||
{"id":5,"name":"Eve Brown","email":"eve.brown@example.com","hire_date":"2018-04-12T00:00:00.000Z","department_id":4}
|
||||
{"id":6,"name":"Alice Moore","email":"alice.moore@example.com","hire_date":"2019-04-20T00:00:00.000Z","department_id":2}
|
||||
{"id":7,"name":"Eve Williams","email":"eve.williams@example.com","hire_date":"2020-04-26T00:00:00.000Z","department_id":4}
|
||||
{"id":8,"name":"Eve Jones","email":"eve.jones@example.com","hire_date":"2022-10-04T00:00:00.000Z","department_id":3}
|
||||
{"id":9,"name":"Diana Miller","email":"diana.miller@example.com","hire_date":"2021-03-28T00:00:00.000Z","department_id":1}
|
||||
{"id":10,"name":"Diana Smith","email":"diana.smith@example.com","hire_date":"2018-04-12T00:00:00.000Z","department_id":2}
|
||||
{"id":11,"name":"Hank Johnson","email":"hank.johnson@example.com","hire_date":"2020-09-16T00:00:00.000Z","department_id":2}
|
||||
{"id":12,"name":"Frank Miller","email":"frank.miller@example.com","hire_date":"2023-01-12T00:00:00.000Z","department_id":4}
|
||||
{"id":13,"name":"Jane Brown","email":"jane.brown@example.com","hire_date":"2023-05-07T00:00:00.000Z","department_id":3}
|
||||
{"id":14,"name":"Grace Davis","email":"grace.davis@example.com","hire_date":"2019-08-22T00:00:00.000Z","department_id":3}
|
||||
{"id":15,"name":"Jane Black","email":"jane.black@example.com","hire_date":"2019-04-28T00:00:00.000Z","department_id":2}
|
||||
{"id":16,"name":"Charlie Smith","email":"charlie.smith@example.com","hire_date":"2019-06-12T00:00:00.000Z","department_id":5}
|
||||
{"id":17,"name":"Eve Johnson","email":"eve.johnson@example.com","hire_date":"2020-11-07T00:00:00.000Z","department_id":5}
|
||||
{"id":18,"name":"Jane Johnson","email":"jane.johnson@example.com","hire_date":"2020-04-06T00:00:00.000Z","department_id":2}
|
||||
{"id":19,"name":"Hank Brown","email":"hank.brown@example.com","hire_date":"2023-05-10T00:00:00.000Z","department_id":2}
|
||||
{"id":20,"name":"Frank Jones","email":"frank.jones@example.com","hire_date":"2020-10-26T00:00:00.000Z","department_id":1}
|
||||
28
e2e-tests/data/charts-sample/employees.table.yaml
Normal file
28
e2e-tests/data/charts-sample/employees.table.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
name: employees
|
||||
columns:
|
||||
- name: id
|
||||
type: int
|
||||
default: null
|
||||
notNull: true
|
||||
- name: name
|
||||
type: varchar(100)
|
||||
default: null
|
||||
notNull: true
|
||||
- name: email
|
||||
type: varchar(100)
|
||||
default: null
|
||||
notNull: true
|
||||
- name: hire_date
|
||||
type: date
|
||||
default: null
|
||||
notNull: true
|
||||
- name: department_id
|
||||
type: int
|
||||
default: null
|
||||
references: departments
|
||||
primaryKey:
|
||||
- id
|
||||
uniques:
|
||||
- name: UQ__employee__AB6E6164E18D883F
|
||||
columns:
|
||||
- email
|
||||
141
e2e-tests/data/charts-sample/finance_reports.jsonl
Normal file
141
e2e-tests/data/charts-sample/finance_reports.jsonl
Normal file
@@ -0,0 +1,141 @@
|
||||
{"__isStreamHeader":true,"pureName":"finance_reports","schemaName":"dbo","objectId":338100245,"createDate":"2025-06-23T12:15:08.727Z","modifyDate":"2025-06-23T12:15:08.750Z","contentHash":"2025-06-23T12:15:08.750Z","columns":[{"columnName":"id","dataType":"int","notNull":true,"autoIncrement":false,"defaultValue":null,"defaultConstraint":null,"computedExpression":null,"hasAutoValue":false},{"columnName":"date","dataType":"date","notNull":true,"autoIncrement":false,"defaultValue":null,"defaultConstraint":null,"computedExpression":null,"hasAutoValue":false},{"columnName":"profit","dataType":"money","notNull":true,"autoIncrement":false,"defaultValue":null,"defaultConstraint":null,"computedExpression":null,"hasAutoValue":false}],"foreignKeys":[{"constraintName":"project_id","constraintType":"foreignKey","schemaName":"dbo","pureName":"finance_reports","refSchemaName":"dbo","refTableName":"projects","updateAction":"NO ACTION","deleteAction":"NO ACTION","columns":[{"columnName":"id","refColumnName":"id"}]}],"indexes":[],"uniques":[],"engine":"mssql@dbgate-plugin-mssql"}
|
||||
{"id":1,"date":"2022-01-01T00:00:00.000Z","profit":73923.4}
|
||||
{"id":1,"date":"2022-01-31T00:00:00.000Z","profit":21837.75}
|
||||
{"id":1,"date":"2022-03-02T00:00:00.000Z","profit":67859.8}
|
||||
{"id":1,"date":"2022-04-01T00:00:00.000Z","profit":77403.3}
|
||||
{"id":1,"date":"2022-05-01T00:00:00.000Z","profit":84083.19}
|
||||
{"id":1,"date":"2022-05-31T00:00:00.000Z","profit":30040.55}
|
||||
{"id":1,"date":"2022-06-30T00:00:00.000Z","profit":50947.14}
|
||||
{"id":1,"date":"2022-07-30T00:00:00.000Z","profit":63345.62}
|
||||
{"id":1,"date":"2022-08-29T00:00:00.000Z","profit":23819.45}
|
||||
{"id":1,"date":"2022-09-28T00:00:00.000Z","profit":-25919.19}
|
||||
{"id":1,"date":"2022-10-28T00:00:00.000Z","profit":27967.6}
|
||||
{"id":1,"date":"2022-11-27T00:00:00.000Z","profit":-37402.36}
|
||||
{"id":1,"date":"2022-12-27T00:00:00.000Z","profit":94528.8}
|
||||
{"id":1,"date":"2023-01-26T00:00:00.000Z","profit":29491.03}
|
||||
{"id":1,"date":"2023-02-25T00:00:00.000Z","profit":81541.29}
|
||||
{"id":2,"date":"2022-01-01T00:00:00.000Z","profit":18070.94}
|
||||
{"id":2,"date":"2022-01-31T00:00:00.000Z","profit":-40609.87}
|
||||
{"id":2,"date":"2022-03-02T00:00:00.000Z","profit":42435.51}
|
||||
{"id":2,"date":"2022-04-01T00:00:00.000Z","profit":-11915.15}
|
||||
{"id":2,"date":"2022-05-01T00:00:00.000Z","profit":-37417.4}
|
||||
{"id":2,"date":"2022-05-31T00:00:00.000Z","profit":23028.66}
|
||||
{"id":2,"date":"2022-06-30T00:00:00.000Z","profit":-6895.49}
|
||||
{"id":2,"date":"2022-07-30T00:00:00.000Z","profit":63114.54}
|
||||
{"id":2,"date":"2022-08-29T00:00:00.000Z","profit":94646.99}
|
||||
{"id":2,"date":"2022-09-28T00:00:00.000Z","profit":99560.77}
|
||||
{"id":2,"date":"2022-10-28T00:00:00.000Z","profit":62216.22}
|
||||
{"id":2,"date":"2022-11-27T00:00:00.000Z","profit":85094.32}
|
||||
{"id":2,"date":"2022-12-27T00:00:00.000Z","profit":-23378.37}
|
||||
{"id":2,"date":"2023-01-26T00:00:00.000Z","profit":47635.86}
|
||||
{"id":2,"date":"2023-02-25T00:00:00.000Z","profit":33727.72}
|
||||
{"id":3,"date":"2022-01-01T00:00:00.000Z","profit":33088.03}
|
||||
{"id":3,"date":"2022-01-31T00:00:00.000Z","profit":66668.91}
|
||||
{"id":3,"date":"2022-03-02T00:00:00.000Z","profit":5344.27}
|
||||
{"id":3,"date":"2022-04-01T00:00:00.000Z","profit":22122.99}
|
||||
{"id":3,"date":"2022-05-01T00:00:00.000Z","profit":27342.01}
|
||||
{"id":3,"date":"2022-05-31T00:00:00.000Z","profit":55479.42}
|
||||
{"id":3,"date":"2022-06-30T00:00:00.000Z","profit":35956.11}
|
||||
{"id":3,"date":"2022-07-30T00:00:00.000Z","profit":9667.12}
|
||||
{"id":3,"date":"2022-08-29T00:00:00.000Z","profit":63430.18}
|
||||
{"id":3,"date":"2022-09-28T00:00:00.000Z","profit":-4883.41}
|
||||
{"id":3,"date":"2022-10-28T00:00:00.000Z","profit":38902.8}
|
||||
{"id":3,"date":"2022-11-27T00:00:00.000Z","profit":-25500.13}
|
||||
{"id":3,"date":"2022-12-27T00:00:00.000Z","profit":65074.21}
|
||||
{"id":3,"date":"2023-01-26T00:00:00.000Z","profit":12570.27}
|
||||
{"id":3,"date":"2023-02-25T00:00:00.000Z","profit":35418.36}
|
||||
{"id":4,"date":"2022-01-01T00:00:00.000Z","profit":68282.98}
|
||||
{"id":4,"date":"2022-01-31T00:00:00.000Z","profit":77778.99}
|
||||
{"id":4,"date":"2022-03-02T00:00:00.000Z","profit":95490.49}
|
||||
{"id":4,"date":"2022-04-01T00:00:00.000Z","profit":-44466.37}
|
||||
{"id":4,"date":"2022-05-01T00:00:00.000Z","profit":40215.71}
|
||||
{"id":4,"date":"2022-05-31T00:00:00.000Z","profit":-31228.87}
|
||||
{"id":4,"date":"2022-06-30T00:00:00.000Z","profit":60667.69}
|
||||
{"id":4,"date":"2022-07-30T00:00:00.000Z","profit":71439.16}
|
||||
{"id":4,"date":"2022-08-29T00:00:00.000Z","profit":-25077.4}
|
||||
{"id":4,"date":"2022-09-28T00:00:00.000Z","profit":-36128.2}
|
||||
{"id":4,"date":"2022-10-28T00:00:00.000Z","profit":36727.68}
|
||||
{"id":4,"date":"2022-11-27T00:00:00.000Z","profit":-24207.2}
|
||||
{"id":4,"date":"2022-12-27T00:00:00.000Z","profit":63846.96}
|
||||
{"id":5,"date":"2022-01-01T00:00:00.000Z","profit":21648.3}
|
||||
{"id":5,"date":"2022-01-31T00:00:00.000Z","profit":59263.22}
|
||||
{"id":5,"date":"2022-03-02T00:00:00.000Z","profit":49154.51}
|
||||
{"id":5,"date":"2022-04-01T00:00:00.000Z","profit":34787.48}
|
||||
{"id":5,"date":"2022-05-01T00:00:00.000Z","profit":-24120.19}
|
||||
{"id":5,"date":"2022-05-31T00:00:00.000Z","profit":98437.86}
|
||||
{"id":5,"date":"2022-06-30T00:00:00.000Z","profit":18614.77}
|
||||
{"id":5,"date":"2022-07-30T00:00:00.000Z","profit":17680.34}
|
||||
{"id":5,"date":"2022-08-29T00:00:00.000Z","profit":74406.86}
|
||||
{"id":5,"date":"2022-09-28T00:00:00.000Z","profit":61845.3}
|
||||
{"id":5,"date":"2022-10-28T00:00:00.000Z","profit":-37889.59}
|
||||
{"id":5,"date":"2022-11-27T00:00:00.000Z","profit":76651.05}
|
||||
{"id":5,"date":"2022-12-27T00:00:00.000Z","profit":58739.6}
|
||||
{"id":5,"date":"2023-01-26T00:00:00.000Z","profit":82605.85}
|
||||
{"id":6,"date":"2022-01-01T00:00:00.000Z","profit":-5206.8}
|
||||
{"id":6,"date":"2022-01-31T00:00:00.000Z","profit":27498.27}
|
||||
{"id":6,"date":"2022-03-02T00:00:00.000Z","profit":-2939.84}
|
||||
{"id":6,"date":"2022-04-01T00:00:00.000Z","profit":-37261.08}
|
||||
{"id":6,"date":"2022-05-01T00:00:00.000Z","profit":37069.04}
|
||||
{"id":6,"date":"2022-05-31T00:00:00.000Z","profit":524.88}
|
||||
{"id":6,"date":"2022-06-30T00:00:00.000Z","profit":-29620.85}
|
||||
{"id":6,"date":"2022-07-30T00:00:00.000Z","profit":35540.81}
|
||||
{"id":6,"date":"2022-08-29T00:00:00.000Z","profit":20608.94}
|
||||
{"id":6,"date":"2022-09-28T00:00:00.000Z","profit":34809.33}
|
||||
{"id":6,"date":"2022-10-28T00:00:00.000Z","profit":-44949.05}
|
||||
{"id":6,"date":"2022-11-27T00:00:00.000Z","profit":-22524.26}
|
||||
{"id":6,"date":"2022-12-27T00:00:00.000Z","profit":37841.58}
|
||||
{"id":7,"date":"2022-01-01T00:00:00.000Z","profit":6903.17}
|
||||
{"id":7,"date":"2022-01-31T00:00:00.000Z","profit":58480.84}
|
||||
{"id":7,"date":"2022-03-02T00:00:00.000Z","profit":48217.34}
|
||||
{"id":7,"date":"2022-04-01T00:00:00.000Z","profit":73592.44}
|
||||
{"id":7,"date":"2022-05-01T00:00:00.000Z","profit":-21831.18}
|
||||
{"id":7,"date":"2022-05-31T00:00:00.000Z","profit":-40926.16}
|
||||
{"id":7,"date":"2022-06-30T00:00:00.000Z","profit":62299.5}
|
||||
{"id":7,"date":"2022-07-30T00:00:00.000Z","profit":95376.53}
|
||||
{"id":7,"date":"2022-08-29T00:00:00.000Z","profit":-13317.36}
|
||||
{"id":7,"date":"2022-09-28T00:00:00.000Z","profit":81565.05}
|
||||
{"id":7,"date":"2022-10-28T00:00:00.000Z","profit":77420.52}
|
||||
{"id":7,"date":"2022-11-27T00:00:00.000Z","profit":-12052.47}
|
||||
{"id":7,"date":"2022-12-27T00:00:00.000Z","profit":37742.07}
|
||||
{"id":7,"date":"2023-01-26T00:00:00.000Z","profit":-8057.99}
|
||||
{"id":8,"date":"2022-01-01T00:00:00.000Z","profit":27213.73}
|
||||
{"id":8,"date":"2022-01-31T00:00:00.000Z","profit":34271.75}
|
||||
{"id":8,"date":"2022-03-02T00:00:00.000Z","profit":-44549.47}
|
||||
{"id":8,"date":"2022-04-01T00:00:00.000Z","profit":15236.34}
|
||||
{"id":8,"date":"2022-05-01T00:00:00.000Z","profit":-27759.81}
|
||||
{"id":8,"date":"2022-05-31T00:00:00.000Z","profit":7955.12}
|
||||
{"id":8,"date":"2022-06-30T00:00:00.000Z","profit":-34484.38}
|
||||
{"id":8,"date":"2022-07-30T00:00:00.000Z","profit":-49758.7}
|
||||
{"id":8,"date":"2022-08-29T00:00:00.000Z","profit":-41990.86}
|
||||
{"id":8,"date":"2022-09-28T00:00:00.000Z","profit":58123.01}
|
||||
{"id":8,"date":"2022-10-28T00:00:00.000Z","profit":30128.78}
|
||||
{"id":8,"date":"2022-11-27T00:00:00.000Z","profit":-10151.17}
|
||||
{"id":8,"date":"2022-12-27T00:00:00.000Z","profit":54048.33}
|
||||
{"id":8,"date":"2023-01-26T00:00:00.000Z","profit":-43123.17}
|
||||
{"id":9,"date":"2022-01-01T00:00:00.000Z","profit":61031.83}
|
||||
{"id":9,"date":"2022-01-31T00:00:00.000Z","profit":68577.58}
|
||||
{"id":9,"date":"2022-03-02T00:00:00.000Z","profit":88698.97}
|
||||
{"id":9,"date":"2022-04-01T00:00:00.000Z","profit":8906.03}
|
||||
{"id":9,"date":"2022-05-01T00:00:00.000Z","profit":28824.73}
|
||||
{"id":9,"date":"2022-05-31T00:00:00.000Z","profit":88280.34}
|
||||
{"id":9,"date":"2022-06-30T00:00:00.000Z","profit":35266.09}
|
||||
{"id":9,"date":"2022-07-30T00:00:00.000Z","profit":-38025.36}
|
||||
{"id":9,"date":"2022-08-29T00:00:00.000Z","profit":-12118.53}
|
||||
{"id":9,"date":"2022-09-28T00:00:00.000Z","profit":-27265.86}
|
||||
{"id":9,"date":"2022-10-28T00:00:00.000Z","profit":56870.57}
|
||||
{"id":9,"date":"2022-11-27T00:00:00.000Z","profit":88078.95}
|
||||
{"id":9,"date":"2022-12-27T00:00:00.000Z","profit":-24059.67}
|
||||
{"id":9,"date":"2023-01-26T00:00:00.000Z","profit":-13301.43}
|
||||
{"id":10,"date":"2022-01-01T00:00:00.000Z","profit":-22479.23}
|
||||
{"id":10,"date":"2022-01-31T00:00:00.000Z","profit":8106.27}
|
||||
{"id":10,"date":"2022-03-02T00:00:00.000Z","profit":69372.19}
|
||||
{"id":10,"date":"2022-04-01T00:00:00.000Z","profit":-11895.74}
|
||||
{"id":10,"date":"2022-05-01T00:00:00.000Z","profit":-33206.5}
|
||||
{"id":10,"date":"2022-05-31T00:00:00.000Z","profit":56073.34}
|
||||
{"id":10,"date":"2022-06-30T00:00:00.000Z","profit":67488.3}
|
||||
{"id":10,"date":"2022-07-30T00:00:00.000Z","profit":48529.23}
|
||||
{"id":10,"date":"2022-08-29T00:00:00.000Z","profit":28680.2}
|
||||
{"id":10,"date":"2022-09-28T00:00:00.000Z","profit":59311.16}
|
||||
{"id":10,"date":"2022-10-28T00:00:00.000Z","profit":25315.78}
|
||||
{"id":10,"date":"2022-11-27T00:00:00.000Z","profit":36116.38}
|
||||
{"id":10,"date":"2022-12-27T00:00:00.000Z","profit":-42040.4}
|
||||
15
e2e-tests/data/charts-sample/finance_reports.table.yaml
Normal file
15
e2e-tests/data/charts-sample/finance_reports.table.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
name: finance_reports
|
||||
columns:
|
||||
- name: id
|
||||
type: int
|
||||
default: null
|
||||
notNull: true
|
||||
references: projects
|
||||
- name: date
|
||||
type: date
|
||||
default: null
|
||||
notNull: true
|
||||
- name: profit
|
||||
type: money
|
||||
default: null
|
||||
notNull: true
|
||||
11
e2e-tests/data/charts-sample/projects.jsonl
Normal file
11
e2e-tests/data/charts-sample/projects.jsonl
Normal file
@@ -0,0 +1,11 @@
|
||||
{"__isStreamHeader":true,"pureName":"projects","schemaName":"dbo","objectId":1301579675,"createDate":"2025-06-12T10:30:34.127Z","modifyDate":"2025-06-23T12:15:08.750Z","contentHash":"2025-06-23T12:15:08.750Z","columns":[{"columnName":"id","dataType":"int","notNull":true,"autoIncrement":false,"defaultValue":null,"defaultConstraint":null,"computedExpression":null,"hasAutoValue":false},{"columnName":"name","dataType":"varchar(100)","notNull":true,"autoIncrement":false,"defaultValue":null,"defaultConstraint":null,"computedExpression":null,"hasAutoValue":false},{"columnName":"start_date","dataType":"date","notNull":false,"autoIncrement":false,"defaultValue":null,"defaultConstraint":null,"computedExpression":null,"hasAutoValue":false},{"columnName":"end_date","dataType":"date","notNull":false,"autoIncrement":false,"defaultValue":null,"defaultConstraint":null,"computedExpression":null,"hasAutoValue":false}],"primaryKey":{"constraintName":"PK__projects__3213E83F26A7ED11","schemaName":"dbo","pureName":"projects","constraintType":"primaryKey","columns":[{"columnName":"id"}]},"foreignKeys":[],"indexes":[],"uniques":[],"engine":"mssql@dbgate-plugin-mssql"}
|
||||
{"id":1,"name":"Apollo Upgrade","start_date":"2020-04-27T00:00:00.000Z","end_date":"2020-10-19T00:00:00.000Z"}
|
||||
{"id":2,"name":"Market Expansion","start_date":"2022-08-04T00:00:00.000Z","end_date":"2023-06-20T00:00:00.000Z"}
|
||||
{"id":3,"name":"AI Integration","start_date":"2020-05-11T00:00:00.000Z","end_date":"2021-07-10T00:00:00.000Z"}
|
||||
{"id":4,"name":"Cost Reduction","start_date":"2022-01-08T00:00:00.000Z","end_date":"2022-07-12T00:00:00.000Z"}
|
||||
{"id":5,"name":"Cloud Migration","start_date":"2021-01-11T00:00:00.000Z","end_date":"2021-05-27T00:00:00.000Z"}
|
||||
{"id":6,"name":"Customer Portal","start_date":"2021-07-13T00:00:00.000Z","end_date":"2022-09-22T00:00:00.000Z"}
|
||||
{"id":7,"name":"Data Lake","start_date":"2021-02-25T00:00:00.000Z","end_date":"2021-08-21T00:00:00.000Z"}
|
||||
{"id":8,"name":"UX Overhaul","start_date":"2021-05-20T00:00:00.000Z","end_date":"2022-09-10T00:00:00.000Z"}
|
||||
{"id":9,"name":"Security Hardening","start_date":"2021-05-28T00:00:00.000Z","end_date":"2022-07-28T00:00:00.000Z"}
|
||||
{"id":10,"name":"Mobile App Revamp","start_date":"2021-11-17T00:00:00.000Z","end_date":"2022-06-04T00:00:00.000Z"}
|
||||
18
e2e-tests/data/charts-sample/projects.table.yaml
Normal file
18
e2e-tests/data/charts-sample/projects.table.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
name: projects
|
||||
columns:
|
||||
- name: id
|
||||
type: int
|
||||
default: null
|
||||
notNull: true
|
||||
- name: name
|
||||
type: varchar(100)
|
||||
default: null
|
||||
notNull: true
|
||||
- name: start_date
|
||||
type: date
|
||||
default: null
|
||||
- name: end_date
|
||||
type: date
|
||||
default: null
|
||||
primaryKey:
|
||||
- id
|
||||
23
e2e-tests/data/files/sql/chart1.sql
Normal file
23
e2e-tests/data/files/sql/chart1.sql
Normal file
@@ -0,0 +1,23 @@
|
||||
-- >>>
|
||||
-- autoExecute: true
|
||||
-- splitterInitialValue: 20%
|
||||
-- selected-chart: 1
|
||||
-- <<<
|
||||
|
||||
SELECT
|
||||
d.name AS department_name,
|
||||
FORMAT(fr.date, 'yyyy-MM') AS month,
|
||||
SUM(fr.profit) AS total_monthly_profit
|
||||
FROM
|
||||
departments d
|
||||
JOIN
|
||||
employees e ON d.id = e.department_id
|
||||
JOIN
|
||||
employee_project ep ON e.id = ep.employee_id
|
||||
JOIN
|
||||
finance_reports fr ON ep.project_id = fr.id
|
||||
GROUP BY
|
||||
d.name, FORMAT(fr.date, 'yyyy-MM')
|
||||
ORDER BY
|
||||
d.name, month;
|
||||
|
||||
8
e2e-tests/env/charts/.env
vendored
Normal file
8
e2e-tests/env/charts/.env
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
CONNECTIONS=mysql
|
||||
|
||||
LABEL_mysql=MySql-connection
|
||||
SERVER_mysql=localhost
|
||||
USER_mysql=root
|
||||
PASSWORD_mysql=Pwd2020Db
|
||||
PORT_mysql=16004
|
||||
ENGINE_mysql=mysql@dbgate-plugin-mysql
|
||||
2
e2e-tests/env/cloud/.env
vendored
Normal file
2
e2e-tests/env/cloud/.env
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
ALLOW_DBGATE_PRIVATE_CLOUD=1
|
||||
REDIRECT_TO_DBGATE_CLOUD_LOGIN=1
|
||||
96
e2e-tests/init/charts.js
Normal file
96
e2e-tests/init/charts.js
Normal file
@@ -0,0 +1,96 @@
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const fs = require('fs');
|
||||
|
||||
const baseDir = path.join(os.homedir(), '.dbgate');
|
||||
|
||||
const dbgateApi = require('dbgate-api');
|
||||
dbgateApi.initializeApiEnvironment();
|
||||
const dbgatePluginMysql = require('dbgate-plugin-mysql');
|
||||
dbgateApi.registerPlugins(dbgatePluginMysql);
|
||||
|
||||
async function copyFolder(source, target) {
|
||||
if (!fs.existsSync(target)) {
|
||||
fs.mkdirSync(target, { recursive: true });
|
||||
}
|
||||
for (const file of fs.readdirSync(source)) {
|
||||
fs.copyFileSync(path.join(source, file), path.join(target, file));
|
||||
}
|
||||
}
|
||||
|
||||
async function initMySqlDatabase(dbname, inputFile) {
|
||||
await dbgateApi.executeQuery({
|
||||
connection: {
|
||||
server: process.env.SERVER_mysql,
|
||||
user: process.env.USER_mysql,
|
||||
password: process.env.PASSWORD_mysql,
|
||||
port: process.env.PORT_mysql,
|
||||
engine: 'mysql@dbgate-plugin-mysql',
|
||||
},
|
||||
sql: `drop database if exists ${dbname}`,
|
||||
});
|
||||
|
||||
await dbgateApi.executeQuery({
|
||||
connection: {
|
||||
server: process.env.SERVER_mysql,
|
||||
user: process.env.USER_mysql,
|
||||
password: process.env.PASSWORD_mysql,
|
||||
port: process.env.PORT_mysql,
|
||||
engine: 'mysql@dbgate-plugin-mysql',
|
||||
},
|
||||
sql: `create database ${dbname}`,
|
||||
});
|
||||
|
||||
await dbgateApi.importDatabase({
|
||||
connection: {
|
||||
server: process.env.SERVER_mysql,
|
||||
user: process.env.USER_mysql,
|
||||
password: process.env.PASSWORD_mysql,
|
||||
port: process.env.PORT_mysql,
|
||||
database: dbname,
|
||||
engine: 'mysql@dbgate-plugin-mysql',
|
||||
},
|
||||
inputFile,
|
||||
});
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const connection = {
|
||||
server: process.env.SERVER_mysql,
|
||||
user: process.env.USER_mysql,
|
||||
password: process.env.PASSWORD_mysql,
|
||||
port: process.env.PORT_mysql,
|
||||
engine: 'mysql@dbgate-plugin-mysql',
|
||||
};
|
||||
|
||||
try {
|
||||
await dbgateApi.executeQuery({
|
||||
connection,
|
||||
sql: 'drop database if exists charts_sample',
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to drop database', err);
|
||||
}
|
||||
|
||||
await dbgateApi.executeQuery({
|
||||
connection,
|
||||
sql: 'create database charts_sample',
|
||||
});
|
||||
|
||||
await dbgateApi.importDbFromFolder({
|
||||
connection: {
|
||||
...connection,
|
||||
database: 'charts_sample',
|
||||
},
|
||||
folder: path.resolve(path.join(__dirname, '../data/charts-sample')),
|
||||
});
|
||||
|
||||
await copyFolder(
|
||||
path.resolve(path.join(__dirname, '../data/files/sql')),
|
||||
path.join(baseDir, 'files-e2etests', 'sql')
|
||||
);
|
||||
|
||||
await initMySqlDatabase('MyChinook', path.resolve(path.join(__dirname, '../data/chinook-mysql.sql')));
|
||||
}
|
||||
|
||||
dbgateApi.runScript(run);
|
||||
@@ -21,6 +21,8 @@
|
||||
"cy:run:browse-data": "cypress run --spec cypress/e2e/browse-data.cy.js",
|
||||
"cy:run:team": "cypress run --spec cypress/e2e/team.cy.js",
|
||||
"cy:run:multi-sql": "cypress run --spec cypress/e2e/multi-sql.cy.js",
|
||||
"cy:run:cloud": "cypress run --spec cypress/e2e/cloud.cy.js",
|
||||
"cy:run:charts": "cypress run --spec cypress/e2e/charts.cy.js",
|
||||
|
||||
"start:add-connection": "node clearTestingData && cd .. && node packer/build/bundle.js --listen-api --run-e2e-tests",
|
||||
"start:portal": "node clearTestingData && cd .. && env-cmd -f e2e-tests/env/portal/.env node e2e-tests/init/portal.js && env-cmd -f e2e-tests/env/portal/.env node packer/build/bundle.js --listen-api --run-e2e-tests",
|
||||
@@ -28,6 +30,8 @@
|
||||
"start:browse-data": "node clearTestingData && cd .. && env-cmd -f e2e-tests/env/browse-data/.env node e2e-tests/init/browse-data.js && env-cmd -f e2e-tests/env/browse-data/.env node packer/build/bundle.js --listen-api --run-e2e-tests",
|
||||
"start:team": "node clearTestingData && cd .. && env-cmd -f e2e-tests/env/team/.env node e2e-tests/init/team.js && env-cmd -f e2e-tests/env/team/.env node packer/build/bundle.js --listen-api --run-e2e-tests",
|
||||
"start:multi-sql": "node clearTestingData && cd .. && env-cmd -f e2e-tests/env/multi-sql/.env node e2e-tests/init/multi-sql.js && env-cmd -f e2e-tests/env/multi-sql/.env node packer/build/bundle.js --listen-api --run-e2e-tests",
|
||||
"start:cloud": "node clearTestingData && cd .. && env-cmd -f e2e-tests/env/cloud/.env node packer/build/bundle.js --listen-api --run-e2e-tests",
|
||||
"start:charts": "node clearTestingData && cd .. && env-cmd -f e2e-tests/env/charts/.env node e2e-tests/init/charts.js && env-cmd -f e2e-tests/env/charts/.env node packer/build/bundle.js --listen-api --run-e2e-tests",
|
||||
|
||||
"test:add-connection": "start-server-and-test start:add-connection http://localhost:3000 cy:run:add-connection",
|
||||
"test:portal": "start-server-and-test start:portal http://localhost:3000 cy:run:portal",
|
||||
@@ -35,8 +39,10 @@
|
||||
"test:browse-data": "start-server-and-test start:browse-data http://localhost:3000 cy:run:browse-data",
|
||||
"test:team": "start-server-and-test start:team http://localhost:3000 cy:run:team",
|
||||
"test:multi-sql": "start-server-and-test start:multi-sql http://localhost:3000 cy:run:multi-sql",
|
||||
"test:cloud": "start-server-and-test start:cloud http://localhost:3000 cy:run:cloud",
|
||||
"test:charts": "start-server-and-test start:charts http://localhost:3000 cy:run:charts",
|
||||
|
||||
"test": "yarn test:add-connection && yarn test:portal && yarn test:oauth && yarn test:browse-data && yarn test:team && yarn test:multi-sql",
|
||||
"test": "yarn test:add-connection && yarn test:portal && yarn test:oauth && yarn test:browse-data && yarn test:team && yarn test:multi-sql && yarn test:cloud && yarn test:charts",
|
||||
"test:ci": "yarn test"
|
||||
},
|
||||
"dependencies": {}
|
||||
|
||||
@@ -4,7 +4,7 @@ const { testWrapper } = require('../tools');
|
||||
const dataReplicator = require('dbgate-api/src/shell/dataReplicator');
|
||||
const deployDb = require('dbgate-api/src/shell/deployDb');
|
||||
const storageModel = require('dbgate-api/src/storageModel');
|
||||
const { runCommandOnDriver, runQueryOnDriver } = require('dbgate-tools');
|
||||
const { runCommandOnDriver, runQueryOnDriver, adaptDatabaseInfo } = require('dbgate-tools');
|
||||
|
||||
describe('Data replicator', () => {
|
||||
test.each(engines.filter(x => !x.skipDataReplicator).map(engine => [engine.label, engine]))(
|
||||
@@ -162,7 +162,7 @@ describe('Data replicator', () => {
|
||||
await deployDb({
|
||||
systemConnection: conn,
|
||||
driver,
|
||||
loadedDbModel: storageModel,
|
||||
loadedDbModel: adaptDatabaseInfo(storageModel, driver),
|
||||
targetSchema: engine.defaultSchemaName,
|
||||
});
|
||||
|
||||
@@ -176,11 +176,11 @@ describe('Data replicator', () => {
|
||||
await queryValue(
|
||||
`select ~is_disabled as ~val from ~auth_methods where ~amoid='790ca4d2-7f01-4800-955b-d691b890cc50'`
|
||||
)
|
||||
).toBeFalsy();
|
||||
).toBeTruthy();
|
||||
|
||||
const DB1 = {
|
||||
auth_methods: [
|
||||
{ id: -1, name: 'Anonymous', amoid: '790ca4d2-7f01-4800-955b-d691b890cc50', is_disabled: 1 },
|
||||
{ id: -1, name: 'Anonymous', amoid: '790ca4d2-7f01-4800-955b-d691b890cc50', is_disabled: 0 },
|
||||
{ id: 10, name: 'OAuth', amoid: '4269b660-54b6-11ef-a3aa-a9021250bf4b' },
|
||||
],
|
||||
auth_methods_config: [{ id: 20, auth_method_id: 10, key: 'oauthClient', value: 'dbgate' }],
|
||||
@@ -266,7 +266,7 @@ describe('Data replicator', () => {
|
||||
await queryValue(
|
||||
`select ~is_disabled as ~val from ~auth_methods where ~amoid='790ca4d2-7f01-4800-955b-d691b890cc50'`
|
||||
)
|
||||
).toBeTruthy();
|
||||
).toEqual('0');
|
||||
|
||||
expect(await queryValue(`select count(*) as ~val from ~auth_methods`)).toEqual('3');
|
||||
expect(await queryValue(`select count(*) as ~val from ~auth_methods_config`)).toEqual('1');
|
||||
|
||||
@@ -193,7 +193,6 @@ describe('DB Import/export', () => {
|
||||
systemConnection: conn,
|
||||
driver,
|
||||
folder: path.join(__dirname, '../../e2e-tests/data/my-guitar-shop'),
|
||||
transformRow: engine.transformModelRow,
|
||||
});
|
||||
|
||||
const res1 = await runQueryOnDriver(conn, driver, dmp => dmp.put(`select count(*) as ~cnt from ~categories`));
|
||||
|
||||
@@ -726,16 +726,6 @@ const firebirdEngine = {
|
||||
// supportRenameSqlObject: true,
|
||||
skipIncrementalAnalysis: true,
|
||||
skipRenameTable: true,
|
||||
transformModelRow: row => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(row).map(([key, value]) => {
|
||||
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(value)) {
|
||||
return [key, value.replace('T', ' ')];
|
||||
}
|
||||
return [key, value];
|
||||
})
|
||||
);
|
||||
},
|
||||
// skipDefaultValue: true,
|
||||
skipDropReferences: true,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "6.4.3-premium-beta.4",
|
||||
"version": "6.5.6",
|
||||
"name": "dbgate-all",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
@@ -43,7 +43,7 @@
|
||||
"build:plugins:frontend": "workspaces-run --only=\"dbgate-plugin-*\" -- yarn build:frontend",
|
||||
"build:plugins:backend": "workspaces-run --only=\"dbgate-plugin-*\" -- yarn build:backend",
|
||||
"build:plugins:frontend:watch": "workspaces-run --parallel --only=\"dbgate-plugin-*\" -- yarn build:frontend:watch",
|
||||
"storage-json": "dbmodel model-to-json storage-db packages/api/src/storageModel.js --commonjs",
|
||||
"storage-json": "node packages/dbmodel/bin/dbmodel.js model-to-json storage-db packages/api/src/storageModel.js --commonjs",
|
||||
"plugins:copydist": "workspaces-run --only=\"dbgate-plugin-*\" -- yarn copydist",
|
||||
"build:app:local": "yarn plugins:copydist && cd app && yarn build:local",
|
||||
"start:app:local": "cd app && yarn start:local",
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
DEVMODE=1
|
||||
SHELL_SCRIPTING=1
|
||||
ALLOW_DBGATE_PRIVATE_CLOUD=1
|
||||
DEVWEB=1
|
||||
# REDIRECT_TO_DBGATE_CLOUD_LOGIN=1
|
||||
# PROD_DBGATE_CLOUD=1
|
||||
# PROD_DBGATE_IDENTITY=1
|
||||
# LOCAL_DBGATE_CLOUD=1
|
||||
# LOCAL_DBGATE_IDENTITY=1
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ const logger = getLogger('authProvider');
|
||||
class AuthProviderBase {
|
||||
amoid = 'none';
|
||||
|
||||
async login(login, password, options = undefined) {
|
||||
async login(login, password, options = undefined, req = undefined) {
|
||||
return {
|
||||
accessToken: jwt.sign(
|
||||
{
|
||||
@@ -23,7 +23,7 @@ class AuthProviderBase {
|
||||
};
|
||||
}
|
||||
|
||||
oauthToken(params) {
|
||||
oauthToken(params, req) {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,21 @@ const {
|
||||
} = require('../auth/authProvider');
|
||||
const storage = require('./storage');
|
||||
const { decryptPasswordString } = require('../utility/crypting');
|
||||
const { createDbGateIdentitySession, startCloudTokenChecking } = require('../utility/cloudIntf');
|
||||
const {
|
||||
createDbGateIdentitySession,
|
||||
startCloudTokenChecking,
|
||||
readCloudTokenHolder,
|
||||
readCloudTestTokenHolder,
|
||||
} = require('../utility/cloudIntf');
|
||||
const socket = require('../utility/socket');
|
||||
const { sendToAuditLog } = require('../utility/auditlog');
|
||||
const {
|
||||
isLoginLicensed,
|
||||
LOGIN_LIMIT_ERROR,
|
||||
markTokenAsLoggedIn,
|
||||
markUserAsActive,
|
||||
markLoginAsLoggedOut,
|
||||
} = require('../utility/loginchecker');
|
||||
|
||||
const logger = getLogger('auth');
|
||||
|
||||
@@ -54,6 +67,11 @@ function authMiddleware(req, res, next) {
|
||||
|
||||
// const isAdminPage = req.headers['x-is-admin-page'] == 'true';
|
||||
|
||||
if (process.env.SKIP_ALL_AUTH) {
|
||||
// API is not authorized for basic auth
|
||||
return next();
|
||||
}
|
||||
|
||||
if (process.env.BASIC_AUTH) {
|
||||
// API is not authorized for basic auth
|
||||
return next();
|
||||
@@ -72,6 +90,8 @@ function authMiddleware(req, res, next) {
|
||||
try {
|
||||
const decoded = jwt.verify(token, getTokenSecret());
|
||||
req.user = decoded;
|
||||
markUserAsActive(decoded.licenseUid, token);
|
||||
|
||||
return next();
|
||||
} catch (err) {
|
||||
if (skipAuth) {
|
||||
@@ -87,12 +107,12 @@ function authMiddleware(req, res, next) {
|
||||
|
||||
module.exports = {
|
||||
oauthToken_meta: true,
|
||||
async oauthToken(params) {
|
||||
async oauthToken(params, req) {
|
||||
const { amoid } = params;
|
||||
return getAuthProviderById(amoid).oauthToken(params);
|
||||
return getAuthProviderById(amoid).oauthToken(params, req);
|
||||
},
|
||||
login_meta: true,
|
||||
async login(params) {
|
||||
async login(params, req) {
|
||||
const { amoid, login, password, isAdminPage } = params;
|
||||
|
||||
if (isAdminPage) {
|
||||
@@ -102,25 +122,52 @@ module.exports = {
|
||||
adminPassword = decryptPasswordString(adminConfig?.adminPassword);
|
||||
}
|
||||
if (adminPassword && adminPassword == password) {
|
||||
if (!(await isLoginLicensed(req, `superadmin`))) {
|
||||
return { error: LOGIN_LIMIT_ERROR };
|
||||
}
|
||||
|
||||
sendToAuditLog(req, {
|
||||
category: 'auth',
|
||||
component: 'AuthController',
|
||||
action: 'login',
|
||||
event: 'login.admin',
|
||||
severity: 'info',
|
||||
message: 'Administration login successful',
|
||||
});
|
||||
|
||||
const licenseUid = `superadmin`;
|
||||
const accessToken = jwt.sign(
|
||||
{
|
||||
login: 'superadmin',
|
||||
permissions: await storage.loadSuperadminPermissions(),
|
||||
roleId: -3,
|
||||
licenseUid,
|
||||
},
|
||||
getTokenSecret(),
|
||||
{
|
||||
expiresIn: getTokenLifetime(),
|
||||
}
|
||||
);
|
||||
markTokenAsLoggedIn(licenseUid, accessToken);
|
||||
|
||||
return {
|
||||
accessToken: jwt.sign(
|
||||
{
|
||||
login: 'superadmin',
|
||||
permissions: await storage.loadSuperadminPermissions(),
|
||||
roleId: -3,
|
||||
},
|
||||
getTokenSecret(),
|
||||
{
|
||||
expiresIn: getTokenLifetime(),
|
||||
}
|
||||
),
|
||||
accessToken,
|
||||
};
|
||||
}
|
||||
|
||||
sendToAuditLog(req, {
|
||||
category: 'auth',
|
||||
component: 'AuthController',
|
||||
action: 'loginFail',
|
||||
event: 'login.adminFailed',
|
||||
severity: 'warn',
|
||||
message: 'Administraton login failed',
|
||||
});
|
||||
|
||||
return { error: 'Login failed' };
|
||||
}
|
||||
|
||||
return getAuthProviderById(amoid).login(login, password);
|
||||
return getAuthProviderById(amoid).login(login, password, undefined, req);
|
||||
},
|
||||
|
||||
getProviders_meta: true,
|
||||
@@ -138,13 +185,39 @@ module.exports = {
|
||||
},
|
||||
|
||||
createCloudLoginSession_meta: true,
|
||||
async createCloudLoginSession({ client }) {
|
||||
const res = await createDbGateIdentitySession(client);
|
||||
async createCloudLoginSession({ client, redirectUri }) {
|
||||
const res = await createDbGateIdentitySession(client, redirectUri);
|
||||
startCloudTokenChecking(res.sid, tokenHolder => {
|
||||
socket.emit('got-cloud-token', tokenHolder);
|
||||
socket.emitChanged('cloud-content-changed');
|
||||
socket.emit('cloud-content-updated');
|
||||
});
|
||||
return res;
|
||||
},
|
||||
|
||||
cloudLoginRedirected_meta: true,
|
||||
async cloudLoginRedirected({ sid }) {
|
||||
const tokenHolder = await readCloudTokenHolder(sid);
|
||||
return tokenHolder;
|
||||
},
|
||||
|
||||
cloudTestLogin_meta: true,
|
||||
async cloudTestLogin({ email }) {
|
||||
const tokenHolder = await readCloudTestTokenHolder(email);
|
||||
return tokenHolder;
|
||||
},
|
||||
|
||||
logoutAdmin_meta: true,
|
||||
async logoutAdmin() {
|
||||
await markLoginAsLoggedOut('superadmin');
|
||||
return true;
|
||||
},
|
||||
|
||||
logoutUser_meta: true,
|
||||
async logoutUser({}, req) {
|
||||
await markLoginAsLoggedOut(req?.user?.licenseUid);
|
||||
return true;
|
||||
},
|
||||
|
||||
authMiddleware,
|
||||
};
|
||||
|
||||
@@ -58,7 +58,7 @@ module.exports = {
|
||||
|
||||
putContent_meta: true,
|
||||
async putContent({ folid, cntid, content, name, type }) {
|
||||
const resp = await putCloudContent(folid, cntid, content, name, type);
|
||||
const resp = await putCloudContent(folid, cntid, content, name, type, {});
|
||||
socket.emitChanged('cloud-content-changed');
|
||||
socket.emit('cloud-content-updated');
|
||||
return resp;
|
||||
@@ -129,7 +129,11 @@ module.exports = {
|
||||
undefined,
|
||||
JSON.stringify(connToSend),
|
||||
getConnectionLabel(conn),
|
||||
'connection'
|
||||
'connection',
|
||||
{
|
||||
connectionColor: conn.connectionColor,
|
||||
connectionEngine: conn.engine,
|
||||
}
|
||||
);
|
||||
return resp;
|
||||
},
|
||||
@@ -157,7 +161,11 @@ module.exports = {
|
||||
cntid,
|
||||
JSON.stringify(recryptedConn),
|
||||
getConnectionLabel(recryptedConn),
|
||||
'connection'
|
||||
'connection',
|
||||
{
|
||||
connectionColor: connection.connectionColor,
|
||||
connectionEngine: connection.engine,
|
||||
}
|
||||
);
|
||||
|
||||
if (resp.apiErrorMessage) {
|
||||
@@ -188,7 +196,10 @@ module.exports = {
|
||||
...conn,
|
||||
displayName: getConnectionLabel(conn) + ' - copy',
|
||||
};
|
||||
const respPut = await putCloudContent(folid, undefined, JSON.stringify(conn2), conn2.displayName, 'connection');
|
||||
const respPut = await putCloudContent(folid, undefined, JSON.stringify(conn2), conn2.displayName, 'connection', {
|
||||
connectionColor: conn.connectionColor,
|
||||
connectionEngine: conn.engine,
|
||||
});
|
||||
return respPut;
|
||||
},
|
||||
|
||||
@@ -224,7 +235,7 @@ module.exports = {
|
||||
|
||||
saveFile_meta: true,
|
||||
async saveFile({ folid, cntid, fileName, data, contentFolder, format }) {
|
||||
const resp = await putCloudContent(folid, cntid, data, fileName, 'file', contentFolder, format);
|
||||
const resp = await putCloudContent(folid, cntid, data, fileName, 'file', { contentFolder, contentType: format });
|
||||
socket.emitChanged('cloud-content-changed');
|
||||
socket.emit('cloud-content-updated');
|
||||
return resp;
|
||||
@@ -247,4 +258,22 @@ module.exports = {
|
||||
await fs.writeFile(filePath, content);
|
||||
return true;
|
||||
},
|
||||
|
||||
folderUsers_meta: true,
|
||||
async folderUsers({ folid }) {
|
||||
const resp = await callCloudApiGet(`content-folders/users/${folid}`);
|
||||
return resp;
|
||||
},
|
||||
|
||||
setFolderUserRole_meta: true,
|
||||
async setFolderUserRole({ folid, email, role }) {
|
||||
const resp = await callCloudApiPost(`content-folders/set-user-role/${folid}`, { email, role });
|
||||
return resp;
|
||||
},
|
||||
|
||||
removeFolderUser_meta: true,
|
||||
async removeFolderUser({ folid, email }) {
|
||||
const resp = await callCloudApiPost(`content-folders/remove-user/${folid}`, { email });
|
||||
return resp;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ const connections = require('../controllers/connections');
|
||||
const { getAuthProviderFromReq } = require('../auth/authProvider');
|
||||
const { checkLicense, checkLicenseKey } = require('../utility/checkLicense');
|
||||
const storage = require('./storage');
|
||||
const { getAuthProxyUrl } = require('../utility/authProxy');
|
||||
const { getAuthProxyUrl, tryToGetRefreshedLicense } = require('../utility/authProxy');
|
||||
const { getPublicHardwareFingerprint } = require('../utility/hardwareFingerprint');
|
||||
const { extractErrorMessage } = require('dbgate-tools');
|
||||
const {
|
||||
@@ -29,6 +29,7 @@ const {
|
||||
} = require('../utility/crypting');
|
||||
|
||||
const lock = new AsyncLock();
|
||||
let cachedSettingsValue = null;
|
||||
|
||||
module.exports = {
|
||||
// settingsValue: {},
|
||||
@@ -108,6 +109,7 @@ module.exports = {
|
||||
),
|
||||
isAdminPasswordMissing,
|
||||
isInvalidToken: req?.isInvalidToken,
|
||||
skipAllAuth: !!process.env.SKIP_ALL_AUTH,
|
||||
adminPasswordState: adminConfig?.adminPasswordState,
|
||||
storageDatabase: process.env.STORAGE_DATABASE,
|
||||
logsFilePath: getLogsFilePath(),
|
||||
@@ -116,7 +118,9 @@ module.exports = {
|
||||
processArgs.runE2eTests ? 'connections-e2etests.jsonl' : 'connections.jsonl'
|
||||
),
|
||||
supportCloudAutoUpgrade: !!process.env.CLOUD_UPGRADE_FILE,
|
||||
allowPrivateCloud: platformInfo.isElectron || !!process.env.ALLOW_DBGATE_PRIVATE_CLOUD,
|
||||
...currentVersion,
|
||||
redirectToDbGateCloudLogin: !!process.env.REDIRECT_TO_DBGATE_CLOUD_LOGIN,
|
||||
};
|
||||
|
||||
return configResult;
|
||||
@@ -143,6 +147,13 @@ module.exports = {
|
||||
return res;
|
||||
},
|
||||
|
||||
async getCachedSettings() {
|
||||
if (!cachedSettingsValue) {
|
||||
cachedSettingsValue = await this.loadSettings();
|
||||
}
|
||||
return cachedSettingsValue;
|
||||
},
|
||||
|
||||
deleteSettings_meta: true,
|
||||
async deleteSettings() {
|
||||
await fs.unlink(path.join(datadir(), processArgs.runE2eTests ? 'settings-e2etests.json' : 'settings.json'));
|
||||
@@ -181,6 +192,7 @@ module.exports = {
|
||||
return {
|
||||
...this.fillMissingSettings(JSON.parse(settingsText)),
|
||||
'other.licenseKey': platformInfo.isElectron ? await this.loadLicenseKey() : undefined,
|
||||
// 'other.licenseKey': await this.loadLicenseKey(),
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -198,21 +210,34 @@ module.exports = {
|
||||
},
|
||||
|
||||
saveLicenseKey_meta: true,
|
||||
async saveLicenseKey({ licenseKey }) {
|
||||
const decoded = jwt.decode(licenseKey);
|
||||
if (!decoded) {
|
||||
return {
|
||||
status: 'error',
|
||||
errorMessage: 'Invalid license key',
|
||||
};
|
||||
}
|
||||
async saveLicenseKey({ licenseKey, forceSave = false, tryToRenew = false }) {
|
||||
if (!forceSave) {
|
||||
const decoded = jwt.decode(licenseKey?.trim());
|
||||
if (!decoded) {
|
||||
return {
|
||||
status: 'error',
|
||||
errorMessage: 'Invalid license key',
|
||||
};
|
||||
}
|
||||
|
||||
const { exp } = decoded;
|
||||
if (exp * 1000 < Date.now()) {
|
||||
return {
|
||||
status: 'error',
|
||||
errorMessage: 'License key is expired',
|
||||
};
|
||||
const { exp } = decoded;
|
||||
if (exp * 1000 < Date.now()) {
|
||||
let renewed = false;
|
||||
if (tryToRenew) {
|
||||
const newLicenseKey = await tryToGetRefreshedLicense(licenseKey);
|
||||
if (newLicenseKey.status == 'ok') {
|
||||
licenseKey = newLicenseKey.token;
|
||||
renewed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!renewed) {
|
||||
return {
|
||||
status: 'error',
|
||||
errorMessage: 'License key is expired',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -256,6 +281,7 @@ module.exports = {
|
||||
updateSettings_meta: true,
|
||||
async updateSettings(values, req) {
|
||||
if (!hasPermission(`settings/change`, req)) return false;
|
||||
cachedSettingsValue = null;
|
||||
|
||||
const res = await lock.acquire('settings', async () => {
|
||||
const currentValue = await this.loadSettings();
|
||||
@@ -264,7 +290,11 @@ module.exports = {
|
||||
if (process.env.STORAGE_DATABASE) {
|
||||
updated = {
|
||||
...currentValue,
|
||||
...values,
|
||||
..._.mapValues(values, v => {
|
||||
if (v === true) return 'true';
|
||||
if (v === false) return 'false';
|
||||
return v;
|
||||
}),
|
||||
};
|
||||
await storage.writeConfig({
|
||||
group: 'settings',
|
||||
@@ -282,7 +312,7 @@ module.exports = {
|
||||
// this.settingsValue = updated;
|
||||
|
||||
if (currentValue['other.licenseKey'] != values['other.licenseKey']) {
|
||||
await this.saveLicenseKey({ licenseKey: values['other.licenseKey'] });
|
||||
await this.saveLicenseKey({ licenseKey: values['other.licenseKey'], forceSave: true });
|
||||
socket.emitChanged(`config-changed`);
|
||||
}
|
||||
}
|
||||
@@ -302,7 +332,7 @@ module.exports = {
|
||||
const resp = await axios.default.get('https://raw.githubusercontent.com/dbgate/dbgate/master/CHANGELOG.md');
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
return ''
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
@@ -312,6 +342,16 @@ module.exports = {
|
||||
return resp;
|
||||
},
|
||||
|
||||
getNewLicense_meta: true,
|
||||
async getNewLicense({ oldLicenseKey }) {
|
||||
const newLicenseKey = await tryToGetRefreshedLicense(oldLicenseKey);
|
||||
const res = await checkLicenseKey(newLicenseKey.token);
|
||||
if (res.status == 'ok') {
|
||||
res.licenseKey = newLicenseKey.token;
|
||||
}
|
||||
return res;
|
||||
},
|
||||
|
||||
recryptDatabaseForExport(db) {
|
||||
const encryptionKey = generateTransportEncryptionKey();
|
||||
const transportEncryptor = createTransportEncryptor(encryptionKey);
|
||||
|
||||
@@ -536,14 +536,14 @@ module.exports = {
|
||||
},
|
||||
|
||||
dbloginAuthToken_meta: true,
|
||||
async dbloginAuthToken({ amoid, code, conid, redirectUri, sid }) {
|
||||
async dbloginAuthToken({ amoid, code, conid, redirectUri, sid }, req) {
|
||||
try {
|
||||
const connection = await this.getCore({ conid });
|
||||
const driver = requireEngineDriver(connection);
|
||||
const accessToken = await driver.getAuthTokenFromCode(connection, { code, redirectUri, sid });
|
||||
const volatile = await this.saveVolatile({ conid, accessToken });
|
||||
const authProvider = getAuthProviderById(amoid);
|
||||
const resp = await authProvider.login(null, null, { conid: volatile._id });
|
||||
const resp = await authProvider.login(null, null, { conid: volatile._id }, req);
|
||||
return resp;
|
||||
} catch (err) {
|
||||
logger.error(extractErrorLogData(err), 'Error getting DB token');
|
||||
@@ -552,18 +552,18 @@ module.exports = {
|
||||
},
|
||||
|
||||
dbloginAuth_meta: true,
|
||||
async dbloginAuth({ amoid, conid, user, password }) {
|
||||
async dbloginAuth({ amoid, conid, user, password }, req) {
|
||||
if (user || password) {
|
||||
const saveResp = await this.saveVolatile({ conid, user, password, test: true });
|
||||
if (saveResp.msgtype == 'connected') {
|
||||
const loginResp = await getAuthProviderById(amoid).login(user, password, { conid: saveResp._id });
|
||||
const loginResp = await getAuthProviderById(amoid).login(user, password, { conid: saveResp._id }, req);
|
||||
return loginResp;
|
||||
}
|
||||
return saveResp;
|
||||
}
|
||||
|
||||
// user and password is stored in connection, volatile connection is not needed
|
||||
const loginResp = await getAuthProviderById(amoid).login(null, null, { conid });
|
||||
const loginResp = await getAuthProviderById(amoid).login(null, null, { conid }, req);
|
||||
return loginResp;
|
||||
},
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ const { decryptConnection } = require('../utility/crypting');
|
||||
const { getSshTunnel } = require('../utility/sshTunnel');
|
||||
const sessions = require('./sessions');
|
||||
const jsldata = require('./jsldata');
|
||||
const { sendToAuditLog } = require('../utility/auditlog');
|
||||
|
||||
const logger = getLogger('databaseConnections');
|
||||
|
||||
@@ -83,8 +84,11 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
handle_response(conid, database, { msgid, ...response }) {
|
||||
const [resolve, reject] = this.requests[msgid];
|
||||
const [resolve, reject, additionalData] = this.requests[msgid];
|
||||
resolve(response);
|
||||
if (additionalData?.auditLogger) {
|
||||
additionalData?.auditLogger(response);
|
||||
}
|
||||
delete this.requests[msgid];
|
||||
},
|
||||
handle_status(conid, database, { status }) {
|
||||
@@ -215,10 +219,10 @@ module.exports = {
|
||||
},
|
||||
|
||||
/** @param {import('dbgate-types').OpenedDatabaseConnection} conn */
|
||||
sendRequest(conn, message) {
|
||||
sendRequest(conn, message, additionalData = {}) {
|
||||
const msgid = crypto.randomUUID();
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
this.requests[msgid] = [resolve, reject];
|
||||
this.requests[msgid] = [resolve, reject, additionalData];
|
||||
try {
|
||||
conn.subprocess.send({ msgid, ...message });
|
||||
} catch (err) {
|
||||
@@ -242,18 +246,57 @@ module.exports = {
|
||||
},
|
||||
|
||||
sqlSelect_meta: true,
|
||||
async sqlSelect({ conid, database, select }, req) {
|
||||
async sqlSelect({ conid, database, select, auditLogSessionGroup }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(opened, { msgtype: 'sqlSelect', select });
|
||||
const res = await this.sendRequest(
|
||||
opened,
|
||||
{ msgtype: 'sqlSelect', select },
|
||||
{
|
||||
auditLogger:
|
||||
auditLogSessionGroup && select?.from?.name?.pureName
|
||||
? response => {
|
||||
sendToAuditLog(req, {
|
||||
category: 'dbop',
|
||||
component: 'DatabaseConnectionsController',
|
||||
event: 'sql.select',
|
||||
action: 'select',
|
||||
severity: 'info',
|
||||
conid,
|
||||
database,
|
||||
schemaName: select?.from?.name?.schemaName,
|
||||
pureName: select?.from?.name?.pureName,
|
||||
sumint1: response?.rows?.length,
|
||||
sessionParam: `${conid}::${database}::${select?.from?.name?.schemaName || '0'}::${
|
||||
select?.from?.name?.pureName
|
||||
}`,
|
||||
sessionGroup: auditLogSessionGroup,
|
||||
message: `Loaded table data from ${select?.from?.name?.pureName}`,
|
||||
});
|
||||
}
|
||||
: null,
|
||||
}
|
||||
);
|
||||
return res;
|
||||
},
|
||||
|
||||
runScript_meta: true,
|
||||
async runScript({ conid, database, sql, useTransaction }, req) {
|
||||
async runScript({ conid, database, sql, useTransaction, logMessage }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
logger.info({ conid, database, sql }, 'Processing script');
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
sendToAuditLog(req, {
|
||||
category: 'dbop',
|
||||
component: 'DatabaseConnectionsController',
|
||||
event: 'sql.runscript',
|
||||
action: 'runscript',
|
||||
severity: 'info',
|
||||
conid,
|
||||
database,
|
||||
detail: sql,
|
||||
message: logMessage || `Running SQL script`,
|
||||
});
|
||||
|
||||
const res = await this.sendRequest(opened, { msgtype: 'runScript', sql, useTransaction });
|
||||
return res;
|
||||
},
|
||||
@@ -262,16 +305,53 @@ module.exports = {
|
||||
async runOperation({ conid, database, operation, useTransaction }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
logger.info({ conid, database, operation }, 'Processing operation');
|
||||
|
||||
sendToAuditLog(req, {
|
||||
category: 'dbop',
|
||||
component: 'DatabaseConnectionsController',
|
||||
event: 'sql.runoperation',
|
||||
action: operation.type,
|
||||
severity: 'info',
|
||||
conid,
|
||||
database,
|
||||
detail: operation,
|
||||
message: `Running DB operation: ${operation.type}`,
|
||||
});
|
||||
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(opened, { msgtype: 'runOperation', operation, useTransaction });
|
||||
return res;
|
||||
},
|
||||
|
||||
collectionData_meta: true,
|
||||
async collectionData({ conid, database, options }, req) {
|
||||
async collectionData({ conid, database, options, auditLogSessionGroup }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(opened, { msgtype: 'collectionData', options });
|
||||
const res = await this.sendRequest(
|
||||
opened,
|
||||
{ msgtype: 'collectionData', options },
|
||||
{
|
||||
auditLogger:
|
||||
auditLogSessionGroup && options?.pureName
|
||||
? response => {
|
||||
sendToAuditLog(req, {
|
||||
category: 'dbop',
|
||||
component: 'DatabaseConnectionsController',
|
||||
event: 'nosql.collectionData',
|
||||
action: 'select',
|
||||
severity: 'info',
|
||||
conid,
|
||||
database,
|
||||
pureName: options?.pureName,
|
||||
sumint1: response?.result?.rows?.length,
|
||||
sessionParam: `${conid}::${database}::${options?.pureName}`,
|
||||
sessionGroup: auditLogSessionGroup,
|
||||
message: `Loaded collection data ${options?.pureName}`,
|
||||
});
|
||||
}
|
||||
: null,
|
||||
}
|
||||
);
|
||||
return res.result || null;
|
||||
},
|
||||
|
||||
@@ -492,6 +572,20 @@ module.exports = {
|
||||
}
|
||||
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
|
||||
sendToAuditLog(req, {
|
||||
category: 'dbop',
|
||||
component: 'DatabaseConnectionsController',
|
||||
action: 'structure',
|
||||
event: 'dbStructure.get',
|
||||
severity: 'info',
|
||||
conid,
|
||||
database,
|
||||
sessionParam: `${conid}::${database}`,
|
||||
sessionGroup: 'getStructure',
|
||||
message: `Loaded database structure for ${database}`,
|
||||
});
|
||||
|
||||
return opened.structure;
|
||||
// const existing = this.opened.find((x) => x.conid == conid && x.database == database);
|
||||
// if (existing) return existing.status;
|
||||
|
||||
@@ -11,6 +11,8 @@ const apps = require('./apps');
|
||||
const getMapExport = require('../utility/getMapExport');
|
||||
const dbgateApi = require('../shell');
|
||||
const { getLogger } = require('dbgate-tools');
|
||||
const platformInfo = require('../utility/platformInfo');
|
||||
const { checkSecureFilePathsWithoutDirectory, checkSecureDirectories } = require('../utility/security');
|
||||
const logger = getLogger('files');
|
||||
|
||||
function serialize(format, data) {
|
||||
@@ -51,6 +53,9 @@ module.exports = {
|
||||
delete_meta: true,
|
||||
async delete({ folder, file }, req) {
|
||||
if (!hasPermission(`files/${folder}/write`, req)) return false;
|
||||
if (!checkSecureFilePathsWithoutDirectory(folder, file)) {
|
||||
return false;
|
||||
}
|
||||
await fs.unlink(path.join(filesdir(), folder, file));
|
||||
socket.emitChanged(`files-changed`, { folder });
|
||||
socket.emitChanged(`all-files-changed`);
|
||||
@@ -60,6 +65,9 @@ module.exports = {
|
||||
rename_meta: true,
|
||||
async rename({ folder, file, newFile }, req) {
|
||||
if (!hasPermission(`files/${folder}/write`, req)) return false;
|
||||
if (!checkSecureFilePathsWithoutDirectory(folder, file, newFile)) {
|
||||
return false;
|
||||
}
|
||||
await fs.rename(path.join(filesdir(), folder, file), path.join(filesdir(), folder, newFile));
|
||||
socket.emitChanged(`files-changed`, { folder });
|
||||
socket.emitChanged(`all-files-changed`);
|
||||
@@ -77,6 +85,9 @@ module.exports = {
|
||||
|
||||
copy_meta: true,
|
||||
async copy({ folder, file, newFile }, req) {
|
||||
if (!checkSecureFilePathsWithoutDirectory(folder, file, newFile)) {
|
||||
return false;
|
||||
}
|
||||
if (!hasPermission(`files/${folder}/write`, req)) return false;
|
||||
await fs.copyFile(path.join(filesdir(), folder, file), path.join(filesdir(), folder, newFile));
|
||||
socket.emitChanged(`files-changed`, { folder });
|
||||
@@ -86,6 +97,10 @@ module.exports = {
|
||||
|
||||
load_meta: true,
|
||||
async load({ folder, file, format }, req) {
|
||||
if (!checkSecureFilePathsWithoutDirectory(folder, file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (folder.startsWith('archive:')) {
|
||||
const text = await fs.readFile(path.join(resolveArchiveFolder(folder.substring('archive:'.length)), file), {
|
||||
encoding: 'utf-8',
|
||||
@@ -105,12 +120,20 @@ module.exports = {
|
||||
|
||||
loadFrom_meta: true,
|
||||
async loadFrom({ filePath, format }, req) {
|
||||
if (!platformInfo.isElectron) {
|
||||
// this is available only in electron app
|
||||
return false;
|
||||
}
|
||||
const text = await fs.readFile(filePath, { encoding: 'utf-8' });
|
||||
return deserialize(format, text);
|
||||
},
|
||||
|
||||
save_meta: true,
|
||||
async save({ folder, file, data, format }, req) {
|
||||
if (!checkSecureFilePathsWithoutDirectory(folder, file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (folder.startsWith('archive:')) {
|
||||
if (!hasPermission(`archive/write`, req)) return false;
|
||||
const dir = resolveArchiveFolder(folder.substring('archive:'.length));
|
||||
@@ -143,6 +166,11 @@ module.exports = {
|
||||
|
||||
saveAs_meta: true,
|
||||
async saveAs({ filePath, data, format }) {
|
||||
if (!platformInfo.isElectron) {
|
||||
// this is available only in electron app
|
||||
return false;
|
||||
}
|
||||
|
||||
await fs.writeFile(filePath, serialize(format, data));
|
||||
},
|
||||
|
||||
@@ -175,10 +203,10 @@ module.exports = {
|
||||
},
|
||||
|
||||
exportChart_meta: true,
|
||||
async exportChart({ filePath, title, config, image }) {
|
||||
async exportChart({ filePath, title, config, image, plugins }) {
|
||||
const fileName = path.parse(filePath).base;
|
||||
const imageFile = fileName.replace('.html', '-preview.png');
|
||||
const html = getChartExport(title, config, imageFile);
|
||||
const html = getChartExport(title, config, imageFile, plugins);
|
||||
await fs.writeFile(filePath, html);
|
||||
if (image) {
|
||||
const index = image.indexOf('base64,');
|
||||
@@ -275,6 +303,11 @@ module.exports = {
|
||||
|
||||
simpleCopy_meta: true,
|
||||
async simpleCopy({ sourceFilePath, targetFilePath }, req) {
|
||||
if (!platformInfo.isElectron) {
|
||||
if (!checkSecureDirectories(sourceFilePath, targetFilePath)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
await fs.copyFile(sourceFilePath, targetFilePath);
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -313,19 +313,9 @@ module.exports = {
|
||||
return true;
|
||||
});
|
||||
processor.finalize();
|
||||
return processor.charts;
|
||||
},
|
||||
|
||||
detectChartColumns_meta: true,
|
||||
async detectChartColumns({ jslid }) {
|
||||
const datastore = new JsonLinesDatastore(getJslFileName(jslid));
|
||||
const processor = new ChartProcessor();
|
||||
processor.autoDetectCharts = false;
|
||||
await datastore.enumRows(row => {
|
||||
processor.addRow(row);
|
||||
return true;
|
||||
});
|
||||
processor.finalize();
|
||||
return processor.availableColumns;
|
||||
return {
|
||||
charts: processor.charts,
|
||||
columns: processor.availableColumns,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -19,6 +19,8 @@ const {
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
const processArgs = require('../utility/processArgs');
|
||||
const platformInfo = require('../utility/platformInfo');
|
||||
const { checkSecureDirectories, checkSecureDirectoriesInScript } = require('../utility/security');
|
||||
const { sendToAuditLog, logJsonRunnerScript } = require('../utility/auditlog');
|
||||
const logger = getLogger('runners');
|
||||
|
||||
function extractPlugins(script) {
|
||||
@@ -269,18 +271,46 @@ module.exports = {
|
||||
},
|
||||
|
||||
start_meta: true,
|
||||
async start({ script }) {
|
||||
async start({ script }, req) {
|
||||
const runid = crypto.randomUUID();
|
||||
|
||||
if (script.type == 'json') {
|
||||
if (!platformInfo.isElectron) {
|
||||
if (!checkSecureDirectoriesInScript(script)) {
|
||||
return { errorMessage: 'Unallowed directories in script' };
|
||||
}
|
||||
}
|
||||
|
||||
logJsonRunnerScript(req, script);
|
||||
|
||||
const js = await jsonScriptToJavascript(script);
|
||||
return this.startCore(runid, scriptTemplate(js, false));
|
||||
}
|
||||
|
||||
if (!platformInfo.allowShellScripting) {
|
||||
sendToAuditLog(req, {
|
||||
category: 'shell',
|
||||
component: 'RunnersController',
|
||||
event: 'script.runFailed',
|
||||
action: 'script',
|
||||
severity: 'warn',
|
||||
detail: script,
|
||||
message: 'Scripts are not allowed',
|
||||
});
|
||||
|
||||
return { errorMessage: 'Shell scripting is not allowed' };
|
||||
}
|
||||
|
||||
sendToAuditLog(req, {
|
||||
category: 'shell',
|
||||
component: 'RunnersController',
|
||||
event: 'script.run.shell',
|
||||
action: 'script',
|
||||
severity: 'info',
|
||||
detail: script,
|
||||
message: 'Running JS script',
|
||||
});
|
||||
|
||||
return this.startCore(runid, scriptTemplate(script, false));
|
||||
},
|
||||
|
||||
@@ -317,6 +347,11 @@ module.exports = {
|
||||
|
||||
loadReader_meta: true,
|
||||
async loadReader({ functionName, props }) {
|
||||
if (!platformInfo.isElectron) {
|
||||
if (props?.fileName && !checkSecureDirectories(props.fileName)) {
|
||||
return { errorMessage: 'Unallowed file' };
|
||||
}
|
||||
}
|
||||
const prefix = extractShellApiPlugins(functionName)
|
||||
.map(packageName => `// @require ${packageName}\n`)
|
||||
.join('');
|
||||
|
||||
@@ -12,6 +12,7 @@ const { testConnectionPermission } = require('../utility/hasPermission');
|
||||
const { MissingCredentialsError } = require('../utility/exceptions');
|
||||
const pipeForkLogs = require('../utility/pipeForkLogs');
|
||||
const { getLogger, extractErrorLogData } = require('dbgate-tools');
|
||||
const { sendToAuditLog } = require('../utility/auditlog');
|
||||
|
||||
const logger = getLogger('serverConnection');
|
||||
|
||||
@@ -145,6 +146,17 @@ module.exports = {
|
||||
if (conid == '__model') return [];
|
||||
testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid);
|
||||
sendToAuditLog(req, {
|
||||
category: 'serverop',
|
||||
component: 'ServerConnectionsController',
|
||||
action: 'listDatabases',
|
||||
event: 'databases.list',
|
||||
severity: 'info',
|
||||
conid,
|
||||
sessionParam: `${conid}`,
|
||||
sessionGroup: 'listDatabases',
|
||||
message: `Loaded databases for connection`,
|
||||
});
|
||||
return opened?.databases ?? [];
|
||||
},
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ const { appdir } = require('../utility/directories');
|
||||
const { getLogger, extractErrorLogData } = require('dbgate-tools');
|
||||
const pipeForkLogs = require('../utility/pipeForkLogs');
|
||||
const config = require('./config');
|
||||
const { sendToAuditLog } = require('../utility/auditlog');
|
||||
|
||||
const logger = getLogger('sessions');
|
||||
|
||||
@@ -146,15 +147,34 @@ module.exports = {
|
||||
},
|
||||
|
||||
executeQuery_meta: true,
|
||||
async executeQuery({ sesid, sql, autoCommit, limitRows, frontMatter }) {
|
||||
async executeQuery({ sesid, sql, autoCommit, autoDetectCharts, limitRows, frontMatter }, req) {
|
||||
const session = this.opened.find(x => x.sesid == sesid);
|
||||
if (!session) {
|
||||
throw new Error('Invalid session');
|
||||
}
|
||||
|
||||
sendToAuditLog(req, {
|
||||
category: 'dbop',
|
||||
component: 'SessionController',
|
||||
action: 'executeQuery',
|
||||
event: 'query.execute',
|
||||
severity: 'info',
|
||||
detail: sql,
|
||||
conid: session.conid,
|
||||
database: session.database,
|
||||
message: 'Executing query',
|
||||
});
|
||||
|
||||
logger.info({ sesid, sql }, 'Processing query');
|
||||
this.dispatchMessage(sesid, 'Query execution started');
|
||||
session.subprocess.send({ msgtype: 'executeQuery', sql, autoCommit, limitRows, frontMatter });
|
||||
session.subprocess.send({
|
||||
msgtype: 'executeQuery',
|
||||
sql,
|
||||
autoCommit,
|
||||
autoDetectCharts: autoDetectCharts || !!frontMatter?.['selected-chart'],
|
||||
limitRows,
|
||||
frontMatter,
|
||||
});
|
||||
|
||||
return { state: 'ok' };
|
||||
},
|
||||
|
||||
@@ -31,6 +31,11 @@ module.exports = {
|
||||
return {};
|
||||
},
|
||||
|
||||
sendAuditLog_meta: true,
|
||||
async sendAuditLog({}) {
|
||||
return null;
|
||||
},
|
||||
|
||||
startRefreshLicense() {},
|
||||
|
||||
async getUsedEngines() {
|
||||
|
||||
@@ -44,6 +44,10 @@ module.exports = {
|
||||
raw: true,
|
||||
},
|
||||
get(req, res) {
|
||||
if (req.query.file.includes('..') || req.query.file.includes('/') || req.query.file.includes('\\')) {
|
||||
res.status(400).send('Invalid file path');
|
||||
return;
|
||||
}
|
||||
res.sendFile(path.join(uploadsdir(), req.query.file));
|
||||
},
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ async function handleExecuteControlCommand({ command }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleExecuteQuery({ sql, autoCommit, limitRows, frontMatter }) {
|
||||
async function handleExecuteQuery({ sql, autoCommit, autoDetectCharts, limitRows, frontMatter }) {
|
||||
lastActivity = new Date().getTime();
|
||||
|
||||
await waitConnected();
|
||||
@@ -146,7 +146,16 @@ async function handleExecuteQuery({ sql, autoCommit, limitRows, frontMatter }) {
|
||||
...driver.getQuerySplitterOptions('stream'),
|
||||
returnRichInfo: true,
|
||||
})) {
|
||||
await handleQueryStream(dbhan, driver, queryStreamInfoHolder, sqlItem, undefined, limitRows, frontMatter);
|
||||
await handleQueryStream(
|
||||
dbhan,
|
||||
driver,
|
||||
queryStreamInfoHolder,
|
||||
sqlItem,
|
||||
undefined,
|
||||
limitRows,
|
||||
frontMatter,
|
||||
autoDetectCharts
|
||||
);
|
||||
// const handler = new StreamHandler(resultIndex);
|
||||
// const stream = await driver.stream(systemConnection, sqlItem, handler);
|
||||
// handler.stream = stream;
|
||||
|
||||
@@ -14,7 +14,7 @@ const crypto = require('crypto');
|
||||
* @param {object} options.driver - driver object. If not provided, it will be loaded from connection
|
||||
* @param {object} options.analysedStructure - analysed structure of the database. If not provided, it will be loaded
|
||||
* @param {string} options.modelFolder - folder with model files (YAML files for tables, SQL files for views, procedures, ...)
|
||||
* @param {import('dbgate-tools').DatabaseModelFile[]} options.loadedDbModel - loaded database model - collection of yaml and SQL files loaded into array
|
||||
* @param {import('dbgate-tools').DatabaseModelFile[] | import('dbgate-types').DatabaseInfo} options.loadedDbModel - loaded database model - collection of yaml and SQL files loaded into array
|
||||
* @param {function[]} options.modelTransforms - array of functions for transforming model
|
||||
* @param {object} options.dbdiffOptionsExtra - extra options for dbdiff
|
||||
* @param {string} options.ignoreNameRegex - regex for ignoring objects by name
|
||||
|
||||
@@ -23,7 +23,7 @@ const { connectUtility } = require('../utility/connectUtility');
|
||||
* @param {object} options.driver - driver object. If not provided, it will be loaded from connection
|
||||
* @param {object} options.analysedStructure - analysed structure of the database. If not provided, it will be loaded
|
||||
* @param {string} options.modelFolder - folder with model files (YAML files for tables, SQL files for views, procedures, ...)
|
||||
* @param {import('dbgate-tools').DatabaseModelFile[]} options.loadedDbModel - loaded database model - collection of yaml and SQL files loaded into array
|
||||
* @param {import('dbgate-tools').DatabaseModelFile[] | import('dbgate-types').DatabaseInfo} options.loadedDbModel - loaded database model - collection of yaml and SQL files loaded into array
|
||||
* @param {function[]} options.modelTransforms - array of functions for transforming model
|
||||
* @param {object} options.dbdiffOptionsExtra - extra options for dbdiff
|
||||
* @param {string} options.ignoreNameRegex - regex for ignoring objects by name
|
||||
|
||||
@@ -3,7 +3,7 @@ const fs = require('fs-extra');
|
||||
const executeQuery = require('./executeQuery');
|
||||
const { connectUtility } = require('../utility/connectUtility');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const { getAlterDatabaseScript, DatabaseAnalyser, runCommandOnDriver } = require('dbgate-tools');
|
||||
const { getAlterDatabaseScript, DatabaseAnalyser, runCommandOnDriver, adaptDatabaseInfo } = require('dbgate-tools');
|
||||
const importDbModel = require('../utility/importDbModel');
|
||||
const jsonLinesReader = require('./jsonLinesReader');
|
||||
const tableWriter = require('./tableWriter');
|
||||
@@ -17,9 +17,8 @@ const copyStream = require('./copyStream');
|
||||
* @param {object} options.driver - driver object. If not provided, it will be loaded from connection
|
||||
* @param {string} options.folder - folder with model files (YAML files for tables, SQL files for views, procedures, ...)
|
||||
* @param {function[]} options.modelTransforms - array of functions for transforming model
|
||||
* @param {((row: Record<string, any>) => Record<string, any>) | undefined} options.transformRow - function to transform each row
|
||||
*/
|
||||
async function importDbFromFolder({ connection, systemConnection, driver, folder, modelTransforms, transformRow }) {
|
||||
async function importDbFromFolder({ connection, systemConnection, driver, folder, modelTransforms }) {
|
||||
if (!driver) driver = requireEngineDriver(connection);
|
||||
const dbhan = systemConnection || (await connectUtility(driver, connection, 'read'));
|
||||
|
||||
@@ -27,10 +26,7 @@ async function importDbFromFolder({ connection, systemConnection, driver, folder
|
||||
if (driver?.databaseEngineTypes?.includes('sql')) {
|
||||
const model = await importDbModel(folder);
|
||||
|
||||
let modelAdapted = {
|
||||
...model,
|
||||
tables: model.tables.map(table => driver.adaptTableInfo(table)),
|
||||
};
|
||||
let modelAdapted = adaptDatabaseInfo(model, driver);
|
||||
for (const transform of modelTransforms || []) {
|
||||
modelAdapted = transform(modelAdapted);
|
||||
}
|
||||
@@ -78,7 +74,7 @@ async function importDbFromFolder({ connection, systemConnection, driver, folder
|
||||
for (const table of modelAdapted.tables) {
|
||||
const fileName = path.join(folder, `${table.pureName}.jsonl`);
|
||||
if (await fs.exists(fileName)) {
|
||||
const src = await jsonLinesReader({ fileName, transformRow });
|
||||
const src = await jsonLinesReader({ fileName });
|
||||
const dst = await tableWriter({
|
||||
systemConnection: dbhan,
|
||||
pureName: table.pureName,
|
||||
@@ -106,7 +102,7 @@ async function importDbFromFolder({ connection, systemConnection, driver, folder
|
||||
for (const file of fs.readdirSync(folder)) {
|
||||
if (!file.endsWith('.jsonl')) continue;
|
||||
const pureName = path.parse(file).name;
|
||||
const src = await jsonLinesReader({ fileName: path.join(folder, file), transformRow });
|
||||
const src = await jsonLinesReader({ fileName: path.join(folder, file) });
|
||||
const dst = await tableWriter({
|
||||
systemConnection: dbhan,
|
||||
pureName,
|
||||
|
||||
@@ -6,11 +6,10 @@ const download = require('./download');
|
||||
const logger = getLogger('jsonLinesReader');
|
||||
|
||||
class ParseStream extends stream.Transform {
|
||||
constructor({ limitRows, transformRow }) {
|
||||
constructor({ limitRows }) {
|
||||
super({ objectMode: true });
|
||||
this.wasHeader = false;
|
||||
this.limitRows = limitRows;
|
||||
this.transformRow = transformRow;
|
||||
this.rowsWritten = 0;
|
||||
}
|
||||
_transform(chunk, encoding, done) {
|
||||
@@ -27,11 +26,7 @@ class ParseStream extends stream.Transform {
|
||||
this.wasHeader = true;
|
||||
}
|
||||
if (!this.limitRows || this.rowsWritten < this.limitRows) {
|
||||
if (this.transformRow) {
|
||||
this.push(this.transformRow(obj));
|
||||
} else {
|
||||
this.push(obj);
|
||||
}
|
||||
this.push(obj);
|
||||
this.rowsWritten += 1;
|
||||
}
|
||||
done();
|
||||
@@ -44,10 +39,9 @@ class ParseStream extends stream.Transform {
|
||||
* @param {string} options.fileName - file name or URL
|
||||
* @param {string} options.encoding - encoding of the file
|
||||
* @param {number} options.limitRows - maximum number of rows to read
|
||||
* @param {((row: Record<string, any>) => Record<string, any>) | undefined} options.transformRow - function to transform each row
|
||||
* @returns {Promise<readerType>} - reader object
|
||||
*/
|
||||
async function jsonLinesReader({ fileName, encoding = 'utf-8', limitRows = undefined, transformRow }) {
|
||||
async function jsonLinesReader({ fileName, encoding = 'utf-8', limitRows = undefined }) {
|
||||
logger.info(`Reading file ${fileName}`);
|
||||
|
||||
const downloadedFile = await download(fileName);
|
||||
@@ -58,7 +52,7 @@ async function jsonLinesReader({ fileName, encoding = 'utf-8', limitRows = undef
|
||||
encoding
|
||||
);
|
||||
const liner = byline(fileStream);
|
||||
const parser = new ParseStream({ limitRows, transformRow });
|
||||
const parser = new ParseStream({ limitRows });
|
||||
return [liner, parser];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,192 @@
|
||||
module.exports = {
|
||||
"tables": [
|
||||
{
|
||||
"pureName": "audit_log",
|
||||
"columns": [
|
||||
{
|
||||
"pureName": "audit_log",
|
||||
"columnName": "id",
|
||||
"dataType": "int",
|
||||
"autoIncrement": true,
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"pureName": "audit_log",
|
||||
"columnName": "created",
|
||||
"dataType": "bigint",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"pureName": "audit_log",
|
||||
"columnName": "modified",
|
||||
"dataType": "bigint",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "audit_log",
|
||||
"columnName": "user_id",
|
||||
"dataType": "int",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "audit_log",
|
||||
"columnName": "user_login",
|
||||
"dataType": "varchar(250)",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "audit_log",
|
||||
"columnName": "category",
|
||||
"dataType": "varchar(50)",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "audit_log",
|
||||
"columnName": "component",
|
||||
"dataType": "varchar(50)",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "audit_log",
|
||||
"columnName": "action",
|
||||
"dataType": "varchar(50)",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "audit_log",
|
||||
"columnName": "severity",
|
||||
"dataType": "varchar(50)",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "audit_log",
|
||||
"columnName": "event",
|
||||
"dataType": "varchar(100)",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "audit_log",
|
||||
"columnName": "message",
|
||||
"dataType": "varchar(250)",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "audit_log",
|
||||
"columnName": "detail",
|
||||
"dataType": "varchar(1000)",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "audit_log",
|
||||
"columnName": "detail_full_length",
|
||||
"dataType": "int",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "audit_log",
|
||||
"columnName": "session_id",
|
||||
"dataType": "varchar(200)",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "audit_log",
|
||||
"columnName": "session_group",
|
||||
"dataType": "varchar(50)",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "audit_log",
|
||||
"columnName": "session_param",
|
||||
"dataType": "varchar(200)",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "audit_log",
|
||||
"columnName": "conid",
|
||||
"dataType": "varchar(100)",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "audit_log",
|
||||
"columnName": "connection_data",
|
||||
"dataType": "varchar(1000)",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "audit_log",
|
||||
"columnName": "database",
|
||||
"dataType": "varchar(200)",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "audit_log",
|
||||
"columnName": "schema_name",
|
||||
"dataType": "varchar(100)",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "audit_log",
|
||||
"columnName": "pure_name",
|
||||
"dataType": "varchar(100)",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "audit_log",
|
||||
"columnName": "sumint_1",
|
||||
"dataType": "int",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "audit_log",
|
||||
"columnName": "sumint_2",
|
||||
"dataType": "int",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"constraintType": "foreignKey",
|
||||
"constraintName": "FK_audit_log_user_id",
|
||||
"pureName": "audit_log",
|
||||
"refTableName": "users",
|
||||
"deleteAction": "SET NULL",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "user_id",
|
||||
"refColumnName": "id"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"constraintName": "idx_audit_log_session",
|
||||
"pureName": "audit_log",
|
||||
"constraintType": "index",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "session_group"
|
||||
},
|
||||
{
|
||||
"columnName": "session_id"
|
||||
},
|
||||
{
|
||||
"columnName": "session_param"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"pureName": "audit_log",
|
||||
"constraintType": "primaryKey",
|
||||
"constraintName": "PK_audit_log",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "id"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"pureName": "auth_methods",
|
||||
"columns": [
|
||||
@@ -50,6 +237,7 @@ module.exports = {
|
||||
"primaryKey": {
|
||||
"pureName": "auth_methods",
|
||||
"constraintType": "primaryKey",
|
||||
"constraintName": "PK_auth_methods",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "id"
|
||||
@@ -61,7 +249,8 @@ module.exports = {
|
||||
"id": -1,
|
||||
"amoid": "790ca4d2-7f01-4800-955b-d691b890cc50",
|
||||
"name": "Anonymous",
|
||||
"type": "none"
|
||||
"type": "none",
|
||||
"is_disabled": 1
|
||||
},
|
||||
{
|
||||
"id": -2,
|
||||
@@ -69,6 +258,9 @@ module.exports = {
|
||||
"name": "Local",
|
||||
"type": "local"
|
||||
}
|
||||
],
|
||||
"preloadedRowsInsertOnly": [
|
||||
"is_disabled"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -103,6 +295,7 @@ module.exports = {
|
||||
"foreignKeys": [
|
||||
{
|
||||
"constraintType": "foreignKey",
|
||||
"constraintName": "FK_auth_methods_config_auth_method_id",
|
||||
"pureName": "auth_methods_config",
|
||||
"refTableName": "auth_methods",
|
||||
"deleteAction": "CASCADE",
|
||||
@@ -114,9 +307,25 @@ module.exports = {
|
||||
]
|
||||
}
|
||||
],
|
||||
"uniques": [
|
||||
{
|
||||
"constraintName": "UQ_auth_methods_config_auth_method_id_key",
|
||||
"pureName": "auth_methods_config",
|
||||
"constraintType": "unique",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "auth_method_id"
|
||||
},
|
||||
{
|
||||
"columnName": "key"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"pureName": "auth_methods_config",
|
||||
"constraintType": "primaryKey",
|
||||
"constraintName": "PK_auth_methods_config",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "id"
|
||||
@@ -154,9 +363,25 @@ module.exports = {
|
||||
}
|
||||
],
|
||||
"foreignKeys": [],
|
||||
"uniques": [
|
||||
{
|
||||
"constraintName": "UQ_config_group_key",
|
||||
"pureName": "config",
|
||||
"constraintType": "unique",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "group"
|
||||
},
|
||||
{
|
||||
"columnName": "key"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"pureName": "config",
|
||||
"constraintType": "primaryKey",
|
||||
"constraintName": "PK_config",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "id"
|
||||
@@ -294,6 +519,12 @@ module.exports = {
|
||||
"dataType": "int",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "connections",
|
||||
"columnName": "useSeparateSchemas",
|
||||
"dataType": "int",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "connections",
|
||||
"columnName": "defaultDatabase",
|
||||
@@ -449,6 +680,7 @@ module.exports = {
|
||||
"primaryKey": {
|
||||
"pureName": "connections",
|
||||
"constraintType": "primaryKey",
|
||||
"constraintName": "PK_connections",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "id"
|
||||
@@ -477,6 +709,7 @@ module.exports = {
|
||||
"primaryKey": {
|
||||
"pureName": "roles",
|
||||
"constraintType": "primaryKey",
|
||||
"constraintName": "PK_roles",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "id"
|
||||
@@ -524,6 +757,7 @@ module.exports = {
|
||||
"foreignKeys": [
|
||||
{
|
||||
"constraintType": "foreignKey",
|
||||
"constraintName": "FK_role_connections_role_id",
|
||||
"pureName": "role_connections",
|
||||
"refTableName": "roles",
|
||||
"deleteAction": "CASCADE",
|
||||
@@ -536,6 +770,7 @@ module.exports = {
|
||||
},
|
||||
{
|
||||
"constraintType": "foreignKey",
|
||||
"constraintName": "FK_role_connections_connection_id",
|
||||
"pureName": "role_connections",
|
||||
"refTableName": "connections",
|
||||
"deleteAction": "CASCADE",
|
||||
@@ -550,6 +785,7 @@ module.exports = {
|
||||
"primaryKey": {
|
||||
"pureName": "role_connections",
|
||||
"constraintType": "primaryKey",
|
||||
"constraintName": "PK_role_connections",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "id"
|
||||
@@ -583,6 +819,7 @@ module.exports = {
|
||||
"foreignKeys": [
|
||||
{
|
||||
"constraintType": "foreignKey",
|
||||
"constraintName": "FK_role_permissions_role_id",
|
||||
"pureName": "role_permissions",
|
||||
"refTableName": "roles",
|
||||
"deleteAction": "CASCADE",
|
||||
@@ -597,6 +834,7 @@ module.exports = {
|
||||
"primaryKey": {
|
||||
"pureName": "role_permissions",
|
||||
"constraintType": "primaryKey",
|
||||
"constraintName": "PK_role_permissions",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "id"
|
||||
@@ -637,6 +875,7 @@ module.exports = {
|
||||
"primaryKey": {
|
||||
"pureName": "users",
|
||||
"constraintType": "primaryKey",
|
||||
"constraintName": "PK_users",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "id"
|
||||
@@ -670,6 +909,7 @@ module.exports = {
|
||||
"foreignKeys": [
|
||||
{
|
||||
"constraintType": "foreignKey",
|
||||
"constraintName": "FK_user_connections_user_id",
|
||||
"pureName": "user_connections",
|
||||
"refTableName": "users",
|
||||
"deleteAction": "CASCADE",
|
||||
@@ -682,6 +922,7 @@ module.exports = {
|
||||
},
|
||||
{
|
||||
"constraintType": "foreignKey",
|
||||
"constraintName": "FK_user_connections_connection_id",
|
||||
"pureName": "user_connections",
|
||||
"refTableName": "connections",
|
||||
"deleteAction": "CASCADE",
|
||||
@@ -696,6 +937,7 @@ module.exports = {
|
||||
"primaryKey": {
|
||||
"pureName": "user_connections",
|
||||
"constraintType": "primaryKey",
|
||||
"constraintName": "PK_user_connections",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "id"
|
||||
@@ -729,6 +971,7 @@ module.exports = {
|
||||
"foreignKeys": [
|
||||
{
|
||||
"constraintType": "foreignKey",
|
||||
"constraintName": "FK_user_permissions_user_id",
|
||||
"pureName": "user_permissions",
|
||||
"refTableName": "users",
|
||||
"deleteAction": "CASCADE",
|
||||
@@ -743,6 +986,7 @@ module.exports = {
|
||||
"primaryKey": {
|
||||
"pureName": "user_permissions",
|
||||
"constraintType": "primaryKey",
|
||||
"constraintName": "PK_user_permissions",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "id"
|
||||
@@ -776,6 +1020,7 @@ module.exports = {
|
||||
"foreignKeys": [
|
||||
{
|
||||
"constraintType": "foreignKey",
|
||||
"constraintName": "FK_user_roles_user_id",
|
||||
"pureName": "user_roles",
|
||||
"refTableName": "users",
|
||||
"deleteAction": "CASCADE",
|
||||
@@ -788,6 +1033,7 @@ module.exports = {
|
||||
},
|
||||
{
|
||||
"constraintType": "foreignKey",
|
||||
"constraintName": "FK_user_roles_role_id",
|
||||
"pureName": "user_roles",
|
||||
"refTableName": "roles",
|
||||
"deleteAction": "CASCADE",
|
||||
@@ -802,6 +1048,7 @@ module.exports = {
|
||||
"primaryKey": {
|
||||
"pureName": "user_roles",
|
||||
"constraintType": "primaryKey",
|
||||
"constraintName": "PK_user_roles",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "id"
|
||||
@@ -815,5 +1062,6 @@ module.exports = {
|
||||
"matviews": [],
|
||||
"functions": [],
|
||||
"procedures": [],
|
||||
"triggers": []
|
||||
"triggers": [],
|
||||
"schedulerEvents": []
|
||||
};
|
||||
9
packages/api/src/utility/auditlog.js
Normal file
9
packages/api/src/utility/auditlog.js
Normal file
@@ -0,0 +1,9 @@
|
||||
// only in DbGate Premium
|
||||
|
||||
async function sendToAuditLog(req, props) {}
|
||||
async function logJsonRunnerScript(req, script) {}
|
||||
|
||||
module.exports = {
|
||||
sendToAuditLog,
|
||||
logJsonRunnerScript,
|
||||
};
|
||||
@@ -40,6 +40,12 @@ function getLicenseHttpHeaders() {
|
||||
return {};
|
||||
}
|
||||
|
||||
async function tryToGetRefreshedLicense(oldLicenseKey) {
|
||||
return {
|
||||
status: 'error',
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isAuthProxySupported,
|
||||
authProxyGetRedirectUrl,
|
||||
@@ -52,4 +58,5 @@ module.exports = {
|
||||
callCompleteOnCursorApi,
|
||||
callRefactorSqlQueryApi,
|
||||
getLicenseHttpHeaders,
|
||||
tryToGetRefreshedLicense,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const axios = require('axios');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs-extra');
|
||||
const _ = require('lodash');
|
||||
const path = require('path');
|
||||
@@ -20,21 +21,26 @@ let cloudFiles = null;
|
||||
|
||||
const DBGATE_IDENTITY_URL = process.env.LOCAL_DBGATE_IDENTITY
|
||||
? 'http://localhost:3103'
|
||||
: process.env.PROD_DBGATE_IDENTITY
|
||||
? 'https://identity.dbgate.io'
|
||||
: process.env.DEVWEB || process.env.DEVMODE
|
||||
? 'https://identity.dbgate.udolni.net'
|
||||
: 'https://identity.dbgate.io';
|
||||
|
||||
const DBGATE_CLOUD_URL = process.env.LOCAL_DBGATE_CLOUD
|
||||
? 'http://localhost:3110'
|
||||
: process.env.PROD_DBGATE_CLOUD
|
||||
? 'https://cloud.dbgate.io'
|
||||
: process.env.DEVWEB || process.env.DEVMODE
|
||||
? 'https://cloud.dbgate.udolni.net'
|
||||
: 'https://cloud.dbgate.io';
|
||||
|
||||
async function createDbGateIdentitySession(client) {
|
||||
async function createDbGateIdentitySession(client, redirectUri) {
|
||||
const resp = await axios.default.post(
|
||||
`${DBGATE_IDENTITY_URL}/api/create-session`,
|
||||
{
|
||||
client,
|
||||
redirectUri,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
@@ -66,7 +72,7 @@ function startCloudTokenChecking(sid, callback) {
|
||||
});
|
||||
// console.log('CHECK RESP:', resp.data);
|
||||
|
||||
if (resp.data.email) {
|
||||
if (resp.data?.email) {
|
||||
clearInterval(interval);
|
||||
callback(resp.data);
|
||||
}
|
||||
@@ -76,6 +82,34 @@ function startCloudTokenChecking(sid, callback) {
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function readCloudTokenHolder(sid) {
|
||||
const resp = await axios.default.get(`${DBGATE_IDENTITY_URL}/api/get-token/${sid}`, {
|
||||
headers: {
|
||||
...getLicenseHttpHeaders(),
|
||||
},
|
||||
});
|
||||
if (resp.data?.email) {
|
||||
return resp.data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function readCloudTestTokenHolder(email) {
|
||||
const resp = await axios.default.post(
|
||||
`${DBGATE_IDENTITY_URL}/api/test-token`,
|
||||
{ email },
|
||||
{
|
||||
headers: {
|
||||
...getLicenseHttpHeaders(),
|
||||
},
|
||||
}
|
||||
);
|
||||
if (resp.data?.email) {
|
||||
return resp.data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function loadCloudFiles() {
|
||||
try {
|
||||
const fileContent = await fs.readFile(path.join(datadir(), 'cloud-files.jsonl'), 'utf-8');
|
||||
@@ -86,6 +120,16 @@ async function loadCloudFiles() {
|
||||
}
|
||||
}
|
||||
|
||||
async function getCloudUsedEngines() {
|
||||
try {
|
||||
const resp = await callCloudApiGet('content-engines');
|
||||
return resp || [];
|
||||
} catch (err) {
|
||||
logger.error(extractErrorLogData(err), 'Error getting cloud content list');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function collectCloudFilesSearchTags() {
|
||||
const res = [];
|
||||
if (platformInfo.isElectron) {
|
||||
@@ -120,11 +164,14 @@ async function collectCloudFilesSearchTags() {
|
||||
const engines = await connections.getUsedEngines();
|
||||
const engineTags = engines.map(engine => engine.split('@')[0]);
|
||||
res.push(...engineTags);
|
||||
const cloudEngines = await getCloudUsedEngines();
|
||||
const cloudEngineTags = cloudEngines.map(engine => engine.split('@')[0]);
|
||||
res.push(...cloudEngineTags);
|
||||
|
||||
// team-premium and trials will return the same cloud files as premium - no need to check
|
||||
res.push(isProApp() ? 'premium' : 'community');
|
||||
|
||||
return res;
|
||||
return _.uniq(res);
|
||||
}
|
||||
|
||||
async function getCloudSigninHolder() {
|
||||
@@ -170,7 +217,7 @@ async function updateCloudFiles(isRefresh) {
|
||||
{
|
||||
headers: {
|
||||
...getLicenseHttpHeaders(),
|
||||
...(await getCloudSigninHeaders()),
|
||||
...(await getCloudInstanceHeaders()),
|
||||
'x-app-version': currentVersion.version,
|
||||
},
|
||||
}
|
||||
@@ -243,13 +290,28 @@ async function callCloudApiGet(endpoint, signinHolder = null, additionalHeaders
|
||||
},
|
||||
validateStatus: status => status < 500,
|
||||
});
|
||||
const { errorMessage } = resp.data;
|
||||
const { errorMessage, isLicenseLimit, limitedLicenseLimits } = resp.data;
|
||||
if (errorMessage) {
|
||||
return { apiErrorMessage: errorMessage };
|
||||
return {
|
||||
apiErrorMessage: errorMessage,
|
||||
apiErrorIsLicenseLimit: isLicenseLimit,
|
||||
apiErrorLimitedLicenseLimits: limitedLicenseLimits,
|
||||
};
|
||||
}
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
async function getCloudInstanceHeaders() {
|
||||
if (!(await fs.exists(path.join(datadir(), 'cloud-instance.txt')))) {
|
||||
const newInstanceId = crypto.randomUUID();
|
||||
await fs.writeFile(path.join(datadir(), 'cloud-instance.txt'), newInstanceId);
|
||||
}
|
||||
const instanceId = await fs.readFile(path.join(datadir(), 'cloud-instance.txt'), 'utf-8');
|
||||
return {
|
||||
'x-cloud-instance': instanceId,
|
||||
};
|
||||
}
|
||||
|
||||
async function callCloudApiPost(endpoint, body, signinHolder = null) {
|
||||
if (!signinHolder) {
|
||||
signinHolder = await getCloudSigninHolder();
|
||||
@@ -293,7 +355,7 @@ async function getCloudContent(folid, cntid) {
|
||||
|
||||
const encryptor = simpleEncryptor.createEncryptor(signinHolder.encryptionKey);
|
||||
|
||||
const { content, name, type, contentFolder, contentType, apiErrorMessage } = await callCloudApiGet(
|
||||
const { content, name, type, contentAttributes, apiErrorMessage } = await callCloudApiGet(
|
||||
`content/${folid}/${cntid}`,
|
||||
signinHolder,
|
||||
{
|
||||
@@ -309,8 +371,7 @@ async function getCloudContent(folid, cntid) {
|
||||
content: encryptor.decrypt(content),
|
||||
name,
|
||||
type,
|
||||
contentFolder,
|
||||
contentType,
|
||||
contentAttributes,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -318,7 +379,7 @@ async function getCloudContent(folid, cntid) {
|
||||
*
|
||||
* @returns Promise<{ cntid: string } | { apiErrorMessage: string }>
|
||||
*/
|
||||
async function putCloudContent(folid, cntid, content, name, type, contentFolder = null, contentType = null) {
|
||||
async function putCloudContent(folid, cntid, content, name, type, contentAttributes) {
|
||||
const signinHolder = await getCloudSigninHolder();
|
||||
if (!signinHolder) {
|
||||
throw new Error('No signed in');
|
||||
@@ -335,8 +396,7 @@ async function putCloudContent(folid, cntid, content, name, type, contentFolder
|
||||
type,
|
||||
kehid: signinHolder.kehid,
|
||||
content: encryptor.encrypt(content),
|
||||
contentFolder,
|
||||
contentType,
|
||||
contentAttributes,
|
||||
},
|
||||
signinHolder
|
||||
);
|
||||
@@ -377,4 +437,6 @@ module.exports = {
|
||||
loadCachedCloudConnection,
|
||||
putCloudContent,
|
||||
removeCloudCachedConnection,
|
||||
readCloudTokenHolder,
|
||||
readCloudTestTokenHolder,
|
||||
};
|
||||
|
||||
@@ -88,13 +88,33 @@ async function extractConnectionSslParams(connection) {
|
||||
return ssl;
|
||||
}
|
||||
|
||||
async function decryptCloudConnection(connection) {
|
||||
const { getCloudFolderEncryptor } = require('./cloudIntf');
|
||||
|
||||
const m = connection?._id?.match(/^cloud\:\/\/(.+)\/(.+)$/);
|
||||
if (!m) {
|
||||
throw new Error('Invalid cloud connection ID format');
|
||||
}
|
||||
|
||||
const folid = m[1];
|
||||
const cntid = m[2];
|
||||
|
||||
const folderEncryptor = await getCloudFolderEncryptor(folid);
|
||||
return decryptConnection(connection, folderEncryptor);
|
||||
}
|
||||
|
||||
async function connectUtility(driver, storedConnection, connectionMode, additionalOptions = null) {
|
||||
const connectionLoaded = await loadConnection(driver, storedConnection, connectionMode);
|
||||
|
||||
const connection = {
|
||||
database: connectionLoaded.defaultDatabase,
|
||||
...decryptConnection(connectionLoaded),
|
||||
};
|
||||
const connection = connectionLoaded?._id?.startsWith('cloud://')
|
||||
? {
|
||||
database: connectionLoaded.defaultDatabase,
|
||||
...(await decryptCloudConnection(connectionLoaded)),
|
||||
}
|
||||
: {
|
||||
database: connectionLoaded.defaultDatabase,
|
||||
...decryptConnection(connectionLoaded),
|
||||
};
|
||||
|
||||
if (!connection.port && driver.defaultPort) {
|
||||
connection.port = driver.defaultPort.toString();
|
||||
|
||||
@@ -91,11 +91,11 @@ function encryptObjectPasswordField(obj, field, encryptor = null) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
function decryptObjectPasswordField(obj, field) {
|
||||
function decryptObjectPasswordField(obj, field, encryptor = null) {
|
||||
if (obj && obj[field] && obj[field].startsWith('crypt:')) {
|
||||
return {
|
||||
...obj,
|
||||
[field]: getInternalEncryptor().decrypt(obj[field].substring('crypt:'.length)),
|
||||
[field]: (encryptor || getInternalEncryptor()).decrypt(obj[field].substring('crypt:'.length)),
|
||||
};
|
||||
}
|
||||
return obj;
|
||||
@@ -115,10 +115,10 @@ function maskConnection(connection) {
|
||||
return _.omit(connection, ['password', 'sshPassword', 'sshKeyfilePassword']);
|
||||
}
|
||||
|
||||
function decryptConnection(connection) {
|
||||
connection = decryptObjectPasswordField(connection, 'password');
|
||||
connection = decryptObjectPasswordField(connection, 'sshPassword');
|
||||
connection = decryptObjectPasswordField(connection, 'sshKeyfilePassword');
|
||||
function decryptConnection(connection, encryptor = null) {
|
||||
connection = decryptObjectPasswordField(connection, 'password', encryptor);
|
||||
connection = decryptObjectPasswordField(connection, 'sshPassword', encryptor);
|
||||
connection = decryptObjectPasswordField(connection, 'sshKeyfilePassword', encryptor);
|
||||
return connection;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
const getChartExport = (title, config, imageFile) => {
|
||||
const getChartExport = (title, config, imageFile, plugins) => {
|
||||
const PLUGIN_TAGS = {
|
||||
zoom: '<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-zoom/1.2.0/chartjs-plugin-zoom.min.js" integrity="sha512-TT0wAMqqtjXVzpc48sI0G84rBP+oTkBZPgeRYIOVRGUdwJsyS3WPipsNh///ay2LJ+onCM23tipnz6EvEy2/UA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>',
|
||||
dataLabels:
|
||||
'<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-datalabels/2.2.0/chartjs-plugin-datalabels.min.js" integrity="sha512-JPcRR8yFa8mmCsfrw4TNte1ZvF1e3+1SdGMslZvmrzDYxS69J7J49vkFL8u6u8PlPJK+H3voElBtUCzaXj+6ig==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>',
|
||||
outlabels:
|
||||
'<script src="https://cdn.jsdelivr.net/npm/@energiency/chartjs-plugin-piechart-outlabels@1.3.4/dist/chartjs-plugin-piechart-outlabels.min.js"></script>',
|
||||
};
|
||||
|
||||
return `<html>
|
||||
<meta charset='utf-8'>
|
||||
|
||||
@@ -8,7 +16,7 @@ const getChartExport = (title, config, imageFile) => {
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js" integrity="sha512-qTXRIMyZIFb8iQcfjXWCO8+M5Tbc38Qi5WzdPOYZHIlZpzBHG3L3by84BBBOiRGiEb7KKtAOAs5qYdUiZiQNNQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-adapter-moment/1.0.0/chartjs-adapter-moment.min.js" integrity="sha512-oh5t+CdSBsaVVAvxcZKy3XJdP7ZbYUBSRCXDTVn0ODewMDDNnELsrG9eDm8rVZAQg7RsDD/8K3MjPAFB13o6eA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js" integrity="sha512-UXumZrZNiOwnTcZSHLOfcTs0aos2MzBWHXOHOuB0J/R44QB0dwY5JgfbvljXcklVf65Gc4El6RjZ+lnwd2az2g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-zoom/1.2.0/chartjs-plugin-zoom.min.js" integrity="sha512-TT0wAMqqtjXVzpc48sI0G84rBP+oTkBZPgeRYIOVRGUdwJsyS3WPipsNh///ay2LJ+onCM23tipnz6EvEy2/UA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
${plugins.map(plugin => PLUGIN_TAGS[plugin] ?? '')}
|
||||
|
||||
<style>
|
||||
a { text-decoration: none }
|
||||
@@ -45,7 +53,7 @@ const getChartExport = (title, config, imageFile) => {
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
Exported from <a href='https://dbgate.org/' target='_blank'>DbGate</a>, powered by <a href='https://www.chartjs.org/' target='_blank'>Chart.js</a>
|
||||
Exported from <a href='https://dbgate.io/' target='_blank'>DbGate</a>, powered by <a href='https://www.chartjs.org/' target='_blank'>Chart.js</a>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ const getMapExport = (geoJson) => {
|
||||
leaflet
|
||||
.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '<a href="https://dbgate.org" title="Exported from DbGate">DbGate</a> | © OpenStreetMap',
|
||||
attribution: '<a href="https://dbgate.io" title="Exported from DbGate">DbGate</a> | © OpenStreetMap',
|
||||
})
|
||||
.addTo(map);
|
||||
|
||||
|
||||
@@ -14,12 +14,9 @@ class QueryStreamTableWriter {
|
||||
this.currentChangeIndex = 1;
|
||||
this.initializedFile = false;
|
||||
this.sesid = sesid;
|
||||
if (isProApp()) {
|
||||
this.chartProcessor = new ChartProcessor();
|
||||
}
|
||||
}
|
||||
|
||||
initializeFromQuery(structure, resultIndex, chartDefinition) {
|
||||
initializeFromQuery(structure, resultIndex, chartDefinition, autoDetectCharts = false) {
|
||||
this.jslid = crypto.randomUUID();
|
||||
this.currentFile = path.join(jsldir(), `${this.jslid}.jsonl`);
|
||||
fs.writeFileSync(
|
||||
@@ -33,8 +30,8 @@ class QueryStreamTableWriter {
|
||||
this.writeCurrentStats(false, false);
|
||||
this.resultIndex = resultIndex;
|
||||
this.initializedFile = true;
|
||||
if (isProApp() && chartDefinition) {
|
||||
this.chartProcessor = new ChartProcessor([chartDefinition]);
|
||||
if (isProApp() && (chartDefinition || autoDetectCharts)) {
|
||||
this.chartProcessor = chartDefinition ? new ChartProcessor([chartDefinition]) : new ChartProcessor();
|
||||
}
|
||||
process.send({ msgtype: 'recordset', jslid: this.jslid, resultIndex, sesid: this.sesid });
|
||||
}
|
||||
@@ -107,7 +104,7 @@ class QueryStreamTableWriter {
|
||||
if (this.chartProcessor) {
|
||||
try {
|
||||
this.chartProcessor.finalize();
|
||||
if (this.chartProcessor.charts.length > 0) {
|
||||
if (isProApp() && this.chartProcessor.charts.length > 0) {
|
||||
process.send({
|
||||
msgtype: 'charts',
|
||||
sesid: this.sesid,
|
||||
@@ -137,12 +134,14 @@ class StreamHandler {
|
||||
startLine,
|
||||
sesid = undefined,
|
||||
limitRows = undefined,
|
||||
frontMatter = undefined
|
||||
frontMatter = undefined,
|
||||
autoDetectCharts = false
|
||||
) {
|
||||
this.recordset = this.recordset.bind(this);
|
||||
this.startLine = startLine;
|
||||
this.sesid = sesid;
|
||||
this.frontMatter = frontMatter;
|
||||
this.autoDetectCharts = autoDetectCharts;
|
||||
this.limitRows = limitRows;
|
||||
this.rowsLimitOverflow = false;
|
||||
this.row = this.row.bind(this);
|
||||
@@ -176,7 +175,8 @@ class StreamHandler {
|
||||
this.currentWriter.initializeFromQuery(
|
||||
Array.isArray(columns) ? { columns } : columns,
|
||||
this.queryStreamInfoHolder.resultIndex,
|
||||
this.frontMatter?.[`chart-${this.queryStreamInfoHolder.resultIndex + 1}`]
|
||||
this.frontMatter?.[`chart-${this.queryStreamInfoHolder.resultIndex + 1}`],
|
||||
this.autoDetectCharts
|
||||
);
|
||||
this.queryStreamInfoHolder.resultIndex += 1;
|
||||
this.rowCounter = 0;
|
||||
@@ -251,7 +251,8 @@ function handleQueryStream(
|
||||
sqlItem,
|
||||
sesid = undefined,
|
||||
limitRows = undefined,
|
||||
frontMatter = undefined
|
||||
frontMatter = undefined,
|
||||
autoDetectCharts = false
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const start = sqlItem.trimStart || sqlItem.start;
|
||||
@@ -261,7 +262,8 @@ function handleQueryStream(
|
||||
start && start.line,
|
||||
sesid,
|
||||
limitRows,
|
||||
frontMatter
|
||||
frontMatter,
|
||||
autoDetectCharts
|
||||
);
|
||||
driver.stream(dbhan, sqlItem.text, handler);
|
||||
});
|
||||
|
||||
18
packages/api/src/utility/loginchecker.js
Normal file
18
packages/api/src/utility/loginchecker.js
Normal file
@@ -0,0 +1,18 @@
|
||||
// only in DbGate Premium
|
||||
|
||||
function markUserAsActive(licenseUid, token) {}
|
||||
|
||||
async function isLoginLicensed(req, licenseUid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function markLoginAsLoggedOut(licenseUid) {}
|
||||
|
||||
const LOGIN_LIMIT_ERROR = '';
|
||||
|
||||
module.exports = {
|
||||
markUserAsActive,
|
||||
isLoginLicensed,
|
||||
markLoginAsLoggedOut,
|
||||
LOGIN_LIMIT_ERROR,
|
||||
};
|
||||
52
packages/api/src/utility/security.js
Normal file
52
packages/api/src/utility/security.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const path = require('path');
|
||||
const { filesdir, archivedir, uploadsdir, appdir } = require('../utility/directories');
|
||||
|
||||
function checkSecureFilePathsWithoutDirectory(...filePaths) {
|
||||
for (const filePath of filePaths) {
|
||||
if (filePath.includes('..') || filePath.includes('/') || filePath.includes('\\')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function checkSecureDirectories(...filePaths) {
|
||||
for (const filePath of filePaths) {
|
||||
if (!filePath.includes('/') && !filePath.includes('\\')) {
|
||||
// If the filePath does not contain any directory separators, it is considered secure
|
||||
continue;
|
||||
}
|
||||
const directory = path.dirname(filePath);
|
||||
if (directory != filesdir() && directory != uploadsdir() && directory != archivedir() && directory != appdir()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function findDisallowedFileNames(node, isAllowed, trace = '$', out = []) {
|
||||
if (node && typeof node === 'object') {
|
||||
if (node?.props?.fileName) {
|
||||
const name = node.props.fileName;
|
||||
const ok = isAllowed(name);
|
||||
if (!ok) out.push({ path: `${trace}.props.fileName`, value: name });
|
||||
}
|
||||
|
||||
// depth-first scan of every property / array index
|
||||
for (const [key, val] of Object.entries(node)) {
|
||||
findDisallowedFileNames(val, isAllowed, `${trace}.${key}`, out);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function checkSecureDirectoriesInScript(script) {
|
||||
const disallowed = findDisallowedFileNames(script, checkSecureDirectories);
|
||||
return disallowed.length == 0;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkSecureDirectories,
|
||||
checkSecureFilePathsWithoutDirectory,
|
||||
checkSecureDirectoriesInScript,
|
||||
};
|
||||
@@ -101,9 +101,10 @@ export class CollectionGridDisplay extends GridDisplay {
|
||||
setCache: ChangeCacheFunc,
|
||||
loadedRows,
|
||||
changeSet,
|
||||
readOnly = false
|
||||
readOnly = false,
|
||||
currentSettings = null
|
||||
) {
|
||||
super(config, setConfig, cache, setCache, driver);
|
||||
super(config, setConfig, cache, setCache, driver, undefined, undefined, currentSettings);
|
||||
const changedDocs = _.compact(changeSet.updates.map(chs => chs.document));
|
||||
const insertedDocs = _.compact(changeSet.inserts.map(chs => chs.fields));
|
||||
this.columns = analyseCollectionDisplayColumns([...(loadedRows || []), ...changedDocs, ...insertedDocs], this);
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface DisplayColumn {
|
||||
notNull?: boolean;
|
||||
autoIncrement?: boolean;
|
||||
isPrimaryKey?: boolean;
|
||||
hasAutoValue?: boolean;
|
||||
|
||||
// NoSQL specific
|
||||
isPartitionKey?: boolean;
|
||||
@@ -71,7 +72,8 @@ export abstract class GridDisplay {
|
||||
protected setCache: ChangeCacheFunc,
|
||||
public driver?: EngineDriver,
|
||||
public dbinfo: DatabaseInfo = null,
|
||||
public serverVersion = null
|
||||
public serverVersion = null,
|
||||
public currentSettings = null
|
||||
) {
|
||||
this.dialect = (driver?.dialectByVersion && driver?.dialectByVersion(serverVersion)) || driver?.dialect;
|
||||
}
|
||||
@@ -206,7 +208,7 @@ export abstract class GridDisplay {
|
||||
get hiddenColumnIndexes() {
|
||||
// console.log('GridDisplay.hiddenColumn', this.config.hiddenColumns);
|
||||
const res = (this.config.hiddenColumns || []).map(x => _.findIndex(this.allColumns, y => y.uniqueName == x));
|
||||
if (this.config.searchInColumns) {
|
||||
if (this.config.searchInColumns && !this.currentSettings?.['dataGrid.showAllColumnsWhenSearch']) {
|
||||
for (let i = 0; i < this.allColumns.length; i++) {
|
||||
if (!filterName(this.config.searchInColumns, this.allColumns[i].columnName)) {
|
||||
res.push(i);
|
||||
|
||||
@@ -17,9 +17,10 @@ export class JslGridDisplay extends GridDisplay {
|
||||
isDynamicStructure: boolean,
|
||||
supportsReload: boolean,
|
||||
editable: boolean = false,
|
||||
driver: EngineDriver = null
|
||||
driver: EngineDriver = null,
|
||||
currentSettings = null
|
||||
) {
|
||||
super(config, setConfig, cache, setCache, driver);
|
||||
super(config, setConfig, cache, setCache, driver, undefined, undefined, currentSettings);
|
||||
|
||||
this.filterable = true;
|
||||
this.sortable = true;
|
||||
|
||||
@@ -106,6 +106,7 @@ export class PerspectiveDataLoader {
|
||||
conid: props.databaseConfig.conid,
|
||||
database: props.databaseConfig.database,
|
||||
select,
|
||||
auditLogSessionGroup: 'perspective',
|
||||
});
|
||||
|
||||
if (response.errorMessage) return response;
|
||||
@@ -152,6 +153,7 @@ export class PerspectiveDataLoader {
|
||||
pureName,
|
||||
aggregate,
|
||||
},
|
||||
auditLogSessionGroup: 'perspective',
|
||||
});
|
||||
|
||||
if (response.errorMessage) return response;
|
||||
@@ -227,6 +229,7 @@ export class PerspectiveDataLoader {
|
||||
conid: props.databaseConfig.conid,
|
||||
database: props.databaseConfig.database,
|
||||
select,
|
||||
auditLogSessionGroup: 'perspective',
|
||||
});
|
||||
|
||||
if (response.errorMessage) return response;
|
||||
@@ -284,6 +287,7 @@ export class PerspectiveDataLoader {
|
||||
conid: props.databaseConfig.conid,
|
||||
database: props.databaseConfig.database,
|
||||
options,
|
||||
auditLogSessionGroup: 'perspective',
|
||||
});
|
||||
|
||||
if (response.errorMessage) return response;
|
||||
@@ -330,6 +334,7 @@ export class PerspectiveDataLoader {
|
||||
conid: props.databaseConfig.conid,
|
||||
database: props.databaseConfig.database,
|
||||
select,
|
||||
auditLogSessionGroup: 'perspective',
|
||||
});
|
||||
|
||||
if (response.errorMessage) return response;
|
||||
@@ -356,6 +361,7 @@ export class PerspectiveDataLoader {
|
||||
conid: props.databaseConfig.conid,
|
||||
database: props.databaseConfig.database,
|
||||
options,
|
||||
auditLogSessionGroup: 'perspective',
|
||||
});
|
||||
|
||||
return response;
|
||||
|
||||
@@ -38,9 +38,10 @@ export class TableGridDisplay extends GridDisplay {
|
||||
serverVersion,
|
||||
public getDictionaryDescription: DictionaryDescriptionFunc = null,
|
||||
isReadOnly = false,
|
||||
public isRawMode = false
|
||||
public isRawMode = false,
|
||||
public currentSettings = null
|
||||
) {
|
||||
super(config, setConfig, cache, setCache, driver, dbinfo, serverVersion);
|
||||
super(config, setConfig, cache, setCache, driver, dbinfo, serverVersion, currentSettings);
|
||||
|
||||
this.table = this.findTable(tableName);
|
||||
if (!this.table) {
|
||||
@@ -265,6 +266,7 @@ export class TableGridDisplay extends GridDisplay {
|
||||
uniqueName,
|
||||
uniquePath,
|
||||
isPrimaryKey: table.primaryKey && !!table.primaryKey.columns.find(x => x.columnName == col.columnName),
|
||||
hasAutoValue: col.hasAutoValue,
|
||||
foreignKey:
|
||||
table.foreignKeys &&
|
||||
table.foreignKeys.find(fk => fk.columns.length == 1 && fk.columns[0].columnName == col.columnName),
|
||||
|
||||
@@ -12,9 +12,10 @@ export class ViewGridDisplay extends GridDisplay {
|
||||
cache: GridCache,
|
||||
setCache: ChangeCacheFunc,
|
||||
dbinfo: DatabaseInfo,
|
||||
serverVersion
|
||||
serverVersion,
|
||||
currentSettings
|
||||
) {
|
||||
super(config, setConfig, cache, setCache, driver, dbinfo, serverVersion);
|
||||
super(config, setConfig, cache, setCache, driver, dbinfo, serverVersion, currentSettings);
|
||||
this.columns = this.getDisplayColumns(view);
|
||||
this.formColumns = this.columns;
|
||||
this.filterable = true;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type ChartTypeEnum = 'bar' | 'line' | 'pie' | 'polarArea';
|
||||
export type ChartTypeEnum = 'bar' | 'line' | 'timeline' | 'pie' | 'polarArea';
|
||||
export type ChartXTransformFunction =
|
||||
| 'identity'
|
||||
| 'date:minute'
|
||||
@@ -17,13 +17,17 @@ export const ChartConstDefaults = {
|
||||
};
|
||||
|
||||
export const ChartLimits = {
|
||||
AUTODETECT_CHART_LIMIT: 10, // limit for auto-detecting charts, to avoid too many charts
|
||||
AUTODETECT_CHART_LIMIT: 10, // limit for auto-detecting charts, to avoid too many charts (after APPLY_LIMIT_AFTER_ROWS rows)
|
||||
AUTODETECT_CHART_TOTAL_LIMIT: 32, // limit for auto-detecting charts, to avoid too many charts (for first APPLY_LIMIT_AFTER_ROWS rows)
|
||||
AUTODETECT_MEASURES_LIMIT: 10, // limit for auto-detecting measures, to avoid too many measures
|
||||
APPLY_LIMIT_AFTER_ROWS: 100,
|
||||
MAX_DISTINCT_VALUES: 10, // max number of distinct values to keep in topDistinctValues
|
||||
VALID_VALUE_RATIO_LIMIT: 0.5, // limit for valid value ratio, y defs below this will not be used in auto-detect
|
||||
PIE_RATIO_LIMIT: 0.05, // limit for other values in pie chart, if the value is below this, it will be grouped into "Other"
|
||||
PIE_COUNT_LIMIT: 10, // limit for number of pie chart slices, if the number of slices is above this, it will be grouped into "Other"
|
||||
MAX_PIE_COUNT_LIMIT: 50, // max pie limit
|
||||
CHART_FILL_LIMIT: 10000, // limit for filled charts (time intervals), to avoid too many points
|
||||
CHART_GROUP_LIMIT: 32, // limit for number of groups in a chart
|
||||
};
|
||||
|
||||
export interface ChartXFieldDefinition {
|
||||
@@ -47,9 +51,12 @@ export interface ChartDefinition {
|
||||
title?: string;
|
||||
pieRatioLimit?: number; // limit for pie chart, if the value is below this, it will be grouped into "Other"
|
||||
pieCountLimit?: number; // limit for number of pie chart slices, if the number of slices is above this, it will be grouped into "Other"
|
||||
trimXCountLimit?: number; // limit for number of x values, if the number of x values is above this, it will be trimmed
|
||||
|
||||
xdef: ChartXFieldDefinition;
|
||||
ydefs: ChartYFieldDefinition[];
|
||||
groupingField?: string;
|
||||
groupTransformFunction?: ChartXTransformFunction;
|
||||
|
||||
useDataLabels?: boolean;
|
||||
dataLabelFormatter?: ChartDataLabelFormatter;
|
||||
@@ -67,6 +74,7 @@ export interface ChartDateParsed {
|
||||
|
||||
export interface ChartAvailableColumn {
|
||||
field: string;
|
||||
dataType: 'none' | 'string' | 'number' | 'date' | 'mixed';
|
||||
}
|
||||
|
||||
export interface ProcessedChart {
|
||||
@@ -75,14 +83,18 @@ export interface ProcessedChart {
|
||||
rowsAdded: number;
|
||||
buckets: { [key: string]: any }; // key is the bucket key, value is aggregated data
|
||||
bucketKeysOrdered: string[];
|
||||
bucketKeyDateParsed: { [key: string]: ChartDateParsed }; // key is the bucket key, value is parsed date
|
||||
bucketKeysSet: Set<string>;
|
||||
bucketKeyDateParsed: { [key: string]: ChartDateParsed }; // key is the bucket key (without group::), value is parsed date
|
||||
isGivenDefinition: boolean; // true if the chart was created with a given definition, false if it was created from raw data
|
||||
invalidXRows: number;
|
||||
invalidYRows: { [key: string]: number }; // key is the y field, value is the count of invalid rows
|
||||
validYRows: { [key: string]: number }; // key is the field, value is the count of valid rows
|
||||
groups: string[];
|
||||
groupSet: Set<string>;
|
||||
|
||||
topDistinctValues: { [key: string]: Set<any> }; // key is the field, value is the set of distinct values
|
||||
availableColumns: ChartAvailableColumn[];
|
||||
errorMessage?: string; // error message if there was an error processing the chart
|
||||
|
||||
definition: ChartDefinition;
|
||||
}
|
||||
|
||||
@@ -3,16 +3,23 @@ import {
|
||||
ChartDateParsed,
|
||||
ChartDefinition,
|
||||
ChartLimits,
|
||||
ChartYFieldDefinition,
|
||||
ProcessedChart,
|
||||
} from './chartDefinitions';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
import _sum from 'lodash/sum';
|
||||
import _zipObject from 'lodash/zipObject';
|
||||
import _mapValues from 'lodash/mapValues';
|
||||
import _pick from 'lodash/pick';
|
||||
import {
|
||||
aggregateChartNumericValuesFromSource,
|
||||
autoAggregateCompactTimelineChart,
|
||||
chartsHaveSimilarRange,
|
||||
computeChartBucketCardinality,
|
||||
computeChartBucketKey,
|
||||
fillChartTimelineBuckets,
|
||||
getChartYRange,
|
||||
runTransformFunction,
|
||||
tryParseChartDate,
|
||||
} from './chartTools';
|
||||
import { getChartScore, getChartYFieldScore } from './chartScoring';
|
||||
@@ -24,6 +31,7 @@ export class ChartProcessor {
|
||||
availableColumns: ChartAvailableColumn[] = [];
|
||||
autoDetectCharts = false;
|
||||
rowsAdded = 0;
|
||||
errorMessage?: string;
|
||||
|
||||
constructor(public givenDefinitions: ChartDefinition[] = []) {
|
||||
for (const definition of givenDefinitions) {
|
||||
@@ -39,6 +47,9 @@ export class ChartProcessor {
|
||||
availableColumns: [],
|
||||
validYRows: {},
|
||||
topDistinctValues: {},
|
||||
groups: [],
|
||||
groupSet: new Set<string>(),
|
||||
bucketKeysSet: new Set<string>(),
|
||||
});
|
||||
}
|
||||
this.autoDetectCharts = this.givenDefinitions.length == 0;
|
||||
@@ -67,6 +78,91 @@ export class ChartProcessor {
|
||||
// this.chartsBySignature[signature] = chart;
|
||||
// return chart;
|
||||
// }
|
||||
runAutoDetectCharts(
|
||||
dateColumns: { [key: string]: ChartDateParsed },
|
||||
numericColumnsForAutodetect: { [key: string]: number },
|
||||
stringColumns: { [key: string]: string }
|
||||
) {
|
||||
const processColumnType = (columns, transformTest, chartType, transformFunction) => {
|
||||
for (const xcol in columns) {
|
||||
for (const groupingField of [undefined, ...Object.keys(stringColumns)]) {
|
||||
if (xcol == groupingField) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let usedChart = this.chartsProcessing.find(
|
||||
chart =>
|
||||
!chart.isGivenDefinition &&
|
||||
chart.definition.xdef.field === xcol &&
|
||||
transformTest(chart.definition.xdef.transformFunction) &&
|
||||
chart.definition.groupingField == groupingField
|
||||
);
|
||||
|
||||
if (
|
||||
!usedChart &&
|
||||
(this.rowsAdded < ChartLimits.APPLY_LIMIT_AFTER_ROWS ||
|
||||
this.chartsProcessing.length < ChartLimits.AUTODETECT_CHART_LIMIT)
|
||||
) {
|
||||
usedChart = {
|
||||
definition: {
|
||||
chartType,
|
||||
xdef: {
|
||||
field: xcol,
|
||||
transformFunction,
|
||||
},
|
||||
ydefs: [
|
||||
{
|
||||
field: '__count',
|
||||
aggregateFunction: 'count',
|
||||
},
|
||||
],
|
||||
groupingField,
|
||||
},
|
||||
rowsAdded: 0,
|
||||
bucketKeysOrdered: [],
|
||||
buckets: {},
|
||||
groups: [],
|
||||
bucketKeyDateParsed: {},
|
||||
isGivenDefinition: false,
|
||||
invalidXRows: 0,
|
||||
invalidYRows: {},
|
||||
availableColumns: [],
|
||||
validYRows: {},
|
||||
topDistinctValues: {},
|
||||
groupSet: new Set<string>(),
|
||||
bucketKeysSet: new Set<string>(),
|
||||
};
|
||||
this.chartsProcessing.push(usedChart);
|
||||
}
|
||||
|
||||
if (!usedChart) {
|
||||
continue; // chart not created - probably too many charts already
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(numericColumnsForAutodetect)) {
|
||||
// if (value == null) continue;
|
||||
// if (key == datecol) continue; // skip date column itself
|
||||
|
||||
const existingYDef = usedChart.definition.ydefs.find(y => y.field === key);
|
||||
if (
|
||||
!existingYDef &&
|
||||
(this.rowsAdded < ChartLimits.APPLY_LIMIT_AFTER_ROWS ||
|
||||
usedChart.definition.ydefs.length < ChartLimits.AUTODETECT_MEASURES_LIMIT)
|
||||
) {
|
||||
const newYDef: ChartYFieldDefinition = {
|
||||
field: key,
|
||||
aggregateFunction: 'sum',
|
||||
};
|
||||
usedChart.definition.ydefs.push(newYDef);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
processColumnType(dateColumns, transform => transform?.startsWith('date:'), 'timeline', 'date:day');
|
||||
processColumnType(stringColumns, transform => transform == 'identity', 'bar', 'identity');
|
||||
}
|
||||
|
||||
addRow(row: any) {
|
||||
const dateColumns: { [key: string]: ChartDateParsed } = {};
|
||||
@@ -76,9 +172,14 @@ export class ChartProcessor {
|
||||
|
||||
for (const [key, value] of Object.entries(row)) {
|
||||
const number: number = typeof value == 'string' ? Number(value) : typeof value == 'number' ? value : NaN;
|
||||
this.availableColumnsDict[key] = {
|
||||
field: key,
|
||||
};
|
||||
let availableColumn = this.availableColumnsDict[key];
|
||||
if (!availableColumn) {
|
||||
availableColumn = {
|
||||
field: key,
|
||||
dataType: 'none',
|
||||
};
|
||||
this.availableColumnsDict[key] = availableColumn;
|
||||
}
|
||||
|
||||
const keyLower = key.toLowerCase();
|
||||
const keyIsId = keyLower.endsWith('_id') || keyLower == 'id' || key.endsWith('Id');
|
||||
@@ -86,6 +187,12 @@ export class ChartProcessor {
|
||||
const parsedDate = tryParseChartDate(value);
|
||||
if (parsedDate) {
|
||||
dateColumns[key] = parsedDate;
|
||||
if (availableColumn.dataType == 'none') {
|
||||
availableColumn.dataType = 'date';
|
||||
}
|
||||
if (availableColumn.dataType != 'date') {
|
||||
availableColumn.dataType = 'mixed';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -94,85 +201,52 @@ export class ChartProcessor {
|
||||
if (!keyIsId) {
|
||||
numericColumnsForAutodetect[key] = number; // for auto-detecting charts
|
||||
}
|
||||
if (availableColumn.dataType == 'none') {
|
||||
availableColumn.dataType = 'number';
|
||||
}
|
||||
if (availableColumn.dataType != 'number') {
|
||||
availableColumn.dataType = 'mixed';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof value === 'string' && isNaN(number) && value.length < 100) {
|
||||
stringColumns[key] = value;
|
||||
if (availableColumn.dataType == 'none') {
|
||||
availableColumn.dataType = 'string';
|
||||
}
|
||||
if (availableColumn.dataType != 'string') {
|
||||
availableColumn.dataType = 'mixed';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// const sortedNumericColumnns = Object.keys(numericColumns).sort();
|
||||
|
||||
if (this.autoDetectCharts) {
|
||||
// create charts from data, if there are no given definitions
|
||||
for (const datecol in dateColumns) {
|
||||
let usedChart = this.chartsProcessing.find(
|
||||
chart =>
|
||||
!chart.isGivenDefinition &&
|
||||
chart.definition.xdef.field === datecol &&
|
||||
chart.definition.xdef.transformFunction?.startsWith('date:')
|
||||
);
|
||||
|
||||
if (
|
||||
!usedChart &&
|
||||
(this.rowsAdded < ChartLimits.APPLY_LIMIT_AFTER_ROWS ||
|
||||
this.chartsProcessing.length < ChartLimits.AUTODETECT_CHART_LIMIT)
|
||||
) {
|
||||
usedChart = {
|
||||
definition: {
|
||||
chartType: 'line',
|
||||
xdef: {
|
||||
field: datecol,
|
||||
transformFunction: 'date:day',
|
||||
},
|
||||
ydefs: [],
|
||||
},
|
||||
rowsAdded: 0,
|
||||
bucketKeysOrdered: [],
|
||||
buckets: {},
|
||||
bucketKeyDateParsed: {},
|
||||
isGivenDefinition: false,
|
||||
invalidXRows: 0,
|
||||
invalidYRows: {},
|
||||
availableColumns: [],
|
||||
validYRows: {},
|
||||
topDistinctValues: {},
|
||||
};
|
||||
this.chartsProcessing.push(usedChart);
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(row)) {
|
||||
if (value == null) continue;
|
||||
if (key == datecol) continue; // skip date column itself
|
||||
let existingYDef = usedChart.definition.ydefs.find(y => y.field === key);
|
||||
if (
|
||||
!existingYDef &&
|
||||
(this.rowsAdded < ChartLimits.APPLY_LIMIT_AFTER_ROWS ||
|
||||
usedChart.definition.ydefs.length < ChartLimits.AUTODETECT_MEASURES_LIMIT)
|
||||
) {
|
||||
existingYDef = {
|
||||
field: key,
|
||||
aggregateFunction: 'sum',
|
||||
};
|
||||
usedChart.definition.ydefs.push(existingYDef);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.runAutoDetectCharts(dateColumns, numericColumnsForAutodetect, stringColumns);
|
||||
}
|
||||
|
||||
// apply on all charts with this date column
|
||||
for (const chart of this.chartsProcessing) {
|
||||
this.applyRawData(
|
||||
chart,
|
||||
row,
|
||||
dateColumns[chart.definition.xdef.field],
|
||||
chart.isGivenDefinition ? numericColumns : numericColumnsForAutodetect,
|
||||
stringColumns
|
||||
);
|
||||
if (chart.errorMessage) {
|
||||
continue; // skip charts with errors
|
||||
}
|
||||
|
||||
this.applyRawData(chart, row, dateColumns[chart.definition.xdef.field], numericColumns, stringColumns);
|
||||
|
||||
if (Object.keys(chart.buckets).length > ChartLimits.CHART_FILL_LIMIT) {
|
||||
chart.errorMessage = `Chart has too many buckets, limit is ${ChartLimits.CHART_FILL_LIMIT}.`;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.chartsProcessing.length; i++) {
|
||||
if (this.chartsProcessing[i].errorMessage) {
|
||||
continue; // skip charts with errors
|
||||
}
|
||||
if (this.chartsProcessing[i].definition.chartType != 'timeline') {
|
||||
continue; // skip non-timeline charts
|
||||
}
|
||||
this.chartsProcessing[i] = autoAggregateCompactTimelineChart(this.chartsProcessing[i]);
|
||||
}
|
||||
|
||||
@@ -210,30 +284,79 @@ export class ChartProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
splitChartsByYDefs() {
|
||||
const newCharts: ProcessedChart[] = [];
|
||||
|
||||
for (const chart of this.chartsProcessing) {
|
||||
if (chart.isGivenDefinition) {
|
||||
newCharts.push(chart);
|
||||
continue;
|
||||
}
|
||||
const yRanges = chart.definition.ydefs.map(ydef => getChartYRange(chart, ydef).max);
|
||||
const yRangeByField = _zipObject(
|
||||
chart.definition.ydefs.map(ydef => ydef.field),
|
||||
yRanges
|
||||
);
|
||||
let ydefsToAssign = chart.definition.ydefs.map(ydef => ydef.field);
|
||||
while (ydefsToAssign.length > 0) {
|
||||
const first = ydefsToAssign.shift();
|
||||
const additionals = [];
|
||||
for (const candidate of ydefsToAssign) {
|
||||
if (chartsHaveSimilarRange(yRangeByField[first], yRangeByField[candidate])) {
|
||||
additionals.push(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
const ydefsCurrent = [first, ...additionals];
|
||||
const partialChart: ProcessedChart = {
|
||||
...chart,
|
||||
definition: {
|
||||
...chart.definition,
|
||||
ydefs: ydefsCurrent.map(y => chart.definition.ydefs.find(yd => yd.field === y) as ChartYFieldDefinition),
|
||||
},
|
||||
buckets: _mapValues(chart.buckets, bucket => _pick(bucket, ydefsCurrent)),
|
||||
};
|
||||
|
||||
newCharts.push(partialChart);
|
||||
ydefsToAssign = ydefsToAssign.filter(y => !additionals.includes(y));
|
||||
}
|
||||
}
|
||||
this.chartsProcessing = newCharts;
|
||||
}
|
||||
|
||||
finalize() {
|
||||
this.splitChartsByYDefs();
|
||||
this.applyLimitsOnCharts();
|
||||
this.availableColumns = Object.values(this.availableColumnsDict);
|
||||
for (const chart of this.chartsProcessing) {
|
||||
if (chart.errorMessage) {
|
||||
this.charts.push({ ...chart, availableColumns: this.availableColumns });
|
||||
continue;
|
||||
}
|
||||
let addedChart: ProcessedChart = chart;
|
||||
if (chart.rowsAdded == 0) {
|
||||
if (chart.rowsAdded == 0 && !chart.isGivenDefinition) {
|
||||
continue; // skip empty charts
|
||||
}
|
||||
const sortOrder = chart.definition.xdef.sortOrder ?? 'ascKeys';
|
||||
if (sortOrder != 'natural') {
|
||||
if (sortOrder == 'ascKeys' || sortOrder == 'descKeys') {
|
||||
if (chart.definition.xdef.transformFunction.startsWith('date:')) {
|
||||
if (chart.definition.chartType == 'timeline' && chart.definition.xdef.transformFunction.startsWith('date:')) {
|
||||
addedChart = autoAggregateCompactTimelineChart(addedChart);
|
||||
fillChartTimelineBuckets(addedChart);
|
||||
}
|
||||
|
||||
addedChart.bucketKeysOrdered = _sortBy(Object.keys(addedChart.buckets));
|
||||
if (addedChart.errorMessage) {
|
||||
this.charts.push(addedChart);
|
||||
continue;
|
||||
}
|
||||
addedChart.bucketKeysOrdered = _sortBy([...addedChart.bucketKeysSet]);
|
||||
if (sortOrder == 'descKeys') {
|
||||
addedChart.bucketKeysOrdered.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
if (sortOrder == 'ascValues' || sortOrder == 'descValues') {
|
||||
addedChart.bucketKeysOrdered = _sortBy(Object.keys(addedChart.buckets), key =>
|
||||
addedChart.bucketKeysOrdered = _sortBy([...addedChart.bucketKeysSet], key =>
|
||||
computeChartBucketCardinality(addedChart.buckets[key])
|
||||
);
|
||||
if (sortOrder == 'descValues') {
|
||||
@@ -256,31 +379,45 @@ export class ChartProcessor {
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
addedChart.definition.trimXCountLimit != null &&
|
||||
addedChart.bucketKeysOrdered.length > addedChart.definition.trimXCountLimit
|
||||
) {
|
||||
addedChart.bucketKeysOrdered = addedChart.bucketKeysOrdered.slice(0, addedChart.definition.trimXCountLimit);
|
||||
}
|
||||
|
||||
if (addedChart) {
|
||||
addedChart.availableColumns = this.availableColumns;
|
||||
this.charts.push(addedChart);
|
||||
}
|
||||
|
||||
this.groupPieOtherBuckets(addedChart);
|
||||
|
||||
addedChart.groups = [...addedChart.groupSet];
|
||||
addedChart.bucketKeysSet = undefined;
|
||||
addedChart.groupSet = undefined;
|
||||
}
|
||||
|
||||
this.charts = [
|
||||
...this.charts.filter(x => x.isGivenDefinition),
|
||||
..._sortBy(
|
||||
this.charts.filter(x => !x.isGivenDefinition),
|
||||
this.charts.filter(x => !x.isGivenDefinition && !x.errorMessage && x.definition.ydefs.length > 0),
|
||||
chart => -getChartScore(chart)
|
||||
),
|
||||
];
|
||||
}
|
||||
groupPieOtherBuckets(chart: ProcessedChart) {
|
||||
if (chart.definition.chartType !== 'pie') {
|
||||
if (chart.definition.chartType != 'pie' && chart.definition.chartType != 'polarArea') {
|
||||
return; // only for pie charts
|
||||
}
|
||||
const ratioLimit = chart.definition.pieRatioLimit ?? ChartLimits.PIE_RATIO_LIMIT;
|
||||
const countLimit = chart.definition.pieCountLimit ?? ChartLimits.PIE_COUNT_LIMIT;
|
||||
if (ratioLimit == 0 && countLimit == 0) {
|
||||
return; // no grouping if limit is 0
|
||||
let countLimit = chart.definition.pieCountLimit ?? ChartLimits.PIE_COUNT_LIMIT;
|
||||
if (!countLimit || countLimit < 1 || countLimit > ChartLimits.MAX_PIE_COUNT_LIMIT) {
|
||||
countLimit = ChartLimits.MAX_PIE_COUNT_LIMIT; // limit to max pie count
|
||||
}
|
||||
// if (ratioLimit == 0 && countLimit == 0) {
|
||||
// return; // no grouping if limit is 0
|
||||
// }
|
||||
const otherBucket: any = {};
|
||||
let newBuckets: any = {};
|
||||
const cardSum = _sum(Object.values(chart.buckets).map(bucket => computeChartBucketCardinality(bucket)));
|
||||
@@ -345,6 +482,15 @@ export class ChartProcessor {
|
||||
}
|
||||
|
||||
const [bucketKey, bucketKeyParsed] = computeChartBucketKey(dateParsed, chart, row);
|
||||
const bucketGroup = chart.definition.groupingField
|
||||
? runTransformFunction(row[chart.definition.groupingField], chart.definition.groupTransformFunction)
|
||||
: null;
|
||||
if (bucketGroup) {
|
||||
chart.groupSet.add(bucketGroup);
|
||||
}
|
||||
if (chart.groupSet.size > ChartLimits.CHART_GROUP_LIMIT) {
|
||||
chart.errorMessage = `Chart has too many groups, limit is ${ChartLimits.CHART_GROUP_LIMIT}.`;
|
||||
}
|
||||
|
||||
if (!bucketKey) {
|
||||
return; // skip if no bucket key
|
||||
@@ -361,14 +507,19 @@ export class ChartProcessor {
|
||||
chart.maxX = bucketKey;
|
||||
}
|
||||
|
||||
if (!chart.buckets[bucketKey]) {
|
||||
chart.buckets[bucketKey] = {};
|
||||
const groupedBucketKey = chart.definition.groupingField ? `${bucketGroup ?? ''}::${bucketKey}` : bucketKey;
|
||||
if (!chart.buckets[groupedBucketKey]) {
|
||||
chart.buckets[groupedBucketKey] = {};
|
||||
}
|
||||
|
||||
if (!chart.bucketKeysSet.has(bucketKey)) {
|
||||
chart.bucketKeysSet.add(bucketKey);
|
||||
if (chart.definition.xdef.sortOrder == 'natural') {
|
||||
chart.bucketKeysOrdered.push(bucketKey);
|
||||
}
|
||||
}
|
||||
|
||||
aggregateChartNumericValuesFromSource(chart, bucketKey, numericColumns, row);
|
||||
aggregateChartNumericValuesFromSource(chart, groupedBucketKey, numericColumns, row);
|
||||
chart.rowsAdded += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,23 @@ import _sum from 'lodash/sum';
|
||||
import { ChartLimits, ChartYFieldDefinition, ProcessedChart } from './chartDefinitions';
|
||||
|
||||
export function getChartScore(chart: ProcessedChart): number {
|
||||
if (chart.errorMessage) {
|
||||
return -1; // negative score for charts with errors
|
||||
}
|
||||
let res = 0;
|
||||
res += chart.rowsAdded * 5;
|
||||
|
||||
const ydefScores = chart.definition.ydefs.map(yField => getChartYFieldScore(chart, yField));
|
||||
const sorted = _sortBy(ydefScores).reverse();
|
||||
res += _sum(sorted.slice(0, ChartLimits.AUTODETECT_MEASURES_LIMIT));
|
||||
|
||||
if (chart.groupSet?.size >= 2 && chart.groupSet?.size <= 6) {
|
||||
res += 50; // bonus for nice grouping
|
||||
}
|
||||
if (chart.groupSet?.size == 1) {
|
||||
res -= 20; // penalty for single group
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,15 +3,19 @@ import _sumBy from 'lodash/sumBy';
|
||||
import {
|
||||
ChartConstDefaults,
|
||||
ChartDateParsed,
|
||||
ChartDefinition,
|
||||
ChartLimits,
|
||||
ChartXTransformFunction,
|
||||
ChartYFieldDefinition,
|
||||
ProcessedChart,
|
||||
} from './chartDefinitions';
|
||||
import { addMinutes, addHours, addDays, addMonths, addYears } from 'date-fns';
|
||||
|
||||
export function getChartDebugPrint(chart: ProcessedChart) {
|
||||
let res = '';
|
||||
res += `Chart: ${chart.definition.chartType} (${chart.definition.xdef.transformFunction})\n`;
|
||||
res += `Chart: ${chart.definition.chartType} (${chart.definition.xdef.transformFunction}): (${chart.definition.ydefs
|
||||
.map(yd => yd.field)
|
||||
.join(', ')})\n`;
|
||||
for (const key of chart.bucketKeysOrdered) {
|
||||
res += `${key}: ${_toPairs(chart.buckets[key])
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
@@ -34,22 +38,53 @@ export function tryParseChartDate(dateInput: any): ChartDateParsed | null {
|
||||
}
|
||||
|
||||
if (typeof dateInput !== 'string') return null;
|
||||
const m = dateInput.match(
|
||||
const dateMatch = dateInput.match(
|
||||
/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(Z|[+-]\d{2}:\d{2})?)?$/
|
||||
);
|
||||
if (!m) return null;
|
||||
const monthMatch = dateInput.match(/^(\d{4})-(\d{2})$/);
|
||||
// const yearMatch = dateInput.match(/^(\d{4})$/);
|
||||
|
||||
const [_notUsed, year, month, day, hour, minute, second, fraction] = m;
|
||||
if (dateMatch) {
|
||||
const [_notUsed, year, month, day, hour, minute, second, fraction] = dateMatch;
|
||||
|
||||
return {
|
||||
year: parseInt(year, 10),
|
||||
month: parseInt(month, 10),
|
||||
day: parseInt(day, 10),
|
||||
hour: parseInt(hour, 10) || 0,
|
||||
minute: parseInt(minute, 10) || 0,
|
||||
second: parseInt(second, 10) || 0,
|
||||
fraction: fraction || undefined,
|
||||
};
|
||||
return {
|
||||
year: parseInt(year, 10),
|
||||
month: parseInt(month, 10),
|
||||
day: parseInt(day, 10),
|
||||
hour: parseInt(hour, 10) || 0,
|
||||
minute: parseInt(minute, 10) || 0,
|
||||
second: parseInt(second, 10) || 0,
|
||||
fraction: fraction || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (monthMatch) {
|
||||
const [_notUsed, year, month] = monthMatch;
|
||||
return {
|
||||
year: parseInt(year, 10),
|
||||
month: parseInt(month, 10),
|
||||
day: 1,
|
||||
hour: 0,
|
||||
minute: 0,
|
||||
second: 0,
|
||||
fraction: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// if (yearMatch) {
|
||||
// const [_notUsed, year] = yearMatch;
|
||||
// return {
|
||||
// year: parseInt(year, 10),
|
||||
// month: 1,
|
||||
// day: 1,
|
||||
// hour: 0,
|
||||
// minute: 0,
|
||||
// second: 0,
|
||||
// fraction: undefined,
|
||||
// };
|
||||
// }
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function pad2Digits(number) {
|
||||
@@ -133,6 +168,33 @@ export function incrementChartDate(value: ChartDateParsed, transform: ChartXTran
|
||||
}
|
||||
}
|
||||
|
||||
export function runTransformFunction(value: string, transformFunction: ChartXTransformFunction): string {
|
||||
const dateParsed = tryParseChartDate(value);
|
||||
switch (transformFunction) {
|
||||
case 'date:year':
|
||||
return dateParsed ? `${dateParsed.year}` : null;
|
||||
case 'date:month':
|
||||
return dateParsed ? `${dateParsed.year}-${pad2Digits(dateParsed.month)}` : null;
|
||||
case 'date:day':
|
||||
return dateParsed ? `${dateParsed.year}-${pad2Digits(dateParsed.month)}-${pad2Digits(dateParsed.day)}` : null;
|
||||
case 'date:hour':
|
||||
return dateParsed
|
||||
? `${dateParsed.year}-${pad2Digits(dateParsed.month)}-${pad2Digits(dateParsed.day)} ${pad2Digits(
|
||||
dateParsed.hour
|
||||
)}`
|
||||
: null;
|
||||
case 'date:minute':
|
||||
return dateParsed
|
||||
? `${dateParsed.year}-${pad2Digits(dateParsed.month)}-${pad2Digits(dateParsed.day)} ${pad2Digits(
|
||||
dateParsed.hour
|
||||
)}:${pad2Digits(dateParsed.minute)}`
|
||||
: null;
|
||||
case 'identity':
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export function computeChartBucketKey(
|
||||
dateParsed: ChartDateParsed,
|
||||
chart: ProcessedChart,
|
||||
@@ -268,7 +330,27 @@ export function compareChartDatesParsed(
|
||||
}
|
||||
}
|
||||
|
||||
function getParentDateBucketKey(bucketKey: string, transform: ChartXTransformFunction): string | null {
|
||||
function extractBucketKeyWithoutGroup(bucketKey: string, definition: ChartDefinition): string {
|
||||
if (definition.groupingField) {
|
||||
const [_group, key] = bucketKey.split('::', 2);
|
||||
return key || bucketKey;
|
||||
}
|
||||
return bucketKey;
|
||||
}
|
||||
|
||||
function getParentDateBucketKey(
|
||||
bucketKey: string,
|
||||
transform: ChartXTransformFunction,
|
||||
isGrouped: boolean
|
||||
): string | null {
|
||||
if (isGrouped) {
|
||||
const [group, key] = bucketKey.split('::', 2);
|
||||
if (!key) {
|
||||
return null; // no parent for grouped bucket
|
||||
}
|
||||
return `${group}::${getParentDateBucketKey(key, transform, false)}`;
|
||||
}
|
||||
|
||||
switch (transform) {
|
||||
case 'date:year':
|
||||
return null; // no parent for year
|
||||
@@ -345,19 +427,30 @@ function createParentChartAggregation(chart: ProcessedChart): ProcessedChart | n
|
||||
validYRows: { ...chart.validYRows }, // copy valid Y rows
|
||||
topDistinctValues: { ...chart.topDistinctValues }, // copy top distinct values
|
||||
availableColumns: chart.availableColumns,
|
||||
groups: [...chart.groups], // copy groups
|
||||
groupSet: new Set(chart.groups), // create a set from the groups
|
||||
bucketKeysSet: new Set<string>(), // initialize empty set for bucket keys
|
||||
};
|
||||
|
||||
for (const [bucketKey, bucketValues] of Object.entries(chart.buckets)) {
|
||||
const parentKey = getParentDateBucketKey(bucketKey, chart.definition.xdef.transformFunction);
|
||||
if (!parentKey) {
|
||||
for (const bucketKey of chart.bucketKeysSet) {
|
||||
res.bucketKeysSet.add(getParentDateBucketKey(bucketKey, chart.definition.xdef.transformFunction, false));
|
||||
}
|
||||
|
||||
for (const [groupedBucketKey, bucketValues] of Object.entries(chart.buckets)) {
|
||||
const groupedParentKey = getParentDateBucketKey(
|
||||
groupedBucketKey,
|
||||
chart.definition.xdef.transformFunction,
|
||||
!!chart.definition.groupingField
|
||||
);
|
||||
if (!groupedParentKey) {
|
||||
// skip if the bucket is already a parent
|
||||
continue;
|
||||
}
|
||||
res.bucketKeyDateParsed[parentKey] = getParentKeyParsed(
|
||||
chart.bucketKeyDateParsed[bucketKey],
|
||||
res.bucketKeyDateParsed[extractBucketKeyWithoutGroup(groupedParentKey, chart.definition)] = getParentKeyParsed(
|
||||
chart.bucketKeyDateParsed[extractBucketKeyWithoutGroup(groupedBucketKey, chart.definition)],
|
||||
chart.definition.xdef.transformFunction
|
||||
);
|
||||
aggregateChartNumericValuesFromChild(res, parentKey, bucketValues);
|
||||
aggregateChartNumericValuesFromChild(res, groupedParentKey, bucketValues);
|
||||
}
|
||||
|
||||
const bucketKeys = Object.keys(res.buckets).sort();
|
||||
@@ -400,7 +493,7 @@ export function aggregateChartNumericValuesFromSource(
|
||||
row: any
|
||||
) {
|
||||
for (const ydef of chart.definition.ydefs) {
|
||||
if (numericColumns[ydef.field] == null) {
|
||||
if (numericColumns[ydef.field] == null && ydef.field != '__count') {
|
||||
if (row[ydef.field]) {
|
||||
chart.invalidYRows[ydef.field] = (chart.invalidYRows[ydef.field] || 0) + 1; // increment invalid row count if the field is not numeric
|
||||
}
|
||||
@@ -527,16 +620,54 @@ export function fillChartTimelineBuckets(chart: ProcessedChart) {
|
||||
const transform = chart.definition.xdef.transformFunction;
|
||||
|
||||
let currentParsed = fromParsed;
|
||||
let count = 0;
|
||||
while (compareChartDatesParsed(currentParsed, toParsed, transform) <= 0) {
|
||||
const bucketKey = stringifyChartDate(currentParsed, transform);
|
||||
if (!chart.buckets[bucketKey]) {
|
||||
chart.buckets[bucketKey] = {};
|
||||
}
|
||||
if (!chart.bucketKeyDateParsed[bucketKey]) {
|
||||
chart.bucketKeyDateParsed[bucketKey] = currentParsed;
|
||||
}
|
||||
chart.bucketKeysSet.add(bucketKey);
|
||||
currentParsed = incrementChartDate(currentParsed, transform);
|
||||
count++;
|
||||
if (count > ChartLimits.CHART_FILL_LIMIT) {
|
||||
chart.errorMessage = `Too many buckets to fill in chart, limit is ${ChartLimits.CHART_FILL_LIMIT}`;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function computeChartBucketCardinality(bucket: { [key: string]: any }): number {
|
||||
return _sumBy(Object.keys(bucket), field => bucket[field]);
|
||||
return _sumBy(Object.keys(bucket ?? {}), field => bucket[field]);
|
||||
}
|
||||
|
||||
export function getChartYRange(chart: ProcessedChart, ydef: ChartYFieldDefinition) {
|
||||
let min = null;
|
||||
let max = null;
|
||||
|
||||
for (const obj of Object.values(chart.buckets)) {
|
||||
const value = obj[ydef.field];
|
||||
if (value != null) {
|
||||
if (min === null || value < min) {
|
||||
min = value;
|
||||
}
|
||||
if (max === null || value > max) {
|
||||
max = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { min, max };
|
||||
}
|
||||
|
||||
export function chartsHaveSimilarRange(range1: number, range2: number) {
|
||||
if (range1 < 0 && range2 < 0) {
|
||||
return Math.abs(range1 - range2) / Math.abs(range1) < 0.5;
|
||||
}
|
||||
if (range1 > 0 && range2 > 0) {
|
||||
return Math.abs(range1 - range2) / Math.abs(range1) < 0.5;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ const DS2 = [
|
||||
{
|
||||
ts1: '2023-10-03T07:10:00Z',
|
||||
ts2: '2024-10-03T07:10:00Z',
|
||||
price1: '13',
|
||||
price1: '22',
|
||||
price2: '24',
|
||||
},
|
||||
{
|
||||
@@ -116,22 +116,42 @@ describe('Chart processor', () => {
|
||||
const processor = new ChartProcessor();
|
||||
processor.addRows(...DS1.slice(0, 3));
|
||||
processor.finalize();
|
||||
expect(processor.charts.length).toEqual(1);
|
||||
const chart = processor.charts[0];
|
||||
expect(chart.definition.xdef.transformFunction).toEqual('date:day');
|
||||
expect(chart.definition.ydefs).toEqual([
|
||||
// console.log(getChartDebugPrint(processor.charts[0]));
|
||||
expect(processor.charts.length).toEqual(6);
|
||||
const chart1 = processor.charts.find(x => !x.definition.groupingField && x.definition.xdef.field === 'timestamp');
|
||||
expect(chart1.definition.xdef.transformFunction).toEqual('date:day');
|
||||
expect(chart1.definition.ydefs).toEqual([
|
||||
expect.objectContaining({
|
||||
field: 'value',
|
||||
}),
|
||||
]);
|
||||
expect(chart.bucketKeysOrdered).toEqual(['2023-10-01', '2023-10-02', '2023-10-03']);
|
||||
expect(chart1.bucketKeysOrdered).toEqual(['2023-10-01', '2023-10-02', '2023-10-03']);
|
||||
|
||||
const chart2 = processor.charts.find(x => x.definition.groupingField && x.definition.xdef.field === 'timestamp');
|
||||
expect(chart2.definition.xdef.transformFunction).toEqual('date:day');
|
||||
expect(chart2.bucketKeysOrdered).toEqual(['2023-10-01', '2023-10-02', '2023-10-03']);
|
||||
expect(chart2.definition.groupingField).toEqual('category');
|
||||
|
||||
const chart3 = processor.charts.find(x => x.definition.xdef.field === 'category');
|
||||
expect(chart3.bucketKeysOrdered).toEqual(['A', 'B']);
|
||||
expect(chart3.definition.groupingField).toBeUndefined();
|
||||
|
||||
const countCharts = processor.charts.filter(
|
||||
x => x.definition.ydefs.length == 1 && x.definition.ydefs[0].field == '__count'
|
||||
);
|
||||
expect(countCharts.length).toEqual(3);
|
||||
});
|
||||
test('By month grouped, autedetected', () => {
|
||||
const processor = new ChartProcessor();
|
||||
processor.addRows(...DS1.slice(0, 4));
|
||||
processor.finalize();
|
||||
expect(processor.charts.length).toEqual(1);
|
||||
const chart = processor.charts[0];
|
||||
expect(processor.charts.length).toEqual(6);
|
||||
const chart = processor.charts.find(
|
||||
x =>
|
||||
!x.definition.groupingField &&
|
||||
x.definition.xdef.field === 'timestamp' &&
|
||||
!x.definition.ydefs.find(y => y.field === '__count')
|
||||
);
|
||||
expect(chart.definition.xdef.transformFunction).toEqual('date:month');
|
||||
expect(chart.bucketKeysOrdered).toEqual([
|
||||
'2023-10',
|
||||
@@ -201,7 +221,7 @@ describe('Chart processor', () => {
|
||||
const processor = new ChartProcessor();
|
||||
processor.addRows(...DS2);
|
||||
processor.finalize();
|
||||
expect(processor.charts.length).toEqual(2);
|
||||
expect(processor.charts.length).toEqual(4);
|
||||
expect(processor.charts[0].definition).toEqual(
|
||||
expect.objectContaining({
|
||||
xdef: expect.objectContaining({
|
||||
@@ -244,8 +264,8 @@ describe('Chart processor', () => {
|
||||
const processor = new ChartProcessor();
|
||||
processor.addRows(...DS3);
|
||||
processor.finalize();
|
||||
expect(processor.charts.length).toEqual(1);
|
||||
const chart = processor.charts[0];
|
||||
expect(processor.charts.length).toEqual(2);
|
||||
const chart = processor.charts.find(x => !x.definition.ydefs.find(y => y.field === '__count'));
|
||||
expect(chart.definition.xdef.transformFunction).toEqual('date:day');
|
||||
expect(chart.definition.ydefs).toEqual([
|
||||
expect.objectContaining({
|
||||
@@ -373,4 +393,33 @@ describe('Chart processor', () => {
|
||||
expect(chart.buckets).toEqual(expectedBuckets);
|
||||
}
|
||||
);
|
||||
|
||||
test('Incorrect chart definition', () => {
|
||||
const processor = new ChartProcessor([
|
||||
{
|
||||
chartType: 'bar',
|
||||
xdef: {
|
||||
field: 'category',
|
||||
transformFunction: 'date:day',
|
||||
},
|
||||
ydefs: [],
|
||||
},
|
||||
]);
|
||||
processor.addRows(...DS1.slice(0, 3));
|
||||
processor.finalize();
|
||||
|
||||
expect(processor.charts.length).toEqual(1);
|
||||
const chart = processor.charts[0];
|
||||
expect(chart.definition.xdef.transformFunction).toEqual('date:day');
|
||||
|
||||
// console.log(getChartDebugPrint(processor.charts[0]));
|
||||
|
||||
// expect(chart.definition.xdef.transformFunction).toEqual('date:day');
|
||||
// expect(chart.definition.ydefs).toEqual([
|
||||
// expect.objectContaining({
|
||||
// field: 'value',
|
||||
// }),
|
||||
// ]);
|
||||
// expect(chart.bucketKeysOrdered).toEqual(['2023-10-01', '2023-10-02', '2023-10-03']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,8 @@ import _omitBy from 'lodash/omitBy';
|
||||
import { DataEditorTypesBehaviour } from 'dbgate-types';
|
||||
import isPlainObject from 'lodash/isPlainObject';
|
||||
|
||||
export const MAX_GRID_TEXT_LENGTH = 1000; // maximum length of text in grid cell, longer text is truncated
|
||||
|
||||
export type EditorDataType =
|
||||
| 'null'
|
||||
| 'objectid'
|
||||
@@ -297,7 +299,9 @@ export function stringifyCellValue(
|
||||
};
|
||||
}
|
||||
}
|
||||
return { value: highlightSpecialCharacters(value), gridStyle: 'textCellStyle' };
|
||||
const valueLimited =
|
||||
value.length > MAX_GRID_TEXT_LENGTH ? value.substring(0, MAX_GRID_TEXT_LENGTH) + '...' : value;
|
||||
return { value: highlightSpecialCharacters(valueLimited), gridStyle: 'textCellStyle' };
|
||||
}
|
||||
default:
|
||||
return { value: value };
|
||||
@@ -640,6 +644,7 @@ export function parseNumberSafe(value) {
|
||||
const frontMatterRe = /^--\ >>>[ \t\r]*\n(.*)\n-- <<<[ \t\r]*\n/s;
|
||||
|
||||
export function getSqlFrontMatter(text: string, yamlModule) {
|
||||
if (!text || !_isString(text)) return null;
|
||||
const match = text.match(frontMatterRe);
|
||||
if (!match) return null;
|
||||
const yamlContentMapped = match[1].replace(/^--[ ]?/gm, '');
|
||||
@@ -647,6 +652,7 @@ export function getSqlFrontMatter(text: string, yamlModule) {
|
||||
}
|
||||
|
||||
export function removeSqlFrontMatter(text: string) {
|
||||
if (!text || !_isString(text)) return null;
|
||||
return text.replace(frontMatterRe, '');
|
||||
}
|
||||
|
||||
@@ -669,5 +675,5 @@ export function setSqlFrontMatter(text: string, data: { [key: string]: any }, ya
|
||||
.map(line => '-- ' + line)
|
||||
.join('\n');
|
||||
const frontMatterContent = `-- >>>\n${yamlContentMapped}\n-- <<<\n`;
|
||||
return frontMatterContent + textClean;
|
||||
return frontMatterContent + (textClean || '');
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
ViewInfo,
|
||||
CollectionInfo,
|
||||
NamedObjectInfo,
|
||||
EngineDriver,
|
||||
} from 'dbgate-types';
|
||||
import _flatten from 'lodash/flatten';
|
||||
import _uniq from 'lodash/uniq';
|
||||
@@ -304,3 +305,11 @@ export function skipDbGateInternalObjects(db: DatabaseInfo) {
|
||||
tables: (db.tables || []).filter(tbl => tbl.pureName != 'dbgate_deploy_journal'),
|
||||
};
|
||||
}
|
||||
|
||||
export function adaptDatabaseInfo(db: DatabaseInfo, driver: EngineDriver): DatabaseInfo {
|
||||
const modelAdapted = {
|
||||
...db,
|
||||
tables: db.tables.map(table => driver.adaptTableInfo(table)),
|
||||
};
|
||||
return modelAdapted;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,11 @@ export interface IndexInfoYaml {
|
||||
included?: string[];
|
||||
}
|
||||
|
||||
export interface UniqueInfoYaml {
|
||||
name: string;
|
||||
columns: string[];
|
||||
}
|
||||
|
||||
export interface TableInfoYaml {
|
||||
name: string;
|
||||
// schema?: string;
|
||||
@@ -38,6 +43,7 @@ export interface TableInfoYaml {
|
||||
primaryKey?: string[];
|
||||
sortingKey?: string[];
|
||||
indexes?: IndexInfoYaml[];
|
||||
uniques?: UniqueInfoYaml[];
|
||||
|
||||
insertKey?: string[];
|
||||
insertOnly?: string[];
|
||||
@@ -121,6 +127,12 @@ export function tableInfoToYaml(table: TableInfo): TableInfoYaml {
|
||||
return idx;
|
||||
});
|
||||
}
|
||||
if (tableCopy.uniques?.length > 0) {
|
||||
res.uniques = tableCopy.uniques.map(unique => ({
|
||||
name: unique.constraintName,
|
||||
columns: unique.columns.map(x => x.columnName),
|
||||
}));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -165,6 +177,12 @@ export function tableInfoFromYaml(table: TableInfoYaml, allTables: TableInfoYaml
|
||||
...(index.included || []).map(columnName => ({ columnName, isIncludedColumn: true })),
|
||||
],
|
||||
})),
|
||||
uniques: table.uniques?.map(unique => ({
|
||||
constraintName: unique.name,
|
||||
pureName: table.name,
|
||||
constraintType: 'unique',
|
||||
columns: unique.columns.map(columnName => ({ columnName })),
|
||||
})),
|
||||
};
|
||||
if (table.primaryKey) {
|
||||
res.primaryKey = {
|
||||
|
||||
1
packages/types/dbinfo.d.ts
vendored
1
packages/types/dbinfo.d.ts
vendored
@@ -66,6 +66,7 @@ export interface ColumnInfo extends NamedObjectInfo {
|
||||
options?: [];
|
||||
canSelectMultipleOptions?: boolean;
|
||||
undropColumnName?: string;
|
||||
hasAutoValue?: boolean;
|
||||
}
|
||||
|
||||
export interface DatabaseObjectInfo extends NamedObjectInfo {
|
||||
|
||||
1
packages/types/dialect.d.ts
vendored
1
packages/types/dialect.d.ts
vendored
@@ -21,6 +21,7 @@ export interface SqlDialect {
|
||||
enableForeignKeyChecks?: boolean;
|
||||
requireStandaloneSelectForScopeIdentity?: boolean;
|
||||
allowMultipleValuesInsert?: boolean;
|
||||
useServerDatabaseFile?: boolean;
|
||||
|
||||
dropColumnDependencies?: string[];
|
||||
changeColumnDependencies?: string[];
|
||||
|
||||
2
packages/types/test-engines.d.ts
vendored
2
packages/types/test-engines.d.ts
vendored
@@ -93,6 +93,4 @@ export type TestEngineInfo = {
|
||||
}>;
|
||||
|
||||
objects?: Array<TestObjectInfo>;
|
||||
|
||||
transformModelRow?: (row: Record<string, any>) => Record<string, any>;
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@ant-design/colors": "^5.0.0",
|
||||
"@energiency/chartjs-plugin-piechart-outlabels": "^1.3.4",
|
||||
"@mdi/font": "^7.1.96",
|
||||
"@rollup/plugin-commonjs": "^20.0.0",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
@@ -63,6 +64,7 @@
|
||||
"chartjs-plugin-zoom": "^1.2.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"debug": "^4.3.4",
|
||||
"flatpickr": "^4.6.13",
|
||||
"fuzzy": "^0.1.3",
|
||||
"highlight.js": "^11.11.1",
|
||||
"interval-operations": "^1.0.7",
|
||||
|
||||
@@ -117,7 +117,7 @@ body {
|
||||
max-width: 16.6666%;
|
||||
}
|
||||
|
||||
.largeFormMarker input[type='text'], .largeFormMarker input[type='number'], .largeFormMarker input[type='password'], .largeFormMarker textarea {
|
||||
.largeFormMarker input[type='text'], .largeFormMarker input[type='number'], .largeFormMarker input[type='password'], .largeFormMarker textarea {
|
||||
width: 100%;
|
||||
padding: 10px 10px;
|
||||
font-size: 14px;
|
||||
@@ -126,6 +126,13 @@ body {
|
||||
border: 1px solid var(--theme-border);
|
||||
}
|
||||
|
||||
.input1 {
|
||||
padding: 5px 5px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--theme-border);
|
||||
}
|
||||
|
||||
.largeFormMarker select {
|
||||
width: 100%;
|
||||
|
||||
@@ -60,10 +60,9 @@
|
||||
installNewCloudTokenListener();
|
||||
initializeAppUpdates();
|
||||
installCloudListeners();
|
||||
refreshPublicCloudFiles();
|
||||
}
|
||||
|
||||
refreshPublicCloudFiles();
|
||||
|
||||
loadedApi = loadedApiValue;
|
||||
|
||||
if (!loadedApi) {
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
on:click={async e => {
|
||||
sessionStorage.setItem('continueTrialConfirmed', '1');
|
||||
const { licenseKey } = e.detail;
|
||||
const resp = await apiCall('config/save-license-key', { licenseKey });
|
||||
const resp = await apiCall('config/save-license-key', { licenseKey, tryToRenew: true });
|
||||
if (resp?.status == 'ok') {
|
||||
internalRedirectTo(isOneOfPage('admin-license') ? '/admin.html' : '/index.html');
|
||||
} else {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
export let module = null;
|
||||
|
||||
export let isBold = false;
|
||||
export let isGrayed = false;
|
||||
export let isChoosed = false;
|
||||
export let isBusy = false;
|
||||
export let statusIcon = undefined;
|
||||
@@ -93,6 +94,7 @@
|
||||
<div
|
||||
class="main"
|
||||
class:isBold
|
||||
class:isGrayed
|
||||
class:isChoosed
|
||||
class:disableHover
|
||||
draggable={true}
|
||||
@@ -111,6 +113,7 @@
|
||||
on:drop
|
||||
bind:this={domDiv}
|
||||
{...divProps}
|
||||
data-testid={$$props['data-testid']}
|
||||
>
|
||||
{#if checkedObjectsStore}
|
||||
<CheckboxField
|
||||
@@ -209,6 +212,10 @@
|
||||
.isBold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.isGrayed {
|
||||
color: var(--theme-font-3);
|
||||
}
|
||||
|
||||
.isChoosed {
|
||||
background-color: var(--theme-bg-3);
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if data.conid && $cloudConnectionsStore[data.conid]}
|
||||
{#if data.conid && $cloudConnectionsStore[data.conid] && $cloudConnectionsStore[data.conid]?._id}
|
||||
<ConnectionAppObject
|
||||
{...$$restProps}
|
||||
{passProps}
|
||||
@@ -109,7 +109,7 @@
|
||||
{passProps}
|
||||
data={{
|
||||
file: data.name,
|
||||
folder: data.contentFolder,
|
||||
folder: data.contentAttributes?.contentFolder,
|
||||
folid: data.folid,
|
||||
cntid: data.cntid,
|
||||
}}
|
||||
@@ -123,6 +123,8 @@
|
||||
icon={'img cloud-connection'}
|
||||
title={data.name}
|
||||
menu={createMenu}
|
||||
colorMark={passProps?.cloudContentColorFactory &&
|
||||
passProps?.cloudContentColorFactory({ cntid: data.cntid, folid: data.folid })}
|
||||
on:click={handleOpenContent}
|
||||
on:dblclick
|
||||
on:expand
|
||||
|
||||
@@ -337,8 +337,7 @@
|
||||
text: _t('connection.duplicate', { defaultMessage: 'Duplicate' }),
|
||||
onClick: handleDuplicate,
|
||||
},
|
||||
!$openedConnections.includes(data._id) &&
|
||||
$cloudSigninTokenHolder &&
|
||||
$cloudSigninTokenHolder &&
|
||||
passProps?.cloudContentList?.length > 0 && {
|
||||
text: _t('connection.copyToCloudFolder', { defaultMessage: 'Copy to cloud folder' }),
|
||||
submenu: passProps?.cloudContentList
|
||||
|
||||
@@ -11,29 +11,44 @@
|
||||
<script lang="ts">
|
||||
import { apiCall } from '../utility/api';
|
||||
import newQuery from '../query/newQuery';
|
||||
import { filterName } from 'dbgate-tools';
|
||||
import { filterName, getSqlFrontMatter, setSqlFrontMatter } from 'dbgate-tools';
|
||||
import { currentActiveCloudTags } from '../stores';
|
||||
import _ from 'lodash';
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
export let data;
|
||||
|
||||
async function handleOpenSqlFile() {
|
||||
const fileData = await apiCall('cloud/public-file-data', { path: data.path });
|
||||
let queryText = fileData.text;
|
||||
if (!relatedToCurrentConnection) {
|
||||
const frontMatter = getSqlFrontMatter(fileData.text, yaml);
|
||||
if (frontMatter?.autoExecute) {
|
||||
queryText = setSqlFrontMatter(fileData.text, _.omit(frontMatter, 'autoExecute'), yaml);
|
||||
}
|
||||
}
|
||||
newQuery({
|
||||
initialData: fileData.text,
|
||||
initialData: queryText,
|
||||
icon: data.icon,
|
||||
});
|
||||
}
|
||||
|
||||
function createMenu() {
|
||||
return [{ text: 'Open', onClick: handleOpenSqlFile }];
|
||||
}
|
||||
|
||||
$: relatedToCurrentConnection = _.intersection($currentActiveCloudTags, data.tags || []).length > 0;
|
||||
</script>
|
||||
|
||||
<AppObjectCore
|
||||
{...$$restProps}
|
||||
{data}
|
||||
icon={'img sql-file'}
|
||||
icon={data.icon ?? 'img sql-file'}
|
||||
title={data.title}
|
||||
menu={createMenu}
|
||||
on:click={handleOpenSqlFile}
|
||||
isGrayed={!relatedToCurrentConnection}
|
||||
data-testid={`public-cloud-file-${data.path}`}
|
||||
>
|
||||
{#if data.description}
|
||||
<div class="info">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts" context="module">
|
||||
import { filterName, getConnectionLabel } from 'dbgate-tools';
|
||||
import { filterName, getConnectionLabel, getSqlFrontMatter } from 'dbgate-tools';
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
interface FileTypeHandler {
|
||||
icon: string;
|
||||
@@ -9,6 +10,7 @@
|
||||
currentConnection: boolean;
|
||||
extension: string;
|
||||
label: string;
|
||||
switchDatabaseOnOpen?: (data: any) => Promise<boolean>;
|
||||
}
|
||||
|
||||
const sql: FileTypeHandler = {
|
||||
@@ -19,6 +21,21 @@
|
||||
currentConnection: true,
|
||||
extension: 'sql',
|
||||
label: 'SQL file',
|
||||
switchDatabaseOnOpen: async data => {
|
||||
const frontMatter = getSqlFrontMatter(data, yaml);
|
||||
if (frontMatter?.connectionId) {
|
||||
const connection = await getConnectionInfo({ conid: frontMatter.connectionId });
|
||||
// console.log('Switching database to', frontMatter.databaseName, 'on connection', connection);
|
||||
if (connection && frontMatter.databaseName) {
|
||||
currentDatabase.set({
|
||||
connection,
|
||||
name: frontMatter.databaseName,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
const shell: FileTypeHandler = {
|
||||
@@ -161,6 +178,7 @@
|
||||
import AppObjectCore from './AppObjectCore.svelte';
|
||||
import { isProApp } from '../utility/proTools';
|
||||
import { saveFileToDisk } from '../utility/exportFileTools';
|
||||
import { getConnectionInfo } from '../utility/metadataLoaders';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -281,6 +299,10 @@
|
||||
let tooltip = undefined;
|
||||
const connProps: any = {};
|
||||
|
||||
if (handler.switchDatabaseOnOpen) {
|
||||
await handler.switchDatabaseOnOpen(dataContent);
|
||||
}
|
||||
|
||||
if (handler.currentConnection) {
|
||||
const connection = _.get($currentDatabase, 'connection') || {};
|
||||
const database = _.get($currentDatabase, 'name');
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
export let narrow = false;
|
||||
export let square = true;
|
||||
export let disabled = false;
|
||||
export let title = undefined;
|
||||
|
||||
let domButton;
|
||||
let isLoading = false;
|
||||
@@ -40,6 +41,7 @@
|
||||
bind:this={domButton}
|
||||
{disabled}
|
||||
data-testid={$$props['data-testid']}
|
||||
{title}
|
||||
>
|
||||
<FontIcon icon={isLoading ? 'icon loading' : icon} />
|
||||
</InlineButton>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user