Compare commits

...

284 Commits

Author SHA1 Message Date
SPRINX0\prochazka
8cbe021ffc v6.5.6 2025-07-17 09:04:06 +02:00
SPRINX0\prochazka
7b39d8025b changelog 2025-07-17 09:03:18 +02:00
SPRINX0\prochazka
47bd35b151 fixed failing test 2025-07-17 08:44:22 +02:00
SPRINX0\prochazka
d7add54a3c v6.5.6-premium-beta.5 2025-07-17 08:19:37 +02:00
SPRINX0\prochazka
d3c937569b SYNC: anonymized cloud instance 2025-07-17 06:18:50 +00:00
SPRINX0\prochazka
94ca613201 v6.5.6-premium-beta.4 2025-07-16 15:52:09 +02:00
SPRINX0\prochazka
30f2f635be v6.5.6-premium-beta.3 2025-07-16 15:51:20 +02:00
SPRINX0\prochazka
57f4d31c21 SYNC: fix 2025-07-16 13:24:47 +00:00
SPRINX0\prochazka
90e4fd7ff5 SYNC: disable splitting queries with blank lines? #1162 2025-07-16 13:16:02 +00:00
SPRINX0\prochazka
17835832f2 v6.5.6-beta.2 2025-07-16 14:51:55 +02:00
SPRINX0\prochazka
949817f597 SYNC: SKIP_ALL_AUTH support 2025-07-16 12:48:38 +00:00
SPRINX0\prochazka
23065f2c4b SYNC: test fix 2025-07-16 12:26:08 +00:00
SPRINX0\prochazka
b623b06cf0 SYNC: bugfix 2025-07-16 12:02:19 +00:00
CI workflows
55c86d8ec7 chore: auto-update github workflows 2025-07-16 11:43:35 +00:00
CI workflows
e955617aa1 Update pro ref 2025-07-16 11:43:18 +00:00
SPRINX0\prochazka
6304610713 SYNC: hard limit for pie chart 2025-07-16 11:43:08 +00:00
SPRINX0\prochazka
47d20928e0 SYNC: try to fix tests 2025-07-16 11:24:06 +00:00
SPRINX0\prochazka
c9a4d02e0d SYNC: new object window screenshot 2025-07-16 11:03:07 +00:00
CI workflows
6513dfb42a chore: auto-update github workflows 2025-07-16 10:52:10 +00:00
CI workflows
3f0412453f Update pro ref 2025-07-16 10:51:29 +00:00
SPRINX0\prochazka
dcba319071 SYNC: disabled messages in new object modal 2025-07-16 10:51:19 +00:00
SPRINX0\prochazka
d19851fc0c SYNC: compare database in new object modal 2025-07-16 10:51:17 +00:00
SPRINX0\prochazka
d6eb06cb72 SYNC: export db window 2025-07-16 10:51:16 +00:00
SPRINX0\prochazka
473080d7ee SYNC: typo 2025-07-16 10:51:15 +00:00
SPRINX0\prochazka
c98a6adb09 SYNC: new object modal testid 2025-07-16 10:51:13 +00:00
SPRINX0\prochazka
2cd56d5041 SYNC: new object button refactor + diagram accesibility 2025-07-16 10:51:12 +00:00
SPRINX0\prochazka
982098672e SYNC: new object modal 2025-07-16 10:51:10 +00:00
SPRINX0\prochazka
445ecea3e6 SYNC: new object modal WIP 2025-07-16 10:51:09 +00:00
SPRINX0\prochazka
db977dfba4 SYNC: next screenshots 2025-07-15 08:32:33 +00:00
CI workflows
a3c12ab9f5 chore: auto-update github workflows 2025-07-15 08:22:58 +00:00
CI workflows
0f7e152650 Update pro ref 2025-07-15 08:22:41 +00:00
SPRINX0\prochazka
b55c7ba9a1 v6.5.6-premium-beta.1 2025-07-15 09:16:08 +02:00
CI workflows
8256c9f7ad chore: auto-update github workflows 2025-07-15 07:12:36 +00:00
CI workflows
59727d7b0b Update pro ref 2025-07-15 07:12:20 +00:00
SPRINX0\prochazka
2dd2210a73 SYNC: separate schemas mode usable for administration 2025-07-15 07:12:08 +00:00
SPRINX0\prochazka
25aafdbebc SYNC: chart screenshots for tutorial 2025-07-15 06:46:01 +00:00
SPRINX0\prochazka
cd5717169c login checker dummy implementation 2025-07-14 15:23:09 +02:00
CI workflows
a38ad5a11e chore: auto-update github workflows 2025-07-14 13:22:34 +00:00
CI workflows
66d9b56976 Update pro ref 2025-07-14 13:22:21 +00:00
SPRINX0\prochazka
ac40bd1e17 SYNC: checking logged users 2025-07-14 13:22:10 +00:00
SPRINX0\prochazka
16d2a9bf99 SYNC: renew license from set license page 2025-07-14 11:41:55 +00:00
SPRINX0\prochazka
b7e6838d26 refresh license fake 2025-07-14 12:28:49 +02:00
CI workflows
21d23b5baa chore: auto-update github workflows 2025-07-14 10:22:13 +00:00
CI workflows
69a2941d57 Update pro ref 2025-07-14 10:21:59 +00:00
SPRINX0\prochazka
3cc2abf8b9 SYNC: better handling of expired license in electron app 2025-07-14 10:21:49 +00:00
Jan Prochazka
6f4173650a v6.5.5 2025-07-04 09:08:49 +02:00
Jan Prochazka
0fcb8bdc0a SYNC: changelog 2025-07-04 07:05:28 +00:00
CI workflows
c0937cf412 chore: auto-update github workflows 2025-07-04 06:44:51 +00:00
CI workflows
d9ab3aab0f Update pro ref 2025-07-04 06:44:36 +00:00
CI workflows
c8652de78b chore: auto-update github workflows 2025-07-04 06:34:19 +00:00
CI workflows
86dc4e2bd5 Update pro ref 2025-07-04 06:34:04 +00:00
Jan Prochazka
1b9c56a9b9 SYNC: fixed data replicator test 2025-07-04 06:33:54 +00:00
Jan Prochazka
08ab504fac SYNC: fix 2025-07-04 06:33:52 +00:00
CI workflows
21c0842fae chore: auto-update github workflows 2025-07-04 06:11:12 +00:00
CI workflows
8d10feaa68 Update pro ref 2025-07-04 06:10:53 +00:00
Jan Prochazka
df2171f253 SYNC: fixed disabling/enabling auth methods for team premium 2025-07-04 06:10:43 +00:00
SPRINX0\prochazka
f5fcd94faf SYNC: fix 2025-07-03 15:36:58 +00:00
CI workflows
15c5dbef00 chore: auto-update github workflows 2025-07-03 15:28:56 +00:00
CI workflows
79df56c096 Update pro ref 2025-07-03 15:28:39 +00:00
SPRINX0\prochazka
d3fffd9530 SYNC: missing audit logs 2025-07-03 15:28:28 +00:00
SPRINX0\prochazka
527c9c8e6e loginchecker placeholder 2025-07-03 16:58:27 +02:00
CI workflows
d285be45cb chore: auto-update github workflows 2025-07-03 14:51:25 +00:00
CI workflows
0dda9c73f6 Update pro ref 2025-07-03 14:51:10 +00:00
SPRINX0\prochazka
d07bf270e7 SYNC: logi checker refactor 2025-07-03 14:50:59 +00:00
CI workflows
eb24dd5d9e chore: auto-update github workflows 2025-07-03 13:25:47 +00:00
CI workflows
ce693c7cd5 Update pro ref 2025-07-03 13:25:32 +00:00
CI workflows
3198890269 chore: auto-update github workflows 2025-07-03 13:11:26 +00:00
CI workflows
eacc93de43 Update pro ref 2025-07-03 13:11:07 +00:00
SPRINX0\prochazka
9795740257 SYNC: check licensed user count 2025-07-03 13:10:55 +00:00
SPRINX0\prochazka
4548f5d8aa fix 2025-07-03 14:09:29 +02:00
SPRINX0\prochazka
8dfd2fb519 markUserAsActive dummy method 2025-07-03 14:07:04 +02:00
CI workflows
83a40f83e1 chore: auto-update github workflows 2025-07-03 11:50:05 +00:00
CI workflows
5b2fcb3c6c Update pro ref 2025-07-03 11:49:50 +00:00
Jan Prochazka
bcd9adb66d Merge pull request #1159 from dbgate/feature/firebird-always-use-text-for-file
feat: add useServerDatabaseFile for firebird
2025-07-03 13:30:21 +02:00
Pavel
5e2dc114ab feat: add useServerDatabaseFile for firebird 2025-07-03 13:27:22 +02:00
SPRINX0\prochazka
1ced4531be auditlog dummy methods 2025-07-03 13:20:07 +02:00
CI workflows
05fe39c0ae chore: auto-update github workflows 2025-07-03 11:18:54 +00:00
CI workflows
3769b2b3ea Update pro ref 2025-07-03 11:18:38 +00:00
Jan Prochazka
f4d5480f6f SYNC: try to fix test 2025-07-02 14:09:25 +00:00
Jan Prochazka
ddf3c0810b SYNC: charts auto detect 2025-07-02 13:49:39 +00:00
Jan Prochazka
6afd6d0aa0 v6.5.5-premium-beta.5 2025-07-02 13:42:41 +02:00
CI workflows
59fe92eb04 chore: auto-update github workflows 2025-07-02 11:41:41 +00:00
CI workflows
0550f32434 Update pro ref 2025-07-02 11:41:25 +00:00
Jan Prochazka
b702cad549 SYNC: fixed chart test 2025-07-02 11:41:15 +00:00
Jan Prochazka
aa5c4d3c5e changelog 2025-07-02 13:29:37 +02:00
CI workflows
6a99445d97 chore: auto-update github workflows 2025-07-02 11:22:53 +00:00
CI workflows
c9880ef47d Update pro ref 2025-07-02 11:22:38 +00:00
CI workflows
c16452dfcb chore: auto-update github workflows 2025-07-02 11:12:38 +00:00
CI workflows
af802c02fc Update pro ref 2025-07-02 11:12:20 +00:00
Jan Prochazka
8028aafeff SYNC: split too different ydefs 2025-07-02 11:12:09 +00:00
Jan Prochazka
b7469062a1 SYNC: charts autodetector test 2025-07-02 08:57:39 +00:00
Jan Prochazka
33b707aa68 SYNC: chart autodetection improved 2025-07-02 08:50:33 +00:00
Jan Prochazka
cd3a1bebff SYNC: autodetect - with grouping field 2025-07-02 08:23:12 +00:00
Jan Prochazka
794dd5a797 SYNC: refactor 2025-07-02 08:23:10 +00:00
CI workflows
a1465432e8 chore: auto-update github workflows 2025-07-02 06:54:14 +00:00
CI workflows
e1f8af0909 Update pro ref 2025-07-02 06:53:57 +00:00
Jan Prochazka
88918be329 SYNC: chart - detect data types 2025-07-02 06:53:47 +00:00
CI workflows
a3fc1dbff0 chore: auto-update github workflows 2025-07-02 06:20:20 +00:00
CI workflows
626c9825cc Update pro ref 2025-07-02 06:20:02 +00:00
Jan Prochazka
c10a84fc79 SYNC: timeline chart type 2025-07-02 06:19:49 +00:00
SPRINX0\prochazka
f14e4fe197 SYNC: month match 2025-07-01 14:53:27 +00:00
CI workflows
6eb218db5e chore: auto-update github workflows 2025-07-01 14:30:39 +00:00
CI workflows
0e77e053b0 Update pro ref 2025-07-01 14:30:23 +00:00
SPRINX0\prochazka
b9a4128a3d SYNC: charts - grouping field support 2025-07-01 14:30:12 +00:00
CI workflows
16f480e1f3 chore: auto-update github workflows 2025-07-01 12:23:46 +00:00
CI workflows
7c42511133 Update pro ref 2025-07-01 12:23:32 +00:00
CI workflows
1b252a84c2 chore: auto-update github workflows 2025-07-01 12:14:57 +00:00
CI workflows
bf833cadff Update pro ref 2025-07-01 12:14:42 +00:00
CI workflows
b6f872882a chore: auto-update github workflows 2025-07-01 12:12:56 +00:00
CI workflows
a18d6fb441 Update pro ref 2025-07-01 12:12:39 +00:00
CI workflows
922e703e81 chore: auto-update github workflows 2025-07-01 10:59:50 +00:00
CI workflows
d7f5817b8b Update pro ref 2025-07-01 10:59:32 +00:00
SPRINX0\prochazka
92a8a4bfa6 SYNC: chart improvements 2025-07-01 10:59:20 +00:00
CI workflows
b480151fc3 chore: auto-update github workflows 2025-07-01 10:35:08 +00:00
CI workflows
37bdbc1bd5 Update pro ref 2025-07-01 10:34:52 +00:00
CI workflows
8eb669139b chore: auto-update github workflows 2025-07-01 10:11:57 +00:00
CI workflows
b485e8cacc Update pro ref 2025-07-01 10:11:39 +00:00
CI workflows
c4bab61c47 chore: auto-update github workflows 2025-07-01 08:45:43 +00:00
CI workflows
72be417ff1 Update pro ref 2025-07-01 08:45:27 +00:00
CI workflows
9be483d7a6 chore: auto-update github workflows 2025-07-01 08:35:12 +00:00
CI workflows
910f2cee2c Update pro ref 2025-07-01 08:34:48 +00:00
SPRINX0\prochazka
1e47ace527 SYNC: fixed diagram zoom GL#57 2025-07-01 07:14:55 +00:00
Jan Prochazka
912b06b145 v6.5.5-premium-beta.4 2025-06-30 14:35:30 +02:00
CI workflows
87d878e287 chore: auto-update github workflows 2025-06-30 12:33:45 +00:00
CI workflows
be886d6bce Update pro ref 2025-06-30 12:33:29 +00:00
Jan Prochazka
0683deb47e v6.5.5-premium-beta.3 2025-06-30 13:53:39 +02:00
CI workflows
114bb22e27 chore: auto-update github workflows 2025-06-30 11:46:31 +00:00
CI workflows
c327ebc3df Update pro ref 2025-06-30 11:46:18 +00:00
Jan Prochazka
92cbd1c69c SYNC: config fixed 2025-06-30 11:46:05 +00:00
CI workflows
7242515e48 chore: auto-update github workflows 2025-06-30 10:32:57 +00:00
CI workflows
401d1a0ac2 Update pro ref 2025-06-30 10:32:41 +00:00
Jan Prochazka
863e042a37 SYNC: fixed exporting chart for electron 2025-06-30 10:32:31 +00:00
Jan Prochazka
39e6c45ec6 v6.5.5-premium-beta.2 2025-06-30 09:25:54 +02:00
CI workflows
0d364d18c7 chore: auto-update github workflows 2025-06-30 06:56:17 +00:00
CI workflows
61444ea390 Update pro ref 2025-06-30 06:56:01 +00:00
CI workflows
106a935efb chore: auto-update github workflows 2025-06-30 06:28:20 +00:00
CI workflows
d175d8a853 Update pro ref 2025-06-30 06:28:04 +00:00
Jan Prochazka
ce6d19a77a SYNC: call adapt db info 2025-06-30 06:27:54 +00:00
CI workflows
0a29273924 chore: auto-update github workflows 2025-06-29 18:32:57 +00:00
CI workflows
5ede64de58 Update pro ref 2025-06-29 18:32:40 +00:00
Jan Prochazka
224c6ad798 SYNC: try to fix oracle test 2025-06-29 18:27:07 +00:00
Jan Prochazka
57b3a0dbe7 v6.5.5-premium-beta.1 2025-06-28 12:00:16 +02:00
CI workflows
f381f708e0 chore: auto-update github workflows 2025-06-27 13:27:16 +00:00
CI workflows
63bf149546 Update pro ref 2025-06-27 13:27:02 +00:00
SPRINX0\prochazka
cb5e671259 SYNC: audit log test 2025-06-27 13:26:51 +00:00
CI workflows
3e38173c4e chore: auto-update github workflows 2025-06-27 13:01:02 +00:00
CI workflows
efacb643fc Update pro ref 2025-06-27 13:00:45 +00:00
SPRINX0\prochazka
1bd153ea0b SYNC: audit log UX 2025-06-27 13:00:32 +00:00
CI workflows
bac3dc5f4c chore: auto-update github workflows 2025-06-27 11:08:55 +00:00
CI workflows
959a853d77 Update pro ref 2025-06-27 11:08:36 +00:00
SPRINX0\prochazka
90bbdd563b SYNC: Merge branch 'feature/audit-logs' 2025-06-27 11:08:23 +00:00
SPRINX0\prochazka
e3c6d05a0a SYNC: try to fix test 2025-06-27 10:32:01 +00:00
SPRINX0\prochazka
930b3d4538 SYNC: cloud test - use test login 2025-06-27 08:51:36 +00:00
SPRINX0\prochazka
74b78141b4 fake method 2025-06-27 10:02:33 +02:00
SPRINX0\prochazka
aa1108cd5b SYNC: try to fix build 2025-06-27 07:42:42 +00:00
SPRINX0\prochazka
f24b1a9db3 audit log fake methods 2025-06-26 16:49:10 +02:00
SPRINX0\prochazka
71b191e740 SYNC: try to fix test 2025-06-25 07:24:42 +00:00
SPRINX0\prochazka
8f6341b903 SYNC: removed baseUrl config 2025-06-25 07:02:07 +00:00
SPRINX0\prochazka
161586db7e SYNC: try to fix test 2025-06-24 15:13:32 +00:00
SPRINX0\prochazka
052262bef9 SYNC: try to fix test 2025-06-24 13:05:38 +00:00
SPRINX0\prochazka
a5a7144707 SYNC: try to fix test 2025-06-24 10:57:54 +00:00
SPRINX0\prochazka
d945e0426d SYNC: try to fix test 2025-06-24 10:29:47 +00:00
SPRINX0\prochazka
926970c4eb SYNC: added missing script 2025-06-24 09:23:53 +00:00
CI workflows
cce36e0f28 chore: auto-update github workflows 2025-06-24 08:33:21 +00:00
CI workflows
48c6dc5be5 Update pro ref 2025-06-24 08:33:05 +00:00
SPRINX0\prochazka
c641830825 SYNC: private cloud test 2025-06-24 08:32:52 +00:00
SPRINX0\prochazka
eba16cc15d SYNC: dbgate cloud redirect workflow 2025-06-24 07:22:43 +00:00
SPRINX0\prochazka
bd88b8411e SYNC: private cloud test 2025-06-23 14:59:05 +00:00
SPRINX0\prochazka
fc121e8750 missing file 2025-06-23 16:51:42 +02:00
CI workflows
d4142fe56a chore: auto-update github workflows 2025-06-23 14:31:30 +00:00
CI workflows
f76a3e72bb Update pro ref 2025-06-23 14:31:11 +00:00
SPRINX0\prochazka
2d400ae7eb SYNC: folder administration modal 2025-06-23 14:30:58 +00:00
SPRINX0\prochazka
edf1632cab SYNC: fixed connection for scripts 2025-06-23 11:29:04 +00:00
SPRINX0\prochazka
a648f1ee67 SYNC: SQL fixed database WIP 2025-06-23 11:10:46 +00:00
SPRINX0\prochazka
d004e6e86c SYNC: new cloud file 2025-06-23 09:05:42 +00:00
SPRINX0\prochazka
fa321d3e8d SYNC: create query on cloud shortcut 2025-06-23 08:52:07 +00:00
SPRINX0\prochazka
e1e53d323f SYNC: dbgate cloud menu refactor 2025-06-23 07:40:46 +00:00
SPRINX0\prochazka
ccb18ca302 v6.5.4 2025-06-20 17:03:39 +02:00
SPRINX0\prochazka
e170f36bc6 v6.5.4-premium-beta.1 2025-06-20 16:46:18 +02:00
SPRINX0\prochazka
4bd9cc51ee SYNC: try to fix e2e test 2025-06-20 14:41:03 +00:00
SPRINX0\prochazka
43ffbda1a4 SYNC: removed vorgotten test.only 2025-06-20 13:32:20 +00:00
SPRINX0\prochazka
8240485fd1 changelog 2025-06-20 15:05:18 +02:00
SPRINX0\prochazka
7f053c0567 v6.5.3 2025-06-20 15:01:47 +02:00
SPRINX0\prochazka
d2922eb0b7 SYNC: cloud connections fix 2025-06-20 12:57:09 +00:00
SPRINX0\prochazka
fec10d453f license detection fix 2025-06-20 14:36:00 +02:00
SPRINX0\prochazka
162040545d SYNC: improved about modal 2025-06-20 12:29:11 +00:00
CI workflows
f14577f8bf chore: auto-update github workflows 2025-06-20 11:57:27 +00:00
CI workflows
e5720bd1be Update pro ref 2025-06-20 11:57:15 +00:00
CI workflows
6d4959bac8 Update pro ref 2025-06-20 11:57:11 +00:00
SPRINX0\prochazka
d668128a34 SYNC: private cloud UX + fixes 2025-06-20 11:46:04 +00:00
SPRINX0\prochazka
f2af38da4c v6.5.3-premium-beta.1 2025-06-19 18:00:13 +02:00
CI workflows
4776d18fd7 chore: auto-update github workflows 2025-06-19 15:57:59 +00:00
CI workflows
cdd0be7b78 Update pro ref 2025-06-19 15:57:43 +00:00
SPRINX0\prochazka
cd505abb22 SYNC: charts fix 2025-06-19 15:57:31 +00:00
SPRINX0\prochazka
28439c010f SYNC: fixed all search column settings for alternative grids #1118 2025-06-19 12:43:15 +00:00
CI workflows
e85f43beb1 chore: auto-update github workflows 2025-06-19 12:08:31 +00:00
CI workflows
a06cbc0840 Update pro ref 2025-06-19 12:08:18 +00:00
SPRINX0\prochazka
adef9728f8 SYNC: charts UX, error handling, bucket count limit 2025-06-19 12:08:06 +00:00
CI workflows
ff1b688b6e chore: auto-update github workflows 2025-06-19 08:56:09 +00:00
CI workflows
3e7574a927 Update pro ref 2025-06-19 08:55:55 +00:00
SPRINX0\prochazka
f852ea90ad changelog 2025-06-18 11:31:53 +02:00
SPRINX0\prochazka
d8f6247c32 v6.5.2 2025-06-18 11:00:46 +02:00
SPRINX0\prochazka
9dc28393a5 links added 2025-06-18 10:50:44 +02:00
SPRINX0\prochazka
c442c98ecf SYNC: fixed test 2025-06-18 08:43:56 +00:00
SPRINX0\prochazka
71e0109927 v6.5.2-premium-beta.1 2025-06-18 10:21:57 +02:00
SPRINX0\prochazka
9c7dd5ed1c SYNC: close chart fix 2025-06-18 08:17:58 +00:00
CI workflows
83620848f2 chore: auto-update github workflows 2025-06-18 08:13:39 +00:00
CI workflows
d548a5b4f3 Update pro ref 2025-06-18 08:13:25 +00:00
CI workflows
b6e5307755 chore: auto-update github workflows 2025-06-18 08:05:11 +00:00
CI workflows
4c5dc5a145 Update pro ref 2025-06-18 08:04:54 +00:00
SPRINX0\prochazka
69ed9172b8 SYNC: chart UX 2025-06-18 08:04:41 +00:00
CI workflows
68551ae176 chore: auto-update github workflows 2025-06-18 07:48:21 +00:00
CI workflows
c97d9d35ba Update pro ref 2025-06-18 07:48:07 +00:00
SPRINX0\prochazka
e86cc97cdf SYNC: changed chart logic 2025-06-18 07:47:56 +00:00
SPRINX0\prochazka
9bff8608c1 SYNC: auto-detect charts is disabled by default #1145 2025-06-18 07:30:16 +00:00
SPRINX0\prochazka
a10fe6994a v6.5.1 2025-06-17 16:08:07 +02:00
SPRINX0\prochazka
67e6a37b59 v6.5.1-beta.1 2025-06-17 15:42:26 +02:00
SPRINX0\prochazka
3075a56735 fixed cloud login 2025-06-17 15:42:16 +02:00
SPRINX0\prochazka
7e4a862cc3 v6.5.0 2025-06-17 10:02:43 +02:00
SPRINX0\prochazka
ed2078ee3b handle license errors 2025-06-17 09:55:59 +02:00
SPRINX0\prochazka
f99c23a622 v6.4.3-premium-beta.10 2025-06-17 09:08:40 +02:00
SPRINX0\prochazka
41e7317764 v6.4.3-beta.9 2025-06-17 09:08:18 +02:00
SPRINX0\prochazka
e0a78c2399 equal behaviour for premium and community 2025-06-17 09:06:08 +02:00
SPRINX0\prochazka
95ad39d2d4 admin premium widget 2025-06-17 09:03:57 +02:00
CI workflows
b831f827b1 chore: auto-update github workflows 2025-06-16 11:25:21 +00:00
SPRINX0\prochazka
c5d8413d9c SYNC: fix 2025-06-16 11:25:08 +00:00
CI workflows
4648ea3424 Update pro ref 2025-06-16 11:25:04 +00:00
SPRINX0\prochazka
e05bd6f231 SYNC: front matter in chart screenshot 2025-06-16 11:24:49 +00:00
CI workflows
caadee7901 chore: auto-update github workflows 2025-06-16 10:58:30 +00:00
CI workflows
18c524117d Update pro ref 2025-06-16 10:58:11 +00:00
SPRINX0\prochazka
ad30fb8b04 SYNC: query result chart screenshot 2025-06-16 10:57:58 +00:00
SPRINX0\prochazka
a8077965a9 icon fix 2025-06-16 10:36:57 +02:00
SPRINX0\prochazka
532ab85ebb public cloud improvements 2025-06-16 10:32:49 +02:00
SPRINX0\prochazka
546227eb37 changelog 2025-06-16 09:55:15 +02:00
CI workflows
7ec3b262d3 chore: auto-update github workflows 2025-06-16 07:24:20 +00:00
CI workflows
c435000d24 Update pro ref 2025-06-16 07:24:03 +00:00
SPRINX0\prochazka
eaa60c281e SYNC: chart screenshot 2025-06-16 07:23:51 +00:00
Jan Prochazka
cd7cf63144 SYNC: copy to cloud folder works for connected DB 2025-06-15 08:22:58 +00:00
CI workflows
6a704aa079 chore: auto-update github workflows 2025-06-15 07:25:21 +00:00
CI workflows
d6b5a1cec8 Update pro ref 2025-06-15 07:25:03 +00:00
Jan Prochazka
9a24ad31cc SYNC: pie chart out labels 2025-06-15 07:24:50 +00:00
CI workflows
9331630b54 chore: auto-update github workflows 2025-06-15 06:58:43 +00:00
CI workflows
904e869d7f Update pro ref 2025-06-15 06:58:29 +00:00
Jan Prochazka
307fa4f5e6 SYNC: trim license 2025-06-15 06:58:15 +00:00
SPRINX0\prochazka
131d16d3ea v6.4.3-beta.8 2025-06-13 16:35:05 +02:00
SPRINX0\prochazka
dbc54c45dd v6.4.3-premium-beta.7 2025-06-13 16:34:50 +02:00
SPRINX0\prochazka
a96a84d509 SYNC: grayed scripts for non active database 2025-06-13 14:31:53 +00:00
SPRINX0\prochazka
3eb8863f67 SYNC: cloud connections in tab names 2025-06-13 13:25:34 +00:00
SPRINX0\prochazka
8737ab077b SYNC: fixed status bar color 2025-06-13 13:14:16 +00:00
SPRINX0\prochazka
50bb6a1d19 SYNC: prod cloud 2025-06-13 13:12:09 +00:00
SPRINX0\prochazka
4181b75af7 SYNC: connection color for cloud connections 2025-06-13 13:08:56 +00:00
CI workflows
620705c87a chore: auto-update github workflows 2025-06-13 12:14:41 +00:00
CI workflows
50b7b93529 Update pro ref 2025-06-13 12:14:22 +00:00
SPRINX0\prochazka
6f18f6bd5c SYNC: debug config 2025-06-13 12:14:07 +00:00
SPRINX0\prochazka
01d256eeee SYNC: don't show private cloud for web app 2025-06-13 11:52:03 +00:00
SPRINX0\prochazka
a1405412a8 debug config 2025-06-13 11:49:19 +02:00
SPRINX0\prochazka
58589b3a15 v6.4.3-premium-beta.6 2025-06-13 09:44:49 +02:00
SPRINX0\prochazka
2983266fdf Merge branch 'master' of https://github.com/dbgate/dbgate 2025-06-12 16:55:58 +02:00
SPRINX0\prochazka
e33df8f12d cloud content refactor 2025-06-12 16:55:55 +02:00
CI workflows
0e0e8e9d18 chore: auto-update github workflows 2025-06-12 13:30:21 +00:00
Jan Prochazka
37f8b54752 Merge pull request #1130 from dbgate/feature/firebird
Feature/firebird
2025-06-12 15:29:58 +02:00
SPRINX0\prochazka
e9a086ad23 SYNC: refresh cloud files improvements 2025-06-12 12:26:35 +00:00
SPRINX0\prochazka
7c06a8ac41 SYNC: fix refresh publis files 2025-06-12 12:13:33 +00:00
SPRINX0\prochazka
70801d958e SYNC: security rename 2025-06-12 11:51:22 +00:00
SPRINX0\prochazka
cf3f95c952 SYNC: security fixes 2025-06-12 11:49:53 +00:00
Pavel
d708616a6a feat: add createFirebirdInsertStream with datetime fields transform 2025-06-12 13:44:24 +02:00
Pavel
17711bc5c9 Revert "feat: transform rows suport for json lines reader"
This reverts commit b74b6b3284.
2025-06-12 13:29:48 +02:00
Pavel
1e2474921b Revert "feat: transform firebird model rows"
This reverts commit 5760ada3b4.
2025-06-12 13:29:46 +02:00
SPRINX0\prochazka
3f37b2b728 security fixes 2025-06-12 10:58:46 +02:00
SPRINX0\prochazka
18b11df672 security: prevent file traversal in uploads 2025-06-12 10:43:27 +02:00
SPRINX0\prochazka
c34f2d4da7 better UX when logging in in electron 2025-06-11 17:25:14 +02:00
SPRINX0\prochazka
d61792581a Merge branch 'master' of https://github.com/dbgate/dbgate 2025-06-11 17:02:40 +02:00
SPRINX0\prochazka
76d9a511b8 sql generate aliases automatically #1122 2025-06-11 17:02:38 +02:00
CI workflows
4248326697 chore: auto-update github workflows 2025-06-11 12:41:59 +00:00
SPRINX0\prochazka
a540b38151 don't generate artifacts for check build 2025-06-11 14:41:34 +02:00
SPRINX0\prochazka
8fb5ef0c1d v6.4.3-premium-beta.5 2025-06-11 14:39:00 +02:00
SPRINX0\prochazka
2da4979e59 UX fix 2025-06-11 14:29:59 +02:00
SPRINX0\prochazka
0146e4a1dd #1111 mssql - handle timestamp and computed columns in clonerows 2025-06-11 11:28:46 +02:00
SPRINX0\prochazka
34bdb72ffd #1118 2025-06-11 11:09:07 +02:00
SPRINX0\prochazka
2ef7c63047 copy column names #1119 2025-06-11 10:34:22 +02:00
SPRINX0\prochazka
95f5417761 fixed grid performance problem - limited length of cell string 2025-06-11 09:30:58 +02:00
SPRINX0\prochazka
4922ec4499 Merge branch 'master' into feature/firebird 2025-06-11 08:09:28 +02:00
SPRINX0\prochazka
20d947a199 readme for firebird 2025-06-10 16:46:16 +02:00
SPRINX0\prochazka
871dc90ee4 fixed community build (missing JslChart.svelte) 2025-06-09 16:32:57 +02:00
161 changed files with 3708 additions and 643 deletions

