1
1
mirror of https://github.com/Fabio286/antares.git synced 2025-06-05 21:59:22 +02:00

Compare commits

...

117 Commits

Author SHA1 Message Date
0b861d962d chore(release): 0.5.2 2022-04-10 10:55:23 +02:00
6cc098c6f0 feat(core): option to allow untrusted SSL connections 2022-04-10 10:49:27 +02:00
40828cb3ff Merge pull request #208 from antares-sql/feat/postgre-import-export
feat(PostgreSQL): import/export
2022-04-07 17:56:37 +02:00
6086ca4a80 feat(PostgreSQL): sql dump importer 2022-04-07 12:49:34 +02:00
d92facf518 chore: update README.md 2022-04-05 14:06:36 +02:00
allcontributors[bot]
e9643a0d6b docs: update .all-contributorsrc [skip ci] 2022-04-04 10:32:23 +02:00
allcontributors[bot]
cc609a7051 docs: update README.md [skip ci] 2022-04-04 10:32:23 +02:00
796f61bf2f feat: french translation updated, closes #222 2022-04-04 10:29:11 +02:00
0f9c991f53 fix(PostgreSQL): wrong values exporting table content 2022-04-02 11:44:55 +02:00
638a88a1fb perf(PostgreSQL): improved views exportation 2022-04-01 18:36:02 +02:00
026d74c8c8 fix: ssh tunnel not properly working, closes #220 2022-04-01 09:51:03 +02:00
77f8cac6cf refactor: update repo links 2022-03-31 16:04:47 +02:00
d6fadf5db0 chore: update README.md 2022-03-29 15:47:50 +02:00
408ddeda56 perf(PostgreSQL): improved dump file 2022-03-27 11:41:35 +02:00
88cd097ec0 chore(release): 0.5.1 2022-03-25 09:26:04 +01:00
f12e6a96dd fix(UI): connection buttons out of screen on small displays, closes #213 2022-03-24 17:34:24 +01:00
a8ca8f2f76 feat(PostgreSQL): export functions and procedures 2022-03-23 13:26:46 +01:00
allcontributors[bot]
6fcae957e1 docs: update .all-contributorsrc [skip ci] 2022-03-22 21:38:32 +01:00
allcontributors[bot]
1cd2d8abf3 docs: update README.md [skip ci] 2022-03-22 21:38:32 +01:00
ed3c5fe559 Merge pull request #210 from SmileYzn/patch-1
Added some missing translations for pt-BR
2022-03-22 21:36:52 +01:00
Cleverson
9601c59392 Update pt-BR.js 2022-03-22 14:38:24 -03:00
Cleverson
14d20a30c1 Added some missing translations for pt-BR
Added some translations for pt-BR
2022-03-22 14:29:33 -03:00
42376b4bc6 feat(PostgreSQL): export triggers 2022-03-22 17:25:34 +01:00
e9079adb25 feat(UI): option to disable blur effects, closes #209 2022-03-22 16:13:44 +01:00
86f011f34f feat(PostgreSQL): export views 2022-03-22 12:59:13 +01:00
bb02479b71 feat(PostgreSQL): export user-defined types before tables 2022-03-22 12:40:14 +01:00
a67071e284 feat(PostgreSQL): export tables 2022-03-21 18:32:45 +01:00
8f3efabb69 feat: export database as zip sql file 2022-03-18 18:12:13 +01:00
db628f7722 fix: numeric scale displayed on non decimal fields 2022-03-17 18:17:59 +01:00
e4bd747381 chore: update bug_report.md 2022-03-15 18:39:46 +01:00
88eb113e53 chore: update README.md 2022-03-12 17:42:34 +01:00
9109b2c328 chore(release): 0.5.0 2022-03-12 14:25:04 +01:00
dd070d008d Merge pull request #129 from toriphes/feat/db-import-export
feat(MySQL): db import export
2022-03-12 14:17:48 +01:00
ee415da127 fix(MySQL): exception exporting empty procedures/functions 2022-03-12 10:37:40 +01:00
f0d368e3e3 refactor: minor refactor 2022-03-12 10:01:22 +01:00
4be55f3fe9 feat(MySQL): support to multi spatial fields export 2022-03-12 09:52:40 +01:00
fd00ea42ee refactor: hide export option for PostgreSQL 2022-03-10 10:54:44 +01:00
50b4411e9a Merge pull request #204 from raliqala/fix-and-minimize-con-string
perf(PostgreSQL): Postgres connection string update, better error handling and any connection string accommodation
2022-03-10 10:52:10 +01:00
Topollo
cd0a5dc034 Merge branch 'fix-and-minimize-con-string' of https://github.com/raliqala/antares into fix-and-minimize-con-string 2022-03-10 11:45:54 +02:00
Topollo
9fd427c4ff removed electron-rebuild 2022-03-10 11:44:52 +02:00
f3759b6541 perf(MySQL): prevent memory leak on large dump import 2022-03-10 10:23:22 +01:00
191a354020 chore: delete UsersraliqAppDataLocalnpm-cache_logs2022-03-05T07_59_37_036Z-debug.log 2022-03-10 09:02:16 +01:00
Topollo
7dc314eecf Merge branch 'master' into fix-and-minimize-con-string 2022-03-09 22:14:28 +02:00
Topollo
330a80fe70 perf(PostgreSQL): Postgres connection update, better error handling and connection string accommodation.
Used the pg package parser function to handle connection strings to lowering chances of errors. All other connection string will work here an example for ssl  ```const args = "postgres://user:password@localhost:5432/db?&sslrootcert=./myCaCertificate.pem&sslcert=./myClientCertificate.pem&sslkey=./myPrivateClientKey.pem&ciphers=mytestcipher&ssl=true"``` for ssh these can be used ```ssh: false, sshHost: '', sshUser: '', sshPass: '', sshKey: '', sshPort: 22,```

