Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nguyen Thanh Nam - Coding test #326

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
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
45 changes: 45 additions & 0 deletions ABOUT_PROJECT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
### How to run
- Start postgres database
```
make start-db
```
- Run database migration
```
make migrate-up
```
- Start server
```
make run
```

### How to run test locally:
```
make test
```

### A sample "curl" command to call my API:
```
curl --request POST \
--url http://localhost:8080/todo \
--header 'Content-Type: application/json' \
--data '{
"userID":2,
"name":"todo1",
"content":"content"
}'
```

### About my solution
I use Clean Architecture. It has some advantages:
- Agnostic to the outside world
- Independent with external services
- Easier to test in isolation
- The Ports and Adapters are replaceable
- Separation of the different rates of change
- High Maintainability

So I think this is my favorite part of the solution.

### What else do I want to improve
- Use test container instead of `sqlmock` is better?
- Increase test case coverage
20 changes: 20 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
CMD_MAIN=cmd/main.go

run:
go run ${CMD_MAIN} server

migrate:
echo \# make migrate name="${name}"
go run $(CMD_MAIN) migrate create $(name)

migrate-up:
go run $(CMD_MAIN) migrate up

migrate-down:
go run $(CMD_MAIN) migrate down 1

test:
go test ./... -count=1 -v -cover

start-db:
docker-compose up -d
82 changes: 82 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package main

import (
"database/sql"
"github.com/gin-gonic/gin"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
"github.com/spf13/cobra"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"log"
"manabieAssignment/cmd/migrations"
"manabieAssignment/config"
todoRepository "manabieAssignment/internal/todo/repository"
"manabieAssignment/internal/todo/transport"
"manabieAssignment/internal/todo/usecase"
userRepository "manabieAssignment/internal/user/repository"
)

func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}

func run() error {
cfg, err := config.Load()
if err != nil {
log.Fatal(err)
}

cmd := &cobra.Command{
Use: "server",
}

cmd.AddCommand(&cobra.Command{
Use: "server",
Run: func(cmd *cobra.Command, args []string) {
err := serveServer(cfg)
if err != nil {
log.Fatal(err)
}
},
}, migrations.MigrateCommand(cfg.DB.MigrationFolder, cfg.DB.Source),
)
return cmd.Execute()
}

func connectDB(cfg config.DBConfig) (*gorm.DB, error) {
sqlDB, err := sql.Open(cfg.Driver, cfg.Source)
if err != nil {
return nil, err
}
gormDB, err := gorm.Open(postgres.New(postgres.Config{
Conn: sqlDB,
}), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
NamingStrategy: schema.NamingStrategy{
SingularTable: false,
},
})
if err != nil {
return nil, err
}
return gormDB, nil
}

func serveServer(cfg *config.Config) error {
db, err := connectDB(cfg.DB)
if err != nil {
log.Fatalf("cannot connect to database %v", err)
}
r := gin.Default()

todoRepo := todoRepository.NewTodoRepository(db)
userRepo := userRepository.NewUserRepository(db)
todoUC := usecase.NewTodoUseCase(todoRepo, userRepo)
transport.NewTodoHandler(r, todoUC)
return r.Run()
}
80 changes: 80 additions & 0 deletions cmd/migrations/migrations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package migrations

import (
"fmt"
migrateV4 "github.com/golang-migrate/migrate/v4"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"io/ioutil"
"strconv"
"strings"
"time"
)

const versionTimeFormat = "20060102150405"

func MigrateCommand(sourceUrl string, databaseUrl string) *cobra.Command {
cmd := &cobra.Command{
Use: "migrate",
Short: "database migration command",
}

cmd.AddCommand(&cobra.Command{
Use: "up",
Short: "migration up",
Run: func(cmd *cobra.Command, args []string) {
m, err := migrateV4.New(sourceUrl, databaseUrl)
fmt.Println(sourceUrl)
fmt.Println(databaseUrl)
if err != nil {
logrus.Fatal(err)
}
logrus.Info("migration up")
if err := m.Up(); err != nil && err != migrateV4.ErrNoChange {
logrus.Fatal(err)
}
},
}, &cobra.Command{
Use: "down",
Short: "step down migration by N(int)",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
m, err := migrateV4.New(sourceUrl, databaseUrl)
if err != nil {
logrus.Fatal(err)
}
down, err := strconv.Atoi(args[0])
if err != nil {
logrus.Fatal("rev should be a number", err)
}
logrus.Infof("migration down %d", -down)
if err := m.Steps(-down); err != nil {
logrus.Fatal(err)
}
},
}, &cobra.Command{
Use: "create",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
folder := strings.ReplaceAll(sourceUrl, "file://", "")
now := time.Now()
ver := now.Format(versionTimeFormat)
name := strings.Join(args, "-")

up := fmt.Sprintf("%s/%s_%s.up.sql", folder, ver, name)
down := fmt.Sprintf("%s/%s_%s.down.sql", folder, ver, name)

logrus.Infof("create migration %s", name)
logrus.Infof("up script %s", up)
logrus.Infof("down script %s", down)

if err := ioutil.WriteFile(up, []byte{}, 0644); err != nil {
logrus.Fatalf("Create migration up error %v", err)
}
if err := ioutil.WriteFile(down, []byte{}, 0644); err != nil {
logrus.Fatalf("Create migration down error %v", err)
}
},
})
return cmd
}
52 changes: 52 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package config

import (
"bytes"
"encoding/json"
"github.com/spf13/viper"
"strings"
)

type Config struct {
DB DBConfig `mapstructure:"DB" json:"db"`
}

type DBConfig struct {
Driver string `mapstructure:"DRIVER" json:"driver"`
Source string `mapstructure:"SOURCE" json:"source"`
MigrationFolder string `mapstructure:"MIGRATION_FOLDER" json:"migration_folder"`
}

func loadDefaultConfig() *Config {
return &Config{
DB: loadDefaultDBConfig(),
}
}

func loadDefaultDBConfig() DBConfig {
return DBConfig{
Driver: "postgres",
Source: "postgresql://admin:pass@localhost:5432/mnb?sslmode=disable",
MigrationFolder: "file://migrations",
}
}

func Load() (*Config, error) {
c := loadDefaultConfig()

viper.SetConfigType("json")
configBuffer, err := json.Marshal(c)

if err != nil {
return nil, err
}

err = viper.ReadConfig(bytes.NewBuffer(configBuffer))
if err != nil {
return nil, err
}
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "__"))
viper.AutomaticEnv()
err = viper.Unmarshal(c)
return c, err
}
17 changes: 17 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
version: "3.7"

volumes:
postgres_data: { }

services:
db:
image: postgres:12-alpine
restart: always
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
environment:
- POSTGRES_USER=admin
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=mnb
66 changes: 66 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
module manabieAssignment

go 1.18

require (
github.com/gin-gonic/gin v1.8.1
github.com/golang-migrate/migrate/v4 v4.15.2
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.5.0
github.com/spf13/viper v1.12.0
github.com/stretchr/testify v1.7.1
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0
gorm.io/driver/postgres v1.3.7
gorm.io/gorm v1.23.6
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.10.0 // indirect
github.com/goccy/go-json v0.9.7 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.12.1 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/pgtype v1.11.0 // indirect
github.com/jackc/pgx/v4 v4.16.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.4 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/lib/pq v1.10.2 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.2.0 // indirect
github.com/subosito/gotenv v1.3.0 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
go.uber.org/atomic v1.7.0 // indirect
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
golang.org/x/text v0.3.7 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0 // indirect
)
Loading