Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,17 @@ func main() {

// Setup repository
redisRepo := redisRepository.NewRedisRepository(cacheInstance)
todoRepo := pgsqlRepository.NewPgsqlTodoRepository(dbInstance)
userRepo := pgsqlRepository.NewPgsqlUserRepository(dbInstance)
todoRepo := pgsqlRepository.NewPgsqlTodoRepository()
userRepo := pgsqlRepository.NewPgsqlUserRepository()

// Setup Service
cryptoSvc := crypto.NewCryptoService()
jwtSvc := jwt.NewJWTService(configApp.JWTSecretKey)

// Setup usecase
ctxTimeout := time.Duration(configApp.ContextTimeout) * time.Second
todoUC := usecase.NewTodoUsecase(todoRepo, redisRepo, ctxTimeout)
authUC := usecase.NewAuthUsecase(userRepo, cryptoSvc, jwtSvc, ctxTimeout)
todoUC := usecase.NewTodoUsecase(dbInstance, todoRepo, redisRepo, ctxTimeout)
authUC := usecase.NewAuthUsecase(dbInstance, userRepo, cryptoSvc, jwtSvc, ctxTimeout)

// Setup app middleware
appMiddleware := appMiddleware.NewMiddleware(jwtSvc)
Expand Down
21 changes: 21 additions & 0 deletions domain/database.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package domain

import (
"context"
"database/sql"
)

// Transaction interface (wraps DBTX)
type Transaction interface {
ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row
QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
Commit() error
Rollback() error
}

// Database interface
type Database interface {
BeginTx(ctx context.Context) (Transaction, error)
Close() error
}
10 changes: 5 additions & 5 deletions domain/todo.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ type Todo struct {

// TodoRepository represent the todos repository contract
type TodoRepository interface {
Create(ctx context.Context, todo *Todo) error
GetByID(ctx context.Context, id int64) (Todo, error)
Fetch(ctx context.Context) ([]Todo, error)
Update(ctx context.Context, todo *Todo) error
Delete(ctx context.Context, id int64) error
Create(ctx context.Context, tx Transaction, todo *Todo) error
GetByID(ctx context.Context, tx Transaction, id int64) (Todo, error)
Fetch(ctx context.Context, tx Transaction) ([]Todo, error)
Update(ctx context.Context, tx Transaction, todo *Todo) error
Delete(ctx context.Context, tx Transaction, id int64) error
}

// TodoUsecase represent the todos usecase contract
Expand Down
4 changes: 2 additions & 2 deletions domain/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ type User struct {

// UserRepository represent the todos repository contract
type UserRepository interface {
Create(ctx context.Context, user *User) error
GetByEmail(ctx context.Context, email string) (User, error)
Create(ctx context.Context, tx Transaction, user *User) error
GetByEmail(ctx context.Context, tx Transaction, email string) (User, error)
}
55 changes: 50 additions & 5 deletions infrastructure/datastore/database.go
Original file line number Diff line number Diff line change
@@ -1,28 +1,73 @@
package datastore

import (
"context"
"database/sql"
"net/url"
"time"

_ "github.com/lib/pq"
"github.com/syahidfrd/go-boilerplate/domain"
)

type Database struct {
db *sql.DB
}

// NewDatabase will create new database instance
func NewDatabase(databaseURL string) (db *sql.DB, err error) {
func NewDatabase(databaseURL string) (domain.Database, error) {
parseDBUrl, _ := url.Parse(databaseURL)
db, err = sql.Open(parseDBUrl.Scheme, databaseURL)
db, err := sql.Open(parseDBUrl.Scheme, databaseURL)
if err != nil {
return
return nil, err
}

if err = db.Ping(); err != nil {
return
return nil, err
}

db.SetConnMaxLifetime(time.Minute * 5)
db.SetMaxIdleConns(0)
db.SetMaxOpenConns(5)

return
return &Database{db: db}, nil
}

func (p *Database) BeginTx(ctx context.Context) (domain.Transaction, error) {
tx, err := p.db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelReadCommitted,
})
if err != nil {
return nil, err
}
return &Transaction{tx: tx}, nil
}

func (p *Database) Close() error {
return p.db.Close()
}

// Transaction implements Transaction interface
type Transaction struct {
tx *sql.Tx
}

func (t *Transaction) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
return t.tx.ExecContext(ctx, query, args...)
}

func (t *Transaction) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row {
return t.tx.QueryRowContext(ctx, query, args...)
}

func (t *Transaction) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) {
return t.tx.QueryContext(ctx, query, args...)
}

func (t *Transaction) Commit() error {
return t.tx.Commit()
}

func (t *Transaction) Rollback() error {
return t.tx.Rollback()
}
28 changes: 12 additions & 16 deletions repository/pgsql/pgsql_todo.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,34 @@ package pgsql

import (
"context"
"database/sql"
"fmt"

"github.com/syahidfrd/go-boilerplate/domain"
)

type pgsqlTodoRepository struct {
db *sql.DB
}

