├── .gitignore ├── LICENSE.txt ├── README.md ├── api └── api-spec.json ├── architecture.excalidraw ├── architecture.png ├── cmd ├── web │ └── main.go └── worker │ └── main.go ├── config.json ├── db └── migrations │ ├── 20231030144428_create_table_users.down.sql │ ├── 20231030144428_create_table_users.up.sql │ ├── 20231030144435_create_table_contacts.down.sql │ ├── 20231030144435_create_table_contacts.up.sql │ ├── 20231030144441_create_table_addresses.down.sql │ └── 20231030144441_create_table_addresses.up.sql ├── go.mod ├── go.sum ├── internal ├── config │ ├── app.go │ ├── fiber.go │ ├── gorm.go │ ├── kafka.go │ ├── logrus.go │ ├── validator.go │ └── viper.go ├── delivery │ ├── http │ │ ├── address_controller.go │ │ ├── contact_controller.go │ │ ├── middleware │ │ │ └── auth_middleware.go │ │ ├── route │ │ │ └── route.go │ │ └── user_controller.go │ └── messaging │ │ ├── address_consumer.go │ │ ├── consumer.go │ │ ├── contact_consumer.go │ │ └── user_consumer.go ├── entity │ ├── address_entity.go │ ├── contact_entity.go │ └── user_entity.go ├── gateway │ └── messaging │ │ ├── address_producer.go │ │ ├── contact_producer.go │ │ ├── producer.go │ │ └── user_producer.go ├── model │ ├── address_event.go │ ├── address_model.go │ ├── auth.go │ ├── contact_event.go │ ├── contact_model.go │ ├── converter │ │ ├── address_converter.go │ │ ├── contact_converter.go │ │ └── user_converter.go │ ├── event.go │ ├── model.go │ ├── user_event.go │ └── user_model.go ├── repository │ ├── address_repository.go │ ├── contact_repository.go │ ├── repository.go │ └── user_repository.go └── usecase │ ├── address_usecase.go │ ├── contact_usecase.go │ └── user_usecase.go └── test ├── address_test.go ├── contact_test.go ├── helper_test.go ├── http-client.env.json ├── init.go ├── manual.http └── user_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golang Clean Architecture Template 2 | 3 | ## Description 4 | 5 | This is golang clean architecture template. 6 | 7 | ## Architecture 8 | 9 | ![Clean Architecture](architecture.png) 10 | 11 | 1. External system perform request (HTTP, gRPC, Messaging, etc) 12 | 2. The Delivery creates various Model from request data 13 | 3. The Delivery calls Use Case, and execute it using Model data 14 | 4. The Use Case create Entity data for the business logic 15 | 5. The Use Case calls Repository, and execute it using Entity data 16 | 6. The Repository use Entity data to perform database operation 17 | 7. The Repository perform database operation to the database 18 | 8. The Use Case create various Model for Gateway or from Entity data 19 | 9. The Use Case calls Gateway, and execute it using Model data 20 | 10. The Gateway using Model data to construct request to external system 21 | 11. The Gateway perform request to external system (HTTP, gRPC, Messaging, etc) 22 | 23 | ## Tech Stack 24 | 25 | - Golang : https://github.com/golang/go 26 | - MySQL (Database) : https://github.com/mysql/mysql-server 27 | - Apache Kafka : https://github.com/apache/kafka 28 | 29 | ## Framework & Library 30 | 31 | - GoFiber (HTTP Framework) : https://github.com/gofiber/fiber 32 | - GORM (ORM) : https://github.com/go-gorm/gorm 33 | - Viper (Configuration) : https://github.com/spf13/viper 34 | - Golang Migrate (Database Migration) : https://github.com/golang-migrate/migrate 35 | - Go Playground Validator (Validation) : https://github.com/go-playground/validator 36 | - Logrus (Logger) : https://github.com/sirupsen/logrus 37 | - Confluent Kafka Golang : https://github.com/confluentinc/confluent-kafka-go 38 | 39 | ## Configuration 40 | 41 | All configuration is in `config.json` file. 42 | 43 | ## API Spec 44 | 45 | All API Spec is in `api` folder. 46 | 47 | ## Database Migration 48 | 49 | All database migration is in `db/migrations` folder. 50 | 51 | ### Create Migration 52 | 53 | ```shell 54 | migrate create -ext sql -dir db/migrations create_table_xxx 55 | ``` 56 | 57 | ### Run Migration 58 | 59 | ```shell 60 | migrate -database "mysql://root:@tcp(localhost:3306)/golang_clean_architecture?charset=utf8mb4&parseTime=True&loc=Local" -path db/migrations up 61 | ``` 62 | 63 | ## Run Application 64 | 65 | ### Run unit test 66 | 67 | ```bash 68 | go test -v ./test/ 69 | ``` 70 | 71 | ### Run web server 72 | 73 | ```bash 74 | go run cmd/web/main.go 75 | ``` 76 | 77 | ### Run worker 78 | 79 | ```bash 80 | go run cmd/worker/main.go 81 | ``` -------------------------------------------------------------------------------- /api/api-spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.3", 3 | "info": { 4 | "title": "Golang Clean Architecture", 5 | "description": "Golang Clean Architecture", 6 | "version": "1.0.0" 7 | }, 8 | "servers": [ 9 | { 10 | "url": "http://localhost:3000" 11 | } 12 | ], 13 | "paths": { 14 | "/api/users": { 15 | "post": { 16 | "tags": [ 17 | "User API" 18 | ], 19 | "description": "Register new user", 20 | "requestBody": { 21 | "content": { 22 | "application/json": { 23 | "schema": { 24 | "type": "object", 25 | "properties": { 26 | "id": { 27 | "type": "string" 28 | }, 29 | "password": { 30 | "type": "string" 31 | }, 32 | "name": { 33 | "type": "string" 34 | } 35 | }, 36 | "required": [ 37 | "id", 38 | "name", 39 | "password" 40 | ] 41 | } 42 | } 43 | } 44 | }, 45 | "responses": { 46 | "200": { 47 | "description": "Success register new user", 48 | "content": { 49 | "application/json": { 50 | "schema": { 51 | "type": "object", 52 | "properties": { 53 | "data": { 54 | "type": "object", 55 | "properties": { 56 | "id": { 57 | "type": "string" 58 | }, 59 | "name": { 60 | "type": "string" 61 | }, 62 | "created_at": { 63 | "type": "number" 64 | }, 65 | "updated_at": { 66 | "type": "number" 67 | } 68 | } 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | }, 77 | "delete": { 78 | "description": "Logout user", 79 | "tags": [ 80 | "User API" 81 | ], 82 | "parameters": [ 83 | { 84 | "name": "Authorization", 85 | "in": "header", 86 | "required": true, 87 | "schema": { 88 | "type": "string" 89 | } 90 | } 91 | ], 92 | "responses": { 93 | "200": { 94 | "description": "Success logout user", 95 | "content": { 96 | "application/json": { 97 | "schema": { 98 | "type": "object", 99 | "properties": { 100 | "data": { 101 | "type": "boolean" 102 | } 103 | } 104 | } 105 | } 106 | } 107 | } 108 | } 109 | } 110 | }, 111 | "/api/users/_login": { 112 | "post": { 113 | "description": "Login user", 114 | "tags": [ 115 | "User API" 116 | ], 117 | "requestBody": { 118 | "content": { 119 | "application/json": { 120 | "schema": { 121 | "type": "object", 122 | "properties": { 123 | "id": { 124 | "type": "string" 125 | }, 126 | "password": { 127 | "type": "string" 128 | } 129 | }, 130 | "required": [ 131 | "id", 132 | "password" 133 | ] 134 | } 135 | } 136 | } 137 | }, 138 | "responses": { 139 | "200": { 140 | "description": "Success login", 141 | "content": { 142 | "application/json": { 143 | "schema": { 144 | "type": "object", 145 | "properties": { 146 | "data": { 147 | "type": "object", 148 | "properties": { 149 | "token": { 150 | "type": "string" 151 | } 152 | } 153 | } 154 | } 155 | } 156 | } 157 | } 158 | } 159 | } 160 | } 161 | }, 162 | "/api/users/_current": { 163 | "get": { 164 | "tags": [ 165 | "User API" 166 | ], 167 | "description": "Get current user", 168 | "parameters": [ 169 | { 170 | "name": "Authorization", 171 | "in": "header", 172 | "required": true, 173 | "schema": { 174 | "type": "string" 175 | } 176 | } 177 | ], 178 | "responses": { 179 | "200": { 180 | "description": "Success register new user", 181 | "content": { 182 | "application/json": { 183 | "schema": { 184 | "type": "object", 185 | "properties": { 186 | "data": { 187 | "type": "object", 188 | "properties": { 189 | "id": { 190 | "type": "string" 191 | }, 192 | "name": { 193 | "type": "string" 194 | }, 195 | "created_at": { 196 | "type": "number" 197 | }, 198 | "updated_at": { 199 | "type": "number" 200 | } 201 | } 202 | } 203 | } 204 | } 205 | } 206 | } 207 | } 208 | } 209 | }, 210 | "patch": { 211 | "tags": [ 212 | "User API" 213 | ], 214 | "description": "Update current user", 215 | "parameters": [ 216 | { 217 | "name": "Authorization", 218 | "in": "header", 219 | "required": true, 220 | "schema": { 221 | "type": "string" 222 | } 223 | } 224 | ], 225 | "requestBody": { 226 | "content": { 227 | "application/json": { 228 | "schema": { 229 | "type": "object", 230 | "properties": { 231 | "password": { 232 | "type": "string" 233 | }, 234 | "name": { 235 | "type": "string" 236 | } 237 | } 238 | } 239 | } 240 | } 241 | }, 242 | "responses": { 243 | "200": { 244 | "description": "Success register new user", 245 | "content": { 246 | "application/json": { 247 | "schema": { 248 | "type": "object", 249 | "properties": { 250 | "data": { 251 | "type": "object", 252 | "properties": { 253 | "id": { 254 | "type": "string" 255 | }, 256 | "name": { 257 | "type": "string" 258 | }, 259 | "created_at": { 260 | "type": "number" 261 | }, 262 | "updated_at": { 263 | "type": "number" 264 | } 265 | } 266 | } 267 | } 268 | } 269 | } 270 | } 271 | } 272 | } 273 | } 274 | }, 275 | "/api/contacts": { 276 | "post": { 277 | "tags": [ 278 | "Contact API" 279 | ], 280 | "description": "Create new contact", 281 | "parameters": [ 282 | { 283 | "name": "Authorization", 284 | "in": "header", 285 | "required": true, 286 | "schema": { 287 | "type": "string" 288 | } 289 | } 290 | ], 291 | "requestBody": { 292 | "content": { 293 | "application/json": { 294 | "schema": { 295 | "type": "object", 296 | "properties": { 297 | "first_name": { 298 | "type": "string" 299 | }, 300 | "last_name": { 301 | "type": "string" 302 | }, 303 | "email": { 304 | "type": "string" 305 | }, 306 | "phone": { 307 | "type": "string" 308 | } 309 | } 310 | } 311 | } 312 | } 313 | }, 314 | "responses": { 315 | "200": { 316 | "description": "Success create new contact", 317 | "content": { 318 | "application/json": { 319 | "schema": { 320 | "type": "object", 321 | "properties": { 322 | "data": { 323 | "type": "object", 324 | "properties": { 325 | "id": { 326 | "type": "string" 327 | }, 328 | "first_name": { 329 | "type": "string" 330 | }, 331 | "last_name": { 332 | "type": "string" 333 | }, 334 | "email": { 335 | "type": "string" 336 | }, 337 | "phone": { 338 | "type": "string" 339 | }, 340 | "created_at": { 341 | "type": "number" 342 | }, 343 | "updated_at": { 344 | "type": "number" 345 | } 346 | } 347 | } 348 | } 349 | } 350 | } 351 | } 352 | } 353 | } 354 | }, 355 | "get": { 356 | "tags": [ 357 | "Contact API" 358 | ], 359 | "description": "Get all contacts", 360 | "parameters": [ 361 | { 362 | "name": "Authorization", 363 | "in": "header", 364 | "required": true, 365 | "schema": { 366 | "type": "string" 367 | } 368 | }, 369 | { 370 | "name": "page", 371 | "in": "query", 372 | "required": false, 373 | "schema": { 374 | "type": "number", 375 | "default": 1 376 | } 377 | }, 378 | { 379 | "name": "size", 380 | "in": "query", 381 | "required": false, 382 | "schema": { 383 | "type": "number", 384 | "default": 10 385 | } 386 | }, 387 | { 388 | "name": "name", 389 | "in": "query", 390 | "required": false, 391 | "schema": { 392 | "type": "string" 393 | } 394 | }, 395 | { 396 | "name": "phone", 397 | "in": "query", 398 | "required": false, 399 | "schema": { 400 | "type": "string" 401 | } 402 | }, 403 | { 404 | "name": "email", 405 | "in": "query", 406 | "required": false, 407 | "schema": { 408 | "type": "string" 409 | } 410 | } 411 | ], 412 | "responses": { 413 | "200": { 414 | "description": "Success get list of contacts", 415 | "content": { 416 | "application/json": { 417 | "schema": { 418 | "type": "object", 419 | "properties": { 420 | "data": { 421 | "type": "array", 422 | "items": { 423 | "type": "object", 424 | "properties": { 425 | "id": { 426 | "type": "string" 427 | }, 428 | "first_name": { 429 | "type": "string" 430 | }, 431 | "last_name": { 432 | "type": "string" 433 | }, 434 | "email": { 435 | "type": "string" 436 | }, 437 | "phone": { 438 | "type": "string" 439 | }, 440 | "created_at": { 441 | "type": "number" 442 | }, 443 | "updated_at": { 444 | "type": "number" 445 | } 446 | } 447 | } 448 | }, 449 | "paging": { 450 | "type": "object", 451 | "properties": { 452 | "page": { 453 | "type": "number" 454 | }, 455 | "size": { 456 | "type": "number" 457 | }, 458 | "total_item": { 459 | "type": "number" 460 | }, 461 | "total_page": { 462 | "type": "number" 463 | } 464 | } 465 | } 466 | } 467 | } 468 | } 469 | } 470 | } 471 | } 472 | } 473 | }, 474 | "/api/contacts/{contactId}": { 475 | "get": { 476 | "tags": [ 477 | "Contact API" 478 | ], 479 | "description": "Get contact by id", 480 | "parameters": [ 481 | { 482 | "name": "Authorization", 483 | "in": "header", 484 | "required": true, 485 | "schema": { 486 | "type": "string" 487 | } 488 | }, 489 | { 490 | "name": "contactId", 491 | "in": "path", 492 | "required": true, 493 | "schema": { 494 | "type": "string" 495 | } 496 | } 497 | ], 498 | "responses": { 499 | "200": { 500 | "description": "Success get contact", 501 | "content": { 502 | "application/json": { 503 | "schema": { 504 | "type": "object", 505 | "properties": { 506 | "data": { 507 | "type": "object", 508 | "properties": { 509 | "id": { 510 | "type": "string" 511 | }, 512 | "first_name": { 513 | "type": "string" 514 | }, 515 | "last_name": { 516 | "type": "string" 517 | }, 518 | "email": { 519 | "type": "string" 520 | }, 521 | "phone": { 522 | "type": "string" 523 | }, 524 | "created_at": { 525 | "type": "number" 526 | }, 527 | "updated_at": { 528 | "type": "number" 529 | } 530 | } 531 | } 532 | } 533 | } 534 | } 535 | } 536 | } 537 | } 538 | }, 539 | "put": { 540 | "tags": [ 541 | "Contact API" 542 | ], 543 | "description": "Update contact by id", 544 | "parameters": [ 545 | { 546 | "name": "Authorization", 547 | "in": "header", 548 | "required": true, 549 | "schema": { 550 | "type": "string" 551 | } 552 | }, 553 | { 554 | "name": "contactId", 555 | "in": "path", 556 | "required": true, 557 | "schema": { 558 | "type": "string" 559 | } 560 | } 561 | ], 562 | "requestBody": { 563 | "content": { 564 | "application/json": { 565 | "schema": { 566 | "type": "object", 567 | "properties": { 568 | "first_name": { 569 | "type": "string" 570 | }, 571 | "last_name": { 572 | "type": "string" 573 | }, 574 | "email": { 575 | "type": "string" 576 | }, 577 | "phone": { 578 | "type": "string" 579 | } 580 | } 581 | } 582 | } 583 | } 584 | }, 585 | "responses": { 586 | "200": { 587 | "description": "Success update contact", 588 | "content": { 589 | "application/json": { 590 | "schema": { 591 | "type": "object", 592 | "properties": { 593 | "data": { 594 | "type": "object", 595 | "properties": { 596 | "id": { 597 | "type": "string" 598 | }, 599 | "first_name": { 600 | "type": "string" 601 | }, 602 | "last_name": { 603 | "type": "string" 604 | }, 605 | "email": { 606 | "type": "string" 607 | }, 608 | "phone": { 609 | "type": "string" 610 | }, 611 | "created_at": { 612 | "type": "number" 613 | }, 614 | "updated_at": { 615 | "type": "number" 616 | } 617 | } 618 | } 619 | } 620 | } 621 | } 622 | } 623 | } 624 | } 625 | }, 626 | "delete": { 627 | "tags": [ 628 | "Contact API" 629 | ], 630 | "description": "Delete contact by id", 631 | "parameters": [ 632 | { 633 | "name": "Authorization", 634 | "in": "header", 635 | "required": true, 636 | "schema": { 637 | "type": "string" 638 | } 639 | }, 640 | { 641 | "name": "contactId", 642 | "in": "path", 643 | "required": true, 644 | "schema": { 645 | "type": "string" 646 | } 647 | } 648 | ], 649 | "responses": { 650 | "200": { 651 | "description": "Success delete contact", 652 | "content": { 653 | "application/json": { 654 | "schema": { 655 | "type": "object", 656 | "properties": { 657 | "data": { 658 | "type": "boolean" 659 | } 660 | } 661 | } 662 | } 663 | } 664 | } 665 | } 666 | } 667 | }, 668 | "/api/contacts/{contactId}/addresses": { 669 | "post": { 670 | "tags": [ 671 | "Address API" 672 | ], 673 | "description": "Create new address", 674 | "parameters": [ 675 | { 676 | "name": "Authorization", 677 | "in": "header", 678 | "required": true, 679 | "schema": { 680 | "type": "string" 681 | } 682 | }, 683 | { 684 | "name": "contactId", 685 | "in": "path", 686 | "required": true, 687 | "schema": { 688 | "type": "string" 689 | } 690 | } 691 | ], 692 | "requestBody": { 693 | "content": { 694 | "application/json": { 695 | "schema": { 696 | "type": "object", 697 | "properties": { 698 | "street": { 699 | "type": "string" 700 | }, 701 | "city": { 702 | "type": "string" 703 | }, 704 | "province": { 705 | "type": "string" 706 | }, 707 | "country": { 708 | "type": "string" 709 | }, 710 | "postal_code": { 711 | "type": "string" 712 | } 713 | } 714 | } 715 | } 716 | } 717 | }, 718 | "responses": { 719 | "200": { 720 | "description": "Success create new address", 721 | "content": { 722 | "application/json": { 723 | "schema": { 724 | "type": "object", 725 | "properties": { 726 | "data": { 727 | "type": "object", 728 | "properties": { 729 | "id": { 730 | "type": "string" 731 | }, 732 | "street": { 733 | "type": "string" 734 | }, 735 | "city": { 736 | "type": "string" 737 | }, 738 | "province": { 739 | "type": "string" 740 | }, 741 | "country": { 742 | "type": "string" 743 | }, 744 | "postal_code": { 745 | "type": "string" 746 | }, 747 | "created_at": { 748 | "type": "number" 749 | }, 750 | "updated_at": { 751 | "type": "number" 752 | } 753 | } 754 | } 755 | } 756 | } 757 | } 758 | } 759 | } 760 | } 761 | }, 762 | "get": { 763 | "tags": [ 764 | "Address API" 765 | ], 766 | "description": "Get all addresses", 767 | "parameters": [ 768 | { 769 | "name": "Authorization", 770 | "in": "header", 771 | "required": true, 772 | "schema": { 773 | "type": "string" 774 | } 775 | }, 776 | { 777 | "name": "contactId", 778 | "in": "path", 779 | "required": true, 780 | "schema": { 781 | "type": "string" 782 | } 783 | } 784 | ], 785 | "responses": { 786 | "200": { 787 | "description": "Success get list of addresses", 788 | "content": { 789 | "application/json": { 790 | "schema": { 791 | "type": "object", 792 | "properties": { 793 | "data": { 794 | "type": "array", 795 | "items": { 796 | "type": "object", 797 | "properties": { 798 | "id": { 799 | "type": "string" 800 | }, 801 | "street": { 802 | "type": "string" 803 | }, 804 | "city": { 805 | "type": "string" 806 | }, 807 | "province": { 808 | "type": "string" 809 | }, 810 | "country": { 811 | "type": "string" 812 | }, 813 | "postal_code": { 814 | "type": "string" 815 | }, 816 | "created_at": { 817 | "type": "number" 818 | }, 819 | "updated_at": { 820 | "type": "number" 821 | } 822 | } 823 | } 824 | } 825 | } 826 | } 827 | } 828 | } 829 | } 830 | } 831 | } 832 | }, 833 | "/api/contacts/{contactId}/addresses/{addressId}": { 834 | "get": { 835 | "tags": [ 836 | "Address API" 837 | ], 838 | "description": "Get address by id", 839 | "parameters": [ 840 | { 841 | "name": "Authorization", 842 | "in": "header", 843 | "required": true, 844 | "schema": { 845 | "type": "string" 846 | } 847 | }, 848 | { 849 | "name": "contactId", 850 | "in": "path", 851 | "required": true, 852 | "schema": { 853 | "type": "string" 854 | } 855 | }, 856 | { 857 | "name": "addressId", 858 | "in": "path", 859 | "required": true, 860 | "schema": { 861 | "type": "string" 862 | } 863 | } 864 | ], 865 | "responses": { 866 | "200": { 867 | "description": "Success create new address", 868 | "content": { 869 | "application/json": { 870 | "schema": { 871 | "type": "object", 872 | "properties": { 873 | "data": { 874 | "type": "object", 875 | "properties": { 876 | "id": { 877 | "type": "string" 878 | }, 879 | "street": { 880 | "type": "string" 881 | }, 882 | "city": { 883 | "type": "string" 884 | }, 885 | "province": { 886 | "type": "string" 887 | }, 888 | "country": { 889 | "type": "string" 890 | }, 891 | "postal_code": { 892 | "type": "string" 893 | }, 894 | "created_at": { 895 | "type": "number" 896 | }, 897 | "updated_at": { 898 | "type": "number" 899 | } 900 | } 901 | } 902 | } 903 | } 904 | } 905 | } 906 | } 907 | } 908 | }, 909 | "put": { 910 | "tags": [ 911 | "Address API" 912 | ], 913 | "description": "Update address by id", 914 | "parameters": [ 915 | { 916 | "name": "Authorization", 917 | "in": "header", 918 | "required": true, 919 | "schema": { 920 | "type": "string" 921 | } 922 | }, 923 | { 924 | "name": "contactId", 925 | "in": "path", 926 | "required": true, 927 | "schema": { 928 | "type": "string" 929 | } 930 | }, 931 | { 932 | "name": "addressId", 933 | "in": "path", 934 | "required": true, 935 | "schema": { 936 | "type": "string" 937 | } 938 | } 939 | ], 940 | "requestBody": { 941 | "content": { 942 | "application/json": { 943 | "schema": { 944 | "type": "object", 945 | "properties": { 946 | "street": { 947 | "type": "string" 948 | }, 949 | "city": { 950 | "type": "string" 951 | }, 952 | "province": { 953 | "type": "string" 954 | }, 955 | "country": { 956 | "type": "string" 957 | }, 958 | "postal_code": { 959 | "type": "string" 960 | } 961 | } 962 | } 963 | } 964 | } 965 | }, 966 | "responses": { 967 | "200": { 968 | "description": "Success create new address", 969 | "content": { 970 | "application/json": { 971 | "schema": { 972 | "type": "object", 973 | "properties": { 974 | "data": { 975 | "type": "object", 976 | "properties": { 977 | "id": { 978 | "type": "string" 979 | }, 980 | "street": { 981 | "type": "string" 982 | }, 983 | "city": { 984 | "type": "string" 985 | }, 986 | "province": { 987 | "type": "string" 988 | }, 989 | "country": { 990 | "type": "string" 991 | }, 992 | "postal_code": { 993 | "type": "string" 994 | }, 995 | "created_at": { 996 | "type": "number" 997 | }, 998 | "updated_at": { 999 | "type": "number" 1000 | } 1001 | } 1002 | } 1003 | } 1004 | } 1005 | } 1006 | } 1007 | } 1008 | } 1009 | }, 1010 | "delete": { 1011 | "tags": [ 1012 | "Address API" 1013 | ], 1014 | "description": "Delete address by id", 1015 | "parameters": [ 1016 | { 1017 | "name": "Authorization", 1018 | "in": "header", 1019 | "required": true, 1020 | "schema": { 1021 | "type": "string" 1022 | } 1023 | }, 1024 | { 1025 | "name": "contactId", 1026 | "in": "path", 1027 | "required": true, 1028 | "schema": { 1029 | "type": "string" 1030 | } 1031 | }, 1032 | { 1033 | "name": "addressId", 1034 | "in": "path", 1035 | "required": true, 1036 | "schema": { 1037 | "type": "string" 1038 | } 1039 | } 1040 | ], 1041 | "responses": { 1042 | "200": { 1043 | "description": "Success create new address", 1044 | "content": { 1045 | "application/json": { 1046 | "schema": { 1047 | "type": "object", 1048 | "properties": { 1049 | "data": { 1050 | "type": "boolean" 1051 | } 1052 | } 1053 | } 1054 | } 1055 | } 1056 | } 1057 | } 1058 | } 1059 | } 1060 | } 1061 | } 1062 | -------------------------------------------------------------------------------- /architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khannedy/golang-clean-architecture/b1aa3e8e9cd84dd8d55f4a3e56831f9226163d31/architecture.png -------------------------------------------------------------------------------- /cmd/web/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "golang-clean-architecture/internal/config" 6 | ) 7 | 8 | func main() { 9 | viperConfig := config.NewViper() 10 | log := config.NewLogger(viperConfig) 11 | db := config.NewDatabase(viperConfig, log) 12 | validate := config.NewValidator(viperConfig) 13 | app := config.NewFiber(viperConfig) 14 | producer := config.NewKafkaProducer(viperConfig, log) 15 | 16 | config.Bootstrap(&config.BootstrapConfig{ 17 | DB: db, 18 | App: app, 19 | Log: log, 20 | Validate: validate, 21 | Config: viperConfig, 22 | Producer: producer, 23 | }) 24 | 25 | webPort := viperConfig.GetInt("web.port") 26 | err := app.Listen(fmt.Sprintf(":%d", webPort)) 27 | if err != nil { 28 | log.Fatalf("Failed to start server: %v", err) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cmd/worker/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "github.com/sirupsen/logrus" 6 | "github.com/spf13/viper" 7 | "golang-clean-architecture/internal/config" 8 | "golang-clean-architecture/internal/delivery/messaging" 9 | "os" 10 | "os/signal" 11 | "syscall" 12 | "time" 13 | ) 14 | 15 | func main() { 16 | viperConfig := config.NewViper() 17 | logger := config.NewLogger(viperConfig) 18 | logger.Info("Starting worker service") 19 | 20 | ctx, cancel := context.WithCancel(context.Background()) 21 | 22 | go RunUserConsumer(logger, viperConfig, ctx) 23 | go RunContactConsumer(logger, viperConfig, ctx) 24 | go RunAddressConsumer(logger, viperConfig, ctx) 25 | 26 | terminateSignals := make(chan os.Signal, 1) 27 | signal.Notify(terminateSignals, syscall.SIGINT, syscall.SIGKILL, syscall.SIGTERM) 28 | 29 | stop := false 30 | for !stop { 31 | select { 32 | case s := <-terminateSignals: 33 | logger.Info("Got one of stop signals, shutting down worker gracefully, SIGNAL NAME :", s) 34 | cancel() 35 | stop = true 36 | } 37 | } 38 | 39 | time.Sleep(5 * time.Second) // wait for all consumers to finish processing 40 | } 41 | 42 | func RunAddressConsumer(logger *logrus.Logger, viperConfig *viper.Viper, ctx context.Context) { 43 | logger.Info("setup address consumer") 44 | addressConsumer := config.NewKafkaConsumer(viperConfig, logger) 45 | addressHandler := messaging.NewAddressConsumer(logger) 46 | messaging.ConsumeTopic(ctx, addressConsumer, "addresses", logger, addressHandler.Consume) 47 | } 48 | 49 | func RunContactConsumer(logger *logrus.Logger, viperConfig *viper.Viper, ctx context.Context) { 50 | logger.Info("setup contact consumer") 51 | contactConsumer := config.NewKafkaConsumer(viperConfig, logger) 52 | contactHandler := messaging.NewContactConsumer(logger) 53 | messaging.ConsumeTopic(ctx, contactConsumer, "contacts", logger, contactHandler.Consume) 54 | } 55 | 56 | func RunUserConsumer(logger *logrus.Logger, viperConfig *viper.Viper, ctx context.Context) { 57 | logger.Info("setup user consumer") 58 | userConsumer := config.NewKafkaConsumer(viperConfig, logger) 59 | userHandler := messaging.NewUserConsumer(logger) 60 | messaging.ConsumeTopic(ctx, userConsumer, "users", logger, userHandler.Consume) 61 | } 62 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "name": "golang-clean-architecture" 4 | }, 5 | "web": { 6 | "prefork": false, 7 | "port": 3000 8 | }, 9 | "log": { 10 | "level": 6 11 | }, 12 | "database": { 13 | "username": "root", 14 | "password": "", 15 | "host": "localhost", 16 | "port": 3306, 17 | "name": "golang_clean_architecture", 18 | "pool": { 19 | "idle": 10, 20 | "max": 100, 21 | "lifetime": 300 22 | } 23 | }, 24 | "kafka": { 25 | "bootstrap": { 26 | "servers": "localhost:9092" 27 | }, 28 | "group": { 29 | "id": "golang-clean-architecture" 30 | }, 31 | "auto": { 32 | "offset": { 33 | "reset": "earliest" 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /db/migrations/20231030144428_create_table_users.down.sql: -------------------------------------------------------------------------------- 1 | drop table users; -------------------------------------------------------------------------------- /db/migrations/20231030144428_create_table_users.up.sql: -------------------------------------------------------------------------------- 1 | create table users 2 | ( 3 | id varchar(100) not null, 4 | name varchar(100) not null, 5 | password varchar(100) not null, 6 | token varchar(100) null, 7 | created_at bigint not null, 8 | updated_at bigint not null, 9 | primary key (id) 10 | ) engine = InnoDB; -------------------------------------------------------------------------------- /db/migrations/20231030144435_create_table_contacts.down.sql: -------------------------------------------------------------------------------- 1 | drop table contacts; -------------------------------------------------------------------------------- /db/migrations/20231030144435_create_table_contacts.up.sql: -------------------------------------------------------------------------------- 1 | create table contacts 2 | ( 3 | id varchar(100) not null, 4 | first_name varchar(100) not null, 5 | last_name varchar(100) null, 6 | email varchar(100) null, 7 | phone varchar(100) null, 8 | user_id varchar(100) not null, 9 | created_at bigint not null, 10 | updated_at bigint not null, 11 | primary key (id), 12 | foreign key fk_contacts_user_id (user_id) references users (id) 13 | ) engine = innodb; -------------------------------------------------------------------------------- /db/migrations/20231030144441_create_table_addresses.down.sql: -------------------------------------------------------------------------------- 1 | drop table addresses; -------------------------------------------------------------------------------- /db/migrations/20231030144441_create_table_addresses.up.sql: -------------------------------------------------------------------------------- 1 | create table addresses 2 | ( 3 | id varchar(100) not null, 4 | contact_id varchar(100) not null, 5 | street varchar(255), 6 | city varchar(255), 7 | province varchar(255), 8 | postal_code varchar(10), 9 | country varchar(100), 10 | created_at bigint not null, 11 | updated_at bigint not null, 12 | primary key (id), 13 | foreign key fk_addresses_contact_id (contact_id) references contacts (id) 14 | ) engine = innodb; -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module golang-clean-architecture 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/confluentinc/confluent-kafka-go/v2 v2.8.0 7 | github.com/go-playground/validator/v10 v10.24.0 8 | github.com/gofiber/fiber/v2 v2.52.6 9 | github.com/google/uuid v1.6.0 10 | github.com/sirupsen/logrus v1.9.3 11 | github.com/spf13/viper v1.19.0 12 | github.com/stretchr/testify v1.10.0 13 | golang.org/x/crypto v0.32.0 14 | gorm.io/driver/mysql v1.5.7 15 | gorm.io/gorm v1.25.12 16 | ) 17 | 18 | require ( 19 | filippo.io/edwards25519 v1.1.0 // indirect 20 | github.com/andybalholm/brotli v1.1.1 // indirect 21 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 22 | github.com/fsnotify/fsnotify v1.8.0 // indirect 23 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 24 | github.com/go-playground/locales v0.14.1 // indirect 25 | github.com/go-playground/universal-translator v0.18.1 // indirect 26 | github.com/go-sql-driver/mysql v1.8.1 // indirect 27 | github.com/hashicorp/hcl v1.0.0 // indirect 28 | github.com/jinzhu/inflection v1.0.0 // indirect 29 | github.com/jinzhu/now v1.1.5 // indirect 30 | github.com/klauspost/compress v1.17.11 // indirect 31 | github.com/leodido/go-urn v1.4.0 // indirect 32 | github.com/magiconair/properties v1.8.9 // indirect 33 | github.com/mattn/go-colorable v0.1.14 // indirect 34 | github.com/mattn/go-isatty v0.0.20 // indirect 35 | github.com/mattn/go-runewidth v0.0.16 // indirect 36 | github.com/mitchellh/mapstructure v1.5.0 // indirect 37 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 38 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 39 | github.com/rivo/uniseg v0.4.7 // indirect 40 | github.com/sagikazarmark/locafero v0.7.0 // indirect 41 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 42 | github.com/sourcegraph/conc v0.3.0 // indirect 43 | github.com/spf13/afero v1.12.0 // indirect 44 | github.com/spf13/cast v1.7.1 // indirect 45 | github.com/spf13/pflag v1.0.5 // indirect 46 | github.com/subosito/gotenv v1.6.0 // indirect 47 | github.com/valyala/bytebufferpool v1.0.0 // indirect 48 | github.com/valyala/fasthttp v1.58.0 // indirect 49 | github.com/valyala/tcplisten v1.0.0 // indirect 50 | go.uber.org/multierr v1.11.0 // indirect 51 | golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect 52 | golang.org/x/net v0.34.0 // indirect 53 | golang.org/x/sys v0.29.0 // indirect 54 | golang.org/x/text v0.21.0 // indirect 55 | gopkg.in/ini.v1 v1.67.0 // indirect 56 | gopkg.in/yaml.v3 v3.0.1 // indirect 57 | ) 58 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= 2 | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 4 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 5 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= 6 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= 7 | github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= 8 | github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= 9 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= 10 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 11 | github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= 12 | github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= 13 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 14 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 15 | github.com/Microsoft/hcsshim v0.11.5 h1:haEcLNpj9Ka1gd3B3tAEs9CpE0c+1IhoL59w/exYU38= 16 | github.com/Microsoft/hcsshim v0.11.5/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU= 17 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= 18 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= 19 | github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= 20 | github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= 21 | github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA= 22 | github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= 23 | github.com/aws/aws-sdk-go-v2/config v1.27.10 h1:PS+65jThT0T/snC5WjyfHHyUgG+eBoupSDV+f838cro= 24 | github.com/aws/aws-sdk-go-v2/config v1.27.10/go.mod h1:BePM7Vo4OBpHreKRUMuDXX+/+JWP38FLkzl5m27/Jjs= 25 | github.com/aws/aws-sdk-go-v2/credentials v1.17.10 h1:qDZ3EA2lv1KangvQB6y258OssCHD0xvaGiEDkG4X/10= 26 | github.com/aws/aws-sdk-go-v2/credentials v1.17.10/go.mod h1:6t3sucOaYDwDssHQa0ojH1RpmVmF5/jArkye1b2FKMI= 27 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4= 28 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg= 29 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg= 30 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I= 31 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0= 32 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc= 33 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= 34 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= 35 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs= 36 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg= 37 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo= 38 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk= 39 | github.com/aws/aws-sdk-go-v2/service/sso v1.20.4 h1:WzFol5Cd+yDxPAdnzTA5LmpHYSWinhmSj4rQChV0ee8= 40 | github.com/aws/aws-sdk-go-v2/service/sso v1.20.4/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM= 41 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 h1:Jux+gDDyi1Lruk+KHF91tK2KCuY61kzoCpvtvJJBtOE= 42 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak= 43 | github.com/aws/aws-sdk-go-v2/service/sts v1.28.6 h1:cwIxeBttqPN3qkaAjcEcsh8NYr8n2HZPkcKgPAi1phU= 44 | github.com/aws/aws-sdk-go-v2/service/sts v1.28.6/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw= 45 | github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= 46 | github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= 47 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 48 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 49 | github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY= 50 | github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE= 51 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 52 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 53 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 54 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 55 | github.com/compose-spec/compose-go/v2 v2.1.3 h1:bD67uqLuL/XgkAK6ir3xZvNLFPxPScEi1KW7R5esrLE= 56 | github.com/compose-spec/compose-go/v2 v2.1.3/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc= 57 | github.com/confluentinc/confluent-kafka-go/v2 v2.8.0 h1:0HlcSNWg4LpLA9nIjzUMIqWHI+w0S68UN7alXAc3TeA= 58 | github.com/confluentinc/confluent-kafka-go/v2 v2.8.0/go.mod h1:hScqtFIGUI1wqHIgM3mjoqEou4VweGGGX7dMpcUKves= 59 | github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= 60 | github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= 61 | github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao= 62 | github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4= 63 | github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= 64 | github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= 65 | github.com/containerd/errdefs v0.1.0 h1:m0wCRBiu1WJT/Fr+iOoQHMQS/eP5myQ8lCv4Dz5ZURM= 66 | github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0= 67 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 68 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 69 | github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= 70 | github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= 71 | github.com/containerd/ttrpc v1.2.5 h1:IFckT1EFQoFBMG4c3sMdT8EP3/aKfumK1msY+Ze4oLU= 72 | github.com/containerd/ttrpc v1.2.5/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= 73 | github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4= 74 | github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0= 75 | github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= 76 | github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= 77 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 78 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 79 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 80 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 81 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 82 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 83 | github.com/docker/buildx v0.15.1 h1:1cO6JIc0rOoC8tlxfXoh1HH1uxaNvYH1q7J7kv5enhw= 84 | github.com/docker/buildx v0.15.1/go.mod h1:16DQgJqoggmadc1UhLaUTPqKtR+PlByN/kyXFdkhFCo= 85 | github.com/docker/cli v27.0.3+incompatible h1:usGs0/BoBW8MWxGeEtqPMkzOY56jZ6kYlSN5BLDioCQ= 86 | github.com/docker/cli v27.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= 87 | github.com/docker/compose/v2 v2.28.1 h1:ORPfiVHrpnRQBDoC3F8JJyWAY8N5gWuo3FgwyivxFdM= 88 | github.com/docker/compose/v2 v2.28.1/go.mod h1:wDtGQFHe99sPLCHXeVbCkc+Wsl4Y/2ZxiAJa/nga6rA= 89 | github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= 90 | github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 91 | github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= 92 | github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 93 | github.com/docker/docker-credential-helpers v0.8.0 h1:YQFtbBQb4VrpoPxhFuzEBPQ9E16qz5SpHLS+uswaCp8= 94 | github.com/docker/docker-credential-helpers v0.8.0/go.mod h1:UGFXcuoQ5TxPiB54nHOZ32AWRqQdECoh/Mg0AlEYb40= 95 | github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0= 96 | github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q= 97 | github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= 98 | github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= 99 | github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= 100 | github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= 101 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 102 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 103 | github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg= 104 | github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg= 105 | github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= 106 | github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 107 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 108 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 109 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 110 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 111 | github.com/fsnotify/fsevents v0.2.0 h1:BRlvlqjvNTfogHfeBOFvSC9N0Ddy+wzQCQukyoD7o/c= 112 | github.com/fsnotify/fsevents v0.2.0/go.mod h1:B3eEk39i4hz8y1zaWS/wPrAP4O6wkIl7HQwKBr1qH/w= 113 | github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= 114 | github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 115 | github.com/fvbommel/sortorder v1.0.2 h1:mV4o8B2hKboCdkJm+a7uX/SIpZob4JzUpc5GGnM45eo= 116 | github.com/fvbommel/sortorder v1.0.2/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= 117 | github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= 118 | github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= 119 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 120 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 121 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 122 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 123 | github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= 124 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 125 | github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= 126 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 127 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 128 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 129 | github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= 130 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 131 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 132 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 133 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 134 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 135 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 136 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 137 | github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg= 138 | github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= 139 | github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 140 | github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= 141 | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 142 | github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= 143 | github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 144 | github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI= 145 | github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= 146 | github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= 147 | github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= 148 | github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= 149 | github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= 150 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 151 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 152 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 153 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 154 | github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= 155 | github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= 156 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 157 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 158 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 159 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 160 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 161 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 162 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 163 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 164 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 165 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 166 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 167 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 168 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= 169 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= 170 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 171 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 172 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 173 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 174 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 175 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 176 | github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= 177 | github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 178 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 179 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 180 | github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= 181 | github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= 182 | github.com/in-toto/in-toto-golang v0.5.0 h1:hb8bgwr0M2hGdDsLjkJ3ZqJ8JFLL/tgYdAxF/XEFBbY= 183 | github.com/in-toto/in-toto-golang v0.5.0/go.mod h1:/Rq0IZHLV7Ku5gielPT4wPHJfH1GdHMCq8+WPxw8/BE= 184 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 185 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 186 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 187 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 188 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 189 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 190 | github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= 191 | github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= 192 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 193 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 194 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 195 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 196 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= 197 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 198 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 199 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 200 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 201 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 202 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 203 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 204 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 205 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 206 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= 207 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= 208 | github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= 209 | github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 210 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 211 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 212 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 213 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 214 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 215 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 216 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 217 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 218 | github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= 219 | github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= 220 | github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= 221 | github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 222 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= 223 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 224 | github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= 225 | github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= 226 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 227 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 228 | github.com/moby/buildkit v0.14.1 h1:2epLCZTkn4CikdImtsLtIa++7DzCimrrZCT1sway+oI= 229 | github.com/moby/buildkit v0.14.1/go.mod h1:1XssG7cAqv5Bz1xcGMxJL123iCv5TYN4Z/qf647gfuk= 230 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 231 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 232 | github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= 233 | github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= 234 | github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= 235 | github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= 236 | github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= 237 | github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= 238 | github.com/moby/sys/mountinfo v0.7.1 h1:/tTvQaSJRr2FshkhXiIpux6fQ2Zvc4j7tAhMTStAG2g= 239 | github.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= 240 | github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= 241 | github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= 242 | github.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI= 243 | github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= 244 | github.com/moby/sys/symlink v0.2.0 h1:tk1rOM+Ljp0nFmfOIBtlV3rTDlWOwFRhjEeAhZB0nZc= 245 | github.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6BMgR/gFs= 246 | github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= 247 | github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= 248 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 249 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 250 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 251 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 252 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 253 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 254 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 255 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 256 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 257 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 258 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= 259 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 260 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 261 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 262 | github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= 263 | github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= 264 | github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= 265 | github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 266 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 267 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 268 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 269 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 270 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 271 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 272 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 273 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= 274 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 275 | github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= 276 | github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= 277 | github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= 278 | github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= 279 | github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= 280 | github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= 281 | github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 282 | github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 283 | github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc h1:zAsgcP8MhzAbhMnB1QQ2O7ZhWYVGYSR2iVcjzQuPV+o= 284 | github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc/go.mod h1:S8xSOnV3CgpNrWd0GQ/OoQfMtlg2uPRSuTzcSGrzwK8= 285 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 286 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 287 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 288 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 289 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 290 | github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= 291 | github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= 292 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 293 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 294 | github.com/secure-systems-lab/go-securesystemslib v0.4.0 h1:b23VGrQhTA8cN2CbBw7/FulN9fTtqYUdS5+Oxzt+DUE= 295 | github.com/secure-systems-lab/go-securesystemslib v0.4.0/go.mod h1:FGBZgq2tXWICsxWQW1msNf49F0Pf2Op5Htayx335Qbs= 296 | github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b h1:h+3JX2VoWTFuyQEo87pStk/a99dzIO1mM9KxIyLPGTU= 297 | github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc= 298 | github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= 299 | github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= 300 | github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= 301 | github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= 302 | github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= 303 | github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= 304 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 305 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 306 | github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= 307 | github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= 308 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 309 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 310 | github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= 311 | github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= 312 | github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= 313 | github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 314 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 315 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 316 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 317 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 318 | github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= 319 | github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= 320 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 321 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 322 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 323 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 324 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 325 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 326 | github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw= 327 | github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8= 328 | github.com/testcontainers/testcontainers-go/modules/compose v0.33.0 h1:PyrUOF+zG+xrS3p+FesyVxMI+9U+7pwhZhyFozH3jKY= 329 | github.com/testcontainers/testcontainers-go/modules/compose v0.33.0/go.mod h1:oqZaUnFEskdZriO51YBquku/jhgzoXHPot6xe1DqKV4= 330 | github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c= 331 | github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw= 332 | github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 h1:QB54BJwA6x8QU9nHY3xJSZR2kX9bgpZekRKGkLTmEXA= 333 | github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375/go.mod h1:xRroudyp5iVtxKqZCrA6n2TLFRBf8bmnjr1UD4x+z7g= 334 | github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= 335 | github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= 336 | github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= 337 | github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= 338 | github.com/tonistiigi/fsutil v0.0.0-20240424095704-91a3fc46842c h1:+6wg/4ORAbnSoGDzg2Q1i3CeMcT/jjhye/ZfnBHy7/M= 339 | github.com/tonistiigi/fsutil v0.0.0-20240424095704-91a3fc46842c/go.mod h1:vbbYqJlnswsbJqWUcJN8fKtBhnEgldDrcagTgnBVKKM= 340 | github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/v/cCndK0AMpt1wiVFb/YYmqB3/QG0= 341 | github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk= 342 | github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab h1:H6aJ0yKQ0gF49Qb2z5hI1UHxSQt4JMyxebFR15KnApw= 343 | github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc= 344 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 345 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 346 | github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE= 347 | github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw= 348 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= 349 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 350 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= 351 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 352 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= 353 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 354 | github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= 355 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 356 | github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= 357 | github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 358 | github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= 359 | github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 360 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= 361 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= 362 | go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 h1:gbhw/u49SS3gkPWiYweQNJGm/uJN5GkI/FrosxSHT7A= 363 | go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1/go.mod h1:GnOaBaFQ2we3b9AGWJpsBa7v1S5RlQzlC3O7dRMxZhM= 364 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= 365 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= 366 | go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= 367 | go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= 368 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.42.0 h1:ZtfnDL+tUrs1F0Pzfwbg2d59Gru9NCH3bgSHBM6LDwU= 369 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.42.0/go.mod h1:hG4Fj/y8TR/tlEDREo8tWstl9fO9gcFkn4xrx0Io8xU= 370 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.42.0 h1:NmnYCiR0qNufkldjVvyQfZTHSdzeHoZ41zggMsdMcLM= 371 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.42.0/go.mod h1:UVAO61+umUsHLtYb8KXXRoHtxUkdOPkYidzW3gipRLQ= 372 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.42.0 h1:wNMDy/LVGLj2h3p6zg4d0gypKfWKSWI14E1C4smOgl8= 373 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.42.0/go.mod h1:YfbDdXAAkemWJK3H/DshvlrxqFB2rtW4rY6ky/3x/H0= 374 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw= 375 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg= 376 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk= 377 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0= 378 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 h1:digkEZCJWobwBqMwC0cwCq8/wkkRy/OowZg5OArWZrM= 379 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0/go.mod h1:/OpE/y70qVkndM0TrxT4KBoN3RsFZP0QaofcfYrj76I= 380 | go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= 381 | go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= 382 | go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= 383 | go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= 384 | go.opentelemetry.io/otel/sdk/metric v1.29.0 h1:K2CfmJohnRgvZ9UAj2/FhIf/okdWcNdBwe1m8xFXiSY= 385 | go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ= 386 | go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= 387 | go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= 388 | go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= 389 | go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= 390 | go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= 391 | go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= 392 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 393 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 394 | golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= 395 | golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= 396 | golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= 397 | golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= 398 | golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= 399 | golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= 400 | golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= 401 | golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 402 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 403 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 404 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 405 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 406 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 407 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 408 | golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= 409 | golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= 410 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 411 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 412 | golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= 413 | golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 414 | google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= 415 | google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= 416 | google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= 417 | google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= 418 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A= 419 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= 420 | google.golang.org/grpc v1.67.3 h1:OgPcDAFKHnH8X3O4WcO4XUc8GRDeKsKReqbQtiCj7N8= 421 | google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s= 422 | google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= 423 | google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 424 | gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= 425 | gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= 426 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 427 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 428 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 429 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 430 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 431 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 432 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 433 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 434 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 435 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 436 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 437 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 438 | gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= 439 | gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= 440 | gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 441 | gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= 442 | gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= 443 | k8s.io/api v0.29.2 h1:hBC7B9+MU+ptchxEqTNW2DkUosJpp1P+Wn6YncZ474A= 444 | k8s.io/api v0.29.2/go.mod h1:sdIaaKuU7P44aoyyLlikSLayT6Vb7bvJNCX105xZXY0= 445 | k8s.io/apimachinery v0.29.2 h1:EWGpfJ856oj11C52NRCHuU7rFDwxev48z+6DSlGNsV8= 446 | k8s.io/apimachinery v0.29.2/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU= 447 | k8s.io/client-go v0.29.2 h1:FEg85el1TeZp+/vYJM7hkDlSTFZ+c5nnK44DJ4FyoRg= 448 | k8s.io/client-go v0.29.2/go.mod h1:knlvFZE58VpqbQpJNbCbctTVXcd35mMyAAwBdpt4jrA= 449 | k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= 450 | k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= 451 | k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= 452 | k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= 453 | k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= 454 | k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 455 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= 456 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 457 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= 458 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= 459 | sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= 460 | sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= 461 | tags.cncf.io/container-device-interface v0.7.2 h1:MLqGnWfOr1wB7m08ieI4YJ3IoLKKozEnnNYBtacDPQU= 462 | tags.cncf.io/container-device-interface v0.7.2/go.mod h1:Xb1PvXv2BhfNb3tla4r9JL129ck1Lxv9KuU6eVOfKto= 463 | -------------------------------------------------------------------------------- /internal/config/app.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/confluentinc/confluent-kafka-go/v2/kafka" 5 | "github.com/go-playground/validator/v10" 6 | "github.com/gofiber/fiber/v2" 7 | "github.com/sirupsen/logrus" 8 | "github.com/spf13/viper" 9 | "golang-clean-architecture/internal/delivery/http" 10 | "golang-clean-architecture/internal/delivery/http/middleware" 11 | "golang-clean-architecture/internal/delivery/http/route" 12 | "golang-clean-architecture/internal/gateway/messaging" 13 | "golang-clean-architecture/internal/repository" 14 | "golang-clean-architecture/internal/usecase" 15 | "gorm.io/gorm" 16 | ) 17 | 18 | type BootstrapConfig struct { 19 | DB *gorm.DB 20 | App *fiber.App 21 | Log *logrus.Logger 22 | Validate *validator.Validate 23 | Config *viper.Viper 24 | Producer *kafka.Producer 25 | } 26 | 27 | func Bootstrap(config *BootstrapConfig) { 28 | // setup repositories 29 | userRepository := repository.NewUserRepository(config.Log) 30 | contactRepository := repository.NewContactRepository(config.Log) 31 | addressRepository := repository.NewAddressRepository(config.Log) 32 | 33 | // setup producer 34 | userProducer := messaging.NewUserProducer(config.Producer, config.Log) 35 | contactProducer := messaging.NewContactProducer(config.Producer, config.Log) 36 | addressProducer := messaging.NewAddressProducer(config.Producer, config.Log) 37 | 38 | // setup use cases 39 | userUseCase := usecase.NewUserUseCase(config.DB, config.Log, config.Validate, userRepository, userProducer) 40 | contactUseCase := usecase.NewContactUseCase(config.DB, config.Log, config.Validate, contactRepository, contactProducer) 41 | addressUseCase := usecase.NewAddressUseCase(config.DB, config.Log, config.Validate, contactRepository, addressRepository, addressProducer) 42 | 43 | // setup controller 44 | userController := http.NewUserController(userUseCase, config.Log) 45 | contactController := http.NewContactController(contactUseCase, config.Log) 46 | addressController := http.NewAddressController(addressUseCase, config.Log) 47 | 48 | // setup middleware 49 | authMiddleware := middleware.NewAuth(userUseCase) 50 | 51 | routeConfig := route.RouteConfig{ 52 | App: config.App, 53 | UserController: userController, 54 | ContactController: contactController, 55 | AddressController: addressController, 56 | AuthMiddleware: authMiddleware, 57 | } 58 | routeConfig.Setup() 59 | } 60 | -------------------------------------------------------------------------------- /internal/config/fiber.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | func NewFiber(config *viper.Viper) *fiber.App { 9 | var app = fiber.New(fiber.Config{ 10 | AppName: config.GetString("app.name"), 11 | ErrorHandler: NewErrorHandler(), 12 | Prefork: config.GetBool("web.prefork"), 13 | }) 14 | 15 | return app 16 | } 17 | 18 | func NewErrorHandler() fiber.ErrorHandler { 19 | return func(ctx *fiber.Ctx, err error) error { 20 | code := fiber.StatusInternalServerError 21 | if e, ok := err.(*fiber.Error); ok { 22 | code = e.Code 23 | } 24 | 25 | return ctx.Status(code).JSON(fiber.Map{ 26 | "errors": err.Error(), 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internal/config/gorm.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "github.com/sirupsen/logrus" 6 | "github.com/spf13/viper" 7 | "gorm.io/driver/mysql" 8 | "gorm.io/gorm" 9 | "gorm.io/gorm/logger" 10 | "time" 11 | ) 12 | 13 | func NewDatabase(viper *viper.Viper, log *logrus.Logger) *gorm.DB { 14 | username := viper.GetString("database.username") 15 | password := viper.GetString("database.password") 16 | host := viper.GetString("database.host") 17 | port := viper.GetInt("database.port") 18 | database := viper.GetString("database.name") 19 | idleConnection := viper.GetInt("database.pool.idle") 20 | maxConnection := viper.GetInt("database.pool.max") 21 | maxLifeTimeConnection := viper.GetInt("database.pool.lifetime") 22 | 23 | dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", username, password, host, port, database) 24 | 25 | db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ 26 | Logger: logger.New(&logrusWriter{Logger: log}, logger.Config{ 27 | SlowThreshold: time.Second * 5, 28 | Colorful: false, 29 | IgnoreRecordNotFoundError: true, 30 | ParameterizedQueries: true, 31 | LogLevel: logger.Info, 32 | }), 33 | }) 34 | if err != nil { 35 | log.Fatalf("failed to connect database: %v", err) 36 | } 37 | 38 | connection, err := db.DB() 39 | if err != nil { 40 | log.Fatalf("failed to connect database: %v", err) 41 | } 42 | 43 | connection.SetMaxIdleConns(idleConnection) 44 | connection.SetMaxOpenConns(maxConnection) 45 | connection.SetConnMaxLifetime(time.Second * time.Duration(maxLifeTimeConnection)) 46 | 47 | return db 48 | } 49 | 50 | type logrusWriter struct { 51 | Logger *logrus.Logger 52 | } 53 | 54 | func (l *logrusWriter) Printf(message string, args ...interface{}) { 55 | l.Logger.Tracef(message, args...) 56 | } 57 | -------------------------------------------------------------------------------- /internal/config/kafka.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/confluentinc/confluent-kafka-go/v2/kafka" 5 | "github.com/sirupsen/logrus" 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | func NewKafkaConsumer(config *viper.Viper, log *logrus.Logger) *kafka.Consumer { 10 | kafkaConfig := &kafka.ConfigMap{ 11 | "bootstrap.servers": config.GetString("kafka.bootstrap.servers"), 12 | "group.id": config.GetString("kafka.group.id"), 13 | "auto.offset.reset": config.GetString("kafka.auto.offset.reset"), 14 | } 15 | 16 | consumer, err := kafka.NewConsumer(kafkaConfig) 17 | if err != nil { 18 | log.Fatalf("Failed to create consumer: %v", err) 19 | } 20 | return consumer 21 | } 22 | 23 | func NewKafkaProducer(config *viper.Viper, log *logrus.Logger) *kafka.Producer { 24 | kafkaConfig := &kafka.ConfigMap{ 25 | "bootstrap.servers": config.GetString("kafka.bootstrap.servers"), 26 | } 27 | 28 | producer, err := kafka.NewProducer(kafkaConfig) 29 | if err != nil { 30 | log.Fatalf("Failed to create producer: %v", err) 31 | } 32 | return producer 33 | } 34 | -------------------------------------------------------------------------------- /internal/config/logrus.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | func NewLogger(viper *viper.Viper) *logrus.Logger { 9 | log := logrus.New() 10 | 11 | log.SetLevel(logrus.Level(viper.GetInt32("log.level"))) 12 | log.SetFormatter(&logrus.JSONFormatter{}) 13 | 14 | return log 15 | } 16 | -------------------------------------------------------------------------------- /internal/config/validator.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/go-playground/validator/v10" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | func NewValidator(viper *viper.Viper) *validator.Validate { 9 | return validator.New() 10 | } 11 | -------------------------------------------------------------------------------- /internal/config/viper.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | // NewViper is a function to load config from config.json 9 | // You can change the implementation, for example load from env file, consul, etcd, etc 10 | func NewViper() *viper.Viper { 11 | config := viper.New() 12 | 13 | config.SetConfigName("config") 14 | config.SetConfigType("json") 15 | config.AddConfigPath("./../") 16 | config.AddConfigPath("./") 17 | err := config.ReadInConfig() 18 | 19 | if err != nil { 20 | panic(fmt.Errorf("Fatal error config file: %w \n", err)) 21 | } 22 | 23 | return config 24 | } 25 | -------------------------------------------------------------------------------- /internal/delivery/http/address_controller.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "github.com/sirupsen/logrus" 6 | "golang-clean-architecture/internal/delivery/http/middleware" 7 | "golang-clean-architecture/internal/model" 8 | "golang-clean-architecture/internal/usecase" 9 | ) 10 | 11 | type AddressController struct { 12 | UseCase *usecase.AddressUseCase 13 | Log *logrus.Logger 14 | } 15 | 16 | func NewAddressController(useCase *usecase.AddressUseCase, log *logrus.Logger) *AddressController { 17 | return &AddressController{ 18 | Log: log, 19 | UseCase: useCase, 20 | } 21 | } 22 | 23 | func (c *AddressController) Create(ctx *fiber.Ctx) error { 24 | auth := middleware.GetUser(ctx) 25 | 26 | request := new(model.CreateAddressRequest) 27 | if err := ctx.BodyParser(request); err != nil { 28 | c.Log.WithError(err).Error("failed to parse request body") 29 | return fiber.ErrBadRequest 30 | } 31 | 32 | request.UserId = auth.ID 33 | request.ContactId = ctx.Params("contactId") 34 | 35 | response, err := c.UseCase.Create(ctx.UserContext(), request) 36 | if err != nil { 37 | c.Log.WithError(err).Error("failed to create address") 38 | return err 39 | } 40 | 41 | return ctx.JSON(model.WebResponse[*model.AddressResponse]{Data: response}) 42 | } 43 | 44 | func (c *AddressController) List(ctx *fiber.Ctx) error { 45 | auth := middleware.GetUser(ctx) 46 | contactId := ctx.Params("contactId") 47 | 48 | request := &model.ListAddressRequest{ 49 | UserId: auth.ID, 50 | ContactId: contactId, 51 | } 52 | 53 | responses, err := c.UseCase.List(ctx.UserContext(), request) 54 | if err != nil { 55 | c.Log.WithError(err).Error("failed to list addresses") 56 | return err 57 | } 58 | 59 | return ctx.JSON(model.WebResponse[[]model.AddressResponse]{Data: responses}) 60 | } 61 | 62 | func (c *AddressController) Get(ctx *fiber.Ctx) error { 63 | auth := middleware.GetUser(ctx) 64 | contactId := ctx.Params("contactId") 65 | addressId := ctx.Params("addressId") 66 | 67 | request := &model.GetAddressRequest{ 68 | UserId: auth.ID, 69 | ContactId: contactId, 70 | ID: addressId, 71 | } 72 | 73 | response, err := c.UseCase.Get(ctx.UserContext(), request) 74 | if err != nil { 75 | c.Log.WithError(err).Error("failed to get address") 76 | return err 77 | } 78 | 79 | return ctx.JSON(model.WebResponse[*model.AddressResponse]{Data: response}) 80 | } 81 | 82 | func (c *AddressController) Update(ctx *fiber.Ctx) error { 83 | auth := middleware.GetUser(ctx) 84 | 85 | request := new(model.UpdateAddressRequest) 86 | if err := ctx.BodyParser(request); err != nil { 87 | c.Log.WithError(err).Error("failed to parse request body") 88 | return fiber.ErrBadRequest 89 | } 90 | 91 | request.UserId = auth.ID 92 | request.ContactId = ctx.Params("contactId") 93 | request.ID = ctx.Params("addressId") 94 | 95 | response, err := c.UseCase.Update(ctx.UserContext(), request) 96 | if err != nil { 97 | c.Log.WithError(err).Error("failed to update address") 98 | return err 99 | } 100 | 101 | return ctx.JSON(model.WebResponse[*model.AddressResponse]{Data: response}) 102 | } 103 | 104 | func (c *AddressController) Delete(ctx *fiber.Ctx) error { 105 | auth := middleware.GetUser(ctx) 106 | contactId := ctx.Params("contactId") 107 | addressId := ctx.Params("addressId") 108 | 109 | request := &model.DeleteAddressRequest{ 110 | UserId: auth.ID, 111 | ContactId: contactId, 112 | ID: addressId, 113 | } 114 | 115 | if err := c.UseCase.Delete(ctx.UserContext(), request); err != nil { 116 | c.Log.WithError(err).Error("failed to delete address") 117 | return err 118 | } 119 | 120 | return ctx.JSON(model.WebResponse[bool]{Data: true}) 121 | } 122 | -------------------------------------------------------------------------------- /internal/delivery/http/contact_controller.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "github.com/sirupsen/logrus" 6 | "golang-clean-architecture/internal/delivery/http/middleware" 7 | "golang-clean-architecture/internal/model" 8 | "golang-clean-architecture/internal/usecase" 9 | "math" 10 | ) 11 | 12 | type ContactController struct { 13 | UseCase *usecase.ContactUseCase 14 | Log *logrus.Logger 15 | } 16 | 17 | func NewContactController(useCase *usecase.ContactUseCase, log *logrus.Logger) *ContactController { 18 | return &ContactController{ 19 | UseCase: useCase, 20 | Log: log, 21 | } 22 | } 23 | 24 | func (c *ContactController) Create(ctx *fiber.Ctx) error { 25 | auth := middleware.GetUser(ctx) 26 | 27 | request := new(model.CreateContactRequest) 28 | if err := ctx.BodyParser(request); err != nil { 29 | c.Log.WithError(err).Error("error parsing request body") 30 | return fiber.ErrBadRequest 31 | } 32 | request.UserId = auth.ID 33 | 34 | response, err := c.UseCase.Create(ctx.UserContext(), request) 35 | if err != nil { 36 | c.Log.WithError(err).Error("error creating contact") 37 | return err 38 | } 39 | 40 | return ctx.JSON(model.WebResponse[*model.ContactResponse]{Data: response}) 41 | } 42 | 43 | func (c *ContactController) List(ctx *fiber.Ctx) error { 44 | auth := middleware.GetUser(ctx) 45 | 46 | request := &model.SearchContactRequest{ 47 | UserId: auth.ID, 48 | Name: ctx.Query("name", ""), 49 | Email: ctx.Query("email", ""), 50 | Phone: ctx.Query("phone", ""), 51 | Page: ctx.QueryInt("page", 1), 52 | Size: ctx.QueryInt("size", 10), 53 | } 54 | 55 | responses, total, err := c.UseCase.Search(ctx.UserContext(), request) 56 | if err != nil { 57 | c.Log.WithError(err).Error("error searching contact") 58 | return err 59 | } 60 | 61 | paging := &model.PageMetadata{ 62 | Page: request.Page, 63 | Size: request.Size, 64 | TotalItem: total, 65 | TotalPage: int64(math.Ceil(float64(total) / float64(request.Size))), 66 | } 67 | 68 | return ctx.JSON(model.WebResponse[[]model.ContactResponse]{ 69 | Data: responses, 70 | Paging: paging, 71 | }) 72 | } 73 | 74 | func (c *ContactController) Get(ctx *fiber.Ctx) error { 75 | auth := middleware.GetUser(ctx) 76 | 77 | request := &model.GetContactRequest{ 78 | UserId: auth.ID, 79 | ID: ctx.Params("contactId"), 80 | } 81 | 82 | response, err := c.UseCase.Get(ctx.UserContext(), request) 83 | if err != nil { 84 | c.Log.WithError(err).Error("error getting contact") 85 | return err 86 | } 87 | 88 | return ctx.JSON(model.WebResponse[*model.ContactResponse]{Data: response}) 89 | } 90 | 91 | func (c *ContactController) Update(ctx *fiber.Ctx) error { 92 | auth := middleware.GetUser(ctx) 93 | 94 | request := new(model.UpdateContactRequest) 95 | if err := ctx.BodyParser(request); err != nil { 96 | c.Log.WithError(err).Error("error parsing request body") 97 | return fiber.ErrBadRequest 98 | } 99 | 100 | request.UserId = auth.ID 101 | request.ID = ctx.Params("contactId") 102 | 103 | response, err := c.UseCase.Update(ctx.UserContext(), request) 104 | if err != nil { 105 | c.Log.WithError(err).Error("error updating contact") 106 | return err 107 | } 108 | 109 | return ctx.JSON(model.WebResponse[*model.ContactResponse]{Data: response}) 110 | } 111 | 112 | func (c *ContactController) Delete(ctx *fiber.Ctx) error { 113 | auth := middleware.GetUser(ctx) 114 | contactId := ctx.Params("contactId") 115 | 116 | request := &model.DeleteContactRequest{ 117 | UserId: auth.ID, 118 | ID: contactId, 119 | } 120 | 121 | if err := c.UseCase.Delete(ctx.UserContext(), request); err != nil { 122 | c.Log.WithError(err).Error("error deleting contact") 123 | return err 124 | } 125 | 126 | return ctx.JSON(model.WebResponse[bool]{Data: true}) 127 | } 128 | -------------------------------------------------------------------------------- /internal/delivery/http/middleware/auth_middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "golang-clean-architecture/internal/model" 6 | "golang-clean-architecture/internal/usecase" 7 | ) 8 | 9 | func NewAuth(userUserCase *usecase.UserUseCase) fiber.Handler { 10 | return func(ctx *fiber.Ctx) error { 11 | request := &model.VerifyUserRequest{Token: ctx.Get("Authorization", "NOT_FOUND")} 12 | userUserCase.Log.Debugf("Authorization : %s", request.Token) 13 | 14 | auth, err := userUserCase.Verify(ctx.UserContext(), request) 15 | if err != nil { 16 | userUserCase.Log.Warnf("Failed find user by token : %+v", err) 17 | return fiber.ErrUnauthorized 18 | } 19 | 20 | userUserCase.Log.Debugf("User : %+v", auth.ID) 21 | ctx.Locals("auth", auth) 22 | return ctx.Next() 23 | } 24 | } 25 | 26 | func GetUser(ctx *fiber.Ctx) *model.Auth { 27 | return ctx.Locals("auth").(*model.Auth) 28 | } 29 | -------------------------------------------------------------------------------- /internal/delivery/http/route/route.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "golang-clean-architecture/internal/delivery/http" 6 | ) 7 | 8 | type RouteConfig struct { 9 | App *fiber.App 10 | UserController *http.UserController 11 | ContactController *http.ContactController 12 | AddressController *http.AddressController 13 | AuthMiddleware fiber.Handler 14 | } 15 | 16 | func (c *RouteConfig) Setup() { 17 | c.SetupGuestRoute() 18 | c.SetupAuthRoute() 19 | } 20 | 21 | func (c *RouteConfig) SetupGuestRoute() { 22 | c.App.Post("/api/users", c.UserController.Register) 23 | c.App.Post("/api/users/_login", c.UserController.Login) 24 | } 25 | 26 | func (c *RouteConfig) SetupAuthRoute() { 27 | c.App.Use(c.AuthMiddleware) 28 | c.App.Delete("/api/users", c.UserController.Logout) 29 | c.App.Patch("/api/users/_current", c.UserController.Update) 30 | c.App.Get("/api/users/_current", c.UserController.Current) 31 | 32 | c.App.Get("/api/contacts", c.ContactController.List) 33 | c.App.Post("/api/contacts", c.ContactController.Create) 34 | c.App.Put("/api/contacts/:contactId", c.ContactController.Update) 35 | c.App.Get("/api/contacts/:contactId", c.ContactController.Get) 36 | c.App.Delete("/api/contacts/:contactId", c.ContactController.Delete) 37 | 38 | c.App.Get("/api/contacts/:contactId/addresses", c.AddressController.List) 39 | c.App.Post("/api/contacts/:contactId/addresses", c.AddressController.Create) 40 | c.App.Put("/api/contacts/:contactId/addresses/:addressId", c.AddressController.Update) 41 | c.App.Get("/api/contacts/:contactId/addresses/:addressId", c.AddressController.Get) 42 | c.App.Delete("/api/contacts/:contactId/addresses/:addressId", c.AddressController.Delete) 43 | } 44 | -------------------------------------------------------------------------------- /internal/delivery/http/user_controller.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "github.com/sirupsen/logrus" 6 | "golang-clean-architecture/internal/delivery/http/middleware" 7 | "golang-clean-architecture/internal/model" 8 | "golang-clean-architecture/internal/usecase" 9 | ) 10 | 11 | type UserController struct { 12 | Log *logrus.Logger 13 | UseCase *usecase.UserUseCase 14 | } 15 | 16 | func NewUserController(useCase *usecase.UserUseCase, logger *logrus.Logger) *UserController { 17 | return &UserController{ 18 | Log: logger, 19 | UseCase: useCase, 20 | } 21 | } 22 | 23 | func (c *UserController) Register(ctx *fiber.Ctx) error { 24 | request := new(model.RegisterUserRequest) 25 | err := ctx.BodyParser(request) 26 | if err != nil { 27 | c.Log.Warnf("Failed to parse request body : %+v", err) 28 | return fiber.ErrBadRequest 29 | } 30 | 31 | response, err := c.UseCase.Create(ctx.UserContext(), request) 32 | if err != nil { 33 | c.Log.Warnf("Failed to register user : %+v", err) 34 | return err 35 | } 36 | 37 | return ctx.JSON(model.WebResponse[*model.UserResponse]{Data: response}) 38 | } 39 | 40 | func (c *UserController) Login(ctx *fiber.Ctx) error { 41 | request := new(model.LoginUserRequest) 42 | err := ctx.BodyParser(request) 43 | if err != nil { 44 | c.Log.Warnf("Failed to parse request body : %+v", err) 45 | return fiber.ErrBadRequest 46 | } 47 | 48 | response, err := c.UseCase.Login(ctx.UserContext(), request) 49 | if err != nil { 50 | c.Log.Warnf("Failed to login user : %+v", err) 51 | return err 52 | } 53 | 54 | return ctx.JSON(model.WebResponse[*model.UserResponse]{Data: response}) 55 | } 56 | 57 | func (c *UserController) Current(ctx *fiber.Ctx) error { 58 | auth := middleware.GetUser(ctx) 59 | 60 | request := &model.GetUserRequest{ 61 | ID: auth.ID, 62 | } 63 | 64 | response, err := c.UseCase.Current(ctx.UserContext(), request) 65 | if err != nil { 66 | c.Log.WithError(err).Warnf("Failed to get current user") 67 | return err 68 | } 69 | 70 | return ctx.JSON(model.WebResponse[*model.UserResponse]{Data: response}) 71 | } 72 | 73 | func (c *UserController) Logout(ctx *fiber.Ctx) error { 74 | auth := middleware.GetUser(ctx) 75 | 76 | request := &model.LogoutUserRequest{ 77 | ID: auth.ID, 78 | } 79 | 80 | response, err := c.UseCase.Logout(ctx.UserContext(), request) 81 | if err != nil { 82 | c.Log.WithError(err).Warnf("Failed to logout user") 83 | return err 84 | } 85 | 86 | return ctx.JSON(model.WebResponse[bool]{Data: response}) 87 | } 88 | 89 | func (c *UserController) Update(ctx *fiber.Ctx) error { 90 | auth := middleware.GetUser(ctx) 91 | 92 | request := new(model.UpdateUserRequest) 93 | if err := ctx.BodyParser(request); err != nil { 94 | c.Log.Warnf("Failed to parse request body : %+v", err) 95 | return fiber.ErrBadRequest 96 | } 97 | 98 | request.ID = auth.ID 99 | response, err := c.UseCase.Update(ctx.UserContext(), request) 100 | if err != nil { 101 | c.Log.WithError(err).Warnf("Failed to update user") 102 | return err 103 | } 104 | 105 | return ctx.JSON(model.WebResponse[*model.UserResponse]{Data: response}) 106 | } 107 | -------------------------------------------------------------------------------- /internal/delivery/messaging/address_consumer.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/confluentinc/confluent-kafka-go/v2/kafka" 6 | "github.com/sirupsen/logrus" 7 | "golang-clean-architecture/internal/model" 8 | ) 9 | 10 | type AddressConsumer struct { 11 | Log *logrus.Logger 12 | } 13 | 14 | func NewAddressConsumer(log *logrus.Logger) *AddressConsumer { 15 | return &AddressConsumer{ 16 | Log: log, 17 | } 18 | } 19 | 20 | func (c AddressConsumer) Consume(message *kafka.Message) error { 21 | addressEvent := new(model.AddressEvent) 22 | if err := json.Unmarshal(message.Value, addressEvent); err != nil { 23 | c.Log.WithError(err).Error("error unmarshalling address event") 24 | return err 25 | } 26 | 27 | // TODO process event 28 | c.Log.Infof("Received topic addresses with event: %v from partition %d", addressEvent, message.TopicPartition.Partition) 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/delivery/messaging/consumer.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | import ( 4 | "context" 5 | "github.com/confluentinc/confluent-kafka-go/v2/kafka" 6 | "github.com/sirupsen/logrus" 7 | "time" 8 | ) 9 | 10 | type ConsumerHandler func(message *kafka.Message) error 11 | 12 | func ConsumeTopic(ctx context.Context, consumer *kafka.Consumer, topic string, log *logrus.Logger, handler ConsumerHandler) { 13 | err := consumer.Subscribe(topic, nil) 14 | if err != nil { 15 | log.Fatalf("Failed to subscribe to topic: %v", err) 16 | } 17 | 18 | run := true 19 | 20 | for run { 21 | select { 22 | case <-ctx.Done(): 23 | run = false 24 | default: 25 | message, err := consumer.ReadMessage(time.Second) 26 | if err == nil { 27 | err := handler(message) 28 | if err != nil { 29 | log.Errorf("Failed to process message: %v", err) 30 | } else { 31 | _, err = consumer.CommitMessage(message) 32 | if err != nil { 33 | log.Fatalf("Failed to commit message: %v", err) 34 | } 35 | } 36 | } else if !err.(kafka.Error).IsTimeout() { 37 | log.Warnf("Consumer error: %v (%v)\n", err, message) 38 | } 39 | } 40 | } 41 | 42 | log.Infof("Closing consumer for topic : %s", topic) 43 | err = consumer.Close() 44 | if err != nil { 45 | panic(err) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /internal/delivery/messaging/contact_consumer.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/confluentinc/confluent-kafka-go/v2/kafka" 6 | "github.com/sirupsen/logrus" 7 | "golang-clean-architecture/internal/model" 8 | ) 9 | 10 | type ContactConsumer struct { 11 | Log *logrus.Logger 12 | } 13 | 14 | func NewContactConsumer(log *logrus.Logger) *ContactConsumer { 15 | return &ContactConsumer{ 16 | Log: log, 17 | } 18 | } 19 | 20 | func (c ContactConsumer) Consume(message *kafka.Message) error { 21 | ContactEvent := new(model.ContactEvent) 22 | if err := json.Unmarshal(message.Value, ContactEvent); err != nil { 23 | c.Log.WithError(err).Error("error unmarshalling Contact event") 24 | return err 25 | } 26 | 27 | // TODO process event 28 | c.Log.Infof("Received topic contacts with event: %v from partition %d", ContactEvent, message.TopicPartition.Partition) 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/delivery/messaging/user_consumer.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/confluentinc/confluent-kafka-go/v2/kafka" 6 | "github.com/sirupsen/logrus" 7 | "golang-clean-architecture/internal/model" 8 | ) 9 | 10 | type UserConsumer struct { 11 | Log *logrus.Logger 12 | } 13 | 14 | func NewUserConsumer(log *logrus.Logger) *UserConsumer { 15 | return &UserConsumer{ 16 | Log: log, 17 | } 18 | } 19 | 20 | func (c UserConsumer) Consume(message *kafka.Message) error { 21 | UserEvent := new(model.UserEvent) 22 | if err := json.Unmarshal(message.Value, UserEvent); err != nil { 23 | c.Log.WithError(err).Error("error unmarshalling User event") 24 | return err 25 | } 26 | 27 | // TODO process event 28 | c.Log.Infof("Received topic users with event: %v from partition %d", UserEvent, message.TopicPartition.Partition) 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/entity/address_entity.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | type Address struct { 4 | ID string `gorm:"column:id;primaryKey"` 5 | ContactId string `gorm:"column:contact_id"` 6 | Street string `gorm:"column:street"` 7 | City string `gorm:"column:city"` 8 | Province string `gorm:"column:province"` 9 | PostalCode string `gorm:"column:postal_code"` 10 | Country string `gorm:"column:country"` 11 | CreatedAt int64 `gorm:"column:created_at;autoCreateTime:milli"` 12 | UpdatedAt int64 `gorm:"column:updated_at;autoCreateTime:milli;autoUpdateTime:milli"` 13 | Contact Contact `gorm:"foreignKey:contact_id;references:id"` 14 | } 15 | 16 | func (a *Address) TableName() string { 17 | return "addresses" 18 | } 19 | -------------------------------------------------------------------------------- /internal/entity/contact_entity.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | type Contact struct { 4 | ID string `gorm:"column:id;primaryKey"` 5 | FirstName string `gorm:"column:first_name"` 6 | LastName string `gorm:"column:last_name"` 7 | Email string `gorm:"column:email"` 8 | Phone string `gorm:"column:phone"` 9 | UserId string `gorm:"column:user_id"` 10 | CreatedAt int64 `gorm:"column:created_at;autoCreateTime:milli"` 11 | UpdatedAt int64 `gorm:"column:updated_at;autoCreateTime:milli;autoUpdateTime:milli"` 12 | User User `gorm:"foreignKey:user_id;references:id"` 13 | Addresses []Address `gorm:"foreignKey:contact_id;references:id"` 14 | } 15 | 16 | func (c *Contact) TableName() string { 17 | return "contacts" 18 | } 19 | -------------------------------------------------------------------------------- /internal/entity/user_entity.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | // User is a struct that represents a user entity 4 | type User struct { 5 | ID string `gorm:"column:id;primaryKey"` 6 | Password string `gorm:"column:password"` 7 | Name string `gorm:"column:name"` 8 | Token string `gorm:"column:token"` 9 | CreatedAt int64 `gorm:"column:created_at;autoCreateTime:milli"` 10 | UpdatedAt int64 `gorm:"column:updated_at;autoCreateTime:milli;autoUpdateTime:milli"` 11 | Contacts []Contact `gorm:"foreignKey:user_id;references:id"` 12 | } 13 | 14 | func (u *User) TableName() string { 15 | return "users" 16 | } 17 | -------------------------------------------------------------------------------- /internal/gateway/messaging/address_producer.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | import ( 4 | "github.com/confluentinc/confluent-kafka-go/v2/kafka" 5 | "github.com/sirupsen/logrus" 6 | "golang-clean-architecture/internal/model" 7 | ) 8 | 9 | type AddressProducer struct { 10 | Producer[*model.AddressEvent] 11 | } 12 | 13 | func NewAddressProducer(producer *kafka.Producer, log *logrus.Logger) *AddressProducer { 14 | return &AddressProducer{ 15 | Producer: Producer[*model.AddressEvent]{ 16 | Producer: producer, 17 | Topic: "addresses", 18 | Log: log, 19 | }, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/gateway/messaging/contact_producer.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | import ( 4 | "github.com/confluentinc/confluent-kafka-go/v2/kafka" 5 | "github.com/sirupsen/logrus" 6 | "golang-clean-architecture/internal/model" 7 | ) 8 | 9 | type ContactProducer struct { 10 | Producer[*model.ContactEvent] 11 | } 12 | 13 | func NewContactProducer(producer *kafka.Producer, log *logrus.Logger) *ContactProducer { 14 | return &ContactProducer{ 15 | Producer: Producer[*model.ContactEvent]{ 16 | Producer: producer, 17 | Topic: "contacts", 18 | Log: log, 19 | }, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/gateway/messaging/producer.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/confluentinc/confluent-kafka-go/v2/kafka" 6 | "github.com/sirupsen/logrus" 7 | "golang-clean-architecture/internal/model" 8 | ) 9 | 10 | type Producer[T model.Event] struct { 11 | Producer *kafka.Producer 12 | Topic string 13 | Log *logrus.Logger 14 | } 15 | 16 | func (p *Producer[T]) GetTopic() *string { 17 | return &p.Topic 18 | } 19 | 20 | func (p *Producer[T]) Send(event T) error { 21 | value, err := json.Marshal(event) 22 | if err != nil { 23 | p.Log.WithError(err).Error("failed to marshal event") 24 | return err 25 | } 26 | 27 | message := &kafka.Message{ 28 | TopicPartition: kafka.TopicPartition{ 29 | Topic: p.GetTopic(), 30 | Partition: kafka.PartitionAny, 31 | }, 32 | Value: value, 33 | Key: []byte(event.GetId()), 34 | } 35 | 36 | err = p.Producer.Produce(message, nil) 37 | if err != nil { 38 | p.Log.WithError(err).Error("failed to produce message") 39 | return err 40 | } 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /internal/gateway/messaging/user_producer.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | import ( 4 | "github.com/confluentinc/confluent-kafka-go/v2/kafka" 5 | "github.com/sirupsen/logrus" 6 | "golang-clean-architecture/internal/model" 7 | ) 8 | 9 | type UserProducer struct { 10 | Producer[*model.UserEvent] 11 | } 12 | 13 | func NewUserProducer(producer *kafka.Producer, log *logrus.Logger) *UserProducer { 14 | return &UserProducer{ 15 | Producer: Producer[*model.UserEvent]{ 16 | Producer: producer, 17 | Topic: "users", 18 | Log: log, 19 | }, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/model/address_event.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type AddressEvent struct { 4 | ID string `json:"id"` 5 | ContactId string `json:"contact_id"` 6 | Street string `json:"street"` 7 | City string `json:"city"` 8 | Province string `json:"province"` 9 | PostalCode string `json:"postal_code"` 10 | Country string `json:"country"` 11 | CreatedAt int64 `json:"created_at"` 12 | UpdatedAt int64 `json:"updated_at"` 13 | } 14 | 15 | func (a *AddressEvent) GetId() string { 16 | return a.ID 17 | } 18 | -------------------------------------------------------------------------------- /internal/model/address_model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type AddressResponse struct { 4 | ID string `json:"id"` 5 | Street string `json:"street"` 6 | City string `json:"city"` 7 | Province string `json:"province"` 8 | PostalCode string `json:"postal_code"` 9 | Country string `json:"country"` 10 | CreatedAt int64 `json:"created_at"` 11 | UpdatedAt int64 `json:"updated_at"` 12 | } 13 | 14 | type ListAddressRequest struct { 15 | UserId string `json:"-" validate:"required"` 16 | ContactId string `json:"-" validate:"required,max=100,uuid"` 17 | } 18 | 19 | type CreateAddressRequest struct { 20 | UserId string `json:"-" validate:"required"` 21 | ContactId string `json:"-" validate:"required,max=100,uuid"` 22 | Street string `json:"street" validate:"max=255"` 23 | City string `json:"city" validate:"max=255"` 24 | Province string `json:"province" validate:"max=255"` 25 | PostalCode string `json:"postal_code" validate:"max=10"` 26 | Country string `json:"country" validate:"max=100"` 27 | } 28 | 29 | type UpdateAddressRequest struct { 30 | UserId string `json:"-" validate:"required"` 31 | ContactId string `json:"-" validate:"required,max=100,uuid"` 32 | ID string `json:"-" validate:"required,max=100,uuid"` 33 | Street string `json:"street" validate:"max=255"` 34 | City string `json:"city" validate:"max=255"` 35 | Province string `json:"province" validate:"max=255"` 36 | PostalCode string `json:"postal_code" validate:"max=10"` 37 | Country string `json:"country" validate:"max=100"` 38 | } 39 | 40 | type GetAddressRequest struct { 41 | UserId string `json:"-" validate:"required"` 42 | ContactId string `json:"-" validate:"required,max=100,uuid"` 43 | ID string `json:"-" validate:"required,max=100,uuid"` 44 | } 45 | 46 | type DeleteAddressRequest struct { 47 | UserId string `json:"-" validate:"required"` 48 | ContactId string `json:"-" validate:"required,max=100,uuid"` 49 | ID string `json:"-" validate:"required,max=100,uuid"` 50 | } 51 | -------------------------------------------------------------------------------- /internal/model/auth.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Auth struct { 4 | // Login user id 5 | ID string 6 | } 7 | -------------------------------------------------------------------------------- /internal/model/contact_event.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type ContactEvent struct { 4 | ID string `json:"id"` 5 | UserID string `json:"user_id"` 6 | FirstName string `json:"first_name"` 7 | LastName string `json:"last_name"` 8 | Email string `json:"email"` 9 | Phone string `json:"phone"` 10 | CreatedAt int64 `json:"created_at"` 11 | UpdatedAt int64 `json:"updated_at"` 12 | } 13 | 14 | func (c *ContactEvent) GetId() string { 15 | return c.ID 16 | } 17 | -------------------------------------------------------------------------------- /internal/model/contact_model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type ContactResponse struct { 4 | ID string `json:"id"` 5 | FirstName string `json:"first_name"` 6 | LastName string `json:"last_name"` 7 | Email string `json:"email"` 8 | Phone string `json:"phone"` 9 | CreatedAt int64 `json:"created_at"` 10 | UpdatedAt int64 `json:"updated_at"` 11 | Addresses []AddressResponse `json:"addresses,omitempty"` 12 | } 13 | 14 | type CreateContactRequest struct { 15 | UserId string `json:"-" validate:"required"` 16 | FirstName string `json:"first_name" validate:"required,max=100"` 17 | LastName string `json:"last_name" validate:"max=100"` 18 | Email string `json:"email" validate:"max=200,email"` 19 | Phone string `json:"phone" validate:"max=20"` 20 | } 21 | 22 | type UpdateContactRequest struct { 23 | UserId string `json:"-" validate:"required"` 24 | ID string `json:"-" validate:"required,max=100,uuid"` 25 | FirstName string `json:"first_name" validate:"required,max=100"` 26 | LastName string `json:"last_name" validate:"max=100"` 27 | Email string `json:"email" validate:"max=200,email"` 28 | Phone string `json:"phone" validate:"max=20"` 29 | } 30 | 31 | type SearchContactRequest struct { 32 | UserId string `json:"-" validate:"required"` 33 | Name string `json:"name" validate:"max=100"` 34 | Email string `json:"email" validate:"max=200"` 35 | Phone string `json:"phone" validate:"max=20"` 36 | Page int `json:"page" validate:"min=1"` 37 | Size int `json:"size" validate:"min=1,max=100"` 38 | } 39 | 40 | type GetContactRequest struct { 41 | UserId string `json:"-" validate:"required"` 42 | ID string `json:"-" validate:"required,max=100,uuid"` 43 | } 44 | 45 | type DeleteContactRequest struct { 46 | UserId string `json:"-" validate:"required"` 47 | ID string `json:"-" validate:"required,max=100,uuid"` 48 | } 49 | -------------------------------------------------------------------------------- /internal/model/converter/address_converter.go: -------------------------------------------------------------------------------- 1 | package converter 2 | 3 | import ( 4 | "golang-clean-architecture/internal/entity" 5 | "golang-clean-architecture/internal/model" 6 | ) 7 | 8 | func AddressToResponse(address *entity.Address) *model.AddressResponse { 9 | return &model.AddressResponse{ 10 | ID: address.ID, 11 | Street: address.Street, 12 | City: address.City, 13 | Province: address.Province, 14 | PostalCode: address.PostalCode, 15 | Country: address.Country, 16 | CreatedAt: address.CreatedAt, 17 | UpdatedAt: address.UpdatedAt, 18 | } 19 | } 20 | 21 | func AddressToEvent(address *entity.Address) *model.AddressEvent { 22 | return &model.AddressEvent{ 23 | ID: address.ID, 24 | ContactId: address.ContactId, 25 | Street: address.Street, 26 | City: address.City, 27 | Province: address.Province, 28 | PostalCode: address.PostalCode, 29 | Country: address.Country, 30 | CreatedAt: address.CreatedAt, 31 | UpdatedAt: address.UpdatedAt, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /internal/model/converter/contact_converter.go: -------------------------------------------------------------------------------- 1 | package converter 2 | 3 | import ( 4 | "golang-clean-architecture/internal/entity" 5 | "golang-clean-architecture/internal/model" 6 | ) 7 | 8 | func ContactToResponse(contact *entity.Contact) *model.ContactResponse { 9 | return &model.ContactResponse{ 10 | ID: contact.ID, 11 | FirstName: contact.FirstName, 12 | LastName: contact.LastName, 13 | Email: contact.Email, 14 | Phone: contact.Phone, 15 | CreatedAt: contact.CreatedAt, 16 | UpdatedAt: contact.UpdatedAt, 17 | } 18 | } 19 | 20 | func ContactToEvent(contact *entity.Contact) *model.ContactEvent { 21 | return &model.ContactEvent{ 22 | ID: contact.ID, 23 | UserID: contact.UserId, 24 | FirstName: contact.FirstName, 25 | LastName: contact.LastName, 26 | Email: contact.Email, 27 | Phone: contact.Phone, 28 | CreatedAt: contact.CreatedAt, 29 | UpdatedAt: contact.UpdatedAt, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /internal/model/converter/user_converter.go: -------------------------------------------------------------------------------- 1 | package converter 2 | 3 | import ( 4 | "golang-clean-architecture/internal/entity" 5 | "golang-clean-architecture/internal/model" 6 | ) 7 | 8 | func UserToResponse(user *entity.User) *model.UserResponse { 9 | return &model.UserResponse{ 10 | ID: user.ID, 11 | Name: user.Name, 12 | CreatedAt: user.CreatedAt, 13 | UpdatedAt: user.UpdatedAt, 14 | } 15 | } 16 | 17 | func UserToTokenResponse(user *entity.User) *model.UserResponse { 18 | return &model.UserResponse{ 19 | Token: user.Token, 20 | } 21 | } 22 | 23 | func UserToEvent(user *entity.User) *model.UserEvent { 24 | return &model.UserEvent{ 25 | ID: user.ID, 26 | Name: user.Name, 27 | CreatedAt: user.CreatedAt, 28 | UpdatedAt: user.UpdatedAt, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/model/event.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Event interface { 4 | GetId() string 5 | } 6 | -------------------------------------------------------------------------------- /internal/model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type WebResponse[T any] struct { 4 | Data T `json:"data"` 5 | Paging *PageMetadata `json:"paging,omitempty"` 6 | Errors string `json:"errors,omitempty"` 7 | } 8 | 9 | type PageResponse[T any] struct { 10 | Data []T `json:"data,omitempty"` 11 | PageMetadata PageMetadata `json:"paging,omitempty"` 12 | } 13 | 14 | type PageMetadata struct { 15 | Page int `json:"page"` 16 | Size int `json:"size"` 17 | TotalItem int64 `json:"total_item"` 18 | TotalPage int64 `json:"total_page"` 19 | } 20 | -------------------------------------------------------------------------------- /internal/model/user_event.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type UserEvent struct { 4 | ID string `json:"id,omitempty"` 5 | Name string `json:"name,omitempty"` 6 | CreatedAt int64 `json:"created_at,omitempty"` 7 | UpdatedAt int64 `json:"updated_at,omitempty"` 8 | } 9 | 10 | func (u *UserEvent) GetId() string { 11 | return u.ID 12 | } 13 | -------------------------------------------------------------------------------- /internal/model/user_model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type UserResponse struct { 4 | ID string `json:"id,omitempty"` 5 | Name string `json:"name,omitempty"` 6 | Token string `json:"token,omitempty"` 7 | CreatedAt int64 `json:"created_at,omitempty"` 8 | UpdatedAt int64 `json:"updated_at,omitempty"` 9 | } 10 | 11 | type VerifyUserRequest struct { 12 | Token string `validate:"required,max=100"` 13 | } 14 | 15 | type RegisterUserRequest struct { 16 | ID string `json:"id" validate:"required,max=100"` 17 | Password string `json:"password" validate:"required,max=100"` 18 | Name string `json:"name" validate:"required,max=100"` 19 | } 20 | 21 | type UpdateUserRequest struct { 22 | ID string `json:"-" validate:"required,max=100"` 23 | Password string `json:"password,omitempty" validate:"max=100"` 24 | Name string `json:"name,omitempty" validate:"max=100"` 25 | } 26 | 27 | type LoginUserRequest struct { 28 | ID string `json:"id" validate:"required,max=100"` 29 | Password string `json:"password" validate:"required,max=100"` 30 | } 31 | 32 | type LogoutUserRequest struct { 33 | ID string `json:"id" validate:"required,max=100"` 34 | } 35 | 36 | type GetUserRequest struct { 37 | ID string `json:"id" validate:"required,max=100"` 38 | } 39 | -------------------------------------------------------------------------------- /internal/repository/address_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "golang-clean-architecture/internal/entity" 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type AddressRepository struct { 10 | Repository[entity.Address] 11 | Log *logrus.Logger 12 | } 13 | 14 | func NewAddressRepository(log *logrus.Logger) *AddressRepository { 15 | return &AddressRepository{ 16 | Log: log, 17 | } 18 | } 19 | 20 | func (r *AddressRepository) FindByIdAndContactId(tx *gorm.DB, address *entity.Address, id string, contactId string) error { 21 | return tx.Where("id = ? AND contact_id = ?", id, contactId).First(address).Error 22 | } 23 | 24 | func (r *AddressRepository) FindAllByContactId(tx *gorm.DB, contactId string) ([]entity.Address, error) { 25 | var addresses []entity.Address 26 | if err := tx.Where("contact_id = ?", contactId).Find(&addresses).Error; err != nil { 27 | return nil, err 28 | } 29 | return addresses, nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/repository/contact_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "golang-clean-architecture/internal/entity" 6 | "golang-clean-architecture/internal/model" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | type ContactRepository struct { 11 | Repository[entity.Contact] 12 | Log *logrus.Logger 13 | } 14 | 15 | func NewContactRepository(log *logrus.Logger) *ContactRepository { 16 | return &ContactRepository{ 17 | Log: log, 18 | } 19 | } 20 | 21 | func (r *ContactRepository) FindByIdAndUserId(db *gorm.DB, contact *entity.Contact, id string, userId string) error { 22 | return db.Where("id = ? AND user_id = ?", id, userId).Take(contact).Error 23 | } 24 | 25 | func (r *ContactRepository) Search(db *gorm.DB, request *model.SearchContactRequest) ([]entity.Contact, int64, error) { 26 | var contacts []entity.Contact 27 | if err := db.Scopes(r.FilterContact(request)).Offset((request.Page - 1) * request.Size).Limit(request.Size).Find(&contacts).Error; err != nil { 28 | return nil, 0, err 29 | } 30 | 31 | var total int64 = 0 32 | if err := db.Model(&entity.Contact{}).Scopes(r.FilterContact(request)).Count(&total).Error; err != nil { 33 | return nil, 0, err 34 | } 35 | 36 | return contacts, total, nil 37 | } 38 | 39 | func (r *ContactRepository) FilterContact(request *model.SearchContactRequest) func(tx *gorm.DB) *gorm.DB { 40 | return func(tx *gorm.DB) *gorm.DB { 41 | tx = tx.Where("user_id = ?", request.UserId) 42 | 43 | if name := request.Name; name != "" { 44 | name = "%" + name + "%" 45 | tx = tx.Where("first_name LIKE ? OR last_name LIKE ?", name, name) 46 | } 47 | 48 | if phone := request.Phone; phone != "" { 49 | phone = "%" + phone + "%" 50 | tx = tx.Where("phone LIKE ?", phone) 51 | } 52 | 53 | if email := request.Email; email != "" { 54 | email = "%" + email + "%" 55 | tx = tx.Where("email LIKE ?", email) 56 | } 57 | 58 | return tx 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /internal/repository/repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import "gorm.io/gorm" 4 | 5 | type Repository[T any] struct { 6 | DB *gorm.DB 7 | } 8 | 9 | func (r *Repository[T]) Create(db *gorm.DB, entity *T) error { 10 | return db.Create(entity).Error 11 | } 12 | 13 | func (r *Repository[T]) Update(db *gorm.DB, entity *T) error { 14 | return db.Save(entity).Error 15 | } 16 | 17 | func (r *Repository[T]) Delete(db *gorm.DB, entity *T) error { 18 | return db.Delete(entity).Error 19 | } 20 | 21 | func (r *Repository[T]) CountById(db *gorm.DB, id any) (int64, error) { 22 | var total int64 23 | err := db.Model(new(T)).Where("id = ?", id).Count(&total).Error 24 | return total, err 25 | } 26 | 27 | func (r *Repository[T]) FindById(db *gorm.DB, entity *T, id any) error { 28 | return db.Where("id = ?", id).Take(entity).Error 29 | } 30 | -------------------------------------------------------------------------------- /internal/repository/user_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "golang-clean-architecture/internal/entity" 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type UserRepository struct { 10 | Repository[entity.User] 11 | Log *logrus.Logger 12 | } 13 | 14 | func NewUserRepository(log *logrus.Logger) *UserRepository { 15 | return &UserRepository{ 16 | Log: log, 17 | } 18 | } 19 | 20 | func (r *UserRepository) FindByToken(db *gorm.DB, user *entity.User, token string) error { 21 | return db.Where("token = ?", token).First(user).Error 22 | } 23 | -------------------------------------------------------------------------------- /internal/usecase/address_usecase.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "github.com/go-playground/validator/v10" 6 | "github.com/gofiber/fiber/v2" 7 | "github.com/google/uuid" 8 | "github.com/sirupsen/logrus" 9 | "golang-clean-architecture/internal/entity" 10 | "golang-clean-architecture/internal/gateway/messaging" 11 | "golang-clean-architecture/internal/model" 12 | "golang-clean-architecture/internal/model/converter" 13 | "golang-clean-architecture/internal/repository" 14 | "gorm.io/gorm" 15 | ) 16 | 17 | type AddressUseCase struct { 18 | DB *gorm.DB 19 | Log *logrus.Logger 20 | Validate *validator.Validate 21 | AddressRepository *repository.AddressRepository 22 | ContactRepository *repository.ContactRepository 23 | AddressProducer *messaging.AddressProducer 24 | } 25 | 26 | func NewAddressUseCase(db *gorm.DB, logger *logrus.Logger, validate *validator.Validate, 27 | contactRepository *repository.ContactRepository, addressRepository *repository.AddressRepository, 28 | addressProducer *messaging.AddressProducer) *AddressUseCase { 29 | return &AddressUseCase{ 30 | DB: db, 31 | Log: logger, 32 | Validate: validate, 33 | ContactRepository: contactRepository, 34 | AddressRepository: addressRepository, 35 | AddressProducer: addressProducer, 36 | } 37 | } 38 | 39 | func (c *AddressUseCase) Create(ctx context.Context, request *model.CreateAddressRequest) (*model.AddressResponse, error) { 40 | tx := c.DB.WithContext(ctx).Begin() 41 | defer tx.Rollback() 42 | 43 | if err := c.Validate.Struct(request); err != nil { 44 | c.Log.WithError(err).Error("failed to validate request body") 45 | return nil, fiber.ErrBadRequest 46 | } 47 | 48 | contact := new(entity.Contact) 49 | if err := c.ContactRepository.FindByIdAndUserId(tx, contact, request.ContactId, request.UserId); err != nil { 50 | c.Log.WithError(err).Error("failed to find contact") 51 | return nil, fiber.ErrNotFound 52 | } 53 | 54 | address := &entity.Address{ 55 | ID: uuid.NewString(), 56 | ContactId: contact.ID, 57 | Street: request.Street, 58 | City: request.City, 59 | Province: request.Province, 60 | PostalCode: request.PostalCode, 61 | Country: request.Country, 62 | } 63 | 64 | if err := c.AddressRepository.Create(tx, address); err != nil { 65 | c.Log.WithError(err).Error("failed to create address") 66 | return nil, fiber.ErrInternalServerError 67 | } 68 | 69 | if err := tx.Commit().Error; err != nil { 70 | c.Log.WithError(err).Error("failed to commit transaction") 71 | return nil, fiber.ErrInternalServerError 72 | } 73 | 74 | event := converter.AddressToEvent(address) 75 | if err := c.AddressProducer.Send(event); err != nil { 76 | c.Log.WithError(err).Error("failed to publish address event") 77 | return nil, fiber.ErrInternalServerError 78 | } 79 | 80 | return converter.AddressToResponse(address), nil 81 | } 82 | 83 | func (c *AddressUseCase) Update(ctx context.Context, request *model.UpdateAddressRequest) (*model.AddressResponse, error) { 84 | tx := c.DB.WithContext(ctx).Begin() 85 | defer tx.Rollback() 86 | 87 | if err := c.Validate.Struct(request); err != nil { 88 | c.Log.WithError(err).Error("failed to validate request body") 89 | return nil, fiber.ErrBadRequest 90 | } 91 | 92 | contact := new(entity.Contact) 93 | if err := c.ContactRepository.FindByIdAndUserId(tx, contact, request.ContactId, request.UserId); err != nil { 94 | c.Log.WithError(err).Error("failed to find contact") 95 | return nil, fiber.ErrNotFound 96 | } 97 | 98 | address := new(entity.Address) 99 | if err := c.AddressRepository.FindByIdAndContactId(tx, address, request.ID, contact.ID); err != nil { 100 | c.Log.WithError(err).Error("failed to find address") 101 | return nil, fiber.ErrNotFound 102 | } 103 | 104 | address.Street = request.Street 105 | address.City = request.City 106 | address.Province = request.Province 107 | address.PostalCode = request.PostalCode 108 | address.Country = request.Country 109 | 110 | if err := c.AddressRepository.Update(tx, address); err != nil { 111 | c.Log.WithError(err).Error("failed to update address") 112 | return nil, fiber.ErrInternalServerError 113 | } 114 | 115 | if err := tx.Commit().Error; err != nil { 116 | c.Log.WithError(err).Error("failed to commit transaction") 117 | return nil, fiber.ErrInternalServerError 118 | } 119 | 120 | event := converter.AddressToEvent(address) 121 | if err := c.AddressProducer.Send(event); err != nil { 122 | c.Log.WithError(err).Error("failed to publish address event") 123 | return nil, fiber.ErrInternalServerError 124 | } 125 | 126 | return converter.AddressToResponse(address), nil 127 | } 128 | 129 | func (c *AddressUseCase) Get(ctx context.Context, request *model.GetAddressRequest) (*model.AddressResponse, error) { 130 | tx := c.DB.WithContext(ctx).Begin() 131 | defer tx.Rollback() 132 | 133 | contact := new(entity.Contact) 134 | if err := c.ContactRepository.FindByIdAndUserId(tx, contact, request.ContactId, request.UserId); err != nil { 135 | c.Log.WithError(err).Error("failed to find contact") 136 | return nil, fiber.ErrNotFound 137 | } 138 | 139 | address := new(entity.Address) 140 | if err := c.AddressRepository.FindByIdAndContactId(tx, address, request.ID, request.ContactId); err != nil { 141 | c.Log.WithError(err).Error("failed to find address") 142 | return nil, fiber.ErrNotFound 143 | } 144 | 145 | if err := tx.Commit().Error; err != nil { 146 | c.Log.WithError(err).Error("failed to commit transaction") 147 | return nil, fiber.ErrInternalServerError 148 | } 149 | 150 | return converter.AddressToResponse(address), nil 151 | } 152 | 153 | func (c *AddressUseCase) Delete(ctx context.Context, request *model.DeleteAddressRequest) error { 154 | tx := c.DB.WithContext(ctx).Begin() 155 | defer tx.Rollback() 156 | 157 | contact := new(entity.Contact) 158 | if err := c.ContactRepository.FindByIdAndUserId(tx, contact, request.ContactId, request.UserId); err != nil { 159 | c.Log.WithError(err).Error("failed to find contact") 160 | return fiber.ErrNotFound 161 | } 162 | 163 | address := new(entity.Address) 164 | if err := c.AddressRepository.FindByIdAndContactId(tx, address, request.ID, request.ContactId); err != nil { 165 | c.Log.WithError(err).Error("failed to find address") 166 | return fiber.ErrNotFound 167 | } 168 | 169 | if err := c.AddressRepository.Delete(tx, address); err != nil { 170 | c.Log.WithError(err).Error("failed to delete address") 171 | return fiber.ErrInternalServerError 172 | } 173 | 174 | if err := tx.Commit().Error; err != nil { 175 | c.Log.WithError(err).Error("failed to commit transaction") 176 | return fiber.ErrInternalServerError 177 | } 178 | 179 | return nil 180 | } 181 | 182 | func (c *AddressUseCase) List(ctx context.Context, request *model.ListAddressRequest) ([]model.AddressResponse, error) { 183 | tx := c.DB.WithContext(ctx).Begin() 184 | defer tx.Rollback() 185 | 186 | contact := new(entity.Contact) 187 | if err := c.ContactRepository.FindByIdAndUserId(tx, contact, request.ContactId, request.UserId); err != nil { 188 | c.Log.WithError(err).Error("failed to find contact") 189 | return nil, fiber.ErrNotFound 190 | } 191 | 192 | addresses, err := c.AddressRepository.FindAllByContactId(tx, contact.ID) 193 | if err != nil { 194 | c.Log.WithError(err).Error("failed to find addresses") 195 | return nil, fiber.ErrInternalServerError 196 | } 197 | 198 | if err := tx.Commit().Error; err != nil { 199 | c.Log.WithError(err).Error("failed to commit transaction") 200 | return nil, fiber.ErrInternalServerError 201 | } 202 | 203 | responses := make([]model.AddressResponse, len(addresses)) 204 | for i, address := range addresses { 205 | responses[i] = *converter.AddressToResponse(&address) 206 | } 207 | 208 | return responses, nil 209 | } 210 | -------------------------------------------------------------------------------- /internal/usecase/contact_usecase.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "github.com/go-playground/validator/v10" 6 | "github.com/gofiber/fiber/v2" 7 | "github.com/google/uuid" 8 | "github.com/sirupsen/logrus" 9 | "golang-clean-architecture/internal/entity" 10 | "golang-clean-architecture/internal/gateway/messaging" 11 | "golang-clean-architecture/internal/model" 12 | "golang-clean-architecture/internal/model/converter" 13 | "golang-clean-architecture/internal/repository" 14 | "gorm.io/gorm" 15 | ) 16 | 17 | type ContactUseCase struct { 18 | DB *gorm.DB 19 | Log *logrus.Logger 20 | Validate *validator.Validate 21 | ContactRepository *repository.ContactRepository 22 | ContactProducer *messaging.ContactProducer 23 | } 24 | 25 | func NewContactUseCase(db *gorm.DB, logger *logrus.Logger, validate *validator.Validate, 26 | contactRepository *repository.ContactRepository, contactProducer *messaging.ContactProducer) *ContactUseCase { 27 | return &ContactUseCase{ 28 | DB: db, 29 | Log: logger, 30 | Validate: validate, 31 | ContactRepository: contactRepository, 32 | ContactProducer: contactProducer, 33 | } 34 | } 35 | 36 | func (c *ContactUseCase) Create(ctx context.Context, request *model.CreateContactRequest) (*model.ContactResponse, error) { 37 | tx := c.DB.WithContext(ctx).Begin() 38 | defer tx.Rollback() 39 | 40 | if err := c.Validate.Struct(request); err != nil { 41 | c.Log.WithError(err).Error("error validating request body") 42 | return nil, fiber.ErrBadRequest 43 | } 44 | 45 | contact := &entity.Contact{ 46 | ID: uuid.New().String(), 47 | FirstName: request.FirstName, 48 | LastName: request.LastName, 49 | Email: request.Email, 50 | Phone: request.Phone, 51 | UserId: request.UserId, 52 | } 53 | 54 | if err := c.ContactRepository.Create(tx, contact); err != nil { 55 | c.Log.WithError(err).Error("error creating contact") 56 | return nil, fiber.ErrInternalServerError 57 | } 58 | 59 | if err := tx.Commit().Error; err != nil { 60 | c.Log.WithError(err).Error("error creating contact") 61 | return nil, fiber.ErrInternalServerError 62 | } 63 | 64 | event := converter.ContactToEvent(contact) 65 | if err := c.ContactProducer.Send(event); err != nil { 66 | c.Log.WithError(err).Error("error publishing contact") 67 | return nil, fiber.ErrInternalServerError 68 | } 69 | 70 | return converter.ContactToResponse(contact), nil 71 | } 72 | 73 | func (c *ContactUseCase) Update(ctx context.Context, request *model.UpdateContactRequest) (*model.ContactResponse, error) { 74 | tx := c.DB.WithContext(ctx).Begin() 75 | defer tx.Rollback() 76 | 77 | contact := new(entity.Contact) 78 | if err := c.ContactRepository.FindByIdAndUserId(tx, contact, request.ID, request.UserId); err != nil { 79 | c.Log.WithError(err).Error("error getting contact") 80 | return nil, fiber.ErrNotFound 81 | } 82 | 83 | if err := c.Validate.Struct(request); err != nil { 84 | c.Log.WithError(err).Error("error validating request body") 85 | return nil, fiber.ErrBadRequest 86 | } 87 | 88 | contact.FirstName = request.FirstName 89 | contact.LastName = request.LastName 90 | contact.Email = request.Email 91 | contact.Phone = request.Phone 92 | 93 | if err := c.ContactRepository.Update(tx, contact); err != nil { 94 | c.Log.WithError(err).Error("error updating contact") 95 | return nil, fiber.ErrInternalServerError 96 | } 97 | 98 | if err := tx.Commit().Error; err != nil { 99 | c.Log.WithError(err).Error("error updating contact") 100 | return nil, fiber.ErrInternalServerError 101 | } 102 | 103 | event := converter.ContactToEvent(contact) 104 | if err := c.ContactProducer.Send(event); err != nil { 105 | c.Log.WithError(err).Error("error publishing contact") 106 | return nil, fiber.ErrInternalServerError 107 | } 108 | 109 | return converter.ContactToResponse(contact), nil 110 | } 111 | 112 | func (c *ContactUseCase) Get(ctx context.Context, request *model.GetContactRequest) (*model.ContactResponse, error) { 113 | tx := c.DB.WithContext(ctx).Begin() 114 | defer tx.Rollback() 115 | 116 | if err := c.Validate.Struct(request); err != nil { 117 | c.Log.WithError(err).Error("error validating request body") 118 | return nil, fiber.ErrBadRequest 119 | } 120 | 121 | contact := new(entity.Contact) 122 | if err := c.ContactRepository.FindByIdAndUserId(tx, contact, request.ID, request.UserId); err != nil { 123 | c.Log.WithError(err).Error("error getting contact") 124 | return nil, fiber.ErrNotFound 125 | } 126 | 127 | if err := tx.Commit().Error; err != nil { 128 | c.Log.WithError(err).Error("error getting contact") 129 | return nil, fiber.ErrInternalServerError 130 | } 131 | 132 | return converter.ContactToResponse(contact), nil 133 | } 134 | 135 | func (c *ContactUseCase) Delete(ctx context.Context, request *model.DeleteContactRequest) error { 136 | tx := c.DB.WithContext(ctx).Begin() 137 | defer tx.Rollback() 138 | 139 | if err := c.Validate.Struct(request); err != nil { 140 | c.Log.WithError(err).Error("error validating request body") 141 | return fiber.ErrBadRequest 142 | } 143 | 144 | contact := new(entity.Contact) 145 | if err := c.ContactRepository.FindByIdAndUserId(tx, contact, request.ID, request.UserId); err != nil { 146 | c.Log.WithError(err).Error("error getting contact") 147 | return fiber.ErrNotFound 148 | } 149 | 150 | if err := c.ContactRepository.Delete(tx, contact); err != nil { 151 | c.Log.WithError(err).Error("error deleting contact") 152 | return fiber.ErrInternalServerError 153 | } 154 | 155 | if err := tx.Commit().Error; err != nil { 156 | c.Log.WithError(err).Error("error deleting contact") 157 | return fiber.ErrInternalServerError 158 | } 159 | 160 | return nil 161 | } 162 | 163 | func (c *ContactUseCase) Search(ctx context.Context, request *model.SearchContactRequest) ([]model.ContactResponse, int64, error) { 164 | tx := c.DB.WithContext(ctx).Begin() 165 | defer tx.Rollback() 166 | 167 | if err := c.Validate.Struct(request); err != nil { 168 | c.Log.WithError(err).Error("error validating request body") 169 | return nil, 0, fiber.ErrBadRequest 170 | } 171 | 172 | contacts, total, err := c.ContactRepository.Search(tx, request) 173 | if err != nil { 174 | c.Log.WithError(err).Error("error getting contacts") 175 | return nil, 0, fiber.ErrInternalServerError 176 | } 177 | 178 | if err := tx.Commit().Error; err != nil { 179 | c.Log.WithError(err).Error("error getting contacts") 180 | return nil, 0, fiber.ErrInternalServerError 181 | } 182 | 183 | responses := make([]model.ContactResponse, len(contacts)) 184 | for i, contact := range contacts { 185 | responses[i] = *converter.ContactToResponse(&contact) 186 | } 187 | 188 | return responses, total, nil 189 | } 190 | -------------------------------------------------------------------------------- /internal/usecase/user_usecase.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "github.com/go-playground/validator/v10" 6 | "github.com/gofiber/fiber/v2" 7 | "github.com/google/uuid" 8 | "github.com/sirupsen/logrus" 9 | "golang-clean-architecture/internal/entity" 10 | "golang-clean-architecture/internal/gateway/messaging" 11 | "golang-clean-architecture/internal/model" 12 | "golang-clean-architecture/internal/model/converter" 13 | "golang-clean-architecture/internal/repository" 14 | "golang.org/x/crypto/bcrypt" 15 | "gorm.io/gorm" 16 | ) 17 | 18 | type UserUseCase struct { 19 | DB *gorm.DB 20 | Log *logrus.Logger 21 | Validate *validator.Validate 22 | UserRepository *repository.UserRepository 23 | UserProducer *messaging.UserProducer 24 | } 25 | 26 | func NewUserUseCase(db *gorm.DB, logger *logrus.Logger, validate *validator.Validate, 27 | userRepository *repository.UserRepository, userProducer *messaging.UserProducer) *UserUseCase { 28 | return &UserUseCase{ 29 | DB: db, 30 | Log: logger, 31 | Validate: validate, 32 | UserRepository: userRepository, 33 | UserProducer: userProducer, 34 | } 35 | } 36 | 37 | func (c *UserUseCase) Verify(ctx context.Context, request *model.VerifyUserRequest) (*model.Auth, error) { 38 | tx := c.DB.WithContext(ctx).Begin() 39 | defer tx.Rollback() 40 | 41 | err := c.Validate.Struct(request) 42 | if err != nil { 43 | c.Log.Warnf("Invalid request body : %+v", err) 44 | return nil, fiber.ErrBadRequest 45 | } 46 | 47 | user := new(entity.User) 48 | if err := c.UserRepository.FindByToken(tx, user, request.Token); err != nil { 49 | c.Log.Warnf("Failed find user by token : %+v", err) 50 | return nil, fiber.ErrNotFound 51 | } 52 | 53 | if err := tx.Commit().Error; err != nil { 54 | c.Log.Warnf("Failed commit transaction : %+v", err) 55 | return nil, fiber.ErrInternalServerError 56 | } 57 | 58 | return &model.Auth{ID: user.ID}, nil 59 | } 60 | 61 | func (c *UserUseCase) Create(ctx context.Context, request *model.RegisterUserRequest) (*model.UserResponse, error) { 62 | tx := c.DB.WithContext(ctx).Begin() 63 | defer tx.Rollback() 64 | 65 | err := c.Validate.Struct(request) 66 | if err != nil { 67 | c.Log.Warnf("Invalid request body : %+v", err) 68 | return nil, fiber.ErrBadRequest 69 | } 70 | 71 | total, err := c.UserRepository.CountById(tx, request.ID) 72 | if err != nil { 73 | c.Log.Warnf("Failed count user from database : %+v", err) 74 | return nil, fiber.ErrInternalServerError 75 | } 76 | 77 | if total > 0 { 78 | c.Log.Warnf("User already exists : %+v", err) 79 | return nil, fiber.ErrConflict 80 | } 81 | 82 | password, err := bcrypt.GenerateFromPassword([]byte(request.Password), bcrypt.DefaultCost) 83 | if err != nil { 84 | c.Log.Warnf("Failed to generate bcrype hash : %+v", err) 85 | return nil, fiber.ErrInternalServerError 86 | } 87 | 88 | user := &entity.User{ 89 | ID: request.ID, 90 | Password: string(password), 91 | Name: request.Name, 92 | } 93 | 94 | if err := c.UserRepository.Create(tx, user); err != nil { 95 | c.Log.Warnf("Failed create user to database : %+v", err) 96 | return nil, fiber.ErrInternalServerError 97 | } 98 | 99 | if err := tx.Commit().Error; err != nil { 100 | c.Log.Warnf("Failed commit transaction : %+v", err) 101 | return nil, fiber.ErrInternalServerError 102 | } 103 | 104 | event := converter.UserToEvent(user) 105 | c.Log.Info("Publishing user created event") 106 | if err = c.UserProducer.Send(event); err != nil { 107 | c.Log.Warnf("Failed publish user created event : %+v", err) 108 | return nil, fiber.ErrInternalServerError 109 | } 110 | 111 | return converter.UserToResponse(user), nil 112 | } 113 | 114 | func (c *UserUseCase) Login(ctx context.Context, request *model.LoginUserRequest) (*model.UserResponse, error) { 115 | tx := c.DB.WithContext(ctx).Begin() 116 | defer tx.Rollback() 117 | 118 | if err := c.Validate.Struct(request); err != nil { 119 | c.Log.Warnf("Invalid request body : %+v", err) 120 | return nil, fiber.ErrBadRequest 121 | } 122 | 123 | user := new(entity.User) 124 | if err := c.UserRepository.FindById(tx, user, request.ID); err != nil { 125 | c.Log.Warnf("Failed find user by id : %+v", err) 126 | return nil, fiber.ErrUnauthorized 127 | } 128 | 129 | if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(request.Password)); err != nil { 130 | c.Log.Warnf("Failed to compare user password with bcrype hash : %+v", err) 131 | return nil, fiber.ErrUnauthorized 132 | } 133 | 134 | user.Token = uuid.New().String() 135 | if err := c.UserRepository.Update(tx, user); err != nil { 136 | c.Log.Warnf("Failed save user : %+v", err) 137 | return nil, fiber.ErrInternalServerError 138 | } 139 | 140 | if err := tx.Commit().Error; err != nil { 141 | c.Log.Warnf("Failed commit transaction : %+v", err) 142 | return nil, fiber.ErrInternalServerError 143 | } 144 | 145 | event := converter.UserToEvent(user) 146 | c.Log.Info("Publishing user created event") 147 | if err := c.UserProducer.Send(event); err != nil { 148 | c.Log.Warnf("Failed publish user created event : %+v", err) 149 | return nil, fiber.ErrInternalServerError 150 | } 151 | 152 | return converter.UserToTokenResponse(user), nil 153 | } 154 | 155 | func (c *UserUseCase) Current(ctx context.Context, request *model.GetUserRequest) (*model.UserResponse, error) { 156 | tx := c.DB.WithContext(ctx).Begin() 157 | defer tx.Rollback() 158 | 159 | if err := c.Validate.Struct(request); err != nil { 160 | c.Log.Warnf("Invalid request body : %+v", err) 161 | return nil, fiber.ErrBadRequest 162 | } 163 | 164 | user := new(entity.User) 165 | if err := c.UserRepository.FindById(tx, user, request.ID); err != nil { 166 | c.Log.Warnf("Failed find user by id : %+v", err) 167 | return nil, fiber.ErrNotFound 168 | } 169 | 170 | if err := tx.Commit().Error; err != nil { 171 | c.Log.Warnf("Failed commit transaction : %+v", err) 172 | return nil, fiber.ErrInternalServerError 173 | } 174 | 175 | return converter.UserToResponse(user), nil 176 | } 177 | 178 | func (c *UserUseCase) Logout(ctx context.Context, request *model.LogoutUserRequest) (bool, error) { 179 | tx := c.DB.WithContext(ctx).Begin() 180 | defer tx.Rollback() 181 | 182 | if err := c.Validate.Struct(request); err != nil { 183 | c.Log.Warnf("Invalid request body : %+v", err) 184 | return false, fiber.ErrBadRequest 185 | } 186 | 187 | user := new(entity.User) 188 | if err := c.UserRepository.FindById(tx, user, request.ID); err != nil { 189 | c.Log.Warnf("Failed find user by id : %+v", err) 190 | return false, fiber.ErrNotFound 191 | } 192 | 193 | user.Token = "" 194 | 195 | if err := c.UserRepository.Update(tx, user); err != nil { 196 | c.Log.Warnf("Failed save user : %+v", err) 197 | return false, fiber.ErrInternalServerError 198 | } 199 | 200 | if err := tx.Commit().Error; err != nil { 201 | c.Log.Warnf("Failed commit transaction : %+v", err) 202 | return false, fiber.ErrInternalServerError 203 | } 204 | 205 | event := converter.UserToEvent(user) 206 | c.Log.Info("Publishing user created event") 207 | if err := c.UserProducer.Send(event); err != nil { 208 | c.Log.Warnf("Failed publish user created event : %+v", err) 209 | return false, fiber.ErrInternalServerError 210 | } 211 | 212 | return true, nil 213 | } 214 | 215 | func (c *UserUseCase) Update(ctx context.Context, request *model.UpdateUserRequest) (*model.UserResponse, error) { 216 | tx := c.DB.WithContext(ctx).Begin() 217 | defer tx.Rollback() 218 | 219 | if err := c.Validate.Struct(request); err != nil { 220 | c.Log.Warnf("Invalid request body : %+v", err) 221 | return nil, fiber.ErrBadRequest 222 | } 223 | 224 | user := new(entity.User) 225 | if err := c.UserRepository.FindById(tx, user, request.ID); err != nil { 226 | c.Log.Warnf("Failed find user by id : %+v", err) 227 | return nil, fiber.ErrNotFound 228 | } 229 | 230 | if request.Name != "" { 231 | user.Name = request.Name 232 | } 233 | 234 | if request.Password != "" { 235 | password, err := bcrypt.GenerateFromPassword([]byte(request.Password), bcrypt.DefaultCost) 236 | if err != nil { 237 | c.Log.Warnf("Failed to generate bcrype hash : %+v", err) 238 | return nil, fiber.ErrInternalServerError 239 | } 240 | user.Password = string(password) 241 | } 242 | 243 | if err := c.UserRepository.Update(tx, user); err != nil { 244 | c.Log.Warnf("Failed save user : %+v", err) 245 | return nil, fiber.ErrInternalServerError 246 | } 247 | 248 | if err := tx.Commit().Error; err != nil { 249 | c.Log.Warnf("Failed commit transaction : %+v", err) 250 | return nil, fiber.ErrInternalServerError 251 | } 252 | 253 | event := converter.UserToEvent(user) 254 | c.Log.Info("Publishing user created event") 255 | if err := c.UserProducer.Send(event); err != nil { 256 | c.Log.Warnf("Failed publish user created event : %+v", err) 257 | return nil, fiber.ErrInternalServerError 258 | } 259 | 260 | return converter.UserToResponse(user), nil 261 | } 262 | -------------------------------------------------------------------------------- /test/address_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/stretchr/testify/assert" 6 | "golang-clean-architecture/internal/model" 7 | "io" 8 | "net/http" 9 | "net/http/httptest" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | func TestCreateAddress(t *testing.T) { 15 | TestCreateContact(t) 16 | 17 | user := GetFirstUser(t) 18 | contact := GetFirstContact(t, user) 19 | 20 | requestBody := model.CreateAddressRequest{ 21 | Street: "Jalan Belum Jadi", 22 | City: "Jakarta", 23 | Province: "DKI Jakarta", 24 | PostalCode: "343443", 25 | Country: "Indonesia", 26 | } 27 | bodyJson, err := json.Marshal(requestBody) 28 | assert.Nil(t, err) 29 | 30 | request := httptest.NewRequest(http.MethodPost, "/api/contacts/"+contact.ID+"/addresses", strings.NewReader(string(bodyJson))) 31 | request.Header.Set("Content-Type", "application/json") 32 | request.Header.Set("Accept", "application/json") 33 | request.Header.Set("Authorization", user.Token) 34 | 35 | response, err := app.Test(request) 36 | assert.Nil(t, err) 37 | 38 | bytes, err := io.ReadAll(response.Body) 39 | assert.Nil(t, err) 40 | 41 | responseBody := new(model.WebResponse[model.AddressResponse]) 42 | err = json.Unmarshal(bytes, responseBody) 43 | assert.Nil(t, err) 44 | 45 | assert.Equal(t, http.StatusOK, response.StatusCode) 46 | assert.Equal(t, requestBody.Street, responseBody.Data.Street) 47 | assert.Equal(t, requestBody.City, responseBody.Data.City) 48 | assert.Equal(t, requestBody.Province, responseBody.Data.Province) 49 | assert.Equal(t, requestBody.Country, responseBody.Data.Country) 50 | assert.Equal(t, requestBody.PostalCode, responseBody.Data.PostalCode) 51 | assert.NotNil(t, responseBody.Data.CreatedAt) 52 | assert.NotNil(t, responseBody.Data.UpdatedAt) 53 | assert.NotNil(t, responseBody.Data.ID) 54 | } 55 | 56 | func TestCreateAddressFailed(t *testing.T) { 57 | TestCreateContact(t) 58 | 59 | user := GetFirstUser(t) 60 | contact := GetFirstContact(t, user) 61 | 62 | requestBody := model.CreateAddressRequest{ 63 | Street: "Jalan Belum Jadi", 64 | City: "Jakarta", 65 | Province: "DKI Jakarta", 66 | PostalCode: "343443343443343443343443343443343443343443", 67 | Country: "Indonesia", 68 | } 69 | bodyJson, err := json.Marshal(requestBody) 70 | assert.Nil(t, err) 71 | 72 | request := httptest.NewRequest(http.MethodPost, "/api/contacts/"+contact.ID+"/addresses", strings.NewReader(string(bodyJson))) 73 | request.Header.Set("Content-Type", "application/json") 74 | request.Header.Set("Accept", "application/json") 75 | request.Header.Set("Authorization", user.Token) 76 | 77 | response, err := app.Test(request) 78 | assert.Nil(t, err) 79 | 80 | bytes, err := io.ReadAll(response.Body) 81 | assert.Nil(t, err) 82 | 83 | responseBody := new(model.WebResponse[model.AddressResponse]) 84 | err = json.Unmarshal(bytes, responseBody) 85 | assert.Nil(t, err) 86 | 87 | assert.Equal(t, http.StatusBadRequest, response.StatusCode) 88 | } 89 | 90 | func TestListAddresses(t *testing.T) { 91 | TestCreateContact(t) 92 | 93 | user := GetFirstUser(t) 94 | contact := GetFirstContact(t, user) 95 | 96 | CreateAddresses(t, contact, 5) 97 | 98 | request := httptest.NewRequest(http.MethodGet, "/api/contacts/"+contact.ID+"/addresses", nil) 99 | request.Header.Set("Accept", "application/json") 100 | request.Header.Set("Authorization", user.Token) 101 | 102 | response, err := app.Test(request) 103 | assert.Nil(t, err) 104 | 105 | bytes, err := io.ReadAll(response.Body) 106 | assert.Nil(t, err) 107 | 108 | responseBody := new(model.WebResponse[[]model.AddressResponse]) 109 | err = json.Unmarshal(bytes, responseBody) 110 | assert.Nil(t, err) 111 | 112 | assert.Equal(t, http.StatusOK, response.StatusCode) 113 | assert.Equal(t, 5, len(responseBody.Data)) 114 | } 115 | 116 | func TestListAddressesFailed(t *testing.T) { 117 | TestCreateContact(t) 118 | 119 | user := GetFirstUser(t) 120 | contact := GetFirstContact(t, user) 121 | 122 | CreateAddresses(t, contact, 5) 123 | 124 | request := httptest.NewRequest(http.MethodGet, "/api/contacts/"+"wrong"+"/addresses", nil) 125 | request.Header.Set("Accept", "application/json") 126 | request.Header.Set("Authorization", user.Token) 127 | 128 | response, err := app.Test(request) 129 | assert.Nil(t, err) 130 | 131 | bytes, err := io.ReadAll(response.Body) 132 | assert.Nil(t, err) 133 | 134 | responseBody := new(model.WebResponse[[]model.AddressResponse]) 135 | err = json.Unmarshal(bytes, responseBody) 136 | assert.Nil(t, err) 137 | 138 | assert.Equal(t, http.StatusNotFound, response.StatusCode) 139 | } 140 | 141 | func TestGetAddress(t *testing.T) { 142 | TestCreateAddress(t) 143 | 144 | user := GetFirstUser(t) 145 | contact := GetFirstContact(t, user) 146 | address := GetFirstAddress(t, contact) 147 | 148 | request := httptest.NewRequest(http.MethodGet, "/api/contacts/"+contact.ID+"/addresses/"+address.ID, nil) 149 | request.Header.Set("Accept", "application/json") 150 | request.Header.Set("Authorization", user.Token) 151 | 152 | response, err := app.Test(request) 153 | assert.Nil(t, err) 154 | 155 | bytes, err := io.ReadAll(response.Body) 156 | assert.Nil(t, err) 157 | 158 | responseBody := new(model.WebResponse[model.AddressResponse]) 159 | err = json.Unmarshal(bytes, responseBody) 160 | assert.Nil(t, err) 161 | 162 | assert.Equal(t, http.StatusOK, response.StatusCode) 163 | assert.Equal(t, address.ID, responseBody.Data.ID) 164 | assert.Equal(t, address.Street, responseBody.Data.Street) 165 | assert.Equal(t, address.City, responseBody.Data.City) 166 | assert.Equal(t, address.Province, responseBody.Data.Province) 167 | assert.Equal(t, address.Country, responseBody.Data.Country) 168 | assert.Equal(t, address.PostalCode, responseBody.Data.PostalCode) 169 | assert.Equal(t, address.CreatedAt, responseBody.Data.CreatedAt) 170 | assert.Equal(t, address.UpdatedAt, responseBody.Data.UpdatedAt) 171 | } 172 | 173 | func TestGetAddressFailed(t *testing.T) { 174 | TestCreateAddress(t) 175 | 176 | user := GetFirstUser(t) 177 | contact := GetFirstContact(t, user) 178 | 179 | request := httptest.NewRequest(http.MethodGet, "/api/contacts/"+contact.ID+"/addresses/"+"wrong", nil) 180 | request.Header.Set("Accept", "application/json") 181 | request.Header.Set("Authorization", user.Token) 182 | 183 | response, err := app.Test(request) 184 | assert.Nil(t, err) 185 | 186 | bytes, err := io.ReadAll(response.Body) 187 | assert.Nil(t, err) 188 | 189 | responseBody := new(model.WebResponse[model.AddressResponse]) 190 | err = json.Unmarshal(bytes, responseBody) 191 | assert.Nil(t, err) 192 | 193 | assert.Equal(t, http.StatusNotFound, response.StatusCode) 194 | } 195 | 196 | func TestUpdateAddress(t *testing.T) { 197 | TestCreateAddress(t) 198 | 199 | user := GetFirstUser(t) 200 | contact := GetFirstContact(t, user) 201 | address := GetFirstAddress(t, contact) 202 | 203 | requestBody := model.CreateAddressRequest{ 204 | Street: "Jalan Lagi Dijieun", 205 | City: "Bandung", 206 | Province: "Jawa Barat", 207 | PostalCode: "343443", 208 | Country: "Indonesia", 209 | } 210 | bodyJson, err := json.Marshal(requestBody) 211 | assert.Nil(t, err) 212 | 213 | request := httptest.NewRequest(http.MethodPut, "/api/contacts/"+contact.ID+"/addresses/"+address.ID, strings.NewReader(string(bodyJson))) 214 | request.Header.Set("Content-Type", "application/json") 215 | request.Header.Set("Accept", "application/json") 216 | request.Header.Set("Authorization", user.Token) 217 | 218 | response, err := app.Test(request) 219 | assert.Nil(t, err) 220 | 221 | bytes, err := io.ReadAll(response.Body) 222 | assert.Nil(t, err) 223 | 224 | responseBody := new(model.WebResponse[model.AddressResponse]) 225 | err = json.Unmarshal(bytes, responseBody) 226 | assert.Nil(t, err) 227 | 228 | assert.Equal(t, http.StatusOK, response.StatusCode) 229 | assert.Equal(t, requestBody.Street, responseBody.Data.Street) 230 | assert.Equal(t, requestBody.City, responseBody.Data.City) 231 | assert.Equal(t, requestBody.Province, responseBody.Data.Province) 232 | assert.Equal(t, requestBody.Country, responseBody.Data.Country) 233 | assert.Equal(t, requestBody.PostalCode, responseBody.Data.PostalCode) 234 | assert.NotNil(t, responseBody.Data.CreatedAt) 235 | assert.NotNil(t, responseBody.Data.UpdatedAt) 236 | assert.NotNil(t, responseBody.Data.ID) 237 | } 238 | 239 | func TestUpdateAddressFailed(t *testing.T) { 240 | TestCreateAddress(t) 241 | 242 | user := GetFirstUser(t) 243 | contact := GetFirstContact(t, user) 244 | address := GetFirstAddress(t, contact) 245 | 246 | requestBody := model.UpdateAddressRequest{ 247 | Street: "Jalan Lagi Dijieun", 248 | City: "Bandung", 249 | Province: "Jawa Barat", 250 | PostalCode: "343443343443343443343443343443343443343443", 251 | Country: "Indonesia", 252 | } 253 | bodyJson, err := json.Marshal(requestBody) 254 | assert.Nil(t, err) 255 | 256 | request := httptest.NewRequest(http.MethodPut, "/api/contacts/"+contact.ID+"/addresses/"+address.ID, strings.NewReader(string(bodyJson))) 257 | request.Header.Set("Content-Type", "application/json") 258 | request.Header.Set("Accept", "application/json") 259 | request.Header.Set("Authorization", user.Token) 260 | 261 | response, err := app.Test(request) 262 | assert.Nil(t, err) 263 | 264 | bytes, err := io.ReadAll(response.Body) 265 | assert.Nil(t, err) 266 | 267 | responseBody := new(model.WebResponse[model.AddressResponse]) 268 | err = json.Unmarshal(bytes, responseBody) 269 | assert.Nil(t, err) 270 | 271 | assert.Equal(t, http.StatusBadRequest, response.StatusCode) 272 | } 273 | 274 | func TestDeleteAddress(t *testing.T) { 275 | TestCreateAddress(t) 276 | 277 | user := GetFirstUser(t) 278 | contact := GetFirstContact(t, user) 279 | address := GetFirstAddress(t, contact) 280 | 281 | request := httptest.NewRequest(http.MethodDelete, "/api/contacts/"+contact.ID+"/addresses/"+address.ID, nil) 282 | request.Header.Set("Accept", "application/json") 283 | request.Header.Set("Authorization", user.Token) 284 | 285 | response, err := app.Test(request) 286 | assert.Nil(t, err) 287 | 288 | bytes, err := io.ReadAll(response.Body) 289 | assert.Nil(t, err) 290 | 291 | responseBody := new(model.WebResponse[bool]) 292 | err = json.Unmarshal(bytes, responseBody) 293 | assert.Nil(t, err) 294 | 295 | assert.Equal(t, http.StatusOK, response.StatusCode) 296 | assert.Equal(t, true, responseBody.Data) 297 | } 298 | 299 | func TestDeleteAddressFailed(t *testing.T) { 300 | TestCreateAddress(t) 301 | 302 | user := GetFirstUser(t) 303 | contact := GetFirstContact(t, user) 304 | 305 | request := httptest.NewRequest(http.MethodDelete, "/api/contacts/"+contact.ID+"/addresses/"+"wrong", nil) 306 | request.Header.Set("Accept", "application/json") 307 | request.Header.Set("Authorization", user.Token) 308 | 309 | response, err := app.Test(request) 310 | assert.Nil(t, err) 311 | 312 | bytes, err := io.ReadAll(response.Body) 313 | assert.Nil(t, err) 314 | 315 | responseBody := new(model.WebResponse[bool]) 316 | err = json.Unmarshal(bytes, responseBody) 317 | assert.Nil(t, err) 318 | 319 | assert.Equal(t, http.StatusNotFound, response.StatusCode) 320 | } 321 | -------------------------------------------------------------------------------- /test/contact_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/google/uuid" 6 | "github.com/stretchr/testify/assert" 7 | "golang-clean-architecture/internal/entity" 8 | "golang-clean-architecture/internal/model" 9 | "io" 10 | "net/http" 11 | "net/http/httptest" 12 | "strings" 13 | "testing" 14 | ) 15 | 16 | func TestCreateContact(t *testing.T) { 17 | TestLogin(t) 18 | 19 | user := new(entity.User) 20 | err := db.Where("id = ?", "khannedy").First(user).Error 21 | assert.Nil(t, err) 22 | 23 | requestBody := model.CreateContactRequest{ 24 | FirstName: "Eko Kurniawan", 25 | LastName: "Khannedy", 26 | Email: "eko@example.com", 27 | Phone: "088888888888", 28 | } 29 | bodyJson, err := json.Marshal(requestBody) 30 | assert.Nil(t, err) 31 | 32 | request := httptest.NewRequest(http.MethodPost, "/api/contacts", strings.NewReader(string(bodyJson))) 33 | request.Header.Set("Content-Type", "application/json") 34 | request.Header.Set("Accept", "application/json") 35 | request.Header.Set("Authorization", user.Token) 36 | 37 | response, err := app.Test(request) 38 | assert.Nil(t, err) 39 | 40 | bytes, err := io.ReadAll(response.Body) 41 | assert.Nil(t, err) 42 | 43 | responseBody := new(model.WebResponse[model.ContactResponse]) 44 | err = json.Unmarshal(bytes, responseBody) 45 | assert.Nil(t, err) 46 | 47 | assert.Equal(t, http.StatusOK, response.StatusCode) 48 | assert.Equal(t, requestBody.FirstName, responseBody.Data.FirstName) 49 | assert.Equal(t, requestBody.LastName, responseBody.Data.LastName) 50 | assert.Equal(t, requestBody.Email, responseBody.Data.Email) 51 | assert.Equal(t, requestBody.Phone, responseBody.Data.Phone) 52 | assert.NotNil(t, responseBody.Data.ID) 53 | assert.NotNil(t, responseBody.Data.CreatedAt) 54 | assert.NotNil(t, responseBody.Data.UpdatedAt) 55 | } 56 | 57 | func TestCreateContactFailed(t *testing.T) { 58 | TestLogin(t) 59 | 60 | user := new(entity.User) 61 | err := db.Where("id = ?", "khannedy").First(user).Error 62 | assert.Nil(t, err) 63 | 64 | requestBody := model.CreateContactRequest{ 65 | FirstName: "", 66 | LastName: "", 67 | Email: "", 68 | Phone: "", 69 | } 70 | bodyJson, err := json.Marshal(requestBody) 71 | assert.Nil(t, err) 72 | 73 | request := httptest.NewRequest(http.MethodPost, "/api/contacts", strings.NewReader(string(bodyJson))) 74 | request.Header.Set("Content-Type", "application/json") 75 | request.Header.Set("Accept", "application/json") 76 | request.Header.Set("Authorization", user.Token) 77 | 78 | response, err := app.Test(request) 79 | assert.Nil(t, err) 80 | 81 | bytes, err := io.ReadAll(response.Body) 82 | assert.Nil(t, err) 83 | 84 | responseBody := new(model.WebResponse[model.ContactResponse]) 85 | err = json.Unmarshal(bytes, responseBody) 86 | assert.Nil(t, err) 87 | 88 | assert.Equal(t, http.StatusBadRequest, response.StatusCode) 89 | assert.NotNil(t, responseBody.Errors) 90 | } 91 | 92 | func TestGetConnect(t *testing.T) { 93 | TestCreateContact(t) 94 | 95 | user := new(entity.User) 96 | err := db.Where("id = ?", "khannedy").First(user).Error 97 | assert.Nil(t, err) 98 | 99 | contact := new(entity.Contact) 100 | err = db.Where("user_id = ?", user.ID).First(contact).Error 101 | assert.Nil(t, err) 102 | 103 | request := httptest.NewRequest(http.MethodGet, "/api/contacts/"+contact.ID, nil) 104 | request.Header.Set("Accept", "application/json") 105 | request.Header.Set("Authorization", user.Token) 106 | 107 | response, err := app.Test(request) 108 | assert.Nil(t, err) 109 | 110 | bytes, err := io.ReadAll(response.Body) 111 | assert.Nil(t, err) 112 | 113 | responseBody := new(model.WebResponse[model.ContactResponse]) 114 | err = json.Unmarshal(bytes, responseBody) 115 | assert.Nil(t, err) 116 | 117 | assert.Equal(t, http.StatusOK, response.StatusCode) 118 | assert.Equal(t, contact.ID, responseBody.Data.ID) 119 | assert.Equal(t, contact.FirstName, responseBody.Data.FirstName) 120 | assert.Equal(t, contact.LastName, responseBody.Data.LastName) 121 | assert.Equal(t, contact.Email, responseBody.Data.Email) 122 | assert.Equal(t, contact.Phone, responseBody.Data.Phone) 123 | assert.Equal(t, contact.CreatedAt, responseBody.Data.CreatedAt) 124 | assert.Equal(t, contact.UpdatedAt, responseBody.Data.UpdatedAt) 125 | } 126 | 127 | func TestGetContactFailed(t *testing.T) { 128 | TestCreateContact(t) 129 | 130 | user := new(entity.User) 131 | err := db.Where("id = ?", "khannedy").First(user).Error 132 | assert.Nil(t, err) 133 | 134 | request := httptest.NewRequest(http.MethodGet, "/api/contacts/"+uuid.NewString(), nil) 135 | request.Header.Set("Accept", "application/json") 136 | request.Header.Set("Authorization", user.Token) 137 | 138 | response, err := app.Test(request) 139 | assert.Nil(t, err) 140 | 141 | bytes, err := io.ReadAll(response.Body) 142 | assert.Nil(t, err) 143 | 144 | responseBody := new(model.WebResponse[model.ContactResponse]) 145 | err = json.Unmarshal(bytes, responseBody) 146 | assert.Nil(t, err) 147 | 148 | assert.Equal(t, http.StatusNotFound, response.StatusCode) 149 | } 150 | 151 | func TestUpdateContact(t *testing.T) { 152 | TestCreateContact(t) 153 | 154 | user := new(entity.User) 155 | err := db.Where("id = ?", "khannedy").First(user).Error 156 | assert.Nil(t, err) 157 | 158 | contact := new(entity.Contact) 159 | err = db.Where("user_id = ?", user.ID).First(contact).Error 160 | assert.Nil(t, err) 161 | 162 | requestBody := model.UpdateContactRequest{ 163 | FirstName: "Eko", 164 | LastName: "Budiman", 165 | Email: "budiman@example.com", 166 | Phone: "089898989", 167 | } 168 | bodyJson, err := json.Marshal(requestBody) 169 | assert.Nil(t, err) 170 | 171 | request := httptest.NewRequest(http.MethodPut, "/api/contacts/"+contact.ID, strings.NewReader(string(bodyJson))) 172 | request.Header.Set("Content-Type", "application/json") 173 | request.Header.Set("Accept", "application/json") 174 | request.Header.Set("Authorization", user.Token) 175 | 176 | response, err := app.Test(request) 177 | assert.Nil(t, err) 178 | 179 | bytes, err := io.ReadAll(response.Body) 180 | assert.Nil(t, err) 181 | 182 | responseBody := new(model.WebResponse[model.ContactResponse]) 183 | err = json.Unmarshal(bytes, responseBody) 184 | assert.Nil(t, err) 185 | 186 | assert.Equal(t, http.StatusOK, response.StatusCode) 187 | assert.Equal(t, requestBody.FirstName, responseBody.Data.FirstName) 188 | assert.Equal(t, requestBody.LastName, responseBody.Data.LastName) 189 | assert.Equal(t, requestBody.Email, responseBody.Data.Email) 190 | assert.Equal(t, requestBody.Phone, responseBody.Data.Phone) 191 | assert.NotNil(t, responseBody.Data.ID) 192 | assert.NotNil(t, responseBody.Data.CreatedAt) 193 | assert.NotNil(t, responseBody.Data.UpdatedAt) 194 | } 195 | 196 | func TestUpdateContactFailed(t *testing.T) { 197 | TestCreateContact(t) 198 | 199 | user := new(entity.User) 200 | err := db.Where("id = ?", "khannedy").First(user).Error 201 | assert.Nil(t, err) 202 | 203 | contact := new(entity.Contact) 204 | err = db.Where("user_id = ?", user.ID).First(contact).Error 205 | assert.Nil(t, err) 206 | 207 | requestBody := model.UpdateContactRequest{ 208 | FirstName: "", 209 | LastName: "", 210 | Email: "", 211 | Phone: "", 212 | } 213 | bodyJson, err := json.Marshal(requestBody) 214 | assert.Nil(t, err) 215 | 216 | request := httptest.NewRequest(http.MethodPut, "/api/contacts/"+contact.ID, strings.NewReader(string(bodyJson))) 217 | request.Header.Set("Content-Type", "application/json") 218 | request.Header.Set("Accept", "application/json") 219 | request.Header.Set("Authorization", user.Token) 220 | 221 | response, err := app.Test(request) 222 | assert.Nil(t, err) 223 | 224 | bytes, err := io.ReadAll(response.Body) 225 | assert.Nil(t, err) 226 | 227 | responseBody := new(model.WebResponse[model.ContactResponse]) 228 | err = json.Unmarshal(bytes, responseBody) 229 | assert.Nil(t, err) 230 | 231 | assert.Equal(t, http.StatusBadRequest, response.StatusCode) 232 | } 233 | 234 | func TestUpdateContactNotFound(t *testing.T) { 235 | TestCreateContact(t) 236 | 237 | user := new(entity.User) 238 | err := db.Where("id = ?", "khannedy").First(user).Error 239 | assert.Nil(t, err) 240 | 241 | requestBody := model.UpdateContactRequest{ 242 | FirstName: "", 243 | LastName: "", 244 | Email: "", 245 | Phone: "", 246 | } 247 | bodyJson, err := json.Marshal(requestBody) 248 | assert.Nil(t, err) 249 | 250 | request := httptest.NewRequest(http.MethodPut, "/api/contacts/"+uuid.NewString(), strings.NewReader(string(bodyJson))) 251 | request.Header.Set("Content-Type", "application/json") 252 | request.Header.Set("Accept", "application/json") 253 | request.Header.Set("Authorization", user.Token) 254 | 255 | response, err := app.Test(request) 256 | assert.Nil(t, err) 257 | 258 | bytes, err := io.ReadAll(response.Body) 259 | assert.Nil(t, err) 260 | 261 | responseBody := new(model.WebResponse[model.ContactResponse]) 262 | err = json.Unmarshal(bytes, responseBody) 263 | assert.Nil(t, err) 264 | 265 | assert.Equal(t, http.StatusNotFound, response.StatusCode) 266 | } 267 | 268 | func TestDeleteContact(t *testing.T) { 269 | TestCreateContact(t) 270 | 271 | user := new(entity.User) 272 | err := db.Where("id = ?", "khannedy").First(user).Error 273 | assert.Nil(t, err) 274 | 275 | contact := new(entity.Contact) 276 | err = db.Where("user_id = ?", user.ID).First(contact).Error 277 | assert.Nil(t, err) 278 | 279 | request := httptest.NewRequest(http.MethodDelete, "/api/contacts/"+contact.ID, nil) 280 | request.Header.Set("Accept", "application/json") 281 | request.Header.Set("Authorization", user.Token) 282 | 283 | response, err := app.Test(request) 284 | assert.Nil(t, err) 285 | 286 | bytes, err := io.ReadAll(response.Body) 287 | assert.Nil(t, err) 288 | 289 | responseBody := new(model.WebResponse[bool]) 290 | err = json.Unmarshal(bytes, responseBody) 291 | assert.Nil(t, err) 292 | 293 | assert.Equal(t, http.StatusOK, response.StatusCode) 294 | assert.Equal(t, true, responseBody.Data) 295 | } 296 | 297 | func TestDeleteContactFailed(t *testing.T) { 298 | TestCreateContact(t) 299 | 300 | user := new(entity.User) 301 | err := db.Where("id = ?", "khannedy").First(user).Error 302 | assert.Nil(t, err) 303 | 304 | request := httptest.NewRequest(http.MethodDelete, "/api/contacts/"+uuid.NewString(), nil) 305 | request.Header.Set("Accept", "application/json") 306 | request.Header.Set("Authorization", user.Token) 307 | 308 | response, err := app.Test(request) 309 | assert.Nil(t, err) 310 | 311 | bytes, err := io.ReadAll(response.Body) 312 | assert.Nil(t, err) 313 | 314 | responseBody := new(model.WebResponse[bool]) 315 | err = json.Unmarshal(bytes, responseBody) 316 | assert.Nil(t, err) 317 | 318 | assert.Equal(t, http.StatusNotFound, response.StatusCode) 319 | } 320 | 321 | func TestSearchContact(t *testing.T) { 322 | TestLogin(t) 323 | 324 | user := new(entity.User) 325 | err := db.Where("id = ?", "khannedy").First(user).Error 326 | assert.Nil(t, err) 327 | 328 | CreateContacts(user, 20) 329 | 330 | request := httptest.NewRequest(http.MethodGet, "/api/contacts", nil) 331 | request.Header.Set("Accept", "application/json") 332 | request.Header.Set("Authorization", user.Token) 333 | 334 | response, err := app.Test(request) 335 | assert.Nil(t, err) 336 | 337 | bytes, err := io.ReadAll(response.Body) 338 | assert.Nil(t, err) 339 | 340 | responseBody := new(model.WebResponse[[]model.ContactResponse]) 341 | err = json.Unmarshal(bytes, responseBody) 342 | assert.Nil(t, err) 343 | 344 | assert.Equal(t, http.StatusOK, response.StatusCode) 345 | assert.Equal(t, 10, len(responseBody.Data)) 346 | assert.Equal(t, int64(20), responseBody.Paging.TotalItem) 347 | assert.Equal(t, int64(2), responseBody.Paging.TotalPage) 348 | assert.Equal(t, 1, responseBody.Paging.Page) 349 | assert.Equal(t, 10, responseBody.Paging.Size) 350 | } 351 | 352 | func TestSearchContactWithPagination(t *testing.T) { 353 | TestLogin(t) 354 | 355 | user := new(entity.User) 356 | err := db.Where("id = ?", "khannedy").First(user).Error 357 | assert.Nil(t, err) 358 | 359 | CreateContacts(user, 20) 360 | 361 | request := httptest.NewRequest(http.MethodGet, "/api/contacts?page=2&size=5", nil) 362 | request.Header.Set("Accept", "application/json") 363 | request.Header.Set("Authorization", user.Token) 364 | 365 | response, err := app.Test(request) 366 | assert.Nil(t, err) 367 | 368 | bytes, err := io.ReadAll(response.Body) 369 | assert.Nil(t, err) 370 | 371 | responseBody := new(model.WebResponse[[]model.ContactResponse]) 372 | err = json.Unmarshal(bytes, responseBody) 373 | assert.Nil(t, err) 374 | 375 | assert.Equal(t, http.StatusOK, response.StatusCode) 376 | assert.Equal(t, 5, len(responseBody.Data)) 377 | assert.Equal(t, int64(20), responseBody.Paging.TotalItem) 378 | assert.Equal(t, int64(4), responseBody.Paging.TotalPage) 379 | assert.Equal(t, 2, responseBody.Paging.Page) 380 | assert.Equal(t, 5, responseBody.Paging.Size) 381 | } 382 | 383 | func TestSearchContactWithFilter(t *testing.T) { 384 | TestLogin(t) 385 | 386 | user := new(entity.User) 387 | err := db.Where("id = ?", "khannedy").First(user).Error 388 | assert.Nil(t, err) 389 | 390 | CreateContacts(user, 20) 391 | 392 | request := httptest.NewRequest(http.MethodGet, "/api/contacts?name=contact&phone=08000000&email=example.com", nil) 393 | request.Header.Set("Accept", "application/json") 394 | request.Header.Set("Authorization", user.Token) 395 | 396 | response, err := app.Test(request) 397 | assert.Nil(t, err) 398 | 399 | bytes, err := io.ReadAll(response.Body) 400 | assert.Nil(t, err) 401 | 402 | responseBody := new(model.WebResponse[[]model.ContactResponse]) 403 | err = json.Unmarshal(bytes, responseBody) 404 | assert.Nil(t, err) 405 | 406 | assert.Equal(t, http.StatusOK, response.StatusCode) 407 | assert.Equal(t, 10, len(responseBody.Data)) 408 | assert.Equal(t, int64(20), responseBody.Paging.TotalItem) 409 | assert.Equal(t, int64(2), responseBody.Paging.TotalPage) 410 | assert.Equal(t, 1, responseBody.Paging.Page) 411 | assert.Equal(t, 10, responseBody.Paging.Size) 412 | } 413 | -------------------------------------------------------------------------------- /test/helper_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/stretchr/testify/assert" 6 | "golang-clean-architecture/internal/entity" 7 | "strconv" 8 | "testing" 9 | ) 10 | 11 | func ClearAll() { 12 | ClearAddresses() 13 | ClearContact() 14 | ClearUsers() 15 | } 16 | 17 | func ClearUsers() { 18 | err := db.Where("id is not null").Delete(&entity.User{}).Error 19 | if err != nil { 20 | log.Fatalf("Failed clear user data : %+v", err) 21 | } 22 | } 23 | 24 | func ClearContact() { 25 | err := db.Where("id is not null").Delete(&entity.Contact{}).Error 26 | if err != nil { 27 | log.Fatalf("Failed clear contact data : %+v", err) 28 | } 29 | } 30 | 31 | func ClearAddresses() { 32 | err := db.Where("id is not null").Delete(&entity.Address{}).Error 33 | if err != nil { 34 | log.Fatalf("Failed clear address data : %+v", err) 35 | } 36 | } 37 | 38 | func CreateContacts(user *entity.User, total int) { 39 | for i := 0; i < total; i++ { 40 | contact := &entity.Contact{ 41 | ID: uuid.NewString(), 42 | FirstName: "Contact", 43 | LastName: strconv.Itoa(i), 44 | Email: "contact" + strconv.Itoa(i) + "@example.com", 45 | Phone: "08000000" + strconv.Itoa(i), 46 | UserId: user.ID, 47 | } 48 | err := db.Create(contact).Error 49 | if err != nil { 50 | log.Fatalf("Failed create contact data : %+v", err) 51 | } 52 | } 53 | } 54 | 55 | func CreateAddresses(t *testing.T, contact *entity.Contact, total int) { 56 | for i := 0; i < total; i++ { 57 | address := &entity.Address{ 58 | ID: uuid.NewString(), 59 | ContactId: contact.ID, 60 | Street: "Jalan Belum Jadi", 61 | City: "Jakarta", 62 | Province: "DKI Jakarta", 63 | PostalCode: "2131323", 64 | Country: "Indonesia", 65 | } 66 | err := db.Create(address).Error 67 | assert.Nil(t, err) 68 | } 69 | } 70 | 71 | func GetFirstUser(t *testing.T) *entity.User { 72 | user := new(entity.User) 73 | err := db.First(user).Error 74 | assert.Nil(t, err) 75 | return user 76 | } 77 | 78 | func GetFirstContact(t *testing.T, user *entity.User) *entity.Contact { 79 | contact := new(entity.Contact) 80 | err := db.Where("user_id = ?", user.ID).First(contact).Error 81 | assert.Nil(t, err) 82 | return contact 83 | } 84 | 85 | func GetFirstAddress(t *testing.T, contact *entity.Contact) *entity.Address { 86 | address := new(entity.Address) 87 | err := db.Where("contact_id = ?", contact.ID).First(address).Error 88 | assert.Nil(t, err) 89 | return address 90 | } 91 | -------------------------------------------------------------------------------- /test/http-client.env.json: -------------------------------------------------------------------------------- 1 | { 2 | "dev": { 3 | "token" : "0cd85818-8720-4121-b8ae-c5dff37869e5", 4 | "contactId": "a1568432-0c07-454f-bc18-9bb8499b85b3", 5 | "addressId": "e4bcd519-f514-4ba2-8f5c-c186ecb56663" 6 | } 7 | } -------------------------------------------------------------------------------- /test/init.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/go-playground/validator/v10" 5 | "github.com/gofiber/fiber/v2" 6 | "github.com/sirupsen/logrus" 7 | "github.com/spf13/viper" 8 | "golang-clean-architecture/internal/config" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | var app *fiber.App 13 | 14 | var db *gorm.DB 15 | 16 | var viperConfig *viper.Viper 17 | 18 | var log *logrus.Logger 19 | 20 | var validate *validator.Validate 21 | 22 | func init() { 23 | viperConfig = config.NewViper() 24 | log = config.NewLogger(viperConfig) 25 | validate = config.NewValidator(viperConfig) 26 | app = config.NewFiber(viperConfig) 27 | db = config.NewDatabase(viperConfig, log) 28 | producer := config.NewKafkaProducer(viperConfig, log) 29 | 30 | config.Bootstrap(&config.BootstrapConfig{ 31 | DB: db, 32 | App: app, 33 | Log: log, 34 | Validate: validate, 35 | Config: viperConfig, 36 | Producer: producer, 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /test/manual.http: -------------------------------------------------------------------------------- 1 | ### Register new user 2 | POST http://localhost:3000/api/users 3 | Content-Type: application/json 4 | 5 | { 6 | "name": "Joko", 7 | "id": "joko", 8 | "password": "joko" 9 | } 10 | 11 | ### Login user 12 | POST http://localhost:3000/api/users/_login 13 | Content-Type: application/json 14 | 15 | { 16 | "id": "joko", 17 | "password": "joko" 18 | } 19 | 20 | ### Get user profile 21 | GET http://localhost:3000/api/users/_current 22 | Accept: application/json 23 | Authorization: {{token}} 24 | 25 | ### Logout user 26 | DELETE http://localhost:3000/api/users 27 | Accept: application/json 28 | Authorization: {{token}} 29 | 30 | ### Update user 31 | PATCH http://localhost:3000/api/users/_current 32 | Content-Type: application/json 33 | Accept: application/json 34 | Authorization: {{token}} 35 | 36 | { 37 | "name": "Joko Morro" 38 | } 39 | 40 | ### Create contact 41 | POST http://localhost:3000/api/contacts 42 | Content-Type: application/json 43 | Accept: application/json 44 | Authorization: {{token}} 45 | 46 | { 47 | "first_name": "Joko", 48 | "last_name": "Morro", 49 | "phone": "08123456789", 50 | "email": "joko@example.com" 51 | } 52 | 53 | ### Get detail contact 54 | GET http://localhost:3000/api/contacts/{{contactId}} 55 | Content-Type: application/json 56 | Accept: application/json 57 | Authorization: {{token}} 58 | 59 | ### Search contacts 60 | GET http://localhost:3000/api/contacts?size=10&page=1&name=jo&phone=0812&email=joko 61 | Content-Type: application/json 62 | Accept: application/json 63 | Authorization: {{token}} 64 | 65 | ### update contact 66 | PUT http://localhost:3000/api/contacts/{{contactId}} 67 | Content-Type: application/json 68 | Accept: application/json 69 | Authorization: {{token}} 70 | 71 | { 72 | "first_name": "Budi", 73 | "last_name": "Nugraha", 74 | "phone": "088324324", 75 | "email": "budi@example.com" 76 | } 77 | 78 | ### delete contact 79 | DELETE http://localhost:3000/api/contacts/{{contactId}} 80 | Accept: application/json 81 | Authorization: {{token}} 82 | 83 | ### get all addresses 84 | GET http://localhost:3000/api/contacts/{{contactId}}/addresses 85 | Accept: application/json 86 | Authorization: {{token}} 87 | 88 | 89 | ### create new addresses 90 | POST http://localhost:3000/api/contacts/{{contactId}}/addresses 91 | Content-Type: application/json 92 | Accept: application/json 93 | Authorization: {{token}} 94 | 95 | { 96 | "street": "Jl. Jalan", 97 | "city": "Jakarta", 98 | "province": "DKI Jakarta", 99 | "country": "Indonesia", 100 | "postal_code": "12345" 101 | } 102 | 103 | ### get address detail 104 | GET http://localhost:3000/api/contacts/{{contactId}}/addresses/{{addressId}} 105 | Accept: application/json 106 | Authorization: {{token}} 107 | 108 | ### update address 109 | PUT http://localhost:3000/api/contacts/{{contactId}}/addresses/{{addressId}} 110 | Content-Type: application/json 111 | Accept: application/json 112 | Authorization: {{token}} 113 | 114 | { 115 | "street": "Jl. Sudah Jadi", 116 | "city": "Jakarta", 117 | "province": "DKI Jakarta", 118 | "country": "Indonesia", 119 | "postal_code": "12345" 120 | } 121 | 122 | ### delete address 123 | DELETE http://localhost:3000/api/contacts/{{contactId}}/addresses/{{addressId}} 124 | Accept: application/json 125 | Authorization: {{token}} -------------------------------------------------------------------------------- /test/user_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/stretchr/testify/assert" 6 | "golang-clean-architecture/internal/entity" 7 | "golang-clean-architecture/internal/model" 8 | "golang.org/x/crypto/bcrypt" 9 | "io" 10 | "net/http" 11 | "net/http/httptest" 12 | "strings" 13 | "testing" 14 | ) 15 | 16 | func TestRegister(t *testing.T) { 17 | ClearAll() 18 | requestBody := model.RegisterUserRequest{ 19 | ID: "khannedy", 20 | Password: "rahasia", 21 | Name: "Eko Khannedy", 22 | } 23 | 24 | bodyJson, err := json.Marshal(requestBody) 25 | assert.Nil(t, err) 26 | 27 | request := httptest.NewRequest(http.MethodPost, "/api/users", strings.NewReader(string(bodyJson))) 28 | request.Header.Set("Content-Type", "application/json") 29 | request.Header.Set("Accept", "application/json") 30 | 31 | response, err := app.Test(request) 32 | assert.Nil(t, err) 33 | 34 | bytes, err := io.ReadAll(response.Body) 35 | assert.Nil(t, err) 36 | 37 | responseBody := new(model.WebResponse[model.UserResponse]) 38 | err = json.Unmarshal(bytes, responseBody) 39 | assert.Nil(t, err) 40 | 41 | assert.Equal(t, http.StatusOK, response.StatusCode) 42 | assert.Equal(t, requestBody.ID, responseBody.Data.ID) 43 | assert.Equal(t, requestBody.Name, responseBody.Data.Name) 44 | assert.NotNil(t, responseBody.Data.CreatedAt) 45 | assert.NotNil(t, responseBody.Data.UpdatedAt) 46 | } 47 | 48 | func TestRegisterError(t *testing.T) { 49 | ClearAll() 50 | requestBody := model.RegisterUserRequest{ 51 | ID: "", 52 | Password: "", 53 | Name: "", 54 | } 55 | 56 | bodyJson, err := json.Marshal(requestBody) 57 | assert.Nil(t, err) 58 | 59 | request := httptest.NewRequest(http.MethodPost, "/api/users", strings.NewReader(string(bodyJson))) 60 | request.Header.Set("Content-Type", "application/json") 61 | request.Header.Set("Accept", "application/json") 62 | 63 | response, err := app.Test(request) 64 | assert.Nil(t, err) 65 | 66 | bytes, err := io.ReadAll(response.Body) 67 | assert.Nil(t, err) 68 | 69 | responseBody := new(model.WebResponse[model.UserResponse]) 70 | err = json.Unmarshal(bytes, responseBody) 71 | assert.Nil(t, err) 72 | 73 | assert.Equal(t, http.StatusBadRequest, response.StatusCode) 74 | assert.NotNil(t, responseBody.Errors) 75 | } 76 | 77 | func TestRegisterDuplicate(t *testing.T) { 78 | ClearAll() 79 | TestRegister(t) // register success 80 | 81 | requestBody := model.RegisterUserRequest{ 82 | ID: "khannedy", 83 | Password: "rahasia", 84 | Name: "Eko Khannedy", 85 | } 86 | 87 | bodyJson, err := json.Marshal(requestBody) 88 | assert.Nil(t, err) 89 | 90 | request := httptest.NewRequest(http.MethodPost, "/api/users", strings.NewReader(string(bodyJson))) 91 | request.Header.Set("Content-Type", "application/json") 92 | request.Header.Set("Accept", "application/json") 93 | 94 | response, err := app.Test(request) 95 | assert.Nil(t, err) 96 | 97 | bytes, err := io.ReadAll(response.Body) 98 | assert.Nil(t, err) 99 | 100 | responseBody := new(model.WebResponse[model.UserResponse]) 101 | err = json.Unmarshal(bytes, responseBody) 102 | assert.Nil(t, err) 103 | 104 | assert.Equal(t, http.StatusConflict, response.StatusCode) 105 | assert.NotNil(t, responseBody.Errors) 106 | } 107 | 108 | func TestLogin(t *testing.T) { 109 | TestRegister(t) // register success 110 | 111 | requestBody := model.LoginUserRequest{ 112 | ID: "khannedy", 113 | Password: "rahasia", 114 | } 115 | 116 | bodyJson, err := json.Marshal(requestBody) 117 | assert.Nil(t, err) 118 | 119 | request := httptest.NewRequest(http.MethodPost, "/api/users/_login", strings.NewReader(string(bodyJson))) 120 | request.Header.Set("Content-Type", "application/json") 121 | request.Header.Set("Accept", "application/json") 122 | 123 | response, err := app.Test(request) 124 | assert.Nil(t, err) 125 | 126 | bytes, err := io.ReadAll(response.Body) 127 | assert.Nil(t, err) 128 | 129 | responseBody := new(model.WebResponse[model.UserResponse]) 130 | err = json.Unmarshal(bytes, responseBody) 131 | assert.Nil(t, err) 132 | 133 | assert.Equal(t, http.StatusOK, response.StatusCode) 134 | assert.NotNil(t, responseBody.Data.Token) 135 | 136 | user := new(entity.User) 137 | err = db.Where("id = ?", requestBody.ID).First(user).Error 138 | assert.Nil(t, err) 139 | assert.Equal(t, user.Token, responseBody.Data.Token) 140 | } 141 | 142 | func TestLoginWrongUsername(t *testing.T) { 143 | ClearAll() 144 | TestRegister(t) // register success 145 | 146 | requestBody := model.LoginUserRequest{ 147 | ID: "wrong", 148 | Password: "rahasia", 149 | } 150 | 151 | bodyJson, err := json.Marshal(requestBody) 152 | assert.Nil(t, err) 153 | 154 | request := httptest.NewRequest(http.MethodPost, "/api/users/_login", strings.NewReader(string(bodyJson))) 155 | request.Header.Set("Content-Type", "application/json") 156 | request.Header.Set("Accept", "application/json") 157 | 158 | response, err := app.Test(request) 159 | assert.Nil(t, err) 160 | 161 | bytes, err := io.ReadAll(response.Body) 162 | assert.Nil(t, err) 163 | 164 | responseBody := new(model.WebResponse[model.UserResponse]) 165 | err = json.Unmarshal(bytes, responseBody) 166 | assert.Nil(t, err) 167 | 168 | assert.Equal(t, http.StatusUnauthorized, response.StatusCode) 169 | assert.NotNil(t, responseBody.Errors) 170 | } 171 | 172 | func TestLoginWrongPassword(t *testing.T) { 173 | ClearAll() 174 | TestRegister(t) // register success 175 | 176 | requestBody := model.LoginUserRequest{ 177 | ID: "khannedy", 178 | Password: "wrong", 179 | } 180 | 181 | bodyJson, err := json.Marshal(requestBody) 182 | assert.Nil(t, err) 183 | 184 | request := httptest.NewRequest(http.MethodPost, "/api/users/_login", strings.NewReader(string(bodyJson))) 185 | request.Header.Set("Content-Type", "application/json") 186 | request.Header.Set("Accept", "application/json") 187 | 188 | response, err := app.Test(request) 189 | assert.Nil(t, err) 190 | 191 | bytes, err := io.ReadAll(response.Body) 192 | assert.Nil(t, err) 193 | 194 | responseBody := new(model.WebResponse[model.UserResponse]) 195 | err = json.Unmarshal(bytes, responseBody) 196 | assert.Nil(t, err) 197 | 198 | assert.Equal(t, http.StatusUnauthorized, response.StatusCode) 199 | assert.NotNil(t, responseBody.Errors) 200 | } 201 | 202 | func TestLogout(t *testing.T) { 203 | ClearAll() 204 | TestLogin(t) // login success 205 | 206 | user := new(entity.User) 207 | err := db.Where("id = ?", "khannedy").First(user).Error 208 | assert.Nil(t, err) 209 | 210 | request := httptest.NewRequest(http.MethodDelete, "/api/users", nil) 211 | request.Header.Set("Content-Type", "application/json") 212 | request.Header.Set("Accept", "application/json") 213 | request.Header.Set("Authorization", user.Token) 214 | 215 | response, err := app.Test(request) 216 | assert.Nil(t, err) 217 | 218 | bytes, err := io.ReadAll(response.Body) 219 | assert.Nil(t, err) 220 | 221 | responseBody := new(model.WebResponse[bool]) 222 | err = json.Unmarshal(bytes, responseBody) 223 | assert.Nil(t, err) 224 | 225 | assert.Equal(t, http.StatusOK, response.StatusCode) 226 | assert.True(t, responseBody.Data) 227 | } 228 | 229 | func TestLogoutWrongAuthorization(t *testing.T) { 230 | ClearAll() 231 | TestLogin(t) // login success 232 | 233 | request := httptest.NewRequest(http.MethodDelete, "/api/users", nil) 234 | request.Header.Set("Content-Type", "application/json") 235 | request.Header.Set("Accept", "application/json") 236 | request.Header.Set("Authorization", "wrong") 237 | 238 | response, err := app.Test(request) 239 | assert.Nil(t, err) 240 | 241 | bytes, err := io.ReadAll(response.Body) 242 | assert.Nil(t, err) 243 | 244 | responseBody := new(model.WebResponse[bool]) 245 | err = json.Unmarshal(bytes, responseBody) 246 | assert.Nil(t, err) 247 | 248 | assert.Equal(t, http.StatusUnauthorized, response.StatusCode) 249 | assert.NotNil(t, responseBody.Errors) 250 | } 251 | 252 | func TestGetCurrentUser(t *testing.T) { 253 | ClearAll() 254 | TestLogin(t) // login success 255 | 256 | user := new(entity.User) 257 | err := db.Where("id = ?", "khannedy").First(user).Error 258 | assert.Nil(t, err) 259 | 260 | request := httptest.NewRequest(http.MethodGet, "/api/users/_current", nil) 261 | request.Header.Set("Content-Type", "application/json") 262 | request.Header.Set("Accept", "application/json") 263 | request.Header.Set("Authorization", user.Token) 264 | 265 | response, err := app.Test(request) 266 | assert.Nil(t, err) 267 | 268 | bytes, err := io.ReadAll(response.Body) 269 | assert.Nil(t, err) 270 | 271 | responseBody := new(model.WebResponse[model.UserResponse]) 272 | err = json.Unmarshal(bytes, responseBody) 273 | assert.Nil(t, err) 274 | 275 | assert.Equal(t, http.StatusOK, response.StatusCode) 276 | assert.Equal(t, user.ID, responseBody.Data.ID) 277 | assert.Equal(t, user.Name, responseBody.Data.Name) 278 | assert.Equal(t, user.CreatedAt, responseBody.Data.CreatedAt) 279 | assert.Equal(t, user.UpdatedAt, responseBody.Data.UpdatedAt) 280 | } 281 | 282 | func TestGetCurrentUserFailed(t *testing.T) { 283 | ClearAll() 284 | TestLogin(t) // login success 285 | 286 | request := httptest.NewRequest(http.MethodGet, "/api/users/_current", nil) 287 | request.Header.Set("Content-Type", "application/json") 288 | request.Header.Set("Accept", "application/json") 289 | request.Header.Set("Authorization", "wrong") 290 | 291 | response, err := app.Test(request) 292 | assert.Nil(t, err) 293 | 294 | bytes, err := io.ReadAll(response.Body) 295 | assert.Nil(t, err) 296 | 297 | responseBody := new(model.WebResponse[model.UserResponse]) 298 | err = json.Unmarshal(bytes, responseBody) 299 | assert.Nil(t, err) 300 | 301 | assert.Equal(t, http.StatusUnauthorized, response.StatusCode) 302 | assert.NotNil(t, responseBody.Errors) 303 | } 304 | 305 | func TestUpdateUserName(t *testing.T) { 306 | ClearAll() 307 | TestLogin(t) // login success 308 | 309 | user := new(entity.User) 310 | err := db.Where("id = ?", "khannedy").First(user).Error 311 | assert.Nil(t, err) 312 | 313 | requestBody := model.UpdateUserRequest{ 314 | Name: "Eko Kurniawan Khannedy", 315 | } 316 | 317 | bodyJson, err := json.Marshal(requestBody) 318 | assert.Nil(t, err) 319 | 320 | request := httptest.NewRequest(http.MethodPatch, "/api/users/_current", strings.NewReader(string(bodyJson))) 321 | request.Header.Set("Content-Type", "application/json") 322 | request.Header.Set("Accept", "application/json") 323 | request.Header.Set("Authorization", user.Token) 324 | 325 | response, err := app.Test(request) 326 | assert.Nil(t, err) 327 | 328 | bytes, err := io.ReadAll(response.Body) 329 | assert.Nil(t, err) 330 | 331 | responseBody := new(model.WebResponse[model.UserResponse]) 332 | err = json.Unmarshal(bytes, responseBody) 333 | assert.Nil(t, err) 334 | 335 | assert.Equal(t, http.StatusOK, response.StatusCode) 336 | assert.Equal(t, user.ID, responseBody.Data.ID) 337 | assert.Equal(t, requestBody.Name, responseBody.Data.Name) 338 | assert.NotNil(t, responseBody.Data.CreatedAt) 339 | assert.NotNil(t, responseBody.Data.UpdatedAt) 340 | } 341 | 342 | func TestUpdateUserPassword(t *testing.T) { 343 | ClearAll() 344 | TestLogin(t) // login success 345 | 346 | user := new(entity.User) 347 | err := db.Where("id = ?", "khannedy").First(user).Error 348 | assert.Nil(t, err) 349 | 350 | requestBody := model.UpdateUserRequest{ 351 | Password: "rahasialagi", 352 | } 353 | 354 | bodyJson, err := json.Marshal(requestBody) 355 | assert.Nil(t, err) 356 | 357 | request := httptest.NewRequest(http.MethodPatch, "/api/users/_current", strings.NewReader(string(bodyJson))) 358 | request.Header.Set("Content-Type", "application/json") 359 | request.Header.Set("Accept", "application/json") 360 | request.Header.Set("Authorization", user.Token) 361 | 362 | response, err := app.Test(request) 363 | assert.Nil(t, err) 364 | 365 | bytes, err := io.ReadAll(response.Body) 366 | assert.Nil(t, err) 367 | 368 | responseBody := new(model.WebResponse[model.UserResponse]) 369 | err = json.Unmarshal(bytes, responseBody) 370 | assert.Nil(t, err) 371 | 372 | assert.Equal(t, http.StatusOK, response.StatusCode) 373 | assert.Equal(t, user.ID, responseBody.Data.ID) 374 | assert.NotNil(t, responseBody.Data.CreatedAt) 375 | assert.NotNil(t, responseBody.Data.UpdatedAt) 376 | 377 | user = new(entity.User) 378 | err = db.Where("id = ?", "khannedy").First(user).Error 379 | assert.Nil(t, err) 380 | 381 | err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(requestBody.Password)) 382 | assert.Nil(t, err) 383 | } 384 | 385 | func TestUpdateFailed(t *testing.T) { 386 | ClearAll() 387 | TestLogin(t) // login success 388 | 389 | requestBody := model.UpdateUserRequest{ 390 | Password: "rahasialagi", 391 | } 392 | 393 | bodyJson, err := json.Marshal(requestBody) 394 | assert.Nil(t, err) 395 | 396 | request := httptest.NewRequest(http.MethodPatch, "/api/users/_current", strings.NewReader(string(bodyJson))) 397 | request.Header.Set("Content-Type", "application/json") 398 | request.Header.Set("Accept", "application/json") 399 | request.Header.Set("Authorization", "wrong") 400 | 401 | response, err := app.Test(request) 402 | assert.Nil(t, err) 403 | 404 | bytes, err := io.ReadAll(response.Body) 405 | assert.Nil(t, err) 406 | 407 | responseBody := new(model.WebResponse[model.UserResponse]) 408 | err = json.Unmarshal(bytes, responseBody) 409 | assert.Nil(t, err) 410 | 411 | assert.Equal(t, http.StatusUnauthorized, response.StatusCode) 412 | assert.NotNil(t, responseBody.Errors) 413 | } 414 | --------------------------------------------------------------------------------