Compare commits
736 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f05f4acb3 | ||
|
|
a0de1b1171 | ||
|
|
f6c90578a9 | ||
|
|
b396c8f820 | ||
|
|
8cc8f9f0b9 | ||
|
|
77ebf0051c | ||
|
|
d5cee7b35b | ||
|
|
3ac96c4ae4 | ||
|
|
60545674c5 | ||
|
|
b5c313e517 | ||
|
|
e3bdad6d77 | ||
|
|
7453afa684 | ||
|
|
86eca6bc7e | ||
|
|
2baf9a1446 | ||
|
|
296038a3de | ||
|
|
32d05edb6a | ||
|
|
ba608ff438 | ||
|
|
5d79b687d5 | ||
|
|
ad1ec70e94 | ||
|
|
7e73f81d4e | ||
|
|
9a0ae06c87 | ||
|
|
902fbbf29a | ||
|
|
a0df43484a | ||
|
|
d2317ab908 | ||
|
|
fa733e2285 | ||
|
|
134737d4a8 | ||
|
|
def3141b6d | ||
|
|
057afe2bd5 | ||
|
|
40203f2823 | ||
|
|
9d711e45f9 | ||
|
|
6ed9bb0258 | ||
|
|
e66f2fcd2d | ||
|
|
1250f5d25a | ||
|
|
bf29ee430e | ||
|
|
c7f1e75d22 | ||
|
|
0886712714 | ||
|
|
989574bb52 | ||
|
|
b99e3ed177 | ||
|
|
f7621ae336 | ||
|
|
38263de9f1 | ||
|
|
260e3c50df | ||
|
|
57327623d1 | ||
|
|
ff9f1d85ab | ||
|
|
661775d5af | ||
|
|
dd1088d02d | ||
|
|
8d265ad6d2 | ||
|
|
346c530f76 | ||
|
|
e31ff5960c | ||
|
|
3fa71cc94a | ||
|
|
f5ea87da7b | ||
|
|
643695bd2b | ||
|
|
bb870ec90f | ||
|
|
9959e61b35 | ||
|
|
92ead26873 | ||
|
|
afb27ec989 | ||
|
|
ff30b6511c | ||
|
|
56306aeaec | ||
|
|
c2c354044a | ||
|
|
ceb216df8c | ||
|
|
b2994ede8c | ||
|
|
0b87c5085c | ||
|
|
2305d086cc | ||
|
|
6c3c1b377e | ||
|
|
07c4c89720 | ||
|
|
fd30c40c5f | ||
|
|
1e59407954 | ||
|
|
d10dc960c5 | ||
|
|
8f19ce2607 | ||
|
|
af7c450f56 | ||
|
|
7467dd2f10 | ||
|
|
40440e957a | ||
|
|
26ff3f45f8 | ||
|
|
4d529e7e3f | ||
|
|
27311afb31 | ||
|
|
50b90e181a | ||
|
|
87efb92f07 | ||
|
|
6b8bf8161e | ||
|
|
6362e2137b | ||
|
|
f6c8588573 | ||
|
|
2a47f60987 | ||
|
|
bebdf3f43b | ||
|
|
2a34a3b48b | ||
|
|
9dbf720b64 | ||
|
|
05c8001c19 | ||
|
|
ddcb7711d4 | ||
|
|
0c48a5ee09 | ||
|
|
a76e742ce6 | ||
|
|
64c7072aca | ||
|
|
5d661ad3a3 | ||
|
|
e8524d3fff | ||
|
|
352b443a3e | ||
|
|
623fda7e6e | ||
|
|
42e446bc36 | ||
|
|
36e37dcec9 | ||
|
|
875011a6ea | ||
|
|
e544c12945 | ||
|
|
f7b15cc17e | ||
|
|
202873be71 | ||
|
|
fcbca318ef | ||
|
|
fe055d4b70 | ||
|
|
e480e08e0e | ||
|
|
eb78481d70 | ||
|
|
912a9d5b51 | ||
|
|
78a59ae8dc | ||
|
|
433d3be8d5 | ||
|
|
35fc2e0f5b | ||
|
|
93edcc4d0a | ||
|
|
0a06ebf9c3 | ||
|
|
94804957e5 | ||
|
|
fcaac322f2 | ||
|
|
5fa9a303cb | ||
|
|
5fa3f39f69 | ||
|
|
b13f01d3b3 | ||
|
|
b85334cb2d | ||
|
|
188c2b7a9c | ||
|
|
b63987dbfc | ||
|
|
09bf18eeca | ||
|
|
e5ec444e52 | ||
|
|
e2c313af66 | ||
|
|
73d8ad36ad | ||
|
|
8896260df7 | ||
|
|
e8433423b4 | ||
|
|
9af28e7d20 | ||
|
|
d67637c7e8 | ||
|
|
d058003cc9 | ||
|
|
1ccdf7f07e | ||
|
|
0d7d0bdd60 | ||
|
|
d7e7b97fb8 | ||
|
|
f360ba7187 | ||
|
|
84a67be272 | ||
|
|
54ddfe2ef9 | ||
|
|
3953f764a5 | ||
|
|
7ef7d9d2af | ||
|
|
f45969f5d3 | ||
|
|
9dbe73d9c3 | ||
|
|
35a48fb3d5 | ||
|
|
dd1cfa677f | ||
|
|
cb2f6e6a78 | ||
|
|
55ad3a05e4 | ||
|
|
e6a6534887 | ||
|
|
5fca73d531 | ||
|
|
081dff38e3 | ||
|
|
30f291c525 | ||
|
|
dab9d33394 | ||
|
|
e74521fdab | ||
|
|
a5ad8dc5f6 | ||
|
|
28c554a75b | ||
|
|
41e55f329c | ||
|
|
73d3d00e9d | ||
|
|
54fec7fd6d | ||
|
|
0413075af6 | ||
|
|
c733ac9c02 | ||
|
|
1970ec29c5 | ||
|
|
08fc3ffce4 | ||
|
|
b057fcfb3e | ||
|
|
67504a9481 | ||
|
|
95cb8c7cb6 | ||
|
|
183365c461 | ||
|
|
4cce1f6670 | ||
|
|
777f72af88 | ||
|
|
dacea78123 | ||
|
|
44b8a14868 | ||
|
|
aa709dbee3 | ||
|
|
b0c7adf0d1 | ||
|
|
f345677d4f | ||
|
|
cc82df92ae | ||
|
|
b151a13f65 | ||
|
|
3f7caa6078 | ||
|
|
db9133af51 | ||
|
|
bfc3717dea | ||
|
|
13f30891b6 | ||
|
|
aaf6715dd1 | ||
|
|
98b3b242eb | ||
|
|
843a080fb8 | ||
|
|
f6e2ddbf11 | ||
|
|
06593a5835 | ||
|
|
b2b6d4ad24 | ||
|
|
c4789bbb9e | ||
|
|
ae8b498300 | ||
|
|
f0475be69a | ||
|
|
db7d02de87 | ||
|
|
61a2398fb8 | ||
|
|
60464052e2 | ||
|
|
16dffba9a9 | ||
|
|
f30b062431 | ||
|
|
fea9c5dd66 | ||
|
|
0c843ea806 | ||
|
|
decfe3197d | ||
|
|
6b1243eef5 | ||
|
|
fcb87bbfc3 | ||
|
|
3f2635a421 | ||
|
|
f70c554966 | ||
|
|
9abc835d53 | ||
|
|
d5648cc944 | ||
|
|
44e0902ded | ||
|
|
cc6bcfb4b3 | ||
|
|
688086e00f | ||
|
|
09498f2ac3 | ||
|
|
7dd9e9a9b1 | ||
|
|
19d135e435 | ||
|
|
d453e52ff3 | ||
|
|
29a77cc053 | ||
|
|
11fd2d1d8a | ||
|
|
b5fe8508b1 | ||
|
|
25881e80db | ||
|
|
e43fa96e34 | ||
|
|
0200c7c78b | ||
|
|
42e573a3ae | ||
|
|
395b0a91b0 | ||
|
|
62c529cf50 | ||
|
|
00a169725e | ||
|
|
bcf0bfd5ef | ||
|
|
19f769480b | ||
|
|
65eb89de95 | ||
|
|
a6fec38d83 | ||
|
|
cfa4e50084 | ||
|
|
8e18b4f692 | ||
|
|
82a9368d01 | ||
|
|
8db86c7e02 | ||
|
|
b3c0aa1701 | ||
|
|
627ec520a5 | ||
|
|
91ae9a92af | ||
|
|
4c1c251aef | ||
|
|
9c1179c451 | ||
|
|
52ed8874e3 | ||
|
|
319e08f5f3 | ||
|
|
c4491050cd | ||
|
|
f0ea35d576 | ||
|
|
70d53e8abe | ||
|
|
8f28ce3659 | ||
|
|
050b46813f | ||
|
|
4bae23ecfa | ||
|
|
6eb16ad750 | ||
|
|
482a823f4f | ||
|
|
9d933d669a | ||
|
|
e44a95d723 | ||
|
|
cae882c8d6 | ||
|
|
026726a6ed | ||
|
|
70d06deeb0 | ||
|
|
6dfe9b798b | ||
|
|
73c14eba6d | ||
|
|
7c91dda170 | ||
|
|
40ebedaef0 | ||
|
|
614f852f71 | ||
|
|
a8e3a6cfec | ||
|
|
be053acf3c | ||
|
|
91741655b7 | ||
|
|
ef25ea1885 | ||
|
|
6b6c5b945f | ||
|
|
c57a67da09 | ||
|
|
2376cb30db | ||
|
|
3a08462018 | ||
|
|
c95677bd83 | ||
|
|
8bffa4a7dd | ||
|
|
6d7cc7d441 | ||
|
|
acc49273c1 | ||
|
|
640b53e45f | ||
|
|
7857771056 | ||
|
|
b8513b3ecd | ||
|
|
0a56e3b782 | ||
|
|
87e75c6ba1 | ||
|
|
cf5afb43eb | ||
|
|
2eb1c04fcf | ||
|
|
4a4c4b41c0 | ||
|
|
032eaf9eb0 | ||
|
|
06a028a093 | ||
|
|
21ceaecec6 | ||
|
|
c5605d63ca | ||
|
|
ca900b0cf0 | ||
|
|
7c5f274f83 | ||
|
|
f9545eaf7f | ||
|
|
216ef7736b | ||
|
|
ae7697f655 | ||
|
|
23225cf86b | ||
|
|
3a396c6555 | ||
|
|
63ad36f758 | ||
|
|
80e1563877 | ||
|
|
3f5c7aecd7 | ||
|
|
abd2492889 | ||
|
|
872468899d | ||
|
|
070ffe0c56 | ||
|
|
7a008e5a9d | ||
|
|
23940aa324 | ||
|
|
1888de8728 | ||
|
|
615397f332 | ||
|
|
e251459512 | ||
|
|
a9c8cee08a | ||
|
|
1638095c98 | ||
|
|
62cedd23b7 | ||
|
|
3d882f47a7 | ||
|
|
88ddc28208 | ||
|
|
800666f813 | ||
|
|
0b8add848a | ||
|
|
cd7edcb443 | ||
|
|
e483fd9e99 | ||
|
|
9664e6f981 | ||
|
|
d1429dd2a1 | ||
|
|
e739aed80d | ||
|
|
28e19402f3 | ||
|
|
45a065f391 | ||
|
|
67e8eb32f7 | ||
|
|
5622e3af77 | ||
|
|
7d34458553 | ||
|
|
8b747796e7 | ||
|
|
4802c36b54 | ||
|
|
988e4345d4 | ||
|
|
e02305879e | ||
|
|
8baad56315 | ||
|
|
14bbc7b057 | ||
|
|
7b6ca27b66 | ||
|
|
38aae142ea | ||
|
|
bd6c116cc0 | ||
|
|
4522c37bfa | ||
|
|
7d789d5712 | ||
|
|
c4c2274488 | ||
|
|
a8b71d452b | ||
|
|
c7d69b0fb5 | ||
|
|
47ea474555 | ||
|
|
e647ab471e | ||
|
|
fd6524867e | ||
|
|
c24cc1dc72 | ||
|
|
e3d1e4f53e | ||
|
|
7b32424143 | ||
|
|
519767fd49 | ||
|
|
505ab2e075 | ||
|
|
00d0c27502 | ||
|
|
d171d7d785 | ||
|
|
09593e0b22 | ||
|
|
771ca6ad83 | ||
|
|
83014d3a5b | ||
|
|
caa2d22dbd | ||
|
|
3c089a5b81 | ||
|
|
d1bf2dbc4b | ||
|
|
a8a9afc936 | ||
|
|
d0cbd5d0a4 | ||
|
|
67e1913683 | ||
|
|
8ff706a17f | ||
|
|
08692dc63f | ||
|
|
41d85d4117 | ||
|
|
f343d414ef | ||
|
|
6cda7b2508 | ||
|
|
9085d49d21 | ||
|
|
bb11d7e62b | ||
|
|
7524b30f50 | ||
|
|
c30724c5da | ||
|
|
1e4c108f6f | ||
|
|
72033e5830 | ||
|
|
1d24fd9942 | ||
|
|
e104feef14 | ||
|
|
ccdce6ef43 | ||
|
|
fccd550d4b | ||
|
|
3a4a10985b | ||
|
|
8ee96bd4a0 | ||
|
|
269046daa5 | ||
|
|
4738113ce3 | ||
|
|
f7a2931253 | ||
|
|
d832057076 | ||
|
|
00fdf14b6e | ||
|
|
ab26f4624a | ||
|
|
8caf5d622e | ||
|
|
0cf8fc79c2 | ||
|
|
65aa6067e1 | ||
|
|
9a2d56bfe4 | ||
|
|
a1b8e7b641 | ||
|
|
137bb7b002 | ||
|
|
e83946f35e | ||
|
|
64af838f40 | ||
|
|
0d31cc4204 | ||
|
|
73a1fce919 | ||
|
|
e8d5bdbfaf | ||
|
|
2e37af1ee4 | ||
|
|
55564ef82a | ||
|
|
2461b48244 | ||
|
|
b05f91f4cb | ||
|
|
238b6d94d1 | ||
|
|
8ee2db1bec | ||
|
|
484aa932d3 | ||
|
|
29aa59771c | ||
|
|
cef6b8520e | ||
|
|
49f8fb71e4 | ||
|
|
375a441abf | ||
|
|
0848008302 | ||
|
|
cacd6ae849 | ||
|
|
e97388e14b | ||
|
|
67b57ab756 | ||
|
|
bcf183abe2 | ||
|
|
f92df5c326 | ||
|
|
28bbf9a01e | ||
|
|
08d6f83a48 | ||
|
|
90af165afd | ||
|
|
8a4ee3e01e | ||
|
|
977818253d | ||
|
|
ec5db6d562 | ||
|
|
2d4098ff6a | ||
|
|
321d95f522 | ||
|
|
53480210d4 | ||
|
|
0b1a4ee33f | ||
|
|
477099e508 | ||
|
|
516d007c22 | ||
|
|
ab4febf938 | ||
|
|
361875d7fc | ||
|
|
c0c1f9d786 | ||
|
|
1d264ab559 | ||
|
|
553329688a | ||
|
|
585731a1b3 | ||
|
|
a6207f01af | ||
|
|
76e51343d0 | ||
|
|
6c246c9eaa | ||
|
|
479cec4209 | ||
|
|
6b85870523 | ||
|
|
a98380a941 | ||
|
|
89a3798d56 | ||
|
|
bf202719eb | ||
|
|
9d9b970fd5 | ||
|
|
56fcfd84e5 | ||
|
|
e7d575dc8e | ||
|
|
b61c454a3a | ||
|
|
a2af86c705 | ||
|
|
2594be70fc | ||
|
|
6ff63e40f0 | ||
|
|
a33f09c185 | ||
|
|
09f14a0717 | ||
|
|
9139ff0f44 | ||
|
|
f770b011ce | ||
|
|
1cc955f997 | ||
|
|
4dfaf1346e | ||
|
|
419ab985c1 | ||
|
|
c5ec22d504 | ||
|
|
5dd03484ea | ||
|
|
4d5cc119f2 | ||
|
|
446e7c139f | ||
|
|
5a6641bc6e | ||
|
|
43c00e88bb | ||
|
|
297df772a1 | ||
|
|
449bbde645 | ||
|
|
b222b916ec | ||
|
|
d3d695ed81 | ||
|
|
c1778bea26 | ||
|
|
1d401e302a | ||
|
|
12fd5f6943 | ||
|
|
817286d326 | ||
|
|
cc2c55b20f | ||
|
|
90169a7624 | ||
|
|
88b4c9daff | ||
|
|
e8b43820b9 | ||
|
|
c0f1a8f8b1 | ||
|
|
b43fe93300 | ||
|
|
3856b4e725 | ||
|
|
153a4bca42 | ||
|
|
20fccf51d9 | ||
|
|
a5d37eb528 | ||
|
|
6af21b8bae | ||
|
|
3b047dbe6d | ||
|
|
007b40bf9b | ||
|
|
0db9ae7cb1 | ||
|
|
c7e1e294ef | ||
|
|
1bf9110f4b | ||
|
|
bb3dad6e1c | ||
|
|
d3a019e8a3 | ||
|
|
48f8908040 | ||
|
|
829ec8d25b | ||
|
|
00aaaad855 | ||
|
|
c48b058b9d | ||
|
|
b553dbb6b9 | ||
|
|
2db17f9eca | ||
|
|
bcc1f91352 | ||
|
|
1c0c2bbc71 | ||
|
|
d236782795 | ||
|
|
54cf6ad411 | ||
|
|
0dac1ada5f | ||
|
|
82b63c70ed | ||
|
|
e84d231a10 | ||
|
|
362ad344d2 | ||
|
|
dcd8e8e43b | ||
|
|
d43304792a | ||
|
|
e4e01c6e1e | ||
|
|
ccb1c26905 | ||
|
|
6c2ee5ffdb | ||
|
|
6d6c360521 | ||
|
|
0459ca886b | ||
|
|
29e6dad713 | ||
|
|
c160fdb628 | ||
|
|
554be51546 | ||
|
|
ff52430e1e | ||
|
|
853eee6701 | ||
|
|
33062da66d | ||
|
|
e3fe5a2beb | ||
|
|
573e404612 | ||
|
|
91c88bd92d | ||
|
|
475f82a35e | ||
|
|
0548bae7af | ||
|
|
0413f4b4d9 | ||
|
|
9e9991c675 | ||
|
|
21502bda65 | ||
|
|
69e1c6c625 | ||
|
|
fcedeb2316 | ||
|
|
a23ff752a3 | ||
|
|
138b0414f2 | ||
|
|
87988d5c3a | ||
|
|
41cf7009b3 | ||
|
|
18860c823d | ||
|
|
55cc51d24a | ||
|
|
2a49eaab12 | ||
|
|
394c6028c9 | ||
|
|
d4bd6e03c9 | ||
|
|
1a94222262 | ||
|
|
94b41fecbc | ||
|
|
9ed6932c1e | ||
|
|
e748591c10 | ||
|
|
fca6b87cb9 | ||
|
|
c23ecfff47 | ||
|
|
8738665dcf | ||
|
|
a05fc90579 | ||
|
|
71f7a705c4 | ||
|
|
9cb2d397ad | ||
|
|
484e7c27a2 | ||
|
|
acdba0c52c | ||
|
|
943544958a | ||
|
|
e78420b1b0 | ||
|
|
69d639b9ea | ||
|
|
a540220c05 | ||
|
|
b1ddcbfbfc | ||
|
|
87aaa281e4 | ||
|
|
704733d80d | ||
|
|
a50458494e | ||
|
|
2a980a7892 | ||
|
|
a3762c6caa | ||
|
|
d6ba822338 | ||
|
|
f146d70e2b | ||
|
|
d62177d996 | ||
|
|
a1993214e2 | ||
|
|
0d806be3dc | ||
|
|
70411b764b | ||
|
|
cd0fb0fdf2 | ||
|
|
9713c9b88e | ||
|
|
d5118909d1 | ||
|
|
bb41236a5f | ||
|
|
9d84c0f213 | ||
|
|
d45fbcb8c8 | ||
|
|
4762597741 | ||
|
|
9c27c224ec | ||
|
|
2268d6126b | ||
|
|
bbc50ea3fb | ||
|
|
11985004b5 | ||
|
|
4f58d2ff80 | ||
|
|
987fe6095a | ||
|
|
d8fcbc8c17 | ||
|
|
833610c88b | ||
|
|
218478c128 | ||
|
|
3f8ff91e2c | ||
|
|
11436c065c | ||
|
|
f1c70f6f82 | ||
|
|
c66ff5820c | ||
|
|
4c87ad50b1 | ||
|
|
6c5f5a7cfb | ||
|
|
9876a76836 | ||
|
|
952bdc4baa | ||
|
|
2afa7a5f58 | ||
|
|
bc9e8a2ea6 | ||
|
|
e2dcfe9940 | ||
|
|
638b04877d | ||
|
|
2a9c67d2f6 | ||
|
|
2c9d424fc8 | ||
|
|
4e76f10175 | ||
|
|
5f2afc037e | ||
|
|
50e61cdce1 | ||
|
|
586f2fed21 | ||
|
|
020b4163d7 | ||
|
|
6b0e1e322a | ||
|
|
0d0bd29812 | ||
|
|
4e4447de8a | ||
|
|
15c9e93e8a | ||
|
|
5531705433 | ||
|
|
dfadf0653d | ||
|
|
742b68453a | ||
|
|
2716db4bf3 | ||
|
|
2d22fef06b | ||
|
|
c78aefe2ab | ||
|
|
155406827e | ||
|
|
a97e6dbcab | ||
|
|
d4989c75ca | ||
|
|
b7b9dde5ae | ||
|
|
34f2fb2a0a | ||
|
|
965a967450 | ||
|
|
40872699c6 | ||
|
|
ec90a8b952 | ||
|
|
90c4b44fdb | ||
|
|
df5062c9a5 | ||
|
|
437155e4c5 | ||
|
|
5ebee161ae | ||
|
|
10c77ad153 | ||
|
|
5e59926556 | ||
|
|
4b1b61328a | ||
|
|
4c6d9f0660 | ||
|
|
a5a7447cec | ||
|
|
dd373f9db9 | ||
|
|
bb0f5e4404 | ||
|
|
c77bc820d4 | ||
|
|
a1ab47a6f9 | ||
|
|
efc07280a6 | ||
|
|
dcb4c5071a | ||
|
|
7b625c6073 | ||
|
|
489c9a905c | ||
|
|
75c578de47 | ||
|
|
f7c4bbc708 | ||
|
|
9c1227273c | ||
|
|
47a045fc24 | ||
|
|
9e9df60d37 | ||
|
|
93b7a9a674 | ||
|
|
b96576ba6f | ||
|
|
0524b4c5b6 | ||
|
|
b7663e2e06 | ||
|
|
24f4e1d898 | ||
|
|
9089f78593 | ||
|
|
8f90dad303 | ||
|
|
c823e18699 | ||
|
|
73bfac2bfb | ||
|
|
08b5bce03c | ||
|
|
ddf8a5806c | ||
|
|
eceab2dde9 | ||
|
|
9c7df42948 | ||
|
|
321d5d71de | ||
|
|
d4a35fb414 | ||
|
|
21feb3a042 | ||
|
|
9adb4b41c6 | ||
|
|
3b3e81e3f7 | ||
|
|
dfa8ca6797 | ||
|
|
0af207d330 | ||
|
|
49337a4112 | ||
|
|
7cd26c4fe4 | ||
|
|
159ba72129 | ||
|
|
cfb772c717 | ||
|
|
500c1c76ba | ||
|
|
834eeabd3f | ||
|
|
8770034bf5 | ||
|
|
423f876d68 | ||
|
|
af54c958ba | ||
|
|
4e16119835 | ||
|
|
d6f9db48aa | ||
|
|
2063005d5c | ||
|
|
c2c54856ff | ||
|
|
cedb740fb0 | ||
|
|
913f89e970 | ||
|
|
7d6bf90a0a | ||
|
|
8a4275fb09 | ||
|
|
d5ebea3764 | ||
|
|
a93aff1bb7 | ||
|
|
c193955fbe | ||
|
|
5f97f7d922 | ||
|
|
54d17a67d4 | ||
|
|
0c94d7fcac | ||
|
|
fca23f65de | ||
|
|
a35d5525a3 | ||
|
|
904d35d26a | ||
|
|
929c08e094 | ||
|
|
97a27381f2 | ||
|
|
f4fe5b9b53 | ||
|
|
00d5b25baa | ||
|
|
c6e95dbb6a | ||
|
|
da6bd9b475 | ||
|
|
2eebe44f87 | ||
|
|
3dd99a44cb | ||
|
|
ec2acebdc9 | ||
|
|
e49c0169da | ||
|
|
49f22e1a3b | ||
|
|
423644e9d9 | ||
|
|
b64b6be68a | ||
|
|
d3c4c18b62 | ||
|
|
fe5826bc8e | ||
|
|
7709e24ccd | ||
|
|
b91cf18aee | ||
|
|
ae9c4b4f98 | ||
|
|
3d25a51cf9 | ||
|
|
18e7171038 | ||
|
|
8cf014efa4 | ||
|
|
78d71602bf | ||
|
|
dcfd6ee1dc | ||
|
|
eb4ecb4cf8 | ||
|
|
bc54564d64 | ||
|
|
1c7052810a | ||
|
|
4bfba2ec02 | ||
|
|
c2dc4d76ba | ||
|
|
ce44e271ae | ||
|
|
ef5bfb5a89 | ||
|
|
7acea0f4ac | ||
|
|
593e61abb9 | ||
|
|
a0aa508e8d | ||
|
|
ca517f9c73 | ||
|
|
ad0e02de5d | ||
|
|
ea05ae0079 | ||
|
|
689eb7baa2 | ||
|
|
a4387155e7 | ||
|
|
565a60e638 | ||
|
|
5dba5a6dfd | ||
|
|
c497c1ceca | ||
|
|
5929a01010 | ||
|
|
182b0e0a75 | ||
|
|
2cc74b594e | ||
|
|
e9430988f4 | ||
|
|
f30bd0a894 | ||
|
|
1c0a8cad56 | ||
|
|
8a4fd302d2 | ||
|
|
db7f8d6a74 | ||
|
|
4f1eb4003a | ||
|
|
3efaac7d1f | ||
|
|
d9387bef1f | ||
|
|
a101f21483 | ||
|
|
8a0d10e50d | ||
|
|
fe1fc7923f | ||
|
|
f0802dc471 | ||
|
|
30ade5867c | ||
|
|
ea54673497 | ||
|
|
2ffd729465 | ||
|
|
ef910f43a6 | ||
|
|
fc333167ac | ||
|
|
390447c948 | ||
|
|
1bb5f4974d | ||
|
|
48e3cf1be5 | ||
|
|
1e540b3fe9 | ||
|
|
60c1090d6c | ||
|
|
71bea87a7a | ||
|
|
a03261bfd4 | ||
|
|
704a04e9bb | ||
|
|
28c1421294 | ||
|
|
7a5bcc62c8 | ||
|
|
321eedefea | ||
|
|
daf9e9d18b | ||
|
|
dd7db5904c | ||
|
|
6bddf3aa83 | ||
|
|
9743569ca7 | ||
|
|
40ed020c0a | ||
|
|
63ac08cc27 | ||
|
|
e16b0ef61f | ||
|
|
14f9a40851 | ||
|
|
ba6abd1e64 | ||
|
|
4ffc5842bb |
12
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: dbgate
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: dbgate
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
29
.github/workflows/build-app-beta.yaml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Electron app
|
||||
name: Electron app BETA
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -11,8 +11,8 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-18.04, windows-2016]
|
||||
# os: [macOS-10.14, windows-2016, ubuntu-18.04]
|
||||
# os: [ubuntu-18.04, windows-2016]
|
||||
os: [macOS-10.15, windows-2016, ubuntu-18.04]
|
||||
|
||||
steps:
|
||||
- name: Context
|
||||
@@ -35,10 +35,14 @@ jobs:
|
||||
- name: fillNativeModulesElectron
|
||||
run: |
|
||||
yarn fillNativeModulesElectron
|
||||
- name: fillPackagedPlugins
|
||||
run: |
|
||||
yarn fillPackagedPlugins
|
||||
- name: Install Snapcraft
|
||||
if: matrix.os == 'ubuntu-18.04'
|
||||
uses: samuelmeuli/action-snapcraft@v1
|
||||
- name: Publish
|
||||
if: matrix.os != 'macOS-10.15'
|
||||
run: |
|
||||
yarn run build:app
|
||||
env:
|
||||
@@ -46,6 +50,13 @@ jobs:
|
||||
WIN_CSC_LINK: ${{ secrets.WINCERT_CERTIFICATE }}
|
||||
WIN_CSC_KEY_PASSWORD: ${{ secrets.WINCERT_PASSWORD }}
|
||||
|
||||
- name: Publish Mac
|
||||
if: matrix.os == 'macOS-10.15'
|
||||
run: |
|
||||
yarn run build:app:mac
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }} # token for electron publish
|
||||
|
||||
- name: Save snap login
|
||||
if: matrix.os == 'ubuntu-18.04'
|
||||
run: 'echo "$SNAPCRAFT_LOGIN" > snapcraft.login'
|
||||
@@ -63,11 +74,21 @@ jobs:
|
||||
run: |
|
||||
mkdir artifacts
|
||||
|
||||
cp app/dist/*.deb artifacts/dbgate-beta.deb || true
|
||||
cp app/dist/*x86*.AppImage artifacts/dbgate-beta.AppImage || true
|
||||
cp app/dist/*arm64*.AppImage artifacts/dbgate-beta-arm64.AppImage || true
|
||||
cp app/dist/*armv7l*.AppImage artifacts/dbgate-beta-armv7l.AppImage || true
|
||||
cp app/dist/*.exe artifacts/dbgate-beta.exe || true
|
||||
cp app/dist/*win*.zip artifacts/dbgate-windows-beta.zip || true
|
||||
cp app/dist/*-mac.dmg artifacts/dbgate-beta.dmg || true
|
||||
cp app/dist/*-mac_arm64.dmg artifacts/dbgate-beta-arm64.dmg || true
|
||||
|
||||
mv app/dist/*.exe artifacts/ || true
|
||||
mv app/dist/*.zip artifacts/ || true
|
||||
mv app/dist/*.AppImage artifacts/ || true
|
||||
mv app/dist/*.deb artifacts/ || true
|
||||
mv app/dist/*.snap artifacts/ || true
|
||||
# mv app/dist/*.dmg artifacts/ || true
|
||||
mv app/dist/*.dmg artifacts/ || true
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v1
|
||||
|
||||
16
.github/workflows/build-app.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
# os: [ubuntu-18.04, windows-2016]
|
||||
os: [macOS-10.14, windows-2016, ubuntu-18.04]
|
||||
os: [macOS-10.15, windows-2016, ubuntu-18.04]
|
||||
|
||||
steps:
|
||||
- name: Context
|
||||
@@ -39,6 +39,9 @@ jobs:
|
||||
- name: fillNativeModulesElectron
|
||||
run: |
|
||||
yarn fillNativeModulesElectron
|
||||
- name: fillPackagedPlugins
|
||||
run: |
|
||||
yarn fillPackagedPlugins
|
||||
- name: Install Snapcraft
|
||||
if: matrix.os == 'ubuntu-18.04'
|
||||
uses: samuelmeuli/action-snapcraft@v1
|
||||
@@ -72,11 +75,16 @@ jobs:
|
||||
mkdir artifacts
|
||||
|
||||
cp app/dist/*.deb artifacts/dbgate-latest.deb || true
|
||||
cp app/dist/*.AppImage artifacts/dbgate-latest.AppImage || true
|
||||
cp app/dist/*x86*.AppImage artifacts/dbgate-latest.AppImage || true
|
||||
cp app/dist/*arm64*.AppImage artifacts/dbgate-latest-arm64.AppImage || true
|
||||
cp app/dist/*armv7l*.AppImage artifacts/dbgate-latest-armv7l.AppImage || true
|
||||
cp app/dist/*.exe artifacts/dbgate-latest.exe || true
|
||||
cp app/dist/*.dmg artifacts/dbgate-latest.dmg || true
|
||||
cp app/dist/*win*.zip artifacts/dbgate-windows-latest.zip || true
|
||||
cp app/dist/*-mac.dmg artifacts/dbgate-latest.dmg || true
|
||||
cp app/dist/*-mac_arm64.dmg artifacts/dbgate-latest-arm64.dmg || true
|
||||
|
||||
mv app/dist/*.exe artifacts/ || true
|
||||
mv app/dist/*.zip artifacts/ || true
|
||||
mv app/dist/*.AppImage artifacts/ || true
|
||||
mv app/dist/*.deb artifacts/ || true
|
||||
mv app/dist/*.dmg artifacts/ || true
|
||||
@@ -118,7 +126,7 @@ jobs:
|
||||
mv app/dist/latest-linux.yml artifacts/latest-linux.yml || true
|
||||
|
||||
- name: Copy latest-mac.yml
|
||||
if: matrix.os == 'macOS-10.14'
|
||||
if: matrix.os == 'macOS-10.15'
|
||||
run: |
|
||||
mv app/dist/latest-mac.yml artifacts/latest-mac.yml || true
|
||||
|
||||
|
||||
47
.github/workflows/build-docker-beta.yaml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: Docker image BETA
|
||||
|
||||
# on: [push]
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-18.04]
|
||||
|
||||
steps:
|
||||
- name: Context
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Use Node.js 10.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10.x
|
||||
- name: yarn install
|
||||
run: |
|
||||
yarn install
|
||||
- name: setCurrentVersion
|
||||
run: |
|
||||
yarn setCurrentVersion
|
||||
- name: Prepare docker image
|
||||
run: |
|
||||
yarn run prepare:docker
|
||||
- name: Build docker image
|
||||
run: |
|
||||
docker build ./docker -t dbgate
|
||||
- name: Push docker image
|
||||
run: |
|
||||
docker tag dbgate dbgate/dbgate:beta
|
||||
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
|
||||
docker push dbgate/dbgate:beta
|
||||
1
.github/workflows/build-docker.yaml
vendored
@@ -21,7 +21,6 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-18.04]
|
||||
# os: [macOS-10.14, windows-2016, ubuntu-18.04]
|
||||
|
||||
steps:
|
||||
- name: Context
|
||||
|
||||
41
.github/workflows/build-npm.yaml
vendored
@@ -21,7 +21,6 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-18.04]
|
||||
# os: [macOS-10.14, windows-2016, ubuntu-18.04]
|
||||
|
||||
steps:
|
||||
- name: Context
|
||||
@@ -80,6 +79,11 @@ jobs:
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
- name: Publish query-splitter
|
||||
working-directory: packages/query-splitter
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
- name: Publish web
|
||||
working-directory: packages/web
|
||||
run: |
|
||||
@@ -89,3 +93,38 @@ jobs:
|
||||
working-directory: packages/dbgate
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
- name: Publish dbgate-plugin-csv
|
||||
working-directory: plugins/dbgate-plugin-csv
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
- name: Publish dbgate-plugin-excel
|
||||
working-directory: plugins/dbgate-plugin-excel
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
- name: Publish dbgate-plugin-mssql
|
||||
working-directory: plugins/dbgate-plugin-mssql
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
- name: Publish dbgate-plugin-mysql
|
||||
working-directory: plugins/dbgate-plugin-mysql
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
- name: Publish dbgate-plugin-mongo
|
||||
working-directory: plugins/dbgate-plugin-mongo
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
- name: Publish dbgate-plugin-postgres
|
||||
working-directory: plugins/dbgate-plugin-postgres
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
- name: Publish dbgate-plugin-sqlite
|
||||
working-directory: plugins/dbgate-plugin-sqlite
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
81
.github/workflows/run-tests.yaml
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
name: Run tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
test-runner:
|
||||
runs-on: ubuntu-latest
|
||||
container: node:10.18-jessie
|
||||
|
||||
steps:
|
||||
- name: Context
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: yarn install
|
||||
run: |
|
||||
yarn install
|
||||
- name: Integration tests
|
||||
run: |
|
||||
cd integration-tests
|
||||
yarn test:ci
|
||||
# yarn wait:ci
|
||||
- name: Filter parser tests
|
||||
if: always()
|
||||
run: |
|
||||
cd packages/filterparser
|
||||
yarn test:ci
|
||||
- name: Query spliiter tests
|
||||
if: always()
|
||||
run: |
|
||||
cd packages/query-splitter
|
||||
yarn test:ci
|
||||
- uses: tanmen/jest-reporter@v1
|
||||
if: always()
|
||||
with:
|
||||
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 }}
|
||||
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 }}
|
||||
result-file: packages/query-splitter/result.json
|
||||
action-name: Query splitter test results
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_PASSWORD: Pwd2020Db
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
mysql:
|
||||
image: mysql:8.0.18
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: Pwd2020Db
|
||||
|
||||
mssql:
|
||||
image: mcr.microsoft.com/mssql/server
|
||||
env:
|
||||
ACCEPT_EULA: Y
|
||||
SA_PASSWORD: Pwd2020Db
|
||||
MSSQL_PID: Express
|
||||
|
||||
# cockroachdb:
|
||||
# image: cockroachdb/cockroach
|
||||
8
.gitignore
vendored
@@ -12,6 +12,12 @@ node_modules
|
||||
build
|
||||
dist
|
||||
|
||||
app/packages/web/public
|
||||
app/packages/plugins
|
||||
docker/public
|
||||
docker/bundle.js
|
||||
docker/plugins
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
@@ -24,3 +30,5 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
app/src/nativeModulesContent.js
|
||||
packages/api/src/nativeModulesContent.js
|
||||
packages/api/src/packagedPluginsContent.js
|
||||
.VSCodeCounter
|
||||
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"jestrunner.jestCommand": "node_modules/.bin/cross-env DEVMODE=1 LOCALTEST=1 node_modules/.bin/jest"
|
||||
}
|
||||
98
CHANGELOG.md
@@ -1,5 +1,103 @@
|
||||
# ChangeLog
|
||||
|
||||
### 4.2.6
|
||||
- Fixed MongoDB import
|
||||
- Configurable thousands separator #136
|
||||
- Using case insensitive text search in postgres
|
||||
|
||||
### 4.2.5
|
||||
- FIXED: Fixed crash when using large model on some installations
|
||||
- FIXED: Postgre SQL CREATE function
|
||||
- FIXED: Analysing of MySQL when modifyDate is not known
|
||||
|
||||
### 4.2.4
|
||||
- ADDED: Query history
|
||||
- ADDED: One-click exports in desktop app
|
||||
- ADDED: JSON array export
|
||||
- FIXED: Procedures in PostgreSQL #122
|
||||
- ADDED: Support of materialized views for PostgreSQL #123
|
||||
- ADDED: Integration tests
|
||||
- FIXED: Fixes in DB structure analysis in PostgreSQL, SQLite, MySQL
|
||||
- FIXED: Save data in SQLite, PostgreSQL
|
||||
- CHANGED: Introduced package dbgate-query-splitter, instead of sql-query-identifier and @verycrazydog/mysql-parse
|
||||
|
||||
### 4.2.3
|
||||
- ADDED: ARM builds for MacOS and Linux
|
||||
- ADDED: Filter by columns in form view
|
||||
|
||||
### 4.2.2
|
||||
- CHANGED: Further startup optimalization (approx. 2 times quicker start of electron app)
|
||||
|
||||
### 4.2.1
|
||||
- FIXED: Fixed+optimalized app startup (esp. on Windows)
|
||||
|
||||
### 4.2.0
|
||||
- ADDED: Support of SQLite database
|
||||
- ADDED: Support of Amazon Redshift database
|
||||
- ADDED: Support of CockcroachDB
|
||||
- CHANGED: DB Model is not auto-refreshed by default, refresh could be invoked from statusbar
|
||||
- FIXED: Fixed race conditions on startup
|
||||
- FIXED: Fixed broken style in data grid under strange circumstances
|
||||
- ADDED: Configure connections with commandline arguments #108
|
||||
- CHANGED: Optimalized algorithm of incremental DB model updates
|
||||
- CHANGED: Loading queries from PostgreSQL doesn't need cursors, using streamed query instead
|
||||
- ADDED: Disconnect command
|
||||
- ADDED: Query executed on server has tab marker (formerly it had only "No DB" marker)
|
||||
- ADDED: Horizontal scroll using shift+mouse wheel #113
|
||||
- ADDED: Cosmetic improvements of MariaDB support
|
||||
|
||||
### 4.1.11
|
||||
- FIX: Fixed crash of API process when using SSH tunnel connection (race condition)
|
||||
|
||||
### 4.1.11
|
||||
- FIX: fixed processing postgre query containing $$
|
||||
- FIX: fixed postgre analysing procedures & functions
|
||||
- FIX: patched svelte crash #105
|
||||
- ADDED: ability to disbale background DB model updates
|
||||
- ADDED: Duplicate connection
|
||||
- ADDED: Duplicate tab
|
||||
- FIX: SSH tunnel connection using keyfile auth #106
|
||||
- FIX: All tables button fix in export #109
|
||||
- CHANGED: Add to favorites moved from toolbar to tab context menu
|
||||
- CHANGED: Toolbar design - current tab related commands are delimited
|
||||
|
||||
### 4.1.10
|
||||
- ADDED: Default database option in connectin settings #96 #92
|
||||
- FIX: Bundle size optimalization for Windows #97
|
||||
- FIX: Popup menu placement on smaller displays #94
|
||||
- ADDED: Browse table data with SQL Server 2008 #93
|
||||
- FIX: Prevented malicious origins / DNS rebinding #91
|
||||
- ADDED: Handle JSON fields in data editor (eg. jsonb field in Postgres) #90
|
||||
- FIX: Fixed crash on Windows with Hyper-V #86
|
||||
- ADDED: Show database server version in status bar
|
||||
- ADDED: Show detailed info about error, when connect to database fails
|
||||
- ADDED: Portable ZIP distribution for Windows #84
|
||||
### 4.1.9
|
||||
- FIX: Incorrect row count info in query result #83
|
||||
|
||||
### 4.1.1
|
||||
- CHANGED: Default plugins are now part of installation
|
||||
### 4.1.0
|
||||
- ADDED: MongoDB support
|
||||
- ADDED: Configurable keyboard shortcuts
|
||||
- ADDED: JSON row cell data view
|
||||
- FIX: Fixed some problems from migration to Svelte
|
||||
|
||||
### 4.0.3
|
||||
- FIX: fixes for FireFox (mainly incorrent handle of bind:clientHeight, replaces with resizeobserver)
|
||||
### 4.0.2
|
||||
- FIX: fixed docker and NPM build
|
||||
### 4.0.0
|
||||
- CHANGED: Excahnged React with Svelte. Changed theme colors. Huge speed and memory optimalization
|
||||
- ADDED: SQL Generator (CREATE, INSERT, DROP)
|
||||
- ADDED: Command palette (F1). Introduced commands, extended some context menus
|
||||
- ADDED: New keyboard shortcuts
|
||||
- ADDED: Switch to recent database feature
|
||||
- ADDED: Macros from free table editor are available also in table data editor
|
||||
- CHANGED: Cell data preview is now in left widgets panel
|
||||
- CHANGED: Toolbar refactor
|
||||
- FIX: Solved reconnecting expired connection
|
||||
|
||||
### 3.9.6
|
||||
- ADDED: Connect using SSH Tunnel
|
||||
- ADDED: Connect using SSL
|
||||
|
||||
55
README.md
@@ -1,36 +1,56 @@
|
||||
[](https://www.npmjs.com/package/dbgate)
|
||||

|
||||
[](https://snapcraft.io/dbgate)
|
||||
[](https://snapcraft.io/dbgate)
|
||||
[](https://github.com/prettier/prettier)
|
||||
[](https://paypal.me/JanProchazkaCz/30eur)
|
||||
|
||||
# DbGate - database administration tool
|
||||
|
||||
DbGate is fast and easy to use database manager. Works with MySQL, PostgreSQL and SQL Server.
|
||||
DbGate modern, fast and easy to use database manager
|
||||
|
||||
* Try it online - [demo.dbgate.org](https://demo.dbgate.org) - online demo application
|
||||
* Download application for Windows, Linux or Mac from [dbgate.org](https://dbgate.org/download/)
|
||||
* Run web version as [NPM package](https://www.npmjs.com/package/dbgate) or as [docker image](https://hub.docker.com/r/dbgate/dbgate)
|
||||
|
||||
Supported databases:
|
||||
* MySQL
|
||||
* PostgreSQL
|
||||
* SQL Server
|
||||
* MongoDB
|
||||
* SQLite
|
||||
* Amazon Redshift
|
||||
* CockroachDB
|
||||
* MariaDB
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
* Connect to Microsoft SQL Server, Postgre SQL, MySQL
|
||||
* Table data editing, with SQL change script preview
|
||||
* Light and dark theme
|
||||
* Master/detail views
|
||||
* Query designer
|
||||
* Form view for comfortable work with tables with many columns
|
||||
* Explore tables, views, procedures, functions
|
||||
* SQL editor, execute SQL script, SQL code formatter, SQL code completion, SQL join wizard
|
||||
* JSON view on MongoDB collections
|
||||
* Explore tables, views, procedures, functions, MongoDB collections
|
||||
* SQL editor
|
||||
* execute SQL script
|
||||
* SQL code formatter
|
||||
* SQL code completion
|
||||
* Add SQL LEFT/INNER/RIGHT join utility
|
||||
* Mongo JavaScript editor, execute Mongo script (with NodeJs syntax)
|
||||
* Runs as application for Windows, Linux and Mac. Or in Docker container on server and in web Browser on client.
|
||||
* Import, export from/to CSV, Excel, JSON
|
||||
* Free table editor - quick table data editing (cleanup data after import/before export, prototype tables etc.)
|
||||
* Archives - backup your data in JSON files on local filesystem (or on DbGate server, when using web application)
|
||||
* Light and dark theme
|
||||
* Charts
|
||||
* For detailed info, how to run DbGate in docker container, visit [docker hub](https://hub.docker.com/r/dbgate/dbgate)
|
||||
* Extensible plugin architecture
|
||||
|
||||
## How to contribute
|
||||
Any contributions are welcome. If you want to contribute without coding, consider following:
|
||||
* 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.
|
||||
* Tell your friends about DbGate or share on social networks - when more people will use DbGate, it will grow to be better
|
||||
|
||||
## Why is DbGate different
|
||||
There are many database managers now, so why DbGate?
|
||||
* Works everywhere - Windows, Linux, Mac, Web browser (+mobile web is planned), without compromises in features
|
||||
@@ -40,12 +60,11 @@ There are many database managers now, so why DbGate?
|
||||
## Design goals
|
||||
* Application simplicity - DbGate takes the best and only the best from old [DbGate](http://www.jenasoft.com/dbgate), [DatAdmin](http://www.jenasoft.com/datadmin) and [DbMouse](http://www.jenasoft.com/dbmouse) .
|
||||
* Minimal dependencies
|
||||
* Frontend - React, styled-components, socket.io
|
||||
* Frontend - Svelte, socket.io
|
||||
* Backend - NodeJs, ExpressJs, socket.io, database connection drivers
|
||||
* JavaScript + TypeScript
|
||||
* App - electron
|
||||
* There is plan to incorporate SQLite to support work with local datasets
|
||||
* Platform independed - will run as web application in single docker container on server, or as application using Electron platform on Linux, Windows and Mac
|
||||
* Platform independent - will run as web application in single docker container on server, or as application using Electron platform on Linux, Windows and Mac
|
||||
|
||||
## Plugins
|
||||
Plugins are standard NPM packages published on [npmjs.com](https://www.npmjs.com).
|
||||
@@ -56,6 +75,8 @@ Currently following extensions can be implemented using plugins:
|
||||
- File format parsers/writers
|
||||
- Database engine connectors
|
||||
|
||||
Basic set of plugins is part of DbGate git repository and is installed with app. Additional plugins pust be downloaded from NPM (this task is handled by DbGate)
|
||||
|
||||
## How to run development environment
|
||||
|
||||
```sh
|
||||
@@ -63,7 +84,7 @@ yarn
|
||||
yarn start
|
||||
```
|
||||
|
||||
If you want to make modifications in TypeScript packages, run TypeScript compiler in watch mode in seconds terminal:
|
||||
If you want to make modifications in libraries or plugins, run library compiler in watch mode in the second terminal:
|
||||
```sh
|
||||
yarn lib
|
||||
```
|
||||
@@ -78,7 +99,7 @@ yarn start
|
||||
```
|
||||
|
||||
## How to run built electron app locally
|
||||
This mode is very similar to production run of electron app. Electron app forks process with API on dynamically allocated port, works with compiled javascript files (doesn't use localhost:5000)
|
||||
This mode is very similar to production run of electron app. Electron app forks process with API on dynamically allocated port, works with compiled javascript files and uses compiled version of plugins (doesn't use localhost:5000)
|
||||
|
||||
```sh
|
||||
cd app
|
||||
@@ -91,15 +112,3 @@ yarn build:app:local
|
||||
yarn start:app:local
|
||||
```
|
||||
|
||||
## Packages
|
||||
Some dbgate packages can be used also without DbGate. You can find them on [NPM repository](https://www.npmjs.com/search?q=keywords:dbgate)
|
||||
|
||||
* [api](https://github.com/dbgate/dbgate/tree/master/packages/api) - backend, Javascript, ExpressJS [](https://www.npmjs.com/package/dbgate-api)
|
||||
* [datalib](https://github.com/dbgate/dbgate/tree/master/packages/datalib) - TypeScript library for utility classes [](https://www.npmjs.com/package/dbgate-datalib)
|
||||
* [app](https://github.com/dbgate/dbgate/tree/master/app) - application (JavaScript) structure, creating specific queries (JavaScript)
|
||||
* [filterparser](https://github.com/dbgate/dbgate/tree/master/packages/filterparser) - TypeScript library for parsing data filter expressions using parsimmon [](https://www.npmjs.com/package/dbgate-filterparser)
|
||||
* [sqltree](https://github.com/dbgate/dbgate/tree/master/packages/sqltree) - JSON representation of SQL query, functions converting to SQL (TypeScript) [](https://www.npmjs.com/package/dbgate-sqltree)
|
||||
* [types](https://github.com/dbgate/dbgate/tree/master/packages/types) - common TypeScript definitions [](https://www.npmjs.com/package/dbgate-types)
|
||||
* [web](https://github.com/dbgate/dbgate/tree/master/packages/web) - frontend in React (JavaScript) [](https://www.npmjs.com/package/dbgate-web)
|
||||
* [tools](https://github.com/dbgate/dbgate/tree/master/packages/tools) - various tools [](https://www.npmjs.com/package/dbgate-tools)
|
||||
|
||||
|
||||
BIN
app/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
app/icons/16x16.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/icons/256x256.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
app/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/icons/48x48.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
app/icons/512x512.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
app/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
@@ -1,41 +1,60 @@
|
||||
{
|
||||
"name": "dbgate",
|
||||
"version": "3.9.5",
|
||||
"version": "4.1.1",
|
||||
"private": true,
|
||||
"author": "Jan Prochazka <jenasoft.database@gmail.com>",
|
||||
"description": "Opensource database administration tool",
|
||||
"dependencies": {
|
||||
"better-sqlite3-with-prebuilds": "^7.1.8",
|
||||
"electron-log": "^4.3.1",
|
||||
"electron-store": "^5.1.1",
|
||||
"electron-updater": "^4.3.5"
|
||||
"electron-updater": "^4.3.5",
|
||||
"patch-package": "^6.4.7"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"build": {
|
||||
"artifactName": "${productName}-${version}-${os}_${arch}.${ext}",
|
||||
"appId": "org.dbgate",
|
||||
"mac": {
|
||||
"category": "database",
|
||||
"icon": "icon512.png",
|
||||
"artifactName": "dbgate-mac-${version}.${ext}",
|
||||
"publish": [
|
||||
"github"
|
||||
],
|
||||
"target": {
|
||||
"target": "default",
|
||||
"arch": [
|
||||
"arm64",
|
||||
"x64"
|
||||
]
|
||||
}
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
"deb",
|
||||
"snap",
|
||||
{
|
||||
"target": "AppImage",
|
||||
"arch": [
|
||||
"x64",
|
||||
"armv7l",
|
||||
"arm64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"icon": "icons/",
|
||||
"category": "Development",
|
||||
"synopsis": "Database manager for SQL Server, MySQL, PostgreSQL, MongoDB and SQLite",
|
||||
"publish": [
|
||||
"github"
|
||||
]
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
"AppImage",
|
||||
"deb",
|
||||
"snap"
|
||||
],
|
||||
"icon": "icon.png",
|
||||
"artifactName": "dbgate-linux-${version}.${ext}",
|
||||
"category": "Development",
|
||||
"synopsis": "Database administration tool for MS SQL, MySQL and PostgreSQL",
|
||||
"publish": [
|
||||
"github"
|
||||
]
|
||||
"appImage": {
|
||||
"license": "./LICENSE",
|
||||
"category": "Development"
|
||||
},
|
||||
"snap": {
|
||||
"publish": [
|
||||
@@ -48,9 +67,9 @@
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
"nsis"
|
||||
"nsis",
|
||||
"zip"
|
||||
],
|
||||
"artifactName": "dbgate-windows-${version}.${ext}",
|
||||
"icon": "icon.ico",
|
||||
"publish": [
|
||||
"github"
|
||||
@@ -68,19 +87,20 @@
|
||||
"start": "cross-env ELECTRON_START_URL=http://localhost:5000 electron .",
|
||||
"start:local": "cross-env electron .",
|
||||
"dist": "electron-builder",
|
||||
"build": "cd ../packages/api && yarn build && cd ../web && yarn build:app && cd ../../app && yarn dist",
|
||||
"build:local": "cd ../packages/api && yarn build && cd ../web && yarn build:app && cd ../../app && yarn predist",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"predist": "copyfiles ../packages/api/dist/* packages && copyfiles \"../packages/web/build/*\" packages && copyfiles \"../packages/web/build/**/*\" packages"
|
||||
"build": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && yarn dist",
|
||||
"build:mac": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && node setMacPlatform x64 && yarn dist && node setMacPlatform arm64 && yarn dist",
|
||||
"build:local": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && yarn predist",
|
||||
"postinstall": "electron-builder install-app-deps && patch-package",
|
||||
"predist": "copyfiles ../packages/api/dist/* packages && copyfiles \"../packages/web/public/*\" packages && copyfiles \"../packages/web/public/**/*\" packages && copyfiles --up 3 \"../plugins/dist/**/*\" packages/plugins"
|
||||
},
|
||||
"main": "src/electron.js",
|
||||
"devDependencies": {
|
||||
"copyfiles": "^2.2.0",
|
||||
"cross-env": "^6.0.3",
|
||||
"electron": "11.2.3",
|
||||
"electron-builder": "22.9.1"
|
||||
"electron-builder": "22.10.5"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"msnodesqlv8": "^2.0.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
8
app/setMacPlatform.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const fs = require('fs');
|
||||
|
||||
const text = fs.readFileSync('package.json', { encoding: 'utf-8' });
|
||||
const json = JSON.parse(text);
|
||||
|
||||
json.build.mac.target.arch = process.argv[2];
|
||||
|
||||
fs.writeFileSync('package.json', JSON.stringify(json, null, 2), { encoding: 'utf-8' });
|
||||
@@ -19,7 +19,6 @@ const store = new Store();
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
let mainWindow;
|
||||
let splashWindow;
|
||||
let mainMenu;
|
||||
|
||||
log.transports.file.level = 'debug';
|
||||
@@ -27,12 +26,19 @@ autoUpdater.logger = log;
|
||||
// TODO - create settings for this
|
||||
// appUpdater.channel = 'beta';
|
||||
|
||||
function hideSplash() {
|
||||
if (splashWindow) {
|
||||
splashWindow.destroy();
|
||||
splashWindow = null;
|
||||
}
|
||||
mainWindow.show();
|
||||
let commands = {};
|
||||
|
||||
function commandItem(id) {
|
||||
const command = commands[id];
|
||||
return {
|
||||
id,
|
||||
label: command ? command.menuName || command.toolbarName || command.name : id,
|
||||
accelerator: command ? command.keyText : undefined,
|
||||
enabled: command ? command.enabled : false,
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_runCommand('${id}')`);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildMenu() {
|
||||
@@ -40,56 +46,17 @@ function buildMenu() {
|
||||
{
|
||||
label: 'File',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Connect to database',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_createNewConnection()`);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Open file',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_openFile()`);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Save',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_tabCommand('save')`);
|
||||
},
|
||||
accelerator: 'Ctrl+S',
|
||||
id: 'save',
|
||||
},
|
||||
{
|
||||
label: 'Save As',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_tabCommand('saveAs')`);
|
||||
},
|
||||
accelerator: 'Ctrl+Shift+S',
|
||||
id: 'saveAs',
|
||||
},
|
||||
commandItem('new.connection'),
|
||||
commandItem('file.open'),
|
||||
commandItem('group.save'),
|
||||
commandItem('group.saveAs'),
|
||||
{ type: 'separator' },
|
||||
{ role: 'close' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Window',
|
||||
submenu: [
|
||||
{
|
||||
label: 'New query',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_newQuery()`);
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Close all tabs',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript('dbgate_closeAll()');
|
||||
},
|
||||
},
|
||||
{ role: 'minimize' },
|
||||
],
|
||||
submenu: [commandItem('new.query'), { type: 'separator' }, commandItem('tabs.closeAll'), { role: 'minimize' }],
|
||||
},
|
||||
|
||||
// {
|
||||
@@ -115,6 +82,7 @@ function buildMenu() {
|
||||
{ role: 'zoomout' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'togglefullscreen' },
|
||||
commandItem('theme.changeTheme'),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -144,12 +112,7 @@ function buildMenu() {
|
||||
require('electron').shell.openExternal('https://github.com/dbgate/dbgate/issues/new');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'About',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_showAbout()`);
|
||||
},
|
||||
},
|
||||
commandItem('about.show'),
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -157,10 +120,22 @@ function buildMenu() {
|
||||
return Menu.buildFromTemplate(template);
|
||||
}
|
||||
|
||||
ipcMain.on('update-menu', async (event, arg) => {
|
||||
const commands = await mainWindow.webContents.executeJavaScript(`dbgate_getCurrentTabCommands()`);
|
||||
mainMenu.getMenuItemById('save').enabled = !!commands.save;
|
||||
mainMenu.getMenuItemById('saveAs').enabled = !!commands.saveAs;
|
||||
ipcMain.on('update-commands', async (event, arg) => {
|
||||
commands = JSON.parse(arg);
|
||||
for (const key of Object.keys(commands)) {
|
||||
const menu = mainMenu.getMenuItemById(key);
|
||||
if (!menu) continue;
|
||||
const command = commands[key];
|
||||
|
||||
// rebuild menu
|
||||
if (menu.label != command.text || menu.accelerator != command.keyText) {
|
||||
mainMenu = buildMenu();
|
||||
mainWindow.setMenu(mainMenu);
|
||||
return;
|
||||
}
|
||||
|
||||
menu.enabled = command.enabled;
|
||||
}
|
||||
});
|
||||
|
||||
function createWindow() {
|
||||
@@ -172,7 +147,6 @@ function createWindow() {
|
||||
title: 'DbGate',
|
||||
...bounds,
|
||||
icon: os.platform() == 'win32' ? 'icon.ico' : path.resolve(__dirname, '../icon.png'),
|
||||
show: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true,
|
||||
@@ -186,12 +160,12 @@ function createWindow() {
|
||||
const startUrl =
|
||||
process.env.ELECTRON_START_URL ||
|
||||
url.format({
|
||||
pathname: path.join(__dirname, '../packages/web/build/index.html'),
|
||||
pathname: path.join(__dirname, '../packages/web/public/index.html'),
|
||||
protocol: 'file:',
|
||||
slashes: true,
|
||||
});
|
||||
mainWindow.webContents.on('did-finish-load', function () {
|
||||
hideSplash();
|
||||
// hideSplash();
|
||||
});
|
||||
mainWindow.on('close', () => {
|
||||
store.set('winBounds', mainWindow.getBounds());
|
||||
@@ -202,33 +176,21 @@ function createWindow() {
|
||||
}
|
||||
}
|
||||
|
||||
splashWindow = new BrowserWindow({
|
||||
width: 300,
|
||||
height: 120,
|
||||
transparent: true,
|
||||
frame: false,
|
||||
});
|
||||
splashWindow.loadURL(
|
||||
url.format({
|
||||
pathname: path.join(__dirname, '../packages/web/build/splash.html'),
|
||||
protocol: 'file:',
|
||||
slashes: true,
|
||||
})
|
||||
);
|
||||
|
||||
if (process.env.ELECTRON_START_URL) {
|
||||
loadMainWindow();
|
||||
} else {
|
||||
const apiProcess = fork(path.join(__dirname, '../packages/api/dist/bundle.js'), [
|
||||
'--dynport',
|
||||
'--is-electron-bundle',
|
||||
'--native-modules',
|
||||
path.join(__dirname, 'nativeModules'),
|
||||
// '../../../src/nativeModules'
|
||||
]);
|
||||
apiProcess.on('message', msg => {
|
||||
if (msg.msgtype == 'listening') {
|
||||
const { port } = msg;
|
||||
const { port, authorization } = msg;
|
||||
global['port'] = port;
|
||||
global['authorization'] = authorization;
|
||||
loadMainWindow();
|
||||
}
|
||||
});
|
||||
|
||||
961
app/yarn.lock
@@ -2,10 +2,10 @@ const fs = require('fs');
|
||||
|
||||
let fillContent = '';
|
||||
|
||||
// if (!process.argv.includes('--electron')) {
|
||||
if (process.platform == 'win32') {
|
||||
fillContent += `content.msnodesqlv8 = () => require('msnodesqlv8');`;
|
||||
}
|
||||
fillContent += `content['better-sqlite3-with-prebuilds'] = () => require('better-sqlite3-with-prebuilds');`;
|
||||
|
||||
const getContent = (empty) => `
|
||||
// this file is generated automatically by script fillNativeModules.js, do not edit it manually
|
||||
|
||||
23
fillPackagedPlugins.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function load() {
|
||||
const plugins = {};
|
||||
|
||||
for (const packageName of fs.readdirSync('plugins')) {
|
||||
if (!packageName.startsWith('dbgate-plugin-')) continue;
|
||||
const dir = path.join('plugins', packageName);
|
||||
const frontend = fs.readFileSync(path.join(dir, 'dist', 'frontend.js'), 'utf-8');
|
||||
const readme = fs.readFileSync(path.join(dir, 'README.md'), 'utf-8');
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8'));
|
||||
plugins[packageName] = {
|
||||
manifest,
|
||||
frontend,
|
||||
readme,
|
||||
};
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
||||
fs.writeFileSync('packages/api/src/packagedPluginsContent.js', `module.exports = () => (${JSON.stringify(load())});`);
|
||||
1
integration-tests/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
dbtemp
|
||||
89
integration-tests/__tests__/object-analyse.spec.js
Normal file
@@ -0,0 +1,89 @@
|
||||
const { testWrapper } = require('../tools');
|
||||
const engines = require('../engines');
|
||||
const _ = require('lodash');
|
||||
|
||||
const initSql = ['CREATE TABLE t1 (id int)', 'CREATE TABLE t2 (id int)'];
|
||||
|
||||
function flatSource() {
|
||||
return _.flatten(
|
||||
engines.map(engine => (engine.objects || []).map(object => [engine.label, object.type, object, engine]))
|
||||
);
|
||||
}
|
||||
|
||||
const obj1Match = expect.objectContaining({
|
||||
pureName: 'obj1',
|
||||
});
|
||||
const view1Match = expect.objectContaining({
|
||||
pureName: 'obj1',
|
||||
columns: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
columnName: 'id',
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
describe('Object analyse', () => {
|
||||
test.each(flatSource())(
|
||||
'Full analysis - %s - %s',
|
||||
testWrapper(async (conn, driver, type, object, engine) => {
|
||||
for (const sql of initSql) await driver.query(conn, sql);
|
||||
|
||||
await driver.query(conn, object.create1);
|
||||
const structure = await driver.analyseFull(conn);
|
||||
|
||||
expect(structure[type].length).toEqual(1);
|
||||
expect(structure[type][0]).toEqual(type.includes('views') ? view1Match : obj1Match);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(flatSource())(
|
||||
'Incremental analysis - add - %s - %s',
|
||||
testWrapper(async (conn, driver, type, object, engine) => {
|
||||
for (const sql of initSql) await driver.query(conn, sql);
|
||||
|
||||
await driver.query(conn, object.create2);
|
||||
const structure1 = await driver.analyseFull(conn);
|
||||
await driver.query(conn, object.create1);
|
||||
const structure2 = await driver.analyseIncremental(conn, structure1);
|
||||
|
||||
expect(structure2[type].length).toEqual(2);
|
||||
expect(structure2[type].find(x => x.pureName == 'obj1')).toEqual(type.includes('views') ? view1Match : obj1Match);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(flatSource())(
|
||||
'Incremental analysis - drop - %s - %s',
|
||||
testWrapper(async (conn, driver, type, object, engine) => {
|
||||
for (const sql of initSql) await driver.query(conn, sql);
|
||||
|
||||
await driver.query(conn, object.create1);
|
||||
await driver.query(conn, object.create2);
|
||||
const structure1 = await driver.analyseFull(conn);
|
||||
await driver.query(conn, object.drop2);
|
||||
const structure2 = await driver.analyseIncremental(conn, structure1);
|
||||
|
||||
expect(structure2[type].length).toEqual(1);
|
||||
expect(structure2[type][0]).toEqual(type.includes('views') ? view1Match : obj1Match);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(flatSource())(
|
||||
'Create SQL - add - %s - %s',
|
||||
testWrapper(async (conn, driver, type, object, engine) => {
|
||||
for (const sql of initSql) await driver.query(conn, sql);
|
||||
|
||||
await driver.query(conn, object.create1);
|
||||
const structure1 = await driver.analyseFull(conn);
|
||||
await driver.query(conn, object.drop1);
|
||||
const structure2 = await driver.analyseIncremental(conn, structure1);
|
||||
expect(structure2[type].length).toEqual(0);
|
||||
|
||||
await driver.query(conn, structure1[type][0].createSql);
|
||||
|
||||
const structure3 = await driver.analyseIncremental(conn, structure2);
|
||||
|
||||
expect(structure3[type].length).toEqual(1);
|
||||
expect(structure3[type][0]).toEqual(type.includes('views') ? view1Match : obj1Match);
|
||||
})
|
||||
);
|
||||
});
|
||||
163
integration-tests/__tests__/query.spec.js
Normal file
@@ -0,0 +1,163 @@
|
||||
const engines = require('../engines');
|
||||
const { splitQuery } = require('dbgate-query-splitter');
|
||||
const { testWrapper } = require('../tools');
|
||||
|
||||
const initSql = ['CREATE TABLE t1 (id int)', 'INSERT INTO t1 (id) VALUES (1)', 'INSERT INTO t1 (id) VALUES (2)'];
|
||||
|
||||
expect.extend({
|
||||
dataRow(row, expected) {
|
||||
for (const key in expected) {
|
||||
if (row[key] != expected[key]) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Different key: ${key}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
pass: true,
|
||||
message: () => '',
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
class StreamHandler {
|
||||
constructor(resolve) {
|
||||
this.results = [];
|
||||
this.resolve = resolve;
|
||||
this.infoRows = [];
|
||||
}
|
||||
row(row) {
|
||||
this.results[this.results.length - 1].rows.push(row);
|
||||
}
|
||||
recordset(columns) {
|
||||
this.results.push({
|
||||
columns,
|
||||
rows: [],
|
||||
});
|
||||
}
|
||||
done(result) {
|
||||
this.resolve(this.results);
|
||||
}
|
||||
info(msg) {
|
||||
this.infoRows.push(msg);
|
||||
}
|
||||
}
|
||||
|
||||
function executeStreamItem(driver, conn, sql) {
|
||||
return new Promise(resolve => {
|
||||
const handler = new StreamHandler(resolve);
|
||||
driver.stream(conn, sql, handler);
|
||||
});
|
||||
}
|
||||
|
||||
async function executeStream(driver, conn, sql) {
|
||||
const results = [];
|
||||
for (const sqlItem of splitQuery(sql, driver.getQuerySplitterOptions('stream'))) {
|
||||
const item = await executeStreamItem(driver, conn, sqlItem);
|
||||
results.push(...item);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
describe('Query', () => {
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Simple query - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
for (const sql of initSql) await driver.query(conn, sql);
|
||||
|
||||
const res = await driver.query(conn, 'SELECT id FROM t1 ORDER BY id');
|
||||
expect(res.columns).toEqual([
|
||||
expect.objectContaining({
|
||||
columnName: 'id',
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(res.rows).toEqual([
|
||||
expect.dataRow({
|
||||
id: 1,
|
||||
}),
|
||||
expect.dataRow({
|
||||
id: 2,
|
||||
}),
|
||||
]);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Simple stream query - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
for (const sql of initSql) await driver.query(conn, sql);
|
||||
const results = await executeStream(driver, conn, 'SELECT id FROM t1 ORDER BY id');
|
||||
expect(results.length).toEqual(1);
|
||||
const res = results[0];
|
||||
|
||||
expect(res.columns).toEqual([expect.objectContaining({ columnName: 'id' })]);
|
||||
expect(res.rows).toEqual([expect.dataRow({ id: 1 }), expect.dataRow({ id: 2 })]);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'More queries - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
for (const sql of initSql) await driver.query(conn, sql);
|
||||
const results = await executeStream(
|
||||
driver,
|
||||
conn,
|
||||
'SELECT id FROM t1 ORDER BY id; SELECT id FROM t1 ORDER BY id DESC'
|
||||
);
|
||||
expect(results.length).toEqual(2);
|
||||
|
||||
const res1 = results[0];
|
||||
expect(res1.columns).toEqual([expect.objectContaining({ columnName: 'id' })]);
|
||||
expect(res1.rows).toEqual([expect.dataRow({ id: 1 }), expect.dataRow({ id: 2 })]);
|
||||
|
||||
const res2 = results[1];
|
||||
expect(res2.columns).toEqual([expect.objectContaining({ columnName: 'id' })]);
|
||||
expect(res2.rows).toEqual([expect.dataRow({ id: 2 }), expect.dataRow({ id: 1 })]);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Script - return data - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
const results = await executeStream(
|
||||
driver,
|
||||
conn,
|
||||
'CREATE TABLE t1 (id int); INSERT INTO t1 (id) VALUES (1); INSERT INTO t1 (id) VALUES (2); SELECT id FROM t1 ORDER BY id; '
|
||||
);
|
||||
expect(results.length).toEqual(1);
|
||||
|
||||
const res1 = results[0];
|
||||
expect(res1.columns).toEqual([expect.objectContaining({ columnName: 'id' })]);
|
||||
expect(res1.rows).toEqual([expect.dataRow({ id: 1 }), expect.dataRow({ id: 2 })]);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Script - no data - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
const results = await executeStream(
|
||||
driver,
|
||||
conn,
|
||||
'CREATE TABLE t1 (id int); INSERT INTO t1 (id) VALUES (1); INSERT INTO t1 (id) VALUES (2) '
|
||||
);
|
||||
expect(results.length).toEqual(0);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Save data query - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
for (const sql of initSql) await driver.query(conn, sql);
|
||||
|
||||
await driver.script(
|
||||
conn,
|
||||
'INSERT INTO t1 (id) VALUES (3);INSERT INTO t1 (id) VALUES (4);UPDATE t1 SET id=10 WHERE id=1;DELETE FROM t1 WHERE id=2;'
|
||||
);
|
||||
const res = await driver.query(conn, 'SELECT COUNT(*) AS cnt FROM t1');
|
||||
// console.log(res);
|
||||
expect(res.rows[0].cnt == 3).toBeTruthy();
|
||||
})
|
||||
);
|
||||
});
|
||||
112
integration-tests/__tests__/table-analyse.spec.js
Normal file
@@ -0,0 +1,112 @@
|
||||
const engines = require('../engines');
|
||||
const { testWrapper } = require('../tools');
|
||||
|
||||
const t1Sql = 'CREATE TABLE t1 (id int not null primary key, val1 varchar(50) null)';
|
||||
const t2Sql = 'CREATE TABLE t2 (id int not null primary key, val2 varchar(50) null)';
|
||||
|
||||
const txMatch = (tname, vcolname, nextcol) =>
|
||||
expect.objectContaining({
|
||||
pureName: tname,
|
||||
columns: [
|
||||
expect.objectContaining({
|
||||
columnName: 'id',
|
||||
notNull: true,
|
||||
dataType: expect.stringContaining('int'),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
columnName: vcolname,
|
||||
notNull: false,
|
||||
dataType: expect.stringMatching(/.*char.*\(50\)/),
|
||||
}),
|
||||
...(nextcol
|
||||
? [
|
||||
expect.objectContaining({
|
||||
columnName: 'nextcol',
|
||||
notNull: false,
|
||||
dataType: expect.stringMatching(/.*char.*\(50\)/),
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
primaryKey: expect.objectContaining({
|
||||
columns: [
|
||||
expect.objectContaining({
|
||||
columnName: 'id',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const t1Match = txMatch('t1', 'val1');
|
||||
const t2Match = txMatch('t2', 'val2');
|
||||
const t2NextColMatch = txMatch('t2', 'val2', true);
|
||||
|
||||
describe('Table analyse', () => {
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Table structure - full analysis - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await driver.query(conn, t1Sql);
|
||||
|
||||
const structure = await driver.analyseFull(conn);
|
||||
|
||||
expect(structure.tables.length).toEqual(1);
|
||||
expect(structure.tables[0]).toEqual(t1Match);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Table add - incremental analysis - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await driver.query(conn, t2Sql);
|
||||
|
||||
const structure1 = await driver.analyseFull(conn);
|
||||
expect(structure1.tables.length).toEqual(1);
|
||||
expect(structure1.tables[0]).toEqual(t2Match);
|
||||
|
||||
await driver.query(conn, t1Sql);
|
||||
const structure2 = await driver.analyseIncremental(conn, structure1);
|
||||
|
||||
expect(structure2.tables.length).toEqual(2);
|
||||
expect(structure2.tables.find(x => x.pureName == 't1')).toEqual(t1Match);
|
||||
expect(structure2.tables.find(x => x.pureName == 't2')).toEqual(t2Match);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Table remove - incremental analysis - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await driver.query(conn, t1Sql);
|
||||
await driver.query(conn, t2Sql);
|
||||
const structure1 = await driver.analyseFull(conn);
|
||||
expect(structure1.tables.length).toEqual(2);
|
||||
expect(structure1.tables.find(x => x.pureName == 't1')).toEqual(t1Match);
|
||||
expect(structure1.tables.find(x => x.pureName == 't2')).toEqual(t2Match);
|
||||
|
||||
await driver.query(conn, 'DROP TABLE t2');
|
||||
const structure2 = await driver.analyseIncremental(conn, structure1);
|
||||
|
||||
expect(structure2.tables.length).toEqual(1);
|
||||
expect(structure2.tables[0]).toEqual(t1Match);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Table change - incremental analysis - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await driver.query(conn, t1Sql);
|
||||
await driver.query(conn, t2Sql);
|
||||
const structure1 = await driver.analyseFull(conn);
|
||||
|
||||
if (engine.dbSnapshotBySeconds) await new Promise(resolve => setTimeout(resolve, 1100));
|
||||
|
||||
await driver.query(conn, 'ALTER TABLE t2 ADD nextcol varchar(50)');
|
||||
const structure2 = await driver.analyseIncremental(conn, structure1);
|
||||
|
||||
expect(structure2).toBeTruthy(); // if falsy, no modification is detected
|
||||
|
||||
expect(structure2.tables.length).toEqual(2);
|
||||
expect(structure2.tables.find(x => x.pureName == 't1')).toEqual(t1Match);
|
||||
expect(structure2.tables.find(x => x.pureName == 't2')).toEqual(t2NextColMatch);
|
||||
})
|
||||
);
|
||||
});
|
||||
0
integration-tests/dbtemp/.gitkeep
Normal file
55
integration-tests/docker-compose.yaml
Normal file
@@ -0,0 +1,55 @@
|
||||
version: '3'
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_PASSWORD: Pwd2020Db
|
||||
ports:
|
||||
- 15000:5432
|
||||
|
||||
mysql:
|
||||
image: mysql:8.0.18
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
restart: always
|
||||
ports:
|
||||
- 15001:3306
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=Pwd2020Db
|
||||
|
||||
mssql:
|
||||
image: mcr.microsoft.com/mssql/server
|
||||
restart: always
|
||||
ports:
|
||||
- 15002:1433
|
||||
environment:
|
||||
- ACCEPT_EULA=Y
|
||||
- SA_PASSWORD=Pwd2020Db
|
||||
- MSSQL_PID=Express
|
||||
|
||||
cockroachdb:
|
||||
image: cockroachdb/cockroach
|
||||
ports:
|
||||
- 15003:26257
|
||||
command: start-single-node --insecure
|
||||
|
||||
# mongodb:
|
||||
# image: mongo:4.0.12
|
||||
# restart: always
|
||||
# volumes:
|
||||
# - mongo-data:/data/db
|
||||
# - mongo-config:/data/configdb
|
||||
# ports:
|
||||
# - 27017:27017
|
||||
|
||||
|
||||
# cockroachdb-init:
|
||||
# image: cockroachdb/cockroach
|
||||
# # build: cockroach
|
||||
# # entrypoint: /cockroach/init.sh
|
||||
# entrypoint: ./cockroach sql --insecure --host="cockroachdb" --execute="CREATE DATABASE IF NOT EXISTS test;"
|
||||
|
||||
# depends_on:
|
||||
# - cockroachdb
|
||||
# restart: on-failure
|
||||
|
||||
115
integration-tests/engines.js
Normal file
@@ -0,0 +1,115 @@
|
||||
const views = {
|
||||
type: 'views',
|
||||
create1: 'CREATE VIEW obj1 AS SELECT id FROM t1',
|
||||
create2: 'CREATE VIEW obj2 AS SELECT id FROM t2',
|
||||
drop1: 'DROP VIEW obj1',
|
||||
drop2: 'DROP VIEW obj2',
|
||||
};
|
||||
const matviews = {
|
||||
type: 'matviews',
|
||||
create1: 'CREATE MATERIALIZED VIEW obj1 AS SELECT id FROM t1',
|
||||
create2: 'CREATE MATERIALIZED VIEW obj2 AS SELECT id FROM t2',
|
||||
drop1: 'DROP MATERIALIZED VIEW obj1',
|
||||
drop2: 'DROP MATERIALIZED VIEW obj2',
|
||||
};
|
||||
|
||||
const engines = [
|
||||
{
|
||||
label: 'MySQL',
|
||||
connection: {
|
||||
engine: 'mysql@dbgate-plugin-mysql',
|
||||
password: 'Pwd2020Db',
|
||||
user: 'root',
|
||||
server: 'mysql',
|
||||
port: 3306,
|
||||
},
|
||||
local: {
|
||||
server: 'localhost',
|
||||
port: 15001,
|
||||
},
|
||||
// skipOnCI: true,
|
||||
objects: [views],
|
||||
dbSnapshotBySeconds: true,
|
||||
},
|
||||
{
|
||||
label: 'PostgreSQL',
|
||||
connection: {
|
||||
engine: 'postgres@dbgate-plugin-postgres',
|
||||
password: 'Pwd2020Db',
|
||||
user: 'postgres',
|
||||
server: 'postgres',
|
||||
port: 5432,
|
||||
},
|
||||
local: {
|
||||
server: 'localhost',
|
||||
port: 15000,
|
||||
},
|
||||
objects: [
|
||||
views,
|
||||
matviews,
|
||||
{
|
||||
type: 'procedures',
|
||||
create1: 'CREATE PROCEDURE obj1() LANGUAGE SQL AS $$ select * from t1 $$',
|
||||
create2: 'CREATE PROCEDURE obj2() LANGUAGE SQL AS $$ select * from t2 $$',
|
||||
drop1: 'DROP PROCEDURE obj1',
|
||||
drop2: 'DROP PROCEDURE obj2',
|
||||
},
|
||||
{
|
||||
type: 'functions',
|
||||
create1: 'CREATE FUNCTION obj1() returns int LANGUAGE plpgsql AS $$ declare res integer; begin select count(*) into res from t1; return res; end; $$',
|
||||
create2: 'CREATE FUNCTION obj2() returns int LANGUAGE plpgsql AS $$ declare res integer; begin select count(*) into res from t2; return res; end; $$',
|
||||
drop1: 'DROP FUNCTION obj1',
|
||||
drop2: 'DROP FUNCTION obj2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'SQL Server',
|
||||
connection: {
|
||||
engine: 'mssql@dbgate-plugin-mssql',
|
||||
password: 'Pwd2020Db',
|
||||
user: 'sa',
|
||||
server: 'mssql',
|
||||
port: 1433,
|
||||
},
|
||||
local: {
|
||||
server: 'localhost',
|
||||
port: 15002,
|
||||
},
|
||||
objects: [
|
||||
views,
|
||||
{
|
||||
type: 'procedures',
|
||||
create1: 'CREATE PROCEDURE obj1 AS SELECT id FROM t1',
|
||||
create2: 'CREATE PROCEDURE obj2 AS SELECT id FROM t2',
|
||||
drop1: 'DROP PROCEDURE obj1',
|
||||
drop2: 'DROP PROCEDURE obj2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'SQLite',
|
||||
generateDbFile: true,
|
||||
connection: {
|
||||
engine: 'sqlite@dbgate-plugin-sqlite',
|
||||
},
|
||||
objects: [views],
|
||||
},
|
||||
{
|
||||
label: 'CockroachDB',
|
||||
connection: {
|
||||
engine: 'cockroach@dbgate-plugin-postgres',
|
||||
user: 'root',
|
||||
server: 'cockroachdb',
|
||||
port: 26257,
|
||||
},
|
||||
local: {
|
||||
server: 'localhost',
|
||||
port: 15003,
|
||||
},
|
||||
skipOnCI: true,
|
||||
objects: [views, matviews],
|
||||
},
|
||||
];
|
||||
|
||||
module.exports = process.env.CITEST ? engines.filter(x => !x.skipOnCI) : engines;
|
||||
25
integration-tests/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "dbgate-integration-tests",
|
||||
"version": "4.1.1",
|
||||
"homepage": "https://dbgate.org/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"author": "Jan Prochazka",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"wait:local": "cross-env DEVMODE=1 LOCALTEST=1 node wait.js",
|
||||
"wait:ci": "cross-env DEVMODE=1 CITEST=1 node wait.js",
|
||||
|
||||
"test:local": "cross-env DEVMODE=1 LOCALTEST=1 jest",
|
||||
"test:ci": "cross-env DEVMODE=1 CITEST=1 jest --runInBand --json --outputFile=result.json --testLocationInResults",
|
||||
|
||||
"run:local": "docker-compose down && docker-compose up -d && yarn wait:local && yarn test:local"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
"jest": "^27.0.1"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
63
integration-tests/tools.js
Normal file
@@ -0,0 +1,63 @@
|
||||
global.DBGATE_TOOLS = require('dbgate-tools');
|
||||
const requireEngineDriver = require('dbgate-api/src/utility/requireEngineDriver');
|
||||
const crypto = require('crypto');
|
||||
|
||||
function randomDbName() {
|
||||
const generatedKey = crypto.randomBytes(6);
|
||||
const newKey = generatedKey.toString('hex');
|
||||
return `db${newKey}`;
|
||||
}
|
||||
|
||||
function extractConnection(engine) {
|
||||
const { connection } = engine;
|
||||
|
||||
if (process.env.LOCALTEST && engine.local) {
|
||||
return {
|
||||
...connection,
|
||||
...engine.local,
|
||||
};
|
||||
}
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
async function connect(engine, database) {
|
||||
const connection = extractConnection(engine);
|
||||
const driver = requireEngineDriver(connection);
|
||||
|
||||
if (engine.generateDbFile) {
|
||||
const conn = await driver.connect({
|
||||
...connection,
|
||||
databaseFile: `dbtemp/${database}`,
|
||||
});
|
||||
return conn;
|
||||
} else {
|
||||
const conn = await driver.connect(connection);
|
||||
await driver.query(conn, `CREATE DATABASE ${database}`);
|
||||
await driver.close(conn);
|
||||
|
||||
const res = await driver.connect({
|
||||
...connection,
|
||||
database,
|
||||
});
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
const testWrapper = body => async (label, ...other) => {
|
||||
const engine = other[other.length - 1];
|
||||
const driver = requireEngineDriver(engine.connection);
|
||||
const conn = await connect(engine, randomDbName());
|
||||
try {
|
||||
await body(conn, driver, ...other);
|
||||
} finally {
|
||||
await driver.close(conn);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
randomDbName,
|
||||
connect,
|
||||
extractConnection,
|
||||
testWrapper,
|
||||
};
|
||||
29
integration-tests/wait.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const requireEngineDriver = require('dbgate-api/src/utility/requireEngineDriver');
|
||||
const engines = require('./engines');
|
||||
const { extractConnection } = require('./tools');
|
||||
global.DBGATE_TOOLS = require('dbgate-tools');
|
||||
|
||||
async function connectEngine(engine) {
|
||||
const connection = extractConnection(engine);
|
||||
const driver = requireEngineDriver(connection);
|
||||
for (;;) {
|
||||
try {
|
||||
const conn = await driver.connect(connection);
|
||||
await driver.getVersion(conn);
|
||||
console.log(`Connect to ${engine.label} - OK`);
|
||||
await driver.close(conn);
|
||||
return;
|
||||
} catch (err) {
|
||||
console.log(`Waiting for ${engine.label}, error: ${err.message}`);
|
||||
await new Promise(resolve => setTimeout(resolve, 2500));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function run() {
|
||||
await new Promise(resolve => setTimeout(resolve, 10000));
|
||||
await Promise.all(engines.map(engine => connectEngine(engine)));
|
||||
}
|
||||
|
||||
run();
|
||||
38
package.json
@@ -1,43 +1,52 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "3.9.6",
|
||||
"version": "4.2.8",
|
||||
"name": "dbgate-all",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
"packages/*",
|
||||
"plugins/*",
|
||||
"integration-tests"
|
||||
],
|
||||
"scripts": {
|
||||
"start:api": "yarn workspace dbgate-api start",
|
||||
"start:api:portal": "yarn workspace dbgate-api start:portal",
|
||||
"start:api:covid": "yarn workspace dbgate-api start:covid",
|
||||
"start:web": "yarn workspace dbgate-web start",
|
||||
"start:web": "yarn workspace dbgate-web dev",
|
||||
"start:sqltree": "yarn workspace dbgate-sqltree start",
|
||||
"start:tools": "yarn workspace dbgate-tools start",
|
||||
"start:datalib": "yarn workspace dbgate-datalib start",
|
||||
"start:filterparser": "yarn workspace dbgate-filterparser start",
|
||||
"start:querysplitter": "yarn workspace dbgate-query-splitter start",
|
||||
"build:sqltree": "yarn workspace dbgate-sqltree build",
|
||||
"build:datalib": "yarn workspace dbgate-datalib build",
|
||||
"build:filterparser": "yarn workspace dbgate-filterparser build",
|
||||
"build:querysplitter": "yarn workspace dbgate-query-splitter build",
|
||||
"build:tools": "yarn workspace dbgate-tools build",
|
||||
"build:lib": "yarn build:tools && yarn build:sqltree && yarn build:filterparser && yarn build:datalib",
|
||||
"build:app": "cd app && yarn install && yarn build",
|
||||
"build:lib": "yarn build:querysplitter && yarn build:tools && yarn build:sqltree && yarn build:filterparser && yarn build:datalib",
|
||||
"build:app": "yarn plugins:copydist && cd app && yarn install && yarn build",
|
||||
"build:app:mac": "yarn plugins:copydist && cd app && yarn install && yarn build:mac",
|
||||
"build:api": "yarn workspace dbgate-api build",
|
||||
"build:web:docker": "yarn workspace dbgate-web build:docker",
|
||||
"build:app:local": "cd app && yarn build:local",
|
||||
"build:web:docker": "yarn workspace dbgate-web build",
|
||||
"build:plugins:frontend": "workspaces-run --only=\"dbgate-plugin-*\" -- yarn build:frontend",
|
||||
"build:plugins:frontend:watch": "workspaces-run --parallel --only=\"dbgate-plugin-*\" -- yarn build:frontend:watch",
|
||||
"plugins:copydist": "workspaces-run --only=\"dbgate-plugin-*\" -- yarn copydist",
|
||||
"build:app:local": "yarn plugins:copydist && cd app && yarn build:local",
|
||||
"start:app:local": "cd app && yarn start:local",
|
||||
"setCurrentVersion": "node setCurrentVersion",
|
||||
"generatePadFile": "node generatePadFile",
|
||||
"fillNativeModules": "node fillNativeModules",
|
||||
"fillNativeModulesElectron": "node fillNativeModules --eletron",
|
||||
"fillNativeModulesElectron": "node fillNativeModules --electron",
|
||||
"fillPackagedPlugins": "node fillPackagedPlugins",
|
||||
"resetPackagedPlugins": "node resetPackagedPlugins",
|
||||
"prettier": "prettier --write packages/api/src && prettier --write packages/datalib/src && prettier --write packages/filterparser/src && prettier --write packages/sqltree/src && prettier --write packages/tools/src && prettier --write packages/types && prettier --write packages/web/src && prettier --write app/src",
|
||||
"copy:docker:build": "copyfiles packages/api/dist/* docker -f && copyfiles packages/web/build/* docker -u 2 && copyfiles \"packages/web/build/**/*\" docker -u 2",
|
||||
"prepare:docker": "yarn build:web:docker && yarn build:api && yarn copy:docker:build",
|
||||
"prepare": "yarn build:lib",
|
||||
"copy:docker:build": "copyfiles packages/api/dist/* docker -f && copyfiles packages/web/public/* docker -u 2 && copyfiles \"packages/web/public/**/*\" docker -u 2 && copyfiles \"plugins/dist/**/*\" docker/plugins -u 2",
|
||||
"prepare:docker": "yarn plugins:copydist && yarn build:web:docker && yarn build:api && yarn copy:docker:build",
|
||||
"start": "concurrently --kill-others-on-fail \"yarn start:api\" \"yarn start:web\"",
|
||||
"lib": "concurrently --kill-others-on-fail \"yarn start:sqltree\" \"yarn start:filterparser\" \"yarn start:datalib\" \"yarn start:tools\"",
|
||||
"lib": "concurrently --kill-others-on-fail \"yarn start:sqltree\" \"yarn start:filterparser\" \"yarn start:datalib\" \"yarn start:tools\" \"yarn start:querysplitter\" \"yarn build:plugins:frontend:watch\"",
|
||||
"ts:api": "yarn workspace dbgate-api ts",
|
||||
"ts:web": "yarn workspace dbgate-web ts",
|
||||
"ts": "yarn ts:api && yarn ts:web",
|
||||
"postinstall": "patch-package && yarn fillNativeModules"
|
||||
"postinstall": "yarn resetPackagedPlugins && yarn build:lib && patch-package && yarn fillNativeModules && yarn build:plugins:frontend"
|
||||
},
|
||||
"dependencies": {
|
||||
"concurrently": "^5.1.0",
|
||||
@@ -46,6 +55,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"copyfiles": "^2.2.0",
|
||||
"prettier": "^2.2.1"
|
||||
"prettier": "^2.2.1",
|
||||
"workspaces-run": "^1.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
CONNECTIONS=mysql
|
||||
|
||||
LABEL_mysql=MySql
|
||||
SERVER_mysql=dbgate.org
|
||||
USER_mysql=reader
|
||||
PASSWORD_mysql=CovidReader2020
|
||||
PORT_mysql=3326
|
||||
ENGINE_mysql=mysql@dbgate-plugin-mysql
|
||||
SINGLE_CONNECTION=mysql
|
||||
SINGLE_DATABASE=covid
|
||||
|
||||
PERMISSIONS=files/charts/read
|
||||
@@ -1,23 +1 @@
|
||||
CONNECTIONS=mysql,postgres
|
||||
|
||||
LABEL_mysql=MySql localhost
|
||||
SERVER_mysql=localhost
|
||||
USER_mysql=root
|
||||
PASSWORD_mysql=test
|
||||
PORT_mysql=3307
|
||||
ENGINE_mysql=mysql
|
||||
|
||||
LABEL_postgres=Postgres localhost
|
||||
SERVER_postgres=localhost
|
||||
USER_postgres=postgres
|
||||
PASSWORD_postgres=test
|
||||
PORT_postgres=5433
|
||||
ENGINE_postgres=postgres
|
||||
|
||||
TOOLBAR=home
|
||||
ICON_home=mdi mdi-home
|
||||
TITLE_home=Home
|
||||
PAGE_home=home.html
|
||||
STARTUP_PAGES=home
|
||||
|
||||
PAGES_DIRECTORY=/home/jena/jenasoft/dbgate-web/pages
|
||||
DEVMODE=1
|
||||
|
||||
17
packages/api/.env-portal
Normal file
@@ -0,0 +1,17 @@
|
||||
DEVMODE=1
|
||||
|
||||
CONNECTIONS=mysql,postgres
|
||||
|
||||
LABEL_mysql=MySql localhost
|
||||
SERVER_mysql=localhost
|
||||
USER_mysql=root
|
||||
PASSWORD_mysql=test
|
||||
PORT_mysql=3307
|
||||
ENGINE_mysql=mysql@dbgate-plugin-mysql
|
||||
|
||||
LABEL_postgres=Postgres localhost
|
||||
SERVER_postgres=localhost
|
||||
USER_postgres=postgres
|
||||
PASSWORD_postgres=test
|
||||
PORT_postgres=5433
|
||||
ENGINE_postgres=postgres@dbgate-plugin-postgres
|
||||
15
packages/api/.env-singledb
Normal file
@@ -0,0 +1,15 @@
|
||||
DEVMODE=1
|
||||
|
||||
CONNECTIONS=mysql
|
||||
|
||||
LABEL_mysql=MySql localhost
|
||||
SERVER_mysql=localhost
|
||||
USER_mysql=root
|
||||
PASSWORD_mysql=test
|
||||
PORT_mysql=3307
|
||||
ENGINE_mysql=mysql@dbgate-plugin-mysql
|
||||
|
||||
SINGLE_CONNECTION=mysql
|
||||
SINGLE_DATABASE=Chinook
|
||||
|
||||
PERMISSIONS=files/charts/read
|
||||
@@ -1,13 +1,12 @@
|
||||
{
|
||||
"name": "dbgate-api",
|
||||
"main": "src/index.js",
|
||||
"version": "3.9.5",
|
||||
"version": "4.1.1",
|
||||
"homepage": "https://dbgate.org/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
|
||||
"author": "Jan Prochazka",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
@@ -19,25 +18,27 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"async-lock": "^1.2.4",
|
||||
"axios": "^0.19.0",
|
||||
"axios": "^0.21.1",
|
||||
"better-sqlite3-with-prebuilds": "^7.1.8",
|
||||
"body-parser": "^1.19.0",
|
||||
"bufferutil": "^4.0.1",
|
||||
"byline": "^5.0.0",
|
||||
"compare-versions": "^3.6.0",
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^6.0.3",
|
||||
"dbgate-sqltree": "^3.9.5",
|
||||
"dbgate-tools": "^3.9.5",
|
||||
"dbgate-sqltree": "^4.1.1",
|
||||
"dbgate-tools": "^4.1.1",
|
||||
"eslint": "^6.8.0",
|
||||
"express": "^4.17.1",
|
||||
"express-basic-auth": "^1.2.0",
|
||||
"express-fileupload": "^1.2.0",
|
||||
"find-free-port": "^2.0.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"fs-extra": "^9.1.0",
|
||||
"fs-reverse": "^0.0.3",
|
||||
"get-port": "^5.1.1",
|
||||
"http": "^0.0.0",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
"line-reader": "^0.4.0",
|
||||
"lodash": "^4.17.15",
|
||||
"lodash": "^4.17.21",
|
||||
"ncp": "^2.0.0",
|
||||
"nedb-promises": "^4.0.1",
|
||||
"node-cron": "^2.0.3",
|
||||
@@ -49,15 +50,18 @@
|
||||
"uuid": "^3.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "nodemon src/index.js",
|
||||
"start:portal": "env-cmd nodemon src/index.js",
|
||||
"start:covid": "env-cmd -f .covid-env nodemon src/index.js",
|
||||
"start": "env-cmd node src/index.js",
|
||||
"start:portal": "env-cmd -f .env-portal node src/index.js",
|
||||
"start:singledb": "env-cmd -f .env-singledb node src/index.js",
|
||||
"start:filedb": "env-cmd node src/index.js /home/jena/test/chinook/Chinook.db",
|
||||
"start:singleconn": "env-cmd node src/index.js --server localhost --user root --port 3307 --engine mysql@dbgate-plugin-mysql --password test",
|
||||
"ts": "tsc",
|
||||
"build": "webpack"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^9.0.11",
|
||||
"@types/lodash": "^4.14.149",
|
||||
"dbgate-types": "^3.9.5",
|
||||
"dbgate-types": "^4.1.1",
|
||||
"env-cmd": "^10.1.0",
|
||||
"node-loader": "^1.0.2",
|
||||
"nodemon": "^2.0.2",
|
||||
|
||||
@@ -1,33 +1,32 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { datadir } = require('../utility/directories');
|
||||
const hasPermission = require('../utility/hasPermission');
|
||||
const socket = require('../utility/socket');
|
||||
const _ = require('lodash');
|
||||
|
||||
const currentVersion = require('../currentVersion');
|
||||
const platformInfo = require('../utility/platformInfo');
|
||||
const connections = require('../controllers/connections');
|
||||
|
||||
module.exports = {
|
||||
settingsValue: {},
|
||||
|
||||
async _init() {
|
||||
try {
|
||||
this.settingsValue = JSON.parse(await fs.readFile(path.join(datadir(), 'settings.json'), { encoding: 'utf-8' }));
|
||||
} catch (err) {
|
||||
this.settingsValue = {};
|
||||
}
|
||||
},
|
||||
|
||||
get_meta: 'get',
|
||||
async get() {
|
||||
// const toolbarButtons = process.env.TOOLBAR;
|
||||
// const toolbar = toolbarButtons
|
||||
// ? toolbarButtons.split(',').map((name) => ({
|
||||
// name,
|
||||
// icon: process.env[`ICON_${name}`],
|
||||
// title: process.env[`TITLE_${name}`],
|
||||
// page: process.env[`PAGE_${name}`],
|
||||
// }))
|
||||
// : null;
|
||||
// const startupPages = process.env.STARTUP_PAGES ? process.env.STARTUP_PAGES.split(',') : [];
|
||||
const permissions = process.env.PERMISSIONS ? process.env.PERMISSIONS.split(',') : null;
|
||||
const singleDatabase =
|
||||
process.env.SINGLE_CONNECTION && process.env.SINGLE_DATABASE
|
||||
? {
|
||||
conid: process.env.SINGLE_CONNECTION,
|
||||
database: process.env.SINGLE_DATABASE,
|
||||
}
|
||||
: null;
|
||||
|
||||
return {
|
||||
runAsPortal: !!process.env.CONNECTIONS,
|
||||
// toolbar,
|
||||
// startupPages,
|
||||
singleDatabase,
|
||||
runAsPortal: !!connections.portalConnections,
|
||||
singleDatabase: connections.singleDatabase,
|
||||
permissions,
|
||||
...currentVersion,
|
||||
};
|
||||
@@ -37,5 +36,26 @@ module.exports = {
|
||||
async platformInfo() {
|
||||
return platformInfo;
|
||||
},
|
||||
|
||||
|
||||
getSettings_meta: 'get',
|
||||
async getSettings() {
|
||||
return this.settingsValue;
|
||||
},
|
||||
|
||||
updateSettings_meta: 'post',
|
||||
async updateSettings(values) {
|
||||
if (!hasPermission(`settings/change`)) return false;
|
||||
try {
|
||||
const updated = {
|
||||
...this.settingsValue,
|
||||
...values,
|
||||
};
|
||||
await fs.writeFile(path.join(datadir(), 'settings.json'), JSON.stringify(updated, undefined, 2));
|
||||
this.settingsValue = updated;
|
||||
socket.emitChanged(`settings-changed`);
|
||||
return updated;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,6 +8,32 @@ const socket = require('../utility/socket');
|
||||
const { encryptConnection } = require('../utility/crypting');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
|
||||
function getNamedArgs() {
|
||||
const res = {};
|
||||
for (let i = 0; i < process.argv.length; i++) {
|
||||
const name = process.argv[i];
|
||||
if (name.startsWith('--')) {
|
||||
let value = process.argv[i + 1];
|
||||
if (value && value.startsWith('--')) value = null;
|
||||
res[name.substring(2)] = value == null ? true : value;
|
||||
i++;
|
||||
} else {
|
||||
if (name.endsWith('.db') || name.endsWith('.sqlite') || name.endsWith('.sqlite3')) {
|
||||
res.databaseFile = name;
|
||||
res.engine = 'sqlite@dbgate-plugin-sqlite';
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function getDatabaseFileLabel(databaseFile) {
|
||||
if (!databaseFile) return databaseFile;
|
||||
const m = databaseFile.match(/[\/]([^\/]+)$/);
|
||||
if (m) return m[1];
|
||||
return databaseFile;
|
||||
}
|
||||
|
||||
function getPortalCollections() {
|
||||
if (process.env.CONNECTIONS) {
|
||||
return _.compact(process.env.CONNECTIONS.split(',')).map(id => ({
|
||||
@@ -17,16 +43,79 @@ function getPortalCollections() {
|
||||
user: process.env[`USER_${id}`],
|
||||
password: process.env[`PASSWORD_${id}`],
|
||||
port: process.env[`PORT_${id}`],
|
||||
databaseUrl: process.env[`URL_${id}`],
|
||||
databaseFile: process.env[`FILE_${id}`],
|
||||
defaultDatabase: process.env[`DATABASE_${id}`],
|
||||
singleDatabase: !!process.env[`DATABASE_${id}`],
|
||||
displayName: process.env[`LABEL_${id}`],
|
||||
}));
|
||||
}
|
||||
|
||||
const args = getNamedArgs();
|
||||
if (args.databaseFile) {
|
||||
return [
|
||||
{
|
||||
_id: 'argv',
|
||||
databaseFile: args.databaseFile,
|
||||
singleDatabase: true,
|
||||
defaultDatabase: getDatabaseFileLabel(args.databaseFile),
|
||||
engine: args.engine,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (args.databaseUrl) {
|
||||
return [
|
||||
{
|
||||
_id: 'argv',
|
||||
useDatabaseUrl: true,
|
||||
...args,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (args.server) {
|
||||
return [
|
||||
{
|
||||
_id: 'argv',
|
||||
...args,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
const portalConnections = getPortalCollections();
|
||||
|
||||
function getSingleDatabase() {
|
||||
if (process.env.SINGLE_CONNECTION && process.env.SINGLE_DATABASE) {
|
||||
// @ts-ignore
|
||||
const connection = portalConnections.find(x => x._id == process.env.SINGLE_CONNECTION);
|
||||
return {
|
||||
connection,
|
||||
name: process.env.SINGLE_DATABASE,
|
||||
};
|
||||
}
|
||||
// @ts-ignore
|
||||
const arg0 = (portalConnections || []).find(x => x._id == 'argv');
|
||||
if (arg0) {
|
||||
// @ts-ignore
|
||||
if (arg0.singleDatabase) {
|
||||
return {
|
||||
connection: arg0,
|
||||
// @ts-ignore
|
||||
name: arg0.defaultDatabase,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const singleDatabase = getSingleDatabase();
|
||||
|
||||
module.exports = {
|
||||
datastore: null,
|
||||
opened: [],
|
||||
singleDatabase,
|
||||
portalConnections,
|
||||
|
||||
async _init() {
|
||||
const dir = datadir();
|
||||
@@ -46,7 +135,7 @@ module.exports = {
|
||||
raw: true,
|
||||
},
|
||||
test(req, res) {
|
||||
const subprocess = fork(process.argv[1], ['connectProcess', ...process.argv.slice(3)]);
|
||||
const subprocess = fork(process.argv[1], ['--start-process', 'connectProcess', ...process.argv.slice(3)]);
|
||||
subprocess.on('message', resp => {
|
||||
if (handleProcessCommunication(resp, subprocess)) return;
|
||||
// @ts-ignore
|
||||
|
||||
@@ -4,6 +4,7 @@ const socket = require('../utility/socket');
|
||||
const { fork } = require('child_process');
|
||||
const { DatabaseAnalyser } = require('dbgate-tools');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
const config = require('./config');
|
||||
|
||||
module.exports = {
|
||||
/** @type {import('dbgate-types').OpenedDatabaseConnection[]} */
|
||||
@@ -17,6 +18,19 @@ module.exports = {
|
||||
existing.structure = structure;
|
||||
socket.emitChanged(`database-structure-changed-${conid}-${database}`);
|
||||
},
|
||||
handle_structureTime(conid, database, { analysedTime }) {
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
if (!existing) return;
|
||||
existing.analysedTime = analysedTime;
|
||||
socket.emitChanged(`database-status-changed-${conid}-${database}`);
|
||||
},
|
||||
handle_version(conid, database, { version }) {
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
if (!existing) return;
|
||||
existing.serverVersion = version;
|
||||
socket.emitChanged(`database-server-version-changed-${conid}-${database}`);
|
||||
},
|
||||
|
||||
handle_error(conid, database, props) {
|
||||
const { error } = props;
|
||||
console.log(`Error in database connection ${conid}, database ${database}: ${error}`);
|
||||
@@ -40,13 +54,18 @@ module.exports = {
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
if (existing) return existing;
|
||||
const connection = await connections.get({ conid });
|
||||
const subprocess = fork(process.argv[1], ['databaseConnectionProcess', ...process.argv.slice(3)]);
|
||||
const subprocess = fork(process.argv[1], [
|
||||
'--start-process',
|
||||
'databaseConnectionProcess',
|
||||
...process.argv.slice(3),
|
||||
]);
|
||||
const lastClosed = this.closed[`${conid}/${database}`];
|
||||
const newOpened = {
|
||||
conid,
|
||||
database,
|
||||
subprocess,
|
||||
structure: lastClosed ? lastClosed.structure : DatabaseAnalyser.createEmptyStructure(),
|
||||
serverVersion: lastClosed ? lastClosed.serverVersion : null,
|
||||
connection,
|
||||
status: { name: 'pending' },
|
||||
};
|
||||
@@ -67,6 +86,7 @@ module.exports = {
|
||||
msgtype: 'connect',
|
||||
connection: { ...connection, database },
|
||||
structure: lastClosed ? lastClosed.structure : null,
|
||||
globalSettings: config.settingsValue,
|
||||
});
|
||||
return newOpened;
|
||||
},
|
||||
@@ -92,12 +112,44 @@ module.exports = {
|
||||
return res;
|
||||
},
|
||||
|
||||
runScript_meta: 'post',
|
||||
async runScript({ conid, database, sql }) {
|
||||
console.log(`Processing script, conid=${conid}, database=${database}, sql=${sql}`);
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(opened, { msgtype: 'runScript', sql });
|
||||
return res;
|
||||
},
|
||||
|
||||
collectionData_meta: 'post',
|
||||
async collectionData({ conid, database, options }) {
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(opened, { msgtype: 'collectionData', options });
|
||||
return res.result;
|
||||
},
|
||||
|
||||
updateCollection_meta: 'post',
|
||||
async updateCollection({ conid, database, changeSet }) {
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(opened, { msgtype: 'updateCollection', changeSet });
|
||||
return res.result;
|
||||
},
|
||||
|
||||
status_meta: 'get',
|
||||
async status({ conid, database }) {
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
if (existing) return existing.status;
|
||||
if (existing) {
|
||||
return {
|
||||
...existing.status,
|
||||
analysedTime: existing.analysedTime,
|
||||
};
|
||||
}
|
||||
const lastClosed = this.closed[`${conid}/${database}`];
|
||||
if (lastClosed) return lastClosed.status;
|
||||
if (lastClosed) {
|
||||
return {
|
||||
...lastClosed.status,
|
||||
analysedTime: lastClosed.analysedTime,
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: 'error',
|
||||
message: 'Not connected',
|
||||
@@ -106,10 +158,14 @@ module.exports = {
|
||||
|
||||
ping_meta: 'post',
|
||||
async ping({ conid, database }) {
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
let existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
|
||||
if (existing) {
|
||||
existing.subprocess.send({ msgtype: 'ping' });
|
||||
} else {
|
||||
existing = await this.ensureOpened(conid, database);
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
connectionStatus: existing ? existing.status : null,
|
||||
@@ -117,13 +173,20 @@ module.exports = {
|
||||
},
|
||||
|
||||
refresh_meta: 'post',
|
||||
async refresh({ conid, database }) {
|
||||
this.close(conid, database);
|
||||
async refresh({ conid, database, keepOpen }) {
|
||||
if (!keepOpen) this.close(conid, database);
|
||||
|
||||
await this.ensureOpened(conid, database);
|
||||
return { status: 'ok' };
|
||||
},
|
||||
|
||||
syncModel_meta: 'post',
|
||||
async syncModel({ conid, database }) {
|
||||
const conn = await this.ensureOpened(conid, database);
|
||||
conn.subprocess.send({ msgtype: 'syncModel' });
|
||||
return { status: 'ok' };
|
||||
},
|
||||
|
||||
close(conid, database, kill = true) {
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
if (existing) {
|
||||
@@ -141,6 +204,12 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
|
||||
disconnect_meta: 'post',
|
||||
async disconnect({ conid, database }) {
|
||||
await this.close(conid, database, true);
|
||||
return { status: 'ok' };
|
||||
},
|
||||
|
||||
structure_meta: 'get',
|
||||
async structure({ conid, database }) {
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
@@ -153,6 +222,22 @@ module.exports = {
|
||||
// };
|
||||
},
|
||||
|
||||
serverVersion_meta: 'get',
|
||||
async serverVersion({ conid, database }) {
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
return opened.serverVersion;
|
||||
},
|
||||
|
||||
sqlPreview_meta: 'post',
|
||||
async sqlPreview({ conid, database, objects, options }) {
|
||||
// wait for structure
|
||||
await this.structure({ conid, database });
|
||||
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(opened, { msgtype: 'sqlPreview', objects, options });
|
||||
return res;
|
||||
},
|
||||
|
||||
// runCommand_meta: 'post',
|
||||
// async runCommand({ conid, database, sql }) {
|
||||
// console.log(`Running SQL command , conid=${conid}, database=${database}, sql=${sql}`);
|
||||
|
||||
@@ -121,7 +121,13 @@ module.exports = {
|
||||
getStats_meta: 'get',
|
||||
getStats({ jslid }) {
|
||||
const file = `${getJslFileName(jslid)}.stats`;
|
||||
if (fs.existsSync(file)) return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||
if (fs.existsSync(file)) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
},
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ module.exports = {
|
||||
listObjects_meta: 'get',
|
||||
async listObjects({ conid, database }) {
|
||||
const opened = await databaseConnections.ensureOpened(conid, database);
|
||||
const types = ['tables', 'views', 'procedures', 'functions', 'triggers'];
|
||||
const types = ['tables', 'collections', 'views', 'procedures', 'functions', 'triggers'];
|
||||
return types.reduce(
|
||||
(res, type) => ({
|
||||
...res,
|
||||
|
||||
@@ -2,43 +2,28 @@ const fs = require('fs-extra');
|
||||
const axios = require('axios');
|
||||
const path = require('path');
|
||||
const { extractPackageName } = require('dbgate-tools');
|
||||
const { pluginsdir, datadir } = require('../utility/directories');
|
||||
const { pluginsdir, packagedPluginsDir } = require('../utility/directories');
|
||||
const socket = require('../utility/socket');
|
||||
const compareVersions = require('compare-versions');
|
||||
const requirePlugin = require('../shell/requirePlugin');
|
||||
const downloadPackage = require('../utility/downloadPackage');
|
||||
const hasPermission = require('../utility/hasPermission');
|
||||
|
||||
// async function loadPackageInfo(dir) {
|
||||
// const readmeFile = path.join(dir, 'README.md');
|
||||
// const packageFile = path.join(dir, 'package.json');
|
||||
|
||||
// if (!(await fs.exists(packageFile))) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// let readme = null;
|
||||
// let manifest = null;
|
||||
// if (await fs.exists(readmeFile)) readme = await fs.readFile(readmeFile, { encoding: 'utf-8' });
|
||||
// if (await fs.exists(packageFile)) manifest = JSON.parse(await fs.readFile(packageFile, { encoding: 'utf-8' }));
|
||||
// return {
|
||||
// readme,
|
||||
// manifest,
|
||||
// };
|
||||
// }
|
||||
|
||||
const preinstallPluginMinimalVersions = {
|
||||
'dbgate-plugin-mssql': '1.1.0',
|
||||
'dbgate-plugin-mysql': '1.1.0',
|
||||
'dbgate-plugin-postgres': '1.1.0',
|
||||
'dbgate-plugin-csv': '1.0.8',
|
||||
'dbgate-plugin-excel': '1.0.6',
|
||||
};
|
||||
const _ = require('lodash');
|
||||
const packagedPluginsContent = require('../packagedPluginsContent');
|
||||
|
||||
module.exports = {
|
||||
script_meta: 'get',
|
||||
async script({ packageName }) {
|
||||
const file = path.join(pluginsdir(), packageName, 'dist', 'frontend.js');
|
||||
const packagedContent = packagedPluginsContent();
|
||||
|
||||
if (packagedContent && packagedContent[packageName]) {
|
||||
return packagedContent[packageName].frontend;
|
||||
}
|
||||
|
||||
const file1 = path.join(packagedPluginsDir(), packageName, 'dist', 'frontend.js');
|
||||
const file2 = path.join(pluginsdir(), packageName, 'dist', 'frontend.js');
|
||||
// @ts-ignore
|
||||
const file = (await fs.exists(file1)) ? file1 : file2;
|
||||
const data = await fs.readFile(file, {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
@@ -62,10 +47,13 @@ module.exports = {
|
||||
const { latest } = infoResp.data['dist-tags'];
|
||||
const manifest = infoResp.data.versions[latest];
|
||||
const { readme } = infoResp.data;
|
||||
// @ts-ignore
|
||||
const isPackaged = await fs.exists(path.join(packagedPluginsDir(), packageName));
|
||||
|
||||
return {
|
||||
readme,
|
||||
manifest,
|
||||
isPackaged,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
@@ -73,52 +61,63 @@ module.exports = {
|
||||
error: err.message,
|
||||
};
|
||||
}
|
||||
|
||||
// const dir = path.join(pluginstmpdir(), packageName);
|
||||
// if (!(await fs.exists(dir))) {
|
||||
// await downloadPackage(packageName, dir);
|
||||
// }
|
||||
// return await loadPackageInfo(dir);
|
||||
// return await {
|
||||
// ...loadPackageInfo(dir),
|
||||
// installed: loadPackageInfo(path.join(pluginsdir(), packageName)),
|
||||
// };
|
||||
},
|
||||
|
||||
installed_meta: 'get',
|
||||
async installed() {
|
||||
const files = await fs.readdir(pluginsdir());
|
||||
const packagedContent = packagedPluginsContent();
|
||||
|
||||
const files1 = packagedContent ? _.keys(packagedContent) : await fs.readdir(packagedPluginsDir());
|
||||
const files2 = await fs.readdir(pluginsdir());
|
||||
|
||||
const res = [];
|
||||
for (const packageName of files) {
|
||||
const manifest = await fs.readFile(path.join(pluginsdir(), packageName, 'package.json')).then(x => JSON.parse(x));
|
||||
const readmeFile = path.join(pluginsdir(), packageName, 'README.md');
|
||||
if (await fs.exists(readmeFile)) {
|
||||
manifest.readme = await fs.readFile(readmeFile, { encoding: 'utf-8' });
|
||||
for (const packageName of _.union(files1, files2)) {
|
||||
if (!/^dbgate-plugin-.*$/.test(packageName)) continue;
|
||||
try {
|
||||
if (packagedContent && packagedContent[packageName]) {
|
||||
const manifest = {
|
||||
...packagedContent[packageName].manifest,
|
||||
};
|
||||
manifest.isPackaged = true;
|
||||
manifest.readme = packagedContent[packageName].readme;
|
||||
res.push(manifest);
|
||||
} else {
|
||||
const isPackaged = files1.includes(packageName);
|
||||
const manifest = await fs
|
||||
.readFile(path.join(isPackaged ? packagedPluginsDir() : pluginsdir(), packageName, 'package.json'), {
|
||||
encoding: 'utf-8',
|
||||
})
|
||||
.then(x => JSON.parse(x));
|
||||
const readmeFile = path.join(isPackaged ? packagedPluginsDir() : pluginsdir(), packageName, 'README.md');
|
||||
// @ts-ignore
|
||||
if (await fs.exists(readmeFile)) {
|
||||
manifest.readme = await fs.readFile(readmeFile, { encoding: 'utf-8' });
|
||||
}
|
||||
manifest.isPackaged = isPackaged;
|
||||
res.push(manifest);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`Skipped plugin ${packageName}, error:`, err.message);
|
||||
}
|
||||
res.push(manifest);
|
||||
}
|
||||
return res;
|
||||
// const res = await Promise.all(
|
||||
// files.map((packageName) =>
|
||||
// fs.readFile(path.join(pluginsdir(), packageName, 'package.json')).then((x) => JSON.parse(x))
|
||||
// )
|
||||
// );
|
||||
},
|
||||
|
||||
async saveRemovePlugins() {
|
||||
await fs.writeFile(path.join(datadir(), 'removed-plugins'), this.removedPlugins.join('\n'));
|
||||
},
|
||||
// async saveRemovePlugins() {
|
||||
// await fs.writeFile(path.join(datadir(), 'removed-plugins'), this.removedPlugins.join('\n'));
|
||||
// },
|
||||
|
||||
install_meta: 'post',
|
||||
async install({ packageName }) {
|
||||
if (!hasPermission(`plugins/install`)) return;
|
||||
const dir = path.join(pluginsdir(), packageName);
|
||||
// @ts-ignore
|
||||
if (!(await fs.exists(dir))) {
|
||||
await downloadPackage(packageName, dir);
|
||||
}
|
||||
socket.emitChanged(`installed-plugins-changed`);
|
||||
this.removedPlugins = this.removedPlugins.filter(x => x != packageName);
|
||||
await this.saveRemovePlugins();
|
||||
// this.removedPlugins = this.removedPlugins.filter(x => x != packageName);
|
||||
// await this.saveRemovePlugins();
|
||||
},
|
||||
|
||||
uninstall_meta: 'post',
|
||||
@@ -127,7 +126,7 @@ module.exports = {
|
||||
const dir = path.join(pluginsdir(), packageName);
|
||||
await fs.rmdir(dir, { recursive: true });
|
||||
socket.emitChanged(`installed-plugins-changed`);
|
||||
this.removedPlugins.push(packageName);
|
||||
// this.removedPlugins.push(packageName);
|
||||
await this.saveRemovePlugins();
|
||||
},
|
||||
|
||||
@@ -135,6 +134,7 @@ module.exports = {
|
||||
async upgrade({ packageName }) {
|
||||
if (!hasPermission(`plugins/install`)) return;
|
||||
const dir = path.join(pluginsdir(), packageName);
|
||||
// @ts-ignore
|
||||
if (await fs.exists(dir)) {
|
||||
await fs.rmdir(dir, { recursive: true });
|
||||
await downloadPackage(packageName, dir);
|
||||
@@ -149,54 +149,55 @@ module.exports = {
|
||||
return content.commands[command](args);
|
||||
},
|
||||
|
||||
authTypes_meta: 'post',
|
||||
authTypes_meta: 'get',
|
||||
async authTypes({ engine }) {
|
||||
const packageName = extractPackageName(engine);
|
||||
const content = requirePlugin(packageName);
|
||||
if (!content.driver || content.driver.engine != engine || !content.driver.getAuthTypes) return null;
|
||||
return content.driver.getAuthTypes() || null;
|
||||
const driver = content.drivers.find(x => x.engine == engine);
|
||||
if (!driver || !driver.getAuthTypes) return null;
|
||||
return driver.getAuthTypes() || null;
|
||||
},
|
||||
|
||||
async _init() {
|
||||
const installed = await this.installed();
|
||||
try {
|
||||
this.removedPlugins = (await fs.readFile(path.join(datadir(), 'removed-plugins'), { encoding: 'utf-8' })).split(
|
||||
'\n'
|
||||
);
|
||||
} catch (err) {
|
||||
this.removedPlugins = [];
|
||||
}
|
||||
// async _init() {
|
||||
// const installed = await this.installed();
|
||||
// try {
|
||||
// this.removedPlugins = (await fs.readFile(path.join(datadir(), 'removed-plugins'), { encoding: 'utf-8' })).split(
|
||||
// '\n'
|
||||
// );
|
||||
// } catch (err) {
|
||||
// this.removedPlugins = [];
|
||||
// }
|
||||
|
||||
for (const packageName of Object.keys(preinstallPluginMinimalVersions)) {
|
||||
const installedVersion = installed.find(x => x.name == packageName);
|
||||
if (installedVersion) {
|
||||
// plugin installed, test, whether upgrade
|
||||
const requiredVersion = preinstallPluginMinimalVersions[packageName];
|
||||
if (compareVersions(installedVersion.version, requiredVersion) < 0) {
|
||||
console.log(
|
||||
`Upgrading preinstalled plugin ${packageName}, found ${installedVersion.version}, required version ${requiredVersion}`
|
||||
);
|
||||
await this.upgrade({ packageName });
|
||||
} else {
|
||||
console.log(
|
||||
`Plugin ${packageName} not upgraded, found ${installedVersion.version}, required version ${requiredVersion}`
|
||||
);
|
||||
}
|
||||
// for (const packageName of Object.keys(preinstallPluginMinimalVersions)) {
|
||||
// const installedVersion = installed.find(x => x.name == packageName);
|
||||
// if (installedVersion) {
|
||||
// // plugin installed, test, whether upgrade
|
||||
// const requiredVersion = preinstallPluginMinimalVersions[packageName];
|
||||
// if (compareVersions(installedVersion.version, requiredVersion) < 0) {
|
||||
// console.log(
|
||||
// `Upgrading preinstalled plugin ${packageName}, found ${installedVersion.version}, required version ${requiredVersion}`
|
||||
// );
|
||||
// await this.upgrade({ packageName });
|
||||
// } else {
|
||||
// console.log(
|
||||
// `Plugin ${packageName} not upgraded, found ${installedVersion.version}, required version ${requiredVersion}`
|
||||
// );
|
||||
// }
|
||||
|
||||
continue;
|
||||
}
|
||||
// continue;
|
||||
// }
|
||||
|
||||
if (this.removedPlugins.includes(packageName)) {
|
||||
// plugin was remvoed, don't install again
|
||||
continue;
|
||||
}
|
||||
// if (this.removedPlugins.includes(packageName)) {
|
||||
// // plugin was remvoed, don't install again
|
||||
// continue;
|
||||
// }
|
||||
|
||||
try {
|
||||
console.log('Preinstalling plugin', packageName);
|
||||
await this.install({ packageName });
|
||||
} catch (err) {
|
||||
console.error('Error preinstalling plugin', packageName, err);
|
||||
}
|
||||
}
|
||||
},
|
||||
// try {
|
||||
// console.log('Preinstalling plugin', packageName);
|
||||
// await this.install({ packageName });
|
||||
// } catch (err) {
|
||||
// console.error('Error preinstalling plugin', packageName, err);
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
};
|
||||
|
||||
54
packages/api/src/controllers/queryHistory.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const fsReverse = require('fs-reverse');
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { datadir } = require('../utility/directories');
|
||||
const _ = require('lodash');
|
||||
const { filterName } = require('dbgate-tools');
|
||||
const socket = require('../utility/socket');
|
||||
|
||||
function readCore(reader, skip, limit, filter) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const res = [];
|
||||
let readed = 0;
|
||||
reader.on('data', line => {
|
||||
if (!line && !line.trim()) return;
|
||||
try {
|
||||
const json = JSON.parse(line);
|
||||
if (filterName(filter, json.sql, json.database)) {
|
||||
if (!skip || readed >= skip) {
|
||||
res.push(json);
|
||||
}
|
||||
readed++;
|
||||
if (limit && readed > (skip || 0) + limit) {
|
||||
reader.destroy();
|
||||
resolve(res);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
reader.destroy();
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
reader.on('end', () => resolve(res));
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
read_meta: 'get',
|
||||
async read({ skip, limit, filter }) {
|
||||
const fileName = path.join(datadir(), 'query-history.jsonl');
|
||||
// @ts-ignore
|
||||
if (!(await fs.exists(fileName))) return [];
|
||||
const reader = fsReverse(fileName);
|
||||
const res = await readCore(reader, skip, limit, filter);
|
||||
return res;
|
||||
},
|
||||
|
||||
write_meta: 'post',
|
||||
async write({ data }) {
|
||||
const fileName = path.join(datadir(), 'query-history.jsonl');
|
||||
await fs.appendFile(fileName, JSON.stringify(data) + '\n');
|
||||
socket.emitChanged('query-history-changed');
|
||||
return 'OK';
|
||||
},
|
||||
};
|
||||
@@ -5,7 +5,7 @@ const uuidv1 = require('uuid/v1');
|
||||
const byline = require('byline');
|
||||
const socket = require('../utility/socket');
|
||||
const { fork } = require('child_process');
|
||||
const { rundir, uploadsdir, pluginsdir } = require('../utility/directories');
|
||||
const { rundir, uploadsdir, pluginsdir, getPluginBackendPath, packagedPluginList } = require('../utility/directories');
|
||||
const { extractShellApiPlugins, extractShellApiFunctionName } = require('dbgate-tools');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
|
||||
@@ -92,7 +92,7 @@ module.exports = {
|
||||
const scriptFile = path.join(uploadsdir(), runid + '.js');
|
||||
fs.writeFileSync(`${scriptFile}`, scriptText);
|
||||
fs.mkdirSync(directory);
|
||||
const pluginNames = fs.readdirSync(pluginsdir());
|
||||
const pluginNames = _.union(fs.readdirSync(pluginsdir()), packagedPluginList);
|
||||
console.log(`RUNNING SCRIPT ${scriptFile}`);
|
||||
// const subprocess = fork(scriptFile, ['--checkParent', '--max-old-space-size=8192'], {
|
||||
const subprocess = fork(scriptFile, ['--checkParent', ...process.argv.slice(3)], {
|
||||
@@ -101,7 +101,7 @@ module.exports = {
|
||||
env: {
|
||||
...process.env,
|
||||
DBGATE_API: global['dbgateApiModulePath'] || process.argv[1],
|
||||
..._.fromPairs(pluginNames.map(name => [`PLUGIN_${_.camelCase(name)}`, path.join(pluginsdir(), name)])),
|
||||
..._.fromPairs(pluginNames.map(name => [`PLUGIN_${_.camelCase(name)}`, getPluginBackendPath(name)])),
|
||||
},
|
||||
});
|
||||
const pipeDispatcher = severity => data =>
|
||||
|
||||
@@ -5,6 +5,7 @@ const _ = require('lodash');
|
||||
const AsyncLock = require('async-lock');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
const lock = new AsyncLock();
|
||||
const config = require('./config');
|
||||
|
||||
module.exports = {
|
||||
opened: [],
|
||||
@@ -17,6 +18,12 @@ module.exports = {
|
||||
existing.databases = databases;
|
||||
socket.emitChanged(`database-list-changed-${conid}`);
|
||||
},
|
||||
handle_version(conid, { version }) {
|
||||
const existing = this.opened.find(x => x.conid == conid);
|
||||
if (!existing) return;
|
||||
existing.version = version;
|
||||
socket.emitChanged(`server-version-changed-${conid}`);
|
||||
},
|
||||
handle_status(conid, { status }) {
|
||||
const existing = this.opened.find(x => x.conid == conid);
|
||||
if (!existing) return;
|
||||
@@ -30,7 +37,11 @@ module.exports = {
|
||||
const existing = this.opened.find(x => x.conid == conid);
|
||||
if (existing) return existing;
|
||||
const connection = await connections.get({ conid });
|
||||
const subprocess = fork(process.argv[1], ['serverConnectionProcess', ...process.argv.slice(3)]);
|
||||
const subprocess = fork(process.argv[1], [
|
||||
'--start-process',
|
||||
'serverConnectionProcess',
|
||||
...process.argv.slice(3),
|
||||
]);
|
||||
const newOpened = {
|
||||
conid,
|
||||
subprocess,
|
||||
@@ -55,7 +66,7 @@ module.exports = {
|
||||
if (newOpened.disconnected) return;
|
||||
this.close(conid, false);
|
||||
});
|
||||
subprocess.send({ msgtype: 'connect', ...connection });
|
||||
subprocess.send({ msgtype: 'connect', ...connection, globalSettings: config.settingsValue });
|
||||
return newOpened;
|
||||
});
|
||||
return res;
|
||||
@@ -75,12 +86,24 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
|
||||
disconnect_meta: 'post',
|
||||
async disconnect({ conid }) {
|
||||
await this.close(conid, true);
|
||||
return { status: 'ok' };
|
||||
},
|
||||
|
||||
listDatabases_meta: 'get',
|
||||
async listDatabases({ conid }) {
|
||||
const opened = await this.ensureOpened(conid);
|
||||
return opened.databases;
|
||||
},
|
||||
|
||||
version_meta: 'get',
|
||||
async version({ conid }) {
|
||||
const opened = await this.ensureOpened(conid);
|
||||
return opened.version;
|
||||
},
|
||||
|
||||
serverStatus_meta: 'get',
|
||||
async serverStatus() {
|
||||
return {
|
||||
@@ -106,8 +129,8 @@ module.exports = {
|
||||
},
|
||||
|
||||
refresh_meta: 'post',
|
||||
async refresh({ conid }) {
|
||||
this.close(conid);
|
||||
async refresh({ conid, keepOpen }) {
|
||||
if (!keepOpen) this.close(conid);
|
||||
|
||||
await this.ensureOpened(conid);
|
||||
return { status: 'ok' };
|
||||
|
||||
@@ -65,7 +65,7 @@ module.exports = {
|
||||
async create({ conid, database }) {
|
||||
const sesid = uuidv1();
|
||||
const connection = await connections.get({ conid });
|
||||
const subprocess = fork(process.argv[1], ['sessionProcess', ...process.argv.slice(3)]);
|
||||
const subprocess = fork(process.argv[1], ['--start-process', 'sessionProcess', ...process.argv.slice(3)]);
|
||||
const newOpened = {
|
||||
conid,
|
||||
database,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
module.exports = {
|
||||
version: '3.9.5',
|
||||
buildTime: '2021-02-08T18:21:44.182Z'
|
||||
version: '4.1.1',
|
||||
buildTime: '2021-04-17T07:22:49.702Z'
|
||||
};
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
const shell = require('./shell');
|
||||
const processArgs = require('./utility/processArgs');
|
||||
const dbgateTools = require('dbgate-tools');
|
||||
|
||||
const argument = process.argv[2];
|
||||
if (argument && argument.endsWith('Process')) {
|
||||
global['DBGATE_TOOLS'] = dbgateTools;
|
||||
|
||||
if (processArgs.startProcess) {
|
||||
const proc = require('./proc');
|
||||
|
||||
const module = proc[argument];
|
||||
const module = proc[processArgs.startProcess];
|
||||
module.start();
|
||||
} else if (!module['parent'] && !process.argv.includes('--checkParent')) {
|
||||
} else if (!module['parent'] && !processArgs.checkParent) {
|
||||
const main = require('./main');
|
||||
|
||||
main.start(argument);
|
||||
main.start();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -6,9 +6,10 @@ const http = require('http');
|
||||
const cors = require('cors');
|
||||
const io = require('socket.io');
|
||||
const fs = require('fs');
|
||||
const findFreePort = require('find-free-port');
|
||||
const getPort = require('get-port');
|
||||
const childProcessChecker = require('./utility/childProcessChecker');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const useController = require('./utility/useController');
|
||||
const socket = require('./utility/socket');
|
||||
@@ -26,10 +27,17 @@ const uploads = require('./controllers/uploads');
|
||||
const plugins = require('./controllers/plugins');
|
||||
const files = require('./controllers/files');
|
||||
const scheduler = require('./controllers/scheduler');
|
||||
const queryHistory = require('./controllers/queryHistory');
|
||||
|
||||
const { rundir } = require('./utility/directories');
|
||||
const platformInfo = require('./utility/platformInfo');
|
||||
const processArgs = require('./utility/processArgs');
|
||||
const timingSafeCheckToken = require('./utility/timingSafeCheckToken');
|
||||
|
||||
function start(argument = null) {
|
||||
let authorization = null;
|
||||
let checkLocalhostOrigin = null;
|
||||
|
||||
function start() {
|
||||
// console.log('process.argv', process.argv);
|
||||
|
||||
const app = express();
|
||||
@@ -49,6 +57,29 @@ function start(argument = null) {
|
||||
);
|
||||
}
|
||||
|
||||
app.use(function (req, res, next) {
|
||||
if (authorization && !timingSafeCheckToken(req.headers.authorization, authorization)) {
|
||||
return res.status(403).json({ error: 'Not authorized!' });
|
||||
}
|
||||
if (checkLocalhostOrigin) {
|
||||
if (
|
||||
req.headers.origin &&
|
||||
req.headers.origin != checkLocalhostOrigin &&
|
||||
req.headers.origin != `http://${checkLocalhostOrigin}`
|
||||
) {
|
||||
console.log('API origin check FAILED');
|
||||
console.log('HEADERS', { ...req.headers, authorization: '***' });
|
||||
return res.status(403).json({ error: 'Not authorized!' });
|
||||
}
|
||||
if (!req.headers.origin && req.headers.host != checkLocalhostOrigin) {
|
||||
console.log('API host check FAILED');
|
||||
console.log('HEADERS', { ...req.headers, authorization: '***' });
|
||||
return res.status(403).json({ error: 'Not authorized!' });
|
||||
}
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(cors());
|
||||
app.use(bodyParser.json({ limit: '50mb' }));
|
||||
|
||||
@@ -72,6 +103,7 @@ function start(argument = null) {
|
||||
useController(app, '/plugins', plugins);
|
||||
useController(app, '/files', files);
|
||||
useController(app, '/scheduler', scheduler);
|
||||
useController(app, '/query-history', queryHistory);
|
||||
|
||||
// if (process.env.PAGES_DIRECTORY) {
|
||||
// app.use('/pages', express.static(process.env.PAGES_DIRECTORY));
|
||||
@@ -79,29 +111,32 @@ function start(argument = null) {
|
||||
|
||||
app.use('/runners/data', express.static(rundir()));
|
||||
|
||||
if (fs.existsSync('/home/dbgate-docker/build')) {
|
||||
if (platformInfo.isDocker) {
|
||||
// server static files inside docker container
|
||||
app.use(express.static('/home/dbgate-docker/build'));
|
||||
app.use(express.static('/home/dbgate-docker/public'));
|
||||
} else {
|
||||
if (argument != 'startNodeWeb') {
|
||||
if (!platformInfo.isNpmDist) {
|
||||
app.get('/', (req, res) => {
|
||||
res.send('DbGate API');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (argument == '--dynport') {
|
||||
if (processArgs.dynport) {
|
||||
childProcessChecker();
|
||||
|
||||
findFreePort(53911, function (err, port) {
|
||||
authorization = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
getPort().then(port => {
|
||||
checkLocalhostOrigin = `localhost:${port}`;
|
||||
server.listen(port, () => {
|
||||
console.log(`DbGate API listening on port ${port}`);
|
||||
process.send({ msgtype: 'listening', port });
|
||||
process.send({ msgtype: 'listening', port, authorization });
|
||||
});
|
||||
});
|
||||
} else if (argument == 'startNodeWeb') {
|
||||
app.use(express.static(path.join(__dirname, '../../dbgate-web/build')));
|
||||
findFreePort(5000, function (err, port) {
|
||||
} else if (platformInfo.isNpmDist) {
|
||||
app.use(express.static(path.join(__dirname, '../../dbgate-web/public')));
|
||||
getPort({ port: 5000 }).then(port => {
|
||||
server.listen(port, () => {
|
||||
console.log(`DbGate API listening on port ${port}`);
|
||||
});
|
||||
|
||||
@@ -2,6 +2,25 @@ const childProcessChecker = require('../utility/childProcessChecker');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const connectUtility = require('../utility/connectUtility');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
const _ = require('lodash');
|
||||
|
||||
function pickSafeConnectionInfo(connection) {
|
||||
return _.mapValues(connection, (v, k) => {
|
||||
if (k == 'engine' || k == 'port' || k == 'authType' || k == 'sshMode' || k == 'passwordMode') return v;
|
||||
if (v === null || v === true || v === false) return v;
|
||||
if (v) return '***';
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
const formatErrorDetail = (e, connection) => `${e.stack}
|
||||
|
||||
Error JSON: ${JSON.stringify(e, undefined, 2)}
|
||||
|
||||
Connection: ${JSON.stringify(pickSafeConnectionInfo(connection), undefined, 2)}
|
||||
|
||||
Platform: ${process.platform}
|
||||
`;
|
||||
|
||||
function start() {
|
||||
childProcessChecker();
|
||||
@@ -14,7 +33,11 @@ function start() {
|
||||
process.send({ msgtype: 'connected', ...res });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.send({ msgtype: 'error', error: e.message });
|
||||
process.send({
|
||||
msgtype: 'error',
|
||||
error: e.message,
|
||||
detail: formatErrorDetail(e, connection),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
const stableStringify = require('json-stable-stringify');
|
||||
const { splitQuery } = require('dbgate-query-splitter');
|
||||
const childProcessChecker = require('../utility/childProcessChecker');
|
||||
const { extractBoolSettingsValue, extractIntSettingsValue } = require('dbgate-tools');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const connectUtility = require('../utility/connectUtility');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
const { SqlGenerator } = require('dbgate-tools');
|
||||
|
||||
let systemConnection;
|
||||
let storedConnection;
|
||||
@@ -10,6 +13,7 @@ let afterConnectCallbacks = [];
|
||||
let analysedStructure = null;
|
||||
let lastPing = null;
|
||||
let lastStatus = null;
|
||||
let analysedTime = 0;
|
||||
|
||||
async function checkedAsyncCall(promise) {
|
||||
try {
|
||||
@@ -26,21 +30,42 @@ async function checkedAsyncCall(promise) {
|
||||
}
|
||||
}
|
||||
|
||||
let loadingModel = false;
|
||||
|
||||
async function handleFullRefresh() {
|
||||
loadingModel = true;
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
setStatusName('loadStructure');
|
||||
analysedStructure = await checkedAsyncCall(driver.analyseFull(systemConnection));
|
||||
analysedTime = new Date().getTime();
|
||||
process.send({ msgtype: 'structure', structure: analysedStructure });
|
||||
process.send({ msgtype: 'structureTime', analysedTime });
|
||||
setStatusName('ok');
|
||||
loadingModel = false;
|
||||
}
|
||||
|
||||
async function handleIncrementalRefresh() {
|
||||
async function handleIncrementalRefresh(forceSend) {
|
||||
loadingModel = true;
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
setStatusName('checkStructure');
|
||||
const newStructure = await checkedAsyncCall(driver.analyseIncremental(systemConnection, analysedStructure));
|
||||
analysedTime = new Date().getTime();
|
||||
if (newStructure != null) {
|
||||
analysedStructure = newStructure;
|
||||
}
|
||||
|
||||
if (forceSend || newStructure != null) {
|
||||
process.send({ msgtype: 'structure', structure: analysedStructure });
|
||||
}
|
||||
|
||||
process.send({ msgtype: 'structureTime', analysedTime });
|
||||
setStatusName('ok');
|
||||
loadingModel = false;
|
||||
}
|
||||
|
||||
function handleSyncModel() {
|
||||
if (loadingModel) return;
|
||||
handleIncrementalRefresh();
|
||||
}
|
||||
|
||||
function setStatus(status) {
|
||||
@@ -55,20 +80,34 @@ function setStatusName(name) {
|
||||
setStatus({ name });
|
||||
}
|
||||
|
||||
async function handleConnect({ connection, structure }) {
|
||||
async function readVersion() {
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
const version = await driver.getVersion(systemConnection);
|
||||
process.send({ msgtype: 'version', version });
|
||||
}
|
||||
|
||||
async function handleConnect({ connection, structure, globalSettings }) {
|
||||
storedConnection = connection;
|
||||
lastPing = new Date().getTime();
|
||||
|
||||
if (!structure) setStatusName('pending');
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
systemConnection = await checkedAsyncCall(connectUtility(driver, storedConnection));
|
||||
readVersion();
|
||||
if (structure) {
|
||||
analysedStructure = structure;
|
||||
handleIncrementalRefresh();
|
||||
handleIncrementalRefresh(true);
|
||||
} else {
|
||||
handleFullRefresh();
|
||||
}
|
||||
setInterval(handleIncrementalRefresh, 30 * 1000);
|
||||
|
||||
if (extractBoolSettingsValue(globalSettings, 'connection.autoRefresh', false)) {
|
||||
setInterval(
|
||||
handleIncrementalRefresh,
|
||||
extractIntSettingsValue(globalSettings, 'connection.autoRefreshInterval', 30, 3, 3600) * 1000
|
||||
);
|
||||
}
|
||||
|
||||
for (const [resolve] of afterConnectCallbacks) {
|
||||
resolve();
|
||||
}
|
||||
@@ -82,6 +121,17 @@ function waitConnected() {
|
||||
});
|
||||
}
|
||||
|
||||
async function handleRunScript({ msgid, sql }) {
|
||||
await waitConnected();
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
try {
|
||||
await driver.script(systemConnection, sql);
|
||||
process.send({ msgtype: 'response', msgid });
|
||||
} catch (err) {
|
||||
process.send({ msgtype: 'response', msgid, errorMessage: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleQueryData({ msgid, sql }) {
|
||||
await waitConnected();
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
@@ -93,6 +143,49 @@ async function handleQueryData({ msgid, sql }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCollectionData({ msgid, options }) {
|
||||
await waitConnected();
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
try {
|
||||
const result = await driver.readCollection(systemConnection, options);
|
||||
process.send({ msgtype: 'response', msgid, result });
|
||||
} catch (err) {
|
||||
process.send({ msgtype: 'response', msgid, errorMessage: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdateCollection({ msgid, changeSet }) {
|
||||
await waitConnected();
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
try {
|
||||
const result = await driver.updateCollection(systemConnection, changeSet);
|
||||
process.send({ msgtype: 'response', msgid, result });
|
||||
} catch (err) {
|
||||
process.send({ msgtype: 'response', msgid, errorMessage: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSqlPreview({ msgid, objects, options }) {
|
||||
await waitConnected();
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
|
||||
try {
|
||||
const dmp = driver.createDumper();
|
||||
const generator = new SqlGenerator(analysedStructure, options, objects, dmp, driver, systemConnection);
|
||||
|
||||
await generator.dump();
|
||||
process.send({ msgtype: 'response', msgid, sql: dmp.s, isTruncated: generator.isTruncated });
|
||||
if (generator.isUnhandledException) {
|
||||
setTimeout(() => {
|
||||
console.log('Exiting because of unhandled exception');
|
||||
process.exit(0);
|
||||
}, 500);
|
||||
}
|
||||
} catch (err) {
|
||||
process.send({ msgtype: 'response', msgid, isError: true, errorMessage: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
// async function handleRunCommand({ msgid, sql }) {
|
||||
// await waitConnected();
|
||||
// const driver = engines(storedConnection);
|
||||
@@ -107,7 +200,12 @@ function handlePing() {
|
||||
const messageHandlers = {
|
||||
connect: handleConnect,
|
||||
queryData: handleQueryData,
|
||||
runScript: handleRunScript,
|
||||
updateCollection: handleUpdateCollection,
|
||||
collectionData: handleCollectionData,
|
||||
sqlPreview: handleSqlPreview,
|
||||
ping: handlePing,
|
||||
syncModel: handleSyncModel,
|
||||
// runCommand: handleRunCommand,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const stableStringify = require('json-stable-stringify');
|
||||
const { extractBoolSettingsValue, extractIntSettingsValue } = require('dbgate-tools');
|
||||
const childProcessChecker = require('../utility/childProcessChecker');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const { decryptConnection } = require('../utility/crypting');
|
||||
@@ -31,6 +32,12 @@ async function handleRefresh() {
|
||||
}
|
||||
}
|
||||
|
||||
async function readVersion() {
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
const version = await driver.getVersion(systemConnection);
|
||||
process.send({ msgtype: 'version', version });
|
||||
}
|
||||
|
||||
function setStatus(status) {
|
||||
const statusString = stableStringify(status);
|
||||
if (lastStatus != statusString) {
|
||||
@@ -45,14 +52,18 @@ function setStatusName(name) {
|
||||
|
||||
async function handleConnect(connection) {
|
||||
storedConnection = connection;
|
||||
const { globalSettings } = storedConnection;
|
||||
setStatusName('pending');
|
||||
lastPing = new Date().getTime();
|
||||
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
try {
|
||||
systemConnection = await connectUtility(driver, storedConnection);
|
||||
readVersion();
|
||||
handleRefresh();
|
||||
setInterval(handleRefresh, 30 * 1000);
|
||||
if (extractBoolSettingsValue(globalSettings, 'connection.autoRefresh', false)) {
|
||||
setInterval(handleRefresh, extractIntSettingsValue(globalSettings, 'connection.autoRefreshInterval', 30, 5, 3600) * 1000);
|
||||
}
|
||||
} catch (err) {
|
||||
setStatus({
|
||||
name: 'error',
|
||||
@@ -71,7 +82,11 @@ async function handleCreateDatabase({ name }) {
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
systemConnection = await connectUtility(driver, storedConnection);
|
||||
console.log(`RUNNING SCRIPT: CREATE DATABASE ${driver.dialect.quoteIdentifier(name)}`);
|
||||
await driver.query(systemConnection, `CREATE DATABASE ${driver.dialect.quoteIdentifier(name)}`);
|
||||
if (driver.createDatabase) {
|
||||
await driver.createDatabase(systemConnection, name);
|
||||
} else {
|
||||
await driver.query(systemConnection, `CREATE DATABASE ${driver.dialect.quoteIdentifier(name)}`);
|
||||
}
|
||||
await handleRefresh();
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ const path = require('path');
|
||||
const fs = require('fs');
|
||||
const _ = require('lodash');
|
||||
const childProcessChecker = require('../utility/childProcessChecker');
|
||||
const goSplit = require('../utility/goSplit');
|
||||
const { splitQuery } = require('dbgate-query-splitter');
|
||||
|
||||
const { jsldir } = require('../utility/directories');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
@@ -17,12 +17,18 @@ let afterConnectCallbacks = [];
|
||||
// let currentHandlers = [];
|
||||
|
||||
class TableWriter {
|
||||
constructor(columns, resultIndex) {
|
||||
constructor(structure, resultIndex) {
|
||||
this.jslid = uuidv1();
|
||||
this.currentFile = path.join(jsldir(), `${this.jslid}.jsonl`);
|
||||
this.currentRowCount = 0;
|
||||
this.currentChangeIndex = 1;
|
||||
fs.writeFileSync(this.currentFile, JSON.stringify({ columns }) + '\n');
|
||||
fs.writeFileSync(
|
||||
this.currentFile,
|
||||
JSON.stringify({
|
||||
...structure,
|
||||
__isStreamHeader: true,
|
||||
}) + '\n'
|
||||
);
|
||||
this.currentStream = fs.createWriteStream(this.currentFile, { flags: 'a' });
|
||||
this.writeCurrentStats(false, false);
|
||||
this.resultIndex = resultIndex;
|
||||
@@ -92,7 +98,7 @@ class StreamHandler {
|
||||
|
||||
recordset(columns) {
|
||||
this.closeCurrentWriter();
|
||||
this.currentWriter = new TableWriter(columns, this.resultIndexHolder.value);
|
||||
this.currentWriter = new TableWriter(Array.isArray(columns) ? { columns } : columns, this.resultIndexHolder.value);
|
||||
this.resultIndexHolder.value += 1;
|
||||
|
||||
// this.writeCurrentStats();
|
||||
@@ -160,7 +166,7 @@ async function handleExecuteQuery({ sql }) {
|
||||
const resultIndexHolder = {
|
||||
value: 0,
|
||||
};
|
||||
for (const sqlItem of goSplit(sql)) {
|
||||
for (const sqlItem of splitQuery(sql, driver.getQuerySplitterOptions('stream'))) {
|
||||
await handleStream(driver, resultIndexHolder, sqlItem);
|
||||
// const handler = new StreamHandler(resultIndex);
|
||||
// const stream = await driver.stream(systemConnection, sqlItem, handler);
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
const EnsureStreamHeaderStream = require('../utility/EnsureStreamHeaderStream');
|
||||
|
||||
function copyStream(input, output) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ensureHeader = new EnsureStreamHeaderStream();
|
||||
const finisher = output['finisher'] || output;
|
||||
finisher.on('finish', resolve);
|
||||
finisher.on('error', reject);
|
||||
input.pipe(output);
|
||||
input.pipe(ensureHeader);
|
||||
ensureHeader.pipe(output);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const goSplit = require('../utility/goSplit');
|
||||
const { splitQuery } = require('dbgate-query-splitter');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const { decryptConnection } = require('../utility/crypting');
|
||||
const connectUtility = require('../utility/connectUtility');
|
||||
|
||||
async function executeQuery({ connection, sql }) {
|
||||
@@ -10,9 +9,9 @@ async function executeQuery({ connection, sql }) {
|
||||
const pool = await connectUtility(driver, connection);
|
||||
console.log(`Connected.`);
|
||||
|
||||
for (const sqlItem of goSplit(sql)) {
|
||||
for (const sqlItem of splitQuery(sql, driver.getQuerySplitterOptions('script'))) {
|
||||
console.log('Executing query', sqlItem);
|
||||
await driver.query(pool, sqlItem);
|
||||
await driver.query(pool, sqlItem, { discardResult: true });
|
||||
}
|
||||
|
||||
console.log(`Query finished`);
|
||||
|
||||
@@ -6,7 +6,9 @@ const copyStream = require('./copyStream');
|
||||
const fakeObjectReader = require('./fakeObjectReader');
|
||||
const consoleObjectWriter = require('./consoleObjectWriter');
|
||||
const jsonLinesWriter = require('./jsonLinesWriter');
|
||||
const jsonArrayWriter = require('./jsonArrayWriter');
|
||||
const jsonLinesReader = require('./jsonLinesReader');
|
||||
const sqlDataWriter = require('./sqlDataWriter');
|
||||
const jslDataReader = require('./jslDataReader');
|
||||
const archiveWriter = require('./archiveWriter');
|
||||
const archiveReader = require('./archiveReader');
|
||||
@@ -26,7 +28,9 @@ const dbgateApi = {
|
||||
tableReader,
|
||||
copyStream,
|
||||
jsonLinesWriter,
|
||||
jsonArrayWriter,
|
||||
jsonLinesReader,
|
||||
sqlDataWriter,
|
||||
fakeObjectReader,
|
||||
consoleObjectWriter,
|
||||
jslDataReader,
|
||||
|
||||
52
packages/api/src/shell/jsonArrayWriter.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const fs = require('fs');
|
||||
const stream = require('stream');
|
||||
|
||||
class StringifyStream extends stream.Transform {
|
||||
constructor() {
|
||||
super({ objectMode: true });
|
||||
this.wasHeader = false;
|
||||
this.wasRecord = false;
|
||||
}
|
||||
_transform(chunk, encoding, done) {
|
||||
let skip = false;
|
||||
|
||||
if (!this.wasHeader) {
|
||||
skip =
|
||||
chunk.__isStreamHeader ||
|
||||
// TODO remove isArray test
|
||||
Array.isArray(chunk.columns);
|
||||
this.wasHeader = true;
|
||||
}
|
||||
if (!skip) {
|
||||
if (!this.wasRecord) {
|
||||
this.push('[\n');
|
||||
} else {
|
||||
this.push(',\n');
|
||||
}
|
||||
this.wasRecord = true;
|
||||
|
||||
this.push(JSON.stringify(chunk));
|
||||
}
|
||||
done();
|
||||
}
|
||||
|
||||
_flush(done) {
|
||||
if (!this.wasRecord) {
|
||||
this.push('[]\n');
|
||||
} else {
|
||||
this.push('\n]\n');
|
||||
}
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
async function jsonArrayWriter({ fileName, encoding = 'utf-8' }) {
|
||||
console.log(`Writing file ${fileName}`);
|
||||
const stringify = new StringifyStream();
|
||||
const fileStream = fs.createWriteStream(fileName, encoding);
|
||||
stringify.pipe(fileStream);
|
||||
stringify['finisher'] = fileStream;
|
||||
return stringify;
|
||||
}
|
||||
|
||||
module.exports = jsonArrayWriter;
|
||||
@@ -3,9 +3,8 @@ const stream = require('stream');
|
||||
const byline = require('byline');
|
||||
|
||||
class ParseStream extends stream.Transform {
|
||||
constructor({ header, limitRows }) {
|
||||
constructor({ limitRows }) {
|
||||
super({ objectMode: true });
|
||||
this.header = header;
|
||||
this.wasHeader = false;
|
||||
this.limitRows = limitRows;
|
||||
this.rowsWritten = 0;
|
||||
@@ -13,7 +12,14 @@ class ParseStream extends stream.Transform {
|
||||
_transform(chunk, encoding, done) {
|
||||
const obj = JSON.parse(chunk);
|
||||
if (!this.wasHeader) {
|
||||
if (!this.header) this.push({ columns: Object.keys(obj).map(columnName => ({ columnName })) });
|
||||
if (
|
||||
!obj.__isStreamHeader &&
|
||||
// TODO remove isArray test
|
||||
!Array.isArray(obj.columns)
|
||||
) {
|
||||
this.push({ columns: Object.keys(obj).map(columnName => ({ columnName })) });
|
||||
}
|
||||
|
||||
this.wasHeader = true;
|
||||
}
|
||||
if (!this.limitRows || this.rowsWritten < this.limitRows) {
|
||||
@@ -24,12 +30,12 @@ class ParseStream extends stream.Transform {
|
||||
}
|
||||
}
|
||||
|
||||
async function jsonLinesReader({ fileName, encoding = 'utf-8', header = true, limitRows = undefined }) {
|
||||
async function jsonLinesReader({ fileName, encoding = 'utf-8', limitRows = undefined }) {
|
||||
console.log(`Reading file ${fileName}`);
|
||||
|
||||
const fileStream = fs.createReadStream(fileName, encoding);
|
||||
const liner = byline(fileStream);
|
||||
const parser = new ParseStream({ header, limitRows });
|
||||
const parser = new ParseStream({ limitRows });
|
||||
liner.pipe(parser);
|
||||
return parser;
|
||||
}
|
||||
|
||||
@@ -8,10 +8,16 @@ class StringifyStream extends stream.Transform {
|
||||
this.wasHeader = false;
|
||||
}
|
||||
_transform(chunk, encoding, done) {
|
||||
let skip = false;
|
||||
if (!this.wasHeader) {
|
||||
if (this.header) this.push(JSON.stringify(chunk) + '\n');
|
||||
skip =
|
||||
(chunk.__isStreamHeader ||
|
||||
// TODO remove isArray test
|
||||
Array.isArray(chunk.columns)) &&
|
||||
!this.header;
|
||||
this.wasHeader = true;
|
||||
} else {
|
||||
}
|
||||
if (!skip) {
|
||||
this.push(JSON.stringify(chunk) + '\n');
|
||||
}
|
||||
done();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
const path = require('path');
|
||||
const { pluginsdir } = require('../utility/directories');
|
||||
const fs = require('fs');
|
||||
const { pluginsdir, packagedPluginsDir, getPluginBackendPath } = require('../utility/directories');
|
||||
const nativeModules = require('../nativeModules');
|
||||
const platformInfo = require('../utility/platformInfo');
|
||||
|
||||
const loadedPlugins = {};
|
||||
|
||||
@@ -8,20 +10,19 @@ const dbgateEnv = {
|
||||
dbgateApi: null,
|
||||
nativeModules,
|
||||
};
|
||||
|
||||
function requirePlugin(packageName, requiredPlugin = null) {
|
||||
if (!packageName) throw new Error('Missing packageName in plugin');
|
||||
if (loadedPlugins[packageName]) return loadedPlugins[packageName];
|
||||
|
||||
if (requiredPlugin == null) {
|
||||
let module;
|
||||
const modulePath = path.join(pluginsdir(), packageName, 'dist', 'backend.js');
|
||||
const modulePath = getPluginBackendPath(packageName);
|
||||
console.log(`Loading module ${packageName} from ${modulePath}`);
|
||||
try {
|
||||
// @ts-ignore
|
||||
module = __non_webpack_require__(modulePath);
|
||||
} catch (err) {
|
||||
console.error('Failed load webpacked module', err);
|
||||
// console.log('Failed load webpacked module', err.message);
|
||||
module = require(modulePath);
|
||||
}
|
||||
requiredPlugin = module.__esModule ? module.default : module;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const childProcessChecker = require('../utility/childProcessChecker');
|
||||
const processArgs = require('../utility/processArgs');
|
||||
|
||||
async function runScript(func) {
|
||||
if (process.argv.includes('--checkParent')) {
|
||||
if (processArgs.checkParent) {
|
||||
childProcessChecker();
|
||||
}
|
||||
try {
|
||||
|
||||
54
packages/api/src/shell/sqlDataWriter.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const fs = require('fs');
|
||||
const stream = require('stream');
|
||||
const path = require('path');
|
||||
const { driverBase } = require('dbgate-tools');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
|
||||
class SqlizeStream extends stream.Transform {
|
||||
constructor({ fileName }) {
|
||||
super({ objectMode: true });
|
||||
this.wasHeader = false;
|
||||
this.tableName = path.parse(fileName).name;
|
||||
this.driver = driverBase;
|
||||
}
|
||||
_transform(chunk, encoding, done) {
|
||||
let skip = false;
|
||||
if (!this.wasHeader) {
|
||||
if (
|
||||
chunk.__isStreamHeader ||
|
||||
// TODO remove isArray test
|
||||
Array.isArray(chunk.columns)
|
||||
) {
|
||||
skip = true;
|
||||
this.tableName = chunk.pureName;
|
||||
if (chunk.engine) {
|
||||
// @ts-ignore
|
||||
this.driver = requireEngineDriver(chunk.engine) || driverBase;
|
||||
}
|
||||
}
|
||||
this.wasHeader = true;
|
||||
}
|
||||
if (!skip) {
|
||||
const dmp = this.driver.createDumper();
|
||||
dmp.put(
|
||||
'^insert ^into %f (%,i) ^values (%,v);\n',
|
||||
{ pureName: this.tableName },
|
||||
Object.keys(chunk),
|
||||
Object.values(chunk)
|
||||
);
|
||||
this.push(dmp.s);
|
||||
}
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
async function sqlDataWriter({ fileName, driver, encoding = 'utf-8' }) {
|
||||
console.log(`Writing file ${fileName}`);
|
||||
const stringify = new SqlizeStream({ fileName });
|
||||
const fileStream = fs.createWriteStream(fileName, encoding);
|
||||
stringify.pipe(fileStream);
|
||||
stringify['finisher'] = fileStream;
|
||||
return stringify;
|
||||
}
|
||||
|
||||
module.exports = sqlDataWriter;
|
||||
@@ -1,6 +1,5 @@
|
||||
const { quoteFullName, fullNameToString } = require('dbgate-tools');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const { decryptConnection } = require('../utility/crypting');
|
||||
const connectUtility = require('../utility/connectUtility');
|
||||
|
||||
async function tableReader({ connection, pureName, schemaName }) {
|
||||
@@ -10,6 +9,13 @@ async function tableReader({ connection, pureName, schemaName }) {
|
||||
|
||||
const fullName = { pureName, schemaName };
|
||||
|
||||
if (driver.dialect.nosql) {
|
||||
// @ts-ignore
|
||||
console.log(`Reading collection ${fullNameToString(fullName)}`);
|
||||
// @ts-ignore
|
||||
return await driver.readQuery(pool, JSON.stringify(fullName));
|
||||
}
|
||||
|
||||
const table = await driver.analyseSingleObject(pool, fullName, 'tables');
|
||||
const query = `select * from ${quoteFullName(driver.dialect, fullName)}`;
|
||||
if (table) {
|
||||
|
||||
@@ -29,7 +29,7 @@ class DatastoreProxy {
|
||||
|
||||
async ensureSubprocess() {
|
||||
if (!this.subprocess) {
|
||||
this.subprocess = fork(process.argv[1], ['jslDatastoreProcess', ...process.argv.slice(3)]);
|
||||
this.subprocess = fork(process.argv[1], ['--start-process', 'jslDatastoreProcess', ...process.argv.slice(3)]);
|
||||
|
||||
this.subprocess.on('message', message => {
|
||||
// @ts-ignore
|
||||
|
||||
35
packages/api/src/utility/EnsureStreamHeaderStream.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const stream = require('stream');
|
||||
|
||||
class EnsureStreamHeaderStream extends stream.Transform {
|
||||
constructor() {
|
||||
super({ objectMode: true });
|
||||
this.wasHeader = false;
|
||||
}
|
||||
_transform(chunk, encoding, done) {
|
||||
if (!this.wasHeader) {
|
||||
if (chunk.__isDynamicStructure) {
|
||||
// ignore dynamic structure header
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!chunk.__isStreamHeader &&
|
||||
// TODO remove isArray test
|
||||
!Array.isArray(chunk.columns)
|
||||
) {
|
||||
this.push({
|
||||
__isStreamHeader: true,
|
||||
__isComputedStructure: true,
|
||||
columns: Object.keys(chunk).map(columnName => ({ columnName })),
|
||||
});
|
||||
}
|
||||
|
||||
this.wasHeader = true;
|
||||
}
|
||||
this.push(chunk);
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = EnsureStreamHeaderStream;
|
||||
@@ -7,6 +7,7 @@ const { getSshTunnelProxy } = require('./sshTunnelProxy');
|
||||
|
||||
async function connectUtility(driver, storedConnection) {
|
||||
const connection = {
|
||||
database: storedConnection.defaultDatabase,
|
||||
...decryptConnection(storedConnection),
|
||||
};
|
||||
|
||||
@@ -38,6 +39,10 @@ async function connectUtility(driver, storedConnection) {
|
||||
connection.ssl.key = await fs.readFile(connection.sslKeyFile);
|
||||
}
|
||||
|
||||
if (connection.sslCertFilePassword) {
|
||||
connection.ssl.password = connection.sslCertFilePassword;
|
||||
}
|
||||
|
||||
if (!connection.ssl.key && !connection.ssl.ca && !connection.ssl.cert) {
|
||||
// TODO: provide this as an option in settings
|
||||
// or per-connection as 'reject self-signed certs'
|
||||
|
||||
@@ -2,6 +2,7 @@ const os = require('os');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const cleanDirectory = require('./cleanDirectory');
|
||||
const platformInfo = require('./platformInfo');
|
||||
|
||||
const createDirectories = {};
|
||||
const ensureDirectory = (dir, clean) => {
|
||||
@@ -39,6 +40,37 @@ const pluginsdir = dirFunc('plugins');
|
||||
const archivedir = dirFunc('archive');
|
||||
const filesdir = dirFunc('files');
|
||||
|
||||
function packagedPluginsDir() {
|
||||
if (platformInfo.isDevMode) {
|
||||
return path.resolve(__dirname, '../../../../plugins');
|
||||
}
|
||||
if (platformInfo.isDocker) {
|
||||
return '/home/dbgate-docker/plugins';
|
||||
}
|
||||
if (platformInfo.isNpmDist) {
|
||||
// node_modules
|
||||
return global['dbgateApiPackagedPluginsPath'];
|
||||
}
|
||||
if (platformInfo.isElectronBundle) {
|
||||
return path.resolve(__dirname, '../../plugins');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const packagedPluginList =
|
||||
packagedPluginsDir() != null ? fs.readdirSync(packagedPluginsDir()).filter(x => x.startsWith('dbgate-plugin-')) : [];
|
||||
|
||||
function getPluginBackendPath(packageName) {
|
||||
if (packagedPluginList.includes(packageName)) {
|
||||
if (platformInfo.isDevMode) {
|
||||
return path.join(packagedPluginsDir(), packageName, 'src', 'backend', 'index.js');
|
||||
}
|
||||
return path.join(packagedPluginsDir(), packageName, 'dist', 'backend.js');
|
||||
}
|
||||
|
||||
return path.join(pluginsdir(), packageName, 'dist', 'backend.js');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
datadir,
|
||||
jsldir,
|
||||
@@ -48,4 +80,7 @@ module.exports = {
|
||||
ensureDirectory,
|
||||
pluginsdir,
|
||||
filesdir,
|
||||
packagedPluginsDir,
|
||||
packagedPluginList,
|
||||
getPluginBackendPath,
|
||||
};
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
function goSplit(sql) {
|
||||
if (!sql) return [];
|
||||
const lines = sql.split('\n');
|
||||
const res = [];
|
||||
let buffer = '';
|
||||
for (const line of lines) {
|
||||
if (/^\s*go\s*$/i.test(line)) {
|
||||
if (buffer.trim()) res.push(buffer);
|
||||
buffer = '';
|
||||
} else {
|
||||
buffer += line + '\n';
|
||||
}
|
||||
}
|
||||
if (buffer.trim()) res.push(buffer);
|
||||
return res;
|
||||
}
|
||||
|
||||
module.exports = goSplit;
|
||||
@@ -1,26 +1,42 @@
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const processArgs = require('./processArgs');
|
||||
|
||||
const p = process;
|
||||
const platform = p.env.OS_OVERRIDE ? p.env.OS_OVERRIDE : p.platform;
|
||||
const platform = process.env.OS_OVERRIDE ? process.env.OS_OVERRIDE : process.platform;
|
||||
const isWindows = platform === 'win32';
|
||||
const isMac = platform === 'darwin';
|
||||
const isLinux = platform === 'linux';
|
||||
const isDocker = fs.existsSync('/home/dbgate-docker/build');
|
||||
const isDocker = fs.existsSync('/home/dbgate-docker/public');
|
||||
const isDevMode = process.env.DEVMODE == '1';
|
||||
const isNpmDist = !!global['dbgateApiModulePath'];
|
||||
|
||||
// function moduleAvailable(name) {
|
||||
// try {
|
||||
// require.resolve(name);
|
||||
// return true;
|
||||
// } catch (e) {
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
|
||||
const isElectronBundle = processArgs.isElectronBundle;
|
||||
|
||||
const platformInfo = {
|
||||
isWindows,
|
||||
isMac,
|
||||
isLinux,
|
||||
isDocker,
|
||||
isSnap: p.env.ELECTRON_SNAP == 'true',
|
||||
isPortable: isWindows && p.env.PORTABLE_EXECUTABLE_DIR,
|
||||
isAppImage: p.env.DESKTOPINTEGRATION === 'AppImageLauncher',
|
||||
sshAuthSock: p.env.SSH_AUTH_SOCK,
|
||||
isElectronBundle,
|
||||
isDevMode,
|
||||
isNpmDist,
|
||||
isSnap: process.env.ELECTRON_SNAP == 'true',
|
||||
isPortable: isWindows && process.env.PORTABLE_EXECUTABLE_DIR,
|
||||
isAppImage: process.env.DESKTOPINTEGRATION === 'AppImageLauncher',
|
||||
sshAuthSock: process.env.SSH_AUTH_SOCK,
|
||||
environment: process.env.NODE_ENV,
|
||||
platform,
|
||||
runningInWebpack: !!p.env.WEBPACK_DEV_SERVER_URL,
|
||||
runningInWebpack: !!process.env.WEBPACK_DEV_SERVER_URL,
|
||||
defaultKeyFile: path.join(os.homedir(), '.ssh/id_rsa'),
|
||||
};
|
||||
|
||||
|
||||
21
packages/api/src/utility/processArgs.js
Normal file
@@ -0,0 +1,21 @@
|
||||
function getNamedArg(name) {
|
||||
const argIndex = process.argv.indexOf(name);
|
||||
if (argIndex > 0) {
|
||||
return process.argv[argIndex + 1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const checkParent = process.argv.includes('--checkParent');
|
||||
const dynport = process.argv.includes('--dynport');
|
||||
const nativeModules = getNamedArg('--native-modules');
|
||||
const startProcess = getNamedArg('--start-process');
|
||||
const isElectronBundle = process.argv.includes('--is-electron-bundle');
|
||||
|
||||
module.exports = {
|
||||
checkParent,
|
||||
nativeModules,
|
||||
startProcess,
|
||||
dynport,
|
||||
isElectronBundle,
|
||||
};
|
||||
@@ -15,7 +15,7 @@ function requireEngineDriver(connection) {
|
||||
if (engine.includes('@')) {
|
||||
const [shortName, packageName] = engine.split('@');
|
||||
const plugin = requirePlugin(packageName);
|
||||
return plugin.driver;
|
||||
return plugin.drivers.find(x => x.engine == engine);
|
||||
}
|
||||
throw new Error(`Could not found engine driver ${engine}`);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ const portfinder = require('portfinder');
|
||||
const stableStringify = require('json-stable-stringify');
|
||||
const _ = require('lodash');
|
||||
const platformInfo = require('./platformInfo');
|
||||
const AsyncLock = require('async-lock');
|
||||
const lock = new AsyncLock();
|
||||
|
||||
const sshConnectionCache = {};
|
||||
const sshTunnelCache = {};
|
||||
@@ -34,7 +36,7 @@ async function getSshConnection(connection) {
|
||||
password: connection.sshMode == 'userPassword' ? connection.sshPassword : undefined,
|
||||
agentSocket: connection.sshMode == 'agent' ? platformInfo.sshAuthSock : undefined,
|
||||
privateKey:
|
||||
connection.sshMode == 'keyFile' && connection.sshKeyFile ? await fs.readFile(connection.sshKeyFile) : undefined,
|
||||
connection.sshMode == 'keyFile' && connection.sshKeyfile ? await fs.readFile(connection.sshKeyfile) : undefined,
|
||||
skipAutoPrivateKey: true,
|
||||
noReadline: true,
|
||||
};
|
||||
@@ -45,36 +47,43 @@ async function getSshConnection(connection) {
|
||||
}
|
||||
|
||||
async function getSshTunnel(connection) {
|
||||
const sshConn = await getSshConnection(connection);
|
||||
const tunnelCacheKey = stableStringify(_.pick(connection, TUNNEL_FIELDS));
|
||||
if (sshTunnelCache[tunnelCacheKey]) return sshTunnelCache[tunnelCacheKey];
|
||||
|
||||
const localPort = await portfinder.getPortPromise({ port: 10000, stopPort: 60000 });
|
||||
// workaround for `getPortPromise` not releasing the port quickly enough
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const tunnelConfig = {
|
||||
fromPort: localPort,
|
||||
toPort: connection.port,
|
||||
toHost: connection.server,
|
||||
};
|
||||
try {
|
||||
const tunnel = await sshConn.forward(tunnelConfig);
|
||||
console.log(
|
||||
`Created SSH tunnel to ${connection.sshHost}-${connection.server}:${connection.port}, using local port ${localPort}`
|
||||
);
|
||||
return await lock.acquire(tunnelCacheKey, async () => {
|
||||
const sshConn = await getSshConnection(connection);
|
||||
if (sshTunnelCache[tunnelCacheKey]) return sshTunnelCache[tunnelCacheKey];
|
||||
|
||||
sshTunnelCache[tunnelCacheKey] = {
|
||||
state: 'ok',
|
||||
localPort,
|
||||
const localPort = await portfinder.getPortPromise({ port: 10000, stopPort: 60000 });
|
||||
// workaround for `getPortPromise` not releasing the port quickly enough
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const tunnelConfig = {
|
||||
fromPort: localPort,
|
||||
toPort: connection.port,
|
||||
toHost: connection.server,
|
||||
};
|
||||
return sshTunnelCache[tunnelCacheKey];
|
||||
} catch (err) {
|
||||
// error is not cached
|
||||
return {
|
||||
state: 'error',
|
||||
message: err.message,
|
||||
};
|
||||
}
|
||||
try {
|
||||
console.log(
|
||||
`Creating SSH tunnel to ${connection.sshHost}-${connection.server}:${connection.port}, using local port ${localPort}`
|
||||
);
|
||||
|
||||
const tunnel = await sshConn.forward(tunnelConfig);
|
||||
console.log(
|
||||
`Created SSH tunnel to ${connection.sshHost}-${connection.server}:${connection.port}, using local port ${localPort}`
|
||||
);
|
||||
|
||||
sshTunnelCache[tunnelCacheKey] = {
|
||||
state: 'ok',
|
||||
localPort,
|
||||
};
|
||||
return sshTunnelCache[tunnelCacheKey];
|
||||
} catch (err) {
|
||||
// error is not cached
|
||||
return {
|
||||
state: 'error',
|
||||
message: err.message,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
9
packages/api/src/utility/timingSafeCheckToken.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const crypto = require('crypto');
|
||||
|
||||
function timingSafeCheckToken(a, b) {
|
||||
if (!a || !b) return false;
|
||||
if (a.length != b.length) return false;
|
||||
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
||||
}
|
||||
|
||||
module.exports = timingSafeCheckToken;
|
||||
@@ -1,10 +1,9 @@
|
||||
{
|
||||
"version": "3.9.5",
|
||||
"version": "4.1.1",
|
||||
"name": "dbgate-datalib",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
"scripts": {
|
||||
"prepare": "yarn build",
|
||||
"build": "tsc",
|
||||
"start": "tsc --watch"
|
||||
},
|
||||
@@ -12,11 +11,11 @@
|
||||
"lib"
|
||||
],
|
||||
"dependencies": {
|
||||
"dbgate-sqltree": "^3.9.5",
|
||||
"dbgate-filterparser": "^3.9.5"
|
||||
"dbgate-sqltree": "^4.1.1",
|
||||
"dbgate-filterparser": "^4.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dbgate-types": "^3.9.5",
|
||||
"dbgate-types": "^4.1.1",
|
||||
"@types/node": "^13.7.0",
|
||||
"typescript": "^3.7.5"
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface ChangeSetItem {
|
||||
pureName: string;
|
||||
schemaName?: string;
|
||||
insertedRowIndex?: number;
|
||||
document?: any;
|
||||
condition?: { [column: string]: string };
|
||||
fields?: { [column: string]: string };
|
||||
}
|
||||
@@ -117,6 +118,43 @@ export function setChangeSetValue(
|
||||
};
|
||||
}
|
||||
|
||||
export function setChangeSetRowData(changeSet: ChangeSet, definition: ChangeSetRowDefinition, document: any): ChangeSet {
|
||||
if (!changeSet || !definition) return changeSet;
|
||||
let [fieldName, existingItem] = findExistingChangeSetItem(changeSet, definition);
|
||||
if (fieldName == 'deletes') {
|
||||
changeSet = revertChangeSetRowChanges(changeSet, definition);
|
||||
[fieldName, existingItem] = findExistingChangeSetItem(changeSet, definition);
|
||||
}
|
||||
if (existingItem) {
|
||||
return {
|
||||
...changeSet,
|
||||
[fieldName]: changeSet[fieldName].map(item =>
|
||||
item == existingItem
|
||||
? {
|
||||
...item,
|
||||
fields: {},
|
||||
document,
|
||||
}
|
||||
: item
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...changeSet,
|
||||
[fieldName]: [
|
||||
...changeSet[fieldName],
|
||||
{
|
||||
pureName: definition.pureName,
|
||||
schemaName: definition.schemaName,
|
||||
condition: definition.condition,
|
||||
insertedRowIndex: definition.insertedRowIndex,
|
||||
document,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// export function batchUpdateChangeSet(
|
||||
// changeSet: ChangeSet,
|
||||
// rowDefinitions: ChangeSetRowDefinition[],
|
||||
@@ -332,6 +370,7 @@ export function getChangeSetInsertedRows(changeSet: ChangeSet, name?: NamedObjec
|
||||
}
|
||||
|
||||
export function changeSetInsertNewRow(changeSet: ChangeSet, name?: NamedObjectInfo): ChangeSet {
|
||||
console.log('INSERT', name);
|
||||
const insertedRows = getChangeSetInsertedRows(changeSet, name);
|
||||
return {
|
||||
...changeSet,
|
||||
@@ -347,5 +386,6 @@ export function changeSetInsertNewRow(changeSet: ChangeSet, name?: NamedObjectIn
|
||||
}
|
||||
|
||||
export function changeSetContainsChanges(changeSet: ChangeSet) {
|
||||
if (!changeSet) return false;
|
||||
return changeSet.deletes.length > 0 || changeSet.updates.length > 0 || changeSet.inserts.length > 0;
|
||||
}
|
||||
|
||||
104
packages/datalib/src/CollectionGridDisplay.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import _ from 'lodash';
|
||||
import { GridDisplay, ChangeCacheFunc, ChangeConfigFunc, DisplayColumn } from './GridDisplay';
|
||||
import { EngineDriver, ViewInfo, ColumnInfo, CollectionInfo } from 'dbgate-types';
|
||||
import { GridConfig, GridCache } from './GridConfig';
|
||||
|
||||
function getObjectKeys(obj) {
|
||||
if (_.isArray(obj)) {
|
||||
return Object.keys(obj)
|
||||
.slice(0, 10)
|
||||
.map(x => parseInt(x));
|
||||
}
|
||||
if (_.isPlainObject(obj)) {
|
||||
return Object.keys(obj);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function createHeaderText(path) {
|
||||
let res = `${path[0]}`;
|
||||
for (let i = 1; i < path.length; i++) {
|
||||
const name = path[i];
|
||||
if (_.isNumber(name)) res += `[${name}]`;
|
||||
else res += `.${name}`;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function getColumnsForObject(basePath, obj, res: any[], display) {
|
||||
for (const name of getObjectKeys(obj)) {
|
||||
const uniqueName = [...basePath, name].join('.');
|
||||
let column = res.find(x => x.uniqueName == uniqueName);
|
||||
if (!column) {
|
||||
column = getDisplayColumn(basePath, name, display);
|
||||
if (basePath.length > 0) {
|
||||
const lastIndex1 = _.findLastIndex(res, x => x.parentHeaderText.startsWith(column.parentHeaderText));
|
||||
const lastIndex2 = _.findLastIndex(res, x => x.headerText == column.parentHeaderText);
|
||||
// console.log(uniqueName, lastIndex1, lastIndex2);
|
||||
if (lastIndex1 >= 0) res.splice(lastIndex1 + 1, 0, column);
|
||||
else if (lastIndex2 >= 0) res.splice(lastIndex2 + 1, 0, column);
|
||||
else res.push(column);
|
||||
} else {
|
||||
res.push(column);
|
||||
}
|
||||
}
|
||||
if (_.isPlainObject(obj[name]) || _.isArray(obj[name])) {
|
||||
column.isExpandable = true;
|
||||
}
|
||||
|
||||
if (display.isExpandedColumn(column.uniqueName)) {
|
||||
getColumnsForObject([...basePath, name], obj[name], res, display);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getDisplayColumn(basePath, columnName, display) {
|
||||
const uniquePath = [...basePath, columnName];
|
||||
const uniqueName = uniquePath.join('.');
|
||||
return {
|
||||
columnName,
|
||||
headerText: createHeaderText(uniquePath),
|
||||
uniqueName,
|
||||
uniquePath,
|
||||
isStructured: true,
|
||||
parentHeaderText: createHeaderText(basePath),
|
||||
filterType: 'mongo',
|
||||
pureName: display.collection?.pureName,
|
||||
schemaName: display.collection?.schemaName,
|
||||
};
|
||||
}
|
||||
|
||||
export function analyseCollectionDisplayColumns(rows, display) {
|
||||
const res = [];
|
||||
for (const row of rows || []) {
|
||||
getColumnsForObject([], row, res, display);
|
||||
}
|
||||
return (
|
||||
res.map(col => ({
|
||||
...col,
|
||||
isChecked: display.isColumnChecked(col),
|
||||
})) || []
|
||||
);
|
||||
}
|
||||
|
||||
export class CollectionGridDisplay extends GridDisplay {
|
||||
constructor(
|
||||
public collection: CollectionInfo,
|
||||
driver: EngineDriver,
|
||||
config: GridConfig,
|
||||
setConfig: ChangeConfigFunc,
|
||||
cache: GridCache,
|
||||
setCache: ChangeCacheFunc,
|
||||
loadedRows
|
||||
) {
|
||||
super(config, setConfig, cache, setCache, driver);
|
||||
this.columns = analyseCollectionDisplayColumns(loadedRows, this);
|
||||
this.filterable = true;
|
||||
this.sortable = true;
|
||||
this.editable = true;
|
||||
this.supportsReload = true;
|
||||
this.isDynamicStructure = true;
|
||||
this.changeSetKeyFields = ['_id'];
|
||||
this.baseCollection = collection;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,14 @@
|
||||
import _ from 'lodash';
|
||||
import { GridConfig, GridCache, GridConfigColumns, createGridCache, GroupFunc } from './GridConfig';
|
||||
import { ForeignKeyInfo, TableInfo, ColumnInfo, EngineDriver, NamedObjectInfo, DatabaseInfo } from 'dbgate-types';
|
||||
import { parseFilter, getFilterType, getFilterValueExpression } from 'dbgate-filterparser';
|
||||
import { filterName } from './filterName';
|
||||
import { ChangeSetFieldDefinition, ChangeSetRowDefinition } from './ChangeSet';
|
||||
import { Expression, Select, treeToSql, dumpSqlSelect, Condition } from 'dbgate-sqltree';
|
||||
import { isTypeLogical } from 'dbgate-tools';
|
||||
import { TableInfo, EngineDriver, DatabaseInfo, SqlDialect } from 'dbgate-types';
|
||||
import { getFilterValueExpression } from 'dbgate-filterparser';
|
||||
import { ChangeCacheFunc, ChangeConfigFunc, DisplayColumn } from './GridDisplay';
|
||||
|
||||
export class FormViewDisplay {
|
||||
isLoadedCorrectly = true;
|
||||
columns: DisplayColumn[];
|
||||
public baseTable: TableInfo;
|
||||
dialect: SqlDialect;
|
||||
|
||||
constructor(
|
||||
public config: GridConfig,
|
||||
@@ -19,8 +16,11 @@ export class FormViewDisplay {
|
||||
public cache: GridCache,
|
||||
protected setCache: ChangeCacheFunc,
|
||||
public driver?: EngineDriver,
|
||||
public dbinfo: DatabaseInfo = null
|
||||
) {}
|
||||
public dbinfo: DatabaseInfo = null,
|
||||
public serverVersion = null
|
||||
) {
|
||||
this.dialect = (driver?.dialectByVersion && driver?.dialectByVersion(serverVersion)) || driver?.dialect;
|
||||
}
|
||||
|
||||
addFilterColumn(column) {
|
||||
if (!column) return;
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface GridConfig extends GridConfigColumns {
|
||||
formViewKey?: { [uniqueName: string]: string };
|
||||
formViewKeyRequested?: { [uniqueName: string]: string };
|
||||
formFilterColumns: string[];
|
||||
formColumnFilterText?: string;
|
||||
}
|
||||
|
||||
export interface GridCache {
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import _ from 'lodash';
|
||||
import { GridConfig, GridCache, GridConfigColumns, createGridCache, GroupFunc } from './GridConfig';
|
||||
import { ForeignKeyInfo, TableInfo, ColumnInfo, EngineDriver, NamedObjectInfo, DatabaseInfo } from 'dbgate-types';
|
||||
import {
|
||||
ForeignKeyInfo,
|
||||
TableInfo,
|
||||
ColumnInfo,
|
||||
EngineDriver,
|
||||
NamedObjectInfo,
|
||||
DatabaseInfo,
|
||||
CollectionInfo,
|
||||
SqlDialect,
|
||||
} from 'dbgate-types';
|
||||
import { parseFilter, getFilterType } from 'dbgate-filterparser';
|
||||
import { filterName } from './filterName';
|
||||
import { filterName } from 'dbgate-tools';
|
||||
import { ChangeSetFieldDefinition, ChangeSetRowDefinition } from './ChangeSet';
|
||||
import { Expression, Select, treeToSql, dumpSqlSelect, Condition } from 'dbgate-sqltree';
|
||||
import { isTypeLogical } from 'dbgate-tools';
|
||||
@@ -18,9 +27,12 @@ export interface DisplayColumn {
|
||||
autoIncrement?: boolean;
|
||||
isPrimaryKey?: boolean;
|
||||
foreignKey?: ForeignKeyInfo;
|
||||
isExpandable?: boolean;
|
||||
isChecked?: boolean;
|
||||
hintColumnName?: string;
|
||||
dataType?: string;
|
||||
filterType?: boolean;
|
||||
isStructured?: boolean;
|
||||
}
|
||||
|
||||
export interface DisplayedColumnEx extends DisplayColumn {
|
||||
@@ -49,16 +61,26 @@ export abstract class GridDisplay {
|
||||
public cache: GridCache,
|
||||
protected setCache: ChangeCacheFunc,
|
||||
public driver?: EngineDriver,
|
||||
public dbinfo: DatabaseInfo = null
|
||||
) {}
|
||||
public dbinfo: DatabaseInfo = null,
|
||||
public serverVersion = null
|
||||
) {
|
||||
this.dialect = (driver?.dialectByVersion && driver?.dialectByVersion(serverVersion)) || driver?.dialect;
|
||||
}
|
||||
dialect: SqlDialect;
|
||||
columns: DisplayColumn[];
|
||||
baseTable?: TableInfo;
|
||||
baseCollection?: CollectionInfo;
|
||||
get baseTableOrCollection(): NamedObjectInfo {
|
||||
return this.baseTable || this.baseCollection;
|
||||
}
|
||||
changeSetKeyFields: string[] = null;
|
||||
sortable = false;
|
||||
groupable = false;
|
||||
filterable = false;
|
||||
editable = false;
|
||||
isLoadedCorrectly = true;
|
||||
supportsReload = false;
|
||||
isDynamicStructure = false;
|
||||
|
||||
setColumnVisibility(uniquePath: string[], isVisible: boolean) {
|
||||
const uniqueName = uniquePath.join('.');
|
||||
@@ -66,7 +88,7 @@ export abstract class GridDisplay {
|
||||
this.includeInColumnSet('hiddenColumns', uniqueName, !isVisible);
|
||||
} else {
|
||||
this.includeInColumnSet('addedColumns', uniqueName, isVisible);
|
||||
this.reload();
|
||||
if (!this.isDynamicStructure) this.reload();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,11 +116,7 @@ export abstract class GridDisplay {
|
||||
}
|
||||
|
||||
reload() {
|
||||
this.setCache(cache => ({
|
||||
// ...cache,
|
||||
...createGridCache(),
|
||||
refreshTime: new Date().getTime(),
|
||||
}));
|
||||
this.setCache(reloadDataCacheFunc);
|
||||
}
|
||||
|
||||
includeInColumnSet(field: keyof GridConfigColumns, uniqueName: string, isIncluded: boolean) {
|
||||
@@ -271,8 +289,8 @@ export abstract class GridDisplay {
|
||||
return this.config.expandedColumns.includes(uniqueName);
|
||||
}
|
||||
|
||||
toggleExpandedColumn(uniqueName: string) {
|
||||
this.includeInColumnSet('expandedColumns', uniqueName, !this.isExpandedColumn(uniqueName));
|
||||
toggleExpandedColumn(uniqueName: string, value?: boolean) {
|
||||
this.includeInColumnSet('expandedColumns', uniqueName, value == null ? !this.isExpandedColumn(uniqueName) : value);
|
||||
}
|
||||
|
||||
getFilter(uniqueName: string) {
|
||||
@@ -290,6 +308,28 @@ export abstract class GridDisplay {
|
||||
this.reload();
|
||||
}
|
||||
|
||||
showFilter(uniqueName) {
|
||||
this.setConfig(cfg => {
|
||||
if (!cfg.filters.uniqueName)
|
||||
return {
|
||||
...cfg,
|
||||
filters: {
|
||||
..._.omitBy(cfg.filters, v => !v),
|
||||
[uniqueName]: '',
|
||||
},
|
||||
};
|
||||
return cfg;
|
||||
});
|
||||
}
|
||||
|
||||
removeFilter(uniqueName) {
|
||||
this.setConfig(cfg => ({
|
||||
...cfg,
|
||||
filters: _.omit(cfg.filters, [uniqueName]),
|
||||
}));
|
||||
this.reload();
|
||||
}
|
||||
|
||||
setFilters(dct) {
|
||||
this.setConfig(cfg => ({
|
||||
...cfg,
|
||||
@@ -365,8 +405,12 @@ export abstract class GridDisplay {
|
||||
getChangeSetField(row, uniqueName, insertedRowIndex): ChangeSetFieldDefinition {
|
||||
const col = this.columns.find(x => x.uniqueName == uniqueName);
|
||||
if (!col) return null;
|
||||
if (!this.baseTable) return null;
|
||||
if (this.baseTable.pureName != col.pureName || this.baseTable.schemaName != col.schemaName) return null;
|
||||
const baseTableOrCollection = this.baseTableOrCollection;
|
||||
if (!baseTableOrCollection) return null;
|
||||
if (baseTableOrCollection.pureName != col.pureName || baseTableOrCollection.schemaName != col.schemaName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...this.getChangeSetRow(row, insertedRowIndex),
|
||||
uniqueName: uniqueName,
|
||||
@@ -375,10 +419,11 @@ export abstract class GridDisplay {
|
||||
}
|
||||
|
||||
getChangeSetRow(row, insertedRowIndex): ChangeSetRowDefinition {
|
||||
if (!this.baseTable) return null;
|
||||
const baseTableOrCollection = this.baseTableOrCollection;
|
||||
if (!baseTableOrCollection) return null;
|
||||
return {
|
||||
pureName: this.baseTable.pureName,
|
||||
schemaName: this.baseTable.schemaName,
|
||||
pureName: baseTableOrCollection.pureName,
|
||||
schemaName: baseTableOrCollection.schemaName,
|
||||
insertedRowIndex,
|
||||
condition: insertedRowIndex == null ? this.getChangeSetCondition(row) : null,
|
||||
};
|
||||
@@ -421,12 +466,75 @@ export abstract class GridDisplay {
|
||||
return select;
|
||||
}
|
||||
|
||||
getRowNumberOverSelect(select: Select, offset: number, count: number): Select {
|
||||
const innerSelect: Select = {
|
||||
commandType: 'select',
|
||||
from: select.from,
|
||||
where: select.where,
|
||||
columns: [
|
||||
...select.columns,
|
||||
{
|
||||
alias: '_rowNumber',
|
||||
exprType: 'rowNumber',
|
||||
orderBy: select.orderBy
|
||||
? select.orderBy.map(x =>
|
||||
x.exprType != 'column'
|
||||
? x
|
||||
: x.source
|
||||
? x
|
||||
: {
|
||||
...x,
|
||||
source: { alias: 'basetbl' },
|
||||
}
|
||||
)
|
||||
: [
|
||||
{
|
||||
...select.columns[0],
|
||||
direction: 'ASC',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const res: Select = {
|
||||
commandType: 'select',
|
||||
selectAll: true,
|
||||
from: {
|
||||
subQuery: innerSelect,
|
||||
alias: '_RowNumberResult',
|
||||
},
|
||||
where: {
|
||||
conditionType: 'between',
|
||||
expr: {
|
||||
exprType: 'column',
|
||||
columnName: '_RowNumber',
|
||||
source: {
|
||||
alias: '_RowNumberResult',
|
||||
},
|
||||
},
|
||||
left: {
|
||||
exprType: 'value',
|
||||
value: offset + 1,
|
||||
},
|
||||
right: {
|
||||
exprType: 'value',
|
||||
value: offset + count,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
getPageQuery(offset: number, count: number) {
|
||||
if (!this.driver) return null;
|
||||
const select = this.createSelect();
|
||||
let select = this.createSelect();
|
||||
if (!select) return null;
|
||||
if (this.driver.dialect.rangeSelect) select.range = { offset: offset, limit: count };
|
||||
else if (this.driver.dialect.limitSelect) select.topRecords = count;
|
||||
if (this.dialect.rangeSelect) select.range = { offset: offset, limit: count };
|
||||
else if (this.dialect.rowNumberOverPaging && offset > 0)
|
||||
select = this.getRowNumberOverSelect(select, offset, count);
|
||||
else if (this.dialect.limitSelect) select.topRecords = count;
|
||||
const sql = treeToSql(this.driver, select, dumpSqlSelect);
|
||||
return sql;
|
||||
}
|
||||
@@ -535,4 +643,19 @@ export abstract class GridDisplay {
|
||||
formViewKeyRequested: null,
|
||||
}));
|
||||
}
|
||||
|
||||
switchToJsonView() {
|
||||
this.setConfig(cfg => ({
|
||||
...cfg,
|
||||
isJsonView: true,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export function reloadDataCacheFunc(cache: GridCache): GridCache {
|
||||
return {
|
||||
// ...cache,
|
||||
...createGridCache(),
|
||||
refreshTime: new Date().getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,34 +1,44 @@
|
||||
import { GridDisplay, ChangeCacheFunc, ChangeConfigFunc } from './GridDisplay';
|
||||
import { QueryResultColumn } from 'dbgate-types';
|
||||
import { GridConfig, GridCache } from './GridConfig';
|
||||
import { analyseCollectionDisplayColumns } from './CollectionGridDisplay';
|
||||
|
||||
export class JslGridDisplay extends GridDisplay {
|
||||
constructor(
|
||||
jslid,
|
||||
columns: QueryResultColumn[],
|
||||
structure,
|
||||
config: GridConfig,
|
||||
setConfig: ChangeConfigFunc,
|
||||
cache: GridCache,
|
||||
setCache: ChangeCacheFunc
|
||||
setCache: ChangeCacheFunc,
|
||||
rows: any
|
||||
) {
|
||||
super(config, setConfig, cache, setCache, null);
|
||||
|
||||
this.filterable = true;
|
||||
|
||||
this.columns = columns
|
||||
.map(col => ({
|
||||
columnName: col.columnName,
|
||||
headerText: col.columnName,
|
||||
uniqueName: col.columnName,
|
||||
uniquePath: [col.columnName],
|
||||
notNull: col.notNull,
|
||||
autoIncrement: col.autoIncrement,
|
||||
pureName: null,
|
||||
schemaName: null,
|
||||
}))
|
||||
?.map(col => ({
|
||||
...col,
|
||||
isChecked: this.isColumnChecked(col),
|
||||
}));
|
||||
if (structure.columns) {
|
||||
this.columns = structure.columns
|
||||
.map(col => ({
|
||||
columnName: col.columnName,
|
||||
headerText: col.columnName,
|
||||
uniqueName: col.columnName,
|
||||
uniquePath: [col.columnName],
|
||||
notNull: col.notNull,
|
||||
autoIncrement: col.autoIncrement,
|
||||
pureName: null,
|
||||
schemaName: null,
|
||||
}))
|
||||
?.map(col => ({
|
||||
...col,
|
||||
isChecked: this.isColumnChecked(col),
|
||||
}));
|
||||
}
|
||||
|
||||
if (structure.__isDynamicStructure) {
|
||||
this.columns = analyseCollectionDisplayColumns(rows, this);
|
||||
}
|
||||
|
||||
if (!this.columns) this.columns = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,4 +19,6 @@ export interface MacroDefinition {
|
||||
export interface MacroSelectedCell {
|
||||
column: string;
|
||||
row: number;
|
||||
rowData: any;
|
||||
value: any;
|
||||
}
|
||||
|
||||
@@ -12,13 +12,12 @@ import {
|
||||
Condition,
|
||||
OrderByExpression,
|
||||
} from 'dbgate-sqltree';
|
||||
import { filterName } from './filterName';
|
||||
import { TableGridDisplay } from './TableGridDisplay';
|
||||
import stableStringify from 'json-stable-stringify';
|
||||
import { ChangeSetFieldDefinition, ChangeSetRowDefinition } from './ChangeSet';
|
||||
|
||||
export class TableFormViewDisplay extends FormViewDisplay {
|
||||
// use utility functions from GridDisplay and publish result in FromViewDisplat interface
|
||||
// use utility functions from GridDisplay and publish result in FromViewDisplay interface
|
||||
private gridDisplay: TableGridDisplay;
|
||||
|
||||
constructor(
|
||||
@@ -28,10 +27,22 @@ export class TableFormViewDisplay extends FormViewDisplay {
|
||||
setConfig: ChangeConfigFunc,
|
||||
cache: GridCache,
|
||||
setCache: ChangeCacheFunc,
|
||||
dbinfo: DatabaseInfo
|
||||
dbinfo: DatabaseInfo,
|
||||
displayOptions,
|
||||
serverVersion
|
||||
) {
|
||||
super(config, setConfig, cache, setCache, driver, dbinfo);
|
||||
this.gridDisplay = new TableGridDisplay(tableName, driver, config, setConfig, cache, setCache, dbinfo);
|
||||
super(config, setConfig, cache, setCache, driver, dbinfo, serverVersion);
|
||||
this.gridDisplay = new TableGridDisplay(
|
||||
tableName,
|
||||
driver,
|
||||
config,
|
||||
setConfig,
|
||||
cache,
|
||||
setCache,
|
||||
dbinfo,
|
||||
displayOptions,
|
||||
serverVersion
|
||||
);
|
||||
this.gridDisplay.addAllExpandedColumnsToSelected = true;
|
||||
|
||||
this.isLoadedCorrectly = this.gridDisplay.isLoadedCorrectly && !!this.driver;
|
||||
@@ -253,8 +264,8 @@ export class TableFormViewDisplay extends FormViewDisplay {
|
||||
};
|
||||
}
|
||||
|
||||
toggleExpandedColumn(uniqueName: string) {
|
||||
this.gridDisplay.toggleExpandedColumn(uniqueName);
|
||||
toggleExpandedColumn(uniqueName: string, value?: boolean) {
|
||||
this.gridDisplay.toggleExpandedColumn(uniqueName, value);
|
||||
this.gridDisplay.reload();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import _ from 'lodash';
|
||||
import { filterName } from 'dbgate-tools';
|
||||
import { GridDisplay, ChangeCacheFunc, DisplayColumn, DisplayedColumnInfo, ChangeConfigFunc } from './GridDisplay';
|
||||
import { TableInfo, EngineDriver, ViewInfo, ColumnInfo, NamedObjectInfo, DatabaseInfo } from 'dbgate-types';
|
||||
import { GridConfig, GridCache, createGridCache } from './GridConfig';
|
||||
import { Expression, Select, treeToSql, dumpSqlSelect } from 'dbgate-sqltree';
|
||||
import { filterName } from './filterName';
|
||||
|
||||
export class TableGridDisplay extends GridDisplay {
|
||||
public table: TableInfo;
|
||||
@@ -17,9 +17,11 @@ export class TableGridDisplay extends GridDisplay {
|
||||
setConfig: ChangeConfigFunc,
|
||||
cache: GridCache,
|
||||
setCache: ChangeCacheFunc,
|
||||
dbinfo: DatabaseInfo
|
||||
dbinfo: DatabaseInfo,
|
||||
public displayOptions: any,
|
||||
serverVersion
|
||||
) {
|
||||
super(config, setConfig, cache, setCache, driver, dbinfo);
|
||||
super(config, setConfig, cache, setCache, driver, dbinfo, serverVersion);
|
||||
|
||||
this.table = this.findTable(tableName);
|
||||
if (!this.table) {
|
||||
@@ -33,6 +35,7 @@ export class TableGridDisplay extends GridDisplay {
|
||||
this.columns = this.getDisplayColumns(this.table, []);
|
||||
this.filterable = true;
|
||||
this.sortable = true;
|
||||
this.groupable = true;
|
||||
this.editable = true;
|
||||
this.supportsReload = true;
|
||||
this.baseTable = this.table;
|
||||
@@ -59,6 +62,7 @@ export class TableGridDisplay extends GridDisplay {
|
||||
...col,
|
||||
isChecked: this.isColumnChecked(col),
|
||||
hintColumnName: col.foreignKey ? `hint_${col.uniqueName}` : null,
|
||||
isExpandable: !!col.foreignKey,
|
||||
})) || []
|
||||
);
|
||||
}
|
||||
@@ -167,7 +171,7 @@ export class TableGridDisplay extends GridDisplay {
|
||||
|
||||
processReferences(select: Select, displayedColumnInfo: DisplayedColumnInfo, options) {
|
||||
this.addJoinsFromExpandedColumns(select, this.columns, 'basetbl', displayedColumnInfo);
|
||||
if (!options.isExport) {
|
||||
if (!options.isExport && this.displayOptions.showHintColumns) {
|
||||
this.addHintsToSelect(select);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,12 +10,14 @@ export class ViewGridDisplay extends GridDisplay {
|
||||
config: GridConfig,
|
||||
setConfig: ChangeConfigFunc,
|
||||
cache: GridCache,
|
||||
setCache: ChangeCacheFunc
|
||||
setCache: ChangeCacheFunc,
|
||||
serverVersion
|
||||
) {
|
||||
super(config, setConfig, cache, setCache, driver);
|
||||
super(config, setConfig, cache, setCache, driver, serverVersion);
|
||||
this.columns = this.getDisplayColumns(view);
|
||||
this.filterable = true;
|
||||
this.sortable = true;
|
||||
this.groupable = true;
|
||||
this.editable = false;
|
||||
this.supportsReload = true;
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ export * from './TableGridDisplay';
|
||||
export * from './ViewGridDisplay';
|
||||
export * from './JslGridDisplay';
|
||||
export * from './ChangeSet';
|
||||
export * from './filterName';
|
||||
export * from './FreeTableGridDisplay';
|
||||
export * from './FreeTableModel';
|
||||
export * from './MacroDefinition';
|
||||
export * from './runMacro';
|
||||
export * from './FormViewDisplay';
|
||||
export * from './TableFormViewDisplay';
|
||||
export * from './CollectionGridDisplay';
|
||||
|
||||
@@ -4,6 +4,8 @@ import uuidv1 from 'uuid/v1';
|
||||
import uuidv4 from 'uuid/v4';
|
||||
import moment from 'moment';
|
||||
import { MacroDefinition, MacroSelectedCell } from './MacroDefinition';
|
||||
import { ChangeSet, setChangeSetValue } from './ChangeSet';
|
||||
import { GridDisplay } from './GridDisplay';
|
||||
|
||||
const getMacroFunction = {
|
||||
transformValue: code => `
|
||||
@@ -183,3 +185,55 @@ export function runMacro(
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export function compileMacroFunction(macro: MacroDefinition, errors = []) {
|
||||
if (!macro) return null;
|
||||
let func;
|
||||
try {
|
||||
func = eval(getMacroFunction[macro.type](macro.code));
|
||||
return func;
|
||||
} catch (err) {
|
||||
errors.push(`Error compiling macro ${macro.name}: ${err.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function runMacroOnValue(compiledFunc, macroArgs, value, rowIndex, row, column, errors = []) {
|
||||
if (!compiledFunc) return value;
|
||||
try {
|
||||
const res = compiledFunc(value, macroArgs, modules, rowIndex, row, column);
|
||||
return res;
|
||||
} catch (err) {
|
||||
errors.push(`Error processing column ${column} on row ${rowIndex}: ${err.message}`);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export function runMacroOnChangeSet(
|
||||
macro: MacroDefinition,
|
||||
macroArgs: {},
|
||||
selectedCells: MacroSelectedCell[],
|
||||
changeSet: ChangeSet,
|
||||
display: GridDisplay
|
||||
): ChangeSet {
|
||||
const errors = [];
|
||||
const compiledMacroFunc = compileMacroFunction(macro, errors);
|
||||
if (!compiledMacroFunc) return null;
|
||||
|
||||
let res = changeSet;
|
||||
for (const cell of selectedCells) {
|
||||
const definition = display.getChangeSetField(cell.rowData, cell.column, undefined);
|
||||
const macroResult = runMacroOnValue(
|
||||
compiledMacroFunc,
|
||||
macroArgs,
|
||||
cell.value,
|
||||
cell.row,
|
||||
cell.rowData,
|
||||
cell.column,
|
||||
errors
|
||||
);
|
||||
res = setChangeSetValue(res, definition, macroResult);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
[](https://github.com/prettier/prettier)
|
||||
[](https://paypal.me/JanProchazkaCz/30eur)
|
||||
[](https://www.npmjs.com/package/dbgate)
|
||||
|
||||
# DbGate - database administration tool
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const path = require('path');
|
||||
|
||||
global.dbgateApiModulePath = path.dirname(path.dirname(require.resolve('dbgate-api')));
|
||||
global.dbgateApiPackagedPluginsPath = path.dirname(global.dbgateApiModulePath);
|
||||
|
||||
const dbgateApi = require('dbgate-api');
|
||||
|
||||
global.dbgateApiModulePath = require.resolve('dbgate-api');
|
||||
|
||||
dbgateApi.getMainModule().start('startNodeWeb');
|
||||
dbgateApi.getMainModule().start();
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
{
|
||||
"name": "dbgate",
|
||||
"version": "3.9.5",
|
||||
"version": "4.1.1",
|
||||
"homepage": "https://dbgate.org/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
|
||||
"description": "Opensource database administration tool - web interface",
|
||||
"author": "Jan Prochazka",
|
||||
"license": "MIT",
|
||||
@@ -19,7 +18,13 @@
|
||||
"web"
|
||||
],
|
||||
"dependencies": {
|
||||
"dbgate-api": "^3.9.5",
|
||||
"dbgate-web": "^3.9.5"
|
||||
"dbgate-api": "^4.1.1",
|
||||
"dbgate-web": "^4.1.1",
|
||||
"dbgate-plugin-csv": "^4.1.1",
|
||||
"dbgate-plugin-excel": "^4.1.1",
|
||||
"dbgate-plugin-mongo": "^4.1.1",
|
||||
"dbgate-plugin-mysql": "^4.1.1",
|
||||
"dbgate-plugin-mssql": "^4.1.1",
|
||||
"dbgate-plugin-postgres": "^4.1.1"
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"version": "3.9.5",
|
||||
"version": "4.1.1",
|
||||
"name": "dbgate-filterparser",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
"scripts": {
|
||||
"prepare": "yarn build",
|
||||
"build": "tsc",
|
||||
"start": "tsc --watch",
|
||||
"test": "jest"
|
||||
"test": "jest",
|
||||
"test:ci": "jest --json --outputFile=result.json --testLocationInResults"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"devDependencies": {
|
||||
"dbgate-types": "^3.9.5",
|
||||
"dbgate-types": "^4.1.1",
|
||||
"@types/jest": "^25.1.4",
|
||||
"@types/node": "^13.7.0",
|
||||
"jest": "^24.9.0",
|
||||
@@ -22,8 +22,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/parsimmon": "^1.10.1",
|
||||
"dbgate-tools": "^3.9.5",
|
||||
"lodash": "^4.17.15",
|
||||
"dbgate-tools": "^4.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.24.0",
|
||||
"parsimmon": "^1.13.0"
|
||||
}
|
||||
|
||||
32
packages/filterparser/src/common.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import P from 'parsimmon';
|
||||
|
||||
export const whitespace = P.regexp(/\s*/m);
|
||||
|
||||
export function token(parser) {
|
||||
return parser.skip(whitespace);
|
||||
}
|
||||
|
||||
export function word(str) {
|
||||
return P.string(str).thru(token);
|
||||
}
|
||||
|
||||
export function interpretEscapes(str) {
|
||||
let escapes = {
|
||||
b: '\b',
|
||||
f: '\f',
|
||||
n: '\n',
|
||||
r: '\r',
|
||||
t: '\t',
|
||||
};
|
||||
return str.replace(/\\(u[0-9a-fA-F]{4}|[^u])/, (_, escape) => {
|
||||
let type = escape.charAt(0);
|
||||
let hex = escape.slice(1);
|
||||
if (type === 'u') {
|
||||
return String.fromCharCode(parseInt(hex, 16));
|
||||
}
|
||||
if (escapes.hasOwnProperty(type)) {
|
||||
return escapes[type];
|
||||
}
|
||||
return type;
|
||||
});
|
||||
}
|
||||
121
packages/filterparser/src/mongoParser.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import P from 'parsimmon';
|
||||
import { interpretEscapes, token, word, whitespace } from './common';
|
||||
|
||||
const operatorCondition = operator => value => ({
|
||||
__placeholder__: {
|
||||
[operator]: value,
|
||||
},
|
||||
});
|
||||
|
||||
const regexCondition = regexString => value => ({
|
||||
__placeholder__: {
|
||||
$regex: regexString.replace('#VALUE#', value),
|
||||
$options: 'i',
|
||||
},
|
||||
});
|
||||
|
||||
const numberTestCondition = () => value => ({
|
||||
$or: [
|
||||
{
|
||||
__placeholder__: {
|
||||
$regex: `.*${value}.*`,
|
||||
$options: 'i',
|
||||
},
|
||||
},
|
||||
{
|
||||
__placeholder__: value,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const testCondition = (operator, value) => () => ({
|
||||
__placeholder__: {
|
||||
[operator]: value,
|
||||
},
|
||||
});
|
||||
|
||||
const compoudCondition = conditionType => conditions => {
|
||||
if (conditions.length == 1) return conditions[0];
|
||||
return {
|
||||
[conditionType]: conditions,
|
||||
};
|
||||
};
|
||||
|
||||
const negateCondition = condition => ({
|
||||
__placeholder__: {
|
||||
$not: condition.__placeholder__,
|
||||
},
|
||||
});
|
||||
|
||||
const createParser = () => {
|
||||
const langDef = {
|
||||
string1: () =>
|
||||
token(P.regexp(/"((?:\\.|.)*?)"/, 1))
|
||||
.map(interpretEscapes)
|
||||
.desc('string quoted'),
|
||||
|
||||
string2: () =>
|
||||
token(P.regexp(/'((?:\\.|.)*?)'/, 1))
|
||||
.map(interpretEscapes)
|
||||
.desc('string quoted'),
|
||||
|
||||
number: () =>
|
||||
token(P.regexp(/-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?/))
|
||||
.map(Number)
|
||||
.desc('number'),
|
||||
|
||||
noQuotedString: () => P.regexp(/[^\s^,^'^"]+/).desc('string unquoted'),
|
||||
|
||||
value: r => P.alt(r.string1, r.string2, r.number, r.noQuotedString),
|
||||
valueTestNum: r => r.number.map(numberTestCondition()),
|
||||
valueTest: r => r.value.map(regexCondition('.*#VALUE#.*')),
|
||||
|
||||
comma: () => word(','),
|
||||
not: () => word('NOT'),
|
||||
notExists: r => r.not.then(r.exists).map(testCondition('$exists', false)),
|
||||
exists: () => word('EXISTS').map(testCondition('$exists', true)),
|
||||
true: () => word('TRUE').map(testCondition('$eq', true)),
|
||||
false: () => word('FALSE').map(testCondition('$eq', false)),
|
||||
|
||||
eq: r => word('=').then(r.value).map(operatorCondition('$eq')),
|
||||
ne: r => word('!=').then(r.value).map(operatorCondition('$ne')),
|
||||
lt: r => word('<').then(r.value).map(operatorCondition('$lt')),
|
||||
gt: r => word('>').then(r.value).map(operatorCondition('$gt')),
|
||||
le: r => word('<=').then(r.value).map(operatorCondition('$lte')),
|
||||
ge: r => word('>=').then(r.value).map(operatorCondition('$gte')),
|
||||
startsWith: r => word('^').then(r.value).map(regexCondition('#VALUE#.*')),
|
||||
endsWith: r => word('$').then(r.value).map(regexCondition('.*#VALUE#')),
|
||||
contains: r => word('+').then(r.value).map(regexCondition('.*#VALUE#.*')),
|
||||
startsWithNot: r => word('!^').then(r.value).map(regexCondition('#VALUE#.*')).map(negateCondition),
|
||||
endsWithNot: r => word('!$').then(r.value).map(regexCondition('.*#VALUE#')).map(negateCondition),
|
||||
containsNot: r => word('~').then(r.value).map(regexCondition('.*#VALUE#.*')).map(negateCondition),
|
||||
|
||||
element: r =>
|
||||
P.alt(
|
||||
r.exists,
|
||||
r.notExists,
|
||||
r.true,
|
||||
r.false,
|
||||
r.eq,
|
||||
r.ne,
|
||||
r.lt,
|
||||
r.gt,
|
||||
r.le,
|
||||
r.ge,
|
||||
r.startsWith,
|
||||
r.endsWith,
|
||||
r.contains,
|
||||
r.startsWithNot,
|
||||
r.endsWithNot,
|
||||
r.containsNot,
|
||||
r.valueTestNum,
|
||||
r.valueTest
|
||||
).trim(whitespace),
|
||||
factor: r => r.element.sepBy(whitespace).map(compoudCondition('$and')),
|
||||
list: r => r.factor.sepBy(r.comma).map(compoudCondition('$or')),
|
||||
};
|
||||
|
||||
return P.createLanguage(langDef);
|
||||
};
|
||||
|
||||
export const mongoParser = createParser();
|
||||
@@ -3,37 +3,8 @@ import moment from 'moment';
|
||||
import { FilterType } from './types';
|
||||
import { Condition } from 'dbgate-sqltree';
|
||||
import { TransformType } from 'dbgate-types';
|
||||
|
||||
const whitespace = P.regexp(/\s*/m);
|
||||
|
||||
function token(parser) {
|
||||
return parser.skip(whitespace);
|
||||
}
|
||||
|
||||
function word(str) {
|
||||
return P.string(str).thru(token);
|
||||
}
|
||||
|
||||
function interpretEscapes(str) {
|
||||
let escapes = {
|
||||
b: '\b',
|
||||
f: '\f',
|
||||
n: '\n',
|
||||
r: '\r',
|
||||
t: '\t',
|
||||
};
|
||||
return str.replace(/\\(u[0-9a-fA-F]{4}|[^u])/, (_, escape) => {
|
||||
let type = escape.charAt(0);
|
||||
let hex = escape.slice(1);
|
||||
if (type === 'u') {
|
||||
return String.fromCharCode(parseInt(hex, 16));
|
||||
}
|
||||
if (escapes.hasOwnProperty(type)) {
|
||||
return escapes[type];
|
||||
}
|
||||
return type;
|
||||
});
|
||||
}
|
||||
import { interpretEscapes, token, word, whitespace } from './common';
|
||||
import { mongoParser } from './mongoParser';
|
||||
|
||||
const binaryCondition = operator => value => ({
|
||||
conditionType: 'binary',
|
||||
@@ -239,7 +210,8 @@ const createParser = (filterType: FilterType) => {
|
||||
yearMonthNum: () => P.regexp(/\d\d\d\d-\d\d?/).map(yearMonthCondition()),
|
||||
yearMonthDayNum: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?/).map(yearMonthDayCondition()),
|
||||
yearMonthDayMinute: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?\s+\d\d?:\d\d?/).map(yearMonthDayMinuteCondition()),
|
||||
yearMonthDaySecond: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?(\s+|T)\d\d?:\d\d?:\d\d?/).map(yearMonthDaySecondCondition()),
|
||||
yearMonthDaySecond: () =>
|
||||
P.regexp(/\d\d\d\d-\d\d?-\d\d?(\s+|T)\d\d?:\d\d?:\d\d?/).map(yearMonthDaySecondCondition()),
|
||||
|
||||
value: r => P.alt(...allowedValues.map(x => r[x])),
|
||||
valueTestEq: r => r.value.map(binaryCondition('=')),
|
||||
@@ -348,9 +320,11 @@ const parsers = {
|
||||
string: createParser('string'),
|
||||
datetime: createParser('datetime'),
|
||||
logical: createParser('logical'),
|
||||
mongo: mongoParser,
|
||||
};
|
||||
|
||||
export function parseFilter(value: string, filterType: FilterType): Condition {
|
||||
// console.log('PARSING', value, 'WITH', filterType);
|
||||
const ast = parsers[filterType].list.tryParse(value);
|
||||
// console.log('AST', ast);
|
||||
return ast;
|
||||
|
||||
@@ -3,5 +3,9 @@ import { parseFilter } from './parseFilter';
|
||||
test('parse string', () => {
|
||||
const ast = parseFilter('"123"', 'string');
|
||||
console.log(JSON.stringify(ast));
|
||||
expect(ast).toBe(3);
|
||||
expect(ast).toEqual({
|
||||
conditionType: 'like',
|
||||
left: { exprType: 'placeholder' },
|
||||
right: { exprType: 'value', value: '%123%' },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// import types from 'dbgate-types';
|
||||
|
||||
export type FilterType = 'number' | 'string' | 'datetime' | 'logical';
|
||||
export type FilterType = 'number' | 'string' | 'datetime' | 'logical' | 'mongo';
|
||||
|
||||
1
packages/query-splitter/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
lib
|
||||