merge with develop

This commit is contained in:
matina 2026-04-15 01:15:44 -07:00
commit 57faf27457
432 changed files with 52215 additions and 48559 deletions

View File

@ -1,8 +1,62 @@
package benefactorapp 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 { type Application struct {
Config Config 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
} }

View File

@ -1,11 +1,23 @@
package benefactorapp 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 { type Config struct {
// HTTP server config // HTTP server config
HTTPServer httpserver.Config `koanf:"http_server"`
// Database config // Database config
MySQLDB database.Config `koanf:"mariadb"`
// Logger config // Logger config
Logger logger.Config `koanf:"logger"`
// Service config // Service config
// Database migration
PathOfMigration string `koanf:"path_of_migration"`
} }

View File

@ -1,15 +1,20 @@
package http package http
import ( import (
benefactor "git.gocasts.ir/ebhomengo/niki/benefactorapp/service"
"net/http" "net/http"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
type Handler struct{} type Handler struct {
BebefactorService benefactor.Service
}
func NewHandler() *Handler { func NewHandler(bService benefactor.Service) *Handler {
return &Handler{} return &Handler{
BebefactorService: bService,
}
} }
func (h Handler) HealthCheck(c echo.Context) error { func (h Handler) HealthCheck(c echo.Context) error {

View File

@ -1,16 +1,16 @@
package http package http
import httpserver "git.gocasts.ir/ebhomengo/niki/delivery/http_server" import httpserver "git.gocasts.ir/ebhomengo/niki/pkg/httpserver"
type Server struct { type Server struct {
HTTPServer *httpserver.Server HTTPServer httpserver.Server
Handler *Handler Handler Handler
} }
func NewServer(httpserver *httpserver.Server) *Server { func NewServer(httpserver httpserver.Server, handler Handler) Server {
return &Server{ return Server{
HTTPServer: httpserver, HTTPServer: httpserver,
Handler: NewHandler(), Handler: handler,
} }
} }

View File

@ -1,6 +1,6 @@
package database package database
import "git.gocasts.ir/ebhomengo/niki/repository/mysql" import "git.gocasts.ir/ebhomengo/niki/pkg/database/mysql"
type DB struct { type DB struct {
conn *mysql.DB conn *mysql.DB

View File

@ -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`;

View File

@ -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`;

View File

@ -1 +1,17 @@
package service 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,
}
}

View File

@ -1 +1,11 @@
package service package service
type ValidatorBenefactorRepository interface {
}
type Validator struct {
repo ValidatorBenefactorRepository
}
func NewValidator(repo ValidatorBenefactorRepository) Validator {
return Validator{repo: repo}
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}

12
cmd/benefactor/main.go Normal file
View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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.`,
}

View File

@ -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)
}

13
cmd/productapp/main.go Normal file
View File

@ -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)
}
}

View File

@ -0,0 +1 @@
package command

View File

@ -0,0 +1 @@
package command

View File

@ -0,0 +1 @@
package command

63
cmd/purchaseapp/main.go Normal file
View File

@ -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)
}

View File

@ -1,15 +1,143 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"strconv"
"git.gocasts.ir/ebhomengo/niki/staffapp/repository/database"
"git.gocasts.ir/ebhomengo/niki/staffapp/service"
) )
func main() { func main() {
fmt.Println(" Staffapp Server Starting...")
staffDb := database.New()
staffService := service.NewStaffService(staffDb)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Staffapp OK!") 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)) log.Fatal(http.ListenAndServe(":8080", nil))
} }

View File

@ -15,9 +15,10 @@ const (
defaultPrefix = "EB_" defaultPrefix = "EB_"
defaultDelimiter = "." defaultDelimiter = "."
defaultSeparator = "__" defaultSeparator = "__"
defaultYamlFilePath = "config.yml"
) )
var defaultYamlFilePath = "config.yml"
var c Config var c Config
type Option struct { type Option struct {

View File

@ -18,8 +18,8 @@ type TestContainer struct {
dockerPool *dockertest.Pool // the connection pool to Docker. dockerPool *dockertest.Pool // the connection pool to Docker.
mariaResource *dockertest.Resource // MariaDB Docker container resource. mariaResource *dockertest.Resource // MariaDB Docker container resource.
redisResource *dockertest.Resource // Redis Docker container resource. redisResource *dockertest.Resource // Redis Docker container resource.
mariaDBConn *mysql.DB // Connection to the MariaDB database. mariaDBConn *mysql.DB // Connection to the MariaDB mysql.
redisDBConn *redisadapter.Adapter // Connection to the Redis database. redisDBConn *redisadapter.Adapter // Connection to the Redis mysql.
containerExpiryInSeconds uint containerExpiryInSeconds uint
} }
@ -158,7 +158,7 @@ func (t *TestContainer) Start() {
return nil return nil
}); err != nil { }); err != nil {
log.Fatalf("Could not connect to database: %s", err) log.Fatalf("Could not connect to mysql: %s", err)
} }
} }

View File

@ -0,0 +1,2 @@
FROM golang:1.25-alpine

View File

@ -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

View File

@ -0,0 +1,8 @@
package entity
type Channel struct {
ID int8
Type NotificationType
Provider string
Config string
}

View File

@ -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"
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
View File

@ -7,23 +7,26 @@ require (
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible github.com/go-ozzo/ozzo-validation v3.6.0+incompatible
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 github.com/go-ozzo/ozzo-validation/v4 v4.3.0
github.com/go-sql-driver/mysql v1.9.3 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/golang-jwt/jwt/v4 v4.5.2
github.com/kavenegar/kavenegar-go v0.0.0-20240205151018-77039f51467d github.com/kavenegar/kavenegar-go v0.0.0-20240205151018-77039f51467d
github.com/knadh/koanf v1.5.0 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-jwt/v4 v4.4.0
github.com/labstack/echo/v4 v4.15.1 github.com/labstack/echo/v4 v4.15.1
github.com/ory/dockertest/v3 v3.12.0 github.com/ory/dockertest/v3 v3.12.0
github.com/redis/go-redis/v9 v9.18.0 github.com/redis/go-redis/v9 v9.18.0
github.com/rubenv/sql-migrate v1.8.1 github.com/rubenv/sql-migrate v1.8.1
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/swaggo/echo-swagger v1.5.2 github.com/swaggo/echo-swagger v1.5.2
github.com/swaggo/swag v1.16.6 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 gopkg.in/natefinch/lumberjack.v2 v2.2.1
) )
require ( require (
dario.cat/mergo v1.0.0 // indirect dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.1.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/KyleBanks/depth v1.2.1 // 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/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/docker/cli v27.4.1+incompatible // indirect github.com/docker/cli v27.4.1+incompatible // indirect
github.com/docker/docker v27.1.1+incompatible // indirect github.com/docker/docker v28.3.3+incompatible // indirect
github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect
github.com/fatih/structs v1.1.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-gorp/gorp/v3 v3.1.0 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/spec v0.20.9 // indirect github.com/go-openapi/spec v0.20.9 // indirect
github.com/go-openapi/swag v0.22.3 // 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/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // 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/josharian/intern v1.0.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect github.com/labstack/gommon v0.4.2 // indirect
github.com/mailru/easyjson v0.7.7 // 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/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // 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/moby/term v0.5.0 // indirect
github.com/opencontainers/go-digest v1.0.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/opencontainers/runc v1.2.3 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sirupsen/logrus v1.9.3 // 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/stretchr/objx v0.5.2 // indirect
github.com/sv-tools/openapi v0.2.1 // indirect github.com/sv-tools/openapi v0.2.1 // indirect
github.com/swaggo/files/v2 v2.0.0 // 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/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect
go.uber.org/atomic v1.11.0 // indirect go.uber.org/atomic v1.11.0 // indirect
golang.org/x/mod v0.30.0 // indirect golang.org/x/mod v0.33.0 // indirect
golang.org/x/net v0.48.0 // indirect golang.org/x/net v0.50.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.39.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.32.0 // indirect golang.org/x/text v0.35.0 // indirect
golang.org/x/time v0.14.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.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect sigs.k8s.io/yaml v1.3.0 // indirect

70
go.sum
View File

@ -1,7 +1,7 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 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= 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.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 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 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= 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/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-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/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.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 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/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 h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI=
github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= 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 v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= 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 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 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= 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/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 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 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.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/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 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= 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-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-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-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.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 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/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.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 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/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 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs=
github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E= 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 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/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 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/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 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs=
github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs= 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.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/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= 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/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 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 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.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= 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 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 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= 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/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 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 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.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= 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 h1:fxE7amCzfZflJO2lHXf4y/y8M1BoAqp+FVmG19oYB80=
github.com/opencontainers/runc v1.2.3/go.mod h1:nSxcWUydXrsBZVYNSkTjoQ/N6rcyTtn+1SD5D4+kRIM= github.com/opencontainers/runc v1.2.3/go.mod h1:nSxcWUydXrsBZVYNSkTjoQ/N6rcyTtn+1SD5D4+kRIM=
github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw= 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/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 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy2U0=
github.com/rubenv/sql-migrate v1.8.1/go.mod h1:BTIKBORjzyxZDS6dzoiw6eAFYJ1iNlGAtjn4LGeVjS8= 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 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/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= 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.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 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.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.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.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 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/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 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.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-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-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-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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 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.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= 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/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-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 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.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.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.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= 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-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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/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-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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= 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.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= 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-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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/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-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-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.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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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-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-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/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-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.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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/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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/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.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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.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 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 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-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.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 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.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= 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-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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -43,7 +43,7 @@ func Config() config.Config {
} }
func MariaDB(cfg config.Config) *mysql.DB { 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() flag.Parse()
if *migrate { if *migrate {
migrator.New(migrator.Config{ migrator.New(migrator.Config{

View File

@ -1 +1,37 @@
package patientapp 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()
}

26
patientapp/cmd/main.go Normal file
View File

@ -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()
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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"
)

View File

@ -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
}

View File

@ -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
}

90
pkg/database/mysql/db.go Normal file
View File

@ -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
}

View File

@ -0,0 +1,5 @@
production:
dialect: mysql
datasource: niki:nikiappt0lk2o20@(localhost:3306)/niki_db?parseTime=true
dir: pkg/database/mysql/migration
table: gorp_migrations

View File

@ -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
)

View File

@ -0,0 +1,5 @@
package mysql
type Scanner interface {
Scan(dest ...any) error
}

View File

@ -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}
}

View File

@ -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
}

View File

@ -0,0 +1,7 @@
package httpserver
import "time"
const (
DefaultShutdownTimeout = 10 * time.Second
)

121
pkg/httpserver/server.go Normal file
View File

@ -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")
}
}

View File

@ -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)
}

84
pkg/logger/logger.go Normal file
View File

@ -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
}
}

34
pkg/path/path.go Normal file
View File

@ -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
}
}

View File

@ -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`;

View File

@ -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`;

View File

@ -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`;

View File

@ -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`;

View File

@ -0,0 +1,10 @@
package category
import "time"
type Category struct {
ID uint
Name string
Slug string
CreatedAt time.Time
}

View File

@ -0,0 +1 @@
package category

View File

@ -0,0 +1 @@
package category

View File

@ -0,0 +1 @@
package category

View File

@ -1 +0,0 @@
package service

View File

@ -1 +0,0 @@
package service

View File

@ -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
}

View File

@ -0,0 +1 @@
package product

View File

@ -0,0 +1 @@
package product

View File

@ -0,0 +1 @@
package product

View File

@ -1 +0,0 @@
package service

View File

@ -1 +0,0 @@
package service

View File

@ -1 +1,61 @@
package purchaseapp 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)
}

View File

@ -1,4 +1,9 @@
package purchaseapp package purchaseapp
import (
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
)
type Config struct { type Config struct {
Mysql mysql.Config `koanf:"mariadb"`
} }

View File

@ -1,8 +0,0 @@
package invoice
type Handler struct {
}
func New() *Handler {
return &Handler{}
}

View File

@ -1,7 +0,0 @@
package invoice
import "github.com/labstack/echo/v4"
func (h Handler) SetRoutes(e *echo.Echo) {
}

View File

@ -1,7 +1,68 @@
package order 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 { type Handler struct {
return &Handler{} 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
}
} }

View File

@ -4,4 +4,5 @@ import "github.com/labstack/echo/v4"
func (h Handler) SetRoutes(e *echo.Echo) { func (h Handler) SetRoutes(e *echo.Echo) {
e.POST("/order/create", h.CreateOrderHandler)
} }

View File

@ -1,33 +1,32 @@
package http package http
import ( 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" "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 { type Server struct {
HTTPServer *httpserver.Server
OrderHandler *order.Handler OrderHandler *order.Handler
InvoiceHandler *invoice.Handler
} }
func New(httpserver *httpserver.Server) *Server { func New(orderSvc orderService.Service) *Server {
return &Server{ return &Server{
HTTPServer: httpserver, OrderHandler: order.New(orderSvc),
OrderHandler: order.New(),
InvoiceHandler: invoice.New(),
} }
} }
func (s *Server) Serve() { 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) 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)
}

View File

@ -1,4 +0,0 @@
package entity
type Invoice struct {
}

View File

@ -1,4 +1,59 @@
package entity package entity
import (
"git.gocasts.ir/ebhomengo/niki/types"
"time"
)
type Order struct { 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"
)

View File

@ -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`;

View File

@ -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`;

View File

@ -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
}

View File

@ -1 +0,0 @@
package invoice

View File

@ -1,4 +0,0 @@
package invoice
type Service struct {
}

View File

@ -1 +0,0 @@
package invoice

View File

@ -1 +1,20 @@
package order 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
}

View File

@ -1,4 +1,42 @@
package order 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 { 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
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"database/sql" "database/sql"
"fmt" "fmt"
_ "github.com/go-sql-driver/mysql"
"sync" "sync"
"time" "time"

View File

@ -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))
}

View File

@ -1,13 +1,64 @@
package database package database
import "git.gocasts.ir/ebhomengo/niki/repository/mysql" import (
"fmt"
"git.gocasts.ir/ebhomengo/niki/staffapp/service"
)
type DB struct { type DB struct {
conn *mysql.DB staffs []service.Staff
nextID int
} }
func New(conn *mysql.DB) *DB { func New() service.StaffRepository {
return &DB{ 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