forked from ebhomengo/niki
merge with develop
This commit is contained in:
commit
57faf27457
|
|
@ -1,8 +1,62 @@
|
|||
package benefactorapp
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"context"
|
||||
benefactorHttp "git.gocasts.ir/ebhomengo/niki/benefactorapp/delivery/http"
|
||||
repo "git.gocasts.ir/ebhomengo/niki/benefactorapp/repository/database"
|
||||
benefactor "git.gocasts.ir/ebhomengo/niki/benefactorapp/service"
|
||||
mySql "git.gocasts.ir/ebhomengo/niki/pkg/database/mysql"
|
||||
httpserver "git.gocasts.ir/ebhomengo/niki/pkg/httpserver"
|
||||
logger "git.gocasts.ir/ebhomengo/niki/pkg/logger"
|
||||
)
|
||||
|
||||
type Application struct {
|
||||
Config Config
|
||||
HTTPServer *http.Server
|
||||
HTTPServer benefactorHttp.Server
|
||||
BenefactorService benefactor.Service
|
||||
BenefactorHandler benefactorHttp.Handler
|
||||
BenefactorRepo benefactor.Repository
|
||||
DBConn mySql.DB
|
||||
}
|
||||
|
||||
func Setup(ctx context.Context, config Config, DB mySql.DB) *Application {
|
||||
log := logger.L()
|
||||
log.Info("logger starting ...")
|
||||
|
||||
db := mySql.New(config.MySQLDB)
|
||||
defer func() {
|
||||
if err := db.CloseStatements(); err != nil {
|
||||
log.Info("Error closing statements: %v\n", err)
|
||||
}
|
||||
}()
|
||||
log.Info("mysql connection starting ...")
|
||||
|
||||
// Initialize repositories
|
||||
benefactorRepo := repo.New(db)
|
||||
benefactorValidator := benefactor.NewValidator(benefactorRepo)
|
||||
|
||||
benefactorSvc := benefactor.NewService(benefactorRepo, benefactorValidator)
|
||||
benefactorHandler := benefactorHttp.NewHandler(benefactorSvc)
|
||||
|
||||
hServer, hErr := httpserver.New(config.HTTPServer)
|
||||
if hErr != nil {
|
||||
log.Error("Http Server error: %v,\n", hErr)
|
||||
}
|
||||
httpServer := benefactorHttp.NewServer(*hServer, *benefactorHandler)
|
||||
|
||||
return &Application{
|
||||
Config: config,
|
||||
HTTPServer: httpServer,
|
||||
BenefactorService: benefactorSvc,
|
||||
BenefactorHandler: *benefactorHandler,
|
||||
BenefactorRepo: benefactorRepo,
|
||||
DBConn: DB,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (app *Application) Start() {
|
||||
log := logger.L()
|
||||
log.Info("app starting ...")
|
||||
// TODO implementaion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,23 @@
|
|||
package benefactorapp
|
||||
|
||||
import (
|
||||
database "git.gocasts.ir/ebhomengo/niki/pkg/database/mysql"
|
||||
httpserver "git.gocasts.ir/ebhomengo/niki/pkg/httpserver"
|
||||
"git.gocasts.ir/ebhomengo/niki/pkg/logger"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
// HTTP server config
|
||||
HTTPServer httpserver.Config `koanf:"http_server"`
|
||||
|
||||
// Database config
|
||||
MySQLDB database.Config `koanf:"mariadb"`
|
||||
|
||||
// Logger config
|
||||
Logger logger.Config `koanf:"logger"`
|
||||
|
||||
// Service config
|
||||
|
||||
// Database migration
|
||||
PathOfMigration string `koanf:"path_of_migration"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
benefactor "git.gocasts.ir/ebhomengo/niki/benefactorapp/service"
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type Handler struct{}
|
||||
type Handler struct {
|
||||
BebefactorService benefactor.Service
|
||||
}
|
||||
|
||||
func NewHandler() *Handler {
|
||||
return &Handler{}
|
||||
func NewHandler(bService benefactor.Service) *Handler {
|
||||
return &Handler{
|
||||
BebefactorService: bService,
|
||||
}
|
||||
}
|
||||
|
||||
func (h Handler) HealthCheck(c echo.Context) error {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
package http
|
||||
|
||||
import httpserver "git.gocasts.ir/ebhomengo/niki/delivery/http_server"
|
||||
import httpserver "git.gocasts.ir/ebhomengo/niki/pkg/httpserver"
|
||||
|
||||
type Server struct {
|
||||
HTTPServer *httpserver.Server
|
||||
Handler *Handler
|
||||
HTTPServer httpserver.Server
|
||||
Handler Handler
|
||||
}
|
||||
|
||||
func NewServer(httpserver *httpserver.Server) *Server {
|
||||
return &Server{
|
||||
func NewServer(httpserver httpserver.Server, handler Handler) Server {
|
||||
return Server{
|
||||
HTTPServer: httpserver,
|
||||
Handler: NewHandler(),
|
||||
Handler: handler,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package database
|
||||
|
||||
import "git.gocasts.ir/ebhomengo/niki/repository/mysql"
|
||||
import "git.gocasts.ir/ebhomengo/niki/pkg/database/mysql"
|
||||
|
||||
type DB struct {
|
||||
conn *mysql.DB
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
-- +migrate Up
|
||||
-- please read this article to understand why we use VARCHAR(191)
|
||||
-- https://www.grouparoo.com/blog/varchar-191#why-varchar-and-not-text
|
||||
CREATE TABLE `benefactors1` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`first_name` VARCHAR(191),
|
||||
`last_name` VARCHAR(191),
|
||||
`phone_number` VARCHAR(191) NOT NULL UNIQUE,
|
||||
`description` TEXT,
|
||||
`email` VARCHAR(191),
|
||||
`gender` ENUM('male','female'),
|
||||
`birth_date` TIMESTAMP,
|
||||
`status` ENUM('active','inactive') NOT NULL DEFAULT 'active',
|
||||
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- +migrate Down
|
||||
DROP TABLE `benefactors1`;
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
-- +migrate Up
|
||||
CREATE TABLE `addresses1` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`postal_code` VARCHAR(191) NOT NULL,
|
||||
`address` TEXT NOT NULL,
|
||||
`lat` FLOAT,
|
||||
`lon` FLOAT,
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`city_id` INT NOT NULL,
|
||||
`province_id` INT NOT NULL,
|
||||
`benefactor_id` INT NOT NULL,
|
||||
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`deleted_at` TIMESTAMP,
|
||||
FOREIGN KEY (`province_id`) REFERENCES `provinces` (`id`),
|
||||
FOREIGN KEY (`city_id`) REFERENCES `cities` (`id`),
|
||||
FOREIGN KEY (`benefactor_id`) REFERENCES `benefactors1` (`id`)
|
||||
);
|
||||
|
||||
-- +migrate Down
|
||||
DROP TABLE `addresses1`;
|
||||
|
|
@ -1 +1,17 @@
|
|||
package service
|
||||
|
||||
type Service struct {
|
||||
repository Repository
|
||||
validator Validator
|
||||
}
|
||||
|
||||
type Repository interface {
|
||||
//GetList(ctx context.Context, ID types.ID) ([]entity.Benefactor, error)
|
||||
}
|
||||
|
||||
func NewService(repo Repository, validator Validator) Service {
|
||||
return Service{
|
||||
repository: repo,
|
||||
validator: validator,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1,11 @@
|
|||
package service
|
||||
|
||||
type ValidatorBenefactorRepository interface {
|
||||
}
|
||||
type Validator struct {
|
||||
repo ValidatorBenefactorRepository
|
||||
}
|
||||
|
||||
func NewValidator(repo ValidatorBenefactorRepository) Validator {
|
||||
return Validator{repo: repo}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
migrator "git.gocasts.ir/ebhomengo/niki/pkg/database/migrator"
|
||||
"git.gocasts.ir/ebhomengo/niki/pkg/logger"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var up bool
|
||||
var down bool
|
||||
|
||||
var migrateCmd = &cobra.Command{
|
||||
Use: "migrate",
|
||||
Short: "Run database migrations",
|
||||
Long: `This command runs the database migrations for the benefactor service.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
migrate()
|
||||
},
|
||||
}
|
||||
|
||||
func migrate() {
|
||||
var cfg = loadAppConfig()
|
||||
mysqlConfig := getMysqlConfig()
|
||||
|
||||
logger.Init(cfg.Logger)
|
||||
log := logger.L()
|
||||
|
||||
migratorCfg := migrator.Config{
|
||||
MysqlConfig: mysqlConfig,
|
||||
MigrationPath: cfg.PathOfMigration,
|
||||
MigrationDBName: "gorp_migrations",
|
||||
}
|
||||
// Run migrations if flags are set
|
||||
if migrateUp || migrateDown {
|
||||
mgr := migrator.New(migratorCfg)
|
||||
if migrateUp {
|
||||
log.Info("Running migrations up...")
|
||||
mgr.Up()
|
||||
log.Info("Migrations up completed.")
|
||||
}
|
||||
if migrateDown {
|
||||
log.Info("Running migrations down...")
|
||||
mgr.Down()
|
||||
log.Info("Migrations down completed.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrateCmd.Flags().BoolVar(&up, "up", false, "Run migrations up")
|
||||
migrateCmd.Flags().BoolVar(&down, "down", false, "Run migrations down")
|
||||
RootCmd.AddCommand(migrateCmd)
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.gocasts.ir/ebhomengo/niki/benefactorapp"
|
||||
cfgloader "git.gocasts.ir/ebhomengo/niki/pkg/cfg_loader"
|
||||
"git.gocasts.ir/ebhomengo/niki/pkg/database/mysql"
|
||||
database "git.gocasts.ir/ebhomengo/niki/pkg/database/mysql"
|
||||
serviceConfigMysql "git.gocasts.ir/ebhomengo/niki/pkg/database/mysql"
|
||||
"git.gocasts.ir/ebhomengo/niki/pkg/path"
|
||||
"github.com/spf13/cobra"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var RootCmd = &cobra.Command{
|
||||
Use: "benefactor_service",
|
||||
Short: "A CLI for benefactor Service",
|
||||
Long: `benefactor Service CLI is a tool to manage and run
|
||||
the benefactor service, including migrations and server startup.`,
|
||||
}
|
||||
|
||||
func getMysqlConfig() mysql.Config {
|
||||
var cfg = loadAppConfig()
|
||||
|
||||
mysqlConfig := serviceConfigMysql.Config{
|
||||
Username: cfg.MySQLDB.Username,
|
||||
Password: cfg.MySQLDB.Password,
|
||||
Port: cfg.MySQLDB.Port,
|
||||
Host: cfg.MySQLDB.Host,
|
||||
DBName: cfg.MySQLDB.DBName,
|
||||
}
|
||||
|
||||
return mysqlConfig
|
||||
}
|
||||
|
||||
func getDB(cfg database.Config) *mysql.DB {
|
||||
|
||||
db := serviceConfigMysql.New(cfg)
|
||||
defer func() {
|
||||
if err := db.CloseStatements(); err != nil {
|
||||
fmt.Printf("Error closing statements: %v\n", err)
|
||||
}
|
||||
}()
|
||||
return db
|
||||
}
|
||||
func loadAppConfig() benefactorapp.Config {
|
||||
var cfg benefactorapp.Config
|
||||
|
||||
projectRoot, err := path.PathProjectRoot()
|
||||
if err != nil {
|
||||
log.Fatalf("Error finding project root: %v", err)
|
||||
}
|
||||
|
||||
yamlPath := os.Getenv("CONFIG_PATH")
|
||||
|
||||
if yamlPath == "" {
|
||||
defaultConfig := filepath.Join(projectRoot, "deploy", "benefactor", "development", "config.yml")
|
||||
if _, err := os.Stat(defaultConfig); err == nil {
|
||||
yamlPath = defaultConfig
|
||||
} else {
|
||||
yamlPath = filepath.Join(projectRoot, "deploy", "benefactor", "development", "config.local.yml")
|
||||
}
|
||||
}
|
||||
|
||||
options := cfgloader.Option{
|
||||
Prefix: "BENEFACTOR_",
|
||||
Delimiter: ".",
|
||||
Separator: "__",
|
||||
YamlFilePath: yamlPath,
|
||||
CallbackEnv: nil,
|
||||
}
|
||||
|
||||
if err := cfgloader.Load(options, &cfg); err != nil {
|
||||
log.Fatalf("Failed to load benefactor config: %v", err)
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.gocasts.ir/ebhomengo/niki/benefactorapp"
|
||||
"git.gocasts.ir/ebhomengo/niki/pkg/logger"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var migrateUp bool
|
||||
var migrateDown bool
|
||||
|
||||
var serveCmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "Start the benefactor service",
|
||||
Long: `This command starts the main benefactor service.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
serve()
|
||||
},
|
||||
}
|
||||
|
||||
func serve() {
|
||||
var cfg = loadAppConfig()
|
||||
|
||||
// Initialize logger
|
||||
logger.Init(cfg.Logger)
|
||||
log := logger.L()
|
||||
|
||||
db := getDB(cfg.MySQLDB)
|
||||
migrate()
|
||||
|
||||
// Start the server
|
||||
log.Info("Starting benefactor Service...")
|
||||
|
||||
// Connect to the database
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
app := benefactorapp.Setup(ctx, cfg, *db)
|
||||
app.Start()
|
||||
}
|
||||
|
||||
func init() {
|
||||
serveCmd.Flags().BoolVar(&migrateUp, "migrate-up", false, "Run migrations up before starting the server")
|
||||
serveCmd.Flags().BoolVar(&migrateDown, "migrate-down", false, "Run migrations down before starting the server")
|
||||
RootCmd.AddCommand(serveCmd)
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"git.gocasts.ir/ebhomengo/niki/cmd/benefactor/command"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := command.RootCmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/repository/migrator"
|
||||
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var up bool
|
||||
var down bool
|
||||
|
||||
var migrateCmd = &cobra.Command{
|
||||
Use: "migrate",
|
||||
Short: "Run database migrations",
|
||||
Long: `This command runs the database migrations for the product service.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
migrate()
|
||||
},
|
||||
}
|
||||
|
||||
func migrate() {
|
||||
workingDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting working directory: %v", err)
|
||||
}
|
||||
|
||||
migrationPath := filepath.Join(workingDir, "productapp", "repository", "migrations")
|
||||
|
||||
// to run migrations when you want to run product service locally
|
||||
if path := os.Getenv("MIGRATION_PATH"); path != "" {
|
||||
migrationPath = path
|
||||
log.Printf("Using override migration path: %s", migrationPath)
|
||||
} else {
|
||||
log.Printf("Using default migration path: %s", migrationPath)
|
||||
}
|
||||
|
||||
// TODO: Load config from environment or config file
|
||||
mgr := migrator.New(migrator.Config{
|
||||
MysqlConfig: mysql.Config{
|
||||
Username: getEnv("DB_USERNAME", "root"),
|
||||
Password: getEnv("DB_PASSWORD", ""),
|
||||
Port: 3306,
|
||||
Host: getEnv("DB_HOST", "localhost"),
|
||||
DBName: getEnv("DB_NAME", "niki_db"),
|
||||
},
|
||||
MigrationPath: migrationPath,
|
||||
MigrationDBName: "product_migrations",
|
||||
})
|
||||
|
||||
if up {
|
||||
mgr.Up()
|
||||
} else if down {
|
||||
mgr.Down()
|
||||
} else {
|
||||
log.Println("Please specify a migration direction with --up or --down")
|
||||
}
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrateCmd.Flags().BoolVar(&up, "up", false, "Run migrations up")
|
||||
migrateCmd.Flags().BoolVar(&down, "down", false, "Run migrations down")
|
||||
RootCmd.AddCommand(migrateCmd)
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package command
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
var RootCmd = &cobra.Command{
|
||||
Use: "product_service",
|
||||
Short: "A CLI for Product Service",
|
||||
Long: `Product Service CLI is a tool to manage and run
|
||||
the product service, including migrations and server startup.`,
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var port string
|
||||
|
||||
var serveCmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "Start the product service",
|
||||
Long: `This command starts the main product service.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
serve()
|
||||
},
|
||||
}
|
||||
|
||||
func serve() {
|
||||
log.Println("Product Service Starting...")
|
||||
|
||||
// TODO: Initialize database connection
|
||||
// TODO: Initialize service dependencies
|
||||
// TODO: Setup HTTP server with routes
|
||||
|
||||
// Setup graceful shutdown
|
||||
go func() {
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sigCh
|
||||
log.Println("Shutting down Product Service gracefully...")
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "Product Service OK!")
|
||||
})
|
||||
|
||||
log.Printf("Product Service listening on port %s", port)
|
||||
if err := http.ListenAndServe(":"+port, nil); err != nil {
|
||||
log.Fatalf("Failed to start server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
serveCmd.Flags().StringVarP(&port, "port", "p", "8080", "Port to run the server on")
|
||||
RootCmd.AddCommand(serveCmd)
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/cmd/productapp/command"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := command.RootCmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
package command
|
||||
|
|
@ -0,0 +1 @@
|
|||
package command
|
||||
|
|
@ -0,0 +1 @@
|
|||
package command
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"git.gocasts.ir/ebhomengo/niki/purchaseapp/delivery/http"
|
||||
purchaseMysql "git.gocasts.ir/ebhomengo/niki/purchaseapp/repository/mysql"
|
||||
"git.gocasts.ir/ebhomengo/niki/purchaseapp/service/order"
|
||||
"git.gocasts.ir/ebhomengo/niki/repository/migrator"
|
||||
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
|
||||
)
|
||||
|
||||
func MariaDB() *mysql.DB {
|
||||
cfg := mysql.Config{
|
||||
Username: "niki",
|
||||
Password: "nikiappt0lk2o20",
|
||||
Port: 3306,
|
||||
Host: "localhost",
|
||||
DBName: "niki_db",
|
||||
}
|
||||
migrate := flag.Bool("migrate", false, "perform database migration")
|
||||
flag.Parse()
|
||||
if *migrate {
|
||||
migrator.New(migrator.Config{
|
||||
MysqlConfig: cfg,
|
||||
MigrationPath: "./purchaseapp/repository/mysql/migration",
|
||||
MigrationDBName: "gorp_migrations",
|
||||
}).Up()
|
||||
}
|
||||
|
||||
return mysql.New(cfg)
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := mysql.Config{
|
||||
Username: "niki",
|
||||
Password: "nikiappt0lk2o20",
|
||||
Port: 3306,
|
||||
Host: "localhost",
|
||||
DBName: "niki_db",
|
||||
}
|
||||
db := mysql.New(cfg)
|
||||
defer func() {
|
||||
if err := db.CloseStatements(); err != nil {
|
||||
fmt.Printf("Error closing statements: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
orderRepo := purchaseMysql.New(db)
|
||||
|
||||
orderSvc := Service(orderRepo)
|
||||
server := HTTPServer(orderSvc)
|
||||
server.Serve()
|
||||
|
||||
}
|
||||
|
||||
func HTTPServer(orderSvc order.Service) *http.Server {
|
||||
return http.New(orderSvc)
|
||||
}
|
||||
|
||||
func Service(orderRepo *purchaseMysql.DB) order.Service {
|
||||
return order.New(orderRepo)
|
||||
}
|
||||
|
|
@ -1,15 +1,143 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/staffapp/repository/database"
|
||||
"git.gocasts.ir/ebhomengo/niki/staffapp/service"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println(" Staffapp Server Starting...")
|
||||
|
||||
staffDb := database.New()
|
||||
staffService := service.NewStaffService(staffDb)
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "Staffapp OK!")
|
||||
})
|
||||
|
||||
http.HandleFunc("/staff", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var newStaff service.Staff
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
err := decoder.Decode(&newStaff)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid request payload: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
createdStaff, err := staffService.RegisterStaff(newStaff.Name, newStaff.LastName, newStaff.PhoneNumber)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to register staff: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(createdStaff)
|
||||
})
|
||||
|
||||
http.HandleFunc("/staff/", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
idStr := r.URL.Path[len("/staff/"):]
|
||||
if idStr == "" {
|
||||
http.Error(w, "Missing staff ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid staff ID format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
|
||||
st, err := staffService.Get(id)
|
||||
if err != nil {
|
||||
|
||||
if err.Error() == "staff not found" {
|
||||
http.Error(w, "Staff not found", http.StatusNotFound)
|
||||
} else {
|
||||
http.Error(w, "Failed to fetch staff: "+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(st)
|
||||
|
||||
case http.MethodPut:
|
||||
|
||||
var staffData struct {
|
||||
Name string `json:"Name"`
|
||||
LastName string `json:"LastName"`
|
||||
PhoneNumber string `json:"PhoneNumber"`
|
||||
}
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
if err := decoder.Decode(&staffData); err != nil {
|
||||
http.Error(w, "Invalid request payload: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
updatedStaff, err := staffService.Update(id, staffData.Name, staffData.LastName, staffData.PhoneNumber)
|
||||
if err != nil {
|
||||
|
||||
if err.Error() == "staff not found" {
|
||||
http.Error(w, "Staff not found", http.StatusNotFound)
|
||||
} else {
|
||||
http.Error(w, "Failed to update staff: "+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(updatedStaff)
|
||||
|
||||
case http.MethodDelete:
|
||||
|
||||
err = staffService.Remove(id)
|
||||
if err != nil {
|
||||
if err.Error() == "staff not found" {
|
||||
http.Error(w, "Staff not found", http.StatusNotFound)
|
||||
} else {
|
||||
http.Error(w, "Failed to remove staff: "+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
})
|
||||
|
||||
http.HandleFunc("/staffs", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
list, err := staffService.List()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to fetch staff list: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(list)
|
||||
})
|
||||
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,9 +15,10 @@ const (
|
|||
defaultPrefix = "EB_"
|
||||
defaultDelimiter = "."
|
||||
defaultSeparator = "__"
|
||||
defaultYamlFilePath = "config.yml"
|
||||
)
|
||||
|
||||
var defaultYamlFilePath = "config.yml"
|
||||
|
||||
var c Config
|
||||
|
||||
type Option struct {
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ type TestContainer struct {
|
|||
dockerPool *dockertest.Pool // the connection pool to Docker.
|
||||
mariaResource *dockertest.Resource // MariaDB Docker container resource.
|
||||
redisResource *dockertest.Resource // Redis Docker container resource.
|
||||
mariaDBConn *mysql.DB // Connection to the MariaDB database.
|
||||
redisDBConn *redisadapter.Adapter // Connection to the Redis database.
|
||||
mariaDBConn *mysql.DB // Connection to the MariaDB mysql.
|
||||
redisDBConn *redisadapter.Adapter // Connection to the Redis mysql.
|
||||
containerExpiryInSeconds uint
|
||||
}
|
||||
|
||||
|
|
@ -158,7 +158,7 @@ func (t *TestContainer) Start() {
|
|||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Fatalf("Could not connect to database: %s", err)
|
||||
log.Fatalf("Could not connect to mysql: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
FROM golang:1.25-alpine
|
||||
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
http_server:
|
||||
host: ""
|
||||
port: 1308
|
||||
shutdown_context_timeout: 10s
|
||||
cors:
|
||||
allow_origins:
|
||||
- "*"
|
||||
|
||||
logger:
|
||||
level: "debug" # Can be `debug`, `info`, `warn`, `error`
|
||||
file_path: "logs/benefactorapp/service.log"
|
||||
use_local_time: true
|
||||
file_max_size_in_mb: 10
|
||||
file_max_age_in_days: 7
|
||||
|
||||
database_retry:
|
||||
max_retries: 3
|
||||
retry_delay: 100ms
|
||||
|
||||
total_shutdown_timeout: 30m
|
||||
|
||||
path_of_migration: "./benefactorapp/repository/migration"
|
||||
|
||||
mariadb:
|
||||
port: 3306
|
||||
host: localhost
|
||||
db_name: niki_db
|
||||
username: niki
|
||||
password: nikiappt0lk2o20
|
||||
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package entity
|
||||
|
||||
type Channel struct {
|
||||
ID int8
|
||||
Type NotificationType
|
||||
Provider string
|
||||
Config string
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package entity
|
||||
|
||||
type Notification struct {
|
||||
ID int8
|
||||
Type NotificationType
|
||||
Recipinet string
|
||||
Body string
|
||||
Status NotificationStatus
|
||||
}
|
||||
|
||||
type NotificationType uint8
|
||||
|
||||
const (
|
||||
Email NotificationType = iota + 1
|
||||
SMS
|
||||
Push
|
||||
)
|
||||
|
||||
func (t NotificationType) String() string {
|
||||
switch t {
|
||||
case Email:
|
||||
return "Email"
|
||||
case SMS:
|
||||
return "SMS"
|
||||
case Push:
|
||||
return "Push"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
type NotificationStatus uint8
|
||||
|
||||
const (
|
||||
Pending NotificationStatus = iota + 1
|
||||
Success
|
||||
Failed
|
||||
)
|
||||
|
||||
func (t NotificationStatus) String() string {
|
||||
switch t {
|
||||
case Pending:
|
||||
return "Pending"
|
||||
case Success:
|
||||
return "Success"
|
||||
case Failed:
|
||||
return "Failed"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package messagebroker
|
||||
|
||||
import "git.gocasts.ir/ebhomengo/niki/domain/notification/entity"
|
||||
|
||||
type redis struct {
|
||||
}
|
||||
|
||||
func (r *redis) AddItem(notification entity.Notification) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *redis) RemoveItem(notification entity.Notification) error {
|
||||
return nil
|
||||
}
|
||||
func (r *redis) HealthCheck() error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
_ "time"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/domain/notification/entity"
|
||||
)
|
||||
|
||||
type Notifservice struct {
|
||||
MessageBroker MessageBroker
|
||||
Repository Repository
|
||||
Channel Channel
|
||||
}
|
||||
|
||||
type MessageBroker interface {
|
||||
AddItem(notification entity.Notification) error
|
||||
RemoveItem(notification entity.Notification) error
|
||||
HealthCheck() error
|
||||
}
|
||||
|
||||
type Repository interface {
|
||||
AddItem(notification entity.Notification) error
|
||||
}
|
||||
|
||||
type Channel interface {
|
||||
SendMessage(notification entity.Notification) error
|
||||
}
|
||||
type NotificationServiceRequest struct {
|
||||
Body string
|
||||
Type entity.NotificationType
|
||||
Recipinet string
|
||||
}
|
||||
|
||||
func (n *Notifservice) NewNotification(r NotificationServiceRequest) *entity.Notification {
|
||||
// TODO add validation of notification properties
|
||||
return &entity.Notification{
|
||||
Type: r.Type,
|
||||
Recipinet: r.Recipinet,
|
||||
Body: r.Body,
|
||||
Status: entity.Pending,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Notifservice) Send(notification entity.Notification) error {
|
||||
if err := n.Channel.SendMessage(notification); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Notifservice) Add(notification *entity.Notification) error {
|
||||
err := n.MessageBroker.AddItem(*notification)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Notifservice) Archive(notification *entity.Notification) error {
|
||||
if err := n.MessageBroker.RemoveItem(*notification); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := n.Repository.AddItem(*notification); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package service
|
||||
|
||||
import "git.gocasts.ir/ebhomengo/niki/domain/notification/entity"
|
||||
|
||||
type sender interface {
|
||||
Send(notification entity.Notification) error
|
||||
}
|
||||
33
go.mod
33
go.mod
|
|
@ -7,23 +7,26 @@ require (
|
|||
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/gocasters/rankr v0.0.0-20260222055437-aadc1fdc6a1d
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
github.com/kavenegar/kavenegar-go v0.0.0-20240205151018-77039f51467d
|
||||
github.com/knadh/koanf v1.5.0
|
||||
github.com/knadh/koanf/v2 v2.3.0
|
||||
github.com/labstack/echo-jwt/v4 v4.4.0
|
||||
github.com/labstack/echo/v4 v4.15.1
|
||||
github.com/ory/dockertest/v3 v3.12.0
|
||||
github.com/redis/go-redis/v9 v9.18.0
|
||||
github.com/rubenv/sql-migrate v1.8.1
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/swaggo/echo-swagger v1.5.2
|
||||
github.com/swaggo/swag v1.16.6
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/crypto v0.48.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
|
|
@ -36,20 +39,21 @@ require (
|
|||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/docker/cli v27.4.1+incompatible // indirect
|
||||
github.com/docker/docker v27.1.1+incompatible // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/docker v28.3.3+incompatible // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/spec v0.20.9 // indirect
|
||||
github.com/go-openapi/swag v0.22.3 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.1.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
|
|
@ -59,14 +63,15 @@ require (
|
|||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/sys/user v0.3.0 // indirect
|
||||
github.com/moby/sys/user v0.4.0 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/opencontainers/runc v1.2.3 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/sv-tools/openapi v0.2.1 // indirect
|
||||
github.com/swaggo/files/v2 v2.0.0 // indirect
|
||||
|
|
@ -77,13 +82,13 @@ require (
|
|||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/mod v0.30.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/mod v0.33.0 // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
|
|
|
|||
70
go.sum
70
go.sum
|
|
@ -1,7 +1,7 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||
|
|
@ -58,6 +58,7 @@ github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un
|
|||
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
|
|
@ -68,10 +69,10 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
|
|||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI=
|
||||
github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY=
|
||||
github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
|
||||
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
|
|
@ -84,8 +85,9 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
|
|||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
|
||||
github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
|
||||
|
|
@ -117,8 +119,10 @@ github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1
|
|||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpGyP1XxdC/w=
|
||||
github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gocasters/rankr v0.0.0-20260222055437-aadc1fdc6a1d h1:uFg0KexbouyuGSzmcU9bp/Y7Ml4NXNYjqsRcF/1hnpk=
|
||||
github.com/gocasters/rankr v0.0.0-20260222055437-aadc1fdc6a1d/go.mod h1:nupI7yU3J4wEdsa/dUte/z9apDKkUWeRXlK4dGfntcY=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
|
|
@ -198,6 +202,8 @@ github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKe
|
|||
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
||||
github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs=
|
||||
github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
|
|
@ -218,6 +224,8 @@ github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBF
|
|||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs=
|
||||
github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs=
|
||||
github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM=
|
||||
github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
|
|
@ -278,8 +286,8 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx
|
|||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
|
||||
github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
||||
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
|
|
@ -293,8 +301,8 @@ github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnu
|
|||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/opencontainers/runc v1.2.3 h1:fxE7amCzfZflJO2lHXf4y/y8M1BoAqp+FVmG19oYB80=
|
||||
github.com/opencontainers/runc v1.2.3/go.mod h1:nSxcWUydXrsBZVYNSkTjoQ/N6rcyTtn+1SD5D4+kRIM=
|
||||
github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw=
|
||||
|
|
@ -337,6 +345,7 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV
|
|||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rubenv/sql-migrate v1.8.1 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy2U0=
|
||||
github.com/rubenv/sql-migrate v1.8.1/go.mod h1:BTIKBORjzyxZDS6dzoiw6eAFYJ1iNlGAtjn4LGeVjS8=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||
|
|
@ -346,8 +355,12 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd
|
|||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
|
|
@ -399,13 +412,14 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
|||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
|
|
@ -415,8 +429,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
|
|||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
|
@ -434,8 +448,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
|||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
|
@ -448,8 +462,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
|
@ -483,8 +497,8 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
|
|
@ -492,8 +506,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
|||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
|
|
@ -508,8 +522,8 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK
|
|||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
|
|
|||
2
main.go
2
main.go
|
|
@ -43,7 +43,7 @@ func Config() config.Config {
|
|||
}
|
||||
|
||||
func MariaDB(cfg config.Config) *mysql.DB {
|
||||
migrate := flag.Bool("migrate", false, "perform database migration")
|
||||
migrate := flag.Bool("migrate", false, "perform mysql migration")
|
||||
flag.Parse()
|
||||
if *migrate {
|
||||
migrator.New(migrator.Config{
|
||||
|
|
|
|||
|
|
@ -1 +1,37 @@
|
|||
package patientapp
|
||||
|
||||
import (
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/config"
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/delivery/http/analytic"
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/repository/mysql"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type Application struct {
|
||||
//Config Config
|
||||
HTTPServer *config.EchoServer
|
||||
DB *mysql.DataBase
|
||||
}
|
||||
|
||||
func Setup(cfg config.Config, conn *mysql.DataBase) Application {
|
||||
|
||||
e := echo.New()
|
||||
|
||||
server := config.EchoServer{
|
||||
Router: e,
|
||||
Config: cfg,
|
||||
}
|
||||
|
||||
return Application{
|
||||
//Config: config,
|
||||
HTTPServer: &server,
|
||||
DB: conn,
|
||||
}
|
||||
}
|
||||
|
||||
func (a Application) Start() {
|
||||
|
||||
server := analytic.NewServer(a.HTTPServer)
|
||||
|
||||
_ = server.Serve()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp"
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/config"
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/repository/mysql"
|
||||
)
|
||||
|
||||
func main() {
|
||||
db := mysql.DataBase{}
|
||||
|
||||
cfg := config.Config{
|
||||
Port: 8080,
|
||||
Cors: config.Cors{
|
||||
AllowOrigins: []string{"*"},
|
||||
},
|
||||
ShutDownCtxTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
app := patientapp.Setup(cfg, &db)
|
||||
|
||||
app.Start()
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Port int `koanf:"port"`
|
||||
Cors Cors `koanf:"cors"`
|
||||
ShutDownCtxTimeout time.Duration `koanf:"shutdown_context_timeout"`
|
||||
}
|
||||
|
||||
type Cors struct {
|
||||
AllowOrigins []string `koanf:"allow_origins"`
|
||||
}
|
||||
|
||||
type EchoServer struct {
|
||||
Router *echo.Echo
|
||||
Config Config
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package analytic
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
svc "git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic"
|
||||
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
service svc.Service
|
||||
}
|
||||
|
||||
func NewHandler(service svc.Service) *Handler {
|
||||
return &Handler{
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) Health(e echo.Context) error {
|
||||
return e.JSON(http.StatusOK, map[string]interface{}{"status": "ok"})
|
||||
}
|
||||
|
||||
func (h *Handler) PatientsAnalytic(e echo.Context) error {
|
||||
var req svc.ListPatientAnalyticRequest
|
||||
|
||||
richErr := richerror.New(richerror.Op("fetchingPatientList.PatientsAnalytic"))
|
||||
|
||||
if err := e.Bind(&req); err != nil {
|
||||
richErr = richErr.WithErr(err)
|
||||
richErr = richErr.WithKind(1)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, richErr)
|
||||
}
|
||||
|
||||
response, err := h.service.List(e.Request().Context(), req)
|
||||
if err != nil {
|
||||
richErr = richErr.WithErr(err)
|
||||
richErr = richErr.WithKind(4)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, richErr)
|
||||
}
|
||||
|
||||
return e.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func (h *Handler) PatientsMapSummary(e echo.Context) error {
|
||||
richErr := richerror.New(richerror.Op("fetchingPatientMapSummary.PatientsMapSummary"))
|
||||
|
||||
var req svc.GetPatientMapSummaryRequest
|
||||
|
||||
if err := e.Bind(&req); err != nil {
|
||||
richErr = richErr.WithErr(err)
|
||||
richErr = richErr.WithKind(1)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, richErr)
|
||||
}
|
||||
|
||||
resp, svcErr := h.service.GetMapSummary(e.Request().Context(), req)
|
||||
if svcErr != nil {
|
||||
richErr = richErr.WithErr(svcErr)
|
||||
richErr = richErr.WithKind(4)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, richErr)
|
||||
}
|
||||
|
||||
return e.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package analytic
|
||||
|
||||
import (
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/repository/mysql"
|
||||
analytic2 "git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func NewPatientAnalyticRouter(s *echo.Group) {
|
||||
|
||||
mysqlRepo := mysql.NewPatientRepo()
|
||||
//rpcRepo := grpc.NewPatientRepo()
|
||||
|
||||
analyticService := analytic2.NewPatientAnalyticService(mysqlRepo)
|
||||
|
||||
h := NewHandler(analyticService)
|
||||
|
||||
s.GET("/patients", h.PatientsAnalytic)
|
||||
s.GET("/patients-summary", h.PatientsMapSummary)
|
||||
s.GET("/health", h.Health)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package analytic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/config"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
HTTPServer *config.EchoServer
|
||||
}
|
||||
|
||||
func NewServer(server *config.EchoServer) *Server {
|
||||
|
||||
return &Server{
|
||||
HTTPServer: server,
|
||||
}
|
||||
}
|
||||
|
||||
func (s Server) Serve() error {
|
||||
s.RegisterRoutes()
|
||||
// Start server
|
||||
return s.HTTPServer.Router.Start(fmt.Sprintf(":%d", s.HTTPServer.Config.Port))
|
||||
}
|
||||
|
||||
func (s Server) Stop(ctx context.Context) error {
|
||||
return s.HTTPServer.Router.Shutdown(ctx)
|
||||
|
||||
}
|
||||
|
||||
func (s Server) RegisterRoutes() {
|
||||
|
||||
v1 := s.HTTPServer.Router.Group("/v1")
|
||||
{
|
||||
// Analytic Group
|
||||
analyticGroup := v1.Group("/analytic")
|
||||
NewPatientAnalyticRouter(analyticGroup)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic"
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
|
||||
)
|
||||
|
||||
type AnalyticRepository struct{}
|
||||
|
||||
func NewPatientRepo() *AnalyticRepository {
|
||||
|
||||
return &AnalyticRepository{}
|
||||
}
|
||||
|
||||
func (db *AnalyticRepository) GetPatients(ctx context.Context, f analytic.PatientFilter) ([]entity.Patient, error) {
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (db *AnalyticRepository) CountPatients(ctx context.Context, f analytic.PatientFilter) (int, error) {
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (db *AnalyticRepository) SummaryByCity(ctx context.Context, provinceID uint, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) {
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (db *AnalyticRepository) SummaryByProvince(ctx context.Context, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) {
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic"
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
|
||||
)
|
||||
|
||||
type DataBase struct{}
|
||||
|
||||
func NewPatientRepo() *DataBase {
|
||||
|
||||
return &DataBase{}
|
||||
}
|
||||
|
||||
func (db *DataBase) GetPatients(ctx context.Context, f analytic.PatientFilter) ([]entity.Patient, error) {
|
||||
|
||||
return nil, nil
|
||||
|
||||
}
|
||||
|
||||
func (db *DataBase) CountPatients(ctx context.Context, f analytic.PatientFilter) (int, error) {
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (db *DataBase) SummaryByCity(ctx context.Context, provinceID uint, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) {
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (db *DataBase) SummaryByProvince(ctx context.Context, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) {
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package analytic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/jalaali/go-jalaali"
|
||||
"time"
|
||||
)
|
||||
|
||||
func normalizeLimitOffset(limit, offset int) (int, int) {
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
return limit, offset
|
||||
}
|
||||
|
||||
// convert age range -> DOB range
|
||||
func ageRangeToDOB(minAge, maxAge *int, now time.Time) (dobFrom, dobTo *string) {
|
||||
if maxAge != nil {
|
||||
t := now.AddDate(-(*maxAge + 1), 0, 1)
|
||||
jy, jm, jd, err := jalaali.ToJalaali(t.Year(), t.Month(), t.Day())
|
||||
if err != nil {
|
||||
}
|
||||
s := fmt.Sprintf("%04d/%02d/%02d", jy, jm, jd)
|
||||
dobFrom = &s
|
||||
}
|
||||
|
||||
if minAge != nil {
|
||||
t := now.AddDate(-*minAge, 0, 0)
|
||||
jy, jm, jd, err := jalaali.ToJalaali(t.Year(), t.Month(), t.Day())
|
||||
if err != nil {
|
||||
}
|
||||
s := fmt.Sprintf("%04d/%02d/%02d", jy, jm, jd)
|
||||
dobTo = &s
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package analytic
|
||||
|
||||
import (
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
|
||||
)
|
||||
|
||||
type ListPatientAnalyticRequest struct {
|
||||
// All fields are optional
|
||||
MinAge *int `query:"minAge,omitempty"`
|
||||
MaxAge *int `query:"maxAge,omitempty"`
|
||||
Sex *entity.Sex `query:"sex,omitempty"`
|
||||
|
||||
City *int64 `query:"city,omitempty"`
|
||||
Province *int64 `query:"province,omitempty"`
|
||||
|
||||
Search *string `query:"search,omitempty"`
|
||||
|
||||
Pagination *Pagination `query:"pagination,omitempty"`
|
||||
}
|
||||
|
||||
type PatientAnalyticItem struct {
|
||||
ID int64 `json:"id"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"Last_name"`
|
||||
DateOfBirth string `json:"dob,omitempty"`
|
||||
Sex entity.Sex `json:"sex"`
|
||||
Phone string `json:"phone"`
|
||||
Address entity.Address `json:"address"`
|
||||
}
|
||||
type PatientAnalyticResponse struct {
|
||||
Items []PatientAnalyticItem `json:"items"`
|
||||
Pagination *Pagination `json:"pagination"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
func ToPatientResponse(patient entity.Patient) PatientAnalyticItem {
|
||||
return PatientAnalyticItem{
|
||||
ID: patient.ID,
|
||||
FirstName: patient.FirstName,
|
||||
LastName: patient.LastName,
|
||||
DateOfBirth: patient.DateOfBirth,
|
||||
Sex: patient.Sex,
|
||||
Phone: patient.Phone,
|
||||
Address: entity.Address{
|
||||
ProvinceID: patient.Address.ProvinceID,
|
||||
CityID: patient.Address.CityID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetPatientMapSummaryRequest =========================== Map ==================================
|
||||
type GetPatientMapSummaryRequest struct {
|
||||
Level entity.MapLevel `query:"level"`
|
||||
ParentID *int `query:"parentID"`
|
||||
|
||||
MinAge *int `query:"minAge,omitempty"`
|
||||
MaxAge *int `query:"maxAge,omitempty"`
|
||||
Sex *entity.Sex `query:"sex,omitempty"`
|
||||
Search *string `query:"search,omitempty"`
|
||||
}
|
||||
|
||||
type GetPatientMapSummaryResponse struct {
|
||||
Level entity.MapLevel `json:"level"`
|
||||
Items map[uint][]entity.MapSummaryItem `json:"items"`
|
||||
}
|
||||
|
||||
// Pagination ================================ Pagination =============================
|
||||
type Pagination struct {
|
||||
Limit int `query:"limit,omitempty"`
|
||||
Offset int `query:"offset,omitempty"`
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package analytic
|
||||
|
||||
import (
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
|
||||
)
|
||||
|
||||
type PatientFilter struct {
|
||||
DOBFrom *string // born after
|
||||
DOBTo *string // born before
|
||||
Sex *entity.Sex
|
||||
|
||||
City *int64
|
||||
Province *int64
|
||||
Country *int64
|
||||
|
||||
Search *string
|
||||
|
||||
Limit int
|
||||
Offset int
|
||||
}
|
||||
|
||||
type PatientMapFilter struct {
|
||||
MinDOB *string
|
||||
MaxDOB *string
|
||||
Sex *entity.Sex
|
||||
Search *string
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
package analytic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidProvinceID = errors.New("invalid province id")
|
||||
ErrInvalidCountryID = errors.New("invalid country id")
|
||||
ErrInvalidMapLevel = errors.New("invalid map level")
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetPatients(ctx context.Context, f PatientFilter) ([]entity.Patient, error)
|
||||
CountPatients(ctx context.Context, f PatientFilter) (int, error)
|
||||
|
||||
SummaryByCity(ctx context.Context, provinceID uint, f PatientMapFilter) (map[uint][]entity.MapSummaryItem, error)
|
||||
SummaryByProvince(ctx context.Context, f PatientMapFilter) (map[uint][]entity.MapSummaryItem, error)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
repository Repository
|
||||
}
|
||||
|
||||
func NewPatientAnalyticService(repo Repository) Service {
|
||||
return Service{
|
||||
repository: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s Service) List(ctx context.Context, req ListPatientAnalyticRequest) (PatientAnalyticResponse, error) {
|
||||
|
||||
limit, offset := normalizeLimitOffset(req.Pagination.Limit, req.Pagination.Offset)
|
||||
|
||||
// convert age range
|
||||
dobFrom, dobTo := ageRangeToDOB(req.MinAge, req.MaxAge, time.Now())
|
||||
|
||||
filter := PatientFilter{
|
||||
DOBFrom: dobFrom,
|
||||
DOBTo: dobTo,
|
||||
Sex: req.Sex,
|
||||
City: req.City,
|
||||
Province: req.Province,
|
||||
Search: req.Search,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
}
|
||||
|
||||
items, err := s.repository.GetPatients(ctx, filter)
|
||||
if err != nil {
|
||||
return PatientAnalyticResponse{}, fmt.Errorf("GetPatients: %w", err)
|
||||
}
|
||||
|
||||
total, err := s.repository.CountPatients(ctx, filter)
|
||||
if err != nil {
|
||||
return PatientAnalyticResponse{}, fmt.Errorf("CountPatients: %w", err)
|
||||
}
|
||||
|
||||
// mapping response
|
||||
out := make([]PatientAnalyticItem, 0, len(items))
|
||||
for _, value := range items {
|
||||
out = append(out, ToPatientResponse(value))
|
||||
}
|
||||
|
||||
return PatientAnalyticResponse{
|
||||
Items: out,
|
||||
Pagination: &Pagination{
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
},
|
||||
Total: total,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func (s Service) GetMapSummary(ctx context.Context, req GetPatientMapSummaryRequest) (GetPatientMapSummaryResponse, error) {
|
||||
|
||||
dobFrom, dobTo := ageRangeToDOB(req.MinAge, req.MaxAge, time.Now())
|
||||
|
||||
filter := PatientMapFilter{
|
||||
MinDOB: dobFrom,
|
||||
MaxDOB: dobTo,
|
||||
Sex: req.Sex,
|
||||
Search: req.Search,
|
||||
}
|
||||
|
||||
switch req.Level {
|
||||
case entity.MapLevelCity:
|
||||
if req.ParentID == nil || *req.ParentID <= 0 {
|
||||
return GetPatientMapSummaryResponse{}, ErrInvalidProvinceID
|
||||
}
|
||||
|
||||
items, err := s.repository.SummaryByCity(ctx, uint(*req.ParentID), filter)
|
||||
if err != nil {
|
||||
return GetPatientMapSummaryResponse{}, fmt.Errorf("SummaryByCity: %w", err)
|
||||
}
|
||||
return GetPatientMapSummaryResponse{Level: req.Level, Items: items}, nil
|
||||
|
||||
case entity.MapLevelProvince:
|
||||
if req.ParentID == nil || *req.ParentID <= 0 {
|
||||
return GetPatientMapSummaryResponse{}, ErrInvalidCountryID
|
||||
}
|
||||
|
||||
items, err := s.repository.SummaryByProvince(ctx, filter)
|
||||
if err != nil {
|
||||
return GetPatientMapSummaryResponse{}, fmt.Errorf("SummaryByProvince: %w", err)
|
||||
}
|
||||
return GetPatientMapSummaryResponse{Level: req.Level, Items: items}, nil
|
||||
|
||||
default:
|
||||
return GetPatientMapSummaryResponse{}, ErrInvalidMapLevel
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package entity
|
||||
|
||||
type Address struct {
|
||||
ID uint
|
||||
PostalCode string
|
||||
Address string
|
||||
Name string
|
||||
Lat float64
|
||||
Lon float64
|
||||
CityID uint
|
||||
ProvinceID uint
|
||||
}
|
||||
|
||||
type AddressAggregated struct {
|
||||
Address Address
|
||||
Province Province
|
||||
City City
|
||||
}
|
||||
|
||||
type Province struct {
|
||||
ID uint
|
||||
Name string
|
||||
}
|
||||
type City struct {
|
||||
ID uint
|
||||
Name string
|
||||
ProvinceID uint
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package entity
|
||||
|
||||
type MapLevel string
|
||||
|
||||
const (
|
||||
MapLevelCity MapLevel = "city"
|
||||
MapLevelProvince MapLevel = "province"
|
||||
MapLevelCountry MapLevel = "country"
|
||||
)
|
||||
|
||||
type MapSummaryItem struct {
|
||||
LocationID int64
|
||||
Name string
|
||||
Count int
|
||||
CentroidLat float64
|
||||
CentroidLng float64
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package entity
|
||||
|
||||
type Patient struct {
|
||||
ID int64
|
||||
FirstName string
|
||||
LastName string
|
||||
DateOfBirth string
|
||||
Sex Sex
|
||||
Phone string
|
||||
Address Address
|
||||
CaseStatus CaseStatus
|
||||
ReferralSource ReferralSource
|
||||
AssignedStaffId int64
|
||||
StartDate string
|
||||
EndDate string
|
||||
}
|
||||
|
||||
// Sex ================================== Sex type ==========================================
|
||||
type Sex string
|
||||
|
||||
const (
|
||||
SexUnknown Sex = "unknown"
|
||||
SexMale Sex = "male"
|
||||
SexFemale Sex = "female"
|
||||
SexOther Sex = "other"
|
||||
)
|
||||
|
||||
func (s Sex) SexValidation() bool {
|
||||
switch s {
|
||||
case SexUnknown, SexMale, SexFemale, SexOther:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// CaseStatus =================================== Case Status =======================================
|
||||
type CaseStatus string
|
||||
|
||||
const (
|
||||
Open CaseStatus = "open"
|
||||
Close CaseStatus = "close"
|
||||
InProgress CaseStatus = "inProgress"
|
||||
)
|
||||
|
||||
func (s CaseStatus) CaseStatusValidation() bool {
|
||||
switch s {
|
||||
case Open, Close, InProgress:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ReferralSource =================================== Referral source =======================================
|
||||
type ReferralSource string
|
||||
|
||||
const (
|
||||
Hospital ReferralSource = "hospital"
|
||||
Community ReferralSource = "community"
|
||||
Other ReferralSource = "other"
|
||||
)
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package cfgloader
|
||||
|
||||
import (
|
||||
"github.com/knadh/koanf"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/knadh/koanf/parsers/yaml"
|
||||
"github.com/knadh/koanf/providers/env"
|
||||
"github.com/knadh/koanf/providers/file"
|
||||
//"github.com/knadh/koanf/v2"
|
||||
)
|
||||
|
||||
type Option struct {
|
||||
Prefix string
|
||||
Delimiter string
|
||||
Separator string
|
||||
YamlFilePath string
|
||||
CallbackEnv func(string) string
|
||||
}
|
||||
|
||||
// defaultCallbackEnv processes environment variable keys based on provided prefix and separator
|
||||
func defaultCallbackEnv(source, prefix, separator string) string {
|
||||
base := strings.ToLower(strings.TrimPrefix(source, prefix))
|
||||
return strings.ReplaceAll(base, separator, ".")
|
||||
}
|
||||
|
||||
// Load function loads configuration from YAML file and environment variables based on provided options
|
||||
func Load(options Option, config interface{}) error {
|
||||
k := koanf.New(options.Delimiter)
|
||||
|
||||
// Load configuration from YAML file if provided
|
||||
if options.YamlFilePath != "" {
|
||||
if err := k.Load(file.Provider(options.YamlFilePath), yaml.Parser()); err != nil {
|
||||
log.Fatalf("Error loading config file: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Define callback function for environment variables
|
||||
callback := options.CallbackEnv
|
||||
if callback == nil {
|
||||
// Set default callback using the prefix and separator from options
|
||||
callback = func(source string) string {
|
||||
return defaultCallbackEnv(source, options.Prefix, options.Separator)
|
||||
}
|
||||
}
|
||||
|
||||
// Load environment variables with the specified prefix and callback
|
||||
if err := k.Load(env.Provider(options.Prefix, options.Delimiter, callback), nil); err != nil {
|
||||
log.Fatalf("Error loading environment variables: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Unmarshal into provided config structure (passing address)
|
||||
if err := k.Unmarshal("", &config); err != nil {
|
||||
log.Fatalf("Error unmarshalling config: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package migrator
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/pkg/database/mysql"
|
||||
migrate "github.com/rubenv/sql-migrate"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
MysqlConfig mysql.Config
|
||||
MigrationPath string
|
||||
MigrationDBName string
|
||||
}
|
||||
|
||||
type Migrator struct {
|
||||
cfg Config
|
||||
dialect string
|
||||
migrations *migrate.FileMigrationSource
|
||||
}
|
||||
|
||||
// TODO - set migration table name
|
||||
// TODO - add limit to Up and Down method
|
||||
|
||||
func New(cfg Config) Migrator {
|
||||
// OR: Read migrations from a folder:
|
||||
migrations := &migrate.FileMigrationSource{
|
||||
Dir: cfg.MigrationPath,
|
||||
}
|
||||
|
||||
return Migrator{cfg: cfg, dialect: "mysql", migrations: migrations}
|
||||
}
|
||||
|
||||
func (m Migrator) Up() {
|
||||
migrate.SetTable(m.cfg.MigrationDBName)
|
||||
|
||||
db, err := sql.Open(m.dialect, fmt.Sprintf("%s:%s@(%s:%d)/%s?parseTime=true",
|
||||
m.cfg.MysqlConfig.Username, m.cfg.MysqlConfig.Password, m.cfg.MysqlConfig.Host, m.cfg.MysqlConfig.Port, m.cfg.MysqlConfig.DBName))
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("can't open mysql db: %w", err))
|
||||
}
|
||||
|
||||
n, err := migrate.Exec(db, m.dialect, m.migrations, migrate.Up)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("can't apply migrations: %w", err))
|
||||
}
|
||||
fmt.Printf("Applied %d migrations!\n", n)
|
||||
}
|
||||
|
||||
func (m Migrator) Down() {
|
||||
migrate.SetTable(m.cfg.MigrationDBName)
|
||||
|
||||
db, err := sql.Open(m.dialect, fmt.Sprintf("%s:%s@(%s:%d)/%s?parseTime=true",
|
||||
m.cfg.MysqlConfig.Username, m.cfg.MysqlConfig.Password, m.cfg.MysqlConfig.Host, m.cfg.MysqlConfig.Port, m.cfg.MysqlConfig.DBName))
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("can't open mysql db: %w", err))
|
||||
}
|
||||
|
||||
n, err := migrate.Exec(db, m.dialect, m.migrations, migrate.Down)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("can't rollback migrations: %w", err))
|
||||
}
|
||||
fmt.Printf("Rollbacked %d migrations!\n", n)
|
||||
}
|
||||
|
||||
func (m Migrator) Status() {
|
||||
// TODO - add status
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
querier "git.gocasts.ir/ebhomengo/niki/pkg/query_transaction/sql"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Username string `koanf:"username"`
|
||||
Password string `koanf:"password"`
|
||||
Port int `koanf:"port"`
|
||||
Host string `koanf:"host"`
|
||||
DBName string `koanf:"db_name"`
|
||||
}
|
||||
|
||||
type DB struct {
|
||||
config Config
|
||||
db *querier.SQLDB
|
||||
mu sync.Mutex
|
||||
statements map[statementKey]*sql.Stmt
|
||||
}
|
||||
|
||||
func (db *DB) Conn() *querier.SQLDB {
|
||||
return db.db
|
||||
}
|
||||
|
||||
// TODO: this temporary to ignore linter error (magic number).
|
||||
const (
|
||||
dbMaxConnLifetime = time.Minute * 3
|
||||
dbMaxOpenConns = 10
|
||||
dbMaxIdleConns = 10
|
||||
)
|
||||
|
||||
func New(config Config) *DB {
|
||||
// parseTime=true changes the output type of DATE and DATETIME values to time.Time
|
||||
// instead of []byte / string
|
||||
// The date or datetime like 0000-00-00 00:00:00 is converted into zero value of time.Time
|
||||
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@(%s:%d)/%s?parseTime=true",
|
||||
config.Username, config.Password, config.Host, config.Port, config.DBName))
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("can't open mysql db: %w", err))
|
||||
}
|
||||
|
||||
// See "Important settings" section.
|
||||
db.SetConnMaxLifetime(dbMaxConnLifetime)
|
||||
db.SetMaxOpenConns(dbMaxOpenConns)
|
||||
db.SetMaxIdleConns(dbMaxIdleConns)
|
||||
|
||||
return &DB{
|
||||
config: config,
|
||||
db: &querier.SQLDB{DB: db},
|
||||
statements: make(map[statementKey]*sql.Stmt),
|
||||
}
|
||||
}
|
||||
|
||||
func (db *DB) PrepareStatement(ctx context.Context, key statementKey, query string) (*sql.Stmt, error) {
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
|
||||
if stmt, ok := db.statements[key]; ok {
|
||||
return stmt, nil
|
||||
}
|
||||
|
||||
stmt, err := db.db.PrepareContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db.statements[key] = stmt
|
||||
|
||||
return stmt, nil
|
||||
}
|
||||
|
||||
func (db *DB) CloseStatements() error {
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
|
||||
for _, stmt := range db.statements {
|
||||
err := stmt.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
production:
|
||||
dialect: mysql
|
||||
datasource: niki:nikiappt0lk2o20@(localhost:3306)/niki_db?parseTime=true
|
||||
dir: pkg/database/mysql/migration
|
||||
table: gorp_migrations
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package mysql
|
||||
|
||||
type statementKey uint
|
||||
|
||||
const (
|
||||
StatementKeyAddressCreateForBenefactor statementKey = iota + 1
|
||||
StatementKeyAddressDeleteForBenefactor
|
||||
StatementKeyAddressIsExistByID
|
||||
StatementKeyAddressGetByID
|
||||
StatementKeyAddressGetAllByBenefactorID
|
||||
StatementKeyAddressUpdate
|
||||
StatementKeyCityGetProvinceIDByID
|
||||
StatementKeyCityIsExistByID
|
||||
StatementKeyCityGetAll
|
||||
StatementKeyProvinceIsExistByID
|
||||
StatementKeyProvinceGetAll
|
||||
StatementKeyAdminAccessControlGetPermissions
|
||||
StatementKeyAdminAdd
|
||||
StatementKeyAdminExistByEmail
|
||||
StatementKeyAdminExistByPhoneNumber
|
||||
StatementKeyAdminAgentExistByID
|
||||
StatementKeyAdminGetByID
|
||||
StatementKeyAdminGetByPhoneNumber
|
||||
StatementKeyAdminAgentGetAll
|
||||
StatementKeyBenefactorGetByID
|
||||
StatementKeyBenefactorGetByIDs
|
||||
StatementKeyBenefactorGetByPhoneNumber
|
||||
StatementKeyBenefactorCreate
|
||||
StatementKeyBenefactorGetAll
|
||||
StatementKeyKindBoxAdd
|
||||
StatementKeyKindBoxAssignReceiverAgent
|
||||
StatementKeyKindBoxEnumerate
|
||||
StatementKeyKindBoxExistForBenefactor
|
||||
StatementKeyKindBoxGetByID
|
||||
StatementKeyKindBoxGetAwaitingReturnByAgent
|
||||
StatementKeyKindBoxRegisterEmptyingRequest
|
||||
StatementKeyKindBoxReturn
|
||||
StatementKeyKindBoxReqAccept
|
||||
StatementKeyKindBoxReqAdd
|
||||
StatementKeyKindBoxReqAssignSenderAgent
|
||||
StatementKeyKindBoxReqDeleteByID
|
||||
StatementKeyKindBoxReqGetByID
|
||||
StatementKeyKindBoxReqGetAwaitingDeliveryByAgent
|
||||
StatementKeyKindBoxReqRollbackToPendingStatus
|
||||
StatementKeyKindBoxReqReject
|
||||
StatementKeyKindBoxReqUpdate
|
||||
StatementKeyKindBoxUpdate
|
||||
StatementKeyReferTimeGetByID
|
||||
StatementKeyReferTimeGetAll
|
||||
StatementKeyBenefactorUpdate
|
||||
StatementKeyBenefactorUpdateStatus
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package mysql
|
||||
|
||||
type Scanner interface {
|
||||
Scan(dest ...any) error
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package redisotp
|
||||
|
||||
import "git.gocasts.ir/ebhomengo/niki/adapter/redis"
|
||||
|
||||
type DB struct {
|
||||
adapter *redis.Adapter
|
||||
}
|
||||
|
||||
func New(adapter *redis.Adapter) *DB {
|
||||
return &DB{adapter: adapter}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package date_parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ParseDate parses a date string in "YYYY-MM-DD" format and returns a time.Time object
|
||||
func ParseDate(input string) (time.Time, error) {
|
||||
const layout = "2006-01-02"
|
||||
|
||||
// Parse the input string
|
||||
convertedDate, err := time.Parse(layout, input)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("invalid date format: %v", err)
|
||||
}
|
||||
|
||||
return convertedDate, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package httpserver
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
DefaultShutdownTimeout = 10 * time.Second
|
||||
)
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
package httpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
echomiddleware "github.com/gocasters/rankr/pkg/echo_middleware"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Host string `koanf:"host"`
|
||||
Port int `koanf:"port"`
|
||||
CORS CORS `koanf:"cors"`
|
||||
ShutdownTimeout time.Duration `koanf:"shutdown_context_timeout"`
|
||||
HideBanner bool `koanf:"hide_banner"`
|
||||
HidePort bool `koanf:"hide_port"`
|
||||
PublicPaths []string `koanf:"public_paths"`
|
||||
|
||||
// Optional Otel middleware can be injected from outside.
|
||||
OtelMiddleware echo.MiddlewareFunc
|
||||
}
|
||||
|
||||
type CORS struct {
|
||||
AllowOrigins []string `koanf:"allow_origins"`
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
router *echo.Echo
|
||||
config *Config
|
||||
|
||||
requireClaimsOnce sync.Once
|
||||
}
|
||||
|
||||
var basePublicPaths = []string{
|
||||
"/v1/login",
|
||||
"/v1/refresh-token",
|
||||
"/v1/me",
|
||||
"/ping",
|
||||
"/ping-otel",
|
||||
}
|
||||
|
||||
func New(cfg Config) (*Server, error) {
|
||||
if cfg.Port < 1 || cfg.Port > 65535 {
|
||||
return nil, fmt.Errorf("invalid port: %d", cfg.Port)
|
||||
}
|
||||
|
||||
if cfg.ShutdownTimeout <= 0 {
|
||||
cfg.ShutdownTimeout = DefaultShutdownTimeout
|
||||
}
|
||||
|
||||
e := echo.New()
|
||||
|
||||
if cfg.OtelMiddleware != nil {
|
||||
e.Use(cfg.OtelMiddleware)
|
||||
}
|
||||
|
||||
e.Use(middleware.RequestID())
|
||||
e.Use(middleware.Logger())
|
||||
e.Use(middleware.Recover())
|
||||
e.Use(
|
||||
middleware.CORSWithConfig(
|
||||
middleware.CORSConfig{
|
||||
AllowOrigins: cfg.CORS.AllowOrigins,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
return &Server{
|
||||
router: e,
|
||||
config: &cfg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetRouter() *echo.Echo {
|
||||
s.requireClaimsOnce.Do(func() {
|
||||
s.router.Use(
|
||||
echomiddleware.RequireUserInfo(
|
||||
echomiddleware.RequireUserInfoOptions{
|
||||
Skipper: newPublicPathSkipper(s.config.PublicPaths...),
|
||||
},
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
return s.router
|
||||
}
|
||||
|
||||
func (s *Server) GetConfig() *Config {
|
||||
return s.config
|
||||
}
|
||||
|
||||
func (s *Server) Start() error {
|
||||
addr := fmt.Sprintf("%s:%d", s.config.Host, s.config.Port)
|
||||
|
||||
s.router.HideBanner = s.config.HideBanner
|
||||
s.router.HidePort = s.config.HidePort
|
||||
|
||||
return s.router.Start(addr)
|
||||
}
|
||||
|
||||
func (s *Server) Stop(ctx context.Context) error {
|
||||
return s.router.Shutdown(ctx)
|
||||
}
|
||||
|
||||
func newPublicPathSkipper(extraPaths ...string) middleware.Skipper {
|
||||
paths := make([]string, 0, len(basePublicPaths)+len(extraPaths))
|
||||
paths = append(paths, basePublicPaths...)
|
||||
paths = append(paths, extraPaths...)
|
||||
staticPublicPathSkipper := echomiddleware.SkipExactPaths(paths...)
|
||||
|
||||
return func(c echo.Context) bool {
|
||||
path := strings.TrimSuffix(c.Request().URL.Path, "/")
|
||||
return staticPublicPathSkipper(c) ||
|
||||
strings.HasSuffix(path, "/health-check") ||
|
||||
strings.HasSuffix(path, "/health_check")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
package httpserver_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gocasters/rankr/pkg/authhttp"
|
||||
"github.com/gocasters/rankr/pkg/httpserver"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestNew tests the constructor function for various scenarios.
|
||||
func TestNew(t *testing.T) {
|
||||
t.Run("successful creation with valid config", func(t *testing.T) {
|
||||
// Arrange
|
||||
cfg := httpserver.Config{
|
||||
Port: 8080,
|
||||
ShutdownTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
// Act
|
||||
server, err := httpserver.New(cfg)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, server)
|
||||
assert.NotNil(t, server.GetRouter(), "GetRouter should return a non-nil router")
|
||||
})
|
||||
|
||||
t.Run("error on invalid port", func(t *testing.T) {
|
||||
// Arrange
|
||||
cfg := httpserver.Config{Port: 0} // Invalid port
|
||||
|
||||
// Act
|
||||
server, err := httpserver.New(cfg)
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, server)
|
||||
assert.Contains(t, err.Error(), "invalid port")
|
||||
})
|
||||
|
||||
t.Run("sets default shutdown timeout", func(t *testing.T) {
|
||||
// Arrange
|
||||
cfg := httpserver.Config{
|
||||
Port: 8080,
|
||||
ShutdownTimeout: 0, // No timeout provided
|
||||
}
|
||||
|
||||
// Act
|
||||
server, err := httpserver.New(cfg)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, server)
|
||||
assert.Equal(t, httpserver.DefaultShutdownTimeout, server.GetConfig().ShutdownTimeout, "Default timeout should be set")
|
||||
})
|
||||
}
|
||||
|
||||
// TestOtelMiddlewareInjection verifies that optional middleware is correctly added.
|
||||
func TestOtelMiddlewareInjection(t *testing.T) {
|
||||
// Arrange
|
||||
middlewareWasCalled := false
|
||||
mockOtelMiddleware := func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
middlewareWasCalled = true
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
cfg := httpserver.Config{
|
||||
Port: 8080,
|
||||
OtelMiddleware: mockOtelMiddleware,
|
||||
}
|
||||
|
||||
server, err := httpserver.New(cfg)
|
||||
assert.NoError(t, err)
|
||||
|
||||
server.GetRouter().GET("/test", func(c echo.Context) error {
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
// Act
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
userInfo, encErr := authhttp.EncodeUserInfo("1")
|
||||
assert.NoError(t, encErr)
|
||||
req.Header.Set("X-User-Info", userInfo)
|
||||
rec := httptest.NewRecorder()
|
||||
server.GetRouter().ServeHTTP(rec, req)
|
||||
|
||||
// Assert
|
||||
assert.True(t, middlewareWasCalled, "The injected Otel middleware should have been called")
|
||||
}
|
||||
|
||||
// TestRouteRegistrationAndResponse confirms that routes can be added and respond correctly.
|
||||
func TestRouteRegistrationAndResponse(t *testing.T) {
|
||||
// Arrange
|
||||
server, err := httpserver.New(httpserver.Config{Port: 8080})
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedResponse := "Hello, Tester!"
|
||||
|
||||
// Act: Register a new GET route using the GetRouter() method.
|
||||
server.GetRouter().GET("/hello", func(c echo.Context) error {
|
||||
return c.String(http.StatusOK, expectedResponse)
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/hello", nil)
|
||||
userInfo, encErr := authhttp.EncodeUserInfo("1")
|
||||
assert.NoError(t, encErr)
|
||||
req.Header.Set("X-User-Info", userInfo)
|
||||
rec := httptest.NewRecorder()
|
||||
server.GetRouter().ServeHTTP(rec, req)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
assert.Equal(t, expectedResponse, rec.Body.String())
|
||||
}
|
||||
|
||||
func TestStopWithTimeout(t *testing.T) {
|
||||
// Arrange
|
||||
server, err := httpserver.New(httpserver.Config{Port: 9090}) // Use a different port to avoid conflicts
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Act: Start the server in a separate goroutine
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
errCh <- server.Start()
|
||||
}()
|
||||
|
||||
// Stop the server and verify shutdown succeeds
|
||||
stopErr := server.Stop(t.Context())
|
||||
assert.NoError(t, stopErr, "StopWithTimeout should not return an error when stopping a running server")
|
||||
|
||||
// Now verify Start() exited due to shutdown
|
||||
startErr := <-errCh
|
||||
assert.ErrorIs(t, startErr, http.ErrServerClosed, "server.Start() should return ErrServerClosed after shutdown")
|
||||
}
|
||||
|
||||
func TestPublicRoutesAndProtectedRoutes(t *testing.T) {
|
||||
server, err := httpserver.New(httpserver.Config{Port: 8080})
|
||||
assert.NoError(t, err)
|
||||
|
||||
server.GetRouter().GET("/v1/module/health-check", func(c echo.Context) error {
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
server.GetRouter().POST("/v1/login", func(c echo.Context) error {
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
server.GetRouter().GET("/secure", func(c echo.Context) error {
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
healthReq := httptest.NewRequest(http.MethodGet, "/v1/module/health-check", nil)
|
||||
healthRec := httptest.NewRecorder()
|
||||
server.GetRouter().ServeHTTP(healthRec, healthReq)
|
||||
assert.Equal(t, http.StatusOK, healthRec.Code)
|
||||
|
||||
loginReq := httptest.NewRequest(http.MethodPost, "/v1/login", nil)
|
||||
loginRec := httptest.NewRecorder()
|
||||
server.GetRouter().ServeHTTP(loginRec, loginReq)
|
||||
assert.Equal(t, http.StatusOK, loginRec.Code)
|
||||
|
||||
secureReq := httptest.NewRequest(http.MethodGet, "/secure", nil)
|
||||
secureRec := httptest.NewRecorder()
|
||||
server.GetRouter().ServeHTTP(secureRec, secureReq)
|
||||
assert.Equal(t, http.StatusUnauthorized, secureRec.Code)
|
||||
}
|
||||
|
||||
func TestConfigPublicPathsAreSkipped(t *testing.T) {
|
||||
server, err := httpserver.New(httpserver.Config{
|
||||
Port: 8080,
|
||||
PublicPaths: []string{" v1/public/info/ "},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
server.GetRouter().GET("/v1/public/info", func(c echo.Context) error {
|
||||
return c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/public/info", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
server.GetRouter().ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package logger
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
var globalLogger *slog.Logger
|
||||
var once sync.Once
|
||||
|
||||
type Config struct {
|
||||
Level string `koanf:"level"`
|
||||
FilePath string `koanf:"file_path"`
|
||||
UseLocalTime bool `koanf:"use_local_time"`
|
||||
FileMaxSizeInMB int `koanf:"file_max_size_in_mb"`
|
||||
FileMaxAgeInDays int `koanf:"file_max_age_in_days"`
|
||||
}
|
||||
|
||||
// Init initializes the global logger instance.
|
||||
func Init(cfg Config) {
|
||||
once.Do(func() {
|
||||
workingDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting current working directory: %v", err)
|
||||
}
|
||||
fileWriter := &lumberjack.Logger{
|
||||
Filename: filepath.Join(workingDir, cfg.FilePath),
|
||||
LocalTime: cfg.UseLocalTime,
|
||||
MaxSize: cfg.FileMaxSizeInMB,
|
||||
MaxAge: cfg.FileMaxAgeInDays,
|
||||
}
|
||||
|
||||
level := mapLevel(cfg.Level)
|
||||
globalLogger = slog.New(
|
||||
slog.NewJSONHandler(io.MultiWriter(fileWriter, os.Stdout), &slog.HandlerOptions{
|
||||
Level: level,
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// L returns the global logger instance.
|
||||
func L() *slog.Logger {
|
||||
return globalLogger
|
||||
}
|
||||
|
||||
// New creates a new logger instance for each service with specific settings.
|
||||
func New(cfg Config) *slog.Logger {
|
||||
workingDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting current working directory: %v", err)
|
||||
}
|
||||
|
||||
fileWriter := &lumberjack.Logger{
|
||||
Filename: filepath.Join(workingDir, cfg.FilePath),
|
||||
LocalTime: cfg.UseLocalTime,
|
||||
MaxSize: cfg.FileMaxSizeInMB,
|
||||
MaxAge: cfg.FileMaxAgeInDays,
|
||||
}
|
||||
return slog.New(
|
||||
slog.NewJSONHandler(io.MultiWriter(fileWriter), &slog.HandlerOptions{}),
|
||||
)
|
||||
}
|
||||
|
||||
func mapLevel(levelStr string) slog.Level {
|
||||
switch levelStr {
|
||||
case "debug":
|
||||
return slog.LevelDebug
|
||||
case "info":
|
||||
return slog.LevelInfo
|
||||
case "warn":
|
||||
return slog.LevelWarn
|
||||
case "error":
|
||||
return slog.LevelError
|
||||
default:
|
||||
return slog.LevelInfo
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package path
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// PathProjectRoot searches upwards from the current directory to find the project root,
|
||||
// identified by the presence of a "go.mod" file.
|
||||
func PathProjectRoot() (string, error) {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for {
|
||||
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
|
||||
return dir, nil
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "go.work")); err == nil {
|
||||
return dir, nil
|
||||
}
|
||||
if fi, err := os.Stat(filepath.Join(dir, ".git")); err == nil && fi.IsDir() {
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
return "", errors.New("go.mod not found in any parent directory")
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
-- +migrate Up
|
||||
CREATE TABLE `categories` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`slug` VARCHAR(255) NOT NULL UNIQUE,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_persian_ci;
|
||||
|
||||
-- +migrate Down
|
||||
DROP TABLE IF EXISTS `categories`;
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
-- +migrate Up
|
||||
CREATE TABLE `products` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`slug` VARCHAR(255) NOT NULL UNIQUE,
|
||||
`description` TEXT,
|
||||
`price` DECIMAL(10, 2) NOT NULL,
|
||||
`stock` INT DEFAULT 0,
|
||||
`is_active` BOOLEAN DEFAULT TRUE,
|
||||
`features` JSON DEFAULT NULL,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`deleted_at` TIMESTAMP DEFAULT NULL,
|
||||
FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE SET NULL
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_persian_ci;
|
||||
|
||||
-- +migrate Down
|
||||
DROP TABLE IF EXISTS `products`;
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
-- +migrate Up
|
||||
CREATE TABLE `category_products` (
|
||||
`category_id` INT NOT NULL,
|
||||
`product_id` INT NOT NULL,
|
||||
PRIMARY KEY (`category_id`, `product_id`),
|
||||
FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_persian_ci;
|
||||
|
||||
-- +migrate Down
|
||||
DROP TABLE IF EXISTS `category_products`;
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
-- +migrate Up
|
||||
CREATE TABLE `product_images` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`product_id` INT NOT NULL,
|
||||
`image_path` VARCHAR(255) NOT NULL,
|
||||
`is_primary` BOOLEAN DEFAULT FALSE,
|
||||
FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_persian_ci;
|
||||
|
||||
-- +migrate Down
|
||||
DROP TABLE IF EXISTS `product_images`;
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package category
|
||||
|
||||
import "time"
|
||||
|
||||
type Category struct {
|
||||
ID uint
|
||||
Name string
|
||||
Slug string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
package category
|
||||
|
|
@ -0,0 +1 @@
|
|||
package category
|
||||
|
|
@ -0,0 +1 @@
|
|||
package category
|
||||
|
|
@ -1 +0,0 @@
|
|||
package service
|
||||
|
|
@ -1 +0,0 @@
|
|||
package service
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package product
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Product struct {
|
||||
ID uint
|
||||
Name string
|
||||
Slug string
|
||||
Description string
|
||||
Price float64
|
||||
Stock int
|
||||
IsActive bool
|
||||
Features string
|
||||
CreatedAt time.Time
|
||||
DeletedAt sql.NullTime
|
||||
}
|
||||
|
||||
type ProductImage struct {
|
||||
ID uint
|
||||
ProductID uint
|
||||
ImagePath string
|
||||
IsPrimary bool
|
||||
}
|
||||
|
||||
type CategoryProduct struct {
|
||||
CategoryID uint
|
||||
ProductID uint
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
package product
|
||||
|
|
@ -0,0 +1 @@
|
|||
package product
|
||||
|
|
@ -0,0 +1 @@
|
|||
package product
|
||||
|
|
@ -1 +0,0 @@
|
|||
package service
|
||||
|
|
@ -1 +0,0 @@
|
|||
package service
|
||||
|
|
@ -1 +1,61 @@
|
|||
package purchaseapp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
purchaseHTTP "git.gocasts.ir/ebhomengo/niki/purchaseapp/delivery/http"
|
||||
purchaseHandler "git.gocasts.ir/ebhomengo/niki/purchaseapp/delivery/http/order"
|
||||
purchaseMysql "git.gocasts.ir/ebhomengo/niki/purchaseapp/repository/mysql"
|
||||
purchaseService "git.gocasts.ir/ebhomengo/niki/purchaseapp/service/order"
|
||||
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
|
||||
)
|
||||
|
||||
type Application struct {
|
||||
Config Config
|
||||
HTTPServer *purchaseHTTP.Server
|
||||
purchaseService purchaseService.Service
|
||||
PurchaseHandler *purchaseHandler.Handler
|
||||
PurchaseRepo purchaseService.Repo
|
||||
DB *mysql.DB
|
||||
}
|
||||
|
||||
func SetUp(ctx context.Context, config Config, DB mysql.DB) *Application {
|
||||
|
||||
//cfg := mysql.Config{
|
||||
// Username: "niki",
|
||||
// Password: "nikiappt0lk2o20",
|
||||
// Port: 3306,
|
||||
// Host: "localhost",
|
||||
// DBName: "niki_db",
|
||||
//}
|
||||
db := mysql.New(config.Mysql)
|
||||
defer func() {
|
||||
if err := db.CloseStatements(); err != nil {
|
||||
fmt.Printf("Error closing statements: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
orderRepo := purchaseMysql.New(db)
|
||||
|
||||
orderSvc := Service(orderRepo)
|
||||
server := HTTPServer(orderSvc)
|
||||
handler := purchaseHandler.New(orderSvc)
|
||||
|
||||
return &Application{
|
||||
Config: Config{},
|
||||
HTTPServer: server,
|
||||
purchaseService: orderSvc,
|
||||
PurchaseHandler: handler,
|
||||
PurchaseRepo: orderRepo,
|
||||
DB: &DB,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func HTTPServer(orderSvc purchaseService.Service) *purchaseHTTP.Server {
|
||||
return purchaseHTTP.New(orderSvc)
|
||||
}
|
||||
|
||||
func Service(orderRepo *purchaseMysql.DB) purchaseService.Service {
|
||||
return purchaseService.New(orderRepo)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
package purchaseapp
|
||||
|
||||
import (
|
||||
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Mysql mysql.Config `koanf:"mariadb"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
package invoice
|
||||
|
||||
type Handler struct {
|
||||
}
|
||||
|
||||
func New() *Handler {
|
||||
return &Handler{}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package invoice
|
||||
|
||||
import "github.com/labstack/echo/v4"
|
||||
|
||||
func (h Handler) SetRoutes(e *echo.Echo) {
|
||||
|
||||
}
|
||||
|
|
@ -1,7 +1,68 @@
|
|||
package order
|
||||
|
||||
type Handler struct{}
|
||||
import (
|
||||
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
|
||||
"git.gocasts.ir/ebhomengo/niki/purchaseapp/entity"
|
||||
"git.gocasts.ir/ebhomengo/niki/purchaseapp/service/order"
|
||||
"github.com/labstack/echo/v4"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func New() *Handler {
|
||||
return &Handler{}
|
||||
type Handler struct {
|
||||
orderSvc *order.Service
|
||||
}
|
||||
|
||||
func New(orderSvc order.Service) *Handler {
|
||||
return &Handler{orderSvc: &orderSvc}
|
||||
}
|
||||
|
||||
func (h *Handler) CreateOrderHandler(c echo.Context) error {
|
||||
var req order.CreateOrderRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
msg, code := getErrorDataFromRichError(err)
|
||||
return echo.NewHTTPError(code, msg)
|
||||
}
|
||||
|
||||
orderItems := req.OrderItems
|
||||
order := entity.Order{
|
||||
ID: 0,
|
||||
UserID: req.UserID,
|
||||
TotalAmount: req.TotalAmount,
|
||||
TotalDiscount: req.TotalDiscount,
|
||||
ShippingID: req.ShippingID,
|
||||
PaymentMethod: req.PaymentMethod,
|
||||
ProcessStatus: entity.WaitingToPay,
|
||||
PaymentStatus: entity.UnPaid,
|
||||
Address: req.Address,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
resp, lErr := h.orderSvc.CreateOrder(order, orderItems)
|
||||
|
||||
if lErr != nil {
|
||||
msg, code := getErrorDataFromRichError(lErr)
|
||||
return echo.NewHTTPError(code, msg)
|
||||
}
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func getErrorDataFromRichError(err error) (message string, code int) {
|
||||
switch err.(type) {
|
||||
case richerror.RichError:
|
||||
re := err.(richerror.RichError)
|
||||
|
||||
return re.Message(), mapKindToCode(re.Kind())
|
||||
default:
|
||||
return err.Error(), http.StatusBadRequest
|
||||
}
|
||||
}
|
||||
|
||||
func mapKindToCode(kind richerror.Kind) int {
|
||||
switch kind {
|
||||
case richerror.KindInvalid:
|
||||
return http.StatusUnprocessableEntity
|
||||
default:
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,4 +4,5 @@ import "github.com/labstack/echo/v4"
|
|||
|
||||
func (h Handler) SetRoutes(e *echo.Echo) {
|
||||
|
||||
e.POST("/order/create", h.CreateOrderHandler)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +1,32 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
httpserver "git.gocasts.ir/ebhomengo/niki/delivery/http_server"
|
||||
"git.gocasts.ir/ebhomengo/niki/purchaseapp/delivery/http/invoice"
|
||||
"git.gocasts.ir/ebhomengo/niki/purchaseapp/delivery/http/order"
|
||||
orderService "git.gocasts.ir/ebhomengo/niki/purchaseapp/service/order"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
HTTPServer *httpserver.Server
|
||||
OrderHandler *order.Handler
|
||||
InvoiceHandler *invoice.Handler
|
||||
}
|
||||
|
||||
func New(httpserver *httpserver.Server) *Server {
|
||||
func New(orderSvc orderService.Service) *Server {
|
||||
return &Server{
|
||||
HTTPServer: httpserver,
|
||||
OrderHandler: order.New(),
|
||||
InvoiceHandler: invoice.New(),
|
||||
OrderHandler: order.New(orderSvc),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Serve() {
|
||||
s.RegisterRoutes()
|
||||
e := echo.New()
|
||||
e.Use(middleware.RequestLogger())
|
||||
e.GET("/purchase/health-check", s.healthCheck)
|
||||
|
||||
s.OrderHandler.SetRoutes(e)
|
||||
|
||||
if err := e.Start(":8088"); err != nil {
|
||||
e.Logger.Error("failed to start server", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Stop() {}
|
||||
|
||||
func (s *Server) RegisterRoutes() {
|
||||
s.HTTPServer.Router.GET("/purchase/health-check", s.healthCheck)
|
||||
s.OrderHandler.SetRoutes(s.HTTPServer.Router)
|
||||
s.InvoiceHandler.SetRoutes(s.HTTPServer.Router)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
package entity
|
||||
|
||||
type Invoice struct {
|
||||
}
|
||||
|
|
@ -1,4 +1,59 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"git.gocasts.ir/ebhomengo/niki/types"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Order struct {
|
||||
ID types.ID
|
||||
UserID types.ID
|
||||
TotalAmount types.Price
|
||||
TotalDiscount types.Price
|
||||
ShippingID types.ID
|
||||
PaymentMethod PaymentMethod
|
||||
ProcessStatus ProcessStatus
|
||||
PaymentStatus PaymentStatus
|
||||
Address string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type OrderItem struct {
|
||||
ID types.ID
|
||||
ProductID types.ID
|
||||
Price types.Price
|
||||
Quantity types.Count
|
||||
PriceWithDiscount types.Price
|
||||
OrderID types.ID
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type PaymentMethod string
|
||||
|
||||
const (
|
||||
Online PaymentMethod = "online"
|
||||
Wallet = "wallet"
|
||||
Cart = "cart"
|
||||
)
|
||||
|
||||
type ProcessStatus string
|
||||
|
||||
const (
|
||||
WaitingToPay ProcessStatus = "waiting-to-pay"
|
||||
processing = "processing"
|
||||
accepted = "accepted"
|
||||
preparing = "preparing"
|
||||
prepared = "prepared"
|
||||
givenToPost = "given-to-post"
|
||||
delivered = "delivered"
|
||||
cancelled = "cancelled"
|
||||
)
|
||||
|
||||
type PaymentStatus string
|
||||
|
||||
const (
|
||||
Paid PaymentStatus = "paid"
|
||||
UnPaid = "unpaid"
|
||||
Cancelled = "cancelled"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
-- +migrate Up
|
||||
-- please read this article to understand why we use VARCHAR(191)
|
||||
-- https://www.grouparoo.com/blog/varchar-191#why-varchar-and-not-text
|
||||
CREATE TABLE `orders` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`user_id` INT NOT NULL,
|
||||
`address` TEXT,
|
||||
`shipping_id` INT NOT NULL,
|
||||
`payment_method` ENUM('online', 'wallet', 'cart') DEFAULT 'online',
|
||||
`payment_status` ENUM('unpaid', 'paid', 'cancelled') DEFAULT 'unpaid',
|
||||
`process_status` ENUM('waiting-to-pay', 'processing', 'accepted', 'preparing', 'prepared', 'given-to-post', 'delivered', 'cancelled') DEFAULT 'waiting-to-pay',
|
||||
`total_amount` INT NOT NULL,
|
||||
`total_discount` INT NULL,
|
||||
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
-- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`)
|
||||
-- FOREIGN KEY (`shipping_id`) REFERENCES `shippings`(`id`)
|
||||
|
||||
);
|
||||
|
||||
-- +migrate Down
|
||||
DROP TABLE `orders`;
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
-- +migrate Up
|
||||
-- please read this article to understand why we use VARCHAR(191)
|
||||
-- https://www.grouparoo.com/blog/varchar-191#why-varchar-and-not-text
|
||||
CREATE TABLE `order_items` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`order_id` INT NOT NULL,
|
||||
`product_id` INT NOT NULL,
|
||||
`quantity` INT DEFAULT 1,
|
||||
`price` INT NOT NULL,
|
||||
`price_with_discount` INT NULL,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (`order_id`) REFERENCES `orders`(`id`)
|
||||
|
||||
);
|
||||
|
||||
-- +migrate Down
|
||||
DROP TABLE `order_items`;
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package mysql
|
||||
|
||||
import (
|
||||
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
|
||||
"git.gocasts.ir/ebhomengo/niki/purchaseapp/entity"
|
||||
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
|
||||
"git.gocasts.ir/ebhomengo/niki/types"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
conn *mysql.DB
|
||||
}
|
||||
|
||||
func New(db *mysql.DB) *DB {
|
||||
return &DB{conn: db}
|
||||
}
|
||||
|
||||
func (d *DB) CreateOrder(order entity.Order, orderItems []entity.OrderItem) (types.ID, error) {
|
||||
|
||||
const Op = "repository.mysql.order.createorder"
|
||||
tx, err := d.conn.Conn().Begin()
|
||||
|
||||
if err != nil {
|
||||
return 0, richerror.New(Op).WithErr(err)
|
||||
}
|
||||
|
||||
defer tx.Rollback()
|
||||
|
||||
query := "insert into orders(user_id, address, shipping_id," +
|
||||
" payment_method, payment_status, process_status," +
|
||||
" total_amount, total_discount) values (?, ?, ?, ?, ?, ?, ?, ?);"
|
||||
res, oErr := tx.Exec(query, order.UserID, order.Address, order.ShippingID,
|
||||
order.PaymentMethod, order.PaymentStatus, order.ProcessStatus,
|
||||
order.TotalAmount, order.TotalDiscount)
|
||||
|
||||
if oErr != nil {
|
||||
return 0, richerror.New(Op).WithErr(oErr)
|
||||
}
|
||||
orderID, insertIDErr := res.LastInsertId()
|
||||
if insertIDErr != nil {
|
||||
return 0, richerror.New(Op).WithErr(insertIDErr)
|
||||
}
|
||||
|
||||
orderItemQuery := "insert into order_items(order_id, product_id, quantity, price, price_with_discount) values(?, ?, ?, ?, ?);"
|
||||
for _, item := range orderItems {
|
||||
_, iErr := tx.Exec(orderItemQuery, orderID, item.ProductID, item.Quantity, item.Price, item.PriceWithDiscount)
|
||||
if iErr != nil {
|
||||
return 0, richerror.New(Op).WithErr(iErr)
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return 0, richerror.New(Op).WithErr(err)
|
||||
}
|
||||
|
||||
return types.ID(orderID), nil
|
||||
}
|
||||
|
||||
func (d *DB) UpdateOrderProcessStatus(orderID types.ID, status string) (bool, error) {
|
||||
const Op = "repository.mysql.order.update-order-process-status"
|
||||
_, err := d.conn.Conn().Exec("update orders set process_status=? where id=?;", status, orderID)
|
||||
|
||||
if err != nil {
|
||||
return false, richerror.New(Op).WithErr(err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
package invoice
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
package invoice
|
||||
|
||||
type Service struct {
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
package invoice
|
||||
|
|
@ -1 +1,20 @@
|
|||
package order
|
||||
|
||||
import (
|
||||
"git.gocasts.ir/ebhomengo/niki/purchaseapp/entity"
|
||||
"git.gocasts.ir/ebhomengo/niki/types"
|
||||
)
|
||||
|
||||
type CreateOrderRequest struct {
|
||||
UserID types.ID `json:"user_id"`
|
||||
Address string `json:"address"`
|
||||
ShippingID types.ID `json:"shipping_id"`
|
||||
PaymentMethod entity.PaymentMethod `json:"payment_method"`
|
||||
TotalAmount types.Price `json:"total_amount"`
|
||||
TotalDiscount types.Price `json:"total_discount"`
|
||||
OrderItems []entity.OrderItem `json:"order_items"`
|
||||
}
|
||||
|
||||
type CreateOrderResponse struct {
|
||||
OrderID types.ID
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,42 @@
|
|||
package order
|
||||
|
||||
import (
|
||||
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
|
||||
"git.gocasts.ir/ebhomengo/niki/purchaseapp/entity"
|
||||
"git.gocasts.ir/ebhomengo/niki/types"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
repo Repo
|
||||
}
|
||||
|
||||
type Repo interface {
|
||||
CreateOrder(order entity.Order, orderItems []entity.OrderItem) (types.ID, error)
|
||||
UpdateOrderProcessStatus(orderID types.ID, status string) (bool, error)
|
||||
}
|
||||
|
||||
func New(orderRepo Repo) Service {
|
||||
return Service{repo: orderRepo}
|
||||
}
|
||||
|
||||
func (s Service) CreateOrder(order entity.Order, orderItems []entity.OrderItem) (CreateOrderResponse, error) {
|
||||
const Op = "purchaseapp.service.CreateOrder"
|
||||
orderID, err := s.repo.CreateOrder(order, orderItems)
|
||||
|
||||
if err != nil {
|
||||
return CreateOrderResponse{}, richerror.New(Op).WithErr(err)
|
||||
}
|
||||
|
||||
return CreateOrderResponse{OrderID: orderID}, nil
|
||||
}
|
||||
|
||||
func (s Service) UpdateOrderProcessStatus(orderID types.ID, status string) (bool, error) {
|
||||
|
||||
const Op = "purchaseapp.service.UpdateOrderProcessStatus"
|
||||
_, err := s.repo.UpdateOrderProcessStatus(orderID, status)
|
||||
if err != nil {
|
||||
return false, richerror.New(Op).WithErr(err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println(" Staffapp Server Starting...")
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "Staffapp OK!")
|
||||
})
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
|
|
@ -1,13 +1,64 @@
|
|||
package database
|
||||
|
||||
import "git.gocasts.ir/ebhomengo/niki/repository/mysql"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/staffapp/service"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
conn *mysql.DB
|
||||
staffs []service.Staff
|
||||
nextID int
|
||||
}
|
||||
|
||||
func New(conn *mysql.DB) *DB {
|
||||
func New() service.StaffRepository {
|
||||
return &DB{
|
||||
conn: conn,
|
||||
staffs: []service.Staff{},
|
||||
nextID: 1,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DB) Create(staff service.Staff) (service.Staff, error) {
|
||||
staff.ID = d.nextID
|
||||
d.staffs = append(d.staffs, staff)
|
||||
d.nextID++
|
||||
return staff, nil
|
||||
}
|
||||
|
||||
func (d *DB) Get(id int) (*service.Staff, error) {
|
||||
for _, v := range d.staffs {
|
||||
if id == v.ID {
|
||||
return &v, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("The user not found with id = %d", id)
|
||||
}
|
||||
|
||||
func (d *DB) List() ([]service.Staff, error) {
|
||||
return d.staffs, nil
|
||||
}
|
||||
func (d *DB) Remove(id int) error {
|
||||
for _, v := range d.staffs {
|
||||
if id == v.ID {
|
||||
d.staffs = append(d.staffs[:id], d.staffs[id+1:]...)
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
return fmt.Errorf("User with ID: %d notfound", id)
|
||||
}
|
||||
func (d *DB) Update(id int, name, lastName, phoneNumber string) (*service.Staff, error) {
|
||||
for _, v := range d.staffs {
|
||||
if id == v.ID {
|
||||
v.Name = name
|
||||
v.LastName = lastName
|
||||
v.PhoneNumber = phoneNumber
|
||||
|
||||
return &v, nil
|
||||
}
|
||||
|
||||
}
|
||||
return nil, fmt.Errorf("User with this Id(%d)couldn't found for updating", id)
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue