├── .clog.toml ├── .github └── workflows │ ├── build_release_static.yml │ └── rust.yml ├── .gitignore ├── .vscode ├── launch.json ├── quick.code-snippets └── settings.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Pipfile ├── README.md ├── _config.yml ├── assets ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.svg ├── index.html ├── oauth2-redirect.html ├── swagger-ui-bundle.js ├── swagger-ui-bundle.js.map ├── swagger-ui-es-bundle-core.js ├── swagger-ui-es-bundle-core.js.map ├── swagger-ui-es-bundle.js ├── swagger-ui-es-bundle.js.map ├── swagger-ui-standalone-preset.js ├── swagger-ui-standalone-preset.js.map ├── swagger-ui.css ├── swagger-ui.css.map ├── swagger-ui.js └── swagger-ui.js.map ├── assets_old ├── 404 ├── axios.min.js ├── bootstrap.min.css ├── bootstrap.min.js ├── gridjs.min.css ├── gridjs.min.js ├── index.html ├── loader.svg ├── old.html ├── script.js └── style.css ├── build.py ├── changelog.md ├── docs └── assets │ └── swagger_demo.png ├── i18n.toml ├── i18n ├── en-US │ └── dcli.ftl └── zh-CN │ └── dcli.ftl ├── plan.toml └── src ├── cli ├── http │ └── mod.rs ├── mod.rs └── shell │ ├── helper.rs │ ├── highlight.rs │ └── mod.rs ├── config.rs ├── main.rs ├── mysql ├── constants.rs └── mod.rs ├── output.rs ├── query.rs └── utils.rs /.clog.toml: -------------------------------------------------------------------------------- 1 | [clog] 2 | repository = "https://github.com/PrivateRookie/dcli" 3 | subtitle = "a database connection manage tool" 4 | link-style = "github" 5 | 6 | changelog = "changelog.md" 7 | -------------------------------------------------------------------------------- /.github/workflows/build_release_static.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Build Static Release 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Allows you to run this workflow manually from the Actions tab 8 | workflow_dispatch: 9 | 10 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 11 | jobs: 12 | # This workflow contains a single job called "build" 13 | build: 14 | # The type of runner that the job will run on 15 | runs-on: ubuntu-latest 16 | 17 | # Steps represent a sequence of tasks that will be executed as part of the job 18 | steps: 19 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 20 | - uses: actions/checkout@v2 21 | 22 | - name: cargo-static-build 23 | uses: zhxiaogg/cargo-static-build@v1.41.1-1 24 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | - name: Security and Licence Scan 24 | uses: ShiftLeftSecurity/scan-action@v1.3.0 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | history.txt 4 | *.log 5 | 6 | .venv -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug executable 'dcli'", 11 | "cargo": { 12 | "args": [ 13 | "build", 14 | "--bin=dcli", 15 | "--package=dcli" 16 | ], 17 | "filter": { 18 | "name": "dcli", 19 | "kind": "bin" 20 | } 21 | }, 22 | "args": [], 23 | "cwd": "${workspaceFolder}" 24 | }, 25 | { 26 | "type": "lldb", 27 | "request": "launch", 28 | "name": "Debug unit tests in executable 'dcli'", 29 | "cargo": { 30 | "args": [ 31 | "test", 32 | "--no-run", 33 | "--bin=dcli", 34 | "--package=dcli" 35 | ], 36 | "filter": { 37 | "name": "dcli", 38 | "kind": "bin" 39 | } 40 | }, 41 | "args": [], 42 | "cwd": "${workspaceFolder}" 43 | } 44 | ] 45 | } -------------------------------------------------------------------------------- /.vscode/quick.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | // Place your dcli 工作区 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and 3 | // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope 4 | // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is 5 | // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: 6 | // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. 7 | // Placeholders with the same ids are connected. 8 | // Example: 9 | // "Print to console": { 10 | // "scope": "javascript,typescript", 11 | // "prefix": "log", 12 | // "body": [ 13 | // "console.log('$1');", 14 | // "$2" 15 | // ], 16 | // "description": "Log output to console" 17 | // } 18 | "Doc i18n": { 19 | "prefix": "doc_i18n", 20 | "description": "条件编译文档", 21 | "body": [ 22 | "#[cfg_attr(feature = \"${1}\", doc = \"${2}\")]", 23 | ] 24 | }, 25 | "Doc en": { 26 | "prefix": "doc_en", 27 | "description": "英文文档", 28 | "body": [ 29 | "#[cfg_attr(feature = \"en-US\", doc = \"${1}\")]", 30 | ] 31 | }, 32 | "Doc zh": { 33 | "prefix": "doc_zh", 34 | "description": "中文文档", 35 | "body": [ 36 | "#[cfg_attr(feature = \"zh-CN\", doc = \"${1}\")]", 37 | ] 38 | }, 39 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Appender", 4 | "Subquery", 5 | "VARCHAR", 6 | "bigdecimal", 7 | "chrono", 8 | "ctes", 9 | "dcli", 10 | "favicon", 11 | "logfile", 12 | "openapi", 13 | "readline", 14 | "rustyline", 15 | "sess", 16 | "sqlparser", 17 | "sqlx", 18 | "subcommand" 19 | ] 20 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dcli" 3 | version = "0.0.8" 4 | authors = ["PrivateRookie <996514515@qq.com>"] 5 | edition = "2018" 6 | description = "MySQL 数据库连接管理工具 | MySQL connection manage tool" 7 | license-file = "LICENSE" 8 | readme = "README.md" 9 | homepage = "https://github.com/PrivateRookie/dcli" 10 | repository = "https://github.com/PrivateRookie/dcli" 11 | keywords = ["cli", "database", "mysql"] 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [features] 16 | default = ["en-US"] 17 | en-US = [] 18 | zh-CN = [] 19 | 20 | [dependencies] 21 | structopt = "0.3.20" 22 | serde = { version = "1.0.118", features = ["derive"] } 23 | serde_json = "1.0.60" 24 | serde_yaml = "0.8.14" 25 | serde-pickle = "0.6.2" 26 | toml = "0.5.7" 27 | csv = "1.1" 28 | comfy-table = "2.1.0" 29 | anyhow = "1.0.34" 30 | sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "mysql", "all-types", "bigdecimal"] } 31 | chrono = "0.4.19" 32 | base64 = "0.13.0" 33 | bigdecimal = "0.2.0" 34 | tokio = { version = "1.4.0", features = ["full"] } 35 | log4rs = "1.0.0-alpha-2" 36 | log = "0.4.11" 37 | 38 | sqlparser = "0.6.1" 39 | rustyline = { version = "8.0.0", features = ["with-fuzzy"] } 40 | rustyline-derive = "0.4.0" 41 | colored = "2" 42 | i18n-embed = { version = "0.12.0", features = ["fluent-system", "desktop-requester"] } 43 | rust-embed = "5" 44 | i18n-embed-fl = "0.5.0" 45 | once_cell = "1.5.2" 46 | warp = "0.3.1" 47 | tracing = "0.1.22" 48 | tracing-subscriber = "0.2.15" 49 | openapiv3 = "0.3.2" 50 | indexmap = "1.3" 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.tuna.tsinghua.edu.cn/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | pandas = "*" 10 | ipython = "*" 11 | pyyaml = "*" 12 | 13 | [requires] 14 | python_version = "3.6" 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dcli 2 | 数据库连接工具 3 | 4 | ## 概述 5 | 6 | dcli 是一个简单的数据库管理工具。因为个人习惯喜欢用命令行,在平时工作中经常需要通过 mysql-client 连接到多个 mysql 数据库,每次连接都需要敲一长串参数或在历史记录中查找之前输入参数。我希望有一个可以替我保管多个 mysql 连接信息,在需要时指定连接名称就能连上数据库的工具,dcli 由此而来。 7 | 8 | **注意: dcli 目前还使用明文保存密码!!!** 9 | 10 | ## 特性 11 | 12 | ### 无 mysql-client 和 openssl 依赖 13 | 14 | 不喜欢在换了一台机器后需要安装额外的 mysql-client 依赖, 特别是 SSL 连接使用的 openssl, 有时候安装 openssl 本身就是一个大麻烦。所以 dcli 使用了纯 rust 实现的 mysql 连接工具 sqlx, 而且最近版本的 sqlx 可以通过 `rustls` 特性使用 rustls 替换 native-tls, 所以无需担心 openssl 的依赖问题🎉。 15 | 16 | ### 可调整表格样式 17 | 18 | ### 支持 i18n 19 | 20 | 通过条件编译和 cargo-i18n fluent 支持国际化,详情见[安装](#安装)。 21 | 22 | ### 更智能的 shell 23 | 24 | mysql-client 提供的 shell 有些简陋,dcli 实现了一个基于 token 的高亮显示和关键字,数据库名称,表名和字段名自动补全,和历史搜索 shell。 25 | 26 | ### 与 jupyter backend 交互(计划中) 27 | 28 | "执行 SQL 获取数据" -> "导出到文件" -> "jupyter notebook 导入",这个工作流在工作中非常常见,但为什么要导出到文件呢,jupyter notebook 可以通过 jupyter protocol 与 jupyter 交互,将 shell 中保存的表格直接发送到 backend,完成导入。让你不需要再保存那么多甚至于过期的文件. 29 | 30 | ## 安装 31 | 32 | 从 crate.io 安装 33 | 34 | 因为 clap 未能支持 i18n, 所以需要通过条件编译支持 clap 帮助信息 i18n, 而程序运行时的信息输出则是通过 35 | `$LANG` 自动获取。 36 | 37 | `export LANG=zh_CN.UTF-8` 可以设置为中文,`export LANG=en_US.UTF-8` 则为英文。 38 | 39 | 40 | ```bash 41 | # 默认为英文版本 42 | cargo install --force dcli 43 | # 安装中文版本 44 | cargo install --no-default-features --features zh-CN --force dcli 45 | ``` 46 | 47 | debian 系可以从 github release 页面下载 dep 包, 接着使用 `dpkg` 命令安装 48 | 49 | 50 | ```bash 51 | sudo dpkg -i dcli__amd64.deb 52 | ``` 53 | 54 | ## 使用 55 | 56 | 使用 `dcli --help` 查看所有可用命令 57 | 58 | ```bash 59 | dcli 0.0.1 60 | 数据连接工具. 61 | 62 | USAGE: 63 | dcli 64 | 65 | FLAGS: 66 | -h, --help Prints help information 67 | -V, --version Prints version information 68 | 69 | SUBCOMMANDS: 70 | conn 使用 `mysql` 命令连接到 mysql 71 | exec 使用一个配置运行命令 72 | help Prints this message or the help of the given subcommand(s) 73 | profile 配置相关命令 74 | shell 运行连接到 mysql 的 shell 75 | style 显示样式相关命令 76 | ``` 77 | 78 | ### 添加一个连接配置 79 | 80 | dcli 将配置文件保存在 `~/.config/dcli.toml` 文件中, 一般情况下你不需要手动修改它。 81 | 82 | 最开始需要添加一个 MySQL 连接配置,通过 `dcli profile add <配置名>` 添加,可以通过 `--port` 等参数设置端口等信息。 83 | 84 | dcli 支持 SSL 连接,默认情况下 dcli 不会尝试进行 SSL 连接,如果需要强制使用 SSL, 通过 `--ssl-mode` 设置 SSL 模式,可选项为 "Disabled", "Preferred", "Required", "VerifyCa", "VerifyIdentity"。 85 | 86 | 当使用 "Required" 或更高级别的 SSL mode 时需要通过 `--ssl-ca` 指定证书才能连接成功。 87 | 88 | 89 | ```bash 90 | dcli-profile-add 0.0.1 91 | 添加一个配置 92 | 93 | USAGE: 94 | dcli profile add [FLAGS] [OPTIONS] 95 | 96 | FLAGS: 97 | -f, --force 是否强制覆盖 98 | --help Prints help information 99 | -V, --version Prints version information 100 | 101 | OPTIONS: 102 | -d, --db 数据库名称 103 | -h, --host 数据库 hostname, IPv6地址请使用带'[]'包围 [default: localhost] 104 | -p, --password 密码 105 | -P, --port 数据库 port 0 ~ 65536 [default: 3306] 106 | --ssl-ca SSL CA 文件路径 107 | --ssl-mode SSL 模式 108 | -u, --user 用户名 109 | 110 | ARGS: 111 | 配置名称 112 | ``` 113 | 114 | ### 执行 SQL 115 | 116 | 添加一个配置后我们就可以通过这个配置连接到 MySQL 执行命令。 117 | 118 | 如果你只想执行单个 SQL 语句,那么你可以使用 `exec` 命令 119 | 120 | ```bash 121 | dcli-exec 0.0.1 122 | 使用一个配置运行命令 123 | 124 | USAGE: 125 | dcli exec --profile [command]... 126 | 127 | FLAGS: 128 | -h, --help Prints help information 129 | -V, --version Prints version information 130 | 131 | OPTIONS: 132 | -p, --profile 配置名 133 | 134 | ARGS: 135 | ... 命令 136 | ``` 137 | 138 | 假设我们添加了名为 "dev" 的配置,想查看该数据中的所有表,可以通过以下命令 139 | 140 | ```bash 141 | dcli exec -p dev show tables; 142 | ┌───────────────────┐ 143 | │ Tables_in_default │ 144 | ╞═══════════════════╡ 145 | │ _sqlx_migrations │ 146 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 147 | │ boxercrab │ 148 | ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ 149 | │ todos │ 150 | └───────────────────┘ 151 | ``` 152 | 153 | 输出表格默认为 "utf8full"模式, 可以通过 `dcli style table <样式名>` 配置,可选项为 154 | 155 | AsciiFull AsciiMd Utf8Full Utf8HBorderOnly 156 | 157 | ### 使用默认的 mysql-client 连接到数据库 158 | 159 | 如果你安装了 mysql-client 且希望使用原生的 mysql shell, 可以通过 `dcli conn -p <配置名>` 使用它。 160 | 161 | ### 使用无依赖的 shell(开发中...) 162 | 163 | 如果你不希望使用原生 mysql shell, 且渴望语法高亮等特性,可以尝试使用 `dcli shell <配置名>` 启动一个 dcli 164 | 实现的 shell。这个 shell 不依赖 mysql-client 和 openssl,这意味着你不需要安装额外的依赖也能连接到 mysql。 165 | 166 | 但 dcli 属于早期阶段,所以很多功能仍然不完整,如有问题请开 ISSUE。 167 | 168 | #### 使用 `dcli plan` 运行一个 http 服务器 169 | 170 | 如果你有多个 SQL 语句需要共享,你可以使用 `plan` 子命令启动一个 http 服务,并将所有 SQL 作为一个 http 接口。 171 | 172 | 首先你需要定义一个 `toml` 文件,可以参考 [plan.toml](./plan.toml) 173 | 174 | ```toml 175 | # http 接口前置路由,对启用代理或 url 冲突时非常有用 176 | prefix = "api" 177 | 178 | [[queries]] 179 | # 设置此 SQL 使用哪个数据库连接配置 180 | profile = "xxx" 181 | # 对应的 SQL 语句 182 | sql = "select * from xxxx" 183 | # 此 SQL 对应的 URL 地址,**不能以 `/` 包围** 184 | url = "some_url" 185 | # 此 SQL 描述 186 | description = "一些有用的描述" 187 | # 非必填,是否分页,如果原 SQL 中最外层含有 `limit` 或 `offset` 则分页不起作用 188 | # 默认开启,可以设置 false 关闭 189 | paging = true 190 | ``` 191 | 192 | 接着运行 `dcli plan plan.toml`,dcli 会在 3030 端口启动 http 服务,打开网页会看到 swagger ui, 按照文档浏览使用即可 193 | 194 | ![index](./docs/assets/swagger_demo.png) 195 | 196 | 197 | ### 设置语言 198 | 199 | 默认情况下 dcli 会尝试读取本地语言设置,自动设置语言. 如果这不和预期, 可以试用 200 | 201 | `dcli style lang` 命令设置语言. 目前支持 `zh-CN` 和 `en-US` 两种语言. 202 | 203 | ### 其他命令 204 | 205 | dcli 使用 structopt 构建命令工具,当你有疑问时可以运行 `dcli help <子命令>` 查看帮助信息。 206 | 207 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /assets/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivateRookie/dcli/7bca3e64d19a31831f9353f16bb3a0362a51a35c/assets/favicon-16x16.png -------------------------------------------------------------------------------- /assets/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivateRookie/dcli/7bca3e64d19a31831f9353f16bb3a0362a51a35c/assets/favicon-32x32.png -------------------------------------------------------------------------------- /assets/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Swagger UI 7 | 8 | 29 | 30 | 31 | 32 |
33 | 34 | 35 | 36 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /assets/oauth2-redirect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Swagger UI: OAuth2 Redirect 5 | 6 | 7 | 8 | 9 | 75 | -------------------------------------------------------------------------------- /assets_old/404: -------------------------------------------------------------------------------- 1 | 404 -------------------------------------------------------------------------------- /assets_old/axios.min.js: -------------------------------------------------------------------------------- 1 | /* axios v0.21.0 | (c) 2020 by Matt Zabriskie */ 2 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.axios=t():e.axios=t()}(this,function(){return function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return e[r].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){e.exports=n(1)},function(e,t,n){"use strict";function r(e){var t=new i(e),n=s(i.prototype.request,t);return o.extend(n,i.prototype,t),o.extend(n,t),n}var o=n(2),s=n(3),i=n(4),a=n(22),u=n(10),c=r(u);c.Axios=i,c.create=function(e){return r(a(c.defaults,e))},c.Cancel=n(23),c.CancelToken=n(24),c.isCancel=n(9),c.all=function(e){return Promise.all(e)},c.spread=n(25),e.exports=c,e.exports.default=c},function(e,t,n){"use strict";function r(e){return"[object Array]"===R.call(e)}function o(e){return"undefined"==typeof e}function s(e){return null!==e&&!o(e)&&null!==e.constructor&&!o(e.constructor)&&"function"==typeof e.constructor.isBuffer&&e.constructor.isBuffer(e)}function i(e){return"[object ArrayBuffer]"===R.call(e)}function a(e){return"undefined"!=typeof FormData&&e instanceof FormData}function u(e){var t;return t="undefined"!=typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&e.buffer instanceof ArrayBuffer}function c(e){return"string"==typeof e}function f(e){return"number"==typeof e}function p(e){return null!==e&&"object"==typeof e}function d(e){if("[object Object]"!==R.call(e))return!1;var t=Object.getPrototypeOf(e);return null===t||t===Object.prototype}function l(e){return"[object Date]"===R.call(e)}function h(e){return"[object File]"===R.call(e)}function m(e){return"[object Blob]"===R.call(e)}function y(e){return"[object Function]"===R.call(e)}function g(e){return p(e)&&y(e.pipe)}function v(e){return"undefined"!=typeof URLSearchParams&&e instanceof URLSearchParams}function x(e){return e.replace(/^\s*/,"").replace(/\s*$/,"")}function w(){return("undefined"==typeof navigator||"ReactNative"!==navigator.product&&"NativeScript"!==navigator.product&&"NS"!==navigator.product)&&("undefined"!=typeof window&&"undefined"!=typeof document)}function b(e,t){if(null!==e&&"undefined"!=typeof e)if("object"!=typeof e&&(e=[e]),r(e))for(var n=0,o=e.length;n=200&&e<300}};u.headers={common:{Accept:"application/json, text/plain, */*"}},s.forEach(["delete","get","head"],function(e){u.headers[e]={}}),s.forEach(["post","put","patch"],function(e){u.headers[e]=s.merge(a)}),e.exports=u},function(e,t,n){"use strict";var r=n(2);e.exports=function(e,t){r.forEach(e,function(n,r){r!==t&&r.toUpperCase()===t.toUpperCase()&&(e[t]=n,delete e[r])})}},function(e,t,n){"use strict";var r=n(2),o=n(13),s=n(16),i=n(5),a=n(17),u=n(20),c=n(21),f=n(14);e.exports=function(e){return new Promise(function(t,n){var p=e.data,d=e.headers;r.isFormData(p)&&delete d["Content-Type"];var l=new XMLHttpRequest;if(e.auth){var h=e.auth.username||"",m=e.auth.password?unescape(encodeURIComponent(e.auth.password)):"";d.Authorization="Basic "+btoa(h+":"+m)}var y=a(e.baseURL,e.url);if(l.open(e.method.toUpperCase(),i(y,e.params,e.paramsSerializer),!0),l.timeout=e.timeout,l.onreadystatechange=function(){if(l&&4===l.readyState&&(0!==l.status||l.responseURL&&0===l.responseURL.indexOf("file:"))){var r="getAllResponseHeaders"in l?u(l.getAllResponseHeaders()):null,s=e.responseType&&"text"!==e.responseType?l.response:l.responseText,i={data:s,status:l.status,statusText:l.statusText,headers:r,config:e,request:l};o(t,n,i),l=null}},l.onabort=function(){l&&(n(f("Request aborted",e,"ECONNABORTED",l)),l=null)},l.onerror=function(){n(f("Network Error",e,null,l)),l=null},l.ontimeout=function(){var t="timeout of "+e.timeout+"ms exceeded";e.timeoutErrorMessage&&(t=e.timeoutErrorMessage),n(f(t,e,"ECONNABORTED",l)),l=null},r.isStandardBrowserEnv()){var g=(e.withCredentials||c(y))&&e.xsrfCookieName?s.read(e.xsrfCookieName):void 0;g&&(d[e.xsrfHeaderName]=g)}if("setRequestHeader"in l&&r.forEach(d,function(e,t){"undefined"==typeof p&&"content-type"===t.toLowerCase()?delete d[t]:l.setRequestHeader(t,e)}),r.isUndefined(e.withCredentials)||(l.withCredentials=!!e.withCredentials),e.responseType)try{l.responseType=e.responseType}catch(t){if("json"!==e.responseType)throw t}"function"==typeof e.onDownloadProgress&&l.addEventListener("progress",e.onDownloadProgress),"function"==typeof e.onUploadProgress&&l.upload&&l.upload.addEventListener("progress",e.onUploadProgress),e.cancelToken&&e.cancelToken.promise.then(function(e){l&&(l.abort(),n(e),l=null)}),p||(p=null),l.send(p)})}},function(e,t,n){"use strict";var r=n(14);e.exports=function(e,t,n){var o=n.config.validateStatus;n.status&&o&&!o(n.status)?t(r("Request failed with status code "+n.status,n.config,null,n.request,n)):e(n)}},function(e,t,n){"use strict";var r=n(15);e.exports=function(e,t,n,o,s){var i=new Error(e);return r(i,t,n,o,s)}},function(e,t){"use strict";e.exports=function(e,t,n,r,o){return e.config=t,n&&(e.code=n),e.request=r,e.response=o,e.isAxiosError=!0,e.toJSON=function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:this.config,code:this.code}},e}},function(e,t,n){"use strict";var r=n(2);e.exports=r.isStandardBrowserEnv()?function(){return{write:function(e,t,n,o,s,i){var a=[];a.push(e+"="+encodeURIComponent(t)),r.isNumber(n)&&a.push("expires="+new Date(n).toGMTString()),r.isString(o)&&a.push("path="+o),r.isString(s)&&a.push("domain="+s),i===!0&&a.push("secure"),document.cookie=a.join("; ")},read:function(e){var t=document.cookie.match(new RegExp("(^|;\\s*)("+e+")=([^;]*)"));return t?decodeURIComponent(t[3]):null},remove:function(e){this.write(e,"",Date.now()-864e5)}}}():function(){return{write:function(){},read:function(){return null},remove:function(){}}}()},function(e,t,n){"use strict";var r=n(18),o=n(19);e.exports=function(e,t){return e&&!r(t)?o(e,t):t}},function(e,t){"use strict";e.exports=function(e){return/^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(e)}},function(e,t){"use strict";e.exports=function(e,t){return t?e.replace(/\/+$/,"")+"/"+t.replace(/^\/+/,""):e}},function(e,t,n){"use strict";var r=n(2),o=["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"];e.exports=function(e){var t,n,s,i={};return e?(r.forEach(e.split("\n"),function(e){if(s=e.indexOf(":"),t=r.trim(e.substr(0,s)).toLowerCase(),n=r.trim(e.substr(s+1)),t){if(i[t]&&o.indexOf(t)>=0)return;"set-cookie"===t?i[t]=(i[t]?i[t]:[]).concat([n]):i[t]=i[t]?i[t]+", "+n:n}}),i):i}},function(e,t,n){"use strict";var r=n(2);e.exports=r.isStandardBrowserEnv()?function(){function e(e){var t=e;return n&&(o.setAttribute("href",t),t=o.href),o.setAttribute("href",t),{href:o.href,protocol:o.protocol?o.protocol.replace(/:$/,""):"",host:o.host,search:o.search?o.search.replace(/^\?/,""):"",hash:o.hash?o.hash.replace(/^#/,""):"",hostname:o.hostname,port:o.port,pathname:"/"===o.pathname.charAt(0)?o.pathname:"/"+o.pathname}}var t,n=/(msie|trident)/i.test(navigator.userAgent),o=document.createElement("a");return t=e(window.location.href),function(n){var o=r.isString(n)?e(n):n;return o.protocol===t.protocol&&o.host===t.host}}():function(){return function(){return!0}}()},function(e,t,n){"use strict";var r=n(2);e.exports=function(e,t){function n(e,t){return r.isPlainObject(e)&&r.isPlainObject(t)?r.merge(e,t):r.isPlainObject(t)?r.merge({},t):r.isArray(t)?t.slice():t}function o(o){r.isUndefined(t[o])?r.isUndefined(e[o])||(s[o]=n(void 0,e[o])):s[o]=n(e[o],t[o])}t=t||{};var s={},i=["url","method","data"],a=["headers","auth","proxy","params"],u=["baseURL","transformRequest","transformResponse","paramsSerializer","timeout","timeoutMessage","withCredentials","adapter","responseType","xsrfCookieName","xsrfHeaderName","onUploadProgress","onDownloadProgress","decompress","maxContentLength","maxBodyLength","maxRedirects","transport","httpAgent","httpsAgent","cancelToken","socketPath","responseEncoding"],c=["validateStatus"];r.forEach(i,function(e){r.isUndefined(t[e])||(s[e]=n(void 0,t[e]))}),r.forEach(a,o),r.forEach(u,function(o){r.isUndefined(t[o])?r.isUndefined(e[o])||(s[o]=n(void 0,e[o])):s[o]=n(void 0,t[o])}),r.forEach(c,function(r){r in t?s[r]=n(e[r],t[r]):r in e&&(s[r]=n(void 0,e[r]))});var f=i.concat(a).concat(u).concat(c),p=Object.keys(e).concat(Object.keys(t)).filter(function(e){return f.indexOf(e)===-1});return r.forEach(p,o),s}},function(e,t){"use strict";function n(e){this.message=e}n.prototype.toString=function(){return"Cancel"+(this.message?": "+this.message:"")},n.prototype.__CANCEL__=!0,e.exports=n},function(e,t,n){"use strict";function r(e){if("function"!=typeof e)throw new TypeError("executor must be a function.");var t;this.promise=new Promise(function(e){t=e});var n=this;e(function(e){n.reason||(n.reason=new o(e),t(n.reason))})}var o=n(23);r.prototype.throwIfRequested=function(){if(this.reason)throw this.reason},r.source=function(){var e,t=new r(function(t){e=t});return{token:t,cancel:e}},e.exports=r},function(e,t){"use strict";e.exports=function(e){return function(t){return e.apply(null,t)}}}])}); 3 | //# sourceMappingURL=axios.min.map -------------------------------------------------------------------------------- /assets_old/gridjs.min.css: -------------------------------------------------------------------------------- 1 | .gridjs-footer button,.gridjs-head button{cursor:pointer;background-color:transparent;background-image:none;padding:0;margin:0;border:none;outline:none}table.gridjs-shadowTable *{margin:0!important;padding:0!important;border:0!important;outline:0!important}.gridjs-head{margin-bottom:5px;padding:5px 1px}.gridjs-head:after{content:"";display:block;clear:both}.gridjs-head:empty{padding:0;border:none}.gridjs-container{overflow:hidden;display:inline-block;padding:2px;color:#000;position:relative;z-index:0}.gridjs-footer{display:block;position:relative;z-index:5;padding:12px 24px;background-color:#fff;box-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px 0 rgba(0,0,0,.26);border-radius:0 0 8px 8px;border-bottom-width:1px;border-color:#e5e7eb;border-top:1px solid #e5e7eb}.gridjs-footer:empty{padding:0;border:none}input.gridjs-input{outline:none;background-color:#fff;border:1px solid #d2d6dc;border-radius:5px;padding:10px 13px;font-size:14px;line-height:1.45;-webkit-appearance:none;-moz-appearance:none;appearance:none}input.gridjs-input:focus{box-shadow:0 0 0 3px rgba(149,189,243,.5);border-color:#9bc2f7}.gridjs-pagination{color:#3d4044}.gridjs-pagination:after{content:"";display:block;clear:both}.gridjs-pagination .gridjs-summary{float:left;margin-top:5px}.gridjs-pagination .gridjs-pages{float:right}.gridjs-pagination .gridjs-pages button{padding:5px 14px;background-color:#fff;border:1px solid #d2d6dc;border-right:none;outline:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.gridjs-pagination .gridjs-pages button:focus{box-shadow:0 0 0 2px rgba(149,189,243,.5)}.gridjs-pagination .gridjs-pages button:hover{background-color:#f7f7f7;color:#3c4257;outline:none}.gridjs-pagination .gridjs-pages button:disabled,.gridjs-pagination .gridjs-pages button:hover:disabled,.gridjs-pagination .gridjs-pages button[disabled]{cursor:default;background-color:#fff;color:#6b7280}.gridjs-pagination .gridjs-pages button.gridjs-spread{cursor:default;box-shadow:none;background-color:#fff}.gridjs-pagination .gridjs-pages button.gridjs-currentPage{background-color:#f7f7f7;font-weight:700}.gridjs-pagination .gridjs-pages button:last-child{border-bottom-right-radius:6px;border-top-right-radius:6px;border-right:1px solid #d2d6dc}.gridjs-pagination .gridjs-pages button:first-child{border-bottom-left-radius:6px;border-top-left-radius:6px}button.gridjs-sort{float:right;height:24px;width:13px;background-color:transparent;background-repeat:no-repeat;background-position-x:center;cursor:pointer;padding:0;margin:0;border:none;outline:none;background-size:contain}button.gridjs-sort-neutral{opacity:.3;background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MDEuOTk4IiBoZWlnaHQ9IjQwMS45OTgiPjxwYXRoIGQ9Ik03My4wOTIgMTY0LjQ1MmgyNTUuODEzYzQuOTQ5IDAgOS4yMzMtMS44MDcgMTIuODQ4LTUuNDI0IDMuNjEzLTMuNjE2IDUuNDI3LTcuODk4IDUuNDI3LTEyLjg0N3MtMS44MTMtOS4yMjktNS40MjctMTIuODVMMjEzLjg0NiA1LjQyNEMyMTAuMjMyIDEuODEyIDIwNS45NTEgMCAyMDAuOTk5IDBzLTkuMjMzIDEuODEyLTEyLjg1IDUuNDI0TDYwLjI0MiAxMzMuMzMxYy0zLjYxNyAzLjYxNy01LjQyNCA3LjkwMS01LjQyNCAxMi44NSAwIDQuOTQ4IDEuODA3IDkuMjMxIDUuNDI0IDEyLjg0NyAzLjYyMSAzLjYxNyA3LjkwMiA1LjQyNCAxMi44NSA1LjQyNHpNMzI4LjkwNSAyMzcuNTQ5SDczLjA5MmMtNC45NTIgMC05LjIzMyAxLjgwOC0xMi44NSA1LjQyMS0zLjYxNyAzLjYxNy01LjQyNCA3Ljg5OC01LjQyNCAxMi44NDdzMS44MDcgOS4yMzMgNS40MjQgMTIuODQ4TDE4OC4xNDkgMzk2LjU3YzMuNjIxIDMuNjE3IDcuOTAyIDUuNDI4IDEyLjg1IDUuNDI4czkuMjMzLTEuODExIDEyLjg0Ny01LjQyOGwxMjcuOTA3LTEyNy45MDZjMy42MTMtMy42MTQgNS40MjctNy44OTggNS40MjctMTIuODQ4IDAtNC45NDgtMS44MTMtOS4yMjktNS40MjctMTIuODQ3LTMuNjE0LTMuNjE2LTcuODk5LTUuNDItMTIuODQ4LTUuNDJ6Ii8+PC9zdmc+");background-position-y:center}button.gridjs-sort-asc{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOTIuMzYyIiBoZWlnaHQ9IjI5Mi4zNjEiPjxwYXRoIGQ9Ik0yODYuOTM1IDE5Ny4yODdMMTU5LjAyOCA2OS4zODFjLTMuNjEzLTMuNjE3LTcuODk1LTUuNDI0LTEyLjg0Ny01LjQyNHMtOS4yMzMgMS44MDctMTIuODUgNS40MjRMNS40MjQgMTk3LjI4N0MxLjgwNyAyMDAuOTA0IDAgMjA1LjE4NiAwIDIxMC4xMzRzMS44MDcgOS4yMzMgNS40MjQgMTIuODQ3YzMuNjIxIDMuNjE3IDcuOTAyIDUuNDI1IDEyLjg1IDUuNDI1aDI1NS44MTNjNC45NDkgMCA5LjIzMy0xLjgwOCAxMi44NDgtNS40MjUgMy42MTMtMy42MTMgNS40MjctNy44OTggNS40MjctMTIuODQ3cy0xLjgxNC05LjIzLTUuNDI3LTEyLjg0N3oiLz48L3N2Zz4=");background-position-y:35%;background-size:10px}button.gridjs-sort-desc{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOTIuMzYyIiBoZWlnaHQ9IjI5Mi4zNjIiPjxwYXRoIGQ9Ik0yODYuOTM1IDY5LjM3N2MtMy42MTQtMy42MTctNy44OTgtNS40MjQtMTIuODQ4LTUuNDI0SDE4LjI3NGMtNC45NTIgMC05LjIzMyAxLjgwNy0xMi44NSA1LjQyNEMxLjgwNyA3Mi45OTggMCA3Ny4yNzkgMCA4Mi4yMjhjMCA0Ljk0OCAxLjgwNyA5LjIyOSA1LjQyNCAxMi44NDdsMTI3LjkwNyAxMjcuOTA3YzMuNjIxIDMuNjE3IDcuOTAyIDUuNDI4IDEyLjg1IDUuNDI4czkuMjMzLTEuODExIDEyLjg0Ny01LjQyOEwyODYuOTM1IDk1LjA3NGMzLjYxMy0zLjYxNyA1LjQyNy03Ljg5OCA1LjQyNy0xMi44NDcgMC00Ljk0OC0xLjgxNC05LjIyOS01LjQyNy0xMi44NXoiLz48L3N2Zz4=");background-position-y:65%;background-size:10px}button.gridjs-sort:focus{outline:none}table.gridjs-table{max-width:100%;border-collapse:collapse;text-align:left;display:table;margin:0;padding:0;overflow:auto;table-layout:fixed}.gridjs-tbody,td.gridjs-td{background-color:#fff}td.gridjs-td{border:1px solid #e5e7eb;padding:12px 24px;box-sizing:content-box}td.gridjs-td:first-child{border-left:none}td.gridjs-td:last-child{border-right:none}td.gridjs-message{text-align:center}th.gridjs-th{color:#6b7280;background-color:#f9fafb;border:1px solid #e5e7eb;border-top:none;padding:14px 24px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;box-sizing:content-box;white-space:nowrap;outline:none;vertical-align:middle}th.gridjs-th-sort{cursor:pointer}th.gridjs-th-sort:focus,th.gridjs-th-sort:hover{background-color:#e5e7eb}th.gridjs-th-fixed{position:-webkit-sticky;position:sticky;box-shadow:0 1px 0 0 #e5e7eb}@supports (-moz-appearance:none){th.gridjs-th-fixed{box-shadow:0 0 0 1px #e5e7eb}}th.gridjs-th:first-child{border-left:none}th.gridjs-th:last-child{border-right:none}.gridjs-tr{border:none}.gridjs-tr-selected td{background-color:#ebf5ff}.gridjs-tr:last-child td{border-bottom:0}.gridjs *,.gridjs :after,.gridjs :before{box-sizing:border-box}.gridjs-wrapper{position:relative;z-index:1;overflow:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;box-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px 0 rgba(0,0,0,.26);border-radius:8px 8px 0 0;display:block;border-top-width:1px;border-color:#e5e7eb}.gridjs-wrapper:last-of-type{border-radius:8px;border-bottom-width:1px}.gridjs-search{float:left}.gridjs-search-input{width:250px}.gridjs-loading-bar{z-index:10;background-color:#fff;opacity:.5}.gridjs-loading-bar,.gridjs-loading-bar:after{position:absolute;left:0;right:0;top:0;bottom:0}.gridjs-loading-bar:after{transform:translateX(-100%);background-image:linear-gradient(90deg,hsla(0,0%,80%,0),hsla(0,0%,80%,.2) 20%,hsla(0,0%,80%,.5) 60%,hsla(0,0%,80%,0));-webkit-animation:shimmer 2s infinite;animation:shimmer 2s infinite;content:""}@-webkit-keyframes shimmer{to{transform:translateX(100%)}}@keyframes shimmer{to{transform:translateX(100%)}}.gridjs-td .gridjs-checkbox{display:block;margin:auto;cursor:pointer} -------------------------------------------------------------------------------- /assets_old/gridjs.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).gridjs={})}(this,(function(t){"use strict"; 2 | /*! ***************************************************************************** 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 10 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 12 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 13 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 14 | PERFORMANCE OF THIS SOFTWARE. 15 | ***************************************************************************** */var e=function(t,n){return(e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n])})(t,n)};function n(t,n){function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}var r=function(){return(r=Object.assign||function(t){for(var e,n=1,r=arguments.length;n0&&o[o.length-1])||6!==i[0]&&2!==i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]3)for(n=[n],i=3;i1&&L(o,e,n),e=O(n,o,o,t.__k,null,o.__e,e),"function"==typeof t.type&&(t.__d=e)))}function R(t,e,n,r,o,i,s,u,l){var p,c,h,f,d,_,g,m,v,b,w,x=e.type;if(void 0!==e.constructor)return null;null!=n.__h&&(l=n.__h,u=e.__e=n.__e,e.__h=null,i=[u]),(p=a.__b)&&p(e);try{t:if("function"==typeof x){if(m=e.props,v=(p=x.contextType)&&r[p.__c],b=p?v?v.props.value:p.__:r,n.__c?g=(c=e.__c=n.__c).__=c.__E:("prototype"in x&&x.prototype.render?e.__c=c=new x(m,b):(e.__c=c=new S(m,b),c.constructor=x,c.render=M),v&&v.sub(c),c.props=m,c.state||(c.state={}),c.context=b,c.__n=r,h=c.__d=!0,c.__h=[]),null==c.__s&&(c.__s=c.state),null!=x.getDerivedStateFromProps&&(c.__s==c.state&&(c.__s=y({},c.__s)),y(c.__s,x.getDerivedStateFromProps(m,c.__s))),f=c.props,d=c.state,h)null==x.getDerivedStateFromProps&&null!=c.componentWillMount&&c.componentWillMount(),null!=c.componentDidMount&&c.__h.push(c.componentDidMount);else{if(null==x.getDerivedStateFromProps&&m!==f&&null!=c.componentWillReceiveProps&&c.componentWillReceiveProps(m,b),!c.__e&&null!=c.shouldComponentUpdate&&!1===c.shouldComponentUpdate(m,c.__s,b)||e.__v===n.__v){c.props=m,c.state=c.__s,e.__v!==n.__v&&(c.__d=!1),c.__v=e,e.__e=n.__e,e.__k=n.__k,c.__h.length&&s.push(c),L(e,u,t);break t}null!=c.componentWillUpdate&&c.componentWillUpdate(m,c.__s,b),null!=c.componentDidUpdate&&c.__h.push((function(){c.componentDidUpdate(f,d,_)}))}c.context=b,c.props=m,c.state=c.__s,(p=a.__r)&&p(e),c.__d=!1,c.__v=e,c.__P=t,p=c.render(c.props,c.state,c.context),c.state=c.__s,null!=c.getChildContext&&(r=y(y({},r),c.getChildContext())),h||null==c.getSnapshotBeforeUpdate||(_=c.getSnapshotBeforeUpdate(f,d)),w=null!=p&&p.type==P&&null==p.key?p.props.children:p,T(t,Array.isArray(w)?w:[w],e,n,r,o,i,s,u,l),c.base=e.__e,e.__h=null,c.__h.length&&s.push(c),g&&(c.__E=c.__=null),c.__e=!1}else null==i&&e.__v===n.__v?(e.__k=n.__k,e.__e=n.__e):e.__e=U(n.__e,e,n,r,o,i,s,l);(p=a.diffed)&&p(e)}catch(t){e.__v=null,(l||null!=i)&&(e.__e=u,e.__h=!!l,i[i.indexOf(u)]=null),a.__e(t,e,n)}return e.__e}function j(t,e){a.__c&&a.__c(e,t),t.some((function(e){try{t=e.__h,e.__h=[],t.some((function(t){t.call(e)}))}catch(t){a.__e(t,e.__v)}}))}function U(t,e,n,r,o,i,s,a){var u,l,p,c,h,f=n.props,g=e.props;if(o="svg"===e.type||o,null!=i)for(u=0;u0&&(this.callbacks[r].forEach((function(t){return t.apply(void 0,e)})),!0)},t}();!function(t){t[t.Initiator=0]="Initiator",t[t.ServerFilter=1]="ServerFilter",t[t.ServerSort=2]="ServerSort",t[t.ServerLimit=3]="ServerLimit",t[t.Extractor=4]="Extractor",t[t.Transformer=5]="Transformer",t[t.Filter=6]="Filter",t[t.Sort=7]="Sort",t[t.Limit=8]="Limit"}(J||(J={}));var tt=function(t){function e(e){var n=t.call(this)||this;return n._props={},n.id=W(),e&&n.setProps(e),n}return n(e,t),e.prototype.process=function(){for(var t=[],e=0;e0?e[0]:null},t.prototype.add=function(t){return t.id?null!==this.get(t.id)?(pt.error("Duplicate plugin ID: "+t.id),this):(this.plugins.push(t),this):(pt.error("Plugin ID cannot be empty"),this)},t.prototype.remove=function(t){return this.plugins.splice(this.plugins.indexOf(this.get(t)),1),this},t.prototype.list=function(t){return(null!=t||null!=t?this.plugins.filter((function(e){return e.position===t})):this.plugins).sort((function(t,e){return t.order-e.order}))},t}(),ft=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.render=function(){var t=this;if(this.props.pluginId){var e=this.config.plugin.get(this.props.pluginId);return e?v(P,{},v(e.component,r(r({plugin:e},e.props),this.props.props))):null}return void 0!==this.props.position?v(P,{},this.config.plugin.list(this.props.position).map((function(e){return v(e.component,r(r({plugin:e},e.props),t.props.props))}))):null},e}(K),dt=function(t){function e(e,n){var r=t.call(this,e,n)||this;r.actions=new ut(r.config.dispatcher),r.store=new st(r.config.dispatcher);var o=e.enabled,i=e.keyword;if(o){i&&r.actions.search(i),r.storeUpdatedFn=r.storeUpdated.bind(r),r.store.on("updated",r.storeUpdatedFn);var s=void 0;s=e.server?new lt({keyword:e.keyword,url:e.server.url,body:e.server.body}):new et({keyword:e.keyword,selector:e.selector}),r.searchProcessor=s,r.config.pipeline.register(s)}return r}return n(e,t),e.prototype.componentWillUnmount=function(){this.config.pipeline.unregister(this.searchProcessor),this.store.off("updated",this.storeUpdatedFn)},e.prototype.storeUpdated=function(t){this.searchProcessor.setProps({keyword:t.keyword})},e.prototype.onChange=function(t){var e=t.target.value;this.actions.search(e)},e.prototype.render=function(){if(!this.props.enabled)return null;var t,e,n,r=this.onChange.bind(this);return this.searchProcessor instanceof lt&&(t=r,e=this.props.debounceTimeout,r=function(){for(var r=[],o=0;o=this.pages||t<0||t===this.state.page)return null;this.setState({page:t}),this.processor.setProps({page:t})},e.prototype.setTotal=function(t){this.setState({total:t})},e.prototype.renderPages=function(){var t=this;if(this.props.buttonsCount<=0)return null;var e=Math.min(this.pages,this.props.buttonsCount),n=Math.min(this.state.page,Math.floor(e/2));return this.state.page+Math.floor(e/2)>=this.pages&&(n=e-(this.pages-this.state.page)),v(P,null,this.pages>e&&this.state.page-n>0&&v(P,null,v("button",{tabIndex:0,role:"button",onClick:this.setPage.bind(this,0),title:this._("pagination.firstPage"),"aria-label":this._("pagination.firstPage"),className:this.config.className.paginationButton},this._("1")),v("button",{tabIndex:-1,className:rt(nt("spread"),this.config.className.paginationButton)},"...")),Array.from(Array(e).keys()).map((function(e){return t.state.page+(e-n)})).map((function(e){return v("button",{tabIndex:0,role:"button",onClick:t.setPage.bind(t,e),className:rt(t.state.page===e?rt(nt("currentPage"),t.config.className.paginationButtonCurrent):null,t.config.className.paginationButton),title:t._("pagination.page",e+1),"aria-label":t._("pagination.page",e+1)},t._(""+(e+1)))})),this.pages>e&&this.pages>this.state.page+n+1&&v(P,null,v("button",{tabIndex:-1,className:rt(nt("spread"),this.config.className.paginationButton)},"..."),v("button",{tabIndex:0,role:"button",onClick:this.setPage.bind(this,this.pages-1),title:this._("pagination.page",this.pages),"aria-label":this._("pagination.page",this.pages),className:this.config.className.paginationButton},this._(""+this.pages))))},e.prototype.renderSummary=function(){return v(P,null,this.props.summary&&this.state.total>0&&v("div",{role:"status","aria-live":"polite",className:rt(nt("summary"),this.config.className.paginationSummary),title:this._("pagination.navigate",this.state.page+1,this.pages)},this._("pagination.showing")," ",v("b",null,this._(""+(this.state.page*this.state.limit+1)))," ",this._("pagination.to")," ",v("b",null,this._(""+Math.min((this.state.page+1)*this.state.limit,this.state.total)))," ",this._("pagination.of")," ",v("b",null,this._(""+this.state.total))," ",this._("pagination.results")))},e.prototype.render=function(){return this.props.enabled?v("div",{className:rt(nt("pagination"),this.config.className.pagination)},this.renderSummary(),v("div",{className:nt("pages")},this.props.prevButton&&v("button",{tabIndex:0,role:"button",disabled:0===this.state.page,onClick:this.setPage.bind(this,this.state.page-1),title:this._("pagination.previous"),"aria-label":this._("pagination.previous"),className:rt(this.config.className.paginationButton,this.config.className.paginationButtonPrev)},this._("pagination.previous")),this.renderPages(),this.props.nextButton&&v("button",{tabIndex:0,role:"button",disabled:this.pages===this.state.page+1||0===this.pages,onClick:this.setPage.bind(this,this.state.page+1),title:this._("pagination.next"),"aria-label":this._("pagination.next"),className:rt(this.config.className.paginationButton,this.config.className.paginationButtonNext)},this._("pagination.next")))):null},e.defaultProps={summary:!0,nextButton:!0,prevButton:!0,buttonsCount:3,limit:10,resetPageOnUpdate:!0},e}(ct);function mt(t,e){return"string"==typeof t?t.indexOf("%")>-1?e/100*parseInt(t,10):parseInt(t,10):t}function vt(t){return t?Math.floor(t)+"px":""}function bt(t,e){if(!t)return null;var n=t.querySelector('thead th[data-column-id="'+e+'"]');return n?n.clientWidth:null}var wt=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.render=function(){if(this.props.tableRef.current){var t=this.props.tableRef.current.base.cloneNode(!0);return t.className+=" "+nt("shadowTable"),t.style.position="absolute",t.style.zIndex="-2147483640",t.style.visibility="hidden",t.style.tableLayout="auto",t.style.width="auto",t.style.padding="0",t.style.margin="0",t.style.border="none",t.style.outline="none",v("div",{ref:function(e){e&&e.appendChild(t)}})}return null},e}(K);function Pt(t){if(!t)return"";var e=t.split(" ");return 1===e.length&&/([a-z][A-Z])+/g.test(t)?t:e.map((function(t,e){return 0==e?t.toLowerCase():t.charAt(0).toUpperCase()+t.slice(1).toLowerCase()})).join("")}var St,xt=function(e){function o(){var t=e.call(this)||this;return t._columns=[],t}return n(o,e),Object.defineProperty(o.prototype,"columns",{get:function(){return this._columns},set:function(t){this._columns=t},enumerable:!1,configurable:!0}),o.prototype.adjustWidth=function(t,e,n,r){if(void 0===r&&(r=!0),!t)return this;var i=t.clientWidth,s={current:null};if(e.current&&r){var a=v(wt,{tableRef:e});a.ref=s,B(a,n.current)}for(var u=0,l=o.tabularFormat(this.columns).reduce((function(t,e){return t.concat(e)}),[]);u0||(!p.width&&r?p.width=vt(bt(s.current.base,p.id)):p.width=vt(mt(p.width,i)))}return e.current&&r&&B(null,n.current),this},o.prototype.setSort=function(t,e){for(var n=0,o=e||this.columns||[];n0&&(i.sort={enabled:!1}),void 0===i.sort&&t.sort&&(i.sort={enabled:!0}),i.sort?"object"==typeof i.sort&&(i.sort=r({enabled:!0},i.sort)):i.sort={enabled:!1},i.columns&&this.setSort(t,i.columns)}},o.prototype.setFixedHeader=function(t,e){for(var n=0,r=e||this.columns||[];n=e?[4,a.process(r)]:[3,4]):[3,6];case 3:return r=i.sent(),this.cache.set(a.id,r),[3,5];case 4:r=this.cache.get(a.id),i.label=5;case 5:return o++,[3,2];case 6:return[3,8];case 7:throw u=i.sent(),pt.error(u),this.emit("error",r),u;case 8:return this.lastProcessorIndexUpdated=n.length,this.emit("afterProcess",r),[2,r]}}))}))},e.prototype.findProcessorIndexByID=function(t){return this.steps.findIndex((function(e){return e.id==t}))},e.prototype.setLastProcessorIndex=function(t){var e=this.findProcessorIndexByID(t.id);this.lastProcessorIndexUpdated>e&&(this.lastProcessorIndexUpdated=e)},e.prototype.processorPropsUpdated=function(t){this.setLastProcessorIndex(t),this.emit("propsUpdated"),this.emit("updated",t)},e.prototype.afterRegistered=function(t){this.setLastProcessorIndex(t),this.emit("afterRegister"),this.emit("updated",t)},e}(Q),Ft=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),Object.defineProperty(e.prototype,"type",{get:function(){return J.Extractor},enumerable:!1,configurable:!0}),e.prototype._process=function(t){return o(this,void 0,void 0,(function(){return i(this,(function(e){switch(e.label){case 0:return[4,this.props.storage.get(t)];case 1:return[2,e.sent()]}}))}))},e}(tt),Et=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),Object.defineProperty(e.prototype,"type",{get:function(){return J.Transformer},enumerable:!1,configurable:!0}),e.prototype._process=function(t){var e=Z.fromArray(t.data);return e.length=t.total,e},e}(tt),It=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),Object.defineProperty(e.prototype,"type",{get:function(){return J.Initiator},enumerable:!1,configurable:!0}),e.prototype._process=function(){return Object.entries(this.props.serverStorageOptions).filter((function(t){t[0];return"function"!=typeof t[1]})).reduce((function(t,e){var n,o=e[0],i=e[1];return r(r({},t),((n={})[o]=i,n))}),{})},e}(tt),Lt=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),Object.defineProperty(e.prototype,"type",{get:function(){return J.Transformer},enumerable:!1,configurable:!0}),e.prototype.castData=function(t){if(!t||!t.length)return[];if(!this.props.header||!this.props.header.columns)return t;var e=xt.leafColumns(this.props.header.columns);return t[0]instanceof Array?t.map((function(t){var n=0;return e.map((function(e,r){return void 0!==e.data?(n++,"function"==typeof e.data?e.data(t):e.data):t[r-n]}))})):"object"!=typeof t[0]||t[0]instanceof Array?[]:t.map((function(t){return e.map((function(e,n){return void 0!==e.data?"function"==typeof e.data?e.data(t):e.data:e.id?t[e.id]:(pt.error("Could not find the correct cell for column at position "+n+".\n Make sure either 'id' or 'selector' is defined for all columns."),null)}))}))},e.prototype._process=function(t){return{data:this.castData(t.data),total:t.total}},e}(tt),Rt=function(){function t(){}return t.createFromConfig=function(t){var e=new Dt;return t.storage instanceof Tt&&e.register(new It({serverStorageOptions:t.server})),e.register(new Ft({storage:t.storage})),e.register(new Lt({header:t.header})),e.register(new Et),e},t}(),jt=function(){function e(t){Object.assign(this,r(r({},e.defaultConfig()),t)),this._userConfig={}}return e.prototype.assign=function(t){for(var e=0,n=Object.keys(t);ee?1:t1&&(l=!0,u=!0):0===i?u=!0:i>0&&!n?(u=!0,l=!0):i>0&&n&&(u=!0),l&&(o=[]),u)o.push({index:t,direction:e,compare:r});else if(c){var h=o.indexOf(a);o[h].direction=e}else if(p){var f=o.indexOf(a);o.splice(f,1)}this.setState(o)},e}(it),qt=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.sortColumn=function(t,e,n,r){this.dispatch("SORT_COLUMN",{index:t,direction:e,multi:n,compare:r})},e.prototype.sortToggle=function(t,e,n){this.dispatch("SORT_COLUMN_TOGGLE",{index:t,multi:e,compare:n})},e}(at),Gt=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),Object.defineProperty(e.prototype,"type",{get:function(){return J.ServerSort},enumerable:!1,configurable:!0}),e.prototype._process=function(t){var e={};return this.props.url&&(e.url=this.props.url(t.url,this.props.columns)),this.props.body&&(e.body=this.props.body(t.body,this.props.columns)),r(r({},t),e)},e}(tt),zt=function(t){function e(e,n){var r=t.call(this,e,n)||this;return r.actions=new qt(r.config.dispatcher),r.store=new Wt(r.config.dispatcher),e.enabled&&(r.sortProcessor=r.getOrCreateSortProcessor(),r.updateStateFn=r.updateState.bind(r),r.store.on("updated",r.updateStateFn),r.state={direction:0}),r}return n(e,t),e.prototype.componentWillUnmount=function(){this.config.pipeline.unregister(this.sortProcessor),this.store.off("updated",this.updateStateFn),this.updateSortProcessorFn&&this.store.off("updated",this.updateSortProcessorFn)},e.prototype.updateState=function(){var t=this,e=this.store.state.find((function(e){return e.index===t.props.index}));e?this.setState({direction:e.direction}):this.setState({direction:0})},e.prototype.updateSortProcessor=function(t){this.sortProcessor.setProps({columns:t})},e.prototype.getOrCreateSortProcessor=function(){var t=J.Sort;this.config.sort&&"object"==typeof this.config.sort.server&&(t=J.ServerSort);var e,n=this.config.pipeline.getStepsByType(t);return n.length>0?e=n[0]:(this.updateSortProcessorFn=this.updateSortProcessor.bind(this),this.store.on("updated",this.updateSortProcessorFn),e=t===J.ServerSort?new Gt(r({columns:this.store.state},this.config.sort.server)):new Bt({columns:this.store.state}),this.config.pipeline.register(e)),e},e.prototype.changeDirection=function(t){t.preventDefault(),t.stopPropagation(),this.actions.sortToggle(this.props.index,!0===t.shiftKey&&this.config.sort.multiColumn,this.props.compare)},e.prototype.render=function(){if(!this.props.enabled)return null;var t=this.state.direction,e="neutral";return 1===t?e="asc":-1===t&&(e="desc"),v("button",{tabIndex:-1,"aria-label":this._("sort.sort"+(1===t?"Desc":"Asc")),title:this._("sort.sort"+(1===t?"Desc":"Asc")),className:rt(nt("sort"),nt("sort",e),this.config.className.sort),onClick:this.changeDirection.bind(this)})},e}(K),Kt=function(t){function e(e,n){var r=t.call(this,e,n)||this;return r.sortRef={current:null},r.thRef={current:null},r.state={style:{}},r}return n(e,t),e.prototype.isSortable=function(){return this.props.column.sort.enabled},e.prototype.onClick=function(t){t.stopPropagation(),this.isSortable()&&this.sortRef.current.changeDirection(t)},e.prototype.keyDown=function(t){this.isSortable()&&13===t.which&&this.onClick(t)},e.prototype.componentDidMount=function(){var t=this;setTimeout((function(){if(t.props.column.fixedHeader&&t.thRef.current){var e=t.thRef.current.offsetTop;"number"==typeof e&&t.setState({style:{top:e}})}}),0)},e.prototype.content=function(){return void 0!==this.props.column.name?this.props.column.name:void 0!==this.props.column.plugin?v(ft,{pluginId:this.props.column.plugin.id,props:{column:this.props.column}}):null},e.prototype.render=function(){var t={};return this.isSortable()&&(t.tabIndex=0),v("th",r({ref:this.thRef,"data-column-id":this.props.column&&this.props.column.id,className:rt(nt("th"),this.isSortable()?nt("th","sort"):null,this.props.column.fixedHeader?nt("th","fixed"):null,this.config.className.th),onClick:this.onClick.bind(this),style:r(r(r(r({},this.config.style.th),{width:this.props.column.width}),this.state.style),this.props.style),onKeyDown:this.keyDown.bind(this),rowSpan:this.props.rowSpan>1?this.props.rowSpan:void 0,colSpan:this.props.colSpan>1?this.props.colSpan:void 0},t),this.content(),this.isSortable()&&v(zt,r({ref:this.sortRef,index:this.props.index},this.props.column.sort)))},e}(K);var $t,Vt,Yt,Xt=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.renderColumn=function(t,e,n,r){var o=function(t,e,n){var r=xt.maximumDepth(t),o=n-e;return{rowSpan:Math.floor(o-r-r/o),colSpan:t.columns&&t.columns.length||1}}(t,e,r),i=o.rowSpan,s=o.colSpan;return v(Kt,{column:t,index:n,colSpan:s,rowSpan:i})},e.prototype.renderRow=function(t,e,n){var r=this,o=xt.leafColumns(this.props.header.columns);return v(At,null,t.map((function(t){return t.hidden?null:r.renderColumn(t,e,o.indexOf(t),n)})))},e.prototype.renderRows=function(){var t=this,e=xt.tabularFormat(this.props.header.columns);return e.map((function(n,r){return t.renderRow(n,r,e.length)}))},e.prototype.render=function(){return this.props.header?v("thead",{key:this.props.header.id,className:rt(nt("thead"),this.config.className.thead)},this.renderRows()):null},e}(K),Zt=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.render=function(){return v("table",{role:"grid",className:rt(nt("table"),this.config.className.table),style:r(r({},this.config.style.table),{width:this.props.width,height:this.props.height})},v(Xt,{header:this.props.header}),v(Mt,{data:this.props.data,status:this.props.status,header:this.props.header}))},e}(K),Jt=function(e){function o(t,n){var r=e.call(this,t,n)||this;return r.headerRef={current:null},r.state={isActive:!0},r}return n(o,e),o.prototype.componentDidMount=function(){0===this.headerRef.current.children.length&&this.setState({isActive:!1})},o.prototype.render=function(){return this.state.isActive?v("div",{ref:this.headerRef,className:rt(nt("head"),this.config.className.header),style:r({},this.config.style.header)},v(ft,{position:t.PluginPosition.Header})):null},o}(K),Qt=function(e){function o(t,n){var r=e.call(this,t,n)||this;return r.footerRef={current:null},r.state={isActive:!0},r}return n(o,e),o.prototype.componentDidMount=function(){0===this.footerRef.current.children.length&&this.setState({isActive:!1})},o.prototype.render=function(){return this.state.isActive?v("div",{ref:this.footerRef,className:rt(nt("footer"),this.config.className.footer),style:r({},this.config.style.footer)},v(ft,{position:t.PluginPosition.Footer})):null},o}(K),te=function(t){function e(e,n){var r=t.call(this,e,n)||this;return r.configContext=function(t,e){var n={__c:e="__cC"+f++,__:t,Consumer:function(t,e){return t.children(e)},Provider:function(t,n,r){return this.getChildContext||(n=[],(r={})[e]=this,this.getChildContext=function(){return r},this.shouldComponentUpdate=function(t){this.props.value!==t.value&&n.some(C)},this.sub=function(t){n.push(t);var e=t.componentWillUnmount;t.componentWillUnmount=function(){n.splice(n.indexOf(t),1),e&&e.call(t)}}),t.children}};return n.Provider.__=n.Consumer.contextType=n}(null),r.state={status:St.Loading,header:e.header,data:null},r}return n(e,t),e.prototype.processPipeline=function(){return o(this,void 0,void 0,(function(){var t,e;return i(this,(function(n){switch(n.label){case 0:this.props.config.eventEmitter.emit("beforeLoad"),this.setState({status:St.Loading}),n.label=1;case 1:return n.trys.push([1,3,,4]),[4,this.props.pipeline.process()];case 2:return t=n.sent(),this.setState({data:t,status:St.Loaded}),this.props.config.eventEmitter.emit("load",t),[3,4];case 3:return e=n.sent(),pt.error(e),this.setState({status:St.Error,data:null}),[3,4];case 4:return[2]}}))}))},e.prototype.componentDidMount=function(){return o(this,void 0,void 0,(function(){var t;return i(this,(function(e){switch(e.label){case 0:return t=this.props.config,[4,this.processPipeline()];case 1:return e.sent(),t.header&&this.state.data&&this.state.data.length&&this.setState({header:t.header.adjustWidth(t.container,t.tableRef,t.tempRef,t.autoWidth)}),this.processPipelineFn=this.processPipeline.bind(this),this.props.pipeline.on("updated",this.processPipelineFn),[2]}}))}))},e.prototype.componentWillUnmount=function(){this.props.pipeline.off("updated",this.processPipelineFn)},e.prototype.componentDidUpdate=function(t,e){e.status!=St.Rendered&&this.state.status==St.Loaded&&(this.setState({status:St.Rendered}),this.props.config.eventEmitter.emit("ready"))},e.prototype.render=function(){return v(this.configContext.Provider,{value:this.props.config},v("div",{role:"complementary",className:rt("gridjs",nt("container"),this.state.status===St.Loading?nt("loading"):null,this.props.config.className.container),style:r(r({},this.props.config.style.container),{width:this.props.width})},this.state.status===St.Loading&&v("div",{className:nt("loading-bar")}),v(Jt,null),v("div",{className:nt("wrapper"),style:{width:this.props.width,height:this.props.height}},v(Zt,{ref:this.props.config.tableRef,data:this.state.data,header:this.state.header,width:this.props.width,height:this.props.height,status:this.state.status})),v(Qt,null)),v("div",{ref:this.props.config.tempRef,id:"gridjs-temp",className:nt("temp")}))},e}(K),ee=function(t){function e(e){var n=t.call(this)||this;return n.config=new jt({instance:n,eventEmitter:n}).update(e),n.plugin=n.config.plugin,n}return n(e,t),e.prototype.updateConfig=function(t){return this.config.update(t),this},e.prototype.createElement=function(){return v(te,{config:this.config,pipeline:this.config.pipeline,header:this.config.header,width:this.config.width,height:this.config.height})},e.prototype.forceRender=function(){return this.config&&this.config.container||pt.error("Container is empty. Make sure you call render() before forceRender()",!0),this.config.pipeline.clearCache(),B(null,this.config.container),B(this.createElement(),this.config.container),this},e.prototype.render=function(t){return t||pt.error("Container element cannot be null",!0),t.childNodes.length>0?(pt.error("The container element "+t+" is not empty. Make sure the container is empty and call render() again"),this):(this.config.container=t,B(this.createElement(),t),this)},e}(Q),ne=0,re=[],oe=a.__b,ie=a.__r,se=a.diffed,ae=a.__c,ue=a.unmount;function le(t,e){a.__h&&a.__h(Vt,t,ne||e),ne=0;var n=Vt.__H||(Vt.__H={__:[],__h:[]});return t>=n.__.length&&n.__.push({}),n.__[t]}function pe(){re.forEach((function(t){if(t.__P)try{t.__H.__h.forEach(he),t.__H.__h.forEach(fe),t.__H.__h=[]}catch(e){t.__H.__h=[],a.__e(e,t.__v)}})),re=[]}a.__b=function(t){Vt=null,oe&&oe(t)},a.__r=function(t){ie&&ie(t),$t=0;var e=(Vt=t.__c).__H;e&&(e.__h.forEach(he),e.__h.forEach(fe),e.__h=[])},a.diffed=function(t){se&&se(t);var e=t.__c;e&&e.__H&&e.__H.__h.length&&(1!==re.push(e)&&Yt===a.requestAnimationFrame||((Yt=a.requestAnimationFrame)||function(t){var e,n=function(){clearTimeout(r),ce&&cancelAnimationFrame(e),setTimeout(t)},r=setTimeout(n,100);ce&&(e=requestAnimationFrame(n))})(pe)),Vt=void 0},a.__c=function(t,e){e.some((function(t){try{t.__h.forEach(he),t.__h=t.__h.filter((function(t){return!t.__||fe(t)}))}catch(n){e.some((function(t){t.__h&&(t.__h=[])})),e=[],a.__e(n,t.__v)}})),ae&&ae(t,e)},a.unmount=function(t){ue&&ue(t);var e=t.__c;if(e&&e.__H)try{e.__H.__.forEach(he)}catch(t){a.__e(t,e.__v)}};var ce="function"==typeof requestAnimationFrame;function he(t){var e=Vt;"function"==typeof t.__c&&t.__c(),Vt=e}function fe(t){var e=Vt;t.__c=t.__(),Vt=e}function de(t,e){return!t||t.length!==e.length||e.some((function(e,n){return e!==t[n]}))}t.BaseActions=at,t.BaseComponent=K,t.BaseStore=it,t.Cell=Y,t.Component=S,t.Config=jt,t.Dispatcher=kt,t.Grid=ee,t.PluginBaseComponent=ct,t.Row=X,t.className=nt,t.createElement=v,t.createRef=w,t.h=v,t.html=V,t.useEffect=function(t,e){var n=le($t++,3);!a.__s&&de(n.__H,e)&&(n.__=t,n.__H=e,Vt.__H.__h.push(n))},t.useRef=function(t){return ne=5,function(t,e){var n=le($t++,7);return de(n.__H,e)&&(n.__=t(),n.__H=e,n.__h=t),n.__}((function(){return{current:t}}),[])},Object.defineProperty(t,"__esModule",{value:!0})})); 16 | //# sourceMappingURL=gridjs.production.min.js.map -------------------------------------------------------------------------------- /assets_old/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | DCli - Database Connection Mange Tool 8 | 9 | 10 |
11 | 12 | 13 |
14 |
15 |
16 |
17 |
18 | 19 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /assets_old/loader.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets_old/old.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | DCli - Database Connection Mange Tool 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 |
17 |
18 |
19 |

DCli - Database Connection Mange Tool

20 |
21 |
22 | 23 |
24 | 25 |
26 |

All Queries

27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | 35 | Loading 36 |
37 |
38 |
39 |
40 |
41 |
45 |

Fetch Data Error

46 |
47 | 48 | 55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | 64 |
65 |
66 |
67 |
68 |
69 |
70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /assets_old/script.js: -------------------------------------------------------------------------------- 1 | const tableClass = { 2 | table: "table table-striped", 3 | }; 4 | 5 | const setLoading = (show) => { 6 | const loading = document.getElementById("loading"); 7 | if (show) { 8 | loading.style.display = "block"; 9 | } else { 10 | loading.style.display = "none"; 11 | } 12 | }; 13 | 14 | const DataGrid = new gridjs.Grid({ 15 | columns: [], 16 | data: [], 17 | }); 18 | const wrapper = document.getElementById("wrapper"); 19 | DataGrid.render(wrapper); 20 | 21 | const fetchData = (url) => { 22 | setLoading(true); 23 | dismissErr(); 24 | // const wrapper = document.getElementById("wrapper") 25 | // while (wrapper.firstChild) { 26 | // wrapper.removeChild(wrapper.firstChild) 27 | // } 28 | axios 29 | .get(url) 30 | .then(function (response) { 31 | const { data } = response; 32 | if (data.length !== 0) { 33 | const columns = Object.keys(data[0]); 34 | DataGrid.updateConfig({ 35 | columns, 36 | data, 37 | // className: tableClass 38 | }).forceRender(); 39 | } 40 | }) 41 | .catch(showFetchErr) 42 | .finally(() => { 43 | setLoading(false); 44 | }); 45 | }; 46 | 47 | const fetchMeta = () => { 48 | axios.get("api/_meta").then((resp) => { 49 | const { prefix } = resp.data; 50 | const { protocol, host } = window.location; 51 | const apis = resp.data.queries.map((query) => { 52 | const isMeta = query.url.endsWith("_meta"); 53 | const url = `${protocol}//${host}/${prefix}/${query.url}`; 54 | const sql = gridjs.html(`${query.sql}`); 55 | const urlCell = gridjs.html(` 56 |
57 | ${url} 58 |
59 | `); 60 | if (isMeta) { 61 | const buttonCell = gridjs.html( 62 | `` 63 | ); 64 | return { ...query, url, urlCell, buttonCell }; 65 | } else { 66 | const buttonCell = gridjs.html( 67 | `` 68 | ); 69 | return { ...query, sql, url, urlCell, buttonCell }; 70 | } 71 | }); 72 | 73 | const newTable = new gridjs.Grid({ 74 | columns: ["name", "profile", "sql", "urlCell", "buttonCell"], 75 | data: apis, 76 | }); 77 | 78 | const ele = document.getElementById("meta"); 79 | while (ele.firstChild) { 80 | ele.removeChild(ele.firstChild); 81 | } 82 | newTable.render(ele); 83 | }); 84 | }; 85 | 86 | const showFetchErr = (e) => { 87 | const errCard = document.getElementById("err-card"); 88 | errCard.classList.replace("d-none", "d-block"); 89 | const errDetail = document.getElementById("err-detail"); 90 | errDetail.innerText = e.toString(); 91 | }; 92 | 93 | const dismissErr = () => { 94 | const errCard = document.getElementById("err-card"); 95 | errCard.classList.replace("d-block", "d-none"); 96 | }; 97 | 98 | const initSwaggerUI = () => { 99 | const ui = SwaggerUIBundle({ 100 | url: `${window.location.protocol}//${window.location.host}/open_api`, 101 | dom_id: "#swagger-ui", 102 | presets: [ 103 | SwaggerUIBundle.presets.apis, 104 | SwaggerUIBundle.SwaggerUIStandalonePreset 105 | ], 106 | }); 107 | window.ui = ui; 108 | }; 109 | 110 | const onLoad = () => { 111 | fetchData(); 112 | initSwaggerUI(); 113 | }; 114 | -------------------------------------------------------------------------------- /assets_old/style.css: -------------------------------------------------------------------------------- 1 | .big-title { 2 | margin-top: 10px; 3 | } 4 | 5 | .api-list { 6 | margin-top: 10px; 7 | margin-bottom: 50px; 8 | text-align: center; 9 | } 10 | 11 | #loading { 12 | display: none; 13 | } 14 | -------------------------------------------------------------------------------- /build.py: -------------------------------------------------------------------------------- 1 | from subprocess import check_call 2 | import shutil 3 | import argparse 4 | 5 | def build_linux_en(ver: str): 6 | file = f'dcli_{ver}-x86_64-unknown-linux-gnu' 7 | print(f"building {file}") 8 | check_call("cargo build --release".split()) 9 | shutil.move("./target/release/dcli", file) 10 | 11 | def build_linux_zh(ver: str): 12 | file = f'dcli-zh-CN_{ver}-x86_64-unknown-linux-gnu' 13 | print(f"building {file}") 14 | check_call("cargo build --release --features zh-CN --no-default-features".split()) 15 | shutil.move("./target/release/dcli", file) 16 | 17 | def build_deb_en(ver: str): 18 | file = f'dcli_{ver}_amd64.deb' 19 | print(f"building {file}") 20 | check_call(f"cargo deb -o {file}".split()) 21 | 22 | def build_deb_zh(ver: str): 23 | file = f'dcli_zh_CN_{ver}_amd64.deb' 24 | print(f"building {file}") 25 | check_call(f"cargo deb -o {file} -- --features zh-CN --no-default-features".split()) 26 | 27 | def run(): 28 | parser = argparse.ArgumentParser('dcli builder') 29 | parser.add_argument('ver') 30 | args = parser.parse_args() 31 | build_linux_en(args.ver) 32 | build_linux_zh(args.ver) 33 | build_deb_en(args.ver) 34 | build_deb_zh(args.ver) 35 | 36 | if __name__ == "__main__": 37 | run() -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | 2 | ## 0.0.7 a database connection manage tool (2021-01-03) 3 | 4 | 5 | #### Bug Fixes 6 | 7 | * update expore web page ([8e4eabbc](https://github.com/PrivateRookie/dcli/commit/8e4eabbc9ec43356c1a4f299f43a6b3b68bfb1a3)) 8 | * fix assets render error ([43296462](https://github.com/PrivateRookie/dcli/commit/432964629212806469dbe613de70dd92a950ae8c)) 9 | * remove unuse code & add `serve` command help ([48d7b0f4](https://github.com/PrivateRookie/dcli/commit/48d7b0f48e196bc0fdb129ed91fd60944c6150b7)) 10 | * fix datetime and timestamp decode ([afd8f65c](https://github.com/PrivateRookie/dcli/commit/afd8f65c039bbd9f24a2f900165771cd3dbf9826)) 11 | 12 | #### Features 13 | 14 | * support paging ([23412c82](https://github.com/PrivateRookie/dcli/commit/23412c829c2d65585fc1f4c5a20908d157ddf0f3)) 15 | * support openapiv3 ([32c8f4f5](https://github.com/PrivateRookie/dcli/commit/32c8f4f5fc951f40e0a06a5950e9a412303e8bab)) 16 | * support browning multi api ([2d7c8622](https://github.com/PrivateRookie/dcli/commit/2d7c8622d6471c62d090cc2818c91c66d7f30d60)) 17 | * add meta api to list all api ([475d0f7d](https://github.com/PrivateRookie/dcli/commit/475d0f7d26148d72ea9fa8103dcec0f7fcf6174f)) 18 | * add serve plan as http api ([02e29765](https://github.com/PrivateRookie/dcli/commit/02e29765dc7105adbb2b0079e009757712e22196)) 19 | * make web service running under offline & change static file serving ([71c64a10](https://github.com/PrivateRookie/dcli/commit/71c64a1021acb62a53181c87408334aeccc9543e)) 20 | * add web page to preview and download ([f82e8642](https://github.com/PrivateRookie/dcli/commit/f82e8642c406113c4c5c5ae6ffd3b6fec9471a7b)) 21 | * encode binary data as base64 string ([7d5ed871](https://github.com/PrivateRookie/dcli/commit/7d5ed8715438728f3013c42c7513a04c72dfc952)) 22 | 23 | 24 | 25 | 26 | ## 0.0.4 support export data & bug fix (2020-12-17) 27 | 28 | 29 | #### Features 30 | 31 | * support csv export ([06ffb29e](https://github.com/PrivateRookie/dcli/commit/06ffb29ed40395bb56e92408fa671eba5ae11cbe)) 32 | * add export command ([e98b0c6f](https://github.com/PrivateRookie/dcli/commit/e98b0c6fa39dace54c9d746706b19e778676cb58)) 33 | * add lang setting ([5834716b](https://github.com/PrivateRookie/dcli/commit/5834716b326931cca8c15d805c0be42149500712)) 34 | 35 | #### Bug Fixes 36 | 37 | * fix connect failed when database is empty ([725df2be](https://github.com/PrivateRookie/dcli/commit/725df2be3e75b5f1bb6cf6f456d54ff9af24af6f)) 38 | * remove unuse deps ([a6aac0ed](https://github.com/PrivateRookie/dcli/commit/a6aac0edd5b9b85516ce717f961816c381def653)) 39 | 40 | 41 | 42 | 43 | ## 0.0.3 i18n support and some features (2020-12-12) 44 | 45 | 46 | #### Features 47 | 48 | * add i18n support ([3386712e](https://github.com/PrivateRookie/dcli/commit/3386712e7d8d087e66112584d67528bbeb11a20f)) 49 | * use pool instead of connection ([08fafa42](https://github.com/PrivateRookie/dcli/commit/08fafa428bf3e371c657c9c3dc2977822461a6c5)) 50 | * add read file feature ([72313957](https://github.com/PrivateRookie/dcli/commit/7231395787f5142551bd2d2ac04d51eb07905e28)) 51 | * add commands ([024ef67d](https://github.com/PrivateRookie/dcli/commit/024ef67df4bdbc1b60b3ea121a83d99108af1ea4)) 52 | 53 | 54 | 55 | 56 | ## 0.0.2 bug fixed (2020-12-12) 57 | 58 | 59 | #### Bug Fixes 60 | 61 | * fix failed to create history file error ([44a05c96](https://github.com/PrivateRookie/dcli/commit/44a05c967525de2f820f6e00ac4eca3b0b7503db)) 62 | * fix print output error ([b92f99fb](https://github.com/PrivateRookie/dcli/commit/b92f99fb40859ed3a43e6778ca9b5a7e17518aec)) 63 | * fix print output error ([0e7d9e10](https://github.com/PrivateRookie/dcli/commit/0e7d9e1006855204c561e5eaeb4696cb0b7c057a)) 64 | * use token base highlight instead of ast ([5d317482](https://github.com/PrivateRookie/dcli/commit/5d317482aa1578a5e6950cd07a439fb9e3e4058d)) 65 | 66 | 67 | 68 | 69 | ## 0.0.1 init release (2020-12-12) 70 | 71 | 72 | #### Features 73 | 74 | * add no dep shell interface ([6428b313](https://github.com/PrivateRookie/dcli/commit/6428b313130e4c0f77ee63a9427de3be33f1f1ab)) 75 | * add shell ([7ac612ef](https://github.com/PrivateRookie/dcli/commit/7ac612ef2632f8e17dbc85503428142862868d4b)) 76 | * add ssl connection support ([63d9c86c](https://github.com/PrivateRookie/dcli/commit/63d9c86c88122abd47c9122d8c12eaceb650fd6c)) 77 | * add sqlx as connect adapter ([467cfb19](https://github.com/PrivateRookie/dcli/commit/467cfb19f9bb7b74d7d2d948e318dd9b60eddf36)) 78 | * add exec command ([4f1174ac](https://github.com/PrivateRookie/dcli/commit/4f1174acdba7681c8eadc4d22cc83ec1bb15ed69)) 79 | * format table & and add "add", "delete" command ([ac7307a5](https://github.com/PrivateRookie/dcli/commit/ac7307a57b2c4ac570f7ce69613fc784898300a1)) 80 | * create config file when not found ([5dca8d5c](https://github.com/PrivateRookie/dcli/commit/5dca8d5c3c499727909605f053cb44fff7a74d19)) 81 | * first runable version ([f4096982](https://github.com/PrivateRookie/dcli/commit/f4096982ee1908d941ea42873ba0ddfb83a37d28)) 82 | 83 | #### Bug Fixes 84 | 85 | * fix single column print bug ([14dd1d3e](https://github.com/PrivateRookie/dcli/commit/14dd1d3e89bdf26c4608b358e465691ff4b1226b)) 86 | * fix mysql url construct error ([c8278b3d](https://github.com/PrivateRookie/dcli/commit/c8278b3d9cb1654d34f584af85b90d2106b510b6)) 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /docs/assets/swagger_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrivateRookie/dcli/7bca3e64d19a31831f9353f16bb3a0362a51a35c/docs/assets/swagger_demo.png -------------------------------------------------------------------------------- /i18n.toml: -------------------------------------------------------------------------------- 1 | fallback_language = "en-US" 2 | 3 | [fluent] 4 | assets_dir = "i18n" 5 | -------------------------------------------------------------------------------- /i18n/en-US/dcli.ftl: -------------------------------------------------------------------------------- 1 | connect-failed = connect failed... 2 | open-file-failed = can't not open file {$file} 3 | read-file-failed = can't read file {$file} 4 | 5 | # config.rs 6 | profile-host = database hostname, IPv6 use '[]' surround 7 | profile-not-found = can't find profile {$name}, avaiable choices are 8 | {$table} 9 | invalid-value = invalid value: ${$val} 10 | home-not-set = $HOME is not set 11 | create-his-dir-failed = can not create history dir. 12 | create-his-file-failed = can not create {$name} history file. 13 | open-config-failed = can not open config file {$file} 14 | ser-config-failed = config file format error 15 | create-config-file = not config file found, create default config {$file} 16 | save-config-filed = save config failed 17 | 18 | # cli/mod.rs 19 | launch-process-failed = launch proces failed 20 | profile-existed = {$name} profile saved 21 | profile-saved = profile saved 22 | profile-deleted = profile deleted 23 | profile-updated = {$name} profile updated 24 | empty-input = empty input 25 | too-many-input = too many input, expect one command 26 | serialize-output-failed = failed to serialize query output 27 | 28 | # cli/shell/mod.rs 29 | load-his-failed = can not load history file. 30 | exit-info = use %exit to exit. -------------------------------------------------------------------------------- /i18n/zh-CN/dcli.ftl: -------------------------------------------------------------------------------- 1 | connect-failed = 连接失败 2 | open-file-failed = 无法打开文件 {$file} 3 | read-file-failed = 无法读取文件 {$file} 4 | 5 | # config.rs 6 | profile-host = 数据库 hostname, IPv6地址请使用'[]'包围 7 | profile-not-found = 未找到配置文件 {$name}, 请在以下选项中选择 8 | {$table} 9 | invalid-value = 无效值: ${$val} 10 | home-not-set = 未设置 $HOME 变量 11 | create-his-dir-failed = 无法创建历史文件夹. 12 | create-his-file-failed = 无法创建 {$name} 的历史文件. 13 | open-config-failed = 无法打开配置文件 {$file} 14 | ser-config-failed = 配置文件格式错误 15 | create-config-file = 未找到配置文件, 创建默认配置 {$file} 16 | save-config-filed = 无法保存配置文件 17 | 18 | # cli/mod.rs 19 | launch-process-failed = 无法启动程序 20 | profile-existed = {$name} 配置已存在 21 | profile-saved = 配置已保存 22 | profile-deleted = 配置已删除 23 | profile-updated = {$name} 配置已更新 24 | empty-input = 空命令 25 | too-many-input = 输入过多, 期望1个SQL语句 26 | serialize-output-failed = 序列化输出失败 27 | 28 | # cli/shell/mod.rs 29 | load-his-failed = 无法载入历史文件. 30 | exit-info = 使用 %exit 退出. -------------------------------------------------------------------------------- /plan.toml: -------------------------------------------------------------------------------- 1 | prefix = "api" 2 | 3 | [[queries]] 4 | profile = "dev" 5 | sql = "SELECT data_blob, data_bin from bin" 6 | url = "bin" 7 | description = "选择所有的二进制文件" 8 | 9 | [[queries]] 10 | description = "Select Date, Time Data" 11 | profile = "dev" 12 | sql = "SELECT ts, datet from dt" 13 | url = "date" 14 | 15 | 16 | [[queries]] 17 | description = "随便试试" 18 | profile = "dev" 19 | sql = "SELECT * from todos" 20 | url = "todos" 21 | -------------------------------------------------------------------------------- /src/cli/http/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, convert::Infallible}; 2 | 3 | use rust_embed::RustEmbed; 4 | use tracing_subscriber::fmt::format::FmtSpan; 5 | use warp::Filter; 6 | use warp::{http::Response, path::FullPath}; 7 | 8 | use crate::{ 9 | mysql::Session, 10 | output::{QueryOutput, QueryOutputMapSer}, 11 | query::{Paging, QueryPlan}, 12 | }; 13 | 14 | #[derive(RustEmbed)] 15 | #[folder = "assets"] 16 | struct Asset; 17 | 18 | const CT_KEY: &str = "Content-Type"; 19 | 20 | pub async fn serve(port: u16, output: QueryOutput) { 21 | let json_resp = output.to_json().unwrap(); 22 | let json_resp_clone = json_resp.clone(); 23 | let csv_resp = output.to_csv().unwrap(); 24 | let yaml_resp = output.to_yaml().unwrap(); 25 | 26 | let data_api = warp::get().and(warp::path("data")).map(move || { 27 | Response::builder() 28 | .header("Content-Type", "application/json") 29 | .body(json_resp.clone()) 30 | }); 31 | let download_csv = warp::get() 32 | .and(warp::path("download")) 33 | .and(warp::path("csv")) 34 | .map(move || { 35 | Response::builder() 36 | .header(CT_KEY, "text/csv") 37 | .body(csv_resp.clone()) 38 | }); 39 | let download_json = warp::get() 40 | .and(warp::path("download")) 41 | .and(warp::path("json")) 42 | .map(move || { 43 | Response::builder() 44 | .header(CT_KEY, "application/json") 45 | .body(json_resp_clone.clone()) 46 | }); 47 | let download_yaml = warp::get() 48 | .and(warp::path("download")) 49 | .and(warp::path("yaml")) 50 | .map(move || { 51 | Response::builder() 52 | .header(CT_KEY, "application/text") 53 | .body(yaml_resp.clone()) 54 | }); 55 | let routes = serve_static() 56 | .or(data_api) 57 | .or(download_csv) 58 | .or(download_json) 59 | .or(download_yaml); 60 | warp::serve(routes).run(([0, 0, 0, 0], port)).await; 61 | } 62 | 63 | fn index() -> impl Filter + Clone { 64 | warp::get().and(warp::path::end()).map(|| { 65 | Response::builder() 66 | .header(CT_KEY, "text/html") 67 | .body(Asset::get("index.html").unwrap()) 68 | }) 69 | } 70 | 71 | fn favicon() -> impl Filter + Clone { 72 | warp::get() 73 | .and(warp::path("favicon.ico")) 74 | .and(warp::path::end()) 75 | .map(|| { 76 | Response::builder() 77 | .header(CT_KEY, "image/svg+xml") 78 | .body(Asset::get("favicon.svg").unwrap()) 79 | }) 80 | } 81 | 82 | fn assets() -> impl Filter + Clone { 83 | warp::get() 84 | .and(warp::path("assets")) 85 | .and(warp::any()) 86 | .and(warp::path::full()) 87 | .map(move |path: FullPath| { 88 | let relative_path = &path.as_str()[8..]; 89 | if let Some(content) = Asset::get(relative_path) { 90 | let ct = if relative_path.ends_with("js") { 91 | "application/javascript; charset=utf-8" 92 | } else if relative_path.ends_with("css") { 93 | "text/css" 94 | } else if relative_path.ends_with("svg") { 95 | "image/svg+xml" 96 | } else { 97 | "text/plain" 98 | }; 99 | Response::builder() 100 | .header(CT_KEY, ct) 101 | .body(content.to_owned()) 102 | } else { 103 | Response::builder() 104 | .status(404) 105 | .body(Asset::get("404").unwrap()) 106 | } 107 | }) 108 | } 109 | 110 | fn serve_static() -> impl Filter + Clone { 111 | index().or(favicon()).or(assets()) 112 | } 113 | 114 | async fn run( 115 | full_path: FullPath, 116 | paging: Option, 117 | plan: QueryPlan, 118 | sessions: HashMap, 119 | ) -> Result { 120 | let output = plan.query(full_path, paging, &sessions).await.unwrap(); 121 | Ok(warp::reply::json(&QueryOutputMapSer(&output))) 122 | } 123 | 124 | pub async fn serve_plan(plan: QueryPlan, sessions: HashMap) { 125 | let prefix = plan.prefix.clone(); 126 | let plan_meta = plan.with_meta(); 127 | let overview = warp::get().and( 128 | warp::path(prefix) 129 | .and(warp::path("_meta")) 130 | .map(move || warp::reply::json(&plan_meta)), 131 | ); 132 | let filter = std::env::var("RUST_LOG").unwrap_or_else(|_| "tracing=info,warp=debug".to_owned()); 133 | if let Err(e) = tracing_subscriber::fmt() 134 | .with_env_filter(filter) 135 | .with_span_events(FmtSpan::CLOSE) 136 | .try_init() 137 | { 138 | println!("failed to set logger {}", e) 139 | } 140 | let open_api_schema = serde_json::to_string_pretty(&plan.open_api()).unwrap(); 141 | let open_api = warp::get() 142 | .and(warp::path("openapi.json")) 143 | .and(warp::path::end()) 144 | .map(move || open_api_schema.clone()); 145 | let opt_query = warp::query::() 146 | .map(Some) 147 | .or_else(|_| async { Ok::<(Option,), std::convert::Infallible>((None,)) }); 148 | let api = warp::get() 149 | .and(warp::path(plan.prefix.clone())) 150 | .and(warp::any()) 151 | .and(warp::path::full()) 152 | .and(opt_query) 153 | .and(warp::any().map(move || plan.clone())) 154 | .and(warp::any().map(move || sessions.clone())) 155 | .and_then(run); 156 | let routes = serve_static().or(overview).or(api).or(open_api); 157 | warp::serve(routes).run(([0, 0, 0, 0], 3030)).await; 158 | } 159 | -------------------------------------------------------------------------------- /src/cli/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | config::{Config, ContentArrange, Lang, Profile, SslMode, TableStyle}, 3 | mysql::Session, 4 | output::Format, 5 | utils::read_file, 6 | }; 7 | use crate::{fl, query::QueryPlan}; 8 | use anyhow::{anyhow, Context, Result}; 9 | use http::serve_plan; 10 | use std::{collections::HashMap, io::Write}; 11 | use structopt::StructOpt; 12 | 13 | mod http; 14 | pub mod shell; 15 | 16 | #[cfg_attr(feature = "zh-CN", doc = "数据库连接工具")] 17 | #[cfg_attr(feature = "en-US", doc = "database connection manage tool")] 18 | #[derive(Debug, StructOpt)] 19 | #[structopt(name = "dcli")] 20 | pub enum DCliCommand { 21 | #[cfg_attr(feature = "zh-CN", doc = "配置相关命令 profile 命令别名")] 22 | #[cfg_attr(feature = "en-US", doc = "profile commands alias")] 23 | P { 24 | #[structopt(subcommand)] 25 | cmd: ProfileCmd, 26 | }, 27 | #[cfg_attr(feature = "zh-CN", doc = "配置相关命令")] 28 | #[cfg_attr(feature = "en-US", doc = "profile command")] 29 | Profile { 30 | #[structopt(subcommand)] 31 | cmd: ProfileCmd, 32 | }, 33 | #[cfg_attr(feature = "zh-CN", doc = "显示样式相关命令")] 34 | #[cfg_attr(feature = "en-US", doc = "style commands")] 35 | Style { 36 | #[structopt(subcommand)] 37 | cmd: StyleCmd, 38 | }, 39 | #[cfg_attr(feature = "zh-CN", doc = "使用 `mysql` 命令连接到 mysql")] 40 | #[cfg_attr(feature = "en-US", doc = "use `mysql` connect to mysql server")] 41 | Conn { 42 | #[cfg_attr(feature = "zh-CN", doc = "连接配置名称")] 43 | #[cfg_attr(feature = "en-US", doc = "profile name")] 44 | profile: String, 45 | #[cfg_attr(feature = "zh-CN", doc = "mysql 命令额外参数, 使用 -e=`` 传递参数")] 46 | #[cfg_attr( 47 | feature = "en-US", 48 | doc = "mysql extra arguments, use -e=`` to pass arguments" 49 | )] 50 | #[structopt(short, long)] 51 | extra: Vec, 52 | }, 53 | #[cfg_attr(feature = "zh-CN", doc = "使用一个配置运行命令")] 54 | #[cfg_attr(feature = "en-US", doc = "use a profile to exec sql")] 55 | Exec { 56 | #[cfg_attr(feature = "zh-CN", doc = "连接配置名称")] 57 | #[cfg_attr(feature = "en-US", doc = "profile name")] 58 | #[structopt(short, long)] 59 | profile: String, 60 | 61 | #[cfg_attr( 62 | feature = "zh-CN", 63 | doc = "命令 使用 @<文件路径> 读取 SQL 文件内容作为输入" 64 | )] 65 | #[cfg_attr( 66 | feature = "en-US", 67 | doc = "sql, use @ to read SQL file as input" 68 | )] 69 | command: Vec, 70 | #[cfg_attr(feature = "zh-CN", doc = "是否垂直打印数据")] 71 | #[cfg_attr(feature = "en-US", doc = "print table in vertical form")] 72 | #[structopt(long, short = "G")] 73 | vertical: bool, 74 | }, 75 | 76 | #[cfg_attr(feature = "zh-CN", doc = "导出查询结果")] 77 | #[cfg_attr(feature = "en-US", doc = "export query output")] 78 | Export { 79 | #[cfg_attr(feature = "zh-CN", doc = "连接配置名称")] 80 | #[cfg_attr(feature = "en-US", doc = "profile name")] 81 | #[structopt(short, long)] 82 | profile: String, 83 | 84 | #[cfg_attr(feature = "zh-CN", doc = "输出格式: csv, json, yaml, toml, pickle")] 85 | #[cfg_attr( 86 | feature = "en-US", 87 | doc = "output format: csv, json, yaml, toml, pickle" 88 | )] 89 | #[structopt(short, long, default_value = "csv")] 90 | format: Format, 91 | 92 | #[cfg_attr( 93 | feature = "zh-CN", 94 | doc = "命令 使用 @<文件路径> 读取 SQL 文件内容作为输入" 95 | )] 96 | #[cfg_attr( 97 | feature = "en-US", 98 | doc = "sql, use @ to read SQL file as input" 99 | )] 100 | command: Vec, 101 | }, 102 | 103 | #[cfg_attr(feature = "zh-CN", doc = "运行连接到 mysql 的 shell")] 104 | #[cfg_attr(feature = "en-US", doc = "launch shell connected to mysql")] 105 | Shell { 106 | #[cfg_attr(feature = "zh-CN", doc = "连接配置名称")] 107 | #[cfg_attr(feature = "en-US", doc = "profile name")] 108 | #[structopt(short, long)] 109 | profile: String, 110 | }, 111 | 112 | #[cfg_attr(feature = "zh-CN", doc = "运行一个 HTTP 服务器以展示,下载数据")] 113 | #[cfg_attr( 114 | feature = "en-US", 115 | doc = "run a HTTP server to display or download data" 116 | )] 117 | Serve { 118 | #[cfg_attr(feature = "zh-CN", doc = "连接配置名称")] 119 | #[cfg_attr(feature = "en-US", doc = "profile name")] 120 | #[structopt(short, long)] 121 | profile: String, 122 | 123 | #[cfg_attr(feature = "zh-CN", doc = "port 1 ~ 65535")] 124 | #[cfg_attr(feature = "en-US", doc = "port 1 ~ 65535")] 125 | #[structopt(default_value = "3030", short = "P", long)] 126 | port: u16, 127 | 128 | #[cfg_attr( 129 | feature = "zh-CN", 130 | doc = "命令 使用 @<文件路径> 读取 SQL 文件内容作为输入" 131 | )] 132 | #[cfg_attr( 133 | feature = "en-US", 134 | doc = "sql, use @ to read SQL file as input" 135 | )] 136 | command: Vec, 137 | }, 138 | 139 | Plan { 140 | plan: String, 141 | }, 142 | } 143 | 144 | #[derive(Debug, StructOpt)] 145 | pub enum ProfileCmd { 146 | #[cfg_attr(feature = "zh-CN", doc = "列出所有配置")] 147 | #[cfg_attr(feature = "en-US", doc = "list all")] 148 | List, 149 | #[cfg_attr(feature = "zh-CN", doc = "添加一个配置")] 150 | #[cfg_attr(feature = "en-US", doc = "add a profile")] 151 | Add { 152 | #[cfg_attr(feature = "zh-CN", doc = "连接配置名称")] 153 | #[cfg_attr(feature = "en-US", doc = "profile name")] 154 | name: String, 155 | 156 | #[cfg_attr(feature = "zh-CN", doc = "是否强制覆盖")] 157 | #[cfg_attr(feature = "en-US", doc = "force override")] 158 | #[structopt(short, long)] 159 | force: bool, 160 | 161 | #[structopt(flatten)] 162 | profile: Profile, 163 | }, 164 | #[cfg_attr(feature = "zh-CN", doc = "删除一个配置")] 165 | #[cfg_attr(feature = "en-US", doc = "delete a profile")] 166 | Del { 167 | #[cfg_attr(feature = "zh-CN", doc = "连接配置名称")] 168 | #[cfg_attr(feature = "en-US", doc = "profile name")] 169 | profile: String, 170 | }, 171 | #[cfg_attr(feature = "zh-CN", doc = "修改已有配置")] 172 | #[cfg_attr(feature = "en-US", doc = "edit a profile")] 173 | Set { 174 | #[cfg_attr(feature = "zh-CN", doc = "连接配置名称")] 175 | #[cfg_attr(feature = "en-US", doc = "profile name")] 176 | name: String, 177 | 178 | #[cfg_attr(feature = "zh-CN", doc = "数据库 hostname, IPv6地址请使用'[]'包围")] 179 | #[cfg_attr( 180 | feature = "en-US", 181 | doc = "database hostname, IPv6 should be surrounded by '[]'" 182 | )] 183 | #[structopt(short = "h", long)] 184 | host: Option, 185 | 186 | #[cfg_attr(feature = "zh-CN", doc = "数据库 port 1 ~ 65535")] 187 | #[cfg_attr(feature = "en-US", doc = "database port 1 ~ 65535")] 188 | #[structopt(short = "P", long)] 189 | port: Option, 190 | 191 | #[cfg_attr(feature = "zh-CN", doc = "数据库名称")] 192 | #[cfg_attr(feature = "en-US", doc = "database name")] 193 | #[structopt(short, long)] 194 | db: Option, 195 | 196 | #[cfg_attr(feature = "zh-CN", doc = "用户名")] 197 | #[cfg_attr(feature = "en-US", doc = "user name")] 198 | #[structopt(short, long)] 199 | user: Option, 200 | 201 | #[cfg_attr(feature = "zh-CN", doc = "密码")] 202 | #[cfg_attr(feature = "en-US", doc = "password")] 203 | #[structopt(short = "pass", long)] 204 | password: Option, 205 | 206 | #[cfg_attr(feature = "zh-CN", doc = "SSL 模式")] 207 | #[cfg_attr(feature = "en-US", doc = "SSL Mode")] 208 | #[structopt(long)] 209 | ssl_mode: Option, 210 | 211 | #[cfg_attr(feature = "zh-CN", doc = "SSL CA 文件路径")] 212 | #[cfg_attr(feature = "en-US", doc = "SSL CA file path")] 213 | #[structopt(long, parse(from_os_str))] 214 | ssl_ca: Option, 215 | }, 216 | } 217 | 218 | #[derive(Debug, StructOpt)] 219 | pub enum StyleCmd { 220 | #[cfg_attr(feature = "zh-CN", doc = "配置打印表格样式")] 221 | #[cfg_attr(feature = "en-US", doc = "config table style")] 222 | Table { 223 | #[cfg_attr( 224 | feature = "zh-CN", 225 | doc = "选项: AsciiFull AsciiMd Utf8Full Utf8HBorderOnly" 226 | )] 227 | #[cfg_attr( 228 | feature = "en-US", 229 | doc = "choices: AsciiFull AsciiMd Utf8Full Utf8HBorderOnly" 230 | )] 231 | #[structopt(long)] 232 | style: Option, 233 | #[cfg_attr( 234 | feature = "zh-CN", 235 | doc = "宽度样式 选项: disabled, dynamic, dynamic-full-width" 236 | )] 237 | #[cfg_attr( 238 | feature = "en-US", 239 | doc = "content width style, options: disabled, dynamic, dynamic-full-width" 240 | )] 241 | #[structopt(long)] 242 | arrange: Option, 243 | }, 244 | #[cfg_attr(feature = "zh-CN", doc = "设置语言")] 245 | #[cfg_attr(feature = "en-US", doc = "set language")] 246 | Lang { 247 | #[cfg_attr(feature = "zh-CN", doc = "语言, 可选 en-US, zh-CN")] 248 | #[cfg_attr(feature = "en-US", doc = "lang, options: en-US, zh-CN")] 249 | name: Option, 250 | }, 251 | } 252 | 253 | impl DCliCommand { 254 | pub async fn run(&self, config: &mut Config) -> Result<()> { 255 | match self { 256 | DCliCommand::Style { cmd } => { 257 | match cmd { 258 | StyleCmd::Table { style, arrange } => { 259 | if let Some(t_style) = style { 260 | config.table_style = t_style.clone() 261 | } 262 | if let Some(arr) = arrange { 263 | config.arrangement = arr.clone() 264 | } 265 | config.save()?; 266 | } 267 | StyleCmd::Lang { name } => { 268 | config.lang = name.clone(); 269 | config.save()?; 270 | } 271 | }; 272 | Ok(()) 273 | } 274 | DCliCommand::Conn { profile, extra } => { 275 | let profile = config.try_get_profile(profile)?; 276 | let mut sys_cmd = profile.cmd(false, extra); 277 | let child = sys_cmd 278 | .spawn() 279 | .with_context(|| fl!("launch-process-failed"))?; 280 | child.wait_with_output().unwrap(); 281 | Ok(()) 282 | } 283 | DCliCommand::Exec { 284 | profile, 285 | command, 286 | vertical, 287 | } => { 288 | let profile = config.try_get_profile(profile)?; 289 | let session = Session::connect_with(&profile).await?; 290 | let to_execute = if command.len() == 1 && command.first().unwrap().starts_with('@') 291 | { 292 | read_file(&command.first().unwrap()[1..])? 293 | } else { 294 | command.join(" ") 295 | }; 296 | for sql in to_execute.split(';') { 297 | if !sql.is_empty() { 298 | let output = session.query(sql).await?; 299 | output.to_print_table(&config, vertical.clone()); 300 | } 301 | } 302 | session.close().await; 303 | Ok(()) 304 | } 305 | DCliCommand::Export { 306 | profile, 307 | command, 308 | format, 309 | } => { 310 | let profile = config.try_get_profile(profile)?; 311 | let session = Session::connect_with(&profile).await?; 312 | let to_execute = if command.len() == 1 && command.first().unwrap().starts_with('@') 313 | { 314 | read_file(&command.first().unwrap()[1..])? 315 | } else { 316 | command.join(" ") 317 | }; 318 | let to_execute = to_execute 319 | .split(';') 320 | .filter(|sql| !sql.is_empty()) 321 | .collect::>(); 322 | if to_execute.is_empty() { 323 | return Err(anyhow!(fl!("empty-input"))); 324 | } else if to_execute.len() > 1 { 325 | return Err(anyhow!(fl!("too-many-input"))); 326 | } else { 327 | let output = session.query(to_execute.first().unwrap()).await?; 328 | match format { 329 | Format::Csv => { 330 | let out = output.to_csv()?; 331 | println!("{}", out); 332 | } 333 | Format::Json => { 334 | let out = output.to_json()?; 335 | println!("{}", out); 336 | } 337 | Format::Yaml => { 338 | let out = output.to_yaml()?; 339 | println!("{}", out); 340 | } 341 | Format::Toml => { 342 | let out = output.to_toml()?; 343 | println!("{}", out); 344 | } 345 | Format::Pickle => { 346 | let mut stdout = std::io::stdout(); 347 | stdout.write_all(&output.to_pickle()?)?; 348 | stdout.flush()?; 349 | } 350 | } 351 | Ok(()) 352 | } 353 | } 354 | DCliCommand::Profile { cmd } | DCliCommand::P { cmd } => { 355 | match cmd { 356 | ProfileCmd::List => { 357 | let mut table = config.new_table(); 358 | table.set_header(vec!["name", "user", "host", "port", "database", "uri"]); 359 | for (p_name, profile) in &config.profiles { 360 | table.add_row(vec![ 361 | p_name, 362 | &profile.user.clone().unwrap_or_default(), 363 | &profile.host, 364 | &profile.port.to_string(), 365 | &profile.db.clone(), 366 | &profile.uri(), 367 | ]); 368 | } 369 | println!("{}", table); 370 | } 371 | ProfileCmd::Add { 372 | name, 373 | force, 374 | profile, 375 | } => { 376 | if config.try_get_profile(name).is_ok() { 377 | if !force { 378 | return Err(anyhow!(fl!("profile-existed", name = name.clone()))); 379 | } 380 | } else { 381 | let mut cp = profile.clone(); 382 | cp.name = name.clone(); 383 | config.profiles.insert(name.clone(), cp); 384 | config.save()?; 385 | println!("{}", fl!("profile-saved")); 386 | } 387 | } 388 | ProfileCmd::Del { profile } => { 389 | let deleted = config.profiles.remove(profile); 390 | if deleted.is_none() { 391 | return Err(anyhow!(fl!("profile-saved"))); 392 | } else { 393 | config.save()?; 394 | println!("{}", fl!("profile-deleted")); 395 | } 396 | } 397 | ProfileCmd::Set { 398 | name, 399 | host, 400 | port, 401 | db, 402 | user, 403 | password, 404 | ssl_mode, 405 | ssl_ca, 406 | } => { 407 | let mut profile = config.try_get_profile(name)?.clone(); 408 | if let Some(host) = host { 409 | profile.host = host.to_string(); 410 | } 411 | if let Some(port) = port { 412 | profile.port = *port; 413 | } 414 | if let Some(db) = db { 415 | profile.db = db.clone() 416 | } 417 | if user.is_some() { 418 | profile.user = user.clone() 419 | } 420 | if password.is_some() { 421 | profile.password = password.clone() 422 | } 423 | if ssl_mode.is_some() { 424 | profile.ssl_mode = ssl_mode.clone() 425 | } 426 | if ssl_ca.is_some() { 427 | profile.ssl_ca = ssl_ca.clone() 428 | } 429 | config.try_set_profile(name, profile)?; 430 | config.save()?; 431 | println!("{}", fl!("profile-updated", name = name.clone())); 432 | } 433 | } 434 | Ok(()) 435 | } 436 | DCliCommand::Shell { profile } => shell::Shell::run(config, profile).await, 437 | DCliCommand::Serve { 438 | profile, 439 | port, 440 | command, 441 | } => { 442 | let profile = config.try_get_profile(profile)?; 443 | let session = Session::connect_with(&profile).await?; 444 | let to_execute = if command.len() == 1 && command.first().unwrap().starts_with('@') 445 | { 446 | read_file(&command.first().unwrap()[1..])? 447 | } else { 448 | command.join(" ") 449 | }; 450 | let to_execute = to_execute 451 | .split(';') 452 | .filter(|sql| !sql.is_empty()) 453 | .collect::>(); 454 | if to_execute.is_empty() { 455 | return Err(anyhow!(fl!("empty-input"))); 456 | } else if to_execute.len() > 1 { 457 | return Err(anyhow!(fl!("too-many-input"))); 458 | } else { 459 | let output = session.query(to_execute.first().unwrap()).await?; 460 | http::serve(*port, output).await; 461 | Ok(()) 462 | } 463 | } 464 | DCliCommand::Plan { plan } => { 465 | let content = read_file(plan)?; 466 | let plan: QueryPlan = toml::from_str(&content)?; 467 | let mut plan_sessions: HashMap = HashMap::new(); 468 | for p in plan.profiles() { 469 | if let Ok(profile) = config.try_get_profile(&p) { 470 | let session = Session::connect_with(profile).await?; 471 | plan_sessions.insert(p, session); 472 | } 473 | } 474 | serve_plan(plan, plan_sessions).await; 475 | Ok(()) 476 | } 477 | } 478 | } 479 | } 480 | -------------------------------------------------------------------------------- /src/cli/shell/helper.rs: -------------------------------------------------------------------------------- 1 | use colored::*; 2 | use sqlparser::dialect::MySqlDialect; 3 | use std::{ 4 | borrow::Cow::{self, Borrowed, Owned}, 5 | collections::{HashMap, HashSet}, 6 | }; 7 | 8 | use crate::mysql::Session; 9 | 10 | use super::highlight::{MonoKaiSchema, SQLHighLight, Schema}; 11 | use rustyline::completion::{Completer, Pair}; 12 | use rustyline::config::OutputStreamType; 13 | use rustyline::error::ReadlineError; 14 | use rustyline::highlight::Highlighter; 15 | use rustyline::hint::Hinter; 16 | use rustyline::validate::{self, Validator}; 17 | use rustyline::{CompletionType, Config, Context, EditMode, Editor}; 18 | use rustyline_derive::Helper; 19 | 20 | #[derive(Helper)] 21 | pub struct MyHelper { 22 | pub databases: HashSet, 23 | pub tables: HashSet, 24 | pub columns: HashMap>, 25 | pub highlighter: DBHighlighter, 26 | pub colored_prompt: String, 27 | } 28 | 29 | #[derive(Debug)] 30 | pub struct DBHighlighter {} 31 | 32 | impl Highlighter for DBHighlighter { 33 | fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> { 34 | let dialect = MySqlDialect {}; 35 | let schema = MonoKaiSchema {}; 36 | let rendered = match sqlparser::tokenizer::Tokenizer::new(&dialect, line).tokenize() { 37 | Ok(tokens) => tokens 38 | .iter() 39 | .map(|t| t.render(&schema)) 40 | .collect::>() 41 | .join(""), 42 | Err(_) => line.to_string(), 43 | }; 44 | Owned(rendered) 45 | } 46 | 47 | fn highlight_prompt<'b, 's: 'b, 'p: 'b>( 48 | &'s self, 49 | prompt: &'p str, 50 | _default: bool, 51 | ) -> Cow<'b, str> { 52 | let mut copy = prompt.to_owned(); 53 | copy.replace_range(.., &"HIGT".red().to_string()); 54 | Owned(copy) 55 | } 56 | 57 | fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { 58 | Borrowed(hint) 59 | } 60 | 61 | fn highlight_candidate<'c>( 62 | &self, 63 | candidate: &'c str, 64 | completion: CompletionType, 65 | ) -> Cow<'c, str> { 66 | let _ = completion; 67 | Borrowed(candidate) 68 | } 69 | 70 | fn highlight_char(&self, _line: &str, _pos: usize) -> bool { 71 | true 72 | } 73 | } 74 | 75 | impl Completer for MyHelper { 76 | type Candidate = Pair; 77 | 78 | fn complete( 79 | &self, 80 | line: &str, 81 | pos: usize, 82 | _ctx: &Context<'_>, 83 | ) -> Result<(usize, Vec), ReadlineError> { 84 | let pattern = line[..pos] 85 | .split_ascii_whitespace() 86 | .last() 87 | .unwrap_or(&line[..pos]); 88 | let mut pairs: Vec = vec![]; 89 | 90 | for kw in crate::mysql::KEYWORDS.iter() { 91 | if kw.starts_with(&pattern.to_ascii_uppercase()) { 92 | pairs.push(Pair { 93 | display: format!("{} {}", "[KEY]".color(MonoKaiSchema::red()), kw), 94 | replacement: kw.to_string(), 95 | }) 96 | } 97 | } 98 | 99 | for db in self.databases.iter() { 100 | if db.contains(pattern) { 101 | pairs.push(Pair { 102 | display: format!("{} {}", "[DB]".color(MonoKaiSchema::cyan()), db), 103 | replacement: db.to_string(), 104 | }) 105 | } 106 | } 107 | 108 | for tab in self.tables.iter() { 109 | if tab.contains(pattern) { 110 | pairs.push(Pair { 111 | display: format!("{} {}", "[TABLE]".color(MonoKaiSchema::purple()), tab), 112 | replacement: tab.to_string(), 113 | }) 114 | } 115 | } 116 | 117 | for (tab, cols) in self.columns.iter() { 118 | for col in cols.iter() { 119 | if col.contains(pattern) { 120 | pairs.push(Pair { 121 | display: format!( 122 | "{} {}.{}", 123 | "[COL]".color(MonoKaiSchema::blue()), 124 | tab, 125 | col 126 | ), 127 | replacement: col.to_string(), 128 | }) 129 | } 130 | } 131 | } 132 | 133 | let idx = line.find(pattern).unwrap_or(0); 134 | Ok((idx, pairs)) 135 | } 136 | } 137 | 138 | impl Hinter for MyHelper { 139 | type Hint = String; 140 | 141 | fn hint(&self, _line: &str, _pos: usize, _ctx: &Context<'_>) -> Option { 142 | None 143 | } 144 | } 145 | 146 | impl Highlighter for MyHelper { 147 | fn highlight_prompt<'b, 's: 'b, 'p: 'b>( 148 | &'s self, 149 | prompt: &'p str, 150 | default: bool, 151 | ) -> Cow<'b, str> { 152 | if default { 153 | Borrowed(&self.colored_prompt) 154 | } else { 155 | Borrowed(prompt) 156 | } 157 | } 158 | 159 | fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { 160 | Owned("\x1b[1m".to_owned() + hint + "\x1b[m") 161 | } 162 | 163 | fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { 164 | self.highlighter.highlight(line, pos) 165 | } 166 | 167 | fn highlight_char(&self, line: &str, pos: usize) -> bool { 168 | self.highlighter.highlight_char(line, pos) 169 | } 170 | } 171 | 172 | impl Validator for MyHelper { 173 | fn validate( 174 | &self, 175 | ctx: &mut validate::ValidationContext, 176 | ) -> rustyline::Result { 177 | let input = ctx.input(); 178 | if !input.starts_with('%') { 179 | if input.chars().all(|c| c.is_whitespace()) || input.ends_with(';') { 180 | Ok(validate::ValidationResult::Valid(None)) 181 | } else { 182 | Ok(validate::ValidationResult::Incomplete) 183 | } 184 | } else { 185 | Ok(validate::ValidationResult::Valid(None)) 186 | } 187 | } 188 | } 189 | 190 | pub async fn get_editor(session: &mut Session) -> anyhow::Result> { 191 | let databases = session.all_databases().await?; 192 | let tables = session.all_tables().await?; 193 | let columns = session.all_columns(&tables).await?; 194 | let config = Config::builder() 195 | .history_ignore_space(true) 196 | .completion_type(CompletionType::List) 197 | .edit_mode(EditMode::Emacs) 198 | .output_stream(OutputStreamType::Stdout) 199 | .build(); 200 | let helper = MyHelper { 201 | databases, 202 | tables, 203 | columns, 204 | highlighter: DBHighlighter {}, 205 | colored_prompt: "".to_string(), 206 | }; 207 | let mut rl = Editor::with_config(config); 208 | rl.set_helper(Some(helper)); 209 | Ok(rl) 210 | } 211 | -------------------------------------------------------------------------------- /src/cli/shell/highlight.rs: -------------------------------------------------------------------------------- 1 | use colored::*; 2 | use sqlparser::{ 3 | dialect::keywords::Keyword, 4 | tokenizer::{Token, Word}, 5 | }; 6 | 7 | pub trait SQLHighLight { 8 | fn render(&self, schema: &S) -> String; 9 | } 10 | 11 | impl SQLHighLight for Token { 12 | fn render(&self, schema: &S) -> String { 13 | match self { 14 | Token::EOF => "EOF".color(S::red()).to_string(), 15 | Token::Word(w) => w.render(schema), 16 | Token::Number(n) => n.color(S::green()).to_string(), 17 | Token::Char(c) => c.to_string(), 18 | Token::SingleQuotedString(s) => { 19 | format!("'{}'", s.color(S::bright_yellow()).to_string()) 20 | } 21 | Token::NationalStringLiteral(s) => { 22 | format!("N'{}'", s.color(S::bright_yellow()).to_string()) 23 | } 24 | Token::HexStringLiteral(s) => format!("X'{}'", s.color(S::bright_yellow()).to_string()), 25 | _ => self.to_string(), 26 | } 27 | } 28 | } 29 | 30 | impl SQLHighLight for Word { 31 | fn render(&self, _schema: &S) -> String { 32 | match self.keyword { 33 | Keyword::NoKeyword => self.to_string().color(S::blue()).to_string(), 34 | _ => self.value.color(S::green()).to_string(), 35 | } 36 | } 37 | } 38 | 39 | pub trait Schema { 40 | fn black() -> Color; 41 | fn red() -> Color; 42 | fn green() -> Color; 43 | fn yellow() -> Color; 44 | fn blue() -> Color; 45 | fn purple() -> Color; 46 | fn cyan() -> Color; 47 | fn white() -> Color; 48 | fn bright_black() -> Color; 49 | fn bright_red() -> Color; 50 | fn bright_green() -> Color; 51 | fn bright_yellow() -> Color; 52 | fn bright_blue() -> Color; 53 | fn bright_purple() -> Color; 54 | fn bright_cyan() -> Color; 55 | fn bright_white() -> Color; 56 | fn background() -> Color; 57 | fn foreground() -> Color; 58 | } 59 | 60 | #[derive(Debug, Clone, Copy)] 61 | pub struct MonoKaiSchema; 62 | 63 | // TODO use macro to reduce code 64 | impl Schema for MonoKaiSchema { 65 | fn black() -> Color { 66 | Color::TrueColor { r: 0, g: 0, b: 0 } 67 | } 68 | 69 | fn red() -> Color { 70 | Color::TrueColor { 71 | r: 216, 72 | g: 30, 73 | b: 0, 74 | } 75 | } 76 | 77 | fn green() -> Color { 78 | Color::TrueColor { 79 | r: 94, 80 | g: 167, 81 | b: 2, 82 | } 83 | } 84 | 85 | fn yellow() -> Color { 86 | Color::TrueColor { 87 | r: 207, 88 | g: 174, 89 | b: 0, 90 | } 91 | } 92 | 93 | fn blue() -> Color { 94 | Color::TrueColor { 95 | r: 66, 96 | g: 122, 97 | b: 179, 98 | } 99 | } 100 | 101 | fn purple() -> Color { 102 | Color::TrueColor { 103 | r: 137, 104 | g: 101, 105 | b: 142, 106 | } 107 | } 108 | 109 | fn cyan() -> Color { 110 | Color::TrueColor { 111 | r: 0, 112 | g: 167, 113 | b: 170, 114 | } 115 | } 116 | 117 | fn white() -> Color { 118 | Color::TrueColor { 119 | r: 219, 120 | g: 222, 121 | b: 216, 122 | } 123 | } 124 | 125 | fn bright_black() -> Color { 126 | Color::TrueColor { 127 | r: 104, 128 | g: 106, 129 | b: 102, 130 | } 131 | } 132 | 133 | fn bright_red() -> Color { 134 | Color::TrueColor { 135 | r: 245, 136 | g: 66, 137 | b: 53, 138 | } 139 | } 140 | 141 | fn bright_green() -> Color { 142 | Color::TrueColor { 143 | r: 253, 144 | g: 235, 145 | b: 97, 146 | } 147 | } 148 | 149 | fn bright_yellow() -> Color { 150 | Color::TrueColor { 151 | r: 253, 152 | g: 235, 153 | b: 97, 154 | } 155 | } 156 | 157 | fn bright_blue() -> Color { 158 | Color::TrueColor { 159 | r: 132, 160 | g: 176, 161 | b: 216, 162 | } 163 | } 164 | 165 | fn bright_purple() -> Color { 166 | Color::TrueColor { 167 | r: 188, 168 | g: 148, 169 | b: 183, 170 | } 171 | } 172 | 173 | fn bright_cyan() -> Color { 174 | Color::TrueColor { 175 | r: 55, 176 | g: 230, 177 | b: 232, 178 | } 179 | } 180 | 181 | fn bright_white() -> Color { 182 | Color::TrueColor { 183 | r: 241, 184 | g: 241, 185 | b: 240, 186 | } 187 | } 188 | 189 | fn background() -> Color { 190 | Color::TrueColor { 191 | r: 40, 192 | g: 42, 193 | b: 58, 194 | } 195 | } 196 | 197 | fn foreground() -> Color { 198 | Color::TrueColor { 199 | r: 234, 200 | g: 242, 201 | b: 241, 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/cli/shell/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{config::Config, utils::read_file}; 2 | use crate::{fl, mysql::Session}; 3 | use anyhow::Context; 4 | use colored::*; 5 | use highlight::{MonoKaiSchema, Schema}; 6 | use rustyline::error::ReadlineError; 7 | use structopt::StructOpt; 8 | 9 | mod helper; 10 | mod highlight; 11 | 12 | #[derive(Debug)] 13 | pub struct Shell; 14 | 15 | #[cfg_attr(feature = "zh-CN", doc = "DCli 内建命令")] 16 | #[cfg_attr(feature = "en-US", doc = "Dcli builtin commands")] 17 | #[derive(Debug, StructOpt)] 18 | #[structopt(name = "DBuiltin")] 19 | pub enum BuiltIn { 20 | #[cfg_attr(feature = "zh-CN", doc = "退出 shell")] 21 | #[cfg_attr(feature = "en-US", doc = "exit shell")] 22 | #[structopt(name = "%exit")] 23 | Exit, 24 | 25 | #[cfg_attr(feature = "zh-CN", doc = "打印帮助信息")] 26 | #[cfg_attr(feature = "en-US", doc = "print help message")] 27 | #[structopt(name = "%help")] 28 | Help, 29 | 30 | #[cfg_attr(feature = "zh-CN", doc = "查看历史")] 31 | #[cfg_attr(feature = "en-US", doc = "list history")] 32 | #[structopt(name = "%his")] 33 | His, 34 | 35 | #[cfg_attr(feature = "zh-CN", doc = "运行 SQL 文件")] 36 | #[cfg_attr(feature = "en-US", doc = "exec SQL file")] 37 | #[structopt(name = "%run")] 38 | Run { 39 | #[cfg_attr(feature = "zh-CN", doc = "文件路径")] 40 | #[cfg_attr(feature = "en-US", doc = "path file")] 41 | path: String, 42 | }, 43 | } 44 | 45 | impl Shell { 46 | pub async fn run(config: &mut Config, profile: &str) -> anyhow::Result<()> { 47 | let profile = config.try_get_profile(profile)?; 48 | let history = profile.load_or_create_history()?; 49 | let mut session = Session::connect_with(&profile).await?; 50 | let mut rl = helper::get_editor(&mut session).await?; 51 | let mut count: usize = 1; 52 | rl.load_history(&history) 53 | .with_context(|| fl!("load-his-failed"))?; 54 | loop { 55 | let p = format!("[{}]: ", count) 56 | .color(MonoKaiSchema::green()) 57 | .to_string(); 58 | rl.helper_mut().unwrap().colored_prompt = p.clone(); 59 | let input = rl.readline(&p); 60 | match input { 61 | Ok(line) => { 62 | if !line.is_empty() { 63 | match Shell::take_builtin(&line) { 64 | Ok(maybe_builtin) => { 65 | if let Some(builtin) = maybe_builtin { 66 | match builtin { 67 | BuiltIn::Exit => { 68 | println!("Exit..."); 69 | break; 70 | } 71 | BuiltIn::Help => { 72 | BuiltIn::clap().print_help().unwrap(); 73 | } 74 | BuiltIn::His => { 75 | rl.history() 76 | .iter() 77 | .enumerate() 78 | .for_each(|(i, h)| println!("{} {}", i, h)); 79 | } 80 | BuiltIn::Run { path } => match read_file(&path) { 81 | Ok(content) => { 82 | for sql in content.split(';') { 83 | if !sql.is_empty() { 84 | let output = session.query(sql).await?; 85 | output.to_print_table(&config, false); 86 | } 87 | } 88 | } 89 | Err(e) => { 90 | println!("{:?}", e); 91 | } 92 | }, 93 | } 94 | rl.add_history_entry(line.as_str()); 95 | } else { 96 | match session.query(&line).await { 97 | Ok(output) => { 98 | output.to_print_table(config, false); 99 | rl.add_history_entry(line.as_str()); 100 | } 101 | Err(e) => { 102 | println!("Server Err: {}", e) 103 | } 104 | } 105 | } 106 | } 107 | Err(e) => { 108 | println!("{}", e) 109 | } 110 | } 111 | } else { 112 | println!(); 113 | } 114 | } 115 | Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => { 116 | println!("{}", fl!("exit-info")); 117 | } 118 | Err(err) => { 119 | println!("Error: {:?}", err); 120 | break; 121 | } 122 | } 123 | count += 1; 124 | } 125 | session.close().await; 126 | rl.append_history(&history).unwrap(); 127 | Ok(()) 128 | } 129 | 130 | fn take_builtin(line: &str) -> anyhow::Result> { 131 | if line.starts_with('%') { 132 | let builtin = 133 | BuiltIn::from_iter_safe(format!("builtin {}", line).split_ascii_whitespace()) 134 | .map_err(|mut e| { 135 | e.message = e 136 | .message 137 | .replace("builtin ", "%") 138 | .replace("builtin --", ""); 139 | e 140 | })?; 141 | Ok(Some(builtin)) 142 | } else { 143 | Ok(None) 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Context, Result}; 2 | use comfy_table::{ 3 | presets::{ASCII_FULL, ASCII_MARKDOWN, UTF8_FULL, UTF8_HORIZONTAL_BORDERS_ONLY}, 4 | ContentArrangement, Table, 5 | }; 6 | use serde::{Deserialize, Serialize}; 7 | use std::{ 8 | collections::HashMap, 9 | fs::File, 10 | io::{Read, Write}, 11 | path::PathBuf, 12 | process::Stdio, 13 | str::FromStr, 14 | }; 15 | use structopt::StructOpt; 16 | 17 | use crate::fl; 18 | 19 | #[derive(Debug, Default, Serialize, Deserialize)] 20 | pub struct Config { 21 | pub profiles: HashMap, 22 | pub table_style: TableStyle, 23 | pub lang: Option, 24 | #[serde(default)] 25 | pub arrangement: ContentArrange, 26 | pub debug: bool, 27 | } 28 | 29 | #[derive(Debug, Clone, Serialize, Deserialize)] 30 | pub enum ContentArrange { 31 | Disabled, 32 | Dynamic, 33 | DynamicFullWidth, 34 | } 35 | 36 | impl Default for ContentArrange { 37 | fn default() -> Self { 38 | ContentArrange::Dynamic 39 | } 40 | } 41 | 42 | impl FromStr for ContentArrange { 43 | type Err = anyhow::Error; 44 | 45 | fn from_str(s: &str) -> Result { 46 | let lower = s.to_ascii_lowercase(); 47 | let arr = match lower.as_str() { 48 | "disabled" => Ok(ContentArrange::Disabled), 49 | "dynamic" => Ok(ContentArrange::Disabled), 50 | "dynamic-full-width" => Ok(ContentArrange::Disabled), 51 | _ => Err(anyhow!(fl!("invalid-value", val = s))), 52 | }?; 53 | Ok(arr) 54 | } 55 | } 56 | 57 | impl ToString for ContentArrange { 58 | fn to_string(&self) -> String { 59 | match self { 60 | ContentArrange::Disabled => "disabled".to_string(), 61 | ContentArrange::Dynamic => "dynamic".to_string(), 62 | ContentArrange::DynamicFullWidth => "dynamic-full-width".to_string(), 63 | } 64 | } 65 | } 66 | 67 | #[derive(Debug, Clone, Serialize, Deserialize)] 68 | pub enum Lang { 69 | #[serde(rename = "en-US")] 70 | EnUS, 71 | #[serde(rename = "zh-CN")] 72 | ZhCN, 73 | } 74 | 75 | impl Default for Lang { 76 | fn default() -> Self { 77 | Lang::EnUS 78 | } 79 | } 80 | 81 | impl FromStr for Lang { 82 | type Err = anyhow::Error; 83 | 84 | fn from_str(s: &str) -> Result { 85 | let lower = s.to_ascii_lowercase(); 86 | if lower.starts_with("en-us") { 87 | Ok(Lang::EnUS) 88 | } else if lower.starts_with("zh-cn") { 89 | Ok(Lang::ZhCN) 90 | } else { 91 | Err(anyhow!(fl!("invalid-value", val = s))) 92 | } 93 | } 94 | } 95 | 96 | impl ToString for Lang { 97 | fn to_string(&self) -> String { 98 | match self { 99 | Lang::EnUS => "en-US", 100 | Lang::ZhCN => "zh-CN", 101 | } 102 | .to_string() 103 | } 104 | } 105 | 106 | #[derive(Debug, Clone, Serialize, Deserialize, StructOpt)] 107 | pub struct Profile { 108 | #[structopt(skip)] 109 | pub name: String, 110 | 111 | #[cfg_attr(feature = "zh-CN", doc = "数据库 hostname, IPv6地址请使用'[]'包围")] 112 | #[cfg_attr( 113 | feature = "en-US", 114 | doc = "database hostname, IPv6 should be surrounded by '[]'" 115 | )] 116 | #[structopt(short = "h", long, default_value = "localhost")] 117 | pub host: String, 118 | 119 | #[cfg_attr(feature = "zh-CN", doc = "数据库 port 1 ~ 65535")] 120 | #[cfg_attr(feature = "en-US", doc = "database port 1 ~ 65535")] 121 | #[structopt(default_value = "3306", short = "P", long)] 122 | pub port: u16, 123 | 124 | #[cfg_attr(feature = "zh-CN", doc = "数据库名称")] 125 | #[cfg_attr(feature = "en-US", doc = "database name")] 126 | pub db: String, 127 | 128 | #[cfg_attr(feature = "zh-CN", doc = "用户名")] 129 | #[cfg_attr(feature = "en-US", doc = "user name")] 130 | #[structopt(short, long)] 131 | pub user: Option, 132 | 133 | #[cfg_attr(feature = "zh-CN", doc = "密码")] 134 | #[cfg_attr(feature = "en-US", doc = "password")] 135 | #[structopt(short = "pass", long)] 136 | pub password: Option, 137 | 138 | #[cfg_attr(feature = "zh-CN", doc = "SSL 模式")] 139 | #[cfg_attr(feature = "en-US", doc = "SSL Mode")] 140 | #[structopt(long)] 141 | pub ssl_mode: Option, 142 | 143 | #[cfg_attr(feature = "zh-CN", doc = "SSL CA 文件路径")] 144 | #[cfg_attr(feature = "en-US", doc = "SSL CA file path")] 145 | #[structopt(long, parse(from_os_str))] 146 | pub ssl_ca: Option, 147 | } 148 | 149 | #[derive(Debug, Clone, Serialize, Deserialize)] 150 | pub enum SslMode { 151 | Disabled, 152 | Preferred, 153 | Required, 154 | VerifyCa, 155 | VerifyIdentity, 156 | } 157 | 158 | impl Default for SslMode { 159 | fn default() -> Self { 160 | SslMode::Preferred 161 | } 162 | } 163 | 164 | impl FromStr for SslMode { 165 | type Err = anyhow::Error; 166 | 167 | fn from_str(s: &str) -> Result { 168 | let val = match &*s.to_ascii_lowercase() { 169 | "disabled" => SslMode::Disabled, 170 | "preferred" => SslMode::Preferred, 171 | "required" => SslMode::Required, 172 | "verify_ca" => SslMode::VerifyCa, 173 | "verify_identity" => SslMode::VerifyIdentity, 174 | _ => return Err(anyhow!(fl!("invalid-value", val = s))), 175 | }; 176 | Ok(val) 177 | } 178 | } 179 | 180 | #[derive(Debug, Clone, Serialize, Deserialize)] 181 | pub enum TableStyle { 182 | AsciiFull, 183 | AsciiMd, 184 | Utf8Full, 185 | Utf8HBorderOnly, 186 | } 187 | 188 | impl Default for TableStyle { 189 | fn default() -> Self { 190 | TableStyle::Utf8Full 191 | } 192 | } 193 | 194 | impl FromStr for TableStyle { 195 | type Err = anyhow::Error; 196 | 197 | fn from_str(s: &str) -> Result { 198 | let val = match &*s.to_ascii_lowercase() { 199 | "asciifull" => TableStyle::AsciiFull, 200 | "asciimd" => TableStyle::AsciiMd, 201 | "utf8full" => TableStyle::Utf8Full, 202 | "utf8hborderonly" => TableStyle::Utf8HBorderOnly, 203 | _ => return Err(anyhow!(fl!("invalid-value", val = s))), 204 | }; 205 | Ok(val) 206 | } 207 | } 208 | 209 | impl Profile { 210 | pub fn uri(&self) -> String { 211 | let mut uri = String::from("mysql://"); 212 | if let Some(user) = &self.user { 213 | uri.push_str(user) 214 | } 215 | if let Some(pass) = &self.password { 216 | uri.push_str(&format!(":{}", pass)) 217 | } 218 | if self.user.is_none() && self.password.is_none() { 219 | uri.push_str(&format!("{}:{}", self.host, self.port)); 220 | } else { 221 | uri.push_str(&format!("@{}:{}", self.host, self.port)); 222 | } 223 | uri.push_str(&format!("/{}", self.db)); 224 | uri 225 | } 226 | 227 | pub fn cmd(&self, piped: bool, args: &Vec) -> std::process::Command { 228 | let mut command = std::process::Command::new("mysql"); 229 | if piped { 230 | command 231 | .stdin(Stdio::piped()) 232 | .stdout(Stdio::piped()) 233 | .stderr(Stdio::piped()); 234 | } 235 | if let Some(user) = &self.user { 236 | command.args(&["--user", user]); 237 | } 238 | if let Some(pass) = &self.password { 239 | command.arg(&format!("--password={}", pass)); 240 | } 241 | command.args(&["--host", &self.host, "--port", &self.port.to_string()]); 242 | command.args(&["--database", &self.db]); 243 | command.args(args); 244 | command 245 | } 246 | 247 | pub fn load_or_create_history(&self) -> Result { 248 | let mut path = PathBuf::from(std::env::var("HOME").with_context(|| fl!("home-not-set"))?); 249 | path.push(".dcli"); 250 | path.push("history"); 251 | if !path.exists() { 252 | std::fs::create_dir_all(&path).with_context(|| fl!("create-his-dir-failed"))? 253 | } 254 | path.push(format!("{}_history.txt", self.name)); 255 | if !path.exists() { 256 | std::fs::File::create(&path) 257 | .with_context(|| fl!("create-his-file-failed", name = self.name.clone()))?; 258 | } 259 | Ok(path) 260 | } 261 | } 262 | 263 | impl Config { 264 | pub fn config_path() -> Result { 265 | let home = std::env::var("HOME").with_context(|| fl!("home-not-set"))?; 266 | let mut file = std::path::Path::new(&home).to_path_buf(); 267 | file.push(".config"); 268 | file.push("dcli.toml"); 269 | Ok(file.to_str().unwrap().to_string()) 270 | } 271 | 272 | pub fn load() -> Result { 273 | let path_str = Self::config_path()?; 274 | let file = std::path::Path::new(&path_str); 275 | if file.exists() { 276 | let mut content = String::new(); 277 | File::open(&file) 278 | .with_context(|| fl!("open-config-failed", file = file.to_str().unwrap_or("")))? 279 | .read_to_string(&mut content) 280 | .unwrap(); 281 | let config: Config = 282 | toml::from_str(&content).with_context(|| fl!("ser-config-failed"))?; 283 | Ok(config) 284 | } else { 285 | println!( 286 | "{}", 287 | fl!("create-config-file", file = file.to_str().unwrap()) 288 | ); 289 | let config = Self::default(); 290 | config.save()?; 291 | Ok(config) 292 | } 293 | } 294 | 295 | pub fn save(&self) -> Result<()> { 296 | let path = Self::config_path()?; 297 | let mut file = 298 | File::create(&path).with_context(|| fl!("open-config-failed", file = path))?; 299 | let tmp_value = toml::Value::try_from(self).unwrap(); 300 | let config_str = toml::to_string_pretty(&tmp_value).unwrap(); 301 | file.write_all(config_str.as_bytes()) 302 | .with_context(|| fl!("save-config-filed"))?; 303 | Ok(()) 304 | } 305 | 306 | pub fn new_table(&self) -> Table { 307 | let mut table = Table::new(); 308 | let preset = match self.table_style { 309 | TableStyle::AsciiFull => ASCII_FULL, 310 | TableStyle::AsciiMd => ASCII_MARKDOWN, 311 | TableStyle::Utf8Full => UTF8_FULL, 312 | TableStyle::Utf8HBorderOnly => UTF8_HORIZONTAL_BORDERS_ONLY, 313 | }; 314 | table.load_preset(preset); 315 | let arr = match self.arrangement { 316 | ContentArrange::Disabled => ContentArrangement::Disabled, 317 | ContentArrange::Dynamic => ContentArrangement::Dynamic, 318 | ContentArrange::DynamicFullWidth => ContentArrangement::DynamicFullWidth, 319 | }; 320 | table.set_content_arrangement(arr); 321 | table 322 | } 323 | 324 | pub fn try_get_profile(&self, name: &str) -> Result<&Profile> { 325 | if let Some(profile) = self.profiles.get(name) { 326 | Ok(profile) 327 | } else { 328 | let mut table = self.new_table(); 329 | table.set_header(vec!["name"]); 330 | self.profiles.keys().into_iter().for_each(|key| { 331 | table.add_row(vec![key]); 332 | }); 333 | let table_str = table.to_string(); 334 | Err(anyhow!(fl!( 335 | "profile-not-found", 336 | name = name, 337 | table = table_str 338 | ))) 339 | } 340 | } 341 | 342 | pub fn try_set_profile(&mut self, name: &str, new_profile: Profile) -> Result<()> { 343 | self.try_get_profile(name)?; 344 | self.profiles.insert(name.to_string(), new_profile); 345 | Ok(()) 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Mutex}; 2 | 3 | use anyhow::Result; 4 | use cli::DCliCommand; 5 | use i18n_embed::{ 6 | fluent::{fluent_language_loader, FluentLanguageLoader}, 7 | DesktopLanguageRequester, 8 | }; 9 | use once_cell::sync::Lazy; 10 | use rust_embed::RustEmbed; 11 | 12 | use config::Config; 13 | use log::LevelFilter; 14 | use log4rs::{ 15 | append::file::FileAppender, 16 | config::{Appender, Config as LogConfig, Root}, 17 | }; 18 | use structopt::StructOpt; 19 | 20 | #[derive(RustEmbed)] 21 | #[folder = "i18n"] 22 | struct Translations; 23 | 24 | pub mod cli; 25 | pub mod config; 26 | pub mod mysql; 27 | pub mod output; 28 | pub mod utils; 29 | pub mod query; 30 | 31 | pub static LOADER: Lazy>> = Lazy::new(|| { 32 | let translations = Translations {}; 33 | let language_loader: FluentLanguageLoader = fluent_language_loader!(); 34 | let requested_languages = DesktopLanguageRequester::requested_languages(); 35 | let _result = i18n_embed::select(&language_loader, &translations, &requested_languages); 36 | language_loader.set_use_isolating(false); 37 | Arc::new(Mutex::new(language_loader)) 38 | }); 39 | 40 | #[macro_export] 41 | macro_rules! fl { 42 | ($message_id:literal) => {{ 43 | i18n_embed_fl::fl!($crate::LOADER.lock().unwrap(), $message_id) 44 | }}; 45 | 46 | ($message_id:literal, $($args:expr),*) => {{ 47 | i18n_embed_fl::fl!($crate::LOADER.lock().unwrap(), $message_id, $($args), *) 48 | }}; 49 | } 50 | 51 | #[tokio::main] 52 | async fn main() -> Result<()> { 53 | let cmd = DCliCommand::from_args(); 54 | let mut config = Config::load()?; 55 | init_log(&config); 56 | if let Some(lang) = &config.lang { 57 | utils::reset_loader(lang) 58 | } 59 | cmd.run(&mut config).await?; 60 | Ok(()) 61 | } 62 | 63 | fn init_log(config: &Config) { 64 | if config.debug { 65 | let logfile = FileAppender::builder().build("dcli.log").unwrap(); 66 | let config = LogConfig::builder() 67 | .appender(Appender::builder().build("logfile", Box::new(logfile))) 68 | .build(Root::builder().appender("logfile").build(LevelFilter::Info)) 69 | .unwrap(); 70 | log4rs::init_config(config).unwrap(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/mysql/constants.rs: -------------------------------------------------------------------------------- 1 | pub const KEYWORDS: [&str; 389] = [ 2 | "ABS", 3 | "ACTION", 4 | "ADD", 5 | "ALL", 6 | "ALLOCATE", 7 | "ALTER", 8 | "AND", 9 | "ANY", 10 | "APPLY", 11 | "ARE", 12 | "ARRAY", 13 | "ARRAY_AGG", 14 | "ARRAY_MAX_CARDINALITY", 15 | "AS", 16 | "ASC", 17 | "ASENSITIVE", 18 | "ASSERT", 19 | "ASYMMETRIC", 20 | "AT", 21 | "ATOMIC", 22 | "AUTHORIZATION", 23 | "AVG", 24 | "AVRO", 25 | "BEGIN", 26 | "BEGIN_FRAME", 27 | "BEGIN_PARTITION", 28 | "BETWEEN", 29 | "BIGINT", 30 | "BINARY", 31 | "BLOB", 32 | "BOOLEAN", 33 | "BOTH", 34 | "BY", 35 | "BYTEA", 36 | "CALL", 37 | "CALLED", 38 | "CARDINALITY", 39 | "CASCADE", 40 | "CASCADED", 41 | "CASE", 42 | "CAST", 43 | "CEIL", 44 | "CEILING", 45 | "CHAIN", 46 | "CHAR", 47 | "CHARACTER", 48 | "CHARACTER_LENGTH", 49 | "CHAR_LENGTH", 50 | "CHECK", 51 | "CLOB", 52 | "CLOSE", 53 | "COALESCE", 54 | "COLLATE", 55 | "COLLECT", 56 | "COLUMN", 57 | "COLUMNS", 58 | "COMMIT", 59 | "COMMITTED", 60 | "CONDITION", 61 | "CONNECT", 62 | "CONSTRAINT", 63 | "CONTAINS", 64 | "CONVERT", 65 | "COPY", 66 | "CORR", 67 | "CORRESPONDING", 68 | "COUNT", 69 | "COVAR_POP", 70 | "COVAR_SAMP", 71 | "CREATE", 72 | "CROSS", 73 | "CSV", 74 | "CUBE", 75 | "CUME_DIST", 76 | "CURRENT", 77 | "CURRENT_CATALOG", 78 | "CURRENT_DATE", 79 | "CURRENT_DEFAULT_TRANSFORM_GROUP", 80 | "CURRENT_PATH", 81 | "CURRENT_ROLE", 82 | "CURRENT_ROW", 83 | "CURRENT_SCHEMA", 84 | "CURRENT_TIME", 85 | "CURRENT_TIMESTAMP", 86 | "CURRENT_TRANSFORM_GROUP_FOR_TYPE", 87 | "CURRENT_USER", 88 | "CURSOR", 89 | "CYCLE", 90 | "DATE", 91 | "DAY", 92 | "DEALLOCATE", 93 | "DEC", 94 | "DECIMAL", 95 | "DECLARE", 96 | "DEFAULT", 97 | "DELETE", 98 | "DENSE_RANK", 99 | "DEREF", 100 | "DESC", 101 | "DESCRIBE", 102 | "DETERMINISTIC", 103 | "DISCONNECT", 104 | "DISTINCT", 105 | "DOUBLE", 106 | "DROP", 107 | "DYNAMIC", 108 | "EACH", 109 | "ELEMENT", 110 | "ELSE", 111 | "END", 112 | "END_EXEC", 113 | "END_FRAME", 114 | "END_PARTITION", 115 | "EQUALS", 116 | "ERROR", 117 | "ESCAPE", 118 | "EVERY", 119 | "EXCEPT", 120 | "EXEC", 121 | "EXECUTE", 122 | "EXISTS", 123 | "EXP", 124 | "EXTENDED", 125 | "EXTERNAL", 126 | "EXTRACT", 127 | "FALSE", 128 | "FETCH", 129 | "FIELDS", 130 | "FILTER", 131 | "FIRST", 132 | "FIRST_VALUE", 133 | "FLOAT", 134 | "FLOOR", 135 | "FOLLOWING", 136 | "FOR", 137 | "FOREIGN", 138 | "FRAME_ROW", 139 | "FREE", 140 | "FROM", 141 | "FULL", 142 | "FUNCTION", 143 | "FUSION", 144 | "GET", 145 | "GLOBAL", 146 | "GRANT", 147 | "GROUP", 148 | "GROUPING", 149 | "GROUPS", 150 | "HAVING", 151 | "HEADER", 152 | "HOLD", 153 | "HOUR", 154 | "IDENTITY", 155 | "IF", 156 | "IN", 157 | "INDEX", 158 | "INDICATOR", 159 | "INNER", 160 | "INOUT", 161 | "INSENSITIVE", 162 | "INSERT", 163 | "INT", 164 | "INTEGER", 165 | "INTERSECT", 166 | "INTERSECTION", 167 | "INTERVAL", 168 | "INTO", 169 | "IS", 170 | "ISOLATION", 171 | "JOIN", 172 | "JSONFILE", 173 | "KEY", 174 | "LAG", 175 | "LANGUAGE", 176 | "LARGE", 177 | "LAST", 178 | "LAST_VALUE", 179 | "LATERAL", 180 | "LEAD", 181 | "LEADING", 182 | "LEFT", 183 | "LEVEL", 184 | "LIKE", 185 | "LIKE_REGEX", 186 | "LIMIT", 187 | "LISTAGG", 188 | "LN", 189 | "LOCAL", 190 | "LOCALTIME", 191 | "LOCALTIMESTAMP", 192 | "LOCATION", 193 | "LOWER", 194 | "MATCH", 195 | "MATERIALIZED", 196 | "MAX", 197 | "MEMBER", 198 | "MERGE", 199 | "METHOD", 200 | "MIN", 201 | "MINUTE", 202 | "MOD", 203 | "MODIFIES", 204 | "MODULE", 205 | "MONTH", 206 | "MULTISET", 207 | "NATIONAL", 208 | "NATURAL", 209 | "NCHAR", 210 | "NCLOB", 211 | "NEW", 212 | "NEXT", 213 | "NO", 214 | "NONE", 215 | "NORMALIZE", 216 | "NOT", 217 | "NTH_VALUE", 218 | "NTILE", 219 | "NULL", 220 | "NULLIF", 221 | "NULLS", 222 | "NUMERIC", 223 | "OBJECT", 224 | "OCCURRENCES_REGEX", 225 | "OCTET_LENGTH", 226 | "OF", 227 | "OFFSET", 228 | "OLD", 229 | "ON", 230 | "ONLY", 231 | "OPEN", 232 | "OR", 233 | "ORC", 234 | "ORDER", 235 | "OUT", 236 | "OUTER", 237 | "OVER", 238 | "OVERFLOW", 239 | "OVERLAPS", 240 | "OVERLAY", 241 | "PARAMETER", 242 | "PARQUET", 243 | "PARTITION", 244 | "PERCENT", 245 | "PERCENTILE_CONT", 246 | "PERCENTILE_DISC", 247 | "PERCENT_RANK", 248 | "PERIOD", 249 | "PORTION", 250 | "POSITION", 251 | "POSITION_REGEX", 252 | "POWER", 253 | "PRECEDES", 254 | "PRECEDING", 255 | "PRECISION", 256 | "PREPARE", 257 | "PRIMARY", 258 | "PROCEDURE", 259 | "RANGE", 260 | "RANK", 261 | "RCFILE", 262 | "READ", 263 | "READS", 264 | "REAL", 265 | "RECURSIVE", 266 | "REF", 267 | "REFERENCES", 268 | "REFERENCING", 269 | "REGCLASS", 270 | "REGR_AVGX", 271 | "REGR_AVGY", 272 | "REGR_COUNT", 273 | "REGR_INTERCEPT", 274 | "REGR_R2", 275 | "REGR_SLOPE", 276 | "REGR_SXX", 277 | "REGR_SXY", 278 | "REGR_SYY", 279 | "RELEASE", 280 | "RENAME", 281 | "REPEATABLE", 282 | "RESTRICT", 283 | "RESULT", 284 | "RETURN", 285 | "RETURNS", 286 | "REVOKE", 287 | "RIGHT", 288 | "ROLLBACK", 289 | "ROLLUP", 290 | "ROW", 291 | "ROWID", 292 | "ROWS", 293 | "ROW_NUMBER", 294 | "SAVEPOINT", 295 | "SCHEMA", 296 | "SCOPE", 297 | "SCROLL", 298 | "SEARCH", 299 | "SECOND", 300 | "SELECT", 301 | "SENSITIVE", 302 | "SEQUENCEFILE", 303 | "SERIALIZABLE", 304 | "SESSION", 305 | "SESSION_USER", 306 | "SET", 307 | "SHOW", 308 | "SIMILAR", 309 | "SMALLINT", 310 | "SOME", 311 | "SPECIFIC", 312 | "SPECIFICTYPE", 313 | "SQL", 314 | "SQLEXCEPTION", 315 | "SQLSTATE", 316 | "SQLWARNING", 317 | "SQRT", 318 | "START", 319 | "STATIC", 320 | "STDDEV_POP", 321 | "STDDEV_SAMP", 322 | "STDIN", 323 | "STORED", 324 | "SUBMULTISET", 325 | "SUBSTRING", 326 | "SUBSTRING_REGEX", 327 | "SUCCEEDS", 328 | "SUM", 329 | "SYMMETRIC", 330 | "SYSTEM", 331 | "SYSTEM_TIME", 332 | "SYSTEM_USER", 333 | "TABLE", 334 | "TABLESAMPLE", 335 | "TEXT", 336 | "TEXTFILE", 337 | "THEN", 338 | "TIES", 339 | "TIME", 340 | "TIMESTAMP", 341 | "TIMEZONE_HOUR", 342 | "TIMEZONE_MINUTE", 343 | "TO", 344 | "TOP", 345 | "TRAILING", 346 | "TRANSACTION", 347 | "TRANSLATE", 348 | "TRANSLATE_REGEX", 349 | "TRANSLATION", 350 | "TREAT", 351 | "TRIGGER", 352 | "TRIM", 353 | "TRIM_ARRAY", 354 | "TRUE", 355 | "TRUNCATE", 356 | "UESCAPE", 357 | "UNBOUNDED", 358 | "UNCOMMITTED", 359 | "UNION", 360 | "UNIQUE", 361 | "UNKNOWN", 362 | "UNNEST", 363 | "UPDATE", 364 | "UPPER", 365 | "USER", 366 | "USING", 367 | "UUID", 368 | "VALUE", 369 | "VALUES", 370 | "VALUE_OF", 371 | "VARBINARY", 372 | "VARCHAR", 373 | "VARYING", 374 | "VAR_POP", 375 | "VAR_SAMP", 376 | "VERSIONING", 377 | "VIEW", 378 | "VIRTUAL", 379 | "WHEN", 380 | "WHENEVER", 381 | "WHERE", 382 | "WIDTH_BUCKET", 383 | "WINDOW", 384 | "WITH", 385 | "WITHIN", 386 | "WITHOUT", 387 | "WORK", 388 | "WRITE", 389 | "YEAR", 390 | "ZONE", 391 | ]; 392 | pub const SCHEMA_TABLE: &str = "information_schema"; 393 | -------------------------------------------------------------------------------- /src/mysql/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | 3 | use crate::{config::Profile, output::QueryOutput}; 4 | use anyhow::{Context, Result}; 5 | use chrono::FixedOffset; 6 | use sqlx::{ 7 | mysql::{MySqlConnectOptions, MySqlRow, MySqlSslMode}, 8 | MySqlPool, 9 | }; 10 | 11 | mod constants; 12 | pub use constants::{KEYWORDS, SCHEMA_TABLE}; 13 | 14 | /// stand for mysql client server session, containing tz info etc... 15 | #[derive(Clone)] 16 | pub struct Session { 17 | pool: MySqlPool, 18 | } 19 | 20 | impl Session { 21 | /// create session with profile 22 | pub async fn connect_with(profile: &Profile) -> Result { 23 | let options = MySqlConnectOptions::new() 24 | .host(&profile.host) 25 | .port(profile.port) 26 | .ssl_mode(MySqlSslMode::Disabled); 27 | let options = if let Some(ref user) = profile.user { 28 | options.username(user) 29 | } else { 30 | options 31 | }; 32 | let options = if let Some(ref pass) = profile.password { 33 | options.password(pass) 34 | } else { 35 | options 36 | }; 37 | let options = options.database(&profile.db); 38 | let options = if let Some(ref ssl_mode) = profile.ssl_mode { 39 | let mode = match ssl_mode { 40 | crate::config::SslMode::Disabled => MySqlSslMode::Disabled, 41 | crate::config::SslMode::Preferred => MySqlSslMode::Preferred, 42 | crate::config::SslMode::Required => MySqlSslMode::Required, 43 | crate::config::SslMode::VerifyCa => MySqlSslMode::VerifyCa, 44 | crate::config::SslMode::VerifyIdentity => MySqlSslMode::VerifyIdentity, 45 | }; 46 | options.ssl_mode(mode) 47 | } else { 48 | options.ssl_mode(MySqlSslMode::Disabled) 49 | }; 50 | let options = if let Some(ref ca_file) = profile.ssl_ca { 51 | options.ssl_ca(ca_file) 52 | } else { 53 | options 54 | }; 55 | let pool = MySqlPool::connect_with(options) 56 | .await 57 | .with_context(|| crate::fl!("connect-failed"))?; 58 | Ok(Self { pool }) 59 | } 60 | 61 | pub async fn all_databases(&self) -> Result> { 62 | let query: Vec<(String,)> = sqlx::query_as("SHOW DATABASES") 63 | .fetch_all(&self.pool) 64 | .await?; 65 | let mut databases = HashSet::new(); 66 | query.into_iter().for_each(|(db,)| { 67 | databases.insert(db); 68 | }); 69 | Ok(databases) 70 | } 71 | 72 | pub async fn all_tables(&self) -> Result> { 73 | let query: Vec<(String,)> = sqlx::query_as("SHOW TABLES").fetch_all(&self.pool).await?; 74 | let mut tables = HashSet::new(); 75 | query.into_iter().for_each(|(t,)| { 76 | tables.insert(t); 77 | }); 78 | Ok(tables) 79 | } 80 | 81 | pub async fn all_columns( 82 | &self, 83 | tables: &HashSet, 84 | ) -> Result>> { 85 | let mut columns: HashMap> = HashMap::new(); 86 | if tables.is_empty() { 87 | return Ok(columns); 88 | } 89 | 90 | let sql = format!( 91 | "SELECT TABLE_NAME, COLUMN_NAME FROM {}.COLUMNS WHERE table_name IN ({})", 92 | SCHEMA_TABLE, 93 | tables 94 | .iter() 95 | .map(|t| format!("'{}'", t)) 96 | .collect::>() 97 | .join(",") 98 | ); 99 | let query: Vec<(String, String)> = sqlx::query_as(&sql).fetch_all(&self.pool).await?; 100 | query.into_iter().for_each(|(table, col)| { 101 | if let Some(table) = columns.get_mut(&table) { 102 | table.insert(col); 103 | } else { 104 | let mut entry = HashSet::new(); 105 | entry.insert(col); 106 | columns.insert(table, entry); 107 | } 108 | }); 109 | Ok(columns) 110 | } 111 | 112 | pub async fn tz_offset(&self) -> Result { 113 | let (offset,): (i32,) = 114 | sqlx::query_as("SELECT TIME_TO_SEC(TIMEDIFF(NOW(), UTC_TIMESTAMP));") 115 | .fetch_one(&self.pool) 116 | .await 117 | .with_context(|| "tz fetch error")?; 118 | Ok(FixedOffset::east(offset)) 119 | } 120 | 121 | pub async fn query(&self, to_exec: &str) -> Result { 122 | let rows: Vec = sqlx::query(to_exec) 123 | .fetch_all(&self.pool) 124 | .await 125 | .with_context(|| "")?; 126 | Ok(QueryOutput { rows }) 127 | } 128 | 129 | pub async fn close(&self) { 130 | self.pool.close().await 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/output.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Context, Result}; 2 | use bigdecimal::BigDecimal; 3 | use chrono::{DateTime, Utc}; 4 | use serde::{ 5 | ser::{SerializeMap, SerializeSeq}, 6 | Serialize, 7 | }; 8 | use sqlx::{ 9 | mysql::{MySqlColumn, MySqlRow, MySqlValueRef}, 10 | types::time::{Date, Time}, 11 | Column, Row, TypeInfo, Value, ValueRef, 12 | }; 13 | use std::{str::FromStr, vec}; 14 | 15 | use crate::{config::Config, fl}; 16 | 17 | #[derive(Debug, Clone, Serialize)] 18 | pub enum Format { 19 | #[serde(rename = "csv")] 20 | Csv, 21 | #[serde(rename = "json")] 22 | Json, 23 | #[serde(rename = "yaml")] 24 | Yaml, 25 | #[serde(rename = "toml")] 26 | Toml, 27 | #[serde(rename = "pickle")] 28 | Pickle, 29 | } 30 | 31 | impl Default for Format { 32 | fn default() -> Self { 33 | Format::Json 34 | } 35 | } 36 | 37 | impl FromStr for Format { 38 | type Err = anyhow::Error; 39 | 40 | fn from_str(s: &str) -> Result { 41 | let lower = s.to_ascii_lowercase(); 42 | if lower == "json" { 43 | Ok(Format::Json) 44 | } else if lower == "csv" { 45 | Ok(Format::Csv) 46 | } else if lower == "yaml" { 47 | Ok(Format::Yaml) 48 | } else if lower == "toml" { 49 | Ok(Format::Toml) 50 | } else if lower == "pickle" { 51 | Ok(Format::Pickle) 52 | } else { 53 | Err(anyhow!(fl!("invalid-value", val = s))) 54 | } 55 | } 56 | } 57 | pub struct QueryOutput { 58 | pub rows: Vec, 59 | } 60 | pub struct DCliColumn<'a> { 61 | pub col: &'a MySqlColumn, 62 | pub val_ref: MySqlValueRef<'a>, 63 | } 64 | 65 | pub struct QueryOutputMapSer<'a>(pub &'a QueryOutput); 66 | struct DcliRowMapSer<'a>(&'a MySqlRow); 67 | struct QueryOutputListSer<'a>(&'a QueryOutput); 68 | struct DcliRowListSer<'a>(&'a MySqlRow); 69 | 70 | impl<'a> Serialize for QueryOutputMapSer<'a> { 71 | fn serialize(&self, serializer: S) -> Result 72 | where 73 | S: serde::Serializer, 74 | { 75 | let mut seq = serializer.serialize_seq(Some(self.0.rows.len()))?; 76 | for row in self.0.rows.iter().map(DcliRowMapSer) { 77 | seq.serialize_element(&row)?; 78 | } 79 | seq.end() 80 | } 81 | } 82 | 83 | impl<'a> Serialize for DcliRowMapSer<'a> { 84 | fn serialize(&self, serializer: S) -> Result 85 | where 86 | S: serde::Serializer, 87 | { 88 | let mut map = serializer.serialize_map(Some(self.0.len()))?; 89 | for col in self.0.columns().iter().map(|c| { 90 | let val_ref = self.0.try_get_raw(c.ordinal()).unwrap(); 91 | DCliColumn { col: c, val_ref } 92 | }) { 93 | map.serialize_entry(col.col.name(), &col)?; 94 | } 95 | map.end() 96 | } 97 | } 98 | 99 | impl<'a> Serialize for QueryOutputListSer<'a> { 100 | fn serialize(&self, serializer: S) -> Result 101 | where 102 | S: serde::Serializer, 103 | { 104 | let mut seq = serializer.serialize_seq(Some(self.0.rows.len()))?; 105 | for row in self.0.rows.iter().map(DcliRowListSer) { 106 | seq.serialize_element(&row)?; 107 | } 108 | seq.end() 109 | } 110 | } 111 | 112 | impl<'a> Serialize for DcliRowListSer<'a> { 113 | fn serialize(&self, serializer: S) -> Result 114 | where 115 | S: serde::Serializer, 116 | { 117 | let mut seq = serializer.serialize_seq(Some(self.0.len()))?; 118 | for col in self.0.columns().iter().map(|c| { 119 | let val_ref = self.0.try_get_raw(c.ordinal()).unwrap(); 120 | DCliColumn { col: c, val_ref } 121 | }) { 122 | seq.serialize_element(&col)?; 123 | } 124 | seq.end() 125 | } 126 | } 127 | 128 | impl<'a> Serialize for DCliColumn<'a> { 129 | fn serialize(&self, serializer: S) -> Result 130 | where 131 | S: serde::Serializer, 132 | { 133 | let val = ValueRef::to_owned(&self.val_ref); 134 | if val.is_null() { 135 | serializer.serialize_none() 136 | } else { 137 | match val.type_info().name() { 138 | "BOOLEAN" => { 139 | let v = val.try_decode::().unwrap(); 140 | serializer.serialize_bool(v) 141 | } 142 | "TINYINT UNSIGNED" | "SMALLINT UNSIGNED" | "INT UNSIGNED" 143 | | "MEDIUMINT UNSIGNED" | "BIGINT UNSIGNED" => { 144 | let v = val.try_decode::().unwrap(); 145 | serializer.serialize_u64(v) 146 | } 147 | "TINYINT" | "SMALLINT" | "INT" | "MEDIUMINT" | "BIGINT" => { 148 | let v = val.try_decode::().unwrap(); 149 | serializer.serialize_i64(v) 150 | } 151 | "FLOAT" => { 152 | let v = val.try_decode::().unwrap(); 153 | serializer.serialize_f32(v) 154 | } 155 | "DOUBLE" => { 156 | let v = val.try_decode::().unwrap(); 157 | serializer.serialize_f64(v) 158 | } 159 | "NULL" => serializer.serialize_none(), 160 | "DATE" => { 161 | let v = val.try_decode::().unwrap(); 162 | serializer.serialize_str(&v.to_string()) 163 | } 164 | "TIME" => { 165 | let v = val.try_decode::