// NewPgsqlTodoRepository will create new an todoRepository object representation of TodoRepository interface
func NewPgsqlTodoRepository(db *sql.DB) *pgsqlTodoRepository {
return &pgsqlTodoRepository{
db: db,
}
func NewPgsqlTodoRepository() *pgsqlTodoRepository {
return &pgsqlTodoRepository{}
}

func (r *pgsqlTodoRepository) Create(ctx context.Context, todo *domain.Todo) (err error) {
func (r *pgsqlTodoRepository) Create(ctx context.Context, tx domain.Transaction, todo *domain.Todo) (err error) {
query := "INSERT INTO todos (name, created_at, updated_at) VALUES ($1, $2, $3)"
_, err = r.db.ExecContext(ctx, query, todo.Name, todo.CreatedAt, todo.UpdatedAt)
_, err = tx.ExecContext(ctx, query, todo.Name, todo.CreatedAt, todo.UpdatedAt)
return
}

func (r *pgsqlTodoRepository) GetByID(ctx context.Context, id int64) (todo domain.Todo, err error) {
func (r *pgsqlTodoRepository) GetByID(ctx context.Context, tx domain.Transaction, id int64) (todo domain.Todo, err error) {
query := "SELECT id, name, created_at, updated_at FROM todos WHERE id = $1"
err = r.db.QueryRowContext(ctx, query, id).Scan(&todo.ID, &todo.Name, &todo.CreatedAt, &todo.UpdatedAt)
err = tx.QueryRowContext(ctx, query, id).Scan(&todo.ID, &todo.Name, &todo.CreatedAt, &todo.UpdatedAt)
return
}

func (r *pgsqlTodoRepository) Fetch(ctx context.Context) (todos []domain.Todo, err error) {
func (r *pgsqlTodoRepository) Fetch(ctx context.Context, tx domain.Transaction) (todos []domain.Todo, err error) {
query := "SELECT id, name, created_at, updated_at FROM todos"
rows, err := r.db.QueryContext(ctx, query)
rows, err := tx.QueryContext(ctx, query)
if err != nil {
return todos, err
}
Expand All @@ -53,9 +49,9 @@ func (r *pgsqlTodoRepository) Fetch(ctx context.Context) (todos []domain.Todo, e
return todos, nil
}

func (r *pgsqlTodoRepository) Update(ctx context.Context, todo *domain.Todo) (err error) {
func (r *pgsqlTodoRepository) Update(ctx context.Context, tx domain.Transaction, todo *domain.Todo) (err error) {
query := "UPDATE todos SET name = $1, updated_at = $2 WHERE id = $3"
res, err := r.db.ExecContext(ctx, query, todo.Name, todo.UpdatedAt, todo.ID)
res, err := tx.ExecContext(ctx, query, todo.Name, todo.UpdatedAt, todo.ID)
if err != nil {
return
}
Expand All @@ -72,9 +68,9 @@ func (r *pgsqlTodoRepository) Update(ctx context.Context, todo *domain.Todo) (er
return
}

func (r *pgsqlTodoRepository) Delete(ctx context.Context, id int64) (err error) {
func (r *pgsqlTodoRepository) Delete(ctx context.Context, tx domain.Transaction, id int64) (err error) {
query := "DELETE FROM todos WHERE id = $1"
res, err := r.db.ExecContext(ctx, query, id)
res, err := tx.ExecContext(ctx, query, id)
if err != nil {
return
}
Expand Down
20 changes: 20 additions & 0 deletions repository/pgsql/pgsql_todo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,15 @@ func TestTodoRepo_Create(t *testing.T) {
}
defer db.Close()

mock.ExpectBegin()

query := "INSERT INTO todos"
mock.ExpectExec(regexp.QuoteMeta(query)).
WithArgs(todo.Name, todo.CreatedAt, todo.UpdatedAt).
WillReturnResult(sqlmock.NewResult(1, 1))

mock.ExpectCommit()

todoRepo := pgsql.NewPgsqlTodoRepository(db)
err = todoRepo.Create(context.TODO(), todo)
assert.NoError(t, err)
Expand All @@ -52,11 +56,15 @@ func TestTodoRepo_GetByID(t *testing.T) {
rows := sqlmock.NewRows([]string{"id", "name", "created_at", "updated_at"}).
AddRow(todoMock.ID, todoMock.Name, todoMock.CreatedAt, todoMock.UpdatedAt)

mock.ExpectBegin()

query := "SELECT id, name, created_at, updated_at FROM todos WHERE id = $1"
mock.ExpectQuery(regexp.QuoteMeta(query)).
WithArgs(1).
WillReturnRows(rows)

mock.ExpectCommit()

todoRepo := pgsql.NewPgsqlTodoRepository(db)
todo, err := todoRepo.GetByID(context.TODO(), 1)
assert.NoError(t, err)
Expand All @@ -80,9 +88,13 @@ func TestTodoRepo_Fetch(t *testing.T) {
AddRow(mockTodos[0].ID, mockTodos[0].Name, mockTodos[0].CreatedAt, mockTodos[0].UpdatedAt).
AddRow(mockTodos[1].ID, mockTodos[1].Name, mockTodos[1].CreatedAt, mockTodos[1].UpdatedAt)

mock.ExpectBegin()

query := "SELECT id, name, created_at, updated_at FROM todos"
mock.ExpectQuery(query).WillReturnRows(rows)

mock.ExpectCommit()

todoRepo := pgsql.NewPgsqlTodoRepository(db)
todos, err := todoRepo.Fetch(context.TODO())
assert.NoError(t, err)
Expand All @@ -103,11 +115,15 @@ func TestTodoRepo_Update(t *testing.T) {
UpdatedAt: time.Now(),
}

mock.ExpectBegin()

query := "UPDATE todos SET name = $1, updated_at = $2 WHERE id = $3"
mock.ExpectExec(regexp.QuoteMeta(query)).
WithArgs(todo.Name, todo.UpdatedAt, todo.ID).
WillReturnResult(sqlmock.NewResult(1, 1))

mock.ExpectCommit()

todoRepo := pgsql.NewPgsqlTodoRepository(db)
err = todoRepo.Update(context.TODO(), todo)
assert.NoError(t, err)
Expand All @@ -120,11 +136,15 @@ func TestTodoRepo_Delete(t *testing.T) {
}
defer db.Close()

mock.ExpectBegin()

query := "DELETE FROM todos WHERE id = $1"
mock.ExpectExec(regexp.QuoteMeta(query)).
WithArgs(1).
WillReturnResult(sqlmock.NewResult(1, 1))

mock.ExpectCommit()

todoRepo := pgsql.NewPgsqlTodoRepository(db)
err = todoRepo.Delete(context.TODO(), 1)
assert.NoError(t, err)
Expand Down
16 changes: 6 additions & 10 deletions repository/pgsql/pgsql_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,25 @@ package pgsql

import (
"context"
"database/sql"

"github.com/syahidfrd/go-boilerplate/domain"
)

type pgsqlUserRepository struct {
db *sql.DB
}

func NewPgsqlUserRepository(db *sql.DB) *pgsqlUserRepository {
return &pgsqlUserRepository{
db: db,
}
func NewPgsqlUserRepository() *pgsqlUserRepository {
return &pgsqlUserRepository{}
}

func (r *pgsqlUserRepository) Create(ctx context.Context, user *domain.User) (err error) {
func (r *pgsqlUserRepository) Create(ctx context.Context, tx domain.Transaction, user *domain.User) (err error) {
query := "INSERT INTO users (email, password, created_at, updated_at) VALUES ($1, $2, $3, $4)"
_, err = r.db.ExecContext(ctx, query, user.Email, user.Password, user.CreatedAt, user.UpdatedAt)
_, err = tx.ExecContext(ctx, query, user.Email, user.Password, user.CreatedAt, user.UpdatedAt)
return
}

func (r *pgsqlUserRepository) GetByEmail(ctx context.Context, email string) (user domain.User, err error) {
func (r *pgsqlUserRepository) GetByEmail(ctx context.Context, tx domain.Transaction, email string) (user domain.User, err error) {
query := "SELECT id, email, password, created_at, updated_at FROM users WHERE email = $1"
err = r.db.QueryRowContext(ctx, query, email).Scan(&user.ID, &user.Email, &user.Password, &user.CreatedAt, &user.UpdatedAt)
err = tx.QueryRowContext(ctx, query, email).Scan(&user.ID, &user.Email, &user.Password, &user.CreatedAt, &user.UpdatedAt)
return
}
9 changes: 9 additions & 0 deletions repository/pgsql/pgsql_user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,15 @@ func TestUserRepo_Create(t *testing.T) {

defer db.Close()

mock.ExpectBegin()

query := "INSERT INTO users"
mock.ExpectExec(regexp.QuoteMeta(query)).
WithArgs(user.Email, user.Password, user.CreatedAt, user.UpdatedAt).
WillReturnResult(sqlmock.NewResult(1, 1))

mock.ExpectCommit()

userRepo := pgsql.NewPgsqlUserRepository(db)
err = userRepo.Create(context.TODO(), user)
assert.NoError(t, err)
Expand All @@ -55,13 +59,18 @@ func TestUserRepo_GetByEmail(t *testing.T) {
rows := sqlmock.NewRows([]string{"id", "email", "password", "created_at", "updated_at"}).
AddRow(userMock.ID, userMock.Email, userMock.Password, userMock.CreatedAt, userMock.UpdatedAt)

mock.ExpectBegin()

query := "SELECT id, email, password, created_at, updated_at FROM users WHERE email = $1"
mock.ExpectQuery(regexp.QuoteMeta(query)).
WithArgs(userMock.Email).
WillReturnRows(rows)

mock.ExpectCommit()

userRepo := pgsql.NewPgsqlUserRepository(db)
user, err := userRepo.GetByEmail(context.TODO(), userMock.Email)

assert.NoError(t, err)
assert.NotNil(t, user)
assert.Equal(t, userMock.ID, user.ID)
Expand Down
Loading