View File

@@ -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: |

View File

@@ -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: |

View File

@@ -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: |

View File

@@ -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: |

View File

@@ -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: |

View File

@@ -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}}

View File

@@ -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

View File

@@ -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

View File

@@ -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 ..

View File

@@ -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: |

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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!

View File

@@ -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'] });

View File

@@ -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();

View 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');
});
});

View 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');
});
});

View File

@@ -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');")

View File

@@ -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');
});
});

View File

@@ -42,3 +42,11 @@ beforeEach(() => {
});
});
});
// Cypress.Screenshot.defaults({
// onBeforeScreenshot() {
// if (window.Chart) {
// Object.values(window.Chart.instances).forEach(c => c.resize());
// }
// },
// });

View 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"}

View 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

View 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"}

View 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

View 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}

View 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

View 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}

View 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

View 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"}

View 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

View 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
View 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
View File

@@ -0,0 +1,2 @@
ALLOW_DBGATE_PRIVATE_CLOUD=1
REDIRECT_TO_DBGATE_CLOUD_LOGIN=1

96
e2e-tests/init/charts.js Normal file
View 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);

View File

@@ -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": {}

View File

@@ -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');

View File

@@ -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`));

View File

@@ -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,
};

View File

@@ -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",

View File

@@ -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

View File

@@ -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 {};
}

View File

@@ -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,
};

View File

@@ -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;
},
};

View File

@@ -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);

View File

@@ -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;
},

View File

@@ -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;

View File

@@ -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;
},

View File

@@ -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,
};
},
};

View File

@@ -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('');

View File

@@ -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 ?? [];
},

View File

@@ -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' };
},

View File

@@ -31,6 +31,11 @@ module.exports = {
return {};
},
sendAuditLog_meta: true,
async sendAuditLog({}) {
return null;
},
startRefreshLicense() {},
async getUsedEngines() {

View File

@@ -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));
},

View 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;

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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];
}

View File

@@ -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": []
};

View File

@@ -0,0 +1,9 @@
// only in DbGate Premium
async function sendToAuditLog(req, props) {}
async function logJsonRunnerScript(req, script) {}
module.exports = {
sendToAuditLog,
logJsonRunnerScript,
};

View File

@@ -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,
};

View File

@@ -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,
};

View File

@@ -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();

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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);

View File

@@ -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);
});

View 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,
};

View 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,
};

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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),

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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']);
});
});

View File

@@ -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 || '');
}

View File

@@ -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;
}

View File

@@ -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 = {

View File

@@ -66,6 +66,7 @@ export interface ColumnInfo extends NamedObjectInfo {
options?: [];
canSelectMultipleOptions?: boolean;
undropColumnName?: string;
hasAutoValue?: boolean;
}
export interface DatabaseObjectInfo extends NamedObjectInfo {

View File

@@ -21,6 +21,7 @@ export interface SqlDialect {
enableForeignKeyChecks?: boolean;
requireStandaloneSelectForScopeIdentity?: boolean;
allowMultipleValuesInsert?: boolean;
useServerDatabaseFile?: boolean;
dropColumnDependencies?: string[];
changeColumnDependencies?: string[];

View File

@@ -93,6 +93,4 @@ export type TestEngineInfo = {
}>;
objects?: Array<TestObjectInfo>;
transformModelRow?: (row: Record<string, any>) => Record<string, any>;
};

View File

@@ -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",

View File

@@ -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%;

View File

@@ -60,10 +60,9 @@
installNewCloudTokenListener();
initializeAppUpdates();
installCloudListeners();
refreshPublicCloudFiles();
}
refreshPublicCloudFiles();
loadedApi = loadedApiValue;
if (!loadedApi) {

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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

View File

@@ -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">

View File

@@ -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');

View File

@@ -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