├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── _test └── dbmeta │ └── main.go ├── code_dao_gorm.md ├── code_dao_sqlx.md ├── code_http.md ├── code_model.md ├── code_protobuf.md ├── custom ├── custom.go.tmpl ├── custom.json ├── custom.md.tmpl └── sample.gen ├── dbmeta ├── codegen.go ├── db_utils.go ├── meta.go ├── meta_mssql.go ├── meta_mysql.go ├── meta_postgres.go ├── meta_sqlite.go ├── meta_unknown.go ├── meta_utils.go ├── util.go └── util_test.go ├── example └── sample.db ├── go.mod ├── main-packr.go ├── main.go ├── packrd └── packed-packr.go ├── readme └── main.go ├── release.history ├── release.sh ├── template ├── GEN_README.md.tmpl ├── Makefile.tmpl ├── README.md.tmpl ├── api.go.tmpl ├── api_add.go.tmpl ├── api_delete.go.tmpl ├── api_get.go.tmpl ├── api_getall.go.tmpl ├── api_update.go.tmpl ├── code_dao_gorm.md.tmpl ├── code_dao_sqlx.md.tmpl ├── code_http.md.tmpl ├── dao_gorm.go.tmpl ├── dao_gorm_add.go.tmpl ├── dao_gorm_delete.go.tmpl ├── dao_gorm_get.go.tmpl ├── dao_gorm_getall.go.tmpl ├── dao_gorm_init.go.tmpl ├── dao_gorm_update.go.tmpl ├── dao_sqlx.go.tmpl ├── dao_sqlx_add.go.tmpl ├── dao_sqlx_delete.go.tmpl ├── dao_sqlx_get.go.tmpl ├── dao_sqlx_getall.go.tmpl ├── dao_sqlx_init.go.tmpl ├── dao_sqlx_update.go.tmpl ├── debug.txt ├── gitignore.tmpl ├── gomod.tmpl ├── http_utils.go.tmpl ├── main_gorm.go.tmpl ├── main_sqlx.go.tmpl ├── mapping.json ├── model.go.tmpl ├── model_base.go.tmpl ├── protobuf.tmpl ├── protomain.go.tmpl ├── protoserver.go.tmpl └── router.go.tmpl ├── test.sh └── utils ├── copy.go ├── options.go └── results.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Go template 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Dependency directories (remove the comment below to include it) 17 | # vendor/ 18 | 19 | go.sum 20 | .idea/ 21 | #packrd/ 22 | #main-packr.go 23 | gen 24 | # 25 | # only store sample db in git 26 | # 27 | example/.gitignore 28 | example/README.md 29 | example/api/ 30 | example/app/ 31 | example/dao/ 32 | example/go.mod 33 | example/model/ 34 | example/Makefile 35 | example/docs/ 36 | example/templates/ 37 | example/go.sum 38 | custom/test/ 39 | example/*.proto 40 | example/grpc 41 | example/apis/ 42 | example/daos/ 43 | example/models/ 44 | tests/ 45 | .env 46 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.13.x 5 | - tip 6 | 7 | before_script: 8 | - go get -t -v github.com/smallnest/gen/... 9 | 10 | script: 11 | - go build main.go 12 | 13 | notifications: 14 | email: 15 | recipients: smallnest@gmail.com 16 | on_success: change 17 | on_failure: always 18 | -------------------------------------------------------------------------------- /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, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | WORKDIR=`pwd` 2 | export PACKR2_EXECUTABLE := $(shell command -v packr2 2> /dev/null) 3 | 4 | #################################################################################################################### 5 | ## 6 | ## help for each task - https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html 7 | ## 8 | #################################################################################################################### 9 | .PHONY: help 10 | 11 | help: ## This help. 12 | @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) 13 | 14 | .DEFAULT_GOAL := help 15 | 16 | check_prereq: ## check pre requisites exist 17 | ifndef PACKR2_EXECUTABLE 18 | go get -u github.com/gobuffalo/packr/v2/packr2 19 | endif 20 | $(warning "found packr2") 21 | 22 | 23 | install: check_prereq ## go install binary info $GOPATH/bin 24 | packr2 install github.com/smallnest/gen 25 | 26 | vet: ## run go vet on the project 27 | go vet . 28 | 29 | tools: ## install dependent tools 30 | go get -u github.com/gogo/protobuf 31 | go get -u github.com/gogo/protobuf/proto 32 | go get -u github.com/gogo/protobuf/jsonpb 33 | go get -u github.com/gogo/protobuf/protoc-gen-gogo 34 | go get -u github.com/gogo/protobuf/gogoproto 35 | go get -u honnef.co/go/tools 36 | go get -u github.com/gordonklaus/ineffassign 37 | go get -u github.com/fzipp/gocyclo 38 | go get -u golang.org/x/lint/golint 39 | go get -u github.com/gobuffalo/packr/v2/packr2 40 | 41 | lint: ## run golint on the project 42 | golint ./... 43 | 44 | staticcheck: ## run staticcheck on the project 45 | staticcheck -ignore "$(shell cat .checkignore)" . 46 | 47 | unused: 48 | unused . 49 | 50 | gocyclo: ## run gocyclo on the project 51 | @ gocyclo -over 20 $(shell find . -name "*.go" |egrep -v "pb\.go|_test\.go") 52 | 53 | check: staticcheck gocyclo ## run code checks on the project 54 | 55 | doc: ## run godoc 56 | godoc -http=:6060 57 | 58 | deps:## analyze project deps 59 | go list -f '{{ join .Deps "\n"}}' . |grep "/" | grep -v "github.com/smallnest/gen"| grep "\." | sort |uniq 60 | 61 | fmt: ## run fmt on the project 62 | ## go fmt . 63 | gofmt -s -d -w -l . 64 | 65 | build: check_prereq ## build gen binary 66 | packr2 build . 67 | 68 | gen: build ## build gen binary 69 | 70 | test: ## run go test on the project 71 | go test -v . 72 | 73 | example: generate_example ## generate example 74 | 75 | generate_example: clean_example ## generate example project code from sqlite db in ./examples 76 | ls -latr ./example 77 | cd ./example && go run .. \ 78 | --sqltype=sqlite3 \ 79 | --connstr "./sample.db" \ 80 | --database main \ 81 | --module github.com/alexj212/generated \ 82 | --verbose \ 83 | --overwrite \ 84 | --out ./ \ 85 | --templateDir=../template \ 86 | --mapping=../template/mapping.json \ 87 | --json \ 88 | --db \ 89 | --api=apis \ 90 | --dao=daos \ 91 | --model=models \ 92 | --generate-dao \ 93 | --generate-proj \ 94 | --protobuf \ 95 | --gorm \ 96 | --guregu \ 97 | --rest \ 98 | --mod \ 99 | --server \ 100 | --makefile \ 101 | --run-gofmt \ 102 | --copy-templates 103 | 104 | test_exec: clean_example ## test example using sqlite db in ./examples 105 | ls -latr ./example 106 | cd ./custom && go run .. \ 107 | --sqltype=sqlite3 \ 108 | --connstr "../example/sample.db" \ 109 | --database main \ 110 | --module github.com/alexj212/generated \ 111 | --context=./custom.json \ 112 | --verbose \ 113 | --overwrite \ 114 | --out ./ \ 115 | --exec=./sample.gen 116 | 117 | 118 | build_example: generate_example ## generate and build example 119 | cd ./example && $(MAKE) example 120 | 121 | run_example: example ## run example project server 122 | cd ./example && ./bin/example 123 | 124 | 125 | clean_example: ## remove generated example code 126 | mkdir -p ./example 127 | rm -rf ./example/Makefile \ 128 | ./example/README.md \ 129 | ./example/api \ 130 | ./example/app \ 131 | ./example/bin \ 132 | ./example/dao \ 133 | ./example/docs \ 134 | ./example/go.mod \ 135 | ./example/go.sum \ 136 | ./example/model \ 137 | ./example/.gitignore \ 138 | ./tests 139 | 140 | 141 | 142 | 143 | 144 | 145 | run_dbmeta: ## generate example project code from sqlite db in ./examples 146 | go run github.com/smallnest/gen/_test/dbmeta \ 147 | --sqltype=sqlite3 \ 148 | --connstr "./example/sample.db" \ 149 | --database main 150 | 151 | ## --table employees_2 152 | 153 | 154 | test: clean_example test_mysql test_postgres test_sqlite3 test_mssql ## test mysql, mssql, postgres and sqlite3 code generation 155 | 156 | test_mysql: ## test sqlite3 code generation 157 | ./test.sh mysql gen_sqlx && cd ./tests/mysql_sqlx && make example 158 | ./test.sh mysql gen_gorm && cd ./tests/mysql_gorm && make example 159 | test_postgres: ## test postgres code generation 160 | ./test.sh postgres gen_sqlx && cd ./tests/postgres_sqlx && make example 161 | ./test.sh postgres gen_gorm && cd ./tests/postgres_gorm && make example 162 | test_mssql: ## test mssql code generation 163 | ./test.sh mssql gen_sqlx && cd ./tests/mssql_sqlx && make example 164 | ./test.sh mssql gen_gorm && cd ./tests/mssql_gorm && make example 165 | test_sqlite3: ## test sqlite3 code generation 166 | ./test.sh sqlite3 gen_sqlx && cd ./tests/sqlite3_sqlx && make example 167 | ./test.sh sqlite3 gen_gorm && cd ./tests/sqlite3_gorm && make example 168 | 169 | 170 | 171 | 172 | set_release: ## populate release info 173 | ./release.sh 174 | 175 | gen_readme: set_release ## generate readme file 176 | go run github.com/smallnest/gen/readme \ 177 | --sqltype=sqlite3 \ 178 | --connstr "./example/sample.db" \ 179 | --database main \ 180 | --table invoices 181 | 182 | 183 | release: gen_readme fmt gen install example ## prepare release 184 | $(info ************ Release completed) 185 | 186 | git_sync: ## git sync upstream 187 | git fetch upstream 188 | git checkout master 189 | git merge upstream/maste -------------------------------------------------------------------------------- /_test/dbmeta/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/droundy/goopt" 9 | "github.com/gobuffalo/packr/v2" 10 | "github.com/jimsmart/schema" 11 | 12 | _ "github.com/denisenkom/go-mssqldb" 13 | _ "github.com/jinzhu/gorm/dialects/mysql" 14 | _ "github.com/lib/pq" 15 | _ "github.com/mattn/go-sqlite3" 16 | 17 | "github.com/smallnest/gen/dbmeta" 18 | ) 19 | 20 | var ( 21 | sqlType = goopt.String([]string{"--sqltype"}, "mysql", "sql database type such as [ mysql, mssql, postgres, sqlite, etc. ]") 22 | sqlConnStr = goopt.String([]string{"-c", "--connstr"}, "nil", "database connection string") 23 | sqlDatabase = goopt.String([]string{"-d", "--database"}, "nil", "Database to for connection") 24 | sqlTable = goopt.String([]string{"-t", "--table"}, "", "Table to build struct from") 25 | baseTemplates *packr.Box 26 | ) 27 | 28 | func init() { 29 | // Setup goopts 30 | goopt.Description = func() string { 31 | return "ORM and RESTful meta data viewer for SQl databases" 32 | } 33 | goopt.Version = "v0.9.27 (08/04/2020)" 34 | goopt.Summary = `dbmeta [-v] --sqltype=mysql --connstr "user:password@/dbname" --database 35 | 36 | sqltype - sql database type such as [ mysql, mssql, postgres, sqlite, etc. ] 37 | 38 | ` 39 | 40 | //Parse options 41 | goopt.Parse(nil) 42 | 43 | } 44 | 45 | func main() { 46 | baseTemplates = packr.New("gen", "./template") 47 | 48 | var err error 49 | var content []byte 50 | content, err = baseTemplates.Find("mapping.json") 51 | if err != nil { 52 | fmt.Printf("Error getting default map[mapping file error: %v\n", err) 53 | return 54 | } 55 | 56 | err = dbmeta.ProcessMappings("internal", content, false) 57 | if err != nil { 58 | fmt.Printf("Error processing default mapping file error: %v\n", err) 59 | return 60 | } 61 | 62 | // Username is required 63 | if sqlConnStr == nil || *sqlConnStr == "" || *sqlConnStr == "nil" { 64 | fmt.Printf("sql connection string is required! Add it with --connstr=s\n\n") 65 | fmt.Println(goopt.Usage()) 66 | return 67 | } 68 | 69 | if sqlDatabase == nil || *sqlDatabase == "" || *sqlDatabase == "nil" { 70 | fmt.Printf("Database can not be null\n\n") 71 | fmt.Println(goopt.Usage()) 72 | return 73 | } 74 | 75 | var db *sql.DB 76 | db, err = sql.Open(*sqlType, *sqlConnStr) 77 | if err != nil { 78 | fmt.Printf("Error in open database: %v\n\n", err.Error()) 79 | return 80 | } 81 | defer func() { _ = db.Close() }() 82 | 83 | err = db.Ping() 84 | if err != nil { 85 | fmt.Printf("Error connecting to database: %v\n\n", err.Error()) 86 | return 87 | } 88 | 89 | var dbTables []string 90 | // parse or read tables 91 | if *sqlTable != "" && *sqlTable != "all" { 92 | dbTables = strings.Split(*sqlTable, ",") 93 | fmt.Printf("showing meta for table(s): %s\n", *sqlTable) 94 | } else { 95 | fmt.Printf("showing meta for all tables\n") 96 | dbTables, err = schema.TableNames(db) 97 | if err != nil { 98 | fmt.Printf("Error in fetching tables information from %s information schema from %s\n", *sqlType, *sqlConnStr) 99 | return 100 | } 101 | } 102 | 103 | for _, tableName := range dbTables { 104 | fmt.Printf("---------------------------\n") 105 | if strings.HasPrefix(tableName, "[") && strings.HasSuffix(tableName, "]") { 106 | tableName = tableName[1 : len(tableName)-1] 107 | } 108 | 109 | fmt.Printf("[%s]\n", tableName) 110 | 111 | tableInfo, err := dbmeta.LoadMeta(*sqlType, db, *sqlDatabase, tableName) 112 | 113 | if err != nil { 114 | fmt.Printf("Error getting table info for %s error: %v\n\n\n\n", tableName, err) 115 | continue 116 | } 117 | 118 | fmt.Printf("\n\nDDL\n%s\n\n\n", tableInfo.DDL()) 119 | 120 | for _, col := range tableInfo.Columns() { 121 | fmt.Printf("%s\n", col.String()) 122 | 123 | colMapping, err := dbmeta.SQLTypeToMapping(strings.ToLower(col.DatabaseTypeName())) 124 | if err != nil { // unknown type 125 | fmt.Printf("unable to find mapping for db type: %s\n", col.DatabaseTypeName()) 126 | continue 127 | } 128 | fmt.Printf(" %s\n", colMapping.String()) 129 | } 130 | 131 | primaryCnt := dbmeta.PrimaryKeyCount(tableInfo) 132 | fmt.Printf("primaryCnt: %d\n", primaryCnt) 133 | 134 | fmt.Printf("\n\n") 135 | delSQL, err := dbmeta.GenerateDeleteSQL(tableInfo) 136 | if err == nil { 137 | fmt.Printf("delSQL: %s\n", delSQL) 138 | } 139 | 140 | updateSQL, err := dbmeta.GenerateUpdateSQL(tableInfo) 141 | if err == nil { 142 | fmt.Printf("updateSQL: %s\n", updateSQL) 143 | } 144 | 145 | insertSQL, err := dbmeta.GenerateInsertSQL(tableInfo) 146 | if err == nil { 147 | fmt.Printf("insertSQL: %s\n", insertSQL) 148 | } 149 | 150 | selectOneSQL, err := dbmeta.GenerateSelectOneSQL(tableInfo) 151 | if err == nil { 152 | fmt.Printf("selectOneSQL: %s\n", selectOneSQL) 153 | } 154 | 155 | selectMultiSQL, err := dbmeta.GenerateSelectMultiSQL(tableInfo) 156 | if err == nil { 157 | fmt.Printf("selectMultiSQL: %s\n", selectMultiSQL) 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /code_dao_gorm.md: -------------------------------------------------------------------------------- 1 | [comment]: <> (This is a generated file please edit source in ./templates) 2 | [comment]: <> (All modification will be lost, you have been warned) 3 | [comment]: <> () 4 | 5 | ## CRUD DAO Functions 6 | `gen` will generate dao functions if the `--generate-dao` is passed to `gen`. The code can be customized with the `--dao=dao` flag to set the name of the dao package. 7 | 8 | Code can be generated in two flavours, SQLX by default and GORM with the flag `--gorm` 9 | 10 | 11 | The code generation, will generate functions for 12 | - [Retrieving records with paging](#Retrieve-Paged-Records) 13 | - [Retrieve a specific record](#Retrieve-record) 14 | - [Create a record](#Create-record) 15 | - [Update a record](#Update-record) 16 | - [Delete a record](#Delete-record) 17 | 18 | ## Retrieve Paged Records 19 | ```go 20 | 21 | // GetAllInvoices is a function to get a slice of record(s) from invoices table in the main database 22 | // params - page - page requested (defaults to 0) 23 | // params - pagesize - number of records in a page (defaults to 20) 24 | // params - order - db sort order column 25 | // error - ErrNotFound, db Find error 26 | func GetAllInvoices(ctx context.Context, page, pagesize int64, order string) (results []*model.Invoices, totalRows int, err error) { 27 | 28 | resultOrm := DB.Model(&model.Invoices{}) 29 | resultOrm.Count(&totalRows) 30 | 31 | if page > 0 { 32 | offset := (page - 1) * pagesize 33 | resultOrm = resultOrm.Offset(offset).Limit(pagesize) 34 | } else { 35 | resultOrm = resultOrm.Limit(pagesize) 36 | } 37 | 38 | if order != "" { 39 | resultOrm = resultOrm.Order(order) 40 | } 41 | 42 | if err = resultOrm.Find(&results).Error; err != nil { 43 | err = ErrNotFound 44 | return nil, -1, err 45 | } 46 | 47 | return results, totalRows, nil 48 | } 49 | 50 | ``` 51 | 52 | ## Retrieve record 53 | ```go 54 | 55 | // GetInvoices is a function to get a single record from the invoices table in the main database 56 | // error - ErrNotFound, db Find error 57 | func GetInvoices(ctx context.Context, argInvoiceID int32, ) (record *model.Invoices, err error) { 58 | record = &model.Invoices{} 59 | if err = DB.First(record, argInvoiceID, ).Error; err != nil { 60 | err = ErrNotFound 61 | return record, err 62 | } 63 | 64 | return record, nil 65 | } 66 | 67 | ``` 68 | 69 | ## Create record 70 | ```go 71 | 72 | // AddInvoices is a function to add a single record to invoices table in the main database 73 | // error - ErrInsertFailed, db save call failed 74 | func AddInvoices(ctx context.Context, record *model.Invoices) (result *model.Invoices, RowsAffected int64, err error) { 75 | db := DB.Save(record) 76 | if err = db.Error; err != nil { 77 | return nil, -1, ErrInsertFailed 78 | } 79 | 80 | return record, db.RowsAffected, nil 81 | } 82 | 83 | ``` 84 | 85 | ## Update record 86 | ```go 87 | 88 | // UpdateInvoices is a function to update a single record from invoices table in the main database 89 | // error - ErrNotFound, db record for id not found 90 | // error - ErrUpdateFailed, db meta data copy failed or db.Save call failed 91 | func UpdateInvoices(ctx context.Context, argInvoiceID int32, updated *model.Invoices) (result *model.Invoices, RowsAffected int64, err error) { 92 | 93 | result = &model.Invoices{} 94 | db := DB.First(result, argInvoiceID, ) 95 | if err = db.Error; err != nil { 96 | return nil, -1, ErrNotFound 97 | } 98 | 99 | if err = Copy(result, updated); err != nil { 100 | return nil, -1, ErrUpdateFailed 101 | } 102 | 103 | db = db.Save(result) 104 | if err = db.Error; err != nil { 105 | return nil, -1, ErrUpdateFailed 106 | } 107 | 108 | return result, db.RowsAffected, nil 109 | } 110 | 111 | ``` 112 | 113 | ## Delete record 114 | ```go 115 | 116 | // DeleteInvoices is a function to delete a single record from invoices table in the main database 117 | // error - ErrNotFound, db Find error 118 | // error - ErrDeleteFailed, db Delete failed error 119 | func DeleteInvoices(ctx context.Context, argInvoiceID int32, ) (rowsAffected int64, err error) { 120 | 121 | record := &model.Invoices{} 122 | db := DB.First(record, argInvoiceID, ) 123 | if db.Error != nil { 124 | return -1, ErrNotFound 125 | } 126 | 127 | db = db.Delete(record) 128 | if err = db.Error; err != nil { 129 | return -1, ErrDeleteFailed 130 | } 131 | 132 | return db.RowsAffected, nil 133 | } 134 | 135 | ``` 136 | -------------------------------------------------------------------------------- /code_dao_sqlx.md: -------------------------------------------------------------------------------- 1 | [comment]: <> (This is a generated file please edit source in ./templates) 2 | [comment]: <> (All modification will be lost, you have been warned) 3 | [comment]: <> () 4 | 5 | ## CRUD DAO Functions 6 | `gen` will generate dao functions if the `--generate-dao` is passed to `gen`. The code can be customized with the `--dao=dao` flag to set the name of the dao package. 7 | 8 | Code can be generated in two flavours, SQLX by default and GORM with the flag `--gorm` 9 | 10 | 11 | The code generation, will generate functions for 12 | - [Retrieving records with paging](#Retrieve-Paged-Records) 13 | - [Retrieve a specific record](#Retrieve-record) 14 | - [Create a record](#Create-record) 15 | - [Update a record](#Update-record) 16 | - [Delete a record](#Delete-record) 17 | 18 | ## Retrieve Paged Records 19 | ```go 20 | 21 | // GetAllInvoices is a function to get a slice of record(s) from invoices table in the main database 22 | // params - page - page requested (defaults to 0) 23 | // params - pagesize - number of records in a page (defaults to 20) 24 | // params - order - db sort order column 25 | // error - ErrNotFound, db Find error 26 | func GetAllInvoices(ctx context.Context, page, pagesize int64, order string) (results []*model.Invoices, totalRows int, err error) { 27 | sql := "SELECT * FROM `invoices`" 28 | 29 | if order != "" { 30 | if strings.ContainsAny(order, "'\"") { 31 | order = "" 32 | } 33 | } 34 | 35 | if order == "" { 36 | order = "InvoiceId" 37 | } 38 | 39 | if DB.DriverName() == "mssql" { 40 | sql = fmt.Sprintf("%s order by %s OFFSET %d ROWS FETCH FIRST %d ROWS ONLY", sql, order, page, pagesize) 41 | } else if DB.DriverName() == "postgres" { 42 | sql = fmt.Sprintf("%s order by `%s` OFFSET %d LIMIT %d", sql, order, page, pagesize) 43 | } else { 44 | sql = fmt.Sprintf("%s order by `%s` LIMIT %d, %d", sql, order, page, pagesize) 45 | } 46 | sql = DB.Rebind(sql) 47 | 48 | if Logger != nil { 49 | Logger(ctx, sql) 50 | } 51 | 52 | err = DB.SelectContext(ctx, &results, sql) 53 | if err != nil { 54 | return nil, -1, err 55 | } 56 | 57 | cnt , err := GetRowCount(ctx, "invoices") 58 | if err != nil { 59 | return results, -2, err 60 | } 61 | 62 | return results, cnt, err 63 | } 64 | 65 | 66 | ``` 67 | 68 | ## Retrieve record 69 | ```go 70 | 71 | // GetInvoices is a function to get a single record from the invoices table in the main database 72 | // error - ErrNotFound, db Find error 73 | func GetInvoices(ctx context.Context, argInvoiceID int32, ) (record *model.Invoices, err error) { 74 | sql := "SELECT * FROM `invoices` WHERE InvoiceId = ?" 75 | sql = DB.Rebind(sql) 76 | 77 | if Logger != nil { 78 | Logger(ctx, sql) 79 | } 80 | 81 | record = &model.Invoices{} 82 | err = DB.GetContext(ctx, record, sql, argInvoiceID, ) 83 | if err != nil { 84 | return nil, err 85 | } 86 | return record, nil 87 | } 88 | 89 | ``` 90 | 91 | ## Create record 92 | ```go 93 | 94 | // AddInvoices is a function to add a single record to invoices table in the main database 95 | // error - ErrInsertFailed, db save call failed 96 | func AddInvoices(ctx context.Context, record *model.Invoices) (result *model.Invoices, RowsAffected int64, err error) { 97 | if DB.DriverName() == "postgres" { 98 | return addInvoicesPostgres( ctx, record) 99 | } else { 100 | return addInvoices( ctx, record) 101 | } 102 | } 103 | 104 | // addInvoicesPostgres is a function to add a single record to invoices table in the main database 105 | // error - ErrInsertFailed, db save call failed 106 | func addInvoicesPostgres(ctx context.Context, record *model.Invoices) (result *model.Invoices, RowsAffected int64, err error) { 107 | sql := "INSERT INTO `invoices` ( CustomerId, InvoiceDate, BillingAddress, BillingCity, BillingState, BillingCountry, BillingPostalCode, Total) values ( ?, ?, ?, ?, ?, ?, ?, ? )" 108 | sql = DB.Rebind(sql) 109 | 110 | if Logger != nil { 111 | Logger(ctx, sql) 112 | } 113 | 114 | rows := int64(1) 115 | sql = fmt.Sprintf("%s returning %s", sql, "InvoiceId") 116 | dbResult := DB.QueryRowContext(ctx, sql, record.CustomerID, record.InvoiceDate, record.BillingAddress, record.BillingCity, record.BillingState, record.BillingCountry, record.BillingPostalCode, record.Total,) 117 | err = dbResult.Scan( record.CustomerID, record.InvoiceDate, record.BillingAddress, record.BillingCity, record.BillingState, record.BillingCountry, record.BillingPostalCode, record.Total,) 118 | 119 | return record, rows, err 120 | } 121 | 122 | // addInvoicesPostgres is a function to add a single record to invoices table in the main database 123 | // error - ErrInsertFailed, db save call failed 124 | func addInvoices(ctx context.Context, record *model.Invoices) (result *model.Invoices, RowsAffected int64, err error) { 125 | sql := "INSERT INTO `invoices` ( CustomerId, InvoiceDate, BillingAddress, BillingCity, BillingState, BillingCountry, BillingPostalCode, Total) values ( ?, ?, ?, ?, ?, ?, ?, ? )" 126 | sql = DB.Rebind(sql) 127 | 128 | if Logger != nil { 129 | Logger(ctx, sql) 130 | } 131 | 132 | rows := int64(0) 133 | 134 | dbResult, err := DB.ExecContext(ctx, sql, record.CustomerID, record.InvoiceDate, record.BillingAddress, record.BillingCity, record.BillingState, record.BillingCountry, record.BillingPostalCode, record.Total,) 135 | if err != nil { 136 | return nil, 0, err 137 | } 138 | 139 | id, err := dbResult.LastInsertId() 140 | rows, err = dbResult.RowsAffected() 141 | 142 | record.InvoiceID = int32(id) 143 | 144 | 145 | return record, rows, err 146 | } 147 | 148 | ``` 149 | 150 | ## Update record 151 | ```go 152 | 153 | // UpdateInvoices is a function to update a single record from invoices table in the main database 154 | // error - ErrNotFound, db record for id not found 155 | // error - ErrUpdateFailed, db meta data copy failed or db.Save call failed 156 | func UpdateInvoices(ctx context.Context, argInvoiceID int32, updated *model.Invoices) (result *model.Invoices, RowsAffected int64, err error) { 157 | sql := "UPDATE `invoices` set CustomerId = ?, InvoiceDate = ?, BillingAddress = ?, BillingCity = ?, BillingState = ?, BillingCountry = ?, BillingPostalCode = ?, Total = ? WHERE InvoiceId = ?" 158 | sql = DB.Rebind(sql) 159 | 160 | if Logger != nil { 161 | Logger(ctx, sql) 162 | } 163 | 164 | dbResult, err := DB.ExecContext(ctx, sql, updated.CustomerID, updated.InvoiceDate, updated.BillingAddress, updated.BillingCity, updated.BillingState, updated.BillingCountry, updated.BillingPostalCode, updated.Total, argInvoiceID, ) 165 | if err != nil { 166 | return nil, 0, err 167 | } 168 | 169 | rows, err := dbResult.RowsAffected() 170 | updated.InvoiceID = argInvoiceID 171 | 172 | return updated, rows, err 173 | } 174 | 175 | ``` 176 | 177 | ## Delete record 178 | ```go 179 | 180 | // DeleteInvoices is a function to delete a single record from invoices table in the main database 181 | // error - ErrNotFound, db Find error 182 | // error - ErrDeleteFailed, db Delete failed error 183 | func DeleteInvoices(ctx context.Context, argInvoiceID int32, ) (rowsAffected int64, err error) { 184 | sql := "DELETE FROM `invoices` where InvoiceId = ?" 185 | sql = DB.Rebind(sql) 186 | 187 | if Logger != nil { 188 | Logger(ctx, sql) 189 | } 190 | 191 | result, err := DB.ExecContext(ctx, sql, argInvoiceID, ) 192 | if err != nil { 193 | return 0, err 194 | } 195 | 196 | return result.RowsAffected() 197 | } 198 | 199 | ``` 200 | -------------------------------------------------------------------------------- /code_http.md: -------------------------------------------------------------------------------- 1 | [comment]: <> (This is a generated file please edit source in ./templates) 2 | [comment]: <> (All modification will be lost, you have been warned) 3 | [comment]: <> () 4 | 5 | ## CRUD Http Handlers 6 | `gen` will generate http handlers if the `--rest` is used. The code can be customized with the `--api=api` flag to set the name of the api package. 7 | 8 | - [Retrieving records with paging](#Retrieve-Paged-Records) 9 | - [Retrieve a specific record](#Retrieve-record) 10 | - [Create a record](#Create-record) 11 | - [Update a record](#Update-record) 12 | - [Delete a record](#Delete-record) 13 | 14 | `gen` will add swagger comments to the source generated, this too can be customized with the following. 15 | ```bash 16 | --swagger_version=1.0 swagger version 17 | --swagger_path=/ swagger base path 18 | --swagger_tos= swagger tos url 19 | --swagger_contact_name=Me swagger contact name 20 | --swagger_contact_url=http://me.com/terms.html swagger contact url 21 | --swagger_contact_email=me@me.com swagger contact email 22 | ``` 23 | 24 | 25 | ## Retrieve Paged Records 26 | ```go 27 | 28 | // GetAllInvoices is a function to get a slice of record(s) from invoices table in the main database 29 | // @Summary Get list of Invoices 30 | // @Tags Invoices 31 | // @Description GetAllInvoices is a handler to get a slice of record(s) from invoices table in the main database 32 | // @Accept json 33 | // @Produce json 34 | // @Param page query int false "page requested (defaults to 0)" 35 | // @Param pagesize query int false "number of records in a page (defaults to 20)" 36 | // @Param order query string false "db sort order column" 37 | // @Success 200 {object} api.PagedResults{data=[]model.Invoices} 38 | // @Failure 400 {object} api.HTTPError 39 | // @Failure 404 {object} api.HTTPError 40 | // @Router /invoices [get] 41 | // http "http://127.0.0.1:8080/invoices?page=0&pagesize=20" X-Api-User:user123 42 | func GetAllInvoices(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 43 | ctx := initializeContext(r) 44 | page, err := readInt(r, "page", 0) 45 | if err != nil || page < 0 { 46 | returnError(ctx, w, r, dao.ErrBadParams) 47 | return 48 | } 49 | 50 | pagesize, err := readInt(r, "pagesize", 20) 51 | if err != nil || pagesize <= 0 { 52 | returnError(ctx, w, r, dao.ErrBadParams) 53 | return 54 | } 55 | 56 | order := r.FormValue("order") 57 | 58 | if err := ValidateRequest(ctx, r, "invoices", model.RetrieveMany); err != nil{ 59 | returnError(ctx, w, r, err) 60 | return 61 | } 62 | 63 | records, totalRows, err := dao.GetAllInvoices(ctx, page, pagesize, order) 64 | if err != nil { 65 | returnError(ctx, w, r, err) 66 | return 67 | } 68 | 69 | result := &PagedResults{Page: page, PageSize: pagesize, Data: records, TotalRecords: totalRows} 70 | writeJSON(ctx, w, result) 71 | } 72 | 73 | ``` 74 | 75 | ## Retrieve record 76 | ```go 77 | 78 | // GetInvoices is a function to get a single record from the invoices table in the main database 79 | // @Summary Get record from table Invoices by argInvoiceID 80 | // @Tags Invoices 81 | // @ID argInvoiceID 82 | // @Description GetInvoices is a function to get a single record from the invoices table in the main database 83 | // @Accept json 84 | // @Produce json 85 | // @Param argInvoiceID path int true "InvoiceId" 86 | // @Success 200 {object} model.Invoices 87 | // @Failure 400 {object} api.HTTPError 88 | // @Failure 404 {object} api.HTTPError "ErrNotFound, db record for id not found - returns NotFound HTTP 404 not found error" 89 | // @Router /invoices/{argInvoiceID} [get] 90 | // http "http://127.0.0.1:8080/invoices/1" X-Api-User:user123 91 | func GetInvoices(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 92 | ctx := initializeContext(r) 93 | 94 | 95 | 96 | argInvoiceID, err := parseInt32(ps, "argInvoiceID") 97 | if err != nil { 98 | returnError(ctx, w, r, err) 99 | return 100 | } 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | if err := ValidateRequest(ctx, r, "invoices", model.RetrieveOne); err != nil{ 112 | returnError(ctx, w, r, err) 113 | return 114 | } 115 | 116 | record, err := dao.GetInvoices(ctx, argInvoiceID, ) 117 | if err != nil { 118 | returnError(ctx, w, r, err) 119 | return 120 | } 121 | 122 | writeJSON(ctx, w, record) 123 | } 124 | 125 | ``` 126 | 127 | ## Create record 128 | ```go 129 | 130 | // AddInvoices add to add a single record to invoices table in the main database 131 | // @Summary Add an record to invoices table 132 | // @Description add to add a single record to invoices table in the main database 133 | // @Tags Invoices 134 | // @Accept json 135 | // @Produce json 136 | // @Param Invoices body model.Invoices true "Add Invoices" 137 | // @Success 200 {object} model.Invoices 138 | // @Failure 400 {object} api.HTTPError 139 | // @Failure 404 {object} api.HTTPError 140 | // @Router /invoices [post] 141 | // echo '{"billing_state": "ZDIUXoQexYEYgpaYJgyitZpjS","billing_country": "xbuWQjVGOCsrKSXCJhxeVSFhQ","billing_postal_code": "fEHjgTFVSOwVZEPNUjpVkmOZV","customer_id": 40,"invoice_date": "2021-12-17T01:29:16.603984343-08:00","billing_address": "GoPOJOqToKgHAXoCRwOLzSsgW","billing_city": "pRhdTdgdcpgiBrgvmCMlFKapm","total": 0.38248355155004915,"invoice_id": 46}' | http POST "http://127.0.0.1:8080/invoices" X-Api-User:user123 142 | func AddInvoices(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 143 | ctx := initializeContext(r) 144 | invoices := &model.Invoices{} 145 | 146 | if err := readJSON(r, invoices); err != nil { 147 | returnError(ctx, w, r, dao.ErrBadParams) 148 | return 149 | } 150 | 151 | 152 | if err := invoices.BeforeSave(); err != nil { 153 | returnError(ctx, w, r, dao.ErrBadParams) 154 | } 155 | 156 | invoices.Prepare() 157 | 158 | if err := invoices.Validate(model.Create); err != nil { 159 | returnError(ctx, w, r, dao.ErrBadParams) 160 | return 161 | } 162 | 163 | if err := ValidateRequest(ctx, r, "invoices", model.Create); err != nil{ 164 | returnError(ctx, w, r, err) 165 | return 166 | } 167 | 168 | var err error 169 | invoices, _, err = dao.AddInvoices(ctx, invoices) 170 | if err != nil { 171 | returnError(ctx, w, r, err) 172 | return 173 | } 174 | 175 | writeJSON(ctx, w, invoices) 176 | } 177 | 178 | ``` 179 | 180 | ## Update record 181 | ```go 182 | 183 | // UpdateInvoices Update a single record from invoices table in the main database 184 | // @Summary Update an record in table invoices 185 | // @Description Update a single record from invoices table in the main database 186 | // @Tags Invoices 187 | // @Accept json 188 | // @Produce json 189 | // @Param argInvoiceID path int true "InvoiceId" 190 | // @Param Invoices body model.Invoices true "Update Invoices record" 191 | // @Success 200 {object} model.Invoices 192 | // @Failure 400 {object} api.HTTPError 193 | // @Failure 404 {object} api.HTTPError 194 | // @Router /invoices/{argInvoiceID} [put] 195 | // echo '{"billing_state": "ZDIUXoQexYEYgpaYJgyitZpjS","billing_country": "xbuWQjVGOCsrKSXCJhxeVSFhQ","billing_postal_code": "fEHjgTFVSOwVZEPNUjpVkmOZV","customer_id": 40,"invoice_date": "2021-12-17T01:29:16.603984343-08:00","billing_address": "GoPOJOqToKgHAXoCRwOLzSsgW","billing_city": "pRhdTdgdcpgiBrgvmCMlFKapm","total": 0.38248355155004915,"invoice_id": 46}' | http PUT "http://127.0.0.1:8080/invoices/1" X-Api-User:user123 196 | func UpdateInvoices(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 197 | ctx := initializeContext(r) 198 | 199 | 200 | 201 | argInvoiceID, err := parseInt32(ps, "argInvoiceID") 202 | if err != nil { 203 | returnError(ctx, w, r, err) 204 | return 205 | } 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | invoices := &model.Invoices{} 217 | if err := readJSON(r, invoices); err != nil { 218 | returnError(ctx, w, r, dao.ErrBadParams) 219 | return 220 | } 221 | 222 | if err := invoices.BeforeSave(); err != nil { 223 | returnError(ctx, w, r, dao.ErrBadParams) 224 | } 225 | 226 | invoices.Prepare() 227 | 228 | if err := invoices.Validate( model.Update); err != nil { 229 | returnError(ctx, w, r, dao.ErrBadParams) 230 | return 231 | } 232 | 233 | if err := ValidateRequest(ctx, r, "invoices", model.Update); err != nil{ 234 | returnError(ctx, w, r, err) 235 | return 236 | } 237 | 238 | invoices, _, err = dao.UpdateInvoices(ctx, 239 | argInvoiceID, 240 | invoices) 241 | if err != nil { 242 | returnError(ctx, w, r, err) 243 | return 244 | } 245 | 246 | writeJSON(ctx, w, invoices) 247 | } 248 | 249 | ``` 250 | 251 | ## Delete record 252 | ```go 253 | 254 | // DeleteInvoices Delete a single record from invoices table in the main database 255 | // @Summary Delete a record from invoices 256 | // @Description Delete a single record from invoices table in the main database 257 | // @Tags Invoices 258 | // @Accept json 259 | // @Produce json 260 | // @Param argInvoiceID path int true "InvoiceId" 261 | // @Success 204 {object} model.Invoices 262 | // @Failure 400 {object} api.HTTPError 263 | // @Failure 500 {object} api.HTTPError 264 | // @Router /invoices/{argInvoiceID} [delete] 265 | // http DELETE "http://127.0.0.1:8080/invoices/1" X-Api-User:user123 266 | func DeleteInvoices(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 267 | ctx := initializeContext(r) 268 | 269 | 270 | argInvoiceID, err := parseInt32(ps, "argInvoiceID") 271 | if err != nil { 272 | returnError(ctx, w, r, err) 273 | return 274 | } 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | if err := ValidateRequest(ctx, r, "invoices", model.Delete); err != nil{ 286 | returnError(ctx, w, r, err) 287 | return 288 | } 289 | 290 | rowsAffected, err := dao.DeleteInvoices(ctx, argInvoiceID, ) 291 | if err != nil { 292 | returnError(ctx, w, r, err) 293 | return 294 | } 295 | 296 | writeRowsAffected(w, rowsAffected ) 297 | } 298 | 299 | ``` 300 | -------------------------------------------------------------------------------- /code_model.md: -------------------------------------------------------------------------------- 1 | ## Go Struct Generation 2 | `gen` will generate a `.go` file per table, mapping every column to a field in struct. The package name can be set with the 3 | `--model` flag. The model code generation can be customized with the following flags. 4 | 5 | 6 | ### Struct Tag Customizations 7 | - `--gorm` - Add GORM struct tag to fields 8 | - `--json` - Add json struct tag to fields 9 | - `--no-json` - Disable json struct tag 10 | - `--json-fmt=` - json name format [snake | camel | lower_camel | none] 11 | - `--protobuf` - Add protobuf struct tag to fields 12 | - `--db` - Add db struct tag to fields 13 | 14 | ### Handling Null Columns 15 | When columns within a db table are nullable, `gen` can generate code with nullable types using the `--guregu` flag, or it can use the default go type. 16 | For more information on the types used check out [guregu null](https://github.com/guregu/null). 17 | 18 | 19 | ```go 20 | /* 21 | DB Table Details 22 | ------------------------------------- 23 | 24 | 25 | CREATE TABLE "albums" 26 | ( 27 | [AlbumId] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 28 | [Title] NVARCHAR(160) NOT NULL, 29 | [ArtistId] INTEGER NOT NULL, 30 | FOREIGN KEY ([ArtistId]) REFERENCES "artists" ([ArtistId]) 31 | ON DELETE NO ACTION ON UPDATE NO ACTION 32 | ) 33 | 34 | JSON Sample 35 | ------------------------------------- 36 | { "title": "FBQslWHyXcrTdtrEZpqMgCklk", "artist_id": 8, "album_id": 8} 37 | */ 38 | ``` 39 | 40 | ```go 41 | // Album struct is a row record of the albums table in the main database 42 | type Album struct { 43 | //[ 0] AlbumId integer null: false primary: true auto: true col: integer len: -1 default: [] 44 | AlbumID int `gorm:"primary_key;AUTO_INCREMENT;column:AlbumId;type:INTEGER;" json:"album_id" db:"AlbumId" protobuf:"int32,0,opt,name=album_id"` 45 | //[ 1] Title nvarchar(160) null: false primary: false auto: false col: nvarchar len: 160 default: [] 46 | Title string `gorm:"column:Title;type:NVARCHAR(160);size:160;" json:"title" db:"Title" protobuf:"string,1,opt,name=title"` 47 | //[ 2] ArtistId integer null: false primary: false auto: false col: integer len: -1 default: [] 48 | ArtistID int `gorm:"column:ArtistId;type:INTEGER;" json:"artist_id" db:"ArtistId" protobuf:"int32,2,opt,name=artist_id"` 49 | } 50 | ``` 51 | 52 | ### Other Functions 53 | Extra funcs are generated for each struct. The funcs are intended for data validation. 54 | 55 | ```go 56 | // TableName sets the insert table name for this struct type 57 | func (a *Album) TableName() string { 58 | return "albums" 59 | } 60 | 61 | // BeforeSave invoked before saving, return an error if field is not populated. 62 | func (a *Album) BeforeSave() error { 63 | return nil 64 | } 65 | 66 | // Prepare invoked before saving, can be used to populate fields etc. 67 | func (a *Album) Prepare() { 68 | } 69 | 70 | // Validate invoked before performing action, return an error if field is not populated. 71 | func (a *Album) Validate(action Action) error { 72 | return nil 73 | } 74 | ``` 75 | -------------------------------------------------------------------------------- /code_protobuf.md: -------------------------------------------------------------------------------- 1 | ## Protocol Buffer Code generation 2 | By using the `--protobuf ` flag, `gen` will generate a `.proto` file to describe the models generated from the database tables. It will also add `proto` struct tags to the generated struct. 3 | You can customize the naming of the fields using the `--proto-fmt` flag. The generated `.proto` will be places in the `--out` directory. 4 | 5 | `--proto-fmt=snake proto name format [snake | camel | lower_camel | none]` 6 | 7 | ```proto 8 | 9 | syntax = "proto3"; 10 | 11 | package main; 12 | 13 | message packet { 14 | enum packet_type_t { 15 | PktAlbum = 0; 16 | PktArtist = 1; 17 | PktCustomer = 2; 18 | PktEmployee = 3; 19 | PktGenre = 4; 20 | PktInvoiceItem = 6; 21 | PktInvoice = 5; 22 | PktMediaType = 7; 23 | PktPlaylistTrack = 9; 24 | PktPlaylist = 8; 25 | PktPurchaseOrder = 11; 26 | PktTrack = 10; 27 | } 28 | 29 | packet_type_t id = 2; 30 | bytes data = 3; 31 | } 32 | 33 | 34 | 35 | message Album { 36 | int32 album_id = 1; 37 | string title = 2; 38 | int32 artist_id = 3; 39 | } 40 | 41 | 42 | message Artist { 43 | int32 artist_id = 1; 44 | string name = 2; 45 | } 46 | 47 | 48 | message Customer { 49 | int32 customer_id = 1; 50 | string first_name = 2; 51 | string last_name = 3; 52 | string company = 4; 53 | string address = 5; 54 | string city = 6; 55 | string state = 7; 56 | string country = 8; 57 | string postal_code = 9; 58 | string phone = 10; 59 | string fax = 11; 60 | string email = 12; 61 | int32 support_rep_id = 13; 62 | } 63 | 64 | 65 | message Employee { 66 | int32 employee_id = 1; 67 | string last_name = 2; 68 | string first_name = 3; 69 | string title = 4; 70 | int32 reports_to = 5; 71 | uint64 birth_date = 6; 72 | uint64 hire_date = 7; 73 | string address = 8; 74 | string city = 9; 75 | string state = 10; 76 | string country = 11; 77 | string postal_code = 12; 78 | string phone = 13; 79 | string fax = 14; 80 | string email = 15; 81 | } 82 | 83 | 84 | message Genre { 85 | int32 genre_id = 1; 86 | string name = 2; 87 | } 88 | 89 | 90 | message InvoiceItem { 91 | int32 invoice_line_id = 1; 92 | int32 invoice_id = 2; 93 | int32 track_id = 3; 94 | float unit_price = 4; 95 | int32 quantity = 5; 96 | } 97 | 98 | 99 | message Invoice { 100 | int32 invoice_id = 1; 101 | int32 customer_id = 2; 102 | uint64 invoice_date = 3; 103 | string billing_address = 4; 104 | string billing_city = 5; 105 | string billing_state = 6; 106 | string billing_country = 7; 107 | string billing_postal_code = 8; 108 | float total = 9; 109 | } 110 | 111 | 112 | message MediaType { 113 | int32 media_type_id = 1; 114 | string name = 2; 115 | } 116 | 117 | 118 | message PlaylistTrack { 119 | int32 playlist_id = 1; 120 | int32 track_id = 2; 121 | } 122 | 123 | 124 | message Playlist { 125 | int32 playlist_id = 1; 126 | string name = 2; 127 | } 128 | 129 | 130 | message PurchaseOrder { 131 | int32 id = 1; 132 | int32 payment_id = 2; 133 | string full_name = 3; 134 | } 135 | 136 | 137 | message Track { 138 | int32 track_id = 1; 139 | string name = 2; 140 | int32 album_id = 3; 141 | int32 media_type_id = 4; 142 | int32 genre_id = 5; 143 | string composer = 6; 144 | int32 milliseconds = 7; 145 | int32 bytes = 8; 146 | float unit_price = 9; 147 | } 148 | ``` 149 | -------------------------------------------------------------------------------- /custom/custom.go.tmpl: -------------------------------------------------------------------------------- 1 | package {{.daoPackageName}} 2 | 3 | import ( 4 | "context" 5 | 6 | "{{.modelFQPN}}" 7 | 8 | "github.com/smallnest/gen/dbmeta" 9 | ) 10 | 11 | 12 | // GetAll{{.StructName}} is a function to get a slice of record(s) from {{.TableName}} table in the {{.DatabaseName}} database 13 | // params - page - page requested (defaults to 0) 14 | // params - pagesize - number of records in a page (defaults to 20) 15 | // params - order - db sort order column 16 | // error - ErrNotFound, db Find error 17 | func GetAll{{.StructName}}(ctx context.Context, page, pagesize int64, order string) ({{.StructName | toLower}} []*{{.modelPackageName}}.{{.StructName}}, totalRows int, err error) { 18 | 19 | {{.StructName | toLower}} = []*{{.modelPackageName}}.{{.StructName}}{} 20 | 21 | {{.StructName | toLower}}Orm := DB.Model(&{{.modelPackageName}}.{{.StructName}}{}) 22 | {{.StructName | toLower}}Orm.Count(&totalRows) 23 | 24 | if page > 0 { 25 | offset := (page - 1) * pagesize 26 | {{.StructName | toLower}}Orm = {{.StructName | toLower}}Orm.Offset(offset).Limit(pagesize) 27 | } else { 28 | {{.StructName | toLower}}Orm = {{.StructName | toLower}}Orm.Limit(pagesize) 29 | } 30 | 31 | if order != "" { 32 | {{.StructName | toLower}}Orm = {{.StructName | toLower}}Orm.Order(order) 33 | } 34 | 35 | if err = {{.StructName | toLower}}Orm.Find(&{{.StructName | toLower}}).Error; err != nil { 36 | err = ErrNotFound 37 | return nil, -1, err 38 | } 39 | 40 | return {{.StructName | toLower}}, totalRows, nil 41 | } 42 | 43 | // Get{{.StructName}} is a function to get a single record from the {{.TableName}} table in the {{.DatabaseName}} database 44 | // error - ErrNotFound, db Find error 45 | func Get{{.StructName}}(ctx context.Context, id interface{}) (record *{{.modelPackageName}}.{{.StructName}}, err error) { 46 | if err = DB.First(record, id).Error; err != nil { 47 | err = ErrNotFound 48 | return nil, err 49 | } 50 | 51 | return record, nil 52 | } 53 | 54 | // Add{{.StructName}} is a function to add a single record to {{.TableName}} table in the {{.DatabaseName}} database 55 | // error - ErrInsertFailed, db save call failed 56 | func Add{{.StructName}}(ctx context.Context, {{.StructName | toLower}} *{{.modelPackageName}}.{{.StructName}}) (result *{{.modelPackageName}}.{{.StructName}}, RowsAffected int64, err error) { 57 | db := DB.Save({{.StructName | toLower}}) 58 | if err = db.Error; err != nil { 59 | return nil, -1, ErrInsertFailed 60 | } 61 | 62 | return {{.StructName | toLower}}, db.RowsAffected, nil 63 | } 64 | 65 | // Update{{.StructName}} is a function to update a single record from {{.TableName}} table in the {{.DatabaseName}} database 66 | // error - ErrNotFound, db record for id not found 67 | // error - ErrUpdateFailed, db meta data copy failed or db.Save call failed 68 | func Update{{.StructName}}(ctx context.Context, id interface{}, updated *{{.modelPackageName}}.{{.StructName}}) (result *{{.modelPackageName}}.{{.StructName}}, RowsAffected int64, err error) { 69 | 70 | result = &{{.modelPackageName}}.{{.StructName}}{} 71 | db := DB.First(result, id) 72 | if err = db.Error; err != nil { 73 | return nil, -1, ErrNotFound 74 | } 75 | 76 | if err = dbmeta.Copy(result, updated); err != nil { 77 | return nil, -1, ErrUpdateFailed 78 | return 79 | } 80 | 81 | db = db.Save(result) 82 | if err = db.Error; err != nil { 83 | return nil, -1, ErrUpdateFailed 84 | } 85 | 86 | return result, db.RowsAffected, nil 87 | } 88 | 89 | // Delete{{.StructName}} is a function to delete a single record from {{.TableName}} table in the {{.DatabaseName}} database 90 | // error - ErrNotFound, db Find error 91 | // error - ErrDeleteFailed, db Delete failed error 92 | func Delete{{.StructName}}(ctx context.Context, id interface{}) (rowsAffected int64, err error) { 93 | 94 | {{.StructName | toLower}} := &{{.modelPackageName}}.{{.StructName}}{} 95 | db := DB.First({{.StructName | toLower}}, id) 96 | if db.Error != nil { 97 | return -1, ErrNotFound 98 | } 99 | 100 | db = db.Delete({{.StructName | toLower}}) 101 | if err = db.Error; err != nil { 102 | return -1, ErrDeleteFailed 103 | } 104 | 105 | return db.RowsAffected, nil 106 | } 107 | 108 | -------------------------------------------------------------------------------- /custom/custom.json: -------------------------------------------------------------------------------- 1 | { 2 | "prop0": "Property 0", 3 | "prop1": "Property 1" 4 | } 5 | -------------------------------------------------------------------------------- /custom/custom.md.tmpl: -------------------------------------------------------------------------------- 1 | custom.md 2 | 3 | {{ range $key, $value := . }} 4 | {{ $desc := spew $value }} 5 | {{ printf "%#-20v" $key }} {{ printf "[%T] %#v" $value $value }}{{ end }} 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /custom/sample.gen: -------------------------------------------------------------------------------- 1 | {{/* 2 | 3 | "CommandLine" [string] "/tmp/go-build728666148/b001/exe/gen --sqltype=sqlite3 --connstr ./sample.db --database main --module github.com/alexj212/generated --verbose --overwrite --out ./ --exec=../sample.gen" 4 | "DatabaseName" [string] "main" 5 | "SwaggerInfo" [*main.swaggerInfo] &main.swaggerInfo{Version:"1.0", Host:"localhost:8080", BasePath:"/", Title:"Sample CRUD api for main db", Description:"Sample CRUD api for main db", TOS:"", ContactName:"Me", ContactUrl:"http://me.com/terms.html", ContactEmail:"me@me.com"} 6 | "apiFQPN" [string] "github.com/alexj212/generated/api" 7 | "apiPackageName" [string] "api" 8 | "daoFQPN" [string] "github.com/alexj212/generated/dao" 9 | "daoPackageName" [string] "dao" 10 | "modelFQPN" [string] "github.com/alexj212/generated/model" 11 | "modelPackageName" [string] "model" 12 | "module" [string] "github.com/alexj212/generated" 13 | "outDir" [string] "./" 14 | "serverHost" [string] "localhost" 15 | "serverPort" [int] 8080 16 | "sqlConnStr" [string] "./sample.db" 17 | "sqlType" [string] "sqlite3" 18 | "structs" [[]string] []string{"Album", "Artist", "Customer", "Employee", "Genre", "Invoice", "InvoiceItem", "MediaType", "Playlist", "PlaylistTrack", "Track"} 19 | "tableInfos" [map[string]*dbmeta.ModelInfo] map[string]*dbmeta.ModelInfo{"albums":(*dbmeta.ModelInfo)(0xc00031fa40), "artists":(*dbmeta.ModelInfo)(0xc00031fb90), "customers":(*dbmeta.ModelInfo)(0xc00031fce0), "employees":(*dbmeta.ModelInfo)(0xc00031fd50), "genres":(*dbmeta.ModelInfo)(0xc00031fdc0), "invoice_items":(*dbmeta.ModelInfo)(0xc00031fea0), "invoices":(*dbmeta.ModelInfo)(0xc00031fe30), "media_types":(*dbmeta.ModelInfo)(0xc00031ff10), "playlist_track":(*dbmeta.ModelInfo)(0xc0001121c0), "playlists":(*dbmeta.ModelInfo)(0xc00031ff80), "tracks":(*dbmeta.ModelInfo)(0xc000112230)} 20 | "tables" [[]string] []string{"albums", "artists", "customers", "employees", "genres", "invoices", "invoice_items", "media_types", "playlists", "playlist_track", "tracks"} 21 | 22 | 23 | GenerateTableFile(tableInfos map[string]*ModelInfo, tableName, templateFilename, outputDirectory, outputFileName string, formatOutput bool) 24 | 25 | "FmtFieldName": dbmeta.FmtFieldName, 26 | "singular": inflection.Singular, 27 | "pluralize": inflection.Plural, 28 | "title": strings.Title, 29 | "toLower": strings.ToLower, 30 | "toLowerCamelCase": camelToLowerCamel, 31 | "toSnakeCase": snaker.CamelToSnake, 32 | "markdownCodeBlock": markdownCodeBlock, 33 | "wrapBash": wrapBash, 34 | "GenerateTableFile": GenerateTableFile, 35 | "GenerateFile": GenerateFile, 36 | 37 | 38 | {{ range $key, $value := . }} 39 | {{ printf "%#-20v [%T] %#v" $key $value $value }}{{ end }} 40 | 41 | 42 | 43 | func (c *Config) GenerateFile(templateFilename, outDir, outputDirectory, outputFileName string, formatOutput bool, overwrite bool) string { 44 | 45 | 46 | */}} 47 | 48 | 49 | {{ range $i, $table := .tables }} 50 | {{$singular := singular $table -}} 51 | {{$plural := pluralize $table -}} 52 | {{$title := title $table -}} 53 | {{$lower := toLower $table -}} 54 | {{$lowerCamel := toLowerCamelCase $table -}} 55 | {{$snakeCase := toSnakeCase $table -}} 56 | {{ printf "[%-2d] %-20s %-20s %-20s %-20s %-20s %-20s %-20s" $i $table $singular $plural $title $lower $lowerCamel $snakeCase}}{{- end }} 57 | 58 | {{ $i := 0 }} 59 | {{ range $tableName , $table := .tableInfos }} 60 | {{$i := inc }} 61 | {{$name := toUpper $table.TableName -}} 62 | {{$filename := printf "My%s.go" $name -}} 63 | {{ printf "[%-2d] %-20s %-20s" $i $name $filename}} 64 | {{ GenerateTableFile $table.TableName "custom.go.tmpl" "test" $filename}}{{- end }} 65 | 66 | {{ GenerateFile "custom.md.tmpl" "test" "custom.md" false}} 67 | 68 | 69 | {{ mkdir "test/alex/test/mkdir" }} 70 | {{ touch "test/alex/test/mkdir/alex.txt" }} 71 | {{ pwd }} 72 | {{ copy "../_test" "test" }} 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /dbmeta/db_utils.go: -------------------------------------------------------------------------------- 1 | package dbmeta 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | ) 7 | 8 | // PrimaryKeyCount return the number of primary keys in table 9 | func PrimaryKeyCount(dbTable DbTableMeta) int { 10 | primaryKeys := 0 11 | for _, col := range dbTable.Columns() { 12 | if col.IsPrimaryKey() { 13 | primaryKeys++ 14 | } 15 | } 16 | return primaryKeys 17 | } 18 | 19 | // PrimaryKeyNames return the list of primary key names 20 | func PrimaryKeyNames(dbTable DbTableMeta) []string { 21 | primaryKeyNames := make([]string, 0) 22 | for _, col := range dbTable.Columns() { 23 | if col.IsPrimaryKey() { 24 | primaryKeyNames = append(primaryKeyNames, col.Name()) 25 | } 26 | } 27 | return primaryKeyNames 28 | } 29 | 30 | // NonPrimaryKeyNames return the list of primary key names 31 | func NonPrimaryKeyNames(dbTable DbTableMeta) []string { 32 | primaryKeyNames := make([]string, 0) 33 | for _, col := range dbTable.Columns() { 34 | if !col.IsPrimaryKey() { 35 | primaryKeyNames = append(primaryKeyNames, col.Name()) 36 | } 37 | } 38 | return primaryKeyNames 39 | } 40 | 41 | // GenerateDeleteSQL generate sql for a delete 42 | func GenerateDeleteSQL(dbTable DbTableMeta) (string, error) { 43 | primaryCnt := PrimaryKeyCount(dbTable) 44 | 45 | if primaryCnt == 0 { 46 | return "", fmt.Errorf("table %s does not have a primary key, cannot generate sql", dbTable.TableName()) 47 | } 48 | 49 | buf := bytes.Buffer{} 50 | buf.WriteString(fmt.Sprintf("DELETE FROM `%s` where", dbTable.TableName())) 51 | 52 | addedKey := 1 53 | for _, col := range dbTable.Columns() { 54 | if col.IsPrimaryKey() { 55 | buf.WriteString(fmt.Sprintf(" %s = ?", col.Name())) 56 | addedKey++ 57 | 58 | if addedKey < primaryCnt { 59 | buf.WriteString(" AND") 60 | } 61 | } 62 | } 63 | 64 | return buf.String(), nil 65 | } 66 | 67 | // GenerateUpdateSQL generate sql for a update 68 | func GenerateUpdateSQL(dbTable DbTableMeta) (string, error) { 69 | primaryCnt := PrimaryKeyCount(dbTable) 70 | // nonPrimaryCnt := len(dbTable.Columns()) - primaryCnt 71 | 72 | if primaryCnt == 0 { 73 | return "", fmt.Errorf("table %s does not have a primary key, cannot generate sql", dbTable.TableName()) 74 | } 75 | 76 | buf := bytes.Buffer{} 77 | buf.WriteString(fmt.Sprintf("UPDATE `%s` set", dbTable.TableName())) 78 | 79 | setCol := 1 80 | for _, col := range dbTable.Columns() { 81 | if !col.IsPrimaryKey() { 82 | if setCol != 1 { 83 | buf.WriteString(",") 84 | } 85 | 86 | buf.WriteString(fmt.Sprintf(" %s = ?", col.Name())) 87 | setCol++ 88 | } 89 | } 90 | 91 | buf.WriteString(" WHERE") 92 | addedKey := 0 93 | for _, col := range dbTable.Columns() { 94 | if col.IsPrimaryKey() { 95 | buf.WriteString(fmt.Sprintf(" %s = ?", col.Name())) 96 | 97 | setCol++ 98 | addedKey++ 99 | 100 | if addedKey < primaryCnt { 101 | buf.WriteString(" AND") 102 | } 103 | } 104 | } 105 | 106 | return buf.String(), nil 107 | } 108 | 109 | // GenerateInsertSQL generate sql for a insert 110 | func GenerateInsertSQL(dbTable DbTableMeta) (string, error) { 111 | primaryCnt := PrimaryKeyCount(dbTable) 112 | 113 | if primaryCnt == 0 { 114 | return "", fmt.Errorf("table %s does not have a primary key, cannot generate sql", dbTable.TableName()) 115 | } 116 | 117 | buf := bytes.Buffer{} 118 | buf.WriteString(fmt.Sprintf("INSERT INTO `%s` (", dbTable.TableName())) 119 | 120 | pastFirst := false 121 | for _, col := range dbTable.Columns() { 122 | if !col.IsAutoIncrement() { 123 | if pastFirst { 124 | buf.WriteString(", ") 125 | } 126 | 127 | buf.WriteString(fmt.Sprintf(" %s", col.Name())) 128 | pastFirst = true 129 | } 130 | } 131 | buf.WriteString(") values ( ") 132 | 133 | pastFirst = false 134 | pos := 1 135 | for _, col := range dbTable.Columns() { 136 | if !col.IsAutoIncrement() { 137 | if pastFirst { 138 | buf.WriteString(", ") 139 | } 140 | 141 | buf.WriteString(fmt.Sprintf("?")) 142 | pos++ 143 | pastFirst = true 144 | } 145 | } 146 | 147 | buf.WriteString(" )") 148 | return buf.String(), nil 149 | } 150 | 151 | // GenerateSelectOneSQL generate sql for selecting one record 152 | func GenerateSelectOneSQL(dbTable DbTableMeta) (string, error) { 153 | primaryCnt := PrimaryKeyCount(dbTable) 154 | 155 | if primaryCnt == 0 { 156 | return "", fmt.Errorf("table %s does not have a primary key, cannot generate sql", dbTable.TableName()) 157 | } 158 | 159 | buf := bytes.Buffer{} 160 | buf.WriteString(fmt.Sprintf("SELECT * FROM `%s` WHERE ", dbTable.TableName())) 161 | 162 | pastFirst := false 163 | pos := 1 164 | for _, col := range dbTable.Columns() { 165 | if col.IsPrimaryKey() { 166 | if pastFirst { 167 | buf.WriteString(" AND ") 168 | } 169 | 170 | buf.WriteString(fmt.Sprintf("%s = ?", col.Name())) 171 | pos++ 172 | pastFirst = true 173 | } 174 | } 175 | return buf.String(), nil 176 | } 177 | 178 | // GenerateSelectMultiSQL generate sql for selecting multiple records 179 | func GenerateSelectMultiSQL(dbTable DbTableMeta) (string, error) { 180 | primaryCnt := PrimaryKeyCount(dbTable) 181 | 182 | if primaryCnt == 0 { 183 | return "", fmt.Errorf("table %s does not have a primary key, cannot generate sql", dbTable.TableName()) 184 | } 185 | 186 | buf := bytes.Buffer{} 187 | buf.WriteString(fmt.Sprintf("SELECT * FROM `%s`", dbTable.TableName())) 188 | return buf.String(), nil 189 | } 190 | -------------------------------------------------------------------------------- /dbmeta/meta_mssql.go: -------------------------------------------------------------------------------- 1 | package dbmeta 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/jimsmart/schema" 9 | ) 10 | 11 | // LoadMsSQLMeta fetch db meta data for MS SQL database 12 | func LoadMsSQLMeta(db *sql.DB, sqlType, sqlDatabase, tableName string) (DbTableMeta, error) { 13 | m := &dbTableMeta{ 14 | sqlType: sqlType, 15 | sqlDatabase: sqlDatabase, 16 | tableName: tableName, 17 | } 18 | 19 | cols, err := schema.ColumnTypes(db, sqlDatabase, tableName) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | m.columns = make([]*columnMeta, len(cols)) 25 | colInfo, err := msSQLloadFromSysColumns(db, tableName) 26 | if err != nil { 27 | return nil, fmt.Errorf("unable to load ddl from ms sql: %v", err) 28 | } 29 | 30 | err = msSQLLoadPrimaryKey(db, tableName, colInfo) 31 | if err != nil { 32 | return nil, fmt.Errorf("unable to load ddl from ms sql: %v", err) 33 | } 34 | 35 | infoSchema, err := LoadTableInfoFromMSSqlInformationSchema(db, tableName) 36 | if err != nil { 37 | fmt.Printf("error calling LoadTableInfoFromMSSqlInformationSchema table: %s error: %v\n", tableName, err) 38 | } 39 | 40 | for i, v := range cols { 41 | 42 | nullable, ok := v.Nullable() 43 | if !ok { 44 | nullable = false 45 | } 46 | isAutoIncrement := false 47 | isPrimaryKey := i == 0 48 | var columnLen int64 = -1 49 | 50 | colInfo, ok := colInfo[v.Name()] 51 | if ok { 52 | isPrimaryKey = colInfo.primaryKey 53 | nullable = colInfo.isNullable 54 | isAutoIncrement = colInfo.isIdentity 55 | dbType := strings.ToLower(v.DatabaseTypeName()) 56 | 57 | if strings.Contains(dbType, "char") || strings.Contains(dbType, "text") { 58 | columnLen = colInfo.maxLength 59 | } 60 | } else { 61 | fmt.Printf("name: %s DatabaseTypeName: %s NOT FOUND in colInfo\n", v.Name(), v.DatabaseTypeName()) 62 | } 63 | 64 | defaultVal := "" 65 | columnType := v.DatabaseTypeName() 66 | colDDL := v.DatabaseTypeName() 67 | 68 | if infoSchema != nil { 69 | infoSchemaColInfo, ok := infoSchema[v.Name()] 70 | if ok { 71 | if infoSchemaColInfo.ColumnDefault != nil { 72 | defaultVal = fmt.Sprintf("%v", infoSchemaColInfo.ColumnDefault) 73 | defaultVal = cleanupDefault(defaultVal) 74 | } 75 | } 76 | } 77 | 78 | colMeta := &columnMeta{ 79 | index: i, 80 | name: v.Name(), 81 | databaseTypeName: columnType, 82 | nullable: nullable, 83 | isPrimaryKey: isPrimaryKey, 84 | isAutoIncrement: isAutoIncrement, 85 | colDDL: colDDL, 86 | defaultVal: defaultVal, 87 | columnType: columnType, 88 | columnLen: columnLen, 89 | } 90 | 91 | m.columns[i] = colMeta 92 | } 93 | 94 | m.ddl = BuildDefaultTableDDL(tableName, m.columns) 95 | m = updateDefaultPrimaryKey(m) 96 | return m, nil 97 | } 98 | 99 | func msSQLLoadPrimaryKey(db *sql.DB, tableName string, colInfo map[string]*msSQLColumnInfo) error { 100 | primaryKeySQL := fmt.Sprintf(` 101 | SELECT Col.Column_Name from 102 | INFORMATION_SCHEMA.TABLE_CONSTRAINTS Tab, 103 | INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE Col 104 | WHERE 105 | Col.Constraint_Name = Tab.Constraint_Name 106 | AND Col.Table_Name = Tab.Table_Name 107 | AND Constraint_Type = 'PRIMARY KEY' 108 | AND Col.Table_Name = '%s' 109 | `, tableName) 110 | res, err := db.Query(primaryKeySQL) 111 | if err != nil { 112 | return fmt.Errorf("unable to load ddl from ms sql: %v", err) 113 | } 114 | defer res.Close() 115 | for res.Next() { 116 | 117 | var columnName string 118 | err = res.Scan(&columnName) 119 | if err != nil { 120 | return fmt.Errorf("unable to load identity info from ms sql Scan: %v", err) 121 | } 122 | 123 | // fmt.Printf("## PRIMARY KEY COLUMN_NAME: %s\n", columnName) 124 | colInfo, ok := colInfo[columnName] 125 | if ok { 126 | colInfo.primaryKey = true 127 | // fmt.Printf("name: %s primary_key: %t\n", colInfo.name, colInfo.primary_key) 128 | } 129 | } 130 | return nil 131 | } 132 | 133 | func msSQLloadFromSysColumns(db *sql.DB, tableName string) (colInfo map[string]*msSQLColumnInfo, err error) { 134 | colInfo = make(map[string]*msSQLColumnInfo) 135 | 136 | identitySQL := fmt.Sprintf(` 137 | SELECT name, is_identity, is_nullable, max_length 138 | FROM sys.columns 139 | WHERE object_id = object_id('dbo.%s')`, tableName) 140 | 141 | res, err := db.Query(identitySQL) 142 | if err != nil { 143 | return nil, fmt.Errorf("unable to load ddl from ms sql: %v", err) 144 | } 145 | 146 | defer res.Close() 147 | for res.Next() { 148 | var name string 149 | var isIdentity, isNullable bool 150 | var maxLength int64 151 | err = res.Scan(&name, &isIdentity, &isNullable, &maxLength) 152 | if err != nil { 153 | return nil, fmt.Errorf("unable to load identity info from ms sql Scan: %v", err) 154 | } 155 | 156 | colInfo[name] = &msSQLColumnInfo{ 157 | name: name, 158 | isIdentity: isIdentity, 159 | isNullable: isNullable, 160 | maxLength: maxLength, 161 | } 162 | } 163 | return colInfo, err 164 | } 165 | 166 | type msSQLColumnInfo struct { 167 | name string 168 | isIdentity bool 169 | isNullable bool 170 | primaryKey bool 171 | maxLength int64 172 | } 173 | 174 | /* 175 | https://www.mssqltips.com/sqlservertip/1512/finding-and-listing-all-columns-in-a-sql-server-database-with-default-values/ 176 | */ 177 | -------------------------------------------------------------------------------- /dbmeta/meta_mysql.go: -------------------------------------------------------------------------------- 1 | package dbmeta 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/jimsmart/schema" 10 | ) 11 | 12 | // LoadMysqlMeta fetch db meta data for MySQL database 13 | func LoadMysqlMeta(db *sql.DB, sqlType, sqlDatabase, tableName string) (DbTableMeta, error) { 14 | m := &dbTableMeta{ 15 | sqlType: sqlType, 16 | sqlDatabase: sqlDatabase, 17 | tableName: tableName, 18 | } 19 | 20 | cols, err := schema.ColumnTypes(db, sqlDatabase, tableName) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | ddl, err := mysqlLoadDDL(db, tableName) 26 | if err != nil { 27 | return nil, fmt.Errorf("mysqlLoadDDL - unable to load ddl from mysql: %v", err) 28 | } 29 | 30 | m.ddl = ddl 31 | colsDDL, primaryKeys := mysqlParseDDL(ddl) 32 | 33 | infoSchema, err := LoadTableInfoFromMSSqlInformationSchema(db, tableName) 34 | if err != nil { 35 | fmt.Printf("error calling LoadTableInfoFromMSSqlInformationSchema table: %s error: %v\n", tableName, err) 36 | } 37 | 38 | m.columns = make([]*columnMeta, len(cols)) 39 | 40 | for i, v := range cols { 41 | notes := "" 42 | nullable, ok := v.Nullable() 43 | if !ok { 44 | nullable = false 45 | } 46 | 47 | colDDL := colsDDL[v.Name()] 48 | isAutoIncrement := strings.Index(colDDL, "AUTO_INCREMENT") > -1 49 | isUnsigned := strings.Index(colDDL, " unsigned ") > -1 || strings.Index(colDDL, " UNSIGNED ") > -1 50 | 51 | _, isPrimaryKey := find(primaryKeys, v.Name()) 52 | defaultVal := "" 53 | columnType, columnLen := ParseSQLType(v.DatabaseTypeName()) 54 | 55 | if isUnsigned { 56 | notes = notes + " column is set for unsigned" 57 | columnType = "u" + columnType 58 | } 59 | 60 | comment := "" 61 | commentIdx := strings.Index(colDDL, "COMMENT '") 62 | if commentIdx > -1 { 63 | re := regexp.MustCompile("COMMENT '(.*?)'") 64 | match := re.FindStringSubmatch(colDDL) 65 | if len(match) > 0 { 66 | comment = match[1] 67 | } 68 | } 69 | 70 | if infoSchema != nil { 71 | infoSchemaColInfo, ok := infoSchema[v.Name()] 72 | if ok { 73 | if infoSchemaColInfo.ColumnDefault != nil { 74 | defaultVal = BytesToString(infoSchemaColInfo.ColumnDefault.([]uint8)) 75 | defaultVal = cleanupDefault(defaultVal) 76 | } 77 | } 78 | } 79 | 80 | colMeta := &columnMeta{ 81 | index: i, 82 | name: v.Name(), 83 | databaseTypeName: columnType, 84 | nullable: nullable, 85 | isPrimaryKey: isPrimaryKey, 86 | isAutoIncrement: isAutoIncrement, 87 | colDDL: colDDL, 88 | defaultVal: defaultVal, 89 | columnType: columnType, 90 | columnLen: columnLen, 91 | notes: strings.Trim(notes, " "), 92 | comment: comment, 93 | } 94 | 95 | dbType := strings.ToLower(colMeta.DatabaseTypeName()) 96 | // fmt.Printf("dbType: %s\n", dbType) 97 | 98 | if strings.Contains(dbType, "char") || strings.Contains(dbType, "text") { 99 | columnLen, err := GetFieldLenFromInformationSchema(db, sqlDatabase, tableName, v.Name()) 100 | if err == nil { 101 | colMeta.columnLen = columnLen 102 | } 103 | } 104 | 105 | m.columns[i] = colMeta 106 | } 107 | 108 | m = updateDefaultPrimaryKey(m) 109 | return m, nil 110 | } 111 | 112 | func mysqlLoadDDL(db *sql.DB, tableName string) (ddl string, err error) { 113 | ddlSQL := fmt.Sprintf("SHOW CREATE TABLE `%s`;", tableName) 114 | res, err := db.Query(ddlSQL) 115 | if err != nil { 116 | return "", fmt.Errorf("unable to load ddl from mysql: %v", err) 117 | } 118 | 119 | defer res.Close() 120 | var ddl1 string 121 | var ddl2 string 122 | if res.Next() { 123 | err = res.Scan(&ddl1, &ddl2) 124 | if err != nil { 125 | return "", fmt.Errorf("unable to load ddl from mysql Scan: %v", err) 126 | } 127 | } 128 | return ddl2, nil 129 | } 130 | 131 | func mysqlParseDDL(ddl string) (colsDDL map[string]string, primaryKeys []string) { 132 | colsDDL = make(map[string]string) 133 | lines := strings.Split(ddl, "\n") 134 | for _, line := range lines { 135 | line = strings.Trim(line, " \t") 136 | if strings.HasPrefix(line, "CREATE TABLE") || strings.HasPrefix(line, "(") || strings.HasPrefix(line, ")") { 137 | continue 138 | } 139 | 140 | if line[0] == '`' { 141 | idx := indexAt(line, "`", 1) 142 | if idx > 0 { 143 | name := line[1:idx] 144 | colDDL := line[idx+1 : len(line)-1] 145 | colsDDL[name] = colDDL 146 | } 147 | } else if strings.HasPrefix(line, "PRIMARY KEY") { 148 | primaryKeyNums := strings.Count(line, "`") / 2 149 | count := 0 150 | currentIdx := 0 151 | idxL := 0 152 | idxR := 0 153 | for { 154 | if count >= primaryKeyNums { 155 | break 156 | } 157 | count++ 158 | idxL = indexAt(line, "`", currentIdx) 159 | currentIdx = idxL + 1 160 | idxR = indexAt(line, "`", currentIdx) 161 | currentIdx = idxR + 1 162 | primaryKeys = append(primaryKeys, line[idxL+1:idxR]) 163 | } 164 | } 165 | } 166 | return 167 | } 168 | 169 | func find(slice []string, val string) (int, bool) { 170 | for i, item := range slice { 171 | if item == val { 172 | return i, true 173 | } 174 | } 175 | return -1, false 176 | } 177 | 178 | /* 179 | https://dataedo.com/kb/query/mysql/list-table-default-constraints 180 | 181 | select table_schema as database_name, 182 | table_name, 183 | column_name, 184 | column_default 185 | from information_schema.columns 186 | where column_default is not null 187 | and table_schema not in ('information_schema', 'sys', 188 | 'performance_schema','mysql') 189 | -- and table_schema = 'your database name' 190 | order by table_schema, 191 | table_name, 192 | ordinal_position; 193 | */ 194 | -------------------------------------------------------------------------------- /dbmeta/meta_postgres.go: -------------------------------------------------------------------------------- 1 | package dbmeta 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/jimsmart/schema" 9 | ) 10 | 11 | // LoadPostgresMeta fetch db meta data for Postgres database 12 | func LoadPostgresMeta(db *sql.DB, sqlType, sqlDatabase, tableName string) (DbTableMeta, error) { 13 | m := &dbTableMeta{ 14 | sqlType: sqlType, 15 | sqlDatabase: sqlDatabase, 16 | tableName: tableName, 17 | } 18 | cols, err := schema.ColumnTypes(db, sqlDatabase, tableName) 19 | if err != nil { 20 | cols, err = schema.ColumnTypes(db, "", tableName) 21 | if err != nil { 22 | return nil, err 23 | } 24 | } 25 | m.columns = make([]*columnMeta, len(cols)) 26 | 27 | colInfo, err := LoadTableInfoFromPostgresInformationSchema(db, tableName) 28 | if err != nil { 29 | return nil, fmt.Errorf("unable to load identity info schema from postgres table: %s error: %v", tableName, err) 30 | } 31 | 32 | err = postgresLoadPrimaryKey(db, tableName, colInfo) 33 | if err != nil { 34 | return nil, fmt.Errorf("unable to load primary key from postgres: %v", err) 35 | } 36 | 37 | for i, v := range cols { 38 | defaultVal := "" 39 | nullable, ok := v.Nullable() 40 | if !ok { 41 | nullable = false 42 | } 43 | isAutoIncrement := false 44 | isPrimaryKey := i == 0 45 | var maxLen int64 46 | 47 | maxLen = -1 48 | colInfo, ok := colInfo[v.Name()] 49 | if ok { 50 | nullable = colInfo.IsNullable == "YES" 51 | isAutoIncrement = colInfo.IsIdentity == "YES" 52 | isPrimaryKey = colInfo.PrimaryKey 53 | 54 | if colInfo.ColumnDefault != nil { 55 | defaultVal = cleanupDefault(fmt.Sprintf("%v", colInfo.ColumnDefault)) 56 | } 57 | 58 | ml, ok := colInfo.CharacterMaximumLength.(int64) 59 | if ok { 60 | maxLen = ml 61 | } 62 | } 63 | 64 | definedType := v.DatabaseTypeName() 65 | colDDL := v.DatabaseTypeName() 66 | if definedType == "" { 67 | definedType = "USER_DEFINED" 68 | colDDL = "VARCHAR" 69 | } 70 | 71 | colMeta := &columnMeta{ 72 | index: i, 73 | name: v.Name(), 74 | databaseTypeName: colDDL, 75 | nullable: nullable, 76 | isPrimaryKey: isPrimaryKey, 77 | isAutoIncrement: isAutoIncrement, 78 | colDDL: colDDL, 79 | columnLen: maxLen, 80 | columnType: definedType, 81 | defaultVal: defaultVal, 82 | } 83 | 84 | m.columns[i] = colMeta 85 | } 86 | 87 | m.ddl = BuildDefaultTableDDL(tableName, m.columns) 88 | m = updateDefaultPrimaryKey(m) 89 | 90 | for _, v := range m.columns { 91 | if !v.isAutoIncrement && v.isPrimaryKey { 92 | val := fmt.Sprintf("%v", v.defaultVal) 93 | if strings.Index(val, "()") > -1 { 94 | v.isAutoIncrement = true 95 | } 96 | } 97 | } 98 | for _, v := range m.columns { 99 | if strings.HasPrefix(v.DatabaseTypeName(), "_") { 100 | v.isArray = true 101 | } 102 | } 103 | return m, nil 104 | } 105 | 106 | func postgresLoadPrimaryKey(db *sql.DB, tableName string, colInfo map[string]*PostgresInformationSchema) error { 107 | primaryKeySQL := fmt.Sprintf(` 108 | SELECT c.column_name 109 | FROM information_schema.key_column_usage AS c 110 | LEFT JOIN information_schema.table_constraints AS t 111 | ON t.constraint_name = c.constraint_name 112 | WHERE t.table_name = '%s' AND t.constraint_type = 'PRIMARY KEY'; 113 | `, tableName) 114 | res, err := db.Query(primaryKeySQL) 115 | if err != nil { 116 | return fmt.Errorf("unable to load ddl from ms sql: %v", err) 117 | } 118 | 119 | defer res.Close() 120 | for res.Next() { 121 | 122 | var columnName string 123 | err = res.Scan(&columnName) 124 | if err != nil { 125 | return fmt.Errorf("unable to load identity info from ms sql Scan: %v", err) 126 | } 127 | 128 | // fmt.Printf("## PRIMARY KEY COLUMN_NAME: %s\n", columnName) 129 | colInfo, ok := colInfo[columnName] 130 | if ok { 131 | colInfo.PrimaryKey = true 132 | // fmt.Printf("name: %s primary_key: %t\n", colInfo.name, colInfo.primary_key) 133 | } 134 | } 135 | return nil 136 | } 137 | 138 | /* 139 | https://dataedo.com/kb/query/postgresql/list-table-default-constraints 140 | 141 | select col.table_schema, 142 | col.table_name, 143 | col.column_name, 144 | col.column_default 145 | from information_schema.columns col 146 | where col.column_default is not null 147 | and col.table_schema not in('information_schema', 'pg_catalog') 148 | order by col.column_name; 149 | */ 150 | -------------------------------------------------------------------------------- /dbmeta/meta_sqlite.go: -------------------------------------------------------------------------------- 1 | package dbmeta 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/jimsmart/schema" 9 | ) 10 | 11 | // LoadSqliteMeta fetch db meta data for Sqlite3 database 12 | func LoadSqliteMeta(db *sql.DB, sqlType, sqlDatabase, tableName string) (DbTableMeta, error) { 13 | if tableName == "sqlite_sequence" || tableName == "sqlite_stat1" { 14 | return nil, fmt.Errorf("unsupported table: %s", tableName) 15 | } 16 | 17 | m := &dbTableMeta{ 18 | sqlType: sqlType, 19 | sqlDatabase: sqlDatabase, 20 | tableName: tableName, 21 | } 22 | 23 | ddl, err := sqliteLoadDDL(db, tableName) 24 | if err != nil { 25 | return nil, fmt.Errorf("unable to load ddl from sqlite_master: %v", err) 26 | } 27 | 28 | m.ddl = ddl 29 | 30 | colsInfos, err := sqliteLoadPragma(db, tableName) 31 | if err != nil { 32 | return nil, fmt.Errorf("unable to load PRAGMA table_info %s: %v", m.tableName, err) 33 | } 34 | 35 | colsDDL := sqliteParseDDL(ddl) 36 | 37 | cols, err := schema.ColumnTypes(db, sqlDatabase, tableName) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | m.columns = make([]*columnMeta, len(cols)) 43 | 44 | for i, v := range cols { 45 | colDDL := colsDDL[v.Name()] 46 | 47 | colDDLLower := strings.ToLower(colDDL) 48 | notNull := strings.Index(colDDLLower, "not null") > -1 49 | isPrimaryKey := strings.Index(colDDLLower, "primary key") > -1 50 | isAutoIncrement := strings.Index(colDDLLower, "autoincrement") > -1 51 | defaultVal := "" 52 | columnLen := int64(-1) 53 | columnType := v.DatabaseTypeName() 54 | 55 | details, ok := colsInfos[v.Name()] 56 | if ok { 57 | isPrimaryKey = details.primaryKey == 1 58 | if details.dfltValue != nil { 59 | defaultVal = details.dfltValue.(string) 60 | } 61 | 62 | notNull = details.notnull == 1 63 | columnType, columnLen = ParseSQLType(details.dataType) 64 | } 65 | 66 | if isPrimaryKey { 67 | notNull = true 68 | } 69 | // fmt.Printf("%s: notNull: %v isPrimaryKey: %v isAutoIncrement: %v\n",colDDL, notNull, isPrimaryKey, isAutoIncrement) 70 | 71 | colMeta := &columnMeta{ 72 | index: i, 73 | name: v.Name(), 74 | databaseTypeName: columnType, 75 | nullable: !notNull, 76 | isPrimaryKey: isPrimaryKey, 77 | isAutoIncrement: isAutoIncrement, 78 | colDDL: colDDL, 79 | defaultVal: defaultVal, 80 | columnType: columnType, 81 | columnLen: columnLen, 82 | } 83 | 84 | m.columns[i] = colMeta 85 | } 86 | 87 | m = updateDefaultPrimaryKey(m) 88 | return m, nil 89 | } 90 | 91 | func sqliteLoadPragma(db *sql.DB, tableName string) (colsInfos map[string]*sqliteColumnInfo, err error) { 92 | pragmaSQL := fmt.Sprintf("PRAGMA table_info('%s');", tableName) 93 | res, err := db.Query(pragmaSQL) 94 | if err != nil { 95 | return nil, fmt.Errorf("unable to load PRAGMA table_info %s: %v", tableName, err) 96 | } 97 | 98 | defer res.Close() 99 | colsInfos = make(map[string]*sqliteColumnInfo) 100 | for res.Next() { 101 | ci := &sqliteColumnInfo{} 102 | err = res.Scan(&ci.cid, &ci.name, &ci.dataType, &ci.notnull, &ci.dfltValue, &ci.primaryKey) 103 | if err != nil { 104 | return nil, fmt.Errorf("unable to load identity info from sqlite Scan: %v", err) 105 | } 106 | colsInfos[ci.name] = ci 107 | 108 | // fmt.Printf("cid: |%2d| name: |%-20s| data_type: |%-20s| notnull: |%d| dflt_value: |%-10T| dflt_value: |%-10v| primary_key: |%d|\n", 109 | // ci.cid, ci.name, ci.data_type, ci.notnull, ci.dflt_value, ci.dflt_value, ci.primary_key) 110 | } 111 | return colsInfos, nil 112 | } 113 | 114 | func sqliteParseDDL(ddl string) map[string]string { 115 | idx1 := strings.Index(ddl, "(") 116 | idx2 := strings.LastIndex(ddl, ")") 117 | 118 | if idx1 > -1 && idx2 > -1 { 119 | ddl = ddl[idx1+1 : idx2] 120 | } 121 | 122 | ddl = strings.Replace(ddl, "\r", "", -1) 123 | ddl = strings.Replace(ddl, "\n", " ", -1) 124 | ddl = strings.TrimPrefix(ddl, "\n") 125 | ddl = strings.TrimSuffix(ddl, "\n") 126 | 127 | colsDDL := make(map[string]string) 128 | 129 | lines := strings.Split(ddl, ",") 130 | for _, line := range lines { 131 | line = strings.Replace(line, "\n", " ", -1) 132 | 133 | line := strings.TrimSpace(line) 134 | 135 | line = TrimSpaceNewlineInString(line) 136 | line = strings.TrimPrefix(line, "\n") 137 | line = strings.TrimSuffix(line, "\n") 138 | line = strings.TrimSuffix(line, ",") 139 | line = strings.Trim(line, " ") 140 | line = strings.Trim(line, ",") 141 | 142 | if len(line) == 0 { 143 | continue 144 | } 145 | 146 | if strings.HasPrefix(line, "FOREIGN KEY") || strings.HasPrefix(line, "CONSTRAINT") { 147 | continue 148 | } 149 | 150 | // fmt.Printf("[%2d] %s\n", i, line) 151 | 152 | parts := strings.Split(line, " ") 153 | name := parts[0] 154 | colDDL := strings.Join(parts[1:], " ") 155 | 156 | name = strings.Trim(name, " \t[]\"") 157 | colDDL = strings.Trim(colDDL, " \t,") 158 | 159 | if len(colDDL) == 0 { 160 | continue 161 | } 162 | 163 | colsDDL[name] = colDDL 164 | 165 | } 166 | return colsDDL 167 | } 168 | 169 | func sqliteLoadDDL(db *sql.DB, tableName string) (string, error) { 170 | var ddl string 171 | ddlSQL := fmt.Sprintf("SELECT sql FROM sqlite_master WHERE type='table' and name = '%s';", tableName) 172 | //_, err := db.Query(ddlSQL) 173 | //if err != nil { 174 | //return "", fmt.Errorf("unable to load ddl from sqlite_master: %v", err) 175 | //} 176 | 177 | row := db.QueryRow(ddlSQL, 0) 178 | err := row.Scan(&ddl) 179 | if err != nil { 180 | return "", err 181 | } 182 | 183 | return ddl, nil 184 | } 185 | 186 | type sqliteColumnInfo struct { 187 | cid int 188 | name string 189 | dataType string 190 | notnull int 191 | dfltValue interface{} 192 | primaryKey int 193 | } 194 | -------------------------------------------------------------------------------- /dbmeta/meta_unknown.go: -------------------------------------------------------------------------------- 1 | package dbmeta 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/jimsmart/schema" 11 | ) 12 | 13 | // LoadUnknownMeta fetch db meta data for unknown database type 14 | func LoadUnknownMeta(db *sql.DB, sqlType, sqlDatabase, tableName string) (DbTableMeta, error) { 15 | m := &dbTableMeta{ 16 | sqlType: sqlType, 17 | sqlDatabase: sqlDatabase, 18 | tableName: tableName, 19 | } 20 | 21 | cols, err := schema.ColumnTypes(db, sqlDatabase, tableName) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | m.columns = make([]*columnMeta, len(cols)) 27 | 28 | infoSchema, err := LoadTableInfoFromMSSqlInformationSchema(db, tableName) 29 | if err != nil { 30 | fmt.Printf("NOTICE unable to load InformationSchema table: %s error: %v\n", tableName, err) 31 | } 32 | 33 | for i, v := range cols { 34 | 35 | nullable, ok := v.Nullable() 36 | if !ok { 37 | nullable = false 38 | } 39 | isAutoIncrement := false 40 | isPrimaryKey := i == 0 41 | colDDL := v.DatabaseTypeName() 42 | 43 | defaultVal := "" 44 | columnType, columnLen := ParseSQLType(v.DatabaseTypeName()) 45 | 46 | if columnLen == -1 { 47 | 48 | dbType := strings.ToLower(v.DatabaseTypeName()) 49 | if strings.Contains(dbType, "varchar") { 50 | re := regexp.MustCompile(`[-]?\d[\d,]*[\.]?[\d{2}]*`) 51 | submatchall := re.FindAllString(dbType, -1) 52 | if len(submatchall) > 0 { 53 | i, err := strconv.Atoi(submatchall[0]) 54 | if err == nil { 55 | columnLen = int64(i) 56 | } 57 | } 58 | } 59 | } 60 | 61 | if infoSchema != nil { 62 | infoSchemaColInfo, ok := infoSchema[v.Name()] 63 | if ok { 64 | if infoSchemaColInfo.ColumnDefault != nil { 65 | defaultVal = BytesToString(infoSchemaColInfo.ColumnDefault.([]uint8)) 66 | defaultVal = cleanupDefault(defaultVal) 67 | } 68 | } 69 | } 70 | 71 | colMeta := &columnMeta{ 72 | index: i, 73 | name: v.Name(), 74 | databaseTypeName: columnType, 75 | nullable: nullable, 76 | isPrimaryKey: isPrimaryKey, 77 | isAutoIncrement: isAutoIncrement, 78 | colDDL: colDDL, 79 | defaultVal: defaultVal, 80 | columnType: columnType, 81 | columnLen: columnLen, 82 | } 83 | 84 | m.columns[i] = colMeta 85 | } 86 | 87 | m.ddl = BuildDefaultTableDDL(tableName, m.columns) 88 | m = updateDefaultPrimaryKey(m) 89 | return m, nil 90 | } 91 | -------------------------------------------------------------------------------- /dbmeta/meta_utils.go: -------------------------------------------------------------------------------- 1 | package dbmeta 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/logrusorgru/aurora" 11 | ) 12 | 13 | var ( 14 | au aurora.Aurora 15 | ) 16 | 17 | func InitColorOutput(_au aurora.Aurora) { 18 | au = _au 19 | } 20 | 21 | // ParseSQLType parse sql type and return raw type and length 22 | func ParseSQLType(dbType string) (resultType string, dbTypeLen int64) { 23 | 24 | resultType = strings.ToLower(dbType) 25 | dbTypeLen = -1 26 | idx1 := strings.Index(resultType, "(") 27 | idx2 := strings.Index(resultType, ")") 28 | 29 | if idx1 > -1 && idx2 > -1 { 30 | sizeStr := resultType[idx1+1 : idx2] 31 | resultType = resultType[0:idx1] 32 | i, err := strconv.Atoi(sizeStr) 33 | if err == nil { 34 | dbTypeLen = int64(i) 35 | } 36 | } 37 | 38 | // fmt.Printf("dbType: %-20s %-20s %d\n", dbType, resultType, dbTypeLen) 39 | return resultType, dbTypeLen 40 | } 41 | 42 | // TrimSpaceNewlineInString replace spaces in string 43 | func TrimSpaceNewlineInString(s string) string { 44 | 45 | re := regexp.MustCompile(` +\r?\n +`) 46 | return re.ReplaceAllString(s, " ") 47 | } 48 | 49 | /* 50 | SELECT 51 | TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, COLUMN_DEFAULT 52 | FROM 53 | INFORMATION_SCHEMA.COLUMNS 54 | WHERE 55 | TABLE_SCHEMA = @SchemaName 56 | AND TABLE_NAME = @TableName 57 | AND COLUMN_NAME = @ColumnName; 58 | */ 59 | 60 | // FindPrimaryKeyFromInformationSchema fetch primary key info from information_schema 61 | func FindPrimaryKeyFromInformationSchema(db *sql.DB, tableName string) (primaryKey string, err error) { 62 | 63 | primaryKeySQL := fmt.Sprintf(` 64 | SELECT Col.Column_Name from 65 | INFORMATION_SCHEMA.TABLE_CONSTRAINTS Tab, 66 | INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE Col 67 | WHERE 68 | Col.Constraint_Name = Tab.Constraint_Name 69 | AND Col.Table_Name = Tab.Table_Name 70 | AND Constraint_Type = 'PRIMARY KEY' 71 | AND Col.Table_Name = '%s' 72 | `, tableName) 73 | res, err := db.Query(primaryKeySQL) 74 | if err != nil { 75 | return "", fmt.Errorf("unable to load ddl from ms sql: %v", err) 76 | } 77 | defer res.Close() 78 | for res.Next() { 79 | var columnName string 80 | err = res.Scan(&columnName) 81 | if err != nil { 82 | return "", fmt.Errorf("unable to load identity info from ms sql Scan: %v", err) 83 | } 84 | 85 | return columnName, nil 86 | } 87 | return "", nil 88 | } 89 | 90 | // PostgresInformationSchema results from a query of the postgres InformationSchema db table 91 | type PostgresInformationSchema struct { 92 | TableCatalog string 93 | TableSchema string 94 | TableName string 95 | OrdinalPosition int 96 | ColumnName string 97 | DataType string 98 | CharacterMaximumLength interface{} 99 | ColumnDefault interface{} 100 | IsNullable string 101 | IsIdentity string 102 | PrimaryKey bool 103 | } 104 | 105 | // LoadTableInfoFromPostgresInformationSchema fetch info from information_schema for postgres database 106 | func LoadTableInfoFromPostgresInformationSchema(db *sql.DB, tableName string) (primaryKey map[string]*PostgresInformationSchema, err error) { 107 | colInfo := make(map[string]*PostgresInformationSchema) 108 | 109 | identitySQL := fmt.Sprintf(` 110 | SELECT TABLE_CATALOG, table_schema, table_name, ordinal_position, column_name, data_type, character_maximum_length, 111 | column_default, is_nullable, is_identity 112 | FROM information_schema.columns 113 | WHERE table_name = '%s' 114 | ORDER BY table_name, ordinal_position; 115 | `, tableName) 116 | 117 | res, err := db.Query(identitySQL) 118 | if err != nil { 119 | return nil, fmt.Errorf("unable to load ddl from %s: %v", tableName, err) 120 | } 121 | defer res.Close() 122 | for res.Next() { 123 | ci := &PostgresInformationSchema{} 124 | err = res.Scan(&ci.TableCatalog, &ci.TableSchema, &ci.TableName, &ci.OrdinalPosition, &ci.ColumnName, &ci.DataType, &ci.CharacterMaximumLength, 125 | &ci.ColumnDefault, &ci.IsNullable, &ci.IsIdentity) 126 | if err != nil { 127 | return nil, fmt.Errorf("unable to load identity info from postgres Scan: %v", err) 128 | } 129 | 130 | colInfo[ci.ColumnName] = ci 131 | } 132 | 133 | return colInfo, nil 134 | } 135 | 136 | // InformationSchema results from a query of the InformationSchema db table 137 | type InformationSchema struct { 138 | TableCatalog string 139 | TableSchema string 140 | TableName string 141 | OrdinalPosition int 142 | ColumnName string 143 | DataType string 144 | CharacterMaximumLength interface{} 145 | ColumnDefault interface{} 146 | IsNullable string 147 | } 148 | 149 | // LoadTableInfoFromMSSqlInformationSchema fetch info from information_schema for ms sql database 150 | func LoadTableInfoFromMSSqlInformationSchema(db *sql.DB, tableName string) (primaryKey map[string]*InformationSchema, err error) { 151 | colInfo := make(map[string]*InformationSchema) 152 | 153 | identitySQL := fmt.Sprintf(` 154 | SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, ORDINAL_POSITION, COLUMN_NAME, DATA_TYPE, character_maximum_length, 155 | column_default, is_nullable 156 | FROM information_schema.columns 157 | WHERE table_name = '%s' 158 | ORDER BY table_name, ordinal_position; 159 | `, tableName) 160 | 161 | res, err := db.Query(identitySQL) 162 | if err != nil { 163 | return nil, fmt.Errorf("unable to load ddl from information_schema: %v", err) 164 | } 165 | defer res.Close() 166 | for res.Next() { 167 | ci := &InformationSchema{} 168 | err = res.Scan(&ci.TableCatalog, &ci.TableSchema, &ci.TableName, &ci.OrdinalPosition, &ci.ColumnName, &ci.DataType, &ci.CharacterMaximumLength, 169 | &ci.ColumnDefault, &ci.IsNullable) 170 | 171 | if err != nil { 172 | return nil, fmt.Errorf("unable to load identity info from information_schema Scan: %v", err) 173 | } 174 | 175 | colInfo[ci.ColumnName] = ci 176 | } 177 | 178 | return colInfo, nil 179 | } 180 | 181 | // GetFieldLenFromInformationSchema fetch field length from database 182 | func GetFieldLenFromInformationSchema(db *sql.DB, tableSchema, tableName, columnName string) (int64, error) { 183 | sql := fmt.Sprintf(` 184 | select CHARACTER_MAXIMUM_LENGTH 185 | from information_schema.columns 186 | where table_schema = '%s' AND 187 | table_name = '%s' AND 188 | COLUMN_NAME = '%s' 189 | `, tableSchema, tableName, columnName) 190 | 191 | res, err := db.Query(sql) 192 | if err != nil { 193 | return -1, fmt.Errorf("unable to load col len from mysql: %v", err) 194 | } 195 | 196 | var colLen int64 197 | defer res.Close() 198 | if res.Next() { 199 | err = res.Scan(&colLen) 200 | if err != nil { 201 | return -1, fmt.Errorf("unable to load ddl from mysql Scan: %v", err) 202 | } 203 | } 204 | _ = res.Close() 205 | return colLen, nil 206 | 207 | } 208 | 209 | func cleanupDefault(val string) string { 210 | if len(val) < 2 { 211 | return val 212 | } 213 | 214 | if strings.HasPrefix(val, "(") && strings.HasSuffix(val, ")") { 215 | return cleanupDefault(val[1 : len(val)-1]) 216 | } 217 | 218 | if strings.Index(val, "nextval(") == 0 && strings.Index(val, "::regclass)") > -1 { 219 | return "" 220 | } 221 | 222 | if strings.LastIndex(val, "::") > -1 { 223 | return cleanupDefault(val[0:strings.LastIndex(val, "::")]) 224 | } 225 | if strings.HasPrefix(val, "'") && strings.HasSuffix(val, "'") { 226 | return cleanupDefault(val[1 : len(val)-1]) 227 | } 228 | // 'G'::mpaa_rating 229 | // ('now'::text)::date 230 | 231 | return val 232 | } 233 | 234 | // BytesToString convert []uint8 to string 235 | func BytesToString(bs []uint8) string { 236 | b := make([]byte, len(bs)) 237 | for i, v := range bs { 238 | b[i] = byte(v) 239 | } 240 | return string(b) 241 | } 242 | 243 | func indexAt(s, sep string, n int) int { 244 | idx := strings.Index(s[n:], sep) 245 | if idx > -1 { 246 | idx += n 247 | } 248 | return idx 249 | } 250 | 251 | func updateDefaultPrimaryKey(m *dbTableMeta) *dbTableMeta { 252 | hasPrimary := false 253 | primaryKeyPos := -1 254 | for i, j := range m.columns { 255 | if j.IsPrimaryKey() { 256 | primaryKeyPos = i 257 | hasPrimary = true 258 | break 259 | } 260 | } 261 | 262 | if !hasPrimary && len(m.columns) > 0 { 263 | comments := fmt.Sprintf("Warning table: %s does not have a primary key defined, setting col position 1 %s as primary key\n", m.tableName, m.columns[0].Name()) 264 | if au != nil { 265 | fmt.Print(au.Yellow(comments)) 266 | } else { 267 | fmt.Printf(comments) 268 | } 269 | 270 | primaryKeyPos = 0 271 | m.columns[0].isPrimaryKey = true 272 | m.columns[0].notes = m.columns[0].notes + comments 273 | } 274 | 275 | if m.columns[primaryKeyPos].nullable { 276 | comments := fmt.Sprintf("Warning table: %s primary key column %s is nullable column, setting it as NOT NULL\n", m.tableName, m.columns[primaryKeyPos].Name()) 277 | 278 | if au != nil { 279 | fmt.Print(au.Yellow(comments)) 280 | } else { 281 | fmt.Printf(comments) 282 | } 283 | 284 | m.columns[primaryKeyPos].nullable = false 285 | m.columns[0].notes = m.columns[0].notes + comments 286 | } 287 | m.primaryKeyPos = primaryKeyPos 288 | return m 289 | } 290 | -------------------------------------------------------------------------------- /dbmeta/util.go: -------------------------------------------------------------------------------- 1 | package dbmeta 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | "unicode" 11 | ) 12 | 13 | // commonInitialisms is a set of common initialisms. 14 | // Only add entries that are highly unlikely to be non-initialisms. 15 | // For instance, "ID" is fine (Freudian code is rare), but "AND" is not. 16 | var commonInitialisms = map[string]bool{ 17 | "API": true, 18 | "ASCII": true, 19 | "CPU": true, 20 | "CSS": true, 21 | "DNS": true, 22 | "EOF": true, 23 | "GUID": true, 24 | "HTML": true, 25 | "HTTP": true, 26 | "HTTPS": true, 27 | "ID": true, 28 | "IP": true, 29 | "JSON": true, 30 | "LHS": true, 31 | "QPS": true, 32 | "RAM": true, 33 | "RHS": true, 34 | "RPC": true, 35 | "SLA": true, 36 | "SMTP": true, 37 | "SSH": true, 38 | "TLS": true, 39 | "TTL": true, 40 | "UI": true, 41 | "UID": true, 42 | "UUID": true, 43 | "URI": true, 44 | "URL": true, 45 | "UTF8": true, 46 | "VM": true, 47 | "XML": true, 48 | "ACL": true, 49 | } 50 | 51 | var intToWordMap = []string{ 52 | "zero", 53 | "one", 54 | "two", 55 | "three", 56 | "four", 57 | "five", 58 | "six", 59 | "seven", 60 | "eight", 61 | "nine", 62 | } 63 | 64 | var parsePrimaryKeys = map[string]string{ 65 | "uint8": "parseUint8", 66 | "uint16": "parseUint16", 67 | "uint32": "parseUint32", 68 | "uint64": "parseUint64", 69 | "int": "parseInt", 70 | "int8": "parseInt8", 71 | "int16": "parseInt16", 72 | "int32": "parseInt32", 73 | "int64": "parseInt64", 74 | "string": "parseString", 75 | "uuid.UUID": "parseUUID", 76 | "time.Time": "parseTime", 77 | "varbinary": "parseBytes", 78 | } 79 | 80 | var reservedFieldNames = map[string]bool{ 81 | "TableName": true, 82 | "BeforeSave": true, 83 | "Prepare": true, 84 | "Validate": true, 85 | "type": true, 86 | } 87 | 88 | // RenameReservedName renames a reserved word 89 | func RenameReservedName(s string) string { 90 | _, match := reservedFieldNames[s] 91 | if match { 92 | return fmt.Sprintf("%s_", s) 93 | } 94 | 95 | return s 96 | } 97 | 98 | // FmtFieldName formats a string as a struct key 99 | // 100 | // Example: 101 | // fmtFieldName("foo_id") 102 | // Output: FooID 103 | func FmtFieldName(s string) string { 104 | name := lintFieldName(s) 105 | runes := []rune(name) 106 | for i, c := range runes { 107 | ok := unicode.IsLetter(c) || unicode.IsDigit(c) 108 | if i == 0 { 109 | ok = unicode.IsLetter(c) 110 | } 111 | if !ok { 112 | runes[i] = '_' 113 | } 114 | } 115 | fieldName := string(runes) 116 | fieldName = RenameReservedName(fieldName) 117 | // fmt.Printf("FmtFieldName:%s=%s\n", s, fieldName) 118 | return fieldName 119 | } 120 | 121 | func isAllLower(name string) (allLower bool) { 122 | allLower = true 123 | for _, r := range name { 124 | if !unicode.IsLower(r) { 125 | allLower = false 126 | break 127 | } 128 | } 129 | return 130 | } 131 | 132 | func lintAllLowerFieldName(name string) string { 133 | runes := []rune(name) 134 | if u := strings.ToUpper(name); commonInitialisms[u] { 135 | copy(runes[0:], []rune(u)) 136 | } else { 137 | runes[0] = unicode.ToUpper(runes[0]) 138 | } 139 | return string(runes) 140 | } 141 | 142 | func lintFieldName(name string) string { 143 | // Fast path for simple cases: "_" and all lowercase. 144 | if name == "_" { 145 | return name 146 | } 147 | 148 | for len(name) > 0 && name[0] == '_' { 149 | name = name[1:] 150 | } 151 | 152 | allLower := isAllLower(name) 153 | 154 | if allLower { 155 | return lintAllLowerFieldName(name) 156 | } 157 | 158 | return lintMixedFieldName(name) 159 | } 160 | 161 | func lintMixedFieldName(name string) string { 162 | // Split camelCase at any lower->upper transition, and split on underscores. 163 | // Check each word for common initialisms. 164 | runes := []rune(name) 165 | w, i := 0, 0 // index of start of word, scan 166 | 167 | for i+1 <= len(runes) { 168 | eow := false // whether we hit the end of a word 169 | 170 | if i+1 == len(runes) { 171 | eow = true 172 | } else if runes[i+1] == '_' { 173 | // underscore; shift the remainder forward over any run of underscores 174 | eow = true 175 | n := 1 176 | for i+n+1 < len(runes) && runes[i+n+1] == '_' { 177 | n++ 178 | } 179 | 180 | // Leave at most one underscore if the underscore is between two digits 181 | if i+n+1 < len(runes) && unicode.IsDigit(runes[i]) && unicode.IsDigit(runes[i+n+1]) { 182 | n-- 183 | } 184 | 185 | copy(runes[i+1:], runes[i+n+1:]) 186 | runes = runes[:len(runes)-n] 187 | } else if unicode.IsLower(runes[i]) && !unicode.IsLower(runes[i+1]) { 188 | // lower->non-lower 189 | eow = true 190 | } 191 | i++ 192 | if !eow { 193 | continue 194 | } 195 | 196 | // [w,i) is a word. 197 | word := string(runes[w:i]) 198 | if u := strings.ToUpper(word); commonInitialisms[u] { 199 | // All the common initialisms are ASCII, 200 | // so we can replace the bytes exactly. 201 | copy(runes[w:], []rune(u)) 202 | } else if strings.ToLower(word) == word { 203 | // already all lowercase, and not the first word, so uppercase the first character. 204 | runes[w] = unicode.ToUpper(runes[w]) 205 | } 206 | w = i 207 | } 208 | return string(runes) 209 | } 210 | 211 | // convert first character ints to strings 212 | func stringifyFirstChar(str string) string { 213 | first := str[:1] 214 | 215 | i, err := strconv.ParseInt(first, 10, 8) 216 | if err != nil { 217 | return str 218 | } 219 | 220 | return intToWordMap[i] + "_" + str[1:] 221 | } 222 | 223 | // Copy a src struct into a destination struct 224 | func Copy(dst interface{}, src interface{}) error { 225 | dstV := reflect.Indirect(reflect.ValueOf(dst)) 226 | srcV := reflect.Indirect(reflect.ValueOf(src)) 227 | 228 | if !dstV.CanAddr() { 229 | return errors.New("copy to value is unaddressable") 230 | } 231 | 232 | if srcV.Type() != dstV.Type() { 233 | return errors.New("different types can not be copied") 234 | } 235 | 236 | for i := 0; i < dstV.NumField(); i++ { 237 | f := srcV.Field(i) 238 | if !isZeroOfUnderlyingType(f.Interface()) { 239 | dstV.Field(i).Set(f) 240 | } 241 | } 242 | 243 | return nil 244 | } 245 | 246 | func isZeroOfUnderlyingType(x interface{}) bool { 247 | return x == nil || reflect.DeepEqual(x, reflect.Zero(reflect.TypeOf(x)).Interface()) 248 | } 249 | 250 | // Pwd template command to return the current working directory 251 | func Pwd() string { 252 | currentWorkingDirectory, err := os.Getwd() 253 | if err != nil { 254 | return fmt.Sprintf("pwd returned an error %v", err) 255 | } 256 | return currentWorkingDirectory 257 | } 258 | -------------------------------------------------------------------------------- /dbmeta/util_test.go: -------------------------------------------------------------------------------- 1 | package dbmeta 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | type Employee struct { 10 | EmpNo int `gorm:"column:emp_no;primary_key" json:"emp_no"` 11 | BirthDate time.Time `gorm:"column:birth_date" json:"birth_date"` 12 | FirstName string `gorm:"column:first_name" json:"first_name"` 13 | LastName string `gorm:"column:last_name" json:"last_name"` 14 | Gender string `gorm:"column:gender" json:"gender"` 15 | HireDate time.Time `gorm:"column:hire_date" json:"hire_date"` 16 | } 17 | 18 | func Test_Copy(t *testing.T) { 19 | now := time.Now() 20 | dst := &Employee{ 21 | EmpNo: 10001, 22 | BirthDate: now, 23 | FirstName: "Tom", 24 | } 25 | 26 | src := &Employee{ 27 | EmpNo: 10001, 28 | BirthDate: now.Add(3600 * time.Second), 29 | FirstName: "Jerry", 30 | Gender: "Male", 31 | } 32 | 33 | err := Copy(dst, src) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | expected := &Employee{ 39 | EmpNo: 10001, 40 | BirthDate: now.Add(3600 * time.Second), 41 | FirstName: "Jerry", 42 | Gender: "Male", 43 | } 44 | 45 | if !reflect.DeepEqual(expected, dst) { 46 | t.Errorf("expect: %+v, but got %+x", expected, dst) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /example/sample.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smallnest/gen/2ab666fa15c3d953cf659529584e1d20b3f04c89/example/sample.db -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/smallnest/gen 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/bxcodec/faker/v3 v3.5.0 7 | github.com/davecgh/go-spew v1.1.1 8 | github.com/denisenkom/go-mssqldb v0.9.0 9 | github.com/droundy/goopt v0.0.0-20170604162106-0b8effe182da 10 | github.com/gobuffalo/packd v1.0.0 11 | github.com/gobuffalo/packr/v2 v2.8.1 12 | github.com/iancoleman/strcase v0.1.2 13 | github.com/jimsmart/schema v0.2.0 14 | github.com/jinzhu/gorm v1.9.16 15 | github.com/jinzhu/inflection v1.0.0 16 | github.com/karrick/godirwalk v1.16.1 // indirect 17 | github.com/lib/pq v1.10.0 18 | github.com/logrusorgru/aurora v2.0.3+incompatible 19 | github.com/mattn/go-sqlite3 v2.0.3+incompatible 20 | github.com/ompluscator/dynamic-struct v1.3.0 21 | github.com/rogpeppe/go-internal v1.6.2 // indirect 22 | github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516 23 | github.com/sirupsen/logrus v1.7.0 // indirect 24 | golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee // indirect 25 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e 26 | ) 27 | -------------------------------------------------------------------------------- /main-packr.go: -------------------------------------------------------------------------------- 1 | // +build !skippackr 2 | // Code generated by github.com/gobuffalo/packr/v2. DO NOT EDIT. 3 | 4 | // You can use the "packr clean" command to clean up this, 5 | // and any other packr generated files. 6 | package main 7 | 8 | import _ "github.com/smallnest/gen/packrd" 9 | -------------------------------------------------------------------------------- /readme/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "os/exec" 9 | "path/filepath" 10 | 11 | _ "github.com/denisenkom/go-mssqldb" 12 | "github.com/droundy/goopt" 13 | "github.com/gobuffalo/packr/v2" 14 | _ "github.com/jinzhu/gorm/dialects/mysql" 15 | _ "github.com/lib/pq" 16 | _ "github.com/mattn/go-sqlite3" 17 | 18 | "github.com/smallnest/gen/dbmeta" 19 | ) 20 | 21 | var ( 22 | sqlType = goopt.String([]string{"--sqltype"}, "mysql", "sql database type such as [ mysql, mssql, postgres, sqlite, etc. ]") 23 | sqlConnStr = goopt.String([]string{"-c", "--connstr"}, "nil", "database connection string") 24 | sqlDatabase = goopt.String([]string{"-d", "--database"}, "nil", "Database to for connection") 25 | sqlTable = goopt.String([]string{"-t", "--table"}, "", "Table to build struct from") 26 | templateDir = goopt.String([]string{"--templateDir"}, "./template", "Template Dir") 27 | baseTemplates *packr.Box 28 | ) 29 | 30 | func init() { 31 | // Setup goopts 32 | goopt.Description = func() string { 33 | return "ORM and RESTful meta data viewer for SQl databases" 34 | } 35 | goopt.Version = "v0.9.27 (08/04/2020)" 36 | goopt.Summary = `dbmeta [-v] --sqltype=mysql --connstr "user:password@/dbname" --database 37 | 38 | sqltype - sql database type such as [ mysql, mssql, postgres, sqlite, etc. ] 39 | 40 | ` 41 | //Parse options 42 | goopt.Parse(nil) 43 | } 44 | 45 | // GenHelp generated help via exec the gen command 46 | func GenHelp() string { 47 | cmd := exec.Command("./gen", "-h") 48 | stdoutStderr, err := cmd.CombinedOutput() 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | 53 | //fmt.Printf("%s\n", stdoutStderr) 54 | return string(stdoutStderr) 55 | } 56 | 57 | func main() { 58 | 59 | baseTemplates = packr.New("gen", "../template") 60 | 61 | err := loadDefaultDBMappings() 62 | if err != nil { 63 | fmt.Printf("Error processing default mapping file error: %v\n", err) 64 | return 65 | } 66 | 67 | // Username is required 68 | if sqlConnStr == nil || *sqlConnStr == "" || *sqlConnStr == "nil" { 69 | fmt.Printf("sql connection string is required! Add it with --connstr=s\n\n") 70 | fmt.Println(goopt.Usage()) 71 | return 72 | } 73 | 74 | if sqlDatabase == nil || *sqlDatabase == "" || *sqlDatabase == "nil" { 75 | fmt.Printf("Database can not be null\n\n") 76 | fmt.Println(goopt.Usage()) 77 | return 78 | } 79 | 80 | db, err := initializeDB() 81 | if err != nil { 82 | return 83 | } 84 | 85 | defer db.Close() 86 | 87 | conf := dbmeta.NewConfig(LoadTemplate) 88 | initialize(conf) 89 | 90 | dbTables := []string{*sqlTable} 91 | excludedDbTables := []string{} 92 | 93 | tableInfos := dbmeta.LoadTableInfo(db, dbTables, excludedDbTables, conf) 94 | conf.ContextMap["tableInfos"] = tableInfos 95 | 96 | for tableName, modelInfo := range tableInfos { 97 | fmt.Printf("%-15s %v\n", tableName, modelInfo.StructName) 98 | ctx := conf.CreateContextForTableFile(tableInfos[*sqlTable]) 99 | 100 | genreadme(conf, "code_dao_gorm.md.tmpl", "./code_dao_gorm.md", ctx) 101 | genreadme(conf, "code_dao_sqlx.md.tmpl", "./code_dao_sqlx.md", ctx) 102 | genreadme(conf, "code_http.md.tmpl", "./code_http.md", ctx) 103 | 104 | help := GenHelp() 105 | conf.ContextMap["GenHelp"] = help 106 | genreadme(conf, "GEN_README.md.tmpl", "./README.md", ctx) 107 | } 108 | 109 | } 110 | 111 | func genreadme(conf *dbmeta.Config, templateName, outputFile string, ctx map[string]interface{}) { 112 | template, err := LoadTemplate(templateName) 113 | if err != nil { 114 | fmt.Printf("Error loading template %v\n", err) 115 | return 116 | } 117 | 118 | sample := ` 119 | {{ range $i, $table := .tables }} 120 | {{$singular := singular $table -}} 121 | {{$plural := pluralize $table -}} 122 | {{$title := title $table -}} 123 | {{$lower := toLower $table -}} 124 | {{$lowerCamel := toLowerCamelCase $table -}} 125 | {{$snakeCase := toSnakeCase $table -}} 126 | {{ printf "[%-2d] %-20s %-20s %-20s %-20s %-20s %-20s %-20s" $i $table $singular $plural $title $lower $lowerCamel $snakeCase}}{{- end }} 127 | 128 | 129 | {{ range $i, $table := .tables }} 130 | {{$name := toUpper $table -}} 131 | {{$filename := printf "My%s" $name -}} 132 | {{ printf "[%-2d] %-20s %-20s" $i $table $filename}} 133 | {{ GenerateTableFile $table "custom.go.tmpl" "test" $filename true}} 134 | {{- end }} 135 | ` 136 | ctx["AdvancesSample"] = sample 137 | 138 | releaseHistory := loadFile("release.history") 139 | ctx["ReleaseHistory"] = releaseHistory 140 | conf.WriteTemplate(template, ctx, outputFile) 141 | } 142 | 143 | func loadFile(src string) string { 144 | // Read entire file content, giving us little control but 145 | // making it very simple. No need to close the file. 146 | content, err := ioutil.ReadFile(src) 147 | if err != nil { 148 | return fmt.Sprintf("error loading %s error: %v", src, err) 149 | } 150 | 151 | // Convert []byte to string and print to screen 152 | text := string(content) 153 | return text 154 | } 155 | 156 | func initialize(conf *dbmeta.Config) { 157 | outDir := "." 158 | module := "github.com/alexj212/test" 159 | modelPackageName := "model" 160 | daoPackageName := "dao" 161 | apiPackageName := "api" 162 | 163 | conf.SQLType = *sqlType 164 | conf.SQLDatabase = *sqlDatabase 165 | conf.ModelPackageName = modelPackageName 166 | conf.DaoPackageName = daoPackageName 167 | conf.APIPackageName = apiPackageName 168 | 169 | conf.AddJSONAnnotation = true 170 | conf.AddXMLAnnotation = true 171 | conf.AddGormAnnotation = true 172 | conf.AddProtobufAnnotation = true 173 | conf.AddDBAnnotation = true 174 | conf.UseGureguTypes = false 175 | conf.JSONNameFormat = "snake" 176 | conf.XMLNameFormat = "snake" 177 | conf.ProtobufNameFormat = "" 178 | conf.Verbose = false 179 | conf.OutDir = outDir 180 | conf.Overwrite = true 181 | 182 | conf.SQLConnStr = *sqlConnStr 183 | conf.ServerPort = 8080 184 | conf.ServerHost = "127.0.0.1" 185 | conf.ServerListen = ":8080" 186 | conf.ServerScheme = "http" 187 | conf.Overwrite = true 188 | 189 | conf.Module = module 190 | conf.ModelFQPN = module + "/" + modelPackageName 191 | conf.DaoFQPN = module + "/" + daoPackageName 192 | conf.APIFQPN = module + "/" + apiPackageName 193 | 194 | conf.Swagger.Version = "1.0.0" 195 | conf.Swagger.BasePath = "/" 196 | conf.Swagger.Title = fmt.Sprintf("Sample CRUD api for %s db", *sqlDatabase) 197 | conf.Swagger.Description = fmt.Sprintf("Sample CRUD api for %s db", *sqlDatabase) 198 | conf.Swagger.TOS = "My Custom TOS" 199 | conf.Swagger.ContactName = "" 200 | conf.Swagger.ContactURL = "" 201 | conf.Swagger.ContactEmail = "" 202 | if conf.ServerPort == 80 { 203 | conf.Swagger.Host = conf.ServerHost 204 | } else { 205 | conf.Swagger.Host = fmt.Sprintf("%s:%d", conf.ServerHost, conf.ServerPort) 206 | } 207 | } 208 | 209 | func initializeDB() (db *sql.DB, err error) { 210 | 211 | db, err = sql.Open(*sqlType, *sqlConnStr) 212 | if err != nil { 213 | fmt.Printf("Error in open database: %v\n\n", err.Error()) 214 | return nil, err 215 | } 216 | 217 | err = db.Ping() 218 | if err != nil { 219 | fmt.Printf("Error pinging database: %v\n\n", err.Error()) 220 | return 221 | } 222 | 223 | return 224 | } 225 | 226 | func loadDefaultDBMappings() error { 227 | var err error 228 | var content []byte 229 | content, err = baseTemplates.Find("mapping.json") 230 | if err != nil { 231 | return err 232 | } 233 | 234 | err = dbmeta.ProcessMappings("internal", content, false) 235 | if err != nil { 236 | return err 237 | } 238 | return nil 239 | } 240 | 241 | // LoadTemplate return template from template dir, falling back to the embedded templates 242 | func LoadTemplate(filename string) (tpl *dbmeta.GenTemplate, err error) { 243 | baseName := filepath.Base(filename) 244 | // fmt.Printf("LoadTemplate: %s / %s\n", filename, baseName) 245 | 246 | if *templateDir != "" { 247 | fpath := filepath.Join(*templateDir, filename) 248 | var b []byte 249 | b, err = ioutil.ReadFile(fpath) 250 | if err == nil { 251 | 252 | absPath, err := filepath.Abs(fpath) 253 | if err != nil { 254 | absPath = fpath 255 | } 256 | // fmt.Printf("Loaded template from file: %s\n", fpath) 257 | tpl = &dbmeta.GenTemplate{Name: "file://" + absPath, Content: string(b)} 258 | return tpl, nil 259 | } 260 | } 261 | 262 | content, err := baseTemplates.FindString(baseName) 263 | if err != nil { 264 | return nil, fmt.Errorf("%s not found internally", baseName) 265 | } 266 | 267 | tpl = &dbmeta.GenTemplate{Name: "internal://" + filename, Content: content} 268 | return tpl, nil 269 | } 270 | -------------------------------------------------------------------------------- /release.history: -------------------------------------------------------------------------------- 1 | - v0.9.27 (08/04/2020) 2 | - Updated '--exec' mode to provide various functions for processing 3 | - copy function updated to provide --include and --exclude patterns. Patterns are processed in order, an include preceeding an exclude will take precedence. Multiple include and excludes can be specified. Files ending with .table.tmpl will be processed for each table. Output filenames will be stored in the proper directory, with a name of the table with the suffix of the template extension. Files ending with .tmpl will be processed as a template and the filename will be the name of the template stripped with the .tmpl suffix. 4 | - When processing templates, files generated with a .go extension will be formatted with the go fmt. 5 | - v0.9.26 (07/31/2020) 6 | - Release scripting 7 | - Added custom script functions to copy, mkdir, touch, pwd 8 | - Fixed custom script exec example 9 | - v0.9.25 (07/26/2020) 10 | - Adhere json-fmt flag for all JSON response so when camel or lower_camel is specified, fields name in GetAll variant and DDL info will also have the same name format 11 | - Fix: Build information embedded through linker in Makefile is not consistent with the variable defined in main file. 12 | - Added --scheme and --listen options. This allows compiled binary to be used behind reverse proxy. 13 | - In addition, template for generating URL was fixed, i.e. when PORT is 80, then PORT is omitted from URL segment. 14 | - v0.9.24 (07/13/2020) 15 | - Fixed array bounds issue parsing mysql db meta 16 | - v0.9.23 (07/10/2020) 17 | - Added postgres types: bigserial, serial, smallserial, bigserial, float4 to mapping.json 18 | - v0.9.22 (07/08/2020) 19 | - Modified gogo.proto check to use GOPATH not hardcoded. 20 | - Updated gen to error exit on first error encountered 21 | - Added color output for error 22 | - Added --no-color option for non colorized output 23 | - v0.9.21 (07/07/2020) 24 | - Repacking templates, update version number in info. 25 | - v0.9.20 (07/07/2020) 26 | - Fixed render error in router.go.tmpl 27 | - upgraded project to use go.mod 1.14 28 | - v0.9.19 (07/07/2020) 29 | - Added --windows flag to write files with CRLF windows line endings, otherwise they are all unix based LF line endings 30 | - v0.9.18 (06/30/2020) 31 | - Fixed naming in templates away from hard coded model package. 32 | - v0.9.17 (06/30/2020) 33 | - Refactored template loading, to better report error in template 34 | - Added option to run gofmt on output directory 35 | - v0.9.16 (06/29/2020) 36 | - Fixes to router.go.tmpl from calvinchengx 37 | - Added postgres db support for inet and timestamptz 38 | - v0.9.15 (06/23/2020) 39 | - Code cleanup using gofmt name suggestions. 40 | - Template updates for generated code cleanup using gofmt name suggestions. 41 | - v0.9.14 (06/23/2020) 42 | - Added model comment on field line if available from database. 43 | - Added exposing TableInfo via api call. 44 | - v0.9.13 (06/22/2020) 45 | - fixed closing of connections via defer 46 | - bug fixes in sqlx generated code 47 | - v0.9.12 (06/14/2020) 48 | - SQLX changed MustExec to Exec and checking/returning error 49 | - Updated field renaming if duplicated, need more elegant renaming solution. 50 | - Added exclude to test.sh 51 | - v0.9.11 (06/13/2020) 52 | - Added ability to pass field, model and file naming format 53 | - updated test scripts 54 | - Fixed sqlx sql query placeholders 55 | - v0.9.10 (06/11/2020) 56 | - Bug fix with retrieving varchar length from mysql 57 | - Added support for mysql unsigned decimal - maps to float 58 | - v0.9.9 (06/11/2020) 59 | - Fixed issue with mysql and table named `order` 60 | - Fixed internals in GetAll generation in gorm and sqlx. 61 | - v0.9.8 (06/10/2020) 62 | - Added ability to set file naming convention for models, dao, apis and grpc `--file_naming={{.}}` 63 | - Added ability to set struct naming convention `--model_naming={{.}}` 64 | - Fixed bug with Makefile generation removing quoted conn string in `make regen` 65 | - v0.9.7 (06/09/2020) 66 | - Added grpc server generation - WIP (looking for code improvements) 67 | - Added ability to exclude tables 68 | - Added support for unsigned from mysql ddl. 69 | - v0.9.6 (06/08/2020) 70 | - Updated SQLX codegen 71 | - Updated templates to split code gen functions into seperate files 72 | - Added code_dao_gorm, code_dao_sqlx to be generated from templates 73 | - v0.9.5 (05/16/2020) 74 | - Added SQLX codegen by default, split dao templates. 75 | - Renamed templates 76 | - v0.9.4 (05/15/2020) 77 | - Documentation updates, samples etc. 78 | - v0.9.3 (05/14/2020) 79 | - Template bug fixes, when using custom api, dao and model package. 80 | - Set primary key if not set to the first column 81 | - Skip code gen if primary key column is not int or string 82 | - validated codegen for mysql, mssql, postgres and sqlite3 83 | - Fixed file naming if table ends with _test.go renames to _tst.go 84 | - Fix for duplicate field names in struct due to renaming 85 | - Added Notes for columns and tables for situations where a primary key is set since not defined in db 86 | - Fixed issue when model contained field that had were named the same as funcs within model. 87 | - v0.9.2 (05/12/2020) 88 | - Code cleanup gofmt, etc. 89 | - v0.9.1 (05/12/2020) 90 | - v0.9 (05/12/2020) 91 | - updated db meta data loading fetching default values 92 | - added default value to GORM tags 93 | - Added protobuf .proto generation 94 | - Added test app to display meta data 95 | - Cleanup DDL generation 96 | - Added support for varchar2, datetime2, float8, USER_DEFINED 97 | - v0.5 98 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | VERSION=$(head -n 1 release.history) 4 | VERSION=${VERSION#"- "} 5 | 6 | VERSION_CODE="goopt.Version = \"${VERSION}\"" 7 | echo "VERSION : ${VERSION}" 8 | echo "VERSION_CODE: ${VERSION_CODE}" 9 | 10 | 11 | sed -i "s~goopt\.Version = \".*\"~goopt.Version = \"${VERSION}\"~g" readme/main.go 12 | sed -i "s~goopt\.Version = \".*\"~goopt.Version = \"${VERSION}\"~g" main.go 13 | sed -i "s~goopt\.Version = \".*\"~goopt.Version = \"${VERSION}\"~g" _test/dbmeta/main.go 14 | 15 | ack "goopt.Version = \".*\"" 16 | -------------------------------------------------------------------------------- /template/Makefile.tmpl: -------------------------------------------------------------------------------- 1 | export PROJ_PATH={{.module}} 2 | 3 | export DATE := $(shell date +%Y.%m.%d-%H%M) 4 | export LATEST_COMMIT := $(shell git log --pretty=format:'%h' -n 1) 5 | export BRANCH := $(shell git branch |grep -v "no branch"| grep \*|cut -d ' ' -f2) 6 | export BUILT_ON_IP := $(shell [ $$(uname) = Linux ] && hostname -i || hostname ) 7 | export BIN_DIR=./bin 8 | export PACKR2_EXECUTABLE := $(shell command -v packr2 2> /dev/null) 9 | export SWAG_EXECUTABLE := $(shell command -v swag 2> /dev/null) 10 | export RUNTIME_VER := $(shell go version) 11 | 12 | export BUILT_ON_OS=$(shell uname -a) 13 | ifeq ($(BRANCH),) 14 | BRANCH := master 15 | endif 16 | 17 | export COMMIT_CNT := $(shell git rev-list HEAD | wc -l | sed 's/ //g' ) 18 | export BUILD_NUMBER := ${BRANCH}-${COMMIT_CNT} 19 | export COMPILE_LDFLAGS=-s -X "main.BuildDate=${DATE}" \ 20 | -X "main.LatestCommit=${LATEST_COMMIT}" \ 21 | -X "main.BuildNumber=${BUILD_NUMBER}" \ 22 | -X "main.BuiltOnIP=${BUILT_ON_IP}" \ 23 | -X "main.BuiltOnOs=${BUILT_ON_OS}" \ 24 | -X "main.RuntimeVer=${RUNTIME_VER}" 25 | 26 | build_info: check_prereq ## Build the container 27 | @echo '' 28 | @echo '---------------------------------------------------------' 29 | @echo 'BUILT_ON_IP $(BUILT_ON_IP)' 30 | @echo 'BUILT_ON_OS $(BUILT_ON_OS)' 31 | @echo 'DATE $(DATE)' 32 | @echo 'LATEST_COMMIT $(LATEST_COMMIT)' 33 | @echo 'BRANCH $(BRANCH)' 34 | @echo 'COMMIT_CNT $(COMMIT_CNT)' 35 | @echo 'BUILD_NUMBER $(BUILD_NUMBER)' 36 | @echo 'COMPILE_LDFLAGS $(COMPILE_LDFLAGS)' 37 | @echo 'PATH $(PATH)' 38 | @echo 'PACKR2_EXECUTABLE $(PACKR2_EXECUTABLE)' 39 | @echo 'SWAG_EXECUTABLE $(SWAG_EXECUTABLE)' 40 | @echo 'RUNTIME_VER $(RUNTIME_VER)' 41 | @echo '---------------------------------------------------------' 42 | @echo '' 43 | 44 | 45 | #################################################################################################################### 46 | ## 47 | ## help for each task - https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html 48 | ## 49 | #################################################################################################################### 50 | .PHONY: help 51 | 52 | help: ## This help. 53 | @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) 54 | 55 | .DEFAULT_GOAL := help 56 | 57 | 58 | 59 | #################################################################################################################### 60 | ## 61 | ## Build of binaries 62 | ## 63 | #################################################################################################################### 64 | all: example test ## build example and run tests 65 | 66 | binaries: example ## build binaries in bin dir 67 | 68 | create_dir: 69 | @mkdir -p $(BIN_DIR) 70 | 71 | check_prereq: create_dir 72 | ifndef PACKR2_EXECUTABLE 73 | go get -u github.com/gobuffalo/packr/v2/packr2 74 | endif 75 | $(warning "found packr2") 76 | 77 | ifndef SWAG_EXECUTABLE 78 | go get -u github.com/swaggo/swag/cmd/swag 79 | endif 80 | $(warning "found swag") 81 | 82 | 83 | 84 | build_app: create_dir 85 | packr2 build -o $(BIN_DIR)/$(BIN_NAME) -a -ldflags '$(COMPILE_LDFLAGS)' $(APP_PATH) 86 | 87 | 88 | example: build_info ## build example binary in bin dir 89 | @echo "build example server" 90 | swag init --dir . --generalInfo ./app/server/main.go 91 | make BIN_NAME=example APP_PATH=$(PROJ_PATH)/app/server build_app 92 | @echo '' 93 | @echo '' 94 | 95 | 96 | 97 | #################################################################################################################### 98 | ## 99 | ## Cleanup of binaries 100 | ## 101 | #################################################################################################################### 102 | 103 | clean_binaries: clean_example ## clean all binaries in bin dir 104 | 105 | 106 | clean_binary: ## clean binary in bin dir 107 | rm -f $(BIN_DIR)/$(BIN_NAME) 108 | 109 | clean_example: ## clean example 110 | make BIN_NAME=example clean_binary 111 | 112 | 113 | 114 | test: ## run tests 115 | go test -v $(PROJ_PATH) 116 | 117 | fmt: ## run fmt on project 118 | #go fmt $(PROJ_PATH)/... 119 | gofmt -s -d -w -l . 120 | 121 | doc: ## launch godoc on port 6060 122 | godoc -http=:6060 123 | 124 | deps: ## display deps for project 125 | {{.deps}} |grep "/" | grep -v $(PROJ_PATH)| grep "\." | sort |uniq 126 | 127 | lint: ## run lint on the project 128 | golint ./... 129 | 130 | staticcheck: ## run staticcheck on the project 131 | staticcheck -ignore "$(shell cat .checkignore)" . 132 | 133 | vet: ## run go vet on the project 134 | go vet . 135 | 136 | tools: ## install dependent tools for code analysis 137 | go get -u github.com/gogo/protobuf 138 | go get -u github.com/gogo/protobuf/proto 139 | go get -u github.com/gogo/protobuf/jsonpb 140 | go get -u github.com/gogo/protobuf/protoc-gen-gogo 141 | go get -u github.com/gogo/protobuf/gogoproto 142 | go get -u honnef.co/go/tools 143 | go get -u github.com/gordonklaus/ineffassign 144 | go get -u github.com/fzipp/gocyclo 145 | go get -u golang.org/x/lint/golint 146 | go get -u github.com/gobuffalo/packr/v2/packr2 147 | 148 | 149 | 150 | regen: ## regenerate generated code 151 | {{.RegenCmdLine}} 152 | 153 | {{if hasField . "ProtocCmdLine"}} 154 | protoc: ## regenerate from .protofile 155 | {{.ProtocCmdLine}} 156 | {{end}} 157 | -------------------------------------------------------------------------------- /template/README.md.tmpl: -------------------------------------------------------------------------------- 1 | [comment]: <> (This is a generated file please edit source in ./templates) 2 | [comment]: <> (All modification will be lost, you have been warned) 3 | [comment]: <> () 4 | ### Sample CRUD API for the {{.sqlType}} database {{.sqlConnStr}} 5 | 6 | ## Example 7 | The project is a RESTful api for accessing the {{.sqlType}} database {{.sqlConnStr}}. 8 | 9 | ## Project Files 10 | The generated project will contain the following code under the `./example` directory. 11 | * Makefile 12 | * useful Makefile for installing tools building project etc. Issue `make` to display help 13 | * .gitignore 14 | * git ignore for go project 15 | * go.mod 16 | * go module setup, pass `--module` flag for setting the project module default `example.com/example` 17 | * README.md 18 | * Project readme 19 | * app/server/main.go 20 | * Sample Gin Server, with swagger init and comments 21 | * api/*.go 22 | * REST crud controllers 23 | * dao/*.go 24 | * DAO functions providing CRUD access to database 25 | * model/*.go 26 | * Structs representing a row for each database table 27 | 28 | The REST api server utilizes the Gin framework, GORM db api and Swag for providing swagger documentation 29 | * [Gin](https://github.com/gin-gonic/gin) 30 | * [Swaggo](https://github.com/swaggo/swag) 31 | * [Gorm](https://github.com/jinzhu/gorm) 32 | 33 | ## Building 34 | ```.bash 35 | make example 36 | ``` 37 | Will create a binary `./bin/example` 38 | 39 | ## Running 40 | ```.bash 41 | ./bin/example 42 | ``` 43 | This will launch the web server on {{$.serverHost}}{{if ne $.serverPort 80}}:{{$.serverPort}}{{end}} 44 | 45 | ## Swagger 46 | The swagger web ui contains the documentation for the http server, it also provides an interactive interface to exercise the api and view results. 47 | {{$.serverScheme}}://{{$.serverHost}}{{if ne $.serverPort 80}}:{{$.serverPort}}{{end}}/swagger/index.html 48 | 49 | ## REST urls for fetching data 50 | 51 | {{range $tableName, $codeInfo := .tableInfos}} 52 | * {{$.serverScheme}}://{{$.serverHost}}{{if ne $.serverPort 80}}:{{$.serverPort}}{{end}}/{{$codeInfo.StructName | toLower}}{{ end }} 53 | 54 | ## Project Generated Details 55 | {{markdownCodeBlock ".bash" (.Config.CmdLineWrapped)}} 56 | 57 | {{/* 58 | */}} 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /template/api.go.tmpl: -------------------------------------------------------------------------------- 1 | package {{.apiPackageName}} 2 | 3 | import ( 4 | "net/http" 5 | 6 | "{{.modelFQPN}}" 7 | "{{.daoFQPN}}" 8 | 9 | "github.com/gin-gonic/gin" 10 | {{if .UseGuregu}} "github.com/guregu/null" {{end}} 11 | "github.com/julienschmidt/httprouter" 12 | ) 13 | 14 | func config{{.StructName}}Router(router *httprouter.Router) { 15 | router.GET("/{{.StructName | toLower}}", GetAll{{.StructName}}) 16 | router.POST("/{{.StructName | toLower}}", Add{{.StructName}}) 17 | router.GET("/{{.StructName | toLower}}{{range $field := .TableInfo.CodeFields}}{{ if $field.PrimaryKeyArgName}}/:{{$field.PrimaryKeyArgName}}{{end}}{{end -}}", Get{{.StructName}}) 18 | router.PUT("/{{.StructName | toLower}}{{range $field := .TableInfo.CodeFields}}{{ if $field.PrimaryKeyArgName}}/:{{$field.PrimaryKeyArgName}}{{end}}{{end -}}", Update{{.StructName}}) 19 | router.DELETE("/{{.StructName | toLower}}{{range $field := .TableInfo.CodeFields}}{{ if $field.PrimaryKeyArgName}}/:{{$field.PrimaryKeyArgName}}{{end}}{{end -}}", Delete{{.StructName}}) 20 | } 21 | 22 | func configGin{{.StructName}}Router(router gin.IRoutes) { 23 | router.GET("/{{.StructName | toLower}}", ConverHttprouterToGin(GetAll{{.StructName}})) 24 | router.POST("/{{.StructName | toLower}}", ConverHttprouterToGin(Add{{.StructName}})) 25 | router.GET("/{{.StructName | toLower}}{{range $field := .TableInfo.CodeFields}}{{ if $field.PrimaryKeyArgName}}/:{{$field.PrimaryKeyArgName}}{{end}}{{end -}}", ConverHttprouterToGin(Get{{.StructName}})) 26 | router.PUT("/{{.StructName | toLower}}{{range $field := .TableInfo.CodeFields}}{{ if $field.PrimaryKeyArgName}}/:{{$field.PrimaryKeyArgName}}{{end}}{{end -}}", ConverHttprouterToGin(Update{{.StructName}})) 27 | router.DELETE("/{{.StructName | toLower}}{{range $field := .TableInfo.CodeFields}}{{ if $field.PrimaryKeyArgName}}/:{{$field.PrimaryKeyArgName}}{{end}}{{end -}}", ConverHttprouterToGin(Delete{{.StructName}})) 28 | } 29 | 30 | {{template "api_getall.go.tmpl" .}} 31 | {{template "api_get.go.tmpl" .}} 32 | {{template "api_add.go.tmpl" .}} 33 | {{template "api_update.go.tmpl" .}} 34 | {{template "api_delete.go.tmpl" .}} 35 | -------------------------------------------------------------------------------- /template/api_add.go.tmpl: -------------------------------------------------------------------------------- 1 | {{define "api_add.go.tmpl"}} 2 | // Add{{.StructName}} add to add a single record to {{.TableName}} table in the {{.DatabaseName}} database 3 | // @Summary Add an record to {{.TableName}} table 4 | // @Description add to add a single record to {{.TableName}} table in the {{.DatabaseName}} database 5 | // @Tags {{.StructName}} 6 | // @Accept json 7 | // @Produce json 8 | // @Param {{.StructName}} body {{.modelPackageName}}.{{.StructName}} true "Add {{.StructName}}" 9 | // @Success 200 {object} {{.modelPackageName}}.{{.StructName}} 10 | // @Failure 400 {object} {{.apiPackageName}}.HTTPError 11 | // @Failure 404 {object} {{.apiPackageName}}.HTTPError 12 | // @Router /{{.StructName | toLower}} [post] 13 | // echo '{{ToJSON .TableInfo.Instance 0}}' | http POST "{{$.serverScheme}}://{{$.serverHost}}{{if ne $.serverPort 80}}:{{$.serverPort}}{{end}}/{{.StructName | toLower}}" X-Api-User:user123 14 | func Add{{.StructName}}(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 15 | ctx := initializeContext(r) 16 | {{.StructName | toLower}} := &{{.modelPackageName}}.{{.StructName}}{} 17 | 18 | if err := readJSON(r, {{.StructName | toLower}}); err != nil { 19 | returnError(ctx, w, r, {{.daoPackageName}}.ErrBadParams) 20 | return 21 | } 22 | 23 | 24 | if err := {{.StructName | toLower}}.BeforeSave(); err != nil { 25 | returnError(ctx, w, r, {{.daoPackageName}}.ErrBadParams) 26 | } 27 | 28 | {{.StructName | toLower}}.Prepare() 29 | 30 | if err := {{.StructName | toLower}}.Validate({{.modelPackageName}}.Create); err != nil { 31 | returnError(ctx, w, r, {{.daoPackageName}}.ErrBadParams) 32 | return 33 | } 34 | 35 | if err := ValidateRequest(ctx, r, "{{.TableName}}", {{.modelPackageName}}.Create); err != nil{ 36 | returnError(ctx, w, r, err) 37 | return 38 | } 39 | 40 | var err error 41 | {{.StructName | toLower}}, _, err = {{.daoPackageName}}.Add{{.StructName}}(ctx, {{.StructName | toLower}}) 42 | if err != nil { 43 | returnError(ctx, w, r, err) 44 | return 45 | } 46 | 47 | writeJSON(ctx, w, {{.StructName | toLower}}) 48 | } 49 | {{end}} 50 | -------------------------------------------------------------------------------- /template/api_delete.go.tmpl: -------------------------------------------------------------------------------- 1 | {{define "api_delete.go.tmpl"}} 2 | // Delete{{.StructName}} Delete a single record from {{.TableName}} table in the {{.DatabaseName}} database 3 | // @Summary Delete a record from {{.TableName}} 4 | // @Description Delete a single record from {{.TableName}} table in the {{.DatabaseName}} database 5 | // @Tags {{.StructName}} 6 | // @Accept json 7 | // @Produce json 8 | {{range $field := .TableInfo.CodeFields}}{{ if $field.PrimaryKeyArgName}}// @Param {{ $field.PrimaryKeyArgName }} path {{ $field.SQLMapping.SwaggerType }} true "{{ $field.ColumnMeta.Name }}"{{end}}{{end}} 9 | // @Success 204 {object} {{.modelPackageName}}.{{.StructName}} 10 | // @Failure 400 {object} {{.apiPackageName}}.HTTPError 11 | // @Failure 500 {object} {{.apiPackageName}}.HTTPError 12 | // @Router /{{.StructName | toLower}}{{range $field := .TableInfo.CodeFields}}{{ if $field.PrimaryKeyArgName}}/{ {{- $field.PrimaryKeyArgName -}} }{{end}}{{end}} [delete] 13 | // http DELETE "{{$.serverScheme}}://{{$.serverHost}}{{if ne $.serverPort 80}}:{{$.serverPort}}{{end}}/{{.StructName | toLower}}{{range $field := .TableInfo.CodeFields}}{{ if $field.PrimaryKeyArgName}}/{{ $field.FakeData }}{{end}}{{end}}" X-Api-User:user123 14 | func Delete{{.StructName}}(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 15 | ctx := initializeContext(r) 16 | {{range $field := .TableInfo.CodeFields}} 17 | {{ if $field.PrimaryKeyArgName }} 18 | {{$field.PrimaryKeyArgName}}, err := {{$field.PrimaryKeyFieldParser}}(ps, "{{$field.PrimaryKeyArgName}}") 19 | if err != nil { 20 | returnError(ctx, w, r, err) 21 | return 22 | } 23 | {{end}}{{end}} 24 | 25 | if err := ValidateRequest(ctx, r, "{{.TableName}}", {{.modelPackageName}}.Delete); err != nil{ 26 | returnError(ctx, w, r, err) 27 | return 28 | } 29 | 30 | rowsAffected, err := {{.daoPackageName}}.Delete{{.StructName}}(ctx,{{range $field := .TableInfo.CodeFields}} {{ if $field.PrimaryKeyArgName }} {{$field.PrimaryKeyArgName}},{{end}}{{end -}}) 31 | if err != nil { 32 | returnError(ctx, w, r, err) 33 | return 34 | } 35 | 36 | writeRowsAffected(w, rowsAffected ) 37 | } 38 | {{end}} 39 | -------------------------------------------------------------------------------- /template/api_get.go.tmpl: -------------------------------------------------------------------------------- 1 | {{define "api_get.go.tmpl"}} 2 | // Get{{.StructName}} is a function to get a single record from the {{.TableName}} table in the {{.DatabaseName}} database 3 | // @Summary Get record from table {{.StructName}} by {{range $field := .TableInfo.CodeFields}}{{ if $field.PrimaryKeyArgName}} {{ $field.PrimaryKeyArgName }} {{end}}{{end}} 4 | // @Tags {{.StructName}} 5 | {{range $field := .TableInfo.CodeFields}}{{ if $field.PrimaryKeyArgName}}// @ID {{ $field.PrimaryKeyArgName }}{{print "\n"}}{{end}}{{end}} // @Description Get{{.StructName}} is a function to get a single record from the {{.TableName}} table in the {{.DatabaseName}} database 6 | // @Accept json 7 | // @Produce json 8 | {{range $field := .TableInfo.CodeFields}}{{ if $field.PrimaryKeyArgName}}// @Param {{ $field.PrimaryKeyArgName }} path {{ $field.SQLMapping.SwaggerType }} true "{{ $field.ColumnMeta.Name }}"{{print "\n"}}{{end}}{{end}} // @Success 200 {object} {{.modelPackageName}}.{{.StructName}} 9 | // @Failure 400 {object} {{.apiPackageName}}.HTTPError 10 | // @Failure 404 {object} {{.apiPackageName}}.HTTPError "ErrNotFound, db record for id not found - returns NotFound HTTP 404 not found error" 11 | // @Router /{{.StructName | toLower}}{{range $field := .TableInfo.CodeFields}}{{ if $field.PrimaryKeyArgName}}/{ {{- $field.PrimaryKeyArgName -}} }{{end}}{{end}} [get] 12 | // http "{{$.serverScheme}}://{{$.serverHost}}{{if ne $.serverPort 80}}:{{$.serverPort}}{{end}}/{{.StructName | toLower}}{{range $field := .TableInfo.CodeFields}}{{ if $field.PrimaryKeyArgName}}/{{ $field.FakeData }}{{end}}{{end}}" X-Api-User:user123 13 | func Get{{.StructName}}(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 14 | ctx := initializeContext(r) 15 | {{range $field := .TableInfo.CodeFields}} 16 | {{ if $field.PrimaryKeyArgName }} 17 | 18 | {{$field.PrimaryKeyArgName}}, err := {{$field.PrimaryKeyFieldParser}}(ps, "{{$field.PrimaryKeyArgName}}") 19 | if err != nil { 20 | returnError(ctx, w, r, err) 21 | return 22 | } 23 | {{end}}{{end}} 24 | 25 | if err := ValidateRequest(ctx, r, "{{.TableName}}", {{.modelPackageName}}.RetrieveOne); err != nil{ 26 | returnError(ctx, w, r, err) 27 | return 28 | } 29 | 30 | record, err := {{.daoPackageName}}.Get{{.StructName}}(ctx,{{range $field := .TableInfo.CodeFields}} {{ if $field.PrimaryKeyArgName }} {{$field.PrimaryKeyArgName}},{{end}}{{end -}}) 31 | if err != nil { 32 | returnError(ctx, w, r, err) 33 | return 34 | } 35 | 36 | writeJSON(ctx, w, record) 37 | } 38 | {{end}} 39 | -------------------------------------------------------------------------------- /template/api_getall.go.tmpl: -------------------------------------------------------------------------------- 1 | {{define "api_getall.go.tmpl"}} 2 | // GetAll{{.StructName}} is a function to get a slice of record(s) from {{.TableName}} table in the {{.DatabaseName}} database 3 | // @Summary Get list of {{.StructName}} 4 | // @Tags {{.StructName}} 5 | // @Description GetAll{{.StructName}} is a handler to get a slice of record(s) from {{.TableName}} table in the {{.DatabaseName}} database 6 | // @Accept json 7 | // @Produce json 8 | // @Param page query int false "page requested (defaults to 0)" 9 | // @Param pagesize query int false "number of records in a page (defaults to 20)" 10 | // @Param order query string false "db sort order column" 11 | // @Success 200 {object} {{.apiPackageName}}.PagedResults{data=[]{{.modelPackageName}}.{{.StructName}}} 12 | // @Failure 400 {object} {{.apiPackageName}}.HTTPError 13 | // @Failure 404 {object} {{.apiPackageName}}.HTTPError 14 | // @Router /{{.StructName | toLower}} [get] 15 | // http "{{$.serverScheme}}://{{$.serverHost}}{{if ne $.serverPort 80}}:{{$.serverPort}}{{end}}/{{.StructName | toLower}}?page=0&pagesize=20" X-Api-User:user123 16 | func GetAll{{.StructName}}(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 17 | ctx := initializeContext(r) 18 | page, err := readInt(r, "page", 0) 19 | if err != nil || page < 0 { 20 | returnError(ctx, w, r, {{.daoPackageName}}.ErrBadParams) 21 | return 22 | } 23 | 24 | pagesize, err := readInt(r, "pagesize", 20) 25 | if err != nil || pagesize <= 0 { 26 | returnError(ctx, w, r, {{.daoPackageName}}.ErrBadParams) 27 | return 28 | } 29 | 30 | order := r.FormValue("order") 31 | 32 | if err := ValidateRequest(ctx, r, "{{.TableName}}", {{.modelPackageName}}.RetrieveMany); err != nil{ 33 | returnError(ctx, w, r, err) 34 | return 35 | } 36 | 37 | records, totalRows, err := {{.daoPackageName}}.GetAll{{.StructName}}(ctx, page, pagesize, order) 38 | if err != nil { 39 | returnError(ctx, w, r, err) 40 | return 41 | } 42 | 43 | result := &PagedResults{Page: page, PageSize: pagesize, Data: records, TotalRecords: totalRows} 44 | writeJSON(ctx, w, result) 45 | } 46 | {{end}} 47 | -------------------------------------------------------------------------------- /template/api_update.go.tmpl: -------------------------------------------------------------------------------- 1 | {{define "api_update.go.tmpl"}} 2 | // Update{{.StructName}} Update a single record from {{.TableName}} table in the {{.DatabaseName}} database 3 | // @Summary Update an record in table {{.TableName}} 4 | // @Description Update a single record from {{.TableName}} table in the {{.DatabaseName}} database 5 | // @Tags {{.StructName}} 6 | // @Accept json 7 | // @Produce json 8 | {{range $field := .TableInfo.CodeFields}}{{ if $field.PrimaryKeyArgName}}// @Param {{ $field.PrimaryKeyArgName }} path {{ $field.SQLMapping.SwaggerType }} true "{{ $field.ColumnMeta.Name }}"{{end}}{{end}} 9 | // @Param {{.StructName}} body {{.modelPackageName}}.{{.StructName}} true "Update {{.StructName}} record" 10 | // @Success 200 {object} {{.modelPackageName}}.{{.StructName}} 11 | // @Failure 400 {object} {{.apiPackageName}}.HTTPError 12 | // @Failure 404 {object} {{.apiPackageName}}.HTTPError 13 | // @Router /{{.StructName | toLower}}{{range $field := .TableInfo.CodeFields}}{{ if $field.PrimaryKeyArgName}}/{ {{- $field.PrimaryKeyArgName -}} }{{end}}{{end}} [put] 14 | // echo '{{ToJSON .TableInfo.Instance 0}}' | http PUT "{{$.serverScheme}}://{{$.serverHost}}{{if ne $.serverPort 80}}:{{$.serverPort}}{{end}}/{{.StructName | toLower}}{{range $field := .TableInfo.CodeFields}}{{ if $field.PrimaryKeyArgName}}/{{ $field.FakeData }}{{end}}{{end}}" X-Api-User:user123 15 | func Update{{.StructName}}(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 16 | ctx := initializeContext(r) 17 | {{range $field := .TableInfo.CodeFields}} 18 | {{ if $field.PrimaryKeyArgName }} 19 | 20 | {{$field.PrimaryKeyArgName}}, err := {{$field.PrimaryKeyFieldParser}}(ps, "{{$field.PrimaryKeyArgName}}") 21 | if err != nil { 22 | returnError(ctx, w, r, err) 23 | return 24 | } 25 | {{end}}{{end}} 26 | 27 | {{.StructName | toLower}} := &{{.modelPackageName}}.{{.StructName}}{} 28 | if err := readJSON(r, {{.StructName | toLower}}); err != nil { 29 | returnError(ctx, w, r, {{.daoPackageName}}.ErrBadParams) 30 | return 31 | } 32 | 33 | if err := {{.StructName | toLower}}.BeforeSave(); err != nil { 34 | returnError(ctx, w, r, {{.daoPackageName}}.ErrBadParams) 35 | } 36 | 37 | {{.StructName | toLower}}.Prepare() 38 | 39 | if err := {{.StructName | toLower}}.Validate( {{.modelPackageName}}.Update); err != nil { 40 | returnError(ctx, w, r, {{.daoPackageName}}.ErrBadParams) 41 | return 42 | } 43 | 44 | if err := ValidateRequest(ctx, r, "{{.TableName}}", {{.modelPackageName}}.Update); err != nil{ 45 | returnError(ctx, w, r, err) 46 | return 47 | } 48 | 49 | {{.StructName | toLower}}, _, err = {{.daoPackageName}}.Update{{.StructName}}(ctx, 50 | {{range $field := .TableInfo.CodeFields}} {{ if $field.PrimaryKeyArgName }} {{$field.PrimaryKeyArgName}},{{end}}{{end}} 51 | {{.StructName | toLower}}) 52 | if err != nil { 53 | returnError(ctx, w, r, err) 54 | return 55 | } 56 | 57 | writeJSON(ctx, w, {{.StructName | toLower}}) 58 | } 59 | {{end}} 60 | 61 | -------------------------------------------------------------------------------- /template/code_dao_gorm.md.tmpl: -------------------------------------------------------------------------------- 1 | [comment]: <> (This is a generated file please edit source in ./templates) 2 | [comment]: <> (All modification will be lost, you have been warned) 3 | [comment]: <> () 4 | 5 | ## CRUD DAO Functions 6 | `gen` will generate dao functions if the `--generate-dao` is passed to `gen`. The code can be customized with the `--dao=dao` flag to set the name of the dao package. 7 | 8 | Code can be generated in two flavours, SQLX by default and GORM with the flag `--gorm` 9 | 10 | 11 | The code generation, will generate functions for 12 | - [Retrieving records with paging](#Retrieve-Paged-Records) 13 | - [Retrieve a specific record](#Retrieve-record) 14 | - [Create a record](#Create-record) 15 | - [Update a record](#Update-record) 16 | - [Delete a record](#Delete-record) 17 | 18 | ## Retrieve Paged Records 19 | ```go 20 | {{template "dao_gorm_getall.go.tmpl" .}} 21 | ``` 22 | 23 | ## Retrieve record 24 | ```go 25 | {{template "dao_gorm_get.go.tmpl" .}} 26 | ``` 27 | 28 | ## Create record 29 | ```go 30 | {{template "dao_gorm_add.go.tmpl" .}} 31 | ``` 32 | 33 | ## Update record 34 | ```go 35 | {{template "dao_gorm_update.go.tmpl" .}} 36 | ``` 37 | 38 | ## Delete record 39 | ```go 40 | {{template "dao_gorm_delete.go.tmpl" .}} 41 | ``` 42 | -------------------------------------------------------------------------------- /template/code_dao_sqlx.md.tmpl: -------------------------------------------------------------------------------- 1 | [comment]: <> (This is a generated file please edit source in ./templates) 2 | [comment]: <> (All modification will be lost, you have been warned) 3 | [comment]: <> () 4 | 5 | ## CRUD DAO Functions 6 | `gen` will generate dao functions if the `--generate-dao` is passed to `gen`. The code can be customized with the `--dao=dao` flag to set the name of the dao package. 7 | 8 | Code can be generated in two flavours, SQLX by default and GORM with the flag `--gorm` 9 | 10 | 11 | The code generation, will generate functions for 12 | - [Retrieving records with paging](#Retrieve-Paged-Records) 13 | - [Retrieve a specific record](#Retrieve-record) 14 | - [Create a record](#Create-record) 15 | - [Update a record](#Update-record) 16 | - [Delete a record](#Delete-record) 17 | 18 | ## Retrieve Paged Records 19 | ```go 20 | {{template "dao_sqlx_getall.go.tmpl" .}} 21 | ``` 22 | 23 | ## Retrieve record 24 | ```go 25 | {{template "dao_sqlx_get.go.tmpl" .}} 26 | ``` 27 | 28 | ## Create record 29 | ```go 30 | {{template "dao_sqlx_add.go.tmpl" .}} 31 | ``` 32 | 33 | ## Update record 34 | ```go 35 | {{template "dao_sqlx_update.go.tmpl" .}} 36 | ``` 37 | 38 | ## Delete record 39 | ```go 40 | {{template "dao_sqlx_delete.go.tmpl" .}} 41 | ``` 42 | -------------------------------------------------------------------------------- /template/code_http.md.tmpl: -------------------------------------------------------------------------------- 1 | [comment]: <> (This is a generated file please edit source in ./templates) 2 | [comment]: <> (All modification will be lost, you have been warned) 3 | [comment]: <> () 4 | 5 | ## CRUD Http Handlers 6 | `gen` will generate http handlers if the `--rest` is used. The code can be customized with the `--api=api` flag to set the name of the api package. 7 | 8 | - [Retrieving records with paging](#Retrieve-Paged-Records) 9 | - [Retrieve a specific record](#Retrieve-record) 10 | - [Create a record](#Create-record) 11 | - [Update a record](#Update-record) 12 | - [Delete a record](#Delete-record) 13 | 14 | `gen` will add swagger comments to the source generated, this too can be customized with the following. 15 | ```bash 16 | --swagger_version=1.0 swagger version 17 | --swagger_path=/ swagger base path 18 | --swagger_tos= swagger tos url 19 | --swagger_contact_name=Me swagger contact name 20 | --swagger_contact_url=http://me.com/terms.html swagger contact url 21 | --swagger_contact_email=me@me.com swagger contact email 22 | ``` 23 | 24 | 25 | ## Retrieve Paged Records 26 | ```go 27 | {{template "api_getall.go.tmpl" .}} 28 | ``` 29 | 30 | ## Retrieve record 31 | ```go 32 | {{template "api_get.go.tmpl" .}} 33 | ``` 34 | 35 | ## Create record 36 | ```go 37 | {{template "api_add.go.tmpl" .}} 38 | ``` 39 | 40 | ## Update record 41 | ```go 42 | {{template "api_update.go.tmpl" .}} 43 | ``` 44 | 45 | ## Delete record 46 | ```go 47 | {{template "api_delete.go.tmpl" .}} 48 | ``` 49 | -------------------------------------------------------------------------------- /template/dao_gorm.go.tmpl: -------------------------------------------------------------------------------- 1 | package {{.daoPackageName}} 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "{{.modelFQPN}}" 8 | 9 | {{if .UseGuregu}} "github.com/guregu/null" {{end}} 10 | "github.com/google/uuid" 11 | ) 12 | 13 | var ( 14 | _ = time.Second 15 | {{if .UseGuregu}} _ = null.Bool{} {{end}} 16 | _ = uuid.UUID{} 17 | ) 18 | 19 | 20 | 21 | {{template "dao_gorm_getall.go.tmpl" .}} 22 | {{template "dao_gorm_get.go.tmpl" .}} 23 | {{template "dao_gorm_add.go.tmpl" .}} 24 | {{template "dao_gorm_update.go.tmpl" .}} 25 | {{template "dao_gorm_delete.go.tmpl" .}} 26 | 27 | -------------------------------------------------------------------------------- /template/dao_gorm_add.go.tmpl: -------------------------------------------------------------------------------- 1 | {{define "dao_gorm_add.go.tmpl"}} 2 | // Add{{.StructName}} is a function to add a single record to {{.TableName}} table in the {{.DatabaseName}} database 3 | // error - ErrInsertFailed, db save call failed 4 | func Add{{.StructName}}(ctx context.Context, record *{{.modelPackageName}}.{{.StructName}}) (result *{{.modelPackageName}}.{{.StructName}}, RowsAffected int64, err error) { 5 | db := DB.Create(record) 6 | if err = db.Error; err != nil { 7 | return nil, -1, ErrInsertFailed 8 | } 9 | 10 | return record, db.RowsAffected, nil 11 | } 12 | {{end}} 13 | -------------------------------------------------------------------------------- /template/dao_gorm_delete.go.tmpl: -------------------------------------------------------------------------------- 1 | {{define "dao_gorm_delete.go.tmpl"}} 2 | // Delete{{.StructName}} is a function to delete a single record from {{.TableName}} table in the {{.DatabaseName}} database 3 | // error - ErrNotFound, db Find error 4 | // error - ErrDeleteFailed, db Delete failed error 5 | func Delete{{.StructName}}(ctx context.Context,{{range $field := .TableInfo.CodeFields}} {{ if $field.PrimaryKeyArgName }} {{$field.PrimaryKeyArgName}} {{$field.GoFieldType}},{{end}}{{end -}}) (rowsAffected int64, err error) { 6 | 7 | record := &{{.modelPackageName}}.{{.StructName}}{} 8 | db := DB.First(record, {{range $field := .TableInfo.CodeFields}} {{ if $field.PrimaryKeyArgName }} "{{$field.ColumnMeta.Name}} = ?",{{$field.PrimaryKeyArgName}},{{end}}{{end}}) 9 | if db.Error != nil { 10 | return -1, ErrNotFound 11 | } 12 | 13 | db = db.Delete(record) 14 | if err = db.Error; err != nil { 15 | return -1, ErrDeleteFailed 16 | } 17 | 18 | return db.RowsAffected, nil 19 | } 20 | {{end}} 21 | -------------------------------------------------------------------------------- /template/dao_gorm_get.go.tmpl: -------------------------------------------------------------------------------- 1 | {{define "dao_gorm_get.go.tmpl"}} 2 | // Get{{.StructName}} is a function to get a single record from the {{.TableName}} table in the {{.DatabaseName}} database 3 | // error - ErrNotFound, db Find error 4 | func Get{{.StructName}}(ctx context.Context,{{range $field := .TableInfo.CodeFields}} {{ if $field.PrimaryKeyArgName }} {{$field.PrimaryKeyArgName}} {{$field.GoFieldType}},{{end}}{{end -}}) (record *{{.modelPackageName}}.{{.StructName}}, err error) { 5 | record = &{{.modelPackageName}}.{{.StructName}}{} 6 | if err = DB.First(record, {{range $field := .TableInfo.CodeFields}} {{ if $field.PrimaryKeyArgName }} {{$field.PrimaryKeyArgName}},{{end}}{{end -}}).Error; err != nil { 7 | err = ErrNotFound 8 | return record, err 9 | } 10 | 11 | return record, nil 12 | } 13 | {{end}} 14 | -------------------------------------------------------------------------------- /template/dao_gorm_getall.go.tmpl: -------------------------------------------------------------------------------- 1 | {{define "dao_gorm_getall.go.tmpl"}} 2 | // GetAll{{.StructName}} is a function to get a slice of record(s) from {{.TableName}} table in the {{.DatabaseName}} database 3 | // params - page - page requested (defaults to 0) 4 | // params - pagesize - number of records in a page (defaults to 20) 5 | // params - order - db sort order column 6 | // error - ErrNotFound, db Find error 7 | func GetAll{{.StructName}}(ctx context.Context, page, pagesize int, order string) (results []*{{.modelPackageName}}.{{.StructName}}, totalRows int64, err error) { 8 | 9 | resultOrm := DB.Model(&{{.modelPackageName}}.{{.StructName}}{}) 10 | resultOrm.Count(&totalRows) 11 | 12 | if page > 0 { 13 | offset := (page - 1) * pagesize 14 | resultOrm = resultOrm.Offset(offset).Limit(pagesize) 15 | } else { 16 | resultOrm = resultOrm.Limit(pagesize) 17 | } 18 | 19 | if order != "" { 20 | resultOrm = resultOrm.Order(order) 21 | } 22 | 23 | if err = resultOrm.Find(&results).Error; err != nil { 24 | err = ErrNotFound 25 | return nil, -1, err 26 | } 27 | 28 | return results, totalRows, nil 29 | } 30 | {{end}} 31 | -------------------------------------------------------------------------------- /template/dao_gorm_init.go.tmpl: -------------------------------------------------------------------------------- 1 | package {{.daoPackageName}} 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "reflect" 8 | 9 | "gorm.io/gorm" 10 | ) 11 | 12 | // BuildInfo is used to define the application build info, and inject values into via the build process. 13 | type BuildInfo struct { 14 | 15 | // BuildDate date string of when build was performed filled in by -X compile flag 16 | BuildDate string 17 | 18 | // LatestCommit date string of when build was performed filled in by -X compile flag 19 | LatestCommit string 20 | 21 | // BuildNumber date string of when build was performed filled in by -X compile flag 22 | BuildNumber string 23 | 24 | // BuiltOnIP date string of when build was performed filled in by -X compile flag 25 | BuiltOnIP string 26 | 27 | // BuiltOnOs date string of when build was performed filled in by -X compile flag 28 | BuiltOnOs string 29 | 30 | // RuntimeVer date string of when build was performed filled in by -X compile flag 31 | RuntimeVer string 32 | } 33 | 34 | type LogSql func(ctx context.Context, sql string) 35 | 36 | var ( 37 | // ErrNotFound error when record not found 38 | ErrNotFound = fmt.Errorf("record Not Found") 39 | 40 | // ErrUnableToMarshalJSON error when json payload corrupt 41 | ErrUnableToMarshalJSON = fmt.Errorf("json payload corrupt") 42 | 43 | // ErrUpdateFailed error when update fails 44 | ErrUpdateFailed = fmt.Errorf("db update error") 45 | 46 | // ErrInsertFailed error when insert fails 47 | ErrInsertFailed = fmt.Errorf("db insert error") 48 | 49 | // ErrDeleteFailed error when delete fails 50 | ErrDeleteFailed = fmt.Errorf("db delete error") 51 | 52 | // ErrBadParams error when bad params passed in 53 | ErrBadParams = fmt.Errorf("bad params error") 54 | 55 | // DB reference to database 56 | DB *gorm.DB 57 | 58 | // AppBuildInfo reference to build info 59 | AppBuildInfo *BuildInfo 60 | 61 | // Logger function that will be invoked before executing sql 62 | Logger LogSql 63 | ) 64 | 65 | 66 | 67 | // Copy a src struct into a destination struct 68 | func Copy(dst interface{}, src interface{}) error { 69 | dstV := reflect.Indirect(reflect.ValueOf(dst)) 70 | srcV := reflect.Indirect(reflect.ValueOf(src)) 71 | 72 | if !dstV.CanAddr() { 73 | return errors.New("copy to value is unaddressable") 74 | } 75 | 76 | if srcV.Type() != dstV.Type() { 77 | return errors.New("different types can not be copied") 78 | } 79 | 80 | for i := 0; i < dstV.NumField(); i++ { 81 | f := srcV.Field(i) 82 | if !isZeroOfUnderlyingType(f.Interface()) { 83 | dstV.Field(i).Set(f) 84 | } 85 | } 86 | 87 | return nil 88 | } 89 | 90 | 91 | func isZeroOfUnderlyingType(x interface{}) bool { 92 | return x == nil || reflect.DeepEqual(x, reflect.Zero(reflect.TypeOf(x)).Interface()) 93 | } 94 | -------------------------------------------------------------------------------- /template/dao_gorm_update.go.tmpl: -------------------------------------------------------------------------------- 1 | {{define "dao_gorm_update.go.tmpl"}} 2 | // Update{{.StructName}} is a function to update a single record from {{.TableName}} table in the {{.DatabaseName}} database 3 | // error - ErrNotFound, db record for id not found 4 | // error - ErrUpdateFailed, db meta data copy failed or db.Save call failed 5 | func Update{{.StructName}}(ctx context.Context, {{range $field := .TableInfo.CodeFields}} {{ if $field.PrimaryKeyArgName }} {{$field.PrimaryKeyArgName}} {{$field.GoFieldType}},{{end}}{{end -}}updated *{{.modelPackageName}}.{{.StructName}}) (result *{{.modelPackageName}}.{{.StructName}}, RowsAffected int64, err error) { 6 | 7 | result = &{{.modelPackageName}}.{{.StructName}}{} 8 | db := DB.First(result,{{range $field := .TableInfo.CodeFields}} {{ if $field.PrimaryKeyArgName }} "{{$field.ColumnMeta.Name}} = ?",{{$field.PrimaryKeyArgName}},{{end}}{{end -}}) 9 | if err = db.Error; err != nil { 10 | return nil, -1, ErrNotFound 11 | } 12 | 13 | if err = Copy(result, updated); err != nil { 14 | return nil, -1, ErrUpdateFailed 15 | } 16 | 17 | db = db.Save(result) 18 | if err = db.Error; err != nil { 19 | return nil, -1, ErrUpdateFailed 20 | } 21 | 22 | return result, db.RowsAffected, nil 23 | } 24 | {{end}} 25 | -------------------------------------------------------------------------------- /template/dao_sqlx.go.tmpl: -------------------------------------------------------------------------------- 1 | package {{.daoPackageName}} 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "{{.modelFQPN}}" 10 | 11 | {{if .UseGuregu}} "github.com/guregu/null" {{end}} 12 | "github.com/google/uuid" 13 | ) 14 | 15 | var ( 16 | _ = time.Second 17 | {{if .UseGuregu}} _ = null.Bool{} {{end}} 18 | _ = uuid.UUID{} 19 | ) 20 | 21 | 22 | /* 23 | {{ $ddl := .TableInfo.DBMeta.DDL }} 24 | {{if $ddl }} 25 | DB Table Details 26 | ------------------------------------- 27 | {{$ddl}} 28 | {{- end}} 29 | 30 | 31 | PrimaryKeyNamesList : {{.PrimaryKeyNamesList}} 32 | PrimaryKeysJoined : {{.PrimaryKeysJoined}} 33 | NonPrimaryKeyNamesList : {{.NonPrimaryKeyNamesList}} 34 | NonPrimaryKeysJoined : {{.NonPrimaryKeysJoined}} 35 | delSql : {{.delSql}} 36 | updateSql : {{.updateSql}} 37 | insertSql : {{.insertSql}} 38 | selectOneSql : {{.selectOneSql}} 39 | selectMultiSql : {{.selectMultiSql}} 40 | 41 | 42 | */ 43 | 44 | 45 | {{template "dao_sqlx_getall.go.tmpl" .}} 46 | {{template "dao_sqlx_get.go.tmpl" .}} 47 | {{template "dao_sqlx_add.go.tmpl" .}} 48 | {{template "dao_sqlx_update.go.tmpl" .}} 49 | {{template "dao_sqlx_delete.go.tmpl" .}} 50 | 51 | 52 | -------------------------------------------------------------------------------- /template/dao_sqlx_add.go.tmpl: -------------------------------------------------------------------------------- 1 | {{define "dao_sqlx_add.go.tmpl"}} 2 | // Add{{.StructName}} is a function to add a single record to {{.TableName}} table in the {{.DatabaseName}} database 3 | // error - ErrInsertFailed, db save call failed 4 | func Add{{.StructName}}(ctx context.Context, record *{{.modelPackageName}}.{{.StructName}}) (result *{{.modelPackageName}}.{{.StructName}}, RowsAffected int64, err error) { 5 | if DB.DriverName() == "postgres" { 6 | return add{{.StructName}}Postgres( ctx, record) 7 | } else { 8 | return add{{.StructName}}( ctx, record) 9 | } 10 | } 11 | 12 | // add{{.StructName}}Postgres is a function to add a single record to {{.TableName}} table in the {{.DatabaseName}} database 13 | // error - ErrInsertFailed, db save call failed 14 | func add{{.StructName}}Postgres(ctx context.Context, record *{{.modelPackageName}}.{{.StructName}}) (result *{{.modelPackageName}}.{{.StructName}}, RowsAffected int64, err error) { 15 | sql := "{{.insertSql}}" 16 | sql = DB.Rebind(sql) 17 | 18 | if Logger != nil { 19 | Logger(ctx, sql) 20 | } 21 | 22 | rows := int64(1) 23 | sql = fmt.Sprintf("%s returning %s", sql, "{{.PrimaryKeysJoined}}") 24 | dbResult := DB.QueryRowContext(ctx, sql, {{range $field := .TableInfo.CodeFields}} {{ if not $field.ColumnMeta.IsAutoIncrement }} record.{{$field.GoFieldName}},{{end}}{{end -}} ) 25 | err = dbResult.Scan({{range $field := .TableInfo.CodeFields}} {{ if not $field.ColumnMeta.IsAutoIncrement }} record.{{$field.GoFieldName}},{{end}}{{end -}}) 26 | 27 | return record, rows, err 28 | } 29 | 30 | // add{{.StructName}}Postgres is a function to add a single record to {{.TableName}} table in the {{.DatabaseName}} database 31 | // error - ErrInsertFailed, db save call failed 32 | func add{{.StructName}}(ctx context.Context, record *{{.modelPackageName}}.{{.StructName}}) (result *{{.modelPackageName}}.{{.StructName}}, RowsAffected int64, err error) { 33 | sql := "{{.insertSql}}" 34 | sql = DB.Rebind(sql) 35 | 36 | if Logger != nil { 37 | Logger(ctx, sql) 38 | } 39 | 40 | rows := int64(0) 41 | 42 | dbResult, err := DB.ExecContext(ctx, sql, {{range $field := .TableInfo.CodeFields}} {{ if not $field.ColumnMeta.IsAutoIncrement }} record.{{$field.GoFieldName}},{{end}}{{end -}} ) 43 | if err != nil { 44 | return nil, 0, err 45 | } 46 | 47 | id, err := dbResult.LastInsertId() 48 | rows, err = dbResult.RowsAffected() 49 | 50 | {{range $field := .TableInfo.CodeFields}}{{ if $field.PrimaryKeyArgName }} record.{{$field.GoFieldName}} = {{$field.GoFieldType}}(id){{print "\n"}}{{end}}{{end}} 51 | 52 | return record, rows, err 53 | } 54 | {{end}} 55 | -------------------------------------------------------------------------------- /template/dao_sqlx_delete.go.tmpl: -------------------------------------------------------------------------------- 1 | {{define "dao_sqlx_delete.go.tmpl"}} 2 | // Delete{{.StructName}} is a function to delete a single record from {{.TableName}} table in the {{.DatabaseName}} database 3 | // error - ErrNotFound, db Find error 4 | // error - ErrDeleteFailed, db Delete failed error 5 | func Delete{{.StructName}}(ctx context.Context,{{range $field := .TableInfo.CodeFields}} {{ if $field.PrimaryKeyArgName }} {{$field.PrimaryKeyArgName}} {{$field.GoFieldType}},{{end}}{{end -}}) (rowsAffected int64, err error) { 6 | sql := "{{.delSql}}" 7 | sql = DB.Rebind(sql) 8 | 9 | if Logger != nil { 10 | Logger(ctx, sql) 11 | } 12 | 13 | result, err := DB.ExecContext(ctx, sql, {{range $field := .TableInfo.CodeFields}} {{ if $field.PrimaryKeyArgName }} {{$field.PrimaryKeyArgName}},{{end}}{{end -}} ) 14 | if err != nil { 15 | return 0, err 16 | } 17 | 18 | return result.RowsAffected() 19 | } 20 | {{end}} 21 | -------------------------------------------------------------------------------- /template/dao_sqlx_get.go.tmpl: -------------------------------------------------------------------------------- 1 | {{define "dao_sqlx_get.go.tmpl"}} 2 | // Get{{.StructName}} is a function to get a single record from the {{.TableName}} table in the {{.DatabaseName}} database 3 | // error - ErrNotFound, db Find error 4 | func Get{{.StructName}}(ctx context.Context,{{range $field := .TableInfo.CodeFields}} {{ if $field.PrimaryKeyArgName }} {{$field.PrimaryKeyArgName}} {{$field.GoFieldType}},{{end}}{{end -}}) (record *{{.modelPackageName}}.{{.StructName}}, err error) { 5 | sql := "{{.selectOneSql}}" 6 | sql = DB.Rebind(sql) 7 | 8 | if Logger != nil { 9 | Logger(ctx, sql) 10 | } 11 | 12 | record = &{{.modelPackageName}}.{{.StructName}}{} 13 | err = DB.GetContext(ctx, record, sql, {{range $field := .TableInfo.CodeFields}} {{ if $field.PrimaryKeyArgName }} {{$field.PrimaryKeyArgName}},{{end}}{{end -}}) 14 | if err != nil { 15 | return nil, err 16 | } 17 | return record, nil 18 | } 19 | {{end}} 20 | -------------------------------------------------------------------------------- /template/dao_sqlx_getall.go.tmpl: -------------------------------------------------------------------------------- 1 | {{define "dao_sqlx_getall.go.tmpl"}} 2 | // GetAll{{.StructName}} is a function to get a slice of record(s) from {{.TableName}} table in the {{.DatabaseName}} database 3 | // params - page - page requested (defaults to 0) 4 | // params - pagesize - number of records in a page (defaults to 20) 5 | // params - order - db sort order column 6 | // error - ErrNotFound, db Find error 7 | func GetAll{{.StructName}}(ctx context.Context, page, pagesize int64, order string) (results []*{{.modelPackageName}}.{{.StructName}}, totalRows int, err error) { 8 | sql := "{{.selectMultiSql}}" 9 | 10 | if order != "" { 11 | if strings.ContainsAny(order, "'\"") { 12 | order = "" 13 | } 14 | } 15 | 16 | if order == "" { 17 | order = "{{.PrimaryKeysJoined}}" 18 | } 19 | 20 | if DB.DriverName() == "mssql" { 21 | sql = fmt.Sprintf("%s order by %s OFFSET %d ROWS FETCH FIRST %d ROWS ONLY", sql, order, page, pagesize) 22 | } else if DB.DriverName() == "postgres" { 23 | sql = fmt.Sprintf("%s order by `%s` OFFSET %d LIMIT %d", sql, order, page, pagesize) 24 | } else { 25 | sql = fmt.Sprintf("%s order by `%s` LIMIT %d, %d", sql, order, page, pagesize) 26 | } 27 | sql = DB.Rebind(sql) 28 | 29 | if Logger != nil { 30 | Logger(ctx, sql) 31 | } 32 | 33 | err = DB.SelectContext(ctx, &results, sql) 34 | if err != nil { 35 | return nil, -1, err 36 | } 37 | 38 | cnt , err := GetRowCount(ctx, "{{.TableName}}") 39 | if err != nil { 40 | return results, -2, err 41 | } 42 | 43 | return results, cnt, err 44 | } 45 | 46 | {{end}} 47 | -------------------------------------------------------------------------------- /template/dao_sqlx_init.go.tmpl: -------------------------------------------------------------------------------- 1 | package {{.daoPackageName}} 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "reflect" 8 | 9 | "github.com/jmoiron/sqlx" 10 | ) 11 | 12 | // BuildInfo is used to define the application build info, and inject values into via the build process. 13 | type BuildInfo struct { 14 | 15 | // BuildDate date string of when build was performed filled in by -X compile flag 16 | BuildDate string 17 | 18 | // LatestCommit date string of when build was performed filled in by -X compile flag 19 | LatestCommit string 20 | 21 | // BuildNumber date string of when build was performed filled in by -X compile flag 22 | BuildNumber string 23 | 24 | // BuiltOnIP date string of when build was performed filled in by -X compile flag 25 | BuiltOnIP string 26 | 27 | // BuiltOnOs date string of when build was performed filled in by -X compile flag 28 | BuiltOnOs string 29 | 30 | // RuntimeVer date string of when build was performed filled in by -X compile flag 31 | RuntimeVer string 32 | } 33 | 34 | type LogSql func(ctx context.Context, sql string) 35 | 36 | var ( 37 | // ErrNotFound error when record not found 38 | ErrNotFound = fmt.Errorf("record Not Found") 39 | 40 | // ErrUnableToMarshalJSON error when json payload corrupt 41 | ErrUnableToMarshalJSON = fmt.Errorf("json payload corrupt") 42 | 43 | // ErrUpdateFailed error when update fails 44 | ErrUpdateFailed = fmt.Errorf("db update error") 45 | 46 | // ErrInsertFailed error when insert fails 47 | ErrInsertFailed = fmt.Errorf("db insert error") 48 | 49 | // ErrDeleteFailed error when delete fails 50 | ErrDeleteFailed = fmt.Errorf("db delete error") 51 | 52 | // ErrBadParams error when bad params passed in 53 | ErrBadParams = fmt.Errorf("bad params error") 54 | 55 | // DB reference to database 56 | DB *sqlx.DB 57 | 58 | // AppBuildInfo reference to build info 59 | AppBuildInfo *BuildInfo 60 | 61 | // Logger function that will be invoked before executing sql 62 | Logger LogSql 63 | ) 64 | 65 | 66 | 67 | // Copy a src struct into a destination struct 68 | func Copy(dst interface{}, src interface{}) error { 69 | dstV := reflect.Indirect(reflect.ValueOf(dst)) 70 | srcV := reflect.Indirect(reflect.ValueOf(src)) 71 | 72 | if !dstV.CanAddr() { 73 | return errors.New("copy to value is unaddressable") 74 | } 75 | 76 | if srcV.Type() != dstV.Type() { 77 | return errors.New("different types can not be copied") 78 | } 79 | 80 | for i := 0; i < dstV.NumField(); i++ { 81 | f := srcV.Field(i) 82 | if !isZeroOfUnderlyingType(f.Interface()) { 83 | dstV.Field(i).Set(f) 84 | } 85 | } 86 | 87 | return nil 88 | } 89 | 90 | 91 | func isZeroOfUnderlyingType(x interface{}) bool { 92 | return x == nil || reflect.DeepEqual(x, reflect.Zero(reflect.TypeOf(x)).Interface()) 93 | } 94 | 95 | func GetRowCount(ctx context.Context, tableName string) (int, error) { 96 | sql := fmt.Sprintf("SELECT count(*) FROM %s", tableName) 97 | if Logger != nil { 98 | Logger(ctx, sql) 99 | } 100 | 101 | cnt := 0 102 | row := DB.QueryRowContext(ctx, sql) 103 | err := row.Scan(&cnt) 104 | if err != nil { 105 | return -1, err 106 | } 107 | 108 | return cnt, err 109 | } 110 | -------------------------------------------------------------------------------- /template/dao_sqlx_update.go.tmpl: -------------------------------------------------------------------------------- 1 | {{define "dao_sqlx_update.go.tmpl"}} 2 | // Update{{.StructName}} is a function to update a single record from {{.TableName}} table in the {{.DatabaseName}} database 3 | // error - ErrNotFound, db record for id not found 4 | // error - ErrUpdateFailed, db meta data copy failed or db.Save call failed 5 | func Update{{.StructName}}(ctx context.Context, {{range $field := .TableInfo.CodeFields}} {{ if $field.PrimaryKeyArgName }} {{$field.PrimaryKeyArgName}} {{$field.GoFieldType}},{{end}}{{end -}}updated *{{.modelPackageName}}.{{.StructName}}) (result *{{.modelPackageName}}.{{.StructName}}, RowsAffected int64, err error) { 6 | sql := "{{.updateSql}}" 7 | sql = DB.Rebind(sql) 8 | 9 | if Logger != nil { 10 | Logger(ctx, sql) 11 | } 12 | 13 | dbResult, err := DB.ExecContext(ctx, sql, {{range $field := .TableInfo.CodeFields}} {{ if not $field.PrimaryKeyArgName }} updated.{{$field.GoFieldName}},{{end}}{{end -}} {{range $field := .TableInfo.CodeFields}} {{ if $field.PrimaryKeyArgName }} {{$field.PrimaryKeyArgName}},{{end}}{{end -}}) 14 | if err != nil { 15 | return nil, 0, err 16 | } 17 | 18 | rows, err := dbResult.RowsAffected() 19 | {{range $field := .TableInfo.CodeFields}} {{ if $field.PrimaryKeyArgName }} updated.{{$field.GoFieldName}} = {{$field.PrimaryKeyArgName}}{{print "\n"}}{{end}}{{end}} 20 | return updated, rows, err 21 | } 22 | {{end}} 23 | -------------------------------------------------------------------------------- /template/debug.txt: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | {{ range $key, $value := . }} 4 | {{ printf "%#-20v" $key }} {{ printf "[%T] %#v" $value $value }}{{ end }} 5 | 6 | table {{.DatabaseName}}.{{.TableName}} { 7 | {{ range $i, $value := .DBColumns }} 8 | {{ printf " [%-02d] %-20s %-20s" $i $value.Name $value.DatabaseTypeName }}{{ end }} 9 | } 10 | */ 11 | -------------------------------------------------------------------------------- /template/gitignore.tmpl: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Go template 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | go.sum 10 | 11 | # Test binary, built with: go test -c 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | bin/ 21 | .idea/ 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /template/gomod.tmpl: -------------------------------------------------------------------------------- 1 | module {{.module}} 2 | 3 | go 1.14 4 | 5 | require ( 6 | cloud.google.com/go v0.37.4 // indirect 7 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 8 | github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc 9 | github.com/gin-gonic/gin v1.6.2 10 | github.com/go-openapi/spec v0.19.7 // indirect 11 | github.com/go-openapi/swag v0.19.9 // indirect 12 | github.com/go-sql-driver/mysql v1.4.1 13 | github.com/gogo/protobuf v1.3.1 14 | github.com/golang/protobuf v1.4.0 // indirect 15 | github.com/grpc-ecosystem/go-grpc-middleware v1.2.0 16 | github.com/guregu/null v3.4.0+incompatible 17 | github.com/jmoiron/sqlx v1.2.0 18 | github.com/julienschmidt/httprouter v1.3.0 19 | github.com/kr/pretty v0.2.0 // indirect 20 | github.com/lib/pq v1.3.0 21 | github.com/mailru/easyjson v0.7.1 // indirect 22 | github.com/mattn/go-sqlite3 v2.0.2+incompatible 23 | github.com/google/uuid v1.3.0 24 | github.com/sirupsen/logrus v1.4.2 25 | github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 26 | github.com/swaggo/gin-swagger v1.2.0 27 | github.com/swaggo/swag v1.6.5 28 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd // indirect 29 | golang.org/x/net v0.0.0-20200421231249-e086a090c8fd // indirect 30 | golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f // indirect 31 | golang.org/x/tools v0.0.0-20200424195722-358506031216 // indirect 32 | google.golang.org/appengine v1.6.5 // indirect 33 | google.golang.org/grpc v1.19.0 34 | gopkg.in/yaml.v2 v2.2.8 35 | ) 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /template/http_utils.go.tmpl: -------------------------------------------------------------------------------- 1 | package {{.apiPackageName}} 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | // HTTPNocacheContent will set the headers for content type along with no caching. 13 | func HTTPNocacheContent(w http.ResponseWriter, content string) { 14 | w.Header().Set("Content-Type", content) 15 | w.Header().Set("Cache-Control", "no-cache, no-store") 16 | w.Header().Set("Pragma", "no-cache") 17 | w.Header().Set("Expires", "0") 18 | } 19 | 20 | // HTTPNocacheJSON will set the headers on an http Response for a text/json content type along with no cache. 21 | func HTTPNocacheJSON(w http.ResponseWriter) { 22 | HTTPNocacheContent(w, "text/json") 23 | } 24 | 25 | // SendJSON will return take a value and serialize it to json and return the http response. 26 | func SendJSON(w http.ResponseWriter, r *http.Request, code int, val interface{}) { 27 | w.Header().Set("Cache-Control", "no-cache, no-store") 28 | w.Header().Set("Pragma", "no-cache") 29 | w.Header().Set("Expires", "0") 30 | 31 | w.Header().Set("Content-Type", "text/plain; charset=utf-8") 32 | w.Header().Set("X-Content-Type-Options", "nosniff") 33 | w.WriteHeader(code) 34 | 35 | bytes, err := json.Marshal(val) 36 | if err != nil { 37 | InternalServerError(w, r, err) 38 | return 39 | } 40 | 41 | _, _ = w.Write(bytes) 42 | } 43 | 44 | // InternalServerError will return an error to the client, sending 500 error code to the client with generic string 45 | func InternalServerError(w http.ResponseWriter, r *http.Request, err error) { 46 | w.Header().Set("Cache-Control", "no-cache, no-store") 47 | w.Header().Set("Pragma", "no-cache") 48 | w.Header().Set("Expires", "0") 49 | http.Error(w, "Internal server error", 500) 50 | } 51 | 52 | // AddHeadersHandler will take a map of string/string and use it to set the key and value as the header name and value respectively. 53 | func AddHeadersHandler(addHeaders map[string]string, h http.Handler) http.Handler { 54 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 55 | 56 | for key, value := range addHeaders { 57 | w.Header().Set(key, value) 58 | } 59 | 60 | h.ServeHTTP(w, r) 61 | }) 62 | } 63 | 64 | // ipRange - a structure that holds the start and end of a range of ip addresses 65 | type ipRange struct { 66 | start net.IP 67 | end net.IP 68 | } 69 | 70 | // inRange - check to see if a given ip address is within a range given 71 | func inRange(r ipRange, ipAddress net.IP) bool { 72 | // strcmp type byte comparison 73 | if bytes.Compare(ipAddress, r.start) >= 0 && bytes.Compare(ipAddress, r.end) < 0 { 74 | return true 75 | } 76 | return false 77 | } 78 | 79 | var privateRanges = []ipRange{ 80 | { 81 | start: net.ParseIP("10.0.0.0"), 82 | end: net.ParseIP("10.255.255.255"), 83 | }, 84 | { 85 | start: net.ParseIP("100.64.0.0"), 86 | end: net.ParseIP("100.127.255.255"), 87 | }, 88 | { 89 | start: net.ParseIP("172.16.0.0"), 90 | end: net.ParseIP("172.31.255.255"), 91 | }, 92 | { 93 | start: net.ParseIP("192.0.0.0"), 94 | end: net.ParseIP("192.0.0.255"), 95 | }, 96 | { 97 | start: net.ParseIP("192.168.0.0"), 98 | end: net.ParseIP("192.168.255.255"), 99 | }, 100 | { 101 | start: net.ParseIP("198.18.0.0"), 102 | end: net.ParseIP("198.19.255.255"), 103 | }, 104 | } 105 | 106 | // IsPrivateSubnet - check to see if this ip is in a private subnet 107 | func IsPrivateSubnet(ipAddress net.IP) bool { 108 | // my use case is only concerned with ipv4 atm 109 | if ipCheck := ipAddress.To4(); ipCheck != nil { 110 | // iterate over all our ranges 111 | for _, r := range privateRanges { 112 | // check if this ip is in a private range 113 | if inRange(r, ipAddress) { 114 | return true 115 | } 116 | } 117 | } 118 | return false 119 | } 120 | 121 | // GetIPAddress will take a http request and check headers if it has been proxied to extract what the server believes to be the client ip address. 122 | func GetIPAddress(r *http.Request) string { 123 | ip := "" 124 | for _, h := range []string{"X-Forwarded-For", "X-Real-Ip"} { 125 | addresses := strings.Split(r.Header.Get(h), ",") 126 | // march from right to left until we get a public address 127 | // that will be the address right before our proxy. 128 | for i := len(addresses) - 1; i >= 0; i-- { 129 | ip = strings.TrimSpace(addresses[i]) 130 | // header can contain spaces too, strip those out. 131 | realIP := net.ParseIP(ip) 132 | if !realIP.IsGlobalUnicast() || IsPrivateSubnet(realIP) { 133 | // bad address, go to next 134 | continue 135 | } 136 | return ip 137 | } 138 | } 139 | 140 | ip, _, _ = net.SplitHostPort(r.RemoteAddr) 141 | return ip 142 | } 143 | 144 | 145 | // FormatRequest generates ascii representation of a request 146 | func FormatRequest(r *http.Request) string { 147 | // Create return string 148 | var request []string 149 | // Add the request string 150 | url := fmt.Sprintf("%v %v %v", r.Method, r.URL, r.Proto) 151 | request = append(request, url) 152 | // Add the host 153 | request = append(request, fmt.Sprintf("Host: %v", r.Host)) 154 | // Loop through headers 155 | for name, headers := range r.Header { 156 | for _, h := range headers { 157 | request = append(request, fmt.Sprintf("%v: %v", name, h)) 158 | } 159 | } 160 | 161 | // If this is a POST, add post data 162 | if r.Method == "POST" { 163 | r.ParseForm() 164 | request = append(request, "\n") 165 | request = append(request, r.Form.Encode()) 166 | } 167 | // Return the request as a string 168 | return strings.Join(request, "\n") 169 | } 170 | -------------------------------------------------------------------------------- /template/main_gorm.go.tmpl: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | 11 | _ "github.com/jinzhu/gorm/dialects/mysql" 12 | _ "github.com/jinzhu/gorm/dialects/sqlite" 13 | _ "github.com/jinzhu/gorm/dialects/postgres" 14 | _ "github.com/jinzhu/gorm/dialects/mssql" 15 | 16 | "github.com/gin-gonic/gin" 17 | "github.com/jinzhu/gorm" 18 | "github.com/swaggo/files" // swagger embed files 19 | "github.com/swaggo/gin-swagger" // gin-swagger middleware 20 | "github.com/droundy/goopt" 21 | 22 | "{{.module}}/{{.apiPackageName}}" 23 | "{{.module}}/{{.daoPackageName}}" 24 | _ "{{.module}}/docs" 25 | "{{.module}}/{{.modelPackageName}}" 26 | ) 27 | 28 | var ( 29 | // BuildDate date string of when build was performed filled in by -X compile flag 30 | BuildDate string 31 | 32 | // LatestCommit date string of when build was performed filled in by -X compile flag 33 | LatestCommit string 34 | 35 | // BuildNumber date string of when build was performed filled in by -X compile flag 36 | BuildNumber string 37 | 38 | // BuiltOnIP date string of when build was performed filled in by -X compile flag 39 | BuiltOnIP string 40 | 41 | // BuiltOnOs date string of when build was performed filled in by -X compile flag 42 | BuiltOnOs string 43 | 44 | // RuntimeVer date string of when build was performed filled in by -X compile flag 45 | RuntimeVer string 46 | 47 | // OsSignal signal used to shutdown 48 | OsSignal chan os.Signal 49 | ) 50 | 51 | // GinServer launch gin server 52 | func GinServer() (err error){ 53 | url := ginSwagger.URL("{{$.serverScheme}}://{{.serverHost}}{{if ne $.serverPort 80}}:{{$.serverPort}}{{end}}/swagger/doc.json") // The url pointing to API definition 54 | 55 | router := gin.Default() 56 | router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url)) 57 | 58 | {{.apiPackageName}}.ConfigGinRouter(router) 59 | router.Run("{{.serverListen}}") 60 | if err != nil { 61 | log.Fatalf("Error starting server, the error is '%v'", err) 62 | } 63 | 64 | return 65 | } 66 | 67 | 68 | 69 | // @title {{.SwaggerInfo.Title}} 70 | // @version {{.SwaggerInfo.Version}} 71 | // @description {{.SwaggerInfo.Description}} 72 | // @termsOfService {{.SwaggerInfo.TOS}} 73 | 74 | // @contact.name {{.SwaggerInfo.ContactName}} 75 | // @contact.url {{.SwaggerInfo.ContactURL}} 76 | // @contact.email {{.SwaggerInfo.ContactEmail}} 77 | 78 | // @license.name Apache 2.0 79 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html 80 | 81 | // @host {{.serverHost}}{{if ne $.serverPort 80}}:{{$.serverPort}}{{end}} 82 | // @BasePath {{.SwaggerInfo.BasePath}} 83 | func main() { 84 | OsSignal = make(chan os.Signal, 1) 85 | 86 | // Define version information 87 | goopt.Version = fmt.Sprintf( 88 | `Application build information 89 | Build date : %s 90 | Build number : %s 91 | Git commit : %s 92 | Runtime version : %s 93 | Built on OS : %s 94 | `, BuildDate, BuildNumber, LatestCommit, RuntimeVer, BuiltOnOs) 95 | goopt.Parse(nil) 96 | 97 | db, err := gorm.Open("{{.sqlType}}", "{{.sqlConnStr}}") 98 | if err != nil { 99 | log.Fatalf("Got error when connect database, the error is '%v'", err) 100 | } 101 | 102 | db.LogMode(true) 103 | {{.daoPackageName}}.DB = db 104 | 105 | {{ $modelPackage := .modelPackageName }} 106 | db.AutoMigrate( 107 | {{range $tableName, $codeInfo := .tableInfos}} &{{ $modelPackage}}.{{$codeInfo.StructName}}{}, 108 | {{end}} ) 109 | 110 | {{.daoPackageName}}.Logger = func(ctx context.Context, sql string) { 111 | fmt.Printf("SQL: %s\n", sql) 112 | } 113 | 114 | go GinServer() 115 | LoopForever() 116 | } 117 | 118 | 119 | 120 | // LoopForever on signal processing 121 | func LoopForever() { 122 | fmt.Printf("Entering infinite loop\n") 123 | 124 | signal.Notify(OsSignal, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1) 125 | _ = <-OsSignal 126 | 127 | fmt.Printf("Exiting infinite loop received OsSignal\n") 128 | 129 | } 130 | -------------------------------------------------------------------------------- /template/main_sqlx.go.tmpl: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | 12 | "github.com/jmoiron/sqlx" 13 | ginSwagger "github.com/swaggo/gin-swagger" 14 | "github.com/swaggo/gin-swagger/swaggerFiles" 15 | "github.com/gin-gonic/gin" 16 | "github.com/droundy/goopt" 17 | 18 | _ "github.com/denisenkom/go-mssqldb" 19 | _ "github.com/go-sql-driver/mysql" 20 | _ "github.com/lib/pq" 21 | _ "github.com/mattn/go-sqlite3" 22 | 23 | 24 | _ "{{.module}}/docs" 25 | "{{.apiFQPN}}" 26 | "{{.daoFQPN}}" 27 | "{{.modelFQPN}}" 28 | 29 | ) 30 | 31 | const UserKey = "user" // UserKey key used for storing User struct in context 32 | 33 | var ( 34 | // BuildDate date string of when build was performed filled in by -X compile flag 35 | BuildDate string 36 | 37 | // LatestCommit date string of when build was performed filled in by -X compile flag 38 | LatestCommit string 39 | 40 | // BuildNumber date string of when build was performed filled in by -X compile flag 41 | BuildNumber string 42 | 43 | // BuiltOnIP date string of when build was performed filled in by -X compile flag 44 | BuiltOnIP string 45 | 46 | // BuiltOnOs date string of when build was performed filled in by -X compile flag 47 | BuiltOnOs string 48 | 49 | // RuntimeVer date string of when build was performed filled in by -X compile flag 50 | RuntimeVer string 51 | 52 | // OsSignal signal used to shutdown 53 | OsSignal chan os.Signal 54 | ) 55 | 56 | // User struct to store info in context 57 | type User struct{ 58 | Name string 59 | } 60 | 61 | func (u *User) String() string { 62 | return u.Name 63 | } 64 | 65 | // GinServer launch gin server 66 | func GinServer() (err error){ 67 | url := ginSwagger.URL("{{$.serverScheme}}://{{.serverHost}}{{if ne $.serverPort 80}}:{{$.serverPort}}{{end}}/swagger/doc.json") // The url pointing to API definition 68 | 69 | router := gin.Default() 70 | router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url)) 71 | 72 | {{.apiPackageName}}.ConfigGinRouter(router) 73 | err = router.Run("{{.serverListen}}") 74 | if err != nil { 75 | log.Fatalf("Error starting server, the error is '%v'", err) 76 | } 77 | 78 | return 79 | } 80 | 81 | 82 | 83 | // @title {{.SwaggerInfo.Title}} 84 | // @version {{.SwaggerInfo.Version}} 85 | // @description {{.SwaggerInfo.Description}} 86 | // @termsOfService {{.SwaggerInfo.TOS}} 87 | 88 | // @contact.name {{.SwaggerInfo.ContactName}} 89 | // @contact.url {{.SwaggerInfo.ContactURL}} 90 | // @contact.email {{.SwaggerInfo.ContactEmail}} 91 | 92 | // @license.name Apache 2.0 93 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html 94 | 95 | // @host {{.serverHost}}{{if ne $.serverPort 80}}:{{$.serverPort}}{{end}} 96 | // @BasePath {{.SwaggerInfo.BasePath}} 97 | func main() { 98 | OsSignal = make(chan os.Signal, 1) 99 | 100 | // Define version information 101 | goopt.Version = fmt.Sprintf( 102 | `Application build information 103 | Build date : %s 104 | Build number : %s 105 | Git commit : %s 106 | Runtime version : %s 107 | Built on OS : %s 108 | `, BuildDate, BuildNumber, LatestCommit, RuntimeVer, BuiltOnOs) 109 | goopt.Parse(nil) 110 | 111 | db, err := sqlx.Open("{{.sqlType}}", "{{.sqlConnStr}}") 112 | if err != nil { 113 | log.Fatalf("Got error when connect database, the error is '%v'", err) 114 | } 115 | 116 | {{.daoPackageName}}.DB = db 117 | {{.daoPackageName}}.Logger = func(ctx context.Context, sql string) { 118 | user, ok := UserFromContext(ctx) 119 | if ok { 120 | fmt.Printf("[%v] SQL: %s\n", user, sql) 121 | } else { 122 | fmt.Printf("SQL: %s\n", sql) 123 | } 124 | } 125 | 126 | {{.apiPackageName}}.ContextInitializer = func(r *http.Request) (ctx context.Context) { 127 | 128 | val, ok := r.Header["X-Api-User"] 129 | if ok { 130 | if len(val) > 0 { 131 | u := &User{Name: val[0]} 132 | ctx = r.Context() 133 | ctx = context.WithValue(ctx, UserKey, u) 134 | r.WithContext(ctx) 135 | } 136 | } 137 | 138 | if ctx == nil { 139 | ctx = r.Context() 140 | } 141 | 142 | return ctx 143 | } 144 | 145 | {{.apiPackageName}}.RequestValidator = func(ctx context.Context, r *http.Request, table string, action {{.modelPackageName}}.Action) error { 146 | user, ok := UserFromContext(ctx) 147 | if !ok { 148 | return fmt.Errorf("unknown user") 149 | } 150 | 151 | fmt.Printf("user: %v accessing %s action: %v\n", user, table, action) 152 | return nil 153 | } 154 | 155 | go GinServer() 156 | LoopForever() 157 | } 158 | 159 | 160 | // UserFromContext retrieve a User from Context if available 161 | func UserFromContext(ctx context.Context) (*User, bool) { 162 | u, ok := ctx.Value(UserKey).(*User) 163 | return u, ok 164 | } 165 | 166 | // LoopForever on signal processing 167 | func LoopForever() { 168 | fmt.Printf("Entering infinite loop\n") 169 | 170 | signal.Notify(OsSignal, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1) 171 | _ = <-OsSignal 172 | 173 | fmt.Printf("Exiting infinite loop received OsSignal\n") 174 | } 175 | -------------------------------------------------------------------------------- /template/model.go.tmpl: -------------------------------------------------------------------------------- 1 | package {{.modelPackageName}} 2 | 3 | import ( 4 | "database/sql" 5 | "time" 6 | 7 | "github.com/google/uuid" 8 | {{if .UseGuregu}} "github.com/guregu/null" {{end}} 9 | "gorm.io/datatypes" 10 | "gorm.io/gorm" 11 | ) 12 | 13 | var ( 14 | _ = datatypes.JSON{} 15 | ) 16 | 17 | /* 18 | DB Table Details 19 | ------------------------------------- 20 | {{ $ddl := .TableInfo.DBMeta.DDL }} 21 | {{if $ddl }} 22 | {{$ddl}} 23 | {{- end}} 24 | 25 | JSON Sample 26 | ------------------------------------- 27 | {{ToJSON .TableInfo.Instance 4}} 28 | 29 | {{if .TableInfo.Notes }} 30 | Comments 31 | ------------------------------------- 32 | {{ .TableInfo.Notes}} 33 | {{end}} 34 | 35 | */ 36 | 37 | {{if not .Config.AddProtobufAnnotation }} 38 | 39 | // {{.StructName}} struct is a row record of the {{.TableName}} table in the {{.DatabaseName}} database 40 | type {{.StructName}} struct { 41 | {{range .TableInfo.Fields}}{{.}} 42 | {{end}} 43 | } 44 | {{else}} 45 | 46 | // {{.StructName}} struct is a row record of the {{.TableName}} table in the {{.DatabaseName}} database 47 | /* 48 | type {{.StructName}} struct { 49 | {{range .TableInfo.Fields}}{{.}} 50 | {{end}} 51 | } 52 | */ 53 | 54 | {{end}} 55 | 56 | var {{.TableName}}TableInfo = &TableInfo { 57 | Name: "{{.TableName}}", 58 | Columns: []*ColumnInfo{ 59 | {{range .TableInfo.CodeFields}} 60 | { 61 | Index: {{.ColumnMeta.Index}}, 62 | Name: "{{.ColumnMeta.Name}}", 63 | Comment: `{{.ColumnMeta.Comment}}`, 64 | Notes: `{{.ColumnMeta.Notes}}`, 65 | Nullable: {{.ColumnMeta.Nullable}}, 66 | DatabaseTypeName: "{{.ColumnMeta.DatabaseTypeName}}", 67 | DatabaseTypePretty: "{{.ColumnMeta.DatabaseTypePretty}}", 68 | IsPrimaryKey: {{.ColumnMeta.IsPrimaryKey}}, 69 | IsAutoIncrement: {{.ColumnMeta.IsAutoIncrement}}, 70 | IsArray: {{.ColumnMeta.IsArray}}, 71 | ColumnType: "{{.ColumnMeta.ColumnType}}", 72 | ColumnLength: {{.ColumnMeta.ColumnLength}}, 73 | GoFieldName: "{{.GoFieldName}}", 74 | GoFieldType: "{{.GoFieldType}}", 75 | JSONFieldName: "{{.JSONFieldName}}", 76 | ProtobufFieldName: "{{.ProtobufFieldName}}", 77 | ProtobufType: "{{.ProtobufType}}", 78 | ProtobufPos: {{.ProtobufPos}}, 79 | }, 80 | {{end}} 81 | }, 82 | } 83 | 84 | 85 | 86 | // TableName sets the insert table name for this struct type 87 | func ({{.ShortStructName}} *{{.StructName}}) TableName() string { 88 | return "{{.TableName}}" 89 | } 90 | 91 | // BeforeSave invoked before saving, return an error if field is not populated. 92 | func ({{.ShortStructName}} *{{.StructName}}) BeforeSave(tx *gorm.DB) error { 93 | return nil 94 | } 95 | 96 | // Prepare invoked before saving, can be used to populate fields etc. 97 | func ({{.ShortStructName}} *{{.StructName}}) Prepare() { 98 | } 99 | 100 | // Validate invoked before performing action, return an error if field is not populated. 101 | func ({{.ShortStructName}} *{{.StructName}}) Validate(action Action) error { 102 | return nil 103 | } 104 | 105 | // TableInfo return table meta data 106 | func ({{.ShortStructName}} *{{.StructName}}) TableInfo() *TableInfo { 107 | return {{.TableName}}TableInfo 108 | } 109 | -------------------------------------------------------------------------------- /template/model_base.go.tmpl: -------------------------------------------------------------------------------- 1 | package {{.modelPackageName}} 2 | 3 | import ( 4 | "fmt" 5 | 6 | gorm "gorm.io/gorm" 7 | ) 8 | 9 | // Action CRUD actions 10 | type Action int32 11 | 12 | var ( 13 | // Create action when record is created 14 | Create = Action(0) 15 | 16 | // RetrieveOne action when a record is retrieved from db 17 | RetrieveOne = Action(1) 18 | 19 | // RetrieveMany action when record(s) are retrieved from db 20 | RetrieveMany = Action(2) 21 | 22 | // Update action when record is updated in db 23 | Update = Action(3) 24 | 25 | // Delete action when record is deleted in db 26 | Delete = Action(4) 27 | 28 | // FetchDDL action when fetching ddl info from db 29 | FetchDDL = Action(5) 30 | 31 | tables map[string]*TableInfo 32 | ) 33 | 34 | 35 | func init() { 36 | tables = make(map[string]*TableInfo) 37 | {{ range $tableName, $tableInfo := .tableInfos }} 38 | tables["{{$tableName}}"] = {{$tableName}}TableInfo 39 | {{- end}} 40 | } 41 | 42 | // String describe the action 43 | func (i Action) String() string { 44 | switch i { 45 | case Create: 46 | return "Create" 47 | case RetrieveOne: 48 | return "RetrieveOne" 49 | case RetrieveMany: 50 | return "RetrieveMany" 51 | case Update: 52 | return "Update" 53 | case Delete: 54 | return "Delete" 55 | case FetchDDL: 56 | return "FetchDDL" 57 | default: 58 | return fmt.Sprintf("unknown action: %d", int(i)) 59 | } 60 | } 61 | 62 | // Model interface methods for database structs generated 63 | type Model interface { 64 | TableName() string 65 | BeforeSave(tx *gorm.DB) error 66 | Prepare() 67 | Validate(action Action) error 68 | TableInfo() *TableInfo 69 | } 70 | 71 | // TableInfo describes a table in the database 72 | type TableInfo struct { 73 | Name string {{ .Config.JSONTag "name" }} 74 | Columns []*ColumnInfo {{ .Config.JSONTag "columns" }} 75 | } 76 | 77 | // ColumnInfo describes a column in the database table 78 | type ColumnInfo struct { 79 | Index int {{ .Config.JSONTag "index" }} 80 | GoFieldName string {{ .Config.JSONTag "go_field_name" }} 81 | GoFieldType string {{ .Config.JSONTag "go_field_type" }} 82 | JSONFieldName string {{ .Config.JSONTag "json_field_name" }} 83 | ProtobufFieldName string {{ .Config.JSONTag "protobuf_field_name" }} 84 | ProtobufType string {{ .Config.JSONTag "protobuf_field_type" }} 85 | ProtobufPos int {{ .Config.JSONTag "protobuf_field_pos" }} 86 | Comment string {{ .Config.JSONTag "comment" }} 87 | Notes string {{ .Config.JSONTag "notes" }} 88 | Name string {{ .Config.JSONTag "name" }} 89 | Nullable bool {{ .Config.JSONTag "is_nullable" }} 90 | DatabaseTypeName string {{ .Config.JSONTag "database_type_name" }} 91 | DatabaseTypePretty string {{ .Config.JSONTag "database_type_pretty" }} 92 | IsPrimaryKey bool {{ .Config.JSONTag "is_primary_key" }} 93 | IsAutoIncrement bool {{ .Config.JSONTag "is_auto_increment" }} 94 | IsArray bool {{ .Config.JSONTag "is_array" }} 95 | ColumnType string {{ .Config.JSONTag "column_type" }} 96 | ColumnLength int64 {{ .Config.JSONTag "column_length" }} 97 | DefaultValue string {{ .Config.JSONTag "default_value" }} 98 | } 99 | 100 | // GetTableInfo retrieve TableInfo for a table 101 | func GetTableInfo(name string) (*TableInfo, bool) { 102 | val, ok := tables[name] 103 | return val, ok 104 | } 105 | -------------------------------------------------------------------------------- /template/protobuf.tmpl: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package {{.DatabaseName}}; 4 | import "google/protobuf/timestamp.proto"; 5 | import "github.com/gogo/protobuf/gogoproto/gogo.proto"; 6 | 7 | 8 | option (gogoproto.populate_all) = true; 9 | option go_package = "{{.Config.ModelPackageName}}"; 10 | option (gogoproto.equal_all) = true; 11 | option (gogoproto.gostring_all) = true; 12 | 13 | message Result { 14 | enum result_type { 15 | Error = 0; 16 | Success = 1; 17 | } 18 | 19 | result_type code = 1 [(gogoproto.customname) = 'Code', (gogoproto.moretags) = 'json:"code" xml:"code" db:"code"']; 20 | string message = 2 [(gogoproto.customname) = 'Message', (gogoproto.moretags) = 'json:"message" xml:"message" db:"message"']; 21 | } 22 | 23 | 24 | message packet { 25 | enum packet_type_t { 26 | Error = 0; 27 | {{ range $tableName, $tableInfo := .tableInfos }} 28 | //Pkt{{ $tableInfo.StructName }} = {{$tableInfo.Index}};{{- end}} 29 | } 30 | 31 | packet_type_t id = 2; 32 | bytes data = 3; 33 | } 34 | service Backend { 35 | {{ range $tableName, $tableInfo := .tableInfos }} 36 | rpc GetAll{{ $tableInfo.StructName }}(GetAll{{ $tableInfo.StructName }}Request) returns (GetAll{{ $tableInfo.StructName }}Response); 37 | rpc Get{{ $tableInfo.StructName }}(Get{{ $tableInfo.StructName }}Request) returns (Get{{ $tableInfo.StructName }}Response); 38 | rpc Add{{ $tableInfo.StructName }}(Add{{ $tableInfo.StructName }}Request) returns (Add{{ $tableInfo.StructName }}Response); 39 | rpc Update{{ $tableInfo.StructName }}(Update{{ $tableInfo.StructName }}Request) returns (Update{{ $tableInfo.StructName }}Response); 40 | rpc Delete{{ $tableInfo.StructName }}(Delete{{ $tableInfo.StructName }}Request) returns (Delete{{ $tableInfo.StructName }}Response); 41 | {{- end}} 42 | } 43 | 44 | {{ range $tableName, $tableInfo := .tableInfos }} 45 | // table: {{$tableName}} 46 | message {{ $tableInfo.StructName }} { 47 | option (gogoproto.goproto_unrecognized) = false; 48 | option (gogoproto.goproto_unkeyed) = false; 49 | option (gogoproto.goproto_sizecache) = false; 50 | 51 | {{ range $i, $field := $tableInfo.CodeFields }} 52 | // Column: {{$field.ColumnMeta.String}} 53 | {{ $field.ProtobufType}} {{ $field.ProtobufFieldName}} = {{ $field.ProtobufPos}} [(gogoproto.customname) = '{{ $field.GoFieldName}}', (gogoproto.moretags) = '{{ escape $field.GoGoMoreTags}}'];{{- end}} 54 | } 55 | 56 | 57 | message GetAll{{ $tableInfo.StructName }}Request { 58 | int64 page = 1; 59 | int64 page_size = 2; 60 | string order = 3; 61 | } 62 | 63 | message GetAll{{ $tableInfo.StructName }}Response { 64 | Result result = 1; 65 | repeated {{$tableInfo.StructName}} data = 2; 66 | int64 page = 3; 67 | int64 page_size = 4; 68 | int64 total_records = 5; 69 | } 70 | 71 | 72 | message Get{{ $tableInfo.StructName }}Request { 73 | {{ $fieldPos := set 0 }}{{ range $i, $field := $tableInfo.CodeFields }}{{ if $field.ColumnMeta.IsPrimaryKey }}{{ $fieldPos := inc}} 74 | {{ $field.ProtobufType}} {{ $field.ProtobufFieldName}} = {{ $fieldPos}} [(gogoproto.customname) = "{{ $field.GoFieldName}}"];{{- end }}{{- end}} 75 | } 76 | 77 | message Get{{ $tableInfo.StructName }}Response { 78 | Result result = 1; 79 | {{$tableInfo.StructName}} data = 2; 80 | } 81 | message Add{{ $tableInfo.StructName }}Request { 82 | {{$tableInfo.StructName}} data = 1; 83 | } 84 | 85 | message Add{{ $tableInfo.StructName }}Response { 86 | Result result = 1; 87 | {{$tableInfo.StructName}} data = 2; 88 | int64 rowsAffected = 3; 89 | } 90 | 91 | message Update{{ $tableInfo.StructName }}Request { 92 | {{$tableInfo.StructName}} data = 1; 93 | } 94 | 95 | message Update{{ $tableInfo.StructName }}Response { 96 | Result result = 1; 97 | {{$tableInfo.StructName}} data = 2; 98 | int64 rowsAffected = 3; 99 | } 100 | 101 | message Delete{{ $tableInfo.StructName }}Request { 102 | {{ $fieldPos := set 0 }}{{ range $i, $field := $tableInfo.CodeFields }}{{ if $field.ColumnMeta.IsPrimaryKey }}{{ $fieldPos := inc}} 103 | {{ $field.ProtobufType}} {{ $field.ProtobufFieldName}} = {{ $fieldPos}} [(gogoproto.customname) = "{{ $field.GoFieldName}}"];{{- end }}{{- end}} 104 | } 105 | 106 | message Delete{{ $tableInfo.StructName }}Response { 107 | Result result = 1; 108 | int64 rows_affected = 2; 109 | } 110 | 111 | {{ end}} 112 | 113 | 114 | 115 | 116 | /* 117 | {{ range $key, $value := . }} 118 | {{ printf "%#-20v" $key }} {{ printf "[%T] %#v" $value $value }}{{ end }} 119 | 120 | */ 121 | -------------------------------------------------------------------------------- /template/protomain.go.tmpl: -------------------------------------------------------------------------------- 1 | package {{.Config.GrpcPackageName}} 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "net" 11 | "os" 12 | "runtime/debug" 13 | 14 | "{{.daoFQPN}}" 15 | "{{.modelFQPN}}" 16 | 17 | "github.com/grpc-ecosystem/go-grpc-middleware" 18 | "github.com/grpc-ecosystem/go-grpc-middleware/auth" 19 | "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus" 20 | "github.com/grpc-ecosystem/go-grpc-middleware/recovery" 21 | "github.com/grpc-ecosystem/go-grpc-middleware/tags" 22 | "github.com/sirupsen/logrus" 23 | "google.golang.org/grpc" 24 | "google.golang.org/grpc/codes" 25 | "google.golang.org/grpc/credentials" 26 | "google.golang.org/grpc/status" 27 | ) 28 | 29 | 30 | func reportError(message string, err error) { 31 | // sentry.CaptureException(fmt.Errorf("%s: %v", message, err)) 32 | log.Printf("🚨 %s – %v", message, err) 33 | } 34 | 35 | func (s *Server) AuthFuncOverride(ctx context.Context, fullMethodName string) (context.Context, error) { 36 | // allowed_endpoints := map[string]bool{ 37 | // "/backend.Backend/CreateUser": true, 38 | // } 39 | 40 | // if allow, ok := allowed_endpoints[fullMethodName]; allow && ok { 41 | // return ctx, nil 42 | // } 43 | return ctx, nil 44 | 45 | } 46 | 47 | 48 | 49 | func panicRecover(p interface{}) error { 50 | reportError(fmt.Sprintf("Panic happened: %s", p), nil) 51 | log.Printf("Panic ouccred: %s", p) 52 | log.Print("Stacktrace from panic: \n" + string(debug.Stack())) 53 | return status.Errorf(codes.Internal, "Internal error") 54 | } 55 | 56 | func RunServer() { 57 | config := LoadConfig() 58 | server := NewServerFromConfig(config) 59 | defer server.Cleanup() 60 | 61 | // if config.SentryDSN != "" { 62 | // sentry.Init(sentry.ClientOptions{Dsn: config.SentryDSN}) 63 | // } 64 | 65 | grpcServer := grpc.NewServer(makeServerOptions(config)...) 66 | {{.modelPackageName}}.RegisterBackendServer(grpcServer, server) 67 | 68 | lis, err := net.Listen("tcp", config.ListenAddr) 69 | if err != nil { 70 | log.Fatalf("failed to listen: %v", err) 71 | } 72 | 73 | log.Printf("Server started on %s", config.ListenAddr) 74 | if err := grpcServer.Serve(lis); err != nil { 75 | log.Fatalf("failed to serve: %v", err) 76 | } 77 | } 78 | 79 | func makeServerOptions(config ServerConfig) []grpc.ServerOption { 80 | transportCredentials, err := getTransportCredentials(config) 81 | if err != nil { 82 | log.Fatalf("failed to get credentials: %v", err) 83 | } 84 | 85 | logger := &logrus.Logger{ 86 | Out: os.Stdout, 87 | Formatter: new(logrus.TextFormatter), 88 | Hooks: make(logrus.LevelHooks), 89 | Level: logrus.DebugLevel, 90 | } 91 | logrusEntry := logrus.NewEntry(logger) 92 | 93 | recoverOptions := []grpc_recovery.Option{ 94 | grpc_recovery.WithRecoveryHandler(panicRecover), 95 | } 96 | unaryServerInterceptors := []grpc.UnaryServerInterceptor{ 97 | grpc_ctxtags.UnaryServerInterceptor(grpc_ctxtags.WithFieldExtractor(grpc_ctxtags.CodeGenRequestFieldExtractor)), 98 | grpc_logrus.UnaryServerInterceptor(logrusEntry), 99 | grpc_auth.UnaryServerInterceptor(nil), 100 | grpc_recovery.UnaryServerInterceptor(recoverOptions...), 101 | } 102 | 103 | return []grpc.ServerOption{ 104 | grpc.MaxRecvMsgSize(config.MaxMessageSizeBytes), 105 | grpc.MaxSendMsgSize(config.MaxMessageSizeBytes), 106 | grpc.Creds(*transportCredentials), 107 | grpc_middleware.WithUnaryServerChain(unaryServerInterceptors...), 108 | grpc_middleware.WithStreamServerChain( 109 | grpc_ctxtags.StreamServerInterceptor(grpc_ctxtags.WithFieldExtractor(grpc_ctxtags.CodeGenRequestFieldExtractor)), 110 | grpc_logrus.StreamServerInterceptor(logrusEntry), 111 | grpc_auth.StreamServerInterceptor(nil), 112 | grpc_recovery.StreamServerInterceptor(recoverOptions...), 113 | ), 114 | } 115 | } 116 | 117 | func getTransportCredentials(cfg ServerConfig) (*credentials.TransportCredentials, error) { 118 | peerCert, err := tls.LoadX509KeyPair(cfg.ServerCert, cfg.ServerKey) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | caCert, err := ioutil.ReadFile(cfg.CACert) 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | caCertPool := x509.NewCertPool() 129 | caCertPool.AppendCertsFromPEM(caCert) 130 | tc := credentials.NewTLS(&tls.Config{ 131 | Certificates: []tls.Certificate{peerCert}, 132 | ClientCAs: caCertPool, 133 | }) 134 | 135 | return &tc, nil 136 | } 137 | -------------------------------------------------------------------------------- /template/protoserver.go.tmpl: -------------------------------------------------------------------------------- 1 | package {{.Config.GrpcPackageName}} 2 | 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | 11 | "{{.daoFQPN}}" 12 | "{{.modelFQPN}}" 13 | 14 | "gopkg.in/yaml.v2" 15 | ) 16 | 17 | // Server server instance 18 | type Server struct { 19 | } 20 | 21 | 22 | 23 | {{ range $tableName, $tableInfo := .tableInfos }} 24 | 25 | 26 | // GetAll{{ $tableInfo.StructName }} is a RPC method to get a slice of record(s) from {{.TableName}} table in the {{$.DatabaseName}} database 27 | func (s *Server) GetAll{{ $tableInfo.StructName }}(context context.Context, request *{{$.modelPackageName}}.GetAll{{ $tableInfo.StructName }}Request) (*{{$.modelPackageName}}.GetAll{{ $tableInfo.StructName }}Response, error) { 28 | 29 | result, totalRows, err := {{$.daoPackageName}}.GetAll{{$tableInfo.StructName}}(context, request.Page, request.PageSize, request.Order) 30 | if err != nil { 31 | response := &{{$.modelPackageName}}.GetAll{{ $tableInfo.StructName }}Response{Result: &{{$.modelPackageName}}.Result{Result: {{$.modelPackageName}}.Result_Error, Message_: fmt.Sprintf("%v", err)}} 32 | return response, nil 33 | } 34 | 35 | response := &{{$.modelPackageName}}.GetAll{{ $tableInfo.StructName }}Response{Result: &{{$.modelPackageName}}.Result{Result: {{$.modelPackageName}}.Result_Success}, Page: request.Page, PageSize: request.PageSize, Data: result, TotalRecords: int64(totalRows) } 36 | return response, nil 37 | } 38 | 39 | // Get{{.StructName}} is a RPC method to get a single record from the {{.TableName}} table in the {{$.DatabaseName}} database 40 | func (s *Server) Get{{ $tableInfo.StructName }}(context context.Context, request *{{$.modelPackageName}}.Get{{ $tableInfo.StructName }}Request) (*{{$.modelPackageName}}.Get{{ $tableInfo.StructName }}Response, error) { 41 | 42 | result, err := {{$.daoPackageName}}.Get{{$tableInfo.StructName}}(context,{{range $field := .CodeFields}} {{ if $field.PrimaryKeyArgName }} request.{{$field.GoFieldName}},{{end}}{{end -}}) 43 | if err != nil { 44 | response := &{{$.modelPackageName}}.Get{{ $tableInfo.StructName }}Response{Result: &{{$.modelPackageName}}.Result{Result: {{$.modelPackageName}}.Result_Error, Message_: fmt.Sprintf("%v", err)}} 45 | return response, nil 46 | } 47 | 48 | response := &{{$.modelPackageName}}.Get{{ $tableInfo.StructName }}Response{} 49 | response.Data = result 50 | response.Result=&{{$.modelPackageName}}.Result{Result: {{$.modelPackageName}}.Result_Success} 51 | return response, nil 52 | } 53 | 54 | // Add{{.StructName}} is a RPC method to add a single record to {{.TableName}} table in the {{$.DatabaseName}} database 55 | func (s *Server) Add{{ $tableInfo.StructName }}(context context.Context, request *{{$.modelPackageName}}.Add{{ $tableInfo.StructName }}Request) (*{{$.modelPackageName}}.Add{{ $tableInfo.StructName }}Response, error) { 56 | 57 | result, rowsAffected, err := {{$.daoPackageName}}.Add{{$tableInfo.StructName}}(context, request.Data) 58 | if err != nil { 59 | response := &{{$.modelPackageName}}.Add{{ $tableInfo.StructName }}Response{Result: &{{$.modelPackageName}}.Result{Result: {{$.modelPackageName}}.Result_Error, Message_: fmt.Sprintf("%v", err)}} 60 | return response, nil 61 | } 62 | 63 | response := &{{$.modelPackageName}}.Add{{ $tableInfo.StructName }}Response{} 64 | response.Data = result 65 | response.Result = &{{$.modelPackageName}}.Result{Result: {{$.modelPackageName}}.Result_Success} 66 | response.RowsAffected = rowsAffected 67 | return response, nil 68 | } 69 | 70 | // Update{{.StructName}} is a RPC method to Update a single record from {{.TableName}} table in the {{$.DatabaseName}} database 71 | func (s *Server) Update{{ $tableInfo.StructName }}(context context.Context, request *{{$.modelPackageName}}.Update{{ $tableInfo.StructName }}Request) (*{{$.modelPackageName}}.Update{{ $tableInfo.StructName }}Response, error) { 72 | 73 | result, rowsAffected, err := {{$.daoPackageName}}.Update{{$tableInfo.StructName}}(context, 74 | {{range $field := .CodeFields}} {{ if $field.PrimaryKeyArgName }} request.Data.{{$field.GoFieldName}},{{end}}{{end}} 75 | request.Data) 76 | if err != nil { 77 | response := &{{$.modelPackageName}}.Update{{ $tableInfo.StructName }}Response{Result: &{{$.modelPackageName}}.Result{Result: {{$.modelPackageName}}.Result_Error, Message_: fmt.Sprintf("%v", err)}} 78 | return response, nil 79 | } 80 | 81 | response := &{{$.modelPackageName}}.Update{{ $tableInfo.StructName }}Response{} 82 | response.Data = result 83 | response.Result = &{{$.modelPackageName}}.Result{Result: {{$.modelPackageName}}.Result_Success} 84 | response.RowsAffected = rowsAffected 85 | return response, nil 86 | } 87 | 88 | // Delete{{.StructName}} is a RPC method to delete a single record from {{.TableName}} table in the {{$.DatabaseName}} database 89 | func (s *Server) Delete{{ $tableInfo.StructName }}(context context.Context, request *{{$.modelPackageName}}.Delete{{ $tableInfo.StructName }}Request) (*{{$.modelPackageName}}.Delete{{ $tableInfo.StructName }}Response, error) { 90 | 91 | rowsAffected, err := {{$.daoPackageName}}.Delete{{$tableInfo.StructName}}(context,{{range $field := .CodeFields}} {{ if $field.PrimaryKeyArgName }} request.{{$field.GoFieldName}},{{end}}{{end -}}) 92 | if err != nil { 93 | response := &{{$.modelPackageName}}.Delete{{ $tableInfo.StructName }}Response{Result: &{{$.modelPackageName}}.Result{Result: {{$.modelPackageName}}.Result_Error, Message_: fmt.Sprintf("%v", err)}} 94 | return response, nil 95 | } 96 | 97 | response := &{{$.modelPackageName}}.Delete{{ $tableInfo.StructName }}Response{RowsAffected: rowsAffected, Result: &{{$.modelPackageName}}.Result{Result: {{$.modelPackageName}}.Result_Success}} 98 | return response, nil 99 | } 100 | 101 | {{- end}} 102 | 103 | 104 | // ServerConfig server configuration 105 | type ServerConfig struct { 106 | ListenAddr string `yaml:"listen_on"` 107 | DatabaseURI string `yaml:"db_uri"` 108 | MaxMessageSizeBytes int `yaml:"max_message_size_bytes"` 109 | ServerCert string `yaml:"server_cert"` 110 | ServerKey string `yaml:"server_key"` 111 | CACert string `yaml:"ca_cert"` 112 | } 113 | 114 | // LoadConfig load server configuration from file pointed to from BACKEND_CONFIG env variable 115 | func LoadConfig() ServerConfig { 116 | configPath := os.Getenv("BACKEND_CONFIG") 117 | content, err := ioutil.ReadFile(configPath) 118 | if err != nil { 119 | log.Fatalf( 120 | "Error loading config from BACKEND_CONFIG=%s: %v", 121 | configPath, 122 | err, 123 | ) 124 | } 125 | 126 | config := ServerConfig{} 127 | err = yaml.Unmarshal([]byte(content), &config) 128 | if err != nil { 129 | log.Fatalf("Error parsing config: %v", err) 130 | } 131 | return config 132 | } 133 | 134 | // NewServerFromConfig create a server from a ServerConfig 135 | func NewServerFromConfig(cfg ServerConfig) *Server { 136 | 137 | return &Server{ 138 | } 139 | } 140 | 141 | // Cleanup server resources 142 | func (s *Server) Cleanup() { 143 | } 144 | 145 | 146 | /* 147 | {{ range $key, $value := . }} 148 | {{ printf "%#-20v" $key }} {{ printf "[%T] %#v" $value $value }}{{ end }} 149 | 150 | */ 151 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | usage() { 4 | echo "usage: $0 " 5 | echo "" 6 | echo " DB_TYPE: sqlite3 | mysql | mssql | postgres" 7 | echo " APP: gen_gorm | gen_sqlx | meta" 8 | exit 0 9 | } 10 | 11 | func_gen_gorm() { 12 | OUT_DIR="./tests/${DB_TYPE}_gorm" 13 | if [[ -d ${OUT_DIR} ]]; then 14 | rm -rf "${OUT_DIR}" 15 | fi 16 | 17 | TABLES_KEY="${DB_TYPE}_tables" 18 | TABLES=${!TABLES_KEY} 19 | 20 | EXTABLES_KEY="${DB_TYPE}_exclude_tables" 21 | EXTABLES=${!EXTABLES_KEY} 22 | 23 | if [[ "${TABLES}" == 'all' || "${TABLES}" == '' ]]; then 24 | echo 'generating gorm code for all tables' 25 | else 26 | echo "generating gorm code for tables: ${TABLES}" 27 | DEFAULT_GEN_OPTIONS="${DEFAULT_GEN_OPTIONS} --table=${TABLES}" 28 | fi 29 | 30 | if [[ "${EXTABLES}" != '' ]]; then 31 | echo "excluding tables: ${EXTABLES}" 32 | fi 33 | 34 | CMD="${GEN_APP} --gorm ${DEFAULT_GEN_OPTIONS} --exclude=\"${EXTABLES}\" --sqltype=\"${DB_TYPE}\" --connstr=\"${DB_CON}\" --database=\"${DB}\" --out=\"${OUT_DIR}\" --module=\"${base_module}/${DB}\"" 35 | eval ${CMD} 36 | } 37 | 38 | func_gen_sqlx() { 39 | OUT_DIR="./tests/${DB_TYPE}_sqlx" 40 | if [[ -d ./tests/${DB_TYPE} ]]; then 41 | rm -rf "${OUT_DIR}" 42 | fi 43 | 44 | TABLES_KEY="${DB_TYPE}_tables" 45 | TABLES=${!TABLES_KEY} 46 | 47 | EXTABLES_KEY="${DB_TYPE}_exclude_tables" 48 | EXTABLES=${!EXTABLES_KEY} 49 | 50 | if [[ "${TABLES}" == 'all' || "${TABLES}" == '' ]]; then 51 | echo 'generating sqlx code for all tables' 52 | else 53 | echo "generating sqlx code for tables: ${TABLES}" 54 | DEFAULT_GEN_OPTIONS="${DEFAULT_GEN_OPTIONS} --table=${TABLES}" 55 | fi 56 | 57 | if [[ "${EXTABLES}" != '' ]]; then 58 | echo "excluding tables: ${EXTABLES}" 59 | fi 60 | 61 | CMD="${GEN_APP} ${DEFAULT_GEN_OPTIONS} --exclude=\"${EXTABLES}\" --sqltype=\"${DB_TYPE}\" --connstr=\"${DB_CON}\" --database=\"${DB}\" --out=\"${OUT_DIR}\" --module=\"${base_module}/${DB}\"" 62 | eval ${CMD} 63 | } 64 | 65 | func_meta() { 66 | DEFAULT_META_OPTIONS="" 67 | 68 | TABLES_KEY="${DB_TYPE}_tables" 69 | TABLES=${!TABLES_KEY} 70 | 71 | if [[ "${TABLES}" == 'all' || "${TABLES}" == '' ]]; then 72 | echo 'showing meta for all tables' 73 | else 74 | echo "showing meta for tables: ${TABLES}" 75 | DEFAULT_META_OPTIONS="${DEFAULT_META_OPTIONS} --table=${TABLES}" 76 | fi 77 | 78 | go run github.com/smallnest/gen/_test/dbmeta \ 79 | --sqltype="${DB_TYPE}" \ 80 | --connstr "${DB_CON}" \ 81 | --database "${DB}" ${DEFAULT_META_OPTIONS} 82 | 83 | } 84 | 85 | func_test() { 86 | echo "test" 87 | echo "${DEFAULT_GEN_OPTIONS}" 88 | eval print_args ${DEFAULT_GEN_OPTIONS} 89 | } 90 | 91 | print_args() { 92 | echo "--------------------------------" 93 | echo "" 94 | args=("$@") 95 | # get number of elements 96 | ELEMENTS=${#args[@]} 97 | 98 | # echo each element in array 99 | # for loop 100 | for (( i=0;i<${ELEMENTS};i++)); do 101 | echo "[$i ] ${args[${i}]}" 102 | done 103 | 104 | } 105 | check_vars(){ 106 | if [ "${USE_GEN}" == "" ]; 107 | then 108 | echo "" >> ./.env 109 | echo "USE_GEN=0" >> ./.env 110 | . .env 111 | fi 112 | 113 | if [ "${DEFAULT_GEN_OPTIONS}" == "" ]; 114 | then 115 | echo "" >> ./.env 116 | echo "DEFAULT_GEN_OPTIONS=\"--json 117 | --db 118 | --protobuf 119 | --api=apis 120 | --dao=daos 121 | --model=models 122 | --guregu 123 | --rest 124 | --mod 125 | --server 126 | --makefile 127 | --generate-dao 128 | --generate-proj 129 | --overwrite 130 | --copy-templates 131 | --host=localhost 132 | --port=8080 133 | --mapping ./template/mapping.json 134 | --templateDir=./template 135 | --file_naming='{{. }}' 136 | --model_naming='{{FmtFieldName .}}' 137 | --field_naming='{{FmtFieldName (stringifyFirstChar .) }}' 138 | --verbose\"" >> ./.env 139 | 140 | . .env 141 | fi 142 | 143 | } 144 | create_env() { 145 | cat >./.env <