Compare commits
1 Commits
main
...
aa1d238b4e
Author | SHA1 | Date | |
---|---|---|---|
aa1d238b4e |
@ -7,7 +7,7 @@ tmp_dir = "tmp"
|
|||||||
bin = "./_output/howmuch"
|
bin = "./_output/howmuch"
|
||||||
cmd = "make build"
|
cmd = "make build"
|
||||||
delay = 1000
|
delay = 1000
|
||||||
exclude_dir = ["assets", "tmp", "vendor", "testdata", "_output", "internal/howmuch/adapter/repo/sqlc"]
|
exclude_dir = ["assets", "tmp", "vendor", "testdata", "_output"]
|
||||||
exclude_file = []
|
exclude_file = []
|
||||||
exclude_regex = ["_test.go"]
|
exclude_regex = ["_test.go"]
|
||||||
exclude_unchanged = false
|
exclude_unchanged = false
|
||||||
|
@ -1,63 +0,0 @@
|
|||||||
# MIT License
|
|
||||||
#
|
|
||||||
# Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
#
|
|
||||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
# of this software and associated documentation files (the "Software"), to deal
|
|
||||||
# in the Software without restriction, including without limitation the rights
|
|
||||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
# copies of the Software, and to permit persons to whom the Software is
|
|
||||||
# furnished to do so, subject to the following conditions:
|
|
||||||
#
|
|
||||||
# The above copyright notice and this permission notice shall be included in all
|
|
||||||
# copies or substantial portions of the Software.
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
# SOFTWARE.
|
|
||||||
|
|
||||||
name: Build and test
|
|
||||||
run-name: ${{ gitea.actor }} is building and testing the project!
|
|
||||||
on: [push]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
Build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
|
|
||||||
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!"
|
|
||||||
- run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
|
|
||||||
- name: Check out repository code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."
|
|
||||||
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
|
|
||||||
- name: List files in the repository
|
|
||||||
run: |
|
|
||||||
ls ${{ gitea.workspace }}
|
|
||||||
- name: Setup Go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: '1.23.1'
|
|
||||||
- run: go version
|
|
||||||
- name: Setup sqlc
|
|
||||||
uses: sqlc-dev/setup-sqlc@v4
|
|
||||||
with:
|
|
||||||
sqlc-version: '1.25.0'
|
|
||||||
- run: sqlc version
|
|
||||||
- name: Build backend
|
|
||||||
run: make build
|
|
||||||
- name: Test backend
|
|
||||||
run: make test
|
|
||||||
- name: Setup node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 18
|
|
||||||
- name: Build frontend
|
|
||||||
run: make web-build
|
|
||||||
- name: Test frontend
|
|
||||||
run: make web-test
|
|
||||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
|
35
.gitignore
vendored
35
.gitignore
vendored
@ -24,38 +24,3 @@ go.work.sum
|
|||||||
|
|
||||||
# Custom
|
# Custom
|
||||||
/_output
|
/_output
|
||||||
/deployment/tmp
|
|
||||||
/tmp/**
|
|
||||||
|
|
||||||
# Vue
|
|
||||||
#
|
|
||||||
# Logs
|
|
||||||
web/logs
|
|
||||||
web/*.log
|
|
||||||
web/npm-debug.log*
|
|
||||||
web/yarn-debug.log*
|
|
||||||
web/yarn-error.log*
|
|
||||||
web/pnpm-debug.log*
|
|
||||||
web/lerna-debug.log*
|
|
||||||
|
|
||||||
web/node_modules
|
|
||||||
web/.DS_Store
|
|
||||||
web/dist
|
|
||||||
web/dist-ssr
|
|
||||||
web/coverage
|
|
||||||
web/*.local
|
|
||||||
|
|
||||||
web/cypress/videos/
|
|
||||||
web/cypress/screenshots/
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
web/.vscode/*
|
|
||||||
web/!.vscode/extensions.json
|
|
||||||
web/.idea
|
|
||||||
web/*.suo
|
|
||||||
web/*.ntvs*
|
|
||||||
web/*.njsproj
|
|
||||||
web/*.sln
|
|
||||||
web/*.sw?
|
|
||||||
|
|
||||||
web/*.tsbuildinfo
|
|
||||||
|
51
Makefile
51
Makefile
@ -4,41 +4,15 @@ COMMON_SELF_DIR := $(dir $(lastword $(MAKEFILE_LIST)))
|
|||||||
ROOT_DIR := $(abspath $(shell cd $(COMMON_SELF_DIR)/ && pwd -P))
|
ROOT_DIR := $(abspath $(shell cd $(COMMON_SELF_DIR)/ && pwd -P))
|
||||||
OUTPUT_DIR := $(ROOT_DIR)/_output
|
OUTPUT_DIR := $(ROOT_DIR)/_output
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# Version
|
|
||||||
|
|
||||||
VERSION_PACKAGE=git.vinchent.xyz/vinchent/howmuch/pkg/version
|
|
||||||
|
|
||||||
ifeq ($(origin VERSION), undefined)
|
|
||||||
VERSION:=$(shell git describe --tags --always --match='v*')
|
|
||||||
endif
|
|
||||||
|
|
||||||
GIT_TREE_STATE="dirty"
|
|
||||||
ifeq (, $(shell git status --porcelain 2>/dev/null))
|
|
||||||
GIT_TREE_STATE ="clean"
|
|
||||||
endif
|
|
||||||
|
|
||||||
GIT_COMMIT:=$(shell git rev-parse HEAD)
|
|
||||||
|
|
||||||
GO_LDFLAGS += \
|
|
||||||
-X $(VERSION_PACKAGE).GitVersion=$(VERSION) \
|
|
||||||
-X $(VERSION_PACKAGE).GitCommit=$(GIT_COMMIT) \
|
|
||||||
-X $(VERSION_PACKAGE).GitTreeState=$(GIT_TREE_STATE) \
|
|
||||||
-X $(VERSION_PACKAGE).BuildDate=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ')
|
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
.PHONY: all
|
.PHONY: all
|
||||||
all: add-copyright format build
|
all: add-copyright format build
|
||||||
web: web-all
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
.PHONY: build
|
.PHONY: build
|
||||||
build: tidy sqlc # build.
|
build: tidy # build.
|
||||||
@go build -v -ldflags "$(GO_LDFLAGS)" -o $(OUTPUT_DIR)/howmuch $(ROOT_DIR)/cmd/howmuch/main.go
|
@go build -v -o $(OUTPUT_DIR)/howmuch $(ROOT_DIR)/cmd/howmuch/main.go
|
||||||
|
|
||||||
.PHONY: sqlc
|
|
||||||
sqlc:
|
|
||||||
@sqlc generate
|
|
||||||
|
|
||||||
.PHONY: format
|
.PHONY: format
|
||||||
format: # format code.
|
format: # format code.
|
||||||
@ -46,8 +20,7 @@ format: # format code.
|
|||||||
|
|
||||||
.PHONY: add-copyright
|
.PHONY: add-copyright
|
||||||
add-copyright: # add license to file headers.
|
add-copyright: # add license to file headers.
|
||||||
@addlicense -v -f $(ROOT_DIR)/LICENSE $(ROOT_DIR) --skip-files=database.yml --skip-dirs=$(OUTPUT_DIR),deployment,migrations,configs,sqlc,web,mock
|
@addlicense -v -f $(ROOT_DIR)/LICENSE -ignore third_party/** -ignore vendor/** -ignore $(OUTPUT_DIR) $(ROOT_DIR)
|
||||||
|
|
||||||
.PHONY: swagger
|
.PHONY: swagger
|
||||||
swagger: # Run swagger.
|
swagger: # Run swagger.
|
||||||
@swagger serve -F=swagger --no-open --port 65534 $(ROOT_DIR)/api/openapi/openapi.yaml
|
@swagger serve -F=swagger --no-open --port 65534 $(ROOT_DIR)/api/openapi/openapi.yaml
|
||||||
@ -56,22 +29,6 @@ swagger: # Run swagger.
|
|||||||
tidy: # Handle packkages.
|
tidy: # Handle packkages.
|
||||||
@go mod tidy
|
@go mod tidy
|
||||||
|
|
||||||
.PHONY: test
|
|
||||||
test:
|
|
||||||
@go test ./...
|
|
||||||
|
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean: # Clean up.
|
clean: # Clean up.
|
||||||
@-rm -vrf $(OUTPUT_DIR)
|
@-rm -vrf $(OUTPUT_DIR)
|
||||||
|
|
||||||
.PHONY: web-build
|
|
||||||
web-build:
|
|
||||||
$(MAKE) -C web build
|
|
||||||
|
|
||||||
.PHONY: web-test
|
|
||||||
web-test:
|
|
||||||
$(MAKE) -C web test
|
|
||||||
|
|
||||||
.PHONY: web-all
|
|
||||||
web-all:
|
|
||||||
$(MAKE) -C web all
|
|
||||||
|
496
README.md
496
README.md
@ -1,33 +1,5 @@
|
|||||||
# howmuch
|
# howmuch
|
||||||
|
|
||||||
<!--toc:start-->
|
|
||||||
- [howmuch](#howmuch)
|
|
||||||
- [Project Diary](#project-diary)
|
|
||||||
- [2024/09/30](#20240930)
|
|
||||||
- [2024/10/01](#20241001)
|
|
||||||
- [Config](#config)
|
|
||||||
- [Business logic](#business-logic)
|
|
||||||
- [Startup framework](#startup-framework)
|
|
||||||
- [2024/10/02](#20241002)
|
|
||||||
- [Logging](#logging)
|
|
||||||
- [Version](#version)
|
|
||||||
- [2024/10/03](#20241003)
|
|
||||||
- [2024/10/04](#20241004)
|
|
||||||
- [2024/10/06](#20241006)
|
|
||||||
- [Workflow](#workflow)
|
|
||||||
- [2024/10/07](#20241007)
|
|
||||||
- [The choice of the front end framework](#the-choice-of-the-front-end-framework)
|
|
||||||
- [2024/10/08](#20241008)
|
|
||||||
- [2024/10/09](#20241009)
|
|
||||||
- [2024/10/11](#20241011)
|
|
||||||
- [2024/10/13](#20241013)
|
|
||||||
- [2024/10/15](#20241015)
|
|
||||||
- [2024/10/16](#20241016)
|
|
||||||
- [2024/10/17](#20241017)
|
|
||||||
- [2024/10/18](#20241018)
|
|
||||||
- [2024/10/19](#20241019)
|
|
||||||
<!--toc:end-->
|
|
||||||
|
|
||||||
A tricount like expense-sharing system written in Go
|
A tricount like expense-sharing system written in Go
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -36,8 +8,6 @@ It is a personal project to learn go and relative technologies.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Project Diary
|
## Project Diary
|
||||||
|
|
||||||
### 2024/09/30
|
### 2024/09/30
|
||||||
@ -61,11 +31,11 @@ Next I need to design the API.
|
|||||||
- add other users to that event
|
- add other users to that event
|
||||||
- A user can only view their own events, but not the events of other users'
|
- A user can only view their own events, but not the events of other users'
|
||||||
- A user can add an expense to the event (reason, date, who payed how much,
|
- A user can add an expense to the event (reason, date, who payed how much,
|
||||||
who benefited how much)
|
who benefited how much)
|
||||||
- Users in the event can edit or delete one entry
|
- Users in the event can edit or delete one entry
|
||||||
- changes are sent to friends in the event
|
- changes are sent to friends in the event
|
||||||
- User can get the money they spent themselves and the money they must pay
|
- User can get the money they spent themselves and the money they must pay
|
||||||
to each other
|
to each other
|
||||||
- User can also get the total amount or the histories.
|
- User can also get the total amount or the histories.
|
||||||
|
|
||||||
That is what I thought of for now.
|
That is what I thought of for now.
|
||||||
@ -109,10 +79,10 @@ The execution of the program is then just a command like `howmuch run`.
|
|||||||
Moreover, in a distributed system, configs can be stored on `etcd`.
|
Moreover, in a distributed system, configs can be stored on `etcd`.
|
||||||
|
|
||||||
> [Kubernetes stores configuration data into etcd for service discovery and
|
> [Kubernetes stores configuration data into etcd for service discovery and
|
||||||
> cluster management; etcd’s consistency is crucial for correctly scheduling
|
cluster management; etcd’s consistency is crucial for correctly scheduling
|
||||||
> and operating services. The Kubernetes API server persists cluster state
|
and operating services. The Kubernetes API server persists cluster state
|
||||||
> into etcd. It uses etcd’s watch API to monitor the cluster and roll out
|
into etcd. It uses etcd’s watch API to monitor the cluster and roll out
|
||||||
> critical configuration changes.](https://etcd.io/docs/v3.5/learning/why/)
|
critical configuration changes.](https://etcd.io/docs/v3.5/learning/why/)
|
||||||
|
|
||||||
#### Business logic
|
#### Business logic
|
||||||
|
|
||||||
@ -120,8 +90,8 @@ Moreover, in a distributed system, configs can be stored on `etcd`.
|
|||||||
- init DBs (Redis, SQL, Kafka, etc.)
|
- init DBs (Redis, SQL, Kafka, etc.)
|
||||||
- init web service (http, https, gRPC, etc.)
|
- init web service (http, https, gRPC, etc.)
|
||||||
- start async tasks like `watch kube-apiserver`; pull data from third-party
|
- start async tasks like `watch kube-apiserver`; pull data from third-party
|
||||||
services; store, register `/metrics` and listen on some port; start kafka
|
services; store, register `/metrics` and listen on some port; start kafka
|
||||||
consumer queue, etc.
|
consumer queue, etc.
|
||||||
- Run specific business logic
|
- Run specific business logic
|
||||||
- Stop the program
|
- Stop the program
|
||||||
- others...
|
- others...
|
||||||
@ -129,456 +99,8 @@ Moreover, in a distributed system, configs can be stored on `etcd`.
|
|||||||
#### Startup framework
|
#### Startup framework
|
||||||
|
|
||||||
When business logic becomes complicated, we cannot spread them into a simple
|
When business logic becomes complicated, we cannot spread them into a simple
|
||||||
`main` function. We need something to handle all those task, sync or async.
|
`main` function. We need something to handle all thoses task, sync or async.
|
||||||
That is why we use `cobra`.
|
That is why we use `cobra`.
|
||||||
|
|
||||||
So for this project, we will use the combination of `pflag`, `viper` and
|
So for this project, we will use the combination of `pflag`, `viper` and
|
||||||
`cobra`.
|
`cobra`.
|
||||||
|
|
||||||
### 2024/10/02
|
|
||||||
|
|
||||||
#### Logging
|
|
||||||
|
|
||||||
Use `zap` for logging system. Log will be output to stdout for dev purpose,
|
|
||||||
but it is also output to files. The log files can then be fetched to
|
|
||||||
`Elasticsearch` for analyzing.
|
|
||||||
|
|
||||||
#### Version
|
|
||||||
|
|
||||||
Add versioning into the app.
|
|
||||||
|
|
||||||
### 2024/10/03
|
|
||||||
|
|
||||||
Set up the web server with some necessary/nice to have middlewares.
|
|
||||||
|
|
||||||
- Recovery, Logger (already included in Default mode)
|
|
||||||
- CORS
|
|
||||||
- RequestId
|
|
||||||
|
|
||||||
Using channel and signal to gracefully shutdown the server.
|
|
||||||
|
|
||||||
A more comprehensible error code design :
|
|
||||||
|
|
||||||
- Classical HTTP code.
|
|
||||||
- Service error code composed by "PlatformError.ServiceError", e.g. "ResourceNotFound.PageNotFound"
|
|
||||||
- error message.
|
|
||||||
|
|
||||||
The service error code helps to identify the problem more precisely.
|
|
||||||
|
|
||||||
### 2024/10/04
|
|
||||||
|
|
||||||
Application architecture design follows [Clean Architecture](https://manakuro.medium.com/clean-architecture-with-go-bce409427d31)
|
|
||||||
that has several layers:
|
|
||||||
|
|
||||||
- Entities: the models of the product
|
|
||||||
- Use cases: the core business rule
|
|
||||||
- Interface Adapters: convert data-in to entities and convert data-out to
|
|
||||||
output ports.
|
|
||||||
- Frameworks and drivers: Web server, DB.
|
|
||||||
|
|
||||||
Based on this logic, we create the following directories:
|
|
||||||
|
|
||||||
- `model`: entities
|
|
||||||
- `infra`: Provides the necessary functions to setup the infrastructure,
|
|
||||||
especially the DB (output-port), but also the router (input-port). Once
|
|
||||||
setup, we don't touch them anymore.
|
|
||||||
- `registry`: Provides a register function for the main to register a service.
|
|
||||||
It takes the pass to the output-port (ex.DBs) and gives back a pass
|
|
||||||
(controller) to the input-port
|
|
||||||
- `adapter`: Controllers are one of the adapters, when they are called,
|
|
||||||
they parse the user input and parse them into models and run the usecase
|
|
||||||
rules. Then they send back the response(input-port). For the output-port
|
|
||||||
part, the `repo` is the implementation of interfaces defined in `usecase/repo`.
|
|
||||||
- `usecase`: with the input of adapter, do what have to be done, and answer
|
|
||||||
with the result. In the meantime, we may have to store things into DBs.
|
|
||||||
Here we use the Repository model to decouple the implementation of the repo
|
|
||||||
with the interface. Thus in `usecase/repo` we only define interfaces.
|
|
||||||
|
|
||||||
Then it comes the real design for the app.
|
|
||||||
|
|
||||||
Following the Agile method, I don't try to define the entire project at the
|
|
||||||
beginning but step by step, starting at the user part.
|
|
||||||
|
|
||||||
```go
|
|
||||||
type User struct {
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
FirstName string
|
|
||||||
LastName string
|
|
||||||
Email string
|
|
||||||
Password string
|
|
||||||
ID int
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Use Buffalo pop `Soda CLI` to create database migrations.
|
|
||||||
|
|
||||||
### 2024/10/06
|
|
||||||
|
|
||||||
Implement the architecture design for User entity.
|
|
||||||
|
|
||||||
Checked out OpenAPI, and found that it was not that simple at all. It needs
|
|
||||||
a whole package of knowledge about the web development!
|
|
||||||
|
|
||||||
For the test-driven part,
|
|
||||||
|
|
||||||
- model layer: just model designs, **nothing to test**
|
|
||||||
- infra: routes and db connections, it works when it works. Nothing to test.
|
|
||||||
- registry: Just return some structs, no logic. **Not worth testing**
|
|
||||||
- adapter:
|
|
||||||
- input-port (controller) test: it is about testing parsing the input
|
|
||||||
value, and the output results writing. The unit test of controller is to
|
|
||||||
**make sure that they behave as defined in the API documentation**. To
|
|
||||||
test, we have to mock the **business service**.
|
|
||||||
- output-port (repo) test: it is about testing converting business model
|
|
||||||
to database model and the interaction with the database. If we are going
|
|
||||||
to test them, it's about simulating different type of database behaviour
|
|
||||||
(success, timeout, etc.). To test, we have to mock the
|
|
||||||
**database connection**.
|
|
||||||
- usecase: This is the core part to test, it's about the core business.
|
|
||||||
We provide the data input and we check the data output in a fake repository.
|
|
||||||
|
|
||||||
With this design, although it may seem overkill for this little project, fits
|
|
||||||
perfectly well with the TDD method.
|
|
||||||
|
|
||||||
Concretely, I will do the TDD for my usecase level development, and for the
|
|
||||||
rest, I just put unit tests aside for later.
|
|
||||||
|
|
||||||
#### Workflow
|
|
||||||
|
|
||||||
1. OAS Definition
|
|
||||||
2. (Integration/Validation test)
|
|
||||||
3. Usecase unit test cases
|
|
||||||
4. Usecase development
|
|
||||||
5. Refactor (2-3-4)
|
|
||||||
6. Input-port/Output-port
|
|
||||||
|
|
||||||
That should be the correct workflow. But to save time, I will cut off the
|
|
||||||
integration test part (the 2nd point).
|
|
||||||
|
|
||||||
### 2024/10/07
|
|
||||||
|
|
||||||
I rethought about the whole API design (even though I have only one yet). I
|
|
||||||
have created `/signup` and `/login` without thinking too much, but in fact
|
|
||||||
it is not quite _RESTful_.
|
|
||||||
|
|
||||||
**REST** is all about resources. While `/signup` and `/login` is quite
|
|
||||||
comprehensible, thus service-oriented, they don't follow the REST philosophy,
|
|
||||||
that is to say, **resource-oriented**.
|
|
||||||
|
|
||||||
If we rethink about `/signup`, what it does is to create a resource of `User`.
|
|
||||||
Thus, for a backend API, it'd better be named as `User.Create`. But what
|
|
||||||
about `/login`, it doesn't do anything about `User`. It would be strange to
|
|
||||||
declare it as a User-relevant method.
|
|
||||||
|
|
||||||
Instead, what `/login` really does, is to **create a session**.
|
|
||||||
In consequence, we have to create a new struct `Session` that can be created,
|
|
||||||
deleted, or updated.
|
|
||||||
|
|
||||||
It might seem overkill, and in real life, even in the official Pet store
|
|
||||||
example of OpenAPI, signup and login are under /user. But it just opened my
|
|
||||||
mind and forces me to **think and design RESTfully**!
|
|
||||||
|
|
||||||
That being said, for the user side, we shall still have `/signup` and `/login`,
|
|
||||||
because on the Front-end, we must be user-centered. We can even make this
|
|
||||||
2 functions on the same page with the same endpoint `/login`. The user enter
|
|
||||||
the email and the password, then clicks on `Login or Signup`. If the login
|
|
||||||
is successful, then he is logged in. Otherwise, if the user doesn't exist
|
|
||||||
yet, we open up 2 more inputs (first name and last name) for signup. They
|
|
||||||
can just provide the extra information and click again on `Signup`.
|
|
||||||
|
|
||||||
That, again, being said, I am thinking about doing some Front-end stuff just
|
|
||||||
to make the validation tests of the product simpler.
|
|
||||||
|
|
||||||
#### The choice of the front end framework
|
|
||||||
|
|
||||||
I have considered several choices.
|
|
||||||
|
|
||||||
If I didn't purposely make the backend code to provide a REST API, I might
|
|
||||||
choose server-side-rendering with `templ + htmx`, or even `template+vanilla
|
|
||||||
javascript`.
|
|
||||||
|
|
||||||
I can still write a rather static Go-frontend-server to serve HTMLs and call
|
|
||||||
my Go backend. _And it might be a good idea if they communicate on Go native
|
|
||||||
rpc._ It worth a try.
|
|
||||||
|
|
||||||
And I have moved on to `Svelte` which seems very simple by design and the
|
|
||||||
whole compile thing makes it really charm. But this is mainly a Go project,
|
|
||||||
to learn something new with a rather small community means potentially more
|
|
||||||
investment. I can learn it later.
|
|
||||||
|
|
||||||
Among `Angular`, `React` and `Vue`, I prefer `Vue`, for several reasons.
|
|
||||||
First, `Angular` is clearly overkill for this small demo project. Second,
|
|
||||||
`React` is good but I personally like the way of Vue doing things. And I
|
|
||||||
work with Vue at work, so I might have more technical help from my colleagues.
|
|
||||||
|
|
||||||
So the plan for this week is to have both the Front end part and Backend part
|
|
||||||
working, just for user signup and login.
|
|
||||||
|
|
||||||
I would like to directly put this stuff on a CI-pipeline for tests and
|
|
||||||
deployment, even I have barely nothing yet. It is always good to do this
|
|
||||||
preparation stuff at the early stage of the project. So we can benefit from
|
|
||||||
them all the way along.
|
|
||||||
|
|
||||||
Moreover, even I am not really finishing the project, it can still be
|
|
||||||
something representable that I can show to a future interviewer.
|
|
||||||
|
|
||||||
### 2024/10/08
|
|
||||||
|
|
||||||
Gitea action setup ! 🎉🎉🎉
|
|
||||||
|
|
||||||
Next step is to run some check and build and test!
|
|
||||||
|
|
||||||
### 2024/10/09
|
|
||||||
|
|
||||||
No code for today neither. But I did some design for the user story and
|
|
||||||
the database model design.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 2024/10/11
|
|
||||||
|
|
||||||
I spent 2 days learning some basic of Vue. Learning Vue takes time. There
|
|
||||||
are a lot of concepts and it needs a lot of practice. Even though I may not
|
|
||||||
need a professional level web page, I don't want to copy one module from this
|
|
||||||
blog and another one from another tutorial. I might just put aside the
|
|
||||||
front-end for now and concentrate on my backend Go app.
|
|
||||||
|
|
||||||
For now, I will just test my backend with `curl`.
|
|
||||||
|
|
||||||
And today's job is to get the login part done!
|
|
||||||
|
|
||||||
### 2024/10/13
|
|
||||||
|
|
||||||
Finally it took more than just one night for me to figure out the JWT.
|
|
||||||
|
|
||||||
The JWT token is simple because it doesn't need to be stored to and fetched
|
|
||||||
from a database. But there is no way to revoke it instead of waiting for the
|
|
||||||
expiry date.
|
|
||||||
|
|
||||||
To do so, we still have to use a database. We can store a logged out user's
|
|
||||||
jti into Redis, and each time we log in, look up the cache to find if the
|
|
||||||
user is logged out. And set the cache's timeout to the expiry time of the
|
|
||||||
token, so that it is removed automatically.
|
|
||||||
|
|
||||||
It'd better to inject the dependency of Redis connection into the `Authn`
|
|
||||||
middleware so that it's simpler to test.
|
|
||||||
|
|
||||||
### 2024/10/15
|
|
||||||
|
|
||||||
Redis is integrated to keep a blacklist of logged out users. BTW `memcached`
|
|
||||||
is also interesting. In case later I want to switch to another key-value
|
|
||||||
storage, I have made an interface. It also helps for the test. I can even
|
|
||||||
just drop the redis and use a bare-hand native hashmap.
|
|
||||||
|
|
||||||
Quite a lot benefits. And then I realised that I have done "wrong" about
|
|
||||||
`sqlc`. I shouldn't have used the pgx driver, instead the `database/sql`
|
|
||||||
driver is more universal, if I want to switch to sqlite or mysql later.
|
|
||||||
|
|
||||||
Well it's not about changing the technical solution every 3 days, but a
|
|
||||||
system than can survive those changes elegantly must be a robust system, with
|
|
||||||
functionalities well decoupled and interfaces well defined.
|
|
||||||
|
|
||||||
I will add some tests for existing code and then it's time to move on to
|
|
||||||
my core business logic.
|
|
||||||
|
|
||||||
### 2024/10/16
|
|
||||||
|
|
||||||
I am facing a design problem. My way to implement the business logic is to
|
|
||||||
first write the core logic code in the domain service level. It will help me
|
|
||||||
to identify if there are any missing part in my model design. Thus, when
|
|
||||||
some of the business logic is done, I can create database migrations and then
|
|
||||||
implement the adapter level's code.
|
|
||||||
|
|
||||||
The problem is that my design depends heavily on the database. Taking the
|
|
||||||
example of adding an expense to en event.
|
|
||||||
|
|
||||||
Input is a valid `ExpenseDTO` which has the `event`, `paiements` and
|
|
||||||
`receptions`. What I must do is to open a database transaction where I:
|
|
||||||
|
|
||||||
1. Get the Event. (Most importantly the `TotalAmount`)
|
|
||||||
2. For each `paiemnt` and `reception` create a transaction related to the
|
|
||||||
`User`. And insert them into the database.
|
|
||||||
3. Update the `TotalAmount`
|
|
||||||
4. Update the caches if any
|
|
||||||
|
|
||||||
If any step fails, the transaction rolls back.
|
|
||||||
|
|
||||||
This has barely no logic at all. I think it is not suitable to try to tie
|
|
||||||
this operation to the domain model.
|
|
||||||
|
|
||||||
However, there is something that worth a domain model level method, that
|
|
||||||
is to calculate the share of each members of the event, where we will have
|
|
||||||
the list of members and the amount of balance they have. And then we will
|
|
||||||
do the calculate and send back a list of money one should pay for another.
|
|
||||||
|
|
||||||
Finally, I think the business logic is still too simple to be put into a
|
|
||||||
"Domain". For now, the service layer is just enough.
|
|
||||||
|
|
||||||
### 2024/10/17
|
|
||||||
|
|
||||||
The following basic use cases are to be implemented at the first time.
|
|
||||||
|
|
||||||
- [X] A user signs up
|
|
||||||
- [X] A user logs in
|
|
||||||
- [ ] A user lists their events (pagination)
|
|
||||||
- [ ] A user sees the detail of an event (description, members, amount)
|
|
||||||
- [ ] A user sees the expenses of an event (total amount, personal expenses, pagination)
|
|
||||||
- [ ] A user sees the detail of an expense: (time, amount, payers, recipients)
|
|
||||||
- [ ] A user adds an expense
|
|
||||||
- [ ] A user updates/changes an expense (may handle some extra access control)
|
|
||||||
- [ ] A user can pay the debt to other members (just a special case of expense)
|
|
||||||
- [ ] A user creates an event (and participate to it)
|
|
||||||
- [ ] A user updates the event info
|
|
||||||
- [ ] A user invites another user by sending a mail with a token.
|
|
||||||
- [ ] A user joins an event by accepting an invitation
|
|
||||||
- [ ] A user cannot see other user's information
|
|
||||||
- [ ] A user cannot see the events that they didn't participated in.
|
|
||||||
|
|
||||||
For the second stage:
|
|
||||||
|
|
||||||
- [ ] A user can archive an event
|
|
||||||
- [ ] A user deletes an expense (may handle some extra access control)
|
|
||||||
- [ ] A user restore a deleted expense
|
|
||||||
- [ ] Audit log for expense updates/deletes
|
|
||||||
- [ ] ~A user quits an event (they cannot actually, but we can make as if they
|
|
||||||
quitted)~ **No we can't quit!**
|
|
||||||
|
|
||||||
With those functionalities, there will be an usable product. And then we can
|
|
||||||
work on other aspects. For example:
|
|
||||||
|
|
||||||
- introduce an admin to handle users.
|
|
||||||
- user info updates
|
|
||||||
- deleting user
|
|
||||||
- More user related contents
|
|
||||||
- Event related contents
|
|
||||||
- ex. Trip journal...
|
|
||||||
|
|
||||||
Stop dreaming... Just do the simple stuff first!
|
|
||||||
|
|
||||||
### 2024/10/18
|
|
||||||
|
|
||||||
I spent some time to figure out this one! But I don't actually need it for now.
|
|
||||||
So I just keep it here:
|
|
||||||
|
|
||||||
```SQL
|
|
||||||
SELECT
|
|
||||||
e.id,
|
|
||||||
e.name,
|
|
||||||
e.description,
|
|
||||||
e.created_at,
|
|
||||||
json_build_object(
|
|
||||||
'id', o.id,
|
|
||||||
'first_name', o.first_name,
|
|
||||||
'last_name', o.last_name
|
|
||||||
) AS owner,
|
|
||||||
json_agg(
|
|
||||||
json_build_object(
|
|
||||||
'id', u.id,
|
|
||||||
'first_name', u.first_name,
|
|
||||||
'last_name', u.last_name
|
|
||||||
)
|
|
||||||
) AS users -- Aggregation for users in the event
|
|
||||||
FROM "event" e
|
|
||||||
JOIN "participation" p ON p.event_id = e.id -- participation linked with the event
|
|
||||||
JOIN "user" u ON u.id = p.user_id -- and the query user
|
|
||||||
JOIN "user" o ON o.id = e.owner_id -- get the owner info
|
|
||||||
WHERE e.id IN (
|
|
||||||
SELECT pt.event_id FROM participation pt WHERE pt.user_id = $1
|
|
||||||
-- consider the events participated by user_id
|
|
||||||
)
|
|
||||||
GROUP BY
|
|
||||||
e.id, e.name, e.description, e.created_at,
|
|
||||||
o.id, o.first_name, o.last_name;
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2024/10/19
|
|
||||||
|
|
||||||
I don't plan to handle deletions at this first stage, but I note down what I
|
|
||||||
have thought of.
|
|
||||||
|
|
||||||
1. Just delete. But keep a replica at the front end of the object that we are
|
|
||||||
deleting. And propose an option to restore (so a new record is added to the DB)
|
|
||||||
2. Just delete, but wait. The request is sent to a queue with a timeout of
|
|
||||||
several seconds, if the user regrets, they can cancel the request. This can be
|
|
||||||
done on the front, but also on the back. I think it is better to do in on the
|
|
||||||
front-end.
|
|
||||||
3. Never deletes. But keep a state in the DB `deleted`. They will just be
|
|
||||||
ignored when counting.
|
|
||||||
4. Deletes when doing database cleanup. They lines deleted will be processed
|
|
||||||
when we cleanup the DB. And they will be definitely deleted at that time.
|
|
||||||
|
|
||||||
I can create a audit log table to log all the critical
|
|
||||||
changes in my `expense` table (update or delete).
|
|
||||||
|
|
||||||
Finished with the basic SQL commands. Learned a lot from SQL about `JOIN`,
|
|
||||||
aggregation and `CTE`. SQL itself has quite amount of things to learn, this
|
|
||||||
is on my future learning plan!
|
|
||||||
|
|
||||||
_I found it quite interesting that simply with SQL, we can simulate the most
|
|
||||||
business logic. It is a must-have competence for software design and
|
|
||||||
development._
|
|
||||||
|
|
||||||
### 2024/10/20
|
|
||||||
|
|
||||||
I was thinking that I should write test for `sqlc` generated code. And then
|
|
||||||
I found out `gomock` and see how it is done in the project of
|
|
||||||
`techschoo/simplebank`. It's a great tutorial project. It makes me questioning
|
|
||||||
my own project's structure. It seems overwhelmed at least at the repo level.
|
|
||||||
|
|
||||||
I don't actually use the sqlc generated object, instead I do a conversion to
|
|
||||||
my `Retrieved` objects. But with some advanced configuration we could make the
|
|
||||||
output of sqlc object directly usable. That will save a lot of code.
|
|
||||||
|
|
||||||
The problem I saw here is the dependency on `sqlc/models`, and the model
|
|
||||||
designed there has no business logic. Everything is done in the handlers
|
|
||||||
and the handlers query directly the DB.
|
|
||||||
|
|
||||||
More concretely, `sqlc` generates `RawJSON` for some fields that are embedded
|
|
||||||
structs. So I have to do the translation somewhere.
|
|
||||||
|
|
||||||
So I will just stick to the plan and keep going with the predefined structure.
|
|
||||||
|
|
||||||
I have to figure out how to use the generated mock files.
|
|
||||||
|
|
||||||
The goals for the next week is to finish the basic operations for each level
|
|
||||||
and run some integration tests with `curl`.
|
|
||||||
|
|
||||||
### 2024/10/22
|
|
||||||
|
|
||||||
I am facing come difficulties on testing of the `repo` functions.
|
|
||||||
|
|
||||||
First, I have to keep the business logic in the service layer. That means I
|
|
||||||
have to create the transaction at the service layer. I don't need to depend
|
|
||||||
on the implementation detail. So I have created a Transaction interface.
|
|
||||||
|
|
||||||
I don't care of the type of `tx` because I will pass it to repo layer and I
|
|
||||||
suppose that it knows what it is doing. Considering this, my repo `Create`
|
|
||||||
function will have to take an any and deduct the type of `tx`. So the layer
|
|
||||||
becomes untestable, because I have to pass a *sql.Tx into it and create a
|
|
||||||
querier.
|
|
||||||
|
|
||||||
Since this repo layer is just a wrapping layer between the `sqlc.models` and
|
|
||||||
my own models, I can extract the conversion part to functions and test them.
|
|
||||||
I'm not testing the whole thing but I test what I can.
|
|
||||||
|
|
||||||
### 2024/10/24
|
|
||||||
|
|
||||||
When writing the tests. I am asking myself the differences between `[]T`,
|
|
||||||
`[]*T` and `*[]T`.
|
|
||||||
|
|
||||||
`*[]T` is simple, it is a reference to the original slice. So modifying it
|
|
||||||
means modifying the original slice.
|
|
||||||
|
|
||||||
But between `[]*T` and `[]T`, the only difference that I see (pointed out by
|
|
||||||
`ChatGPT`) is how the memory is allocated. With `[]T` it might be better for
|
|
||||||
the GC to deal with the memory free. I thing for my project I will stick to
|
|
||||||
`[]T`.
|
|
||||||
|
|
||||||
### 2024/10/25
|
|
||||||
|
|
||||||
Read this [article](https://konradreiche.com/blog/two-common-go-interface-misuses/)
|
|
||||||
today, maybe I am abusing the usage of interfaces?
|
|
||||||
|
@ -1,828 +0,0 @@
|
|||||||
# MIT License
|
|
||||||
#
|
|
||||||
# Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
#
|
|
||||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
# of this software and associated documentation files (the "Software"), to deal
|
|
||||||
# in the Software without restriction, including without limitation the rights
|
|
||||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
# copies of the Software, and to permit persons to whom the Software is
|
|
||||||
# furnished to do so, subject to the following conditions:
|
|
||||||
#
|
|
||||||
# The above copyright notice and this permission notice shall be included in all
|
|
||||||
# copies or substantial portions of the Software.
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
# SOFTWARE.
|
|
||||||
|
|
||||||
swagger: "2.0"
|
|
||||||
openapi: 3.1.0
|
|
||||||
info:
|
|
||||||
title: Swagger Petstore - OpenAPI 3.1
|
|
||||||
description: |-
|
|
||||||
This is a sample Pet Store Server based on the OpenAPI 3.1 specification. You can find out more about
|
|
||||||
Swagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!
|
|
||||||
You can now help us improve the API whether it's by making changes to the definition itself or to the code.
|
|
||||||
That way, with time, we can improve the API in general, and expose some of the new features in OAS3.
|
|
||||||
|
|
||||||
Some useful links:
|
|
||||||
- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)
|
|
||||||
- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)
|
|
||||||
|
|
||||||
termsOfService: http://swagger.io/terms/
|
|
||||||
contact:
|
|
||||||
email: apiteam@swagger.io
|
|
||||||
license:
|
|
||||||
name: Apache 2.0
|
|
||||||
url: http://www.apache.org/licenses/LICENSE-2.0.html
|
|
||||||
version: 1.0.11
|
|
||||||
externalDocs:
|
|
||||||
description: Find out more about Swagger
|
|
||||||
url: http://swagger.io
|
|
||||||
servers:
|
|
||||||
- url: https://petstore3.swagger.io/api/v3
|
|
||||||
tags:
|
|
||||||
- name: pet
|
|
||||||
description: Everything about your Pets
|
|
||||||
externalDocs:
|
|
||||||
description: Find out more
|
|
||||||
url: http://swagger.io
|
|
||||||
- name: store
|
|
||||||
description: Access to Petstore orders
|
|
||||||
externalDocs:
|
|
||||||
description: Find out more about our store
|
|
||||||
url: http://swagger.io
|
|
||||||
- name: user
|
|
||||||
description: Operations about user
|
|
||||||
paths:
|
|
||||||
/pet:
|
|
||||||
put:
|
|
||||||
tags:
|
|
||||||
- pet
|
|
||||||
summary: Update an existing pet
|
|
||||||
description: Update an existing pet by Id
|
|
||||||
operationId: updatePet
|
|
||||||
requestBody:
|
|
||||||
description: Update an existent pet in the store
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Pet'
|
|
||||||
application/xml:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Pet'
|
|
||||||
application/x-www-form-urlencoded:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Pet'
|
|
||||||
required: true
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Successful operation
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Pet'
|
|
||||||
application/xml:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Pet'
|
|
||||||
'400':
|
|
||||||
description: Invalid ID supplied
|
|
||||||
'404':
|
|
||||||
description: Pet not found
|
|
||||||
'422':
|
|
||||||
description: Validation exception
|
|
||||||
security:
|
|
||||||
- petstore_auth:
|
|
||||||
- write:pets
|
|
||||||
- read:pets
|
|
||||||
post:
|
|
||||||
tags:
|
|
||||||
- pet
|
|
||||||
summary: Add a new pet to the store
|
|
||||||
description: Add a new pet to the store
|
|
||||||
operationId: addPet
|
|
||||||
requestBody:
|
|
||||||
description: Create a new pet in the store
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Pet'
|
|
||||||
application/xml:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Pet'
|
|
||||||
application/x-www-form-urlencoded:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Pet'
|
|
||||||
required: true
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Successful operation
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Pet'
|
|
||||||
application/xml:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Pet'
|
|
||||||
'400':
|
|
||||||
description: Invalid input
|
|
||||||
'422':
|
|
||||||
description: Validation exception
|
|
||||||
security:
|
|
||||||
- petstore_auth:
|
|
||||||
- write:pets
|
|
||||||
- read:pets
|
|
||||||
/pet/findByStatus:
|
|
||||||
get:
|
|
||||||
tags:
|
|
||||||
- pet
|
|
||||||
summary: Finds Pets by status
|
|
||||||
description: Multiple status values can be provided with comma separated strings
|
|
||||||
operationId: findPetsByStatus
|
|
||||||
parameters:
|
|
||||||
- name: status
|
|
||||||
in: query
|
|
||||||
description: Status values that need to be considered for filter
|
|
||||||
required: false
|
|
||||||
explode: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
default: available
|
|
||||||
enum:
|
|
||||||
- available
|
|
||||||
- pending
|
|
||||||
- sold
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: successful operation
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/Pet'
|
|
||||||
application/xml:
|
|
||||||
schema:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/Pet'
|
|
||||||
'400':
|
|
||||||
description: Invalid status value
|
|
||||||
security:
|
|
||||||
- petstore_auth:
|
|
||||||
- write:pets
|
|
||||||
- read:pets
|
|
||||||
/pet/findByTags:
|
|
||||||
get:
|
|
||||||
tags:
|
|
||||||
- pet
|
|
||||||
summary: Finds Pets by tags
|
|
||||||
description: Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.
|
|
||||||
operationId: findPetsByTags
|
|
||||||
parameters:
|
|
||||||
- name: tags
|
|
||||||
in: query
|
|
||||||
description: Tags to filter by
|
|
||||||
required: false
|
|
||||||
explode: true
|
|
||||||
schema:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: successful operation
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/Pet'
|
|
||||||
application/xml:
|
|
||||||
schema:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/Pet'
|
|
||||||
'400':
|
|
||||||
description: Invalid tag value
|
|
||||||
security:
|
|
||||||
- petstore_auth:
|
|
||||||
- write:pets
|
|
||||||
- read:pets
|
|
||||||
/pet/{petId}:
|
|
||||||
get:
|
|
||||||
tags:
|
|
||||||
- pet
|
|
||||||
summary: Find pet by ID
|
|
||||||
description: Returns a single pet
|
|
||||||
operationId: getPetById
|
|
||||||
parameters:
|
|
||||||
- name: petId
|
|
||||||
in: path
|
|
||||||
description: ID of pet to return
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: successful operation
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Pet'
|
|
||||||
application/xml:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Pet'
|
|
||||||
'400':
|
|
||||||
description: Invalid ID supplied
|
|
||||||
'404':
|
|
||||||
description: Pet not found
|
|
||||||
security:
|
|
||||||
- api_key: []
|
|
||||||
- petstore_auth:
|
|
||||||
- write:pets
|
|
||||||
- read:pets
|
|
||||||
post:
|
|
||||||
tags:
|
|
||||||
- pet
|
|
||||||
summary: Updates a pet in the store with form data
|
|
||||||
description: ''
|
|
||||||
operationId: updatePetWithForm
|
|
||||||
parameters:
|
|
||||||
- name: petId
|
|
||||||
in: path
|
|
||||||
description: ID of pet that needs to be updated
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
- name: name
|
|
||||||
in: query
|
|
||||||
description: Name of pet that needs to be updated
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
- name: status
|
|
||||||
in: query
|
|
||||||
description: Status of pet that needs to be updated
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
responses:
|
|
||||||
'400':
|
|
||||||
description: Invalid input
|
|
||||||
security:
|
|
||||||
- petstore_auth:
|
|
||||||
- write:pets
|
|
||||||
- read:pets
|
|
||||||
delete:
|
|
||||||
tags:
|
|
||||||
- pet
|
|
||||||
summary: Deletes a pet
|
|
||||||
description: delete a pet
|
|
||||||
operationId: deletePet
|
|
||||||
parameters:
|
|
||||||
- name: api_key
|
|
||||||
in: header
|
|
||||||
description: ''
|
|
||||||
required: false
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
- name: petId
|
|
||||||
in: path
|
|
||||||
description: Pet id to delete
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
responses:
|
|
||||||
'400':
|
|
||||||
description: Invalid pet value
|
|
||||||
security:
|
|
||||||
- petstore_auth:
|
|
||||||
- write:pets
|
|
||||||
- read:pets
|
|
||||||
/pet/{petId}/uploadImage:
|
|
||||||
post:
|
|
||||||
tags:
|
|
||||||
- pet
|
|
||||||
summary: uploads an image
|
|
||||||
description: ''
|
|
||||||
operationId: uploadFile
|
|
||||||
parameters:
|
|
||||||
- name: petId
|
|
||||||
in: path
|
|
||||||
description: ID of pet to update
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
- name: additionalMetadata
|
|
||||||
in: query
|
|
||||||
description: Additional Metadata
|
|
||||||
required: false
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
application/octet-stream:
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
format: binary
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: successful operation
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/ApiResponse'
|
|
||||||
security:
|
|
||||||
- petstore_auth:
|
|
||||||
- write:pets
|
|
||||||
- read:pets
|
|
||||||
/store/inventory:
|
|
||||||
get:
|
|
||||||
tags:
|
|
||||||
- store
|
|
||||||
summary: Returns pet inventories by status
|
|
||||||
description: Returns a map of status codes to quantities
|
|
||||||
operationId: getInventory
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: successful operation
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
additionalProperties:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
security:
|
|
||||||
- api_key: []
|
|
||||||
/store/order:
|
|
||||||
post:
|
|
||||||
tags:
|
|
||||||
- store
|
|
||||||
summary: Place an order for a pet
|
|
||||||
description: Place a new order in the store
|
|
||||||
operationId: placeOrder
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Order'
|
|
||||||
application/xml:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Order'
|
|
||||||
application/x-www-form-urlencoded:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Order'
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: successful operation
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Order'
|
|
||||||
'400':
|
|
||||||
description: Invalid input
|
|
||||||
'422':
|
|
||||||
description: Validation exception
|
|
||||||
/store/order/{orderId}:
|
|
||||||
get:
|
|
||||||
tags:
|
|
||||||
- store
|
|
||||||
summary: Find purchase order by ID
|
|
||||||
description: For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.
|
|
||||||
operationId: getOrderById
|
|
||||||
parameters:
|
|
||||||
- name: orderId
|
|
||||||
in: path
|
|
||||||
description: ID of order that needs to be fetched
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: successful operation
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Order'
|
|
||||||
application/xml:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Order'
|
|
||||||
'400':
|
|
||||||
description: Invalid ID supplied
|
|
||||||
'404':
|
|
||||||
description: Order not found
|
|
||||||
delete:
|
|
||||||
tags:
|
|
||||||
- store
|
|
||||||
summary: Delete purchase order by ID
|
|
||||||
description: For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors
|
|
||||||
operationId: deleteOrder
|
|
||||||
parameters:
|
|
||||||
- name: orderId
|
|
||||||
in: path
|
|
||||||
description: ID of the order that needs to be deleted
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
responses:
|
|
||||||
'400':
|
|
||||||
description: Invalid ID supplied
|
|
||||||
'404':
|
|
||||||
description: Order not found
|
|
||||||
/user:
|
|
||||||
post:
|
|
||||||
tags:
|
|
||||||
- user
|
|
||||||
summary: Create user
|
|
||||||
description: This can only be done by the logged in user.
|
|
||||||
operationId: createUser
|
|
||||||
requestBody:
|
|
||||||
description: Created user object
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/User'
|
|
||||||
application/xml:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/User'
|
|
||||||
application/x-www-form-urlencoded:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/User'
|
|
||||||
responses:
|
|
||||||
default:
|
|
||||||
description: successful operation
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/User'
|
|
||||||
application/xml:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/User'
|
|
||||||
/user/createWithList:
|
|
||||||
post:
|
|
||||||
tags:
|
|
||||||
- user
|
|
||||||
summary: Creates list of users with given input array
|
|
||||||
description: Creates list of users with given input array
|
|
||||||
operationId: createUsersWithListInput
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/User'
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Successful operation
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/User'
|
|
||||||
application/xml:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/User'
|
|
||||||
default:
|
|
||||||
description: successful operation
|
|
||||||
/user/login:
|
|
||||||
get:
|
|
||||||
tags:
|
|
||||||
- user
|
|
||||||
summary: Logs user into the system
|
|
||||||
description: ''
|
|
||||||
operationId: loginUser
|
|
||||||
parameters:
|
|
||||||
- name: username
|
|
||||||
in: query
|
|
||||||
description: The user name for login
|
|
||||||
required: false
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
- name: password
|
|
||||||
in: query
|
|
||||||
description: The password for login in clear text
|
|
||||||
required: false
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: successful operation
|
|
||||||
headers:
|
|
||||||
X-Rate-Limit:
|
|
||||||
description: calls per hour allowed by the user
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
X-Expires-After:
|
|
||||||
description: date in UTC when token expires
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
content:
|
|
||||||
application/xml:
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
'400':
|
|
||||||
description: Invalid username/password supplied
|
|
||||||
/user/logout:
|
|
||||||
get:
|
|
||||||
tags:
|
|
||||||
- user
|
|
||||||
summary: Logs out current logged in user session
|
|
||||||
description: ''
|
|
||||||
operationId: logoutUser
|
|
||||||
parameters: []
|
|
||||||
responses:
|
|
||||||
default:
|
|
||||||
description: successful operation
|
|
||||||
/user/{username}:
|
|
||||||
get:
|
|
||||||
tags:
|
|
||||||
- user
|
|
||||||
summary: Get user by user name
|
|
||||||
description: ''
|
|
||||||
operationId: getUserByName
|
|
||||||
parameters:
|
|
||||||
- name: username
|
|
||||||
in: path
|
|
||||||
description: 'The name that needs to be fetched. Use user1 for testing. '
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: successful operation
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/User'
|
|
||||||
application/xml:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/User'
|
|
||||||
'400':
|
|
||||||
description: Invalid username supplied
|
|
||||||
'404':
|
|
||||||
description: User not found
|
|
||||||
put:
|
|
||||||
tags:
|
|
||||||
- user
|
|
||||||
summary: Update user
|
|
||||||
description: This can only be done by the logged in user.
|
|
||||||
operationId: updateUser
|
|
||||||
parameters:
|
|
||||||
- name: username
|
|
||||||
in: path
|
|
||||||
description: name that need to be deleted
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
requestBody:
|
|
||||||
description: Update an existent user in the store
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/User'
|
|
||||||
application/xml:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/User'
|
|
||||||
application/x-www-form-urlencoded:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/User'
|
|
||||||
responses:
|
|
||||||
default:
|
|
||||||
description: successful operation
|
|
||||||
delete:
|
|
||||||
tags:
|
|
||||||
- user
|
|
||||||
summary: Delete user
|
|
||||||
description: This can only be done by the logged in user.
|
|
||||||
operationId: deleteUser
|
|
||||||
parameters:
|
|
||||||
- name: username
|
|
||||||
in: path
|
|
||||||
description: The name that needs to be deleted
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
responses:
|
|
||||||
'400':
|
|
||||||
description: Invalid username supplied
|
|
||||||
'404':
|
|
||||||
description: User not found
|
|
||||||
components:
|
|
||||||
schemas:
|
|
||||||
Order:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
examples: [10]
|
|
||||||
petId:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
examples: [198772]
|
|
||||||
quantity:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
examples: [7]
|
|
||||||
shipDate:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
status:
|
|
||||||
type: string
|
|
||||||
description: Order Status
|
|
||||||
examples: [approved]
|
|
||||||
enum:
|
|
||||||
- placed
|
|
||||||
- approved
|
|
||||||
- delivered
|
|
||||||
complete:
|
|
||||||
type: boolean
|
|
||||||
xml:
|
|
||||||
name: order
|
|
||||||
Customer:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
examples: [100000]
|
|
||||||
username:
|
|
||||||
type: string
|
|
||||||
examples: [fehguy]
|
|
||||||
address:
|
|
||||||
type: array
|
|
||||||
xml:
|
|
||||||
name: addresses
|
|
||||||
wrapped: true
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/Address'
|
|
||||||
xml:
|
|
||||||
name: customer
|
|
||||||
Address:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
street:
|
|
||||||
type: string
|
|
||||||
examples: [437 Lytton]
|
|
||||||
city:
|
|
||||||
type: string
|
|
||||||
examples: [Palo Alto]
|
|
||||||
state:
|
|
||||||
type: string
|
|
||||||
examples: [CA]
|
|
||||||
zip:
|
|
||||||
type: string
|
|
||||||
examples: ['94301']
|
|
||||||
xml:
|
|
||||||
name: address
|
|
||||||
Category:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
examples: [1]
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
examples: [Dogs]
|
|
||||||
xml:
|
|
||||||
name: category
|
|
||||||
User:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
examples: [10]
|
|
||||||
username:
|
|
||||||
type: string
|
|
||||||
examples: [theUser]
|
|
||||||
firstName:
|
|
||||||
type: string
|
|
||||||
examples: [John]
|
|
||||||
lastName:
|
|
||||||
type: string
|
|
||||||
examples: [James]
|
|
||||||
email:
|
|
||||||
type: string
|
|
||||||
examples: [john@email.com]
|
|
||||||
password:
|
|
||||||
type: string
|
|
||||||
examples: ['12345']
|
|
||||||
phone:
|
|
||||||
type: string
|
|
||||||
examples: ['12345']
|
|
||||||
userStatus:
|
|
||||||
type: integer
|
|
||||||
description: User Status
|
|
||||||
format: int32
|
|
||||||
examples: [1]
|
|
||||||
xml:
|
|
||||||
name: user
|
|
||||||
Tag:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
xml:
|
|
||||||
name: tag
|
|
||||||
Pet:
|
|
||||||
required:
|
|
||||||
- name
|
|
||||||
- photoUrls
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
examples: [10]
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
examples: [doggie]
|
|
||||||
category:
|
|
||||||
$ref: '#/components/schemas/Category'
|
|
||||||
photoUrls:
|
|
||||||
type: array
|
|
||||||
xml:
|
|
||||||
wrapped: true
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
xml:
|
|
||||||
name: photoUrl
|
|
||||||
tags:
|
|
||||||
type: array
|
|
||||||
xml:
|
|
||||||
wrapped: true
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/Tag'
|
|
||||||
status:
|
|
||||||
type: string
|
|
||||||
description: pet status in the store
|
|
||||||
enum:
|
|
||||||
- available
|
|
||||||
- pending
|
|
||||||
- sold
|
|
||||||
xml:
|
|
||||||
name: pet
|
|
||||||
ApiResponse:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
code:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
type:
|
|
||||||
type: string
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
xml:
|
|
||||||
name: '##default'
|
|
||||||
requestBodies:
|
|
||||||
Pet:
|
|
||||||
description: Pet object that needs to be added to the store
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Pet'
|
|
||||||
application/xml:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Pet'
|
|
||||||
UserArray:
|
|
||||||
description: List of user object
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/User'
|
|
||||||
securitySchemes:
|
|
||||||
petstore_auth:
|
|
||||||
type: oauth2
|
|
||||||
flows:
|
|
||||||
implicit:
|
|
||||||
authorizationUrl: https://petstore3.swagger.io/oauth/authorize
|
|
||||||
scopes:
|
|
||||||
write:pets: modify pets in your account
|
|
||||||
read:pets: read your pets
|
|
||||||
api_key:
|
|
||||||
type: apiKey
|
|
||||||
name: api_key
|
|
||||||
in: header
|
|
@ -21,185 +21,795 @@
|
|||||||
# SOFTWARE.
|
# SOFTWARE.
|
||||||
|
|
||||||
swagger: "2.0"
|
swagger: "2.0"
|
||||||
openapi: 3.1.0
|
openapi: "3.1.0"
|
||||||
info:
|
info:
|
||||||
title: Howmuch Expense-sharing app
|
title: Howmuch expense-sharing
|
||||||
description: |-
|
description: |-
|
||||||
A simple web application that can help you share your expense with your
|
A simple web application that can help you share your expense with your
|
||||||
friends.
|
friends.
|
||||||
|
|
||||||
contact:
|
contact:
|
||||||
email: vinchent@vinchent.xyz
|
email: vinchent@vinchent.vinchent
|
||||||
license:
|
license:
|
||||||
name: MIT
|
name: MIT
|
||||||
url: https://opensource.org/license/MIT
|
url: https://opensource.org/license/MIT
|
||||||
version: '0.0.1'
|
version: 0.0.1
|
||||||
|
# externalDocs:
|
||||||
|
# description: Find out more about Swagger
|
||||||
|
# url: http://swagger.io
|
||||||
servers:
|
servers:
|
||||||
- url: https:/localhost:8000/v1
|
- url: http://localhost:8080/v1
|
||||||
tags:
|
tags:
|
||||||
|
- name: event
|
||||||
|
description: Event with your friends
|
||||||
|
- name: expense
|
||||||
|
description: Who paid how much for whom else
|
||||||
- name: user
|
- name: user
|
||||||
- name: session
|
description: Operations about user
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
/user/create:
|
/event:
|
||||||
|
put:
|
||||||
|
tags:
|
||||||
|
- event
|
||||||
|
summary: Update an existing pet
|
||||||
|
description: Update an existing pet by Id
|
||||||
|
operationId: updatePet
|
||||||
|
requestBody:
|
||||||
|
description: Update an existent pet in the store
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Pet'
|
||||||
|
application/xml:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Pet'
|
||||||
|
application/x-www-form-urlencoded:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Pet'
|
||||||
|
required: true
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Pet'
|
||||||
|
application/xml:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Pet'
|
||||||
|
'400':
|
||||||
|
description: Invalid ID supplied
|
||||||
|
'404':
|
||||||
|
description: Pet not found
|
||||||
|
'422':
|
||||||
|
description: Validation exception
|
||||||
|
security:
|
||||||
|
- petstore_auth:
|
||||||
|
- write:pets
|
||||||
|
- read:pets
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- event
|
||||||
|
summary: Add a new pet to the store
|
||||||
|
description: Add a new pet to the store
|
||||||
|
operationId: addPet
|
||||||
|
requestBody:
|
||||||
|
description: Create a new pet in the store
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Pet'
|
||||||
|
application/xml:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Pet'
|
||||||
|
application/x-www-form-urlencoded:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Pet'
|
||||||
|
required: true
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Pet'
|
||||||
|
application/xml:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Pet'
|
||||||
|
'400':
|
||||||
|
description: Invalid input
|
||||||
|
'422':
|
||||||
|
description: Validation exception
|
||||||
|
security:
|
||||||
|
- petstore_auth:
|
||||||
|
- write:pets
|
||||||
|
- read:pets
|
||||||
|
/event/findByStatus:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- event
|
||||||
|
summary: Finds Pets by status
|
||||||
|
description: Multiple status values can be provided with comma separated strings
|
||||||
|
operationId: findPetsByStatus
|
||||||
|
parameters:
|
||||||
|
- name: status
|
||||||
|
in: query
|
||||||
|
description: Status values that need to be considered for filter
|
||||||
|
required: false
|
||||||
|
explode: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
default: available
|
||||||
|
enum:
|
||||||
|
- available
|
||||||
|
- pending
|
||||||
|
- sold
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Pet'
|
||||||
|
application/xml:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Pet'
|
||||||
|
'400':
|
||||||
|
description: Invalid status value
|
||||||
|
security:
|
||||||
|
- petstore_auth:
|
||||||
|
- write:pets
|
||||||
|
- read:pets
|
||||||
|
/event/findByTags:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- event
|
||||||
|
summary: Finds Pets by tags
|
||||||
|
description: Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.
|
||||||
|
operationId: findPetsByTags
|
||||||
|
parameters:
|
||||||
|
- name: tags
|
||||||
|
in: query
|
||||||
|
description: Tags to filter by
|
||||||
|
required: false
|
||||||
|
explode: true
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Pet'
|
||||||
|
application/xml:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Pet'
|
||||||
|
'400':
|
||||||
|
description: Invalid tag value
|
||||||
|
security:
|
||||||
|
- petstore_auth:
|
||||||
|
- write:pets
|
||||||
|
- read:pets
|
||||||
|
/event/{eventId}:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- event
|
||||||
|
summary: Find pet by ID
|
||||||
|
description: Returns a single pet
|
||||||
|
operationId: getPetById
|
||||||
|
parameters:
|
||||||
|
- name: petId
|
||||||
|
in: path
|
||||||
|
description: ID of pet to return
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Pet'
|
||||||
|
application/xml:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Pet'
|
||||||
|
'400':
|
||||||
|
description: Invalid ID supplied
|
||||||
|
'404':
|
||||||
|
description: Pet not found
|
||||||
|
security:
|
||||||
|
- api_key: []
|
||||||
|
- petstore_auth:
|
||||||
|
- write:pets
|
||||||
|
- read:pets
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- event
|
||||||
|
summary: Updates a pet in the store with form data
|
||||||
|
description: ''
|
||||||
|
operationId: updatePetWithForm
|
||||||
|
parameters:
|
||||||
|
- name: petId
|
||||||
|
in: path
|
||||||
|
description: ID of pet that needs to be updated
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
- name: name
|
||||||
|
in: query
|
||||||
|
description: Name of pet that needs to be updated
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: status
|
||||||
|
in: query
|
||||||
|
description: Status of pet that needs to be updated
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'400':
|
||||||
|
description: Invalid input
|
||||||
|
security:
|
||||||
|
- petstore_auth:
|
||||||
|
- write:pets
|
||||||
|
- read:pets
|
||||||
|
delete:
|
||||||
|
tags:
|
||||||
|
- event
|
||||||
|
summary: Deletes a pet
|
||||||
|
description: delete a pet
|
||||||
|
operationId: deletePet
|
||||||
|
parameters:
|
||||||
|
- name: api_key
|
||||||
|
in: header
|
||||||
|
description: ''
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: petId
|
||||||
|
in: path
|
||||||
|
description: Pet id to delete
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
responses:
|
||||||
|
'400':
|
||||||
|
description: Invalid pet value
|
||||||
|
security:
|
||||||
|
- petstore_auth:
|
||||||
|
- write:pets
|
||||||
|
- read:pets
|
||||||
|
/event/{eventId}/uploadImage:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- event
|
||||||
|
summary: uploads an image
|
||||||
|
description: ''
|
||||||
|
operationId: uploadFile
|
||||||
|
parameters:
|
||||||
|
- name: petId
|
||||||
|
in: path
|
||||||
|
description: ID of pet to update
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
- name: additionalMetadata
|
||||||
|
in: query
|
||||||
|
description: Additional Metadata
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/octet-stream:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ApiResponse'
|
||||||
|
security:
|
||||||
|
- petstore_auth:
|
||||||
|
- write:pets
|
||||||
|
- read:pets
|
||||||
|
/expense/inventory:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- expense
|
||||||
|
summary: Returns pet inventories by status
|
||||||
|
description: Returns a map of status codes to quantities
|
||||||
|
operationId: getInventory
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
security:
|
||||||
|
- api_key: []
|
||||||
|
/expense/order:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- expense
|
||||||
|
summary: Place an order for a pet
|
||||||
|
description: Place a new order in the store
|
||||||
|
operationId: placeOrder
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Order'
|
||||||
|
application/xml:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Order'
|
||||||
|
application/x-www-form-urlencoded:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Order'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Order'
|
||||||
|
'400':
|
||||||
|
description: Invalid input
|
||||||
|
'422':
|
||||||
|
description: Validation exception
|
||||||
|
/expense/order/{orderId}:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- expense
|
||||||
|
summary: Find purchase order by ID
|
||||||
|
description: For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.
|
||||||
|
operationId: getOrderById
|
||||||
|
parameters:
|
||||||
|
- name: orderId
|
||||||
|
in: path
|
||||||
|
description: ID of order that needs to be fetched
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Order'
|
||||||
|
application/xml:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Order'
|
||||||
|
'400':
|
||||||
|
description: Invalid ID supplied
|
||||||
|
'404':
|
||||||
|
description: Order not found
|
||||||
|
delete:
|
||||||
|
tags:
|
||||||
|
- expense
|
||||||
|
summary: Delete purchase order by ID
|
||||||
|
description: For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors
|
||||||
|
operationId: deleteOrder
|
||||||
|
parameters:
|
||||||
|
- name: orderId
|
||||||
|
in: path
|
||||||
|
description: ID of the order that needs to be deleted
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
responses:
|
||||||
|
'400':
|
||||||
|
description: Invalid ID supplied
|
||||||
|
'404':
|
||||||
|
description: Order not found
|
||||||
|
/user:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
- user
|
- user
|
||||||
description: Create a new user
|
summary: Create user
|
||||||
|
description: This can only be done by the logged in user.
|
||||||
|
operationId: createUser
|
||||||
requestBody:
|
requestBody:
|
||||||
description: Create a new user
|
description: Created user object
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/UserCreateRequest'
|
$ref: '#/components/schemas/User'
|
||||||
|
application/xml:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/User'
|
||||||
|
application/x-www-form-urlencoded:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/User'
|
||||||
responses:
|
responses:
|
||||||
'200':
|
default:
|
||||||
description: Successful operation
|
description: successful operation
|
||||||
'400':
|
|
||||||
description: Client side error
|
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
$ref: '#/components/schemas/User'
|
||||||
properties:
|
application/xml:
|
||||||
code:
|
|
||||||
type: string
|
|
||||||
example: FailedOperation.UserExisted
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
example: "Email already existed."
|
|
||||||
'500':
|
|
||||||
description: Server side error
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ErrResponse'
|
$ref: '#/components/schemas/User'
|
||||||
/session/create:
|
/user/createWithList:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
- session
|
- user
|
||||||
description: Create a new session for a user
|
summary: Creates list of users with given input array
|
||||||
|
description: Creates list of users with given input array
|
||||||
|
operationId: createUsersWithListInput
|
||||||
requestBody:
|
requestBody:
|
||||||
description: Create session
|
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/SessionCreateRequest'
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/User'
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Successful operation
|
description: Successful operation
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
$ref: '#/components/schemas/User'
|
||||||
properties:
|
application/xml:
|
||||||
token:
|
|
||||||
type: string
|
|
||||||
example: fakjshdflauhkjhsometokenakjsdhfaksj
|
|
||||||
'400':
|
|
||||||
description: Client side error
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
schema:
|
||||||
type: object
|
$ref: '#/components/schemas/User'
|
||||||
properties:
|
default:
|
||||||
code:
|
description: successful operation
|
||||||
type: string
|
/user/login:
|
||||||
example: AuthFailure
|
get:
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
example: "wrong email password."
|
|
||||||
'500':
|
|
||||||
description: Server side error
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/ErrResponse'
|
|
||||||
/session/delete:
|
|
||||||
post:
|
|
||||||
tags:
|
tags:
|
||||||
- session
|
- user
|
||||||
description: Delete an existing session for a user
|
summary: Logs user into the system
|
||||||
|
description: ''
|
||||||
|
operationId: loginUser
|
||||||
|
parameters:
|
||||||
|
- name: username
|
||||||
|
in: query
|
||||||
|
description: The user name for login
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: password
|
||||||
|
in: query
|
||||||
|
description: The password for login in clear text
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Successful operation
|
description: successful operation
|
||||||
headers:
|
headers:
|
||||||
|
X-Rate-Limit:
|
||||||
|
description: calls per hour allowed by the user
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
X-Expires-After:
|
X-Expires-After:
|
||||||
description: date in UTC when token expires
|
description: date in UTC when token expires
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
content:
|
content:
|
||||||
|
application/xml:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
'400':
|
'400':
|
||||||
description: Client side error
|
description: Invalid username/password supplied
|
||||||
|
/user/logout:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- user
|
||||||
|
summary: Logs out current logged in user session
|
||||||
|
description: ''
|
||||||
|
operationId: logoutUser
|
||||||
|
parameters: []
|
||||||
|
responses:
|
||||||
|
default:
|
||||||
|
description: successful operation
|
||||||
|
/user/{username}:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- user
|
||||||
|
summary: Get user by user name
|
||||||
|
description: ''
|
||||||
|
operationId: getUserByName
|
||||||
|
parameters:
|
||||||
|
- name: username
|
||||||
|
in: path
|
||||||
|
description: 'The name that needs to be fetched. Use user1 for testing. '
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
$ref: '#/components/schemas/User'
|
||||||
properties:
|
application/xml:
|
||||||
code:
|
|
||||||
type: string
|
|
||||||
example: AuthFailure
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
example: "user not logged in."
|
|
||||||
'500':
|
|
||||||
description: Server side error
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ErrResponse'
|
$ref: '#/components/schemas/User'
|
||||||
security:
|
'400':
|
||||||
- jwt: []
|
description: Invalid username supplied
|
||||||
|
'404':
|
||||||
|
description: User not found
|
||||||
|
put:
|
||||||
|
tags:
|
||||||
|
- user
|
||||||
|
summary: Update user
|
||||||
|
description: This can only be done by the logged in user.
|
||||||
|
operationId: updateUser
|
||||||
|
parameters:
|
||||||
|
- name: username
|
||||||
|
in: path
|
||||||
|
description: name that need to be deleted
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
requestBody:
|
||||||
|
description: Update an existent user in the store
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/User'
|
||||||
|
application/xml:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/User'
|
||||||
|
application/x-www-form-urlencoded:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/User'
|
||||||
|
responses:
|
||||||
|
default:
|
||||||
|
description: successful operation
|
||||||
|
delete:
|
||||||
|
tags:
|
||||||
|
- user
|
||||||
|
summary: Delete user
|
||||||
|
description: This can only be done by the logged in user.
|
||||||
|
operationId: deleteUser
|
||||||
|
parameters:
|
||||||
|
- name: username
|
||||||
|
in: path
|
||||||
|
description: The name that needs to be deleted
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'400':
|
||||||
|
description: Invalid username supplied
|
||||||
|
'404':
|
||||||
|
description: User not found
|
||||||
components:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
UserCreateRequest:
|
Order:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
email:
|
id:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
examples: [10]
|
||||||
|
petId:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
examples: [198772]
|
||||||
|
quantity:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
examples: [7]
|
||||||
|
shipDate:
|
||||||
type: string
|
type: string
|
||||||
example: bruce@wayne.com
|
format: date-time
|
||||||
first_name:
|
status:
|
||||||
type: string
|
type: string
|
||||||
example: Bruce
|
description: Order Status
|
||||||
last_name:
|
examples: [approved]
|
||||||
type: string
|
enum:
|
||||||
example: Wayne
|
- placed
|
||||||
password:
|
- approved
|
||||||
type: string
|
- delivered
|
||||||
example: verystrongpassword
|
complete:
|
||||||
required:
|
type: boolean
|
||||||
- email
|
xml:
|
||||||
- fist_name
|
name: order
|
||||||
- last_name
|
Customer:
|
||||||
- password
|
|
||||||
SessionCreateRequest:
|
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
examples: [100000]
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
examples: [fehguy]
|
||||||
|
address:
|
||||||
|
type: array
|
||||||
|
xml:
|
||||||
|
name: addresses
|
||||||
|
wrapped: true
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Address'
|
||||||
|
xml:
|
||||||
|
name: customer
|
||||||
|
Address:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
street:
|
||||||
|
type: string
|
||||||
|
examples: [437 Lytton]
|
||||||
|
city:
|
||||||
|
type: string
|
||||||
|
examples: [Palo Alto]
|
||||||
|
state:
|
||||||
|
type: string
|
||||||
|
examples: [CA]
|
||||||
|
zip:
|
||||||
|
type: string
|
||||||
|
examples: ['94301']
|
||||||
|
xml:
|
||||||
|
name: address
|
||||||
|
Category:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
examples: [1]
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
examples: [Dogs]
|
||||||
|
xml:
|
||||||
|
name: category
|
||||||
|
User:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
examples: [10]
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
examples: [theUser]
|
||||||
|
firstName:
|
||||||
|
type: string
|
||||||
|
examples: [John]
|
||||||
|
lastName:
|
||||||
|
type: string
|
||||||
|
examples: [James]
|
||||||
email:
|
email:
|
||||||
type: string
|
type: string
|
||||||
example: bruce@wayne.com
|
examples: [john@email.com]
|
||||||
password:
|
password:
|
||||||
type: string
|
type: string
|
||||||
example: verystrongpassword
|
examples: ['12345']
|
||||||
|
phone:
|
||||||
|
type: string
|
||||||
|
examples: ['12345']
|
||||||
|
userStatus:
|
||||||
|
type: integer
|
||||||
|
description: User Status
|
||||||
|
format: int32
|
||||||
|
examples: [1]
|
||||||
|
xml:
|
||||||
|
name: user
|
||||||
|
Tag:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
xml:
|
||||||
|
name: tag
|
||||||
|
Pet:
|
||||||
required:
|
required:
|
||||||
- email
|
- name
|
||||||
- password
|
- photoUrls
|
||||||
ErrResponse:
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
examples: [10]
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
examples: [doggie]
|
||||||
|
category:
|
||||||
|
$ref: '#/components/schemas/Category'
|
||||||
|
photoUrls:
|
||||||
|
type: array
|
||||||
|
xml:
|
||||||
|
wrapped: true
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
xml:
|
||||||
|
name: photoUrl
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
xml:
|
||||||
|
wrapped: true
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Tag'
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
description: pet status in the store
|
||||||
|
enum:
|
||||||
|
- available
|
||||||
|
- pending
|
||||||
|
- sold
|
||||||
|
xml:
|
||||||
|
name: pet
|
||||||
|
ApiResponse:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
code:
|
code:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
type:
|
||||||
type: string
|
type: string
|
||||||
example: InternalError
|
|
||||||
message:
|
message:
|
||||||
type: string
|
type: string
|
||||||
example: "Server internal error."
|
xml:
|
||||||
|
name: '##default'
|
||||||
|
requestBodies:
|
||||||
|
Pet:
|
||||||
|
description: Pet object that needs to be added to the store
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Pet'
|
||||||
|
application/xml:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Pet'
|
||||||
|
UserArray:
|
||||||
|
description: List of user object
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/User'
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
jwt:
|
petstore_auth:
|
||||||
name: Bearer authentication
|
type: oauth2
|
||||||
type: http
|
flows:
|
||||||
bearerFormat: "JWT"
|
implicit:
|
||||||
scheme: bearer
|
authorizationUrl: https://petstore3.swagger.io/oauth/authorize
|
||||||
|
scopes:
|
||||||
|
write:pets: modify pets in your account
|
||||||
|
read:pets: read your pets
|
||||||
|
api_key:
|
||||||
|
type: apiKey
|
||||||
|
name: api_key
|
||||||
|
in: header
|
||||||
|
@ -28,7 +28,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch"
|
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch"
|
||||||
// _ "go.uber.org/automaxprocs"
|
_ "go.uber.org/automaxprocs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -1,35 +1,37 @@
|
|||||||
dev-mode: true
|
# MIT License
|
||||||
|
#
|
||||||
web:
|
# Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
||||||
addr: :8000
|
#
|
||||||
shutdown-timeout: 10
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
token-secret: nzMC12IJBMiiV2AAktTFpZP4BbGAf09lFPV_sATKcwI
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
token-expiry-time: 24h
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
|
||||||
db:
|
db:
|
||||||
# DB host
|
# DB host
|
||||||
host: 127.0.0.1
|
host: 127.0.0.1
|
||||||
# DB username
|
# DB username
|
||||||
username: postgres
|
username: howmuch
|
||||||
# DB password
|
# DB password
|
||||||
password: example
|
password: howmuch
|
||||||
# DB name
|
# DB name
|
||||||
database: howmuch
|
database: howmuch
|
||||||
|
# max idle connections
|
||||||
max-open-conns: 100
|
max-idle-connections: 100
|
||||||
max-idle-conns: 100
|
# max open connections
|
||||||
max-lifetime: 10s
|
max-open-connections: 100
|
||||||
|
# max connection life time
|
||||||
cache:
|
max-connection-life-time: 10s
|
||||||
host: 127.0.0.1:6379
|
|
||||||
password: ""
|
|
||||||
|
|
||||||
log:
|
|
||||||
level: debug
|
|
||||||
disalbe-caller: false
|
|
||||||
disable-stacktrace: false
|
|
||||||
# console or json
|
|
||||||
format: console
|
|
||||||
output-paths:
|
|
||||||
- stdout
|
|
||||||
- /tmp/howmuch.log
|
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
development:
|
|
||||||
dialect: postgres
|
|
||||||
database: howmuch
|
|
||||||
user: postgres
|
|
||||||
password: example
|
|
||||||
host: 127.0.0.1
|
|
||||||
pool: 5
|
|
@ -1,34 +0,0 @@
|
|||||||
services:
|
|
||||||
|
|
||||||
postgres:
|
|
||||||
image: postgres:alpine
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
deploy:
|
|
||||||
mode: replicated
|
|
||||||
replicas: 1
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: postgres
|
|
||||||
POSTGRES_PASSWORD: example
|
|
||||||
POSTGRES_DB: howmuch
|
|
||||||
volumes:
|
|
||||||
- ../../db_data_howmuch/postgres/:/var/lib/postgresql/data
|
|
||||||
|
|
||||||
adminer:
|
|
||||||
image: adminer
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- 8080:8080
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:alpine
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
deploy:
|
|
||||||
mode: replicated
|
|
||||||
replicas: 1
|
|
||||||
command: redis-server --save 20 1 --loglevel warning
|
|
||||||
volumes:
|
|
||||||
- ../../db_data_howmuch/redis/:/data
|
|
@ -1,7 +0,0 @@
|
|||||||
# Platform level error code design
|
|
||||||
|
|
||||||
- InternalError
|
|
||||||
- InvalidParameter
|
|
||||||
- AuthFailure
|
|
||||||
- ResourceNotFound
|
|
||||||
- FailedOperation
|
|
Binary file not shown.
Before Width: | Height: | Size: 358 KiB |
Binary file not shown.
Before Width: | Height: | Size: 116 KiB |
40
go.mod
40
go.mod
@ -3,72 +3,52 @@ module git.vinchent.xyz/vinchent/howmuch
|
|||||||
go 1.23.1
|
go 1.23.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/fsnotify/fsnotify v1.7.0
|
|
||||||
github.com/gin-contrib/cors v1.7.2
|
|
||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gin-gonic/gin v1.10.0
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
|
||||||
github.com/google/uuid v1.6.0
|
|
||||||
github.com/gosuri/uitable v0.0.4
|
|
||||||
github.com/jackc/pgx/v5 v5.7.1
|
|
||||||
github.com/pkg/errors v0.9.1
|
|
||||||
github.com/redis/go-redis/v9 v9.6.1
|
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
github.com/spf13/pflag v1.0.5
|
|
||||||
github.com/spf13/viper v1.19.0
|
github.com/spf13/viper v1.19.0
|
||||||
github.com/stretchr/testify v1.9.0
|
go.uber.org/automaxprocs v1.6.0
|
||||||
go.uber.org/zap v1.27.0
|
|
||||||
golang.org/x/crypto v0.27.0
|
|
||||||
golang.org/x/net v0.26.0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bytedance/sonic v1.11.6 // indirect
|
github.com/bytedance/sonic v1.11.6 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
|
||||||
github.com/fatih/color v1.14.1 // indirect
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.22.1 // indirect
|
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/magiconair/properties v1.8.7 // indirect
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
|
||||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
github.com/spf13/afero v1.11.0 // indirect
|
github.com/spf13/afero v1.11.0 // indirect
|
||||||
github.com/spf13/cast v1.6.0 // indirect
|
github.com/spf13/cast v1.6.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
|
go.uber.org/multierr v1.9.0 // indirect
|
||||||
golang.org/x/arch v0.8.0 // indirect
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
|
golang.org/x/crypto v0.23.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||||
golang.org/x/sync v0.8.0 // indirect
|
golang.org/x/net v0.25.0 // indirect
|
||||||
golang.org/x/sys v0.25.0 // indirect
|
golang.org/x/sys v0.20.0 // indirect
|
||||||
golang.org/x/text v0.18.0 // indirect
|
golang.org/x/text v0.15.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.1 // indirect
|
google.golang.org/protobuf v1.34.1 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
82
go.sum
82
go.sum
@ -1,13 +1,7 @@
|
|||||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
|
||||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
|
||||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
|
||||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
|
||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
|
||||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||||
@ -17,18 +11,12 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
|
||||||
github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
|
|
||||||
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
|
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw=
|
|
||||||
github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
|
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
@ -39,31 +27,17 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
|||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
|
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||||
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY=
|
|
||||||
github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
|
|
||||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
|
||||||
github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
|
|
||||||
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
|
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
@ -78,13 +52,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
|||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
@ -94,15 +63,11 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
|||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4=
|
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||||
github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
|
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
@ -140,35 +105,32 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
|||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
|
||||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
@ -1,52 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package controller
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/core"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Admin interface {
|
|
||||||
CreateUser(core.Context)
|
|
||||||
GetUserById(core.Context)
|
|
||||||
UpdateUser(core.Context)
|
|
||||||
DeleteUser(core.Context)
|
|
||||||
ListUsers(core.Context)
|
|
||||||
}
|
|
||||||
|
|
||||||
type AdminController struct{}
|
|
||||||
|
|
||||||
func (ac *AdminController) CreateUser(core.Context) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ac *AdminController) GetUserById(core.Context) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ac *AdminController) UpdateUser(core.Context) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ac *AdminController) DeleteUser(core.Context) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ac *AdminController) ListUsers(core.Context) {
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package controller
|
|
||||||
|
|
||||||
// AppController is the controller structure that holds service controllers.
|
|
||||||
type AppController struct {
|
|
||||||
// User must implement User interface
|
|
||||||
User interface{ User }
|
|
||||||
|
|
||||||
Admin interface{ Admin }
|
|
||||||
|
|
||||||
Session interface{ Session }
|
|
||||||
}
|
|
@ -1,115 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package controller
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/usecase"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/core"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/errno"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/log"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/middleware/authn"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/token"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Session interface {
|
|
||||||
Create(*gin.Context)
|
|
||||||
Delete(*gin.Context)
|
|
||||||
}
|
|
||||||
|
|
||||||
type SessionController struct {
|
|
||||||
userUsecase usecase.User
|
|
||||||
cache core.Cache
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSessionController(u usecase.User, cache core.Cache) Session {
|
|
||||||
return &SessionController{u, cache}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Token struct {
|
|
||||||
Token string `json:"token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type createParams struct {
|
|
||||||
Email string `json:"email" binding:"required,email"`
|
|
||||||
Password string `json:"password" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create creates a session for a user and returns a token
|
|
||||||
//
|
|
||||||
// Since we use JWT method, this token is not stored anywhere. Thus it
|
|
||||||
// stops at the controller level.
|
|
||||||
func (sc *SessionController) Create(ctx *gin.Context) {
|
|
||||||
var user model.UserExistRequest
|
|
||||||
|
|
||||||
if err := ctx.Bind(&user); err != nil {
|
|
||||||
log.ErrorLog("param error", "err", err)
|
|
||||||
core.WriteResponse(ctx, UserParamsErr, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := sc.userUsecase.Exist(ctx, &user)
|
|
||||||
if err != nil {
|
|
||||||
core.WriteResponse(ctx, err, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// user exists. Generate the token for the user
|
|
||||||
tokenString, err := token.Sign(user.Email)
|
|
||||||
if err != nil {
|
|
||||||
core.WriteResponse(ctx, errno.InternalServerErr, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
core.WriteResponse(ctx, nil, Token{
|
|
||||||
Token: tokenString,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete deletes a session by putting the jwt token into the cache
|
|
||||||
func (sc *SessionController) Delete(ctx *gin.Context) {
|
|
||||||
tk, err := token.ParseRequest(ctx)
|
|
||||||
if err != nil || tk == nil {
|
|
||||||
// Unlikely
|
|
||||||
core.WriteResponse(ctx, authn.ErrTokenInvalid, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
exp := time.Until(tk.Expiry)
|
|
||||||
key := fmt.Sprintf("jwt:%s", tk.Identity)
|
|
||||||
|
|
||||||
log.DebugLog("session delete", "key", key, "exp", exp.String())
|
|
||||||
err = sc.cache.Set(ctx, key, tk.Raw, exp)
|
|
||||||
if err != nil {
|
|
||||||
// unexpected
|
|
||||||
log.ErrorLog("error writing logged out jwt into cache", "err", err)
|
|
||||||
core.WriteResponse(ctx, errno.InternalServerErr, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
core.WriteResponse(ctx, nil, "logged out")
|
|
||||||
}
|
|
@ -1,190 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package controller
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/controller/usecasemock"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/usecase"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/errno"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/middleware/authn"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/test"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/token"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// {{{ Test Cache
|
|
||||||
|
|
||||||
type testCache struct {
|
|
||||||
kvMap map[string]interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *testCache) Get(ctx context.Context, key string) (string, error) {
|
|
||||||
val, ok := c.kvMap[key]
|
|
||||||
if ok {
|
|
||||||
return val.(string), nil
|
|
||||||
}
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *testCache) Set(
|
|
||||||
ctx context.Context,
|
|
||||||
key string,
|
|
||||||
value interface{},
|
|
||||||
expiration time.Duration,
|
|
||||||
) error {
|
|
||||||
c.kvMap[key] = value
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *testCache) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// }}}
|
|
||||||
|
|
||||||
func TestSessionCreate(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
Name string
|
|
||||||
User createParams
|
|
||||||
Errno *errno.Errno
|
|
||||||
}{
|
|
||||||
{"registered user", createParams{
|
|
||||||
Email: "correct@correct.com",
|
|
||||||
Password: "strong password",
|
|
||||||
}, errno.OK},
|
|
||||||
{"unregistered user", createParams{
|
|
||||||
Email: "unregistered@error.com",
|
|
||||||
Password: "strong password",
|
|
||||||
}, usecase.UserNotExist},
|
|
||||||
{"wrong email", createParams{
|
|
||||||
Email: "error.com",
|
|
||||||
Password: "strong password",
|
|
||||||
}, UserParamsErr},
|
|
||||||
{"no passwrd", createParams{
|
|
||||||
Email: "no@error.com",
|
|
||||||
Password: "",
|
|
||||||
}, UserParamsErr},
|
|
||||||
}
|
|
||||||
|
|
||||||
token.Init("secret", 1*time.Second)
|
|
||||||
|
|
||||||
for _, tst := range tests {
|
|
||||||
t.Run(tst.Name, func(t *testing.T) {
|
|
||||||
testUserUsecase := usecasemock.NewtestUserUsecase()
|
|
||||||
sessionController := NewSessionController(testUserUsecase, nil)
|
|
||||||
r := gin.New()
|
|
||||||
r.POST(
|
|
||||||
"/session/create",
|
|
||||||
func(ctx *gin.Context) { sessionController.Create(ctx) },
|
|
||||||
)
|
|
||||||
user, _ := json.Marshal(tst.User)
|
|
||||||
res := test.PerformRequest(t, r, "POST", "/session/create", bytes.NewReader(user),
|
|
||||||
test.Header{
|
|
||||||
Key: "content-type",
|
|
||||||
Value: "application/json",
|
|
||||||
})
|
|
||||||
|
|
||||||
assert.Equal(t, tst.Errno.HTTP, res.Result().StatusCode, res.Body)
|
|
||||||
|
|
||||||
if tst.Errno.HTTP != http.StatusOK {
|
|
||||||
var got errno.Errno
|
|
||||||
err := json.NewDecoder(res.Result().Body).Decode(&got)
|
|
||||||
// XXX: the http status is not in the json. So it must be reset back the the struct
|
|
||||||
got.HTTP = res.Result().StatusCode
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, tst.Errno, &got)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var got Token
|
|
||||||
err := json.NewDecoder(res.Result().Body).Decode(&got)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
tkResp, err := token.Parse(got.Token)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, tst.User.Email, tkResp.Identity)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSessionDelete(t *testing.T) {
|
|
||||||
testUserUsecase := usecasemock.NewtestUserUsecase()
|
|
||||||
kvMap := make(map[string]interface{}, 1)
|
|
||||||
tc := &testCache{kvMap: kvMap}
|
|
||||||
sessionController := NewSessionController(testUserUsecase, tc)
|
|
||||||
r := gin.New()
|
|
||||||
session := r.Group("/session")
|
|
||||||
{
|
|
||||||
session.POST("/create", func(ctx *gin.Context) { sessionController.Create(ctx) })
|
|
||||||
session.Use(authn.Authn(tc))
|
|
||||||
session.POST("/delete", func(ctx *gin.Context) { sessionController.Delete(ctx) })
|
|
||||||
}
|
|
||||||
|
|
||||||
params := createParams{
|
|
||||||
Email: "correct@correct.com",
|
|
||||||
Password: "strong password",
|
|
||||||
}
|
|
||||||
user, _ := json.Marshal(params)
|
|
||||||
res := test.PerformRequest(t, r, "POST", "/session/create", bytes.NewReader(user),
|
|
||||||
test.Header{
|
|
||||||
Key: "content-type",
|
|
||||||
Value: "application/json",
|
|
||||||
})
|
|
||||||
|
|
||||||
var tk Token
|
|
||||||
_ = json.NewDecoder(res.Result().Body).Decode(&tk)
|
|
||||||
tkResp, _ := token.Parse(tk.Token)
|
|
||||||
|
|
||||||
// Log out
|
|
||||||
res = test.PerformRequest(t, r, "POST", "/session/delete", nil,
|
|
||||||
test.Header{
|
|
||||||
Key: "Authorization",
|
|
||||||
Value: fmt.Sprintf("Bearer %s", tkResp.Raw),
|
|
||||||
})
|
|
||||||
|
|
||||||
var loggedOut string
|
|
||||||
err := json.NewDecoder(res.Result().Body).Decode(&loggedOut)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, "logged out", loggedOut)
|
|
||||||
|
|
||||||
// Try to access the handler with the old token
|
|
||||||
res = test.PerformRequest(t, r, "POST", "/session/delete", nil,
|
|
||||||
test.Header{
|
|
||||||
Key: "Authorization",
|
|
||||||
Value: fmt.Sprintf("Bearer %s", tkResp.Raw),
|
|
||||||
})
|
|
||||||
|
|
||||||
var unauth errno.Errno
|
|
||||||
err = json.NewDecoder(res.Result().Body).Decode(&unauth)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
unauth.HTTP = res.Result().StatusCode
|
|
||||||
assert.Equal(t, *authn.ErrLoggedOut, unauth)
|
|
||||||
}
|
|
@ -1,66 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package usecasemock
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/usecase"
|
|
||||||
)
|
|
||||||
|
|
||||||
type testUserUsecase struct{}
|
|
||||||
|
|
||||||
func NewtestUserUsecase() usecase.User {
|
|
||||||
return &testUserUsecase{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*testUserUsecase) Create(
|
|
||||||
ctx context.Context,
|
|
||||||
u *model.UserCreateRequest,
|
|
||||||
) (*model.UserInfoResponse, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*testUserUsecase) Exist(ctx context.Context, u *model.UserExistRequest) error {
|
|
||||||
switch u.Email {
|
|
||||||
case "a@b.c":
|
|
||||||
if u.Password == "strong password" {
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
return usecase.UserWrongPassword
|
|
||||||
}
|
|
||||||
case "unregistered@error.com":
|
|
||||||
return usecase.UserNotExist
|
|
||||||
}
|
|
||||||
// Should never reach here
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*testUserUsecase) GetUserBaseResponseByID(
|
|
||||||
ctx context.Context,
|
|
||||||
userID int,
|
|
||||||
) (*model.UserBaseResponse, error) {
|
|
||||||
// TODO:
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package controller
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/usecase"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/core"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/errno"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// User is the user controller interface, it describes all the handlers
|
|
||||||
// that need to be implemented for the /user endpoint
|
|
||||||
type User interface {
|
|
||||||
Create(core.Context)
|
|
||||||
UpdateInfo(*gin.Context)
|
|
||||||
ChangePassword(*gin.Context)
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserController struct {
|
|
||||||
userUsecase usecase.User
|
|
||||||
}
|
|
||||||
|
|
||||||
var UserParamsErr = &errno.Errno{
|
|
||||||
HTTP: http.StatusBadRequest,
|
|
||||||
Code: errno.ErrorCode(errno.InvalidParameterCode, "UserParamsErr"),
|
|
||||||
Message: "user info is not correct",
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUserController(us usecase.User) User {
|
|
||||||
return &UserController{
|
|
||||||
userUsecase: us,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (uc *UserController) Create(ctx core.Context) {
|
|
||||||
var userRequest model.UserCreateRequest
|
|
||||||
|
|
||||||
if err := ctx.Bind(&userRequest); err != nil {
|
|
||||||
core.WriteResponse(ctx, UserParamsErr, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := uc.userUsecase.Create(ctx, &userRequest)
|
|
||||||
if err != nil {
|
|
||||||
core.WriteResponse(ctx, err, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
core.WriteResponse(ctx, errno.OK, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (uc *UserController) UpdateInfo(ctx *gin.Context) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (uc *UserController) ChangePassword(ctx *gin.Context) {
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package repo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/repo/sqlc"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/repo"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
type dbRepository struct {
|
|
||||||
db *sql.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryTimeout = 3 * time.Second
|
|
||||||
|
|
||||||
func NewDBRepository(db *sql.DB) repo.DBRepository {
|
|
||||||
return &dbRepository{
|
|
||||||
db: db,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// XXX: Do I need rollback? in which cases?
|
|
||||||
|
|
||||||
func (dr *dbRepository) Transaction(
|
|
||||||
ctx context.Context,
|
|
||||||
txFunc func(txCtx context.Context, tx interface{}) (interface{}, error),
|
|
||||||
) (interface{}, error) {
|
|
||||||
tx, err := dr.db.BeginTx(ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if p := recover(); p != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
log.PanicLog("transaction panicked!")
|
|
||||||
} else if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
log.ErrorLog("transaction failed!", "err", err)
|
|
||||||
} else {
|
|
||||||
err = tx.Commit()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
data, err := txFunc(ctx, tx)
|
|
||||||
return data, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func getQueries(queries *sqlc.Queries, tx any) *sqlc.Queries {
|
|
||||||
transaction, ok := tx.(*sql.Tx)
|
|
||||||
if ok {
|
|
||||||
return sqlc.New(transaction)
|
|
||||||
}
|
|
||||||
return queries
|
|
||||||
}
|
|
@ -1,276 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package repo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/repo/sqlc"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/repo"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
type eventRepository struct {
|
|
||||||
queries *sqlc.Queries
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewEventRepository(db *sql.DB) repo.EventRepository {
|
|
||||||
return &eventRepository{
|
|
||||||
queries: sqlc.New(db),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create implements repo.EventRepository.
|
|
||||||
func (e *eventRepository) Create(
|
|
||||||
ctx context.Context,
|
|
||||||
evEntity *model.EventEntity,
|
|
||||||
tx any,
|
|
||||||
) (*model.EventEntity, error) {
|
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
queries := getQueries(e.queries, tx)
|
|
||||||
|
|
||||||
event, err := queries.InsertEvent(timeoutCtx, sqlc.InsertEventParams{
|
|
||||||
Name: evEntity.Name,
|
|
||||||
Description: sql.NullString{String: evEntity.Description, Valid: true},
|
|
||||||
TotalAmount: sql.NullInt32{Int32: int32(evEntity.TotalAmount), Valid: true},
|
|
||||||
DefaultCurrency: evEntity.DefaultCurrency,
|
|
||||||
OwnerID: int32(evEntity.OwnerID),
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &model.EventEntity{
|
|
||||||
ID: int(event.ID),
|
|
||||||
Name: event.Name,
|
|
||||||
Description: event.Description.String,
|
|
||||||
TotalAmount: int(event.TotalAmount.Int32),
|
|
||||||
DefaultCurrency: event.DefaultCurrency,
|
|
||||||
OwnerID: int(event.OwnerID),
|
|
||||||
CreatedAt: event.CreatedAt,
|
|
||||||
UpdatedAt: event.UpdatedAt,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func convToEventRetrieved(eventDTO *sqlc.GetEventByIDRow) (*model.EventRetrieved, error) {
|
|
||||||
// marshal owner and users
|
|
||||||
var owner model.UserBaseRetrieved
|
|
||||||
err := json.Unmarshal(eventDTO.Owner, &owner)
|
|
||||||
if err != nil {
|
|
||||||
// Unexpected
|
|
||||||
log.ErrorLog("json unmarshal error", "err", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var users []model.UserBaseRetrieved
|
|
||||||
err = json.Unmarshal(eventDTO.Users, &users)
|
|
||||||
if err != nil {
|
|
||||||
// Unexpected
|
|
||||||
log.ErrorLog("json unmarshal error", "err", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
eventRetrieved := &model.EventRetrieved{
|
|
||||||
ID: int(eventDTO.ID),
|
|
||||||
Name: eventDTO.Name,
|
|
||||||
Description: eventDTO.Description.String,
|
|
||||||
TotalAmount: model.MakeMoney(
|
|
||||||
int(eventDTO.TotalAmount.Int32),
|
|
||||||
model.Currency(eventDTO.DefaultCurrency),
|
|
||||||
),
|
|
||||||
DefaultCurrency: model.Currency(eventDTO.DefaultCurrency),
|
|
||||||
CreatedAt: eventDTO.CreatedAt,
|
|
||||||
UpdatedAt: eventDTO.UpdatedAt,
|
|
||||||
Owner: &owner,
|
|
||||||
Users: users,
|
|
||||||
}
|
|
||||||
|
|
||||||
return eventRetrieved, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetByID implements repo.EventRepository.
|
|
||||||
func (e *eventRepository) GetByID(
|
|
||||||
ctx context.Context,
|
|
||||||
eventID int,
|
|
||||||
tx any,
|
|
||||||
) (*model.EventRetrieved, error) {
|
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
queries := getQueries(e.queries, tx)
|
|
||||||
|
|
||||||
eventDTO, err := queries.GetEventByID(timeoutCtx, int32(eventID))
|
|
||||||
if err != nil {
|
|
||||||
log.ErrorLog("query error", "err", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return convToEventRetrieved(&eventDTO)
|
|
||||||
}
|
|
||||||
|
|
||||||
func convToEventList(eventsDTO []sqlc.ListEventsByUserIDRow) ([]model.EventListRetrieved, error) {
|
|
||||||
events := make([]model.EventListRetrieved, len(eventsDTO))
|
|
||||||
|
|
||||||
for i, evDTO := range eventsDTO {
|
|
||||||
var owner model.UserBaseRetrieved
|
|
||||||
err := json.Unmarshal(evDTO.Owner, &owner)
|
|
||||||
if err != nil {
|
|
||||||
// Unexpected
|
|
||||||
log.ErrorLog("json unmarshal error", "err", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ev := model.EventListRetrieved{
|
|
||||||
ID: int(evDTO.ID),
|
|
||||||
Name: evDTO.Name,
|
|
||||||
Description: evDTO.Description.String,
|
|
||||||
Owner: &owner,
|
|
||||||
CreatedAt: evDTO.CreatedAt,
|
|
||||||
}
|
|
||||||
|
|
||||||
events[i] = ev
|
|
||||||
}
|
|
||||||
|
|
||||||
return events, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListEventsByUserID implements repo.EventRepository.
|
|
||||||
func (e *eventRepository) ListEventsByUserID(
|
|
||||||
ctx context.Context,
|
|
||||||
userID int,
|
|
||||||
tx any,
|
|
||||||
) ([]model.EventListRetrieved, error) {
|
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
queries := getQueries(e.queries, tx)
|
|
||||||
|
|
||||||
eventsDTO, err := queries.ListEventsByUserID(timeoutCtx, int32(userID))
|
|
||||||
if err != nil {
|
|
||||||
log.ErrorLog("query error", "err", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return convToEventList(eventsDTO)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateInfo implements repo.EventRepository.
|
|
||||||
func (e *eventRepository) UpdateEventByID(
|
|
||||||
ctx context.Context,
|
|
||||||
event *model.EventUpdateEntity,
|
|
||||||
tx any,
|
|
||||||
) error {
|
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
queries := getQueries(e.queries, tx)
|
|
||||||
|
|
||||||
err := queries.UpdateEventByID(timeoutCtx, sqlc.UpdateEventByIDParams{
|
|
||||||
ID: int32(event.ID),
|
|
||||||
Name: event.Name,
|
|
||||||
Description: sql.NullString{String: event.Description, Valid: true},
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetParticipation implements repo.EventRepository.
|
|
||||||
func (e *eventRepository) GetParticipation(
|
|
||||||
ctx context.Context,
|
|
||||||
userID, eventID int,
|
|
||||||
tx any,
|
|
||||||
) (*model.ParticipationEntity, error) {
|
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
queries := getQueries(e.queries, tx)
|
|
||||||
|
|
||||||
partDTO, err := queries.GetParticipation(timeoutCtx, sqlc.GetParticipationParams{
|
|
||||||
UserID: int32(userID),
|
|
||||||
EventID: int32(eventID),
|
|
||||||
})
|
|
||||||
|
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
|
||||||
// No error, but participation not found
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &model.ParticipationEntity{
|
|
||||||
ID: int(partDTO.ID),
|
|
||||||
UserID: int(partDTO.UserID),
|
|
||||||
EventID: int(partDTO.EventID),
|
|
||||||
InvitedByUserID: int(partDTO.InvitedByUserID.Int32),
|
|
||||||
CreatedAt: partDTO.CreatedAt,
|
|
||||||
UpdatedAt: partDTO.UpdatedAt,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertParticipation implements repo.EventRepository.
|
|
||||||
func (e *eventRepository) InsertParticipation(
|
|
||||||
ctx context.Context,
|
|
||||||
userID int,
|
|
||||||
eventID int,
|
|
||||||
invitedByUserID int,
|
|
||||||
tx any,
|
|
||||||
) (*model.ParticipationEntity, error) {
|
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
queries := getQueries(e.queries, tx)
|
|
||||||
|
|
||||||
var invitedBy sql.NullInt32
|
|
||||||
if invitedByUserID == 0 {
|
|
||||||
invitedBy = sql.NullInt32{Int32: 0, Valid: false}
|
|
||||||
} else {
|
|
||||||
invitedBy = sql.NullInt32{Int32: int32(invitedByUserID), Valid: true}
|
|
||||||
}
|
|
||||||
participationDTO, err := queries.InsertParticipation(timeoutCtx, sqlc.InsertParticipationParams{
|
|
||||||
UserID: int32(userID),
|
|
||||||
EventID: int32(eventID),
|
|
||||||
InvitedByUserID: invitedBy,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &model.ParticipationEntity{
|
|
||||||
ID: int(participationDTO.ID),
|
|
||||||
UserID: int(participationDTO.UserID),
|
|
||||||
EventID: int(participationDTO.EventID),
|
|
||||||
InvitedByUserID: int(participationDTO.InvitedByUserID.Int32),
|
|
||||||
CreatedAt: participationDTO.CreatedAt,
|
|
||||||
UpdatedAt: participationDTO.UpdatedAt,
|
|
||||||
}, nil
|
|
||||||
}
|
|
@ -1,122 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package repo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/repo/sqlc"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestConvToEventRetrieved(t *testing.T) {
|
|
||||||
input := &sqlc.GetEventByIDRow{
|
|
||||||
ID: 123,
|
|
||||||
Name: "event",
|
|
||||||
Description: sql.NullString{Valid: false},
|
|
||||||
TotalAmount: sql.NullInt32{Valid: false},
|
|
||||||
DefaultCurrency: "EUR",
|
|
||||||
CreatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
|
||||||
UpdatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
|
||||||
Owner: json.RawMessage(`{"id":1, "first_name":"owner", "last_name":"owner"}`),
|
|
||||||
Users: json.RawMessage(`[{"id":1, "first_name":"owner", "last_name":"owner"}]`),
|
|
||||||
}
|
|
||||||
|
|
||||||
want := &model.EventRetrieved{
|
|
||||||
ID: 123,
|
|
||||||
Name: "event",
|
|
||||||
Description: "",
|
|
||||||
TotalAmount: model.Money{Amount: 0, Currency: "EUR"},
|
|
||||||
DefaultCurrency: model.Currency("EUR"),
|
|
||||||
CreatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
|
||||||
UpdatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
|
||||||
Owner: &model.UserBaseRetrieved{
|
|
||||||
ID: 1,
|
|
||||||
FirstName: "owner",
|
|
||||||
LastName: "owner",
|
|
||||||
},
|
|
||||||
Users: []model.UserBaseRetrieved{
|
|
||||||
{
|
|
||||||
ID: 1,
|
|
||||||
FirstName: "owner",
|
|
||||||
LastName: "owner",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
got, err := convToEventRetrieved(input)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, want, got)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConvToEventList(t *testing.T) {
|
|
||||||
input := []sqlc.ListEventsByUserIDRow{
|
|
||||||
{
|
|
||||||
ID: 123,
|
|
||||||
Name: "event",
|
|
||||||
Description: sql.NullString{Valid: false},
|
|
||||||
CreatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
|
||||||
Owner: json.RawMessage(`{"id":1, "first_name":"owner", "last_name":"owner"}`),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 456,
|
|
||||||
Name: "event2",
|
|
||||||
Description: sql.NullString{String: "super event", Valid: true},
|
|
||||||
CreatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
|
||||||
Owner: json.RawMessage(`{"id":1, "first_name":"owner", "last_name":"owner"}`),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
want := []model.EventListRetrieved{
|
|
||||||
{
|
|
||||||
ID: 123,
|
|
||||||
Name: "event",
|
|
||||||
Description: "",
|
|
||||||
CreatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
|
||||||
Owner: &model.UserBaseRetrieved{
|
|
||||||
ID: 1,
|
|
||||||
FirstName: "owner",
|
|
||||||
LastName: "owner",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 456,
|
|
||||||
Name: "event2",
|
|
||||||
Description: "super event",
|
|
||||||
CreatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
|
||||||
Owner: &model.UserBaseRetrieved{
|
|
||||||
ID: 1,
|
|
||||||
FirstName: "owner",
|
|
||||||
LastName: "owner",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
got, err := convToEventList(input)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, want, got)
|
|
||||||
}
|
|
@ -1,267 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package repo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/repo/sqlc"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/repo"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
type expenseRepository struct {
|
|
||||||
queries *sqlc.Queries
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewExpenseRepository(db *sql.DB) repo.ExpenseRepository {
|
|
||||||
return &expenseRepository{
|
|
||||||
queries: sqlc.New(db),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteExpense implements repo.ExpenseRepository.
|
|
||||||
func (e *expenseRepository) DeleteExpense(ctx context.Context, expenseID int, tx any) error {
|
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
queries := getQueries(e.queries, tx)
|
|
||||||
return queries.DeleteExpense(timeoutCtx, int32(expenseID))
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteTransactionsOfExpense implements repo.ExpenseRepository.
|
|
||||||
func (e *expenseRepository) DeleteTransactionsOfExpense(
|
|
||||||
ctx context.Context,
|
|
||||||
expenseID int,
|
|
||||||
tx any,
|
|
||||||
) error {
|
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
queries := getQueries(e.queries, tx)
|
|
||||||
return queries.DeleteTransactionsOfExpenseID(timeoutCtx, int32(expenseID))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetExpenseByID implements repo.ExpenseRepository.
|
|
||||||
func (e *expenseRepository) GetExpenseByID(
|
|
||||||
ctx context.Context,
|
|
||||||
expenseID int,
|
|
||||||
tx any,
|
|
||||||
) (*model.ExpenseRetrieved, error) {
|
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
queries := getQueries(e.queries, tx)
|
|
||||||
|
|
||||||
expenseDTO, err := queries.GetExpenseByID(timeoutCtx, int32(expenseID))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
expense, err := convToExpenseRetrieved(&expenseDTO)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return expense, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func convToPayments(raw json.RawMessage) ([]model.Payment, error) {
|
|
||||||
var paymentsRetrieved []model.PaymentRetrieved
|
|
||||||
err := json.Unmarshal(raw, &paymentsRetrieved)
|
|
||||||
if err != nil {
|
|
||||||
// Unexpected
|
|
||||||
log.ErrorLog("json unmarshal error", "err", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
payments := make([]model.Payment, len(paymentsRetrieved))
|
|
||||||
for i, p := range paymentsRetrieved {
|
|
||||||
payment := model.Payment{
|
|
||||||
PayerID: p.PayerID,
|
|
||||||
PayerFirstName: p.PayerFirstName,
|
|
||||||
PayerLastName: p.PayerLastName,
|
|
||||||
Amount: model.MakeMoney(p.Amount, model.Currency(p.Currency)),
|
|
||||||
}
|
|
||||||
payments[i] = payment
|
|
||||||
}
|
|
||||||
|
|
||||||
return payments, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func convToBenefits(raw json.RawMessage) ([]model.Benefit, error) {
|
|
||||||
var benefitsRetrieved []model.BenefitRetrieved
|
|
||||||
err := json.Unmarshal(raw, &benefitsRetrieved)
|
|
||||||
if err != nil {
|
|
||||||
// Unexpected
|
|
||||||
log.ErrorLog("json unmarshal error", "err", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
benefits := make([]model.Benefit, len(benefitsRetrieved))
|
|
||||||
for i, b := range benefitsRetrieved {
|
|
||||||
benefit := model.Benefit{
|
|
||||||
RecipientID: b.RecipientID,
|
|
||||||
RecipientFirstName: b.RecipientFirstName,
|
|
||||||
RecipientLastName: b.RecipientLastName,
|
|
||||||
Amount: model.MakeMoney(b.Amount, model.Currency(b.Currency)),
|
|
||||||
}
|
|
||||||
benefits[i] = benefit
|
|
||||||
}
|
|
||||||
|
|
||||||
return benefits, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func convToExpenseRetrieved(expenseDTO *sqlc.GetExpenseByIDRow) (*model.ExpenseRetrieved, error) {
|
|
||||||
payments, err := convToPayments(expenseDTO.Payments)
|
|
||||||
if err != nil {
|
|
||||||
// Unexpected
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
benefits, err := convToBenefits(expenseDTO.Benefits)
|
|
||||||
if err != nil {
|
|
||||||
// Unexpected
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
expenseRetrieved := &model.ExpenseRetrieved{
|
|
||||||
ID: int(expenseDTO.ID),
|
|
||||||
CreatedAt: expenseDTO.CreatedAt,
|
|
||||||
UpdatedAt: expenseDTO.UpdatedAt,
|
|
||||||
Amount: model.MakeMoney(int(expenseDTO.Amount), model.Currency(expenseDTO.Currency)),
|
|
||||||
EventID: int(expenseDTO.EventID),
|
|
||||||
Detail: model.ExpenseDetail{
|
|
||||||
Name: expenseDTO.Name.String,
|
|
||||||
Place: expenseDTO.Place.String,
|
|
||||||
},
|
|
||||||
Payments: payments,
|
|
||||||
Benefits: benefits,
|
|
||||||
}
|
|
||||||
|
|
||||||
return expenseRetrieved, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertExpense implements repo.ExpenseRepository.
|
|
||||||
func (e *expenseRepository) InsertExpense(
|
|
||||||
ctx context.Context,
|
|
||||||
expenseEntity *model.ExpenseEntity,
|
|
||||||
tx any,
|
|
||||||
) (*model.ExpenseEntity, error) {
|
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
queries := getQueries(e.queries, tx)
|
|
||||||
|
|
||||||
expenseDTO, err := queries.InsertExpense(timeoutCtx, sqlc.InsertExpenseParams{
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
Amount: int32(expenseEntity.Amount),
|
|
||||||
Currency: expenseEntity.Currency,
|
|
||||||
EventID: int32(expenseEntity.EventID),
|
|
||||||
Name: sql.NullString{String: expenseEntity.Name, Valid: true},
|
|
||||||
Place: sql.NullString{String: expenseEntity.Place, Valid: true},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &model.ExpenseEntity{
|
|
||||||
ID: int(expenseDTO.ID),
|
|
||||||
CreatedAt: expenseDTO.CreatedAt,
|
|
||||||
UpdatedAt: expenseDTO.CreatedAt,
|
|
||||||
Amount: int(expenseDTO.Amount),
|
|
||||||
Currency: expenseDTO.Currency,
|
|
||||||
EventID: int(expenseDTO.EventID),
|
|
||||||
Name: expenseDTO.Name.String,
|
|
||||||
Place: expenseDTO.Place.String,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListExpensesByEventID implements repo.ExpenseRepository.
|
|
||||||
func (e *expenseRepository) ListExpensesByEventID(
|
|
||||||
ctx context.Context,
|
|
||||||
id int,
|
|
||||||
tx any,
|
|
||||||
) ([]model.ExpensesListRetrieved, error) {
|
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
queries := getQueries(e.queries, tx)
|
|
||||||
|
|
||||||
listDTO, err := queries.ListExpensesByEventID(timeoutCtx, int32(id))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
res := make([]model.ExpensesListRetrieved, len(listDTO))
|
|
||||||
for i, dto := range listDTO {
|
|
||||||
elem := model.ExpensesListRetrieved{
|
|
||||||
ID: int(dto.ID),
|
|
||||||
CreatedAt: dto.CreatedAt,
|
|
||||||
UpdatedAt: dto.UpdatedAt,
|
|
||||||
Amount: model.MakeMoney(int(dto.Amount), model.Currency(dto.Currency)),
|
|
||||||
EventID: int(dto.EventID),
|
|
||||||
Detail: model.ExpenseDetail{
|
|
||||||
Name: dto.Name.String,
|
|
||||||
Place: dto.Place.String,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
res[i] = elem
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateExpenseByID implements repo.ExpenseRepository.
|
|
||||||
func (e *expenseRepository) UpdateExpenseByID(
|
|
||||||
ctx context.Context,
|
|
||||||
expenseUpdate *model.ExpenseUpdateEntity,
|
|
||||||
tx any,
|
|
||||||
) (*model.ExpenseEntity, error) {
|
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
queries := getQueries(e.queries, tx)
|
|
||||||
|
|
||||||
expenseDTO, err := queries.UpdateExpenseByID(timeoutCtx, sqlc.UpdateExpenseByIDParams{
|
|
||||||
ID: int32(expenseUpdate.ID),
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
Amount: int32(expenseUpdate.Amount),
|
|
||||||
Currency: expenseUpdate.Currency,
|
|
||||||
Name: sql.NullString{String: expenseUpdate.Name, Valid: true},
|
|
||||||
Place: sql.NullString{String: expenseUpdate.Place, Valid: true},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &model.ExpenseEntity{
|
|
||||||
ID: int(expenseDTO.ID),
|
|
||||||
CreatedAt: expenseDTO.CreatedAt,
|
|
||||||
UpdatedAt: expenseDTO.CreatedAt,
|
|
||||||
Amount: int(expenseDTO.Amount),
|
|
||||||
Currency: expenseDTO.Currency,
|
|
||||||
EventID: int(expenseDTO.EventID),
|
|
||||||
Name: expenseDTO.Name.String,
|
|
||||||
Place: expenseDTO.Place.String,
|
|
||||||
}, nil
|
|
||||||
}
|
|
@ -1,96 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package repo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/repo/sqlc"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestConvToExpenseRetrieved(t *testing.T) {
|
|
||||||
input := &sqlc.GetExpenseByIDRow{
|
|
||||||
ID: 123,
|
|
||||||
CreatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
|
||||||
UpdatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
|
||||||
Amount: 123,
|
|
||||||
Currency: "EUR",
|
|
||||||
EventID: 123,
|
|
||||||
Name: sql.NullString{Valid: false},
|
|
||||||
Place: sql.NullString{Valid: false},
|
|
||||||
Payments: json.RawMessage(
|
|
||||||
`[{"payer_id": 1, "payer_first_name": "toto", "payer_last_name": "titi", "amount": 10, "currency": "EUR"},
|
|
||||||
{"payer_id": 2, "payer_first_name": "tata", "payer_last_name": "titi", "amount": 10, "currency": "EUR"}]`,
|
|
||||||
),
|
|
||||||
Benefits: json.RawMessage(
|
|
||||||
`[{"recipient_id": 1, "recipient_first_name": "toto", "recipient_last_name": "titi", "amount": 10, "currency": "EUR"},
|
|
||||||
{"recipient_id": 2, "recipient_first_name": "tata", "recipient_last_name": "titi", "amount": 10, "currency": "EUR"}]`,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
want := &model.ExpenseRetrieved{
|
|
||||||
ID: 123,
|
|
||||||
CreatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
|
||||||
UpdatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
|
||||||
Amount: model.Money{Amount: 123, Currency: model.Currency("EUR")},
|
|
||||||
EventID: 123,
|
|
||||||
Detail: model.ExpenseDetail{},
|
|
||||||
Payments: []model.Payment{
|
|
||||||
{
|
|
||||||
PayerID: 1,
|
|
||||||
PayerFirstName: "toto",
|
|
||||||
PayerLastName: "titi",
|
|
||||||
Amount: model.Money{Amount: 10, Currency: model.Currency("EUR")},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
PayerID: 2,
|
|
||||||
PayerFirstName: "tata",
|
|
||||||
PayerLastName: "titi",
|
|
||||||
Amount: model.Money{Amount: 10, Currency: model.Currency("EUR")},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Benefits: []model.Benefit{
|
|
||||||
{
|
|
||||||
RecipientID: 1,
|
|
||||||
RecipientFirstName: "toto",
|
|
||||||
RecipientLastName: "titi",
|
|
||||||
Amount: model.Money{Amount: 10, Currency: model.Currency("EUR")},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RecipientID: 2,
|
|
||||||
RecipientFirstName: "tata",
|
|
||||||
RecipientLastName: "titi",
|
|
||||||
Amount: model.Money{Amount: 10, Currency: model.Currency("EUR")},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
got, err := convToExpenseRetrieved(input)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, want, got)
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.27.0
|
|
||||||
|
|
||||||
package sqlc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DBTX interface {
|
|
||||||
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
|
|
||||||
PrepareContext(context.Context, string) (*sql.Stmt, error)
|
|
||||||
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
|
|
||||||
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(db DBTX) *Queries {
|
|
||||||
return &Queries{db: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Queries struct {
|
|
||||||
db DBTX
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
|
|
||||||
return &Queries{
|
|
||||||
db: tx,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,64 +0,0 @@
|
|||||||
-- name: InsertEvent :one
|
|
||||||
INSERT INTO "event" (
|
|
||||||
name, description, total_amount, default_currency, owner_id, created_at, updated_at
|
|
||||||
) VALUES ( $1, $2, $3, $4, $5, $6, $7 )
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: ListEventsByUserID :many
|
|
||||||
SELECT
|
|
||||||
e.id,
|
|
||||||
e.name,
|
|
||||||
e.description,
|
|
||||||
e.created_at,
|
|
||||||
json_build_object(
|
|
||||||
'id', o.id,
|
|
||||||
'first_name', o.first_name,
|
|
||||||
'last_name', o.last_name
|
|
||||||
) AS owner
|
|
||||||
FROM "event" e
|
|
||||||
JOIN "participation" p ON p.event_id = e.id -- participation linked with the event
|
|
||||||
JOIN "user" o ON o.id = e.owner_id -- get the owner info
|
|
||||||
WHERE e.id IN (
|
|
||||||
SELECT pt.event_id FROM participation pt WHERE pt.user_id = $1 -- consider the events participated by user_id
|
|
||||||
)
|
|
||||||
GROUP BY
|
|
||||||
e.id, e.name, e.description, e.created_at,
|
|
||||||
o.id, o.first_name, o.last_name;
|
|
||||||
|
|
||||||
-- name: GetEventByID :one
|
|
||||||
SELECT
|
|
||||||
e.id,
|
|
||||||
e.name,
|
|
||||||
e.description,
|
|
||||||
e.total_amount,
|
|
||||||
e.default_currency,
|
|
||||||
e.created_at,
|
|
||||||
e.updated_at,
|
|
||||||
json_build_object(
|
|
||||||
'id', o.id,
|
|
||||||
'first_name', o.first_name,
|
|
||||||
'last_name', o.last_name
|
|
||||||
) AS owner,
|
|
||||||
json_agg(
|
|
||||||
json_build_object(
|
|
||||||
'id', u.id,
|
|
||||||
'first_name', u.first_name,
|
|
||||||
'last_name', u.last_name
|
|
||||||
)
|
|
||||||
) AS users -- Aggregation for users in the event
|
|
||||||
FROM "event" e
|
|
||||||
JOIN "participation" p ON p.event_id = e.id -- participation linked with the event
|
|
||||||
JOIN "user" u ON u.id = p.user_id -- and the query user
|
|
||||||
JOIN "user" o ON o.id = e.owner_id -- get the owner info
|
|
||||||
WHERE e.id = $1
|
|
||||||
GROUP BY
|
|
||||||
e.id, e.name, e.description, e.created_at, e.updated_at,
|
|
||||||
e.total_amount, e.default_currency,
|
|
||||||
o.id, o.first_name, o.last_name;
|
|
||||||
|
|
||||||
-- name: UpdateEventByID :exec
|
|
||||||
UPDATE "event"
|
|
||||||
SET name = $2, description = $3, updated_at = $4
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
||||||
|
|
@ -1,197 +0,0 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.27.0
|
|
||||||
// source: event.sql
|
|
||||||
|
|
||||||
package sqlc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const getEventByID = `-- name: GetEventByID :one
|
|
||||||
SELECT
|
|
||||||
e.id,
|
|
||||||
e.name,
|
|
||||||
e.description,
|
|
||||||
e.total_amount,
|
|
||||||
e.default_currency,
|
|
||||||
e.created_at,
|
|
||||||
e.updated_at,
|
|
||||||
json_build_object(
|
|
||||||
'id', o.id,
|
|
||||||
'first_name', o.first_name,
|
|
||||||
'last_name', o.last_name
|
|
||||||
) AS owner,
|
|
||||||
json_agg(
|
|
||||||
json_build_object(
|
|
||||||
'id', u.id,
|
|
||||||
'first_name', u.first_name,
|
|
||||||
'last_name', u.last_name
|
|
||||||
)
|
|
||||||
) AS users -- Aggregation for users in the event
|
|
||||||
FROM "event" e
|
|
||||||
JOIN "participation" p ON p.event_id = e.id -- participation linked with the event
|
|
||||||
JOIN "user" u ON u.id = p.user_id -- and the query user
|
|
||||||
JOIN "user" o ON o.id = e.owner_id -- get the owner info
|
|
||||||
WHERE e.id = $1
|
|
||||||
GROUP BY
|
|
||||||
e.id, e.name, e.description, e.created_at, e.updated_at,
|
|
||||||
e.total_amount, e.default_currency,
|
|
||||||
o.id, o.first_name, o.last_name
|
|
||||||
`
|
|
||||||
|
|
||||||
type GetEventByIDRow struct {
|
|
||||||
ID int32
|
|
||||||
Name string
|
|
||||||
Description sql.NullString
|
|
||||||
TotalAmount sql.NullInt32
|
|
||||||
DefaultCurrency string
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
Owner json.RawMessage
|
|
||||||
Users json.RawMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) GetEventByID(ctx context.Context, id int32) (GetEventByIDRow, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, getEventByID, id)
|
|
||||||
var i GetEventByIDRow
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.Description,
|
|
||||||
&i.TotalAmount,
|
|
||||||
&i.DefaultCurrency,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
&i.Owner,
|
|
||||||
&i.Users,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertEvent = `-- name: InsertEvent :one
|
|
||||||
INSERT INTO "event" (
|
|
||||||
name, description, total_amount, default_currency, owner_id, created_at, updated_at
|
|
||||||
) VALUES ( $1, $2, $3, $4, $5, $6, $7 )
|
|
||||||
RETURNING id, name, description, default_currency, owner_id, created_at, updated_at, total_amount
|
|
||||||
`
|
|
||||||
|
|
||||||
type InsertEventParams struct {
|
|
||||||
Name string
|
|
||||||
Description sql.NullString
|
|
||||||
TotalAmount sql.NullInt32
|
|
||||||
DefaultCurrency string
|
|
||||||
OwnerID int32
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) InsertEvent(ctx context.Context, arg InsertEventParams) (Event, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, insertEvent,
|
|
||||||
arg.Name,
|
|
||||||
arg.Description,
|
|
||||||
arg.TotalAmount,
|
|
||||||
arg.DefaultCurrency,
|
|
||||||
arg.OwnerID,
|
|
||||||
arg.CreatedAt,
|
|
||||||
arg.UpdatedAt,
|
|
||||||
)
|
|
||||||
var i Event
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.Description,
|
|
||||||
&i.DefaultCurrency,
|
|
||||||
&i.OwnerID,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
&i.TotalAmount,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const listEventsByUserID = `-- name: ListEventsByUserID :many
|
|
||||||
SELECT
|
|
||||||
e.id,
|
|
||||||
e.name,
|
|
||||||
e.description,
|
|
||||||
e.created_at,
|
|
||||||
json_build_object(
|
|
||||||
'id', o.id,
|
|
||||||
'first_name', o.first_name,
|
|
||||||
'last_name', o.last_name
|
|
||||||
) AS owner
|
|
||||||
FROM "event" e
|
|
||||||
JOIN "participation" p ON p.event_id = e.id -- participation linked with the event
|
|
||||||
JOIN "user" o ON o.id = e.owner_id -- get the owner info
|
|
||||||
WHERE e.id IN (
|
|
||||||
SELECT pt.event_id FROM participation pt WHERE pt.user_id = $1 -- consider the events participated by user_id
|
|
||||||
)
|
|
||||||
GROUP BY
|
|
||||||
e.id, e.name, e.description, e.created_at,
|
|
||||||
o.id, o.first_name, o.last_name
|
|
||||||
`
|
|
||||||
|
|
||||||
type ListEventsByUserIDRow struct {
|
|
||||||
ID int32
|
|
||||||
Name string
|
|
||||||
Description sql.NullString
|
|
||||||
CreatedAt time.Time
|
|
||||||
Owner json.RawMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) ListEventsByUserID(ctx context.Context, userID int32) ([]ListEventsByUserIDRow, error) {
|
|
||||||
rows, err := q.db.QueryContext(ctx, listEventsByUserID, userID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []ListEventsByUserIDRow
|
|
||||||
for rows.Next() {
|
|
||||||
var i ListEventsByUserIDRow
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.Description,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.Owner,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateEventByID = `-- name: UpdateEventByID :exec
|
|
||||||
UPDATE "event"
|
|
||||||
SET name = $2, description = $3, updated_at = $4
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
type UpdateEventByIDParams struct {
|
|
||||||
ID int32
|
|
||||||
Name string
|
|
||||||
Description sql.NullString
|
|
||||||
UpdatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdateEventByID(ctx context.Context, arg UpdateEventByIDParams) error {
|
|
||||||
_, err := q.db.ExecContext(ctx, updateEventByID,
|
|
||||||
arg.ID,
|
|
||||||
arg.Name,
|
|
||||||
arg.Description,
|
|
||||||
arg.UpdatedAt,
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
@ -1,64 +0,0 @@
|
|||||||
-- name: InsertExpense :one
|
|
||||||
INSERT INTO "expense" (
|
|
||||||
created_at, updated_at, amount, currency, event_id, name, place
|
|
||||||
) VALUES ( $1, $2, $3, $4, $5, $6, $7 )
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: DeleteExpense :exec
|
|
||||||
DELETE FROM "expense" WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: DeleteTransactionsOfExpenseID :exec
|
|
||||||
DELETE FROM "transaction" WHERE transaction.expense_id = $1;
|
|
||||||
|
|
||||||
-- name: UpdateExpenseByID :one
|
|
||||||
UPDATE "expense"
|
|
||||||
SET updated_at = $2, amount = $3, currency = $4, name = $5, place = $6
|
|
||||||
WHERE id = $1
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: ListExpensesByEventID :many
|
|
||||||
SELECT
|
|
||||||
ex.id, ex.created_at, ex.updated_at, ex.amount, ex.currency, ex.event_id,
|
|
||||||
ex.name, ex.place
|
|
||||||
FROM "expense" ex
|
|
||||||
JOIN "event" ev ON ev.id = ex.event_id
|
|
||||||
WHERE ev.id = $1;
|
|
||||||
|
|
||||||
-- name: GetExpenseByID :one
|
|
||||||
WITH payer_transaction as (
|
|
||||||
SELECT pt.expense_id,
|
|
||||||
json_agg(json_build_object(
|
|
||||||
'payer_id', p.id,
|
|
||||||
'payer_first_name', p.first_name,
|
|
||||||
'payer_last_name', p.last_name,
|
|
||||||
'amount', pt.amount,
|
|
||||||
'currency', pt.currency
|
|
||||||
)) AS payments
|
|
||||||
FROM "transaction" pt
|
|
||||||
JOIN "user" p ON p.id = pt.user_id
|
|
||||||
WHERE pt.is_income = FALSE
|
|
||||||
GROUP BY pt.expense_id
|
|
||||||
), -- For each expense, aggregate payment info
|
|
||||||
recipient_transaction as (
|
|
||||||
SELECT rt.expense_id,
|
|
||||||
json_agg(json_build_object(
|
|
||||||
'recipient_id', p.id,
|
|
||||||
'recipient_first_name', p.first_name,
|
|
||||||
'recipient_last_name', p.last_name,
|
|
||||||
'amount', rt.amount,
|
|
||||||
'currency', rt.currency
|
|
||||||
)) AS benefits
|
|
||||||
FROM "transaction" rt
|
|
||||||
JOIN "user" p ON p.id = rt.user_id
|
|
||||||
WHERE rt.is_income = TRUE
|
|
||||||
GROUP BY rt.expense_id
|
|
||||||
) -- For each expense, aggregate benefits info
|
|
||||||
SELECT
|
|
||||||
ex.id, ex.created_at, ex.updated_at, ex.amount, ex.currency, ex.event_id,
|
|
||||||
ex.name, ex.place,
|
|
||||||
COALESCE(pt.payments, '[]') AS payments,
|
|
||||||
COALESCE(rt.benefits, '[]') AS benefits
|
|
||||||
FROM "expense" ex
|
|
||||||
LEFT JOIN "payer_transaction" pt ON pt.expense_id = ex.id
|
|
||||||
LEFT JOIN "recipient_transaction" rt ON rt.expense_id = ex.id
|
|
||||||
WHERE ex.id = $1;
|
|
@ -1,223 +0,0 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.27.0
|
|
||||||
// source: expense.sql
|
|
||||||
|
|
||||||
package sqlc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const deleteExpense = `-- name: DeleteExpense :exec
|
|
||||||
DELETE FROM "expense" WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) DeleteExpense(ctx context.Context, id int32) error {
|
|
||||||
_, err := q.db.ExecContext(ctx, deleteExpense, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteTransactionsOfExpenseID = `-- name: DeleteTransactionsOfExpenseID :exec
|
|
||||||
DELETE FROM "transaction" WHERE transaction.expense_id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) DeleteTransactionsOfExpenseID(ctx context.Context, expenseID int32) error {
|
|
||||||
_, err := q.db.ExecContext(ctx, deleteTransactionsOfExpenseID, expenseID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getExpenseByID = `-- name: GetExpenseByID :one
|
|
||||||
WITH payer_transaction as (
|
|
||||||
SELECT pt.expense_id,
|
|
||||||
json_agg(json_build_object(
|
|
||||||
'payer_id', p.id,
|
|
||||||
'payer_first_name', p.first_name,
|
|
||||||
'payer_last_name', p.last_name,
|
|
||||||
'amount', pt.amount,
|
|
||||||
'currency', pt.currency
|
|
||||||
)) AS payments
|
|
||||||
FROM "transaction" pt
|
|
||||||
JOIN "user" p ON p.id = pt.user_id
|
|
||||||
WHERE pt.is_income = FALSE
|
|
||||||
GROUP BY pt.expense_id
|
|
||||||
), -- For each expense, aggregate payment info
|
|
||||||
recipient_transaction as (
|
|
||||||
SELECT rt.expense_id,
|
|
||||||
json_agg(json_build_object(
|
|
||||||
'recipient_id', p.id,
|
|
||||||
'recipient_first_name', p.first_name,
|
|
||||||
'recipient_last_name', p.last_name,
|
|
||||||
'amount', rt.amount,
|
|
||||||
'currency', rt.currency
|
|
||||||
)) AS benefits
|
|
||||||
FROM "transaction" rt
|
|
||||||
JOIN "user" p ON p.id = rt.user_id
|
|
||||||
WHERE rt.is_income = TRUE
|
|
||||||
GROUP BY rt.expense_id
|
|
||||||
) -- For each expense, aggregate benefits info
|
|
||||||
SELECT
|
|
||||||
ex.id, ex.created_at, ex.updated_at, ex.amount, ex.currency, ex.event_id,
|
|
||||||
ex.name, ex.place,
|
|
||||||
COALESCE(pt.payments, '[]') AS payments,
|
|
||||||
COALESCE(rt.benefits, '[]') AS benefits
|
|
||||||
FROM "expense" ex
|
|
||||||
LEFT JOIN "payer_transaction" pt ON pt.expense_id = ex.id
|
|
||||||
LEFT JOIN "recipient_transaction" rt ON rt.expense_id = ex.id
|
|
||||||
WHERE ex.id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
type GetExpenseByIDRow struct {
|
|
||||||
ID int32
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
Amount int32
|
|
||||||
Currency string
|
|
||||||
EventID int32
|
|
||||||
Name sql.NullString
|
|
||||||
Place sql.NullString
|
|
||||||
Payments json.RawMessage
|
|
||||||
Benefits json.RawMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) GetExpenseByID(ctx context.Context, id int32) (GetExpenseByIDRow, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, getExpenseByID, id)
|
|
||||||
var i GetExpenseByIDRow
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
&i.Amount,
|
|
||||||
&i.Currency,
|
|
||||||
&i.EventID,
|
|
||||||
&i.Name,
|
|
||||||
&i.Place,
|
|
||||||
&i.Payments,
|
|
||||||
&i.Benefits,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertExpense = `-- name: InsertExpense :one
|
|
||||||
INSERT INTO "expense" (
|
|
||||||
created_at, updated_at, amount, currency, event_id, name, place
|
|
||||||
) VALUES ( $1, $2, $3, $4, $5, $6, $7 )
|
|
||||||
RETURNING id, created_at, updated_at, amount, currency, event_id, name, place
|
|
||||||
`
|
|
||||||
|
|
||||||
type InsertExpenseParams struct {
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
Amount int32
|
|
||||||
Currency string
|
|
||||||
EventID int32
|
|
||||||
Name sql.NullString
|
|
||||||
Place sql.NullString
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) InsertExpense(ctx context.Context, arg InsertExpenseParams) (Expense, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, insertExpense,
|
|
||||||
arg.CreatedAt,
|
|
||||||
arg.UpdatedAt,
|
|
||||||
arg.Amount,
|
|
||||||
arg.Currency,
|
|
||||||
arg.EventID,
|
|
||||||
arg.Name,
|
|
||||||
arg.Place,
|
|
||||||
)
|
|
||||||
var i Expense
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
&i.Amount,
|
|
||||||
&i.Currency,
|
|
||||||
&i.EventID,
|
|
||||||
&i.Name,
|
|
||||||
&i.Place,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const listExpensesByEventID = `-- name: ListExpensesByEventID :many
|
|
||||||
SELECT
|
|
||||||
ex.id, ex.created_at, ex.updated_at, ex.amount, ex.currency, ex.event_id,
|
|
||||||
ex.name, ex.place
|
|
||||||
FROM "expense" ex
|
|
||||||
JOIN "event" ev ON ev.id = ex.event_id
|
|
||||||
WHERE ev.id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ListExpensesByEventID(ctx context.Context, id int32) ([]Expense, error) {
|
|
||||||
rows, err := q.db.QueryContext(ctx, listExpensesByEventID, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []Expense
|
|
||||||
for rows.Next() {
|
|
||||||
var i Expense
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
&i.Amount,
|
|
||||||
&i.Currency,
|
|
||||||
&i.EventID,
|
|
||||||
&i.Name,
|
|
||||||
&i.Place,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateExpenseByID = `-- name: UpdateExpenseByID :one
|
|
||||||
UPDATE "expense"
|
|
||||||
SET updated_at = $2, amount = $3, currency = $4, name = $5, place = $6
|
|
||||||
WHERE id = $1
|
|
||||||
RETURNING id, created_at, updated_at, amount, currency, event_id, name, place
|
|
||||||
`
|
|
||||||
|
|
||||||
type UpdateExpenseByIDParams struct {
|
|
||||||
ID int32
|
|
||||||
UpdatedAt time.Time
|
|
||||||
Amount int32
|
|
||||||
Currency string
|
|
||||||
Name sql.NullString
|
|
||||||
Place sql.NullString
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdateExpenseByID(ctx context.Context, arg UpdateExpenseByIDParams) (Expense, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, updateExpenseByID,
|
|
||||||
arg.ID,
|
|
||||||
arg.UpdatedAt,
|
|
||||||
arg.Amount,
|
|
||||||
arg.Currency,
|
|
||||||
arg.Name,
|
|
||||||
arg.Place,
|
|
||||||
)
|
|
||||||
var i Expense
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
&i.Amount,
|
|
||||||
&i.Currency,
|
|
||||||
&i.EventID,
|
|
||||||
&i.Name,
|
|
||||||
&i.Place,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.27.0
|
|
||||||
|
|
||||||
package sqlc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Admin struct {
|
|
||||||
ID int32
|
|
||||||
Email string
|
|
||||||
Password string
|
|
||||||
AccessLevel int32
|
|
||||||
}
|
|
||||||
|
|
||||||
type Event struct {
|
|
||||||
ID int32
|
|
||||||
Name string
|
|
||||||
Description sql.NullString
|
|
||||||
DefaultCurrency string
|
|
||||||
OwnerID int32
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
TotalAmount sql.NullInt32
|
|
||||||
}
|
|
||||||
|
|
||||||
type Expense struct {
|
|
||||||
ID int32
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
Amount int32
|
|
||||||
Currency string
|
|
||||||
EventID int32
|
|
||||||
Name sql.NullString
|
|
||||||
Place sql.NullString
|
|
||||||
}
|
|
||||||
|
|
||||||
type Participation struct {
|
|
||||||
ID int32
|
|
||||||
UserID int32
|
|
||||||
EventID int32
|
|
||||||
InvitedByUserID sql.NullInt32
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type Transaction struct {
|
|
||||||
ID int32
|
|
||||||
ExpenseID int32
|
|
||||||
UserID int32
|
|
||||||
Amount int32
|
|
||||||
Currency string
|
|
||||||
IsIncome bool
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type User struct {
|
|
||||||
ID int32
|
|
||||||
Email string
|
|
||||||
FirstName string
|
|
||||||
LastName string
|
|
||||||
Password string
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
-- name: InsertParticipation :one
|
|
||||||
INSERT INTO participation (
|
|
||||||
user_id, event_id, invited_by_user_id, created_at, updated_at
|
|
||||||
) VALUES ($1, $2, $3, $4, $5)
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: GetParticipation :one
|
|
||||||
SELECT id, user_id, event_id, invited_by_user_id, created_at, updated_at
|
|
||||||
FROM "participation"
|
|
||||||
WHERE user_id = $1 AND event_id = $2;
|
|
@ -1,72 +0,0 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.27.0
|
|
||||||
// source: participation.sql
|
|
||||||
|
|
||||||
package sqlc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const getParticipation = `-- name: GetParticipation :one
|
|
||||||
SELECT id, user_id, event_id, invited_by_user_id, created_at, updated_at
|
|
||||||
FROM "participation"
|
|
||||||
WHERE user_id = $1 AND event_id = $2
|
|
||||||
`
|
|
||||||
|
|
||||||
type GetParticipationParams struct {
|
|
||||||
UserID int32
|
|
||||||
EventID int32
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) GetParticipation(ctx context.Context, arg GetParticipationParams) (Participation, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, getParticipation, arg.UserID, arg.EventID)
|
|
||||||
var i Participation
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.UserID,
|
|
||||||
&i.EventID,
|
|
||||||
&i.InvitedByUserID,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertParticipation = `-- name: InsertParticipation :one
|
|
||||||
INSERT INTO participation (
|
|
||||||
user_id, event_id, invited_by_user_id, created_at, updated_at
|
|
||||||
) VALUES ($1, $2, $3, $4, $5)
|
|
||||||
RETURNING id, user_id, event_id, invited_by_user_id, created_at, updated_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type InsertParticipationParams struct {
|
|
||||||
UserID int32
|
|
||||||
EventID int32
|
|
||||||
InvitedByUserID sql.NullInt32
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) InsertParticipation(ctx context.Context, arg InsertParticipationParams) (Participation, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, insertParticipation,
|
|
||||||
arg.UserID,
|
|
||||||
arg.EventID,
|
|
||||||
arg.InvitedByUserID,
|
|
||||||
arg.CreatedAt,
|
|
||||||
arg.UpdatedAt,
|
|
||||||
)
|
|
||||||
var i Participation
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.UserID,
|
|
||||||
&i.EventID,
|
|
||||||
&i.InvitedByUserID,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.27.0
|
|
||||||
|
|
||||||
package sqlc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Querier interface {
|
|
||||||
DeleteExpense(ctx context.Context, id int32) error
|
|
||||||
DeleteTransactionsOfExpenseID(ctx context.Context, expenseID int32) error
|
|
||||||
GetEventByID(ctx context.Context, id int32) (GetEventByIDRow, error)
|
|
||||||
GetExpenseByID(ctx context.Context, id int32) (GetExpenseByIDRow, error)
|
|
||||||
GetParticipation(ctx context.Context, arg GetParticipationParams) (Participation, error)
|
|
||||||
GetUserByEmail(ctx context.Context, email string) (User, error)
|
|
||||||
GetUserByID(ctx context.Context, id int32) (User, error)
|
|
||||||
InsertEvent(ctx context.Context, arg InsertEventParams) (Event, error)
|
|
||||||
InsertExpense(ctx context.Context, arg InsertExpenseParams) (Expense, error)
|
|
||||||
InsertParticipation(ctx context.Context, arg InsertParticipationParams) (Participation, error)
|
|
||||||
InsertTransaction(ctx context.Context, arg InsertTransactionParams) error
|
|
||||||
InsertUser(ctx context.Context, arg InsertUserParams) (User, error)
|
|
||||||
ListEventsByUserID(ctx context.Context, userID int32) ([]ListEventsByUserIDRow, error)
|
|
||||||
ListExpensesByEventID(ctx context.Context, id int32) ([]Expense, error)
|
|
||||||
UpdateEventByID(ctx context.Context, arg UpdateEventByIDParams) error
|
|
||||||
UpdateExpenseByID(ctx context.Context, arg UpdateExpenseByIDParams) (Expense, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Querier = (*Queries)(nil)
|
|
@ -1,5 +0,0 @@
|
|||||||
-- name: InsertTransaction :exec
|
|
||||||
INSERT INTO "transaction" (
|
|
||||||
created_at, updated_at, amount, currency, expense_id, user_id, is_income
|
|
||||||
) VALUES ( $1, $2, $3, $4, $5, $6, $7 )
|
|
||||||
RETURNING *;
|
|
@ -1,41 +0,0 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.27.0
|
|
||||||
// source: transaction.sql
|
|
||||||
|
|
||||||
package sqlc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const insertTransaction = `-- name: InsertTransaction :exec
|
|
||||||
INSERT INTO "transaction" (
|
|
||||||
created_at, updated_at, amount, currency, expense_id, user_id, is_income
|
|
||||||
) VALUES ( $1, $2, $3, $4, $5, $6, $7 )
|
|
||||||
RETURNING id, expense_id, user_id, amount, currency, is_income, created_at, updated_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type InsertTransactionParams struct {
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
Amount int32
|
|
||||||
Currency string
|
|
||||||
ExpenseID int32
|
|
||||||
UserID int32
|
|
||||||
IsIncome bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) InsertTransaction(ctx context.Context, arg InsertTransactionParams) error {
|
|
||||||
_, err := q.db.ExecContext(ctx, insertTransaction,
|
|
||||||
arg.CreatedAt,
|
|
||||||
arg.UpdatedAt,
|
|
||||||
arg.Amount,
|
|
||||||
arg.Currency,
|
|
||||||
arg.ExpenseID,
|
|
||||||
arg.UserID,
|
|
||||||
arg.IsIncome,
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
-- name: InsertUser :one
|
|
||||||
INSERT INTO "user" (
|
|
||||||
email, first_name, last_name, password, created_at, updated_at
|
|
||||||
) VALUES ( $1, $2, $3, $4, $5, $6 )
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: GetUserByEmail :one
|
|
||||||
SELECT id, email, first_name, last_name, password, created_at, updated_at
|
|
||||||
FROM "user"
|
|
||||||
WHERE email = $1;
|
|
||||||
|
|
||||||
-- name: GetUserByID :one
|
|
||||||
SELECT id, email, first_name, last_name, password, created_at, updated_at
|
|
||||||
FROM "user"
|
|
||||||
WHERE id = $1;
|
|
@ -1,91 +0,0 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.27.0
|
|
||||||
// source: user.sql
|
|
||||||
|
|
||||||
package sqlc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const getUserByEmail = `-- name: GetUserByEmail :one
|
|
||||||
SELECT id, email, first_name, last_name, password, created_at, updated_at
|
|
||||||
FROM "user"
|
|
||||||
WHERE email = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, getUserByEmail, email)
|
|
||||||
var i User
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Email,
|
|
||||||
&i.FirstName,
|
|
||||||
&i.LastName,
|
|
||||||
&i.Password,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getUserByID = `-- name: GetUserByID :one
|
|
||||||
SELECT id, email, first_name, last_name, password, created_at, updated_at
|
|
||||||
FROM "user"
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetUserByID(ctx context.Context, id int32) (User, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, getUserByID, id)
|
|
||||||
var i User
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Email,
|
|
||||||
&i.FirstName,
|
|
||||||
&i.LastName,
|
|
||||||
&i.Password,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertUser = `-- name: InsertUser :one
|
|
||||||
INSERT INTO "user" (
|
|
||||||
email, first_name, last_name, password, created_at, updated_at
|
|
||||||
) VALUES ( $1, $2, $3, $4, $5, $6 )
|
|
||||||
RETURNING id, email, first_name, last_name, password, created_at, updated_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type InsertUserParams struct {
|
|
||||||
Email string
|
|
||||||
FirstName string
|
|
||||||
LastName string
|
|
||||||
Password string
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) InsertUser(ctx context.Context, arg InsertUserParams) (User, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, insertUser,
|
|
||||||
arg.Email,
|
|
||||||
arg.FirstName,
|
|
||||||
arg.LastName,
|
|
||||||
arg.Password,
|
|
||||||
arg.CreatedAt,
|
|
||||||
arg.UpdatedAt,
|
|
||||||
)
|
|
||||||
var i User
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Email,
|
|
||||||
&i.FirstName,
|
|
||||||
&i.LastName,
|
|
||||||
&i.Password,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
@ -1,133 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package repo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/repo/sqlc"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/repo"
|
|
||||||
)
|
|
||||||
|
|
||||||
type userRepository struct {
|
|
||||||
queries *sqlc.Queries
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUserRepository(db *sql.DB) repo.UserRepository {
|
|
||||||
return &userRepository{
|
|
||||||
queries: sqlc.New(db),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create
|
|
||||||
func (u *userRepository) Create(
|
|
||||||
ctx context.Context,
|
|
||||||
userEntity *model.UserEntity,
|
|
||||||
tx any,
|
|
||||||
) (*model.UserEntity, error) {
|
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
queries := getQueries(u.queries, tx)
|
|
||||||
|
|
||||||
userDB, err := queries.InsertUser(timeoutCtx, sqlc.InsertUserParams{
|
|
||||||
Email: userEntity.Email,
|
|
||||||
FirstName: userEntity.FirstName,
|
|
||||||
LastName: userEntity.LastName,
|
|
||||||
Password: userEntity.Password,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &model.UserEntity{
|
|
||||||
ID: int(userDB.ID),
|
|
||||||
Email: userDB.Email,
|
|
||||||
FirstName: userDB.FirstName,
|
|
||||||
LastName: userDB.LastName,
|
|
||||||
Password: userDB.Password,
|
|
||||||
CreatedAt: userDB.CreatedAt,
|
|
||||||
UpdatedAt: userDB.CreatedAt,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetByEmail if not found, return nil for user but not error.
|
|
||||||
func (u *userRepository) GetByEmail(
|
|
||||||
ctx context.Context,
|
|
||||||
email string,
|
|
||||||
tx any,
|
|
||||||
) (*model.UserEntity, error) {
|
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
queries := getQueries(u.queries, tx)
|
|
||||||
|
|
||||||
userDB, err := queries.GetUserByEmail(timeoutCtx, email)
|
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
|
||||||
// No query error, but user not found
|
|
||||||
return nil, nil
|
|
||||||
} else if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &model.UserEntity{
|
|
||||||
ID: int(userDB.ID),
|
|
||||||
Email: userDB.Email,
|
|
||||||
FirstName: userDB.FirstName,
|
|
||||||
LastName: userDB.LastName,
|
|
||||||
Password: userDB.Password,
|
|
||||||
CreatedAt: userDB.CreatedAt,
|
|
||||||
UpdatedAt: userDB.CreatedAt,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userRepository) GetByID(ctx context.Context, id int, tx any) (*model.UserEntity, error) {
|
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
queries := getQueries(u.queries, tx)
|
|
||||||
|
|
||||||
userDB, err := queries.GetUserByID(timeoutCtx, int32(id))
|
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
|
||||||
// No query error, but user not found
|
|
||||||
return nil, nil
|
|
||||||
} else if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &model.UserEntity{
|
|
||||||
ID: int(userDB.ID),
|
|
||||||
Email: userDB.Email,
|
|
||||||
FirstName: userDB.FirstName,
|
|
||||||
LastName: userDB.LastName,
|
|
||||||
Password: userDB.Password,
|
|
||||||
CreatedAt: userDB.CreatedAt,
|
|
||||||
UpdatedAt: userDB.CreatedAt,
|
|
||||||
}, nil
|
|
||||||
}
|
|
@ -23,12 +23,11 @@
|
|||||||
package howmuch
|
package howmuch
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/log"
|
|
||||||
"github.com/fsnotify/fsnotify"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
@ -40,25 +39,8 @@ const (
|
|||||||
configType = "yaml"
|
configType = "yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
func defaultConfig() {
|
|
||||||
viper.SetDefault("dev-mode", true)
|
|
||||||
|
|
||||||
// web
|
|
||||||
viper.SetDefault("web.addr", ":8000")
|
|
||||||
viper.SetDefault("web.shutdown-timeout", "10")
|
|
||||||
|
|
||||||
// db
|
|
||||||
viper.SetDefault("db.host", "localhost")
|
|
||||||
viper.SetDefault("db.port", 5432)
|
|
||||||
viper.SetDefault("db.username", "postgres")
|
|
||||||
viper.SetDefault("db.password", "example")
|
|
||||||
viper.SetDefault("db.database", "howmuch")
|
|
||||||
viper.SetDefault("db.sslmode", "disable")
|
|
||||||
}
|
|
||||||
|
|
||||||
// initConfig reads in config file and ENV variables if set.
|
// initConfig reads in config file and ENV variables if set.
|
||||||
func initConfig() {
|
func initConfig() {
|
||||||
defaultConfig()
|
|
||||||
if cfgFile != "" {
|
if cfgFile != "" {
|
||||||
// Use config file from the flag.
|
// Use config file from the flag.
|
||||||
viper.SetConfigFile(cfgFile)
|
viper.SetConfigFile(cfgFile)
|
||||||
@ -82,27 +64,8 @@ func initConfig() {
|
|||||||
viper.SetEnvKeyReplacer(replacer)
|
viper.SetEnvKeyReplacer(replacer)
|
||||||
|
|
||||||
if err := viper.ReadInConfig(); err != nil {
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
log.ErrorLog("Failed to read viper configuration file, use default config", "err", err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// watching reloading conf
|
fmt.Fprintln(os.Stdout, "Using config file:", viper.ConfigFileUsed())
|
||||||
viper.OnConfigChange(func(e fsnotify.Event) {
|
|
||||||
log.InfoLog("Config file changed:", e.Name)
|
|
||||||
})
|
|
||||||
viper.WatchConfig()
|
|
||||||
|
|
||||||
log.DebugLog("Using config file", "file", viper.ConfigFileUsed())
|
|
||||||
}
|
|
||||||
|
|
||||||
// logOptions set log options from the configs read by viper.
|
|
||||||
func logOptions() *log.Options {
|
|
||||||
return &log.Options{
|
|
||||||
Level: viper.GetString("log.level"),
|
|
||||||
Development: viper.GetBool("dev-mode"),
|
|
||||||
DisableCaller: viper.GetBool("log.disable-caller"),
|
|
||||||
DisableStacktrace: viper.GetBool("log.disable-stacktrace"),
|
|
||||||
Format: viper.GetString("log.format"),
|
|
||||||
OutputPaths: viper.GetStringSlice("log.output-paths"),
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -23,25 +23,14 @@
|
|||||||
package howmuch
|
package howmuch
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/infra/datastore"
|
"github.com/fsnotify/fsnotify"
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/infra/router"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/registry"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/log"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/token"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/pkg/version/verflag"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"golang.org/x/net/context"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var cfgFile string
|
var cfgFile string
|
||||||
@ -58,14 +47,6 @@ func NewHowMuchCommand() *cobra.Command {
|
|||||||
Long: `howmuch is a expense-sharing application that can help friends
|
Long: `howmuch is a expense-sharing application that can help friends
|
||||||
to share their expense of an event or a trip`,
|
to share their expense of an event or a trip`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
// print version and exit?
|
|
||||||
verflag.PrintVersion()
|
|
||||||
|
|
||||||
// init log
|
|
||||||
log.Init(logOptions())
|
|
||||||
// Sync flush the buffer
|
|
||||||
defer log.Sync()
|
|
||||||
|
|
||||||
return run()
|
return run()
|
||||||
},
|
},
|
||||||
Args: func(cmd *cobra.Command, args []string) error {
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
@ -91,94 +72,32 @@ to share their expense of an event or a trip`,
|
|||||||
// when this action is called directly.
|
// when this action is called directly.
|
||||||
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||||
|
|
||||||
// --version
|
|
||||||
verflag.AddFlags(rootCmd.PersistentFlags())
|
|
||||||
|
|
||||||
return rootCmd
|
return rootCmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func run() error {
|
func run() error {
|
||||||
// Set Gin running mode
|
fmt.Println("How much do I owe you?")
|
||||||
isDev := viper.GetBool("dev-mode")
|
|
||||||
if isDev {
|
|
||||||
gin.SetMode(gin.DebugMode)
|
|
||||||
} else {
|
|
||||||
gin.SetMode(gin.ReleaseMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init DB
|
settings, _ := json.MarshalIndent(viper.AllSettings(), "", " ")
|
||||||
dbConfString := fmt.Sprintf(
|
|
||||||
"host=%s port=%d dbname=%s user=%s password=%s sslmode=%s",
|
|
||||||
viper.GetString("db.host"),
|
|
||||||
viper.GetInt("db.port"),
|
|
||||||
viper.GetString("db.database"),
|
|
||||||
viper.GetString("db.username"),
|
|
||||||
viper.GetString("db.password"),
|
|
||||||
viper.GetString("db.sslmode"),
|
|
||||||
)
|
|
||||||
// TODO: viper conf should be parsed into a struct directly
|
|
||||||
dbExtraConf := &datastore.DbExtraConf{
|
|
||||||
MaxOpenConns: viper.GetInt("db.max-open-conns"),
|
|
||||||
MaxIdleConns: viper.GetInt("db.max-idle-conns"),
|
|
||||||
MaxLifetime: viper.GetDuration("db.max-lifetime"),
|
|
||||||
}
|
|
||||||
dbConn := datastore.NewDB(dbConfString, dbExtraConf)
|
|
||||||
if dbConn == nil {
|
|
||||||
log.FatalLog("DB connection failure")
|
|
||||||
}
|
|
||||||
defer dbConn.Close()
|
|
||||||
|
|
||||||
// Init Cache
|
// watching reloading conf
|
||||||
cache := datastore.NewCache(&redis.Options{
|
viper.OnConfigChange(func(e fsnotify.Event) {
|
||||||
Addr: viper.GetString("cache.host"),
|
fmt.Println("Config file changed:", e.Name)
|
||||||
Password: viper.GetString("cache.password"),
|
|
||||||
DB: 0,
|
|
||||||
})
|
})
|
||||||
defer cache.Close()
|
viper.WatchConfig()
|
||||||
|
|
||||||
// Init token
|
fmt.Println(string(settings))
|
||||||
token.Init(viper.GetString("web.token-secret"), viper.GetDuration("web.token-expiry-time"))
|
fmt.Println(viper.GetString("db.username"))
|
||||||
|
|
||||||
// Register the core service
|
r := gin.Default()
|
||||||
r := registry.NewRegistry(dbConn, cache)
|
|
||||||
|
|
||||||
engine := gin.Default()
|
r.GET("/", func(ctx *gin.Context) {
|
||||||
|
ctx.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "how much?",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
engine = router.Routes(engine, r.NewAppController(), cache)
|
r.Run(":8080")
|
||||||
|
|
||||||
server := http.Server{
|
|
||||||
Addr: viper.GetString("web.addr"),
|
|
||||||
Handler: engine,
|
|
||||||
}
|
|
||||||
|
|
||||||
log.InfoLog("Server running", "port", viper.GetString("web.addr"))
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
|
|
||||||
log.FatalLog(err.Error())
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
signalChan := make(chan os.Signal, 1)
|
|
||||||
|
|
||||||
signal.Notify(signalChan, syscall.SIGTERM, syscall.SIGINT)
|
|
||||||
<-signalChan
|
|
||||||
|
|
||||||
shutdownTimeout := viper.GetDuration("web.shutdown-timeout")
|
|
||||||
log.DebugLog("Shutdown", "timeout", shutdownTimeout)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(
|
|
||||||
context.Background(),
|
|
||||||
shutdownTimeout*time.Second,
|
|
||||||
)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if err := server.Shutdown(ctx); err != nil {
|
|
||||||
log.ErrorLog("Server forced shutdown", "err", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.InfoLog("Ciao!")
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1,66 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package datastore
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/core"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/log"
|
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RedisCache struct {
|
|
||||||
redis *redis.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCache(opt interface{}) core.Cache {
|
|
||||||
redisOpt := opt.(*redis.Options)
|
|
||||||
return &RedisCache{redis.NewClient(redisOpt)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *RedisCache) Get(ctx context.Context, key string) (string, error) {
|
|
||||||
val, err := c.redis.Get(ctx, key).Result()
|
|
||||||
if err == redis.Nil {
|
|
||||||
log.DebugLog("redis key not found", "key", key)
|
|
||||||
return "", nil
|
|
||||||
} else if err != nil {
|
|
||||||
log.DebugLog("redis cache get error", "err", err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return val, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *RedisCache) Set(
|
|
||||||
ctx context.Context,
|
|
||||||
key string,
|
|
||||||
value interface{},
|
|
||||||
expiration time.Duration,
|
|
||||||
) error {
|
|
||||||
return c.redis.Set(ctx, key, value, expiration).Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *RedisCache) Close() error {
|
|
||||||
return c.redis.Close()
|
|
||||||
}
|
|
@ -1,87 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package datastore
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/log"
|
|
||||||
_ "github.com/jackc/pgx/v5"
|
|
||||||
_ "github.com/jackc/pgx/v5/stdlib"
|
|
||||||
)
|
|
||||||
|
|
||||||
var counts int
|
|
||||||
|
|
||||||
type DbExtraConf struct {
|
|
||||||
MaxOpenConns int
|
|
||||||
MaxIdleConns int
|
|
||||||
MaxLifetime time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDB creates a new database for the application
|
|
||||||
func NewDB(dsn string, opts interface{}) *sql.DB {
|
|
||||||
var db *sql.DB
|
|
||||||
var err error
|
|
||||||
|
|
||||||
for {
|
|
||||||
db, err = openDB(dsn)
|
|
||||||
if err != nil {
|
|
||||||
log.WarnLog("postgres not ready", "err", err)
|
|
||||||
counts++
|
|
||||||
} else {
|
|
||||||
log.InfoLog("connected to postgres")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if counts > 10 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
log.InfoLog("retry in 2 seconds")
|
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
if db == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
extraConf, ok := opts.(DbExtraConf)
|
|
||||||
if ok {
|
|
||||||
db.SetMaxOpenConns(extraConf.MaxOpenConns)
|
|
||||||
db.SetMaxIdleConns(extraConf.MaxIdleConns)
|
|
||||||
db.SetConnMaxLifetime(extraConf.MaxLifetime)
|
|
||||||
}
|
|
||||||
return db
|
|
||||||
}
|
|
||||||
|
|
||||||
func openDB(dsn string) (*sql.DB, error) {
|
|
||||||
db, err := sql.Open("pgx", dsn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := db.Ping(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return db, nil
|
|
||||||
}
|
|
@ -1,84 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/controller"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/core"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/errno"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/middleware"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/middleware/authn"
|
|
||||||
"github.com/gin-contrib/cors"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Routes can take some options to init middlewares.
|
|
||||||
// - Cache
|
|
||||||
func Routes(engine *gin.Engine, c controller.AppController, opt ...interface{}) *gin.Engine {
|
|
||||||
cache, ok := opt[0].(core.Cache)
|
|
||||||
if !ok {
|
|
||||||
panic("the first option must be a cache driver")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Middlewares
|
|
||||||
// Cors
|
|
||||||
corsCfg := cors.DefaultConfig()
|
|
||||||
corsCfg.AllowAllOrigins = true
|
|
||||||
corsCfg.AllowHeaders = append(corsCfg.AllowHeaders, "Authorization", "Accept", "X-CSRF-Token")
|
|
||||||
engine.Use(cors.New(corsCfg))
|
|
||||||
|
|
||||||
// Use my request id middleware
|
|
||||||
// TODO: I might use the community version later
|
|
||||||
engine.Use(middleware.RequestID())
|
|
||||||
|
|
||||||
// Route for the 404 error
|
|
||||||
engine.NoRoute(func(ctx *gin.Context) {
|
|
||||||
core.WriteResponse(ctx, errno.PageNotFoundErr, nil)
|
|
||||||
})
|
|
||||||
|
|
||||||
v1 := engine.Group("/v1")
|
|
||||||
{
|
|
||||||
userV1 := v1.Group("/user")
|
|
||||||
{
|
|
||||||
userV1.POST("/create", func(ctx *gin.Context) { c.User.Create(ctx) })
|
|
||||||
|
|
||||||
userV1.Use(authn.Authn(cache))
|
|
||||||
userV1.GET(
|
|
||||||
":id/info",
|
|
||||||
func(ctx *gin.Context) { ctx.JSON(http.StatusOK, "Hello world") },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionV1 := v1.Group("/session")
|
|
||||||
{
|
|
||||||
sessionV1.POST("/create", func(ctx *gin.Context) { c.Session.Create(ctx) })
|
|
||||||
sessionV1.Use(authn.Authn(cache))
|
|
||||||
sessionV1.POST("/delete", func(ctx *gin.Context) { c.Session.Delete(ctx) })
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return engine
|
|
||||||
}
|
|
@ -1,136 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package model
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// {{{ Request Object (from controller to service)
|
|
||||||
|
|
||||||
type EventCreateRequest struct {
|
|
||||||
Name string `json:"name" binding:"requiered"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
OwnerID int `json:"owner_id" binding:"requiered,number"`
|
|
||||||
DefaultCurrency Currency `json:"currency"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// }}}
|
|
||||||
// {{{ Response View Object (from service to controller)
|
|
||||||
|
|
||||||
type EventListResponse struct {
|
|
||||||
ID int
|
|
||||||
Name string
|
|
||||||
Description string
|
|
||||||
Owner *UserBaseResponse
|
|
||||||
CreatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type EventInfoResponse struct {
|
|
||||||
ID int
|
|
||||||
|
|
||||||
Name string
|
|
||||||
Description string
|
|
||||||
|
|
||||||
TotalAmount Money
|
|
||||||
|
|
||||||
Owner *UserBaseResponse
|
|
||||||
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
|
|
||||||
Users []UserBaseResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
// }}}
|
|
||||||
// {{{ Entity (DB In)
|
|
||||||
|
|
||||||
type EventEntity struct {
|
|
||||||
ID int
|
|
||||||
|
|
||||||
Name string
|
|
||||||
Description string
|
|
||||||
TotalAmount int
|
|
||||||
DefaultCurrency string
|
|
||||||
OwnerID int
|
|
||||||
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type EventUpdateEntity struct {
|
|
||||||
ID int
|
|
||||||
Name string
|
|
||||||
Description string
|
|
||||||
CreatedAt time.Time
|
|
||||||
// TODO: maybe I can change owner too
|
|
||||||
}
|
|
||||||
|
|
||||||
// }}}
|
|
||||||
// {{{ Retrieved (DB out)
|
|
||||||
|
|
||||||
type EventRetrieved struct {
|
|
||||||
ID int
|
|
||||||
|
|
||||||
Name string
|
|
||||||
Description string
|
|
||||||
|
|
||||||
Users []UserBaseRetrieved
|
|
||||||
|
|
||||||
TotalAmount Money
|
|
||||||
DefaultCurrency Currency
|
|
||||||
Owner *UserBaseRetrieved
|
|
||||||
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type EventListRetrieved struct {
|
|
||||||
ID int
|
|
||||||
Name string
|
|
||||||
Description string
|
|
||||||
CreatedAt time.Time
|
|
||||||
Owner *UserBaseRetrieved
|
|
||||||
}
|
|
||||||
|
|
||||||
// }}}
|
|
||||||
// {{{ DO Domain Object (Contains the domain service)
|
|
||||||
|
|
||||||
type Event struct {
|
|
||||||
ID int
|
|
||||||
|
|
||||||
Name string
|
|
||||||
Description string
|
|
||||||
|
|
||||||
// lazy get using participation join
|
|
||||||
Users []UserDO
|
|
||||||
// lazy get
|
|
||||||
Expenses []Expense
|
|
||||||
|
|
||||||
TotalAmount Money
|
|
||||||
DefaultCurrency Currency
|
|
||||||
Owner *UserDO
|
|
||||||
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// }}}
|
|
@ -1,142 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package model
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// {{{ Requrest
|
|
||||||
|
|
||||||
type ExpenseRequest struct {
|
|
||||||
Amount Money `json:"money" binding:"required"`
|
|
||||||
Payments []Payment `json:"payments" binding:"required"`
|
|
||||||
Benefits []Benefit `json:"benefits" binding:"required"`
|
|
||||||
EventID int `json:"event_id" binding:"required"`
|
|
||||||
Detail ExpenseDetail `json:"detail"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// }}}
|
|
||||||
// {{{ Response
|
|
||||||
|
|
||||||
type (
|
|
||||||
ExpenseGetResponse Expense
|
|
||||||
ExpenseResponse ExpenseRetrieved
|
|
||||||
)
|
|
||||||
|
|
||||||
// }}}
|
|
||||||
// {{{ Retrieved
|
|
||||||
|
|
||||||
type ExpenseRetrieved Expense
|
|
||||||
|
|
||||||
type ExpensesListRetrieved struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
|
|
||||||
Amount Money `json:"money"`
|
|
||||||
EventID int `json:"event_id"`
|
|
||||||
|
|
||||||
Detail ExpenseDetail `json:"detail"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PaymentRetrieved struct {
|
|
||||||
PayerID int `json:"payer_id"`
|
|
||||||
PayerFirstName string `json:"payer_first_name"`
|
|
||||||
PayerLastName string `json:"payer_last_name"`
|
|
||||||
Amount int `json:"amount"`
|
|
||||||
Currency string `json:"currency"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BenefitRetrieved struct {
|
|
||||||
RecipientID int `json:"recipient_id"`
|
|
||||||
RecipientFirstName string `json:"recipient_first_name"`
|
|
||||||
RecipientLastName string `json:"recipient_last_name"`
|
|
||||||
Amount int `json:"amount"`
|
|
||||||
Currency string `json:"currency"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// }}}
|
|
||||||
// {{{ Entity
|
|
||||||
|
|
||||||
type ExpenseEntity struct {
|
|
||||||
ID int
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
|
|
||||||
Amount int
|
|
||||||
Currency string
|
|
||||||
EventID int
|
|
||||||
|
|
||||||
// ExpenseDetail
|
|
||||||
Name string
|
|
||||||
Place string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExpenseUpdateEntity struct {
|
|
||||||
ID int
|
|
||||||
UpdatedAt time.Time
|
|
||||||
|
|
||||||
Amount int
|
|
||||||
Currency string
|
|
||||||
|
|
||||||
// Expense Detail
|
|
||||||
Name string
|
|
||||||
Place string
|
|
||||||
}
|
|
||||||
|
|
||||||
// }}}
|
|
||||||
// {{{ Domain Models
|
|
||||||
|
|
||||||
type ExpenseDetail struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Place string `json:"place"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Payment struct {
|
|
||||||
PayerID int `json:"payer_id" binding:"required,number"`
|
|
||||||
PayerFirstName string `json:"payer_first_name"`
|
|
||||||
PayerLastName string `json:"payer_last_name"`
|
|
||||||
Amount Money `json:"amount" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Benefit struct {
|
|
||||||
RecipientID int `json:"recipient_id" binding:"required,number"`
|
|
||||||
RecipientFirstName string `json:"recipient_first_name"`
|
|
||||||
RecipientLastName string `json:"recipient_last_name"`
|
|
||||||
Amount Money `json:"amount" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Expense struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
|
|
||||||
Amount Money `json:"money"`
|
|
||||||
EventID int `json:"event_id"`
|
|
||||||
|
|
||||||
Detail ExpenseDetail `json:"detail"`
|
|
||||||
|
|
||||||
Payments []Payment `json:"payments"`
|
|
||||||
Benefits []Benefit `json:"benefits"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// }}}
|
|
@ -1,66 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package model
|
|
||||||
|
|
||||||
type Currency string
|
|
||||||
|
|
||||||
// TODO: may handle a more complexe logic with the exchange rate.
|
|
||||||
|
|
||||||
// XXX: Here we suppose that the currency is the same for every piece
|
|
||||||
// of money involved in the calculate.
|
|
||||||
|
|
||||||
const (
|
|
||||||
EUR Currency = "EUR"
|
|
||||||
USD Currency = "USD"
|
|
||||||
CNY Currency = "CNY"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Money struct {
|
|
||||||
Amount int `json:"amount" binding:"required,number"`
|
|
||||||
Currency Currency `json:"currency" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func MakeMoney(amount int, currency Currency) Money {
|
|
||||||
return Money{amount, currency}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Add(cur Currency, money ...Money) Money {
|
|
||||||
var sum Money
|
|
||||||
sum.Currency = cur
|
|
||||||
|
|
||||||
for _, m := range money {
|
|
||||||
sum.Amount += m.Amount
|
|
||||||
}
|
|
||||||
|
|
||||||
return sum
|
|
||||||
}
|
|
||||||
|
|
||||||
func Diff(cur Currency, money1 Money, money2 Money) Money {
|
|
||||||
var diff Money
|
|
||||||
|
|
||||||
diff.Currency = cur
|
|
||||||
|
|
||||||
diff.Amount = money1.Amount - money2.Amount
|
|
||||||
|
|
||||||
return diff
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package model
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type ParticipationEntity Participation
|
|
||||||
|
|
||||||
// Participation is the association between Users and Events
|
|
||||||
type Participation struct {
|
|
||||||
ID int
|
|
||||||
|
|
||||||
UserID int
|
|
||||||
EventID int
|
|
||||||
InvitedByUserID int
|
|
||||||
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package model
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// {{{ Entity
|
|
||||||
|
|
||||||
type TransactionEntity Transaction
|
|
||||||
|
|
||||||
// }}}
|
|
||||||
// {{{ Domain object
|
|
||||||
|
|
||||||
type Transaction struct {
|
|
||||||
ID int
|
|
||||||
|
|
||||||
ExpenseID int
|
|
||||||
UserID int
|
|
||||||
Amount int
|
|
||||||
Currency string
|
|
||||||
IsIncome bool // To note that the direction of the money (payment or income)
|
|
||||||
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// }}}
|
|
||||||
// Transaction is the association between Expenses and Users
|
|
@ -1,100 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package model
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// {{{ Request (from controller to service)
|
|
||||||
|
|
||||||
type UserCreateRequest struct {
|
|
||||||
Email string `json:"email" binding:"required,email"`
|
|
||||||
FirstName string `json:"first_name" binding:"required"`
|
|
||||||
LastName string `json:"last_name" binding:"required"`
|
|
||||||
Password string `json:"password" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserExistRequest struct {
|
|
||||||
Email string `json:"email" binding:"required,email"`
|
|
||||||
Password string `json:"password" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// }}}
|
|
||||||
// {{{ Response View Object (from service to controller)
|
|
||||||
|
|
||||||
type UserBaseResponse UserBaseRetrieved
|
|
||||||
|
|
||||||
type UserInfoResponse struct {
|
|
||||||
// UserBaseResponse
|
|
||||||
ID int `json:"id"`
|
|
||||||
FirstName string `json:"first_name"`
|
|
||||||
LastName string `json:"last_name"`
|
|
||||||
|
|
||||||
Email string `json:"email"`
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// }}}
|
|
||||||
// {{{ Entity (DB In)
|
|
||||||
|
|
||||||
type UserEntity struct {
|
|
||||||
ID int
|
|
||||||
|
|
||||||
Email string
|
|
||||||
FirstName string
|
|
||||||
LastName string
|
|
||||||
Password string
|
|
||||||
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// }}}
|
|
||||||
// {{{ Retrieved (DB out)
|
|
||||||
|
|
||||||
type UserBaseRetrieved struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
FirstName string `json:"first_name"`
|
|
||||||
LastName string `json:"last_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// }}}
|
|
||||||
// {{{ DO Domain Object (Contains the domain service)
|
|
||||||
|
|
||||||
// TODO: For now I don't know what to do with this model
|
|
||||||
type UserDO struct {
|
|
||||||
ID int
|
|
||||||
|
|
||||||
Email string
|
|
||||||
FirstName string
|
|
||||||
LastName string
|
|
||||||
Password string
|
|
||||||
|
|
||||||
// Lazy aggregate with the Participation join
|
|
||||||
EventIDs []int
|
|
||||||
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// }}}
|
|
@ -1,30 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package registry
|
|
||||||
|
|
||||||
import "git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/controller"
|
|
||||||
|
|
||||||
// NewUserController returns an admin controller's implementation
|
|
||||||
func (r *registry) NewAdminController() controller.Admin {
|
|
||||||
return &controller.AdminController{}
|
|
||||||
}
|
|
@ -1,61 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/controller"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/core"
|
|
||||||
)
|
|
||||||
|
|
||||||
// registry is an implementation of Registry interface.
|
|
||||||
// It needs a db connection to provide the necessary support.
|
|
||||||
// It might holds other drivers when the projects grows. For example
|
|
||||||
// the object needed to connect to Redis or Kafka.
|
|
||||||
type registry struct {
|
|
||||||
db *sql.DB
|
|
||||||
cache core.Cache
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registry returns a new app controller that will be used by main()/run()
|
|
||||||
// AppController is essentially the struct that holds all the controllers,
|
|
||||||
// classed by their domains.
|
|
||||||
type Registry interface {
|
|
||||||
NewAppController() controller.AppController
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRegistry returns a new Registry's implementation.
|
|
||||||
func NewRegistry(db *sql.DB, cache core.Cache) Registry {
|
|
||||||
return ®istry{db: db, cache: cache}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAppController creates a new AppController with controller struct for
|
|
||||||
// each domain.
|
|
||||||
func (r *registry) NewAppController() controller.AppController {
|
|
||||||
return controller.AppController{
|
|
||||||
User: r.NewUserController(),
|
|
||||||
Admin: r.NewAdminController(),
|
|
||||||
Session: r.NewSessionController(),
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/controller"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/repo"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/usecase"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewSessionController returns a session controller's implementation
|
|
||||||
func (r *registry) NewSessionController() controller.Session {
|
|
||||||
u := usecase.NewUserUsecase(repo.NewUserRepository(r.db), repo.NewDBRepository(r.db))
|
|
||||||
return controller.NewSessionController(u, r.cache)
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/controller"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/repo"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/usecase"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewUserController returns a user controller's implementation
|
|
||||||
func (r *registry) NewUserController() controller.User {
|
|
||||||
u := usecase.NewUserUsecase(repo.NewUserRepository(r.db), repo.NewDBRepository(r.db))
|
|
||||||
return controller.NewUserController(u)
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package repo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DBRepository interface {
|
|
||||||
Transaction(
|
|
||||||
ctx context.Context,
|
|
||||||
txFunc func(txCtx context.Context, tx interface{}) (interface{}, error),
|
|
||||||
) (interface{}, error)
|
|
||||||
}
|
|
@ -1,74 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package repo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
type EventRepository interface {
|
|
||||||
Create(ctx context.Context, evEntity *model.EventEntity, tx any) (*model.EventEntity, error)
|
|
||||||
|
|
||||||
// UpdateEventByID updates the event related information (name, descriptions)
|
|
||||||
UpdateEventByID(ctx context.Context, event *model.EventUpdateEntity, tx any) error
|
|
||||||
|
|
||||||
GetByID(ctx context.Context, eventID int, tx any) (*model.EventRetrieved, error)
|
|
||||||
|
|
||||||
// related to events of a user
|
|
||||||
ListEventsByUserID(ctx context.Context, userID int, tx any) ([]model.EventListRetrieved, error)
|
|
||||||
|
|
||||||
InsertParticipation(
|
|
||||||
ctx context.Context,
|
|
||||||
userID, eventID, invitedByUserID int,
|
|
||||||
tx any,
|
|
||||||
) (*model.ParticipationEntity, error)
|
|
||||||
|
|
||||||
GetParticipation(
|
|
||||||
ctx context.Context,
|
|
||||||
userID, eventID int,
|
|
||||||
tx any,
|
|
||||||
) (*model.ParticipationEntity, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExpenseRepository interface {
|
|
||||||
DeleteExpense(ctx context.Context, expenseID int, tx any) error
|
|
||||||
DeleteTransactionsOfExpense(ctx context.Context, expenseID int, tx any) error
|
|
||||||
GetExpenseByID(ctx context.Context, expenseID int, tx any) (*model.ExpenseRetrieved, error)
|
|
||||||
InsertExpense(
|
|
||||||
ctx context.Context,
|
|
||||||
expenseEntity *model.ExpenseEntity,
|
|
||||||
tx any,
|
|
||||||
) (*model.ExpenseEntity, error)
|
|
||||||
ListExpensesByEventID(
|
|
||||||
ctx context.Context,
|
|
||||||
id int,
|
|
||||||
tx any,
|
|
||||||
) ([]model.ExpensesListRetrieved, error)
|
|
||||||
UpdateExpenseByID(
|
|
||||||
ctx context.Context,
|
|
||||||
expenseUpdate *model.ExpenseUpdateEntity,
|
|
||||||
tx any,
|
|
||||||
) (*model.ExpenseEntity, error)
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package repo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UserRepository interface {
|
|
||||||
Create(
|
|
||||||
ctx context.Context,
|
|
||||||
u *model.UserEntity,
|
|
||||||
tx any,
|
|
||||||
) (*model.UserEntity, error)
|
|
||||||
GetByEmail(ctx context.Context, email string, tx any) (*model.UserEntity, error)
|
|
||||||
GetByID(ctx context.Context, id int, tx any) (*model.UserEntity, error)
|
|
||||||
}
|
|
@ -1,190 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package usecase
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/repo"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/errno"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/log"
|
|
||||||
"golang.org/x/net/context"
|
|
||||||
)
|
|
||||||
|
|
||||||
type eventUsecase struct {
|
|
||||||
userUC User
|
|
||||||
eventRepo repo.EventRepository
|
|
||||||
expenseRepo repo.ExpenseRepository
|
|
||||||
|
|
||||||
dbRepo repo.DBRepository
|
|
||||||
}
|
|
||||||
|
|
||||||
var ErrNoParticipation = &errno.Errno{
|
|
||||||
HTTP: http.StatusUnauthorized,
|
|
||||||
Code: errno.ErrorCode(errno.AuthFailureCode, "NoParticipation"),
|
|
||||||
Message: "user doesn't have access to this event",
|
|
||||||
}
|
|
||||||
|
|
||||||
// For the controller
|
|
||||||
type Event interface{}
|
|
||||||
|
|
||||||
func NewEventUsecase(
|
|
||||||
uuc User,
|
|
||||||
ev repo.EventRepository,
|
|
||||||
ex repo.ExpenseRepository,
|
|
||||||
db repo.DBRepository,
|
|
||||||
) Event {
|
|
||||||
return &eventUsecase{uuc, ev, ex, db}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (evuc *eventUsecase) CreateEvent(
|
|
||||||
ctx context.Context,
|
|
||||||
evRequest *model.EventCreateRequest,
|
|
||||||
) (*model.EventInfoResponse, error) {
|
|
||||||
// transfer evRequest to evEntity
|
|
||||||
|
|
||||||
evEntity := &model.EventEntity{
|
|
||||||
Name: evRequest.Name,
|
|
||||||
Description: evRequest.Description,
|
|
||||||
OwnerID: evRequest.OwnerID,
|
|
||||||
TotalAmount: 0,
|
|
||||||
DefaultCurrency: string(evRequest.DefaultCurrency),
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := evuc.dbRepo.Transaction(
|
|
||||||
ctx,
|
|
||||||
func(txCtx context.Context, tx any) (any, error) {
|
|
||||||
// Create the event
|
|
||||||
created, err := evuc.eventRepo.Create(ctx, evEntity, tx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// participate to the event
|
|
||||||
participation, err := evuc.eventRepo.InsertParticipation(
|
|
||||||
ctx,
|
|
||||||
created.OwnerID,
|
|
||||||
created.ID,
|
|
||||||
0,
|
|
||||||
tx,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if participation == nil {
|
|
||||||
// Unexpected error
|
|
||||||
log.ErrorLog(
|
|
||||||
"participation existed for event-user pair",
|
|
||||||
"userID",
|
|
||||||
created.OwnerID,
|
|
||||||
"eventID",
|
|
||||||
created.ID,
|
|
||||||
)
|
|
||||||
return nil, errno.InternalServerErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: App log, maybe can be sent to some third party service.
|
|
||||||
log.InfoLog(
|
|
||||||
"created new event",
|
|
||||||
"name",
|
|
||||||
created.Name,
|
|
||||||
"owner",
|
|
||||||
created.OwnerID,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Construct the response
|
|
||||||
ownerResponse, err := evuc.userUC.GetUserBaseResponseByID(ctx, created.OwnerID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
evResponse := &model.EventInfoResponse{
|
|
||||||
ID: created.ID,
|
|
||||||
Name: created.Name,
|
|
||||||
Description: created.Description,
|
|
||||||
TotalAmount: model.MakeMoney(
|
|
||||||
created.TotalAmount,
|
|
||||||
model.Currency(created.DefaultCurrency),
|
|
||||||
),
|
|
||||||
Owner: ownerResponse,
|
|
||||||
CreatedAt: created.CreatedAt,
|
|
||||||
UpdatedAt: created.UpdatedAt,
|
|
||||||
Users: []model.UserBaseResponse{*ownerResponse},
|
|
||||||
}
|
|
||||||
return evResponse, err
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
res := data.(*model.EventInfoResponse)
|
|
||||||
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (evuc *eventUsecase) ListEvents(
|
|
||||||
ctx context.Context,
|
|
||||||
userID int,
|
|
||||||
) ([]model.EventListResponse, error) {
|
|
||||||
eventListRetrieved, err := evuc.eventRepo.ListEventsByUserID(ctx, userID, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the user is a member of the event
|
|
||||||
|
|
||||||
responses := make([]model.EventListResponse, len(eventListRetrieved))
|
|
||||||
|
|
||||||
for i, retrieved := range eventListRetrieved {
|
|
||||||
ownner := model.UserBaseResponse(*retrieved.Owner)
|
|
||||||
res := model.EventListResponse{
|
|
||||||
ID: retrieved.ID,
|
|
||||||
Name: retrieved.Name,
|
|
||||||
Description: retrieved.Description,
|
|
||||||
Owner: &ownner,
|
|
||||||
CreatedAt: retrieved.CreatedAt,
|
|
||||||
}
|
|
||||||
responses[i] = res
|
|
||||||
}
|
|
||||||
return responses, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEventDetail
|
|
||||||
func (evuc *eventUsecase) GetEventDetail(
|
|
||||||
ctx context.Context,
|
|
||||||
userID, eventID int,
|
|
||||||
) (*model.EventInfoResponse, error) {
|
|
||||||
// Check if the user has the right to get this event
|
|
||||||
// err := evuc.participationRepo.CheckParticipation(ctx, userID, eventID)
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, ErrNoParticipation
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Get the eventDetail
|
|
||||||
// TODO: This can also be put into the cache
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package repomock
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TestDBRepository struct{}
|
|
||||||
|
|
||||||
func (tdr *TestDBRepository) Transaction(
|
|
||||||
ctx context.Context,
|
|
||||||
txFunc func(txCtx context.Context, tx interface{}) (interface{}, error),
|
|
||||||
) (interface{}, error) {
|
|
||||||
return txFunc(ctx, nil)
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package repomock
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
)
|
|
||||||
|
|
||||||
var UserTestDummyErr = errors.New("dummy error")
|
|
||||||
|
|
||||||
type TestUserRepository struct{}
|
|
||||||
|
|
||||||
func (tur *TestUserRepository) Create(
|
|
||||||
ctx context.Context,
|
|
||||||
u *model.UserEntity,
|
|
||||||
tx any,
|
|
||||||
) (*model.UserEntity, error) {
|
|
||||||
user := *u
|
|
||||||
|
|
||||||
user.ID = 123
|
|
||||||
|
|
||||||
if user.Email == "duplicate@error.com" {
|
|
||||||
return nil, errors.New("blabla (SQLSTATE 23505)")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tur *TestUserRepository) GetByEmail(
|
|
||||||
ctx context.Context,
|
|
||||||
email string,
|
|
||||||
tx any,
|
|
||||||
) (*model.UserEntity, error) {
|
|
||||||
hashedPwd, _ := bcrypt.GenerateFromPassword([]byte("strongHashed"), 12)
|
|
||||||
switch email {
|
|
||||||
case "a@b.c":
|
|
||||||
return &model.UserEntity{
|
|
||||||
ID: 123,
|
|
||||||
Email: "a@b.c",
|
|
||||||
Password: string(hashedPwd),
|
|
||||||
}, nil
|
|
||||||
case "query@error.com":
|
|
||||||
return nil, UserTestDummyErr
|
|
||||||
case "inexist@error.com":
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, UserTestDummyErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tur *TestUserRepository) GetByID(
|
|
||||||
ctx context.Context,
|
|
||||||
id int,
|
|
||||||
tx any,
|
|
||||||
) (*model.UserEntity, error) {
|
|
||||||
hashedPwd, _ := bcrypt.GenerateFromPassword([]byte("strongHashed"), 12)
|
|
||||||
switch id {
|
|
||||||
case 123:
|
|
||||||
return &model.UserEntity{
|
|
||||||
ID: 123,
|
|
||||||
Email: "a@b.c",
|
|
||||||
Password: string(hashedPwd),
|
|
||||||
}, nil
|
|
||||||
case 456:
|
|
||||||
return nil, UserTestDummyErr
|
|
||||||
case 789:
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, UserTestDummyErr
|
|
||||||
}
|
|
@ -1,173 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package usecase
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"regexp"
|
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/repo"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/errno"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/log"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
UserExisted = &errno.Errno{
|
|
||||||
HTTP: http.StatusBadRequest,
|
|
||||||
Code: errno.ErrorCode(errno.FailedOperationCode, "UserExisted"),
|
|
||||||
Message: "email already existed.",
|
|
||||||
}
|
|
||||||
UserNotExist = &errno.Errno{
|
|
||||||
HTTP: http.StatusBadRequest,
|
|
||||||
Code: errno.ErrorCode(errno.ResourceNotFoundCode, "UserNotExist"),
|
|
||||||
Message: "user does not exists.",
|
|
||||||
}
|
|
||||||
UserWrongPassword = &errno.Errno{
|
|
||||||
HTTP: http.StatusBadRequest,
|
|
||||||
Code: errno.ErrorCode(errno.AuthFailureCode, "UserWrongPassword"),
|
|
||||||
Message: "wrong password.",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
type userUsecase struct {
|
|
||||||
userRepo repo.UserRepository
|
|
||||||
dbRepo repo.DBRepository
|
|
||||||
}
|
|
||||||
|
|
||||||
type User interface {
|
|
||||||
Create(ctx context.Context, u *model.UserCreateRequest) (*model.UserInfoResponse, error)
|
|
||||||
Exist(ctx context.Context, u *model.UserExistRequest) error
|
|
||||||
GetUserBaseResponseByID(ctx context.Context, userID int) (*model.UserBaseResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUserUsecase(r repo.UserRepository, d repo.DBRepository) User {
|
|
||||||
return &userUsecase{
|
|
||||||
userRepo: r,
|
|
||||||
dbRepo: d,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (uuc *userUsecase) Create(
|
|
||||||
ctx context.Context,
|
|
||||||
u *model.UserCreateRequest,
|
|
||||||
) (*model.UserInfoResponse, error) {
|
|
||||||
// Hash the password
|
|
||||||
encrypted, err := bcrypt.GenerateFromPassword([]byte(u.Password), 12)
|
|
||||||
if err != nil {
|
|
||||||
log.ErrorLog("encrypt password error", "err", err)
|
|
||||||
return nil, errno.InternalServerErr
|
|
||||||
}
|
|
||||||
u.Password = string(encrypted)
|
|
||||||
data, err := uuc.dbRepo.Transaction(
|
|
||||||
ctx,
|
|
||||||
func(txCtx context.Context, tx interface{}) (interface{}, error) {
|
|
||||||
created, err := uuc.userRepo.Create(txCtx, &model.UserEntity{
|
|
||||||
Email: u.Email,
|
|
||||||
Password: u.Password,
|
|
||||||
FirstName: u.FirstName,
|
|
||||||
LastName: u.LastName,
|
|
||||||
}, tx)
|
|
||||||
if err != nil {
|
|
||||||
match, _ := regexp.MatchString("SQLSTATE 23505", err.Error())
|
|
||||||
if match {
|
|
||||||
return nil, UserExisted
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: App log, maybe can be sent to some third party service.
|
|
||||||
log.InfoLog(
|
|
||||||
"created new user",
|
|
||||||
"email",
|
|
||||||
created.Email,
|
|
||||||
"name",
|
|
||||||
fmt.Sprintf("%s %s", created.FirstName, created.LastName),
|
|
||||||
)
|
|
||||||
|
|
||||||
return created, err
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
// TODO: We should wrap the error at service level
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
userEntity := data.(*model.UserEntity)
|
|
||||||
|
|
||||||
user := &model.UserInfoResponse{
|
|
||||||
ID: userEntity.ID,
|
|
||||||
Email: userEntity.Email,
|
|
||||||
FirstName: userEntity.FirstName,
|
|
||||||
LastName: userEntity.LastName,
|
|
||||||
CreatedAt: userEntity.CreatedAt,
|
|
||||||
UpdatedAt: userEntity.UpdatedAt,
|
|
||||||
}
|
|
||||||
|
|
||||||
return user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (uuc *userUsecase) Exist(ctx context.Context, u *model.UserExistRequest) error {
|
|
||||||
got, err := uuc.userRepo.GetByEmail(ctx, u.Email, nil)
|
|
||||||
// Any query error?
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// User exists?
|
|
||||||
if got == nil {
|
|
||||||
return UserNotExist
|
|
||||||
}
|
|
||||||
|
|
||||||
// Password correct?
|
|
||||||
err = bcrypt.CompareHashAndPassword([]byte(got.Password), []byte(u.Password))
|
|
||||||
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
|
|
||||||
return UserWrongPassword
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (uuc *userUsecase) GetUserBaseResponseByID(
|
|
||||||
ctx context.Context,
|
|
||||||
userID int,
|
|
||||||
) (*model.UserBaseResponse, error) {
|
|
||||||
// TODO: should try first to get from the cache
|
|
||||||
// If not exists, get from the DB. And then put back
|
|
||||||
// into the cache with a timeout.
|
|
||||||
// Refresh the cache when the user data is updated (for now it cannot be updated)
|
|
||||||
got, err := uuc.userRepo.GetByID(ctx, userID, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
userBaseVo := &model.UserBaseResponse{
|
|
||||||
ID: got.ID,
|
|
||||||
FirstName: got.FirstName,
|
|
||||||
LastName: got.LastName,
|
|
||||||
}
|
|
||||||
return userBaseVo, nil
|
|
||||||
}
|
|
@ -1,99 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package usecase
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/usecase/repomock"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCreateUser(t *testing.T) {
|
|
||||||
t.Run("normal create", func(t *testing.T) {
|
|
||||||
ctx := context.Background()
|
|
||||||
userUsecase := NewUserUsecase(&repomock.TestUserRepository{}, &repomock.TestDBRepository{})
|
|
||||||
input := &model.UserCreateRequest{
|
|
||||||
Email: "a@b.c",
|
|
||||||
FirstName: "James",
|
|
||||||
LastName: "Bond",
|
|
||||||
Password: "verystrong",
|
|
||||||
}
|
|
||||||
want := &model.UserInfoResponse{
|
|
||||||
ID: 123,
|
|
||||||
}
|
|
||||||
|
|
||||||
got, err := userUsecase.Create(ctx, input)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, want.ID, got.ID)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("duplicate create", func(t *testing.T) {
|
|
||||||
ctx := context.Background()
|
|
||||||
userUsecase := NewUserUsecase(&repomock.TestUserRepository{}, &repomock.TestDBRepository{})
|
|
||||||
input := &model.UserCreateRequest{
|
|
||||||
Email: "duplicate@error.com",
|
|
||||||
FirstName: "James",
|
|
||||||
LastName: "Bond",
|
|
||||||
Password: "verystrong",
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := userUsecase.Create(ctx, input)
|
|
||||||
assert.EqualError(t, err, UserExisted.Error())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUserExist(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
Name string
|
|
||||||
User *model.UserExistRequest
|
|
||||||
ExpErr error
|
|
||||||
}{
|
|
||||||
{"user exists", &model.UserExistRequest{
|
|
||||||
Email: "a@b.c",
|
|
||||||
Password: "strongHashed",
|
|
||||||
}, nil},
|
|
||||||
{"query error", &model.UserExistRequest{
|
|
||||||
Email: "query@error.com",
|
|
||||||
Password: "strongHashed",
|
|
||||||
}, repomock.UserTestDummyErr},
|
|
||||||
{"user doesn not exist", &model.UserExistRequest{
|
|
||||||
Email: "inexist@error.com",
|
|
||||||
Password: "strongHashed",
|
|
||||||
}, UserNotExist},
|
|
||||||
{"wrong password", &model.UserExistRequest{
|
|
||||||
Email: "a@b.c",
|
|
||||||
Password: "wrongHashed",
|
|
||||||
}, UserWrongPassword},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tst := range testCases {
|
|
||||||
ctx := context.Background()
|
|
||||||
userUsecase := NewUserUsecase(&repomock.TestUserRepository{}, &repomock.TestDBRepository{})
|
|
||||||
|
|
||||||
err := userUsecase.Exist(ctx, tst.User)
|
|
||||||
assert.ErrorIs(t, err, tst.ExpErr)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Cache interface {
|
|
||||||
Get(ctx context.Context, key string) (string, error)
|
|
||||||
Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error
|
|
||||||
Close() error
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type Context interface {
|
|
||||||
// Context
|
|
||||||
Deadline() (deadline time.Time, ok bool)
|
|
||||||
Done() <-chan struct{}
|
|
||||||
Err() error
|
|
||||||
Value(key any) any
|
|
||||||
|
|
||||||
// Request
|
|
||||||
Bind(obj any) error
|
|
||||||
GetHeader(key string) string
|
|
||||||
|
|
||||||
// Response
|
|
||||||
JSON(code int, obj any)
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/errno"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ErrResponse struct {
|
|
||||||
Code string `json:"code"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteResponse writes the response to the HTTP response writer with HTTP code
|
|
||||||
// and potential errors.
|
|
||||||
func WriteResponse(c Context, err error, data any) {
|
|
||||||
// No error, write json response directly
|
|
||||||
if err == nil {
|
|
||||||
c.JSON(http.StatusOK, data)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
error := errno.Decode(err)
|
|
||||||
c.JSON(error.HTTP, ErrResponse{
|
|
||||||
Code: error.Code,
|
|
||||||
Message: error.Message,
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package errno
|
|
||||||
|
|
||||||
import "net/http"
|
|
||||||
|
|
||||||
type PlatformLevelErrCode string
|
|
||||||
|
|
||||||
const (
|
|
||||||
InternalErrorCode = "InternalError"
|
|
||||||
InvalidParameterCode = "InvalidParameter"
|
|
||||||
AuthFailureCode = "AuthFailure"
|
|
||||||
ResourceNotFoundCode = "ResourceNotFound"
|
|
||||||
FailedOperationCode = "FailedOperation"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
OK = &Errno{HTTP: http.StatusOK, Code: "", Message: ""}
|
|
||||||
|
|
||||||
InternalServerErr = &Errno{
|
|
||||||
HTTP: http.StatusInternalServerError,
|
|
||||||
Code: InternalErrorCode,
|
|
||||||
Message: "Internal server error",
|
|
||||||
}
|
|
||||||
|
|
||||||
PageNotFoundErr = &Errno{
|
|
||||||
HTTP: http.StatusNotFound,
|
|
||||||
Code: ErrorCode(ResourceNotFoundCode, "PageNotFound"),
|
|
||||||
Message: "Page not found",
|
|
||||||
}
|
|
||||||
)
|
|
@ -1,55 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package errno
|
|
||||||
|
|
||||||
type Errno struct {
|
|
||||||
HTTP int
|
|
||||||
Code string
|
|
||||||
Message string
|
|
||||||
}
|
|
||||||
|
|
||||||
func ErrorCode(platformErrCode string, resourceErrCode string) string {
|
|
||||||
return platformErrCode + "." + resourceErrCode
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error implements Error() method in error interface
|
|
||||||
func (err *Errno) Error() string {
|
|
||||||
return err.Message
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode tries to get the Errno from the error
|
|
||||||
func Decode(err error) *Errno {
|
|
||||||
if err == nil {
|
|
||||||
return OK
|
|
||||||
}
|
|
||||||
|
|
||||||
switch typed := err.(type) {
|
|
||||||
case *Errno:
|
|
||||||
return typed
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
internErr := InternalServerErr
|
|
||||||
internErr.Message = err.Error()
|
|
||||||
return internErr
|
|
||||||
}
|
|
@ -1,183 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package log
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/core"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/shared"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
"go.uber.org/zap/zapcore"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Logger interface {
|
|
||||||
FatalLog(msg string, keyValues ...interface{})
|
|
||||||
PanicLog(msg string, keyValues ...interface{})
|
|
||||||
ErrorLog(msg string, keyValues ...interface{})
|
|
||||||
WarnLog(msg string, keyValues ...interface{})
|
|
||||||
InfoLog(msg string, keyValues ...interface{})
|
|
||||||
DebugLog(msg string, keyValues ...interface{})
|
|
||||||
Sync()
|
|
||||||
}
|
|
||||||
|
|
||||||
// zapLogger is an implementation of Logger interface
|
|
||||||
type zapLogger struct {
|
|
||||||
z *zap.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
mu sync.Mutex
|
|
||||||
|
|
||||||
// default global logger
|
|
||||||
std = NewLogger(NewOptions())
|
|
||||||
)
|
|
||||||
|
|
||||||
// Init initializes global logger with options
|
|
||||||
func Init(opts *Options) {
|
|
||||||
mu.Lock()
|
|
||||||
defer mu.Unlock()
|
|
||||||
|
|
||||||
std = NewLogger(opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLogger(opts *Options) *zapLogger {
|
|
||||||
if opts == nil {
|
|
||||||
opts = NewOptions()
|
|
||||||
}
|
|
||||||
|
|
||||||
var zapLevel zapcore.Level
|
|
||||||
// If unknown level, use info
|
|
||||||
if err := zapLevel.UnmarshalText([]byte(opts.Level)); err != nil {
|
|
||||||
zapLevel = zap.InfoLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
encoderCfg := zap.NewProductionEncoderConfig()
|
|
||||||
encoderCfg.TimeKey = "timestamp"
|
|
||||||
encoderCfg.MessageKey = "message"
|
|
||||||
encoderCfg.EncodeTime = zapcore.ISO8601TimeEncoder
|
|
||||||
|
|
||||||
config := zap.Config{
|
|
||||||
Level: zap.NewAtomicLevelAt(zapLevel),
|
|
||||||
Development: opts.Development,
|
|
||||||
DisableCaller: opts.DisableCaller,
|
|
||||||
DisableStacktrace: opts.DisableStacktrace,
|
|
||||||
Sampling: nil,
|
|
||||||
Encoding: opts.Format,
|
|
||||||
EncoderConfig: encoderCfg,
|
|
||||||
OutputPaths: opts.OutputPaths,
|
|
||||||
ErrorOutputPaths: []string{
|
|
||||||
"stderr",
|
|
||||||
},
|
|
||||||
InitialFields: map[string]interface{}{
|
|
||||||
"pid": os.Getpid(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
z := zap.Must(config.Build(zap.AddStacktrace(zapcore.PanicLevel), zap.AddCallerSkip(1)))
|
|
||||||
|
|
||||||
zap.RedirectStdLog(z)
|
|
||||||
|
|
||||||
return &zapLogger{z: z}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CtxLog writes context's information into the log
|
|
||||||
func CtxLog(ctx core.Context) *zapLogger {
|
|
||||||
return std.CtxLog(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (z *zapLogger) CtxLog(ctx core.Context) *zapLogger {
|
|
||||||
zz := z.clone()
|
|
||||||
|
|
||||||
if rid := ctx.GetHeader(shared.XRequestID); rid != "" {
|
|
||||||
zz.z = zz.z.With(zap.Any(shared.XRequestID, rid))
|
|
||||||
}
|
|
||||||
|
|
||||||
if user := ctx.GetHeader(shared.XUserName); user != "" {
|
|
||||||
zz.z = zz.z.With(zap.Any(shared.XUserName, user))
|
|
||||||
}
|
|
||||||
|
|
||||||
return zz
|
|
||||||
}
|
|
||||||
|
|
||||||
func (z *zapLogger) clone() *zapLogger {
|
|
||||||
zz := *z
|
|
||||||
return &zz
|
|
||||||
}
|
|
||||||
|
|
||||||
func (z *zapLogger) FatalLog(msg string, keyValues ...interface{}) {
|
|
||||||
z.z.Sugar().Fatalw(msg, keyValues...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (z *zapLogger) PanicLog(msg string, keyValues ...interface{}) {
|
|
||||||
z.z.Sugar().Panicw(msg, keyValues...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (z *zapLogger) ErrorLog(msg string, keyValues ...interface{}) {
|
|
||||||
z.z.Sugar().Errorw(msg, keyValues...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (z *zapLogger) WarnLog(msg string, keyValues ...interface{}) {
|
|
||||||
z.z.Sugar().Warnw(msg, keyValues...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (z *zapLogger) InfoLog(msg string, keyValues ...interface{}) {
|
|
||||||
z.z.Sugar().Infow(msg, keyValues...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (z *zapLogger) DebugLog(msg string, keyValues ...interface{}) {
|
|
||||||
z.z.Sugar().Debugw(msg, keyValues...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (z *zapLogger) Sync() {
|
|
||||||
_ = z.z.Sync()
|
|
||||||
}
|
|
||||||
|
|
||||||
func FatalLog(msg string, keyValues ...interface{}) {
|
|
||||||
std.z.Sugar().Fatalw(msg, keyValues...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func PanicLog(msg string, keyValues ...interface{}) {
|
|
||||||
std.z.Sugar().Panicw(msg, keyValues...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ErrorLog(msg string, keyValues ...interface{}) {
|
|
||||||
std.z.Sugar().Errorw(msg, keyValues...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func WarnLog(msg string, keyValues ...interface{}) {
|
|
||||||
std.z.Sugar().Warnw(msg, keyValues...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func InfoLog(msg string, keyValues ...interface{}) {
|
|
||||||
std.z.Sugar().Infow(msg, keyValues...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func DebugLog(msg string, keyValues ...interface{}) {
|
|
||||||
std.z.Sugar().Debugw(msg, keyValues...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Sync() {
|
|
||||||
_ = std.z.Sync()
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package log
|
|
||||||
|
|
||||||
// Options define the logger options
|
|
||||||
type Options struct {
|
|
||||||
Level string
|
|
||||||
Development bool
|
|
||||||
DisableCaller bool
|
|
||||||
DisableStacktrace bool
|
|
||||||
Format string
|
|
||||||
OutputPaths []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewOptions() *Options {
|
|
||||||
return &Options{
|
|
||||||
Level: "debug",
|
|
||||||
Development: true,
|
|
||||||
DisableCaller: false,
|
|
||||||
DisableStacktrace: false,
|
|
||||||
Format: "console",
|
|
||||||
OutputPaths: []string{
|
|
||||||
"stderr",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,77 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package authn
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/core"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/errno"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/log"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/shared"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/token"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ErrTokenInvalid = &errno.Errno{
|
|
||||||
HTTP: http.StatusUnauthorized,
|
|
||||||
Code: errno.ErrorCode(errno.AuthFailureCode, "TokenInvalid"),
|
|
||||||
Message: "invalid token",
|
|
||||||
}
|
|
||||||
|
|
||||||
var ErrLoggedOut = &errno.Errno{
|
|
||||||
HTTP: http.StatusUnauthorized,
|
|
||||||
Code: errno.ErrorCode(errno.AuthFailureCode, "LoggedOut"),
|
|
||||||
Message: "logged out, please log in",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authn authenticates a user's access by validating their token.
|
|
||||||
func Authn(cache core.Cache) gin.HandlerFunc {
|
|
||||||
return func(ctx *gin.Context) {
|
|
||||||
tk, err := token.ParseRequest(ctx)
|
|
||||||
if err != nil || tk == nil {
|
|
||||||
core.WriteResponse(ctx, ErrTokenInvalid, nil)
|
|
||||||
ctx.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
key := fmt.Sprintf("jwt:%s", tk.Identity)
|
|
||||||
|
|
||||||
val, err := cache.Get(ctx, key)
|
|
||||||
if err != nil {
|
|
||||||
log.ErrorLog("cache get token", "err", err)
|
|
||||||
ctx.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if val != "" {
|
|
||||||
// blacklist
|
|
||||||
core.WriteResponse(ctx, ErrLoggedOut, nil)
|
|
||||||
ctx.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Header(shared.XUserName, tk.Identity)
|
|
||||||
ctx.Next()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,117 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package authn
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/errno"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/shared"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/test"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/token"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
var loggedOutID string
|
|
||||||
|
|
||||||
type testCache struct{}
|
|
||||||
|
|
||||||
func (tc *testCache) Get(ctx context.Context, key string) (string, error) {
|
|
||||||
loggedOutKey := fmt.Sprintf("jwt:%s", loggedOutID)
|
|
||||||
if key == loggedOutKey {
|
|
||||||
return "found", nil
|
|
||||||
}
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *testCache) Set(
|
|
||||||
ctx context.Context,
|
|
||||||
key string,
|
|
||||||
value interface{},
|
|
||||||
expiration time.Duration,
|
|
||||||
) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *testCache) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAuthn(t *testing.T) {
|
|
||||||
token.Init("secret", 1*time.Second)
|
|
||||||
tk, _ := token.Sign("user")
|
|
||||||
|
|
||||||
tkParsed, _ := token.Parse(tk)
|
|
||||||
loggedOutID = tkParsed.Identity
|
|
||||||
|
|
||||||
cache := &testCache{}
|
|
||||||
|
|
||||||
t.Run("token found in cache", func(t *testing.T) {
|
|
||||||
r := gin.New()
|
|
||||||
r.Use(Authn(cache))
|
|
||||||
|
|
||||||
r.GET("/example", func(c *gin.Context) {
|
|
||||||
c.Status(http.StatusOK)
|
|
||||||
})
|
|
||||||
res := test.PerformRequest(
|
|
||||||
t,
|
|
||||||
r,
|
|
||||||
"GET",
|
|
||||||
"/example",
|
|
||||||
nil,
|
|
||||||
test.Header{Key: shared.XUserName, Value: "user"},
|
|
||||||
test.Header{Key: "Authorization", Value: fmt.Sprintf("Bearer %s", tk)},
|
|
||||||
)
|
|
||||||
assert.Equal(t, http.StatusUnauthorized, res.Result().StatusCode, res.Body)
|
|
||||||
|
|
||||||
var err errno.Errno
|
|
||||||
json.NewDecoder(res.Result().Body).Decode(&err)
|
|
||||||
assert.Equal(t, "AuthFailure.LoggedOut", err.Code)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("token not found in cache", func(t *testing.T) {
|
|
||||||
newTk, _ := token.Sign("user2")
|
|
||||||
r := gin.New()
|
|
||||||
r.Use(Authn(cache))
|
|
||||||
|
|
||||||
r.GET("/example", func(c *gin.Context) {
|
|
||||||
c.Status(http.StatusOK)
|
|
||||||
})
|
|
||||||
res := test.PerformRequest(
|
|
||||||
t,
|
|
||||||
r,
|
|
||||||
"GET",
|
|
||||||
"/example",
|
|
||||||
nil,
|
|
||||||
test.Header{Key: shared.XUserName, Value: "user2"},
|
|
||||||
test.Header{Key: "Authorization", Value: fmt.Sprintf("Bearer %s", newTk)},
|
|
||||||
)
|
|
||||||
assert.Equal(t, http.StatusOK, res.Result().StatusCode, res.Body)
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/shared"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
func RequestID() gin.HandlerFunc {
|
|
||||||
return func(ctx *gin.Context) {
|
|
||||||
var rid string
|
|
||||||
|
|
||||||
if rid = ctx.GetHeader(shared.XRequestID); rid != "" {
|
|
||||||
ctx.Request.Header.Add(shared.XRequestID, rid)
|
|
||||||
ctx.Next()
|
|
||||||
}
|
|
||||||
|
|
||||||
rid = uuid.NewString()
|
|
||||||
ctx.Request.Header.Add(shared.XRequestID, rid)
|
|
||||||
ctx.Header(shared.XRequestID, rid)
|
|
||||||
ctx.Next()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,67 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/shared"
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/test"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRequestID(t *testing.T) {
|
|
||||||
r := gin.New()
|
|
||||||
r.Use(RequestID())
|
|
||||||
var got string
|
|
||||||
wanted := "123"
|
|
||||||
|
|
||||||
r.GET("/example", func(c *gin.Context) {
|
|
||||||
got = c.GetHeader(shared.XRequestID)
|
|
||||||
c.Status(http.StatusOK)
|
|
||||||
})
|
|
||||||
|
|
||||||
r.POST("/example", func(c *gin.Context) {
|
|
||||||
got = c.GetHeader(shared.XRequestID)
|
|
||||||
c.String(http.StatusAccepted, "ok")
|
|
||||||
})
|
|
||||||
|
|
||||||
// Test with Request ID
|
|
||||||
_ = test.PerformRequest(
|
|
||||||
t,
|
|
||||||
r,
|
|
||||||
"GET",
|
|
||||||
"/example?a=100",
|
|
||||||
nil,
|
|
||||||
test.Header{Key: shared.XRequestID, Value: wanted},
|
|
||||||
)
|
|
||||||
assert.Equal(t, "123", got)
|
|
||||||
|
|
||||||
res := test.PerformRequest(t, r, "GET", "/example?a=100", nil)
|
|
||||||
assert.NotEqual(t, "", got)
|
|
||||||
assert.NoError(t, uuid.Validate(got))
|
|
||||||
assert.Equal(t, res.Header()[shared.XRequestID][0], got)
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package shared
|
|
||||||
|
|
||||||
const (
|
|
||||||
XRequestID = "X-Request-Id"
|
|
||||||
XUserName = "X-Username"
|
|
||||||
)
|
|
@ -1,52 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Header struct {
|
|
||||||
Key string
|
|
||||||
Value string
|
|
||||||
}
|
|
||||||
|
|
||||||
func PerformRequest(
|
|
||||||
t testing.TB,
|
|
||||||
r http.Handler,
|
|
||||||
method, path string,
|
|
||||||
body io.Reader,
|
|
||||||
headers ...Header,
|
|
||||||
) *httptest.ResponseRecorder {
|
|
||||||
t.Helper()
|
|
||||||
req := httptest.NewRequest(method, path, body)
|
|
||||||
for _, h := range headers {
|
|
||||||
req.Header.Add(h.Key, h.Value)
|
|
||||||
}
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
r.ServeHTTP(w, req)
|
|
||||||
return w
|
|
||||||
}
|
|
@ -1,117 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package token
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
secretKey string
|
|
||||||
expiryTime time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
type Claims struct {
|
|
||||||
IdentityKey string `json:"identity_key"`
|
|
||||||
jwt.RegisteredClaims
|
|
||||||
}
|
|
||||||
|
|
||||||
type TokenResp struct {
|
|
||||||
Raw string
|
|
||||||
Identity string
|
|
||||||
Expiry time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
once sync.Once
|
|
||||||
config Config
|
|
||||||
)
|
|
||||||
|
|
||||||
var ErrMissingHeader = errors.New("Authorization is needed in the header")
|
|
||||||
|
|
||||||
func Init(secretKey string, expiryTime time.Duration) {
|
|
||||||
once.Do(func() {
|
|
||||||
config.secretKey = secretKey
|
|
||||||
config.expiryTime = expiryTime
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func Sign(identityKey string) (string, error) {
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, Claims{
|
|
||||||
identityKey,
|
|
||||||
jwt.RegisteredClaims{
|
|
||||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(config.expiryTime)),
|
|
||||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
|
||||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return token.SignedString([]byte(config.secretKey))
|
|
||||||
}
|
|
||||||
|
|
||||||
func Parse(tokenString string) (*TokenResp, error) {
|
|
||||||
token, err := jwt.ParseWithClaims(
|
|
||||||
tokenString,
|
|
||||||
&Claims{},
|
|
||||||
func(t *jwt.Token) (interface{}, error) {
|
|
||||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
|
||||||
return nil, jwt.ErrSignatureInvalid
|
|
||||||
}
|
|
||||||
return []byte(config.secretKey), nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if claims, ok := token.Claims.(*Claims); ok {
|
|
||||||
return &TokenResp{
|
|
||||||
tokenString,
|
|
||||||
claims.IdentityKey,
|
|
||||||
claims.ExpiresAt.Time,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseRequest(c *gin.Context) (*TokenResp, error) {
|
|
||||||
// NOTE: Authorization: Bearer sdkfjlsfjlskdfjlsjdflk...slkdfjlka
|
|
||||||
header := c.GetHeader("Authorization")
|
|
||||||
|
|
||||||
if len(header) == 0 {
|
|
||||||
return nil, ErrMissingHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the token
|
|
||||||
var t string
|
|
||||||
fmt.Sscanf(header, "Bearer %s", &t)
|
|
||||||
|
|
||||||
return Parse(t)
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
DROP TABLE IF EXISTS "user";
|
|
||||||
DROP SEQUENCE IF EXISTS user_id_seq;
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
|||||||
CREATE SEQUENCE user_id_seq INCREMENT 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1;
|
|
||||||
|
|
||||||
CREATE TABLE "public"."user" (
|
|
||||||
"id" integer DEFAULT nextval('user_id_seq') NOT NULL,
|
|
||||||
"email" character varying(255) NOT NULL UNIQUE,
|
|
||||||
"first_name" character varying(255) NOT NULL,
|
|
||||||
"last_name" character varying(255) NOT NULL,
|
|
||||||
"password" character varying(60) NOT NULL,
|
|
||||||
"created_at" timestamp NOT NULL,
|
|
||||||
"updated_at" timestamp NOT NULL,
|
|
||||||
CONSTRAINT "user_pkey" PRIMARY KEY ("id")
|
|
||||||
) WITH (oids = false);
|
|
||||||
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
|||||||
DROP TABLE IF EXISTS "admin";
|
|
||||||
DROP SEQUENCE IF EXISTS admin_id_seq;
|
|
@ -1,9 +0,0 @@
|
|||||||
CREATE SEQUENCE admin_id_seq INCREMENT 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1;
|
|
||||||
|
|
||||||
CREATE TABLE "public"."admin" (
|
|
||||||
"id" integer DEFAULT nextval('admin_id_seq') NOT NULL,
|
|
||||||
"email" character varying(255) NOT NULL,
|
|
||||||
"password" character varying(255) NOT NULL,
|
|
||||||
"access_level" integer NOT NULL,
|
|
||||||
CONSTRAINT "admin_pkey" PRIMARY KEY ("id")
|
|
||||||
) WITH (oids = false);
|
|
@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE "admin"
|
|
||||||
ADD CONSTRAINT unique_email UNIQUE ("email");
|
|
@ -1 +0,0 @@
|
|||||||
DROP TABLE "event"
|
|
@ -1,10 +0,0 @@
|
|||||||
CREATE TABLE "event" (
|
|
||||||
"id" serial NOT NULL,
|
|
||||||
PRIMARY KEY ("id"),
|
|
||||||
"name" character varying(255) NOT NULL,
|
|
||||||
"description" character varying(10000) NULL,
|
|
||||||
"default_currency" character varying(255) NOT NULL,
|
|
||||||
"owner_id" integer NOT NULL,
|
|
||||||
"created_at" date NOT NULL,
|
|
||||||
"updated_at" date NOT NULL
|
|
||||||
);
|
|
@ -1 +0,0 @@
|
|||||||
DROP TABLE participation;
|
|
@ -1,16 +0,0 @@
|
|||||||
CREATE TABLE "participation" (
|
|
||||||
"id" serial NOT NULL,
|
|
||||||
PRIMARY KEY ("id"),
|
|
||||||
"user_id" integer NOT NULL,
|
|
||||||
"event_id" integer NOT NULL,
|
|
||||||
"invited_by_user_id" integer NULL,
|
|
||||||
"created_at" date NOT NULL,
|
|
||||||
"updated_at" date NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE "participation"
|
|
||||||
ADD FOREIGN KEY ("event_id") REFERENCES "event" ("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
ALTER TABLE "participation"
|
|
||||||
ADD FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE "event"
|
|
||||||
ADD "total_amount" integer NULL;
|
|
@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE "participation"
|
|
||||||
ADD CONSTRAINT unique_user_event UNIQUE ("user_id", "event_id");
|
|
@ -1,14 +0,0 @@
|
|||||||
CREATE TABLE "transaction" (
|
|
||||||
"id" serial NOT NULL,
|
|
||||||
PRIMARY KEY ("id"),
|
|
||||||
"expense_id" integer NOT NULL,
|
|
||||||
"user_id" integer NOT NULL,
|
|
||||||
"amount" integer NOT NULL,
|
|
||||||
"currency" character varying(255) NOT NULL,
|
|
||||||
"is_income" boolean NOT NULL DEFAULT FALSE,
|
|
||||||
"created_at" date NOT NULL,
|
|
||||||
"updated_at" date NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE "transaction"
|
|
||||||
ADD FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
@ -1 +0,0 @@
|
|||||||
DROP TABLE "expense";
|
|
@ -1,14 +0,0 @@
|
|||||||
CREATE TABLE "expense" (
|
|
||||||
"id" serial NOT NULL,
|
|
||||||
PRIMARY KEY ("id"),
|
|
||||||
"created_at" date NOT NULL,
|
|
||||||
"updated_at" date NOT NULL,
|
|
||||||
"amount" integer NOT NULL,
|
|
||||||
"currency" character varying NOT NULL,
|
|
||||||
"event_id" integer NOT NULL,
|
|
||||||
"name" character varying(255) NULL,
|
|
||||||
"place" character varying(1000) NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE "expense"
|
|
||||||
ADD FOREIGN KEY ("event_id") REFERENCES "event" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
|
@ -1 +0,0 @@
|
|||||||
DROP TABLE transaction;
|
|
@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE "transaction"
|
|
||||||
ADD FOREIGN KEY ("expense_id") REFERENCES "expense" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
|
@ -1,48 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package verflag
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/pkg/version"
|
|
||||||
"github.com/spf13/pflag"
|
|
||||||
)
|
|
||||||
|
|
||||||
const versionFlagName = "version"
|
|
||||||
|
|
||||||
var doPrint *bool
|
|
||||||
|
|
||||||
func AddFlags(fs *pflag.FlagSet) {
|
|
||||||
doPrint = fs.BoolP("version", "v", true, "Print version and exit")
|
|
||||||
// By default, set to false
|
|
||||||
*doPrint = false
|
|
||||||
}
|
|
||||||
|
|
||||||
func PrintVersion() {
|
|
||||||
if *doPrint {
|
|
||||||
fmt.Printf("%s\n", version.Get().String())
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,85 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package version
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"github.com/gosuri/uitable"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Version info format
|
|
||||||
var (
|
|
||||||
GitVersion = "v0.0.0-master+$Format:%h$"
|
|
||||||
// BuildDate is the output of $(date -u + '%Y-%m-%dT%H:%M:%SZ')
|
|
||||||
BuildDate = "1970-01-01T00:00:00Z"
|
|
||||||
// GitCommit is the SHA1
|
|
||||||
GitCommit = "$Format:%H$"
|
|
||||||
// GitTreeState clean or dirty
|
|
||||||
GitTreeState = ""
|
|
||||||
)
|
|
||||||
|
|
||||||
type Info struct {
|
|
||||||
GitVersion string `json:"git_version"`
|
|
||||||
GitCommit string `json:"git_commit"`
|
|
||||||
GitTreeState string `json:"git_tree_state"`
|
|
||||||
BuildDate string `json:"build_date"`
|
|
||||||
GoVersion string `json:"go_version"`
|
|
||||||
Compiler string `json:"compiler"`
|
|
||||||
Platform string `json:"platform"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (info Info) String() string {
|
|
||||||
table := uitable.New()
|
|
||||||
table.RightAlign(0)
|
|
||||||
table.MaxColWidth = 80
|
|
||||||
table.Separator = " "
|
|
||||||
table.AddRow("git version:", info.GitVersion)
|
|
||||||
table.AddRow("git commit:", info.GitCommit)
|
|
||||||
table.AddRow("git tree state:", info.GitTreeState)
|
|
||||||
table.AddRow("build date:", info.BuildDate)
|
|
||||||
table.AddRow("go version:", info.GoVersion)
|
|
||||||
table.AddRow("compiler:", info.Compiler)
|
|
||||||
table.AddRow("platform:", info.Platform)
|
|
||||||
|
|
||||||
return table.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (info Info) JSON() string {
|
|
||||||
s, _ := json.Marshal(info)
|
|
||||||
return string(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Get() Info {
|
|
||||||
return Info{
|
|
||||||
GitVersion: GitVersion,
|
|
||||||
GitCommit: GitCommit,
|
|
||||||
GitTreeState: GitTreeState,
|
|
||||||
BuildDate: BuildDate,
|
|
||||||
GoVersion: runtime.Version(),
|
|
||||||
Compiler: runtime.Compiler,
|
|
||||||
Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user