├── .gitignore ├── internal ├── pkg │ ├── database │ │ ├── gen.go │ │ └── database.go │ ├── bus │ │ ├── bus_test.go │ │ └── bus.go │ ├── validator │ │ └── validator.go │ ├── cache │ │ └── simple_cache.go │ ├── config │ │ └── config.go │ ├── jwt │ │ └── jwt.go │ ├── server │ │ └── server.go │ └── logger │ │ └── logger.go └── app │ ├── module.go │ └── app.go ├── config.toml ├── run.sh ├── modules └── users │ ├── domain │ ├── repository │ │ ├── user_repository.go │ │ └── user_repository_impl.go │ ├── entity │ │ └── user.go │ └── service │ │ └── user_service.go │ ├── dto │ ├── request │ │ └── user_request.go │ └── response │ │ └── user_response.go │ ├── module.go │ └── handler │ └── user_handler.go ├── cleanup.sh ├── Dockerfile ├── docker-compose.yml ├── main.go ├── go.mod ├── README.MD └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | logs/ -------------------------------------------------------------------------------- /internal/pkg/database/gen.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "gorm.io/gen" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | func NewGen(db *gorm.DB, path string, fc interface{}, models ...interface{}) { 9 | g := gen.NewGenerator(gen.Config{ 10 | OutPath: path, 11 | Mode: gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface, 12 | WithUnitTest: true, 13 | }) 14 | 15 | g.UseDB(db) 16 | g.ApplyBasic(models...) 17 | g.ApplyInterface(fc, models...) 18 | g.Execute() 19 | } 20 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | [server] 2 | app_name="Backend Modules" 3 | mode = "info" 4 | port = "9988" 5 | http_timeout = 60 6 | cache_expired = 24 7 | cache_purged = 60 8 | api_version = "1" 9 | 10 | [database] 11 | db_driver = "mysql" 12 | db_host = "localhost" 13 | db_port = "3307" 14 | db_name = "backend_modules" 15 | db_username = "user" 16 | db_password = "password" 17 | 18 | [pool] 19 | conn_idle = 200 20 | conn_max = 300 21 | conn_lifetime = 60 22 | 23 | [jwt] 24 | day_expired = 60 25 | signature_key = "SuperShy!" -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Make sure the script exits on any error 4 | set -e 5 | 6 | echo "Starting the application with Docker Compose..." 7 | 8 | # Build and start the containers 9 | docker-compose up --build -d 10 | 11 | echo "Application is running!" 12 | echo "API is available at http://localhost:8080" 13 | echo "MySQL database is available at localhost:3306" 14 | echo "" 15 | echo "To view logs:" 16 | echo " docker-compose logs -f app" 17 | echo "" 18 | echo "To stop the application:" 19 | echo " docker-compose down" -------------------------------------------------------------------------------- /internal/pkg/bus/bus_test.go: -------------------------------------------------------------------------------- 1 | package bus 2 | 3 | import "testing" 4 | 5 | type testHandler struct { 6 | called bool 7 | } 8 | 9 | func (h *testHandler) Handle(event Event) { 10 | h.called = true 11 | } 12 | 13 | func TestEventBus(t *testing.T) { 14 | bus := NewEventBus() 15 | 16 | handler := &testHandler{} 17 | bus.Subscribe("test", handler) 18 | 19 | event := Event{Type: "test", Payload: "Hello, world!"} 20 | bus.Publish(event) 21 | 22 | bus.wg.Wait() 23 | 24 | if !handler.called { 25 | t.Errorf("Handler was not called") 26 | } 27 | 28 | t.Log("EventBus test passed") 29 | } 30 | -------------------------------------------------------------------------------- /internal/pkg/validator/validator.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import "github.com/go-playground/validator" 4 | 5 | // CustomValidator is a custom validator for Echo 6 | type CustomValidator struct { 7 | validator *validator.Validate 8 | } 9 | 10 | // NewCustomValidator 11 | func NewCustomValidator() *CustomValidator { 12 | return &CustomValidator{ 13 | validator: validator.New(), 14 | } 15 | } 16 | 17 | // Validate validates a struct 18 | func (cv *CustomValidator) Validate(i interface{}) error { 19 | if err := cv.validator.Struct(i); err != nil { 20 | return err 21 | } 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /modules/users/domain/repository/user_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "go-modular-boilerplate/modules/users/domain/entity" 6 | ) 7 | 8 | // UserRepository defines the user repository interface 9 | type UserRepository interface { 10 | FindAll(ctx context.Context) ([]*entity.User, error) 11 | FindByID(ctx context.Context, id uint) (*entity.User, error) 12 | FindByEmail(ctx context.Context, email string) (*entity.User, error) 13 | Create(ctx context.Context, user *entity.User) error 14 | Update(ctx context.Context, user *entity.User) error 15 | Delete(ctx context.Context, id uint) error 16 | } 17 | -------------------------------------------------------------------------------- /modules/users/dto/request/user_request.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | // CreateUserRequest represents a request to create a user 4 | type CreateUserRequest struct { 5 | Name string `json:"name" validate:"required"` 6 | Email string `json:"email" validate:"required,email"` 7 | Password string `json:"password" validate:"required,min=6"` 8 | } 9 | 10 | // UpdateUserRequest represents a request to update a user 11 | type UpdateUserRequest struct { 12 | Name string `json:"name" validate:"required"` 13 | Email string `json:"email" validate:"required,email"` 14 | Password string `json:"password" validate:"omitempty,min=6"` 15 | } 16 | -------------------------------------------------------------------------------- /cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Stopping and removing containers..." 4 | docker-compose down 5 | 6 | echo "Do you want to remove volumes as well? (This will delete all data) [y/N]" 7 | read -r remove_volumes 8 | 9 | if [[ "$remove_volumes" =~ ^[Yy]$ ]]; then 10 | echo "Removing volumes..." 11 | docker-compose down -v 12 | echo "Volumes removed." 13 | fi 14 | 15 | echo "Do you want to remove all related Docker images? [y/N]" 16 | read -r remove_images 17 | 18 | if [[ "$remove_images" =~ ^[Yy]$ ]]; then 19 | echo "Removing images..." 20 | docker rmi $(docker images -q backend_modules:latest mysql:8.0) 2>/dev/null || true 21 | echo "Images removed." 22 | fi 23 | 24 | echo "Cleanup completed." -------------------------------------------------------------------------------- /internal/app/module.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "go-modular-boilerplate/internal/pkg/bus" 5 | "go-modular-boilerplate/internal/pkg/logger" 6 | 7 | "github.com/labstack/echo" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | // Module represents an application module 12 | type Module interface { 13 | // Name returns the name of the module 14 | Name() string 15 | 16 | // Initialize initializes the module 17 | Initialize(db *gorm.DB, logger *logger.Logger, event *bus.EventBus) error 18 | 19 | // RegisterRoutes registers the module's routes 20 | RegisterRoutes(e *echo.Echo, group string) 21 | 22 | // Migrations returns the module's database migrations 23 | Migrations() error 24 | 25 | // Logger returns the module's logger 26 | Logger() *logger.Logger 27 | } 28 | -------------------------------------------------------------------------------- /modules/users/domain/entity/user.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // User represents a user entity 8 | type User struct { 9 | ID uint `gorm:"primaryKey" json:"id"` 10 | Name string `json:"name"` 11 | Email string `json:"email"` 12 | Password string `json:"-"` 13 | CreatedAt time.Time `json:"created_at"` 14 | UpdatedAt time.Time `json:"updated_at"` 15 | } 16 | 17 | // TableName specifies the table name for User 18 | func (*User) TableName() string { 19 | return "users" 20 | } 21 | 22 | // NewUser creates a new user 23 | func NewUser(name, email, password string) *User { 24 | now := time.Now() 25 | return &User{ 26 | Name: name, 27 | Email: email, 28 | Password: password, 29 | CreatedAt: now, 30 | UpdatedAt: now, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Step 1: Use the official Golang image to build the app 2 | # This uses the latest Go version 3 | FROM golang:1.23.1-alpine3.20 AS builder 4 | 5 | # Step 2: Set the working directory inside the container 6 | WORKDIR /app 7 | 8 | # Step 3: Copy the go.mod and go.sum files to the working directory 9 | COPY go.mod go.sum ./ 10 | 11 | # Step 4: Download all Go module dependencies 12 | # Dependencies will be cached if the go.mod and go.sum files haven't changed 13 | RUN go mod download 14 | 15 | # Step 5: Copy the rest of the application code to the container 16 | COPY . . 17 | 18 | # Step 6: Build the Go app 19 | RUN go build -o main ./cmd/app/ 20 | 21 | # Step 7: Use a minimal base image for running the app 22 | FROM alpine:latest 23 | 24 | # Step 8: Set the working directory for the minimal base image 25 | WORKDIR /app 26 | 27 | # Step 9: Copy the Go app from the builder stage 28 | COPY --from=builder /app/main . 29 | 30 | # Step 10: Copy Configuration 31 | COPY config.toml . 32 | 33 | # Step 11: Command to run the app 34 | CMD ["./main", "-c", "config.toml"] -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | backend_modules: 3 | container_name: backend_modules 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | ports: 8 | - "9000:9000" 9 | restart: always 10 | links: 11 | - db 12 | networks: 13 | - infrastructure 14 | depends_on: 15 | db: 16 | condition: service_healthy 17 | db: 18 | image: mysql:8.0 19 | container_name: db 20 | restart: always 21 | environment: 22 | MYSQL_ROOT_HOST: "%" 23 | MYSQL_ROOT_PASSWORD: root_password 24 | MYSQL_DATABASE: backend_modules 25 | MYSQL_USER: user 26 | MYSQL_PASSWORD: password 27 | TZ: "Asia/Jakarta" 28 | ports: 29 | - "3307:3306" 30 | volumes: 31 | - mysql_data:/var/lib/mysql 32 | networks: 33 | - infrastructure 34 | healthcheck: 35 | test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ] 36 | interval: 10s 37 | timeout: 5s 38 | retries: 3 39 | 40 | volumes: 41 | mysql_data: 42 | 43 | networks: 44 | infrastructure: 45 | driver: bridge -------------------------------------------------------------------------------- /modules/users/dto/response/user_response.go: -------------------------------------------------------------------------------- 1 | // internal/modules/user/interfaces/dto/response/user_response.go 2 | 3 | package response 4 | 5 | import ( 6 | "go-modular-boilerplate/modules/users/domain/entity" 7 | "time" 8 | ) 9 | 10 | // UserResponse represents a user response 11 | type UserResponse struct { 12 | ID uint `json:"id"` 13 | Name string `json:"name"` 14 | Email string `json:"email"` 15 | CreatedAt time.Time `json:"created_at"` 16 | UpdatedAt time.Time `json:"updated_at"` 17 | } 18 | 19 | // FromEntity converts a user entity to a user response 20 | func FromEntity(user *entity.User) *UserResponse { 21 | return &UserResponse{ 22 | ID: user.ID, 23 | Name: user.Name, 24 | Email: user.Email, 25 | CreatedAt: user.CreatedAt, 26 | UpdatedAt: user.UpdatedAt, 27 | } 28 | } 29 | 30 | // FromEntities converts a slice of user entities to a slice of user responses 31 | func FromEntities(users []*entity.User) []*UserResponse { 32 | userResponses := make([]*UserResponse, len(users)) 33 | for i, user := range users { 34 | userResponses[i] = FromEntity(user) 35 | } 36 | return userResponses 37 | } 38 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "go-modular-boilerplate/internal/app" 6 | "go-modular-boilerplate/internal/pkg/config" 7 | "go-modular-boilerplate/internal/pkg/logger" 8 | user "go-modular-boilerplate/modules/users" 9 | "log" 10 | "os" 11 | ) 12 | 13 | var configFile *string 14 | 15 | func init() { 16 | configFile = flag.String("c", "config.toml", "configuration file") 17 | flag.Parse() 18 | } 19 | 20 | func main() { 21 | 22 | // Load configuration 23 | cfg := config.NewConfig(*configFile) 24 | if err := cfg.Initialize(); err != nil { 25 | log.Fatalf("Error reading config : %v", err) 26 | os.Exit(1) 27 | } 28 | 29 | // initialize logger 30 | logCfg := logger.DefaultConfig() 31 | 32 | // Start the application 33 | app, err := app.NewApp(&logCfg) 34 | if err != nil { 35 | log.Fatalf("Error creating application : %v", err) 36 | os.Exit(1) 37 | } 38 | 39 | // register modules 40 | app.RegisterModule(user.NewModule()) 41 | 42 | // initialize the application 43 | if err := app.Initialize(); err != nil { 44 | log.Fatalf("Error initializing application : %v", err) 45 | os.Exit(1) 46 | } 47 | 48 | // Start the application 49 | app.Start() 50 | } 51 | -------------------------------------------------------------------------------- /internal/pkg/cache/simple_cache.go: -------------------------------------------------------------------------------- 1 | package simplecache 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/patrickmn/go-cache" 7 | ) 8 | 9 | var Cache *cache.Cache 10 | 11 | type SimpleCache struct { 12 | Cache *cache.Cache 13 | ExpiredAt int 14 | PurgeTime int 15 | } 16 | 17 | type ICache interface { 18 | Open() *cache.Cache 19 | Set(key string, data interface{}) 20 | Get(key string) *interface{} 21 | Delete(key string) 22 | } 23 | 24 | func NewSimpleCache(s SimpleCache) ICache { 25 | 26 | return &SimpleCache{ 27 | ExpiredAt: s.ExpiredAt, 28 | PurgeTime: s.PurgeTime, 29 | } 30 | } 31 | 32 | func (s *SimpleCache) Open() *cache.Cache { 33 | cacheInstance := cache.New(time.Minute*time.Duration(s.ExpiredAt), time.Minute*time.Duration(s.PurgeTime)) 34 | s.Cache = cacheInstance 35 | 36 | return cacheInstance 37 | } 38 | 39 | func (s *SimpleCache) Set(key string, data interface{}) { 40 | s.Cache.Set(key, data, time.Minute*time.Duration(s.ExpiredAt)) 41 | } 42 | 43 | func (s *SimpleCache) Get(key string) *interface{} { 44 | data, found := s.Cache.Get(key) 45 | 46 | if found { 47 | return &data 48 | } 49 | 50 | return nil 51 | } 52 | 53 | func (s *SimpleCache) Delete(key string) { 54 | s.Cache.Delete(key) 55 | } 56 | -------------------------------------------------------------------------------- /internal/pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | type Config struct { 13 | filename string 14 | } 15 | 16 | func NewConfig(filename string) Config { 17 | return Config{filename: filename} 18 | } 19 | func (c *Config) Initialize() error { 20 | 21 | configName := filepath.Base(c.filename) 22 | 23 | configExtension := filepath.Ext(c.filename) 24 | configExtension = strings.TrimPrefix(configExtension, ".") 25 | 26 | viper.SetConfigName(configName) 27 | viper.SetConfigType(configExtension) 28 | viper.AddConfigPath(filepath.Dir(c.filename)) 29 | 30 | viper.AutomaticEnv() 31 | err := viper.ReadInConfig() 32 | 33 | if err != nil { 34 | if _, ok := err.(viper.ConfigFileNotFoundError); ok { 35 | return err 36 | } 37 | return err 38 | } 39 | 40 | return nil 41 | } 42 | 43 | func checkKey(key string) { 44 | if !viper.IsSet(key) { 45 | log.Fatalf("Configuration key %s not found; aborting \n", key) 46 | os.Exit(1) 47 | } 48 | } 49 | 50 | func GetString(key string) string { 51 | checkKey(key) 52 | return viper.GetString(key) 53 | } 54 | 55 | func GetInt(key string) int { 56 | checkKey(key) 57 | return viper.GetInt(key) 58 | } 59 | 60 | func GetBool(key string) bool { 61 | checkKey(key) 62 | return viper.GetBool(key) 63 | } 64 | -------------------------------------------------------------------------------- /modules/users/domain/repository/user_repository_impl.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "go-modular-boilerplate/internal/pkg/database" 7 | "go-modular-boilerplate/modules/users/domain/entity" 8 | ) 9 | 10 | var ( 11 | ERR_RECORD_NOT_FOUND = errors.New("record not found") 12 | ) 13 | 14 | type UserRepositoryImpl struct{} 15 | 16 | // Create implements UserRepository. 17 | func (r UserRepositoryImpl) Create(ctx context.Context, user *entity.User) error { 18 | return database.DB.WithContext(ctx).Create(user).Error 19 | } 20 | 21 | // Delete implements UserRepository. 22 | func (r UserRepositoryImpl) Delete(ctx context.Context, id uint) error { 23 | return database.DB.WithContext(ctx).Delete(&entity.User{}, id).Error 24 | } 25 | 26 | // FindAll finds all users 27 | func (r UserRepositoryImpl) FindAll(ctx context.Context) ([]*entity.User, error) { 28 | var users []*entity.User 29 | result := database.DB.WithContext(ctx).Find(&users) 30 | if result.Error != nil { 31 | return nil, result.Error 32 | } 33 | return users, nil 34 | } 35 | 36 | // FindByEmail implements UserRepository. 37 | func (r UserRepositoryImpl) FindByEmail(ctx context.Context, email string) (*entity.User, error) { 38 | var user entity.User 39 | result := database.DB.WithContext(ctx).Where("email = ?", email).First(&user) 40 | if result.Error != nil { 41 | if result.RowsAffected == 0 { 42 | return nil, ERR_RECORD_NOT_FOUND 43 | } 44 | 45 | return nil, result.Error 46 | } 47 | return &user, nil 48 | } 49 | 50 | // FindByID implements UserRepository. 51 | func (r UserRepositoryImpl) FindByID(ctx context.Context, id uint) (*entity.User, error) { 52 | var user entity.User 53 | result := database.DB.WithContext(ctx).First(&user, id) 54 | if result.Error != nil { 55 | return nil, result.Error 56 | } 57 | return &user, nil 58 | } 59 | 60 | // Update implements UserRepository. 61 | func (r UserRepositoryImpl) Update(ctx context.Context, user *entity.User) error { 62 | return database.DB.WithContext(ctx).Save(user).Error 63 | } 64 | 65 | func NewUserRepositoryImpl() UserRepository { 66 | return UserRepositoryImpl{} 67 | } 68 | -------------------------------------------------------------------------------- /modules/users/domain/service/user_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "go-modular-boilerplate/modules/users/domain/entity" 7 | "go-modular-boilerplate/modules/users/domain/repository" 8 | ) 9 | 10 | // Errors 11 | var ( 12 | ErrUserNotFound = errors.New("user not found") 13 | ErrEmailAlreadyUsed = errors.New("email already in use") 14 | ) 15 | 16 | // UserService handles user domain logic 17 | type UserService struct { 18 | userRepo repository.UserRepository 19 | } 20 | 21 | // NewUserService creates a new user service 22 | func NewUserService(userRepo repository.UserRepository) *UserService { 23 | return &UserService{ 24 | userRepo: userRepo, 25 | } 26 | } 27 | 28 | // GetAllUsers gets all users 29 | func (s *UserService) GetAllUsers(ctx context.Context) ([]*entity.User, error) { 30 | return s.userRepo.FindAll(ctx) 31 | } 32 | 33 | // GetUserByID gets a user by ID 34 | func (s *UserService) GetUserByID(ctx context.Context, id uint) (*entity.User, error) { 35 | user, err := s.userRepo.FindByID(ctx, id) 36 | if err != nil { 37 | return nil, err 38 | } 39 | if user == nil { 40 | return nil, ErrUserNotFound 41 | } 42 | return user, nil 43 | } 44 | 45 | // CreateUser creates a new user 46 | func (s *UserService) CreateUser(ctx context.Context, user *entity.User) error { 47 | // existingUser, err := s.userRepo.FindByEmail(ctx, user.Email) 48 | // if err != nil && err != repository.ERR_RECORD_NOT_FOUND { 49 | // return err 50 | // } 51 | // if existingUser != nil { 52 | // return ErrEmailAlreadyUsed 53 | // } 54 | 55 | return s.userRepo.Create(ctx, user) 56 | } 57 | 58 | // UpdateUser updates a user 59 | func (s *UserService) UpdateUser(ctx context.Context, user *entity.User) error { 60 | existingUser, err := s.userRepo.FindByID(ctx, user.ID) 61 | if err != nil { 62 | return err 63 | } 64 | if existingUser == nil { 65 | return ErrUserNotFound 66 | } 67 | 68 | return s.userRepo.Update(ctx, user) 69 | } 70 | 71 | // DeleteUser deletes a user 72 | func (s *UserService) DeleteUser(ctx context.Context, id uint) error { 73 | existingUser, err := s.userRepo.FindByID(ctx, id) 74 | if err != nil { 75 | return err 76 | } 77 | if existingUser == nil { 78 | return ErrUserNotFound 79 | } 80 | 81 | return s.userRepo.Delete(ctx, id) 82 | } 83 | -------------------------------------------------------------------------------- /internal/pkg/database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | "gorm.io/driver/mysql" 10 | "gorm.io/driver/postgres" 11 | "gorm.io/gorm" 12 | ) 13 | 14 | var ( 15 | DB *gorm.DB 16 | POSGRES_CONFIG = "user=%s password=%s dbname=%s host=%s port=%s sslmode=%s" 17 | MYSQL_CONFIG = "%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local" 18 | ) 19 | 20 | type DBModel struct { 21 | ServerMode string `config:"server_mode"` 22 | Driver string `config:"db_driver"` 23 | Host string `config:"db_host"` 24 | Port string `config:"db_port"` 25 | Name string `config:"db_name"` 26 | Username string `config:"db_username"` 27 | Password string `config:"db_password"` 28 | MaxIdleConn int `config:"conn_idle"` 29 | MaxOpenConn int `config:"conn_max"` 30 | ConnLifeTime int `config:"conn_lifetime"` 31 | } 32 | 33 | func (c *DBModel) OpenDB() (*gorm.DB, *error) { 34 | 35 | var connection gorm.Dialector 36 | 37 | switch c.Driver { 38 | case "postgres": 39 | connectionUrl := fmt.Sprintf(POSGRES_CONFIG, c.Username, c.Password, c.Name, c.Host, c.Port, "disable") 40 | connection = postgres.Open(connectionUrl) 41 | case "mysql": 42 | connectionUrl := fmt.Sprintf(MYSQL_CONFIG, c.Username, c.Password, c.Host, c.Port, c.Name) 43 | connection = mysql.Open(connectionUrl) 44 | default: 45 | log.Fatal("No Database Selected!, Please check config.toml") 46 | os.Exit(1) 47 | } 48 | 49 | db, err := gorm.Open(connection, &gorm.Config{}) 50 | if err != nil { 51 | log.Fatalf("Cannot Connect to DB With Message %s", err.Error()) 52 | return nil, &err 53 | } 54 | 55 | conPool, err := db.DB() 56 | if err != nil { 57 | log.Fatalf("Cannot Create Connection Pool to DB With Message %s", err.Error()) 58 | return nil, &err 59 | } 60 | 61 | /** SetMaxIdleConns sets the maximum number of connections in the idle connection pool. 62 | **/ 63 | conPool.SetMaxIdleConns(c.MaxIdleConn) 64 | 65 | /** SetMaxOpenConns sets the maximum number of open connections to the database. 66 | **/ 67 | conPool.SetMaxOpenConns(c.MaxOpenConn) 68 | 69 | /** SetConnMaxLifetime sets the maximum amount of time a connection may be reused. 70 | **/ 71 | conPool.SetConnMaxLifetime(time.Duration(c.ConnLifeTime) * time.Minute) 72 | 73 | return db, nil 74 | } 75 | -------------------------------------------------------------------------------- /modules/users/module.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "go-modular-boilerplate/internal/pkg/bus" 5 | "go-modular-boilerplate/internal/pkg/logger" 6 | "go-modular-boilerplate/modules/users/domain/entity" 7 | "go-modular-boilerplate/modules/users/domain/repository" 8 | "go-modular-boilerplate/modules/users/domain/service" 9 | "go-modular-boilerplate/modules/users/handler" 10 | 11 | "github.com/labstack/echo" 12 | "gorm.io/gorm" 13 | ) 14 | 15 | // Module implements the application Module interface for the user module 16 | type Module struct { 17 | db *gorm.DB 18 | logger *logger.Logger 19 | userService *service.UserService 20 | userHandler *handler.UserHandler 21 | event *bus.EventBus 22 | } 23 | 24 | // Name returns the name of the module 25 | func (m *Module) Name() string { 26 | return "user" 27 | } 28 | 29 | // Initialize initializes the module 30 | func (m *Module) Initialize(db *gorm.DB, log *logger.Logger, event *bus.EventBus) error { 31 | m.db = db 32 | m.logger = log 33 | m.event = event 34 | 35 | m.logger.Info("Initializing user module") 36 | 37 | // Initialize repositories 38 | userRepo := repository.NewUserRepositoryImpl() 39 | m.logger.Debug("User repository initialized") 40 | 41 | // Initialize services 42 | m.userService = service.NewUserService(userRepo) 43 | m.logger.Debug("User service initialized") 44 | 45 | // Initialize handlers 46 | m.userHandler = handler.NewUserHandler(m.logger, m.event, m.userService) 47 | m.logger.Debug("User handler initialized") 48 | 49 | // register event listeners 50 | m.logger.Info("Registering user module event listeners") 51 | m.event.SubscribeFunc("user.created", m.userHandler.Handle) 52 | 53 | m.logger.Info("User module initialized successfully") 54 | return nil 55 | } 56 | 57 | // RegisterRoutes registers the module's routes 58 | func (m *Module) RegisterRoutes(e *echo.Echo, basePath string) { 59 | m.logger.Info("Registering user routes at %s/users", basePath) 60 | m.userHandler.RegisterRoutes(e, basePath) 61 | m.logger.Debug("User routes registered successfully") 62 | } 63 | 64 | // Migrations returns the module's migrations 65 | func (m *Module) Migrations() error { 66 | m.logger.Info("Registering user module migrations") 67 | return m.db.AutoMigrate(&entity.User{}) 68 | } 69 | 70 | // Logger returns the module's logger 71 | func (m *Module) Logger() *logger.Logger { 72 | return m.logger 73 | } 74 | 75 | // NewModule creates a new user module 76 | func NewModule() *Module { 77 | return &Module{} 78 | } 79 | -------------------------------------------------------------------------------- /internal/pkg/bus/bus.go: -------------------------------------------------------------------------------- 1 | package bus 2 | 3 | import "sync" 4 | 5 | // Event represents an event in our system 6 | type Event struct { 7 | Type string 8 | Payload interface{} 9 | } 10 | 11 | // EventHandler is an interface for event handlers 12 | type EventHandler interface { 13 | Handle(event Event) 14 | } 15 | 16 | // EventHandlerFunc is a function type that implements EventHandler 17 | type EventHandlerFunc func(event Event) 18 | 19 | // Handle calls the function itself 20 | func (f EventHandlerFunc) Handle(event Event) { 21 | f(event) 22 | } 23 | 24 | // EventBus manages the event distribution 25 | type EventBus struct { 26 | eventChannel chan Event 27 | handlers map[string][]EventHandler 28 | mu sync.RWMutex 29 | wg sync.WaitGroup 30 | } 31 | 32 | // NewEventBus creates a new event bus 33 | func NewEventBus() *EventBus { 34 | bus := &EventBus{ 35 | eventChannel: make(chan Event, 100), // Buffer size of 100 events 36 | handlers: make(map[string][]EventHandler), 37 | } 38 | go bus.processEvents() 39 | return bus 40 | } 41 | 42 | // Subscribe registers a handler for a specific event type 43 | func (bus *EventBus) Subscribe(eventType string, handler EventHandler) { 44 | bus.mu.Lock() 45 | defer bus.mu.Unlock() 46 | bus.handlers[eventType] = append(bus.handlers[eventType], handler) 47 | } 48 | 49 | // SubscribeFunc registers a function as a handler for a specific event type 50 | func (bus *EventBus) SubscribeFunc(eventType string, handlerFunc func(event Event)) { 51 | bus.Subscribe(eventType, EventHandlerFunc(handlerFunc)) 52 | } 53 | 54 | // Publish sends an event to the event bus 55 | func (bus *EventBus) Publish(event Event) { 56 | bus.wg.Add(1) 57 | bus.eventChannel <- event 58 | } 59 | 60 | // processEvents processes events from the event channel 61 | func (bus *EventBus) processEvents() { 62 | for event := range bus.eventChannel { 63 | bus.mu.RLock() 64 | handlers, exists := bus.handlers[event.Type] 65 | bus.mu.RUnlock() 66 | 67 | if exists { 68 | for _, handler := range handlers { 69 | // Create a closure to ensure we use the correct handler and event 70 | func(handler EventHandler, event Event) { 71 | defer bus.wg.Done() 72 | handler.Handle(event) 73 | }(handler, event) 74 | } 75 | } else { 76 | bus.wg.Done() 77 | } 78 | } 79 | } 80 | 81 | // Wait waits for all published events to be processed 82 | func (bus *EventBus) Wait() { 83 | bus.wg.Wait() 84 | } 85 | 86 | // Close shuts down the event bus 87 | func (bus *EventBus) Close() { 88 | close(bus.eventChannel) 89 | } 90 | -------------------------------------------------------------------------------- /internal/pkg/jwt/jwt.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | gojwt "github.com/golang-jwt/jwt" 8 | ) 9 | 10 | type JWT interface { 11 | GenerateToken(data map[string]interface{}) (string, error) 12 | ValidateToken(token string) (bool, error) 13 | ParseToken(tokenString string) (map[string]interface{}, error) 14 | } 15 | 16 | type JWTImpl struct { 17 | SignatureKey string 18 | Expiration int 19 | } 20 | 21 | func NewJWTImpl(signatureKey string, expiration int) JWT { 22 | return &JWTImpl{SignatureKey: signatureKey, Expiration: expiration} 23 | } 24 | 25 | func (j *JWTImpl) GenerateToken(data map[string]interface{}) (string, error) { 26 | var mySigningKey = []byte(j.SignatureKey) 27 | token := gojwt.New(gojwt.SigningMethodHS256) 28 | claims := token.Claims.(gojwt.MapClaims) 29 | 30 | for key, value := range data { 31 | claims[key] = value 32 | } 33 | 34 | /** 35 | -jwt expires in day- 36 | for example, if j.Expiration is 20, then the token will expire in 20 days 37 | **/ 38 | expirationDuration := time.Duration(j.Expiration) * 24 * time.Hour * 7 39 | expirationTime := time.Now().Add(expirationDuration).Unix() 40 | claims["exp"] = expirationTime 41 | 42 | tokenString, err := token.SignedString(mySigningKey) 43 | 44 | if err != nil { 45 | return "", err 46 | } 47 | return tokenString, nil 48 | } 49 | 50 | func (j *JWTImpl) ValidateToken(tokenString string) (bool, error) { 51 | token, err := gojwt.Parse(tokenString, func(token *gojwt.Token) (interface{}, error) { 52 | if _, ok := token.Method.(*gojwt.SigningMethodHMAC); !ok { 53 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 54 | } 55 | 56 | return []byte(j.SignatureKey), nil 57 | }) 58 | 59 | if err != nil { 60 | return false, err 61 | } 62 | 63 | claims, ok := token.Claims.(gojwt.MapClaims) 64 | if !ok || !token.Valid { 65 | return false, nil 66 | } 67 | 68 | expirationTime := claims["exp"].(float64) 69 | if time.Now().Unix() > int64(expirationTime) { 70 | return false, nil 71 | } 72 | 73 | return true, nil 74 | } 75 | 76 | func (j *JWTImpl) ParseToken(tokenString string) (map[string]interface{}, error) { 77 | token, err := gojwt.Parse(tokenString, func(token *gojwt.Token) (interface{}, error) { 78 | if _, ok := token.Method.(*gojwt.SigningMethodHMAC); !ok { 79 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 80 | } 81 | return []byte(j.SignatureKey), nil 82 | }) 83 | 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | claims, ok := token.Claims.(gojwt.MapClaims) 89 | if !ok || !token.Valid { 90 | return nil, fmt.Errorf("invalid token") 91 | } 92 | 93 | return claims, nil 94 | } 95 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go-modular-boilerplate 2 | 3 | go 1.23.1 4 | 5 | require ( 6 | github.com/go-playground/validator v9.31.0+incompatible 7 | github.com/golang-jwt/jwt v3.2.2+incompatible 8 | github.com/labstack/echo v3.3.10+incompatible 9 | github.com/natefinch/lumberjack v2.0.0+incompatible 10 | github.com/patrickmn/go-cache v2.1.0+incompatible 11 | github.com/spf13/viper v1.20.0 12 | go.uber.org/zap v1.27.0 13 | gorm.io/driver/mysql v1.5.7 14 | gorm.io/driver/postgres v1.5.11 15 | gorm.io/gen v0.3.26 16 | gorm.io/gorm v1.25.12 17 | ) 18 | 19 | require ( 20 | filippo.io/edwards25519 v1.1.0 // indirect 21 | github.com/BurntSushi/toml v1.4.0 // indirect 22 | github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect 23 | github.com/fsnotify/fsnotify v1.8.0 // indirect 24 | github.com/go-playground/locales v0.14.1 // indirect 25 | github.com/go-playground/universal-translator v0.18.1 // indirect 26 | github.com/go-sql-driver/mysql v1.9.0 // indirect 27 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 28 | github.com/google/uuid v1.6.0 // indirect 29 | github.com/jackc/pgpassfile v1.0.0 // indirect 30 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 31 | github.com/jackc/pgx/v5 v5.7.2 // indirect 32 | github.com/jackc/puddle/v2 v2.2.2 // indirect 33 | github.com/jinzhu/inflection v1.0.0 // indirect 34 | github.com/jinzhu/now v1.1.5 // indirect 35 | github.com/labstack/gommon v0.4.2 // indirect 36 | github.com/leodido/go-urn v1.4.0 // indirect 37 | github.com/mattn/go-colorable v0.1.14 // indirect 38 | github.com/mattn/go-isatty v0.0.20 // indirect 39 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 40 | github.com/sagikazarmark/locafero v0.8.0 // indirect 41 | github.com/sourcegraph/conc v0.3.0 // indirect 42 | github.com/spf13/afero v1.14.0 // indirect 43 | github.com/spf13/cast v1.7.1 // indirect 44 | github.com/spf13/pflag v1.0.6 // indirect 45 | github.com/subosito/gotenv v1.6.0 // indirect 46 | github.com/valyala/bytebufferpool v1.0.0 // indirect 47 | github.com/valyala/fasttemplate v1.2.2 // indirect 48 | go.uber.org/multierr v1.11.0 // indirect 49 | golang.org/x/crypto v0.36.0 // indirect 50 | golang.org/x/mod v0.24.0 // indirect 51 | golang.org/x/net v0.37.0 // indirect 52 | golang.org/x/sync v0.12.0 // indirect 53 | golang.org/x/sys v0.31.0 // indirect 54 | golang.org/x/text v0.23.0 // indirect 55 | golang.org/x/tools v0.31.0 // indirect 56 | gopkg.in/go-playground/assert.v1 v1.2.1 // indirect 57 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect 58 | gopkg.in/yaml.v2 v2.4.0 // indirect 59 | gopkg.in/yaml.v3 v3.0.1 // indirect 60 | gorm.io/datatypes v1.2.5 // indirect 61 | gorm.io/hints v1.1.2 // indirect 62 | gorm.io/plugin/dbresolver v1.5.3 // indirect 63 | ) 64 | -------------------------------------------------------------------------------- /internal/pkg/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | "time" 12 | ) 13 | 14 | type IServer interface { 15 | Run() 16 | RunWithSSL() 17 | } 18 | 19 | type ServerContext struct { 20 | Handler http.Handler 21 | Host string 22 | 23 | CertFile interface{} 24 | KeyFile interface{} 25 | 26 | Timeout time.Duration 27 | ReadTimeout time.Duration 28 | WriteTimeout time.Duration 29 | IdleTimeout time.Duration 30 | } 31 | 32 | func NewServer(s ServerContext) IServer { 33 | return ServerContext{ 34 | Host: s.Host, 35 | CertFile: s.CertFile, 36 | KeyFile: s.KeyFile, 37 | Timeout: s.Timeout, 38 | ReadTimeout: s.ReadTimeout, 39 | WriteTimeout: s.WriteTimeout, 40 | IdleTimeout: s.IdleTimeout, 41 | } 42 | } 43 | 44 | func (s ServerContext) Run() { 45 | // Set up a channel to listen to for interrupt signals 46 | var runChan = make(chan os.Signal, 1) 47 | 48 | // Set up a context to allow for graceful server shutdowns in the event 49 | // of an OS interrupt (defers the cancel just in case) 50 | 51 | ctx, cancel := context.WithTimeout( 52 | context.Background(), 53 | s.Timeout, 54 | ) 55 | defer cancel() 56 | 57 | // Define server options 58 | server := &http.Server{ 59 | Addr: s.Host, 60 | Handler: s.Handler, 61 | ReadTimeout: s.Timeout * time.Second, 62 | WriteTimeout: s.WriteTimeout * time.Second, 63 | IdleTimeout: s.IdleTimeout * time.Second, 64 | } 65 | 66 | fmt.Println(` 67 | .___ _____ ________ 68 | | | _____/ ____\___________ / _____/ ____ 69 | | |/ \ __\\_ __ \__ \ / \ ___ / _ \ 70 | | | | \ | | | \// __ \_ \ \_\ ( <_> ) 71 | |___|___| /__| |__| (____ / \______ /\____/ 72 | \/ \/ \/ 73 | 74 | - Simple Boilerplate made easy - 75 | 76 | `) 77 | 78 | // info 79 | log.Printf("Server Running on : %v", s.Host) 80 | 81 | // Handle ctrl+c/ctrl+x interrupt 82 | signal.Notify(runChan, os.Interrupt, syscall.SIGTERM) 83 | 84 | // Run the server on a new goroutine 85 | go func() { 86 | if err := server.ListenAndServe(); err != nil { 87 | if err != http.ErrServerClosed { 88 | log.Fatalf("Server failed to start due to err: %v", err) 89 | } 90 | } 91 | }() 92 | 93 | // Block on this channel listeninf for those previously defined syscalls assign 94 | // to variable so we can let the user know why the server is shutting down 95 | interrupt := <-runChan 96 | 97 | // If we get one of the pre-prescribed syscalls, gracefully terminate the server 98 | // while alerting the user 99 | log.Fatalf("Server is shutting down due to %+v\n", interrupt) 100 | 101 | if err := server.Shutdown(ctx); err != nil { 102 | log.Fatalf("Server was unable to gracefully shutdown due to err: %+v", err) 103 | } 104 | } 105 | 106 | func (s ServerContext) RunWithSSL() { 107 | 108 | } 109 | -------------------------------------------------------------------------------- /internal/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "go-modular-boilerplate/internal/pkg/bus" 6 | "go-modular-boilerplate/internal/pkg/config" 7 | "go-modular-boilerplate/internal/pkg/database" 8 | "go-modular-boilerplate/internal/pkg/logger" 9 | "go-modular-boilerplate/internal/pkg/server" 10 | _validator "go-modular-boilerplate/internal/pkg/validator" 11 | "time" 12 | 13 | "github.com/labstack/echo" 14 | "github.com/labstack/echo/middleware" 15 | "gorm.io/gorm" 16 | ) 17 | 18 | // App represents the application 19 | type App struct { 20 | db *gorm.DB 21 | server *server.ServerContext 22 | modules []Module 23 | r *echo.Echo 24 | logger *logger.Logger 25 | } 26 | 27 | // NewApp creates a new application 28 | func NewApp(cfg *logger.Config) (*App, error) { 29 | appLogger, err := logger.NewLogger(*cfg, config.GetString("server.app_name")) 30 | if err != nil { 31 | return nil, err 32 | } 33 | defer appLogger.Sync() 34 | return &App{ 35 | modules: make([]Module, 0), 36 | logger: appLogger, 37 | }, nil 38 | } 39 | 40 | func (a *App) SetRouter() *echo.Echo { 41 | return echo.New() 42 | } 43 | 44 | // RegisterModule registers a module with the application 45 | func (a *App) RegisterModule(module Module) { 46 | a.modules = append(a.modules, module) 47 | a.logger.Info("Registered module: %s", module.Name()) 48 | } 49 | 50 | // Initialize initializes the application 51 | func (a *App) Initialize() error { 52 | a.logger.Info("Initializing application...") 53 | 54 | // Initialize database 55 | var err *error 56 | a.db, err = a.SetDatabase().OpenDB() 57 | if err != nil { 58 | a.logger.Error("Failed to initialize database: %v", err) 59 | return *err 60 | } 61 | 62 | // Set database instance for all modules 63 | database.DB = a.db 64 | 65 | // event bus initialization 66 | event := bus.NewEventBus() 67 | 68 | // initialize router 69 | a.r = a.SetRouter() 70 | a.r.Use(middleware.Logger()) 71 | a.r.Use(middleware.Recover()) 72 | a.r.Use(middleware.CORS()) 73 | 74 | // validate request 75 | a.r.Validator = _validator.NewCustomValidator() 76 | 77 | // Initialize modules 78 | for _, module := range a.modules { 79 | a.logger.Info("Initializing module: %s", module.Name()) 80 | 81 | // Create module-specific logger 82 | moduleLogger := a.logger.WithPrefix(module.Name()) 83 | if err := module.Initialize(a.db, moduleLogger, event); err != nil { 84 | a.logger.Error("Failed to initialize module %s: %v", module.Name(), err) 85 | return err 86 | } 87 | 88 | a.logger.Info("Module initialized: %s", module.Name()) 89 | } 90 | 91 | // Run migrations for all modules 92 | for _, module := range a.modules { 93 | err := module.Migrations() 94 | if err != nil { 95 | a.logger.Error("Failed to run migrations for module %s: %v", module.Name(), err) 96 | } 97 | a.logger.Info("Migrations completed for module: %s", module.Name()) 98 | } 99 | 100 | // Initialize HTTP server 101 | a.server = a.SetServer() 102 | 103 | // api version 104 | version := fmt.Sprintf("/api/v%s", config.GetString("server.api_version")) 105 | 106 | // Register routes for all modules 107 | for _, module := range a.modules { 108 | a.logger.Info("Registering routes for module: %s", module.Name()) 109 | module.RegisterRoutes(a.r, version) 110 | a.logger.Info("Routes registered for module: %s", module.Name()) 111 | } 112 | 113 | // append handler to server 114 | a.server.Handler = a.r 115 | 116 | a.logger.Info("Application initialization completed") 117 | 118 | for _, v := range a.r.Routes() { 119 | fmt.Printf("PATH: %v | METHOD: %v\n", v.Path, v.Method) 120 | } 121 | 122 | return nil 123 | } 124 | 125 | // Start starts the application 126 | func (a *App) Start() { 127 | a.logger.Info("Starting server on %s", a.server.Host) 128 | a.server.Run() 129 | } 130 | 131 | // setup database model 132 | func (a *App) SetDatabase() *database.DBModel { 133 | return &database.DBModel{ 134 | ServerMode: config.GetString("server.mode"), 135 | Driver: config.GetString("database.db_driver"), 136 | Host: config.GetString("database.db_host"), 137 | Port: config.GetString("database.db_port"), 138 | Name: config.GetString("database.db_name"), 139 | Username: config.GetString("database.db_username"), 140 | Password: config.GetString("database.db_password"), 141 | MaxIdleConn: config.GetInt("pool.conn_idle"), 142 | MaxOpenConn: config.GetInt("pool.conn_max"), 143 | ConnLifeTime: config.GetInt("pool.conn_lifetime"), 144 | } 145 | } 146 | 147 | // Setup Web Server 148 | func (a *App) SetServer() *server.ServerContext { 149 | return &server.ServerContext{ 150 | Host: ":" + config.GetString("server.port"), 151 | ReadTimeout: time.Duration(config.GetInt("server.http_timeout")), 152 | WriteTimeout: time.Duration(config.GetInt("server.http_timeout")), 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Modular Go Application 2 | 3 | A scalable, modular Golang application built with Echo framework, GORM ORM, and Domain-Driven Design principles. This project features a dynamic module registration system that allows adding new functionality without modifying the core application. 4 | 5 | ## Features 6 | 7 | - **Modular Architecture**: Each feature is contained in its own module with clear boundaries 8 | - **Dynamic Module Binding**: Modules are registered at runtime and loaded automatically 9 | - **Domain-Driven Design**: Clean separation of domain, application, and infrastructure layers 10 | - **RESTful API**: Built with Echo framework for high performance 11 | - **Database Support**: MySQL integration with GORM 12 | - **Docker Support**: Ready for containerized deployment 13 | - **Comprehensive Logging**: Module-aware logging system 14 | 15 | ## Modules 16 | 17 | The application currently includes the following modules for examples: 18 | 19 | 1. **User Module**: 20 | - User management functionality 21 | - CRUD operations for user accounts 22 | 23 | 24 | ## Getting Started 25 | 26 | ### Prerequisites 27 | 28 | - Go 1.20 or higher 29 | - MySQL 8.0 or higher 30 | - Docker and Docker Compose (for containerized setup) 31 | 32 | ### Running with Docker 33 | 34 | The easiest way to run the application is using Docker Compose: 35 | 36 | 1. Clone the repository: 37 | ```bash 38 | git clone https://github.com/zakirkun/go-modular-boilerplate.git 39 | cd go-modular-boilerplate 40 | ``` 41 | 42 | 2. Make the helper scripts executable: 43 | ```bash 44 | chmod +x run.sh cleanup.sh 45 | ``` 46 | 47 | 3. Start the application: 48 | ```bash 49 | ./run.sh 50 | ``` 51 | 52 | 4. The API will be available at http://localhost:8080 53 | 54 | 5. To stop the application: 55 | ```bash 56 | docker-compose down 57 | ``` 58 | 59 | ### Running Locally 60 | 61 | 1. Clone the repository: 62 | ```bash 63 | git clone https://github.com/zakirkun/go-modular-boilerplate.git 64 | cd go-modular-boilerplate 65 | ``` 66 | 67 | 2. Install dependencies: 68 | ```bash 69 | go mod download 70 | ``` 71 | 72 | 3. Set up your environment variables (copy from .env.example): 73 | ```bash 74 | config.toml 75 | # Edit config.toml file with your local configuration 76 | ``` 77 | 78 | 4. Run the application: 79 | ```bash 80 | go run main.go 81 | ``` 82 | 83 | ## API Endpoints 84 | 85 | ### User Module 86 | 87 | - `GET /api/users`: Get all users 88 | - `GET /api/users/:id`: Get a user by ID 89 | - `POST /api/users`: Create a new user 90 | - `PUT /api/users/:id`: Update a user 91 | - `DELETE /api/users/:id`: Delete a user 92 | 93 | ## Configuration 94 | 95 | 96 | ### Logging 97 | - `LOG_LEVEL`: Logging level (DEBUG, INFO, WARN, ERROR, OFF) (default: "INFO") 98 | 99 | ## Adding a New Module 100 | 101 | To create a new module: 102 | 103 | 1. Create a new directory under `modules` 104 | 2. Implement the module interface defined in `internal/app/module.go` 105 | 3. Register the module in `main.go` 106 | 107 | Example of minimal module implementation: 108 | 109 | ```go 110 | package mymodule 111 | 112 | import ( 113 | "github.com/labstack/echo/v4" 114 | "go-modular-boilerplate/your-project/pkg/logger" 115 | "gorm.io/gorm" 116 | ) 117 | 118 | type Module struct { 119 | db *gorm.DB 120 | logger *logger.Logger 121 | } 122 | 123 | func (m *Module) Name() string { 124 | return "mymodule" 125 | } 126 | 127 | func (m *Module) Initialize(db *gorm.DB, log *logger.Logger) error { 128 | m.db = db 129 | m.logger = log 130 | m.logger.Info("My module initialized") 131 | return nil 132 | } 133 | 134 | func (m *Module) RegisterRoutes(e *echo.Echo, basePath string) { 135 | // Register your routes here 136 | } 137 | 138 | func (m *Module) Migrations() []interface{} { 139 | return []interface{}{ 140 | // Your entity structs for migration 141 | } 142 | } 143 | 144 | func (m *Module) Logger() *logger.Logger { 145 | return m.logger 146 | } 147 | 148 | func NewModule() *Module { 149 | return &Module{} 150 | } 151 | ``` 152 | 153 | ## Docker Support 154 | 155 | The application includes: 156 | 157 | - `Dockerfile`: Multi-stage build for the Go application 158 | - `docker-compose.yml`: Configuration for the app and MySQL 159 | - `init.sql`: Database initialization script 160 | - Helper scripts: 161 | - `run.sh`: Start the application with Docker Compose 162 | - `cleanup.sh`: Clean up Docker resources 163 | 164 | ## Logging 165 | 166 | The application uses a custom logging system that: 167 | 168 | - Supports multiple log levels (DEBUG, INFO, WARN, ERROR, OFF) 169 | - Includes timestamps and module names in log entries 170 | - Creates module-specific loggers 171 | - Can be configured globally via environment variables 172 | 173 | Example log output: 174 | ``` 175 | 2025-03-17 14:30:05.123 [app] INFO: Registered module: user 176 | 2025-03-17 14:30:05.125 [user] INFO: Initializing user module 177 | 2025-03-17 14:30:05.130 [user] INFO: User module initialized successfully 178 | ``` 179 | 180 | ## License 181 | 182 | This project is licensed under the MIT License - see the LICENSE file for details. -------------------------------------------------------------------------------- /modules/users/handler/user_handler.go: -------------------------------------------------------------------------------- 1 | // internal/modules/user/interfaces/handler/user_handler.go 2 | 3 | package handler 4 | 5 | import ( 6 | "fmt" 7 | "go-modular-boilerplate/internal/pkg/bus" 8 | "go-modular-boilerplate/internal/pkg/logger" 9 | "go-modular-boilerplate/modules/users/domain/entity" 10 | "go-modular-boilerplate/modules/users/domain/service" 11 | "go-modular-boilerplate/modules/users/dto/request" 12 | "go-modular-boilerplate/modules/users/dto/response" 13 | "net/http" 14 | "strconv" 15 | 16 | "github.com/labstack/echo" 17 | ) 18 | 19 | // UserHandler handles HTTP requests for users 20 | type UserHandler struct { 21 | userService *service.UserService 22 | log *logger.Logger 23 | event *bus.EventBus 24 | } 25 | 26 | // NewUserHandler creates a new user handler 27 | func NewUserHandler(log *logger.Logger, event *bus.EventBus, userService *service.UserService) *UserHandler { 28 | return &UserHandler{ 29 | userService: userService, 30 | log: log, 31 | event: event, 32 | } 33 | } 34 | 35 | // Event Bus Event user created 36 | func (h *UserHandler) Handle(event bus.Event) { 37 | fmt.Printf("User created: %v", event.Payload) 38 | } 39 | 40 | // GetAllUsers gets all users 41 | func (h *UserHandler) GetAllUsers(c echo.Context) error { 42 | ctx := c.Request().Context() 43 | 44 | users, err := h.userService.GetAllUsers(ctx) 45 | if err != nil { 46 | return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) 47 | } 48 | 49 | return c.JSON(http.StatusOK, response.FromEntities(users)) 50 | } 51 | 52 | // GetUser gets a user by ID 53 | func (h *UserHandler) GetUser(c echo.Context) error { 54 | ctx := c.Request().Context() 55 | 56 | id, err := strconv.ParseUint(c.Param("id"), 10, 32) 57 | if err != nil { 58 | return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid user ID"}) 59 | } 60 | 61 | user, err := h.userService.GetUserByID(ctx, uint(id)) 62 | if err != nil { 63 | if err == service.ErrUserNotFound { 64 | return c.JSON(http.StatusNotFound, map[string]string{"error": "User not found"}) 65 | } 66 | return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) 67 | } 68 | 69 | return c.JSON(http.StatusOK, response.FromEntity(user)) 70 | } 71 | 72 | // CreateUser creates a new user 73 | func (h *UserHandler) CreateUser(c echo.Context) error { 74 | ctx := c.Request().Context() 75 | 76 | req := new(request.CreateUserRequest) 77 | if err := c.Bind(req); err != nil { 78 | return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) 79 | } 80 | 81 | if err := c.Validate(req); err != nil { 82 | return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) 83 | } 84 | 85 | user := entity.NewUser(req.Name, req.Email, req.Password) 86 | err := h.userService.CreateUser(ctx, user) 87 | if err != nil { 88 | if err == service.ErrEmailAlreadyUsed { 89 | return c.JSON(http.StatusConflict, map[string]string{"error": "Email already in use"}) 90 | } 91 | return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) 92 | } 93 | 94 | // event bus publish 95 | h.event.Publish(bus.Event{Type: "user.created", Payload: user}) 96 | 97 | return c.JSON(http.StatusCreated, response.FromEntity(user)) 98 | } 99 | 100 | // UpdateUser updates a user 101 | func (h *UserHandler) UpdateUser(c echo.Context) error { 102 | ctx := c.Request().Context() 103 | 104 | id, err := strconv.ParseUint(c.Param("id"), 10, 32) 105 | if err != nil { 106 | return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid user ID"}) 107 | } 108 | 109 | req := new(request.UpdateUserRequest) 110 | if err := c.Bind(req); err != nil { 111 | return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) 112 | } 113 | 114 | if err := c.Validate(req); err != nil { 115 | return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) 116 | } 117 | 118 | user, err := h.userService.GetUserByID(ctx, uint(id)) 119 | if err != nil { 120 | if err == service.ErrUserNotFound { 121 | return c.JSON(http.StatusNotFound, map[string]string{"error": "User not found"}) 122 | } 123 | return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) 124 | } 125 | 126 | user.Name = req.Name 127 | user.Email = req.Email 128 | if req.Password != "" { 129 | user.Password = req.Password 130 | } 131 | 132 | err = h.userService.UpdateUser(ctx, user) 133 | if err != nil { 134 | return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) 135 | } 136 | 137 | return c.JSON(http.StatusOK, response.FromEntity(user)) 138 | } 139 | 140 | // DeleteUser deletes a user 141 | func (h *UserHandler) DeleteUser(c echo.Context) error { 142 | ctx := c.Request().Context() 143 | 144 | id, err := strconv.ParseUint(c.Param("id"), 10, 32) 145 | if err != nil { 146 | return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid user ID"}) 147 | } 148 | 149 | err = h.userService.DeleteUser(ctx, uint(id)) 150 | if err != nil { 151 | if err == service.ErrUserNotFound { 152 | return c.JSON(http.StatusNotFound, map[string]string{"error": "User not found"}) 153 | } 154 | return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) 155 | } 156 | 157 | return c.NoContent(http.StatusNoContent) 158 | } 159 | 160 | // RegisterRoutes registers the user routes 161 | func (h *UserHandler) RegisterRoutes(e *echo.Echo, basePath string) { 162 | group := e.Group(basePath + "/users") 163 | 164 | group.GET("", h.GetAllUsers) 165 | group.GET("/:id", h.GetUser) 166 | group.POST("", h.CreateUser) 167 | group.PUT("/:id", h.UpdateUser) 168 | group.DELETE("/:id", h.DeleteUser) 169 | } 170 | -------------------------------------------------------------------------------- /internal/pkg/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/natefinch/lumberjack" 8 | "go.uber.org/zap" 9 | "go.uber.org/zap/zapcore" 10 | ) 11 | 12 | // Log levels 13 | const ( 14 | DebugLevel = "debug" 15 | InfoLevel = "info" 16 | WarnLevel = "warn" 17 | ErrorLevel = "error" 18 | FatalLevel = "fatal" 19 | ) 20 | 21 | // Logger wraps zap logger 22 | type Logger struct { 23 | zap *zap.Logger 24 | sugar *zap.SugaredLogger 25 | prefix string 26 | level zapcore.Level 27 | } 28 | 29 | // Config holds the logger configuration 30 | type Config struct { 31 | Level string `json:"level"` 32 | Encoding string `json:"encoding"` 33 | OutputPath string `json:"output_path"` 34 | MaxSize int `json:"max_size"` // Maximum size in megabytes before log file rotates 35 | MaxBackups int `json:"max_backups"` // Maximum number of old log files to retain 36 | MaxAge int `json:"max_age"` // Maximum number of days to retain old log files 37 | Compress bool `json:"compress"` // Whether to compress old log files 38 | } 39 | 40 | // DefaultConfig returns the default configuration 41 | func DefaultConfig() Config { 42 | return Config{ 43 | Level: InfoLevel, 44 | Encoding: "json", 45 | OutputPath: "logs/app.log", 46 | MaxSize: 100, 47 | MaxBackups: 3, 48 | MaxAge: 28, 49 | Compress: true, 50 | } 51 | } 52 | 53 | // stringToZapLevel converts a string level to a zapcore level 54 | func stringToZapLevel(level string) zapcore.Level { 55 | switch level { 56 | case DebugLevel: 57 | return zapcore.DebugLevel 58 | case InfoLevel: 59 | return zapcore.InfoLevel 60 | case WarnLevel: 61 | return zapcore.WarnLevel 62 | case ErrorLevel: 63 | return zapcore.ErrorLevel 64 | case FatalLevel: 65 | return zapcore.FatalLevel 66 | default: 67 | return zapcore.InfoLevel 68 | } 69 | } 70 | 71 | // NewLogger creates a new logger with the given configuration 72 | func NewLogger(config Config, prefix string) (*Logger, error) { 73 | // Create directory for logs if it doesn't exist 74 | logDir := filepath.Dir(config.OutputPath) 75 | if err := os.MkdirAll(logDir, 0755); err != nil { 76 | return nil, err 77 | } 78 | 79 | // Set up log rotation 80 | lumberJackLogger := &lumberjack.Logger{ 81 | Filename: config.OutputPath, 82 | MaxSize: config.MaxSize, 83 | MaxBackups: config.MaxBackups, 84 | MaxAge: config.MaxAge, 85 | Compress: config.Compress, 86 | } 87 | 88 | // Set up encoder config 89 | encoderConfig := zapcore.EncoderConfig{ 90 | TimeKey: "timestamp", 91 | LevelKey: "level", 92 | NameKey: "logger", 93 | CallerKey: "caller", 94 | FunctionKey: zapcore.OmitKey, 95 | MessageKey: "message", 96 | StacktraceKey: "stacktrace", 97 | LineEnding: zapcore.DefaultLineEnding, 98 | EncodeLevel: zapcore.CapitalLevelEncoder, 99 | EncodeTime: zapcore.ISO8601TimeEncoder, 100 | EncodeDuration: zapcore.SecondsDurationEncoder, 101 | EncodeCaller: zapcore.ShortCallerEncoder, 102 | } 103 | 104 | // Determine the level 105 | level := stringToZapLevel(config.Level) 106 | 107 | // Create the core 108 | var core zapcore.Core 109 | if config.Encoding == "json" { 110 | core = zapcore.NewCore( 111 | zapcore.NewJSONEncoder(encoderConfig), 112 | zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(lumberJackLogger)), 113 | level, 114 | ) 115 | } else { 116 | core = zapcore.NewCore( 117 | zapcore.NewConsoleEncoder(encoderConfig), 118 | zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(lumberJackLogger)), 119 | level, 120 | ) 121 | } 122 | 123 | // Create the logger 124 | zapLogger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1)) 125 | defer zapLogger.Sync() 126 | 127 | // If prefix is provided, add it to the logger 128 | if prefix != "" { 129 | zapLogger = zapLogger.Named(prefix) 130 | } 131 | 132 | // Create the sugared logger 133 | sugarLogger := zapLogger.Sugar() 134 | 135 | // Return the logger 136 | return &Logger{ 137 | zap: zapLogger, 138 | sugar: sugarLogger, 139 | prefix: prefix, 140 | level: level, 141 | }, nil 142 | } 143 | 144 | // WithPrefix creates a new logger with the given prefix 145 | func (l *Logger) WithPrefix(prefix string) *Logger { 146 | newLogger := l.zap.Named(prefix) 147 | return &Logger{ 148 | zap: newLogger, 149 | sugar: newLogger.Sugar(), 150 | prefix: prefix, 151 | level: l.level, 152 | } 153 | } 154 | 155 | // Debug logs a debug message 156 | func (l *Logger) Debug(msg string, fields ...interface{}) { 157 | l.sugar.Debugw(msg, fields...) 158 | } 159 | 160 | // Info logs an info message 161 | func (l *Logger) Info(msg string, fields ...interface{}) { 162 | l.sugar.Infow(msg, fields...) 163 | } 164 | 165 | // Warn logs a warning message 166 | func (l *Logger) Warn(msg string, fields ...interface{}) { 167 | l.sugar.Warnw(msg, fields...) 168 | } 169 | 170 | // Error logs an error message 171 | func (l *Logger) Error(msg string, fields ...interface{}) { 172 | l.sugar.Errorw(msg, fields...) 173 | } 174 | 175 | // Fatal logs a fatal message 176 | func (l *Logger) Fatal(msg string, fields ...interface{}) { 177 | l.sugar.Fatalw(msg, fields...) 178 | } 179 | 180 | // Sync flushes the logger buffers 181 | func (l *Logger) Sync() error { 182 | return l.zap.Sync() 183 | } 184 | 185 | // Global logger 186 | var defaultLogger *Logger 187 | 188 | // InitDefaultLogger initializes the default logger 189 | func InitDefaultLogger(config Config) error { 190 | var err error 191 | defaultLogger, err = NewLogger(config, "") 192 | return err 193 | } 194 | 195 | // Default returns the default logger 196 | func Default() *Logger { 197 | if defaultLogger == nil { 198 | config := DefaultConfig() 199 | defaultLogger, _ = NewLogger(config, "") 200 | } 201 | return defaultLogger 202 | } 203 | 204 | // Debug logs a debug message to the default logger 205 | func Debug(msg string, fields ...interface{}) { 206 | Default().Debug(msg, fields...) 207 | } 208 | 209 | // Info logs an info message to the default logger 210 | func Info(msg string, fields ...interface{}) { 211 | Default().Info(msg, fields...) 212 | } 213 | 214 | // Warn logs a warning message to the default logger 215 | func Warn(msg string, fields ...interface{}) { 216 | Default().Warn(msg, fields...) 217 | } 218 | 219 | // Error logs an error message to the default logger 220 | func Error(msg string, fields ...interface{}) { 221 | Default().Error(msg, fields...) 222 | } 223 | 224 | // Fatal logs a fatal message to the default logger 225 | func Fatal(msg string, fields ...interface{}) { 226 | Default().Fatal(msg, fields...) 227 | } 228 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 2 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 3 | github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= 4 | github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 9 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 10 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 11 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 12 | github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= 13 | github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 14 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 15 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 16 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 17 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 18 | github.com/go-playground/validator v9.31.0+incompatible h1:UA72EPEogEnq76ehGdEDp4Mit+3FDh548oRqwVgNsHA= 19 | github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig= 20 | github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 21 | github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo= 22 | github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw= 23 | github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= 24 | github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 25 | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 26 | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 27 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= 28 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 29 | github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= 30 | github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= 31 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 32 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 33 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 34 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 35 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 36 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 37 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= 38 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 39 | github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= 40 | github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= 41 | github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= 42 | github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 43 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 44 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 45 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 46 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 47 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 48 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 49 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 50 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 51 | github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= 52 | github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= 53 | github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= 54 | github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= 55 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 56 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 57 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 58 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 59 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 60 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 61 | github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 62 | github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= 63 | github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 64 | github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= 65 | github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= 66 | github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= 67 | github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= 68 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 69 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 70 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 71 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 72 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 73 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 74 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 75 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 76 | github.com/sagikazarmark/locafero v0.8.0 h1:mXaMVw7IqxNBxfv3LdWt9MDmcWDQ1fagDH918lOdVaQ= 77 | github.com/sagikazarmark/locafero v0.8.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= 78 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 79 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 80 | github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= 81 | github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= 82 | github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= 83 | github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 84 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 85 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 86 | github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY= 87 | github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= 88 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 89 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 90 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 91 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 92 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 93 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 94 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 95 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 96 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 97 | github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 98 | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 99 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 100 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 101 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 102 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 103 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 104 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 105 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 106 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 107 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 108 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 109 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 110 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 111 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 112 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 113 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 114 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 115 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 116 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 117 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 118 | golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= 119 | golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 120 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 121 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 122 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 123 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 124 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 125 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 126 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 127 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 128 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 129 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 130 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 131 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 132 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 133 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 134 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 135 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 136 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 137 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 138 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 139 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 140 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 141 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 142 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 143 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 144 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 145 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 146 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 147 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 148 | golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= 149 | golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= 150 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 151 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 152 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 153 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 154 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 155 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 156 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= 157 | gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= 158 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 159 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 160 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 161 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 162 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 163 | gorm.io/datatypes v1.2.5 h1:9UogU3jkydFVW1bIVVeoYsTpLRgwDVW3rHfJG6/Ek9I= 164 | gorm.io/datatypes v1.2.5/go.mod h1:I5FUdlKpLb5PMqeMQhm30CQ6jXP8Rj89xkTeCSAaAD4= 165 | gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= 166 | gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= 167 | gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= 168 | gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= 169 | gorm.io/driver/sqlite v1.5.0 h1:zKYbzRCpBrT1bNijRnxLDJWPjVfImGEn0lSnUY5gZ+c= 170 | gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I= 171 | gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g= 172 | gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g= 173 | gorm.io/gen v0.3.26 h1:sFf1j7vNStimPRRAtH4zz5NiHM+1dr6eA9aaRdplyhY= 174 | gorm.io/gen v0.3.26/go.mod h1:a5lq5y3w4g5LMxBcw0wnO6tYUCdNutWODq5LrIt75LE= 175 | gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= 176 | gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= 177 | gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 178 | gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= 179 | gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= 180 | gorm.io/hints v1.1.2 h1:b5j0kwk5p4+3BtDtYqqfY+ATSxjj+6ptPgVveuynn9o= 181 | gorm.io/hints v1.1.2/go.mod h1:/ARdpUHAtyEMCh5NNi3tI7FsGh+Cj/MIUlvNxCNCFWg= 182 | gorm.io/plugin/dbresolver v1.5.3 h1:wFwINGZZmttuu9h7XpvbDHd8Lf9bb8GNzp/NpAMV2wU= 183 | gorm.io/plugin/dbresolver v1.5.3/go.mod h1:TSrVhaUg2DZAWP3PrHlDlITEJmNOkL0tFTjvTEsQ4XE= 184 | --------------------------------------------------------------------------------