├── .air.toml ├── .gitignore ├── README.md ├── admin ├── .gitignore ├── index.html ├── package.json ├── public │ └── vite.svg ├── src │ ├── App.css │ ├── App.tsx │ ├── assets │ │ └── react.svg │ ├── config │ │ ├── api.provider.ts │ │ └── constants.ts │ ├── index.css │ ├── main.tsx │ ├── resources │ │ └── posts │ │ │ ├── components │ │ │ ├── Create │ │ │ │ └── index.tsx │ │ │ ├── Edit │ │ │ │ └── index.tsx │ │ │ ├── List │ │ │ │ └── index.tsx │ │ │ ├── MobileGrid │ │ │ │ └── index.tsx │ │ │ └── Show │ │ │ │ └── index.tsx │ │ │ └── resource.ts │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock ├── cmd └── crudgen │ └── crudgen.go ├── common ├── env_keys.go ├── helpers.go └── structs.go ├── crud ├── crud_service.go ├── repository.go ├── router.go ├── service.go ├── structs.go └── utils.go ├── db ├── database.go ├── migrations.go ├── models │ └── post.go └── seed │ └── posts.go ├── docs ├── docs.go ├── swagger.json └── swagger.yaml ├── generators ├── create_service.go ├── init_project.go └── templates │ ├── controller.tmpl │ ├── db │ ├── database.tmpl │ └── migrations.tmpl │ ├── env.tmpl │ ├── gitignore.tmpl │ ├── main.tmpl │ ├── model.tmpl │ ├── repository.tmpl │ ├── routes.tmpl │ └── service.tmpl ├── go.mod ├── go.sum ├── main.go ├── pkg ├── helpers │ └── ensure_dir.go └── writetemplate │ └── process_template.go ├── posts ├── controller.go ├── repository.go ├── routes.go └── service.go ├── requests ├── crud.http └── posts.http └── templates ├── db ├── database.tmpl └── migrations.tmpl └── main.tmpl /.air.toml: -------------------------------------------------------------------------------- 1 | root = "." 2 | testdata_dir = "testdata" 3 | tmp_dir = "tmp" 4 | 5 | [build] 6 | args_bin = [] 7 | bin = "./tmp/main" 8 | cmd = "go build -o ./tmp/main ." 9 | delay = 1000 10 | exclude_dir = ["assets", "tmp", "vendor", "testdata"] 11 | exclude_file = [] 12 | exclude_regex = ["_test.go"] 13 | exclude_unchanged = false 14 | follow_symlink = false 15 | full_bin = "" 16 | include_dir = [] 17 | include_ext = ["go", "tpl", "tmpl", "html"] 18 | kill_delay = "0s" 19 | log = "build-errors.log" 20 | send_interrupt = false 21 | stop_on_error = true 22 | 23 | [color] 24 | app = "" 25 | build = "yellow" 26 | main = "magenta" 27 | runner = "green" 28 | watcher = "cyan" 29 | 30 | [log] 31 | time = false 32 | 33 | [misc] 34 | clean_on_exit = false 35 | 36 | [screen] 37 | clear_on_rebuild = false 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | tmp 3 | .env 4 | *.local.db -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go gin crud starter 2 | This repo could be used as a kind of inherited CRUD api, and it will save a lot of time. 3 | 4 | ## story 5 | 6 | I used to handle crud operations in Nodejs with nest crud package, and it is fully compatible with react admin. 7 | I've never spent any time to make sorting or selecting or filtering functionalities. I was able to create 8 | admin dashboard in no time. When I switched to golang I missed these productive tools so I decided to create it. 9 | 10 | 11 | ## Features 12 | - Full crud features 13 | - sorting, selecting and do complex filters like nested "and" & "or" queries 14 | - add joins and nested joins from uri 15 | - compatible with [ra-admin](https://www.radmin.com/) 16 | - inspired from [@nestjsx/crud](https://github.com/nestjsx/crud) 17 | 18 | ## packages I am using 19 | - Gin 20 | - Gorm 21 | - Postgres 22 | - Air 23 | - Swagger 24 | 25 | Fell free to make PR to add other frameworks or ORMs 26 | ## How to use 27 | 28 | ### Install 29 | `go install github.com/ElegantSoft/go-restful-generator/cmd/crudgen@latest` 30 | 31 | - create new golang project 32 | - go get github.com/ElegantSoft/go-restful-generator@latest 33 | - create db/models folder for models 34 | - create db/db.go with package db for db connection 35 | 36 | 37 | `crudgen service --name service-name --path lib/apps/service-name` 38 | 39 | ### Configuration 40 | - Create `.env` file for configuration. 41 | - Add to it `DB_URL` as a connection string which is in used by [Gorm](https://gorm.io/docs/connecting_to_the_database.html) 42 | - By default `PORT` is configured on 8080, but you can specify in the `.env` file a different `PORT` if you like so. 43 | - If you want to have the documentation also updated then follow the next steps: 44 | - Install `swaggo` with `go install github.com/swaggo/swag/cmd/swag@latest` 45 | - Run in terminal `swag init` which will update the swagger files in the `docs` directory. 46 | 47 | ### See examples as posts then duplicate it 48 | 49 | First you can see how crud api works by running project and go to `/docs/index.html` 50 | 51 | - duplicate posts folder 52 | - change package name 53 | - create your entity in models folder 54 | - replace `model` struct in `repository.go` 55 | 56 | ### requests params 57 | - [Query params](#query-params) 58 | - [select](#select) 59 | - [search](#search) 60 | - [filter conditions](#filter-conditions) 61 | - [filter](#filter) 62 | - [or](#or) 63 | - [sort](#sort) 64 | - [join](#join) 65 | - [limit](#limit) 66 | - [offset](#offset) 67 | - [page](#page) 68 | - [cache](#cache) 69 | - [Frontend usage](#frontend-usage) 70 | - [Customize](#customize) 71 | - [Usage](#usage) 72 | 73 | ## Query params 74 | 75 | By default, we support these param names: 76 | 77 | `fields` - get selected fields in GET result 78 | 79 | `s` - search conditions (`$and`, `$or` with all possible variations) 80 | 81 | `filter` - filter GET result by `AND` type of condition 82 | 83 | `join` - receive joined relational resources in GET result (with all or selected fields) 84 | 85 | `sort` - sort GET result by some `field` in `ASC | DESC` order 86 | 87 | `limit` - limit the amount of received resources 88 | 89 | `page` - receive a portion of limited amount of resources 90 | 91 | 92 | **_Notice:_** You can easily map your own query params names and chose another string delimiters by applying [global options](https://github.com/nestjsx/crud/wiki/Controllers#global-options). 93 | 94 | Here is the description of each of those using default params names: 95 | 96 | ### select 97 | 98 | Selects fields that should be returned in the reponse body. 99 | 100 | _Syntax:_ 101 | 102 | > ?fields=**field1**,**field2**,... 103 | 104 | _Example:_ 105 | 106 | > ?fields=**email**,**name** 107 | 108 | ### search 109 | 110 | Adds a search condition as a JSON string to you request. You can combine `$and`, `$or` and use any [condition](#filter-conditions) you need. Make sure it's being sent encoded or just use [`RequestQueryBuilder`](#frontend-usage) 111 | 112 | _Syntax:_ 113 | 114 | > ?s={"name": "Michael"} 115 | 116 | _Some examples:_ 117 | 118 | - Search by field `name` that can be either `null` OR equals `Superman` 119 | 120 | > ?s={"name": {"**\$or**": {"**\$isnull**": true, "**\$eq**": "Superman"}}} 121 | 122 | - Search an entity where `isActive` is `true` AND `createdAt` not equal `2008-10-01T17:04:32` 123 | 124 | > ?s={"**\$and**": [{"isActive": true}, {"createdAt": {"**$ne**": "2008-10-01T17:04:32"}}]} 125 | 126 | ...which is the same as: 127 | 128 | > ?s={"isActive": true, "createdAt": {"**\$ne**": "2008-10-01T17:04:32"}} 129 | 130 | - Search an entity where `isActive` is `false` OR `updatedAt` is not `null` 131 | 132 | > ?s={"**\$or**": [{"isActive": false}, {"updatedAt": {"**$notnull**": true}}]} 133 | 134 | So the amount of combinations is really huge. 135 | 136 | **_Notice:_** if search query param is present, then [filter](#filter) and [or](#or) query params will be ignored. 137 | 138 | ### filter conditions 139 | 140 | - **`$eq`** (`=`, equal) 141 | - **`$ne`** (`!=`, not equal) 142 | - **`$gt`** (`>`, greater than) 143 | - **`$lt`** (`<`, lower that) 144 | - **`$gte`** (`>=`, greater than or equal) 145 | - **`$lte`** (`<=`, lower than or equal) 146 | - **`$cont`** (`LIKE %val%`, contains) 147 | 148 | ### filter 149 | 150 | Adds fields request condition (multiple conditions) to your request. 151 | 152 | _Syntax:_ 153 | 154 | > ?filter=**field**||**\$condition**||**value** 155 | 156 | > ?join=**relation**&filter=**relation**.**field**||**\$condition**||**value** 157 | 158 | **_Notice:_** Using nested filter shall join relation first. 159 | 160 | _Examples:_ 161 | 162 | > ?filter=**name**||**\$eq**||**batman** 163 | 164 | > ?filter=**isVillain**||**\$eq**||**false**&filter=**city**||**\$eq**||**Arkham** (multiple filters are treated as a combination of `AND` type of conditions) 165 | 166 | > ?filter=**shots**||**\$in**||**12**,**26** (some conditions accept multiple values separated by commas) 167 | 168 | > ?filter=**power**||**\$isnull** (some conditions don't accept value) 169 | 170 | ### or 171 | 172 | Adds `OR` conditions to the request. 173 | 174 | _Syntax:_ 175 | 176 | > ?or=**field**||**\$condition**||**value** 177 | 178 | It uses the same [filter conditions](#filter-conditions). 179 | 180 | _Rules and examples:_ 181 | 182 | - If there is only **one** `or` present (without `filter`) then it will be interpreted as simple [filter](#filter): 183 | 184 | > ?or=**name**||**\$eq**||**batman** 185 | 186 | - If there are **multiple** `or` present (without `filter`) then it will be interpreted as a compination of `OR` conditions, as follows: 187 | `WHERE {or} OR {or} OR ...` 188 | 189 | > ?or=**name**||**\$eq**||**batman**&or=**name**||**\$eq**||**joker** 190 | 191 | - If there are **one** `or` and **one** `filter` then it will be interpreted as `OR` condition, as follows: 192 | `WHERE {filter} OR {or}` 193 | 194 | > ?filter=**name**||**\$eq**||**batman**&or=**name**||**\$eq**||**joker** 195 | 196 | - If present **both** `or` and `filter` in any amount (**one** or **miltiple** each) then both interpreted as a combitation of `AND` conditions and compared with each other by `OR` condition, as follows: 197 | `WHERE ({filter} AND {filter} AND ...) OR ({or} AND {or} AND ...)` 198 | 199 | > ?filter=**type**||**\$eq**||**hero**&filter=**status**||**\$eq**||**alive**&or=**type**||**\$eq**||**villain**&or=**status**||**\$eq**||**dead** 200 | 201 | ### sort 202 | 203 | Adds sort by field (by multiple fields) and order to query result. 204 | 205 | _Syntax:_ 206 | 207 | > ?sort=**field**,**ASC|DESC** 208 | 209 | _Examples:_ 210 | 211 | > ?sort=**name**,**ASC** 212 | 213 | > ?sort=**name**,**ASC**&sort=**id**,**DESC** 214 | 215 | ### join 216 | 217 | Receive joined relational objects in GET result (with all or selected fields). You can join as many relations as allowed in your [CrudOptions](https://github.com/nestjsx/crud/wiki/Controllers#join). 218 | 219 | _Syntax:_ 220 | 221 | > ?join=**relation** 222 | 223 | > ?join=**relation**||**field1**,**field2**,... 224 | 225 | > ?join=**relation1**||**field11**,**field12**,...&join=**relation1**.**nested**||**field21**,**field22**,...&join=... 226 | 227 | _Examples:_ 228 | 229 | > ?join=**profile** 230 | 231 | > ?join=**profile**||**firstName**,**email** 232 | 233 | > ?join=**profile**||**firstName**,**email**&join=**notifications**||**content**&join=**tasks** 234 | 235 | > ?join=**relation1**&join=**relation1**.**nested**&join=**relation1**.**nested**.**deepnested** 236 | 237 | **_Notice:_** primary field/column always persists in relational objects. To use nested relations, the parent level **MUST** be set before the child level like example above. 238 | 239 | ### limit 240 | 241 | Receive `N` amount of entities. 242 | 243 | _Syntax:_ 244 | 245 | > ?limit=**number** 246 | 247 | _Example:_ 248 | 249 | > ?limit=**10** 250 | 251 | ### offset 252 | 253 | Limit the amount of received resources 254 | 255 | _Syntax:_ 256 | 257 | > ?offset=**number** 258 | 259 | _Example:_ 260 | 261 | > ?offset=**10** 262 | 263 | ### page 264 | 265 | Receive a portion of limited amount of resources. 266 | 267 | _Syntax:_ 268 | 269 | > ?page=**number** 270 | 271 | _Example:_ 272 | 273 | > ?page=**2** 274 | -------------------------------------------------------------------------------- /admin/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /admin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "admin", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@nestjsx/crud-request": "^5.0.0-alpha.3", 13 | "react": "^18.2.0", 14 | "react-admin": "^4.3.1", 15 | "react-dom": "^18.2.0" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "^18.0.17", 19 | "@types/react-dom": "^18.0.6", 20 | "@vitejs/plugin-react": "^2.1.0", 21 | "typescript": "^4.6.4", 22 | "vite": "^3.1.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /admin/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | } 13 | .logo:hover { 14 | filter: drop-shadow(0 0 2em #646cffaa); 15 | } 16 | .logo.react:hover { 17 | filter: drop-shadow(0 0 2em #61dafbaa); 18 | } 19 | 20 | @keyframes logo-spin { 21 | from { 22 | transform: rotate(0deg); 23 | } 24 | to { 25 | transform: rotate(360deg); 26 | } 27 | } 28 | 29 | @media (prefers-reduced-motion: no-preference) { 30 | a:nth-of-type(2) .logo { 31 | animation: logo-spin infinite 20s linear; 32 | } 33 | } 34 | 35 | .card { 36 | padding: 2em; 37 | } 38 | 39 | .read-the-docs { 40 | color: #888; 41 | } 42 | -------------------------------------------------------------------------------- /admin/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { Admin, fetchUtils, Resource } from 'react-admin' 3 | import { apiProvider } from './config/api.provider' 4 | import { API_URL } from './config/constants' 5 | import { postsResource } from './resources/posts/resource' 6 | 7 | const httpClient = (url: string, options: any = {}) => { 8 | if (!options.headers) { 9 | options.headers = new Headers({ Accept: "application/json" }) 10 | } 11 | const token = localStorage.getItem("token") 12 | options.headers.set("Authorization", `Bearer ${token}`) 13 | return fetchUtils.fetchJson(url, options) 14 | } 15 | 16 | const dataProvider = apiProvider(API_URL, httpClient) 17 | 18 | function App() { 19 | const [count, setCount] = useState(0) 20 | 21 | 22 | return ( 23 | 24 | 25 | 26 | ) 27 | } 28 | 29 | export default App 30 | -------------------------------------------------------------------------------- /admin/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/config/api.provider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RequestQueryBuilder, 3 | QueryFilterArr, 4 | CondOperator, 5 | } from "@nestjsx/crud-request"; 6 | import { 7 | CREATE, 8 | DELETE, 9 | DELETE_MANY, 10 | fetchUtils, 11 | GET_LIST, 12 | GET_MANY, 13 | GET_MANY_REFERENCE, 14 | GET_ONE, 15 | UPDATE, 16 | UPDATE_MANY, 17 | } from "react-admin"; 18 | 19 | export const apiProvider = (apiUrl, httpClient = fetchUtils.fetchJson) => { 20 | const composeFilter = (paramsFilter) => { 21 | if ( 22 | paramsFilter === "" || 23 | (typeof paramsFilter.q !== "undefined" && paramsFilter.q === "") 24 | ) { 25 | paramsFilter = {}; 26 | } 27 | 28 | const flatFilter = fetchUtils.flattenObject(paramsFilter); 29 | const filter = Object.keys(flatFilter).map((key) => { 30 | const splitKey = key.split("||"); 31 | const ops = splitKey[1] ? splitKey[1] : "cont"; 32 | let field = splitKey[0]; 33 | 34 | if (field.indexOf("_") === 0 && field.indexOf(".") > -1) { 35 | field = field.split(/\.(.+)/)[1]; 36 | } 37 | return { field, operator: ops, value: flatFilter[key] }; 38 | }); 39 | console.log("filter -> ", filter); 40 | return filter; 41 | }; 42 | 43 | const convertDataRequestToHTTP = (type, resource, params) => { 44 | let url = ""; 45 | const options: any = {}; 46 | switch (type) { 47 | case GET_LIST: { 48 | const { page, perPage } = params.pagination; 49 | 50 | let query = RequestQueryBuilder.create({ 51 | filter: composeFilter(params.filter) as QueryFilterArr, 52 | }) 53 | .setLimit(perPage) 54 | .setPage(page) 55 | .sortBy(params.sort) 56 | .setOffset((page - 1) * perPage) 57 | .query(); 58 | const regex = /filter%5B\d%5D/gi; 59 | console.log(query.replace(regex, "filter")); 60 | query = query.replaceAll(regex, "filter"); 61 | if (resource === "moderators") { 62 | url = `${apiUrl}/users/moderators?${query}`; 63 | } else { 64 | url = `${apiUrl}/${resource}?${query}`; 65 | } 66 | 67 | break; 68 | } 69 | case GET_ONE: { 70 | if (resource === "moderators") { 71 | url = `${apiUrl}/users/${params.id}`; 72 | } else { 73 | url = `${apiUrl}/${resource}/${params.id}`; 74 | } 75 | break; 76 | } 77 | case GET_MANY: { 78 | const query = RequestQueryBuilder.create() 79 | .setFilter({ 80 | field: "id", 81 | operator: CondOperator.IN, 82 | value: `${params.ids}`, 83 | }) 84 | .query(); 85 | 86 | if (resource === "moderators") { 87 | url = `${apiUrl}/users?${query}`; 88 | } else { 89 | url = `${apiUrl}/${resource}?${query}`; 90 | } 91 | break; 92 | } 93 | case GET_MANY_REFERENCE: { 94 | const { page, perPage } = params.pagination; 95 | const filter = composeFilter(params.filter) as QueryFilterArr; 96 | 97 | filter.push({ 98 | field: params.target, 99 | operator: CondOperator.EQUALS, 100 | value: params.id, 101 | }); 102 | 103 | const query = RequestQueryBuilder.create({ 104 | filter, 105 | }) 106 | .sortBy(params.sort) 107 | .setLimit(perPage) 108 | .setOffset((page - 1) * perPage) 109 | .query(); 110 | 111 | if (resource === "moderators") { 112 | url = `${apiUrl}/users?${query}`; 113 | } else { 114 | url = `${apiUrl}/${resource}?${query}`; 115 | } 116 | break; 117 | } 118 | case UPDATE: { 119 | if (resource === "moderators") { 120 | url = `${apiUrl}/users/${params.id}`; 121 | } else { 122 | url = `${apiUrl}/${resource}/${params.id}`; 123 | } 124 | options.method = "PATCH"; 125 | options.body = JSON.stringify(params.data); 126 | break; 127 | } 128 | case CREATE: { 129 | if (resource === "tickets") { 130 | const formData = new FormData(); 131 | for (const param in params.data) { 132 | // when using multiple files 133 | if (param === "files") { 134 | params.data[param].forEach((file) => { 135 | formData.append("files", file.rawFile); 136 | }); 137 | continue; 138 | } 139 | 140 | formData.append(param, params.data[param]); 141 | } 142 | url = `${apiUrl}/${resource}`; 143 | options.method = "POST"; 144 | options.body = formData; 145 | break; 146 | } else if (resource === "moderators") { 147 | url = `${apiUrl}/users/admin-create`; 148 | } else { 149 | url = `${apiUrl}/${resource}`; 150 | } 151 | options.method = "POST"; 152 | options.body = JSON.stringify(params.data); 153 | break; 154 | } 155 | case DELETE: { 156 | if (resource === "moderators") { 157 | url = `${apiUrl}/users/${params.id}`; 158 | } else { 159 | url = `${apiUrl}/${resource}/${params.id}`; 160 | } 161 | options.method = "DELETE"; 162 | break; 163 | } 164 | default: 165 | throw new Error(`Unsupported fetch action type ${type}`); 166 | } 167 | return { url, options }; 168 | }; 169 | 170 | const convertHTTPResponse = (response, type, resource, params) => { 171 | const { headers, json } = response; 172 | switch (type) { 173 | case GET_LIST: 174 | case GET_MANY_REFERENCE: 175 | return { 176 | data: json.data, 177 | total: json.total, 178 | }; 179 | case CREATE: 180 | return { data: { ...params.data, id: json.id } }; 181 | default: 182 | return { data: json }; 183 | } 184 | }; 185 | 186 | return (type, resource, params) => { 187 | if (type === UPDATE_MANY) { 188 | return Promise.all( 189 | params.ids.map((id) => 190 | httpClient(`${apiUrl}/${resource}/${id}`, { 191 | method: "PUT", 192 | body: JSON.stringify(params.data), 193 | }) 194 | ) 195 | ).then((responses) => ({ 196 | data: responses.map((response) => response.json), 197 | })); 198 | } 199 | if (type === DELETE_MANY) { 200 | return Promise.all( 201 | params.ids.map((id) => 202 | httpClient(`${apiUrl}/${resource}/${id}`, { 203 | method: "DELETE", 204 | }) 205 | ) 206 | ).then((responses) => ({ 207 | data: responses.map((response) => response.json), 208 | })); 209 | } 210 | 211 | const { url, options } = convertDataRequestToHTTP(type, resource, params); 212 | return httpClient(url, options).then((response) => 213 | convertHTTPResponse(response, type, resource, params) 214 | ); 215 | }; 216 | }; 217 | -------------------------------------------------------------------------------- /admin/src/config/constants.ts: -------------------------------------------------------------------------------- 1 | export const API_URL = "http://localhost:8080" 2 | 3 | -------------------------------------------------------------------------------- /admin/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid transparent; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #1a1a1a; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | 59 | @media (prefers-color-scheme: light) { 60 | :root { 61 | color: #213547; 62 | background-color: #ffffff; 63 | } 64 | a:hover { 65 | color: #747bff; 66 | } 67 | button { 68 | background-color: #f9f9f9; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /admin/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | 5 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 6 | 7 | 8 | 9 | ) 10 | -------------------------------------------------------------------------------- /admin/src/resources/posts/components/Create/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Create, SimpleForm, TextInput } from "react-admin" 3 | 4 | export const CategoryCreate = (props) => { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /admin/src/resources/posts/components/Edit/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | BooleanInput, 3 | Edit, 4 | RadioButtonGroupInput, 5 | SimpleForm, 6 | TextInput, 7 | } from "react-admin" 8 | import * as React from "react" 9 | 10 | export const CategoryEdit = (props) => { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /admin/src/resources/posts/components/List/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react" 2 | import { 3 | Datagrid, 4 | List, 5 | TextField, 6 | Pagination, 7 | Filter, 8 | TextInput, 9 | SingleFieldList, 10 | NumberInput, 11 | ChipField, 12 | BooleanField, 13 | ArrayField, 14 | DatagridProps, 15 | } from "react-admin" 16 | import { MobileGrid } from "../MobileGrid" 17 | 18 | const PostPagination = () => ( 19 | 20 | ) 21 | 22 | 23 | // const BlacklistFilter = (props) => ( 24 | // 25 | // 26 | // 27 | // 28 | // ) 29 | 30 | const filters = [ 31 | , 32 | , 33 | ] 34 | 35 | export const CategoryList = () => { 36 | 37 | return ( 38 | } 40 | filters={filters} 41 | hasCreate 42 | perPage={25} 43 | > 44 | 45 | 46 | 47 | 48 | 49 | 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /admin/src/resources/posts/components/MobileGrid/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Card, CardHeader, CardContent } from "@material-ui/core" 3 | import { makeStyles } from "@material-ui/core/styles" 4 | import { 5 | DateField, 6 | EditButton, 7 | NumberField, 8 | TextField, 9 | BooleanField, 10 | useTranslate, 11 | RecordMap, 12 | Identifier, 13 | Record, 14 | } from "react-admin" 15 | 16 | const useListStyles = makeStyles((theme) => ({ 17 | card: { 18 | height: "100%", 19 | display: "flex", 20 | flexDirection: "column", 21 | margin: "0.5rem 0", 22 | }, 23 | cardTitleContent: { 24 | display: "flex", 25 | flexDirection: "row", 26 | alignItems: "center", 27 | justifyContent: "space-between", 28 | }, 29 | cardContent: theme.typography.body1, 30 | cardContentRow: { 31 | display: "flex", 32 | flexDirection: "row", 33 | alignItems: "center", 34 | margin: "0.5rem 0", 35 | }, 36 | })) 37 | 38 | interface MobileGridProps { 39 | ids?: Identifier[] 40 | data?: RecordMap 41 | basePath?: string 42 | } 43 | 44 | export const MobileGrid = (props: MobileGridProps) => { 45 | const { ids, data, basePath } = props 46 | const translate = useTranslate() 47 | const classes = useListStyles() 48 | 49 | if (!ids || !data || !basePath) { 50 | return null 51 | } 52 | 53 | return ( 54 |
55 | {ids.map((id) => ( 56 | 57 | 60 | 61 | المعرف: 62 | 63 | 68 |
69 | } 70 | /> 71 | 72 | 73 | {"الإسم: "} 74 | 75 | 76 | 77 | ))} 78 | 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /admin/src/resources/posts/components/Show/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { 3 | Show, 4 | SimpleShowLayout, 5 | TextField, 6 | EditButton, 7 | TopToolbar, 8 | } from "react-admin" 9 | 10 | 11 | export const CategoryShow = () => 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /admin/src/resources/posts/resource.ts: -------------------------------------------------------------------------------- 1 | import { CategoryList } from "./components/List" 2 | import { CategoryCreate } from "./components/Create" 3 | import { CategoryEdit } from "./components/Edit" 4 | import { CategoryShow } from "./components/Show" 5 | 6 | export const postsResource = { 7 | list: CategoryList, 8 | show: CategoryShow, 9 | edit: CategoryEdit, 10 | create: CategoryCreate, 11 | } 12 | -------------------------------------------------------------------------------- /admin/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /admin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "noImplicitAny": false 19 | }, 20 | "include": ["src"], 21 | "references": [{ "path": "./tsconfig.node.json" }] 22 | } 23 | -------------------------------------------------------------------------------- /admin/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /admin/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /cmd/crudgen/crudgen.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ElegantSoft/go-restful-generator/common" 6 | "github.com/ElegantSoft/go-restful-generator/generators" 7 | "github.com/spf13/cobra" 8 | "log" 9 | "os" 10 | ) 11 | 12 | func main() { 13 | //promptGetServiceName := promptui.Prompt{ 14 | // Label: "service name", 15 | //} 16 | moduleName := common.GetModuleName() 17 | 18 | rootCmd := &cobra.Command{ 19 | Use: "crudgen", 20 | Short: "crudgen gen cli tool", 21 | Run: func(cmd *cobra.Command, args []string) { 22 | log.Println("Please use crudgen init or crudgen service") 23 | }, 24 | } 25 | 26 | initCmd := &cobra.Command{ 27 | Use: "init", 28 | Short: "init new project structure", 29 | Run: func(cmd *cobra.Command, args []string) { 30 | generators.InitNewProject(moduleName) 31 | }, 32 | } 33 | 34 | var serviceName string 35 | var servicePath string 36 | 37 | generateServiceCmd := &cobra.Command{ 38 | Use: "service", 39 | Short: "generate new service", 40 | Run: func(cmd *cobra.Command, args []string) { 41 | if serviceName == "" { 42 | log.Fatal("you must set service name ex: --name posts") 43 | return 44 | } 45 | generators.GenerateService(moduleName, serviceName, servicePath) 46 | }, 47 | } 48 | 49 | rootCmd.PersistentFlags().StringVar(&serviceName, "name", "", "service name ex: posts") 50 | rootCmd.PersistentFlags().StringVar(&servicePath, "path", "", "service path default lib/service-name ex: services/service-name") 51 | 52 | rootCmd.AddCommand(initCmd, generateServiceCmd) 53 | 54 | if err := rootCmd.Execute(); err != nil { 55 | fmt.Println(err) 56 | os.Exit(1) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /common/env_keys.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | const ( 4 | KJwtSecret string = "JWT_SECRET" 5 | KUserHeader string = "user_id" 6 | KAWSSession string = "aws_session" 7 | ) 8 | -------------------------------------------------------------------------------- /common/helpers.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "golang.org/x/mod/modfile" 5 | "os" 6 | "reflect" 7 | ) 8 | 9 | func Unique(intSlice []uint) []uint { 10 | keys := make(map[uint]bool) 11 | var list []uint 12 | for _, entry := range intSlice { 13 | if _, value := keys[entry]; !value { 14 | keys[entry] = true 15 | list = append(list, entry) 16 | } 17 | } 18 | return list 19 | } 20 | 21 | func GetIdFromCtx(id interface{}) uint { 22 | idFloat, _ := id.(float64) 23 | return uint(idFloat) 24 | } 25 | 26 | func Contains(val interface{}, slice interface{}) bool { 27 | found := false 28 | for _, v := range slice.([]uint) { 29 | if v == val { 30 | found = true 31 | } 32 | } 33 | return found 34 | } 35 | func StringsContains(val string, slice []string) bool { 36 | found := false 37 | for _, v := range slice { 38 | if v == val { 39 | found = true 40 | } 41 | } 42 | return found 43 | } 44 | 45 | func HashIntersection(a interface{}, b interface{}) []interface{} { 46 | set := make([]interface{}, 0) 47 | hash := make(map[interface{}]bool) 48 | av := reflect.ValueOf(a) 49 | bv := reflect.ValueOf(b) 50 | 51 | for i := 0; i < av.Len(); i++ { 52 | el := av.Index(i).Interface() 53 | hash[el] = true 54 | } 55 | 56 | for i := 0; i < bv.Len(); i++ { 57 | el := bv.Index(i).Interface() 58 | if _, found := hash[el]; found { 59 | set = append(set, el) 60 | } 61 | } 62 | 63 | return set 64 | } 65 | 66 | func removeDuplicateAdjacent(checkText string) string { 67 | newText := "" 68 | for i := range []rune(checkText) { 69 | if i+1 < len([]rune(checkText)) { 70 | if []rune(checkText)[i] != []rune(checkText)[i+1] { 71 | newText += string([]rune(checkText)[i]) 72 | } 73 | 74 | } else { 75 | newText += string([]rune(checkText)[i]) 76 | } 77 | 78 | } 79 | return newText 80 | } 81 | 82 | func GetModuleName() string { 83 | goModBytes, err := os.ReadFile("go.mod") 84 | if err != nil { 85 | panic(err) 86 | } 87 | 88 | modName := modfile.ModulePath(goModBytes) 89 | 90 | return modName 91 | } 92 | -------------------------------------------------------------------------------- /common/structs.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | type ById struct { 4 | ID string `uri:"id" binding:"required"` 5 | } 6 | -------------------------------------------------------------------------------- /crud/crud_service.go: -------------------------------------------------------------------------------- 1 | package crud 2 | -------------------------------------------------------------------------------- /crud/repository.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | ) 6 | 7 | type Repo[T any] interface { 8 | FindOne(cond *T, dest *T) error 9 | Update(cond *T, updatedColumns *T) error 10 | Delete(cond *T) error 11 | Create(data *T) error 12 | getTx() *gorm.DB 13 | } 14 | 15 | type Repository[T any] struct { 16 | DB *gorm.DB 17 | Model T 18 | } 19 | 20 | func (r *Repository[T]) FindOne(cond *T, dest *T) error { 21 | return r.DB.Where(cond).First(dest).Error 22 | } 23 | 24 | func (r *Repository[T]) Update(cond *T, updatedColumns *T) error { 25 | return r.DB.Model(r.Model).Select("*").Where(cond).Updates(updatedColumns).Error 26 | } 27 | 28 | func (r *Repository[T]) Delete(cond *T) error { 29 | if err := r.DB.Model(r.Model).Delete(cond); err != nil { 30 | return err.Error 31 | } 32 | return nil 33 | } 34 | 35 | func (r *Repository[T]) Create(data *T) error { 36 | return r.DB.Create(data).Error 37 | } 38 | 39 | func (r *Repository[T]) getTx() *gorm.DB { 40 | return r.DB.Model(r.Model) 41 | } 42 | 43 | func NewRepository[T any](db *gorm.DB, model T) Repo[T] { 44 | return &Repository[T]{ 45 | DB: db, 46 | Model: model, 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /crud/router.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "net/http" 7 | 8 | "github.com/ElegantSoft/go-restful-generator/db" 9 | "github.com/ElegantSoft/go-restful-generator/db/models" 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | func RegisterRoutes(routerGroup *gin.RouterGroup) { 14 | repo := NewRepository[models.Post](db.DB, models.Post{}) 15 | s := NewService[models.Post](repo) 16 | 17 | routerGroup.GET("", func(ctx *gin.Context) { 18 | var api GetAllRequest 19 | if err := ctx.ShouldBindQuery(&api); err != nil { 20 | ctx.JSON(400, gin.H{"message": err.Error()}) 21 | return 22 | } 23 | 24 | var result []models.Post 25 | var totalRows int64 26 | err := s.Find(api, &result, &totalRows) 27 | if err != nil { 28 | ctx.JSON(400, gin.H{"message": err.Error()}) 29 | return 30 | } 31 | 32 | var data interface{} 33 | if api.Page > 0 { 34 | data = map[string]interface{}{ 35 | "data": result, 36 | "total": totalRows, 37 | "totalPages": int(math.Ceil(float64(totalRows) / float64(api.Limit))), 38 | } 39 | } else { 40 | data = result 41 | } 42 | ctx.JSON(200, data) 43 | }) 44 | 45 | routerGroup.GET(":id", func(ctx *gin.Context) { 46 | var api GetAllRequest 47 | var item ById 48 | if err := ctx.ShouldBindQuery(&api); err != nil { 49 | ctx.JSON(400, gin.H{"message": err.Error()}) 50 | return 51 | } 52 | if err := ctx.ShouldBindUri(&item); err != nil { 53 | ctx.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) 54 | return 55 | } 56 | 57 | api.Filter = append(api.Filter, fmt.Sprintf("id||$eq||%s", item.ID)) 58 | 59 | var result models.Post 60 | 61 | err := s.FindOne(api, &result) 62 | if err != nil { 63 | ctx.JSON(400, gin.H{"message": err.Error()}) 64 | return 65 | } 66 | ctx.JSON(200, result) 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /crud/service.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "encoding/json" 5 | "gorm.io/gorm" 6 | "strings" 7 | ) 8 | 9 | type Service[T any] struct { 10 | Repo Repo[T] 11 | Qtb *QueryToDBConverter 12 | } 13 | 14 | func (svc *Service[T]) FindTrx(api GetAllRequest) (error, *gorm.DB) { 15 | var s map[string]interface{} 16 | if len(api.S) > 0 { 17 | err := json.Unmarshal([]byte(api.S), &s) 18 | if err != nil { 19 | return err, nil 20 | } 21 | } 22 | 23 | tx := svc.Repo.getTx() 24 | if len(api.Fields) > 0 { 25 | fields := strings.Split(api.Fields, ",") 26 | tx.Select(fields) 27 | } 28 | if len(api.Join) > 0 { 29 | svc.Qtb.relationsMapper(api.Join, tx) 30 | } 31 | 32 | if len(api.Filter) > 0 { 33 | svc.Qtb.filterMapper(api.Filter, tx) 34 | } 35 | 36 | if len(api.Sort) > 0 { 37 | svc.Qtb.sortMapper(api.Sort, tx) 38 | } 39 | 40 | err := svc.Qtb.searchMapper(s, tx) 41 | if err != nil { 42 | return err, nil 43 | } 44 | 45 | tx.Limit(api.Limit) 46 | 47 | return nil, tx 48 | } 49 | 50 | func (svc *Service[T]) Find(api GetAllRequest, result interface{}, totalRows *int64) error { 51 | err, tx := svc.FindTrx(api) 52 | tx.Count(totalRows) 53 | if api.Page > 0 { 54 | tx.Offset((api.Page - 1) * api.Limit) 55 | } 56 | if err != nil { 57 | return err 58 | } 59 | return tx.Find(result).Error 60 | } 61 | 62 | func (svc *Service[T]) FindOne(api GetAllRequest, result interface{}) error { 63 | var s map[string]interface{} 64 | if len(api.S) > 0 { 65 | err := json.Unmarshal([]byte(api.S), &s) 66 | if err != nil { 67 | return err 68 | } 69 | } 70 | 71 | tx := svc.Repo.getTx() 72 | 73 | if len(api.Fields) > 0 { 74 | fields := strings.Split(api.Fields, ",") 75 | tx.Select(fields) 76 | } 77 | if len(api.Join) > 0 { 78 | svc.Qtb.relationsMapper(api.Join, tx) 79 | } 80 | 81 | if len(api.Filter) > 0 { 82 | svc.Qtb.filterMapper(api.Filter, tx) 83 | } 84 | 85 | if len(api.Sort) > 0 { 86 | svc.Qtb.sortMapper(api.Sort, tx) 87 | } 88 | 89 | err := svc.Qtb.searchMapper(s, tx) 90 | if err != nil { 91 | return err 92 | } 93 | return tx.First(result).Error 94 | } 95 | 96 | func (svc *Service[T]) Create(data *T) error { 97 | return svc.Repo.Create(data) 98 | } 99 | 100 | func (svc *Service[T]) Delete(cond *T) error { 101 | return svc.Repo.Delete(cond) 102 | } 103 | 104 | func (svc *Service[T]) Update(cond *T, updatedColumns *T) error { 105 | return svc.Repo.Update(cond, updatedColumns) 106 | } 107 | 108 | func NewService[T any](repo Repo[T]) *Service[T] { 109 | return &Service[T]{ 110 | Repo: repo, 111 | Qtb: &QueryToDBConverter{}, 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /crud/structs.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | const ( 4 | AND = "$and" 5 | OR = "$or" 6 | SEPARATOR = "||" 7 | SortSeparator = "," 8 | ) 9 | 10 | type GetAllRequest struct { 11 | Page int `json:"page" form:"page"` 12 | Limit int `json:"limit" form:"limit"` 13 | Join string `json:"join" form:"join"` 14 | S string `json:"s" form:"s"` 15 | Fields string `json:"fields" form:"fields"` 16 | Filter []string `json:"filter" form:"filter"` 17 | Sort []string `json:"sort" form:"sort"` 18 | } 19 | 20 | var filterConditions = map[string]string{ 21 | "eq": "=", 22 | "ne": "!=", 23 | "gt": ">", 24 | "lt": "<", 25 | "gte": ">=", 26 | "lte": "<=", 27 | "$in": "in", 28 | "cont": "ILIKE", 29 | "isnull": "IS NULL", 30 | "notnull": "IS NOT NULL", 31 | } 32 | 33 | type ById struct { 34 | ID string `uri:"id" binding:"required"` 35 | } 36 | -------------------------------------------------------------------------------- /crud/utils.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "golang.org/x/text/cases" 7 | "golang.org/x/text/language" 8 | "strings" 9 | 10 | "gorm.io/gorm" 11 | ) 12 | 13 | const ( 14 | ContainOperator = "cont" 15 | NotNullOperator = "notnull" 16 | IsNullOperator = "isnull" 17 | InOperator = "$in" 18 | ) 19 | 20 | var AndValueNotSlice = errors.New("the value of $and or $or not array") 21 | 22 | type QueryToDBConverter struct { 23 | } 24 | 25 | func (q *QueryToDBConverter) searchMapper(s map[string]interface{}, tx *gorm.DB) error { 26 | for k := range s { 27 | if k == AND { 28 | vals, ok := s[k].([]interface{}) 29 | if !ok { 30 | return AndValueNotSlice 31 | } 32 | for _, field := range vals { 33 | keyAndVal, ok := field.(map[string]interface{}) 34 | if ok { 35 | for whereField, whereVal := range keyAndVal { 36 | whereValMap, ok := whereVal.(map[string]interface{}) 37 | if ok { 38 | for operatorKey, value := range whereValMap { 39 | operator, ok := filterConditions[operatorKey] 40 | if ok { 41 | if operatorKey == NotNullOperator || operatorKey == IsNullOperator { 42 | tx.Where(fmt.Sprintf("%s %s", whereField, operator)) 43 | } else if operatorKey == InOperator { 44 | valSlice := strings.Split(value.(string), ",") 45 | tx.Where(fmt.Sprintf("%s IN ?", whereField), valSlice) 46 | } else { 47 | 48 | if operatorKey == ContainOperator { 49 | value = fmt.Sprintf("%%%s%%", value) 50 | } 51 | tx.Where(fmt.Sprintf("%s %s ?", whereField, operator), value) 52 | } 53 | } 54 | } 55 | 56 | } else { 57 | 58 | tx.Where(whereField, whereVal) 59 | } 60 | } 61 | } 62 | } 63 | } else if k == OR { 64 | vals, ok := s[k].([]interface{}) 65 | if !ok { 66 | return AndValueNotSlice 67 | } 68 | for i, field := range vals { 69 | keyAndVal, ok := field.(map[string]interface{}) 70 | if ok { 71 | for whereField, whereVal := range keyAndVal { 72 | whereValMap, ok := whereVal.(map[string]interface{}) 73 | if ok { 74 | for operatorKey, value := range whereValMap { 75 | operator, ok := filterConditions[operatorKey] 76 | if ok { 77 | if operatorKey == NotNullOperator || operatorKey == IsNullOperator { 78 | if i == 0 { 79 | tx.Where(fmt.Sprintf("%s %s", whereField, operator)) 80 | } else { 81 | tx.Or(fmt.Sprintf("%s %s", whereField, operator)) 82 | } 83 | } else if operatorKey == InOperator { 84 | if i == 0 { 85 | valSlice := strings.Split(value.(string), ",") 86 | tx.Where(fmt.Sprintf("%s IN ?", whereField), valSlice) 87 | } else { 88 | valSlice := strings.Split(value.(string), ",") 89 | tx.Or(fmt.Sprintf("%s IN ?", whereField), valSlice) 90 | } 91 | } else { 92 | if operatorKey == ContainOperator { 93 | value = fmt.Sprintf("%%%s%%", value) 94 | } 95 | if i == 0 { 96 | tx.Where(fmt.Sprintf("%s %s ?", whereField, operator), value) 97 | } else { 98 | tx.Or(fmt.Sprintf("%s %s ?", whereField, operator), value) 99 | } 100 | } 101 | } 102 | } 103 | 104 | } else { 105 | if i == 0 { 106 | tx.Where(whereField, whereVal) 107 | } else { 108 | tx.Or(whereField, whereVal) 109 | } 110 | } 111 | } 112 | } 113 | } 114 | 115 | } 116 | 117 | } 118 | return nil 119 | } 120 | 121 | func (q *QueryToDBConverter) relationsMapper(joinString string, tx *gorm.DB) { 122 | relations := strings.Split(joinString, ",") 123 | for _, relation := range relations { 124 | nestedRelationsSlice := strings.Split(relation, ".") 125 | titledSlice := make([]string, len(nestedRelationsSlice)) 126 | for i, relation := range nestedRelationsSlice { 127 | titledSlice[i] = cases.Title(language.English, cases.NoLower).String(relation) 128 | } 129 | nestedRelation := strings.Join(titledSlice, ".") 130 | if len(nestedRelation) > 0 { 131 | tx.Preload(nestedRelation) 132 | } 133 | } 134 | } 135 | 136 | func (q *QueryToDBConverter) filterMapper(filters []string, tx *gorm.DB) { 137 | for _, filter := range filters { 138 | filterParams := strings.Split(filter, SEPARATOR) 139 | if len(filterParams) >= 2 { 140 | operator, ok := filterConditions[filterParams[1]] 141 | if ok { 142 | if filterParams[1] == NotNullOperator || filterParams[1] == IsNullOperator { 143 | tx.Where(fmt.Sprintf("%s %s", filterParams[0], operator)) 144 | } else { 145 | if len(filterParams) == 3 { 146 | 147 | if filterParams[1] == ContainOperator { 148 | tx.Where(fmt.Sprintf("%s %s ?", filterParams[0], operator), fmt.Sprintf("%%%s%%", filterParams[2])) 149 | } else if filterParams[1] == InOperator { 150 | valSlice := strings.Split(filterParams[2], ",") 151 | tx.Where(fmt.Sprintf("%s IN ?", filterParams[0]), valSlice) 152 | } else { 153 | tx.Where(fmt.Sprintf("%s %s ?", filterParams[0], operator), filterParams[2]) 154 | 155 | } 156 | } 157 | } 158 | } 159 | } 160 | } 161 | } 162 | 163 | func (q *QueryToDBConverter) sortMapper(sorts []string, tx *gorm.DB) { 164 | for _, sort := range sorts { 165 | sortParams := strings.Split(sort, SortSeparator) 166 | if len(sortParams) == 2 { 167 | tx.Order(fmt.Sprintf("%s %s", sortParams[0], strings.ToLower(sortParams[1]))) 168 | } else { 169 | tx.Order(fmt.Sprintf("%s desc", sortParams[0])) 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /db/database.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "gorm.io/driver/postgres" 5 | "gorm.io/driver/sqlite" 6 | "gorm.io/gorm" 7 | "gorm.io/gorm/logger" 8 | "log" 9 | "os" 10 | "time" 11 | ) 12 | 13 | var DB *gorm.DB 14 | 15 | func Open(dsn string) error { 16 | newLogger := logger.New( 17 | log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer 18 | logger.Config{ 19 | SlowThreshold: time.Second, // Slow SQL threshold 20 | LogLevel: logger.Info, // Log level 21 | Colorful: true, // Disable color 22 | 23 | }, 24 | ) 25 | var err error 26 | DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{ 27 | Logger: newLogger, 28 | SkipDefaultTransaction: true, 29 | PrepareStmt: false, 30 | }) 31 | if err != nil { 32 | return err 33 | } 34 | return nil 35 | } 36 | 37 | func OpenTestDB() error { 38 | newLogger := logger.New( 39 | log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer 40 | logger.Config{ 41 | SlowThreshold: time.Second, // Slow SQL threshold 42 | LogLevel: logger.Info, // Log level 43 | Colorful: true, // Disable color 44 | 45 | }, 46 | ) 47 | var err error 48 | DB, err = gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{ 49 | Logger: newLogger, 50 | SkipDefaultTransaction: true, 51 | PrepareStmt: true, 52 | }) 53 | if err != nil { 54 | return err 55 | } 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /db/migrations.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | func AddUUIDExtension() { 4 | DB.Exec(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp";`) 5 | } 6 | -------------------------------------------------------------------------------- /db/models/post.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "time" 6 | ) 7 | 8 | type Post struct { 9 | ID uuid.UUID `json:"id,omitempty" gorm:"type:uuid; default:uuid_generate_v4()"` 10 | Title string `json:"title,omitempty"` 11 | Description string `json:"description,omitempty"` 12 | CategoryID uuid.NullUUID `json:"category_id,omitempty"` 13 | Category *Category `json:"category,omitempty"` 14 | Price uint32 `json:"price,omitempty"` 15 | UpdatedAt time.Time `json:"updated_at,omitempty"` 16 | CreatedAt time.Time `json:"created_at,omitempty"` 17 | } 18 | 19 | type Category struct { 20 | ID uuid.UUID `json:"id,omitempty" gorm:"type:uuid; default:uuid_generate_v4()"` 21 | Name string `json:"name,omitempty"` 22 | Posts *[]Post `json:"posts,omitempty"` 23 | UpdatedAt time.Time `json:"updated_at,omitempty"` 24 | CreatedAt time.Time `json:"created_at,omitempty"` 25 | } 26 | -------------------------------------------------------------------------------- /db/seed/posts.go: -------------------------------------------------------------------------------- 1 | package seed 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/ElegantSoft/go-restful-generator/db" 7 | "github.com/ElegantSoft/go-restful-generator/db/models" 8 | "github.com/bxcodec/faker/v3" 9 | "github.com/google/uuid" 10 | ) 11 | 12 | func SeedPosts() { 13 | for i := 0; i < 20; i++ { 14 | cat := models.Category{ 15 | Name: faker.Word(), 16 | } 17 | log.Printf("will create cat %v %v", i, cat) 18 | err := db.DB.Create(&cat).Error 19 | if err != nil { 20 | log.Printf("error while seed category %+v", err) 21 | } 22 | for l := 0; l < 50; l++ { 23 | post := models.Post{ 24 | Title: faker.Word(), 25 | Description: faker.Paragraph(), 26 | CategoryID: uuid.NullUUID{ 27 | UUID: cat.ID, 28 | Valid: true, 29 | }, 30 | Price: uint32(l + 1*i + 1), 31 | } 32 | err := db.DB.Create(&post).Error 33 | if err != nil { 34 | log.Printf("error while seed post %+v", err) 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docs/docs.go: -------------------------------------------------------------------------------- 1 | // Package docs GENERATED BY SWAG; DO NOT EDIT 2 | // This file was generated by swaggo/swag 3 | package docs 4 | 5 | import "github.com/swaggo/swag" 6 | 7 | const docTemplate = `{ 8 | "schemes": {{ marshal .Schemes }}, 9 | "swagger": "2.0", 10 | "info": { 11 | "description": "{{escape .Description}}", 12 | "title": "{{.Title}}", 13 | "contact": { 14 | "name": "API Support", 15 | "url": "http://www.swagger.io/support", 16 | "email": "support@swagger.io" 17 | }, 18 | "license": { 19 | "name": "Apache 2.0", 20 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 21 | }, 22 | "version": "{{.Version}}" 23 | }, 24 | "host": "{{.Host}}", 25 | "basePath": "{{.BasePath}}", 26 | "paths": { 27 | "/posts": { 28 | "get": { 29 | "tags": [ 30 | "posts" 31 | ], 32 | "parameters": [ 33 | { 34 | "type": "string", 35 | "description": "{'$and': [ {'title': { '$cont':'cul' } } ]}", 36 | "name": "s", 37 | "in": "query" 38 | }, 39 | { 40 | "type": "string", 41 | "description": "fields to select eg: name,age", 42 | "name": "fields", 43 | "in": "query" 44 | }, 45 | { 46 | "type": "integer", 47 | "description": "page of pagination", 48 | "name": "page", 49 | "in": "query" 50 | }, 51 | { 52 | "type": "integer", 53 | "description": "limit of pagination", 54 | "name": "limit", 55 | "in": "query" 56 | }, 57 | { 58 | "type": "string", 59 | "description": "join relations eg: category, parent", 60 | "name": "join", 61 | "in": "query" 62 | }, 63 | { 64 | "type": "array", 65 | "items": { 66 | "type": "string" 67 | }, 68 | "description": "filters eg: name||$eq||ad price||$gte||200", 69 | "name": "filter", 70 | "in": "query" 71 | }, 72 | { 73 | "type": "array", 74 | "items": { 75 | "type": "string" 76 | }, 77 | "description": "filters eg: created_at,desc title,asc", 78 | "name": "sort", 79 | "in": "query" 80 | } 81 | ], 82 | "responses": { 83 | "200": { 84 | "description": "OK", 85 | "schema": { 86 | "type": "array", 87 | "items": { 88 | "$ref": "#/definitions/posts.model" 89 | } 90 | } 91 | } 92 | } 93 | }, 94 | "post": { 95 | "tags": [ 96 | "posts" 97 | ], 98 | "parameters": [ 99 | { 100 | "description": "item to create", 101 | "name": "{object}", 102 | "in": "body", 103 | "required": true, 104 | "schema": { 105 | "$ref": "#/definitions/posts.model" 106 | } 107 | } 108 | ], 109 | "responses": { 110 | "201": { 111 | "description": "Created", 112 | "schema": { 113 | "$ref": "#/definitions/posts.model" 114 | } 115 | } 116 | } 117 | } 118 | }, 119 | "/posts/{id}": { 120 | "get": { 121 | "tags": [ 122 | "posts" 123 | ], 124 | "parameters": [ 125 | { 126 | "type": "string", 127 | "description": "uuid of item", 128 | "name": "id", 129 | "in": "path", 130 | "required": true 131 | } 132 | ], 133 | "responses": { 134 | "200": { 135 | "description": "OK", 136 | "schema": { 137 | "$ref": "#/definitions/posts.model" 138 | } 139 | } 140 | } 141 | }, 142 | "put": { 143 | "tags": [ 144 | "posts" 145 | ], 146 | "parameters": [ 147 | { 148 | "type": "string", 149 | "description": "uuid of item", 150 | "name": "id", 151 | "in": "path", 152 | "required": true 153 | }, 154 | { 155 | "description": "update body", 156 | "name": "item", 157 | "in": "body", 158 | "required": true, 159 | "schema": { 160 | "$ref": "#/definitions/posts.model" 161 | } 162 | } 163 | ], 164 | "responses": { 165 | "200": { 166 | "description": "ok", 167 | "schema": { 168 | "type": "string" 169 | } 170 | } 171 | } 172 | }, 173 | "delete": { 174 | "tags": [ 175 | "posts" 176 | ], 177 | "parameters": [ 178 | { 179 | "type": "string", 180 | "description": "uuid of item", 181 | "name": "id", 182 | "in": "path", 183 | "required": true 184 | } 185 | ], 186 | "responses": { 187 | "200": { 188 | "description": "ok", 189 | "schema": { 190 | "type": "string" 191 | } 192 | } 193 | } 194 | } 195 | } 196 | }, 197 | "definitions": { 198 | "models.Category": { 199 | "type": "object", 200 | "properties": { 201 | "created_at": { 202 | "type": "string" 203 | }, 204 | "id": { 205 | "type": "string" 206 | }, 207 | "name": { 208 | "type": "string" 209 | }, 210 | "posts": { 211 | "type": "array", 212 | "items": { 213 | "$ref": "#/definitions/models.Post" 214 | } 215 | }, 216 | "updated_at": { 217 | "type": "string" 218 | } 219 | } 220 | }, 221 | "models.Post": { 222 | "type": "object", 223 | "properties": { 224 | "category": { 225 | "$ref": "#/definitions/models.Category" 226 | }, 227 | "category_id": { 228 | "type": "string" 229 | }, 230 | "created_at": { 231 | "type": "string" 232 | }, 233 | "description": { 234 | "type": "string" 235 | }, 236 | "id": { 237 | "type": "string" 238 | }, 239 | "price": { 240 | "type": "integer" 241 | }, 242 | "title": { 243 | "type": "string" 244 | }, 245 | "updated_at": { 246 | "type": "string" 247 | } 248 | } 249 | }, 250 | "posts.model": { 251 | "type": "object", 252 | "properties": { 253 | "category": { 254 | "$ref": "#/definitions/models.Category" 255 | }, 256 | "category_id": { 257 | "type": "string" 258 | }, 259 | "created_at": { 260 | "type": "string" 261 | }, 262 | "description": { 263 | "type": "string" 264 | }, 265 | "id": { 266 | "type": "string" 267 | }, 268 | "price": { 269 | "type": "integer" 270 | }, 271 | "title": { 272 | "type": "string" 273 | }, 274 | "updated_at": { 275 | "type": "string" 276 | } 277 | } 278 | } 279 | } 280 | }` 281 | 282 | // SwaggerInfo holds exported Swagger Info so clients can modify it 283 | var SwaggerInfo = &swag.Spec{ 284 | Version: "", 285 | Host: "", 286 | BasePath: "", 287 | Schemes: []string{}, 288 | Title: "", 289 | Description: "", 290 | InfoInstanceName: "swagger", 291 | SwaggerTemplate: docTemplate, 292 | } 293 | 294 | func init() { 295 | swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) 296 | } 297 | -------------------------------------------------------------------------------- /docs/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "contact": { 5 | "name": "API Support", 6 | "url": "http://www.swagger.io/support", 7 | "email": "support@swagger.io" 8 | }, 9 | "license": { 10 | "name": "Apache 2.0", 11 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 12 | } 13 | }, 14 | "paths": { 15 | "/posts": { 16 | "get": { 17 | "tags": [ 18 | "posts" 19 | ], 20 | "parameters": [ 21 | { 22 | "type": "string", 23 | "description": "{'$and': [ {'title': { '$cont':'cul' } } ]}", 24 | "name": "s", 25 | "in": "query" 26 | }, 27 | { 28 | "type": "string", 29 | "description": "fields to select eg: name,age", 30 | "name": "fields", 31 | "in": "query" 32 | }, 33 | { 34 | "type": "integer", 35 | "description": "page of pagination", 36 | "name": "page", 37 | "in": "query" 38 | }, 39 | { 40 | "type": "integer", 41 | "description": "limit of pagination", 42 | "name": "limit", 43 | "in": "query" 44 | }, 45 | { 46 | "type": "string", 47 | "description": "join relations eg: category, parent", 48 | "name": "join", 49 | "in": "query" 50 | }, 51 | { 52 | "type": "array", 53 | "items": { 54 | "type": "string" 55 | }, 56 | "description": "filters eg: name||$eq||ad price||$gte||200", 57 | "name": "filter", 58 | "in": "query" 59 | }, 60 | { 61 | "type": "array", 62 | "items": { 63 | "type": "string" 64 | }, 65 | "description": "filters eg: created_at,desc title,asc", 66 | "name": "sort", 67 | "in": "query" 68 | } 69 | ], 70 | "responses": { 71 | "200": { 72 | "description": "OK", 73 | "schema": { 74 | "type": "array", 75 | "items": { 76 | "$ref": "#/definitions/posts.model" 77 | } 78 | } 79 | } 80 | } 81 | }, 82 | "post": { 83 | "tags": [ 84 | "posts" 85 | ], 86 | "parameters": [ 87 | { 88 | "description": "item to create", 89 | "name": "{object}", 90 | "in": "body", 91 | "required": true, 92 | "schema": { 93 | "$ref": "#/definitions/posts.model" 94 | } 95 | } 96 | ], 97 | "responses": { 98 | "201": { 99 | "description": "Created", 100 | "schema": { 101 | "$ref": "#/definitions/posts.model" 102 | } 103 | } 104 | } 105 | } 106 | }, 107 | "/posts/{id}": { 108 | "get": { 109 | "tags": [ 110 | "posts" 111 | ], 112 | "parameters": [ 113 | { 114 | "type": "string", 115 | "description": "uuid of item", 116 | "name": "id", 117 | "in": "path", 118 | "required": true 119 | } 120 | ], 121 | "responses": { 122 | "200": { 123 | "description": "OK", 124 | "schema": { 125 | "$ref": "#/definitions/posts.model" 126 | } 127 | } 128 | } 129 | }, 130 | "put": { 131 | "tags": [ 132 | "posts" 133 | ], 134 | "parameters": [ 135 | { 136 | "type": "string", 137 | "description": "uuid of item", 138 | "name": "id", 139 | "in": "path", 140 | "required": true 141 | }, 142 | { 143 | "description": "update body", 144 | "name": "item", 145 | "in": "body", 146 | "required": true, 147 | "schema": { 148 | "$ref": "#/definitions/posts.model" 149 | } 150 | } 151 | ], 152 | "responses": { 153 | "200": { 154 | "description": "ok", 155 | "schema": { 156 | "type": "string" 157 | } 158 | } 159 | } 160 | }, 161 | "delete": { 162 | "tags": [ 163 | "posts" 164 | ], 165 | "parameters": [ 166 | { 167 | "type": "string", 168 | "description": "uuid of item", 169 | "name": "id", 170 | "in": "path", 171 | "required": true 172 | } 173 | ], 174 | "responses": { 175 | "200": { 176 | "description": "ok", 177 | "schema": { 178 | "type": "string" 179 | } 180 | } 181 | } 182 | } 183 | } 184 | }, 185 | "definitions": { 186 | "models.Category": { 187 | "type": "object", 188 | "properties": { 189 | "created_at": { 190 | "type": "string" 191 | }, 192 | "id": { 193 | "type": "string" 194 | }, 195 | "name": { 196 | "type": "string" 197 | }, 198 | "posts": { 199 | "type": "array", 200 | "items": { 201 | "$ref": "#/definitions/models.Post" 202 | } 203 | }, 204 | "updated_at": { 205 | "type": "string" 206 | } 207 | } 208 | }, 209 | "models.Post": { 210 | "type": "object", 211 | "properties": { 212 | "category": { 213 | "$ref": "#/definitions/models.Category" 214 | }, 215 | "category_id": { 216 | "type": "string" 217 | }, 218 | "created_at": { 219 | "type": "string" 220 | }, 221 | "description": { 222 | "type": "string" 223 | }, 224 | "id": { 225 | "type": "string" 226 | }, 227 | "price": { 228 | "type": "integer" 229 | }, 230 | "title": { 231 | "type": "string" 232 | }, 233 | "updated_at": { 234 | "type": "string" 235 | } 236 | } 237 | }, 238 | "posts.model": { 239 | "type": "object", 240 | "properties": { 241 | "category": { 242 | "$ref": "#/definitions/models.Category" 243 | }, 244 | "category_id": { 245 | "type": "string" 246 | }, 247 | "created_at": { 248 | "type": "string" 249 | }, 250 | "description": { 251 | "type": "string" 252 | }, 253 | "id": { 254 | "type": "string" 255 | }, 256 | "price": { 257 | "type": "integer" 258 | }, 259 | "title": { 260 | "type": "string" 261 | }, 262 | "updated_at": { 263 | "type": "string" 264 | } 265 | } 266 | } 267 | } 268 | } -------------------------------------------------------------------------------- /docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | definitions: 2 | models.Category: 3 | properties: 4 | created_at: 5 | type: string 6 | id: 7 | type: string 8 | name: 9 | type: string 10 | posts: 11 | items: 12 | $ref: '#/definitions/models.Post' 13 | type: array 14 | updated_at: 15 | type: string 16 | type: object 17 | models.Post: 18 | properties: 19 | category: 20 | $ref: '#/definitions/models.Category' 21 | category_id: 22 | type: string 23 | created_at: 24 | type: string 25 | description: 26 | type: string 27 | id: 28 | type: string 29 | price: 30 | type: integer 31 | title: 32 | type: string 33 | updated_at: 34 | type: string 35 | type: object 36 | posts.model: 37 | properties: 38 | category: 39 | $ref: '#/definitions/models.Category' 40 | category_id: 41 | type: string 42 | created_at: 43 | type: string 44 | description: 45 | type: string 46 | id: 47 | type: string 48 | price: 49 | type: integer 50 | title: 51 | type: string 52 | updated_at: 53 | type: string 54 | type: object 55 | info: 56 | contact: 57 | email: support@swagger.io 58 | name: API Support 59 | url: http://www.swagger.io/support 60 | license: 61 | name: Apache 2.0 62 | url: http://www.apache.org/licenses/LICENSE-2.0.html 63 | paths: 64 | /posts: 65 | get: 66 | parameters: 67 | - description: '{''$and'': [ {''title'': { ''$cont'':''cul'' } } ]}' 68 | in: query 69 | name: s 70 | type: string 71 | - description: 'fields to select eg: name,age' 72 | in: query 73 | name: fields 74 | type: string 75 | - description: page of pagination 76 | in: query 77 | name: page 78 | type: integer 79 | - description: limit of pagination 80 | in: query 81 | name: limit 82 | type: integer 83 | - description: 'join relations eg: category, parent' 84 | in: query 85 | name: join 86 | type: string 87 | - description: 'filters eg: name||$eq||ad price||$gte||200' 88 | in: query 89 | items: 90 | type: string 91 | name: filter 92 | type: array 93 | - description: 'filters eg: created_at,desc title,asc' 94 | in: query 95 | items: 96 | type: string 97 | name: sort 98 | type: array 99 | responses: 100 | "200": 101 | description: OK 102 | schema: 103 | items: 104 | $ref: '#/definitions/posts.model' 105 | type: array 106 | tags: 107 | - posts 108 | post: 109 | parameters: 110 | - description: item to create 111 | in: body 112 | name: '{object}' 113 | required: true 114 | schema: 115 | $ref: '#/definitions/posts.model' 116 | responses: 117 | "201": 118 | description: Created 119 | schema: 120 | $ref: '#/definitions/posts.model' 121 | tags: 122 | - posts 123 | /posts/{id}: 124 | delete: 125 | parameters: 126 | - description: uuid of item 127 | in: path 128 | name: id 129 | required: true 130 | type: string 131 | responses: 132 | "200": 133 | description: ok 134 | schema: 135 | type: string 136 | tags: 137 | - posts 138 | get: 139 | parameters: 140 | - description: uuid of item 141 | in: path 142 | name: id 143 | required: true 144 | type: string 145 | responses: 146 | "200": 147 | description: OK 148 | schema: 149 | $ref: '#/definitions/posts.model' 150 | tags: 151 | - posts 152 | put: 153 | parameters: 154 | - description: uuid of item 155 | in: path 156 | name: id 157 | required: true 158 | type: string 159 | - description: update body 160 | in: body 161 | name: item 162 | required: true 163 | schema: 164 | $ref: '#/definitions/posts.model' 165 | responses: 166 | "200": 167 | description: ok 168 | schema: 169 | type: string 170 | tags: 171 | - posts 172 | swagger: "2.0" 173 | -------------------------------------------------------------------------------- /generators/create_service.go: -------------------------------------------------------------------------------- 1 | package generators 2 | 3 | import ( 4 | _ "embed" 5 | "github.com/ElegantSoft/go-restful-generator/pkg/writetemplate" 6 | "github.com/iancoleman/strcase" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | ) 11 | 12 | //go:embed templates/routes.tmpl 13 | var routerTemplate string 14 | 15 | //go:embed templates/controller.tmpl 16 | var controllerTemplate string 17 | 18 | //go:embed templates/service.tmpl 19 | var serviceTemplate string 20 | 21 | //go:embed templates/repository.tmpl 22 | var repositoryTemplate string 23 | 24 | //go:embed templates/model.tmpl 25 | var modelTemplate string 26 | 27 | func GenerateService(packageName string, serviceName string, servicePath string) { 28 | 29 | type Data struct { 30 | PackageName string 31 | ServiceName string 32 | } 33 | data := Data{PackageName: packageName, ServiceName: serviceName} 34 | if servicePath == "" { 35 | servicePath = "lib/" + strcase.ToKebab(serviceName) 36 | } 37 | 38 | err := os.MkdirAll(servicePath, os.ModePerm) 39 | if err != nil { 40 | log.Fatal(err) 41 | return 42 | } 43 | 44 | writetemplate.ProcessTemplate(routerTemplate, "router.tmpl", filepath.Join(servicePath, "router.go"), data) 45 | writetemplate.ProcessTemplate(controllerTemplate, "controller.tmpl", filepath.Join(servicePath, "controller.go"), data) 46 | writetemplate.ProcessTemplate(serviceTemplate, "service.tmpl", filepath.Join(servicePath, "service.go"), data) 47 | writetemplate.ProcessTemplate(repositoryTemplate, "repository.tmpl", filepath.Join(servicePath, "repository.go"), data) 48 | writetemplate.ProcessTemplate(modelTemplate, "model.tmpl", filepath.Join("db/models", strcase.ToSnake(serviceName)+".go"), data) 49 | 50 | } 51 | -------------------------------------------------------------------------------- /generators/init_project.go: -------------------------------------------------------------------------------- 1 | package generators 2 | 3 | import ( 4 | _ "embed" 5 | "github.com/ElegantSoft/go-restful-generator/pkg/writetemplate" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | //go:embed templates/main.tmpl 11 | var mainTemplate string 12 | 13 | //go:embed templates/db/database.tmpl 14 | var databaseTemplate string 15 | 16 | //go:embed templates/db/migrations.tmpl 17 | var migrationsTemplate string 18 | 19 | //go:embed templates/env.tmpl 20 | var envTemplate string 21 | 22 | //go:embed templates/gitignore.tmpl 23 | var gitignoreTemplate string 24 | 25 | func InitNewProject(packageName string) { 26 | 27 | type Data struct { 28 | PackageName string 29 | } 30 | data := Data{PackageName: packageName} 31 | 32 | writetemplate.ProcessTemplate(mainTemplate, "main.tmpl", filepath.Join("main.go"), data) 33 | err := os.Mkdir("db/models", os.ModePerm) 34 | if err != nil { 35 | panic(err) 36 | } 37 | err = os.Mkdir("lib", os.ModePerm) 38 | if err != nil { 39 | panic(err) 40 | } 41 | writetemplate.ProcessTemplate(databaseTemplate, "database.tmpl", filepath.Join("db/database.go"), data) 42 | writetemplate.ProcessTemplate(migrationsTemplate, "migrations.tmpl", filepath.Join("db/migrations.go"), data) 43 | writetemplate.ProcessTemplate(envTemplate, "env.tmpl", filepath.Join(".env"), data) 44 | writetemplate.ProcessTemplate(gitignoreTemplate, "gitignore.tmpl", filepath.Join(".gitignore"), data) 45 | 46 | } 47 | -------------------------------------------------------------------------------- /generators/templates/controller.tmpl: -------------------------------------------------------------------------------- 1 | package {{.ServiceName | camelcase | lower}} 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "net/http" 7 | 8 | "{{.PackageName}}/common" 9 | "github.com/ElegantSoft/go-restful-generator/crud" 10 | "github.com/gin-gonic/gin" 11 | "github.com/google/uuid" 12 | ) 13 | 14 | type Controller struct { 15 | service *Service 16 | } 17 | 18 | // @Success 200 {array} model 19 | // @Tags {{.ServiceName}} 20 | // @param s query string false "{'and': [ {'title': { 'cont':'cul' } } ]}" 21 | // @param fields query string false "fields to select eg: name,age" 22 | // @param page query int false "page of pagination" 23 | // @param limit query int false "limit of pagination" 24 | // @param join query string false "join relations eg: category, parent" 25 | // @param filter query []string false "filters eg: name||eq||ad price||gte||200" 26 | // @param sort query []string false "filters eg: created_at,desc title,asc" 27 | // @Router /api/v1/{{.ServiceName | kebabcase}} [get] 28 | func (c *Controller) findAll(ctx *gin.Context) { 29 | var api crud.GetAllRequest 30 | if api.Limit == 0 { 31 | api.Limit = 20 32 | } 33 | if err := ctx.ShouldBindQuery(&api); err != nil { 34 | ctx.JSON(400, gin.H{"message": err.Error()}) 35 | return 36 | } 37 | 38 | var result []model 39 | var totalRows int64 40 | err := c.service.Find(api, &result, &totalRows) 41 | if err != nil { 42 | ctx.JSON(400, gin.H{"message": err.Error()}) 43 | return 44 | } 45 | 46 | var data interface{} 47 | if api.Page > 0 { 48 | data = map[string]interface{}{ 49 | "data": result, 50 | "total": totalRows, 51 | "totalPages": int(math.Ceil(float64(totalRows) / float64(api.Limit))), 52 | } 53 | } else { 54 | data = result 55 | } 56 | ctx.JSON(200, data) 57 | } 58 | 59 | // @Success 200 {object} model 60 | // @Tags {{.ServiceName}} 61 | // @param id path string true "uuid of item" 62 | // @Router /api/v1/{{.ServiceName | kebabcase}}/{id} [get] 63 | func (c *Controller) findOne(ctx *gin.Context) { 64 | var api crud.GetAllRequest 65 | var item common.ById 66 | if err := ctx.ShouldBindQuery(&api); err != nil { 67 | ctx.JSON(400, gin.H{"message": err.Error()}) 68 | return 69 | } 70 | if err := ctx.ShouldBindUri(&item); err != nil { 71 | ctx.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) 72 | return 73 | } 74 | 75 | api.Filter = append(api.Filter, fmt.Sprintf("id||eq||%s", item.ID)) 76 | 77 | var result model 78 | 79 | err := c.service.FindOne(api, &result) 80 | if err != nil { 81 | ctx.JSON(400, gin.H{"message": err.Error()}) 82 | return 83 | } 84 | ctx.JSON(200, result) 85 | } 86 | 87 | // @Success 201 {object} model 88 | // @Tags {{.ServiceName}} 89 | // @param {object} body model true "item to create" 90 | // @Router /api/v1/{{.ServiceName | kebabcase}} [post] 91 | func (c *Controller) create(ctx *gin.Context) { 92 | var item model 93 | if err := ctx.ShouldBind(&item); err != nil { 94 | ctx.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) 95 | return 96 | } 97 | err := c.service.Create(&item) 98 | if err != nil { 99 | ctx.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) 100 | return 101 | } 102 | ctx.JSON(http.StatusCreated, gin.H{"data": item}) 103 | } 104 | 105 | // @Success 200 {string} string "ok" 106 | // @Tags {{.ServiceName}} 107 | // @param id path string true "uuid of item" 108 | // @Router /api/v1/{{.ServiceName | kebabcase}}/{id} [delete] 109 | func (c *Controller) delete(ctx *gin.Context) { 110 | var item common.ById 111 | if err := ctx.ShouldBindUri(&item); err != nil { 112 | ctx.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) 113 | return 114 | } 115 | 116 | id, err := uuid.ParseBytes([]byte(item.ID)) 117 | if err != nil { 118 | ctx.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) 119 | return 120 | } 121 | err = c.service.Delete(&model{ID: id}) 122 | if err != nil { 123 | ctx.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) 124 | return 125 | } 126 | ctx.JSON(http.StatusOK, gin.H{"message": "deleted"}) 127 | } 128 | 129 | // @Success 200 {string} string "ok" 130 | // @Tags {{.ServiceName}} 131 | // @param id path string true "uuid of item" 132 | // @param item body model true "update body" 133 | // @Router /api/v1/{{.ServiceName | kebabcase}}/{id} [patch] 134 | func (c *Controller) update(ctx *gin.Context) { 135 | var item model 136 | var byId common.ById 137 | if err := ctx.ShouldBind(&item); err != nil { 138 | ctx.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) 139 | return 140 | } 141 | if err := ctx.ShouldBindUri(&byId); err != nil { 142 | ctx.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) 143 | return 144 | } 145 | id, err := uuid.Parse(byId.ID) 146 | if err != nil { 147 | ctx.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) 148 | return 149 | } 150 | if err := ctx.ShouldBindUri(&byId); err != nil { 151 | ctx.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) 152 | return 153 | } 154 | err = c.service.Update(&model{ID: id}, &item) 155 | if err != nil { 156 | ctx.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) 157 | return 158 | } 159 | ctx.JSON(http.StatusOK, item) 160 | } 161 | 162 | func NewController(service *Service) *Controller { 163 | return &Controller{ 164 | service: service, 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /generators/templates/db/database.tmpl: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "gorm.io/driver/sqlite" 5 | "gorm.io/driver/postgres" 6 | "gorm.io/gorm" 7 | "gorm.io/gorm/logger" 8 | "log" 9 | "os" 10 | "time" 11 | ) 12 | 13 | var DB *gorm.DB 14 | 15 | func Open(dsn string) error { 16 | newLogger := logger.New( 17 | log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer 18 | logger.Config{ 19 | SlowThreshold: time.Second, // Slow SQL threshold 20 | LogLevel: logger.Info, // Log level 21 | Colorful: true, // Disable color 22 | 23 | }, 24 | ) 25 | var err error 26 | DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{ 27 | Logger: newLogger, 28 | SkipDefaultTransaction: true, 29 | PrepareStmt: false, 30 | }) 31 | if err != nil { 32 | return err 33 | } 34 | return nil 35 | } 36 | 37 | func OpenTestDB() error { 38 | newLogger := logger.New( 39 | log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer 40 | logger.Config{ 41 | SlowThreshold: time.Second, // Slow SQL threshold 42 | LogLevel: logger.Info, // Log level 43 | Colorful: true, // Disable color 44 | 45 | }, 46 | ) 47 | var err error 48 | DB, err = gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{ 49 | Logger: newLogger, 50 | SkipDefaultTransaction: true, 51 | PrepareStmt: true, 52 | }) 53 | if err != nil { 54 | return err 55 | } 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /generators/templates/db/migrations.tmpl: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | func AddUUIDExtension() { 4 | DB.Exec(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp";`) 5 | } 6 | -------------------------------------------------------------------------------- /generators/templates/env.tmpl: -------------------------------------------------------------------------------- 1 | DB_URL="host=localhost user=root password=password dbname=db port=5432 sslmode=disable" 2 | -------------------------------------------------------------------------------- /generators/templates/gitignore.tmpl: -------------------------------------------------------------------------------- 1 | .idea 2 | tmp 3 | .env 4 | *.local.db 5 | -------------------------------------------------------------------------------- /generators/templates/main.tmpl: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "{{.PackageName}}/db" 7 | 8 | "github.com/gin-contrib/cors" 9 | "github.com/gin-gonic/gin" 10 | "github.com/joho/godotenv" 11 | swaggerFiles "github.com/swaggo/files" 12 | ginSwagger "github.com/swaggo/gin-swagger" 13 | _ "{{.PackageName}}/docs" 14 | ) 15 | 16 | // @contact.name API Support 17 | // @contact.url http://www.swagger.io/support 18 | // @contact.email support@swagger.io 19 | 20 | // @license.name Apache 2.0 21 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html 22 | func main() { 23 | if err := godotenv.Load(); err != nil { 24 | log.Fatal("Error loading .env file") 25 | } 26 | server := gin.New() 27 | server.Use(gin.Recovery()) 28 | 29 | config := cors.DefaultConfig() 30 | // config.AllowOrigins = []string{"http://localhost:5173"} 31 | config.AllowAllOrigins = true 32 | config.AddAllowHeaders("Authorization") 33 | 34 | server.Use(cors.New(config)) 35 | 36 | if os.Getenv("GIN_MODE") == "debug" { 37 | server.Use(gin.Logger()) 38 | } 39 | gin.SetMode(os.Getenv("GIN_MODE")) 40 | 41 | if err := db.Open(os.Getenv("DB_URL")); err != nil { 42 | log.Fatal(err) 43 | } 44 | log.Println("server started") 45 | 46 | server.GET("", func(ctx *gin.Context) { 47 | ctx.JSON(200, gin.H{"message": "ok 5"}) 48 | }) 49 | 50 | // migrations 51 | // TODO: run this migration if u need uuid support for postgres 52 | // db.AddUUIDExtension() 53 | 54 | // if err := db.DB.AutoMigrate( 55 | // TODO: add models here to auto migrate 56 | // ); err != nil { 57 | // log.Fatal(err) 58 | // } 59 | 60 | server.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) 61 | 62 | 63 | err := server.Run() 64 | if err != nil { 65 | log.Printf("error while starting server %+v", err) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /generators/templates/model.tmpl: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "time" 6 | ) 7 | 8 | 9 | type {{.ServiceName | camelcase | title}} struct { 10 | ID uuid.UUID `json:"id,omitempty" gorm:"type:uuid; default:uuid_generate_v4()"` 11 | UpdatedAt time.Time `json:"updated_at,omitempty"` 12 | CreatedAt time.Time `json:"created_at,omitempty"` 13 | } 14 | -------------------------------------------------------------------------------- /generators/templates/repository.tmpl: -------------------------------------------------------------------------------- 1 | package {{.ServiceName | camelcase | lower}} 2 | 3 | import ( 4 | "github.com/ElegantSoft/go-restful-generator/crud" 5 | "{{.PackageName}}/db" 6 | "{{.PackageName}}/db/models" 7 | ) 8 | 9 | type model = models.{{.ServiceName | camelcase | title}} 10 | 11 | type Repository struct { 12 | crud.Repository[model] 13 | } 14 | 15 | func InitRepository() *Repository { 16 | return &Repository{ 17 | Repository: crud.Repository[model]{ 18 | DB: db.DB, 19 | Model: &model{}, 20 | }, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /generators/templates/routes.tmpl: -------------------------------------------------------------------------------- 1 | package {{.ServiceName | camelcase | lower}} 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | func RegisterRoutes(routerGroup *gin.RouterGroup) { 6 | service := InitService() 7 | controller := NewController(service) 8 | 9 | routerGroup.GET("", controller.findAll) 10 | routerGroup.GET(":id", controller.findOne) 11 | routerGroup.POST("", controller.create) 12 | routerGroup.DELETE(":id", controller.delete) 13 | routerGroup.PATCH(":id", controller.update) 14 | } 15 | -------------------------------------------------------------------------------- /generators/templates/service.tmpl: -------------------------------------------------------------------------------- 1 | package {{.ServiceName | camelcase | lower}} 2 | 3 | import ( 4 | "github.com/ElegantSoft/go-restful-generator/crud" 5 | ) 6 | 7 | type Service struct { 8 | crud.Service[model] 9 | repo *Repository 10 | } 11 | 12 | func NewService(repository *Repository) *Service { 13 | return &Service{ 14 | Service: *crud.NewService[model](repository), 15 | repo: repository, 16 | } 17 | } 18 | 19 | func InitService() *Service { 20 | return &Service{ 21 | repo: InitRepository(), 22 | Service: *crud.NewService[model](InitRepository()), 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ElegantSoft/go-restful-generator 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/Masterminds/sprig v2.22.0+incompatible 7 | github.com/bxcodec/faker/v3 v3.8.0 8 | github.com/gin-contrib/cors v1.4.0 9 | github.com/gin-gonic/gin v1.8.1 10 | github.com/go-playground/validator/v10 v10.11.0 11 | github.com/google/uuid v1.3.0 12 | github.com/iancoleman/strcase v0.2.0 13 | github.com/joho/godotenv v1.4.0 14 | github.com/spf13/cobra v1.5.0 15 | github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a 16 | github.com/swaggo/gin-swagger v1.5.3 17 | github.com/swaggo/swag v1.8.5 18 | golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 19 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 20 | gorm.io/driver/postgres v1.3.9 21 | gorm.io/driver/sqlite v1.3.6 22 | gorm.io/gorm v1.23.8 23 | ) 24 | 25 | require ( 26 | github.com/KyleBanks/depth v1.2.1 // indirect 27 | github.com/Masterminds/goutils v1.1.1 // indirect 28 | github.com/Masterminds/semver v1.5.0 // indirect 29 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect 30 | github.com/gin-contrib/sse v0.1.0 // indirect 31 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 32 | github.com/go-openapi/jsonreference v0.20.0 // indirect 33 | github.com/go-openapi/spec v0.20.7 // indirect 34 | github.com/go-openapi/swag v0.22.3 // indirect 35 | github.com/go-playground/locales v0.14.0 // indirect 36 | github.com/go-playground/universal-translator v0.18.0 // indirect 37 | github.com/goccy/go-json v0.9.11 // indirect 38 | github.com/google/go-cmp v0.5.8 // indirect 39 | github.com/huandu/xstrings v1.3.2 // indirect 40 | github.com/imdario/mergo v0.3.13 // indirect 41 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 42 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 43 | github.com/jackc/pgconn v1.13.0 // indirect 44 | github.com/jackc/pgio v1.0.0 // indirect 45 | github.com/jackc/pgpassfile v1.0.0 // indirect 46 | github.com/jackc/pgproto3/v2 v2.3.1 // indirect 47 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect 48 | github.com/jackc/pgtype v1.12.0 // indirect 49 | github.com/jackc/pgx/v4 v4.17.2 // indirect 50 | github.com/jinzhu/inflection v1.0.0 // indirect 51 | github.com/jinzhu/now v1.1.5 // indirect 52 | github.com/josharian/intern v1.0.0 // indirect 53 | github.com/json-iterator/go v1.1.12 // indirect 54 | github.com/leodido/go-urn v1.2.1 // indirect 55 | github.com/mailru/easyjson v0.7.7 // indirect 56 | github.com/manifoldco/promptui v0.9.0 // indirect 57 | github.com/mattn/go-isatty v0.0.16 // indirect 58 | github.com/mattn/go-sqlite3 v1.14.15 // indirect 59 | github.com/mitchellh/copystructure v1.2.0 // indirect 60 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 61 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 62 | github.com/modern-go/reflect2 v1.0.2 // indirect 63 | github.com/pelletier/go-toml/v2 v2.0.5 // indirect 64 | github.com/rakyll/statik v0.1.7 // indirect 65 | github.com/spf13/pflag v1.0.5 // indirect 66 | github.com/ugorji/go/codec v1.2.7 // indirect 67 | golang.org/x/net v0.0.0-20220909164309-bea034e7d591 // indirect 68 | golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41 // indirect 69 | golang.org/x/text v0.3.7 // indirect 70 | golang.org/x/tools v0.1.12 // indirect 71 | google.golang.org/protobuf v1.28.1 // indirect 72 | gopkg.in/yaml.v2 v2.4.0 // indirect 73 | gopkg.in/yaml.v3 v3.0.1 // indirect 74 | ) 75 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= 3 | github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 4 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= 5 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 6 | github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= 7 | github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 8 | github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= 9 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 10 | github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= 11 | github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= 12 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 13 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 14 | github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= 15 | github.com/bxcodec/faker/v3 v3.8.0 h1:F59Qqnsh0BOtZRC+c4cXoB/VNYDMS3R5mlSpxIap1oU= 16 | github.com/bxcodec/faker/v3 v3.8.0/go.mod h1:gF31YgnMSMKgkvl+fyEo1xuSMbEuieyqfeslGYFjneM= 17 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 18 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= 19 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 20 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 21 | github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= 22 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 23 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 24 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 25 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 26 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 27 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 28 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 29 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 30 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 31 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 32 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 33 | github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g= 34 | github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs= 35 | github.com/gin-contrib/gzip v0.0.5 h1:mhnVU32YnnBh2LPH2iqRqsA/eR7SAqRaD388jL2s/j0= 36 | github.com/gin-contrib/gzip v0.0.5/go.mod h1:OPIK6HR0Um2vNmBUTlayD7qle4yVVRZT0PyhdUigrKk= 37 | github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= 38 | github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= 39 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 40 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 41 | github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= 42 | github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= 43 | github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= 44 | github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= 45 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 46 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 47 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 48 | github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= 49 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 50 | github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= 51 | github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= 52 | github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= 53 | github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= 54 | github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ= 55 | github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= 56 | github.com/go-openapi/spec v0.20.7 h1:1Rlu/ZrOCCob0n+JKKJAWhNWMPW8bOZRg8FJaY+0SKI= 57 | github.com/go-openapi/spec v0.20.7/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= 58 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 59 | github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= 60 | github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU= 61 | github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= 62 | github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= 63 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 64 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 65 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 66 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 67 | github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= 68 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= 69 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 70 | github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= 71 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= 72 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 73 | github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= 74 | github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw= 75 | github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= 76 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 77 | github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM= 78 | github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 79 | github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= 80 | github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 81 | github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= 82 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 83 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 84 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 85 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 86 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 87 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 88 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 89 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 90 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 91 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 92 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 93 | github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= 94 | github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 95 | github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= 96 | github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= 97 | github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= 98 | github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= 99 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 100 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 101 | github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= 102 | github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= 103 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 104 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 105 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 106 | github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= 107 | github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= 108 | github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= 109 | github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= 110 | github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= 111 | github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= 112 | github.com/jackc/pgconn v1.12.1 h1:rsDFzIpRk7xT4B8FufgpCCeyjdNpKyghZeSefViE5W8= 113 | github.com/jackc/pgconn v1.12.1/go.mod h1:ZkhRC59Llhrq3oSfrikvwQ5NaxYExr6twkdkMLaKono= 114 | github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys= 115 | github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI= 116 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 117 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 118 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= 119 | github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= 120 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= 121 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= 122 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 123 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 124 | github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= 125 | github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= 126 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= 127 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= 128 | github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 129 | github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 130 | github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 131 | github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 132 | github.com/jackc/pgproto3/v2 v2.3.0 h1:brH0pCGBDkBW07HWlN/oSBXrmo3WB0UvZd1pIuDcL8Y= 133 | github.com/jackc/pgproto3/v2 v2.3.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 134 | github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y= 135 | github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 136 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= 137 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 138 | github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= 139 | github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= 140 | github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 141 | github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= 142 | github.com/jackc/pgtype v1.11.0 h1:u4uiGPz/1hryuXzyaBhSk6dnIyyG2683olG2OV+UUgs= 143 | github.com/jackc/pgtype v1.11.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= 144 | github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w= 145 | github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= 146 | github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= 147 | github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= 148 | github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= 149 | github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= 150 | github.com/jackc/pgx/v4 v4.16.1 h1:JzTglcal01DrghUqt+PmzWsZx/Yh7SC/CTQmSBMTd0Y= 151 | github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ= 152 | github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E= 153 | github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw= 154 | github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 155 | github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 156 | github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 157 | github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 158 | github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 159 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 160 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 161 | github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 162 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 163 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 164 | github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= 165 | github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 166 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 167 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 168 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 169 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 170 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 171 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 172 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 173 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 174 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 175 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 176 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 177 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 178 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 179 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 180 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 181 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 182 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 183 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 184 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 185 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 186 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 187 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 188 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 189 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 190 | github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= 191 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 192 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 193 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 194 | github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 195 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 196 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 197 | github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= 198 | github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= 199 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 200 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 201 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 202 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 203 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 204 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 205 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 206 | github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= 207 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 208 | github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 209 | github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I= 210 | github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 211 | github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= 212 | github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 213 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 214 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 215 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 216 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 217 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 218 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 219 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 220 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 221 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 222 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 223 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 224 | github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= 225 | github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= 226 | github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= 227 | github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= 228 | github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= 229 | github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= 230 | github.com/pelletier/go-toml/v2 v2.0.2 h1:+jQXlF3scKIcSEKkdHzXhCTDLPFi5r1wnK6yPS+49Gw= 231 | github.com/pelletier/go-toml/v2 v2.0.2/go.mod h1:MovirKjgVRESsAvNZlAjtFwV867yGuwRkXbG66OzopI= 232 | github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= 233 | github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= 234 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 235 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 236 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 237 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 238 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 239 | github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= 240 | github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= 241 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 242 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 243 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 244 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 245 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 246 | github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= 247 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 248 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 249 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 250 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 251 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 252 | github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= 253 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 254 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 255 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 256 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 257 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 258 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 259 | github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= 260 | github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= 261 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 262 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 263 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 264 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 265 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 266 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 267 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 268 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 269 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 270 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 271 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 272 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 273 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 274 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 275 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 276 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 277 | github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe h1:K8pHPVoTgxFJt1lXuIzzOX7zZhZFldJQK/CgKx9BFIc= 278 | github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= 279 | github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a h1:kAe4YSu0O0UFn1DowNo2MY5p6xzqtJ/wQ7LZynSvGaY= 280 | github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= 281 | github.com/swaggo/gin-swagger v1.5.0 h1:hlLbxPj6qvbtX2wpbsZuOIlcnPRCUDGccA0zMKVNpME= 282 | github.com/swaggo/gin-swagger v1.5.0/go.mod h1:3mKpZClKx7mnUGsiwJeEkNhnr1VHMkMaTAXIoFYUXrA= 283 | github.com/swaggo/gin-swagger v1.5.3 h1:8mWmHLolIbrhJJTflsaFoZzRBYVmEE7JZGIq08EiC0Q= 284 | github.com/swaggo/gin-swagger v1.5.3/go.mod h1:3XJKSfHjDMB5dBo/0rrTXidPmgLeqsX89Yp4uA50HpI= 285 | github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= 286 | github.com/swaggo/swag v1.8.3 h1:3pZSSCQ//gAH88lfmxM3Cd1+JCsxV8Md6f36b9hrZ5s= 287 | github.com/swaggo/swag v1.8.3/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg= 288 | github.com/swaggo/swag v1.8.5 h1:7NgtfXsXE+jrcOwRyiftGKW7Ppydj7tZiVenuRf1fE4= 289 | github.com/swaggo/swag v1.8.5/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg= 290 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 291 | github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= 292 | github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= 293 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 294 | github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= 295 | github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= 296 | github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= 297 | github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 298 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 299 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 300 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 301 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 302 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 303 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 304 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 305 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 306 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 307 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 308 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 309 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 310 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 311 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 312 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 313 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 314 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 315 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 316 | golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 317 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 318 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 319 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 320 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 321 | golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= 322 | golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 323 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 324 | golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= 325 | golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 326 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 327 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 328 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 329 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 330 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= 331 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 332 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 333 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 334 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 335 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 336 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 337 | golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= 338 | golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 339 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 340 | golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 341 | golang.org/x/net v0.0.0-20220617184016-355a448f1bc9 h1:Yqz/iviulwKwAREEeUd3nbBFn0XuyJqkoft2IlrvOhc= 342 | golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 343 | golang.org/x/net v0.0.0-20220909164309-bea034e7d591 h1:D0B/7al0LLrVC8aWF4+oxpv/m8bc7ViFfVS8/gXGdqI= 344 | golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= 345 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 346 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 347 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 348 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 349 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 350 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 351 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 352 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 353 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 354 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 355 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 356 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 357 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 358 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 359 | golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 360 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 361 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 362 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 363 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 364 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 365 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 366 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 367 | golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41 h1:ohgcoMbSofXygzo6AD2I1kz3BFmW1QArPYTtwEM3UXc= 368 | golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 369 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 370 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 371 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 372 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 373 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 374 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 375 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 376 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 377 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 378 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 379 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 380 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 381 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 382 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 383 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 384 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 385 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 386 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 387 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 388 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 389 | golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= 390 | golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY= 391 | golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= 392 | golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= 393 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 394 | golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 395 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 396 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 397 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 398 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 399 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 400 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 401 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 402 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 403 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= 404 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 405 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 406 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 407 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 408 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 409 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 410 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 411 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= 412 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 413 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 414 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 415 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 416 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 417 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 418 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 419 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 420 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 421 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 422 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 423 | gorm.io/driver/postgres v1.3.7 h1:FKF6sIMDHDEvvMF/XJvbnCl0nu6KSKUaPXevJ4r+VYQ= 424 | gorm.io/driver/postgres v1.3.7/go.mod h1:f02ympjIcgtHEGFMZvdgTxODZ9snAHDb4hXfigBVuNI= 425 | gorm.io/driver/postgres v1.3.9 h1:lWGiVt5CijhQAg0PWB7Od1RNcBw/jS4d2cAScBcSDXg= 426 | gorm.io/driver/postgres v1.3.9/go.mod h1:qw/FeqjxmYqW5dBcYNBsnhQULIApQdk7YuuDPktVi1U= 427 | gorm.io/driver/sqlite v1.3.4 h1:NnFOPVfzi4CPsJPH4wXr6rMkPb4ElHEqKMvrsx9c9Fk= 428 | gorm.io/driver/sqlite v1.3.4/go.mod h1:B+8GyC9K7VgzJAcrcXMRPdnMcck+8FgJynEehEPM16U= 429 | gorm.io/driver/sqlite v1.3.6 h1:Fi8xNYCUplOqWiPa3/GuCeowRNBRGTf62DEmhMDHeQQ= 430 | gorm.io/driver/sqlite v1.3.6/go.mod h1:Sg1/pvnKtbQ7jLXxfZa+jSHvoX8hoZA8cn4xllOMTgE= 431 | gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= 432 | gorm.io/gorm v1.23.6 h1:KFLdNgri4ExFFGTRGGFWON2P1ZN28+9SJRN8voOoYe0= 433 | gorm.io/gorm v1.23.6/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= 434 | gorm.io/gorm v1.23.7/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= 435 | gorm.io/gorm v1.23.8 h1:h8sGJ+biDgBA1AD1Ha9gFCx7h8npU7AsLdlkX0n2TpE= 436 | gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= 437 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 438 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/ElegantSoft/go-restful-generator/crud" 8 | "github.com/ElegantSoft/go-restful-generator/db" 9 | "github.com/ElegantSoft/go-restful-generator/db/models" 10 | _ "github.com/ElegantSoft/go-restful-generator/docs" 11 | "github.com/ElegantSoft/go-restful-generator/posts" 12 | "github.com/gin-contrib/cors" 13 | "github.com/gin-gonic/gin" 14 | "github.com/joho/godotenv" 15 | swaggerFiles "github.com/swaggo/files" 16 | ginSwagger "github.com/swaggo/gin-swagger" 17 | ) 18 | 19 | // @contact.name API Support 20 | // @contact.url http://www.swagger.io/support 21 | // @contact.email support@swagger.io 22 | 23 | // @license.name Apache 2.0 24 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html 25 | func main() { 26 | if err := godotenv.Load(); err != nil { 27 | log.Fatal("Error loading .env file") 28 | } 29 | server := gin.New() 30 | server.Use(gin.Recovery()) 31 | 32 | config := cors.DefaultConfig() 33 | // config.AllowOrigins = []string{"http://localhost:5173"} 34 | config.AllowAllOrigins = true 35 | config.AddAllowHeaders("Authorization") 36 | 37 | server.Use(cors.New(config)) 38 | 39 | if os.Getenv("GIN_MODE") == "debug" { 40 | server.Use(gin.Logger()) 41 | } 42 | gin.SetMode(os.Getenv("GIN_MODE")) 43 | if err := db.Open(os.Getenv("DB_URL")); err != nil { 44 | log.Fatal(err) 45 | } 46 | log.Println("server started") 47 | 48 | server.GET("", func(ctx *gin.Context) { 49 | ctx.JSON(200, gin.H{"message": "ok 5"}) 50 | }) 51 | 52 | // migrations 53 | db.AddUUIDExtension() 54 | 55 | if err := db.DB.AutoMigrate( 56 | models.Category{}, 57 | models.Post{}, 58 | ); err != nil { 59 | log.Fatal(err) 60 | } 61 | 62 | crudGroup := server.Group("crud") 63 | postsGroup := server.Group("posts") 64 | 65 | crud.RegisterRoutes(crudGroup) 66 | posts.RegisterRoutes(postsGroup) 67 | 68 | server.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) 69 | 70 | // seed.SeedPosts() 71 | 72 | err := server.Run(":8081") 73 | if err != nil { 74 | log.Printf("error while starting server %+v", err) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /pkg/helpers/ensure_dir.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | ) 7 | 8 | func EnsureDir(dirName string) error { 9 | err := os.Mkdir(dirName, os.ModeDir) 10 | if err == nil { 11 | return nil 12 | } 13 | if os.IsExist(err) { 14 | // check that the existing path is a directory 15 | info, err := os.Stat(dirName) 16 | if err != nil { 17 | return err 18 | } 19 | if !info.IsDir() { 20 | return errors.New("path exists but is not a directory") 21 | } 22 | return nil 23 | } 24 | return err 25 | } 26 | -------------------------------------------------------------------------------- /pkg/writetemplate/process_template.go: -------------------------------------------------------------------------------- 1 | package writetemplate 2 | 3 | import ( 4 | "github.com/Masterminds/sprig" 5 | "html/template" 6 | "log" 7 | "os" 8 | ) 9 | 10 | func ProcessTemplate(templatePath string, fileName string, outputPath string, data interface{}) { 11 | tmpl := template.Must(template.New("").Funcs(sprig.FuncMap()).Parse(templatePath)) 12 | f, err := os.Create(outputPath) 13 | if err != nil { 14 | log.Println("create file: ", err) 15 | return 16 | } 17 | err = tmpl.Execute(f, data) 18 | if err != nil { 19 | log.Print("execute: ", err) 20 | return 21 | } 22 | log.Printf("create file: %s", fileName) 23 | //var processed bytes.Buffer 24 | //err := tmpl.ExecuteTemplate(&processed, fileName, data) 25 | //if err != nil { 26 | // log.Fatalf("Unable to parse data into template: %v\n", err) 27 | //} 28 | //formatted, err := format.Source(processed.Bytes()) 29 | //if err != nil { 30 | // log.Fatalf("Could not format processed template: %v\n", err) 31 | //} 32 | //fmt.Println("Writing file: ", outputPath) 33 | //f, _ := os.Create(outputPath) 34 | //w := bufio.NewWriter(f) 35 | //w.WriteString(string(formatted)) 36 | //w.Flush() 37 | } 38 | -------------------------------------------------------------------------------- /posts/controller.go: -------------------------------------------------------------------------------- 1 | package posts 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "net/http" 7 | 8 | "github.com/ElegantSoft/go-restful-generator/common" 9 | "github.com/ElegantSoft/go-restful-generator/crud" 10 | "github.com/gin-gonic/gin" 11 | "github.com/google/uuid" 12 | ) 13 | 14 | type Controller struct { 15 | service *Service 16 | } 17 | 18 | // @Success 200 {array} model 19 | // @Tags posts 20 | // @param s query string false "{'$and': [ {'title': { '$cont':'cul' } } ]}" 21 | // @param fields query string false "fields to select eg: name,age" 22 | // @param page query int false "page of pagination" 23 | // @param limit query int false "limit of pagination" 24 | // @param join query string false "join relations eg: category, parent" 25 | // @param filter query []string false "filters eg: name||$eq||ad price||$gte||200" 26 | // @param sort query []string false "filters eg: created_at,desc title,asc" 27 | // @Router /posts [get] 28 | func (c *Controller) findAll(ctx *gin.Context) { 29 | var api crud.GetAllRequest 30 | if api.Limit == 0 { 31 | api.Limit = 20 32 | } 33 | if err := ctx.ShouldBindQuery(&api); err != nil { 34 | ctx.JSON(400, gin.H{"message": err.Error()}) 35 | return 36 | } 37 | 38 | var result []model 39 | var totalRows int64 40 | api.Join = api.Join + ",category" 41 | err := c.service.Find(api, &result, &totalRows) 42 | if err != nil { 43 | ctx.JSON(400, gin.H{"message": err.Error()}) 44 | return 45 | } 46 | 47 | var data interface{} 48 | if api.Page > 0 { 49 | data = map[string]interface{}{ 50 | "data": result, 51 | "total": totalRows, 52 | "totalPages": int(math.Ceil(float64(totalRows) / float64(api.Limit))), 53 | } 54 | } else { 55 | data = result 56 | } 57 | ctx.JSON(200, data) 58 | } 59 | 60 | // @Success 200 {object} model 61 | // @Tags posts 62 | // @param id path string true "uuid of item" 63 | // @Router /posts/{id} [get] 64 | func (c *Controller) findOne(ctx *gin.Context) { 65 | var api crud.GetAllRequest 66 | var item common.ById 67 | if err := ctx.ShouldBindQuery(&api); err != nil { 68 | ctx.JSON(400, gin.H{"message": err.Error()}) 69 | return 70 | } 71 | if err := ctx.ShouldBindUri(&item); err != nil { 72 | ctx.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) 73 | return 74 | } 75 | 76 | api.Filter = append(api.Filter, fmt.Sprintf("id||eq||%s", item.ID)) 77 | 78 | var result model 79 | 80 | err := c.service.FindOne(api, &result) 81 | if err != nil { 82 | ctx.JSON(400, gin.H{"message": err.Error()}) 83 | return 84 | } 85 | ctx.JSON(200, result) 86 | } 87 | 88 | // @Success 201 {object} model 89 | // @Tags posts 90 | // @param {object} body model true "item to create" 91 | // @Router /posts [post] 92 | func (c *Controller) create(ctx *gin.Context) { 93 | var item model 94 | if err := ctx.ShouldBind(&item); err != nil { 95 | ctx.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) 96 | return 97 | } 98 | err := c.service.Create(&item) 99 | if err != nil { 100 | ctx.JSON(http.StatusNotFound, gin.H{"message": err.Error()}) 101 | return 102 | } 103 | ctx.JSON(http.StatusCreated, gin.H{"data": item}) 104 | } 105 | 106 | // @Success 200 {string} string "ok" 107 | // @Tags posts 108 | // @param id path string true "uuid of item" 109 | // @Router /posts/{id} [delete] 110 | func (c *Controller) delete(ctx *gin.Context) { 111 | var item common.ById 112 | if err := ctx.ShouldBindUri(&item); err != nil { 113 | ctx.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) 114 | return 115 | } 116 | 117 | id, err := uuid.ParseBytes([]byte(item.ID)) 118 | if err != nil { 119 | ctx.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) 120 | return 121 | } 122 | err = c.service.Delete(&model{ID: id}) 123 | if err != nil { 124 | ctx.JSON(http.StatusNotFound, gin.H{"message": err.Error()}) 125 | return 126 | } 127 | ctx.JSON(http.StatusOK, gin.H{"message": "deleted"}) 128 | } 129 | 130 | // @Success 200 {string} string "ok" 131 | // @Tags posts 132 | // @param id path string true "uuid of item" 133 | // @param item body model true "update body" 134 | // @Router /posts/{id} [put] 135 | func (c *Controller) update(ctx *gin.Context) { 136 | var item model 137 | var byId common.ById 138 | if err := ctx.ShouldBind(&item); err != nil { 139 | ctx.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) 140 | return 141 | } 142 | if err := ctx.ShouldBindUri(&byId); err != nil { 143 | ctx.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) 144 | return 145 | } 146 | id, err := uuid.Parse(byId.ID) 147 | if err != nil { 148 | ctx.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) 149 | return 150 | } 151 | if err := ctx.ShouldBindUri(&byId); err != nil { 152 | ctx.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) 153 | return 154 | } 155 | err = c.service.Update(&model{ID: id}, &item) 156 | if err != nil { 157 | ctx.JSON(http.StatusNotFound, gin.H{"message": err.Error()}) 158 | return 159 | } 160 | ctx.JSON(http.StatusOK, item) 161 | } 162 | 163 | func NewController(service *Service) *Controller { 164 | return &Controller{ 165 | service: service, 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /posts/repository.go: -------------------------------------------------------------------------------- 1 | package posts 2 | 3 | import ( 4 | "github.com/ElegantSoft/go-restful-generator/crud" 5 | "github.com/ElegantSoft/go-restful-generator/db" 6 | "github.com/ElegantSoft/go-restful-generator/db/models" 7 | ) 8 | 9 | type model = models.Post 10 | 11 | type Repository struct { 12 | crud.Repository[model] 13 | } 14 | 15 | func InitRepository() *Repository { 16 | return &Repository{ 17 | Repository: crud.Repository[model]{ 18 | DB: db.DB, 19 | Model: model{}, 20 | }, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /posts/routes.go: -------------------------------------------------------------------------------- 1 | package posts 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | func RegisterRoutes(routerGroup *gin.RouterGroup) { 6 | service := InitService() 7 | controller := NewController(service) 8 | 9 | routerGroup.GET("", controller.findAll) 10 | routerGroup.GET(":id", controller.findOne) 11 | routerGroup.POST("", controller.create) 12 | routerGroup.DELETE(":id", controller.delete) 13 | routerGroup.PATCH(":id", controller.update) 14 | } 15 | -------------------------------------------------------------------------------- /posts/service.go: -------------------------------------------------------------------------------- 1 | package posts 2 | 3 | import ( 4 | "github.com/ElegantSoft/go-restful-generator/crud" 5 | ) 6 | 7 | type Service struct { 8 | crud.Service[model] 9 | repo *Repository 10 | } 11 | 12 | func NewService(repository *Repository) *Service { 13 | return &Service{ 14 | Service: *crud.NewService[model](repository), 15 | repo: repository, 16 | } 17 | } 18 | 19 | func InitService() *Service { 20 | return &Service{ 21 | repo: InitRepository(), 22 | Service: *crud.NewService[model](InitRepository()), 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /requests/crud.http: -------------------------------------------------------------------------------- 1 | 2 | 3 | GET http://localhost:8080/crud?join=category&fields=title,id,category_id&filter=title||$cont||haru&filter=id||$eq||11c457ed-4fc4-4d65-b106-1b2732869ae2&sort=title&sort=id,asc 4 | 5 | ### get one 6 | GET http://localhost:8080/crud/11c457ed-4fc4-4d65-b106-1b2732869ae2?join=category 7 | 8 | 9 | #{"title": {"$cont": "hAru"}} -------------------------------------------------------------------------------- /requests/posts.http: -------------------------------------------------------------------------------- 1 | 2 | 3 | GET http://localhost:8081/posts?page=1&filter=id||$in||11c457ed-4fc4-4d65-b106-1b2732869ae2 4 | 5 | ### get one 6 | GET http://localhost:8081/posts/11c457ed-4fc4-4d65-b106-1b2732869ae2 7 | 8 | 9 | #{"title": {"$cont": "hAru"}} -------------------------------------------------------------------------------- /templates/db/database.tmpl: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "gorm.io/driver/sqlite" 5 | "gorm.io/gorm" 6 | "gorm.io/gorm/logger" 7 | "log" 8 | "os" 9 | "time" 10 | ) 11 | 12 | var DB *gorm.DB 13 | 14 | func Open(dsn string) error { 15 | newLogger := logger.New( 16 | log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer 17 | logger.Config{ 18 | SlowThreshold: time.Second, // Slow SQL threshold 19 | LogLevel: logger.Info, // Log level 20 | Colorful: true, // Disable color 21 | 22 | }, 23 | ) 24 | var err error 25 | DB, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{ 26 | Logger: newLogger, 27 | SkipDefaultTransaction: true, 28 | PrepareStmt: false, 29 | }) 30 | if err != nil { 31 | return err 32 | } 33 | return nil 34 | } 35 | 36 | func OpenTestDB() error { 37 | newLogger := logger.New( 38 | log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer 39 | logger.Config{ 40 | SlowThreshold: time.Second, // Slow SQL threshold 41 | LogLevel: logger.Info, // Log level 42 | Colorful: true, // Disable color 43 | 44 | }, 45 | ) 46 | var err error 47 | DB, err = gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{ 48 | Logger: newLogger, 49 | SkipDefaultTransaction: true, 50 | PrepareStmt: true, 51 | }) 52 | if err != nil { 53 | return err 54 | } 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /templates/db/migrations.tmpl: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | func AddUUIDExtension() { 4 | DB.Exec(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp";`) 5 | } 6 | -------------------------------------------------------------------------------- /templates/main.tmpl: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "{{.PackageName}}/db" 7 | 8 | "github.com/gin-contrib/cors" 9 | "github.com/gin-gonic/gin" 10 | "github.com/joho/godotenv" 11 | swaggerFiles "github.com/swaggo/files" 12 | ginSwagger "github.com/swaggo/gin-swagger" 13 | "{{.PackageName}}/db/models" 14 | _ "{{.PackageName}}/docs" 15 | ) 16 | 17 | // @contact.name API Support 18 | // @contact.url http://www.swagger.io/support 19 | // @contact.email support@swagger.io 20 | 21 | // @license.name Apache 2.0 22 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html 23 | func main() { 24 | if err := godotenv.Load(); err != nil { 25 | log.Fatal("Error loading .env file") 26 | } 27 | server := gin.New() 28 | server.Use(gin.Recovery()) 29 | 30 | config := cors.DefaultConfig() 31 | // config.AllowOrigins = []string{"http://localhost:5173"} 32 | config.AllowAllOrigins = true 33 | config.AddAllowHeaders("Authorization") 34 | 35 | server.Use(cors.New(config)) 36 | 37 | if os.Getenv("GIN_MODE") == "debug" { 38 | server.Use(gin.Logger()) 39 | } 40 | gin.SetMode(os.Getenv("GIN_MODE")) 41 | 42 | if err := db.Open(os.Getenv("DB_URL")); err != nil { 43 | log.Fatal(err) 44 | } 45 | log.Println("server started") 46 | 47 | server.GET("", func(ctx *gin.Context) { 48 | ctx.JSON(200, gin.H{"message": "ok 5"}) 49 | }) 50 | 51 | // migrations 52 | db.AddUUIDExtension() 53 | 54 | if err := db.DB.AutoMigrate( 55 | models.Category{}, 56 | models.Post{}, 57 | ); err != nil { 58 | log.Fatal(err) 59 | } 60 | 61 | server.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) 62 | 63 | 64 | err := server.Run() 65 | if err != nil { 66 | log.Printf("error while starting server %+v", err) 67 | } 68 | } 69 | --------------------------------------------------------------------------------