├── .github └── workflows │ └── ci.yaml ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── internal ├── entviz │ ├── entviz.go │ ├── entviz_test.go │ ├── generate.go │ ├── generated.go │ ├── genqlient.graphql │ ├── genqlient.yaml │ └── schema.graphql └── integration │ ├── docker-compose.yaml │ ├── integration_test.go │ └── testdata │ ├── ent │ └── schema │ │ └── user.go │ └── schema-type │ └── schema │ └── user.go └── main.go /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: [ push, pull_request ] 3 | 4 | jobs: 5 | lint: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | - uses: actions/setup-go@v3 10 | with: 11 | go-version: '1.19.5' 12 | - name: Run linters 13 | uses: golangci/golangci-lint-action@v3.3.1 14 | with: 15 | version: v1.50.1 16 | args: --timeout 3m 17 | test: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | - uses: actions/setup-go@v3 22 | with: 23 | go-version: '1.19.5' 24 | - uses: actions/cache@v3 25 | with: 26 | path: ~/go/pkg/mod 27 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 28 | restore-keys: | 29 | ${{ runner.os }}-go- 30 | - name: Run tests 31 | run: go test -race ./internal/entviz/... 32 | integration: 33 | runs-on: ubuntu-latest 34 | services: 35 | mysql: 36 | image: mysql 37 | env: 38 | MYSQL_ROOT_PASSWORD: pass 39 | MYSQL_DATABASE: dev 40 | ports: 41 | - "3306:3306" 42 | options: >- 43 | --health-cmd "mysqladmin ping -ppass" 44 | --health-interval 10s 45 | --health-start-period 10s 46 | --health-timeout 5s 47 | --health-retries 10 48 | postgres: 49 | image: postgres 50 | env: 51 | POSTGRES_DB: dev 52 | POSTGRES_PASSWORD: pass 53 | ports: 54 | - 5432:5432 55 | options: >- 56 | --health-cmd pg_isready 57 | --health-interval 10s 58 | --health-timeout 5s 59 | --health-retries 5 60 | steps: 61 | - uses: actions/checkout@v3 62 | - uses: actions/setup-go@v3 63 | with: 64 | go-version: '1.19.5' 65 | - uses: actions/cache@v3 66 | with: 67 | path: ~/go/pkg/mod 68 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 69 | restore-keys: | 70 | ${{ runner.os }}-go- 71 | - name: Run tests 72 | run: go test -race ./internal/integration/... 73 | generate: 74 | runs-on: ubuntu-latest 75 | steps: 76 | - uses: actions/checkout@v3 77 | - uses: actions/setup-go@v3 78 | with: 79 | go-version: '1.19.5' 80 | - uses: actions/cache@v3 81 | with: 82 | path: ~/go/pkg/mod 83 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 84 | restore-keys: | 85 | ${{ runner.os }}-go- 86 | - name: Run go generate 87 | run: go generate ./... 88 | - name: Check generated files 89 | run: | 90 | status=$(git status --porcelain) 91 | if [ -n "$status" ]; then 92 | echo "you need to run 'go generate ./...' and commit the changes" 93 | echo "$status" 94 | git --no-pager diff 95 | exit 1 96 | fi -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # entviz 2 | 3 | Visualize [Ent](https://entgo.io) schemas with beautiful ERDs on [atlasgo.cloud](https://gh.atlasgo.cloud/explore). 4 | 5 | ![image](https://user-images.githubusercontent.com/1522681/214566936-37f0eb02-30d0-4ea9-8b29-8b71c1bdbc0d.png) 6 | 7 | 8 | ## Usage 9 | 10 | ```shell 11 | go run -mod=mod ariga.io/entviz --help 12 | ``` 13 | 14 | ```shell 15 | Usage of ariga.io/entviz 16 | go run -mod=mod ariga.io/entviz 17 | Flags: 18 | -dev-url string 19 | dev database to be used to generate the schema (default "sqlite3://file?mode=memory&cache=shared&_fk=1") 20 | -global-unique-id 21 | enable the Global Unique ID feature 22 | ``` 23 | 24 | ## Example 25 | 26 | Share Ent schema using `SQLite` dev database. 27 | 28 | ```shell 29 | ❯ go run -mod=mod ariga.io/entviz ./ent/schema 30 | Here is a public link to your schema visualization 31 | https://gh.atlasgo.cloud/explore/c3aa3f24 32 | ``` 33 | 34 | For `MySQL` or `Postgres` check the examples below: 35 | 36 | ```shell 37 | ❯ go run -mod=mod ariga.io/entviz -dev-url "mysql://user:pass@localhost:3306/database" ./ent/schema 38 | ❯ go run -mod=mod ariga.io/entviz -dev-url "postgres://postgres:pass@localhost:5432/database?sslmode=disable" ./ent/schema 39 | ``` 40 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module ariga.io/entviz 2 | 3 | go 1.19 4 | 5 | require ( 6 | ariga.io/atlas v0.9.1-0.20230119123307-a3ab6808892b 7 | entgo.io/ent v0.11.5-0.20221212200803-3328201ba8a9 8 | github.com/Khan/genqlient v0.5.0 9 | github.com/go-sql-driver/mysql v1.7.0 10 | github.com/lib/pq v1.10.7 11 | github.com/mattn/go-sqlite3 v1.14.16 12 | github.com/stretchr/testify v1.8.1 13 | ) 14 | 15 | require ( 16 | github.com/agext/levenshtein v1.2.1 // indirect 17 | github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/go-openapi/inflect v0.19.0 // indirect 20 | github.com/google/go-cmp v0.5.6 // indirect 21 | github.com/google/uuid v1.3.0 // indirect 22 | github.com/hashicorp/hcl/v2 v2.13.0 // indirect 23 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect 24 | github.com/pmezard/go-difflib v1.0.0 // indirect 25 | github.com/vektah/gqlparser/v2 v2.4.5 // indirect 26 | github.com/zclconf/go-cty v1.8.0 // indirect 27 | golang.org/x/mod v0.7.0 // indirect 28 | golang.org/x/sys v0.2.0 // indirect 29 | golang.org/x/text v0.3.7 // indirect 30 | golang.org/x/tools v0.3.1-0.20221202221704-aa9f4b2f3d57 // indirect 31 | gopkg.in/yaml.v3 v3.0.1 // indirect 32 | ) 33 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | ariga.io/atlas v0.9.1-0.20230119123307-a3ab6808892b h1:f1868Z/5iWzfVMgjOBwjjP/mRCxOSbXtAl+9DAYb4kg= 2 | ariga.io/atlas v0.9.1-0.20230119123307-a3ab6808892b/go.mod h1:T230JFcENj4ZZzMkZrXFDSkv+2kXkUgpJ5FQQ5hMcKU= 3 | entgo.io/ent v0.11.5-0.20221212200803-3328201ba8a9 h1:cbHpH6nLmNvMwEcVCiTm5+stuExtwbdUpEW1gdkfEFY= 4 | entgo.io/ent v0.11.5-0.20221212200803-3328201ba8a9/go.mod h1:u7eKwNWAo/VlHIKxgwbmsFy3J7cKDxwi3jyF5TW/okY= 5 | github.com/99designs/gqlgen v0.17.2/go.mod h1:K5fzLKwtph+FFgh9j7nFbRUdBKvTcGnsta51fsMTn3o= 6 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 7 | github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= 8 | github.com/Khan/genqlient v0.5.0 h1:TMZJ+tl/BpbmGyIBiXzKzUftDhw4ZWxQZ+1ydn0gyII= 9 | github.com/Khan/genqlient v0.5.0/go.mod h1:EpIvDVXYm01GP6AXzjA7dKriPTH6GmtpmvTAwUUqIX8= 10 | github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= 11 | github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= 12 | github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= 13 | github.com/agnivade/levenshtein v1.1.0/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= 14 | github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= 15 | github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= 16 | github.com/alexflint/go-arg v1.4.2 h1:lDWZAXxpAnZUq4qwb86p/3rIJJ2Li81EoMbTMujhVa0= 17 | github.com/alexflint/go-arg v1.4.2/go.mod h1:9iRbDxne7LcR/GSvEr7ma++GLpdIU1zrghf2y2768kM= 18 | github.com/alexflint/go-scalar v1.0.0 h1:NGupf1XV/Xb04wXskDFzS0KWOLH632W/EO4fAFi+A70= 19 | github.com/alexflint/go-scalar v1.0.0/go.mod h1:GpHzbCOZXEKMEcygYQ5n/aa4Aq84zbxjy3MxYW0gjYw= 20 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= 21 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= 22 | github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= 23 | github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= 24 | github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= 25 | github.com/bradleyjkemp/cupaloy/v2 v2.6.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= 26 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 27 | github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 28 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 29 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 30 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 31 | github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= 32 | github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4= 33 | github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= 34 | github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= 35 | github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 36 | github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= 37 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 38 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 39 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 40 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 41 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 42 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 43 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 44 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 45 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 46 | github.com/hashicorp/hcl/v2 v2.13.0 h1:0Apadu1w6M11dyGFxWnmhhcMjkbAiKCv7G1r/2QgCNc= 47 | github.com/hashicorp/hcl/v2 v2.13.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0= 48 | github.com/kevinmbeaulieu/eq-go v1.0.0/go.mod h1:G3S8ajA56gKBZm4UB9AOyoOS37JO3roToPzKNM8dtdM= 49 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 50 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 51 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 52 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 53 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 54 | github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= 55 | github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= 56 | github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 57 | github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc= 58 | github.com/matryer/moq v0.2.3/go.mod h1:9RtPYjTnH1bSBIkpvtHkFN7nbWAnO7oRpdJkEIn6UtE= 59 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 60 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 61 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 62 | github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= 63 | github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 64 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= 65 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 66 | github.com/mitchellh/mapstructure v1.2.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 67 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 68 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 69 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 70 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 71 | github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= 72 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 73 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 74 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 75 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 76 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 77 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 78 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 79 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 80 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 81 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 82 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 83 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 84 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 85 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 86 | github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= 87 | github.com/vektah/gqlparser/v2 v2.4.0/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0= 88 | github.com/vektah/gqlparser/v2 v2.4.5 h1:C02NsyEsL4TXJB7ndonqTfuQOL4XPIu0aAWugdmTgmc= 89 | github.com/vektah/gqlparser/v2 v2.4.5/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0= 90 | github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= 91 | github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= 92 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 93 | github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 94 | github.com/zclconf/go-cty v1.8.0 h1:s4AvqaeQzJIu3ndv4gVIhplVD0krU+bgrcLSVUnaWuA= 95 | github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= 96 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 97 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 98 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 99 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 100 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 101 | golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= 102 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= 103 | golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= 104 | golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 105 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 106 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 107 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 108 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 109 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 110 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 111 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 112 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 113 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 114 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 115 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 116 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 117 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 118 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 119 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 120 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 121 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 122 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 123 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 124 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 125 | golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 126 | golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= 127 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 128 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 129 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 130 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 131 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 132 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 133 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 134 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 135 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 136 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 137 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 138 | golang.org/x/tools v0.0.0-20200815165600-90abf76919f3/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 139 | golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= 140 | golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= 141 | golang.org/x/tools v0.3.1-0.20221202221704-aa9f4b2f3d57 h1:/X0t/E4VxbZE7MLS7auvE7YICHeVvbIa9vkOVvYW/24= 142 | golang.org/x/tools v0.3.1-0.20221202221704-aa9f4b2f3d57/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= 143 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 144 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 145 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 146 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 147 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 148 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 149 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 150 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 151 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 152 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 153 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 154 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 155 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 156 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 157 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 158 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 159 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 160 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 161 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 162 | -------------------------------------------------------------------------------- /internal/entviz/entviz.go: -------------------------------------------------------------------------------- 1 | package entviz 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | "time" 11 | 12 | _ "ariga.io/atlas/sql/mysql" 13 | _ "ariga.io/atlas/sql/postgres" 14 | atlas "ariga.io/atlas/sql/schema" 15 | "ariga.io/atlas/sql/sqlclient" 16 | _ "ariga.io/atlas/sql/sqlite" 17 | "entgo.io/ent/dialect" 18 | "entgo.io/ent/dialect/sql/schema" 19 | "entgo.io/ent/entc" 20 | "entgo.io/ent/entc/gen" 21 | "entgo.io/ent/schema/field" 22 | "github.com/Khan/genqlient/graphql" 23 | 24 | _ "github.com/go-sql-driver/mysql" 25 | _ "github.com/lib/pq" 26 | _ "github.com/mattn/go-sqlite3" 27 | ) 28 | 29 | // ParseDevURL parses the devURL and returns the Ent dialect as well the Atlas driver name. 30 | func ParseDevURL(devURL string) (string, Driver, error) { 31 | parsed, err := url.Parse(devURL) 32 | if err != nil { 33 | return "", "", err 34 | } 35 | switch strings.ToLower(parsed.Scheme) { 36 | case "sqlite", "sqlite3": 37 | return dialect.SQLite, DriverSqlite, nil 38 | case "mysql": 39 | return dialect.MySQL, DriverMysql, nil 40 | case "postgres": 41 | return dialect.Postgres, DriverPostgresql, nil 42 | } 43 | return "", "", fmt.Errorf("unknow dialect: %s", parsed.Scheme) 44 | } 45 | 46 | var ( 47 | errSkip = errors.New("skip") 48 | ) 49 | 50 | // HCLOptions are the options that can be provided to HCL. 51 | type HCLOptions struct { 52 | SchemaPath string 53 | Dialect string 54 | DevURL string 55 | GlobalUniqueID bool 56 | } 57 | 58 | // HCL generates an Atlas HCL document from an Ent schema. 59 | // Most of the code below is taken from https://github.com/rotemtam/entprint. 60 | func HCL(ctx context.Context, hclOpts HCLOptions) ([]byte, error) { 61 | graph, err := entc.LoadGraph(hclOpts.SchemaPath, &gen.Config{}) 62 | if err != nil { 63 | return nil, fmt.Errorf("loading schema: %w", err) 64 | } 65 | var sch *atlas.Schema 66 | opts := []schema.MigrateOption{ 67 | schema.WithGlobalUniqueID(hclOpts.GlobalUniqueID), 68 | schema.WithDiffHook(func(differ schema.Differ) schema.Differ { 69 | return schema.DiffFunc(func(current, desired *atlas.Schema) ([]atlas.Change, error) { 70 | sch = desired 71 | return nil, errSkip 72 | }) 73 | }), 74 | schema.WithDialect(hclOpts.Dialect), 75 | } 76 | mig, err := schema.NewMigrateURL(hclOpts.DevURL, opts...) 77 | if err != nil { 78 | return nil, fmt.Errorf("creating migration engine: %w", err) 79 | } 80 | tbl, err := graph.Tables() 81 | if err != nil { 82 | return nil, fmt.Errorf("reading tables: %w", err) 83 | } 84 | for _, tb := range tbl { 85 | for _, cl := range tb.Columns { 86 | if cl.Type == field.TypeOther && cl.SchemaType[hclOpts.Dialect] == "" { 87 | return nil, fmt.Errorf("%s: schema type for column %s.%s not defined", hclOpts.Dialect, tb.Name, cl.Name) 88 | } 89 | } 90 | } 91 | if err := mig.Create(ctx, tbl...); err != nil && !errors.Is(err, errSkip) { 92 | return nil, fmt.Errorf("creating schema: %w", err) 93 | } 94 | drv, err := sqlclient.Open(ctx, hclOpts.DevURL) 95 | if err != nil { 96 | return nil, fmt.Errorf("opening sql client: %w", err) 97 | } 98 | norm, ok := drv.Driver.(atlas.Normalizer) 99 | if ok { 100 | sch, err = norm.NormalizeSchema(ctx, sch) 101 | if err != nil { 102 | return nil, fmt.Errorf("normalizing schema: %w", err) 103 | } 104 | } 105 | spec, err := drv.MarshalSpec(sch) 106 | if err != nil { 107 | return nil, fmt.Errorf("marshaling schema: %w", err) 108 | } 109 | return spec, nil 110 | } 111 | 112 | const userAgent = "EntViz" 113 | 114 | type shareOpts struct { 115 | endpoint string 116 | httpClient *http.Client 117 | } 118 | 119 | type ShareOption func(opts *shareOpts) 120 | 121 | // ShareWithEndpoint allows providing a custom endpoint to shareHCL. 122 | func ShareWithEndpoint(endpoint string) ShareOption { 123 | return func(opts *shareOpts) { 124 | opts.endpoint = endpoint 125 | } 126 | } 127 | 128 | // ShareWithHttpClient allows proving a custom *http.Client for shareHCL. 129 | func ShareWithHttpClient(httpClient *http.Client) ShareOption { 130 | return func(opts *shareOpts) { 131 | opts.httpClient = httpClient 132 | } 133 | } 134 | 135 | // Share create and returns an Atlas Cloud Explore link for the given HCL document. 136 | func Share(ctx context.Context, hclDocument []byte, driver Driver, opts ...ShareOption) (string, error) { 137 | shareOpts := shareOpts{ 138 | endpoint: "https://gh.atlasgo.cloud/api/query", 139 | httpClient: &http.Client{Timeout: 60 * time.Second}, 140 | } 141 | for _, opt := range opts { 142 | opt(&shareOpts) 143 | } 144 | u, err := url.Parse(shareOpts.endpoint) 145 | if err != nil { 146 | return "", fmt.Errorf("parsing endpoint: %w", err) 147 | } 148 | gql := graphql.NewClient(shareOpts.endpoint, &entvizDo{httpClient: shareOpts.httpClient}) 149 | visualize, err := VisualizeMutation(ctx, gql, string(hclDocument), driver) 150 | if err != nil { 151 | return "", fmt.Errorf("visualize request: %w", err) 152 | } 153 | share, err := ShareVisualizationMutation(ctx, gql, visualize.Visualize.Node.ExtID) 154 | if err != nil { 155 | return "", fmt.Errorf("share request: %w", err) 156 | } 157 | if !share.ShareVisualization.Success { 158 | return "", fmt.Errorf("could not share the visualization: %s", visualize.Visualize.Node.ExtID) 159 | } 160 | return fmt.Sprintf("%s://%s/explore/%s", u.Scheme, u.Host, visualize.Visualize.Node.ExtID), nil 161 | } 162 | 163 | // entvizDo implements graphql.Doer and sets 164 | // the userAgent on each request. 165 | type entvizDo struct { 166 | httpClient *http.Client 167 | } 168 | 169 | func (e *entvizDo) Do(r *http.Request) (*http.Response, error) { 170 | r.Header.Set("User-Agent", userAgent) 171 | return e.httpClient.Do(r) 172 | } 173 | -------------------------------------------------------------------------------- /internal/entviz/entviz_test.go: -------------------------------------------------------------------------------- 1 | package entviz 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | 11 | "entgo.io/ent/dialect" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | const testHCL = `table "users" { 16 | schema = schema.main 17 | column "id" { 18 | null = false 19 | type = integer 20 | auto_increment = true 21 | } 22 | column "name" { 23 | null = false 24 | type = text 25 | } 26 | primary_key { 27 | columns = [column.id] 28 | } 29 | } 30 | schema "main" { 31 | } 32 | ` 33 | 34 | func Test_Share(t *testing.T) { 35 | t.Parallel() 36 | ctx := context.Background() 37 | t.Run("normal flow", func(t *testing.T) { 38 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 39 | require.Equal(t, userAgent, r.UserAgent()) 40 | require.Equal(t, "application/json", r.Header.Get("Content-Type")) 41 | body, err := io.ReadAll(r.Body) 42 | require.NoError(t, err) 43 | w.WriteHeader(http.StatusOK) 44 | w.Header().Set("Content-Type", "application/json") 45 | 46 | if bytes.Contains(body, []byte(`VisualizeMutation`)) { 47 | require.Equal(t, visualizeQueryRequest, string(body)) 48 | _, _ = w.Write([]byte(`{"data":{"visualize":{"node":{"extID":"23098224"}}}}`)) 49 | } 50 | if bytes.Contains(body, []byte(`ShareVisualizationMutation`)) { 51 | require.Equal(t, shareQueryRequest, string(body)) 52 | _, _ = w.Write([]byte(`{"data":{"shareVisualization":{"success":true}}}`)) 53 | } 54 | })) 55 | defer srv.Close() 56 | link, err := Share(ctx, []byte(testHCL), dialect.SQLite, 57 | ShareWithHttpClient(&http.Client{}), 58 | ShareWithEndpoint(srv.URL), 59 | ) 60 | require.NoError(t, err) 61 | require.Equal(t, srv.URL+"/explore/23098224", link) 62 | }) 63 | t.Run("rate limited", func(t *testing.T) { 64 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 65 | require.Equal(t, userAgent, r.UserAgent()) 66 | require.Equal(t, "application/json", r.Header.Get("Content-Type")) 67 | w.WriteHeader(http.StatusTooManyRequests) 68 | })) 69 | defer srv.Close() 70 | _, err := Share(ctx, []byte(testHCL), dialect.SQLite, 71 | ShareWithHttpClient(&http.Client{}), 72 | ShareWithEndpoint(srv.URL), 73 | ) 74 | require.ErrorContains(t, err, "visualize request: returned error 429 Too Many Requests: ") 75 | }) 76 | } 77 | 78 | const ( 79 | shareQueryRequest = "{\"query\":\"\\nmutation ShareVisualizationMutation ($extID: String!) {\\n\\tshareVisualization(input: {fromID:$extID}) {\\n\\t\\tsuccess\\n\\t}\\n}\\n\",\"variables\":{\"extID\":\"23098224\"},\"operationName\":\"ShareVisualizationMutation\"}" 80 | visualizeQueryRequest = "{\"query\":\"\\nmutation VisualizeMutation ($text: String!, $driver: Driver!) {\\n\\tvisualize(input: {text:$text,type:HCL,driver:$driver}) {\\n\\t\\tnode {\\n\\t\\t\\textID\\n\\t\\t}\\n\\t}\\n}\\n\",\"variables\":{\"text\":\"table \\\"users\\\" {\\n schema = schema.main\\n column \\\"id\\\" {\\n null = false\\n type = integer\\n auto_increment = true\\n }\\n column \\\"name\\\" {\\n null = false\\n type = text\\n }\\n primary_key {\\n columns = [column.id]\\n }\\n}\\nschema \\\"main\\\" {\\n}\\n\",\"driver\":\"sqlite3\"},\"operationName\":\"VisualizeMutation\"}" 81 | ) 82 | -------------------------------------------------------------------------------- /internal/entviz/generate.go: -------------------------------------------------------------------------------- 1 | package entviz 2 | 3 | //go:generate go run -mod=mod github.com/Khan/genqlient 4 | -------------------------------------------------------------------------------- /internal/entviz/generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/Khan/genqlient, DO NOT EDIT. 2 | 3 | package entviz 4 | 5 | import ( 6 | "context" 7 | 8 | "github.com/Khan/genqlient/graphql" 9 | ) 10 | 11 | type Driver string 12 | 13 | const ( 14 | DriverSqlite Driver = "SQLITE" 15 | DriverMysql Driver = "MYSQL" 16 | DriverPostgresql Driver = "POSTGRESQL" 17 | ) 18 | 19 | // ShareVisualizationMutationResponse is returned by ShareVisualizationMutation on success. 20 | type ShareVisualizationMutationResponse struct { 21 | ShareVisualization ShareVisualizationMutationShareVisualizationShareVisualizationPayload `json:"shareVisualization"` 22 | } 23 | 24 | // GetShareVisualization returns ShareVisualizationMutationResponse.ShareVisualization, and is useful for accessing the field via an interface. 25 | func (v *ShareVisualizationMutationResponse) GetShareVisualization() ShareVisualizationMutationShareVisualizationShareVisualizationPayload { 26 | return v.ShareVisualization 27 | } 28 | 29 | // ShareVisualizationMutationShareVisualizationShareVisualizationPayload includes the requested fields of the GraphQL type ShareVisualizationPayload. 30 | type ShareVisualizationMutationShareVisualizationShareVisualizationPayload struct { 31 | Success bool `json:"success"` 32 | } 33 | 34 | // GetSuccess returns ShareVisualizationMutationShareVisualizationShareVisualizationPayload.Success, and is useful for accessing the field via an interface. 35 | func (v *ShareVisualizationMutationShareVisualizationShareVisualizationPayload) GetSuccess() bool { 36 | return v.Success 37 | } 38 | 39 | // VisualizeMutationResponse is returned by VisualizeMutation on success. 40 | type VisualizeMutationResponse struct { 41 | Visualize VisualizeMutationVisualizeVisualizationPayload `json:"visualize"` 42 | } 43 | 44 | // GetVisualize returns VisualizeMutationResponse.Visualize, and is useful for accessing the field via an interface. 45 | func (v *VisualizeMutationResponse) GetVisualize() VisualizeMutationVisualizeVisualizationPayload { 46 | return v.Visualize 47 | } 48 | 49 | // VisualizeMutationVisualizeVisualizationPayload includes the requested fields of the GraphQL type VisualizationPayload. 50 | type VisualizeMutationVisualizeVisualizationPayload struct { 51 | Node VisualizeMutationVisualizeVisualizationPayloadNodeVisualization `json:"node"` 52 | } 53 | 54 | // GetNode returns VisualizeMutationVisualizeVisualizationPayload.Node, and is useful for accessing the field via an interface. 55 | func (v *VisualizeMutationVisualizeVisualizationPayload) GetNode() VisualizeMutationVisualizeVisualizationPayloadNodeVisualization { 56 | return v.Node 57 | } 58 | 59 | // VisualizeMutationVisualizeVisualizationPayloadNodeVisualization includes the requested fields of the GraphQL type Visualization. 60 | type VisualizeMutationVisualizeVisualizationPayloadNodeVisualization struct { 61 | ExtID string `json:"extID"` 62 | } 63 | 64 | // GetExtID returns VisualizeMutationVisualizeVisualizationPayloadNodeVisualization.ExtID, and is useful for accessing the field via an interface. 65 | func (v *VisualizeMutationVisualizeVisualizationPayloadNodeVisualization) GetExtID() string { 66 | return v.ExtID 67 | } 68 | 69 | // __ShareVisualizationMutationInput is used internally by genqlient 70 | type __ShareVisualizationMutationInput struct { 71 | ExtID string `json:"extID"` 72 | } 73 | 74 | // GetExtID returns __ShareVisualizationMutationInput.ExtID, and is useful for accessing the field via an interface. 75 | func (v *__ShareVisualizationMutationInput) GetExtID() string { return v.ExtID } 76 | 77 | // __VisualizeMutationInput is used internally by genqlient 78 | type __VisualizeMutationInput struct { 79 | Text string `json:"text"` 80 | Driver Driver `json:"driver"` 81 | } 82 | 83 | // GetText returns __VisualizeMutationInput.Text, and is useful for accessing the field via an interface. 84 | func (v *__VisualizeMutationInput) GetText() string { return v.Text } 85 | 86 | // GetDriver returns __VisualizeMutationInput.Driver, and is useful for accessing the field via an interface. 87 | func (v *__VisualizeMutationInput) GetDriver() Driver { return v.Driver } 88 | 89 | func ShareVisualizationMutation( 90 | ctx context.Context, 91 | client graphql.Client, 92 | extID string, 93 | ) (*ShareVisualizationMutationResponse, error) { 94 | req := &graphql.Request{ 95 | OpName: "ShareVisualizationMutation", 96 | Query: ` 97 | mutation ShareVisualizationMutation ($extID: String!) { 98 | shareVisualization(input: {fromID:$extID}) { 99 | success 100 | } 101 | } 102 | `, 103 | Variables: &__ShareVisualizationMutationInput{ 104 | ExtID: extID, 105 | }, 106 | } 107 | var err error 108 | 109 | var data ShareVisualizationMutationResponse 110 | resp := &graphql.Response{Data: &data} 111 | 112 | err = client.MakeRequest( 113 | ctx, 114 | req, 115 | resp, 116 | ) 117 | 118 | return &data, err 119 | } 120 | 121 | func VisualizeMutation( 122 | ctx context.Context, 123 | client graphql.Client, 124 | text string, 125 | driver Driver, 126 | ) (*VisualizeMutationResponse, error) { 127 | req := &graphql.Request{ 128 | OpName: "VisualizeMutation", 129 | Query: ` 130 | mutation VisualizeMutation ($text: String!, $driver: Driver!) { 131 | visualize(input: {text:$text,type:HCL,driver:$driver}) { 132 | node { 133 | extID 134 | } 135 | } 136 | } 137 | `, 138 | Variables: &__VisualizeMutationInput{ 139 | Text: text, 140 | Driver: driver, 141 | }, 142 | } 143 | var err error 144 | 145 | var data VisualizeMutationResponse 146 | resp := &graphql.Response{Data: &data} 147 | 148 | err = client.MakeRequest( 149 | ctx, 150 | req, 151 | resp, 152 | ) 153 | 154 | return &data, err 155 | } 156 | -------------------------------------------------------------------------------- /internal/entviz/genqlient.graphql: -------------------------------------------------------------------------------- 1 | mutation VisualizeMutation($text: String!, $driver: Driver!) { 2 | visualize(input: { text: $text, type: HCL, driver: $driver }) { 3 | node { 4 | extID 5 | } 6 | } 7 | } 8 | 9 | mutation ShareVisualizationMutation($extID: String!) { 10 | shareVisualization(input: { fromID: $extID }) { 11 | success 12 | } 13 | } -------------------------------------------------------------------------------- /internal/entviz/genqlient.yaml: -------------------------------------------------------------------------------- 1 | # Default genqlient config; for full documentation see: 2 | # https://github.com/Khan/genqlient/blob/main/docs/genqlient.yaml 3 | schema: schema.graphql 4 | operations: 5 | - genqlient.graphql 6 | generated: generated.go 7 | -------------------------------------------------------------------------------- /internal/entviz/schema.graphql: -------------------------------------------------------------------------------- 1 | type Mutation { 2 | shareVisualization(input: ShareVisualizationInput!): ShareVisualizationPayload! 3 | visualize(input: VisualizeInput!): VisualizationPayload! 4 | } 5 | 6 | input VisualizeInput { 7 | text: String! 8 | driver: Driver! 9 | type: FormatType = HCL 10 | } 11 | 12 | enum Driver { 13 | SQLITE 14 | MYSQL 15 | POSTGRESQL 16 | } 17 | 18 | enum FormatType { 19 | HCL 20 | } 21 | 22 | type ShareVisualizationPayload { 23 | success: Boolean! 24 | } 25 | 26 | input ShareVisualizationInput { 27 | fromID: String! 28 | toID: String 29 | } 30 | 31 | type VisualizationPayload { 32 | node: Visualization 33 | } 34 | 35 | type Visualization { 36 | extID: String! 37 | } -------------------------------------------------------------------------------- /internal/integration/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | image: postgres 4 | ports: 5 | - "5432:5432" 6 | environment: 7 | POSTGRES_DB: dev 8 | POSTGRES_PASSWORD: pass 9 | healthcheck: 10 | test: pg_isready -U postgres 11 | mysql: 12 | image: mysql 13 | ports: 14 | - "3306:3306" 15 | environment: 16 | MYSQL_DATABASE: dev 17 | MYSQL_ROOT_PASSWORD: pass 18 | healthcheck: 19 | test: mysqladmin ping -ppass 20 | -------------------------------------------------------------------------------- /internal/integration/integration_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "ariga.io/entviz/internal/entviz" 8 | "entgo.io/ent/dialect" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | const ( 13 | testSQLiteHCL = `table "users" { 14 | schema = schema.main 15 | column "id" { 16 | null = false 17 | type = integer 18 | auto_increment = true 19 | } 20 | column "name" { 21 | null = false 22 | type = text 23 | } 24 | primary_key { 25 | columns = [column.id] 26 | } 27 | } 28 | schema "main" { 29 | } 30 | ` 31 | testMySQLHCL = `table "users" { 32 | schema = schema.dev 33 | collate = "utf8mb4_bin" 34 | column "id" { 35 | null = false 36 | type = bigint 37 | auto_increment = true 38 | } 39 | column "name" { 40 | null = false 41 | type = varchar(255) 42 | } 43 | primary_key { 44 | columns = [column.id] 45 | } 46 | } 47 | schema "dev" { 48 | charset = "utf8mb4" 49 | collate = "utf8mb4_0900_ai_ci" 50 | } 51 | ` 52 | testPostgresHCL = `table "users" { 53 | schema = schema.public 54 | column "id" { 55 | null = false 56 | type = bigint 57 | identity { 58 | generated = BY_DEFAULT 59 | } 60 | } 61 | column "name" { 62 | null = false 63 | type = character_varying 64 | } 65 | primary_key { 66 | columns = [column.id] 67 | } 68 | } 69 | schema "public" { 70 | } 71 | ` 72 | ) 73 | 74 | func TestIntegrationSQLite(t *testing.T) { 75 | t.Parallel() 76 | ctx := context.Background() 77 | t.Run("valid schema", func(t *testing.T) { 78 | t.Parallel() 79 | hclDocument, err := entviz.HCL(ctx, entviz.HCLOptions{ 80 | SchemaPath: "./testdata/ent/schema", 81 | Dialect: dialect.SQLite, 82 | DevURL: "sqlite3://file?mode=memory&cache=shared&_fk=1", 83 | GlobalUniqueID: false, 84 | }) 85 | require.NoError(t, err) 86 | require.Equal(t, testSQLiteHCL, string(hclDocument)) 87 | }) 88 | t.Run("missing schema type", func(t *testing.T) { 89 | t.Parallel() 90 | _, err := entviz.HCL(ctx, entviz.HCLOptions{ 91 | SchemaPath: "./testdata/schema-type/schema", 92 | Dialect: dialect.SQLite, 93 | DevURL: "sqlite3://file?mode=memory&cache=shared&_fk=1", 94 | GlobalUniqueID: false, 95 | }) 96 | require.Error(t, err) 97 | require.ErrorContains(t, err, "sqlite3: schema type for column users.create_time not defined") 98 | }) 99 | } 100 | 101 | func TestIntegrationMySQL(t *testing.T) { 102 | t.Parallel() 103 | ctx := context.Background() 104 | hclDocument, err := entviz.HCL(ctx, entviz.HCLOptions{ 105 | SchemaPath: "./testdata/ent/schema", 106 | Dialect: dialect.MySQL, 107 | DevURL: "mysql://root:pass@localhost:3306/dev", 108 | GlobalUniqueID: false, 109 | }) 110 | require.NoError(t, err) 111 | require.Equal(t, testMySQLHCL, string(hclDocument)) 112 | } 113 | 114 | func TestIntegrationPostgres(t *testing.T) { 115 | t.Parallel() 116 | ctx := context.Background() 117 | hclDocument, err := entviz.HCL(ctx, entviz.HCLOptions{ 118 | SchemaPath: "./testdata/ent/schema", 119 | Dialect: dialect.Postgres, 120 | DevURL: "postgres://postgres:pass@localhost:5432/dev?sslmode=disable", 121 | GlobalUniqueID: false, 122 | }) 123 | require.NoError(t, err) 124 | require.Equal(t, testPostgresHCL, string(hclDocument)) 125 | } 126 | -------------------------------------------------------------------------------- /internal/integration/testdata/ent/schema/user.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/field" 6 | ) 7 | 8 | // User holds the schema definition for the User entity. 9 | type User struct { 10 | ent.Schema 11 | } 12 | 13 | // Fields of the User. 14 | func (User) Fields() []ent.Field { 15 | return []ent.Field{ 16 | field.String("name"), 17 | } 18 | } 19 | 20 | // Edges of the User. 21 | func (User) Edges() []ent.Edge { 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /internal/integration/testdata/schema-type/schema/user.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "database/sql" 5 | 6 | "entgo.io/ent" 7 | "entgo.io/ent/schema/field" 8 | ) 9 | 10 | // User holds the schema definition for the User entity. 11 | type User struct { 12 | ent.Schema 13 | } 14 | 15 | // Fields of the User. 16 | func (User) Fields() []ent.Field { 17 | return []ent.Field{ 18 | field.String("name"), 19 | field.Other("create_time", &sql.NullTime{}).SchemaType(map[string]string{"not": "defined"}), 20 | } 21 | } 22 | 23 | // Edges of the User. 24 | func (User) Edges() []ent.Edge { 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "os/signal" 9 | 10 | "ariga.io/entviz/internal/entviz" 11 | ) 12 | 13 | var ( 14 | devURL string 15 | globalUniqueID bool 16 | ) 17 | 18 | func init() { 19 | flag.StringVar(&devURL, "dev-url", "sqlite3://file?mode=memory&cache=shared&_fk=1", "dev database to be used to generate the schema") 20 | flag.BoolVar(&globalUniqueID, "global-unique-id", false, "enable the Global Unique ID feature") 21 | flag.Usage = func() { 22 | fmt.Fprintf(flag.CommandLine.Output(), "Usage of ariga.io/entviz\n") 23 | fmt.Fprintf(flag.CommandLine.Output(), "\tgo run -mod=mod ariga.io/entviz \nFlags:\n") 24 | flag.PrintDefaults() 25 | } 26 | } 27 | 28 | func main() { 29 | ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) 30 | flag.Parse() 31 | schemaPath := flag.Arg(0) 32 | if schemaPath == "" { 33 | flag.Usage() 34 | os.Exit(1) 35 | } 36 | parsedDialect, atlasDriverName, err := entviz.ParseDevURL(devURL) 37 | if err != nil { 38 | fmt.Fprintf(os.Stderr, "error: invalid dev-url: %v\n", err) 39 | os.Exit(1) 40 | } 41 | hcl, err := entviz.HCL(ctx, entviz.HCLOptions{ 42 | SchemaPath: schemaPath, 43 | Dialect: parsedDialect, 44 | DevURL: devURL, 45 | GlobalUniqueID: globalUniqueID, 46 | }) 47 | if err != nil { 48 | fmt.Fprintf(os.Stderr, "error: %v\n", err) 49 | os.Exit(1) 50 | } 51 | link, err := entviz.Share(ctx, hcl, atlasDriverName) 52 | if err != nil { 53 | fmt.Fprintf(os.Stderr, "error: %v\n", err) 54 | os.Exit(1) 55 | } 56 | fmt.Fprintf(os.Stdout, "Here is a public link to your schema visualization\n\t%s\n", link) 57 | } 58 | --------------------------------------------------------------------------------