no breaking changes
2022-03-09 21:52:13 +02:00
Topollo
b2ce533b82 Merge branch 'postgres_con_string' into fix-and-minimize-con-string 2022-03-09 19:41:31 +02:00
Topollo
12ce6b1135 Update testStringDecode.js 2022-03-09 19:40:04 +02:00
Topollo
71c5829702 hi 2022-03-09 19:31:41 +02:00
1c4d5b05b3 feat(UI): toggle tables checkbox by column on export modal 2022-03-07 23:07:42 +01:00
e85197a2cc chore: UsersraliqAppDataLocalnpm-cache_logs2022-03-05T07_59_37_036Z-debug.log 2022-03-07 20:10:44 +01:00
Giulio Ganci
abf2b92e6e feat(UI): auto-refresh schema at the end of the import process 2022-03-06 19:54:32 +01:00
Giulio Ganci
3be826df4b feat(MySQL): enhance export characters escaping 2022-03-06 19:37:12 +01:00
9db6bfd255 refactor: show contributors in settings 2022-03-06 16:02:24 +01:00
allcontributors[bot]
110adf90de docs: update .all-contributorsrc [skip ci] 2022-03-06 09:48:30 +01:00
allcontributors[bot]
40e4fbda15 docs: update README.md [skip ci] 2022-03-06 09:48:30 +01:00
8e983ad2cb refactor(PostgreSQL): minor refactor on UI and code for pg connection string 2022-03-06 09:45:55 +01:00
Topollo
6305752ad1 feat(PostgreSQL): Postgress connection string feature for local and server connection string
This feature is based on this [issue](https://github.com/Fabio286/antares/issues/193)  I tested with the following posgress connection strings postgresql://postgres:pgpassword@127.0.0.1:5432/my_local_databse?connection=local postgres://serveruser:serverpass@test.db.elephantsql.com/my_remote_databse?connection=server  postgres://serveruser:serverpass@test.db.elephantsql.com:5432/my_remote_databse postgresql://postgres:pgpassword@127.0.0.1:5432/my_local_databse.

The connection string decoder is loaded before "test-connection", "check-connection", and "connect"
2022-03-06 09:45:55 +01:00
cc02e2c5a8 refactor(PostgreSQL): minor refactor on UI and code for pg connection string 2022-03-06 09:40:11 +01:00
Topollo
f4a63eae2a feat(PostgreSQL): Postgress connection string feature for local and server connection string
This feature is based on this [issue](https://github.com/Fabio286/antares/issues/193)  I tested with the following posgress connection strings postgresql://postgres:pgpassword@127.0.0.1:5432/my_local_databse?connection=local postgres://serveruser:serverpass@test.db.elephantsql.com/my_remote_databse?connection=server  postgres://serveruser:serverpass@test.db.elephantsql.com:5432/my_remote_databse postgresql://postgres:pgpassword@127.0.0.1:5432/my_local_databse.

The connection string decoder is loaded before "test-connection", "check-connection", and "connect"
2022-03-06 02:20:10 +02:00
763be8532d fix: wrong soft sort algorithm for numeric fields, closes #199 2022-03-05 21:58:56 +01:00
Giulio Ganci
a6f5645a22 feat(UI): better real-time import stats 2022-03-05 18:55:11 +01:00
Giulio Ganci
bbe13f27dc perf(MySQL): import tasks managed with async queue 2022-03-05 18:54:08 +01:00
f444746f46 perf(MySQL): import performance improvement 2022-03-05 17:00:05 +01:00
b4af645941 fix(MySQL):spatial fielts support on export 2022-03-03 22:52:50 +01:00
a5c8daa5b8 chore: workers debug config 2022-03-01 22:15:33 +01:00
1a9fc37285 fix(MySQL): missing initial delimiter for exported procedures 2022-03-01 19:05:53 +01:00
f0351e5b94 fix(MySQL): missing functions and procedures definer escapes on exporter 2022-03-01 10:15:06 +01:00
9bda4e71b7 Merge branch 'feat/db-import-export' of https://github.com/toriphes/antares into pr/toriphes/129 2022-02-28 23:33:16 +01:00
7c00055034 Merge branch 'master' of https://github.com/Fabio286/antares into pr/toriphes/129 2022-02-28 22:00:13 +01:00
4479a9600b fix: wrong schema and table size on explore bar 2022-02-28 14:19:07 +01:00
Giulio Ganci
7a6bd8bdbd fix: sql parser hangs during import 2022-02-27 17:33:03 +01:00
ad713fcf35 chore(release): 0.4.4 2022-02-27 14:07:24 +01:00
251795e2d2 Merge branch 'master' into feat/db-import-export 2022-02-26 10:19:15 +01:00
45cda7a7cc Merge branch 'master' of https://github.com/Fabio286/antares into pr/toriphes/129 2022-02-26 10:14:00 +01:00
b7039553cc fix: bigint support, closes #197 2022-02-26 10:02:23 +01:00
ddee68b4c2 ci: set windows-2019 for GitHub Actions 2022-02-26 10:01:46 +01:00
573ac6d42e perf: use fork() for the import process 2022-02-24 13:14:57 +01:00
265f28b4d9 fix: zero-padded bit fields beyond length 2022-02-21 21:40:26 +01:00
1990d9a3d4 perf(MySQL): improved several field types support on exporter 2022-02-19 12:40:54 +01:00
748d44977e perf: use fork() for the export process 2022-02-18 18:16:13 +01:00
4051eff382 build: webpack workers configuration 2022-02-17 18:47:50 +01:00
4276586e11 fix(MySQL): procedures exportation 2022-02-16 12:58:24 +01:00
832fb0fb03 Merge branch 'feat/db-import-export' of https://github.com/toriphes/antares into pr/toriphes/129 2022-02-16 09:16:14 +01:00
328ab61757 Merge branch 'master' of https://github.com/Fabio286/antares into pr/toriphes/129 2022-02-16 09:14:46 +01:00
95d15de1bd chore: update README.md 2022-02-16 08:57:54 +01:00
7dcd4441c4 feat(SQLite): manual commit mode 2022-02-15 09:23:07 +01:00
d81e0911ab feat(PostgreSQL): manual commit mode 2022-02-15 09:23:07 +01:00
5bfff649e9 feat: reminder for uncommitted changes closing a tab 2022-02-15 09:23:07 +01:00
76743e8f7c feat: execution notification for ROLLBACK and COMMIT 2022-02-15 09:23:07 +01:00
4ed2f9a939 feat(MySQL): manual commit mode 2022-02-15 09:23:07 +01:00
c5eb73ed3f chore: update electron 2022-02-15 09:21:14 +01:00
fa3f3e1fd8 fix(MySQL): default value not displayed for DECIMAL fields 2022-02-05 09:43:37 +01:00
2c7c97852f chore(release): 0.4.3 2022-01-30 12:39:54 +01:00
48ebf23bd1 feat(MySQL): spatial fields support (#165)
* feat: POINT field support

* feat(MySQL): support to LINESTRING, POLYGON and GEOMETRY fields

* refactor: removed links from map attribution

* feat(MySQL): support to MULTIPOINT, MULTILINESTRING, MULTIPOLYGON and GEOMCOLLECTION fields

* test: temporary fix on Windows tests
2022-01-30 11:45:24 +01:00
64deedc5ad test: temporary skip tests on WIndows 2022-01-30 11:44:01 +01:00
9f033fb994 fix: indexes and foreign keys not cleared after deletion of related field, closes #182 2022-01-28 23:57:53 +01:00
401cb49687 refactor: improved temporary fix to Windows 7 style frame 2022-01-28 09:19:49 +01:00
1356011ba3 fix(Windows): temporary fix to Windows 7 style frame on app startup, closes #169 2022-01-27 23:40:03 +01:00
0cfd7938ee fix: scale on numeric fields that doesn't support it 2022-01-27 09:12:01 +01:00
745b55a942 chore: update dependencies 2022-01-25 09:08:45 +01:00
eef7c1dcec perf: support of scale in field's length setting 2022-01-22 12:29:49 +01:00
aa8fc545d7 Rename zh_CN.js to zh-CN.js 2022-01-20 09:02:20 +01:00
a780c7e0ff Merge pull request #179 from Fabio286/all-contributors/add-goYou
docs: add goYou as a contributor for translation
2022-01-20 08:56:33 +01:00
allcontributors[bot]
9a1bb0599f docs: update .all-contributorsrc [skip ci] 2022-01-20 07:56:07 +00:00
allcontributors[bot]
d847870f67 docs: update README.md [skip ci] 2022-01-20 07:56:06 +00:00
cd24371576 Merge pull request #177 from goYou/master
feat: add Simplified Chinese translation
2022-01-20 08:54:56 +01:00
goYou
6ef565cf07 feat: add Simplified Chinese translation 2022-01-20 01:50:18 +08:00
46b45c8ab6 fix(PostgreSQL): schema different than public not automatically selected, closes #172 2022-01-17 09:15:18 +01:00
f28531a225 build: resolved dependency conflicts 2022-01-16 11:50:35 +01:00
8fb1f0803e fix: cell copy returns "undefined" in some conditions, closes #170 2022-01-14 18:37:37 +01:00
Giulio Ganci
4e9f8d16ee feat: initial mysql import support 2021-12-28 15:30:07 +01:00
Giulio Ganci
d25c62b4da feat: delete dump file when the export is canceled 2021-11-04 23:09:28 +01:00
Giulio Ganci
8cf738bac8 fix(MySQL): export crash with large databases 2021-11-04 22:36:45 +01:00
409ed54608 perf: split the export select query to avoid running out of memory 2021-11-01 16:12:20 +01:00
d9d3bf2bc9 perf: avoid to load schema elements if already loaded in export modal 2021-11-01 14:52:45 +01:00
9e9de7b5c5 Merge branch 'master' of https://github.com/Fabio286/antares into pr/toriphes/129 2021-11-01 10:09:37 +01:00
Giulio Ganci
b2a5b40c03 feat: mysql export for trigger, views, schedulers, functions and routines 2021-10-31 17:22:59 +01:00
Giulio Ganci
0de2321920 feat: initial db export implementation 2021-10-29 12:58:37 +02:00
82 changed files with 4944 additions and 506 deletions

View File

@@ -1,6 +1,6 @@
{
"projectName": "antares",
"projectOwner": "Fabio286",
"projectOwner": "antares-sql",
"repoType": "github",
"repoHost": "https://github.com",
"files": [
@@ -129,6 +129,42 @@
"contributions": [
"code"
]
},
{
"login": "goYou",
"name": "goYou",
"avatar_url": "https://avatars.githubusercontent.com/u/62732795?v=4",
"profile": "https://github.com/goYou",
"contributions": [
"translation"
]
},
{
"login": "raliqala",
"name": "Topollo",
"avatar_url": "https://avatars.githubusercontent.com/u/30502407?v=4",
"profile": "https://github.com/raliqala",
"contributions": [
"code"
]
},
{
"login": "SmileYzn",
"name": "Cleverson",
"avatar_url": "https://avatars.githubusercontent.com/u/5851851?v=4",
"profile": "https://github.com/SmileYzn",
"contributions": [
"translation"
]
},
{
"login": "fredatgithub",
"name": "fred",
"avatar_url": "https://avatars.githubusercontent.com/u/6720055?v=4",
"profile": "https://github.com/fredatgithub",
"contributions": [
"translation"
]
}
],
"contributorsPerLine": 7,

View File

@@ -26,13 +26,14 @@ If applicable, add screenshots to help explain your problem.
**Application (please complete the following information):**
- Client [e.g. MySQL]
- Version [e.g. 0.14.0]
- Distribution: [e.g. exe, Linux Store, AppImage, dmg]
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Version [e.g. 22]
- OS: [e.g. Windows 11]
- Version [e.g. 21H2]
**Additional context**
Add any other context about the problem here.

View File

@@ -8,7 +8,7 @@ jobs:
strategy:
matrix:
os: [windows-latest]
os: [windows-2019]
steps:
- name: Check out Git repository

3
.gitignore vendored
View File

@@ -5,4 +5,5 @@ node_modules
thumbs.db
NOTES.md
*.txt
package-lock.json
package-lock.json
*.heapsnapshot

14
.vscode/launch.json vendored
View File

@@ -2,8 +2,8 @@
"version": "0.2.0",
"configurations": [
{
"cwd": "${workspaceFolder}",
"name": "Electron: Main",
"cwd": "${workspaceFolder}",
"port": 9222,
"protocol": "inspector",
"request": "attach",
@@ -18,7 +18,17 @@
"sourceMaps": true,
"type": "chrome",
"webRoot": "${workspaceFolder}"
}
},
{
"name": "Electron: Worker",
"cwd": "${workspaceFolder}",
"port": 9224,
"protocol": "inspector",
"request": "attach",
"sourceMaps": true,
"type": "node",
"timeout": 1000000
},
],
"compounds": [
{

View File

@@ -4,7 +4,8 @@
"core",
"MySQL",
"PostgreSQL",
"SQLite"
"SQLite",
"Windows"
],
"svg.preview.background": "transparent"
}

View File

@@ -2,6 +2,128 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [0.5.2](https://github.com/antares-sql/antares/compare/v0.5.1...v0.5.2) (2022-04-10)
### Features
* **core:** option to allow untrusted SSL connections ([6cc098c](https://github.com/antares-sql/antares/commit/6cc098c6f02fb52cc71c0141431ab75f12744a1c))
* french translation updated, closes [#222](https://github.com/antares-sql/antares/issues/222) ([796f61b](https://github.com/antares-sql/antares/commit/796f61bf2feab0da515901e2137dc7bf04371d7d))
* **PostgreSQL:** export functions and procedures ([a8ca8f2](https://github.com/antares-sql/antares/commit/a8ca8f2f76ab36c4afe84d602709386315f4b7d1))
* **PostgreSQL:** export tables ([a67071e](https://github.com/antares-sql/antares/commit/a67071e28470bcbd0ec26780bb86f3c65750ded8))
* **PostgreSQL:** export triggers ([42376b4](https://github.com/antares-sql/antares/commit/42376b4bc6dd8b630402d09b026d9fbc0b8646bb))
* **PostgreSQL:** export user-defined types before tables ([bb02479](https://github.com/antares-sql/antares/commit/bb02479b71bf75a6e69e28af57c5fe213d3f30bc))
* **PostgreSQL:** export views ([86f011f](https://github.com/antares-sql/antares/commit/86f011f34fec9d6829bce324493fea888a863ffc))
* **PostgreSQL:** sql dump importer ([6086ca4](https://github.com/antares-sql/antares/commit/6086ca4a80b9ad6a07086446253d781f052d3abc))
### Bug Fixes
* **PostgreSQL:** wrong values exporting table content ([0f9c991](https://github.com/antares-sql/antares/commit/0f9c991f539560913fa0e9361a16e6448a066a27))
* ssh tunnel not properly working, closes [#220](https://github.com/antares-sql/antares/issues/220) ([026d74c](https://github.com/antares-sql/antares/commit/026d74c8c88c605a3c8c963c211078f5b3dcfda1))
### Improvements
* **PostgreSQL:** improved dump file ([408dded](https://github.com/antares-sql/antares/commit/408ddeda5634ab6bf41eff760271669170b60eb6))
* **PostgreSQL:** improved views exportation ([638a88a](https://github.com/antares-sql/antares/commit/638a88a1fb35c048ff4c6d120aaaef831c846f58))
### [0.5.1](https://github.com/Fabio286/antares/compare/v0.5.0...v0.5.1) (2022-03-25)
### Features
* export database as zip sql file ([8f3efab](https://github.com/Fabio286/antares/commit/8f3efabb6962c55c23a43c8da1433185dbc3fb41))
* **UI:** option to disable blur effects, closes [#209](https://github.com/Fabio286/antares/issues/209) ([e9079ad](https://github.com/Fabio286/antares/commit/e9079adb25ec28e9546acd54bc2565b8d6e28120))
### Bug Fixes
* numeric scale displayed on non decimal fields ([db628f7](https://github.com/Fabio286/antares/commit/db628f77226b161c3df31b7450b92a6e58754ab7))
* **UI:** connection buttons out of screen on small displays, closes [#213](https://github.com/Fabio286/antares/issues/213) ([f12e6a9](https://github.com/Fabio286/antares/commit/f12e6a96dd66140b06c55eda775af48a666627dd))
## [0.5.0](https://github.com/Fabio286/antares/compare/v0.4.4...v0.5.0) (2022-03-12)
### Features
* delete dump file when the export is canceled ([d25c62b](https://github.com/Fabio286/antares/commit/d25c62b4da9480e040d0bfac8b76732a4c69a5f1))
* initial db export implementation ([0de2321](https://github.com/Fabio286/antares/commit/0de232192076d1de827424c593ac9dff63903531))
* initial mysql import support ([4e9f8d1](https://github.com/Fabio286/antares/commit/4e9f8d16ee3c204d5f0c2bed081206f8b38207a6))
* mysql export for trigger, views, schedulers, functions and routines ([b2a5b40](https://github.com/Fabio286/antares/commit/b2a5b40c03d56bced5a7968c3454f36060e56dd0))
* **MySQL:** enhance export characters escaping ([3be826d](https://github.com/Fabio286/antares/commit/3be826df4b02ff0df0aa922d96755b31b7155784))
* **MySQL:** support to multi spatial fields export ([4be55f3](https://github.com/Fabio286/antares/commit/4be55f3fe9bb48324b780734762f2ff6da2ccb61))
* **PostgreSQL:** :sparkles: Postgress connection string feature for local and server connection string ([6305752](https://github.com/Fabio286/antares/commit/6305752ad117cc29c04bce3ce3df321f743cdc44))
* **PostgreSQL:** :sparkles: Postgress connection string feature for local and server connection string ([f4a63ea](https://github.com/Fabio286/antares/commit/f4a63eae2aca2a84647a5027137614950aef1eac))
* **UI:** auto-refresh schema at the end of the import process ([abf2b92](https://github.com/Fabio286/antares/commit/abf2b92e6e66b6668e698c5addf4e3c00ae5157b))
* **UI:** better real-time import stats ([a6f5645](https://github.com/Fabio286/antares/commit/a6f5645a226454cc2c415311ac321ba3d4db4454))
* **UI:** toggle tables checkbox by column on export modal ([1c4d5b0](https://github.com/Fabio286/antares/commit/1c4d5b05b3f94b3e7bef930aa7f89bdaa596c0b9))
### Bug Fixes
* **MySQL:** exception exporting empty procedures/functions ([ee415da](https://github.com/Fabio286/antares/commit/ee415da127d6d0de95aac901a2a01af863736344))
* **MySQL:** export crash with large databases ([8cf738b](https://github.com/Fabio286/antares/commit/8cf738bac85698fddd0504eef7844279e8c11f44))
* **MySQL:** missing functions and procedures definer escapes on exporter ([f0351e5](https://github.com/Fabio286/antares/commit/f0351e5b94830f9f52256096c2601b0ca9cd811d))
* **MySQL:** missing initial delimiter for exported procedures ([1a9fc37](https://github.com/Fabio286/antares/commit/1a9fc3728580f789727256d7893ca4bb90c16a50))
* **MySQL:** procedures exportation ([4276586](https://github.com/Fabio286/antares/commit/4276586e1141500401ff1ab570b29e485f459987))
* sql parser hangs during import ([7a6bd8b](https://github.com/Fabio286/antares/commit/7a6bd8bdbd69e3b5fe265d0bb0be844699dd77c2))
* wrong schema and table size on explore bar ([4479a96](https://github.com/Fabio286/antares/commit/4479a9600b5e59ef1bcf9135d661b4d7900a4bde))
* wrong soft sort algorithm for numeric fields, closes [#199](https://github.com/Fabio286/antares/issues/199) ([763be85](https://github.com/Fabio286/antares/commit/763be8532d2b61d0b4d45e72343f6a2e5fee1db9))
### Improvements
* avoid to load schema elements if already loaded in export modal ([d9d3bf2](https://github.com/Fabio286/antares/commit/d9d3bf2bc9d39ce8eec5dffbecbf767fbcf47782))
* **MySQL:** import performance improvement ([f444746](https://github.com/Fabio286/antares/commit/f444746f465ed0e8bd2e4c007faf17e167814278))
* **MySQL:** import tasks managed with async queue ([bbe13f2](https://github.com/Fabio286/antares/commit/bbe13f27dc29f997898f8c13f36b5d582770b21d))
* **MySQL:** improved several field types support on exporter ([1990d9a](https://github.com/Fabio286/antares/commit/1990d9a3d441f0e2075ac7e893d5b166275c48c0))
* **MySQL:** prevent memory leak on large dump import ([f3759b6](https://github.com/Fabio286/antares/commit/f3759b65411a40d92b98208176cdf8e6dd8230ce))
* **PostgreSQL:** :zap: Postgres connection update, better error handling and connection string accommodation. ([330a80f](https://github.com/Fabio286/antares/commit/330a80fe70b81f466f5e883029f42087b4b5c411))
* split the export select query to avoid running out of memory ([409ed54](https://github.com/Fabio286/antares/commit/409ed54608ad402b63fcc26a6e724bc447ba89d2))
* use fork() for the export process ([748d449](https://github.com/Fabio286/antares/commit/748d44977e76c6c8d6344df52e8e3ccfab84f670))
* use fork() for the import process ([573ac6d](https://github.com/Fabio286/antares/commit/573ac6d42ef833f250d102e5b30ae6cf5877f330))
### [0.4.4](https://github.com/Fabio286/antares/compare/v0.4.3...v0.4.4) (2022-02-27)
### Features
* execution notification for ROLLBACK and COMMIT ([76743e8](https://github.com/Fabio286/antares/commit/76743e8f7c02b824cb21540bfbcbe66ba43de8fa))
* **MySQL:** manual commit mode ([4ed2f9a](https://github.com/Fabio286/antares/commit/4ed2f9a93937b4293436a64318b7d6ae3a0d93c2))
* **PostgreSQL:** manual commit mode ([d81e091](https://github.com/Fabio286/antares/commit/d81e0911ab82fb75745ab11e67e867a00d8ac273))
* reminder for uncommitted changes closing a tab ([5bfff64](https://github.com/Fabio286/antares/commit/5bfff649e92f6fe5aba4b16aa4c8d5a5a70b31b2))
* **SQLite:** manual commit mode ([7dcd444](https://github.com/Fabio286/antares/commit/7dcd4441c49fafc0f47e12c2129708fe1092e1a4))
### Bug Fixes
* bigint support, closes [#197](https://github.com/Fabio286/antares/issues/197) ([b703955](https://github.com/Fabio286/antares/commit/b7039553ccaac4fd59e521530c4a053922854130))
* **MySQL:** default value not displayed for DECIMAL fields ([fa3f3e1](https://github.com/Fabio286/antares/commit/fa3f3e1fd8101f19f18df71e90d60fd37cdddaee))
* zero-padded bit fields beyond length ([265f28b](https://github.com/Fabio286/antares/commit/265f28b4d94cde4608a1d6d3d306824134808ec2))
### [0.4.3](https://github.com/Fabio286/antares/compare/v0.4.2...v0.4.3) (2022-01-30)
### Features
* add Simplified Chinese translation ([6ef565c](https://github.com/Fabio286/antares/commit/6ef565cf078cb3f5b7bcdc226894cddeb6239db9))
* **MySQL:** spatial fields support ([#165](https://github.com/Fabio286/antares/issues/165)) ([48ebf23](https://github.com/Fabio286/antares/commit/48ebf23bd1574408f429f2e1200ce878352007f6))
### Bug Fixes
* cell copy returns "undefined" in some conditions, closes [#170](https://github.com/Fabio286/antares/issues/170) ([8fb1f08](https://github.com/Fabio286/antares/commit/8fb1f0803efd9df0b66521e73bb6e1a229cf9691))
* indexes and foreign keys not cleared after deletion of related field, closes [#182](https://github.com/Fabio286/antares/issues/182) ([9f033fb](https://github.com/Fabio286/antares/commit/9f033fb994916b4fb165e81e55e86127ca817791))
* **PostgreSQL:** schema different than public not automatically selected, closes [#172](https://github.com/Fabio286/antares/issues/172) ([46b45c8](https://github.com/Fabio286/antares/commit/46b45c8ab64fb6837a532c4f8342167e4fd794bb))
* scale on numeric fields that doesn't support it ([0cfd793](https://github.com/Fabio286/antares/commit/0cfd7938ee7d607dbad66ae452d0200223a6bab2))
* **Windows:** temporary fix to Windows 7 style frame on app startup, closes [#169](https://github.com/Fabio286/antares/issues/169) ([1356011](https://github.com/Fabio286/antares/commit/1356011ba3b7dd72e12cb252a8787ce48a364fd4))
### Improvements
* support of scale in field's length setting ([eef7c1d](https://github.com/Fabio286/antares/commit/eef7c1dcecc6593ab0e69ed678187a57fe0a4fb6))
### [0.4.2](https://github.com/Fabio286/antares/compare/v0.4.1...v0.4.2) (2022-01-10)

View File

@@ -31,15 +31,15 @@ We are actively working on it, hoping to provide new cool features, improvements
- Query suggestions and auto complete.
- Query history: search through the last 1000 queries.
- SSH tunnel support.
- Manual commit mode.
- Import and export database dumps.
- Dark and light theme.
- Editor themes.
- Scratchpad.
- Secure password storage.
## Philosophy
Why are we developing an SQL client when there are a lot of them on the market?
The main goal is to develop a totally free, full featured, cross platform and open source alternative, empowered by JavaScript's ecosystem.
The main goal is to develop a **forever 100% free (without paid premium feature)**, full featured, as possible community driven, cross platform and open source alternative, empowered by JavaScript ecosystem.
A modern application created with minimalism and semplicity in mind, with features in the right places, not hundreds of tiny buttons, nested tabs or submenu; productivity comes first.
## Installation
@@ -72,7 +72,6 @@ This is a roadmap with major features will come in near future.
- Users management (add/edit/delete).
- More context menu shortcuts.
- More keyboard shortcuts.
- Import/export and migration.
- Support for other databases.
- Apple Silicon distribution
@@ -105,7 +104,7 @@ This is a roadmap with major features will come in near future.
- 🌍 [Translate Antares](https://github.com/Fabio286/antares/wiki/Translate-Antares)
- 📖 [Contributors Guide](https://github.com/Fabio286/antares/wiki/Contributors-Guide)
- 🚧 [Project Board](https://github.com/users/Fabio286/projects/1)
- 🚧 [Project Board](https://github.com/antares-sql/antares/projects/1)
## Contributors ✨
@@ -116,9 +115,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://fabiodistasio.it/"><img src="https://avatars.githubusercontent.com/u/31471771?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Fabio Di Stasio</b></sub></a><br /><a href="https://github.com/Fabio286/antares/commits?author=Fabio286" title="Code">💻</a> <a href="#translation-Fabio286" title="Translation">🌍</a> <a href="https://github.com/Fabio286/antares/commits?author=Fabio286" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.linkedin.com/in/giulioganci/"><img src="https://avatars.githubusercontent.com/u/4192159?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Giulio Ganci</b></sub></a><br /><a href="https://github.com/Fabio286/antares/commits?author=toriphes" title="Code">💻</a></td>
<td align="center"><a href="https://christianratz.de/"><img src="https://avatars.githubusercontent.com/u/2630316?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Christian Ratz</b></sub></a><br /><a href="https://github.com/Fabio286/antares/commits?author=digitalgopnik" title="Code">💻</a> <a href="#translation-digitalgopnik" title="Translation">🌍</a></td>
<td align="center"><a href="https://fabiodistasio.it/"><img src="https://avatars.githubusercontent.com/u/31471771?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Fabio Di Stasio</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=Fabio286" title="Code">💻</a> <a href="#translation-Fabio286" title="Translation">🌍</a> <a href="https://github.com/antares-sql/antares/commits?author=Fabio286" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.linkedin.com/in/giulioganci/"><img src="https://avatars.githubusercontent.com/u/4192159?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Giulio Ganci</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=toriphes" title="Code">💻</a></td>
<td align="center"><a href="https://christianratz.de/"><img src="https://avatars.githubusercontent.com/u/2630316?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Christian Ratz</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=digitalgopnik" title="Code">💻</a> <a href="#translation-digitalgopnik" title="Translation">🌍</a></td>
<td align="center"><a href="https://reverb6821.github.io/"><img src="https://avatars.githubusercontent.com/u/55198803?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Giuseppe Gigliotti</b></sub></a><br /><a href="#translation-reverb6821" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/Mohd-PH"><img src="https://avatars.githubusercontent.com/u/9362157?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mohd-PH</b></sub></a><br /><a href="#translation-Mohd-PH" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/hongkfui"><img src="https://avatars.githubusercontent.com/u/37477191?v=4?s=100" width="100px;" alt=""/><br /><sub><b>hongkfui</b></sub></a><br /><a href="#translation-hongkfui" title="Translation">🌍</a></td>
@@ -129,8 +128,14 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center"><a href="https://ngoquocdat.com/"><img src="https://avatars.githubusercontent.com/u/56961917?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ngô Quốc Đạt</b></sub></a><br /><a href="#translation-datlechin" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/IsamuSugi"><img src="https://avatars.githubusercontent.com/u/7746658?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Isamu Sugiura</b></sub></a><br /><a href="#translation-IsamuSugi" title="Translation">🌍</a></td>
<td align="center"><a href="http://rsacchetto.nexxontech.it/"><img src="https://avatars.githubusercontent.com/u/18429412?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Riccardo Sacchetto</b></sub></a><br /><a href="#platform-Occhioverde" title="Packaging/porting to new platform">📦</a></td>
<td align="center"><a href="https://kilianstallinger.com"><img src="https://avatars.githubusercontent.com/u/5290318?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kilian Stallinger</b></sub></a><br /><a href="https://github.com/Fabio286/antares/commits?author=kilianstallz" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/wenj91"><img src="https://avatars.githubusercontent.com/u/12549338?v=4?s=100" width="100px;" alt=""/><br /><sub><b>文杰</b></sub></a><br /><a href="https://github.com/Fabio286/antares/commits?author=wenj91" title="Code">💻</a></td>
<td align="center"><a href="https://kilianstallinger.com"><img src="https://avatars.githubusercontent.com/u/5290318?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kilian Stallinger</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=kilianstallz" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/wenj91"><img src="https://avatars.githubusercontent.com/u/12549338?v=4?s=100" width="100px;" alt=""/><br /><sub><b>文杰</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=wenj91" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/goYou"><img src="https://avatars.githubusercontent.com/u/62732795?v=4?s=100" width="100px;" alt=""/><br /><sub><b>goYou</b></sub></a><br /><a href="#translation-goYou" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/raliqala"><img src="https://avatars.githubusercontent.com/u/30502407?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Topollo</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=raliqala" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/SmileYzn"><img src="https://avatars.githubusercontent.com/u/5851851?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Cleverson</b></sub></a><br /><a href="#translation-SmileYzn" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/fredatgithub"><img src="https://avatars.githubusercontent.com/u/6720055?v=4?s=100" width="100px;" alt=""/><br /><sub><b>fred</b></sub></a><br /><a href="#translation-fredatgithub" title="Translation">🌍</a></td>
</tr>
</table>

View File

@@ -1,20 +1,21 @@
{
"name": "antares",
"productName": "Antares",
"version": "0.4.2",
"version": "0.5.2",
"description": "A modern, fast and productivity driven SQL client with a focus in UX.",
"license": "MIT",
"repository": "https://github.com/Fabio286/antares.git",
"repository": "https://github.com/antares-sql/antares.git",
"scripts": {
"debug": "npm run rebuild:electron && npm run debug-runner",
"debug-runner": "node scripts/devRunner.js --remote-debug",
"compile": "npm run compile:main && npm run compile:renderer",
"compile": "npm run compile:main && npm run compile:workers && npm run compile:renderer",
"compile:main": "webpack --mode=production --config webpack.main.config.js",
"compile:workers": "webpack --mode=production --config webpack.workers.config.js",
"compile:renderer": "webpack --mode=production --config webpack.renderer.config.js",
"build": "cross-env NODE_ENV=production npm run compile",
"build:local": "npm run build && electron-builder",
"build:appx": "npm run build:local -- --win appx",
"rebuild:electron": "npm run postinstall",
"rebuild:electron": "rimraf ./dist && npm run postinstall",
"release": "standard-version",
"release:pre": "npm run release -- --prerelease alpha",
"postinstall": "electron-builder install-app-deps",
@@ -105,18 +106,21 @@
"dependencies": {
"@electron/remote": "^2.0.1",
"@mdi/font": "^6.1.95",
"@turf/helpers": "^6.5.0",
"@vscode/vscode-languagedetection": "^1.0.21",
"ace-builds": "^1.4.13",
"better-sqlite3": "^7.4.4",
"better-sqlite3": "^7.5.0",
"electron-log": "^4.4.1",
"electron-store": "^8.0.1",
"electron-updater": "^4.3.9",
"electron-updater": "^4.6.1",
"electron-window-state": "^5.0.3",
"faker": "^5.5.3",
"leaflet": "^1.7.1",
"marked": "^4.0.0",
"moment": "^2.29.1",
"mysql2": "^2.3.2",
"pg": "^8.7.1",
"pg-query-stream": "^4.2.3",
"pgsql-ast-parser": "^7.2.1",
"source-map-support": "^0.5.20",
"spectre.css": "^0.5.9",
@@ -133,24 +137,24 @@
"all-contributors-cli": "^6.20.0",
"babel-loader": "^8.2.3",
"chalk": "^4.1.2",
"clean-webpack-plugin": "^4.0.0",
"cross-env": "^7.0.2",
"css-loader": "^6.5.0",
"electron": "^16.0.1",
"electron": "^17.0.1",
"electron-builder": "^22.14.11",
"electron-devtools-installer": "^3.2.0",
"eslint": "^7.32.0",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-promise": "^5.2.0",
"eslint-plugin-vue": "^8.0.3",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.4.3",
"mini-css-extract-plugin": "~2.4.5",
"node-loader": "^2.0.0",
"playwright": "^1.16.3",
"playwright": "^1.18.1",
"progress-webpack-plugin": "^1.0.12",
"rimraf": "^3.0.2",
"sass": "^1.42.1",
"sass-loader": "^12.3.0",
"standard-version": "^9.3.1",

View File

@@ -12,7 +12,7 @@ const { spawn } = require('child_process');
const mainConfig = require('../webpack.main.config');
const rendererConfig = require('../webpack.renderer.config');
// const workersConfig = require('../webpack.workers.config');
const workersConfig = require('../webpack.workers.config');
let electronProcess = null;
let manualRestart = null;
@@ -64,7 +64,7 @@ async function restartElectron () {
}
function startMain () {
const webpackSetup = webpack(mainConfig);
const webpackSetup = webpack([mainConfig, workersConfig]);
webpackSetup.compilers.forEach((compiler) => {
const { name } = compiler;

View File

@@ -134,7 +134,7 @@ export default class {
{ name: 'phoneNumberFormat', group: 'phone', types: ['string'] },
{ name: 'phoneFormats', group: 'phone', types: ['string'] },
{ name: 'number', group: 'random', types: ['string', 'number'], params: ['min', 'max'] },
{ name: 'number', group: 'datatype', types: ['string', 'number'], params: ['min', 'max'] },
{ name: 'float', group: 'random', types: ['string', 'float'], params: ['min', 'max'] },
{ name: 'arrayElement', group: 'random', types: ['string'] },
{ name: 'arrayElements', group: 'random', types: ['string'] },

View File

@@ -38,6 +38,9 @@ module.exports = {
databaseEdit: false,
schemaEdit: false,
schemaDrop: false,
schemaExport: false,
exportByChunks: false,
schemaImport: false,
tableSettings: false,
tableOptions: false,
tableArray: false,

View File

@@ -34,6 +34,9 @@ module.exports = {
schedulerAdd: true,
schemaEdit: true,
schemaDrop: true,
schemaExport: true,
exportByChunks: true,
schemaImport: true,
tableSettings: true,
viewSettings: true,
triggerSettings: true,

View File

@@ -31,6 +31,8 @@ module.exports = {
routineAdd: true,
functionAdd: true,
schemaDrop: true,
schemaExport: true,
schemaImport: true,
databaseEdit: false,
tableSettings: true,
viewSettings: true,

View File

@@ -66,6 +66,7 @@ module.exports = [
{
name: 'DECIMAL',
length: true,
scale: true,
collation: false,
unsigned: false,
zerofill: false
@@ -120,7 +121,7 @@ module.exports = [
{
name: 'JSON',
length: false,
collation: true,
collation: false,
unsigned: false,
zerofill: false
}
@@ -218,56 +219,56 @@ module.exports = [
types: [
{
name: 'POINT',
length: true,
length: false,
collation: false,
unsigned: false,
zerofill: false
},
{
name: 'LINESTRING',
length: true,
length: false,
collation: false,
unsigned: false,
zerofill: false
},
{
name: 'POLYGON',
length: true,
length: false,
collation: false,
unsigned: false,
zerofill: false
},
{
name: 'GEOMETRY',
length: true,
length: false,
collation: false,
unsigned: false,
zerofill: false
},
{
name: 'MULTIPOINT',
length: true,
length: false,
collation: false,
unsigned: false,
zerofill: false
},
{
name: 'MULTILINESTRING',
length: true,
length: false,
collation: false,
unsigned: false,
zerofill: false
},
{
name: 'MULTIPOLYGON',
length: true,
length: false,
collation: false,
unsigned: false,
zerofill: false
},
{
name: 'GEOMETRYCOLLECTION',
length: true,
name: 'GEOMCOLLECTION',
length: false,
collation: false,
unsigned: false,
zerofill: false

View File

@@ -22,11 +22,6 @@ module.exports = [
length: false,
unsigned: true
},
{
name: 'NUMERIC',
length: true,
unsigned: true
},
{
name: 'SMALLSERIAL',
length: false,
@@ -52,6 +47,12 @@ module.exports = [
length: false,
unsigned: true
},
{
name: 'NUMERIC',
length: true,
unsigned: true,
scale: true
},
{
name: 'DOUBLE PRECISION',
length: false,

View File

@@ -8,7 +8,9 @@ export const TEXT = [
export const LONG_TEXT = [
'TEXT',
'MEDIUMTEXT',
'LONGTEXT'
'LONGTEXT',
'JSON',
'VARBINARY'
];
export const ARRAY = [
@@ -39,6 +41,7 @@ export const NUMBER = [
export const FLOAT = [
'FLOAT',
'DECIMAL',
'DOUBLE',
'REAL',
'DOUBLE PRECISION',
@@ -82,3 +85,24 @@ export const BIT = [
'BIT',
'BIT VARYING'
];
export const SPATIAL = [
'POINT',
'LINESTRING',
'POLYGON',
'GEOMETRY',
'MULTIPOINT',
'MULTILINESTRING',
'MULTIPOLYGON',
'GEOMCOLLECTION',
'GEOMETRYCOLLECTION'
];
// Used to check multi spatial fields only
export const IS_MULTI_SPATIAL = [
'MULTIPOINT',
'MULTILINESTRING',
'MULTIPOLYGON',
'GEOMCOLLECTION',
'GEOMETRYCOLLECTION'
];

View File

@@ -0,0 +1,10 @@
/**
*
* @param {any[]} array
* @returns {number}
*/
export function getArrayDepth (array) {
return Array.isArray(array)
? 1 + Math.max(0, ...array.map(getArrayDepth))
: 0;
}

View File

@@ -33,7 +33,7 @@ const lookup = {
*/
export default function hexToBinary (hex) {
let binary = '';
for (let i = 0, len = hex.length; i < len; i++)
for (let i = 0; i < hex.length; i++)
binary += lookup[hex[i]];
return binary;

View File

@@ -23,7 +23,7 @@ export function mimeFromHex (hex) {
case '425A68':
return { ext: 'bz2', mime: 'application/x-bzip2' };
default:
switch (hex) { // 4 bites
switch (hex) { // 4 bytes
case '89504E47':
return { ext: 'png', mime: 'image/png' };
case '47494638':

View File

@@ -1,4 +1,4 @@
import { app, ipcMain } from 'electron';
import { app, ipcMain, dialog } from 'electron';
export default () => {
ipcMain.on('close-app', () => {
@@ -9,4 +9,12 @@ export default () => {
const key = false;
event.returnValue = key;
});
ipcMain.handle('showOpenDialog', (event, options) => {
return dialog.showOpenDialog(options);
});
ipcMain.handle('get-download-dir-path', () => {
return app.getPath('downloads');
});
};

View File

@@ -24,7 +24,8 @@ export default connections => {
key: conn.key ? fs.readFileSync(conn.key) : null,
cert: conn.cert ? fs.readFileSync(conn.cert) : null,
ca: conn.ca ? fs.readFileSync(conn.ca) : null,
ciphers: conn.ciphers
ciphers: conn.ciphers,
rejectUnauthorized: !conn.untrustedConnection
};
}
@@ -40,7 +41,7 @@ export default connections => {
}
try {
const connection = await ClientsFactory.getConnection({
const connection = await ClientsFactory.getClient({
client: conn.client,
params
});
@@ -84,7 +85,8 @@ export default connections => {
key: conn.key ? fs.readFileSync(conn.key) : null,
cert: conn.cert ? fs.readFileSync(conn.cert) : null,
ca: conn.ca ? fs.readFileSync(conn.ca) : null,
ciphers: conn.ciphers
ciphers: conn.ciphers,
rejectUnauthorized: !conn.untrustedConnection
};
}
@@ -100,7 +102,7 @@ export default connections => {
}
try {
const connection = ClientsFactory.getConnection({
const connection = ClientsFactory.getClient({
client: conn.client,
params,
poolSize: 5

View File

@@ -1,7 +1,15 @@
import fs from 'fs';
import path from 'path';
import { fork } from 'child_process';
import { ipcMain, dialog } from 'electron';
import { ipcMain } from 'electron';
// @TODO: need some factories
const isDevelopment = process.env.NODE_ENV !== 'production';
export default connections => {
let exporter = null;
let importer = null;
ipcMain.handle('create-schema', async (event, params) => {
try {
await connections[params.uid].createSchema(params);
@@ -37,9 +45,16 @@ export default connections => {
ipcMain.handle('get-schema-collation', async (event, params) => {
try {
const collation = await connections[params.uid].getDatabaseCollation(params);
const collation = await connections[params.uid].getDatabaseCollation(
params
);
return { status: 'success', response: collation.rows.length ? collation.rows[0].DEFAULT_COLLATION_NAME : '' };
return {
status: 'success',
response: collation.rows.length
? collation.rows[0].DEFAULT_COLLATION_NAME
: ''
};
}
catch (err) {
return { status: 'error', response: err.toString() };
@@ -48,7 +63,9 @@ export default connections => {
ipcMain.handle('get-structure', async (event, params) => {
try {
const structure = await connections[params.uid].getStructure(params.schemas);
const structure = await connections[params.uid].getStructure(
params.schemas
);
return { status: 'success', response: structure };
}
@@ -135,7 +152,7 @@ export default connections => {
}
});
ipcMain.handle('raw-query', async (event, { uid, query, schema, tabUid }) => {
ipcMain.handle('raw-query', async (event, { uid, query, schema, tabUid, autocommit }) => {
if (!query) return;
try {
@@ -144,6 +161,7 @@ export default connections => {
details: true,
schema,
tabUid,
autocommit,
comments: false
});
@@ -154,6 +172,169 @@ export default connections => {
}
});
ipcMain.handle('export', (event, { uid, type, tables, ...rest }) => {
if (exporter !== null) return;
return new Promise((resolve, reject) => {
(async () => {
if (fs.existsSync(rest.outputFile)) { // If file exists ask for replace
const result = await dialog.showMessageBox({
type: 'warning',
message: `File ${rest.outputFile} already exists. Do you want to replace it?`,
detail: 'A file with the same name already exists in the target folder. Replacing it will overwrite its current contents.',
buttons: ['Cancel', 'Replace'],
defaultId: 0,
cancelId: 0
});
if (result.response !== 1) {
resolve({
type: 'error',
response: 'Operation aborted'
});
return;
}
}
// Init exporter process
exporter = fork(isDevelopment ? './dist/exporter.js' : path.resolve(__dirname, './exporter.js'), [], {
execArgv: isDevelopment ? ['--inspect=9224'] : undefined
});
exporter.send({
type: 'init',
client: {
name: type,
config: await connections[uid].getDbConfig()
},
tables,
options: rest
});
// Exporter message listener
exporter.on('message', ({ type, payload }) => {
switch (type) {
case 'export-progress':
event.sender.send('export-progress', payload);
break;
case 'end':
setTimeout(() => { // Ensures that writing process has finished
exporter.kill();
exporter = null;
}, 2000);
resolve({ status: 'success', response: payload });
break;
case 'cancel':
exporter.kill();
exporter = null;
resolve({ status: 'error', response: 'Operation cancelled' });
break;
case 'error':
exporter.kill();
exporter = null;
resolve({ status: 'error', response: payload });
break;
}
});
exporter.on('exit', code => {
exporter = null;
resolve({ status: 'error', response: `Operation ended with code: ${code}` });
});
})();
});
});
ipcMain.handle('abort-export', async event => {
let willAbort = false;
if (exporter) {
const result = await dialog.showMessageBox({
type: 'warning',
message: 'Are you sure you want to abort the export',
buttons: ['Cancel', 'Abort'],
defaultId: 0,
cancelId: 0
});
if (result.response === 1) {
willAbort = true;
exporter.send({ type: 'cancel' });
}
}
return { status: 'success', response: { willAbort } };
});
ipcMain.handle('import-sql', async (event, options) => {
if (importer !== null) return;
return new Promise((resolve, reject) => {
(async () => {
const dbConfig = await connections[options.uid].getDbConfig();
// Init importer process
importer = fork(isDevelopment ? './dist/importer.js' : path.resolve(__dirname, './importer.js'), [], {
execArgv: isDevelopment ? ['--inspect=9224'] : undefined
});
importer.send({
type: 'init',
dbConfig,
options
});
// Importer message listener
importer.on('message', ({ type, payload }) => {
switch (type) {
case 'import-progress':
event.sender.send('import-progress', payload);
break;
case 'query-error':
event.sender.send('query-error', payload);
break;
case 'end':
setTimeout(() => { // Ensures that writing process has finished
importer?.kill();
importer = null;
}, 2000);
resolve({ status: 'success', response: payload });
break;
case 'cancel':
importer.kill();
importer = null;
resolve({ status: 'error', response: 'Operation cancelled' });
break;
case 'error':
importer.kill();
importer = null;
resolve({ status: 'error', response: payload });
break;
}
});
})();
});
});
ipcMain.handle('abort-import-sql', async event => {
let willAbort = false;
if (importer) {
const result = await dialog.showMessageBox({
type: 'warning',
message: 'Are you sure you want to abort the import',
buttons: ['Cancel', 'Abort'],
defaultId: 0,
cancelId: 0
});
if (result.response === 1) {
willAbort = true;
importer.send({ type: 'cancel' });
}
}
return { status: 'success', response: { willAbort } };
});
ipcMain.handle('kill-tab-query', async (event, { uid, tabUid }) => {
if (!tabUid) return;
@@ -165,4 +346,40 @@ export default connections => {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('commit-tab', async (event, { uid, tabUid }) => {
if (!tabUid) return;
try {
await connections[uid].commitTab(tabUid);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('rollback-tab', async (event, { uid, tabUid }) => {
if (!tabUid) return;
try {
await connections[uid].rollbackTab(tabUid);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('destroy-connection-to-commit', async (event, { uid, tabUid }) => {
if (!tabUid) return;
try {
await connections[uid].destroyConnectionToCommit(tabUid);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
};

View File

@@ -149,7 +149,7 @@ export default (connections) => {
}
}
}
else if ([...BIT].includes(params.type)) {
else if (BIT.includes(params.type)) {
escapedParam = `b'${sqlEscaper(params.content)}'`;
reload = true;
}

View File

@@ -1,4 +1,10 @@
'use strict';
const queryLogger = sql => {
// Remove comments, newlines and multiple spaces
const escapedSql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '').replace(/\s\s+/g, ' ');
console.log(escapedSql);
};
/**
* As Simple As Possible Query Builder Core
*
@@ -17,7 +23,7 @@ export class AntaresCore {
this._poolSize = args.poolSize || false;
this._connection = null;
this._ssh = null;
this._logger = args.logger || console.log;
this._logger = args.logger || queryLogger;
this._queryDefaults = {
schema: '',

View File

@@ -2,13 +2,6 @@
import { MySQLClient } from './clients/MySQLClient';
import { PostgreSQLClient } from './clients/PostgreSQLClient';
import { SQLiteClient } from './clients/SQLiteClient';
const queryLogger = sql => {
// Remove comments, newlines and multiple spaces
const escapedSql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '').replace(/\s\s+/g, ' ');
console.log(escapedSql);
};
export class ClientsFactory {
/**
* Returns a database connection based on received args.
@@ -29,9 +22,7 @@ export class ClientsFactory {
* @returns Database Connection
* @memberof ClientsFactory
*/
static getConnection (args) {
args.logger = queryLogger;
static getClient (args) {
switch (args.client) {
case 'mysql':
case 'maria':

View File

@@ -10,6 +10,7 @@ export class MySQLClient extends AntaresCore {
this._schema = null;
this._runningConnections = new Map();
this._connectionsToCommit = new Map();
this.types = {
0: 'DECIMAL',
@@ -100,10 +101,32 @@ export class MySQLClient extends AntaresCore {
.filter(_type => _type.name === type.toUpperCase())[0];
}
_reducer (acc, curr) {
const type = typeof curr;
switch (type) {
case 'number':
case 'string':
return [...acc, curr];
case 'object':
if (Array.isArray(curr))
return [...acc, ...curr];
else {
const clausoles = [];
for (const key in curr)
clausoles.push(`\`${key}\` ${curr[key]}`);
return clausoles;
}
}
}
/**
*
* @returns dbConfig
* @memberof MySQLClient
*/
async connect () {
async getDbConfig () {
delete this._params.application_name;
const dbConfig = {
@@ -111,7 +134,9 @@ export class MySQLClient extends AntaresCore {
port: this._params.port,
user: this._params.user,
password: this._params.password,
ssl: null
ssl: null,
supportBigNumbers: true,
bigNumberStrings: true
};
if (this._params.schema?.length) dbConfig.database = this._params.schema;
@@ -126,6 +151,8 @@ export class MySQLClient extends AntaresCore {
remoteAddr: this._params.host,
remotePort: this._params.port
});
dbConfig.host = this._ssh.config.host;
dbConfig.port = tunnel.localPort;
}
catch (err) {
@@ -134,48 +161,17 @@ export class MySQLClient extends AntaresCore {
}
}
if (!this._poolSize) {
this._connection = await mysql.createConnection(dbConfig);
return dbConfig;
}
// ANSI_QUOTES check
const res = await this.getVariable('sql_mode', 'global');
const sqlMode = res?.value.split(',');
const hasAnsiQuotes = sqlMode.includes('ANSI_QUOTES');
if (this._params.readonly)
await this.raw('SET SESSION TRANSACTION READ ONLY');
if (hasAnsiQuotes)
await this.raw(`SET SESSION sql_mode = "${sqlMode.filter(m => m !== 'ANSI_QUOTES').join(',')}"`);
}
else {
this._connection = mysql.createPool({
...dbConfig,
connectionLimit: this._poolSize,
typeCast: (field, next) => {
if (field.type === 'DATETIME')
return field.string();
else
return next();
}
});
// ANSI_QUOTES check
const res = await this.getVariable('sql_mode', 'global');
const sqlMode = res?.value.split(',');
const hasAnsiQuotes = sqlMode.includes('ANSI_QUOTES');
if (hasAnsiQuotes)
await this._connection.query(`SET SESSION sql_mode = "${sqlMode.filter(m => m !== 'ANSI_QUOTES').join(',')}"`);
this._connection.on('connection', connection => {
if (this._params.readonly)
connection.query('SET SESSION TRANSACTION READ ONLY');
if (hasAnsiQuotes)
connection.query(`SET SESSION sql_mode = "${sqlMode.filter(m => m !== 'ANSI_QUOTES').join(',')}"`);
});
}
/**
* @memberof MySQLClient
*/
async connect () {
if (!this._poolSize)
this._connection = await this.getConnection();
else
this._connection = await this.getConnectionPool();
}
/**
@@ -186,6 +182,64 @@ export class MySQLClient extends AntaresCore {
if (this._ssh) this._ssh.close();
}
async getConnection () {
const dbConfig = await this.getDbConfig();
const connection = await mysql.createConnection({
...dbConfig,
typeCast: (field, next) => {
if (field.type === 'DATETIME')
return field.string();
else
return next();
}
});
// ANSI_QUOTES check
const [res] = await connection.query('SHOW GLOBAL VARIABLES LIKE \'%sql_mode%\'');
const sqlMode = res[0]?.Variable_name?.split(',');
const hasAnsiQuotes = sqlMode.includes('ANSI_QUOTES');
if (this._params.readonly)
await connection.query('SET SESSION TRANSACTION READ ONLY');
if (hasAnsiQuotes)
await connection.query(`SET SESSION sql_mode = "${sqlMode.filter(m => m !== 'ANSI_QUOTES').join(',')}"`);
return connection;
}
async getConnectionPool () {
const dbConfig = await this.getDbConfig();
const connection = mysql.createPool({
...dbConfig,
connectionLimit: this._poolSize,
typeCast: (field, next) => {
if (field.type === 'DATETIME')
return field.string();
else
return next();
}
});
// ANSI_QUOTES check
const [res] = await connection.query('SHOW GLOBAL VARIABLES LIKE \'%sql_mode%\'');
const sqlMode = res[0]?.Variable_name?.split(',');
const hasAnsiQuotes = sqlMode.includes('ANSI_QUOTES');
if (hasAnsiQuotes)
await connection.query(`SET SESSION sql_mode = "${sqlMode.filter(m => m !== 'ANSI_QUOTES').join(',')}"`);
connection.on('connection', conn => {
if (this._params.readonly)
conn.query('SET SESSION TRANSACTION READ ONLY');
if (hasAnsiQuotes)
conn.query(`SET SESSION sql_mode = "${sqlMode.filter(m => m !== 'ANSI_QUOTES').join(',')}"`);
});
return connection;
}
/**
* Executes an USE query
*
@@ -254,7 +308,7 @@ export class MySQLClient extends AntaresCore {
break;
}
const tableSize = table.Data_length + table.Index_length;
const tableSize = Number(table.Data_length) + Number(table.Index_length);
schemaSize += tableSize;
return {
@@ -395,7 +449,7 @@ export class MySQLClient extends AntaresCore {
return acc;
}, '')
.replaceAll('\n', '')
.split(',')
.split(/,\s?(?![^(]*\))/)
.map(f => {
try {
const fieldArr = f.trim().split(' ');
@@ -435,18 +489,25 @@ export class MySQLClient extends AntaresCore {
return rows.map(field => {
let numLength = field.COLUMN_TYPE.match(/int\(([^)]+)\)/);
numLength = numLength ? +numLength.pop() : null;
numLength = numLength ? +numLength.pop() : field.NUMERIC_PRECISION || null;
const enumValues = /(enum|set)/.test(field.COLUMN_TYPE)
? field.COLUMN_TYPE.match(/\(([^)]+)\)/)[0].slice(1, -1)
: null;
const defaultValue = (remappedFields && remappedFields[field.COLUMN_NAME])
? remappedFields[field.COLUMN_NAME].default
: field.COLUMN_DEFAULT;
return {
name: field.COLUMN_NAME,
key: field.COLUMN_KEY.toLowerCase(),
type: (remappedFields && remappedFields[field.COLUMN_NAME]) ? remappedFields[field.COLUMN_NAME].type : field.DATA_TYPE,
type: (remappedFields && remappedFields[field.COLUMN_NAME])
? remappedFields[field.COLUMN_NAME].type
: field.DATA_TYPE.toUpperCase(),
schema: field.TABLE_SCHEMA,
table: field.TABLE_NAME,
numPrecision: field.NUMERIC_PRECISION,
numScale: Number(field.NUMERIC_SCALE),
numLength,
enumValues,
datePrecision: field.DATETIME_PRECISION,
@@ -455,10 +516,11 @@ export class MySQLClient extends AntaresCore {
unsigned: field.COLUMN_TYPE.includes('unsigned'),
zerofill: field.COLUMN_TYPE.includes('zerofill'),
order: field.ORDINAL_POSITION,
default: (remappedFields && remappedFields[field.COLUMN_NAME]) ? remappedFields[field.COLUMN_NAME].default : field.COLUMN_DEFAULT,
default: defaultValue,
charset: field.CHARACTER_SET_NAME,
collation: field.COLLATION_NAME,
autoIncrement: field.EXTRA.includes('auto_increment'),
generated: field.EXTRA.toLowerCase().includes('generated'),
onUpdate: field.EXTRA.toLowerCase().includes('on update')
? field.EXTRA.substr(field.EXTRA.indexOf('on update') + 9, field.EXTRA.length).trim()
: '',
@@ -1269,6 +1331,36 @@ export class MySQLClient extends AntaresCore {
return await this.killProcess(id);
}
/**
*
* @param {string} tabUid
* @returns {Promise<null>}
*/
async commitTab (tabUid) {
const connection = this._connectionsToCommit.get(tabUid);
if (connection)
return await connection.query('COMMIT');
}
/**
*
* @param {string} tabUid
* @returns {Promise<null>}
*/
async rollbackTab (tabUid) {
const connection = this._connectionsToCommit.get(tabUid);
if (connection)
return await connection.query('ROLLBACK');
}
destroyConnectionToCommit (tabUid) {
const connection = this._connectionsToCommit.get(tabUid);
if (connection) {
connection.destroy();
this._connectionsToCommit.delete(tabUid);
}
}
/**
* CREATE TABLE
*
@@ -1295,7 +1387,7 @@ export class MySQLClient extends AntaresCore {
const length = typeInfo.length ? field.enumValues || field.numLength || field.charLength || field.datePrecision : false;
newColumns.push(`\`${field.name}\`
${field.type.toUpperCase()}${length ? `(${length})` : ''}
${field.type.toUpperCase()}${length ? `(${length}${field.numScale ? `,${field.numScale}` : ''})` : ''}
${field.unsigned ? 'UNSIGNED' : ''}
${field.zerofill ? 'ZEROFILL' : ''}
${field.nullable ? 'NULL' : 'NOT NULL'}
@@ -1364,7 +1456,7 @@ export class MySQLClient extends AntaresCore {
const length = typeInfo.length ? addition.enumValues || addition.numLength || addition.charLength || addition.datePrecision : false;
alterColumns.push(`ADD COLUMN \`${addition.name}\`
${addition.type.toUpperCase()}${length ? `(${length})` : ''}
${addition.type.toUpperCase()}${length ? `(${length}${addition.numScale ? `,${addition.numScale}` : ''})` : ''}
${addition.unsigned ? 'UNSIGNED' : ''}
${addition.zerofill ? 'ZEROFILL' : ''}
${addition.nullable ? 'NULL' : 'NOT NULL'}
@@ -1402,7 +1494,7 @@ export class MySQLClient extends AntaresCore {
const length = typeInfo.length ? change.enumValues || change.numLength || change.charLength || change.datePrecision : false;
alterColumns.push(`CHANGE COLUMN \`${change.orgName}\` \`${change.name}\`
${change.type.toUpperCase()}${length ? `(${length})` : ''}
${change.type.toUpperCase()}${length ? `(${length}${change.numScale ? `,${change.numScale}` : ''})` : ''}
${change.unsigned ? 'UNSIGNED' : ''}
${change.zerofill ? 'ZEROFILL' : ''}
${change.nullable ? 'NULL' : 'NOT NULL'}
@@ -1533,7 +1625,7 @@ export class MySQLClient extends AntaresCore {
let insertRaw = '';
if (this._query.insert.length) {
const fieldsList = Object.keys(this._query.insert[0]);
const fieldsList = Object.keys(this._query.insert[0]).map(col => '`' + col + '`');
const rowsList = this._query.insert.map(el => `(${Object.values(el).join(', ')})`);
insertRaw = `(${fieldsList.join(', ')}) VALUES ${rowsList.join(', ')} `;
@@ -1573,6 +1665,7 @@ export class MySQLClient extends AntaresCore {
details: false,
split: true,
comments: true,
autocommit: true,
...args
};
@@ -1587,8 +1680,21 @@ export class MySQLClient extends AntaresCore {
.filter(Boolean)
.map(q => q.trim())
: [sql];
let connection;
const isPool = typeof this._connection.getConnection === 'function';
const connection = isPool ? await this._connection.getConnection() : this._connection;
if (!args.autocommit && args.tabUid) { // autocommit OFF
if (this._connectionsToCommit.has(args.tabUid))
connection = this._connectionsToCommit.get(args.tabUid);
else {
connection = await this.getConnection();
await connection.query('SET SESSION autocommit=0');
this._connectionsToCommit.set(args.tabUid, connection);
}
}
else// autocommit ON
connection = isPool ? await this._connection.getConnection() : this._connection;
if (args.tabUid && isPool)
this._runningConnections.set(args.tabUid, connection.connection.connectionId);
@@ -1653,7 +1759,7 @@ export class MySQLClient extends AntaresCore {
});
}
catch (err) {
if (isPool) {
if (isPool && args.autocommit) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
@@ -1665,7 +1771,7 @@ export class MySQLClient extends AntaresCore {
keysArr = keysArr ? [...keysArr, ...response] : response;
}
catch (err) {
if (isPool) {
if (isPool && args.autocommit) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
@@ -1683,7 +1789,7 @@ export class MySQLClient extends AntaresCore {
keys: keysArr
});
}).catch((err) => {
if (isPool) {
if (isPool && args.autocommit) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
@@ -1694,7 +1800,7 @@ export class MySQLClient extends AntaresCore {
resultsArr.push({ rows, report, fields, keys, duration });
}
if (isPool) {
if (isPool && args.autocommit) {
connection.release();
this._runningConnections.delete(args.tabUid);
}

View File

@@ -9,7 +9,6 @@ function pgToString (value) {
return value.toString();
}
types.setTypeParser(20, a => parseInt(a));// bigint string to number
types.setTypeParser(1082, pgToString); // date
types.setTypeParser(1083, pgToString); // time
types.setTypeParser(1114, pgToString); // timestamp
@@ -22,6 +21,7 @@ export class PostgreSQLClient extends AntaresCore {
this._schema = null;
this._runningConnections = new Map();
this._connectionsToCommit = new Map();
this.types = {};
for (const key in types.builtins)
@@ -71,9 +71,11 @@ export class PostgreSQLClient extends AntaresCore {
}
/**
*
* @returns dbConfig
* @memberof PostgreSQLClient
*/
async connect () {
async getDbConfig () {
const dbConfig = {
host: this._params.host,
port: this._params.port,
@@ -94,6 +96,8 @@ export class PostgreSQLClient extends AntaresCore {
remoteAddr: this._params.host,
remotePort: this._params.port
});
dbConfig.host = this._ssh.config.host;
dbConfig.port = tunnel.localPort;
}
catch (err) {
@@ -102,24 +106,43 @@ export class PostgreSQLClient extends AntaresCore {
}
}
if (!this._poolSize) {
const client = new Client(dbConfig);
await client.connect();
this._connection = client;
return dbConfig;
}
if (this._params.readonly)
await this.raw('SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY');
}
else {
const pool = new Pool({ ...dbConfig, max: this._poolSize });
this._connection = pool;
/**
* @memberof PostgreSQLClient
*/
async connect () {
if (!this._poolSize)
this._connection = await this.getConnection();
else
this._connection = await this.getConnectionPool();
}
if (this._params.readonly) {
this._connection.on('connect', connection => {
connection.query('SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY');
});
}
async getConnection () {
const dbConfig = await this.getDbConfig();
const client = new Client(dbConfig);
await client.connect();
const connection = client;
if (this._params.readonly)
await connection.query('SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY');
return connection;
}
async getConnectionPool () {
const dbConfig = await this.getDbConfig();
const pool = new Pool({ ...dbConfig, max: this._poolSize });
const connection = pool;
if (this._params.readonly) {
connection.on('connect', conn => {
conn.query('SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY');
});
}
return connection;
}
/**
@@ -131,15 +154,23 @@ export class PostgreSQLClient extends AntaresCore {
}
/**
* Executes an "USE" query
* Executes an 'SET search_path TO "${schema}"' query
*
* @param {String} schema
* @param {Object?} connection optional
* @memberof PostgreSQLClient
*/
use (schema) {
use (schema, connection) {
this._schema = schema;
if (schema)
return this.raw(`SET search_path TO "${schema}"`);
if (schema) {
const sql = `SET search_path TO "${schema}"`;
if (connection === undefined)
return this.raw(sql);
else
return connection.query(sql);
}
}
/**
@@ -209,7 +240,7 @@ export class PostgreSQLClient extends AntaresCore {
if (schemas.has(db.database)) {
// TABLES
const remappedTables = tablesArr.filter(table => table.Db === db.database).map(table => {
const tableSize = +table.data_length + table.index_length;
const tableSize = Number(table.data_length) + Number(table.index_length);
schemaSize += tableSize;
return {
@@ -318,6 +349,7 @@ export class PostgreSQLClient extends AntaresCore {
isArray,
schema: field.table_schema,
table: field.table_name,
numScale: field.numeric_scale,
numPrecision: field.numeric_precision,
datePrecision: field.datetime_precision,
charLength: field.character_maximum_length,
@@ -540,7 +572,7 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof MySQLClient
*/
async dropSchema (params) {
return await this.raw(`DROP SCHEMA "${params.database}"`);
return await this.raw(`DROP SCHEMA "${params.database}" CASCADE`);
}
/**
@@ -1109,6 +1141,40 @@ export class PostgreSQLClient extends AntaresCore {
return await this.raw(`SELECT pg_cancel_backend(${id})`);
}
/**
*
* @param {string} tabUid
* @returns {Promise<null>}
*/
async commitTab (tabUid) {
const connection = this._connectionsToCommit.get(tabUid);
if (connection) {
await connection.query('COMMIT');
return this.destroyConnectionToCommit(tabUid);
}
}
/**
*
* @param {string} tabUid
* @returns {Promise<null>}
*/
async rollbackTab (tabUid) {
const connection = this._connectionsToCommit.get(tabUid);
if (connection) {
await connection.query('ROLLBACK');
return this.destroyConnectionToCommit(tabUid);
}
}
destroyConnectionToCommit (tabUid) {
const connection = this._connectionsToCommit.get(tabUid);
if (connection) {
connection.end();
this._connectionsToCommit.delete(tabUid);
}
}
/**
* CREATE TABLE
*
@@ -1136,7 +1202,7 @@ export class PostgreSQLClient extends AntaresCore {
const length = typeInfo.length ? field.enumValues || field.numLength || field.charLength || field.datePrecision : false;
newColumns.push(`"${field.name}"
${field.type.toUpperCase()}${length ? `(${length})` : ''}
${field.type.toUpperCase()}${length ? `(${length}${field.numScale !== null ? `,${field.numScale}` : ''})` : ''}
${field.unsigned ? 'UNSIGNED' : ''}
${field.zerofill ? 'ZEROFILL' : ''}
${field.nullable ? 'NULL' : 'NOT NULL'}
@@ -1200,7 +1266,7 @@ export class PostgreSQLClient extends AntaresCore {
const length = typeInfo.length ? addition.numLength || addition.charLength || addition.datePrecision : false;
alterColumns.push(`ADD COLUMN "${addition.name}"
${addition.type.toUpperCase()}${length ? `(${length})` : ''}${addition.isArray ? '[]' : ''}
${addition.type.toUpperCase()}${length ? `(${length}${addition.numScale !== null ? `,${addition.numScale}` : ''})` : ''}${addition.isArray ? '[]' : ''}
${addition.unsigned ? 'UNSIGNED' : ''}
${addition.zerofill ? 'ZEROFILL' : ''}
${addition.nullable ? 'NULL' : 'NOT NULL'}
@@ -1246,7 +1312,7 @@ export class PostgreSQLClient extends AntaresCore {
localType = change.type.toLowerCase();
}
alterColumns.push(`ALTER COLUMN "${change.name}" TYPE ${localType}${length ? `(${length})` : ''}${change.isArray ? '[]' : ''} USING "${change.name}"::${localType}`);
alterColumns.push(`ALTER COLUMN "${change.name}" TYPE ${localType}${length ? `(${length}${change.numScale ? `,${change.numScale}` : ''})` : ''}${change.isArray ? '[]' : ''} USING "${change.name}"::${localType}`);
alterColumns.push(`ALTER COLUMN "${change.name}" ${change.nullable ? 'DROP NOT NULL' : 'SET NOT NULL'}`);
alterColumns.push(`ALTER COLUMN "${change.name}" ${change.default ? `SET DEFAULT ${change.default}` : 'DROP DEFAULT'}`);
@@ -1420,6 +1486,7 @@ export class PostgreSQLClient extends AntaresCore {
details: false,
split: true,
comments: true,
autocommit: true,
...args
};
@@ -1434,14 +1501,26 @@ export class PostgreSQLClient extends AntaresCore {
.map(q => q.trim())
: [sql];
let connection;
const isPool = this._connection instanceof Pool;
const connection = isPool ? await this._connection.connect() : this._connection;
if (!args.autocommit && args.tabUid) { // autocommit OFF
if (this._connectionsToCommit.has(args.tabUid))
connection = this._connectionsToCommit.get(args.tabUid);
else {
connection = await this.getConnection();
await connection.query('START TRANSACTION');
this._connectionsToCommit.set(args.tabUid, connection);
}
}
else// autocommit ON
connection = isPool ? await this._connection.connect() : this._connection;
if (args.tabUid && isPool)
this._runningConnections.set(args.tabUid, connection.processID);
if (args.schema && args.schema !== 'public')
await this.use(args.schema);
await this.use(args.schema, connection);
for (const query of queries) {
if (!query) continue;
@@ -1545,7 +1624,7 @@ export class PostgreSQLClient extends AntaresCore {
});
}
catch (err) {
if (isPool) {
if (isPool && args.autocommit) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
@@ -1557,7 +1636,7 @@ export class PostgreSQLClient extends AntaresCore {
keysArr = keysArr ? [...keysArr, ...response] : response;
}
catch (err) {
if (isPool) {
if (isPool && args.autocommit) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
@@ -1576,7 +1655,7 @@ export class PostgreSQLClient extends AntaresCore {
});
}
catch (err) {
if (isPool) {
if (isPool && args.autocommit) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
@@ -1588,7 +1667,7 @@ export class PostgreSQLClient extends AntaresCore {
resultsArr.push({ rows, report, fields, keys, duration });
}
if (isPool) {
if (isPool && args.autocommit) {
connection.release();
this._runningConnections.delete(args.tabUid);
}

View File

@@ -9,6 +9,7 @@ export class SQLiteClient extends AntaresCore {
super(args);
this._schema = null;
this._connectionsToCommit = new Map();
}
_getTypeInfo (type) {
@@ -21,7 +22,11 @@ export class SQLiteClient extends AntaresCore {
* @memberof SQLiteClient
*/
async connect () {
this._connection = sqlite(this._params.databasePath, {
this._connection = this.getConnection();
}
getConnection () {
return sqlite(this._params.databasePath, {
fileMustExist: true,
readonly: this._params.readonly
});
@@ -158,7 +163,7 @@ export class SQLiteClient extends AntaresCore {
nullable: !field.notnull,
unsigned: null,
zerofill: null,
order: field.cid + 1,
order: typeof field.cid === 'string' ? +field.cid + 1 : field.cid + 1,
default: field.dflt_value,
charset: null,
collation: null,
@@ -446,6 +451,40 @@ export class SQLiteClient extends AntaresCore {
async killProcess () {}
/**
*
* @param {string} tabUid
* @returns {Promise<null>}
*/
async commitTab (tabUid) {
const connection = this._connectionsToCommit.get(tabUid);
if (connection) {
connection.prepare('COMMIT').run();
return this.destroyConnectionToCommit(tabUid);
}
}
/**
*
* @param {string} tabUid
* @returns {Promise<null>}
*/
async rollbackTab (tabUid) {
const connection = this._connectionsToCommit.get(tabUid);
if (connection) {
connection.prepare('ROLLBACK').run();
return this.destroyConnectionToCommit(tabUid);
}
}
destroyConnectionToCommit (tabUid) {
const connection = this._connectionsToCommit.get(tabUid);
if (connection) {
connection.close();
this._connectionsToCommit.delete(tabUid);
}
}
/**
* CREATE TABLE
*
@@ -666,6 +705,7 @@ export class SQLiteClient extends AntaresCore {
details: false,
split: true,
comments: true,
autocommit: true,
...args
};
@@ -679,7 +719,20 @@ export class SQLiteClient extends AntaresCore {
.filter(Boolean)
.map(q => q.trim())
: [sql];
const connection = this._connection;
let connection;
if (!args.autocommit && args.tabUid) { // autocommit OFF
if (this._connectionsToCommit.has(args.tabUid))
connection = this._connectionsToCommit.get(args.tabUid);
else {
connection = this.getConnection();
connection.prepare('BEGIN TRANSACTION').run();
this._connectionsToCommit.set(args.tabUid, connection);
}
}
else// autocommit ON
connection = this._connection;
for (const query of queries) {
if (!query) continue;

View File

@@ -0,0 +1,85 @@
import fs from 'fs';
import { createGzip } from 'zlib';
import path from 'path';
import EventEmitter from 'events';
export class BaseExporter extends EventEmitter {
constructor (tables, options) {
super();
this._tables = tables;
this._options = options;
this._isCancelled = false;
this._outputFileStream = fs.createWriteStream(this._options.outputFile, { flags: 'w' });
this._processedStream = null;
this._state = {};
if (this._options.outputFormat === 'sql.zip') {
const outputZipStream = createGzip();
outputZipStream.pipe(this._outputFileStream);
this._processedStream = outputZipStream;
}
else
this._processedStream = this._outputFileStream;
this._processedStream.once('error', err => {
this._isCancelled = true;
this.emit('error', err);
});
}
async run () {
try {
this.emit('start', this);
await this.dump();
}
catch (err) {
this.emit('error', err);
throw err;
}
finally {
this._processedStream.end();
this.emit('end');
}
}
get isCancelled () {
return this._isCancelled;
}
get outputFile () {
return this._options.outputFile;
}
outputFileExists () {
return fs.existsSync(this._options.outputFile);
}
cancel () {
this._isCancelled = true;
this.emit('cancel');
this.emitUpdate({ op: 'cancelling' });
}
emitUpdate (state) {
this.emit('progress', { ...this._state, ...state });
}
writeString (data) {
if (this._isCancelled) return;
try {
fs.accessSync(this._options.outputFile);
}
catch (err) {
this._isCancelled = true;
const fileName = path.basename(this._options.outputFile);
this.emit('error', `The file ${fileName} is not accessible`);
}
this._processedStream.write(data);
}
dump () {
throw new Error('Exporter must implement the "dump" method');
}
}

View File

@@ -0,0 +1,418 @@
import { SqlExporter } from './SqlExporter';
import { BLOB, BIT, DATE, DATETIME, FLOAT, SPATIAL, IS_MULTI_SPATIAL, NUMBER } from 'common/fieldTypes';
import hexToBinary from 'common/libs/hexToBinary';
import { getArrayDepth } from 'common/libs/getArrayDepth';
import moment from 'moment';
import { lineString, point, polygon } from '@turf/helpers';
export default class MysqlExporter extends SqlExporter {
constructor (...args) {
super(...args);
this._commentChar = '#';
}
async getSqlHeader () {
let dump = await super.getSqlHeader();
dump += `
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
SET NAMES utf8mb4;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;`;
return dump;
}
async getFooter () {
const footer = await super.getFooter();
return `/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
${footer}
`;
}
async getCreateTable (tableName) {
const { rows } = await this._client.raw(
`SHOW CREATE TABLE \`${this.schemaName}\`.\`${tableName}\``
);
if (rows.length !== 1) return '';
const col = 'Create View' in rows[0] ? 'Create View' : 'Create Table';
return rows[0][col] + ';';
}
getDropTable (tableName) {
return `DROP TABLE IF EXISTS \`${tableName}\`;`;
}
async * getTableInsert (tableName) {
let rowCount = 0;
let sqlStr = '';
const countResults = await this._client.raw(`SELECT COUNT(1) as count FROM \`${this.schemaName}\`.\`${tableName}\``);
if (countResults.rows.length === 1) rowCount = countResults.rows[0].count;
if (rowCount > 0) {
let queryLength = 0;
let rowsWritten = 0;
let rowIndex = 0;
const { sqlInsertDivider, sqlInsertAfter } = this._options;
const columns = await this._client.getTableColumns({
table: tableName,
schema: this.schemaName
});
const notGeneratedColumns = columns.filter(col => !col.generated);
const columnNames = notGeneratedColumns.map(col => '`' + col.name + '`');
const insertStmt = `INSERT INTO \`${tableName}\` (${columnNames.join(
', '
)}) VALUES`;
sqlStr += `LOCK TABLES \`${tableName}\` WRITE;\n`;
sqlStr += `/*!40000 ALTER TABLE \`${tableName}\` DISABLE KEYS */;`;
sqlStr += '\n\n';
yield sqlStr;
yield insertStmt;
const stream = await this._queryStream(
`SELECT ${columnNames.join(', ')} FROM \`${this.schemaName}\`.\`${tableName}\``
);
for await (const row of stream) {
if (this.isCancelled) {
stream.destroy();
yield null;
return;
}
let sqlInsertString = '';
if (
(sqlInsertDivider === 'bytes' && queryLength >= sqlInsertAfter * 1024) ||
(sqlInsertDivider === 'rows' && rowsWritten === sqlInsertAfter)
) {
sqlInsertString += `;\n${insertStmt}\n\t(`;
queryLength = 0;
rowsWritten = 0;
}
else if (parseInt(rowIndex) === 0) sqlInsertString += '\n\t(';
else sqlInsertString += ',\n\t(';
for (const i in notGeneratedColumns) {
const column = notGeneratedColumns[i];
const val = row[column.name];
if (val === null) sqlInsertString += 'NULL';
else if (DATE.includes(column.type)) {
sqlInsertString += moment(val).isValid()
? this.escapeAndQuote(moment(val).format('YYYY-MM-DD'))
: val;
}
else if (DATETIME.includes(column.type)) {
let datePrecision = '';
for (let i = 0; i < column.precision; i++)
datePrecision += i === 0 ? '.S' : 'S';
sqlInsertString += moment(val).isValid()
? this.escapeAndQuote(moment(val).format(`YYYY-MM-DD HH:mm:ss${datePrecision}`))
: this.escapeAndQuote(val);
}
else if (BIT.includes(column.type))
sqlInsertString += `b'${hexToBinary(Buffer.from(val).toString('hex'))}'`;
else if (BLOB.includes(column.type))
sqlInsertString += `X'${val.toString('hex').toUpperCase()}'`;
else if (NUMBER.includes(column.type))
sqlInsertString += val;
else if (FLOAT.includes(column.type))
sqlInsertString += parseFloat(val);
else if (SPATIAL.includes(column.type)) {
let geoJson;
if (IS_MULTI_SPATIAL.includes(column.type)) {
const features = [];
for (const element of val)
features.push(this.getMarkers(element));
geoJson = {
type: 'FeatureCollection',
features
};
}
else
geoJson = this._getGeoJSON(val);
sqlInsertString += `ST_GeomFromGeoJSON('${JSON.stringify(geoJson)}')`;
}
else if (val === '') sqlInsertString += '\'\'';
else {
sqlInsertString += typeof val === 'string'
? this.escapeAndQuote(val)
: typeof val === 'object'
? this.escapeAndQuote(JSON.stringify(val))
: val;
}
if (parseInt(i) !== notGeneratedColumns.length - 1)
sqlInsertString += ', ';
}
sqlInsertString += ')';
queryLength += sqlInsertString.length;
rowsWritten++;
rowIndex++;
yield sqlInsertString;
}
sqlStr = ';\n\n';
sqlStr += `/*!40000 ALTER TABLE \`${tableName}\` ENABLE KEYS */;\n`;
sqlStr += 'UNLOCK TABLES;';
yield sqlStr;
}
}
async getViews () {
const { rows: views } = await this._client.raw(
`SHOW TABLE STATUS FROM \`${this.schemaName}\` WHERE Comment = 'VIEW'`
);
let sqlString = '';
for (const view of views) {
sqlString += `DROP VIEW IF EXISTS \`${view.Name}\`;\n`;
const viewSyntax = await this.getCreateTable(view.Name);
sqlString += viewSyntax.replaceAll('`' + this.schemaName + '`.', '');
sqlString += '\n';
}
return sqlString;
}
async getTriggers () {
const { rows: triggers } = await this._client.raw(
`SHOW TRIGGERS FROM \`${this.schemaName}\``
);
const generatedTables = this._tables
.filter(t => t.includeStructure)
.map(t => t.table);
let sqlString = '';
for (const trigger of triggers) {
const {
Trigger: name,
Timing: timing,
Event: event,
Table: table,
Statement: statement,
sql_mode: sqlMode
} = trigger;
if (!generatedTables.includes(table)) continue;
const definer = this.getEscapedDefiner(trigger.Definer);
sqlString += '/*!50003 SET @OLD_SQL_MODE=@@SQL_MODE*/;;\n';
sqlString += `/*!50003 SET SQL_MODE="${sqlMode}" */;\n`;
sqlString += 'DELIMITER ;;\n';
sqlString += '/*!50003 CREATE*/ ';
sqlString += `/*!50017 DEFINER=${definer}*/ `;
sqlString += `/*!50003 TRIGGER \`${name}\` ${timing} ${event} ON \`${table}\` FOR EACH ROW ${statement}*/;;\n`;
sqlString += 'DELIMITER ;\n';
sqlString += '/*!50003 SET SQL_MODE=@OLD_SQL_MODE */;\n\n';
}
return sqlString;
}
async getSchedulers () {
const { rows: schedulers } = await this._client.raw(
`SELECT *, EVENT_SCHEMA AS \`Db\`, EVENT_NAME AS \`Name\` FROM information_schema.\`EVENTS\` WHERE EVENT_SCHEMA = '${this.schemaName}'`
);
let sqlString = '';
for (const scheduler of schedulers) {
const {
EVENT_NAME: name,
SQL_MODE: sqlMode,
EVENT_TYPE: type,
INTERVAL_VALUE: intervalValue,
INTERVAL_FIELD: intervalField,
STARTS: starts,
ENDS: ends,
EXECUTE_AT: at,
ON_COMPLETION: onCompletion,
STATUS: status,
EVENT_DEFINITION: definition
} = scheduler;
const definer = this.getEscapedDefiner(scheduler.DEFINER);
const comment = this.escapeAndQuote(scheduler.EVENT_COMMENT);
sqlString += `/*!50106 DROP EVENT IF EXISTS \`${name}\` */;\n`;
sqlString += '/*!50003 SET @OLD_SQL_MODE=@@SQL_MODE*/;;\n';
sqlString += `/*!50003 SET SQL_MODE='${sqlMode}' */;\n`;
sqlString += 'DELIMITER ;;\n';
sqlString += '/*!50106 CREATE*/ ';
sqlString += `/*!50117 DEFINER=${definer}*/ `;
sqlString += `/*!50106 EVENT \`${name}\` ON SCHEDULE `;
if (type === 'RECURRING') {
sqlString += `EVERY ${intervalValue} ${intervalField} STARTS '${starts}' `;
if (ends) sqlString += `ENDS '${ends}' `;
}
else sqlString += `AT '${at}' `;
sqlString += `ON COMPLETION ${onCompletion} ${
status === 'disabled' ? 'DISABLE' : 'ENABLE'
} COMMENT ${comment || '\'\''} DO ${definition}*/;;\n`;
sqlString += 'DELIMITER ;\n';
sqlString += '/*!50003 SET SQL_MODE=@OLD_SQL_MODE*/;;\n';
}
return sqlString;
}
async getFunctions () {
const { rows: functions } = await this._client.raw(
`SHOW FUNCTION STATUS WHERE \`Db\` = '${this.schemaName}';`
);
let sqlString = '';
for (const func of functions) {
const definer = this.getEscapedDefiner(func.Definer);
sqlString += await this.getRoutineSyntax(
func.Name,
func.Type,
definer
);
}
return sqlString;
}
async getRoutines () {
const { rows: routines } = await this._client.raw(
`SHOW PROCEDURE STATUS WHERE \`Db\` = '${this.schemaName}';`
);
let sqlString = '';
for (const routine of routines) {
const definer = this.getEscapedDefiner(routine.Definer);
sqlString += await this.getRoutineSyntax(
routine.Name,
routine.Type,
definer
);
}
return sqlString;
}
async getRoutineSyntax (name, type, definer) {
const { rows: routines } = await this._client.raw(
`SHOW CREATE ${type} \`${this.schemaName}\`.\`${name}\``
);
if (routines.length === 0) return '';
const routine = routines[0];
const fieldName = `Create ${type === 'PROCEDURE' ? 'Procedure' : 'Function'}`;
const sqlMode = routine.sql_mode;
const createProcedure = routine[fieldName];
let sqlString = '';
if (createProcedure) { // If procedure body not empty
const startOffset = createProcedure.indexOf(type);
const procedureBody = createProcedure.substring(startOffset);
sqlString += `/*!50003 DROP ${type} IF EXISTS ${name}*/;;\n`;
sqlString += '/*!50003 SET @OLD_SQL_MODE=@@SQL_MODE*/;;\n';
sqlString += `/*!50003 SET SQL_MODE="${sqlMode}"*/;;\n`;
sqlString += 'DELIMITER ;;\n';
sqlString += `/*!50003 CREATE*/ /*!50020 DEFINER=${definer}*/ /*!50003 ${procedureBody}*/;;\n`;
sqlString += 'DELIMITER ;\n';
sqlString += '/*!50003 SET SQL_MODE=@OLD_SQL_MODE*/;\n';
}
return sqlString;
}
async _queryStream (sql) {
if (process.env.NODE_ENV === 'development') console.log('EXPORTER:', sql);
const isPool = typeof this._client._connection.getConnection === 'function';
const connection = isPool ? await this._client._connection.getConnection() : this._client._connection;
const stream = connection.connection.query(sql).stream();
const dispose = () => connection.destroy();
stream.on('end', dispose);
stream.on('error', dispose);
stream.on('close', dispose);
return stream;
}
getEscapedDefiner (definer) {
return definer
.split('@')
.map(part => '`' + part + '`')
.join('@');
}
escapeAndQuote (val) {
// eslint-disable-next-line no-control-regex
const CHARS_TO_ESCAPE = /[\0\b\t\n\r\x1a"'\\]/g;
const CHARS_ESCAPE_MAP = {
'\0': '\\0',
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\r': '\\r',
'\x1a': '\\Z',
'"': '\\"',
'\'': '\\\'',
'\\': '\\\\'
};
let chunkIndex = CHARS_TO_ESCAPE.lastIndex = 0;
let escapedVal = '';
let match;
while ((match = CHARS_TO_ESCAPE.exec(val))) {
escapedVal += val.slice(chunkIndex, match.index) + CHARS_ESCAPE_MAP[match[0]];
chunkIndex = CHARS_TO_ESCAPE.lastIndex;
}
if (chunkIndex === 0)
return `'${val}'`;
if (chunkIndex < val.length)
return `'${escapedVal + val.slice(chunkIndex)}'`;
return `'${escapedVal}'`;
}
_getGeoJSON (val) {
if (Array.isArray(val)) {
if (getArrayDepth(val) === 1)
return lineString(val.reduce((acc, curr) => [...acc, [curr.x, curr.y]], []));
else
return polygon(val.map(arr => arr.reduce((acc, curr) => [...acc, [curr.x, curr.y]], [])));
}
else
return point([val.x, val.y]);
}
}

View File

@@ -0,0 +1,495 @@
import { SqlExporter } from './SqlExporter';
import { BLOB, BIT, DATE, DATETIME, FLOAT, NUMBER, TEXT_SEARCH } from 'common/fieldTypes';
import hexToBinary from 'common/libs/hexToBinary';
import { getArrayDepth } from 'common/libs/getArrayDepth';
import moment from 'moment';
import { lineString, point, polygon } from '@turf/helpers';
import QueryStream from 'pg-query-stream';
export default class PostgreSQLExporter extends SqlExporter {
async getSqlHeader () {
let dump = await super.getSqlHeader();
dump += `
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;\n\n\n`;
if (this.schemaName !== 'public') dump += `CREATE SCHEMA "${this.schemaName}";\n\n`;
dump += await this.getCreateTypes();
return dump;
}
async getCreateTable (tableName) {
let createSql = '';
const sequences = [];
const columnsSql = [];
const arrayTypes = {
_int2: 'smallint',
_int4: 'integer',
_int8: 'bigint',
_float4: 'real',
_float8: 'double precision',
_char: '"char"',
_varchar: 'character varying'
};
// Table columns
const { rows } = await this._client.raw(`
SELECT *
FROM "information_schema"."columns"
WHERE "table_schema" = '${this.schemaName}'
AND "table_name" = '${tableName}'
ORDER BY "ordinal_position" ASC
`, { schema: 'information_schema' });
if (!rows.length) return '';
for (const column of rows) {
let fieldType = column.data_type;
if (fieldType === 'USER-DEFINED') fieldType = `"${this.schemaName}".${column.udt_name}`;
else if (fieldType === 'ARRAY') {
if (Object.keys(arrayTypes).includes(fieldType))
fieldType = arrayTypes[type] + '[]';
else
fieldType = column.udt_name.replaceAll('_', '') + '[]';
}
const columnArr = [
`"${column.column_name}"`,
`${fieldType}${column.character_maximum_length ? `(${column.character_maximum_length})` : ''}`
];
if (column.column_default) {
columnArr.push(`DEFAULT ${column.column_default}`);
if (column.column_default.includes('nextval')) {
const sequenceName = column.column_default.split('\'')[1];
sequences.push(sequenceName);
}
}
if (column.is_nullable === 'NO') columnArr.push('NOT NULL');
columnsSql.push(columnArr.join(' '));
}
// Table sequences
for (let sequence of sequences) {
if (sequence.includes('.')) sequence = sequence.split('.')[1];
const { rows } = await this._client
.select('*')
.schema('information_schema')
.from('sequences')
.where({ sequence_schema: `= '${this.schemaName}'`, sequence_name: `= '${sequence}'` })
.run();
if (rows.length) {
createSql += `CREATE SEQUENCE "${this.schemaName}"."${sequence}"
START WITH ${rows[0].start_value}
INCREMENT BY ${rows[0].increment}
MINVALUE ${rows[0].minimum_value}
MAXVALUE ${rows[0].maximum_value}
CACHE 1;\n`;
// createSql += `\nALTER TABLE "${sequence}" OWNER TO ${this._client._params.user};\n\n`;
}
}
// Table create
createSql += `\nCREATE TABLE "${this.schemaName}"."${tableName}"(
${columnsSql.join(',\n ')}
);\n`;
// createSql += `\nALTER TABLE "${tableName}" OWNER TO ${this._client._params.user};\n\n`;
// Table indexes
createSql += '\n';
const { rows: indexes } = await this._client
.select('*')
.schema('pg_catalog')
.from('pg_indexes')
.where({ schemaname: `= '${this.schemaName}'`, tablename: `= '${tableName}'` })
.run();
for (const index of indexes)
createSql += `${index.indexdef};\n`;
// Table foreigns
const { rows: foreigns } = await this._client.raw(`
SELECT
tc.table_schema,
tc.constraint_name,
tc.table_name,
kcu.column_name,
ccu.table_schema AS foreign_table_schema,
ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name,
rc.update_rule,
rc.delete_rule
FROM information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
ON tc.constraint_name = kcu.constraint_name
AND tc.table_schema = kcu.table_schema
JOIN information_schema.constraint_column_usage AS ccu
ON ccu.constraint_name = tc.constraint_name
AND ccu.table_schema = tc.table_schema
JOIN information_schema.referential_constraints AS rc
ON rc.constraint_name = kcu.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = '${this.schemaName}'
AND tc.table_name = '${tableName}'
`);
for (const foreign of foreigns) {
this._postTablesSql += `\nALTER TABLE ONLY "${this.schemaName}"."${tableName}"
ADD CONSTRAINT "${foreign.constraint_name}" FOREIGN KEY ("${foreign.column_name}") REFERENCES "${this.schemaName}"."${foreign.foreign_table_name}" ("${foreign.foreign_column_name}") ON UPDATE ${foreign.update_rule} ON DELETE ${foreign.delete_rule};\n`;
}
return createSql;
}
getDropTable (tableName) {
return `DROP TABLE IF EXISTS "${this.schemaName}"."${tableName}";`;
}
async * getTableInsert (tableName) {
let rowCount = 0;
const sqlStr = '';
const countResults = await this._client.raw(`SELECT COUNT(1) as count FROM "${this.schemaName}"."${tableName}"`);
if (countResults.rows.length === 1) rowCount = countResults.rows[0].count;
if (rowCount > 0) {
const columns = await this._client.getTableColumns({
table: tableName,
schema: this.schemaName
});
const columnNames = columns.map(col => '"' + col.name + '"').join(', ');
yield sqlStr;
const stream = await this._queryStream(
`SELECT ${columnNames} FROM "${this.schemaName}"."${tableName}"`
);
for await (const row of stream) {
if (this.isCancelled) {
stream.destroy();
yield null;
return;
}
let sqlInsertString = `INSERT INTO "${this.schemaName}"."${tableName}" (${columnNames}) VALUES`;
sqlInsertString += ' (';
for (const i in columns) {
const column = columns[i];
const val = row[column.name];
if (val === null) sqlInsertString += 'NULL';
else if (DATE.includes(column.type)) {
sqlInsertString += moment(val).isValid()
? this.escapeAndQuote(moment(val).format('YYYY-MM-DD'))
: val;
}
else if (DATETIME.includes(column.type)) {
let datePrecision = '';
for (let i = 0; i < column.precision; i++)
datePrecision += i === 0 ? '.S' : 'S';
sqlInsertString += moment(val).isValid()
? this.escapeAndQuote(moment(val).format(`YYYY-MM-DD HH:mm:ss${datePrecision}`))
: this.escapeAndQuote(val);
}
else if (column.isArray) {
let parsedVal;
if (Array.isArray(val))
parsedVal = JSON.stringify(val).replaceAll('[', '{').replaceAll(']', '}');
else
parsedVal = typeof val === 'string' ? val.replaceAll('[', '{').replaceAll(']', '}') : '';
sqlInsertString += `'${parsedVal}'`;
}
else if (TEXT_SEARCH.includes(column.type))
sqlInsertString += `'${val.replaceAll('\'', '\'\'')}'`;
else if (BIT.includes(column.type))
sqlInsertString += `b'${hexToBinary(Buffer.from(val).toString('hex'))}'`;
else if (BLOB.includes(column.type))
sqlInsertString += `decode('${val.toString('hex').toUpperCase()}', 'hex')`;
else if (NUMBER.includes(column.type))
sqlInsertString += val;
else if (FLOAT.includes(column.type))
sqlInsertString += parseFloat(val);
else if (val === '') sqlInsertString += '\'\'';
else {
sqlInsertString += typeof val === 'string'
? this.escapeAndQuote(val)
: typeof val === 'object'
? this.escapeAndQuote(JSON.stringify(val))
: val;
}
if (parseInt(i) !== columns.length - 1)
sqlInsertString += ', ';
}
sqlInsertString += ');\n';
yield sqlInsertString;
}
yield sqlStr;
}
}
async getCreateTypes () {
let sqlString = '';
const { rows: types } = await this._client.raw(`
SELECT pg_type.typname, pg_enum.enumlabel
FROM pg_type
JOIN pg_enum ON pg_enum.enumtypid = pg_type.oid;
`);
if (types.length) { // TODO: refactor
sqlString += this.buildComment('Dump of types\n------------------------------------------------------------') + '\n\n';
const typesArr = types.reduce((arr, type) => {
if (arr.every(el => el.name !== type.typname))
arr.push({ name: type.typname, enums: [this.escapeAndQuote(type.enumlabel)] });
else {
const i = arr.findIndex(el => el.name === type.typname);
arr[i].enums.push(this.escapeAndQuote(type.enumlabel));
}
return arr;
}, []);
for (const type of typesArr) {
sqlString += `CREATE TYPE "${this.schemaName}"."${type.name}" AS ENUM (
${type.enums.join(',\n\t')}
);`;
}
// sqlString += `\nALTER TYPE "${tableName}" OWNER TO ${this._client._params.user};\n`
}
return sqlString;
}
async getCreateAggregates () {
let sqlString = '';
const { rows: aggregates } = await this._client.raw(`
SELECT proname
FROM pg_proc
WHERE prokind = 'a'
AND pronamespace::regnamespace::text = '${this.schemaName}'
ORDER BY 1;
`);
if (aggregates.length) {
for (const aggregate of aggregates) {
const { rows: aggregateDef } = await this._client.raw(
`SELECT
format(
E'CREATE AGGREGATE %s (\n%s\n);'
, (pg_identify_object('pg_proc'::regclass, aggfnoid, 0)).identity
, array_to_string(
ARRAY[
format(E'\tSFUNC = %s', aggtransfn::regproc)
, format(E'\tSTYPE = %s', format_type(aggtranstype, NULL))
, CASE aggfinalfn WHEN '-'::regproc THEN NULL ELSE format(E'\tFINALFUNC = %s',aggfinalfn::text) END
, CASE aggsortop WHEN 0 THEN NULL ELSE format(E'\tSORTOP = %s', oprname) END
, CASE WHEN agginitval IS NULL THEN NULL ELSE format(E'\tINITCOND = %s', agginitval) END
]
, E',\n'
)
)
FROM pg_aggregate
LEFT JOIN pg_operator ON pg_operator.oid = aggsortop
WHERE aggfnoid = '${this.schemaName}.${aggregate.proname}'::regproc;`
);
if (aggregateDef.length)
sqlString += '\n\n' + aggregateDef[0].format;
}
}
return sqlString + '\n\n\n';
}
async getViews () {
const { rows: views } = await this._client.raw(`SELECT * FROM "pg_views" WHERE "schemaname"='${this.schemaName}'`);
let sqlString = '';
for (const view of views) {
sqlString += `\nDROP VIEW IF EXISTS "${view.viewname}";\n`;
// const { rows: columns } = await this._client
// .select('*')
// .schema('information_schema')
// .from('columns')
// .where({ table_schema: `= '${this.schemaName}'`, table_name: `= '${view.viewname}'` })
// .orderBy({ ordinal_position: 'ASC' })
// .run();
// sqlString += `
// CREATE VIEW "${this.schemaName}"."${view.viewname}" AS
// SELECT
// ${columns.reduce((acc, curr) => {
// const fieldType = curr.data_type === 'USER-DEFINED' ? curr.udt_name : curr.data_type;
// acc.push(`NULL::${fieldType}${curr.character_maximum_length ? `(${curr.character_maximum_length})` : ''} AS "${curr.column_name}"`);
// return acc;
// }, []).join(',\n ')};
// `;
sqlString += `\nCREATE OR REPLACE VIEW "${this.schemaName}"."${view.viewname}" AS \n${view.definition}\n`;
}
return sqlString;
}
async getTriggers () {
let sqlString = '';
// Trigger functions
const { rows: triggerFunctions } = await this._client.raw(
`SELECT DISTINCT routine_name AS name FROM information_schema.routines WHERE routine_type = 'FUNCTION' AND routine_schema = '${this.schemaName}' AND data_type = 'trigger'`
);
for (const func of triggerFunctions) {
const { rows: functionDef } = await this._client.raw(
`SELECT pg_get_functiondef((SELECT oid FROM pg_proc WHERE proname = '${func.name}')) AS definition`
);
sqlString += `\n${functionDef[0].definition};\n`;
}
const { rows: triggers } = await this._client.raw(
`SELECT * FROM "information_schema"."triggers" WHERE "trigger_schema"='${this.schemaName}'`
);
const remappedTriggers = triggers.reduce((acc, trigger) => {
const i = acc.findIndex(t => t.trigger_name === trigger.trigger_name && t.event_object_table === trigger.event_object_table);
if (i === -1) {
trigger.events = [trigger.event_manipulation];
acc.push(trigger);
}
else
acc[i].events.push(trigger.event_manipulation);
return acc;
}, []);
for (const trigger of remappedTriggers)
sqlString += `\nCREATE TRIGGER "${trigger.trigger_name}" ${trigger.action_timing} ${trigger.events.join(' OR ')} ON "${this.schemaName}"."${trigger.event_object_table}" FOR EACH ${trigger.action_orientation} ${trigger.action_statement};\n`;
return sqlString;
}
async getFunctions () {
let sqlString = '';
const { rows: functions } = await this._client.raw(
`SELECT DISTINCT routine_name AS name FROM information_schema.routines WHERE routine_type = 'FUNCTION' AND routine_schema = '${this.schemaName}' AND data_type != 'trigger'`
);
for (const func of functions) {
const { rows: functionDef } = await this._client.raw(
`SELECT pg_get_functiondef((SELECT oid FROM pg_proc WHERE proname = '${func.name}')) AS definition`
);
sqlString += `\n${functionDef[0].definition};\n`;
}
sqlString += await this.getCreateAggregates();
return sqlString;
}
async getRoutines () {
let sqlString = '';
const { rows: functions } = await this._client.raw(
`SELECT DISTINCT routine_name AS name FROM information_schema.routines WHERE routine_type = 'PROCEDURE' AND routine_schema = '${this.schemaName}'`
);
for (const func of functions) {
const { rows: functionDef } = await this._client.raw(
`SELECT pg_get_functiondef((SELECT oid FROM pg_proc WHERE proname = '${func.name}')) AS definition`
);
sqlString += `\n${functionDef[0].definition};\n`;
}
return sqlString;
}
async _queryStream (sql) {
if (process.env.NODE_ENV === 'development') console.log('EXPORTER:', sql);
const connection = await this._client.getConnection();
const query = new QueryStream(sql, null);
const stream = connection.query(query);
const dispose = () => connection.end();
stream.on('end', dispose);
stream.on('error', dispose);
stream.on('close', dispose);
return stream;
}
getEscapedDefiner (definer) {
return definer
.split('@')
.map(part => '`' + part + '`')
.join('@');
}
escapeAndQuote (val) {
// eslint-disable-next-line no-control-regex
const CHARS_TO_ESCAPE = /[\0\b\t\n\r\x1a"'\\]/g;
const CHARS_ESCAPE_MAP = {
'\0': '\\0',
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\r': '\\r',
'\x1a': '\\Z',
'"': '\\"',
'\'': '\\\'',
'\\': '\\\\'
};
let chunkIndex = CHARS_TO_ESCAPE.lastIndex = 0;
let escapedVal = '';
let match;
while ((match = CHARS_TO_ESCAPE.exec(val))) {
escapedVal += val.slice(chunkIndex, match.index) + CHARS_ESCAPE_MAP[match[0]];
chunkIndex = CHARS_TO_ESCAPE.lastIndex;
}
if (chunkIndex === 0)
return `'${val}'`;
if (chunkIndex < val.length)
return `'${escapedVal + val.slice(chunkIndex)}'`;
return `'${escapedVal}'`;
}
_getGeoJSON (val) {
if (Array.isArray(val)) {
if (getArrayDepth(val) === 1)
return lineString(val.reduce((acc, curr) => [...acc, [curr.x, curr.y]], []));
else
return polygon(val.map(arr => arr.reduce((acc, curr) => [...acc, [curr.x, curr.y]], [])));
}
else
return point([val.x, val.y]);
}
}

View File

@@ -0,0 +1,169 @@
import moment from 'moment';
import { BaseExporter } from '../BaseExporter';
export class SqlExporter extends BaseExporter {
constructor (client, tables, options) {
super(tables, options);
this._client = client;
this._commentChar = '--';
this._postTablesSql = '';
}
get schemaName () {
return this._options.schema;
}
get host () {
return this._client._params.host;
}
async getServerVersion () {
const version = await this._client.getVersion();
return `${version.name} ${version.number}`;
}
async dump () {
const { includes } = this._options;
const extraItems = Object.keys(includes).filter(key => includes[key]);
const totalTableToProcess = this._tables.filter(
t => t.includeStructure || t.includeContent || t.includeDropStatement
).length;
const processingItemCount = totalTableToProcess + extraItems.length;
const exportState = {
totalItems: processingItemCount,
currentItemIndex: 0,
currentItem: '',
op: ''
};
const header = await this.getSqlHeader();
this.writeString(header);
this.writeString('\n\n\n');
for (const item of this._tables) {
// user abort operation
if (this.isCancelled) return;
// skip item if not set to output any detail for them
if (
!item.includeStructure &&
!item.includeContent &&
!item.includeDropStatement
)
continue;
exportState.currentItemIndex++;
exportState.currentItem = item.table;
exportState.op = 'FETCH';
this.emitUpdate(exportState);
const tableHeader = this.buildComment(
`Dump of table ${item.table}\n------------------------------------------------------------`
);
this.writeString(tableHeader);
this.writeString('\n\n');
if (item.includeDropStatement) {
const dropTableSyntax = this.getDropTable(item.table);
this.writeString(dropTableSyntax);
this.writeString('\n\n');
}
if (item.includeStructure) {
const createTableSyntax = await this.getCreateTable(item.table);
this.writeString(createTableSyntax);
this.writeString('\n\n');
}
if (item.includeContent) {
exportState.op = 'WRITE';
this.emitUpdate(exportState);
for await (const sqlStr of this.getTableInsert(item.table)) {
if (this.isCancelled) return;
this.writeString(sqlStr);
}
this.writeString('\n\n');
}
this.writeString('\n\n');
}
// SQL to execute after tables creation
if (this._postTablesSql) {
this.writeString(this._postTablesSql);
this.writeString('\n\n');
}
for (const item of extraItems) {
const processingMethod = `get${item.charAt(0).toUpperCase() + item.slice(1)}`;
exportState.currentItemIndex++;
exportState.currentItem = item;
exportState.op = 'PROCESSING';
this.emitUpdate(exportState);
if (this[processingMethod]) {
const data = await this[processingMethod]();
if (data !== '') {
const header =
this.buildComment(
`Dump of ${item}\n------------------------------------------------------------`
) + '\n\n';
this.writeString(header);
this.writeString(data);
this.writeString('\n\n');
}
}
}
const footer = await this.getFooter();
this.writeString(footer);
}
buildComment (text) {
return text
.split('\n')
.map(txt => `${this._commentChar} ${txt}`)
.join('\n');
}
async getSqlHeader () {
const serverVersion = await this.getServerVersion();
const header = `************************************************************
Antares - SQL Client
Version ${process.env.PACKAGE_VERSION}
https://antares-sql.app/
https://github.com/antares-sql/antares
Host: ${this.host} (${serverVersion})
Database: ${this.schemaName}
Generation time: ${moment().format()}
************************************************************`;
return this.buildComment(header);
}
async getFooter () {
return this.buildComment(`Dump completed on ${moment().format()}`);
}
getCreateTable (tableName) {
throw new Error(
'Sql Exporter must implement the "getCreateTable" method'
);
}
getDropTable (tableName) {
throw new Error('Sql Exporter must implement the "getDropTable" method');
}
getTableInsert (tableName) {
throw new Error(
'Sql Exporter must implement the "getTableInsert" method'
);
}
}

View File

@@ -0,0 +1,53 @@
import fs from 'fs';
import EventEmitter from 'events';
export class BaseImporter extends EventEmitter {
constructor (options) {
super();
this._options = options;
this._isCancelled = false;
this._fileHandler = fs.createReadStream(this._options.file, {
flags: 'r',
highWaterMark: 4 * 1024
});
this._state = {};
this._fileHandler.once('error', err => {
this._isCancelled = true;
this.emit('error', err);
});
}
async run () {
try {
this.emit('start', this);
await this.import();
}
catch (err) {
this.emit('error', err);
throw err;
}
finally {
this._fileHandler.close();
this.emit('end');
}
}
get isCancelled () {
return this._isCancelled;
}
cancel () {
this._isCancelled = true;
this.emit('cancel');
this.emitUpdate({ op: 'cancelling' });
}
emitUpdate (state) {
this.emit('progress', { ...this._state, ...state });
}
import () {
throw new Error('Exporter must implement the "import" method');
}
}

View File

@@ -0,0 +1,80 @@
import fs from 'fs/promises';
import MySQLParser from '../../parsers/MySQLParser';
import { BaseImporter } from '../BaseImporter';
export default class MySQLImporter extends BaseImporter {
constructor (client, options) {
super(options);
this._client = client;
}
async import () {
try {
const { size: totalFileSize } = await fs.stat(this._options.file);
const parser = new MySQLParser();
let readPosition = 0;
let queryCount = 0;
this.emitUpdate({
fileSize: totalFileSize,
readPosition: 0,
percentage: 0,
queryCount: 0
});
return new Promise((resolve, reject) => {
this._fileHandler.pipe(parser);
parser.on('error', reject);
parser.on('close', async () => {
console.log('TOTAL QUERIES', queryCount);
console.log('import end');
resolve();
});
parser.on('data', async (query) => {
queryCount++;
parser.pause();
try {
await this._client.query(query);
}
catch (error) {
this.emit('query-error', {
sql: query,
message: error.sqlMessage,
sqlSnippet: error.sql,
time: new Date().getTime()
});
}
this.emitUpdate({
queryCount,
readPosition,
percentage: readPosition / totalFileSize * 100
});
this._fileHandler.pipe(parser);
parser.resume();
});
parser.on('pause', () => {
this._fileHandler.unpipe(parser);
this._fileHandler.readableFlowing = false;
});
this._fileHandler.on('data', (chunk) => {
readPosition += chunk.length;
});
this._fileHandler.on('error', (err) => {
console.log(err);
reject(err);
});
});
}
catch (err) {
console.log(err);
}
}
}

View File

@@ -0,0 +1,80 @@
import fs from 'fs/promises';
import PostgreSQLParser from '../../parsers/PostgreSQLParser';
import { BaseImporter } from '../BaseImporter';
export default class PostgreSQLImporter extends BaseImporter {
constructor (client, options) {
super(options);
this._client = client;
}
async import () {
try {
const { size: totalFileSize } = await fs.stat(this._options.file);
const parser = new PostgreSQLParser();
let readPosition = 0;
let queryCount = 0;
this.emitUpdate({
fileSize: totalFileSize,
readPosition: 0,
percentage: 0,
queryCount: 0
});
return new Promise((resolve, reject) => {
this._fileHandler.pipe(parser);
parser.on('error', reject);
parser.on('close', async () => {
console.log('TOTAL QUERIES', queryCount);
console.log('import end');
resolve();
});
parser.on('data', async (query) => {
queryCount++;
parser.pause();
try {
await this._client.query(query);
}
catch (error) {
this.emit('query-error', {
sql: query,
message: error.hint || error.toString(),
sqlSnippet: error.sql,
time: new Date().getTime()
});
}
this.emitUpdate({
queryCount,
readPosition,
percentage: readPosition / totalFileSize * 100
});
this._fileHandler.pipe(parser);
parser.resume();
});
parser.on('pause', () => {
this._fileHandler.unpipe(parser);
this._fileHandler.readableFlowing = false;
});
this._fileHandler.on('data', (chunk) => {
readPosition += chunk.length;
});
this._fileHandler.on('error', (err) => {
console.log(err);
reject(err);
});
});
}
catch (err) {
console.log(err);
}
}
}

View File

@@ -0,0 +1,93 @@
import { Transform } from 'stream';
export default class MySQLParser extends Transform {
constructor (opts) {
opts = {
delimiter: ';',
encoding: 'utf8',
writableObjectMode: true,
readableObjectMode: true,
...opts
};
super(opts);
this._buffer = '';
this._lastChar = '';
this._last9Chars = '';
this.encoding = opts.encoding;
this.delimiter = opts.delimiter;
this.isEscape = false;
this.currentQuote = null;
this.isDelimiter = false;
}
_transform (chunk, encoding, next) {
for (const char of chunk.toString(this.encoding)) {
this.checkEscape();
this._buffer += char;
this._lastChar = char;
this._last9Chars += char.trim().toLocaleLowerCase();
if (this._last9Chars.length > 9)
this._last9Chars = this._last9Chars.slice(-9);
this.checkNewDelimiter(char);
this.checkQuote(char);
const query = this.getQuery();
if (query)
this.push(query);
}
next();
}
checkEscape () {
if (this._buffer.length > 0) {
this.isEscape = this._lastChar === '\\'
? !this.isEscape
: false;
}
}
checkNewDelimiter (char) {
if (this.currentQuote === null && this._last9Chars === 'delimiter') {
this.isDelimiter = true;
this._buffer = '';
}
else {
const isNewLine = char === '\n' || char === '\r';
if (isNewLine && this.isDelimiter) {
this.isDelimiter = false;
this.delimiter = this._buffer.trim();
this._buffer = '';
}
}
}
checkQuote (char) {
const isQuote = !this.isEscape && (char === '\'' || char === '"');
if (isQuote && this.currentQuote === char)
this.currentQuote = null;
else if (isQuote && this.currentQuote === null)
this.currentQuote = char;
}
getQuery () {
if (this.isDelimiter)
return false;
let query = false;
let demiliterFound = false;
if (this.currentQuote === null && this._buffer.length >= this.delimiter.length)
demiliterFound = this._last9Chars.slice(-this.delimiter.length) === this.delimiter;
if (demiliterFound) {
const parsedStr = this._buffer.trim();
query = parsedStr.slice(0, parsedStr.length - this.delimiter.length);
this._buffer = '';
}
return query;
}
}

View File

@@ -0,0 +1,142 @@
import { Transform } from 'stream';
export default class PostgreSQLParser extends Transform {
constructor (opts) {
opts = {
delimiter: ';',
encoding: 'utf8',
writableObjectMode: true,
readableObjectMode: true,
...opts
};
super(opts);
this._buffer = '';
this._lastChar = '';
this._lastChars = '';
this.encoding = opts.encoding;
this.delimiter = opts.delimiter;// ';'
this._bodyWrapper = '';
this._bodyWrapperBuffer = '';
this.isEscape = false;
this.currentQuote = null;
this._firstDollarFound = false;
this._isBody = false;
this._isSingleLineComment = false;
this._isMultiLineComment = false;
}
get _isComment () {
return this._isSingleLineComment || this._isMultiLineComment;
}
_transform (chunk, encoding, next) {
for (const char of chunk.toString(this.encoding)) {
this.checkEscape();
this._buffer += char;
this._lastChar = char;
this._lastChars += char;
if (this._lastChars.length > this._bodyWrapper.length)
this._lastChars = this._lastChars.slice(-(this._bodyWrapper.length || 2));
this.checkBodyWrapper(char);
this.checkQuote(char);
this.checkCommentRow();
const query = this.getQuery();
if (query)
this.push(query);
}
next();
}
checkEscape () {
if (this._buffer.length > 0) {
this.isEscape = this._lastChar === '\\'
? !this.isEscape
: false;
}
}
checkCommentRow () {
if (this._isBody) return;
if (!this._isComment) {
if (this.currentQuote === null && this._lastChars.includes('--'))
this._isSingleLineComment = true;
if (this.currentQuote === null && this._lastChars.includes('/*'))
this._isMultiLineComment = true;
}
else {
if (this._isSingleLineComment && (this._lastChar === '\n' || this._lastChar === '\r')) {
this._buffer = '';
this._isSingleLineComment = false;
}
if (this._isMultiLineComment && this._lastChars.includes('*/')) {
this._buffer = '';
this._isMultiLineComment = false;
}
}
}
checkBodyWrapper (char) {
if (this._isBody)
this._isBody = this._lastChars !== this._bodyWrapper;
if (this.currentQuote === null && char === '$' && !this._firstDollarFound && !this._bodyWrapper) {
this._firstDollarFound = true;
this._bodyWrapperBuffer += char;
this._isBody = true;
}
else if (this._firstDollarFound) {
if (char === '\n' || char === ' ') {
this._firstDollarFound = false;
this._bodyWrapperBuffer = '';
this._bodyWrapper = '';
this._isBody = false;
return;
}
this._bodyWrapperBuffer += char;
const isEndDollar = char === '$';
if (isEndDollar) {
this._firstDollarFound = false;
this._bodyWrapper = this._bodyWrapperBuffer;
this._bodyWrapperBuffer = '';
}
}
}
checkQuote (char) {
const isQuote = !this.isEscape && (char === '\'' || char === '"');
if (isQuote && this.currentQuote === char)
this.currentQuote = null;
else if (isQuote && this.currentQuote === null)
this.currentQuote = char;
}
getQuery () {
if (this._isBody || this._isComment)
return false;
let query = false;
let demiliterFound = false;
if (this.currentQuote === null && this._buffer.length >= this.delimiter.length)
demiliterFound = this._lastChars.slice(-this.delimiter.length) === this.delimiter;
if (demiliterFound) {
const parsedStr = this._buffer.trim();
query = parsedStr;
this._buffer = '';
this._bodyWrapper = '';
}
return query;
}
}

View File

@@ -81,13 +81,6 @@ async function createMainWindow () {
mainWindow = null;
});
window.webContents.on('devtools-opened', () => {
window.focus();
setImmediate(() => {
window.focus();
});
});
return window;
}

View File

@@ -0,0 +1,64 @@
import fs from 'fs';
import { ClientsFactory } from '../libs/ClientsFactory';
import MysqlExporter from '../libs/exporters/sql/MysqlExporter.js';
import PostgreSQLExporter from '../libs/exporters/sql/PostgreSQLExporter';
let exporter;
process.on('message', async ({ type, client, tables, options }) => {
if (type === 'init') {
const connection = await ClientsFactory.getClient({
client: client.name,
params: client.config,
poolSize: 5
});
await connection.connect();
switch (client.name) {
case 'mysql':
case 'maria':
exporter = new MysqlExporter(connection, tables, options);
break;
case 'pg':
exporter = new PostgreSQLExporter(connection, tables, options);
break;
default:
process.send({
type: 'error',
payload: `"${client.name}" exporter not aviable`
});
return;
}
exporter.once('error', err => {
console.error(err);
process.send({
type: 'error',
payload: err.toString()
});
});
exporter.once('end', () => {
process.send({
type: 'end',
payload: { cancelled: exporter.isCancelled }
});
connection.destroy();
});
exporter.once('cancel', () => {
fs.unlinkSync(exporter.outputFile);
process.send({ type: 'cancel' });
});
exporter.on('progress', state => {
process.send({
type: 'export-progress',
payload: state
});
});
exporter.run();
}
else if (type === 'cancel')
exporter.cancel();
});

View File

@@ -0,0 +1,72 @@
import { ClientsFactory } from '../libs/ClientsFactory';
import MySQLImporter from '../libs/importers/sql/MysqlImporter';
import PostgreSQLImporter from '../libs/importers/sql/PostgreSQLImporter';
let importer;
process.on('message', async ({ type, dbConfig, options }) => {
if (type === 'init') {
const connection = await ClientsFactory.getClient({
client: options.type,
params: {
...dbConfig,
schema: options.schema
},
poolSize: 1
});
const pool = await connection.getConnectionPool();
switch (options.type) {
case 'mysql':
case 'maria':
importer = new MySQLImporter(pool, options);
break;
case 'pg':
importer = new PostgreSQLImporter(pool, options);
break;
default:
process.send({
type: 'error',
payload: `"${options.type}" importer not aviable`
});
return;
}
importer.once('error', err => {
console.error(err);
process.send({
type: 'error',
payload: err.toString()
});
});
importer.once('end', () => {
process.send({
type: 'end',
payload: { cancelled: importer.isCancelled }
});
});
importer.once('cancel', () => {
process.send({ type: 'cancel' });
});
importer.on('progress', state => {
process.send({
type: 'import-progress',
payload: state
});
});
importer.on('query-error', state => {
process.send({
type: 'query-error',
payload: state
});
});
importer.run();
}
else if (type === 'cancel')
importer.cancel();
});

View File

@@ -1,5 +1,5 @@
<template>
<div id="wrapper" :class="`theme-${applicationTheme}`">
<div id="wrapper" :class="[`theme-${applicationTheme}`, !disableBlur || 'no-blur']">
<TheTitleBar />
<div id="window-content">
<TheSettingBar />
@@ -10,7 +10,9 @@
:key="connection.uid"
:connection="connection"
/>
<WorkspaceAddConnectionPanel v-if="selectedWorkspace === 'NEW'" />
<div class="connection-panel-wrapper">
<WorkspaceAddConnectionPanel v-if="selectedWorkspace === 'NEW'" />
</div>
</div>
<TheFooter />
<TheNotificationsBoard />
@@ -51,6 +53,7 @@ export default {
isScratchpad: 'application/isScratchpad',
connections: 'connections/getConnections',
applicationTheme: 'settings/getApplicationTheme',
disableBlur: 'settings/getDisableBlur',
isUnsavedDiscardModal: 'workspaces/isUnsavedDiscardModal'
})
},
@@ -130,5 +133,15 @@ export default {
> .columns {
height: calc(100vh - #{$footer-height});
}
.connection-panel-wrapper{
height: calc(100vh - #{$excluding-size});
width: 100%;
padding-top: 15vh;
display: flex;
justify-content: center;
align-items: flex-start;
overflow: auto;
}
}
</style>

View File

@@ -110,5 +110,4 @@ export default {
.modal.modal-sm .modal-container {
padding: 0;
}
</style>

View File

@@ -0,0 +1,108 @@
<template>
<div id="map" class="map" />
</template>
<script>
import L from 'leaflet';
import {
point,
lineString,
polygon
} from '@turf/helpers';
import { getArrayDepth } from 'common/libs/getArrayDepth';
export default {
name: 'BaseMap',
props: {
points: [Object, Array],
isMultiSpatial: Boolean
},
data () {
return {
map: null,
markers: [],
center: null
};
},
mounted () {
if (this.isMultiSpatial) {
for (const element of this.points)
this.markers.push(this.getMarkers(element));
}
else {
this.markers = this.getMarkers(this.points);
if (!Array.isArray(this.points))
this.center = [this.points.y, this.points.x];
}
this.map = L.map('map', {
center: this.center || [0, 0],
zoom: 15,
minZoom: 1,
attributionControl: false
});
L.control.attribution({ prefix: '<b>Leaflet</b>' }).addTo(this.map);
const geoJsonObj = L.geoJSON(this.markers, {
style: function () {
return {
weight: 2,
fillColor: '#ff7800',
color: '#ff7800',
opacity: 0.8,
fillOpacity: 0.4
};
},
pointToLayer: function (feature, latlng) {
return L.circleMarker(latlng, {
radius: 7,
weight: 2,
fillColor: '#ff7800',
color: '#ff7800',
opacity: 0.8,
fillOpacity: 0.4
});
}
}).addTo(this.map);
const southWest = L.latLng(-90, -180);
const northEast = L.latLng(90, 180);
const bounds = L.latLngBounds(southWest, northEast);
this.map.setMaxBounds(bounds);
if (!this.center) this.map.fitBounds(geoJsonObj.getBounds());
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <b>OpenStreetMap</b>'
}).addTo(this.map);
},
methods: {
getMarkers (points) {
if (Array.isArray(points)) {
if (getArrayDepth(points) === 1)
return lineString(points.reduce((acc, curr) => [...acc, [curr.x, curr.y]], []));
else
return polygon(points.map(arr => arr.reduce((acc, curr) => [...acc, [curr.x, curr.y]], [])));
}
else
return point([points.x, points.y]);
}
}
};
</script>
<style lang="scss">
.map {
height: 400px;
}
.marker-icon {
display: flex;
justify-content: center;
align-items: center;
background: $primary-color;
border-radius: 50%;
box-shadow: 0 0 5px 1px darken($body-font-color-dark, 40%);
}
</style>

View File

@@ -78,6 +78,7 @@ export default {
.file-uploader-message {
display: flex;
word-break: keep-all;
border-radius: $border-radius 0 0 $border-radius;
}

View File

@@ -7,7 +7,7 @@
@blur="$emit('blur')"
>
<option v-if="!isValidDefault" :value="value">
{{ value }} - {{ $t('message.invalidDefault') }}
{{ value === null ? 'NULL' : value }}
</option>
<option
v-for="row in foreignList"

View File

@@ -0,0 +1,511 @@
<template>
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-database-arrow-down mr-1" />
<span class="cut-text">{{ $t('message.exportSchema') }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="modal-body pb-0">
<div class="container">
<div class="columns">
<div class="col-3">
<label class="form-label">{{ $t('message.directoryPath') }}</label>
</div>
<div class="col-9">
<fieldset class="input-group">
<input
v-model="basePath"
class="form-input"
type="text"
required
readonly
:placeholder="$t('message.schemaName')"
>
<button
type="button"
class="btn btn-primary input-group-btn"
@click.prevent="openPathDialog"
>
{{ $t('word.change') }}
</button>
</fieldset>
</div>
</div>
</div>
<div class="columns export-options">
<div class="column col-8 left">
<div class="columns mb-2">
<div class="column col-auto d-flex text-italic ">
<i class="mdi mdi-file-document-outline mr-2" />
{{ filename }}
</div>
<div class="column col-auto col-ml-auto ">
<button
class="btn btn-dark btn-sm"
:title="$t('word.refresh')"
@click="refresh"
>
<i class="mdi mdi-database-refresh" />
</button>
<button
class="btn btn-dark btn-sm"
:title="$t('message.uncheckAllTables')"
:disabled="isRefreshing"
@click="uncheckAllTables"
>
<i class="mdi mdi-file-tree-outline" />
</button>
<button
class="btn btn-dark btn-sm"
:title="$t('message.checkAllTables')"
:disabled="isRefreshing"
@click="checkAllTables"
>
<i class="mdi mdi-file-tree" />
</button>
</div>
</div>
<div class="workspace-query-results">
<div ref="table" class="table table-hover">
<div class="thead">
<div class="tr text-center">
<div class="th no-border" style="width: 50%;" />
<div class="th no-border">
<label
class="form-checkbox m-0 px-2 form-inline"
@click.prevent="toggleAllTablesOption('includeStructure')"
>
<input
type="checkbox"
:indeterminate.prop="includeStructureStatus === 2"
:checked.prop="!!includeStructureStatus"
>
<i class="form-icon" />
</label>
</div>
<div class="th no-border">
<label
class="form-checkbox m-0 px-2 form-inline"
@click.prevent="toggleAllTablesOption('includeContent')"
>
<input
type="checkbox"
:indeterminate.prop="includeContentStatus === 2"
:checked.prop="!!includeContentStatus"
>
<i class="form-icon" />
</label>
</div>
<div class="th no-border">
<label
class="form-checkbox m-0 px-2 form-inline"
@click.prevent="toggleAllTablesOption('includeDropStatement')"
>
<input
type="checkbox"
:indeterminate.prop="includeDropStatementStatus === 2"
:checked.prop="!!includeDropStatementStatus"
>
<i class="form-icon" />
</label>
</div>
</div>
<div class="tr">
<div class="th" style="width: 50%;">
<div class="table-column-title">
<span>{{ $t('word.table') }}</span>
</div>
</div>
<div class="th text-center">
<div class="table-column-title">
<span>{{ $t('word.structure') }}</span>
</div>
</div>
<div class="th text-center">
<div class="table-column-title">
<span>{{ $t('word.content') }}</span>
</div>
</div>
<div class="th text-center">
<div class="table-column-title">
<span>{{ $t('word.drop') }}</span>
</div>
</div>
</div>
</div>
<div class="tbody">
<div
v-for="item in tables"
:key="item.name"
class="tr"
>
<div class="td">
{{ item.table }}
</div>
<div class="td text-center">
<label class="form-checkbox m-0 px-2 form-inline">
<input
v-model="item.includeStructure"
type="checkbox"
><i class="form-icon" />
</label>
</div>
<div class="td text-center">
<label class="form-checkbox m-0 px-2 form-inline">
<input
v-model="item.includeContent"
type="checkbox"
><i class="form-icon" />
</label>
</div>
<div class="td text-center">
<label class="form-checkbox m-0 px-2 form-inline">
<input
v-model="item.includeDropStatement"
type="checkbox"
><i class="form-icon" />
</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="column col-4">
<h5 class="h5">
{{ $t('word.options') }}
</h5>
<span class="h6">{{ $t('word.includes') }}:</span>
<label
v-for="(_, key) in options.includes"
:key="key"
class="form-checkbox"
>
<input v-model="options.includes[key]" type="checkbox"><i class="form-icon" /> {{ $tc(`word.${key}`, 2) }}
</label>
<div v-if="customizations.exportByChunks">
<div class="h6 mt-4 mb-2">
{{ $t('message.newInserStmtEvery') }}:
</div>
<div class="columns">
<div class="column col-6">
<input
v-model.number="options.sqlInsertAfter"
type="number"
class="form-input"
value="250"
>
</div>
<div class="column col-6">
<select v-model="options.sqlInsertDivider" class="form-select">
<option value="bytes">
KiB
</option>
<option value="rows">
{{ $tc('word.row', 2) }}
</option>
</select>
</div>
</div>
</div>
<div class="h6 mb-2 mt-4">
{{ $t('message.ourputFormat') }}:
</div>
<div class="columns">
<div class="column h5 mb-4">
<select v-model="options.outputFormat" class="form-select">
<option value="sql">
{{ $t('message.singleFile', {ext: '.sql'}) }}
</option>
<option value="sql.zip">
{{ $t('message.zipCompressedFile', {ext: '.sql'}) }}
</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer columns">
<div class="column col modal-progress-wrapper text-left">
<div v-if="progressPercentage > 0" class="export-progress">
<span class="progress-status">
{{ progressPercentage }}% - {{ progressStatus }}
</span>
<progress
class="progress d-block"
:value="progressPercentage"
max="100"
/>
</div>
</div>
<div class="column col-auto px-0">
<button class="btn btn-link" @click.stop="closeModal">
{{ $t('word.close') }}
</button>
<button
class="btn btn-primary mr-2"
:class="{'loading': isExporting}"
:disabled="isExporting || isRefreshing"
autofocus
@click.prevent="startExport"
>
{{ $t('word.export') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { ipcRenderer } from 'electron';
import { mapActions, mapGetters } from 'vuex';
import moment from 'moment';
import customizations from 'common/customizations';
import Application from '@/ipc-api/Application';
import Schema from '@/ipc-api/Schema';
export default {
name: 'ModalExportSchema',
props: {
selectedSchema: String
},
data () {
return {
isExporting: false,
isRefreshing: false,
progressPercentage: 0,
progressStatus: '',
tables: [],
options: {
includes: {},
outputFormat: 'sql',
sqlInsertAfter: 250,
sqlInsertDivider: 'bytes'
},
basePath: ''
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace',
getDatabaseVariable: 'workspaces/getDatabaseVariable'
}),
currentWorkspace () {
return this.getWorkspace(this.selectedWorkspace);
},
customizations () {
return this.currentWorkspace.customizations;
},
schemaItems () {
const db = this.currentWorkspace.structure.find(db => db.name === this.selectedSchema);
if (db)
return db.tables.filter(table => table.type === 'table');
return [];
},
filename () {
const date = moment().format('YYYY-MM-DD');
return `${this.selectedSchema}_${date}.${this.options.outputFormat}`;
},
dumpFilePath () {
return `${this.basePath}/${this.filename}`;
},
includeStructureStatus () {
if (this.tables.every(item => item.includeStructure)) return 1;
else if (this.tables.some(item => item.includeStructure)) return 2;
else return 0;
},
includeContentStatus () {
if (this.tables.every(item => item.includeContent)) return 1;
else if (this.tables.some(item => item.includeContent)) return 2;
else return 0;
},
includeDropStatementStatus () {
if (this.tables.every(item => item.includeDropStatement)) return 1;
else if (this.tables.some(item => item.includeDropStatement)) return 2;
else return 0;
}
},
async created () {
if (!this.schemaItems.length) await this.refresh();
window.addEventListener('keydown', this.onKey);
this.basePath = await Application.getDownloadPathDirectory();
this.tables = this.schemaItems.map(item => ({
table: item.name,
includeStructure: true,
includeContent: true,
includeDropStatement: true
}));
const structure = ['functions', 'views', 'triggers', 'routines', 'schedulers'];
structure.forEach(feat => {
const val = customizations[this.currentWorkspace.client][feat];
if (val)
this.$set(this.options.includes, feat, true);
});
ipcRenderer.on('export-progress', this.updateProgress);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
ipcRenderer.off('export-progress', this.updateProgress);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
refreshSchema: 'workspaces/refreshSchema'
}),
async startExport () {
this.isExporting = true;
const { uid, client } = this.currentWorkspace;
const params = {
uid,
type: client,
schema: this.selectedSchema,
outputFile: this.dumpFilePath,
tables: [...this.tables],
...this.options
};
try {
const { status, response } = await Schema.export(params);
if (status === 'success')
this.progressStatus = response.cancelled ? this.$t('word.aborted') : this.$t('word.completed');
else {
this.progressStatus = response;
this.addNotification({ status: 'error', message: response });
}
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isExporting = false;
},
updateProgress (event, state) {
this.progressPercentage = Number((state.currentItemIndex / state.totalItems * 100).toFixed(1));
switch (state.op) {
case 'PROCESSING':
this.progressStatus = this.$t('message.processingTableExport', { table: state.currentItem });
break;
case 'FETCH':
this.progressStatus = this.$t('message.fechingTableExport', { table: state.currentItem });
break;
case 'WRITE':
this.progressStatus = this.$t('message.writingTableExport', { table: state.currentItem });
break;
}
},
async closeModal () {
let willClose = true;
if (this.isExporting) {
willClose = false;
const { response } = await Schema.abortExport();
willClose = response.willAbort;
}
if (willClose)
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
},
checkAllTables () {
this.tables = this.tables.map(item => ({ ...item, includeStructure: true, includeContent: true, includeDropStatement: true }));
},
uncheckAllTables () {
this.tables = this.tables.map(item => ({ ...item, includeStructure: false, includeContent: false, includeDropStatement: false }));
},
toggleAllTablesOption (option) {
const options = ['includeStructure', 'includeContent', 'includeDropStatement'];
if (!options.includes(option)) return;
if (this[`${option}Status`] !== 1)
this.tables = this.tables.map(item => ({ ...item, [option]: true }));
else
this.tables = this.tables.map(item => ({ ...item, [option]: false }));
},
async refresh () {
this.isRefreshing = true;
await this.refreshSchema({ uid: this.currentWorkspace.uid, schema: this.selectedSchema });
this.isRefreshing = false;
},
async openPathDialog () {
const result = await Application.showOpenDialog({ properties: ['openDirectory'] });
if (result && !result.canceled)
this.basePath = result.filePaths[0];
}
}
};
</script>
<style lang="scss" scoped>
.export-options {
flex: 1;
overflow: hidden;
.left {
display: flex;
flex-direction: column;
flex: 1;
}
}
.workspace-query-results {
flex: 1 0 1px;
.table {
width: 100% !important;
}
.form-checkbox {
min-height: 0.8rem;
padding: 0;
.form-icon {
top: 0.1rem;
}
}
}
.modal {
.modal-container {
max-width: 800px;
}
.modal-body {
max-height: 60vh;
display: flex;
flex-direction: column;
}
.modal-footer {
display: flex;
}
}
.progress-status {
font-style: italic;
font-size: 80%;
}
</style>

View File

@@ -6,7 +6,7 @@
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-playlist-plus mr-1" />
<span class="cut-text">{{ $t('message.tableFiller') }}</span>
<span class="cut-text">{{ $tc('message.insertRow', 2) }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />

View File

@@ -0,0 +1,181 @@
<template>
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-database-arrow-up mr-1" />
<span class="cut-text">{{ $t('message.importSchema') }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="modal-body pb-0">
{{ sqlFile }}
<div v-if="queryErrors.length > 0" class="mt-2">
<label>{{ $tc('message.importQueryErrors', queryErrors.length) }}</label>
<textarea
v-model="formattedQueryErrors"
class="form-input"
rows="5"
readonly
/>
</div>
</div>
<div class="modal-footer columns">
<div class="column col modal-progress-wrapper text-left">
<div class="import-progress">
<span class="progress-status">
{{ progressPercentage }}% - {{ progressStatus }} - {{ $tc('message.executedQueries', queryCount) }}
</span>
<progress
class="progress d-block"
:value="progressPercentage"
max="100"
/>
</div>
</div>
<div class="column col-auto px-0">
<button class="btn btn-link" @click.stop="closeModal">
{{ completed ? $t('word.close') : $t('word.cancel') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { ipcRenderer } from 'electron';
import { mapActions, mapGetters } from 'vuex';
import moment from 'moment';
import Schema from '@/ipc-api/Schema';
export default {
name: 'ModalImportSchema',
props: {
selectedSchema: String
},
data () {
return {
sqlFile: '',
isImporting: false,
progressPercentage: 0,
queryCount: 0,
completed: false,
progressStatus: 'Reading',
queryErrors: []
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
currentWorkspace () {
return this.getWorkspace(this.selectedWorkspace);
},
formattedQueryErrors () {
return this.queryErrors.map(err =>
`Time: ${moment(err.time).format('HH:mm:ss.S')} (${err.time})\nError: ${err.message}`
).join('\n\n');
}
},
async created () {
window.addEventListener('keydown', this.onKey);
ipcRenderer.on('import-progress', this.updateProgress);
ipcRenderer.on('query-error', this.handleQueryError);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
ipcRenderer.off('import-progress', this.updateProgress);
ipcRenderer.off('query-error', this.handleQueryError);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
refreshSchema: 'workspaces/refreshSchema'
}),
async startImport (sqlFile) {
this.isImporting = true;
this.sqlFile = sqlFile;
const { uid, client } = this.currentWorkspace;
const params = {
uid,
type: client,
schema: this.selectedSchema,
file: sqlFile
};
try {
this.completed = false;
const { status, response } = await Schema.import(params);
if (status === 'success')
this.progressStatus = response.cancelled ? this.$t('word.aborted') : this.$t('word.completed');
else {
this.progressStatus = response;
this.addNotification({ status: 'error', message: response });
}
this.refreshSchema({ uid, schema: this.selectedSchema });
this.completed = true;
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isImporting = false;
},
updateProgress (event, state) {
this.progressPercentage = Number(state.percentage).toFixed(1);
this.queryCount = Number(state.queryCount);
},
handleQueryError (event, err) {
this.queryErrors.push(err);
},
async closeModal () {
let willClose = true;
if (this.isImporting) {
willClose = false;
const { response } = await Schema.abortImport();
willClose = response.willAbort;
}
if (willClose)
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
}
}
};
</script>
<style lang="scss" scoped>
.modal {
.modal-container {
max-width: 800px;
}
.modal-body {
max-height: 60vh;
display: flex;
flex-direction: column;
}
.modal-footer {
display: flex;
}
}
.progress-status {
font-style: italic;
font-size: 80%;
}
</style>

View File

@@ -117,6 +117,19 @@
</label>
</div>
</div>
<div class="form-group mb-0">
<div class="col-7 col-sm-12">
<label class="form-label">
{{ $t('message.disableBlur') }}
</label>
</div>
<div class="col-5 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleDisableBlur">
<input type="checkbox" :checked="disableBlur">
<i class="form-icon" />
</label>
</div>
</div>
<div class="form-group">
<div class="col-7 col-sm-12">
<label class="form-label">
@@ -282,12 +295,18 @@
<div class="text-center">
<img src="../images/logo.svg" width="128">
<h4>{{ appName }}</h4>
<p>
<p class="mb-2">
{{ $t('word.version') }} {{ appVersion }}<br>
<a class="c-hand" @click="openOutside('https://github.com/Fabio286/antares')"><i class="mdi mdi-github d-inline" /> GitHub</a> <a class="c-hand" @click="openOutside('https://twitter.com/AntaresSQL')"><i class="mdi mdi-twitter d-inline" /> Twitter</a> <a class="c-hand" @click="openOutside('https://antares-sql.app/')"><i class="mdi mdi-web d-inline" /> Website</a><br>
<small>{{ $t('word.author') }} <a class="c-hand" @click="openOutside('https://github.com/Fabio286')">Fabio Di Stasio</a></small><br>
<small>{{ $t('message.madeWithJS') }}</small>
<a class="c-hand" @click="openOutside('https://github.com/antares-sql/antares')"><i class="mdi mdi-github d-inline" /> GitHub</a> <a class="c-hand" @click="openOutside('https://twitter.com/AntaresSQL')"><i class="mdi mdi-twitter d-inline" /> Twitter</a> <a class="c-hand" @click="openOutside('https://antares-sql.app/')"><i class="mdi mdi-web d-inline" /> Website</a><br>
<small>{{ $t('word.author') }} <a class="c-hand" @click="openOutside('https://github.com/Fabio286')">{{ appAuthor }}</a></small><br>
</p>
<div class="mb-2">
<small class="d-block text-uppercase">{{ $t('word.contributors') }}:</small>
<div class="d-block py-1">
<small v-for="(contributor, i) in otherContributors" :key="i">{{ i !== 0 ? ', ' : '' }}{{ contributor }}</small>
</div>
<small>{{ $t('message.madeWithJS') }}</small>
</div>
</div>
</div>
</div>
@@ -313,6 +332,7 @@ export default {
},
data () {
return {
appAuthor: 'Fabio Di Stasio',
localLocale: null,
localPageSize: null,
localTimeout: null,
@@ -367,7 +387,8 @@ export default {
{ code: 'vibrant_ink', name: 'Vibrant Ink' }
]
}
]
],
contributors: process.env.APP_CONTRIBUTORS
};
},
computed: {
@@ -381,6 +402,7 @@ export default {
selectedLineWrap: 'settings/getLineWrap',
notificationsTimeout: 'settings/getNotificationsTimeout',
restoreTabs: 'settings/getRestoreTabs',
disableBlur: 'settings/getDisableBlur',
applicationTheme: 'settings/getApplicationTheme',
editorTheme: 'settings/getEditorTheme',
editorFontSize: 'settings/getEditorFontSize',
@@ -417,6 +439,12 @@ GROUP BY
ORDER BY
employee.id ASC;
`;
},
otherContributors () {
return this.contributors
.split(',')
.filter(c => !c.includes(this.appAuthor))
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
}
},
created () {
@@ -436,6 +464,7 @@ ORDER BY
changeLocale: 'settings/changeLocale',
changePageSize: 'settings/changePageSize',
changeRestoreTabs: 'settings/changeRestoreTabs',
changeDisableBlur: 'settings/changeDisableBlur',
changeAutoComplete: 'settings/changeAutoComplete',
changeLineWrap: 'settings/changeLineWrap',
changeApplicationTheme: 'settings/changeApplicationTheme',
@@ -463,6 +492,9 @@ ORDER BY
toggleRestoreSession () {
this.changeRestoreTabs(!this.restoreTabs);
},
toggleDisableBlur () {
this.changeDisableBlur(!this.disableBlur);
},
toggleAutoComplete () {
this.changeAutoComplete(!this.selectedAutoComplete);
},

View File

@@ -11,11 +11,11 @@
<div class="footer-right-elements">
<ul class="footer-elements">
<li class="footer-element footer-link" @click="openOutside('https://www.treedom.net/en/user/fabio-di-stasio/event/antares-for-the-planet')">
<i class="mdi mdi-18px mdi-tree mr-1" />
<small>{{ $t('message.plantATree') }}</small>
<li class="footer-element footer-link" @click="openOutside('https://www.paypal.com/paypalme/fabiodistasio')">
<i class="mdi mdi-18px mdi-coffee mr-1" />
<small>{{ $t('word.donate') }}</small>
</li>
<li class="footer-element footer-link" @click="openOutside('https://github.com/Fabio286/antares/issues')">
<li class="footer-element footer-link" @click="openOutside('https://github.com/antares-sql/antares/issues')">
<i class="mdi mdi-18px mdi-bug" />
</li>
<li class="footer-element footer-link" @click="showSettingModal('about')">

View File

@@ -24,7 +24,11 @@
@mousedown.left="selectTab({uid: workspace.uid, tab: tab.uid})"
@mouseup.middle="closeTab(tab)"
>
<a v-if="tab.type === 'query'" class="tab-link">
<a
v-if="tab.type === 'query'"
class="tab-link"
:class="{'badge': tab.isChanged}"
>
<i class="mdi mdi-18px mdi-code-tags mr-1" />
<span>
<span>{{ tab.content || 'Query' | cutText }} #{{ tab.index }}</span>
@@ -446,7 +450,9 @@
/>
</template>
</div>
<WorkspaceEditConnectionPanel v-else :connection="connection" />
<div v-else class="connection-panel-wrapper">
<WorkspaceEditConnectionPanel :connection="connection" />
</div>
<ModalProcessesList
v-if="isProcessesModal"
:connection="connection"

View File

@@ -34,7 +34,7 @@
<fieldset class="m-0" :disabled="isBusy">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.connectionName') }}</label>
<label class="form-label cut-text">{{ $t('word.connectionName') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -47,7 +47,7 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.client') }}</label>
<label class="form-label cut-text">{{ $t('word.client') }}</label>
</div>
<div class="column col-8 col-sm-12">
<select v-model="connection.client" class="form-select">
@@ -61,9 +61,22 @@
</select>
</div>
</div>
<div v-if="connection.client === 'pg'" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.connectionString') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
ref="pgString"
v-model="connection.pgConnString"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP</label>
<label class="form-label cut-text">{{ $t('word.hostName') }}/IP</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -75,7 +88,7 @@
</div>
<div v-if="customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.database') }}</label>
<label class="form-label cut-text">{{ $t('word.database') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
@@ -88,7 +101,7 @@
</div>
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}</label>
<label class="form-label cut-text">{{ $t('word.port') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -102,7 +115,7 @@
</div>
<div v-if="customizations.database" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.database') }}</label>
<label class="form-label cut-text">{{ $t('word.database') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -114,7 +127,7 @@
</div>
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label>
<label class="form-label cut-text">{{ $t('word.user') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -127,7 +140,7 @@
</div>
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}</label>
<label class="form-label cut-text">{{ $t('word.password') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -140,7 +153,7 @@
</div>
<div v-if="customizations.connectionSchema" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.schema') }}</label>
<label class="form-label cut-text">{{ $t('word.schema') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -176,7 +189,7 @@
<form class="form-horizontal">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">
<label class="form-label cut-text">
{{ $t('message.enableSsl') }}
</label>
</div>
@@ -190,7 +203,7 @@
<fieldset class="m-0" :disabled="isBusy || !connection.ssl">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.privateKey') }}</label>
<label class="form-label cut-text">{{ $t('word.privateKey') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
@@ -203,7 +216,7 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.certificate') }}</label>
<label class="form-label cut-text">{{ $t('word.certificate') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
@@ -216,7 +229,7 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.caCertificate') }}</label>
<label class="form-label cut-text">{{ $t('word.caCertificate') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
@@ -229,7 +242,7 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.ciphers') }}</label>
<label class="form-label cut-text">{{ $t('word.ciphers') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -240,6 +253,14 @@
>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12" />
<div class="column col-8 col-sm-12">
<label class="form-checkbox form-inline">
<input v-model="connection.untrustedConnection" type="checkbox"><i class="form-icon" /> {{ $t('message.untrustedConnection') }}
</label>
</div>
</div>
</fieldset>
</form>
</div>
@@ -249,7 +270,7 @@
<form class="form-horizontal">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">
<label class="form-label cut-text">
{{ $t('message.enableSsh') }}
</label>
</div>
@@ -263,7 +284,7 @@
<fieldset class="m-0" :disabled="isBusy || !connection.ssh">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP</label>
<label class="form-label cut-text">{{ $t('word.hostName') }}/IP</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -275,7 +296,7 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label>
<label class="form-label cut-text">{{ $t('word.user') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -287,7 +308,7 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}</label>
<label class="form-label cut-text">{{ $t('word.password') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -299,7 +320,7 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}</label>
<label class="form-label cut-text">{{ $t('word.port') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -313,7 +334,7 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.privateKey') }}</label>
<label class="form-label cut-text">{{ $t('word.privateKey') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
@@ -326,7 +347,7 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.passphrase') }}</label>
<label class="form-label cut-text">{{ $t('word.passphrase') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -407,12 +428,14 @@ export default {
key: '',
ca: '',
ciphers: '',
untrustedConnection: false,
ssh: false,
sshHost: '',
sshUser: '',
sshPass: '',
sshKey: '',
sshPort: 22
sshPort: 22,
pgConnString: ''
},
isConnecting: false,
isTesting: false,
@@ -540,12 +563,12 @@ export default {
<style lang="scss" scoped>
.connection-panel {
margin-top: 15vh;
margin-left: auto;
margin-right: auto;
margin-bottom: 1rem;
.panel {
width: 450px;
min-width: 450px;
border-radius: $border-radius;
.panel-body {

View File

@@ -34,7 +34,7 @@
<fieldset class="m-0" :disabled="isBusy">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.connectionName') }}</label>
<label class="form-label cut-text">{{ $t('word.connectionName') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -47,7 +47,7 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.client') }}</label>
<label class="form-label cut-text">{{ $t('word.client') }}</label>
</div>
<div class="column col-8 col-sm-12">
<select v-model="localConnection.client" class="form-select">
@@ -61,9 +61,22 @@
</select>
</div>
</div>
<div v-if="connection.client === 'pg'" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.connectionString') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
ref="pgString"
v-model="localConnection.pgConnString"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP</label>
<label class="form-label cut-text">{{ $t('word.hostName') }}/IP</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -75,7 +88,7 @@
</div>
<div v-if="customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.database') }}</label>
<label class="form-label cut-text">{{ $t('word.database') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
@@ -88,7 +101,7 @@
</div>
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}</label>
<label class="form-label cut-text">{{ $t('word.port') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -102,7 +115,7 @@
</div>
<div v-if="customizations.database" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.database') }}</label>
<label class="form-label cut-text">{{ $t('word.database') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -114,7 +127,7 @@
</div>
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label>
<label class="form-label cut-text">{{ $t('word.user') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -127,7 +140,7 @@
</div>
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}</label>
<label class="form-label cut-text">{{ $t('word.password') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -140,7 +153,7 @@
</div>
<div v-if="customizations.connectionSchema" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.schema') }}</label>
<label class="form-label cut-text">{{ $t('word.schema') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -176,7 +189,7 @@
<form class="form-horizontal">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">
<label class="form-label cut-text">
{{ $t('message.enableSsl') }}
</label>
</div>
@@ -190,7 +203,7 @@
<fieldset class="m-0" :disabled="isBusy || !localConnection.ssl">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.privateKey') }}</label>
<label class="form-label cut-text">{{ $t('word.privateKey') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
@@ -203,7 +216,7 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.certificate') }}</label>
<label class="form-label cut-text">{{ $t('word.certificate') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
@@ -216,7 +229,7 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.caCertificate') }}</label>
<label class="form-label cut-text">{{ $t('word.caCertificate') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
@@ -229,7 +242,7 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.ciphers') }}</label>
<label class="form-label cut-text">{{ $t('word.ciphers') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -240,6 +253,14 @@
>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12" />
<div class="column col-8 col-sm-12">
<label class="form-checkbox form-inline">
<input v-model="localConnection.untrustedConnection" type="checkbox"><i class="form-icon" /> {{ $t('message.untrustedConnection') }}
</label>
</div>
</div>
</fieldset>
</form>
</div>
@@ -249,7 +270,7 @@
<form class="form-horizontal">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">
<label class="form-label cut-text">
{{ $t('message.enableSsh') }}
</label>
</div>
@@ -263,7 +284,7 @@
<fieldset class="m-0" :disabled="isBusy || !localConnection.ssh">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP</label>
<label class="form-label cut-text">{{ $t('word.hostName') }}/IP</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -275,7 +296,7 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label>
<label class="form-label cut-text">{{ $t('word.user') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -287,7 +308,7 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}</label>
<label class="form-label cut-text">{{ $t('word.password') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -299,7 +320,7 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}</label>
<label class="form-label cut-text">{{ $t('word.port') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -313,7 +334,7 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.privateKey') }}</label>
<label class="form-label cut-text">{{ $t('word.privateKey') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
@@ -326,7 +347,7 @@
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.passphrase') }}</label>
<label class="form-label cut-text">{{ $t('word.passphrase') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
@@ -519,12 +540,12 @@ export default {
<style lang="scss" scoped>
.connection-panel {
margin-top: 15vh;
margin-left: auto;
margin-right: auto;
margin-bottom: 1rem;
.panel {
width: 450px;
min-width: 450px;
border-radius: $border-radius;
.panel-body {

View File

@@ -58,6 +58,20 @@
</div>
</div>
</div>
<div
v-if="workspace.customizations.schemaExport"
class="context-element"
@click="showExportSchemaModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-database-arrow-down text-light pr-1" /> {{ $t('word.export') }}</span>
</div>
<div
v-if="workspace.customizations.schemaImport"
class="context-element"
@click="initImport"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-database-arrow-up text-light pr-1" /> {{ $t('word.import') }}</span>
</div>
<div
v-if="workspace.customizations.schemaEdit"
class="context-element"
@@ -95,6 +109,17 @@
:selected-schema="selectedSchema"
@close="hideEditModal"
/>
<ModalExportSchema
v-if="isExportSchemaModal"
:selected-schema="selectedSchema"
@close="hideExportSchemaModal"
/>
<ModalImportSchema
v-if="isImportSchemaModal"
ref="importModalRef"
:selected-schema="selectedSchema"
@close="hideImportSchemaModal"
/>
</BaseContextMenu>
</template>
@@ -103,14 +128,19 @@ import { mapGetters, mapActions } from 'vuex';
import BaseContextMenu from '@/components/BaseContextMenu';
import ConfirmModal from '@/components/BaseConfirmModal';
import ModalEditSchema from '@/components/ModalEditSchema';
import ModalExportSchema from '@/components/ModalExportSchema';
import ModalImportSchema from '@/components/ModalImportSchema';
import Schema from '@/ipc-api/Schema';
import Application from '@/ipc-api/Application';
export default {
name: 'WorkspaceExploreBarSchemaContext',
components: {
BaseContextMenu,
ConfirmModal,
ModalEditSchema
ModalEditSchema,
ModalExportSchema,
ModalImportSchema
},
props: {
contextEvent: MouseEvent,
@@ -119,7 +149,9 @@ export default {
data () {
return {
isDeleteModal: false,
isEditModal: false
isEditModal: false,
isExportSchemaModal: false,
isImportSchemaModal: false
};
},
computed: {
@@ -170,6 +202,30 @@ export default {
this.isEditModal = false;
this.closeContext();
},
showExportSchemaModal () {
this.isExportSchemaModal = true;
},
hideExportSchemaModal () {
this.isExportSchemaModal = false;
this.closeContext();
},
showImportSchemaModal () {
this.isImportSchemaModal = true;
},
hideImportSchemaModal () {
this.isImportSchemaModal = false;
this.closeContext();
},
async initImport () {
const result = await Application.showOpenDialog({ properties: ['openFile'], filters: [{ name: 'SQL', extensions: ['sql'] }] });
if (result && !result.canceled) {
const file = result.filePaths[0];
this.showImportSchemaModal();
this.$nextTick(() => {
this.$refs.importModalRef.startImport(file);
});
}
},
closeContext () {
this.$emit('close-context');
},

View File

@@ -616,6 +616,14 @@ export default {
},
removeField (uid) {
this.localFields = this.localFields.filter(field => field._antares_id !== uid);
this.localKeyUsage = this.localKeyUsage.filter(fk =>// Clear foreign keys
this.localFields.some(field => field.name === fk.field)
);
this.localIndexes = this.localIndexes.filter(index =>// Clear indexes
this.localFields.some(field =>
index.fields.includes(field.name)
)
);
},
addNewIndex (payload) {
this.localIndexes = [...this.localIndexes, {

View File

@@ -99,6 +99,9 @@
<span v-if="localRow.enumValues">
{{ localRow.enumValues }}
</span>
<span v-else-if="localRow.numScale">
{{ localLength }}, {{ localRow.numScale }}
</span>
<span v-else>
{{ localLength }}
</span>
@@ -112,6 +115,16 @@
class="editable-field form-input input-sm px-1"
@blur="editOFF"
>
<input
v-else-if="fieldType.scale"
ref="editField"
v-model="editingContent"
type="text"
autofocus
class="editable-field form-input input-sm px-1"
@keypress="checkLengthScale"
@blur="editOFF"
>
<input
v-else
ref="editField"
@@ -480,6 +493,11 @@ export default {
this.editingContent = this.localRow.enumValues;
this.originalContent = this.localRow.enumValues;
}
else if (this.fieldType.scale && field === 'length') {
const scale = this.localRow.numScale !== null ? this.localRow.numScale : 0;
this.editingContent = `${content}, ${scale}`;
this.originalContent = `${content}, ${scale}`;
}
else {
this.editingContent = content;
this.originalContent = content;
@@ -502,10 +520,17 @@ export default {
if (this.editingField === 'name')
this.$emit('rename-field', { old: this.localRow[this.editingField], new: this.editingContent });
this.localRow[this.editingField] = this.editingContent;
if (this.editingField === 'numLength' && this.fieldType.scale) {
const [length, scale] = this.editingContent.split(',');
this.localRow.numLength = +length;
this.localRow.numScale = scale ? +scale : null;
}
else
this.localRow[this.editingField] = this.editingContent;
if (this.editingField === 'type' && this.editingContent !== this.originalContent) {
this.localRow.numLength = null;
this.localRow.numScale = null;
this.localRow.charLength = null;
this.localRow.datePrecision = null;
this.localRow.enumValues = '';
@@ -560,6 +585,15 @@ export default {
this.originalContent = null;
this.editingField = null;
},
checkLengthScale (e) {
e = (e) || window.event;
const charCode = (e.which) ? e.which : e.keyCode;
if (((charCode > 31 && (charCode < 48 || charCode > 57)) && charCode !== 44) || (charCode === 44 && e.target.value.includes(',')))
e.preventDefault();
else
return true;
},
hideDefaultModal () {
this.isDefaultModal = false;
}

View File

@@ -46,6 +46,24 @@
<span>{{ $t('word.run') }}</span>
</button>
</div>
<button
v-if="!autocommit"
class="btn btn-dark btn-sm"
:class="{'loading':isQuering}"
@click="commitTab()"
>
<i class="mdi mdi-24px mdi-cube-send pr-1" />
<span>{{ $t('word.commit') }}</span>
</button>
<button
v-if="!autocommit"
class="btn btn-dark btn-sm"
:class="{'loading':isQuering}"
@click="rollbackTab()"
>
<i class="mdi mdi-24px mdi-undo-variant pr-1" />
<span>{{ $t('word.rollback') }}</span>
</button>
<button
class="btn btn-link btn-sm mr-0"
:disabled="!query || isQuering"
@@ -78,7 +96,7 @@
</button>
<div class="dropdown table-dropdown pr-2">
<button
:disabled="!results.length || isQuering"
:disabled="!hasResults || isQuering"
class="btn btn-dark btn-sm dropdown-toggle mr-0 pr-0"
tabindex="0"
>
@@ -95,6 +113,17 @@
</li>
</ul>
</div>
<div class="input-group pr-2" :title="$t('message.commitMode')">
<i class="input-group-addon addon-sm mdi mdi-24px mdi-source-commit p-0" />
<select v-model="autocommit" class="form-select select-sm text-bold">
<option :value="true">
{{ $t('message.autoCommit') }}
</option>
<option :value="false">
{{ $t('message.manualCommit') }}
</option>
</select>
</div>
</div>
<div class="workspace-query-info">
<div
@@ -104,11 +133,19 @@
>
<i class="mdi mdi-timer-sand mdi-rotate-180 pr-1" /> <b>{{ durationsCount / 1000 }}s</b>
</div>
<div v-if="resultsCount">
{{ $t('word.results') }}: <b>{{ resultsCount.toLocaleString() }}</b>
<div
v-if="resultsCount"
class="d-flex"
:title="$t('word.results')"
>
<i class="mdi mdi-equal pr-1" /> <b>{{ resultsCount.toLocaleString() }}</b>
</div>
<div v-if="affectedCount !== null">
{{ $t('message.affectedRows') }}: <b>{{ affectedCount }}</b>
<div
v-if="hasAffected"
class="d-flex"
:title="$t('message.affectedRows')"
>
<i class="mdi mdi-target pr-1" /> <b>{{ affectedCount }}</b>
</div>
<div class="input-group" :title="$t('word.schema')">
<i class="input-group-addon addon-sm mdi mdi-24px mdi-database" />
@@ -182,6 +219,7 @@ export default {
isQuering: false,
isCancelling: false,
showCancel: false,
autocommit: true,
results: [],
selectedSchema: null,
resultsCount: 0,
@@ -200,6 +238,9 @@ export default {
workspace () {
return this.getWorkspace(this.connection.uid);
},
tabUid () {
return this.$vnode.key;
},
breadcrumbsSchema () {
return this.workspace.breadcrumbs.schema || null;
},
@@ -214,6 +255,12 @@ export default {
},
history () {
return this.getHistoryByWorkspace(this.connection.uid) || [];
},
hasResults () {
return this.results.length && this.results[0].rows;
},
hasAffected () {
return this.affectedCount || (!this.resultsCount && this.affectedCount !== null);
}
},
watch: {
@@ -251,12 +298,18 @@ export default {
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
const params = {
uid: this.connection.uid,
tabUid: this.tab.uid
};
Schema.destroyConnectionToCommit(params);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
updateTabContent: 'workspaces/updateTabContent',
setUnsavedChanges: 'workspaces/setUnsavedChanges',
saveHistory: 'history/saveHistory'
}),
async runQuery (query) {
@@ -270,6 +323,7 @@ export default {
uid: this.connection.uid,
schema: this.selectedSchema,
tabUid: this.tab.uid,
autocommit: this.autocommit,
query
};
@@ -294,6 +348,8 @@ export default {
content: query
});
this.saveHistory(params);
if (!this.autocommit)
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: true });
}
else
this.addNotification({ status: 'error', message: response });
@@ -391,6 +447,42 @@ export default {
},
downloadTable (format) {
this.$refs.queryTable.downloadTable(format, `${this.tab.type}-${this.tab.index}`);
},
async commitTab () {
this.isQuering = true;
try {
const params = {
uid: this.connection.uid,
tabUid: this.tab.uid
};
await Schema.commitTab(params);
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: false });
this.addNotification({ status: 'success', message: this.$t('message.actionSuccessful', { action: 'COMMIT' }) });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isQuering = false;
},
async rollbackTab () {
this.isQuering = true;
try {
const params = {
uid: this.connection.uid,
tabUid: this.tab.uid
};
await Schema.rollbackTab(params);
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: false });
this.addNotification({ status: 'success', message: this.$t('message.actionSuccessful', { action: 'ROLLBACK' }) });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isQuering = false;
}
}
};
@@ -419,10 +511,12 @@ export default {
.workspace-query-runner-footer {
display: flex;
flex-wrap: wrap;
row-gap: 0.4rem;
justify-content: space-between;
padding: 0.3rem 0.6rem 0.4rem;
align-items: center;
height: 42px;
min-height: 42px;
.workspace-query-buttons,
.workspace-query-info {

View File

@@ -175,8 +175,10 @@ export default {
if (this.currentSort && !this.isHardSort) {
return [...this.localResults].sort((a, b) => {
let modifier = 1;
const valA = typeof a[this.currentSort] === 'string' ? a[this.currentSort].toLowerCase() : a[this.currentSort];
const valB = typeof b[this.currentSort] === 'string' ? b[this.currentSort].toLowerCase() : b[this.currentSort];
let valA = typeof a[this.currentSort] === 'string' ? a[this.currentSort].toLowerCase() : a[this.currentSort];
if (!isNaN(valA)) valA = Number(valA);
let valB = typeof b[this.currentSort] === 'string' ? b[this.currentSort].toLowerCase() : b[this.currentSort];
if (!isNaN(valB)) valB = Number(valB);
if (this.currentSortDir === 'desc') modifier = -1;
if (valA < valB) return -1 * modifier;
if (valA > valB) return 1 * modifier;
@@ -280,6 +282,7 @@ export default {
fieldLength (field) {
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
else if (TEXT.includes(field.type)) return field.charLength;
else if (field.numScale) return `${field.numPrecision}, ${field.numScale}`;
return field.length;
},
keyName (key) {
@@ -407,10 +410,13 @@ export default {
const row = this.localResults.find(row => this.selectedRows.includes(row._antares_id));
const cellName = Object.keys(row).find(prop => [
this.selectedCell.field,
this.selectedCell.orgField,
`${this.fields[0].table}.${this.selectedCell.field}`,
`${this.fields[0].tableAlias}.${this.selectedCell.field}`
].includes(prop));
const valueToCopy = row[cellName];
let valueToCopy = row[cellName];
if (typeof valueToCopy === 'object')
valueToCopy = JSON.stringify(valueToCopy);
navigator.clipboard.writeText(valueToCopy);
},
copyRow () {

View File

@@ -61,8 +61,6 @@ export default {
selectedRows: Array,
selectedCell: Object
},
computed: {
},
methods: {
showConfirmModal () {
this.$emit('show-delete-modal');

View File

@@ -10,7 +10,7 @@
:key="cKey"
class="td p-0"
tabindex="0"
@contextmenu.prevent="openContext($event, { id: row._antares_id, field: cKey })"
@contextmenu.prevent="openContext($event, { id: row._antares_id, orgField: cKey })"
>
<template v-if="cKey !== '_antares_id'">
<span
@@ -123,6 +123,21 @@
</div>
</template>
</ConfirmModal>
<ConfirmModal
v-if="isMapModal"
:hide-footer="true"
size="medium"
@hide="hideEditorModal"
>
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-map mr-1" /> <span class="cut-text">"{{ editingField }}"</span>
</div>
</template>
<template #body>
<BaseMap :points="editingContent" :is-multi-spatial="isMultiSpatial" />
</template>
</ConfirmModal>
<ConfirmModal
v-if="isBlobEditor"
:confirm-text="$t('word.update')"
@@ -187,10 +202,27 @@ import { mimeFromHex } from 'common/libs/mimeFromHex';
import { formatBytes } from 'common/libs/formatBytes';
import { bufferToBase64 } from 'common/libs/bufferToBase64';
import hexToBinary from 'common/libs/hexToBinary';
import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BOOLEAN, DATE, TIME, DATETIME, BLOB, BIT, HAS_TIMEZONE } from 'common/fieldTypes';
import {
TEXT,
LONG_TEXT,
ARRAY,
TEXT_SEARCH,
NUMBER,
FLOAT,
BOOLEAN,
DATE,
TIME,
DATETIME,
BLOB,
BIT,
HAS_TIMEZONE,
SPATIAL,
IS_MULTI_SPATIAL
} from 'common/fieldTypes';
import { VueMaskDirective } from 'v-mask';
import ConfirmModal from '@/components/BaseConfirmModal';
import TextEditor from '@/components/BaseTextEditor';
import BaseMap from '@/components/BaseMap';
import ForeignKeySelect from '@/components/ForeignKeySelect';
export default {
@@ -198,7 +230,8 @@ export default {
components: {
ConfirmModal,
TextEditor,
ForeignKeySelect
ForeignKeySelect,
BaseMap
},
directives: {
mask: VueMaskDirective
@@ -239,7 +272,8 @@ export default {
if (BIT.includes(type)) {
if (typeof val === 'number') val = [val];
const hex = Buffer.from(val).toString('hex');
return hexToBinary(hex);
const bitString = hexToBinary(hex);
return parseInt(bitString).toString().padStart(precision, '0');
}
if (ARRAY.includes(type)) {
@@ -248,7 +282,10 @@ export default {
return val;
}
return val;
if (SPATIAL.includes(type))
return val;
return typeof val === 'object' ? JSON.stringify(val) : val;
}
},
props: {
@@ -263,6 +300,8 @@ export default {
isInlineEditor: {},
isTextareaEditor: false,
isBlobEditor: false,
isMapModal: false,
isMultiSpatial: false,
willBeDeleted: false,
originalContent: null,
editingContent: null,
@@ -331,6 +370,9 @@ export default {
if (BOOLEAN.includes(this.editingType))
return { type: 'boolean', mask: false };
if (SPATIAL.includes(this.editingType))
return { type: 'map', mask: false };
return { type: 'text', mask: false };
},
isImage () {
@@ -403,7 +445,7 @@ export default {
return bufferToBase64(val);
},
editON (event, content, field) {
if (!this.isEditable) return;
if (!this.isEditable || this.editingType === 'none') return;
window.addEventListener('keydown', this.onKey);
@@ -419,6 +461,15 @@ export default {
return;
}
if (SPATIAL.includes(type)) {
if (content) {
this.isMultiSpatial = IS_MULTI_SPATIAL.includes(type);
this.isMapModal = true;
this.editingContent = this.$options.filters.typeFormat(content, type);
}
return;
}
if (BLOB.includes(type)) {
this.isBlobEditor = true;
this.editingContent = content || '';
@@ -490,6 +541,8 @@ export default {
hideEditorModal () {
this.isTextareaEditor = false;
this.isBlobEditor = false;
this.isMapModal = false;
this.isMultiSpatial = false;
},
downloadFile () {
const downloadLink = document.createElement('a');
@@ -526,7 +579,7 @@ export default {
return this.keyUsage.find(key => key.field === keyName);
},
openContext (event, payload) {
payload.field = this.fields[payload.field].name;// Ensures field name only
payload.field = this.fields[payload.orgField].name;// Ensures field name only
payload.isEditable = this.isEditable;
this.$emit('contextmenu', event, payload);
},

View File

@@ -85,7 +85,7 @@
@click="showFakerModal"
>
<i class="mdi mdi-24px mdi-playlist-plus mr-1" />
<span>{{ $t('message.tableFiller') }}</span>
<span>{{ $tc('message.insertRow', 2) }}</span>
</button>
<div class="dropdown table-dropdown pr-2">

View File

@@ -80,6 +80,7 @@ module.exports = {
deterministic: 'Deterministic',
context: 'Context',
export: 'Export',
import: 'Import',
returns: 'Returns',
timing: 'Timing',
state: 'State',
@@ -122,9 +123,23 @@ module.exports = {
select: 'Select',
passphrase: 'Passphrase',
filter: 'Filter',
change: 'Change',
views: 'Views',
triggers: 'Triggers',
routines: 'Routines',
functions: 'Functions',
schedulers: 'Schedulers',
includes: 'Includes',
drop: 'Drop',
completed: 'Completed',
aborted: 'Aborted',
disabled: 'Disabled',
enable: 'Enable',
disable: 'Disable'
disable: 'Disable',
commit: 'Commit',
rollback: 'Rollback',
connectionString: 'Connection string',
contributors: 'Contributors'
},
message: {
appWelcome: 'Welcome to Antares SQL Client!',
@@ -250,9 +265,30 @@ module.exports = {
searchForQueries: 'Search for queries',
killProcess: 'Kill process',
closeTab: 'Close tab',
exportSchema: 'Export schema',
importSchema: 'Import schema',
directoryPath: 'Directory path',
newInserStmtEvery: 'New INSERT statement every',
processingTableExport: 'Processing {table}',
fechingTableExport: 'Fetching {table} data',
writingTableExport: 'Writing {table} data',
checkAllTables: 'Check all tables',
uncheckAllTables: 'Uncheck all tables',
goToDownloadPage: 'Go to download page',
readOnlyMode: 'Read-only mode',
killQuery: 'Kill query'
killQuery: 'Kill query',
insertRow: 'Insert row | Insert rows',
commitMode: 'Commit mode',
autoCommit: 'Auto commit',
manualCommit: 'Manual commit',
actionSuccessful: '{action} successful',
importQueryErrors: 'Warning: {n} error has accurrend | Warning: {n} errors occurred',
executedQueries: '{n} query executed | {n} queries executed',
ourputFormat: 'Output format',
singleFile: 'Single {ext} file',
zipCompressedFile: 'ZIP compressed {ext} file',
disableBlur: 'Disable blur',
untrustedConnection: 'Untrusted connection'
},
faker: {
address: 'Address',

View File

@@ -1,196 +1,298 @@
module.exports = {
word: {
edit: 'Éditer',
save: 'Enregistrer',
edit: 'Editer',
save: 'Sauver',
close: 'Fermer',
delete: 'Supprimer',
delete: 'Effacer',
confirm: 'Confirmer',
cancel: 'Annuler',
send: 'Envoyer',
connectionName: 'Nom de connexion',
connectionName: 'Nom de la connexion',
client: 'Client',
hostName: 'Nom d\'hôte',
hostName: 'Nom du host',
port: 'Port',
user: 'Utilisateur',
password: 'Mot de passe',
credentials: 'Identifiants',
connect: 'Se connecter',
credentials: 'Credentials',
connect: 'Connexion',
connected: 'Connecté',
disconnect: 'Se déconnecter',
disconnect: 'Déconnection',
disconnected: 'Déconnecté',
refresh: 'Rafraichir',
refresh: 'Rafrechir',
settings: 'Paramètres',
general: 'Général',
themes: 'Thèmes',
update: 'Mise à jour',
about: 'À propos',
language: 'Langue',
about: 'A propos de',
language: 'Langage',
version: 'Version',
donate: 'Faire un don',
run: 'Exécuter',
donate: 'Faire une donation',
run: 'Lancer',
schema: 'Schéma',
results: 'Résutats',
results: 'Resultats',
size: 'Taille',
seconds: 'Secondes',
type: 'Type',
mimeType: 'Mime-Type',
download: 'Télécharger',
add: 'Ajouter',
data: 'Données',
data: 'Donnée',
properties: 'Propriétés',
insert: 'Insérer',
connecting: 'Connexion',
connecting: 'Connexion en cours',
name: 'Nom',
collation: 'Collation',
collation: 'Langage',
clear: 'Effacer',
options: 'Options',
autoRefresh: 'Auto-rafraichissement',
indexes: 'Index',
foreignKeys: 'Clés étrangères',
length: 'Taille',
unsigned: 'Non-signé',
default: 'Défaut',
autoRefresh: 'Auto-rafraichissemnt',
indexes: 'Indexs',
foreignKeys: 'Clefs étrangères',
length: 'Longueur',
unsigned: 'non signé',
default: 'Par défault',
comment: 'Commentaire',
key: 'Clé | Clés',
key: 'Clef | Clefs',
order: 'Ordre',
expression: 'Expression',
autoIncrement: 'Auto Increment',
engine: 'Engine',
autoIncrement: 'Auto Incrémentation',
engine: 'Moteur',
field: 'Champ | Champs',
approximately: 'Approximativement',
total: 'Totale',
total: 'Total',
table: 'Table',
discard: 'Abandonner',
discard: 'Jeter',
stay: 'Rester',
author: 'Auteur',
light: 'Clair',
light: 'Léger',
dark: 'Sombre',
autoCompletion: 'Completion auto',
autoCompletion: 'Auto Completion',
application: 'Application',
editor: 'Editeur',
editor: 'Editor',
view: 'Vue',
definer: 'Définisseur',
algorithm: 'Algorithme',
algorithme: 'Algorithme',
trigger: 'Déclencheur | Déclencheurs',
storedRoutine: 'Procedure stockée | Procedures stockées',
scheduler: 'Opération planifiée | Opérations planifiées',
event: 'Evenement',
storedRoutine: 'Routine stockée | Routines stockées',
scheduler: 'Planificateur | Planificateurs',
event: 'Évènement',
parameters: 'Paramètres',
function: 'Fonction | Fonctions',
deterministic: 'Déterministe',
context: 'Contextz',
export: 'Exporter',
returns: 'Retourner',
timing: 'Horaire',
state: 'État',
context: 'Contexte',
export: 'Export',
import: 'Import',
returns: 'Retourne',
timing: 'Temps',
state: 'Etat',
execution: 'Exécution',
starts: 'Débuts',
ends: 'Fins',
starts: 'Commence',
ends: 'Fini',
ssl: 'SSL',
privateKey: 'Clé privée',
certificate: 'Certificat',
caCertificate: 'CA certificat',
ciphers: 'Chiffrement',
upload: 'Charger',
browse: 'Parcourir',
faker: 'Faker',
sshTunnel: 'SSH tunnel'
caCertificate: 'Certificat CA',
ciphers: 'Codes secrets',
upload: 'Téléverser',
browse: 'Naviguer',
faker: 'Imposteur',
sshTunnel: 'SSH tunnel',
content: 'Contenu',
cut: 'Couper',
copy: 'Copier',
paste: 'Coller',
tools: 'Outils',
variables: 'Variables',
processes: 'Processus',
Database: 'Base de données',
scratchpad: 'Bloc-notes',
array: 'Tableau',
changelog: 'Log de changement',
Format: 'Format',
structure: 'Structure',
small: 'Petit',
medium: 'Moyen',
large: 'Grand',
row: 'Ligne | Lignes',
cell: 'Cellule | Cellules',
triggerFunction: 'Fonction de déclenchement | Fonctions de déclenchement',
all: 'Tout',
duplicate: 'Double',
routine: 'Routine',
new: 'Nouveau',
history: 'Passé',
select: 'Sélectionner',
passphrase: 'Phrase',
filter: 'Filtre',
change: 'Changement',
views: 'Vues',
triggers: 'Déclencheurs',
routines: 'Routines',
fonctions: 'Fonctions',
schedulers: 'Programmateurs',
includes: 'Inclut',
drop: 'Effacer',
completed: 'Terminé',
aborted: 'Annulé',
disabled: 'Désactivé',
enable: 'activer',
disable: 'Désactiver',
commit: 'Appliquer',
rollback: 'Retour arrière',
connectionString: 'Chaîne de connexion',
contributors: 'Contributeurs'
},
message: {
appWelcome: 'Bienvenu sur le client SQL Antares!',
appFirstStep: 'Première étape: Créer une nouvelle connexion à une base de données.',
appWelcome: 'Bienvenue dans le client SQL Antares!',
appFirstStep: 'Votre première étape : créer une nouvelle connexion à la base de données.',
addConnection: 'Ajouter une connexion',
createConnection: 'Créer une connexion',
createNewConnection: 'Créer une nouvelle connexion',
askCredentials: 'Demander les identifiants',
askCredentials: 'Demander des informations d\'identification',
testConnection: 'Tester la connexion',
editConnection: 'Editer la connexion',
editConnection: 'Modifier la connexion',
deleteConnection: 'Supprimer la connexion',
deleteCorfirm: 'Êtes-vous sûr de vouloir annuler',
connectionSuccessfullyMade: 'Connexion établie avec succès!',
madeWithJS: 'Créé avec 💛 et JavaScript!',
checkForUpdates: 'Rechercher des mises à jour',
deleteCorfirm: 'Confirmez-vous l\'annulation de',
connectionSuccessfullyMade: 'Connexion établie avec succès !',
madeWithJS: 'Fait avec 💛 et JavaScript !',
checkForUpdates: 'Vérifier les mises à jour',
noUpdatesAvailable: 'Aucune mise à jour disponible',
checkingForUpdate: 'Recherche de mise à jour',
checkFailure: 'Erreur lors de la recherche, essayez plus tard',
updateAvailable: 'Une mise à jour est disponible',
checkingForUpdate: 'Vérification des mises à jour',
checkFailure: 'La vérification a échoué, veuillez essayer plus tard',
updateAvailable: 'Mise à jour disponible',
downloadingUpdate: 'Téléchargement de la mise à jour',
updateDownloaded: 'Mise à jour téléchargée',
restartToInstall: 'Redémarrer Antares pour l\'installer',
unableEditFieldWithoutPrimary: 'Impossible de modifier un champ sans clé primaire dans l\'ensemble de résultats',
editCell: 'Modifier une cellule',
deleteRows: 'Supprimer une ligne | Supprimer {count} lignes',
confirmToDeleteRows: 'Êtes-vous sûr de vouloir supprimer une ligne? | Êtes-vous sûr de vouloir supprimer {count} lignes?',
notificationsTimeout: 'Délai d\'expiration des notifications',
uploadFile: 'Charger un fichier',
addNewRow: 'Ajouter une ligne',
restartToInstall: 'Redémarrer Antares pour installer',
unableEditFieldWithoutPrimary: 'Impossible de modifier un champ sans clé primaire dans le jeu de résultats',
editCell: 'Editer la cellule',
deleteRows: 'Effacer la ligne | Effacer {count} lignes',
confirmToDeleteRows: 'Confirmez-vous la suppression d\'une ligne ? | Confirmez-vous la suppression de {count} lignes ?',
notificationsTimeout: 'Délai d\'attente pour les notifications',
uploadFile: 'Télécharger un fichier',
addNewRow: 'Ajouter une nouvelle ligne',
numberOfInserts: 'Nombre d\'insertions',
openNewTab: 'Ouvrir un nouvel onglet',
affectedRows: 'Lignes concernées',
affectedRows: 'Lignes affectées',
createNewDatabase: 'Créer une nouvelle base de données',
databaseName: 'Nom par défaut',
databaseName: 'Nom de la base de données',
serverDefault: 'Serveur par défaut',
deleteDatabase: 'Supprimer la base de données',
editDatabase: 'Modifier la base de données',
editDatabase: 'Editer la base de données',
clearChanges: 'Effacer les modifications',
addNewField: 'Ajouter un champ',
manageIndexes: 'Gérer les index',
manageForeignKeys: 'Gérer les clés étrangères',
allowNull: 'NULL autorisé',
zeroFill: 'Remplissage zéro',
addNewField: 'Ajouter un nouveau champ',
manageIndexes: 'Gère les index',
manageForeignKeys: 'Gère les clés étrangères',
allowNull: 'Autoriser NULL',
zeroFill: 'Remplissage avec zéro',
customValue: 'Valeur personnalisée',
onUpdate: 'Lors d\'une mise à jour',
onUpdate: 'lors de la mise à jour',
deleteField: 'Supprimer le champ',
createNewIndex: 'Créer un index',
createNewIndex: 'Créer un nouvel index',
addToIndex: 'Ajouter à l\'index',
createNewTable: 'Créer une nouvelle table',
emptyTable: 'Table vide',
deleteTable: 'Supprimer la table',
emptyCorfirm: 'Êtes-vous sûr de vouloir videz',
unsavedChanges: 'Changements non sauvegardés',
discardUnsavedChanges: 'Vous avez des modifications non sauvegardées. En quittant cet onglet, ces changements seront supprimés.',
thereAreNoIndexes: 'Il n\'y a pas d\'indexes',
emptyTable: 'Vider la table',
deleteTable: 'Supprimer une table',
emptyCorfirm: 'Confirmez-vous que vous souhaitez vider la table ?',
unsavedChanges: 'Modifications non sauvegardées',
discardUnsavedChanges: 'Vous avez des modifications non sauvegardées. En fermant cet onglet, ces modifications seront supprimées',
thereAreNoIndexes: 'Il n\'y a pas d\'index',
thereAreNoForeign: 'Il n\'y a pas de clés étrangères',
createNewForeign: 'Créer une clés étrangère',
createNewForeign: 'Créer une nouvelle clé étrangère',
referenceTable: 'Table de référence',
referenceField: 'CHamp de référence',
foreignFields: 'Champ étrangé',
referenceField: 'Champ de référence',
foreignFields: 'Champs étrangers',
invalidDefault: 'Valeur par défaut invalide',
onDelete: 'Lors de la suppression',
onDelete: 'lors de l\'effacement',
applicationTheme: 'Thème de l\'application',
editorTheme: 'Thème de l\'éditeur',
wrapLongLines: 'Retour à la ligne automatique',
selectStatement: 'Sélectionnez la déclaration',
triggerStatement: 'Déclaration de déclencheur',
wrapLongLines: 'Retour à la ligne pour les lignes longues',
selectStatement: 'Sélectionner un état',
triggerStatement: 'Déclencher un état',
sqlSecurity: 'Sécurité SQL',
updateOption: 'Options de mises à jour',
deleteView: 'Supprimer la vue',
updateOption: 'Mide à jour d\'une option',
deleteView: 'Effacer une vue',
createNewView: 'Créer une nouvelle vue',
deleteTrigger: 'Supprimer le déclencheur',
createNewTrigger: 'Créer un nouveau déclencheur',
currentUser: 'Utilisateur actuel',
routineBody: 'Contenu de la procédure',
deleteTrigger: 'Effacer un déclencheur',
createNewTrigger: 'Cr"er un nouveau déclencheur',
currentUser: 'utilisateur courant',
routineBody: 'Corps de la routine',
dataAccess: 'Accès aux données',
thereAreNoParameters: 'Il n\'y a pas de paramètres',
thereAreNoParameters: 'Il n\'y a pas de paramètre',
createNewParameter: 'Créer un nouveau paramètre',
createNewRoutine: 'Créer une nouvelle procédure stockée',
deleteRoutine: 'Supprimer une procédure stockée',
functionBody: 'Contenu de la fonction',
createNewRoutine: 'Créer une nouvelle routine stockée',
deleteRoutine: 'Effacer une routine stockée',
functionBody: 'Corps d\'une fonction',
createNewFunction: 'Créer une nouvelle fonction',
deleteFunction: 'Supprimer la fonction',
schedulerBody: 'Contenu du opération planifiée',
createNewScheduler: 'Créere une nouvelle opération planifiée',
deleteScheduler: 'Supprimer l\'opération planifiée',
preserveOnCompletion: 'Préserver à l\'achèvement',
deleteFunction: 'Effacer une fonction',
schedulerBody: 'Corps d\'un programmateur',
createNewScheduler: 'Créer un nouveau programmateur',
deleteScheduler: 'Effacer un programmateur',
preserveOnCompletion: 'Préserver à la terminaison',
enableSsl: 'Activer le SSL',
manualValue: 'Valeur manuelle',
tableFiller: 'Remplisseur de table',
enableSsh: 'Activer le SSH'
fakeDataLanguage: 'Language de données fausses',
searchForElements: 'Rechercher des éléments',
selectAll: 'Tout séélectionner',
queryDuration: 'Temps de requêtage',
includeBetaUpdates: 'Inclure les mises à jour beta',
setNull: 'Définir comme NULL',
processesList: 'List des processus',
processInfo: 'Information sur le processus',
manageUsers: 'Organisation des utilisateurs',
createNewSchema: 'Créer un nouveau schéma',
schemaName: 'Nom du Schéma',
editSchema: 'Modifier le schéma',
deleteSchema: 'Effacer le schéma',
markdownSupported: 'Support du Markdown',
plantATree: 'Planter un arbre',
dataTabPageSize: 'Taille de la page de l\'onglet données',
enableSsh: 'Activer le SSH',
pageNumber: 'Numéro de la page',
duplicateTable: 'Copier la table',
noOpenTabs: 'Il n\'y a pas d\'onglet ouvert, naviguer vers la barre gauche ou :',
noSchema: 'Pas de schéma',
restorePreviourSession: 'Restorer une session précédente',
runQuery: 'Lancer la requête',
thereAreNoTableFields: 'Il n\'y a pas de champ table',
newTable: 'Nouvelle table',
newView: 'Nouvelle vue',
newTrigger: 'Nouveau déclencheur',
newRoutine: 'Nouvelle routine',
newFunction: 'Nouvelle function',
newScheduler: 'Nouveau déclencheur',
newTriggerFunction: 'Nouvelle fonction de déclencheur',
thereIsNoQueriesYet: 'Il n\'y a pas encore de requête',
searchForQueries: 'Rechercher des requêtes',
killProcess: 'tuer un processus',
closeTab: 'Fermer un onglet',
exportSchema: 'Exporter un schéma',
importSchema: 'Importer un schéma',
directoryPath: 'chemin du répertoire',
newInserStmtEvery: 'Nouvelle déclaration d\'insertion tous les',
processingTableExport: 'Traitement {table}',
fechingTableExport: 'Recherche des données de la table {table}',
writingTableExport: 'Ecriture des données de la table {table}',
checkAllTables: 'Vérification de toutes les tables',
uncheckAllTables: 'Désélectionner toutes les tables',
goToDownloadPage: 'Aller vers la page de téléchargement',
readOnlyMode: 'Mode lecture seule',
killQuery: 'Terminer une requête',
insertRow: 'Insérer un ligne | Insérer des lignes',
commitMode: 'Mode insertion directe',
autoCommit: 'Insertion directe automatique',
manualCommit: 'Insertion directe manuelle',
actionSuccessful: '{action} réussie',
importQueryErrors: 'Attention : {n} erreurs se sont produites | Attention: {n} erreurs sont apparues',
executedQueries: '{n} requêtes ont été exécutées | {n} requêtes exécutées',
ourputFormat: 'Format de sortie',
singleFile: 'Seul fichier avec l\'extension {ext}',
zipCompressedFile: 'Fichier compréssé avec l\'extension {ext}',
disableBlur: 'Désactiver le floue'
},
faker: {
address: 'Adresse',
commerce: 'Commerce',
company: 'Entreprise',
company: 'Companie',
database: 'Base de données',
date: 'Date',
finance: 'Finance',
@@ -204,105 +306,105 @@ module.exports = {
random: 'Aléatoire',
system: 'Système',
time: 'Temps',
vehicle: 'Véhicle',
vehicle: 'Véhicule',
zipCode: 'Code postal',
zipCodeByState: 'Code postal par région',
zipCodeByState: 'Code postal par état',
city: 'Ville',
cityPrefix: 'Préfixe de la ville',
citySuffix: 'Suffixe de la ville',
streetName: 'Ne de la rue',
streetAddress: 'Adresse',
streetName: 'Nom de la rue',
streetAddress: 'Adresse de la rue',
streetSuffix: 'Suffixe de la rue',
streetPrefix: 'Préfixe de la rue',
secondaryAddress: 'Adresse secondaire',
county: 'Comté',
county: 'Département',
country: 'Pays',
countryCode: 'Code du pays',
state: 'Région',
stateAbbr: 'Abbreviation de la région',
state: 'Etat',
stateAbbr: 'Abviation de l\'état',
latitude: 'Latitude',
longitude: 'Longitude',
direction: 'Direction',
cardinalDirection: 'Orientation cardinale',
ordinalDirection: 'Orientation originale',
nearbyGPSCoordinate: 'Coordonnées GPS des environs',
timeZone: 'Fuseau horaire',
cardinalDirection: 'Direction cardinale',
ordinalDirection: 'Direction Ordinale',
nearbyGPSCoordinate: 'Coordonnées GPS proches',
timeZone: 'fuseau horaire',
color: 'Couleur',
department: 'Département',
productName: 'Nom de produit',
department: 'Départment',
productName: 'Nom du produit',
price: 'Prix',
productAdjective: 'Adjectif du produit',
productMaterial: 'Matériau du produit',
productMaterial: 'Matériaux du produit',
product: 'Produit',
productDescription: 'Description du produit',
suffixes: 'Suffixes',
companyName: 'Nom de l\'entreprise',
companySuffix: 'Suffixe de l\'entreprise',
catchPhrase: 'Slogan',
companyName: 'Nom de la société',
companySuffix: 'Suffixe de la société',
catchPhrase: 'Phrase de rappel',
bs: 'BS',
catchPhraseAdjective: 'Adjectif du slogan',
catchPhraseDescriptor: 'Descripteur de slogan',
catchPhraseNoun: 'Nom de la phrase d\'accroche',
catchPhraseAdjective: 'Adjectif de la phrase de rappel',
catchPhraseDescriptor: 'Descripteur de la phrase de rappel',
catchPhraseNoun: 'Nom de la phrase de rappel',
bsAdjective: 'Adjectif BS',
bsBuzz: 'BS buzz',
bsNoun: 'Nom BS',
column: 'Colonne',
type: 'Type',
collation: 'Collation',
engine: 'Engine',
collation: 'Langue',
engine: 'Moteur',
past: 'Passé',
future: 'Futur',
future: 'Future',
between: 'Entre',
recent: 'Récent',
soon: 'Bientôt',
month: 'Mois',
weekday: 'Mercredi',
account: 'Compte',
accountName: 'Nom de compte',
routingNumber: 'Numéros de routage',
weekday: 'Jour de la semaine',
account: 'compte',
accountName: 'Nom du compte',
routingNumber: 'Numéro de la routing',
mask: 'Masque',
amount: 'Quantité',
transactionType: 'Type de transaction',
amount: 'Montant',
transactionType: 'Type de la transaction',
currencyCode: 'Code de la devise',
currencyName: 'Nom de la devise',
currencySymbol: 'Symbole de la devise',
bitcoinAddress: 'Adresse Bitcoin',
litecoinAddress: 'Adresse Litecoin',
creditCardNumber: 'Numero de carte de crédit',
creditCardCVV: 'Cryptogramme',
creditCardNumber: 'Numéro de la carte de bancaire',
creditCardCVV: 'Numéro de vérification',
ethereumAddress: 'Adresse Ethereum',
iban: 'Iban',
bic: 'Bic',
transactionDescription: 'Description de la transaction',
branch: 'Branche',
commitEntry: 'Valider l\'entrée',
commitMessage: 'Valider le message',
commitSha: 'Valider le SHA',
shortSha: 'SHA court',
abbreviation: 'Abbréviation',
commitEntry: 'Insérer les données',
commitMessage: 'Insérer le message',
commitSha: 'Insérer SHA',
shortSha: 'SHA réduit',
abbreviation: 'Abréviation',
adjective: 'Adjectif',
noun: 'Nom',
verb: 'Verbe',
ingverb: 'Ingverb',
phrase: 'Phrase',
avatar: 'Avatar',
email: 'Email',
exampleEmail: 'Exemple d\'email',
userName: 'Nom d\'utilisateur',
email: 'mail',
exampleEmail: 'Exemple de mail',
userName: 'Nom utilisateur',
protocol: 'Protocole',
url: 'Url',
domainName: 'Nom de domaine',
domainSuffix: 'Suffixe du nom de domaine',
domainWord: 'Mot de domaine',
domainName: 'Nom du domaine',
domainSuffix: 'Suffixe du domaine',
domainWord: 'Mot domaine',
ip: 'Ip',
ipv6: 'Ipv6',
userAgent: 'User agent',
userAgent: 'Agent utilisateur',
mac: 'Mac',
password: 'Mot de passe',
word: 'Mot',
words: 'Mots',
sentence: 'Phrase',
slug: 'Slug',
slug: 'Identifiant',
sentences: 'Phrases',
paragraph: 'Paragraphe',
paragraphs: 'Paragraphes',
@@ -310,45 +412,45 @@ module.exports = {
lines: 'Lignes',
genre: 'Genre',
firstName: 'Prénom',
lastName: 'Nom',
lastName: 'Nom de famille',
middleName: 'Deuxième prénom',
findName: 'Nom et prénom',
jobTitle: 'Intitulé du poste',
gender: 'Genre',
findName: 'Nom complet',
jobTitle: 'Nom du poste',
gender: 'Sexe',
prefix: 'Préfixe',
suffix: 'Suffixe',
title: 'Titre',
jobDescriptor: 'Descripteur de poste',
jobArea: 'Domaine d\'activité',
jobType: 'Type de poste',
title: 'Fonction',
jobDescriptor: 'Desciption du travail',
jobArea: 'Zone du travail',
jobType: 'Type de travail',
phoneNumber: 'Numéro de téléphone',
phoneNumberFormat: 'Format du numéro de téléphone',
phoneFormats: 'Formats de téléphone',
number: 'Numéro',
float: 'Nombre décimaux',
arrayElement: 'Élément de Liste',
arrayElements: 'Éléments de liste',
objectElement: 'Élément d\'objet',
phoneNumberFormat: 'Format du Numéro de téléphone',
phoneFormats: 'Formats téléphone',
number: 'Nombre',
float: 'décimal',
arrayElement: 'élément du tableau',
arrayElements: 'éléments du tableau',
objectElement: 'élément de l\'object',
uuid: 'Uuid',
boolean: 'Boolean',
boolean: 'Booléen',
image: 'Image',
locale: 'Locale',
alpha: 'Alpha',
alphaNumeric: 'Alphanumerique',
hexaDecimal: 'Hexadecimale',
fileName: 'Nom deu fichier',
hexaDecimal: 'Hexadécimal',
fileName: 'Nom du fichier',
commonFileName: 'Nom de fichier commun',
mimeType: 'Mime type',
commonFileType: 'Type de dossier commun',
mimeType: 'Type Mime',
commonFileType: 'Type de fichier commun',
commonFileExt: 'Extension de fichier commun',
fileType: 'Type de fichier',
fileExt: 'Extension de fichier',
directoryPath: 'Chemin du répertoire',
filePath: 'Chemin d\'accès au fichier',
filePath: 'Chemin du fichier',
semver: 'Semver',
manufacturer: 'Fabricant',
manufacturer: 'Constructeur',
model: 'Modèle',
fuel: 'Carburant',
vin: 'Vin'
fuel: 'Essence',
vin: 'Plaque d\'imatriculation'
}
};

View File

@@ -13,7 +13,8 @@ const i18n = new VueI18n({
'pt-BR': require('./pt-BR'),
'de-DE': require('./de-DE'),
'vi-VN': require('./vi-VN'),
'ja-JP': require('./ja-JP')
'ja-JP': require('./ja-JP'),
'zh-CN': require('./zh-CN')
}
});
export default i18n;

View File

@@ -80,6 +80,7 @@ module.exports = {
deterministic: 'Deterministico',
context: 'Contesto',
export: 'Esporta',
import: 'Importa',
returns: 'Ritorna',
timing: 'Temporizzazione',
state: 'Stato',
@@ -122,6 +123,16 @@ module.exports = {
select: 'Seleziona',
passphrase: 'Passphrase',
filter: 'Filtra',
change: 'Cambia',
views: 'Viste',
triggers: 'Trigger',
routines: 'Routine',
functions: 'Function',
schedulers: 'Scheduler',
includes: 'Includi',
drop: 'Drop',
completed: 'Completato',
aborted: 'Annullato',
disabled: 'Disabilitato',
enable: 'Abilita',
disable: 'Disabilita'
@@ -237,6 +248,15 @@ module.exports = {
noOpenTabs: 'Non ci sono tab aperte, naviga nella barra sinistra o:',
noSchema: 'Nessuno schema',
restorePreviourSession: 'Ripristina sessione precedente',
exportSchema: 'Esporta schema',
importSchema: 'Importa schema',
directoryPath: 'Percorso directory',
newInserStmtEvery: 'Nuova istruzione INSERT ogni',
processingTableExport: 'Processo {table}',
fechingTableExport: 'Ricavo i dati {table}',
writingTableExport: 'Scrittura dati {table}',
checkAllTables: 'Seleziona tutte le tabelle',
uncheckAllTables: 'Deseleziona tutte le tabelle',
runQuery: 'Esegui query',
thereAreNoTableFields: 'Non ci sono campi della tabella',
newTable: 'Nuova tabella',
@@ -251,7 +271,9 @@ module.exports = {
killProcess: 'Uccidi processo',
closeTab: 'Chiudi tab',
goToDownloadPage: 'Vai alla pagina di download',
readOnlyMode: 'Modalità sola lettura'
readOnlyMode: 'Modalità sola lettura',
importQueryErrors: 'Attenzione: si è verificato un errore | Attenzione si sono verificati {n} errori',
executedQueries: '{n} query eseguite | {n} query eseguite'
},
faker: {
address: 'Indirizzo',

View File

@@ -106,7 +106,39 @@ module.exports = {
array: 'Array',
changelog: 'Logs de alteração',
format: 'Formato',
sshTunnel: 'SSH túnel'
sshTunnel: 'SSH túnel',
structure: 'Estrutura',
small: 'Pequeno',
medium: 'Médio',
large: 'Grande',
row: 'Linha | Linhas',
cell: 'Celula | Células',
triggerFunction: 'Gatinho de função | Gatilhos de Funções',
all: 'Todos',
duplicate: 'Duplicado',
routine: 'Rotina',
new: 'Novo',
history: 'Histórico',
select: 'Seleciomar',
passphrase: 'Palavara-Passe',
filter: 'Filtrar',
change: 'Alterar',
views: 'Visualizações',
triggers: 'Gatilhos',
routines: 'Rotinas',
functions: 'Funções',
schedulers: 'Agendadores',
includes: 'Includes',
drop: 'Drop',
completed: 'Completo',
aborted: 'Abortado',
disabled: 'Inativo',
enable: 'Ativo',
disable: 'Disable',
commit: 'Enviar',
rollback: 'Reverter',
connectionString: 'String da conexão',
contributors: 'Contribuintes'
},
message: {
appWelcome: 'Bem vindo ao Antares SQL Client!',
@@ -212,7 +244,48 @@ module.exports = {
deleteSchema: 'Apagar schema',
markdownSupported: 'Markdown suportado',
plantATree: 'Plante uma árvore',
enableSsh: 'Habilitar SSH'
enableSsh: 'Habilitar SSH',
pageNumber: 'Número de página',
duplicateTable: 'Duplicar tabela',
noOpenTabs: 'Nenhuma aba aberta, navege na barra lateral ou:',
noSchema: 'Nenhum banco de dados',
restorePreviourSession: 'Restaurar sessão anterior',
runQuery: 'Executar Query',
thereAreNoTableFields: 'Nenhum campo na tabela',
newTable: 'Nova tabela',
newView: 'Nova vista',
newTrigger: 'Novo gatilho',
newRoutine: 'Nova rotina',
newFunction: 'Nova função',
newScheduler: 'Novo agendento',
newTriggerFunction: 'Novo gatinho de função',
thereIsNoQueriesYet: 'Nenhuma consulta ainda',
searchForQueries: 'Pesquisar por consultas',
killProcess: 'Matar processo',
closeTab: 'Fechar aba',
exportSchema: 'Exportar banco de dados',
importSchema: 'Importar banco de dados',
directoryPath: 'Caminho da pasta',
newInserStmtEvery: 'Nova query INSERT',
processingTableExport: 'Processando {table}',
fechingTableExport: 'Carregando dados da tabela {table}',
writingTableExport: 'Escrevendo dados na tabela {table}',
checkAllTables: 'Marcar todas as tabelas',
uncheckAllTables: 'Desmarcar todas as tabelas',
goToDownloadPage: 'Ir a página de download',
readOnlyMode: 'Modo somente leitura',
killQuery: 'Matar consulta',
insertRow: 'Inserir linha | Inserir linhas',
commitMode: 'Modo de confirmação',
autoCommit: 'Auto confirmar',
manualCommit: 'Confirmar manualmente',
actionSuccessful: '{action} teve sucesso',
importQueryErrors: 'Aviso: ocorreu {n} erro | Aviso: ocorreram {n} erros',
executedQueries: '{n} consulta executada | {n} consultas executadas',
ourputFormat: 'Formato da saída',
singleFile: 'Arquivo {ext} único',
zipCompressedFile: 'Arquivo compactado {ext} ZIP',
disableBlur: 'Desabilitar Blur'
},
faker: {
address: 'Endereço',

View File

@@ -7,5 +7,6 @@ export default {
'pt-BR': 'Português (Brasil)',
'de-DE': 'Deutsch (Deutschland)',
'vi-VN': 'Tiếng Việt',
'ja-JP': '日本語'
'ja-JP': '日本語',
'zh-CN': '简体中文'
};

421
src/renderer/i18n/zh-CN.js Normal file
View File

@@ -0,0 +1,421 @@
module.exports = {
word: {
edit: '编辑',
save: '保存',
close: '关闭',
delete: '删除',
confirm: '确定',
cancel: '取消',
send: '发送',
connectionName: '连接名称',
client: 'Client',
hostName: '主机名',
port: '端口',
user: '用户',
password: '密码',
credentials: '凭据',
connect: '连接',
connected: '已连接',
disconnect: '断开连接',
disconnected: '已断开',
refresh: '刷新',
settings: '设置',
general: '一般',
themes: '主题',
update: '更新',
about: '关于',
language: '语言',
version: '版本',
donate: '捐赠',
run: '运行',
schema: 'schema',
results: '结果',
size: '尺寸',
seconds: '秒',
type: '类型',
mimeType: 'MIME类型',
download: '下载',
add: '新增',
data: '数据',
properties: '属性',
insert: '插入',
connecting: '连接中',
name: '名称',
collation: '排序规则',
clear: '清除',
options: '选项',
autoRefresh: '自动刷新',
indexes: '索引',
foreignKeys: '外键',
length: '长度',
unsigned: '无符号',
default: '默认',
comment: '注释',
key: '键',
order: 'Order',
expression: '表达式',
autoIncrement: '自动增量',
engine: 'Engine',
field: '字段',
approximately: '大约',
total: '总计',
table: '表',
discard: '弃置',
stay: '等待',
author: '作者',
light: 'Light',
dark: 'Dark',
autoCompletion: '自动完成',
application: '应用程序',
editor: '编辑器',
view: '视图',
definer: '定义者',
algorithm: 'Algorithm',
trigger: '触发器',
storedRoutine: '存储例程',
scheduler: '调度器',
event: '事件',
parameters: '参数',
function: '函数',
deterministic: 'Deterministic',
context: '上下文',
export: '导出',
returns: '返回',
timing: '定时器',
state: '状态',
execution: '执行',
starts: '开始',
ends: '结束',
ssl: 'SSL',
privateKey: '私钥',
certificate: '证书',
caCertificate: 'CA 证书',
ciphers: 'Ciphers',
upload: '上传',
browse: '浏览',
faker: 'Faker',
content: '内容',
cut: '剪切',
copy: '复制',
paste: '粘贴',
tools: '工具',
variables: '变量',
processes: '进程',
database: '数据库',
scratchpad: 'Scratchpad',
array: '数组',
changelog: '更改日志',
format: '格式',
sshTunnel: 'SSH 隧道',
structure: '结构',
small: '小',
medium: '中',
large: '大',
row: '行',
cell: '单元格',
triggerFunction: '触发函数',
all: '全部',
duplicate: '重复',
routine: '例程',
new: 'New',
history: '历史记录',
select: '选择',
passphrase: '密码',
filter: '过滤器',
disabled: '禁用',
enable: '启用',
disable: '是否禁用'
},
message: {
appWelcome: '欢迎来到Antares SQL Client!',
appFirstStep: '你的第一步: 创建一个新的数据库连接.',
addConnection: '添加连接',
createConnection: '创建连接',
createNewConnection: '创建新的连接',
askCredentials: '询问凭证',
testConnection: '测试连接',
editConnection: '编辑连接',
deleteConnection: '删除连接',
deleteCorfirm: '您是否确认取消',
connectionSuccessfullyMade: '连接成功建立!',
madeWithJS: '用💛和JavaScript制造!',
checkForUpdates: '检查更新',
noUpdatesAvailable: '没有可用的更新',
checkingForUpdate: '正在检查更新',
checkFailure: '检查失败,请稍后再试',
updateAvailable: '可用的更新',
downloadingUpdate: '正在下载更新',
updateDownloaded: '更新已下载',
restartToInstall: '重启Antares完成更新',
unableEditFieldWithoutPrimary: '无法编辑一个在结果集中没有主键的字段',
editCell: '编辑单元格',
deleteRows: '删除行 | 删除{count}行',
confirmToDeleteRows: '你是否确认要删除一行? | 您是否确认要删除{count}行?',
notificationsTimeout: '通知超时',
uploadFile: '上传文件',
addNewRow: '添加新行',
numberOfInserts: '插入的数量',
openNewTab: '打开一个新标签',
affectedRows: '受影响的行',
createNewDatabase: '创建新的数据库',
databaseName: '数据库名称',
serverDefault: '默认服务器',
deleteDatabase: '删除数据库',
editDatabase: '编辑数据库',
clearChanges: '清除变化',
addNewField: '添加新字段',
manageIndexes: '管理索引',
manageForeignKeys: '管理外键',
allowNull: '允许NULL',
zeroFill: '填充零',
customValue: '自定义值',
onUpdate: '在更新时',
deleteField: '删除字段',
createNewIndex: '创建新的索引',
addToIndex: '添加到索引',
createNewTable: '创建新表',
emptyTable: '清空表',
deleteTable: '删除表',
emptyCorfirm: '你是否确认清空',
unsavedChanges: '未保存的更改',
discardUnsavedChanges: '你有一些未保存的修改。关闭这个标签,这些变化将被丢弃.',
thereAreNoIndexes: '没有索引',
thereAreNoForeign: '没有外键',
createNewForeign: '创建新的外键',
referenceTable: '参考表',
referenceField: '参考字段',
foreignFields: '外键字段',
invalidDefault: '无效的默认值',
onDelete: '在删除时',
applicationTheme: '应用主题',
editorTheme: '编辑器主题',
wrapLongLines: '超出换行显示',
selectStatement: '选择语句',
triggerStatement: '触发器语句',
sqlSecurity: 'SQL安全',
updateOption: '更新选项',
deleteView: '删除视图',
createNewView: '创建新视图',
deleteTrigger: '删除触发器',
createNewTrigger: '创建新的触发器',
currentUser: '当前用户',
routineBody: '例程主体',
dataAccess: '数据访问',
thereAreNoParameters: '没有参数',
createNewParameter: '创建新参数',
createNewRoutine: '创建新的例程',
deleteRoutine: '删除例程',
functionBody: '函数体',
createNewFunction: '创建新函数',
deleteFunction: '删除函数',
schedulerBody: '调度器主体',
createNewScheduler: '创建新的调度器',
deleteScheduler: '删除调度器',
preserveOnCompletion: '完成时保存',
enableSsl: '启用SSL',
manualValue: '手动值',
tableFiller: '表填充器',
fakeDataLanguage: '伪造的数据语言',
searchForElements: '搜索元素',
selectAll: '选择所有',
queryDuration: '查询时间',
includeBetaUpdates: '包括测试版更新',
setNull: '设置NULL',
processesList: '进程列表',
processInfo: '进程信息',
manageUsers: '管理用户',
createNewSchema: '创建新模式',
schemaName: '模式名称',
editSchema: '编辑模式',
deleteSchema: '删除模式',
markdownSupported: '支持Markdown',
plantATree: '种植一棵树',
dataTabPageSize: '数据标签的页面大小',
enableSsh: '启用SSH',
pageNumber: '页数',
duplicateTable: '重复的表格',
noOpenTabs: '没有打开的标签,在左栏导航或:',
noSchema: '没有模式',
restorePreviourSession: '恢复以前的会话',
runQuery: '运行查询',
thereAreNoTableFields: '没有表的字段',
newTable: '新表',
newView: '新视图',
newTrigger: '新触发器',
newRoutine: '新例程',
newFunction: '新函数',
newScheduler: '新调度器',
newTriggerFunction: '新触发函数',
thereIsNoQueriesYet: '还没有查询',
searchForQueries: '搜索查询',
killProcess: '杀死进程',
closeTab: '关闭标签',
goToDownloadPage: '跳转到下载页面',
readOnlyMode: '只读模式',
killQuery: '停止查询'
},
faker: {
address: '地址',
commerce: '商业',
company: '公司',
database: '数据库',
date: '日期',
finance: '财务',
git: 'Git',
hacker: '黑客',
internet: '互联网',
lorem: 'Lorem',
name: '姓名',
music: '音乐',
phone: '电话',
random: '随机',
system: '系统',
time: '时间',
vehicle: '车辆',
zipCode: '邮政编码',
zipCodeByState: '按州的邮编',
city: '城市',
cityPrefix: '城市前缀',
citySuffix: '城市后缀',
streetName: '街道名称',
streetAddress: '街道地址',
streetSuffix: '街道前缀',
streetPrefix: '街道后缀',
secondaryAddress: '次要地址',
county: '县',
country: '国家',
countryCode: '国家代码',
state: '州',
stateAbbr: '州的缩写',
latitude: '纬度',
longitude: '经度',
direction: '方向',
cardinalDirection: 'Cardinal direction',
ordinalDirection: 'Ordinal direction',
nearbyGPSCoordinate: '附近的GPS坐标',
timeZone: '时区',
color: '颜色',
department: '部门',
productName: '产品名称',
price: '价格',
productAdjective: '产品形容词',
productMaterial: '产品材料',
product: '产品',
productDescription: '产品描述',
suffixes: '后缀',
companyName: '公司名称',
companySuffix: '公司后缀',
catchPhrase: 'Catch phrase',
bs: 'BS',
catchPhraseAdjective: 'Catch phrase adjective',
catchPhraseDescriptor: 'Catch phrase descriptor',
catchPhraseNoun: 'Catch phrase noun',
bsAdjective: 'BS adjective',
bsBuzz: 'BS buzz',
bsNoun: 'BS noun',
column: '列',
type: '类型',
collation: '校对',
engine: 'Engine',
past: '过去',
future: '未来',
between: '之间',
recent: '最近',
soon: '很快',
month: '月',
weekday: '工作日',
account: '账户',
accountName: '账户名称',
routingNumber: '路由号码',
mask: '掩码',
amount: '金额',
transactionType: '交易类型',
currencyCode: '货币代码',
currencyName: '货币名称',
currencySymbol: '货币符号',
bitcoinAddress: '比特币地址',
litecoinAddress: '莱特币地址',
creditCardNumber: '信用卡号码',
creditCardCVV: '信用卡CVV',
ethereumAddress: '以太坊地址',
iban: 'Iban',
bic: 'Bic',
transactionDescription: '交易描述',
branch: '分支',
commitEntry: '提交条目',
commitMessage: '提交信息',
commitSha: '提交 SHA',
shortSha: 'Short SHA',
abbreviation: '缩写',
adjective: '形容词',
noun: '名词',
verb: '动词',
ingverb: 'Ingverb',
phrase: '短语',
avatar: '头像',
email: '电子邮箱',
exampleEmail: '电子邮件例子',
userName: '用户名',
protocol: '协议',
url: 'Url',
domainName: 'Domin name',
domainSuffix: '域名后缀',
domainWord: 'Domain word',
ip: 'Ip',
ipv6: 'Ipv6',
userAgent: 'User agent',
mac: 'Mac',
password: '密码',
word: 'Word',
words: 'Words',
sentence: '句子',
slug: 'Slug',
sentences: '句子',
paragraph: '段落',
paragraphs: '段落',
text: '文本',
lines: '行',
genre: 'Genre',
firstName: '名',
lastName: '姓氏',
middleName: '中间名',
findName: '全名',
jobTitle: '职位名称',
gender: '性别',
prefix: '前缀',
suffix: '后缀',
title: '标题',
jobDescriptor: '工作描述',
jobArea: '工作领域',
jobType: '工作类型',
phoneNumber: '电话号码',
phoneNumberFormat: '电话号码格式',
phoneFormats: '电话格式',
number: 'Number',
float: 'Float',
arrayElement: '数组元素',
arrayElements: '数组元素',
objectElement: '对象元素',
uuid: 'Uuid',
boolean: 'Boolean',
image: 'Image',
locale: 'Locale',
alpha: 'Alpha',
alphaNumeric: 'Alphanumeric',
hexaDecimal: 'Hexadecimal',
fileName: '文件名',
commonFileName: '普通文件名',
mimeType: 'MIME类型',
commonFileType: '常见的文件类型',
commonFileExt: '常见的文件扩展名',
fileType: '文件类型',
fileExt: '文件扩展名',
directoryPath: '目录路径',
filePath: '文件路径',
semver: 'Semver',
manufacturer: '制造商',
model: '型号',
fuel: 'Fuel',
vin: 'Vin'
}
};

View File

@@ -3,6 +3,7 @@
import Vue from 'vue';
import '@mdi/font/css/materialdesignicons.css';
import 'leaflet/dist/leaflet.css';
import '@/scss/main.scss';
import App from '@/App.vue';

View File

@@ -5,4 +5,12 @@ export default class {
static getKey (params) {
return ipcRenderer.sendSync('get-key', params);
}
static showOpenDialog (options) {
return ipcRenderer.invoke('showOpenDialog', options);
}
static getDownloadPathDirectory () {
return ipcRenderer.invoke('get-download-dir-path');
}
}

View File

@@ -1,17 +1,21 @@
'use strict';
import { ipcRenderer } from 'electron';
import connStringConstruct from '../libs/connStringDecode';
export default class {
static makeTest (params) {
params = connStringConstruct(params);
return ipcRenderer.invoke('test-connection', params);
}
static checkConnection (params) {
return ipcRenderer.invoke('check-connection', params);
static connect (params) {
params = connStringConstruct(params);
return ipcRenderer.invoke('connect', params);
}
static connect (params) {
return ipcRenderer.invoke('connect', params);
static checkConnection (uid) {
return ipcRenderer.invoke('check-connection', uid);
}
static disconnect (uid) {

View File

@@ -50,6 +50,18 @@ export default class {
return ipcRenderer.invoke('kill-tab-query', params);
}
static commitTab (params) {
return ipcRenderer.invoke('commit-tab', params);
}
static rollbackTab (params) {
return ipcRenderer.invoke('rollback-tab', params);
}
static destroyConnectionToCommit (params) {
return ipcRenderer.invoke('destroy-connection-to-commit', params);
}
static useSchema (params) {
return ipcRenderer.invoke('use-schema', params);
}
@@ -57,4 +69,20 @@ export default class {
static rawQuery (params) {
return ipcRenderer.invoke('raw-query', params);
}
static export (params) {
return ipcRenderer.invoke('export', params);
}
static abortExport () {
return ipcRenderer.invoke('abort-export');
}
static import (params) {
return ipcRenderer.invoke('import-sql', params);
}
static abortImport () {
return ipcRenderer.invoke('abort-import-sql');
}
}

View File

@@ -0,0 +1,48 @@
import formatter from 'pg-connection-string'; // parses a connection string
const formatHost = host => {
const results = host === 'localhost' ? '127.0.0.1' : host;
return results;
};
const checkForSSl = conn => {
return conn.includes('ssl=true');
};
const connStringConstruct = (args) => {
if (!args.pgConnString)
return args;
if (typeof args.pgConnString !== 'string')
return args;
const stringArgs = formatter.parse(args.pgConnString);
const client = args.client || 'pg';
args.client = client;
args.host = formatHost(stringArgs.host);
args.database = stringArgs.database;
args.port = stringArgs.port || '5432';
args.user = stringArgs.user;
args.password = stringArgs.password;
// ssh
args.ssh = stringArgs.ssh || args.ssh;
args.sshHost = stringArgs.sshHost;
args.sshUser = stringArgs.sshUser;
args.sshPass = stringArgs.sshPass;
args.sshKey = stringArgs.sshKey;
args.sshPort = stringArgs.sshPort;
// ssl mode
args.ssl = checkForSSl(args.pgConnString);
args.cert = stringArgs.sslcert;
args.key = stringArgs.sslkey;
args.ca = stringArgs.sslrootcert;
args.ciphers = stringArgs.ciphers;
return args;
};
export default connStringConstruct;

View File

@@ -81,6 +81,15 @@
"tsvector": $array-color,
"tsquery": $array-color,
"pg_node_tree": $array-color,
"point": $array-color,
"linestring": $array-color,
"polygon": $array-color,
"geometry": $array-color,
"multipoint": $array-color,
"multilinestring": $array-color,
"multipolygon": $array-color,
"geomcollection": $array-color,
"geometrycollection": $array-color,
"aclitem": $array-color,
"unknown": $unknown-color,
)

View File

@@ -165,6 +165,18 @@ option:checked {
}
}
}
.modal-overlay{
background: rgba( 255, 255, 255, 0.1);
box-shadow: 0 8px 32px 0 rgba( 31, 38, 135, 0.37 );
}
}
#wrapper:not(.no-blur){
.modal-overlay{
backdrop-filter: blur( 4px );
-webkit-backdrop-filter: blur( 4px );
}
}
.tab {

View File

@@ -6,7 +6,7 @@ export default {
namespaced: true,
strict: true,
state: {
notes: persistentStore.get('notes', '# HOW TO SUPPORT ANTARES\n\n- [ ] Leave a star to Antares [GitHub repo](https://github.com/Fabio286/antares)\n- [ ] Send feedbacks and advices\n- [ ] Report for bugs\n- [ ] If you enjoy, share Antares with friends\n\n# ABOUT SCRATCHPAD\n\nThis is a scratchpad where you can save your **personal notes**. It supports `markdown` format, but you are free to use plain text.\nThis content is just a placeholder, feel free to clear it to make space for your notes.\n')
notes: persistentStore.get('notes', '# HOW TO SUPPORT ANTARES\n\n- [ ] Leave a star to Antares [GitHub repo](https://github.com/antares-sql/antares)\n- [ ] Send feedbacks and advices\n- [ ] Report for bugs\n- [ ] If you enjoy, share Antares with friends\n\n# ABOUT SCRATCHPAD\n\nThis is a scratchpad where you can save your **personal notes**. It supports `markdown` format, but you are free to use plain text.\nThis content is just a placeholder, feel free to clear it to make space for your notes.\n')
},
getters: {
getNotes: state => state.notes

View File

@@ -20,7 +20,8 @@ export default {
application_theme: persistentStore.get('application_theme', defaultAppTheme),
editor_theme: persistentStore.get('editor_theme', defaultEditorTheme),
editor_font_size: persistentStore.get('editor_font_size', 'medium'),
restore_tabs: persistentStore.get('restore_tabs', true)
restore_tabs: persistentStore.get('restore_tabs', true),
disable_blur: persistentStore.get('disable_blur', false)
},
getters: {
getLocale: state => state.locale,
@@ -33,7 +34,8 @@ export default {
getApplicationTheme: state => state.application_theme,
getEditorTheme: state => state.editor_theme,
getEditorFontSize: state => state.editor_font_size,
getRestoreTabs: state => state.restore_tabs
getRestoreTabs: state => state.restore_tabs,
getDisableBlur: state => state.disable_blur
},
mutations: {
SET_LOCALE (state, locale) {
@@ -80,6 +82,10 @@ export default {
SET_RESTORE_TABS (state, val) {
state.restore_tabs = val;
persistentStore.set('restore_tabs', state.restore_tabs);
},
SET_DISABLE_BLUR (state, val) {
state.disable_blur = val;
persistentStore.set('disable_blur', state.disable_blur);
}
},
actions: {
@@ -115,6 +121,9 @@ export default {
},
changeRestoreTabs ({ commit }, size) {
commit('SET_RESTORE_TABS', size);
},
changeDisableBlur ({ commit }, val) {
commit('SET_DISABLE_BLUR', val);
}
}
};

View File

@@ -12,7 +12,6 @@ const { strict: assert } = require('assert');
*/
const windowState = await electronApp.evaluate(({ BrowserWindow }) => {
const mainWindow = BrowserWindow.getAllWindows()[0];
const getState = () => ({
isVisible: mainWindow.isVisible(),
isDevToolsOpened: mainWindow.webContents.isDevToolsOpened()

View File

@@ -1,70 +1,72 @@
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const webpack = require('webpack');
const ProgressPlugin = require('progress-webpack-plugin');
const { dependencies, devDependencies } = require('./package.json');
const { dependencies, devDependencies, version } = require('./package.json');
const externals = Object.keys(dependencies).concat(Object.keys(devDependencies));
const isDevMode = process.env.NODE_ENV === 'development';
const whiteListedModules = [];
module.exports = [
{ // Main
name: 'main',
mode: process.env.NODE_ENV,
devtool: isDevMode ? 'eval-source-map' : false,
entry: {
main: path.join(__dirname, './src/main/main.js')
module.exports = { // Main
name: 'main',
mode: process.env.NODE_ENV,
devtool: isDevMode ? 'eval-source-map' : false,
entry: {
main: path.join(__dirname, './src/main/main.js')
},
target: 'electron-main',
output: {
libraryTarget: 'commonjs2',
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
node: {
global: true,
__dirname: isDevMode,
__filename: isDevMode
},
externals: externals.filter((d) => !whiteListedModules.includes(d)),
resolve: {
extensions: ['.js', '.json'],
alias: {
src: path.join(__dirname, 'src/'),
common: path.resolve(__dirname, 'src/common')
},
target: 'electron-main',
output: {
libraryTarget: 'commonjs2',
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
node: {
global: true,
__dirname: isDevMode,
__filename: isDevMode
},
externals: externals.filter((d) => !whiteListedModules.includes(d)),
resolve: {
extensions: ['.js', '.json'],
alias: {
src: path.join(__dirname, 'src/'),
common: path.resolve(__dirname, 'src/common')
},
fallback: {
'pg-native': false,
'cpu-features': false,
cardinal: false
}
},
plugins: [
new ProgressPlugin(true),
new CleanWebpackPlugin({ root: path.join(__dirname, 'dist') })
],
module: {
rules: [
{
test: /\.node$/,
loader: 'node-loader',
options: {
name: '[path][name].[ext]'
}
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
},
{
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'file-loader'
}]
}
]
fallback: {
'pg-native': false,
'cpu-features': false,
cardinal: false
}
},
plugins: [
new ProgressPlugin(true),
new webpack.DefinePlugin({
'process.env': {
PACKAGE_VERSION: `"${version}"`
}
})
],
module: {
rules: [
{
test: /\.node$/,
loader: 'node-loader',
options: {
name: '[path][name].[ext]'
}
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
},
{
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'file-loader'
}]
}
]
}
];
};

View File

@@ -1,3 +1,4 @@
const fs = require('fs');
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
@@ -6,6 +7,11 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ProgressPlugin = require('progress-webpack-plugin');
const { dependencies, devDependencies, version } = require('./package.json');
const { contributors } = JSON.parse(fs.readFileSync('./.all-contributorsrc', 'utf-8'));
const parsedContributors = contributors.reduce((acc, c) => {
acc.push(c.name);
return acc;
}, []).join(',');
const externals = Object.keys(dependencies).concat(Object.keys(devDependencies));
const isDevMode = process.env.NODE_ENV === 'development';
@@ -64,7 +70,8 @@ const config = {
new VueLoaderPlugin(),
new webpack.DefinePlugin({
'process.env': {
PACKAGE_VERSION: `"${version}"`
PACKAGE_VERSION: `"${version}"`,
APP_CONTRIBUTORS: `"${parsedContributors}"`
}
})
],

81
webpack.workers.config.js Normal file
View File

@@ -0,0 +1,81 @@
const path = require('path');
const webpack = require('webpack');
const ProgressPlugin = require('progress-webpack-plugin');
const { dependencies, devDependencies, version } = require('./package.json');
const externals = Object.keys(dependencies).concat(Object.keys(devDependencies));
const isDevMode = process.env.NODE_ENV === 'development';
const whiteListedModules = [];
const config = {
name: 'workers',
mode: process.env.NODE_ENV,
devtool: isDevMode ? 'eval-source-map' : false,
entry: {
exporter: path.join(__dirname, './src/main/workers/exporter.js'),
importer: path.join(__dirname, './src/main/workers/importer.js')
},
target: 'node',
output: {
libraryTarget: 'commonjs2',
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
node: {
global: true,
__dirname: isDevMode,
__filename: isDevMode
},
externals: externals.filter((d) => !whiteListedModules.includes(d)),
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.node$/,
use: 'node-loader'
}
]
},
resolve: {
extensions: ['.js', '.json'],
alias: {
src: path.join(__dirname, 'src/'),
common: path.resolve(__dirname, 'src/common')
},
fallback: {
'pg-native': false,
'cpu-features': false,
cardinal: false
}
},
plugins: [
new ProgressPlugin(true),
new webpack.DefinePlugin({
'process.env': {
PACKAGE_VERSION: `"${version}"`
}
})
]
};
/**
* Adjust rendererConfig for production settings
*/
if (isDevMode) {
// any dev only config
config.plugins.push(new webpack.HotModuleReplacementPlugin());
}
else {
config.plugins.push(
new webpack.LoaderOptionsPlugin({
minimize: true
})
);
}
module.exports = config;