├── .gitignore ├── LICENSE ├── Project_Default.xml ├── README.md ├── Taskfile.yml ├── backend ├── README.md ├── Taskfile.yml ├── Taskfile_windows.yml ├── backend.iml ├── cmd │ └── api │ │ └── main.go ├── configs │ ├── config.go │ └── config_test.go ├── docs │ ├── docs.go │ ├── http-client │ │ ├── README.md │ │ ├── endpoints.http │ │ └── http-client.env.json │ ├── swagger.json │ └── swagger.yaml ├── go.mod ├── go.sum └── internal │ ├── db │ ├── README.md │ ├── account.go │ ├── account_integration_test.go │ ├── account_test.go │ ├── dbmodels │ │ └── account.go │ └── service.go │ ├── handlers │ ├── account.go │ ├── account_test.go │ └── service.go │ ├── mocks │ ├── account_handler.go │ ├── account_repository.go │ └── account_service.go │ └── services │ ├── account.go │ ├── account_test.go │ └── service.go ├── docker ├── backend │ ├── .env │ └── Dockerfile ├── docker-compose.yml └── frontend │ ├── .env │ ├── Dockerfile │ └── nginx.conf ├── frontend ├── README.md ├── Taskfile.yml ├── Taskfile_windows.yml ├── assets │ ├── main.js │ └── ui.png ├── cmd │ └── ui │ │ └── main.go ├── frontend.iml ├── go.mod └── templates │ └── index.html └── scripts └── build.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env 26 | 27 | hack/ 28 | 29 | # Ignore everything in .idea/ by default 30 | .idea/* 31 | *.iml 32 | 33 | # Allow project-specific files 34 | !.idea/modules.xml 35 | !.idea/vcs.xml 36 | !.idea/inspectionProfiles/ 37 | !.idea/runConfigurations/ 38 | 39 | # Exclude user-specific files 40 | .idea/workspace.xml 41 | .idea/misc.xml 42 | .idea/libraries/ 43 | .idea/shelf/ 44 | 45 | # Ignore IntelliJ cache/output 46 | out/ 47 | 48 | backend/bin 49 | frontend/bin -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # demo1_fullstack_golang 2 | 3 | Demo application containing fullstack solution in pure Golang. 4 | 5 | ## Codebase structure 6 | 7 | ```text 8 | /demo1_fullstack_golang 9 | ├── /backend 10 | │ ├── /cmd 11 | │ │ ├── /api 12 | │ │ │ └── main.go # Main entry point for the backend service 13 | │ ├── /internal # Internal packages (business logic, database, etc.) 14 | │ │ ├── /auth 15 | │ │ ├── /db 16 | │ │ └── /services 17 | │ ├── /pkg # Shared reusable libraries 18 | │ ├── /configs # Configuration files 19 | │ ├── /docs # API documentation 20 | │ └── go.mod 21 | │ 22 | ├── /frontend 23 | │ ├── /assets # Static assets (CSS, images, etc.) 24 | │ ├── /cmd 25 | │ │ └── /ui 26 | │ │ └── main.go # Entry point for WebAssembly-based frontend 27 | │ ├── /templates # HTML templates if using SSR 28 | │ └── go.mod 29 | │ 30 | ├── /scripts # Automation scripts (e.g., build, deploy) 31 | │ ├── build.sh 32 | ├── /docker # Docker configuration for backend and frontend 33 | │ ├── /backend 34 | │ │ └── Dockerfile 35 | │ ├── /frontend 36 | │ │ └── Dockerfile 37 | │ └── docker-compose.yml 38 | └── README.md 39 | ``` 40 | 41 | ## Setup 42 | 43 | ### Prerequisites 44 | 45 | - Go SDK 1.23 or higher from [https://golang.org/dl/](https://golang.org/dl/) 46 | - Docker from [https://docs.docker.com/get-docker/](https://docs.docker.com/get-docker/) 47 | - Taskfile from [https://taskfile.dev](https://taskfile.dev) 48 | - docker-compose from [https://docs.docker.com/compose/install/](https://docs.docker.com/compose/install/) 49 | 50 | ### Installation 51 | 52 | 1. Clone the repository: 53 | 54 | ```bash 55 | git clone 56 | ``` 57 | 58 | 1. Install dependencies: 59 | 60 | ```bash 61 | go mod download 62 | ``` 63 | 64 | 1. Start the backend service: 65 | 66 | ```text 67 | go run ./backend/cmd/api 68 | ``` 69 | 70 | The backend service will start on `http://localhost:8080`. 71 | 72 | ### Environment variables 73 | 74 | | Variable | Description | Default Value | 75 | |-----------|---------------------------------|------------------------------------------------------------------------------| 76 | | BASE_URL | Base URL for downstream services | `https://vault.immudb.io/ics/api/v1/ledger/default/collection/default` | 77 | | API_KEY | API key for authentication | `your-api-key` | 78 | | SKIP_TLS | Skip TLS verification (true/false) | `false` | 79 | 80 | ```text 81 | export BASE_URL="https://vault.immudb.io/ics/api/v1/ledger/default/collection/default" 82 | export API_KEY="" 83 | export SKIP_TLS="false" 84 | ``` 85 | 86 | ### Endpoints 87 | 88 | | **Endpoint** | **Method** | **Description** | **Request Body** | **Response** | 89 | |--------------------------|------------|------------------------------------------------|---------------------------------------------------------------------------------------------------------------|---------------------------------------| 90 | | `/healthz` | GET | Health check to verify the service is running | None | `200 OK`: `"Backend is healthy!"` | 91 | | `/swagger/` | GET | Access Swagger documentation | None | Swagger UI | 92 | | `/accounts` | PUT | Create a new account | ```json { "id": "string", "name": "string", "email": "string" } ``` | `200 OK`: Account created | 93 | | `/accounts/retrieve` | POST | Retrieve accounts with pagination | ```json { "page": 1, "perPage": 100 } ``` | `200 OK`: List of accounts | 94 | 95 | 1. Start the frontend service: 96 | 97 | ```text 98 | go run ./frontend/cmd/ui 99 | ``` 100 | 101 | The frontend service will start on `http://localhost:3000`. 102 | 103 | ## Manual E2E Testing 104 | 105 | Please use `IntelliJ IDEA` or any other REST client to test the API endpoints manually. 106 | 107 | Code is available in the [http-client](backend/docs/http-client) directory. 108 | 109 | 110 | ## Development Workflow 111 | 112 | 1. Use the following developer workflows in `Taskfile` 113 | 114 | ```text 115 | task 116 | ``` 117 | 118 | Example output: 119 | 120 | ```text 121 | task: [default] task --list 122 | task: Available tasks for this project: 123 | * clean: Clean up unused Docker resources 124 | * clean-all: Delete all Docker objects including images, containers, volumes, and networks 125 | * default: Show available Docker tasks 126 | * down: Stop all services with Docker Compose 127 | * logs: View logs from Docker Compose services 128 | * rebuild: Rebuild and restart services with Docker Compose 129 | * up: Start all services with Docker Compose 130 | ``` 131 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | # Global variables 4 | vars: 5 | DOCKER_COMPOSE_FILE: "./docker/docker-compose.yml" 6 | 7 | tasks: 8 | default: 9 | desc: "Show available Docker tasks" 10 | cmds: 11 | - task --list 12 | 13 | up: 14 | desc: "Start all services with Docker Compose" 15 | cmds: 16 | - echo "Starting all services..." 17 | - docker-compose -f {{.DOCKER_COMPOSE_FILE}} up -d 18 | 19 | down: 20 | desc: "Stop all services with Docker Compose" 21 | cmds: 22 | - echo "Stopping all services..." 23 | - docker-compose -f {{.DOCKER_COMPOSE_FILE}} down 24 | 25 | rebuild: 26 | desc: "Rebuild and restart services with Docker Compose" 27 | cmds: 28 | - task down 29 | - echo "Rebuilding services..." 30 | - docker-compose -f {{.DOCKER_COMPOSE_FILE}} build 31 | - task up 32 | 33 | logs: 34 | desc: "View logs from Docker Compose services" 35 | cmds: 36 | - docker-compose -f {{.DOCKER_COMPOSE_FILE}} logs -f 37 | 38 | clean: 39 | desc: "Clean up unused Docker resources" 40 | cmds: 41 | - echo "Cleaning up unused Docker resources..." 42 | - docker-compose -f {{.DOCKER_COMPOSE_FILE}} down --volumes --remove-orphans 43 | - docker system prune -f --volumes 44 | 45 | clean-all: 46 | desc: "Delete all Docker objects including images, containers, volumes, and networks" 47 | cmds: 48 | - echo "Stopping all running containers..." 49 | - | 50 | docker ps -q | while read -r container; do 51 | docker stop "$container"; 52 | done 53 | - echo "Removing all containers..." 54 | - | 55 | docker ps -aq | while read -r container; do 56 | docker rm "$container"; 57 | done 58 | - echo "Removing all images..." 59 | - | 60 | docker images -q | while read -r image; do 61 | docker rmi -f "$image"; 62 | done 63 | - echo "Removing all volumes..." 64 | - | 65 | docker volume ls -q | while read -r volume; do 66 | docker volume rm "$volume"; 67 | done 68 | - echo "Removing all networks (except default)..." 69 | - | 70 | docker network ls --filter "type=custom" -q | while read -r network; do 71 | docker network rm "$network"; 72 | done 73 | - echo "Pruning unused Docker objects..." 74 | - docker system prune -af --volumes 75 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | ## Development Workflow 2 | 3 | ### Backend 4 | 5 | 1. Use the following developer workflows in `Taskfile` 6 | 7 | ```text 8 | task 9 | ``` 10 | 11 | Example output: 12 | 13 | ```text 14 | == 15 | Tasks available 4 this infra KUBE. 16 | 17 | task: Available tasks for this project: 18 | * build:compile: Compile the Go binary 19 | * build:default: Show available tasks 20 | * build:mockery:clean: Delete all generated mocks in the 'internal/mocks/' directory 21 | * build:mockery:generate: Generate mocks for all interfaces, placing them in 'internal/mocks' directory 22 | * build:mockery:install: Install mockery tool for generating mocks 23 | * build:run: Run the Go server 24 | * build:swagger:clean: Delete all generated contracts in docs directory. 25 | * build:swagger:init: Generate Swagger documentation 26 | * build:swagger:install: Install Swagger CLI tool 27 | * build:test: Run tests for the project 28 | * default: List all commands defined. 29 | ``` 30 | -------------------------------------------------------------------------------- /backend/Taskfile.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # HINT: (API docs) https://taskfile.dev 3 | # HINT: (Pragmatic use cases) https://tsh.io/blog/taskfile-or-gnu-make-for-automation/ 4 | 5 | version: 3 6 | 7 | includes: 8 | build: ./Taskfile_{{OS}}.yml 9 | 10 | silent: true 11 | 12 | output: 'interleaved' 13 | 14 | tasks: 15 | default: 16 | label: 'default' 17 | desc: 'List all commands defined.' 18 | summary: | 19 | Orchestrates execution of other functions/tasks implemented per OS platform. 20 | 21 | It will provision a component/solution or execute a workflow in an automatic fashion. 22 | cmds: 23 | - 'echo ==' 24 | - 'echo Tasks available 4 this {{.KUBE_TYPE}} KUBE.' 25 | - 'echo' 26 | - 'task -l' 27 | # Hint: signature 28 | vars: 29 | KUBE_TYPE: 'infra' 30 | ... -------------------------------------------------------------------------------- /backend/Taskfile_windows.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | # Global variables 4 | vars: 5 | APP_NAME: "backend_component" 6 | MAIN_FILE: "cmd/api/main.go" 7 | SWAGGER_DOCS_DIR: "docs" 8 | SWAGGER_CMD: "github.com/swaggo/swag/cmd/swag" 9 | 10 | tasks: 11 | # Default task: show help 12 | default: 13 | desc: "Show available tasks" 14 | cmds: 15 | - task --list 16 | 17 | # Run the server 18 | run: 19 | desc: "Run the Go server" 20 | cmds: 21 | - echo "Starting the server..." 22 | - go run {{.MAIN_FILE}} 23 | 24 | # Build the binary 25 | compile: 26 | desc: "Compile the Go binary" 27 | cmds: 28 | - echo "Compiling the binary..." 29 | - go build -o bin/{{.APP_NAME}} {{.MAIN_FILE}} 30 | 31 | # Run tests 32 | test: 33 | desc: "Run tests for the project" 34 | cmds: 35 | - echo "Running tests..." 36 | - go test ./... -cover 37 | 38 | # Install Swagger CLI 39 | swagger:install: 40 | desc: "Install Swagger CLI tool" 41 | cmds: 42 | - go install {{.SWAGGER_CMD}}@latest 43 | silent: true 44 | 45 | # Generate Swagger documentation 46 | swagger:init: 47 | desc: "Generate Swagger documentation" 48 | cmds: 49 | - echo "Generating Swagger documentation..." 50 | - swag init -g {{.MAIN_FILE}} --output {{.SWAGGER_DOCS_DIR}} --parseDependency --parseDepth 3 51 | deps: 52 | - swagger:install 53 | 54 | # Clean up build swagger artifacts 55 | swagger:clean: 56 | desc: "Delete all generated contracts in {{.SWAGGER_DOCS_DIR}} directory." 57 | cmds: 58 | - PowerShell -Command "Remove-Item -Recurse -Force bin; Remove-Item -Recurse -Force {{.SWAGGER_DOCS_DIR}}/*.json; Remove-Item -Recurse -Force {{.SWAGGER_DOCS_DIR}}/*.yaml"; \ 59 | silent: true 60 | 61 | # Install mockery tool 62 | mockery:install: 63 | desc: "Install mockery tool for generating mocks" 64 | cmds: 65 | - go install github.com/vektra/mockery/v2@latest 66 | 67 | # Generate mocks for all interfaces 68 | mockery:generate: 69 | desc: "Generate mocks for all interfaces, placing them in 'internal/mocks' directory" 70 | deps: 71 | - install_mockery 72 | cmds: 73 | - echo "Generating mocks for all interfaces into 'internal/mocks' directory..." 74 | - | 75 | mockery --all --output=internal/mocks --case=underscore --with-expecter 76 | 77 | # Clean up generated mocks 78 | mockery:clean: 79 | desc: "Delete all generated mocks in the 'internal/mocks/' directory" 80 | cmds: 81 | - del /q internal\\mocks\\* 82 | summary: "Clean up all generated mocks in the 'internal/mocks/' directory" 83 | -------------------------------------------------------------------------------- /backend/backend.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /backend/cmd/api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | httpSwagger "github.com/swaggo/http-swagger" 9 | 10 | "github.com/norbix/demo1_fullstack_golang/backend/configs" 11 | "github.com/norbix/demo1_fullstack_golang/backend/internal/db" 12 | "github.com/norbix/demo1_fullstack_golang/backend/internal/handlers" 13 | "github.com/norbix/demo1_fullstack_golang/backend/internal/services" 14 | 15 | _ "github.com/norbix/demo1_fullstack_golang/backend/docs" 16 | 17 | "github.com/gorilla/mux" 18 | ) 19 | 20 | // @title Backend Component API 21 | // @version 1.0 22 | // @description This is a sample server for managing accounts. 23 | // @host localhost:8080 24 | // @BasePath / 25 | func main() { 26 | // Load configuration 27 | config, err := configs.LoadConfig() // Load configuration 28 | if err != nil { 29 | log.Fatalf("Error loading configuration: %s", err) 30 | } 31 | 32 | // Initialize dependencies 33 | accountRepo := db.NewAccountRepo(config, nil) // Repository layer 34 | accountService := services.NewAccountService(accountRepo) // Service layer 35 | accountHandler := handlers.NewAccountHandler(accountService) // HTTP handlers 36 | 37 | // Create a router 38 | router := mux.NewRouter() 39 | 40 | // Register endpoints 41 | router.HandleFunc("/healthz", healthHandler).Methods("GET", "OPTIONS") 42 | router.PathPrefix("/swagger/").Handler(httpSwagger.WrapHandler) 43 | router.HandleFunc("/accounts", accountHandler.CreateAccount).Methods("PUT", "OPTIONS") 44 | router.HandleFunc("/accounts/retrieve", accountHandler.GetAccounts).Methods("POST", "OPTIONS") 45 | 46 | // Add CORS middleware 47 | router.Use(corsMiddleware) 48 | 49 | // Start the server 50 | fmt.Println("Starting Backend Component on port 8080...") 51 | if err := http.ListenAndServe(":8080", router); err != nil { 52 | log.Fatalf("Error starting server: %s", err) 53 | } 54 | } 55 | 56 | // @Summary Health check 57 | // @Description Responds with "Backend is healthy!" if the service is up. 58 | // @Tags health 59 | // @Accept json 60 | // @Produce json 61 | // @Success 200 {string} string "Backend is healthy!" 62 | // @Router /healthz [get] 63 | func healthHandler(w http.ResponseWriter, r *http.Request) { 64 | // Respond with HTTP 200 OK 65 | w.WriteHeader(http.StatusOK) 66 | 67 | // Write a simple health status message 68 | _, _ = w.Write([]byte("Backend is healthy!")) 69 | } 70 | 71 | func enableCORS(w http.ResponseWriter) { 72 | w.Header().Set("Access-Control-Allow-Origin", "*") 73 | w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") 74 | w.Header().Set("Access-Control-Allow-Headers", "Content-Type") 75 | } 76 | 77 | // Hack: Should be in a separate package 78 | func corsMiddleware(next http.Handler) http.Handler { 79 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 80 | log.Printf("Applying CORS middleware for %s %s\n", r.Method, r.URL.Path) 81 | enableCORS(w) // Add CORS headers 82 | if r.Method == http.MethodOptions { 83 | log.Println("Responding to preflight request") 84 | w.WriteHeader(http.StatusOK) 85 | return 86 | } 87 | next.ServeHTTP(w, r) 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /backend/configs/config.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "strconv" 7 | ) 8 | 9 | type Config struct { 10 | BaseURL string 11 | APIKey string 12 | SkipTLS bool 13 | } 14 | 15 | func LoadConfig() (*Config, error) { 16 | baseURL := os.Getenv("BASE_URL") 17 | apiKey := os.Getenv("API_KEY") 18 | skipTLS := os.Getenv("SKIP_TLS") 19 | 20 | if baseURL == "" || apiKey == "" { 21 | return nil, errors.New("missing required environment variables") 22 | } 23 | 24 | // Convert skipTLS to a boolean 25 | skipTLSBool, err := strconv.ParseBool(skipTLS) 26 | if err != nil && skipTLS != "" { 27 | return nil, errors.New("invalid value for SKIP_TLS, must be true or false") 28 | } 29 | 30 | return &Config{ 31 | BaseURL: baseURL, 32 | APIKey: apiKey, 33 | SkipTLS: skipTLSBool, 34 | }, nil 35 | } 36 | -------------------------------------------------------------------------------- /backend/configs/config_test.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | // ConfigTestSuite defines the test suite for the Config package 12 | type ConfigTestSuite struct { 13 | suite.Suite 14 | originalBaseURL string 15 | originalAPIKey string 16 | originalSkipTLS string 17 | } 18 | 19 | // SetupSuite runs once before the tests are run 20 | func (suite *ConfigTestSuite) SetupSuite() { 21 | // Store the original values of the environment variables 22 | suite.originalBaseURL = os.Getenv("BASE_URL") 23 | suite.originalAPIKey = os.Getenv("API_KEY") 24 | } 25 | 26 | // TearDownSuite runs once after all tests in the suite are run 27 | func (suite *ConfigTestSuite) TearDownTest() { 28 | // Restore the original values of the environment variables 29 | err := os.Setenv("BASE_URL", suite.originalBaseURL) 30 | if err != nil { 31 | suite.Fail("Unable to restore original BASE_URL") 32 | } 33 | 34 | err = os.Setenv("API_KEY", suite.originalAPIKey) 35 | if err != nil { 36 | suite.Fail("Unable to restore original API_KEY") 37 | } 38 | } 39 | 40 | // SetupTest runs before each test in the suite 41 | func (suite *ConfigTestSuite) SetupTest() { 42 | // Clear the environment variables before each test 43 | err := os.Unsetenv("BASE_URL") 44 | if err != nil { 45 | suite.Fail("Unable to unset BASE_URL") 46 | } 47 | 48 | err = os.Unsetenv("API_KEY") 49 | if err != nil { 50 | suite.Fail("Unable to unset API_KEY") 51 | } 52 | } 53 | 54 | // TestLoadConfig_OK tests the LoadConfig function when the environment variables are properly set 55 | func (suite *ConfigTestSuite) TestLoadConfig_OK() { 56 | // Given: Environment variables are properly set 57 | err := os.Setenv("BASE_URL", "http://example.com") 58 | if err != nil { 59 | suite.Fail("Unable to set BASE_URL") 60 | } 61 | 62 | err = os.Setenv("API_KEY", "test-api-key") 63 | if err != nil { 64 | suite.Fail("Unable to set API_KEY") 65 | } 66 | 67 | // When: LoadConfig is called 68 | config, err := LoadConfig() 69 | 70 | // Then: It should return the expected Config struct with no error 71 | assert.NoError(suite.T(), err) 72 | assert.NotNil(suite.T(), config) 73 | assert.Equal(suite.T(), "http://example.com", config.BaseURL) 74 | assert.Equal(suite.T(), "test-api-key", config.APIKey) 75 | } 76 | 77 | // TestLoadConfig_MissingAPIKey tests the LoadConfig function when the API_KEY environment variable is missing 78 | func (suite *ConfigTestSuite) TestLoadConfig_MissingBaseURL() { 79 | // Given: BASE_URL is not set 80 | err := os.Setenv("API_KEY", "test-api-key") 81 | if err != nil { 82 | suite.Fail("Unable to set API_KEY") 83 | } 84 | 85 | // When: LoadConfig is called 86 | config, err := LoadConfig() 87 | 88 | // Then: It should return an error 89 | assert.Error(suite.T(), err) 90 | assert.Nil(suite.T(), config) 91 | assert.Equal(suite.T(), "missing required environment variables", err.Error()) 92 | } 93 | 94 | // TestLoadConfig_MissingAPIKey tests LoadConfig function when the API_KEY environment variable is missing 95 | func (suite *ConfigTestSuite) TestLoadConfig_MissingAPIKey() { 96 | // Given: API_KEY is not set 97 | err := os.Setenv("BASE_URL", "http://example.com") 98 | if err != nil { 99 | suite.Fail("Unable to set BASE_URL") 100 | } 101 | 102 | // When: LoadConfig is called 103 | config, err := LoadConfig() 104 | 105 | // Then: It should return an error 106 | assert.Error(suite.T(), err) 107 | assert.Nil(suite.T(), config) 108 | assert.Equal(suite.T(), "missing required environment variables", err.Error()) 109 | } 110 | 111 | // TestLoadConfig_NoEnvVariables tests LoadConfig function when no environment variables are set 112 | func (suite *ConfigTestSuite) TestLoadConfig_NoEnvVariables() { 113 | // Given: No environment variables are set 114 | 115 | // When: LoadConfig is called 116 | config, err := LoadConfig() 117 | 118 | // Then: It should return an error 119 | assert.Error(suite.T(), err) 120 | assert.Nil(suite.T(), config) 121 | assert.Equal(suite.T(), "missing required environment variables", err.Error()) 122 | } 123 | 124 | // Test Suite Runner 125 | func TestConfigTestSuite(t *testing.T) { 126 | suite.Run(t, new(ConfigTestSuite)) 127 | } 128 | -------------------------------------------------------------------------------- /backend/docs/docs.go: -------------------------------------------------------------------------------- 1 | // Package docs Code generated by swaggo/swag. DO NOT EDIT 2 | package docs 3 | 4 | import "github.com/swaggo/swag" 5 | 6 | const docTemplate = `{ 7 | "schemes": {{ marshal .Schemes }}, 8 | "swagger": "2.0", 9 | "info": { 10 | "description": "{{escape .Description}}", 11 | "title": "{{.Title}}", 12 | "contact": {}, 13 | "version": "{{.Version}}" 14 | }, 15 | "host": "{{.Host}}", 16 | "basePath": "{{.BasePath}}", 17 | "paths": { 18 | "/accounts": { 19 | "put": { 20 | "description": "Creates a new account with the provided details.", 21 | "consumes": [ 22 | "application/json" 23 | ], 24 | "produces": [ 25 | "application/json" 26 | ], 27 | "tags": [ 28 | "accounts" 29 | ], 30 | "summary": "Create an account", 31 | "parameters": [ 32 | { 33 | "description": "Account data", 34 | "name": "account", 35 | "in": "body", 36 | "required": true, 37 | "schema": { 38 | "$ref": "#/definitions/github_com_norbix_demo1_fullstack_golang_backend_internal_db_dbmodels.Account" 39 | } 40 | } 41 | ], 42 | "responses": { 43 | "201": { 44 | "description": "Created", 45 | "schema": { 46 | "type": "string" 47 | } 48 | }, 49 | "400": { 50 | "description": "Invalid request body", 51 | "schema": { 52 | "type": "string" 53 | } 54 | }, 55 | "500": { 56 | "description": "Internal server error", 57 | "schema": { 58 | "type": "string" 59 | } 60 | } 61 | } 62 | } 63 | }, 64 | "/accounts/retrieve": { 65 | "post": { 66 | "description": "Retrieves accounts with pagination.", 67 | "consumes": [ 68 | "application/json" 69 | ], 70 | "produces": [ 71 | "application/json" 72 | ], 73 | "tags": [ 74 | "accounts" 75 | ], 76 | "summary": "Retrieve accounts", 77 | "parameters": [ 78 | { 79 | "description": "Pagination details", 80 | "name": "pagination", 81 | "in": "body", 82 | "required": true, 83 | "schema": { 84 | "type": "object", 85 | "additionalProperties": { 86 | "type": "integer" 87 | } 88 | } 89 | } 90 | ], 91 | "responses": { 92 | "200": { 93 | "description": "OK", 94 | "schema": { 95 | "type": "object", 96 | "additionalProperties": true 97 | } 98 | }, 99 | "400": { 100 | "description": "Invalid request body", 101 | "schema": { 102 | "type": "string" 103 | } 104 | }, 105 | "500": { 106 | "description": "Internal server error", 107 | "schema": { 108 | "type": "string" 109 | } 110 | } 111 | } 112 | } 113 | }, 114 | "/healthz": { 115 | "get": { 116 | "description": "Responds with \"Backend is healthy!\" if the service is up.", 117 | "consumes": [ 118 | "application/json" 119 | ], 120 | "produces": [ 121 | "application/json" 122 | ], 123 | "tags": [ 124 | "health" 125 | ], 126 | "summary": "Health check", 127 | "responses": { 128 | "200": { 129 | "description": "Backend is healthy!", 130 | "schema": { 131 | "type": "string" 132 | } 133 | } 134 | } 135 | } 136 | } 137 | }, 138 | "definitions": { 139 | "github_com_norbix_demo1_fullstack_golang_backend_internal_db_dbmodels.Account": { 140 | "type": "object", 141 | "properties": { 142 | "account_name": { 143 | "type": "string" 144 | }, 145 | "account_number": { 146 | "type": "string" 147 | }, 148 | "address": { 149 | "type": "string" 150 | }, 151 | "amount": { 152 | "type": "number" 153 | }, 154 | "iban": { 155 | "type": "string" 156 | }, 157 | "type": { 158 | "$ref": "#/definitions/github_com_norbix_demo1_fullstack_golang_backend_internal_db_dbmodels.AccountType" 159 | } 160 | } 161 | }, 162 | "github_com_norbix_demo1_fullstack_golang_backend_internal_db_dbmodels.AccountType": { 163 | "type": "string", 164 | "enum": [ 165 | "sending", 166 | "receiving" 167 | ], 168 | "x-enum-varnames": [ 169 | "Sending", 170 | "Receiving" 171 | ] 172 | } 173 | } 174 | }` 175 | 176 | // SwaggerInfo holds exported Swagger Info so clients can modify it 177 | var SwaggerInfo = &swag.Spec{ 178 | Version: "1.0", 179 | Host: "localhost:8080", 180 | BasePath: "/", 181 | Schemes: []string{}, 182 | Title: "Backend Component API", 183 | Description: "This is a sample server for managing accounts.", 184 | InfoInstanceName: "swagger", 185 | SwaggerTemplate: docTemplate, 186 | LeftDelim: "{{", 187 | RightDelim: "}}", 188 | } 189 | 190 | func init() { 191 | swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) 192 | } 193 | -------------------------------------------------------------------------------- /backend/docs/http-client/README.md: -------------------------------------------------------------------------------- 1 | # Info 2 | 3 | In order to use the IDEA HTTP Client for manual E2E testing, please reinitialize the `PAT` token in file [http-client.env.json](http-client.env.json). 4 | -------------------------------------------------------------------------------- /backend/docs/http-client/endpoints.http: -------------------------------------------------------------------------------- 1 | ### PUT Account to Local API 2 | PUT {{base_url}}/accounts 3 | Content-Type: application/json 4 | 5 | { 6 | "name": "John Doe", 7 | "id": 1, 8 | "account_number": "123456789", 9 | "timestamp": "2023-05-10T12:00:00Z", 10 | "email": "johndoe@example.com", 11 | "age": 30, 12 | "address": "123 Main Street", 13 | "city": "New York", 14 | "country": "USA", 15 | "phone": "+1-123-456-7890", 16 | "is_active": true 17 | } 18 | 19 | ### PUT Document to Vault 20 | PUT {{vault_url}}/ics/api/v1/ledger/default/collection/default/document 21 | Accept: application/json 22 | Content-Type: application/json 23 | X-API-Key: {{api_key}} 24 | 25 | { 26 | "name": "John Doe", 27 | "id": 1, 28 | "timestamp": "2023-05-10T12:00:00Z", 29 | "email": "johndoe@example.com", 30 | "age": 30, 31 | "address": "123 Main Street", 32 | "city": "New York", 33 | "country": "USA", 34 | "phone": "+1-123-456-7890", 35 | "is_active": true 36 | } 37 | 38 | ### Retrieve Accounts to Local API 39 | POST {{base_url}}/accounts/retrieve 40 | Accept: application/json 41 | Content-Type: application/json 42 | 43 | { 44 | "page": 1, 45 | "perPage": 100 46 | } 47 | 48 | ### Search Documents in Vault 49 | POST {{vault_url}}/ics/api/v1/ledger/default/collection/default/documents/search 50 | Accept: application/json 51 | Content-Type: application/json 52 | X-API-Key: {{api_key}} 53 | 54 | { 55 | "page": 1, 56 | "perPage": 100 57 | } 58 | -------------------------------------------------------------------------------- /backend/docs/http-client/http-client.env.json: -------------------------------------------------------------------------------- 1 | { 2 | "local": { 3 | "base_url": "http://localhost:8080", 4 | "vault_url": "https://vault.immudb.io", 5 | "api_key": "default.jKXkTQquKyXAEfz1qHei1A.gTmSG38ipa8QNz4jPVLUJuw6etoejMTkqZ9fxwvovQ9xNBV_" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /backend/docs/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "description": "This is a sample server for managing accounts.", 5 | "title": "Backend Component API", 6 | "contact": {}, 7 | "version": "1.0" 8 | }, 9 | "host": "localhost:8080", 10 | "basePath": "/", 11 | "paths": { 12 | "/accounts": { 13 | "put": { 14 | "description": "Creates a new account with the provided details.", 15 | "consumes": [ 16 | "application/json" 17 | ], 18 | "produces": [ 19 | "application/json" 20 | ], 21 | "tags": [ 22 | "accounts" 23 | ], 24 | "summary": "Create an account", 25 | "parameters": [ 26 | { 27 | "description": "Account data", 28 | "name": "account", 29 | "in": "body", 30 | "required": true, 31 | "schema": { 32 | "$ref": "#/definitions/github_com_norbix_demo1_fullstack_golang_backend_internal_db_dbmodels.Account" 33 | } 34 | } 35 | ], 36 | "responses": { 37 | "201": { 38 | "description": "Created", 39 | "schema": { 40 | "type": "string" 41 | } 42 | }, 43 | "400": { 44 | "description": "Invalid request body", 45 | "schema": { 46 | "type": "string" 47 | } 48 | }, 49 | "500": { 50 | "description": "Internal server error", 51 | "schema": { 52 | "type": "string" 53 | } 54 | } 55 | } 56 | } 57 | }, 58 | "/accounts/retrieve": { 59 | "post": { 60 | "description": "Retrieves accounts with pagination.", 61 | "consumes": [ 62 | "application/json" 63 | ], 64 | "produces": [ 65 | "application/json" 66 | ], 67 | "tags": [ 68 | "accounts" 69 | ], 70 | "summary": "Retrieve accounts", 71 | "parameters": [ 72 | { 73 | "description": "Pagination details", 74 | "name": "pagination", 75 | "in": "body", 76 | "required": true, 77 | "schema": { 78 | "type": "object", 79 | "additionalProperties": { 80 | "type": "integer" 81 | } 82 | } 83 | } 84 | ], 85 | "responses": { 86 | "200": { 87 | "description": "OK", 88 | "schema": { 89 | "type": "object", 90 | "additionalProperties": true 91 | } 92 | }, 93 | "400": { 94 | "description": "Invalid request body", 95 | "schema": { 96 | "type": "string" 97 | } 98 | }, 99 | "500": { 100 | "description": "Internal server error", 101 | "schema": { 102 | "type": "string" 103 | } 104 | } 105 | } 106 | } 107 | }, 108 | "/healthz": { 109 | "get": { 110 | "description": "Responds with \"Backend is healthy!\" if the service is up.", 111 | "consumes": [ 112 | "application/json" 113 | ], 114 | "produces": [ 115 | "application/json" 116 | ], 117 | "tags": [ 118 | "health" 119 | ], 120 | "summary": "Health check", 121 | "responses": { 122 | "200": { 123 | "description": "Backend is healthy!", 124 | "schema": { 125 | "type": "string" 126 | } 127 | } 128 | } 129 | } 130 | } 131 | }, 132 | "definitions": { 133 | "github_com_norbix_demo1_fullstack_golang_backend_internal_db_dbmodels.Account": { 134 | "type": "object", 135 | "properties": { 136 | "account_name": { 137 | "type": "string" 138 | }, 139 | "account_number": { 140 | "type": "string" 141 | }, 142 | "address": { 143 | "type": "string" 144 | }, 145 | "amount": { 146 | "type": "number" 147 | }, 148 | "iban": { 149 | "type": "string" 150 | }, 151 | "type": { 152 | "$ref": "#/definitions/github_com_norbix_demo1_fullstack_golang_backend_internal_db_dbmodels.AccountType" 153 | } 154 | } 155 | }, 156 | "github_com_norbix_demo1_fullstack_golang_backend_internal_db_dbmodels.AccountType": { 157 | "type": "string", 158 | "enum": [ 159 | "sending", 160 | "receiving" 161 | ], 162 | "x-enum-varnames": [ 163 | "Sending", 164 | "Receiving" 165 | ] 166 | } 167 | } 168 | } -------------------------------------------------------------------------------- /backend/docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | basePath: / 2 | definitions: 3 | github_com_norbix_demo1_fullstack_golang_backend_internal_db_dbmodels.Account: 4 | properties: 5 | account_name: 6 | type: string 7 | account_number: 8 | type: string 9 | address: 10 | type: string 11 | amount: 12 | type: number 13 | iban: 14 | type: string 15 | type: 16 | $ref: '#/definitions/github_com_norbix_demo1_fullstack_golang_backend_internal_db_dbmodels.AccountType' 17 | type: object 18 | github_com_norbix_demo1_fullstack_golang_backend_internal_db_dbmodels.AccountType: 19 | enum: 20 | - sending 21 | - receiving 22 | type: string 23 | x-enum-varnames: 24 | - Sending 25 | - Receiving 26 | host: localhost:8080 27 | info: 28 | contact: {} 29 | description: This is a sample server for managing accounts. 30 | title: Backend Component API 31 | version: "1.0" 32 | paths: 33 | /accounts: 34 | put: 35 | consumes: 36 | - application/json 37 | description: Creates a new account with the provided details. 38 | parameters: 39 | - description: Account data 40 | in: body 41 | name: account 42 | required: true 43 | schema: 44 | $ref: '#/definitions/github_com_norbix_demo1_fullstack_golang_backend_internal_db_dbmodels.Account' 45 | produces: 46 | - application/json 47 | responses: 48 | "201": 49 | description: Created 50 | schema: 51 | type: string 52 | "400": 53 | description: Invalid request body 54 | schema: 55 | type: string 56 | "500": 57 | description: Internal server error 58 | schema: 59 | type: string 60 | summary: Create an account 61 | tags: 62 | - accounts 63 | /accounts/retrieve: 64 | post: 65 | consumes: 66 | - application/json 67 | description: Retrieves accounts with pagination. 68 | parameters: 69 | - description: Pagination details 70 | in: body 71 | name: pagination 72 | required: true 73 | schema: 74 | additionalProperties: 75 | type: integer 76 | type: object 77 | produces: 78 | - application/json 79 | responses: 80 | "200": 81 | description: OK 82 | schema: 83 | additionalProperties: true 84 | type: object 85 | "400": 86 | description: Invalid request body 87 | schema: 88 | type: string 89 | "500": 90 | description: Internal server error 91 | schema: 92 | type: string 93 | summary: Retrieve accounts 94 | tags: 95 | - accounts 96 | /healthz: 97 | get: 98 | consumes: 99 | - application/json 100 | description: Responds with "Backend is healthy!" if the service is up. 101 | produces: 102 | - application/json 103 | responses: 104 | "200": 105 | description: Backend is healthy! 106 | schema: 107 | type: string 108 | summary: Health check 109 | tags: 110 | - health 111 | swagger: "2.0" 112 | -------------------------------------------------------------------------------- /backend/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/norbix/demo1_fullstack_golang/backend 2 | 3 | go 1.23.3 4 | 5 | require ( 6 | github.com/gorilla/mux v1.8.1 7 | github.com/stretchr/testify v1.10.0 8 | github.com/swaggo/http-swagger v1.3.4 9 | github.com/swaggo/swag v1.16.4 10 | ) 11 | 12 | require ( 13 | github.com/KyleBanks/depth v1.2.1 // indirect 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 16 | github.com/go-openapi/jsonreference v0.21.0 // indirect 17 | github.com/go-openapi/spec v0.21.0 // indirect 18 | github.com/go-openapi/swag v0.23.0 // indirect 19 | github.com/josharian/intern v1.0.0 // indirect 20 | github.com/mailru/easyjson v0.7.7 // indirect 21 | github.com/pmezard/go-difflib v1.0.0 // indirect 22 | github.com/stretchr/objx v0.5.2 // indirect 23 | github.com/swaggo/files v1.0.1 // indirect 24 | golang.org/x/net v0.31.0 // indirect 25 | golang.org/x/tools v0.27.0 // indirect 26 | gopkg.in/yaml.v3 v3.0.1 // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /backend/go.sum: -------------------------------------------------------------------------------- 1 | github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= 2 | github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 6 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 7 | github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= 8 | github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= 9 | github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= 10 | github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= 11 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 12 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 13 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 14 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 15 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 16 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 17 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 18 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 19 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 20 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 21 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 22 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 23 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 24 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 25 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 26 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 27 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 28 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 29 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 30 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 31 | github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= 32 | github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= 33 | github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww= 34 | github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ= 35 | github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= 36 | github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= 37 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 38 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 39 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 40 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 41 | golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= 42 | golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 43 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 44 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 45 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 46 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 47 | golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= 48 | golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= 49 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 50 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 51 | golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= 52 | golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 53 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 54 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 55 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 56 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 57 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 58 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 59 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 60 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 61 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 62 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 63 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 64 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 65 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 66 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 67 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 68 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 69 | golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= 70 | golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= 71 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 72 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 73 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 74 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 75 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 76 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 77 | -------------------------------------------------------------------------------- /backend/internal/db/README.md: -------------------------------------------------------------------------------- 1 | # Info 2 | 3 | Package directory structure: 4 | 5 | ```text 6 | internal/db/ 7 | ├── account.go # Implements the AccountService interface 8 | ├── account_test.go # Unit tests for account.go 9 | ├── dbmodels/ # Holds shared data models 10 | │ └── account.go # Defines the Account struct 11 | ├── service.go # Defines the interface and a struct for database services 12 | ├── service_test.go # Unit tests for service.go 13 | └── suite_test.go # Test suite setup 14 | ``` 15 | -------------------------------------------------------------------------------- /backend/internal/db/account.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net/http" 10 | 11 | "github.com/norbix/demo1_fullstack_golang/backend/internal/db/dbmodels" 12 | ) 13 | 14 | // CreateAccount sends account data to the immudb Vault for storage. 15 | func (repo accountRepositoryImpl) CreateAccount(account dbmodels.Account) (map[string]interface{}, error) { 16 | url := fmt.Sprintf("%s/document", repo.config.BaseURL) 17 | 18 | // Serialize account data 19 | payload, err := json.Marshal(account) 20 | if err != nil { 21 | log.Printf("[ERROR] Failed to serialize account data: %v", err) 22 | return nil, fmt.Errorf("failed to serialize account data: %w", err) 23 | } 24 | 25 | log.Printf("[INFO] Sending request to immudb Vault. URL: %s, Payload: %s", url, string(payload)) 26 | 27 | // Create the HTTP request 28 | req, err := http.NewRequest("PUT", url, bytes.NewBuffer(payload)) 29 | if err != nil { 30 | log.Printf("[ERROR] Failed to create HTTP request: %v", err) 31 | return nil, fmt.Errorf("failed to create request: %w", err) 32 | } 33 | req.Header.Set("Content-Type", "application/json") 34 | req.Header.Set("accept", "application/json") 35 | req.Header.Set("X-API-Key", repo.config.APIKey) 36 | 37 | // Send the request using the injected HTTP client 38 | resp, err := repo.client.Do(req) 39 | if err != nil { 40 | log.Printf("[ERROR] Failed to send request to immudb Vault: %v", err) 41 | return nil, fmt.Errorf("failed to send request: %w", err) 42 | } 43 | defer resp.Body.Close() 44 | 45 | log.Printf("[INFO] Response Status Code: %d", resp.StatusCode) 46 | 47 | // Read the response body 48 | body, err := io.ReadAll(resp.Body) 49 | if err != nil { 50 | log.Printf("[ERROR] Failed to read response body: %v", err) 51 | return nil, fmt.Errorf("failed to read response body: %w", err) 52 | } 53 | 54 | log.Printf("[INFO] Raw response from immudb Vault: %s", string(body)) 55 | 56 | // Check for non-OK status code 57 | if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK { 58 | log.Printf("[ERROR] Unexpected status code: %d, Response: %s", resp.StatusCode, string(body)) 59 | return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) 60 | } 61 | 62 | // Parse the response body into a map 63 | var response map[string]interface{} 64 | if err := json.Unmarshal(body, &response); err != nil { 65 | log.Printf("[ERROR] Failed to decode response body: %v", err) 66 | return nil, fmt.Errorf("failed to decode response body: %w", err) 67 | } 68 | 69 | log.Printf("[INFO] Account successfully created in immudb Vault. Response: %v", response) 70 | 71 | return response, nil 72 | } 73 | 74 | // GetAccounts retrieves a list of accounts from the immudb Vault. 75 | func (repo accountRepositoryImpl) GetAccounts(page, perPage int) (map[string]interface{}, error) { 76 | url := fmt.Sprintf("%s/documents/search", repo.config.BaseURL) 77 | query := map[string]interface{}{ 78 | "page": page, 79 | "perPage": perPage, 80 | } 81 | 82 | payload, err := json.Marshal(query) 83 | if err != nil { 84 | log.Printf("[ERROR] Failed to serialize query: %v", err) 85 | return nil, fmt.Errorf("failed to serialize query: %w", err) 86 | } 87 | 88 | log.Printf("[INFO] Sending request to immudb Vault. URL: %s, Payload: %s", url, string(payload)) 89 | 90 | req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload)) 91 | if err != nil { 92 | log.Printf("[ERROR] Failed to create request: %v", err) 93 | return nil, fmt.Errorf("failed to create request: %w", err) 94 | } 95 | req.Header.Set("Content-Type", "application/json") 96 | req.Header.Set("accept", "application/json") 97 | req.Header.Set("X-API-Key", repo.config.APIKey) 98 | 99 | resp, err := repo.client.Do(req) 100 | if err != nil { 101 | log.Printf("[ERROR] Failed to send request to immudb Vault: %v", err) 102 | return nil, fmt.Errorf("failed to send request: %w", err) 103 | } 104 | defer resp.Body.Close() 105 | 106 | log.Printf("[INFO] Response Status Code: %d", resp.StatusCode) 107 | 108 | body, err := io.ReadAll(resp.Body) 109 | if err != nil { 110 | log.Printf("[ERROR] Failed to read response body: %v", err) 111 | return nil, fmt.Errorf("failed to read response body: %w", err) 112 | } 113 | 114 | log.Printf("[INFO] Raw response from immudb Vault: %s", string(body)) 115 | 116 | var response map[string]interface{} 117 | if err := json.Unmarshal(body, &response); err != nil { 118 | log.Printf("[ERROR] Failed to decode response: %v", err) 119 | return nil, fmt.Errorf("failed to decode response: %w", err) 120 | } 121 | 122 | return response, nil 123 | } 124 | -------------------------------------------------------------------------------- /backend/internal/db/account_integration_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/norbix/demo1_fullstack_golang/backend/configs" 10 | "github.com/norbix/demo1_fullstack_golang/backend/internal/db/dbmodels" 11 | ) 12 | 13 | func TestAccountRepository_CreateAccount_Integration(t *testing.T) { 14 | config := &configs.Config{ 15 | BaseURL: "https://vault.immudb.io/ics/api/v1/ledger/default/collection/default", 16 | APIKey: "default.jKXkTQquKyXAEfz1qHei1A.gTmSG38ipa8QNz4jPVLUJuw6etoejMTkqZ9fxwvovQ9xNBV_", 17 | SkipTLS: true, 18 | } 19 | client := &http.Client{} 20 | repo := NewAccountRepo(config, client) 21 | 22 | t.Run("Valid Account", func(t *testing.T) { 23 | account := dbmodels.Account{ 24 | AccountNumber: "12345", 25 | Amount: 100.0, 26 | } 27 | 28 | // Call the actual implementation 29 | response, err := repo.CreateAccount(account) 30 | 31 | // Assert the results 32 | assert.NotNil(t, response) 33 | assert.NoError(t, err) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /backend/internal/db/account_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/norbix/demo1_fullstack_golang/backend/internal/db/dbmodels" 10 | "github.com/norbix/demo1_fullstack_golang/backend/internal/mocks" 11 | ) 12 | 13 | func TestAccountRepository_CreateAccount(t *testing.T) { 14 | mockRepo := new(mocks.AccountRepository) 15 | 16 | t.Run("Valid Account", func(t *testing.T) { 17 | account := dbmodels.Account{ 18 | AccountNumber: "12345", 19 | Amount: 100.0, 20 | } 21 | 22 | expectedResponse := map[string]interface{}{ 23 | "documentId": "abc123", 24 | "transactionId": "txn456", 25 | } 26 | 27 | mockRepo.EXPECT(). 28 | CreateAccount(account). 29 | Return(expectedResponse, nil) 30 | 31 | response, err := mockRepo.CreateAccount(account) 32 | 33 | assert.NoError(t, err) 34 | assert.Equal(t, expectedResponse, response) 35 | 36 | mockRepo.AssertCalled(t, "CreateAccount", account) 37 | }) 38 | 39 | t.Run("Serialization Error", func(t *testing.T) { 40 | account := dbmodels.Account{ 41 | AccountNumber: "invalid-\xe9", // Invalid UTF-8 sequence 42 | } 43 | 44 | mockRepo.EXPECT(). 45 | CreateAccount(account). 46 | Return(nil, errors.New("serialization error")) 47 | 48 | response, err := mockRepo.CreateAccount(account) 49 | 50 | assert.Nil(t, response) 51 | assert.EqualError(t, err, "serialization error") 52 | }) 53 | } 54 | 55 | func TestAccountRepository_GetAccounts(t *testing.T) { 56 | mockRepo := new(mocks.AccountRepository) 57 | 58 | t.Run("Valid Pagination", func(t *testing.T) { 59 | page := 1 60 | perPage := 10 61 | 62 | expectedResponse := map[string]interface{}{ 63 | "accounts": []map[string]interface{}{ 64 | {"AccountNumber": "12345", "Amount": 100.0}, 65 | {"AccountNumber": "67890", "Amount": 200.0}, 66 | }, 67 | "total": 2, 68 | } 69 | 70 | // Configure mock for Valid Pagination 71 | mockRepo.EXPECT(). 72 | GetAccounts(page, perPage). 73 | Return(expectedResponse, nil) 74 | 75 | response, err := mockRepo.GetAccounts(page, perPage) 76 | 77 | assert.NoError(t, err) 78 | assert.Equal(t, expectedResponse, response) 79 | 80 | mockRepo.AssertCalled(t, "GetAccounts", page, perPage) 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /backend/internal/db/dbmodels/account.go: -------------------------------------------------------------------------------- 1 | package dbmodels 2 | 3 | const ( 4 | Sending AccountType = "sending" 5 | Receiving AccountType = "receiving" 6 | ) 7 | 8 | type AccountType string 9 | 10 | type Account struct { 11 | AccountNumber string `json:"account_number"` 12 | AccountName string `json:"account_name"` 13 | IBAN string `json:"iban"` 14 | Address string `json:"address"` 15 | Amount float64 `json:"amount"` 16 | Type AccountType `json:"type"` 17 | } 18 | -------------------------------------------------------------------------------- /backend/internal/db/service.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "crypto/tls" 5 | "net/http" 6 | 7 | "github.com/norbix/demo1_fullstack_golang/backend/configs" 8 | "github.com/norbix/demo1_fullstack_golang/backend/internal/db/dbmodels" 9 | ) 10 | 11 | type AccountRepository interface { 12 | CreateAccount(dbmodels.Account) (map[string]interface{}, error) 13 | GetAccounts(int, int) (map[string]interface{}, error) 14 | } 15 | 16 | type accountRepositoryImpl struct { 17 | config *configs.Config 18 | client *http.Client 19 | } 20 | 21 | func NewAccountRepo(config *configs.Config, client *http.Client) AccountRepository { 22 | if config.SkipTLS { 23 | client = &http.Client{ 24 | Transport: &http.Transport{ 25 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 26 | }, 27 | } 28 | } else { 29 | client = http.DefaultClient 30 | } 31 | 32 | return accountRepositoryImpl{ 33 | config: config, 34 | client: client, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/internal/handlers/account.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/norbix/demo1_fullstack_golang/backend/internal/db/dbmodels" 8 | ) 9 | 10 | // @Summary Create an account 11 | // @Description Creates a new account with the provided details. 12 | // @Tags accounts 13 | // @Accept json 14 | // @Produce json 15 | // @Param account body dbmodels.Account true "Account data" 16 | // @Success 201 {string} string "Created" 17 | // @Failure 400 {string} string "Invalid request body" 18 | // @Failure 500 {string} string "Internal server error" 19 | // @Router /accounts [put] 20 | func (h accountHandlerImpl) CreateAccount(w http.ResponseWriter, r *http.Request) { 21 | var account dbmodels.Account 22 | // Parse the request body into the account struct 23 | if err := json.NewDecoder(r.Body).Decode(&account); err != nil { 24 | http.Error(w, "Invalid request body", http.StatusBadRequest) 25 | return 26 | } 27 | 28 | // Validate the input 29 | if account.AccountNumber == "" { 30 | http.Error(w, "Account number is required", http.StatusBadRequest) 31 | return 32 | } 33 | if account.Amount < 0 { 34 | http.Error(w, "Amount cannot be negative", http.StatusBadRequest) 35 | return 36 | } 37 | 38 | // Call the service layer to create the account 39 | response, err := h.accountService.CreateAccount(account) 40 | if err != nil { 41 | http.Error(w, err.Error(), http.StatusInternalServerError) 42 | return 43 | } 44 | 45 | // Return the response 46 | w.Header().Set("Content-Type", "application/json") 47 | w.WriteHeader(http.StatusCreated) 48 | err = json.NewEncoder(w).Encode(response) 49 | if err != nil { 50 | http.Error(w, "Internal server error", http.StatusInternalServerError) 51 | return 52 | } 53 | } 54 | 55 | // @Summary Retrieve accounts 56 | // @Description Retrieves accounts with pagination. 57 | // @Tags accounts 58 | // @Accept json 59 | // @Produce json 60 | // @Param pagination body map[string]int true "Pagination details" 61 | // @Success 200 {object} map[string]interface{} 62 | // @Failure 400 {string} string "Invalid request body" 63 | // @Failure 500 {string} string "Internal server error" 64 | // @Router /accounts/retrieve [post] 65 | func (h accountHandlerImpl) GetAccounts(w http.ResponseWriter, r *http.Request) { 66 | var pagination struct { 67 | Page int `json:"page"` 68 | PerPage int `json:"perPage"` 69 | } 70 | 71 | if err := json.NewDecoder(r.Body).Decode(&pagination); err != nil { 72 | http.Error(w, "Invalid request body", http.StatusBadRequest) 73 | return 74 | } 75 | 76 | response, err := h.accountService.GetAccounts(pagination.Page, pagination.PerPage) 77 | if err != nil { 78 | http.Error(w, err.Error(), http.StatusInternalServerError) 79 | return 80 | } 81 | 82 | w.Header().Set("Content-Type", "application/json") 83 | w.WriteHeader(http.StatusOK) 84 | err = json.NewEncoder(w).Encode(response) 85 | if err != nil { 86 | http.Error(w, "Internal server error", http.StatusInternalServerError) 87 | return 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /backend/internal/handlers/account_test.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | 12 | "github.com/norbix/demo1_fullstack_golang/backend/internal/db/dbmodels" 13 | "github.com/norbix/demo1_fullstack_golang/backend/internal/mocks" 14 | ) 15 | 16 | func TestAccountHandler_CreateAccount(t *testing.T) { 17 | mockService := new(mocks.AccountService) 18 | handler := NewAccountHandler(mockService) 19 | 20 | t.Run("Valid Account", func(t *testing.T) { 21 | account := dbmodels.Account{ 22 | AccountNumber: "12345", 23 | Amount: 100.0, 24 | } 25 | 26 | expectedResponse := map[string]interface{}{ 27 | "documentId": "674b8ac7000000000000000a53d8e43d", 28 | "transactionId": "11", 29 | } 30 | 31 | mockService.EXPECT().CreateAccount(account).Return(expectedResponse, nil) 32 | 33 | body, _ := json.Marshal(account) 34 | req := httptest.NewRequest(http.MethodPut, "/accounts", bytes.NewBuffer(body)) 35 | req.Header.Set("Content-Type", "application/json") 36 | rr := httptest.NewRecorder() 37 | 38 | handler.CreateAccount(rr, req) 39 | 40 | assert.Equal(t, http.StatusCreated, rr.Code) // Expecting 201 Created 41 | var response map[string]interface{} 42 | json.NewDecoder(rr.Body).Decode(&response) 43 | assert.Equal(t, expectedResponse, response) 44 | }) 45 | 46 | t.Run("Invalid Account Number", func(t *testing.T) { 47 | account := dbmodels.Account{ 48 | AccountNumber: "", 49 | Amount: 100.0, 50 | } 51 | 52 | body, _ := json.Marshal(account) 53 | req := httptest.NewRequest(http.MethodPut, "/accounts", bytes.NewBuffer(body)) 54 | req.Header.Set("Content-Type", "application/json") 55 | rr := httptest.NewRecorder() 56 | 57 | handler.CreateAccount(rr, req) 58 | 59 | assert.Equal(t, http.StatusBadRequest, rr.Code) 60 | mockService.AssertNotCalled(t, "CreateAccount") 61 | }) 62 | 63 | t.Run("Negative Amount", func(t *testing.T) { 64 | account := dbmodels.Account{ 65 | AccountNumber: "12345", 66 | Amount: -10.0, 67 | } 68 | 69 | body, _ := json.Marshal(account) 70 | req := httptest.NewRequest(http.MethodPut, "/accounts", bytes.NewBuffer(body)) 71 | req.Header.Set("Content-Type", "application/json") 72 | rr := httptest.NewRecorder() 73 | 74 | handler.CreateAccount(rr, req) 75 | 76 | assert.Equal(t, http.StatusBadRequest, rr.Code) 77 | mockService.AssertNotCalled(t, "CreateAccount") 78 | }) 79 | } 80 | func TestAccountHandler_GetAccounts(t *testing.T) { 81 | mockService := new(mocks.AccountService) 82 | handler := NewAccountHandler(mockService) 83 | 84 | t.Run("Valid Pagination", func(t *testing.T) { 85 | pagination := map[string]int{ 86 | "page": 1, 87 | "perPage": 10, 88 | } 89 | 90 | expectedResponse := map[string]interface{}{ 91 | "accounts": []interface{}{ 92 | map[string]interface{}{"AccountNumber": "12345", "Amount": 100.0}, 93 | map[string]interface{}{"AccountNumber": "67890", "Amount": 200.0}, 94 | }, 95 | "total": float64(2), 96 | } 97 | 98 | mockService.EXPECT().GetAccounts(1, 10).Return(expectedResponse, nil) 99 | 100 | body, _ := json.Marshal(pagination) 101 | req := httptest.NewRequest(http.MethodPost, "/accounts/retrieve", bytes.NewBuffer(body)) 102 | req.Header.Set("Content-Type", "application/json") 103 | rr := httptest.NewRecorder() 104 | 105 | handler.GetAccounts(rr, req) 106 | 107 | assert.Equal(t, http.StatusOK, rr.Code) 108 | var response map[string]interface{} 109 | json.NewDecoder(rr.Body).Decode(&response) 110 | assert.Equal(t, expectedResponse, response) 111 | }) 112 | 113 | t.Run("Invalid Request Body", func(t *testing.T) { 114 | body := []byte("invalid-json") 115 | 116 | req := httptest.NewRequest(http.MethodPost, "/accounts/retrieve", bytes.NewBuffer(body)) 117 | req.Header.Set("Content-Type", "application/json") 118 | rr := httptest.NewRecorder() 119 | 120 | handler.GetAccounts(rr, req) 121 | 122 | assert.Equal(t, http.StatusBadRequest, rr.Code) 123 | assert.Equal(t, "Invalid request body\n", rr.Body.String()) 124 | }) 125 | } 126 | -------------------------------------------------------------------------------- /backend/internal/handlers/service.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/norbix/demo1_fullstack_golang/backend/internal/services" 7 | ) 8 | 9 | type AccountHandler interface { 10 | CreateAccount(http.ResponseWriter, *http.Request) 11 | GetAccounts(http.ResponseWriter, *http.Request) 12 | } 13 | 14 | type accountHandlerImpl struct { 15 | accountService services.AccountService 16 | } 17 | 18 | func NewAccountHandler(accountService services.AccountService) AccountHandler { 19 | return accountHandlerImpl{accountService: accountService} 20 | } 21 | -------------------------------------------------------------------------------- /backend/internal/mocks/account_handler.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.49.1. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | http "net/http" 7 | 8 | mock "github.com/stretchr/testify/mock" 9 | ) 10 | 11 | // AccountHandler is an autogenerated mock type for the AccountHandler type 12 | type AccountHandler struct { 13 | mock.Mock 14 | } 15 | 16 | type AccountHandler_Expecter struct { 17 | mock *mock.Mock 18 | } 19 | 20 | func (_m *AccountHandler) EXPECT() *AccountHandler_Expecter { 21 | return &AccountHandler_Expecter{mock: &_m.Mock} 22 | } 23 | 24 | // CreateAccount provides a mock function with given fields: _a0, _a1 25 | func (_m *AccountHandler) CreateAccount(_a0 http.ResponseWriter, _a1 *http.Request) { 26 | _m.Called(_a0, _a1) 27 | } 28 | 29 | // AccountHandler_CreateAccount_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateAccount' 30 | type AccountHandler_CreateAccount_Call struct { 31 | *mock.Call 32 | } 33 | 34 | // CreateAccount is a helper method to define mock.On call 35 | // - _a0 http.ResponseWriter 36 | // - _a1 *http.Request 37 | func (_e *AccountHandler_Expecter) CreateAccount(_a0 interface{}, _a1 interface{}) *AccountHandler_CreateAccount_Call { 38 | return &AccountHandler_CreateAccount_Call{Call: _e.mock.On("CreateAccount", _a0, _a1)} 39 | } 40 | 41 | func (_c *AccountHandler_CreateAccount_Call) Run(run func(_a0 http.ResponseWriter, _a1 *http.Request)) *AccountHandler_CreateAccount_Call { 42 | _c.Call.Run(func(args mock.Arguments) { 43 | run(args[0].(http.ResponseWriter), args[1].(*http.Request)) 44 | }) 45 | return _c 46 | } 47 | 48 | func (_c *AccountHandler_CreateAccount_Call) Return() *AccountHandler_CreateAccount_Call { 49 | _c.Call.Return() 50 | return _c 51 | } 52 | 53 | func (_c *AccountHandler_CreateAccount_Call) RunAndReturn(run func(http.ResponseWriter, *http.Request)) *AccountHandler_CreateAccount_Call { 54 | _c.Call.Return(run) 55 | return _c 56 | } 57 | 58 | // GetAccounts provides a mock function with given fields: _a0, _a1 59 | func (_m *AccountHandler) GetAccounts(_a0 http.ResponseWriter, _a1 *http.Request) { 60 | _m.Called(_a0, _a1) 61 | } 62 | 63 | // AccountHandler_GetAccounts_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAccounts' 64 | type AccountHandler_GetAccounts_Call struct { 65 | *mock.Call 66 | } 67 | 68 | // GetAccounts is a helper method to define mock.On call 69 | // - _a0 http.ResponseWriter 70 | // - _a1 *http.Request 71 | func (_e *AccountHandler_Expecter) GetAccounts(_a0 interface{}, _a1 interface{}) *AccountHandler_GetAccounts_Call { 72 | return &AccountHandler_GetAccounts_Call{Call: _e.mock.On("GetAccounts", _a0, _a1)} 73 | } 74 | 75 | func (_c *AccountHandler_GetAccounts_Call) Run(run func(_a0 http.ResponseWriter, _a1 *http.Request)) *AccountHandler_GetAccounts_Call { 76 | _c.Call.Run(func(args mock.Arguments) { 77 | run(args[0].(http.ResponseWriter), args[1].(*http.Request)) 78 | }) 79 | return _c 80 | } 81 | 82 | func (_c *AccountHandler_GetAccounts_Call) Return() *AccountHandler_GetAccounts_Call { 83 | _c.Call.Return() 84 | return _c 85 | } 86 | 87 | func (_c *AccountHandler_GetAccounts_Call) RunAndReturn(run func(http.ResponseWriter, *http.Request)) *AccountHandler_GetAccounts_Call { 88 | _c.Call.Return(run) 89 | return _c 90 | } 91 | 92 | // NewAccountHandler creates a new instance of AccountHandler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 93 | // The first argument is typically a *testing.T value. 94 | func NewAccountHandler(t interface { 95 | mock.TestingT 96 | Cleanup(func()) 97 | }) *AccountHandler { 98 | mock := &AccountHandler{} 99 | mock.Mock.Test(t) 100 | 101 | t.Cleanup(func() { mock.AssertExpectations(t) }) 102 | 103 | return mock 104 | } 105 | -------------------------------------------------------------------------------- /backend/internal/mocks/account_repository.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.49.1. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | dbmodels "github.com/norbix/demo1_fullstack_golang/backend/internal/db/dbmodels" 7 | mock "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | // AccountRepository is an autogenerated mock type for the AccountRepository type 11 | type AccountRepository struct { 12 | mock.Mock 13 | } 14 | 15 | type AccountRepository_Expecter struct { 16 | mock *mock.Mock 17 | } 18 | 19 | func (_m *AccountRepository) EXPECT() *AccountRepository_Expecter { 20 | return &AccountRepository_Expecter{mock: &_m.Mock} 21 | } 22 | 23 | // CreateAccount provides a mock function with given fields: _a0 24 | func (_m *AccountRepository) CreateAccount(_a0 dbmodels.Account) (map[string]interface{}, error) { 25 | ret := _m.Called(_a0) 26 | 27 | if len(ret) == 0 { 28 | panic("no return value specified for CreateAccount") 29 | } 30 | 31 | var r0 map[string]interface{} 32 | var r1 error 33 | if rf, ok := ret.Get(0).(func(dbmodels.Account) (map[string]interface{}, error)); ok { 34 | return rf(_a0) 35 | } 36 | if rf, ok := ret.Get(0).(func(dbmodels.Account) map[string]interface{}); ok { 37 | r0 = rf(_a0) 38 | } else { 39 | if ret.Get(0) != nil { 40 | r0 = ret.Get(0).(map[string]interface{}) 41 | } 42 | } 43 | 44 | if rf, ok := ret.Get(1).(func(dbmodels.Account) error); ok { 45 | r1 = rf(_a0) 46 | } else { 47 | r1 = ret.Error(1) 48 | } 49 | 50 | return r0, r1 51 | } 52 | 53 | // AccountRepository_CreateAccount_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateAccount' 54 | type AccountRepository_CreateAccount_Call struct { 55 | *mock.Call 56 | } 57 | 58 | // CreateAccount is a helper method to define mock.On call 59 | // - _a0 dbmodels.Account 60 | func (_e *AccountRepository_Expecter) CreateAccount(_a0 interface{}) *AccountRepository_CreateAccount_Call { 61 | return &AccountRepository_CreateAccount_Call{Call: _e.mock.On("CreateAccount", _a0)} 62 | } 63 | 64 | func (_c *AccountRepository_CreateAccount_Call) Run(run func(_a0 dbmodels.Account)) *AccountRepository_CreateAccount_Call { 65 | _c.Call.Run(func(args mock.Arguments) { 66 | run(args[0].(dbmodels.Account)) 67 | }) 68 | return _c 69 | } 70 | 71 | func (_c *AccountRepository_CreateAccount_Call) Return(_a0 map[string]interface{}, _a1 error) *AccountRepository_CreateAccount_Call { 72 | _c.Call.Return(_a0, _a1) 73 | return _c 74 | } 75 | 76 | func (_c *AccountRepository_CreateAccount_Call) RunAndReturn(run func(dbmodels.Account) (map[string]interface{}, error)) *AccountRepository_CreateAccount_Call { 77 | _c.Call.Return(run) 78 | return _c 79 | } 80 | 81 | // GetAccounts provides a mock function with given fields: _a0, _a1 82 | func (_m *AccountRepository) GetAccounts(_a0 int, _a1 int) (map[string]interface{}, error) { 83 | ret := _m.Called(_a0, _a1) 84 | 85 | if len(ret) == 0 { 86 | panic("no return value specified for GetAccounts") 87 | } 88 | 89 | var r0 map[string]interface{} 90 | var r1 error 91 | if rf, ok := ret.Get(0).(func(int, int) (map[string]interface{}, error)); ok { 92 | return rf(_a0, _a1) 93 | } 94 | if rf, ok := ret.Get(0).(func(int, int) map[string]interface{}); ok { 95 | r0 = rf(_a0, _a1) 96 | } else { 97 | if ret.Get(0) != nil { 98 | r0 = ret.Get(0).(map[string]interface{}) 99 | } 100 | } 101 | 102 | if rf, ok := ret.Get(1).(func(int, int) error); ok { 103 | r1 = rf(_a0, _a1) 104 | } else { 105 | r1 = ret.Error(1) 106 | } 107 | 108 | return r0, r1 109 | } 110 | 111 | // AccountRepository_GetAccounts_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAccounts' 112 | type AccountRepository_GetAccounts_Call struct { 113 | *mock.Call 114 | } 115 | 116 | // GetAccounts is a helper method to define mock.On call 117 | // - _a0 int 118 | // - _a1 int 119 | func (_e *AccountRepository_Expecter) GetAccounts(_a0 interface{}, _a1 interface{}) *AccountRepository_GetAccounts_Call { 120 | return &AccountRepository_GetAccounts_Call{Call: _e.mock.On("GetAccounts", _a0, _a1)} 121 | } 122 | 123 | func (_c *AccountRepository_GetAccounts_Call) Run(run func(_a0 int, _a1 int)) *AccountRepository_GetAccounts_Call { 124 | _c.Call.Run(func(args mock.Arguments) { 125 | run(args[0].(int), args[1].(int)) 126 | }) 127 | return _c 128 | } 129 | 130 | func (_c *AccountRepository_GetAccounts_Call) Return(_a0 map[string]interface{}, _a1 error) *AccountRepository_GetAccounts_Call { 131 | _c.Call.Return(_a0, _a1) 132 | return _c 133 | } 134 | 135 | func (_c *AccountRepository_GetAccounts_Call) RunAndReturn(run func(int, int) (map[string]interface{}, error)) *AccountRepository_GetAccounts_Call { 136 | _c.Call.Return(run) 137 | return _c 138 | } 139 | 140 | // NewAccountRepository creates a new instance of AccountRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 141 | // The first argument is typically a *testing.T value. 142 | func NewAccountRepository(t interface { 143 | mock.TestingT 144 | Cleanup(func()) 145 | }) *AccountRepository { 146 | mock := &AccountRepository{} 147 | mock.Mock.Test(t) 148 | 149 | t.Cleanup(func() { mock.AssertExpectations(t) }) 150 | 151 | return mock 152 | } 153 | -------------------------------------------------------------------------------- /backend/internal/mocks/account_service.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.49.1. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | dbmodels "github.com/norbix/demo1_fullstack_golang/backend/internal/db/dbmodels" 7 | mock "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | // AccountService is an autogenerated mock type for the AccountService type 11 | type AccountService struct { 12 | mock.Mock 13 | } 14 | 15 | type AccountService_Expecter struct { 16 | mock *mock.Mock 17 | } 18 | 19 | func (_m *AccountService) EXPECT() *AccountService_Expecter { 20 | return &AccountService_Expecter{mock: &_m.Mock} 21 | } 22 | 23 | // CreateAccount provides a mock function with given fields: _a0 24 | func (_m *AccountService) CreateAccount(_a0 dbmodels.Account) (map[string]interface{}, error) { 25 | ret := _m.Called(_a0) 26 | 27 | if len(ret) == 0 { 28 | panic("no return value specified for CreateAccount") 29 | } 30 | 31 | var r0 map[string]interface{} 32 | var r1 error 33 | if rf, ok := ret.Get(0).(func(dbmodels.Account) (map[string]interface{}, error)); ok { 34 | return rf(_a0) 35 | } 36 | if rf, ok := ret.Get(0).(func(dbmodels.Account) map[string]interface{}); ok { 37 | r0 = rf(_a0) 38 | } else { 39 | if ret.Get(0) != nil { 40 | r0 = ret.Get(0).(map[string]interface{}) 41 | } 42 | } 43 | 44 | if rf, ok := ret.Get(1).(func(dbmodels.Account) error); ok { 45 | r1 = rf(_a0) 46 | } else { 47 | r1 = ret.Error(1) 48 | } 49 | 50 | return r0, r1 51 | } 52 | 53 | // AccountService_CreateAccount_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateAccount' 54 | type AccountService_CreateAccount_Call struct { 55 | *mock.Call 56 | } 57 | 58 | // CreateAccount is a helper method to define mock.On call 59 | // - _a0 dbmodels.Account 60 | func (_e *AccountService_Expecter) CreateAccount(_a0 interface{}) *AccountService_CreateAccount_Call { 61 | return &AccountService_CreateAccount_Call{Call: _e.mock.On("CreateAccount", _a0)} 62 | } 63 | 64 | func (_c *AccountService_CreateAccount_Call) Run(run func(_a0 dbmodels.Account)) *AccountService_CreateAccount_Call { 65 | _c.Call.Run(func(args mock.Arguments) { 66 | run(args[0].(dbmodels.Account)) 67 | }) 68 | return _c 69 | } 70 | 71 | func (_c *AccountService_CreateAccount_Call) Return(_a0 map[string]interface{}, _a1 error) *AccountService_CreateAccount_Call { 72 | _c.Call.Return(_a0, _a1) 73 | return _c 74 | } 75 | 76 | func (_c *AccountService_CreateAccount_Call) RunAndReturn(run func(dbmodels.Account) (map[string]interface{}, error)) *AccountService_CreateAccount_Call { 77 | _c.Call.Return(run) 78 | return _c 79 | } 80 | 81 | // GetAccounts provides a mock function with given fields: _a0, _a1 82 | func (_m *AccountService) GetAccounts(_a0 int, _a1 int) (map[string]interface{}, error) { 83 | ret := _m.Called(_a0, _a1) 84 | 85 | if len(ret) == 0 { 86 | panic("no return value specified for GetAccounts") 87 | } 88 | 89 | var r0 map[string]interface{} 90 | var r1 error 91 | if rf, ok := ret.Get(0).(func(int, int) (map[string]interface{}, error)); ok { 92 | return rf(_a0, _a1) 93 | } 94 | if rf, ok := ret.Get(0).(func(int, int) map[string]interface{}); ok { 95 | r0 = rf(_a0, _a1) 96 | } else { 97 | if ret.Get(0) != nil { 98 | r0 = ret.Get(0).(map[string]interface{}) 99 | } 100 | } 101 | 102 | if rf, ok := ret.Get(1).(func(int, int) error); ok { 103 | r1 = rf(_a0, _a1) 104 | } else { 105 | r1 = ret.Error(1) 106 | } 107 | 108 | return r0, r1 109 | } 110 | 111 | // AccountService_GetAccounts_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAccounts' 112 | type AccountService_GetAccounts_Call struct { 113 | *mock.Call 114 | } 115 | 116 | // GetAccounts is a helper method to define mock.On call 117 | // - _a0 int 118 | // - _a1 int 119 | func (_e *AccountService_Expecter) GetAccounts(_a0 interface{}, _a1 interface{}) *AccountService_GetAccounts_Call { 120 | return &AccountService_GetAccounts_Call{Call: _e.mock.On("GetAccounts", _a0, _a1)} 121 | } 122 | 123 | func (_c *AccountService_GetAccounts_Call) Run(run func(_a0 int, _a1 int)) *AccountService_GetAccounts_Call { 124 | _c.Call.Run(func(args mock.Arguments) { 125 | run(args[0].(int), args[1].(int)) 126 | }) 127 | return _c 128 | } 129 | 130 | func (_c *AccountService_GetAccounts_Call) Return(_a0 map[string]interface{}, _a1 error) *AccountService_GetAccounts_Call { 131 | _c.Call.Return(_a0, _a1) 132 | return _c 133 | } 134 | 135 | func (_c *AccountService_GetAccounts_Call) RunAndReturn(run func(int, int) (map[string]interface{}, error)) *AccountService_GetAccounts_Call { 136 | _c.Call.Return(run) 137 | return _c 138 | } 139 | 140 | // NewAccountService creates a new instance of AccountService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 141 | // The first argument is typically a *testing.T value. 142 | func NewAccountService(t interface { 143 | mock.TestingT 144 | Cleanup(func()) 145 | }) *AccountService { 146 | mock := &AccountService{} 147 | mock.Mock.Test(t) 148 | 149 | t.Cleanup(func() { mock.AssertExpectations(t) }) 150 | 151 | return mock 152 | } 153 | -------------------------------------------------------------------------------- /backend/internal/services/account.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/norbix/demo1_fullstack_golang/backend/internal/db/dbmodels" 7 | ) 8 | 9 | func (s accountServiceImpl) CreateAccount(account dbmodels.Account) (map[string]interface{}, error) { 10 | // Business rule: Ensure account number is not empty 11 | if account.AccountNumber == "" { 12 | return nil, errors.New("account number is required") 13 | } 14 | 15 | // Business rule: Ensure amount is non-negative 16 | if account.Amount < 0 { 17 | return nil, errors.New("amount cannot be negative") 18 | } 19 | 20 | // Delegate persistence to the repository 21 | response, err := s.repo.CreateAccount(account) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | return response, nil 27 | } 28 | 29 | // GetAccounts retrieves a list of accounts. 30 | func (s accountServiceImpl) GetAccounts(page, perPage int) (map[string]interface{}, error) { 31 | response, err := s.repo.GetAccounts(page, perPage) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | return response, nil 37 | } 38 | -------------------------------------------------------------------------------- /backend/internal/services/account_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/norbix/demo1_fullstack_golang/backend/internal/db/dbmodels" 9 | "github.com/norbix/demo1_fullstack_golang/backend/internal/mocks" 10 | ) 11 | 12 | func TestAccountService_CreateAccount(t *testing.T) { 13 | mockRepo := new(mocks.AccountRepository) 14 | service := NewAccountService(mockRepo) 15 | 16 | t.Run("Valid Account", func(t *testing.T) { 17 | account := dbmodels.Account{ 18 | AccountNumber: "12345", 19 | Amount: 100.0, 20 | } 21 | 22 | expectedResponse := map[string]interface{}{ 23 | "documentId": "abc123", 24 | "transactionId": "txn456", 25 | } 26 | 27 | // Mocking repository behavior for valid input 28 | mockRepo.On("CreateAccount", account).Return(expectedResponse, nil) 29 | 30 | response, err := service.CreateAccount(account) 31 | 32 | assert.NoError(t, err) 33 | assert.Equal(t, expectedResponse, response) 34 | 35 | mockRepo.AssertCalled(t, "CreateAccount", account) 36 | }) 37 | } 38 | 39 | func TestAccountService_GetAccounts(t *testing.T) { 40 | mockRepo := new(mocks.AccountRepository) 41 | service := NewAccountService(mockRepo) 42 | 43 | t.Run("Valid Request", func(t *testing.T) { 44 | page := 1 45 | perPage := 10 46 | 47 | expectedResponse := map[string]interface{}{ 48 | "accounts": []interface{}{ 49 | map[string]interface{}{"AccountNumber": "12345", "Amount": 100.0}, 50 | map[string]interface{}{"AccountNumber": "67890", "Amount": 200.0}, 51 | }, 52 | "total": float64(2), 53 | } 54 | 55 | mockRepo.On("GetAccounts", page, perPage).Return(expectedResponse, nil) 56 | 57 | response, err := service.GetAccounts(page, perPage) 58 | 59 | assert.NoError(t, err) 60 | assert.Equal(t, expectedResponse, response) 61 | 62 | mockRepo.AssertCalled(t, "GetAccounts", page, perPage) 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /backend/internal/services/service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/norbix/demo1_fullstack_golang/backend/internal/db" 5 | "github.com/norbix/demo1_fullstack_golang/backend/internal/db/dbmodels" 6 | ) 7 | 8 | // AccountService defines the interface for account-related operations. 9 | type AccountService interface { 10 | CreateAccount(dbmodels.Account) (map[string]interface{}, error) 11 | GetAccounts(int, int) (map[string]interface{}, error) 12 | } 13 | 14 | type accountServiceImpl struct { 15 | repo db.AccountRepository 16 | } 17 | 18 | func NewAccountService(repo db.AccountRepository) AccountService { 19 | return accountServiceImpl{repo: repo} 20 | } 21 | -------------------------------------------------------------------------------- /docker/backend/.env: -------------------------------------------------------------------------------- 1 | BASE_URL=https://vault.immudb.io/ics/api/v1/ledger/default/collection/default 2 | API_KEY=default.jKXkTQquKyXAEfz1qHei1A.gTmSG38ipa8QNz4jPVLUJuw6etoejMTkqZ9fxwvovQ9xNBV_ 3 | SKIP_TLS=true 4 | -------------------------------------------------------------------------------- /docker/backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1: Build 2 | FROM golang:1.23 AS builder 3 | 4 | # Set the working directory inside the container 5 | WORKDIR /app 6 | 7 | # Copy only the go.mod and go.sum files first to leverage Docker's layer caching 8 | COPY ./backend/go.mod ./ 9 | RUN go mod download 10 | 11 | # Copy the entire backend source code 12 | COPY ./backend/ ./ 13 | 14 | # Enable CGO and build the Go application 15 | ENV CGO_ENABLED=1 GOOS=linux GOARCH=amd64 16 | # Build the Go application 17 | RUN go build -o backend ./cmd/api 18 | 19 | # Stage 2: Run 20 | # Non-distroless image with the required GLIBC 21 | FROM debian:bookworm-slim 22 | 23 | # Set the working directory inside the container 24 | WORKDIR /app 25 | 26 | # Copy the compiled binary from the builder stage 27 | COPY --from=builder /app/backend . 28 | 29 | # Expose the application port 30 | EXPOSE 8080 31 | 32 | # Use the distroless base entrypoint 33 | ENTRYPOINT ["/app/backend"] 34 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | backend: 5 | build: 6 | context: .. 7 | dockerfile: docker/backend/Dockerfile 8 | container_name: backend-service 9 | ports: 10 | - "8080:8080" 11 | env_file: 12 | - backend/.env 13 | healthcheck: 14 | test: ["CMD", "curl", "-f", "http://localhost:8080/healthz"] 15 | interval: 30s 16 | timeout: 10s 17 | retries: 3 18 | networks: 19 | - app-network 20 | 21 | frontend: 22 | build: 23 | context: .. 24 | dockerfile: docker/frontend/Dockerfile 25 | container_name: frontend-service 26 | ports: 27 | - "3000:3000" 28 | env_file: 29 | - frontend/.env 30 | depends_on: 31 | - backend 32 | healthcheck: 33 | test: ["CMD", "curl", "-f", "http://localhost:3000"] 34 | interval: 30s 35 | timeout: 10s 36 | retries: 3 37 | networks: 38 | - app-network 39 | 40 | networks: 41 | app-network: 42 | driver: bridge 43 | 44 | -------------------------------------------------------------------------------- /docker/frontend/.env: -------------------------------------------------------------------------------- 1 | API_BASE_URL=http://backend-service:8080 2 | -------------------------------------------------------------------------------- /docker/frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1: Build 2 | FROM golang:1.23.3 AS builder 3 | 4 | # Set the working directory 5 | WORKDIR /app 6 | 7 | # Copy the build script and make it executable 8 | COPY scripts/build.sh /app/scripts/build.sh 9 | RUN chmod +x /app/scripts/build.sh 10 | 11 | # Copy the Go module files to leverage Docker caching 12 | COPY ./frontend/go.mod /app/frontend/ 13 | WORKDIR /app/frontend 14 | RUN go mod download 15 | 16 | # Copy the source code 17 | COPY frontend/ /app/frontend 18 | 19 | # Build the WebAssembly frontend using the build script 20 | RUN /app/scripts/build.sh frontend 21 | 22 | # Stage 2: Run 23 | FROM nginx:alpine 24 | 25 | # Set the working directory 26 | WORKDIR /usr/share/nginx/html 27 | 28 | # Copy static assets and the Wasm binary from the builder stage 29 | COPY --from=builder /app/frontend/build/ /usr/share/nginx/html 30 | 31 | # Copy the nginx configuration if needed (optional) 32 | COPY docker/frontend/nginx.conf /etc/nginx/nginx.conf 33 | 34 | # Expose the frontend port 35 | EXPOSE 3000 36 | 37 | # Start the nginx server 38 | ENTRYPOINT ["nginx", "-g", "daemon off;"] 39 | -------------------------------------------------------------------------------- /docker/frontend/nginx.conf: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 1024; 3 | } 4 | 5 | http { 6 | include mime.types; 7 | default_type application/octet-stream; 8 | 9 | server { 10 | listen 3000; 11 | server_name localhost; 12 | 13 | root /usr/share/nginx/html; 14 | index index.html; 15 | 16 | location / { 17 | try_files $uri /index.html; 18 | } 19 | 20 | # Serve .wasm files with the correct MIME type 21 | location ~ \.wasm$ { 22 | add_header Content-Type application/wasm; 23 | try_files $uri =404; 24 | } 25 | 26 | error_page 404 /index.html; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | ## Development Workflow 2 | 3 | ## Frontend 4 | 5 | 1. Use the following developer workflows in `Taskfile` 6 | 7 | ```text 8 | task 9 | ``` 10 | 11 | Example output: 12 | 13 | ```text 14 | == 15 | Tasks available 4 this infra KUBE. 16 | 17 | task: Available tasks for this project: 18 | * build:clean: Clean up generated frontend files on Windows 19 | * build:compile: Build the frontend WebAssembly binary on Windows 20 | * default: List all commands defined. 21 | ``` 22 | 23 | ## UI 24 | 25 | ![UI](assets/ui.png) -------------------------------------------------------------------------------- /frontend/Taskfile.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # HINT: (API docs) https://taskfile.dev 3 | # HINT: (Pragmatic use cases) https://tsh.io/blog/taskfile-or-gnu-make-for-automation/ 4 | 5 | version: 3 6 | 7 | includes: 8 | build: ./Taskfile_{{OS}}.yml 9 | 10 | silent: true 11 | 12 | output: 'interleaved' 13 | 14 | tasks: 15 | default: 16 | label: 'default' 17 | desc: 'List all commands defined.' 18 | summary: | 19 | Orchestrates execution of other functions/tasks implemented per OS platform. 20 | 21 | It will provision a component/solution or execute a workflow in an automatic fashion. 22 | cmds: 23 | - 'echo ==' 24 | - 'echo Tasks available 4 this {{.KUBE_TYPE}} KUBE.' 25 | - 'echo' 26 | - 'task -l' 27 | # Hint: signature 28 | vars: 29 | KUBE_TYPE: 'infra' 30 | ... -------------------------------------------------------------------------------- /frontend/Taskfile_windows.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | # Global variables 4 | vars: 5 | MAIN_FILE: "cmd/ui/main.go" 6 | BUILD_DIR: "./bin" 7 | ASSETS_DIR: "./assets" 8 | 9 | tasks: 10 | compile: 11 | desc: "Build the frontend WebAssembly binary on Windows" 12 | cmds: 13 | - echo "Building frontend WebAssembly (Windows)..." 14 | - mkdir "{{.BUILD_DIR}}" 2>nul || echo "Directory already exists" 15 | - GOOS=js GOARCH=wasm go build -o "{{.BUILD_DIR}}/main.wasm" "{{.MAIN_FILE}}" 16 | - echo "Copying assets from {{.ASSETS_DIR}} to {{.BUILD_DIR}}..." 17 | # Hack 18 | - xcopy ".\assets" ".\bin" /e /i /y 19 | - echo "Frontend build complete! Files located in {{.BUILD_DIR}}" 20 | 21 | clean: 22 | desc: "Clean up generated frontend files on Windows" 23 | cmds: 24 | - echo "Cleaning up frontend build directory (Windows)..." 25 | # TODO: debug this command 26 | - rmdir /s /q "{{.BUILD_DIR}}" 2>nul || echo "No directory to remove" 27 | -------------------------------------------------------------------------------- /frontend/assets/main.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | const go = new Go(); // Provided by wasm_exec.js 3 | const response = await fetch("main.wasm"); 4 | const wasmBytes = await response.arrayBuffer(); 5 | const wasm = await WebAssembly.instantiate(wasmBytes, go.importObject); 6 | 7 | go.run(wasm.instance); 8 | 9 | // Add your event handlers 10 | function createAccount() { 11 | const name = document.getElementById("account_name").value; 12 | const number = document.getElementById("account_number").value; 13 | const address = document.getElementById("address").value; 14 | const amount = parseFloat(document.getElementById("amount").value); 15 | const iban = document.getElementById("iban").value; 16 | const type = document.getElementById("type").value; 17 | window.createAccount(name, number, address, amount, iban, type); 18 | } 19 | 20 | function retrieveAccounts() { 21 | const page = parseInt(document.getElementById("page").value); 22 | const perPage = parseInt(document.getElementById("size").value); // Use "perPage" 23 | window.retrieveAccounts(page, perPage); 24 | } 25 | 26 | function healthCheck() { 27 | console.log("Health Check button clicked"); 28 | window.healthCheck(); 29 | } 30 | 31 | document.querySelector("#create-account-btn").onclick = createAccount; 32 | document.querySelector("#retrieve-accounts-btn").onclick = retrieveAccounts; 33 | document.querySelector("#health-check-btn").onclick = healthCheck; 34 | })(); 35 | -------------------------------------------------------------------------------- /frontend/assets/ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/norbix/demo1_fullstack_golang/90ef243ea7558e710852e75ae5bac074539fed80/frontend/assets/ui.png -------------------------------------------------------------------------------- /frontend/cmd/ui/main.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | // +build js,wasm 3 | 4 | package main 5 | 6 | import ( 7 | "encoding/json" 8 | "fmt" 9 | "syscall/js" 10 | ) 11 | 12 | func main() { 13 | fmt.Println("WebAssembly Loaded!") 14 | 15 | // Expose Go functions to JavaScript 16 | js.Global().Set("createAccount", js.FuncOf(createAccount)) 17 | js.Global().Set("retrieveAccounts", js.FuncOf(retrieveAccounts)) 18 | js.Global().Set("healthCheck", js.FuncOf(healthCheck)) 19 | 20 | // Keep the application running 21 | select {} 22 | } 23 | 24 | // CreateAccount sends a PUT request to the backend to create an account 25 | func createAccount(this js.Value, args []js.Value) interface{} { 26 | account := map[string]interface{}{ 27 | "account_name": args[0].String(), 28 | "account_number": args[1].String(), 29 | "address": args[2].String(), 30 | "amount": args[3].Float(), 31 | "iban": args[4].String(), 32 | "type": args[5].String(), 33 | } 34 | 35 | data, _ := json.Marshal(account) 36 | go func() { 37 | resp, err := httpRequest("PUT", "http://localhost:8080/accounts", string(data)) 38 | if err != nil { 39 | js.Global().Get("alert").Invoke("Failed to create account: " + err.Error()) 40 | return 41 | } 42 | js.Global().Get("alert").Invoke("Account created successfully: " + resp) 43 | }() 44 | return nil 45 | } 46 | 47 | func retrieveAccounts(this js.Value, args []js.Value) interface{} { 48 | pagination := map[string]interface{}{ 49 | "page": args[0].Int(), 50 | "perPage": args[1].Int(), // Use "perPage" instead of "size" 51 | } 52 | 53 | data, _ := json.Marshal(pagination) 54 | go func() { 55 | resp, err := httpRequest("POST", "http://localhost:8080/accounts/retrieve", string(data)) 56 | if err != nil { 57 | js.Global().Get("alert").Invoke("Failed to retrieve accounts: " + err.Error()) 58 | return 59 | } 60 | 61 | // Parse the response JSON 62 | var result map[string]interface{} 63 | if err := json.Unmarshal([]byte(resp), &result); err != nil { 64 | js.Global().Get("alert").Invoke("Failed to parse accounts response: " + err.Error()) 65 | return 66 | } 67 | 68 | // Get the "revisions" array from the response 69 | revisions, ok := result["revisions"].([]interface{}) 70 | if !ok { 71 | js.Global().Get("alert").Invoke("Invalid accounts response format") 72 | return 73 | } 74 | 75 | // Access the table's tbody element in the DOM 76 | document := js.Global().Get("document") 77 | tableBody := document.Call("querySelector", "#accounts-table tbody") 78 | tableBody.Set("innerHTML", "") // Clear existing rows 79 | 80 | // Populate the table with the retrieved accounts 81 | for _, rev := range revisions { 82 | revision := rev.(map[string]interface{}) 83 | documentData := revision["document"].(map[string]interface{}) 84 | 85 | row := document.Call("createElement", "tr") 86 | row.Set("innerHTML", ` 87 | `+getString(documentData["_id"])+` 88 | `+getString(documentData["account_name"])+` 89 | `+getString(documentData["account_number"])+` 90 | `+getString(documentData["address"])+` 91 | `+getFloat(documentData["amount"])+` 92 | `+getString(documentData["iban"])+` 93 | `+getString(documentData["type"])+` 94 | `) 95 | tableBody.Call("appendChild", row) 96 | } 97 | }() 98 | return nil 99 | } 100 | 101 | // Helper function to get string value from interface{} 102 | func getString(value interface{}) string { 103 | if value == nil { 104 | return "" 105 | } 106 | return fmt.Sprintf("%v", value) 107 | } 108 | 109 | // Helper function to get float value from interface{} 110 | func getFloat(value interface{}) string { 111 | if value == nil { 112 | return "0" 113 | } 114 | return fmt.Sprintf("%.2f", value) 115 | } 116 | 117 | // HealthCheck sends a GET request to check backend health 118 | func healthCheck(this js.Value, args []js.Value) interface{} { 119 | go func() { 120 | resp, err := httpRequest("GET", "http://localhost:8080/healthz", "") 121 | if err != nil { 122 | js.Global().Get("alert").Invoke("Health check failed: " + err.Error()) 123 | return 124 | } 125 | js.Global().Get("alert").Invoke("Health check response: " + resp) 126 | }() 127 | return nil 128 | } 129 | 130 | func httpRequest(method, url, body string) (string, error) { 131 | fmt.Printf("Making %s request to %s with body: %s\n", method, url, body) 132 | 133 | fetch := js.Global().Get("fetch") 134 | options := map[string]interface{}{ 135 | "method": method, 136 | "headers": map[string]interface{}{ 137 | "Content-Type": "application/json", 138 | }, 139 | } 140 | if body != "" { 141 | options["body"] = body 142 | } 143 | 144 | promise := fetch.Invoke(url, options) 145 | resultChan := make(chan js.Value) 146 | errChan := make(chan error) 147 | 148 | promise.Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 149 | response := args[0] 150 | response.Call("text").Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 151 | fmt.Printf("Response received: %s\n", args[0].String()) 152 | resultChan <- args[0] 153 | return nil 154 | })).Call("catch", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 155 | fmt.Printf("Error extracting response text: %s\n", args[0].String()) 156 | errChan <- fmt.Errorf(args[0].String()) 157 | return nil 158 | })) 159 | return nil 160 | })).Call("catch", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 161 | fmt.Printf("Fetch request failed: %s\n", args[0].String()) 162 | errChan <- fmt.Errorf(args[0].String()) 163 | return nil 164 | })) 165 | 166 | select { 167 | case result := <-resultChan: 168 | return result.String(), nil 169 | case err := <-errChan: 170 | return "", err 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /frontend/frontend.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /frontend/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/norbix/demo1_fullstack_golang/frontend 2 | 3 | go 1.23.3 4 | -------------------------------------------------------------------------------- /frontend/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Go Wasm Frontend 6 | 22 | 23 | 24 |

Account Management

25 | 26 |
27 | 28 |
29 |

Create Account

30 | 31 | 32 | 33 | 34 | 35 | 39 | 40 |
41 | 42 |
43 |

Retrieve Accounts

44 | 45 | 46 | 47 |
48 | 49 |
50 |

Retrieved Accounts

51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
IDAccount NameAccount NumberAddressAmountIBANType
67 |
68 | 69 |
70 |

Health Check

71 | 72 |
73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$1" == "frontend" ]; then 4 | echo "Building frontend..." 5 | GOOS=js GOARCH=wasm go build -o ./build/main.wasm ./cmd/ui/main.go 6 | cp $(go env GOROOT)/misc/wasm/wasm_exec.js ./build/ 7 | cp ./templates/index.html ./build/ 8 | cp ./assets/* ./build/ 9 | echo "Frontend build complete!" 10 | else 11 | echo "Unknown build target: $1" 12 | exit 1 13 | fi 14 | --------------------------------------------------------------------------------