Compare commits

...

6 Commits

Author SHA1 Message Date
Muyao CHEN
6f9ff9ab96 fix: make user signup work for the minimum
A lot of work is still to be done ...
2024-10-06 18:26:57 +02:00
Muyao CHEN
7b8abf8e5c feat: Create user usecase 2024-10-06 16:18:28 +02:00
Muyao CHEN
4546665461 feat(sql): Add unique constraint to admin's email 2024-10-06 13:44:27 +02:00
Muyao CHEN
2c1beb30f6 fix(db): use a normal pgx.Conn first 2024-10-06 13:33:40 +02:00
Muyao CHEN
43a1d0509c feat(mkfile): build sqlc 2024-10-06 13:18:21 +02:00
Muyao CHEN
b14b8788ab fix: wrong directory for sqlc generated code 2024-10-06 10:29:09 +02:00
34 changed files with 622 additions and 41 deletions

1
.gitignore vendored
View File

@ -25,3 +25,4 @@ go.work.sum
# Custom
/_output
/deployment/db_data
/tmp/**

View File

@ -33,9 +33,12 @@ all: add-copyright format build
# ==============================================================================
.PHONY: build
build: tidy # build.
build: tidy sqlc # build.
@go build -v -ldflags "$(GO_LDFLAGS)" -o $(OUTPUT_DIR)/howmuch $(ROOT_DIR)/cmd/howmuch/main.go 2>/dev/null
.PHONY: sqlc
@sqlc generate
.PHONY: format
format: # format code.
@gofmt -s -w ./

View File

@ -198,9 +198,46 @@ type User struct {
Use Buffalo pop `Soda CLI` to create database migrations.
### 2024/10/07
### 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).

View File

@ -39,7 +39,7 @@ tags:
- name: user
paths:
/signup:
/user/signup:
post:
tags:
- user

7
docs/error_code.md Normal file
View File

@ -0,0 +1,7 @@
# Platform level error code design
- InternalError
- InvalidParameter
- AuthFailure
- ResourceNotFound
- FailedOperation

2
go.mod
View File

@ -35,7 +35,6 @@ require (
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/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
@ -61,7 +60,6 @@ require (
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/text v0.18.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect

View File

@ -25,33 +25,66 @@ 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 {
Signup(core.Context)
UpdateInfo(core.Context)
Login(core.Context)
Logout(core.Context)
ChangePassword(core.Context)
UpdateInfo(*gin.Context)
Login(*gin.Context)
Logout(*gin.Context)
ChangePassword(*gin.Context)
}
type UserController struct{}
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) Signup(ctx core.Context) {
ctx.JSON(http.StatusOK, "hello")
var params model.User
if err := ctx.Bind(&params); err != nil {
core.WriteResponse(ctx, UserParamsErr, nil)
return
}
// TODO: check params validity (govalidator)
_, err := uc.userUsecase.Create(ctx, &params)
if err != nil {
core.WriteResponse(ctx, err, nil)
return
}
core.WriteResponse(ctx, errno.OK, nil)
}
func (uc *UserController) UpdateInfo(ctx core.Context) {
func (uc *UserController) UpdateInfo(ctx *gin.Context) {
}
func (uc *UserController) Login(ctx core.Context) {
func (uc *UserController) Login(ctx *gin.Context) {
}
func (uc *UserController) Logout(ctx core.Context) {
func (uc *UserController) Logout(ctx *gin.Context) {
}
func (uc *UserController) ChangePassword(ctx core.Context) {
func (uc *UserController) ChangePassword(ctx *gin.Context) {
}

View File

@ -0,0 +1,68 @@
// 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/usecase/repo"
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/log"
"github.com/jackc/pgx/v5"
)
type dbRepository struct {
db *pgx.Conn
}
func NewDBRepository(db *pgx.Conn) 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.Begin(ctx)
if err != nil {
return nil, err
}
defer func() {
if p := recover(); p != nil {
tx.Rollback(ctx)
log.PanicLog("transaction panicked!")
} else if err != nil {
tx.Rollback(ctx)
log.ErrorLog("transaction failed!", "err", err)
} else {
err = tx.Commit(ctx)
}
}()
data, err := txFunc(ctx, tx)
return data, err
}

View File

@ -0,0 +1,88 @@
// 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"
"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"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
)
type userRepository struct {
db *pgx.Conn
}
const insertTimeout = 1 * time.Second
func NewUserRepository(db *pgx.Conn) repo.UserRepository {
return &userRepository{
db: db,
}
}
// Create
func (ur *userRepository) Create(
ctx context.Context,
transaction interface{},
u *model.User,
) (*model.User, error) {
timeoutCtx, cancel := context.WithTimeout(ctx, insertTimeout)
defer cancel()
args := sqlc.InsertUserParams{
Email: u.Email,
FirstName: u.FirstName,
LastName: u.LastName,
Password: u.Password,
CreatedAt: pgtype.Timestamp{Time: time.Now(), Valid: true},
UpdatedAt: pgtype.Timestamp{Time: time.Now(), Valid: true},
}
tx, ok := transaction.(pgx.Tx)
if !ok {
return nil, errors.New("transaction is not a pgx.Tx")
}
queries := sqlc.New(tx)
userDB, err := queries.InsertUser(timeoutCtx, args)
if err != nil {
return nil, err
}
return &model.User{
ID: int(userDB.ID),
Email: userDB.Email,
FirstName: userDB.FirstName,
LastName: userDB.LastName,
Password: userDB.Password,
CreatedAt: userDB.CreatedAt.Time,
UpdatedAt: userDB.CreatedAt.Time,
}, nil
}

View File

@ -37,6 +37,7 @@ import (
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/log"
"git.vinchent.xyz/vinchent/howmuch/pkg/version/verflag"
"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/net/context"
@ -105,8 +106,7 @@ func run() error {
}
// Init DB
dbPool, err := datastore.NewDB(
fmt.Sprintf(
dbConfString := fmt.Sprintf(
"host=%s port=%d dbname=%s user=%s password=%s sslmode=%s",
viper.GetString("db.host"),
viper.GetInt("db.port"),
@ -114,15 +114,19 @@ func run() error {
viper.GetString("db.username"),
viper.GetString("db.password"),
viper.GetString("db.sslmode"),
),
)
dbConf, err := pgx.ParseConfig(dbConfString)
if err != nil {
log.FatalLog("DB connection config failure", "err", err, "cfg string", dbConfString)
}
dbConn, err := datastore.NewDB(dbConf)
if err != nil {
log.FatalLog("DB connection failure", "err", err)
}
defer dbPool.Close()
defer dbConn.Close(context.Background())
// Register the core service
r := registry.NewRegistry(dbPool)
r := registry.NewRegistry(dbConn)
engine := gin.Default()

View File

@ -25,12 +25,12 @@ package datastore
import (
"context"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/jackc/pgx/v5"
)
// NewDB creates a new database for the application
func NewDB(dsn string) (*pgxpool.Pool, error) {
conn, err := pgxpool.New(context.Background(), dsn)
func NewDB(connConfig *pgx.ConnConfig) (*pgx.Conn, error) {
conn, err := pgx.ConnectConfig(context.Background(), connConfig)
if err != nil {
return nil, err
}

View File

@ -48,7 +48,13 @@ func Routes(engine *gin.Engine, c controller.AppController) *gin.Engine {
core.WriteResponse(ctx, errno.PageNotFoundErr, nil)
})
engine.POST("/signup", func(ctx *gin.Context) { c.User.Signup(ctx) })
v1 := engine.Group("/v1")
{
userV1 := v1.Group("/user")
{
userV1.POST("/signup", func(ctx *gin.Context) { c.User.Signup(ctx) })
}
}
return engine
}

View File

@ -0,0 +1,36 @@
// 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"
// User model
type User struct {
ID int `json:"id"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Password string `json:"password"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

View File

@ -24,7 +24,7 @@ package registry
import (
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/controller"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/jackc/pgx/v5"
)
// registry is an implementation of Registry interface.
@ -32,7 +32,7 @@ import (
// It might holds other drivers when the projects grows. For example
// the object needed to connect to Redis or Kafka.
type registry struct {
db *pgxpool.Pool
db *pgx.Conn
}
// Registry returns a new app controller that will be used by main()/run()
@ -43,7 +43,7 @@ type Registry interface {
}
// NewRegistry returns a new Registry's implementation.
func NewRegistry(db *pgxpool.Pool) Registry {
func NewRegistry(db *pgx.Conn) Registry {
return &registry{db: db}
}

View File

@ -22,9 +22,14 @@
package registry
import "git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/controller"
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 {
return &controller.UserController{}
u := usecase.NewUserUsecase(repo.NewUserRepository(r.db), repo.NewDBRepository(r.db))
return controller.NewUserController(u)
}

View File

@ -0,0 +1,32 @@
// 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)
}

View File

@ -0,0 +1,34 @@
// 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 TestDBRepository struct{}
func (tdr *TestDBRepository) Transaction(
ctx context.Context,
txFunc func(txCtx context.Context, tx interface{}) (interface{}, error),
) (interface{}, error) {
return txFunc(ctx, nil)
}

View File

@ -0,0 +1,42 @@
// 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 TestUserRepository struct{}
func (tur *TestUserRepository) Create(
ctx context.Context,
transaction interface{},
u *model.User,
) (*model.User, error) {
user := *u
user.ID = 123
return &user, nil
}

View File

@ -0,0 +1,33 @@
// 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, transaction interface{}, u *model.User) (*model.User, error)
}

View File

@ -0,0 +1,78 @@
// 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"
"fmt"
"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 userUsecase struct {
userRepo repo.UserRepository
dbRepo repo.DBRepository
}
type User interface {
Create(ctx context.Context, u *model.User) (*model.User, 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.User) (*model.User, error) {
data, err := uuc.dbRepo.Transaction(
ctx,
func(txCtx context.Context, tx interface{}) (interface{}, error) {
u, err := uuc.userRepo.Create(txCtx, tx, u)
if err != nil {
return nil, err
}
// TODO: App log, maybe can be sent to some third party service.
log.InfoLog(
"created new user",
"email",
u.Email,
"name",
fmt.Sprintf("%s %s", u.FirstName, u.LastName),
)
return u, err
},
)
if err != nil {
return nil, err
}
user := data.(*model.User)
return user, nil
}

View File

@ -0,0 +1,49 @@
// 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/repo"
"github.com/stretchr/testify/assert"
)
func TestCreateUser(t *testing.T) {
ctx := context.Background()
userUsecase := NewUserUsecase(&repo.TestUserRepository{}, &repo.TestDBRepository{})
input := &model.User{
Email: "a@b.c",
FirstName: "James",
LastName: "Bond",
Password: "verystrong",
}
want := input
want.ID = 123
got, err := userUsecase.Create(ctx, input)
assert.NoError(t, err)
assert.Equal(t, want, got)
}

View File

@ -22,6 +22,18 @@
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
// Response
JSON(code int, obj any)
}

View File

@ -24,18 +24,28 @@ 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: "InternalError",
Code: InternalErrorCode,
Message: "Internal server error",
}
PageNotFoundErr = &Errno{
HTTP: http.StatusNotFound,
Code: "ResourceNotFound.PageNotFound",
Code: ErrorCode(ResourceNotFoundCode, "PageNotFound"),
Message: "Page not found",
}
)

View File

@ -28,6 +28,10 @@ type Errno struct {
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

View File

@ -0,0 +1,2 @@
ALTER TABLE "admin"
ADD CONSTRAINT unique_email UNIQUE ("email");

View File

@ -23,9 +23,9 @@
version: "2"
sql:
- engine: "postgresql"
queries: "internal/app/adapter/repo/sqlc"
queries: "internal/howmuch/adapter/repo/sqlc"
schema: "migrations"
gen:
go:
out: "internal/app/controller/repo/sqlc"
out: "internal/howmuch/adapter/repo/sqlc"
sql_package: "pgx/v5"

View File

@ -1 +0,0 @@
exit status 2exit status 2exit status 2exit status 2exit status 2